开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情
redis分布式锁
基础概念
定义:在分布式环境下不同实例之间抢一把锁
背景:多服务并发的时候,每个服务都是单独的,加锁操作彼此不能感知,即无法共享锁信息
实现:
就是利用 setnx 命令,确保可以排他地设置一个键值对。
思路
- 读请求过来,判断缓存中是否存在数据,存在立马返回,不存在则往下走。
- 读请求尝试获取读锁,使用
setnx命令- 向Redis中尝试set一个值,当且仅当值不存在时设置成功。
- 设置成功返回 1,否则返回 0
- 获取成功后,查询数据库,重新构建缓存。
-
设置过期时间:防止加锁实例崩溃之后,没有人去释放锁
- 时间长度:过短导致业务没完成锁就过期;过长会出现实例真的奔溃后,其他实例长时间拿不到锁
- 解决方法:续约,过期时间不必设置得很长,如果奔溃没有人再续约,自然过期;时间不够用,自动发起续约
-
用UUid作为值:用唯一的值比较是是否是某个实例加的锁,确保同一把锁的值不会冲突
- 这个时候解锁操作已经不是一个原子性操作了,第一步是查询判断,第二步才是解锁操作。需要用到Redis的lua脚本,保证原子性
测试:
redis.Cmdable 的类型,它是一个接口,本质上就是为了我们能够在测试阶段注入一个 mock 实现。
mockgen -package=mocks -destination=mocks/redis_cmdable.mock.go github.com/go-redis/redis/v9 Cmdable
集成测试
手动续约的问题:
-
多久续约一次,续多长:让用户指定续约的间隔。和网络,Redis服务器的稳定性有关。约定的过期时间
-
如果返回error怎么处理?
-
能处理:超时
- 再次尝试续约,因为超时具有偶发性;缺点,如果是Reids服务器奔溃或者网络不通。会导致无限次尝试续约
-
无法处理,向用户报错:服务器问题,不是自己的锁没拿到锁
-
-
如果确认续约失败了,怎么中断后续的业务:手动检测分布式锁,手动中断
-
续约次数上限?无,手动续约即可
java实现中,可以使用Redisson实现分布式锁,解决锁自动续约的问题
加锁重试
-
如果超时,直接加锁
-
检查key对应的值是不是超时加锁请求的值
- 是:直接返回,前一次加锁成功,考虑重置过期时间
- 不是:直接返回加锁失败
-
重试策略:
- 超时了,可以充实;如果此时正有人吃有所,需要等别人释放锁
- 设计成迭代器的形态:用户可以轻易通过扩展接口实现自己的重试策略;缺点:没有引入上下文的概念,用户在实现接口的时候没有办法根据上下文决定要不要重试。
- 重试的问题:幂等性,大量请求超时堆积
Redlock
在集群模式的Redis中,如果主节点宕机,会出现锁异常的场景。比如说主节点加锁之后宕机,未同步给其他节点。其他节点通过一段时间升级成主节点,也进行加锁。当原主节点重启,会发现原先的加锁已经丢失。
Redlock用来解决集群中的分布式锁的问题。
singleflight优化
在非常高并发,并且热点集中的情况下,可以考虑结合 singleflight 来进行优化。也就是说,本地所有的 goroutine 自己先竞争一把,胜利者再去抢全局的分布式锁。
总结
- 分布式锁怎么实现:利用 setnx 命令,在分布式环境下不同实例之间抢一把锁。设置锁的过期时间并且使用uuid对锁进行唯一表示。前者避免宕机对整个流程的影响,后者判断加锁者是否是自己。过期时间因项目而定,面对极端实例可以引入续约机制。加锁失败可以重试,需要使用lua脚本
- 分布式锁的过期时间怎么设置?根据自己的业务耗时进行定义,时间长短不好定义,可以定一个适合的时间,如果遇到执行时间特别耗时的实例,可以手动续约
- 分布式锁加锁失败的原因:超时,网络故障,Redis服务器故障,锁被人持有着
- 如何优化,使用singleFlight
参考文献
用万字长文来讲讲本地锁至分布式锁的演进和Redis实现,扩展 Redlock 红锁
极客学堂《用Redis实现一个分布式锁》,实现代码:github.com/gotomicro/r…