Redis Lua

前言

上面这里是根据上一节中分析的Redis锁的Demo去分析具体这里面的代码或者说是Lua脚本是如何去获取锁的具体分析,具体代码在这里。Redisson

1.分析Lua脚本获取重入锁的分析

前提: 在执行redissonLock.lock()方法开始,进入的流程如下:

  1. ##主要是实现加锁和锁的续命
  2. redissonLock.lock();
  3. ##深入执行的代码
  4. @Override
  5. public void lock() {
  6. try {
  7. lockInterruptibly();
  8. } catch (InterruptedException e) {
  9. Thread.currentThread().interrupt();
  10. }
  11. }
  12. ##进入lockInterruptibly方法内部
  13. @Override
  14. public void lockInterruptibly() throws InterruptedException {
  15. lockInterruptibly(-1, null);
  16. }
  17. ##进入重参lockInterruptibly方法内部
  18. @Override
  19. public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
  20. // 获取当前线程ID
  21. long threadId = Thread.currentThread().getId();
  22. // 尝试获取锁的剩余时间
  23. Long ttl = tryAcquire(leaseTime, unit, threadId);
  24. // lock acquired ttl为空,说明没有线程持有该锁,直接返回 让当前线程加锁成功
  25. if (ttl == null) {
  26. return;
  27. }
  28. RFuture<RedissonLockEntry> future = subscribe(threadId);
  29. commandExecutor.syncSubscription(future);
  30. // 死循环
  31. try {
  32. while (true) {
  33. // 再此尝试获取锁的剩余时间 ,如果为null, 跳出循环
  34. ttl = tryAcquire(leaseTime, unit, threadId);
  35. // lock acquired
  36. if (ttl == null) {
  37. break;
  38. }
  39. // waiting for message 如果ttl >=0 说明 有其他线程持有该锁
  40. if (ttl >= 0) {
  41. // 获取信号量,尝试加锁,设置最大等待市场为ttl
  42. getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
  43. } else {
  44. // 如果ttl小于0 (-1 ,-2 ) 说明已经过期,直接获取
  45. getEntry(threadId).getLatch().acquire();
  46. }
  47. }
  48. } finally {
  49. unsubscribe(future, threadId);
  50. }
  51. // get(lockAsync(leaseTime, unit));
  52. }
  53. ##获取锁方法tryAcquireAsync(leaseTime, unit, threadId)方法
  54. private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
  55. return get(tryAcquireAsync(leaseTime, unit, threadId));
  56. }
  57. private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
  58. if (leaseTime != -1) {
  59. return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
  60. }
  61. // 刚开始 leaseTime 传入的是 -1 ,所以走这个分支
  62. // 1)尝试加锁 待会细看 先把主要的逻辑梳理完
  63. RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
  64. // 2) 注册监听事件
  65. ttlRemainingFuture.addListener(new FutureListener<Long>() {
  66. @Override
  67. public void operationComplete(Future<Long> future) throws Exception {
  68. if (!future.isSuccess()) {
  69. return;
  70. }
  71. Long ttlRemaining = future.getNow();
  72. // lock acquired
  73. if (ttlRemaining == null) {
  74. // 3)获取锁成功的话,给锁延长过期时间
  75. scheduleExpirationRenewal(threadId);
  76. }
  77. }
  78. });
  79. return ttlRemainingFuture;
  80. }
  81. // 1)尝试加锁 待会细看 先把主要的逻辑梳理完
  82. RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);

结果:上面最后其实归纳一句就是执行该方法tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command)
内部就是执行Lua脚本,下面具体就是分析这个

