如果所有服务同时争抢一个资源,系统会怎样?
想象这样一个场景:你的电商平台正在搞"秒杀"活动,库存只有100件商品,却有上万用户在同一毫秒点击"立即购买"。如果没有有效的协调机制,多个服务实例可能同时读取到"还有库存",然后各自扣减,最终导致超卖------卖出200件、300件,甚至更多。这不仅造成业务损失,还可能引发客户投诉和信任危机。
在分布式系统中,这种对共享资源的并发访问问题尤为棘手。单机锁(如 synchronized 或 ReentrantLock)只在单个 JVM 内有效,无法跨节点同步。这时候,Redis 分布式锁就成了解决这类问题的关键工具之一。
什么是 Redis 分布式锁?
简单来说,Redis 分布式锁 是一种利用 Redis 的原子操作特性,在多个分布式节点之间实现互斥访问共享资源的机制。它的核心思想是:谁先成功在 Redis 中"占位",谁就获得执行权,其他节点必须等待锁释放后才能尝试获取。
为什么选择 Redis?原因有三:
- 高性能:Redis 是内存数据库,读写速度极快,适合高并发场景。
- 原子操作支持 :如
SET key value NX PX命令,能一次性完成"设值+过期时间+仅当不存在时设置"的操作。 - 部署广泛:大多数微服务架构中已集成 Redis,无需额外引入新组件。
如何正确实现一个 Redis 分布式锁?
很多人第一反应是用 SETNX(Set if Not eXists)命令:
bash
SETNX lock_key my_value
如果返回 1,说明加锁成功;返回 0,则说明锁已被占用。看起来没问题?但这里有两个致命缺陷:
- 没有自动过期机制:如果持有锁的服务宕机,锁永远不会释放,导致死锁。
- 解锁不安全 :任意客户端都能执行
DEL lock_key删除锁,可能误删别人的锁。
正确姿势:带唯一标识 + 自动过期
Redis 官方推荐使用 SET 命令的扩展参数:
bash
SET lock_key unique_value NX PX 30000
NX:仅当 key 不存在时才设置(相当于 SETNX)PX 30000:设置 30 秒自动过期(防止死锁)unique_value:每个客户端使用唯一标识(如 UUID),用于后续安全解锁
安全解锁:必须校验 value
不能直接 DEL,而要用 Lua 脚本保证原子性:
lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
这段脚本确保:只有持有相同 value 的客户端才能删除锁,避免误删。
在 Java 中调用示例(使用 Jedis):
java
String lockKey = "order:lock";
String requestId = UUID.randomUUID().toString();
int expireTime = 30000; // 30秒
// 尝试加锁
Boolean locked = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if (locked != null && locked) {
try {
// 执行业务逻辑:如扣库存、创建订单
processOrder();
} finally {
// 安全释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
}
}
实践中的常见陷阱与解决方案
1. 锁过期时间设置不当
- 问题:业务执行时间超过锁的过期时间,锁提前释放,其他线程进入,导致并发。
- 对策 :
- 预估业务最大耗时,适当延长过期时间。
- 使用 锁续期(Watchdog 机制) :如 Redisson 的
RLock会在后台定期延长锁的有效期,只要业务还在运行,锁就不会失效。
2. 主从切换导致锁丢失
- 问题:Redis 主节点加锁后未同步到从节点,主挂了,从升主,新主没有锁信息,导致多个客户端同时获得锁。
- 对策 :
- 使用 Redlock 算法(由 Redis 作者提出):向多个独立 Redis 节点同时加锁,只有多数成功才算获取锁。
- 但 Redlock 实现复杂,且在极端网络分区下仍有争议。更推荐使用强一致性的 ZooKeeper 或 etcd 来实现高可靠锁。
大多数业务场景下,单 Redis 实例 + 合理过期时间 + 唯一 ID 解锁,已经足够。除非对一致性要求极高(如金融交易),否则不必过度设计。
3. 忙等待 vs 阻塞等待
- 简单实现中,客户端通常采用"轮询重试"方式获取锁,浪费 CPU 和网络。
- 更优雅的方式是使用 Redis 的 Pub/Sub 机制 或 Redisson 的 wait/notify 模型,让等待者在锁释放时被唤醒。
Redisson:生产级分布式锁的首选
对于 Java 开发者,Redisson 是一个成熟的 Redis 客户端,内置了多种分布式锁实现:
java
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认30秒自动续期
try {
// 业务逻辑
} finally {
lock.unlock();
}
它自动处理了:
- 唯一 ID 生成
- Lua 脚本解锁
- Watchdog 自动续期
- 可重入锁支持(同一线程可多次加锁)
大大降低了出错概率,建议在生产环境中优先考虑。
总结与思考
Redis 分布式锁不是银弹,但它是在性能与一致性之间取得良好平衡的实用方案。关键在于理解其原理,避开常见陷阱,并根据业务场景合理选择实现方式。
值得思考的是:是否真的需要分布式锁? 有时通过业务设计(如将库存分片、使用消息队列削峰)可以避免竞争,比加锁更高效、更可靠。
技术没有绝对的对错,只有是否适合。在追求高并发的同时,别忘了系统的可维护性和容错能力------毕竟,一个能稳定运行的系统,远比一个理论上完美的设计更有价值。