Redis 分布式锁

如果所有服务同时争抢一个资源,系统会怎样?

想象这样一个场景:你的电商平台正在搞"秒杀"活动,库存只有100件商品,却有上万用户在同一毫秒点击"立即购买"。如果没有有效的协调机制,多个服务实例可能同时读取到"还有库存",然后各自扣减,最终导致超卖------卖出200件、300件,甚至更多。这不仅造成业务损失,还可能引发客户投诉和信任危机。

在分布式系统中,这种对共享资源的并发访问问题尤为棘手。单机锁(如 synchronizedReentrantLock)只在单个 JVM 内有效,无法跨节点同步。这时候,Redis 分布式锁就成了解决这类问题的关键工具之一。


什么是 Redis 分布式锁?

简单来说,Redis 分布式锁 是一种利用 Redis 的原子操作特性,在多个分布式节点之间实现互斥访问共享资源的机制。它的核心思想是:谁先成功在 Redis 中"占位",谁就获得执行权,其他节点必须等待锁释放后才能尝试获取。

为什么选择 Redis?原因有三:

  1. 高性能:Redis 是内存数据库,读写速度极快,适合高并发场景。
  2. 原子操作支持 :如 SET key value NX PX 命令,能一次性完成"设值+过期时间+仅当不存在时设置"的操作。
  3. 部署广泛:大多数微服务架构中已集成 Redis,无需额外引入新组件。

如何正确实现一个 Redis 分布式锁?

很多人第一反应是用 SETNX(Set if Not eXists)命令:

bash 复制代码
SETNX lock_key my_value

如果返回 1,说明加锁成功;返回 0,则说明锁已被占用。看起来没问题?但这里有两个致命缺陷:

  1. 没有自动过期机制:如果持有锁的服务宕机,锁永远不会释放,导致死锁。
  2. 解锁不安全 :任意客户端都能执行 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 分布式锁不是银弹,但它是在性能与一致性之间取得良好平衡的实用方案。关键在于理解其原理,避开常见陷阱,并根据业务场景合理选择实现方式。

值得思考的是:是否真的需要分布式锁? 有时通过业务设计(如将库存分片、使用消息队列削峰)可以避免竞争,比加锁更高效、更可靠。

技术没有绝对的对错,只有是否适合。在追求高并发的同时,别忘了系统的可维护性和容错能力------毕竟,一个能稳定运行的系统,远比一个理论上完美的设计更有价值。

相关推荐
Lyra_Infra24 分钟前
Docker OCI Runtime 启动失败问题排查与解决
后端·docker·架构
XuCoder26 分钟前
Redis 哨兵模式:它到底是怎么保证高可用的
后端
敲个大西瓜1 小时前
Springboot核心面试题
java·spring boot·后端
运维行者_2 小时前
网络监控与ITSM集成:从告警到工单,实现运维自动化闭环
开发语言·网络·分布式·后端·架构·flask·php
子兮曰2 小时前
解剖 Claude Code:从入口架构逆向工程看 AI 编程工具的信任边界设计
前端·后端·claude
颜进强3 小时前
前端看后端 15:什么是 DNS?
前端·后端·ai编程
文艺理科生3 小时前
LangChain.js-v1-记忆管理最佳实践
前端·javascript·后端
子兮曰3 小时前
Elysia vs Hono 完整性能对比:Bun 专属优化还是跨平台通用,2026 实测数据给出的答案
前端·后端·typescript
小满zs3 小时前
Go语言第七章(Map)
后端·go
Undoom3 小时前
我用豆包Seed-Evolving搞了个高中物理可视化工具,结果被自己的高中物理水平整破防了
后端