关于Lua第一阶段脚本的分析

  • Lua脚本如下(非公平锁),具体就是对相关的逻辑细节分析(在tryLockInnerAsync方法内的Lua脚本): ```lua // 如果 lockKey不存在 ,设置 使用hset设置 lockKey ,field为 uuid:threadId ,value为1 ,并设置过期时间

//就是这个命令

//127.0.0.1:6379> hset lockkey uuid:threadId 1 //(integer) 1 //127.0.0.1:6379> PEXPIRE lockkey internalLockLeaseTime

“if (redis.call(‘exists’, KEYS[1]) == 0) then “ + “redis.call(‘hset’, KEYS[1], ARGV[2], 1); “ + “redis.call(‘pexpire’, KEYS[1], ARGV[1]); “ + “return nil; “ + “end; “ +

  1. > 备注:
  2. > KEYS[1] ---------> getName()<br />ARGV[1] ---------> internalLockLeaseTime<br />ARGV[2] ---------> getLockName(threadId) 实现 id : threadId
  3. - 在执行`redis.call('exists', KEYS[1]) == 0 `中判断 KEYS[1] 对应的锁名是否存在,不存在就执行下面Lua脚本
  4. - 在执行`redis.call('hset', KEYS[1], ARGV[2], 1)`就是对应`hset lockkey uuid:threadId 1`指令创建`lockkey`key,并对对其内部设置的键值对大致如下
  5. ```json
  6. //其json对应的key为lockkey,其value值如下
  7. {
  8. "8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
  9. }
  10. 当中“8743c9c0-0795-4907-87fd-6c719a6b4586:1”对应值1为可重入做的次数,假如重入了就会叠加1,释放时候会减1,直到为0就释放锁
  • 执行redis.call('pexpire', KEYS[1], ARGV[1])就是对应执行PEXPIRE lockkey internalLockLeaseTime指令,对lockkey设定生命时间,其中使用[PEXPIRE ](http://redis.cn/commands/pexpire.html)是因为设置的key生命时间十一毫秒为单位,而[EXPIRE](http://redis.cn/commands/expire.html)则为秒为单位

    关于Lua第二阶段Redisson为何重入原理

    这是链接上面第一阶段的代码分析之后是怎么操作的,代码如下: ```lua

// 如果 lockKey 存在和 filed 和 当前线程的uuid:threadId相同 key 加1 ,执行多少次 就加多次 设置过期时间 其实就是如下命令 //127.0.0.1:6379> HEXISTS lockkey uuid:threadId //(integer) 1 //127.0.0.1:6379> PEXPIRE lockkey internalLockLeaseTime

“if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then “ + “redis.call(‘hincrby’, KEYS[1], ARGV[2], 1); “ + “redis.call(‘pexpire’, KEYS[1], ARGV[1]); “ + “return nil; “ + “end; “ +

// 最后返回 lockkey的 pttl

“return redis.call(‘pttl’, KEYS[1]);”


- 分析第一个判断指令`redis.call('hexists', KEYS[1], ARGV[2]) == 1`测主要判断lockkey是否存在外,还判断其内部结构`8743c9c0-0795-4907-87fd-6c719a6b4586:1`这个键也是否存在。【具体对`hexists`与`exists`的区别,下面补充资料中有提到】
- 假如lockkey及其内部 `id : threadId`存在,就执行`redis.call('hincrby', KEYS[1], ARGV[2], 1)`指令,给对应的`id : threadId`值加1,数据如下:
```json
lockkey:{
  "8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
}

hincrby指令变为如下
lockkey:{
  "8743c9c0-0795-4907-87fd-6c719a6b4586:1":2
}
  • 接着执行redis.call('pexpire', KEYS[1], ARGV[1])指令,给keylock再次添加有效时间。
  • 假如if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)条件不成立,就返回nil,获取不到锁资源,然后退出

    截取资料

  1. 使用Redisson实现可重入分布式锁原理
  2. Redis进阶- Redisson分布式锁实现原理及源码解析
  3. Redis中文网之命令

    补充资料

    redis指令中hexistsexists的区别?

  • 举例redis存储的机构如下

    KEY1:{
    "KEY2":VALUE2
    }
    

    **exists**是判断最外层key1是否存在,**hexists**是判断一个hash结构内部的subkey是否存在 **exists key1 key2**

    redis执行Lua脚本时,那些具有原子性?

  • Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI / EXEC 包围的事务很类似。 在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。 另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。写一个跑得很快很顺溜的脚本并不难, 因为脚本的运行开销(overhead)非常少,但是当你不得不使用一些跑得比较慢的脚本时,请小心, 因为当这些蜗牛脚本在慢吞吞地运行的时候,其他客户端会因为服务器正忙而无法执行命令。【Redis中文官网截取