Redis 分布式锁
分布式锁的演变
- 本地锁(单机用)
- 利用redis进行分布式锁 使用 set
- 防止死锁 加过期时间 使用 setnx
- 防止A请求未执行完 锁过期删除 B请求加锁后 A完成后误删该锁 使用 Hash结构, 规定每个请求只能删除自己的锁
- 保证并发安全,申请锁和加过期时间需要 原子性,用 lua脚本 加锁或解锁
- 考虑到 重入性 (每个请求只拿到一个锁后,可以多函数或线程共用) 使用 Hash结构进行加减(hincrby) 操作
- 为了保证业务执行过长,锁不会过期。需要对锁进行 续期。
分布式锁的特性
- 独占性
- 高可用
- 防死锁
- 不乱抢
- 可重入
1. setNX
- 注意死锁
- 注意lock过期时间和业务执行时间
- 加锁不成功时,等待获取机制 ( 注意停几毫秒 )
- 一般情况下完全够用
2. 考虑look重入性
- 当一个锁被创建出来之后再同一个请求中不需要再额外申请其他锁,一个锁可以被重复使用,所以使用到 Hash数据结构的锁
- 如果业务上需要应用到多个函数的锁的情况下,申请一个锁之后固定当前请求的uuid+当前线程id。入一个函数(或者线程)向Hash的uuid +1
- 直到请求结束后,检查线程锁是否归0 如果是释放该锁,如果不是说明其他线程未能执行完毕。
- 考虑可重入性的递减。 加锁几次就要减几次,到0后删除
锁的操作保证原子性应对高并发
- 当一个锁(Hash结构) 创建锁和增加过期时间,两步需要lua脚本进行执行保证原子性。
// lua 脚本编写
// 加锁操作 如果锁不存在或者当前请求锁的uuid字段存在 则进行加一 (重入性)
KEYS[1] // -- 分布式锁的key
ARGV[1] // -- 锁的唯一标识,通常是线程ID或调用者标识
ARGV[2] // -- 锁的过期时间,单位为毫秒
if redis.call('exists', KEYS[1]) == 0 //锁不存在
or redis.call('hexists', KEYS[1], ARGV[1]) == 1 // 当前请求有锁 进入函数(或新开线程)无需额外申请锁
then
redis.call('hincrby',KEYS[1], ARGV[1], 1)
redis.call('expire',KEYS[1], ARGV[2])
return true
else
return false
end
//解锁操作
if redis.call("hexists",KEYS[1], ARGV[1]) == 0 then // 无锁 无需解锁
return false
else if redis.call("hincrby",KEYS[1], ARGV[1], -1) == 0 then // 锁存在且是自己请求的锁 进行减一操作,如果减为0 则解锁(删key)
retrun redis.call("DEL",KEYS[1])
else
return 0
- 当lock 创建时,需要同步启动一个定时续期任务,锁存在并过该定时时间进行续期,防止业务未完直接释放锁。
- 当主线程执行完业务流程 并释放锁之后,续期机制同时结束。