如何解决资源被并发访问

分布式锁实现(Redis RedLock、ZooKeeper、数据库乐观锁)及注意事项

为什么需要分布式锁?

单机系统中,我们可以用 synchronizedReentrantLock 等本地锁来控制并发访问共享资源。

但在分布式系统中,多个进程/服务实例部署在不同机器上,本地锁无法跨进程生效。这时就需要一个跨进程、跨机器的协调机制 —— 分布式锁

典型场景:

  • 秒杀系统中扣减库存(防止超卖)
  • 分布式任务调度(避免重复执行)
  • 支付回调幂等处理
  • 数据一致性更新(如用户积分、优惠券发放)

三种主流分布式锁实现详解


基于 Redis 的分布式锁(含 RedLock)

▶ 基础版:SETNX + EXPIRE

1
SET lock_key lock_value NX PX 30000
  • NX:仅当 key 不存在时才设置(实现“互斥”)
  • PX 30000:设置 30 秒自动过期(防止死锁)
  • lock_value:建议设为唯一标识(如 request_id 或 UUID),用于安全释放

释放锁(Lua 脚本保证原子性)

1
2
3
4
5
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

为什么用 Lua?因为“先判断再删除”不是原子操作,可能误删别人的锁!

▶ RedLock(Redis 官方推荐的高可用方案)

RedLock 是为了解决单点 Redis 故障导致锁失效的问题。

原理简述

  • 客户端向 N 个独立的 Redis 节点(通常 N ≥ 5)依次请求加锁
  • 只有在大多数节点(N/2 + 1) 上成功加锁,且总耗时 < 锁过期时间,才算加锁成功
  • 释放锁时,向所有节点发送释放请求

举例:5 个 Redis 节点,至少 3 个加锁成功 + 总耗时 < 30ms → 加锁成功

优点:

  • 性能极高(Redis 内存操作)
  • 实现简单、生态成熟(Redisson 等客户端已封装)

缺点与注意事项:

问题说明解决方案
时钟漂移问题RedLock 依赖时间判断,若节点间时钟不同步,可能导致多个客户端同时持有锁尽量保证服务器时钟同步(NTP)
网络分区/节点故障若多数节点宕机,锁可能失效或无法获取适用于允许“偶尔失败”的场景,不适合强一致性系统
锁续期问题业务执行时间 > 锁过期时间 → 锁自动释放 → 其他线程获得锁 → 数据错乱使用“看门狗”机制自动续期(如 Redisson 的 watchdog
锁误删没有校验 value,可能删掉别人的锁必须用 Lua 脚本校验 value 一致性

建议:生产环境推荐使用 Redisson 客户端,它内置了可重入锁、公平锁、联锁、红锁、看门狗续期等高级功能。


基于 ZooKeeper 的分布式锁

▶ 实现原理(临时顺序节点)

ZooKeeper 天然支持分布式协调,其锁机制基于:

  • 所有客户端在同一个父节点(如 /locks)下创建临时顺序节点(如 /locks/lock_0000000001
  • 客户端获取所有子节点,判断自己是否是序号最小的那个
    • 是 → 获得锁
    • 否 → 监听前一个节点(Watcher),等它删除后再尝试

举例:

  • A 创建 /lock_1,是最小 → 拿到锁
  • B 创建 /lock_2,监听 /lock_1
  • A 执行完删除 /lock_1 → B 收到通知 → 检查自己现在最小 → 拿到锁

优点:

  • 强一致性(ZAB 协议保证)
  • 支持公平锁(按创建顺序获取)
  • 临时节点自动释放(客户端断开连接自动删除,避免死锁)
  • 支持 Watcher 机制,无忙等,性能较好

缺点与注意事项:

问题说明解决方案
性能较低相比 Redis,ZK 写操作较慢(需过半节点 ACK)适合对一致性要求高、并发量不极端的场景
运维复杂需维护 ZooKeeper 集群,配置较重适合已有 ZK 基础设施的团队
羊群效应(Herd Effect)大量客户端监听同一节点,一旦释放,所有客户端同时被唤醒竞争使用“只监听前一个节点”策略避免

扩展:Curator 客户端提供了 InterProcessMutex,封装了可重入分布式锁,推荐使用。


基于数据库的乐观锁(非“锁”,而是“无锁并发控制”)

严格来说,这不是“锁”,而是一种并发冲突检测机制,常用于更新场景。

▶ 实现方式:版本号(version)或时间戳

1
2
3
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 5;
  • 如果返回影响行数 = 1 → 更新成功
  • 如果影响行数 = 0 → 说明 version 已被别人改过 → 重试或报错

优点:

  • 无需额外中间件,直接利用数据库
  • 适合读多写少、冲突不频繁的场景
  • 实现简单,易于理解

缺点与注意事项:

问题说明解决方案
ABA 问题version 从 5 → 6 → 5(中间被改回),可能误判使用时间戳或递增序列号,避免回退
高并发下性能差大量更新失败+重试,数据库压力大限制重试次数、结合本地缓存/队列削峰
不适用“互斥执行”场景不能阻止多个线程“同时进入临界区”,只能最后提交时检测冲突仅适用于“最终一致性可接受”的业务

适用场景举例:商品库存扣减(配合重试)、用户资料更新、配置项修改等。


三种方案对比总结

方案一致性性能实现复杂度适用场景
Redis(RedLock)弱一致性(AP)极高中等高并发、允许少量失败(如秒杀)
ZooKeeper强一致性(CP)中等较高金融、强一致、公平调度场景
数据库乐观锁最终一致低~中简单低频更新、冲突少、已有 DB 的轻量场景

选型建议:

  • 追求性能 + 接受偶尔失败 → Redis
  • 追求强一致 + 公平性 → ZooKeeper
  • 不想引入中间件 + 更新不频繁 → 数据库乐观锁

通用注意事项(无论哪种锁)

  1. 设置合理的过期时间

    • 防止程序崩溃导致死锁
    • 但也不能太短,避免业务未执行完就释放
  2. 锁的粒度要细

    • 不要锁整个表或整个服务,按业务 ID 加锁(如 lock:order:12345
  3. 加锁与业务操作必须原子/事务保护

    • 避免“加锁成功但业务未执行”的中间状态
  4. 异常处理与锁释放

    • 必须在 finally 块或使用 try-with-resource 释放锁
    • 避免因异常导致锁未释放
  5. 避免死锁 & 饥饿

    • 设置超时获取锁机制(如 tryLock(timeout))
    • 公平锁避免某些线程永远拿不到锁
  6. 监控与告警

    • 记录锁等待时间、获取失败次数
    • 设置阈值告警,及时发现性能瓶颈或死锁风险

总结:

分布式锁的本质,是在分布式环境下模拟“互斥访问共享资源”的能力;选型需权衡一致性、性能、复杂度;实现时务必考虑异常、超时、释放、监控等生产级细节。

版权声明:本文为原创,依据 CC BY-NC-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。