今天读了redis分布式锁的相关内容,刚才写了很多,结果发布的时候,出了bug,正文内容全没了,重新写一下。
目前redis因其优良的性能及多种数据结构的支持,已经被越来越多的公司使用,其中一些高并发场景,如抢购、秒杀、抢票等,会用到redis来实现分布式锁及减库存操作,但是,在使用过程中,可能出现一些超卖的情况,这些基本上是由于redis分布式锁实现方式及减库存操作非原子性引起的,在此分析下常见的分布式锁实现方案。
1.redis setnex实现分布式锁。
1.1 目前,redis已经支持set(key,val,ex|px,nx|xx)来保证原子性,但是在低版本(2.6.12以前),只能通过setnx和setex两条命令来实现,是非原子性操作(如果一个进程在setnx以后panic了,那这个锁就永远不能unlock了),具体可参考SET - Redis 命令参考。常见的加锁方式如下:
//10s nx加锁
set(k,v,100s,nx)
//业务逻辑
...
//解锁
unlock
1.2 这种情况下,只关心k是否已经存在,不关心val的值。但是,在高并发模式下,这种方式会出现问题。比如,秒杀时,负载极高时,可能出现进程A业务逻辑处理时间超过10s的情况,这种情况下,进程A还在处理业务逻辑时,锁已经失效了。此时进程B可以获取到锁,并处理业务,如果在处理过程中,A走到了解锁逻辑,就会把B进程的锁unlock掉。这样逐级往后,会出现套娃情况,后果无法预料。
1.3 因此做优化,来保证进程A只能unlock掉自己的锁。可以启用val,比较不同进程的val值,与set值相同,才可以unlock,此时需要保证不同进程获取到的val不一样,可以用各种开源的getuuid算法。伪代码如下:
//10s nx加锁
set(k,v,100s,nx)
//业务逻辑
...
//解锁
if(val == get(k)){unlock
}
但是这样,解锁还不是原子操作,为了保证原子操作,可以用redis的lua脚本功能,用lua脚本的原子性来保证解锁的原子性。得解。但是有一个最大的问题,它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失(主要表现情况是:redis在master加锁了,还未来得及同步到slave,发生了主从切换,导致锁丢失,这种情况下此种实现方式无解,详细参考3.redlock)。
2.redisson:Java实现的对各种redis的各种功能封装,包括分布式锁,红锁,哨兵、主从等,原子操作主要是通过lua脚本来实现的,感兴趣的同学可以去看下源码。具体功能可以参考redisson官网:https://redisson.org/。
3.redlock(红锁):这是一个大神写的,主要用于redis多实例分布式锁,其实现依赖于redis的多个无关联的实例。只要n/2+1个实例顺序加锁成功(超过设置的超时时间视为失败),并且加锁时间和最后总结下,如果并发量不大,可以考虑1.1的方式;如果高并发,则可以考虑1.2和1.3的方案;如果对加锁有极致的要求,则可以考虑redlock。
以上。