Redis实现分布式锁全局锁—Redis客户端Redisson中分布式锁RLock实现

转载:https://blog.csdn.net/u010963948/article/details/79240356

1. 前因

    以前实现过一个Redis实现的全局锁, 虽然能用, 但是感觉很不完善, 不可重入, 参数太多等等.

    最近看到了一个新的Redis客户端Redisson, 看了下源码, 发现了一个比较好的锁实现RLock, 于是记录下.

2. Maven依赖

[html]  view plain  copy
  1. <dependency>  
  2.     <groupId>org.redisson</groupId>  
  3.     <artifactId>redisson</artifactId>  
  4.     <version>1.2.1</version>  
  5. </dependency>  

3. 初试

    Redisson中RLock的使用很简单, 来看看一个最简单的例子.

[html]  view plain  copy
  1. import org.redisson.Redisson;  
  2. import org.redisson.core.RLock;  
  3.   
  4. public class Temp {  
  5.   
  6.     public static void main(String[] args) throws Exception {  
  7.         Redisson redisson = Redisson.create();  
  8.   
  9.         RLock lock = redisson.getLock("haogrgr");  
  10.         lock.lock();  
  11.         try {  
  12.             System.out.println("hagogrgr");  
  13.         }  
  14.         finally {  
  15.             lock.unlock();  
  16.         }  
  17.   
  18.         redisson.shutdown();  
  19.     }  
  20.   
  21. }  


4. RLock接口

    通过上面的例子可以看出, 使用起来和juc里面的Lock接口使用很类似, 那么来看看RLock这个接口.

[javascript]  view plain  copy
  1. Rlock  
  2. |  
  3. ----------Lock  
  4.           |  
  5.           ----------void lock()  
  6.           |  
  7.           ----------void lockInterruptibly()  
  8.           |  
  9.           ----------boolean tryLock()  
  10.           |  
  11.           ----------boolean tryLock(long time, TimeUnit unit)  
  12.           |  
  13.           ----------void unlock()  
  14.           |  
  15.           ----------Condition newCondition()  
  16. |  
  17. ----------RObject  
  18.           |  
  19.           ----------String getName()  
  20.           |  
  21.           ----------void delete()  
  22. |  
  23. ----------void lockInterruptibly(long leaseTime, TimeUnit unit)  
  24. |  
  25. ----------boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)  
  26. |  
  27. ----------void lock(long leaseTime, TimeUnit unit)  
  28. |  
  29. ----------void forceUnlock()  
  30. |  
  31. ----------boolean isLocked();  
  32. |  
  33. ----------boolean isHeldByCurrentThread()  
  34. |  
  35. ----------int getHoldCount()  

    可以看到, 该接口主要继承了Lock接口, 然后扩展了部分方法, 比如: 

    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)

    新加入的leaseTime主要是用来设置锁的过期时间, 形象的解释就是, 如果超过leaseTime还没有解锁的话, 我就强制解锁.

5. RLock接口的实现

    具体的实现类是RedissonLock, 下面来大概看看实现原理. 先看看 (3) 中例子执行时, 所运行的命令(通过monitor命令):

[javascript]  view plain  copy
  1. 127.0.0.1:6379> monitor  
  2. OK  
  3. 1434959509.494805 [0 127.0.0.1:57911] "SETNX" "haogrgr" "{\"@class\":\"org.redisson.RedissonLock$LockValue\",\"counter\":1,\"id\":\"c374addc-523f-4943-b6e0-c26f7ab061e3\",\"threadId\":1}"  
  4. 1434959509.494805 [0 127.0.0.1:57911] "GET" "haogrgr"  
  5. 1434959509.524805 [0 127.0.0.1:57911] "MULTI"  
  6. 1434959509.529805 [0 127.0.0.1:57911] "DEL" "haogrgr"  
  7. 1434959509.529805 [0 127.0.0.1:57911] "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"  
  8. 1434959509.529805 [0 127.0.0.1:57911] "EXEC"  


    可以看到, 大概原理是, 通过判断Redis中是否有某一key, 来判断是加锁还是等待, 最后的publish是一个解锁后, 通知阻塞在lock的线程.

    分布式锁的实现依赖的单点, 这里Redis就是单点, 通过在Redis中维护状态信息来实现全局的锁. 那么来看看RedissonLock如何

    实现可重入, 保证原子性等等细节.

