redission 分布式锁 可重入锁(一)

742 阅读5分钟

组建了一个每日学习群 互相督促 已经开始一段时间了,有想法的可以 加微信 yuyezhiji 麻烦备注原因

1 不要去 开课吧 问原因就 自己百度去

2 背景

3 思考

本节就是要分阶段把 公平锁每个阶段 中redis的变化 图解画出来

4 应用场景

5 使用可重入锁的步骤

5.1 RLock rLock = redissonClient.getLock("lockKey");

5.1.1 入参

commandExecutor name 例如 lockKey

5.1.2 RLock 对象的属性

id UUID 7982de28-5221-4492-81c9-5ecf86d46de9

internalLockLeaseTime 默认30s watch dog 默认的 调度间隔

entryName 7982de28-5221-4492-81c9-5ecf86d46de9:lockKey

pubSub

5.2 rLock.lock();

5.2.1 知识点

rLock用的是 RedissonLock

默认leaseTime 为-1 只要不unlock 永久持有锁

5.2.2 步骤

5.2.3 获取线程Id

5.2.4 执行tryAcquire方法

参数

KEYS【1】 锁的名字 lockKey

ARGV【1】 存活时间 30s 30000

ARGV【2】 client 锁的名称 this.id + ":" + threadId;   例如 13aa66f1-d6b0-43b8-8ca6-fa364d52b571:1

    lock 持续时间 有限 evalWriteAsync方法的执行
    执行方法

```
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;\
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;\
return redis.call('pttl', KEYS[1]);
```

方法解析

```
# 是否加过锁\
if (redis.call('exists', lockKey) == 0) then\
    # 之前没有加过锁 \
    redis.call('hset', lockKey, 13aa66f1-d6b0-43b8-8ca6-fa364d52b571:1, 1);\
    # 设置锁的过期时间\
    redis.call('pexpire', lockKey, 30000);\
    return nil;\
end;\
# 之前已经加过锁 本次为 续约\
if (redis.call('hexists', lockKey, 13aa66f1-d6b0-43b8-8ca6-fa364d52b571:1) == 1)then\
    # 可重入锁 多次\
    redis.call('hincrby', lockKey, 13aa66f1-d6b0-43b8-8ca6-fa364d52b571:1, 1);\
    redis.call('pexpire', lockKey, 30000);\
    return nil;\
end;\
return redis.call('pttl', lockKey);
```

执行完之后 redis的存储情况
lockName 的一个map

```
{\
    '13aa66f1-d6b0-43b8-8ca6-fa364d52b571:1': 1\
\
}
```

5.3 rLock.unlock();

evalWriteAsync

getNodeSource

前提

前面加锁的参数和 方法都准备好了 该找到redis 去执行了

步骤

calcSlot 从16384中 用 hash 找到 要去哪个slot 执行lua

使用的是 MasterSlaveConnectionManager 类

存储结构

MasterSlaveConnectionManager 类 slot2entry Map对象

一个map key是0-16383 value是 MasterSlaveEntry 实体类

在redis 链接初始化的时候就初始化好了

6 基本概念

6.1 watch dog

看门狗 默认第一次 锁30s ,每10s 就把lockKey 重新续约为30s

6.2 lua 脚本

有的时候 redis 进行多次操作 ,默认是没有原子性的,需要保证一致性 就需要使用 lua脚本
可以保证redis 数据一致性
6.2.1 应用场景
    1. 减库存的时候 为了保证高并发的访问 就可以模仿ConcurrentHashMap 的实现 ,把库存分到多个reids 集群中,然后lua 实现取库存,如果redis01 库存不够 就看看其他的redis 可以凑出来不
    2 对redis 进行多次操作,需要保证一致性
    

7 前面思考和应用场景解答 (在此之前 请再次思考前面的问题 )

7.1 如果用不同的线程 unlock 有什么返回

1 不加锁 直接 解锁 返回成功 
2 a线程 加锁 另一个b线程 解锁 
报错
attempt to unlock lock, not locked by current thread by node id: c80537ed-565d-41b1-9506-7890d4a49e5c thread-id: 66
解决方法:
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
     lock.unlock();
}

请看 RedissonDistributedLock 类 unlock 方法

7.2 用的云Redis 线上分布式锁 没锁住 怎么回事

背景:
1 可能单机的时候 好使 上正式环境 有的时候 就不好使
2 用的是阿里的redis 集群部署的服务器 ,我们RedisConfig 连接的是 域名链接
猜测:
1. 可能是集群 然后每次加锁/判断 不是同一个redis
2  用户lockKey 锁的不对(实际是这个问题 后面案例分析 会说到)

验证:
1. 开一个线程池 , 尝试同时加锁,看有没有同时加锁成功的.如果 有就说明是集群的问题

结果:
1. 只有一个线程抢到了 分布式锁

问阿里云的问题 和 反馈
1. redis 一个key 访问 ,会不会落到集群中的同一个节点
  答 会的,这个key存在某个节点,访问都是走这个节点的
  
为什么:
1 虽然业务系统 slot 需要链接的都是域名的url,但是阿里云 对同一个key的访问都是落到同一个redis 节点


预防:
1. 分布式锁 不应该使用集群部署 可以切换为单机 或者 主从模式
    1.1 Redis的主从同步(replication)是异步进行的,如果向master发送请求修改了数据后master突然出现异常,发生高可用切换,缓冲区的数据可能无法同步到新的master(原replica)上,导致数据不一致。如果丢失的数据跟分布式锁有关,则会导致锁的机制出现问题,从而引起业务异常
    解决办法[:](https://help.aliyun.com/document_detail/146758.html#section-zlb-ois-q4f)

思考:
1. 如果redission 直接链接的配置的多个ip 是集群访问呢
   答: 也不会有问题 也是落到同一个节点

7.3 怎么知道 去哪个redis 上执行lua脚本呢,怎么存储的呢,负载均衡怎么做的

MasterSlaveConnectionManager 类 slot2entry Map对象

一个map key是0-16383 value是 MasterSlaveEntry 实体类
请打个断点 看一下

7.4 redis 分为 16384个槽 想到了什么

1. 一个组件 想支持 分片部署 常见办法 就是 slot分片 
   1.1 后面可以给出分片代码 (如果有想看的 可以后面来一节 说下nacos 关于distro 协议 filter 拦截,把请求发到指定的node节点的代码)
   1.2 线程安全Map 底层分为16块,为了减少并发冲突 也是分片的概念
2 

8 总结:

Redission 分布式锁(一).png

9 下节预知

  1. watch dog 讲解 2 阿里规约关于分布式锁 为什么要那么写

10 目标:

  1. 随着学习 把一些坑解决 形成一个 redission的基础组件

代码地址: gitee.com/gf-8/yuye-p…

项目: yuye-test-redission

类地址: 对应的test 包之下Test类

11 思考题

就是应用场景 那两个 调用哪个方法?有什么需要注意的么