Redis分布式锁
Redis 分布式锁:解决秒杀中的超卖问题
现在你正在参与一个热门产品的秒杀活动
初步尝试:直接库存判断
我们开始的想法非常简单:只需要判断商品库存是否大于 0,如果是,就扣除库存;否则,秒杀失败。
1 | javaif (goods > 0) { |
然而,在多线程的情况下,同步操作的竞争可能导致数据混乱。许多请求同时到达,库存可能被错误地扣减,甚至出现超卖现象!
引入同步锁的思路
为了解决这个问题,很多人会想到使用同步锁:
1 | javasynchronized(goods) { |
这样一来,超卖问题得到了一定的缓解。但随之而来的问题是,多个线程情况下,必须等待持有锁的线程完成操作,这会增加服务器的压力,导致性能瓶颈,吞吐量也大幅下降。
有办法解决吞吐量问题吗
有的,兄弟!有的。
我们直接使用 Nginx 来进行负载均衡。通过水平扩展服务器并使用 Nginx 实现分布式集群部署,没有什么性能问题是堆量解决不了的。
但新的问题出现了,秒杀功能再次出现超卖的请开给你。因为同步锁的作用仅限于 JVM 级别,它只能锁住单个线程,而在分布式环境中,每台服务器的锁并不能相互影响。
终极解决方案:分布式锁
在这个时候,分布式锁便应运而生。当前主流的分布式锁方案有 Redis 和 Zookeeper。在这里,我们介绍一下Redis分布式锁,因为它的使用频率更高,且操作相对简便,一般公司用Redis的未必用得上Zookeeper,用Zookeeper的99%有Redis。
使用 Redis 的 SETNX 命令,我们可以轻松实现分布式锁。当一个线程尝试在 Redis 中存储一个值时,如果 goods 键没有值,它将成功存储并返回 true,表示加锁成功;如果已存在值,则返回 false。
但是,请务必记住:使用 SETNX 时,一定要设置 过期时间。否则,如果持有锁的服务器崩溃,其他正常请求将被阻塞,最终引发死锁。而设置过期时间后,锁会自动释放,确保其他请求可以顺利进行。
新问题:锁的过期与业务处理超时
然而,这又引出了新的问题:如果业务处理时间超过锁的过期时间,其他线程便可以获得锁,结果是“线程一”完成工作后,释放的是“线程二”的锁,造成超卖。
那么,如何解决这个问题呢?
实际上,我们面临两个主要问题:
- 锁过期时,业务处理尚未完成。
- 线程处理完后释放的是其他线程的锁。
解决方案如下:
- 延长锁的过期时间,同时实现一个看门狗机制,定期检查线程是否存活,若存活则重置过期时间。
- 为锁分配一个唯一 ID(可以使用雪花算法或 UUID)。
当然,手动实现这些机制可能会相对繁琐。不如直接使用 Redis 的 Redisson 组件,它已经为我们封装了这些复杂的逻辑。
Redis 集群的注意事项
最后,值得一提的是,许多公司使用 Redis 集群(主从架构),在这种情况下,如果主节点宕机,可能会影响锁的有效性。因为 Redis 集群采用的是 AP 模式(高可用、高性能,但不能保证高一致性)。当您设置一个锁时,锁只会在主节点上设置。如果主节点成功设置锁,而从节点尚未同步,则会导致线程安全问题。在这种情况下,使用 Redlock 解决方案可以确保所有节点都成功存储锁后,才能返回响应,避免潜在的超卖问题。

