加锁

  1. 生成一个特殊值(比如随机值+当前线程ID),记录在在ThreadLocal里。
  2. 通过setnx向特定的key写入第一步生成的特殊值,同时设置失效时间,操作成功则代表加锁成功。
SET resource_name my_random_value NX PX 30000

说明

  • 设置一个失效时间是为了避免死锁。
  • 写入一个特殊值是为了避免加锁与解锁不是同一线程,比如A线程先获取锁后,在某个时间点锁已过期但A线程业务仍未执行完,些时B线程获取锁开始执行业务,接着A线程结束业务操作后会释放B线程加的锁
  • 写入特殊值与设置失效时间是同时的是为了保证加锁是原子操作。

解锁

根据key取得特殊值,从ThreadLocal里取出特殊数,进行二者判断,删除redis上的key数据,要保证获取数据、判断特殊值是否匹配及删除数据这三个操作是原子的,可通过lua脚本实现。

if redis.call("get",KEYS[1]) == ARGV[1] then
     return redis.call("del",KEYS[1])
else
     return 0
end

潜在的问题:过期时间设多长?

设置过小,有可能上一个线程还没执行完锁的逻辑锁就释放了,另一个线程获取锁然后出现并发问题。
设置过大,如果客户端断线了,这个锁要等待很长时间。
这个问题Redisson提供了自动延期机制也就是watch dog

Redisson中客户端一旦加锁成功,如果客户端没有主动设置失效时间就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10s(可配置)检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。
如果客户端出现了宕机,那客户端加的锁会怎么样呢?首先因为续锁线程(一个定时任务)和加锁线程同在一个JVM实例里,机器宕机后续锁线程也会挂掉所以不会出现一直续期的情形。另外在在客户端没有设置失效时间,Redisson会有一个默认的过期时间30s(也可以通过Config.lockWatchdogTimeout配置进行调整),因为续期线程已不存在,所以到了时间后自然会删除锁,这样就不会存在死锁的情形。
另外需要说明下,线程被中断或在续期过程中设置过期时间失败都会释放锁。

image.png

这个方案是目前最优的分布式锁方案,但是如果在Redis集群环境下依然存在问题:

由于Redis集群数据同步为异步,假设在Master节点获取到锁后未完成数据同步情况下Master节点crash,此时在新的Master节点依然可以获取锁,所以多个Client同时获取到了锁。针对这个问题,之前提出了Redlock解决方案,不过现在已被官方弃用了,原因是Redisson RedLock 是基于联锁 MultiLock 实现的,但是使用过程中需要自己判断 key 落在哪个节点上,对使用者不是很友好。这个是被弃用的说明:Redlock弃用原因,下面是英文文档:
https://github.com/redisson/r...

8.4. RedLock
This object is deprecated. RLock operations now propagated to all Redis slaves.

redis的三种客户端
在项目中一般会混用jedis与redssion,可能会用到redssion的watch dog
最后还有缓存雪崩/穿透/击穿等问题的应对

针对缓存雪崩一般的应对方案有互斥量和永不过期,这里对互斥量的方案作下补充:使用互斥量不好的一点是限制了请求的并发,其实我们可以考虑使用Semphore对一定的线程数量进行控制,这样在不对数据库有较大影响的前提下允许了并发量。

redis bigkey hotkey问题
针对bigkey一是看能否调整数据结构(如由string->hash),二是避免使用redis存储
针对hotkey一是不设置过期时间,二是使用JVM缓存,三是进行资源隔离(如hystrix)或不同的线程池

参考的文档:
RedissonBaseLock源码
Redisson分布式锁源码分析
Redlock的说明
Redlock源码
Redlock源码分析
Redisson锁续期定制化调整
Redisson重连后WatchDog失效问题解决
Redisson说明文档


步履不停
38 声望13 粉丝

好走的都是下坡路