redis分布式锁

186 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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

集成测试

手动续约的问题:

  1. 多久续约一次,续多长:让用户指定续约的间隔。和网络,Redis服务器的稳定性有关。约定的过期时间

  2. 如果返回error怎么处理?

    • 能处理:超时

      • 再次尝试续约,因为超时具有偶发性;缺点,如果是Reids服务器奔溃或者网络不通。会导致无限次尝试续约
    • 无法处理,向用户报错:服务器问题,不是自己的锁没拿到锁

  3. 如果确认续约失败了,怎么中断后续的业务:手动检测分布式锁,手动中断

  4. 续约次数上限?无,手动续约即可

java实现中,可以使用Redisson实现分布式锁,解决锁自动续约的问题

加锁重试

  1. 如果超时,直接加锁

  2. 检查key对应的值是不是超时加锁请求的值

    1. 是:直接返回,前一次加锁成功,考虑重置过期时间
    2. 不是:直接返回加锁失败
  3. 重试策略:

    1. 超时了,可以充实;如果此时正有人吃有所,需要等别人释放锁
    2. 设计成迭代器的形态:用户可以轻易通过扩展接口实现自己的重试策略;缺点:没有引入上下文的概念,用户在实现接口的时候没有办法根据上下文决定要不要重试。
  1. 重试的问题:幂等性,大量请求超时堆积

Redlock

在集群模式的Redis中,如果主节点宕机,会出现锁异常的场景。比如说主节点加锁之后宕机,未同步给其他节点。其他节点通过一段时间升级成主节点,也进行加锁。当原主节点重启,会发现原先的加锁已经丢失。

Redlock用来解决集群中的分布式锁的问题。

singleflight优化

在非常高并发,并且热点集中的情况下,可以考虑结合 singleflight 来进行优化。也就是说,本地所有的 goroutine 自己先竞争一把,胜利者再去抢全局的分布式锁。

总结

  1. 分布式锁怎么实现:利用 setnx 命令,在分布式环境下不同实例之间抢一把锁。设置锁的过期时间并且使用uuid对锁进行唯一表示。前者避免宕机对整个流程的影响,后者判断加锁者是否是自己。过期时间因项目而定,面对极端实例可以引入续约机制。加锁失败可以重试,需要使用lua脚本
  2. 分布式锁的过期时间怎么设置?根据自己的业务耗时进行定义,时间长短不好定义,可以定一个适合的时间,如果遇到执行时间特别耗时的实例,可以手动续约
  3. 分布式锁加锁失败的原因:超时,网络故障,Redis服务器故障,锁被人持有着
  4. 如何优化,使用singleFlight

参考文献

用万字长文来讲讲本地锁至分布式锁的演进和Redis实现,扩展 Redlock 红锁

极客学堂《用Redis实现一个分布式锁》,实现代码:github.com/gotomicro/r…