一、为什么需要分布式锁?——从单机到分布式的必然选择
1.1 单机锁的局限性
在传统单体架构中,我们习惯使用 synchronized、ReentrantLock 等同步机制来控制并发访问。但这些锁机制存在致命缺陷:
- 仅限于单 JVM:只能锁住同一个 Java 虚拟机内的线程
- 无法跨进程:当服务部署在多台服务器上时,每台机器都有独立的锁实例
- 无法跨网络:不同服务实例之间无法感知彼此的锁状态
1.2 真实业务场景的痛点
电商秒杀场景:库存只有 10 件商品,但成千上万的用户同时下单。在分布式部署架构下:
- 服务 A 实例扣减库存到 5 件
- 服务 B 实例同时读取库存还是 10 件,也扣减到 5 件
- 最终库存变成 0,但实际只卖出了 10 件,却扣减了 20 件库存
支付对账场景:多个对账任务同时执行,都需要更新对账状态表,如果没有分布式锁,可能导致状态错乱。
1.3 分布式锁的核心价值
- 跨进程互斥:保证同一时刻只有一个客户端能执行关键代码
- 数据一致性:避免多个服务实例同时修改共享资源导致的数据不一致
- 业务可靠性:确保关键业务逻辑的原子性和完整性
二、分布式锁的六大必备条件——生产环境的底线要求
一个真正可用的分布式锁,必须满足以下核心条件:
| 条件 | 说明 | 重要性 |
|---|---|---|
| 互斥性 | 任意时刻,只有一个客户端能持有锁 | ⭐⭐⭐⭐⭐ |
| 防死锁 | 锁持有者崩溃时,锁能自动释放(如设置超时时间) | ⭐⭐⭐⭐⭐ |
| 容错性 | 锁服务本身高可用,部分节点故障不影响整体功能 | ⭐⭐⭐⭐ |
| 可重入性 | 同一线程可重复获取已持有的锁,避免自己阻塞自己 | ⭐⭐⭐ |
| 高性能 | 加锁、解锁操作要快,延迟低,避免成为系统瓶颈 | ⭐⭐⭐⭐ |
| 公平性 | 按照请求顺序获取锁,避免饥饿现象(可选但重要) | ⭐⭐⭐ |
重点强调:在生产环境中,互斥性和防死锁是绝对不能妥协的底线要求!
三、分布式锁的常见实现方案——技术选型指南
3.1 主流方案对比
实现方式核心原理优点缺点适用场景数据库乐观锁版本号或唯一键约束简单,无需额外组件性能差,易死锁,不适合高并发低并发、快速验证场景 Redis 方案 SETNX + 过期时间 + Lua 脚本高性能,实现简单,生态成熟依赖时钟,主从切换可能丢锁高并发、允许极少量不一致的场景 ZooKeeper 方案临时顺序节点 + Watch 机制强一致性,无死锁风险,天然公平性能相对较低,运维成本高金融级、强一致性要求场景 Etcd 方案 Raft 共识 + Lease + Revision 强一致,云原生友好生态相对小众 Kubernetes
| 实现方式 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 数据库乐观锁 | 版本号或唯一键约束 | 简单,无需额外组件 | 性能差,易死锁,不适合高并发 | 低并发、快速验证场景 |
| Redis方案 | SETNX + 过期时间 + Lua脚本 | 高性能,实现简单,生态成熟 | 依赖时钟,主从切换可能丢锁 | 高并发、允许极少量不一致的场景 |
| ZooKeeper方案 | 临时顺序节点 + Watch机制 | 强一致性,无死锁风险,天然公平 | 性能相对较低,运维成本高 | 金融级、强一致性要求场景 |
| Etcd方案 | Raft共识 + Lease + Revision | 强一致,云原生友好 | 生态相对小众 | Kubernetes环境、云原生架构 |
环境、云原生架构
3.2 方案选型建议
- 首选 Redis:90% 的业务场景,特别是高并发、低延迟要求的场景
- 金融级场景选 ZooKeeper/Etcd:对数据一致性要求极高的场景,如资金转账、库存扣减
- 避免自研:除非有特殊需求,否则优先使用成熟框架(如 Redisson、Curator)
四、Redis 分布式锁实战——高性能方案详解
4.1 核心原理深度剖析
基础命令:
SET lock_key unique_value NX PX 30000
NX:Only set the key if it does not already exist(保证互斥)PX:Set the expiration time in milliseconds(防死锁)unique_value:UUID 等唯一标识(安全释放锁)
释放锁的原子性问题:
-- Lua 脚本保证原子性
if Redis.call(「get」, KEYS[1]) == ARGV[1] then
return Redis.call(「del」, KEYS[1])
else
return 0
end
为什么要用 Lua 脚本?
因为”判断 value 是否匹配”和”删除 key”是两个操作,如果不原子执行,可能出现:
- 客户端 A 判断 value 匹配
- 锁恰好过期
- 客户端 B 获取到新锁
- 客户端 A 删除了客户端 B 的锁
4.2 Spring Boot 完整实现(生产级)
4.2.1 基础依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-Redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>
4.2.2 Redisson 配置
@Configuration
public class RedissonConfig {
@Value(「${spring.Redis.host}」)
private String redisHost;
@Value(「${spring.Redis.port}」)
private int redisPort;
@Bean(destroyMethod = 「shutdown」)
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress(「Redis://」 + redisHost + 「:」 + redisPort)
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5)
.setRetryAttempts(3)
.setRetryInterval(1000);
return Redisson.create(config);
}
}
4.2.3 业务代码示例
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private InventoryService inventoryService;
/**
* 创建订单(带分布式锁)
* /
public Order createOrder(String userId, String productId, int quantity) {
// 锁 key:订单创建锁 + 产品 ID
String lockKey = 「order:create:」 + productId;
// 获取锁(看门狗自动续期)
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待 10 秒,锁自动释放时间 30 秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException(「系统繁忙,请稍后重试」);
}
// 检查库存
if (!inventoryService.checkStock(productId, quantity)) {
throw new BusinessException(「库存不足」);
}
// 扣减库存并创建订单
inventoryService.deductStock(productId, quantity);
return orderRepository.save(new Order(userId, productId, quantity));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException(「获取锁被中断」);
} finally {
// 释放锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
4.3 Redisson 实现原理揭秘
看门狗(Watchdog)机制:
- 当锁未指定 leaseTimeout 时,默认 30 秒过期
- 后台启动定时任务,每 10 秒检查一次
- 如果客户端仍持有锁,自动重置过期时间为 30 秒
- 业务完成后手动释放锁,取消看门狗
优势:
- 无需手动设置过期时间
- 避免业务执行时间过长导致锁提前释放
- 自动续期,保证业务完整性
五、ZooKeeper 分布式锁实战——强一致性方案
5.1 核心原理深度解析
ZooKeeper 节点类型:
- 持久节点:客户端断开后依然存在
- 临时节点:客户端会话结束自动删除(关键!)
- 顺序节点:父节点下自动生成递增序号
加锁流程(公平锁) :
- 所有客户端在
/locks/order下创建临时顺序节点 - 获取所有子节点,判断自己创建的节点序号是否最小
- 如果是最小,获取锁成功
- 如果不是最小,监听前一个序号节点的删除事件
- 前一个节点删除后,重新判断
释放锁:删除临时节点或会话断开自动删除
优势:
- 强一致性:ZAB 协议保证数据一致性
- 无死锁:临时节点自动释放
- 天然公平:按创建顺序获取锁
5.2 Spring Boot 完整实现(Curator 框架)
5.2.1 依赖配置
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.5.0</version>
</dependency>
5.2.2 Curator 客户端配置
@Configuration
public class ZookeeperConfig {
@Value(「${zookeeper.address}」)
private String zkAddress;
@Value(「${zookeeper.sessionTimeout}」)
private int sessionTimeout = 30000;
@Value(「${zookeeper.connectionTimeout}」)
private int connectionTimeout = 15000;
@Bean(initMethod = 「start」, destroyMethod = 「close」)
public CuratorFramework curatorFramework() {
// 重试策略:指数退避,初始 1 秒,最多 3 次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
return CuratorFrameworkFactory.builder()
.connectString(zkAddress)
.sessionTimeoutMs(sessionTimeout)
.connectionTimeoutMs(connectionTimeout)
.retryPolicy(retryPolicy)
.build();
}
}
5.2.3 业务代码示例
@Service
public class PaymentService {
@Autowired
private CuratorFramework curatorFramework;
@Autowired
private AccountService accountService;
/**
* 转账操作(强一致性要求)
* /
public boolean transfer(String fromAccount, String toAccount, BigDecimal amount) {
// 锁路径:/locks/transfer/{fromAccount}
String lockPath = 「/locks/transfer/」 + fromAccount;
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
try {
// 尝试获取锁,最多等待 15 秒
if (!lock.acquire(15, TimeUnit.SECONDS)) {
throw new BusinessException(「获取锁超时,请稍后重试」);
}
// 检查余额
BigDecimal balance = accountService.getBalance(fromAccount);
if (balance.compareTo(amount) < 0) {
throw new BusinessException(「余额不足」);
}
// 执行转账(强一致性要求)
accountService.deduct(fromAccount, amount);
accountService.add(toAccount, amount);
return true;
} catch (Exception e) {
log.error(「转账失败」, e);
throw new BusinessException(「转账失败:」 + e.getMessage());
} finally {
try {
// 释放锁
if (lock.isAcquiredInThisProcess()) {
lock.release();
}
} catch (Exception e) {
log.error(「释放锁失败」, e);
}
}
}
}
5.3 ZooKeeper vs Redis 深度对比
维度 ZooKeeperRedis 一致性强一致性(ZAB 协议)最终一致性(主从异步)性能较低(涉及磁盘写入)极高(内存操作)可靠性无单点风险,自动故障转移依赖哨兵/集群,主从切换可能丢锁实现复杂度较高(需要维护 ZK 集群)较低(Redis
| 维度 | ZooKeeper | Redis |
|---|---|---|
| 一致性 | 强一致性(ZAB协议) | 最终一致性(主从异步) |
| 性能 | 较低(涉及磁盘写入) | 极高(内存操作) |
| 可靠性 | 无单点风险,自动故障转移 | 依赖哨兵/集群,主从切换可能丢锁 |
| 实现复杂度 | 较高(需要维护ZK集群) | 较低(Redis部署简单) |
| 适用场景 | 金融级、强一致性要求 | 高并发、允许极少量不一致 |
部署简单)适用场景金融级、强一致性要求高并发、允许极少量不一致
六、生产环境最佳实践——避坑指南
6.1 Redis 方案注意事项
- 避免锁过期问题:业务执行时间可能超过锁过期时间
-
使用 Redisson 看门狗自动续期
-
业务拆分,避免长事务
- 主从切换风险:
-
单机 Redis + 哨兵架构足够应对大多数场景
-
极端重要场景考虑 Redlock 算法(争议较大)
- 性能优化:
-
使用连接池
-
合理设置超时时间
-
避免在锁内执行 IO 操作
6.2 ZooKeeper 方案注意事项
- 会话超时设置:
-
合理配置 sessionTimeout,避免网络抖动导致频繁释放锁
-
一般设置为 30-60 秒
- 连接管理:
-
使用连接池
-
处理连接断开重连
-
监控 ZK 集群状态
- 节点路径设计:
-
避免创建过多节点
-
合理设计节点层级
-
定期清理无用节点
6.3 通用最佳实践
- 锁粒度:尽量细粒度锁,避免大范围锁竞争
- 超时机制:必须设置获取锁的超时时间,避免无限等待
- 异常处理:完善的异常处理和日志记录
- 监控告警:监控锁的获取时间、持有时间、失败率
- 降级策略:当锁服务不可用时,有降级方案
七、总结与选型决策树
7.1 技术选型决策树
7.2 核心结论
- 90% 场景选 Redis:性能高、实现简单、生态成熟
- 10% 关键场景选 ZooKeeper:强一致性、无死锁风险、金融级要求
- 永远不要自研:除非有特殊需求,否则优先使用成熟框架
- 锁是最后手段:优先考虑无锁化设计,如分库分表、消息队列