管理设计篇之"分布式锁" [2026重制版]
核心变更说明:本文基于原本文档第52篇重写,全面更新至2026年技术栈。新增 Redis 7.x/Redisson 3.x、ZooKeeper 3.8、etcd v3.5 对比分析,深入剖析 Redlock 算法争议与替代方案,补充性能基准测试数据(来源:Redis.io 官方 Benchmark),提供完整的生产级代码示例。
一、问题背景:为什么需要分布式锁
在单机多线程环境下,我们使用 synchronized、ReentrantLock 或数据库行锁来保证共享资源的互斥访问。然而,当系统演变为分布式架构后,服务部署在多台机器上,甚至跨机房、跨可用区部署,传统的进程内锁机制完全失效。
1.1 典型业务场景
| 场景 | 描述 | 并发风险 |
|---|---|---|
| 库存扣减 | 电商秒杀、商品库存扣减 | 超卖问题 |
| 金额操作 | 银行转账、账户余额更新 | 数据不一致 |
| 任务调度 | 定时任务防止重复执行 | 任务重复执行 |
| 幂等控制 | API 接口防重复提交 | 重复下单 |
| 配置变更 | 配置中心热更新 | 配置冲突 |
1.2 分布式锁的核心要求
根据 Redis 官方文档 对分布式锁的定义,一个合格的分布式锁必须满足以下三个核心特性:
- 互斥性(Mutual Exclusion):在任意时刻,只有一个客户端能持有锁
- 避免死锁(Deadlock Free):即使持有锁的客户端崩溃或网络分区,锁最终仍能被其他客户端获取
- 容错性(Fault Tolerance):只要大部分锁服务节点正常运行,客户端就能进行加锁和解锁操作
此外,生产环境还需要考虑:
- 可重入性:同一线程可多次获取同一把锁
- 阻塞/非阻塞:支持获取锁失败时的策略选择
- 公平性:按请求顺序获取锁(可选)
二、技术原理与架构演进
2.1 分布式锁的实现方式全景图
2.2 主流方案对比
| 特性 | Redis (Redisson) | ZooKeeper (Curator) | etcd (JetCD) | 数据库 |
|---|---|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ 极高 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 高 | ⭐⭐ 低 |
| 可靠性 | ⭐⭐⭐⭐ 高(需集群) | ⭐⭐⭐⭐⭐ 极高(ZAB协议) | ⭐⭐⭐⭐⭐ 极高(Raft) | ⭐⭐⭐ 中等 |
| 一致性 | 最终一致 | 强一致 | 强一致 | 强一致 |
| 实现复杂度 | 低(SDK成熟) | 中等 | 低 | 高 |
| 可重入支持 | ✅ 原生支持 | ✅ 支持 | ✅ 支持 | ❌ 需自行实现 |
| Watch/通知 | Pub/Sub | 原生 Watch | Watch | 轮询 |
| 适用场景 | 高并发、性能敏感 | 强一致、协调服务 | K8s原生、云原生 | 已有DB依赖 |
三、Redis 分布式锁深度剖析
3.1 基础实现:SET NX PX
根据 Redis 官方文档,正确的加锁命令如下:
# 加锁:仅在 key 不存在时设置,并设置过期时间
SET resource_name my_random_value NX PX 30000参数说明:
NX:只在 key 不存在时才设置(Not eXist)PX:设置过期时间,单位毫秒(此处为30秒)my_random_value:全局唯一的标识符,用于安全释放锁
解锁脚本(Lua):
-- 比对 value,确保只有锁的持有者才能释放
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end3.2 为什么需要随机值?
这是为了解决误释放问题。考虑以下场景:
解决方案:每个客户端生成唯一标识(如 UUID + 线程ID),释放锁时先比对 value。
3.3 Redlock 算法:争议与现状
Redis 作者 Antirez 在 Redlock 文章 中提出了 Redlock 算法,试图在 Redis 单节点故障时保证安全性。
Redlock 算法流程
- 获取当前时间戳 T1
- 按相同顺序依次向 N 个 Redis 实例请求加锁
- 计算耗时,只有在大多数节点(N/2 + 1)获取成功且总耗时小于锁的有效时间时,才算获取成功
- 如果获取失败,向所有实例发送解锁指令
Redlock 的争议
Martin Kleppmann(剑桥大学分布式系统研究员)在 文章 中指出 Redlock 的根本缺陷:
问题核心:Redis 使用异步复制,在主节点宕机时可能发生锁状态丢失。即使使用 Redlock,也无法在 CAP 定理中同时满足一致性和可用性。
Martin 的论证(GC 暂停场景):
Antirez 的回应:承认 Redlock 不适合需要强一致性的场景(如金融交易),但在多数业务场景下是足够的。
3.4 生产级推荐:Redisson
Redisson 是 Java 生态中最成熟的 Redis 客户端,内置了完善的分布式锁实现。
Maven 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.27.0</version>
</dependency>配置示例
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
// 单节点模式
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("your_password")
.setDatabase(0)
.setConnectionPoolSize(64)
.setConnectionMinimumIdleSize(24)
.setIdleConnectionTimeout(10000)
.setConnectTimeout(5000)
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
return Redisson.create(config);
}
// 集群模式(推荐生产使用)
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClusterClient() {
Config config = new Config();
config.useClusterServers()
// 集群节点列表
.addNodeAddress("redis://node1:6379",
"redis://node2:6379",
"redis://node3:6379")
.setPassword("your_password")
.setScanInterval(5000); // 集群状态扫描间隔
return Redisson.create(config);
}
}可重入锁使用示例
@Service
public class InventoryService {
@Autowired
private RedissonClient redissonClient;
/**
* 扣减库存 - 使用可重入锁
* @param 商品ID
* @param 扣减数量
* @return 是否成功
*/
public boolean deductStock(String productId, int quantity) {
// 定义锁的名称(建议包含业务标识)
String lockKey = "lock:inventory:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待 10 秒,锁定后 30 秒自动释放
// watchdog 机制会自动续期
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!acquired) {
log.warn("获取库存锁失败,商品ID: {}", productId);
return false;
}
// ====== 业务逻辑开始 ======
// 1. 查询库存
Integer currentStock = getStockFromDB(productId);
if (currentStock < quantity) {
log.info("库存不足,商品ID: {},当前库存: {}", productId, currentStock);
return false;
}
// 2. 扣减库存
boolean success = updateStockInDB(productId, currentStock - quantity);
if (success) {
log.info("扣减库存成功,商品ID: {},数量: {}", productId, quantity);
}
// ====== 业务逻辑结束 ======
return success;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取锁被中断", e);
return false;
} finally {
// 仅当锁是当前线程持有的才释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Redisson 锁的 Watchdog 机制
Redisson 的看门狗机制是其核心亮点:
关键点:
- 只有在不指定
leaseTime时才会启用 watchdog - watchdog 默认每 10 秒 续期一次,续期为 30 秒
- 业务执行完毕或显式调用
unlock()后停止续期 - 适用于执行时间不确定的业务场景
四、ZooKeeper 分布式锁
4.1 原理:临时顺序节点
ZooKeeper 通过 ZAB(Zookeeper Atomic Broadcast)协议保证强一致性,其分布式锁实现基于临时顺序节点。
4.2 Curator 实现示例
Apache Curator 是 ZooKeeper 的客户端框架,提供了高级别的锁API。
Maven 依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>代码示例
@Service
public class ZkDistributedLockService {
private final InterProcessMutex lock;
public ZkDistributedLockService(CuratorFramework client) {
// 创建互斥锁,路径为 /locks/my-lock
this.lock = new InterProcessMutex(client, "/locks/my-lock");
}
public void doWithLock() {
try {
// 尝试获取锁,最多等待 10 秒
boolean acquired = lock.acquire(10, TimeUnit.SECONDS);
if (acquired) {
try {
// 执行业务逻辑
executeBusinessLogic();
} finally {
lock.release();
}
} else {
log.warn("获取 ZooKeeper 锁超时");
}
} catch (Exception e) {
log.error("ZooKeeper 锁异常", e);
}
}
}4.3 ZooKeeper vs Redis 锁对比
| 维度 | ZooKeeper | Redis |
|---|---|---|
| 一致性模型 | CP(强一致) | AP(最终一致) |
| 性能(QPS) | ~10,000(受限于磁盘IO) | ~100,000+(纯内存) |
| 锁释放 | 会话断开自动释放 | 依赖 TTL / Watchdog |
| 实现复杂度 | 较高(需维护会话) | 较低(SDK成熟) |
| 适用场景 | 强一致需求(金融、支付) | 高并发、性能敏感 |
五、etcd 分布式锁
5.1 简介
etcd 是 Cloud Native Computing Foundation (CNCF) 的毕业项目,采用 Raft 共识算法,是 Kubernetes 的数据存储后端。etcd v3.5+ 提供了成熟的并发原语。
5.2 核心原理:Lease + Transaction
// 使用 JetCD 客户端(推荐)
// Maven: io.etcd:jetcd-core:0.7.6
@Service
public class EtcdDistributedLockService {
private final Client etcdClient;
private final Lease leaseClient;
private final Lock lockClient;
public EtcdDistributedLockService() {
this.etcdClient = Client.builder()
.endpoints("http://etcd1:2379", "http://etcd2:2379")
.build();
this.leaseClient = etcdClient.getLeaseClient();
this.lockClient = etcdClient.getLockClient();
}
public void doWithLock(String lockName) {
try {
// 获取一个 30 秒的租约
LeaseGrantResponse leaseResp = leaseClient.grant(30).get();
long leaseId = leaseResp.getID();
// 使用租约创建锁
LockResponse lockResp = lockClient.lock(
ByteSequence.from(lockName, StandardCharsets.UTF_8),
leaseId
).get();
try {
// 业务逻辑
businessLogic();
} finally {
// 解锁
lockClient.unlock(lockResp.getKey()).get();
}
} catch (Exception e) {
log.error("etcd 锁操作异常", e);
}
}
}5.3 etcd 的优势
- Raft 协议:强一致性保证,无单点故障
- Watch 机制:高效的变更通知
- 与 Kubernetes 原生集成:云原生应用首选
- gRPC 接口:高性能、跨语言支持
六、性能基准测试
以下数据来源于各项目官方 Benchmark 和社区测试报告:
6.1 吞吐量对比(单机 QPS)
| 方案 | 加锁 QPS | 解锁 QPS | 平均延迟 | P99 延迟 | 测试环境 |
|---|---|---|---|---|---|
| Redis (单节点) | ~120,000 | ~120,000 | 0.5ms | 2ms | Redis 7.2, 8核32G |
| Redis (Cluster) | ~400,000 | ~400,000 | 0.8ms | 5ms | 3主3从, 同上 |
| Redisson RLock | ~95,000 | ~95,000 | 1.0ms | 8ms | 含 watchdog 开销 |
| ZooKeeper | ~12,000 | ~12,000 | 5ms | 25ms | ZK 3.8, 8核16G |
| etcd v3.5 | ~18,000 | ~18,000 | 3ms | 15ms | 3节点集群 |
| MySQL (FOR UPDATE) | ~3,000 | ~3,000 | 15ms | 80ms | MySQL 8.0, SSD |
数据来源:Redis Benchmark、ZooKeeper Wiki、etcd Benchmark
6.2 不同并发下的延迟表现
图例:蓝色=Redis,橙色=etcd,绿色=ZooKeeper
七、实战案例:电商库存扣减
7.1 架构设计
7.2 完整代码实现
/**
* 库存扣减服务 - 生产级实现
*
* 设计要点:
* 1. 使用 Redisson 可重入锁
* 2. 双层校验(缓存 + 数据库 CAS)
* 3. 异步扣减,提升吞吐
* 4. 完善的日志与监控
*/
@Service
@Slf4j
public class StockDeductionServiceImpl implements StockDeductionService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private ProductMapper productMapper;
@Autowired
private ApplicationEventPublisher eventPublisher;
/** 锁等待时间(秒) */
private static final long LOCK_WAIT_TIME = 5;
/** 锁持有时间(秒) */
private static final long LOCK_LEASE_TIME = 10;
/** 库存缓存 Key 前缀 */
private static final String STOCK_CACHE_PREFIX = "stock:";
@Override
public StockDeductResult deduct(String productId, int quantity, String orderId) {
long startTime = System.currentTimeMillis();
// 1. 快速路径:检查缓存中的库存是否充足
String cacheKey = STOCK_CACHE_PREFIX + productId;
String stockStr = stringRedisTemplate.opsForValue().get(cacheKey);
if (stockStr != null) {
int cachedStock = Integer.parseInt(stockStr);
if (cachedStock < quantity) {
return StockDeductResult.fail("库存不足", cachedStock);
}
}
// 2. 获取分布式锁
String lockKey = "lock:stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean locked = lock.tryLock(
LOCK_WAIT_TIME,
LOCK_LEASE_TIME,
TimeUnit.SECONDS
);
if (!locked) {
log.warn("获取库存锁失败,productId: {}, orderId: {}", productId, orderId);
return StockDeductResult.fail("系统繁忙,请稍后重试", -1);
}
// 3. 二次校验库存(双重检查)
stockStr = stringRedisTemplate.opsForValue().get(cacheKey);
int currentStock = (stockStr != null) ? Integer.parseInt(stockStr)
: getStockFromDB(productId);
if (currentStock < quantity) {
// 刷新缓存
refreshStockCache(productId, currentStock);
return StockDeductResult.fail("库存不足", currentStock);
}
// 4. 数据库 CAS 更新(乐观锁)
int affected = productMapper.decreaseStockWithCAS(productId, quantity, currentStock);
if (affected > 0) {
// 5. 更新缓存
int newStock = currentStock - quantity;
stringRedisTemplate.opsForValue().set(cacheKey, String.valueOf(newStock), 1, TimeUnit.HOURS);
// 6. 发布事件(异步处理后续逻辑)
eventPublisher.publishEvent(new StockDeductedEvent(productId, quantity, orderId));
long cost = System.currentTimeMillis() - startTime;
log.info("库存扣减成功,productId: {}, orderId: {}, 耗时: {}ms", productId, orderId, cost);
return StockDeductResult.success(newStock);
} else {
// CAS 失败,说明有并发修改
log.warn("库存扣减CAS失败,productId: {}", productId);
return StockDeductResult.fail("库存变化,请重试", -1);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return StockDeductResult.fail("操作中断", -1);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 从数据库查询库存(带兜底)
*/
private int getStockFromDB(String productId) {
Product product = productMapper.selectById(productId);
if (product == null) {
throw new BusinessException("商品不存在");
}
return product.getStock();
}
/**
* 刷新库存缓存
*/
private void refreshStockCache(String productId, int stock) {
stringRedisTemplate.opsForValue().set(
STOCK_CACHE_PREFIX + productId,
String.valueOf(stock),
1, TimeUnit.HOURS
);
}
}7.3 Mapper 层(CAS 实现)
@Mapper
public interface ProductMapper {
/**
* 使用乐观锁扣减库存
* UPDATE ... SET stock = stock - #{quantity} WHERE id = #{id} AND stock = #{expectedStock}
*/
@Update("UPDATE product SET stock = stock - #{quantity}, " +
"update_time = NOW() " +
"WHERE id = #{productId} AND stock >= #{quantity} AND stock = #{expectedStock}")
int decreaseStockWithCAS(@Param("productId") String productId,
@Param("quantity") int quantity,
@Param("expectedStock") int expectedStock);
}八、2026 最佳实践总结
8.1 选型决策树
8.2 生产环境 Checklist
- 锁粒度:锁的 Key 要足够细(如
lock:order:{userId}:{orderId}),避免锁范围过大 - 超时设置:根据业务执行时间合理设置 leaseTime,建议设置为正常耗时的 3~5 倍
- 锁的 value:必须使用全局唯一标识(UUID + ThreadId),防止误释放
- finally 解锁:必须在 finally 块中释放锁,并判断是否为当前线程持有
- 可重入:同一线程内嵌套调用时使用可重入锁
- 监控告警:监控锁等待时间、获取失败率,设置合理的阈值告警
- 降级策略:获取锁失败时的降级处理(返回友好提示、进入排队等)
- 集群部署:Redis 使用 Cluster 或 Sentinel 模式,避免单点故障
- 避免嵌套锁:多个资源的加锁要按固定顺序,防止死锁
- 日志记录:记录锁的获取/释放/等待耗时,便于排查问题
8.3 常见陷阱与规避
| 陷阱 | 说明 | 规避方法 |
|---|---|---|
| 锁未释放 | 忘记在 finally 中 unlock | 使用 try-with-resources 或标准模板 |
| 锁时间太短 | 业务未完成锁就过期了 | 使用 watchdog 或合理估算时间 |
| 锁时间太长 | 故障时长时间不释放 | 设置合理的超时时间 |
| Redis 主从切换丢锁 | 异步复制导致锁丢失 | 使用 Redlock 或接受风险 |
| GC 导致的时钟跳跃 | STW 导致锁提前过期 | 使用 ZooKeeper/etcd 或增大缓冲 |
| 网络分区 | 脑裂情况下的安全问题 | 采用多数派决策(Paxos/Raft) |
九、延伸资源
官方文档
- Redis 分布式锁: https://redis.io/docs/reference/patterns/distributed-locks/
- Redisson 文档: https://redisson.wiki/
- ZooKeeper 官方文档: https://zookeeper.apache.org/doc/current/zookeeperProgrammers.html
- etcd 官方文档: https://etcd.io/docs/latest/dev-guide/api_concurrency_reference_v3/
经典论文与讨论
- Martin Kleppmann: "How to do distributed locking" - https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
- Antirez (Redis作者): "Is Redlock safe?" - https://redis.io/topics/distlock-argument-maturity
- Google Chubby Lock Service 论文 - https://research.google/pubs/pub27897/
开源项目
- Redisson: Java Redis 客户端,内置分布式锁
- Apache Curator: ZooKeeper 客户端框架
- JetCD: etcd Java 客户端
- DistributedLock: 微软开源的 .NET 分布式锁库
本文版本:2026 重制版 | 基于本文档第52篇原文重构 最后更新:2026-06-06 | 技术栈:Redis 7.2 / Redisson 3.27 / ZooKeeper 3.8 / etcd 3.5