摘要:在分布式系统中,并发控制是绕不开的核心话题。当多个服务实例同时操作共享资源时,单机锁已经失效,分布式锁应运而生。本文将带你深入理解分布式锁的本质,掌握Redis和ZooKeeper两种主流实现方案,并提供Spring Boot完整代码示例,助你构建高可用、强一致的分布式系统。
一、为什么需要分布式锁?——从单机到分布式的必然选择
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环境、云原生架构 |
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 深度对比
| 维度 | 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:强一致性、无死锁风险、金融级要求
- 永远不要自研:除非有特殊需求,否则优先使用成熟框架
- 锁是最后手段:优先考虑无锁化设计,如分库分表、消息队列
📢 关注我,获取更多技术干货!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!更多Java进阶、分布式系统、微服务架构的实战干货,尽在我的公众号:
【卷毛的技术笔记】
🔥 专注后端技术深度解析
🔥 每周一篇硬核原创文章
🔥 陪伴你从初级到架构师的成长之路
微信搜索「卷毛的技术笔记」立即关注!技术路上,我们一起成长! 💪