Redis实现分布式锁的正确姿势

发布者:上海IT外包来源:http://www.lanmon.net点击数:1929

在我们日常工作中,除了Spring和Mybatis外,用到最多无外乎分布式缓存框架——Redis。可是良多工作良多年的伴侣对Redis还处于一个最根本的使用和熟悉。所以我就像把本身对分布式缓存的一些理解和应用清算一个系列,希望可以辅佐到大师加深对Redis的理解。本系列的文章思绪先从Redis的应用起头。再解析Redis的内部实现事理。末了以经常会问到Redist相干的面试

为了实现分布式锁,必要确保锁同时满足以下四个前提:

  1. 互斥性。在肆意时辰,只需一个客户端能持有锁

  2. 不会发送死锁。即使一个客户端持有锁的时代崩溃而没有主动释放锁,也必要保证后续其他客户端可以加锁成功

  3. 加锁息争锁必需是统一个客户端,客户端本身不能把别人加的锁给释放了。

  4. 容错性。只需大局部的Redis节点正常运转,客户端就可以停止加锁息争锁把持。

三、Redis实现分布式锁的错误姿态

3.1 加锁错误姿态

在讲解使用Redis实现分布式锁的精确姿态之前,我们有必要来看下错误实现编制。

首先,为了保证互斥性和不会发送死锁2个前提,所以我们在加锁把持的时辰,必要使用SETNX指令来保证互斥性——只需一个客户端可以持有锁。为了保证不会发送死锁,必要给锁加一个过不时辰,如许就可以保证即使持有锁的客户端时代崩溃了也不会不息不释放锁。

为了保证这2个前提,有些人错误的实现会用如下代码来实现加锁把持:

/**      * 实现加锁的错误姿态      * @param jedis      * @param lockKey      * @param requestId      * @param expireTime      */     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 和expire是两条Redis指令,不具备原子性,若是轨范在实行完setnx之后俄然崩溃,导致没有设置锁的过不时辰,从而就导致死锁了。由于这个客户端持有的所有不会被其他客户端释放,持有锁的客户端又崩溃了,也不会主动释放。从而该锁永久不会释放,导致其他客户端也获得不能锁。从而其他客户端不息梗阻。所以针对该代码精确姿态应该保证setnx和expire原子性

实现加锁把持的错误姿态2。详细实现如下代码所示

