Redis 分布式锁:解决秒杀中的超卖问题

现在你正在参与一个热门产品的秒杀活动

初步尝试:直接库存判断

我们开始的想法非常简单:只需要判断商品库存是否大于 0,如果是,就扣除库存;否则,秒杀失败。

1
2
3
4
5
javaif (goods > 0) {
goods--;
} else {
// 秒杀失败
}

然而,在多线程的情况下,同步操作的竞争可能导致数据混乱。许多请求同时到达,库存可能被错误地扣减,甚至出现超卖现象!

引入同步锁的思路

为了解决这个问题,很多人会想到使用同步锁:

1
2
3
4
5
6
7
javasynchronized(goods) {
if (goods > 0) {
goods--;
} else {
// 秒杀失败
}
}

这样一来,超卖问题得到了一定的缓解。但随之而来的问题是,多个线程情况下,必须等待持有锁的线程完成操作,这会增加服务器的压力,导致性能瓶颈,吞吐量也大幅下降。

有办法解决吞吐量问题吗

有的,兄弟!有的。

我们直接使用 Nginx 来进行负载均衡。通过水平扩展服务器并使用 Nginx 实现分布式集群部署,没有什么性能问题是堆量解决不了的。

但新的问题出现了,秒杀功能再次出现超卖的请开给你。因为同步锁的作用仅限于 JVM 级别,它只能锁住单个线程,而在分布式环境中,每台服务器的锁并不能相互影响。

终极解决方案:分布式锁

在这个时候,分布式锁便应运而生。当前主流的分布式锁方案有 Redis 和 Zookeeper。在这里,我们介绍一下Redis分布式锁,因为它的使用频率更高,且操作相对简便,一般公司用Redis的未必用得上Zookeeper,用Zookeeper的99%有Redis。

使用 Redis 的 SETNX 命令,我们可以轻松实现分布式锁。当一个线程尝试在 Redis 中存储一个值时,如果 goods 键没有值,它将成功存储并返回 true,表示加锁成功;如果已存在值,则返回 false。

但是,请务必记住:使用 SETNX 时,一定要设置 过期时间。否则,如果持有锁的服务器崩溃,其他正常请求将被阻塞,最终引发死锁。而设置过期时间后,锁会自动释放,确保其他请求可以顺利进行。

新问题:锁的过期与业务处理超时

然而,这又引出了新的问题:如果业务处理时间超过锁的过期时间,其他线程便可以获得锁,结果是“线程一”完成工作后,释放的是“线程二”的锁,造成超卖。

那么,如何解决这个问题呢?

实际上,我们面临两个主要问题:

  1. 锁过期时,业务处理尚未完成。
  2. 线程处理完后释放的是其他线程的锁。

解决方案如下:

  1. 延长锁的过期时间,同时实现一个看门狗机制,定期检查线程是否存活,若存活则重置过期时间。
  2. 为锁分配一个唯一 ID(可以使用雪花算法或 UUID)。

当然,手动实现这些机制可能会相对繁琐。不如直接使用 Redis 的 Redisson 组件,它已经为我们封装了这些复杂的逻辑。

Redis 集群的注意事项

最后,值得一提的是,许多公司使用 Redis 集群(主从架构),在这种情况下,如果主节点宕机,可能会影响锁的有效性。因为 Redis 集群采用的是 AP 模式(高可用、高性能,但不能保证高一致性)。当您设置一个锁时,锁只会在主节点上设置。如果主节点成功设置锁,而从节点尚未同步,则会导致线程安全问题。在这种情况下,使用 Redlock 解决方案可以确保所有节点都成功存储锁后,才能返回响应,避免潜在的超卖问题。