一、redis实现分布式锁的次要原理:
1.加锁
最简略的办法是应用setnx命令。key是锁的惟一标识,按业务来决定命名。比方想要给一种商品的秒杀流动加锁,能够给key命名为 “lock_sale_商品ID” 。而value设置成什么呢?咱们能够权且设置成1。加锁的伪代码如下:
setnx(key,1)
当一个线程执行setnx返回1,阐明key本来不存在,该线程胜利失去了锁;当一个线程执行setnx返回0,阐明key曾经存在,该线程抢锁失败。
2.解锁
有加锁就得有解锁。当失去锁的线程执行完工作,须要开释锁,以便其余线程能够进入。开释锁的最简略形式是执行del指令,伪代码如下:
del(key)
开释锁之后,其余线程就能够继续执行setnx命令来取得锁。
3.锁超时
锁超时是什么意思呢?如果一个失去锁的线程在执行工作的过程中挂掉,来不及显式地开释锁,这块资源将会永远被锁住,别的线程再也别想进来。
所以,setnx的key必须设置一个超时工夫,以保障即便没有被显式开释,这把锁也要在肯定工夫后主动开释。setnx不反对超时参数,所以须要额定的指令,伪代码如下:
expire(key, 30)
二、加锁的代码
/** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 申请标识 * @param expireTime 超期工夫 * @return 是否获取胜利 */public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { Long result = jedis.setnx(lockKey, requestId); if (result == 1) { // 若在这里程序忽然解体,则无奈设置过期工夫,将产生死锁 jedis.expire(lockKey, expireTime); }}
下面的代码有一个致命的问题,就是加锁和设置过期工夫不是原子操作。
那么会有两种极其状况:
一种是在并发状况下,两个线程同时执行setnx,那么失去的后果都是1,这样两个线程同时拿到了锁。
别一种是如代码正文所示,即执行完setnx,程序解体没有执行过期工夫,那这把锁就永远不会被开释,造成了死锁。
之所以有人这样实现,是因为低版本的jedis并不反对多参数的set()办法。正确的代码如下:
/** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 申请标识 * @param expireTime 超期工夫 * @return 是否获取胜利 */public static boolean tryGetDistributedLock(Jedis jedis,String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { return true; } return false;}
这个set()办法一共有五个形参:
第一个为key,咱们应用key来当锁,因为key是惟一的。
第二个为value,咱们传的是requestId,很多童鞋可能不明确,有key作为锁不就够了吗,为什么还要用到value?起因就是,通过给value赋值为requestId,咱们就晓得这把锁是哪个申请加的了,在解锁的时候就能够有根据。requestId能够应用UUID.randomUUID().toString()
办法生成。
第三个为nxxx,这个参数咱们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,咱们进行set操作;若key曾经存在,则不做任何操作;
第四个为expx,这个参数咱们传的是PX,意思是咱们要给这个key加一个过期的设置,具体工夫由第五个参数决定。
第五个为time,与第四个参数相响应,代表key的过期工夫。
总的来说,执行下面的set()办法就只会导致两种后果:1. 以后没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value示意加锁的客户端。2. 已有锁存在,不做任何操作。
二、解锁的代码
public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey);}
这段代码的问题是容易导致误删,如果某线程胜利失去了锁,并且设置的超时工夫是30秒。如果某些起因导致线程A执行的很慢很慢,过了30秒都没执行完,这时候锁过期主动开释,线程B失去了锁。
随后,线程A执行完了工作,线程A接着执行del指令来开释锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。
怎么防止这种状况呢?能够在del开释锁之前做一个判断,验证以后的锁是不是本人加的锁。
至于具体的实现,能够在加锁的时候把以后的线程ID当做value,并在删除之前验证key对应的value是不是本人线程的ID。
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { // 判断加锁与解锁是不是同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 若在此时,这把锁忽然不是这个客户端的,则会误会锁 jedis.del(lockKey); }}
然而,这样做又隐含了一个新的问题,判断和开释锁是两个独立操作,不是原子性。
解决方案就是应用lua脚本,把它变成原子操作,代码如下:
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 开释分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 申请标识 * @return 是否开释胜利 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }}
三、续约问题
下面加锁最初的代码就完满了吗?假想这样一个场景,如果过期工夫为30S,A线程超过30S还没执行完,然而主动过期了。这时候B线程就会再拿到锁,造成了同时有两个线程持有锁。这个问题能够归结为”续约“问题,即A没执行完时应该过期工夫续约,执行实现能力开释锁。怎么办呢?咱们能够让取得锁的线程开启一个守护线程,用来给快要过期的锁“续约”。
其实,前面解锁呈现的删除非本人锁,也属于“续约”问题。
四、集群同步提早问题
用于redis的服务必定不能是单机,因为单机就不是高可用了,一量挂掉整个分布式锁就没用了。
在集群场景下,如果A在master拿到了锁,在没有把数据同步到slave时,master挂掉了。B再拿锁就会从slave拿锁,而且会拿到。又呈现了两个线程同时拿到锁。
基于以上的思考,Redis 的作者也思考到这个问题,他提出了一个 RedLock 的算法。
这个算法的意思大略是这样的:假如 Redis 的部署模式是 Redis Cluster,总共有 5 个 Master 节点。
通过以下步骤获取一把锁:
- 获取以后工夫戳,单位是毫秒。
- 轮流尝试在每个 Master 节点上创立锁,过期工夫设置较短,个别就几十毫秒。
- 尝试在大多数节点上建设一个锁,比方 5 个节点就要求是 3 个节点(n / 2 +1)。
- 客户端计算建设好锁的工夫,如果建设锁的工夫小于超时工夫,就算建设胜利了。
- 要是锁建设失败了,那么就顺次删除这个锁。
- 只有他人建设了一把分布式锁,你就得一直轮询去尝试获取锁。
然而这样的这种算法还是颇具争议的,可能还会存在不少的问题,无奈保障加锁的过程肯定正确。
这个问题的根本原因就是redis的集群属于AP,分布式锁属于CP,用AP去实现CP是不可能的。
五、Redisson
Redisson是架设在Redis根底上的一个Java驻内存数据网格(In-Memory Data Grid)。充沛的利用了Redis键值数据库提供的一系列劣势,基于Java实用工具包中罕用接口,为使用者提供了一系列具备分布式个性的常用工具类。
Redisson通过lua脚本解决了下面的原子性问题,通过“看门狗”解决了续约问题,然而它应该解决不了集群中的同步提早问题。
总结
redis分布式锁的计划,无论用何种形式实现都会有续约问题与集群同步提早问题。总的来说,是一个不太靠谱的计划。如果谋求高正确率,不能采纳这种计划。
然而它也有长处,就是比较简单,在某些非严格要求的场景是能够应用的,比方社交零碎一类,交易系统一类不能呈现反复交易则不倡议用。