6. 加锁源码分析

    从最简单的无参数的lock参数来看源码.

[javascript]  view plain  copy
  1. public void lock() {  
  2.     try {  
  3.         lockInterruptibly();  
  4.     } catch (InterruptedException e) {  
  5.         Thread.currentThread().interrupt();  
  6.         return;  
  7.     }  
  8. }  
  9.   
  10. public void lockInterruptibly() throws InterruptedException {  
  11.     lockInterruptibly(-1, null);    //leaseTime : -1 表示key不设置过期时间  
  12. }  
  13.   
  14. public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {  
  15.     Long ttl;  
  16.     if (leaseTime != -1) {  
  17.         ttl = tryLockInner(leaseTime, unit);  
  18.     } else {  
  19.         ttl = tryLockInner();  
  20.     }  
  21.     // lock acquired  
  22.     if (ttl == null) {  
  23.         return;  
  24.     }  
  25.   
  26.     subscribe().awaitUninterruptibly();  
  27.   
  28.     try {  
  29.         while (true) {  
  30.             if (leaseTime != -1) {  
  31.                 ttl = tryLockInner(leaseTime, unit);  
  32.             } else {  
  33.                 ttl = tryLockInner();  
  34.             }  
  35.             // lock acquired  
  36.             if (ttl == null) {  
  37.                 break;  
  38.             }  
  39.   
  40.             // waiting for message  
  41.             RedissonLockEntry entry = ENTRIES.get(getEntryName());  
  42.             if (ttl >= 0) {  
  43.                 entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);  
  44.             } else {  
  45.                 entry.getLatch().acquire();  
  46.             }  
  47.         }  
  48.     } finally {  
  49.         unsubscribe();  
  50.     }  
  51. }  


    代码有点多, 但是没关系, 慢慢分解, 由于这里我们是调用无参数的lock方法, 所以最后执行到的方法是:

[javascript]  view plain  copy
  1. private Long tryLockInner() {  
  2.     final LockValue currentLock = new LockValue(id, Thread.currentThread().getId());    //保存锁的状态: 客户端UUID+线程ID来唯一标识某一JVM实例的某一线程  
  3.     currentLock.incCounter();    //用来保存重入次数, 实现可重入功能, 初始情况是1  
  4.   
  5.     //Redisson封装了交互的细节, 具体的逻辑为execute方法逻辑.  
  6.     return connectionManager.write(getName(), new SyncOperation<LockValue, Long>() {  
  7.   
  8.         @Override  
  9.         public Long execute(RedisConnection<Object, LockValue> connection) {  
  10.             Boolean res = connection.setnx(getName(), currentLock);    //如果key:haogrgr不存在, 就set并返回true, 否则返回false  
  11.             if (!res) {    //如果设置失败, 那么表示有锁竞争了, 于是获取当前锁的状态, 如果拥有者是当前线程, 就累加重入次数并set新值  
  12.                 connection.watch(getName());    //通过watch命令配合multi来实现简单的事务功能  
  13.                 LockValue lock = (LockValue) connection.get(getName());  
  14.                 if (lock != null && lock.equals(currentLock)) {    //LockValue的equals实现为比较客户id和threadid是否一样  
  15.                     lock.incCounter();    //如果当前线程已经获取过锁, 则累加加锁次数, 并set更新  
  16.                     connection.multi();  
  17.                     connection.set(getName(), lock);  
  18.                     if (connection.exec().size() == 1) {  
  19.                         return null;    //set成功,   
  20.                     }  
  21.                 }  
  22.                 connection.unwatch();  
  23.   
  24.                 //走到这里, 说明上面set的时候, 其他客户端在  watch之后->set之前 有其他客户端修改了key值  
  25.                 //则获取key的过期时间, 如果是永不过期, 则返回-1, 具体处理后面说明  
  26.                 Long ttl = connection.pttl(getName());  
  27.                 return ttl;  
  28.             }  
  29.             return null;  
  30.         }  
  31.     });  
  32. }  

    tryLockInner的逻辑已经看完了,  可以知道, 有三种情况:

    (1) key不存在, 加锁: 

         当key不存在时, 设置锁的初始状态并set, 具体来看就是 setnx   haogrgr   LockValue{ id: Redisson对象的id,  threadId: 当前线程id,  counter: 当前重入次数,这里为第一次获取,所以为1}  

         通过上面的操作. 达到获取锁的目的, 通过setnx来达到实现类似于  if(map.get(key) == null) { map.put(key) } 的功能, 防止多个客户端同时set时, 新值覆盖老值.

    (2)key存在, 且获取锁的当前线程, 重入:

         这里就是锁重入的情况, 也就是锁的拥有者第二次调用lock方法, 这时, 通过先get, 然后比较客户端ID和当前线程ID来判断拥有锁的线程是不是当前线程.(客户端ID+线程ID才能唯一定位锁拥有者线程)

         判断发现当前是重入情况, 则累加LockValue的counter, 然后重新set回去, 这里使用到了watch和multi命令, 防止   get -> set   期间其他客户端修改了key的值.

    (3)key存在, 且是其他线程获取的锁, 等待:

         首先尝试获取锁(setnx), 失败后发现锁拥有者不是当前线程, 则获取key的过期时间, 返回过期时间

    那么接下来看看tryLockInner调用完成后的处理代码.

[javascript]  view plain  copy
  1. public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {  
  2.     Long ttl;  
  3.     if (leaseTime != -1) {  
  4.         ttl = tryLockInner(leaseTime, unit);  
  5.     } else {  
  6.         ttl = tryLockInner();   //lock()方法调用会走的逻辑  
  7.     }  
  8.     // lock acquired  
  9.     if (ttl == null) {   //加锁成功(新获取锁, 重入情况) tryLockInner会返回null, 失败会返回key超时时间, 或者-1(key未设置超时时间)  
  10.         return;   //加锁成功, 返回  
  11.     }  
  12.   
  13.     //subscribe这个方法代码有点多, Redisson通过netty来和redis通讯, 然后subscribe返回的是一个Future类型,  
  14.     //Future的awaitUninterruptibly()调用会阻塞, 然后Redisson通过Redis的pubsub来监听unlock的topic(getChannelName())  
  15.     //例如, 5中所看到的命令 "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"  
  16.     //当解锁时, 会向名为 getChannelName() 的topic来发送解锁消息("0")  
  17.     //而这里 subscribe() 中监听这个topic, 在订阅成功时就会唤醒阻塞在awaitUninterruptibly()的方法.   
  18.     //所以线程在这里只会阻塞很短的时间(订阅成功即唤醒, 并不代表已经解锁)  
  19.     subscribe().awaitUninterruptibly();  
  20.   
  21.     try {  
  22.         while (true) {    //循环, 不断重试lock  
  23.             if (leaseTime != -1) {  
  24.                 ttl = tryLockInner(leaseTime, unit);  
  25.             } else {  
  26.                 ttl = tryLockInner();   //不多说了  
  27.             }  
  28.             // lock acquired  
  29.             if (ttl == null) {  
  30.                 break;  
  31.             }  
  32.   
  33.               
  34.             // 这里才是真正的等待解锁消息, 收到解锁消息, 就唤醒, 然后尝试获取锁, 成功返回, 失败则阻塞在acquire().  
  35.             // 收到订阅成功消息, 则唤醒阻塞上面的subscribe().awaitUninterruptibly();  
  36.             // 收到解锁消息, 则唤醒阻塞在下面的entry.getLatch().acquire();  
  37.             RedissonLockEntry entry = ENTRIES.get(getEntryName());  
  38.             if (ttl >= 0) {  
  39.                 entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);  
  40.             } else {  
  41.                 entry.getLatch().acquire();  
  42.             }  
  43.         }  
  44.     } finally {  
  45.         unsubscribe();  //加锁成功或异常,解除订阅  
  46.     }  
  47. }  

    主要的代码都加上了详细的注释, subscribe() 方法的代码复杂些, 但具体就是利用redis的pubsub提供一个通知机制来减少不断的重试.

    很多的Redis锁实现都是失败后sleep一定时间后重试, 在锁被占用时间较长时, 不断的重试是浪费, 而sleep也会导致不必要的时间浪费(在sleep期间可能已经解锁了), sleep时间太长, 时间浪费, 太短, 重试次数会增加~~~.

    到这里lock的逻辑已经看完了, 其他的比如tryLock方法逻辑和lock类似, 不过加了超时时间, 然后还有一种lock方法就是对key加上了过期时间.