/**      * 实现加锁的错误姿态2      * @param jedis      * @param lockKey      * @param expireTime      * @return      */     public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {         long expires = System.currentTimeMillis() + expireTime;         String expiresStr = String.valueOf(expires);         // 若是当前锁不存在,前往加锁成功         if (jedis.setnx(lockKey, expiresStr) == 1) {             return true;         }          // 若是锁存在,获取锁的过不时辰         String currentValueStr = jedis.get(lockKey);         if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过时,获取上一个锁的过不时辰,并设置如今锁的过不时辰 String oldValueStr = jedis.getSet(lockKey, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情形,只需一个线程的设置值和当前值不异,它才有权利加锁 return true; } } // 其他情形,同等前往加锁失败 return false; }

这个加锁把持咋一看没有弊端对吧。那以上这段代码的问题弊端出在哪里呢?

1. 由于客户端本身生成过不时辰,所以必要强迫要求分布式情形下所有客户端的时辰必需同步。

2. 当锁过时的时辰,若是多个客户端同时实行jedis.getSet()编制,虽然终极只需一个客户端加锁,可是这个客户端的锁的过不时辰可能被其他客户端笼盖。不具备加锁息争锁必需是统一个客户端的特征。处理上面这段代码的编制就是为每个客户端加锁添加一个独一标示,已确保加锁息争锁把持是来自统一个客户端。

3.2 解锁错误姿态

分布式锁的实现无法就2个编制,一个加锁,一个就是解锁。下面我们来看下解锁的错误姿态。

错误姿态1.

/**      * 解锁错误姿态1      * @param jedis      * @param lockKey      */     public static void wrongReleaseLock1(Jedis jedis, String lockKey) {         jedis.del(lockKey);     }

上面实现是最简单直接的解锁编制,这种不先断定拥有者而直接解锁的编制,会导致任何客户端都可以随时解锁。即使这把锁不是它上锁的。

错误姿态2:

/**      * 解锁错误姿态2      * @param jedis      * @param lockKey      * @param requestId      */     public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {          // 断定加锁与解锁是不是统一个客户端         if (requestId.equals(jedis.get(lockKey))) {             // 若在此时,这把锁俄然不是这个客户端的,则会曲解锁             jedis.del(lockKey);         }

既然错误姿态1中没有断定锁的拥有者,那姿态2中断定了拥有者,那错误缘故缘由又在哪里呢?谜底又是原子性上面。由于断定和删除不是一个原子性把持。在并发的时辰很可能产生解除了别的客户端加的锁。详细场景有:客户端A加锁,一段时辰之后客户端A停止解锁把持时,在实行jedis.del()之前,锁俄然过时了,此时客户端B考试考试加锁成功,然后客户端A再实行del编制,则客户端A将客户端B的锁给解除了。从而不也不满足加锁息争锁必需是统一个客户端特征。处理思绪就是必要保证GET和DEL把持在一个事务中停止,保证其原子性。

四、Redis实现分布式锁的精确姿态

刚刚引见完了错误的姿态后,从上面错误姿态中,我们可以晓得,要使用Redis实现分布式锁。加锁把持的精确姿态为:

  1. 使用setnx呼吁保证互斥性

  2. 必要设置锁的过不时辰,按捺死锁

  3. setnx和设置过不时辰必要保持原子性,按捺在设置setnx成功之后在设置过不时辰客户端崩溃导致死锁

  4. 加锁的Value 值为一个独一标示。可以接纳UUID作为独一标示。加锁成功后必要把独一标示前往给客户端来用来客户端停止解锁把持

解锁的精确姿态为:

1. 必要拿加锁成功的独一标示要停止解锁,从而保证加锁息争锁的是统一个客户端

在此我向大师保举一个架构进修交流圈:830478757 辅佐冲破瓶颈 晋升思维才能

2. 解锁把持必要斗劲独一标示是否相称,相称再实行删除把持。这2个把持可以接纳Lua剧本编制使2个呼吁的原子性。

Redis分布式锁实现的精确姿态的实当代码:

public interface DistributedLock {     /**      * 获取锁      * @author zhi.li      * @return 锁标识      */     String acquire();      /**      * 释放锁      * @author zhi.li      * @param indentifier      * @return      */     boolean release(String indentifier); }  /**  * @author zhi.li  * @Description  * @created 2019/1/1 20:32  */ @Slf4j public class RedisDistributedLock implements DistributedLock{      private static final String LOCK_SUCCESS = "OK";     private static final Long RELEASE_SUCCESS = 1L;     private static final String SET_IF_NOT_EXIST = "NX";     private static final String SET_WITH_EXPIRE_TIME = "PX";      /**      * redis 客户端      */     private Jedis jedis;      /**      * 分布式锁的键值      */     private String lockKey;      /**      * 锁的超不时辰 10s      */     int expireTime = 10 * 1000;      /**      * 锁等待,防止线程饥饿      */     int acquireTimeout  = 1 * 1000;      /**      * 获取指定键值的锁      * @param jedis jedis Redis客户端      * @param lockKey 锁的键值      */     public RedisDistributedLock(Jedis jedis, String lockKey) {         this.jedis = jedis;         this.lockKey = lockKey;     }      /**      * 获取指定键值的锁,同时设置获取锁超不时辰      * @param jedis jedis Redis客户端      * @param lockKey 锁的键值      * @param acquireTimeout 获取锁超不时辰      */     public RedisDistributedLock(Jedis jedis,String lockKey, int acquireTimeout) {         this.jedis = jedis;         this.lockKey = lockKey;         this.acquireTimeout = acquireTimeout;     }      /**      * 获取指定键值的锁,同时设置获取锁超不时辰和锁过不时辰      * @param jedis jedis Redis客户端      * @param lockKey 锁的键值      * @param acquireTimeout 获取锁超不时辰      * @param expireTime 锁失效时辰      */     public RedisDistributedLock(Jedis jedis, String lockKey, int acquireTimeout, int expireTime) {         this.jedis = jedis;         this.lockKey = lockKey;         this.acquireTimeout = acquireTimeout;         this.expireTime = expireTime;     }      @Override     public String acquire() {         try {             // 获取锁的超不时辰,跨越这个时辰则抛却获取锁             long end = System.currentTimeMillis() + acquireTimeout;             // 随机生成一个value             String requireToken = UUID.randomUUID().toString();             while (System.currentTimeMillis() < end) { String result = jedis.set(lockKey, requireToken, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return requireToken; } try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } catch (Exception e) { log.error("acquire lock due to error", e); } return null; } @Override public boolean release(String identify) { if(identify == null){ return false; } String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = new Object(); try { result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identify)); if (RELEASE_SUCCESS.equals(result)) { log.info("release lock success, requestToken:{}", identify); return true; }}catch (Exception e){ log.error("release lock due to error",e); }finally { if(jedis != null){ jedis.close(); } } log.info("release lock failed, requestToken:{}, result:{}", identify, result); return false; } }
下面就以秒杀库存数目为场景,测试下上面实现的分布式锁的了局。详细测试代码如下:public class RedisDistributedLockTest {     static int n = 500;     public static void secskill() {         System.out.println(--n);     }      public static void main(String[] args) {         Runnable runnable = () -> {             RedisDistributedLock lock = null;             String unLockIdentify = null;             try {                 Jedis conn = new Jedis("127.0.0.1",6379);                 lock = new RedisDistributedLock(conn, "test1");                 unLockIdentify = lock.acquire();                 System.out.println(Thread.currentThread().getName() + "正在运转");                 在此我向大师保举一个架构进修交流圈:830478757 辅佐冲破瓶颈 晋升思维才能                 secskill();             } finally {                 if (lock != null) {                     lock.release(unLockIdentify);                 }             }         };          for (int i = 0; i < 10; i++) { Thread t = new Thread(runnable); t.start(); } } }

运转了局如下图所示。从图中可以看出,统一个资源在统一个时辰只能被一个线程获取,从而保证了库存数目N的递减是挨次的。

IT外包
>
400-635-8089
立即
咨询
电话咨询
服务热线
400-635-8089
微信咨询
微信咨询
微信咨询
公众号
公众号
公众号
返回顶部