分布式锁实现条件
原子性:加锁和释放锁的操作必须满足原子性。
无死锁:不会发生死锁(例如已获得锁的线程/进程在释放锁之前突然异常退出,导致其他线程/进程会一直在循环等待锁被释放)。
互斥性:同一个时刻只能有一个线程/进程占有锁,其他线程/进程必须等待直到锁被释放。
可重入性:当前线程/进程获得锁之后,还可以继续调用获取锁的操作,第二次以及之后的获取锁的操作不会被阻塞等待(释放锁的操作也是一样的,调用多次之后,只有最后一次释放锁的时候才会真正地释放锁),这个条件根据业务来决定是否需要实现。
Redis实现分布式方式
分布式锁实现需要支持加锁和设置有效期是原子性的,需要保证Redis执行2个操作是原子的。可以使用SET命令和Lua脚本等方式实现。
1. SET命令
从2.6.12版本开始,redis为SET命令增加了一系列选项:
EX seconds – 设置键key的过期时间,单位时秒;
PX milliseconds – 设置键key的过期时间,单位时毫秒;
NX – 只有键key不存在的时候才会设置key的值;
XX – 只有键key存在的时候才会设置key的值。
因此可以在单个SET命令中实现只有key不存在的时候设置一个key,并且同时设置key的有效时间,这整个过程都是原子性的。
实现注意点:
分布式锁,同一业务使用锁定的key需要保持一致;
value的有效期根据业务设置合理时间,尽量避免因执行时间过长导致锁自动释放的情况。当然,调用方崩溃导致无法主动释放锁,这个时候应该在合适时间后被动释放。
存在锁定时间过长,导致锁被动释放,当执行完成再释放锁时可能错误释放其他调用方的锁。考虑设置的value为唯一(如线程ID+时间戳),释放时比对value和自己设置的值是否一致,一致的情况下才释放锁(该过程,为了保证原子性,考虑使用Lua脚本实现判断和删除)。
减少因锁时间到期被动释放的情况,一般的处理方式是获得锁的线程开启一个守护线程,用来给快要过期的锁"续航"。比如过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁"续航"20秒。守护线程从第29秒开始执行,每20秒执行一次检查。当线程A执行完任务,会显式关掉守护线程。
Redis集群中,在master上加上锁,master异步复制给slave,如果复制完成前master宕机,主备切换,会导致新master未加上,其他请求过来时可以加锁,这时候存在不互斥情况。
2. Lua脚本
Java redis 库Redisson利用Lua脚本实现了读锁、写锁、公平锁等。
redisson/src/main/java/org/redisson/RedissonWriteLock.java 中写锁实现如下:

其中
KEY[1]:加锁的key;
ARGV[1]:锁key的默认生存时间,默认30秒;
ARGV[2]:加锁的客户端的ID,UUID_01:threadId_01:write。
实现逻辑:
使用Hash存储锁结构,
1. 获取加锁key hash中的mode值,没有被加锁,model=false;
2. hset lock_key mode write;
3. hset lock_key client_id 1;
4. pexpire lock_key 30000;
5. 当再有加锁请求,此时mode=write;
6. 通过hexists lock_key call_client_id 获取值,判断call_client_id 是否等于client_id (可重入锁实现);
7. 如果不等于,直接返回 lock_key的有效时间,以便调用方根据时间重试(一般客户端采用while + sleep方式重试加锁);
8. 如果call_client_id 等于client_id,说明是同一个客户端再次加锁;
9. hincrby lock_key client_id 1, 使hash结构中的client_id 值增加1,表示获得锁此时加1;
10. pexpire lock_key pttl + 30000, 对锁到期时间再增加一个默认生成时间;
11. 返回 lock_key 到期时间。
锁的释放过程中是对锁hash结构中的client_id值 -1, 当对应的值变为0时,del lock_key 删除锁。
加锁时间默认为30秒,客户端加锁成功后会启动watch dog线程,它是一个后端线程,每隔指定时间(如10秒)检查下,如果客户端还持有锁,锁时间快到期时,则自动延长一定时间。
redisson/src/main/java/org/redisson/RedissonReadLock.java中读锁实现如下:

其中
KEY[1]:加锁的key;
KEY[2]:加锁的超时前缀,如{lock_key}:UUID_01:threadId_01:rwlock_timeout;
ARGV[1]:锁key的默认生存时间,默认30秒;
ARGV[2]:客户端id,即锁名称,如UUID_01:threadId_01;
ARGV[3]:写锁名称,如UUID_01:threadId_01:write。
实现逻辑:
使用Hash存储锁结构,
1. 获取加锁key hash中的mode值,没有被加锁,model=false;
2. hset lock_key mode read;
3. hset lock_key client_id 1;
4. set {lock_key}:UUID_01:threadId_01:rwlock_timeout:1 1;
5. pexpire {lock_key}:UUID_01:threadId_01:rwlock_timeout:1 30000;
6. pexpire lock_key 30000;
7. 当再有加锁请求,此时model=read;如果mode不等于read,或者write且写锁锁定1次则直接返回锁到期时间;
8. hincrby lock_key UUID_01:threadId_01 1, 将读锁次数增加1;
9. set {lock_key}:UUID_01:threadId_01:rwlock_timeout:2 1;
10. pexpire {lock_key}:UUID_01:threadId_01:rwlock_timeout:2 30000;
11. pexpire lock_key max(pttl,30000),设置锁时间为锁到期时间和默认时间中较大的值。
读锁加定时,其他调用方不能修改锁定内容,等所有读锁释放后才可以进行修改,释放过程逐渐对锁hash结构中client_id字段-1,直到所有读锁均释放了才删除锁。