7. 解锁源码

    unlock的逻辑相对简单

[javascript]  view plain  copy
  1. public void unlock() {  
  2.     connectionManager.write(getName(), new SyncOperation<Object, Void>() {  
  3.         @Override  
  4.         public Void execute(RedisConnection<Object, Object> connection) {  
  5.             LockValue lock = (LockValue) connection.get(getName());  
  6.             if (lock != null) {  
  7.                 LockValue currentLock = new LockValue(id, Thread.currentThread().getId());  
  8.                 if (lock.equals(currentLock)) {  
  9.                     if (lock.getCounter() > 1) {  
  10.                         lock.decCounter();  
  11.                         connection.set(getName(), lock);  
  12.                     } else {  
  13.                         unlock(connection);  
  14.                     }  
  15.                 } else {  
  16.                     throw new IllegalMonitorStateException("Attempt to unlock lock, not locked by current id: "  
  17.                             + id + " thread-id: " + Thread.currentThread().getId());  
  18.                 }  
  19.             } else {  
  20.                 // could be deleted  
  21.             }  
  22.             return null;  
  23.         }  
  24.     });  
  25. }  
  26.   
  27. private void unlock(RedisConnection<Object, Object> connection) {  
  28.     int counter = 0;  
  29.     while (counter < 5) {  
  30.         connection.multi();  
  31.         connection.del(getName());  
  32.         connection.publish(getChannelName(), unlockMessage);  
  33.         List<Object> res = connection.exec();  
  34.         if (res.size() == 2) {  
  35.             return;  
  36.         }  
  37.         counter++;  
  38.     }  
  39.     throw new IllegalStateException("Can't unlock lock after 5 attempts. Current id: "  
  40.             + id + " thread-id: " + Thread.currentThread().getId());  
  41. }  

    具体的逻辑比较简单, 我就不注释了, 大概就是, 如果是多次重入的, 就以此递减然后 set, 如果是只lock一次的, 就删除, 然后publish一条解锁的message到getChannelName() tocpic.

    这里解锁会重试五次, 失败就抛异常.

8.总结

    逻辑并不复杂, 但是通过记录客户端ID和线程ID来唯一标识线程, 实现重入功能, 通过pub sub功能来减少空转.

    优点: 实现了Lock的大部分功能, 提供了特殊情况方法(如:强制解锁, 判断当前线程是否已经获取锁, 超时强制解锁等功能), 可重入, 减少重试.

    缺点: 使用依赖Redisson, 而Redisson依赖netty, 如果简单使用, 引入了较多的依赖, pub sub的实时性需要测试, 没有监控等功能, 查问题麻烦, 统计功能也没有(例如慢lock日志, 2333333).


  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值