分布式锁常见的三种实现方式:java
数据库乐观锁;git
基于Redis的分布式锁;github
基于ZooKeeper的分布式锁。面试
本地面试考点是,你对Redis使用熟悉吗?Redis中是如何实现分布式锁的。redis
Redis要实现分布式锁,如下条件应该获得知足算法
互斥性数据库
在任意时刻,只有一个客户端能持有锁。并发
不能死锁dom
客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其余客户端能加锁。异步
容错性
只要大部分的Redis节点正常运行,客户端就能够加锁和解锁。
能够直接经过 set key value px milliseconds nx
命令实现加锁, 经过Lua脚本实现解锁。
//获取锁(unique_value能够是UUID等) SET resource_name unique_value NX PX 30000 //释放锁(lua脚本中,必定要比较value,防止误解锁) if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
代码解释
set 命令要用 set key value px milliseconds nx
,替代 setnx + expire
须要分两次执行命令的方式,保证了原子性,
value 要具备惟一性,可使用UUID.randomUUID().toString()
方法生成,用来标识这把锁是属于哪一个请求加的,在解锁的时候就能够有依据;
释放锁时要验证 value 值,防止误解锁;
经过 Lua 脚原本避免 Check And Set 模型的并发问题,由于在释放锁的时候由于涉及到多个Redis操做 (利用了eval命令执行Lua脚本的原子性);
加锁代码分析
首先,set()加入了NX参数,能够保证若是已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,知足互斥性。其次,因为咱们对锁设置了过时时间,即便锁的持有者后续发生崩溃而没有解锁,锁也会由于到了过时时间而自动解锁(即key被删除),不会发生死锁。最后,由于咱们将value赋值为requestId,用来标识这把锁是属于哪一个请求加的,那么在客户端在解锁的时候就能够进行校验是不是同一个客户端。
解锁代码分析
将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。在执行的时候,首先会获取锁对应的value值,检查是否与requestId相等,若是相等则解锁(删除key)。
存在的风险
若是存储锁对应key的那个节点挂了的话,就可能存在丢失锁的风险,致使出现多个客户端持有锁的状况,这样就不能实现资源的独享了。
客户端A从master获取到锁
在master将锁同步到slave以前,master宕掉了(Redis的主从同步一般是异步的)。
主从切换,slave节点被晋级为master节点
客户端B取得了同一个资源被客户端A已经获取到的另一个锁。致使存在同一时刻存不止一个线程获取到锁的状况。
这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。而后执行以下步骤获取一把锁:
获取当前时间戳,单位是毫秒;
跟上面相似,轮流尝试在每一个 master 节点上建立锁,过时时间较短,通常就几十毫秒;
尝试在大多数节点上创建一个锁,好比 5 个节点就要求是 3 个节点 n / 2 + 1;
客户端计算创建好锁的时间,若是创建锁的时间小于超时时间,就算创建成功了;
要是锁创建失败了,那么就依次以前创建过的锁删除;
只要别人创建了一把分布式锁,你就得不断轮询去尝试获取锁。
Redis 官方给出了以上两种基于 Redis 实现分布式锁的方法,详细说明能够查看:
https://redis.io/topics/distlock 。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不只提供了一系列的分布式的Java经常使用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。
Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者可以将精力更集中地放在处理业务逻辑上。
Redisson 分布式重入锁用法
Redisson 支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例:
// 1.构造redisson实现分布式锁必要的Config Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0); // 2.构造RedissonClient RedissonClient redissonClient = Redisson.create(config); // 3.获取锁对象实例(没法保证是按线程的顺序获取到) RLock rLock = redissonClient.getLock(lockKey); try { /** * 4.尝试获取锁 * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败 * leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完) */ boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS); if (res) { //成功得到锁,在这里处理业务 } } catch (Exception e) { throw new RuntimeException("aquire lock fail"); }finally{ //不管如何, 最后都要解锁 rLock.unlock(); }
加锁流程图
解锁流程图
咱们能够看到,RedissonLock是可重入的,而且考虑了失败重试,能够设置锁的最大等待时间, 在实现上也作了一些优化,减小了无效的锁申请,提高了资源的利用率。
须要特别注意的是,RedissonLock 一样没有解决 节点挂掉的时候,存在丢失锁的风险的问题。而现实状况是有一些场景没法容忍的,因此 Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是须要额外的为 RedissonRedLock 搭建Redis环境。
因此,若是业务场景能够容忍这种小几率的错误,则推荐使用 RedissonLock, 若是没法容忍,则推荐使用 RedissonRedLock。
https://github.com/javazhiyin/advanced-java/
https://crazyfzw.github.io/2019/04/15/distributed-locks-with-redis/
最近三期