摘要:本文全面解析Java分布式锁机制,涵盖分布式锁核心原理、Redis实现与Redlock算法、ZooKeeper实现、数据库乐观锁对比,以及生产环境实战经验与最佳实践。适合需要解决多服务实例并发问题的Java开发者。
前言:为什么分布式锁比想象中复杂
早些年我做过一个秒杀系统,上线后发现偶尔会出现超卖问题。排查后才知道是分布式锁用错了。用的是Redis的SETNX,看起来没问题,但实际在高并发下出现了锁竞争异常,导致同一个商品被多次售出。
那时候我对分布式锁的理解就是"加个锁就行",完全没意识到在分布式环境下,锁的实现要考虑:网络延迟、进程崩溃、时钟同步、锁过期、并发竞争等一系列问题。每一个细节没处理好,都可能导致锁失效。
这篇文章会系统梳理分布式锁的原理、各种实现方案的优劣、实际项目中的踩坑经验。希望能帮你避开我曾经踩过的坑。
一、分布式锁的核心概念
1.1 为什么需要分布式锁
单机环境下,Java的synchronized或ReentrantLock就能保证线程安全(可参考Java锁机制深度解析)。但分布式系统中,多个服务实例部署在不同机器上,内存不共享,本地锁就失效了。
问题场景:
// 秒杀扣库存
public boolean reduceStock(Long productId) {
synchronized (this) { // 本地锁,只对当前JVM有效
Product product = productDao.getById(productId);
if (product.getStock() > 0) {
product.setStock(product.getStock() - 1);
productDao.update(product);
return true;
}
return false;
}
}部署三个实例:
实例A: 执行synchronized → 获得本地锁 → 查库存=10 → 扣库存
实例B: 执行synchronized → 获得本地锁 → 查库存=10 → 扣库存
实例C: 执行synchronized → 获得本地锁 → 查库存=10 → 扣库存三个实例同时执行,库存被扣了三次,出现超卖。
根本原因:每个实例的锁只保护自己的进程,不同进程之间没有协调机制。
1.2 分布式锁的核心要求
一个可靠的分布式锁必须满足:
互斥性:同一时刻只有一个客户端持有锁
防死锁:即使客户端崩溃,锁也能自动释放
容错性:锁服务故障时,不影响业务(可能退化)
可重入:同一线程可以多次获取锁
公平性(可选):按请求顺序分配锁
锁续期:长时间任务执行时,锁不会自动过期
1.3 分布式锁的实现方式对比

二、Redis分布式锁详解
2.1 基础实现:SETNX
最早期的实现使用SETNX命令:
public class RedisLock {
private Jedis jedis;
private String lockKey;
private String lockValue;
public boolean tryLock() {
lockValue = UUID.randomUUID().toString();
Long result = jedis.setnx(lockKey, lockValue);
if (result == 1) {
return true;
}
return false;
}
public void unlock() {
jedis.del(lockKey);
}
}问题1:死锁
如果获取锁后进程崩溃,锁永远不会释放。
解决:设置过期时间
public boolean tryLock() {
lockValue = UUID.randomUUID().toString();
Long result = jedis.setnx(lockKey, lockValue);
if (result == 1) {
jedis.expire(lockKey, 30); // 30秒过期
return true;
}
return false;
}问题2:SETNX和EXPIRE不是原子操作
如果SETNX成功后,还没执行EXPIRE就崩溃,仍然会死锁。
解决:使用SET命令的NX和EX选项(Redis 2.6.12+)
public boolean tryLock(long expireTime) {
lockValue = UUID.randomUUID().toString();
String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
return "OK".equals(result);
}这是原子操作,要么同时成功,要么同时失败。
2.2 锁续期问题
设置了30秒过期,但如果任务执行超过30秒怎么办?

解决:看门狗机制(Watchdog)
给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

public class RedisLockWithWatchdog {
private Jedis jedis;
private String lockKey;
private String lockValue;
private ScheduledExecutorService watchdog;
private volatile boolean locked = false;
public boolean tryLock(long expireTime) {
lockValue = UUID.randomUUID().toString();
String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
if ("OK".equals(result)) {
locked = true;
startWatchdog(expireTime);
return true;
}
return false;
}
// 看门狗:定期续期
private void startWatchdog(long expireTime) {
watchdog = Executors.newSingleThreadScheduledExecutor();
watchdog.scheduleAtFixedRate(() -> {
if (locked) {
jedis.expire(lockKey, expireTime);
}
}, expireTime / 3, expireTime / 3, TimeUnit.SECONDS);
}
public void unlock() {
locked = false;
if (watchdog != null) {
watchdog.shutdown();
}
// 只释放自己的锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, lockValue);
}
}看门狗每expireTime/3秒续期一次,保持锁不过期。
2.3 锁误释放问题
客户端A的锁过期后,被客户端B获取。A执行完释放锁,实际释放了B的锁。
解决:释放锁时检查是否是自己的锁
public void unlock() {
// Lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, lockValue);
}只有lockValue匹配时才删除,避免误删别人的锁。
2.4 Redisson实现
Redisson是Redis分布式锁的成熟实现,推荐直接使用:
public class RedissonLockDemo {
private RedissonClient redisson;
public void process() {
RLock lock = redisson.getLock("my-lock");
try {
// 尝试获取锁,最多等待100秒,锁自动过期30秒
boolean acquired = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (acquired) {
// 执行业务逻辑
doBusiness();
} else {
// 获取锁失败
handleLockFailed();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 只释放自己持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Redisson的优势:
看门狗自动续期:默认30秒过期,看门狗每10秒续期
可重入锁:同一线程可以多次获取
公平锁:支持公平锁模式
联锁:可以同时锁定多个资源
读写锁:支持分布式读写锁
红锁:多节点Redlock算法
2.5 Redlock算法
Redis主从架构下,主节点获取锁后还没同步到从节点就崩溃,从节点升级为主节点,新客户端也能获取锁,出现两个锁。
Redlock解决方案:
在多个独立的Redis节点上同时获取锁,只有多数节点成功才算真正获取锁。
public class RedlockDemo {
private RedissonClient client1;
private RedissonClient client2;
private RedissonClient client3;
public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
RLock lock1 = client1.getLock(lockKey);
RLock lock2 = client2.getLock(lockKey);
RLock lock3 = client3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
return redLock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return false;
}
}
public void unlock(String lockKey) {
RLock lock1 = client1.getLock(lockKey);
RLock lock2 = client2.getLock(lockKey);
RLock lock3 = client3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.unlock();
}
}Redlock流程:

争议:
Redlock算法在学术界有争议,主要问题:
多节点时钟同步假设
网络分区情况下的行为
实际项目中,如果可靠性要求极高,建议用ZooKeeper。
三、ZooKeeper分布式锁详解
3.1 ZooKeeper锁原理
ZooKeeper的顺序临时节点实现锁:
创建顺序临时节点:
每个客户端在锁目录下创建临时顺序节点:

判断是否获得锁:
由于序号的递增性,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。其他客户端监听前一个节点的删除事件。

临时节点特性:
客户端会话断开时,临时节点自动删除,避免死锁。
3.2 ZooKeeper锁实现
使用Apache Curator框架:
public class ZooKeeperLockDemo {
private CuratorFramework client;
public void process() {
InterProcessMutex lock = new InterProcessMutex(client, "/locks/my-lock");
try {
// 尝试获取锁,最多等待10秒
boolean acquired = lock.acquire(10, TimeUnit.SECONDS);
if (acquired) {
// 执行业务逻辑
doBusiness();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 释放锁
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}Curator提供的锁类型:

3.3 ZooKeeper锁的优势
可靠性高:
ZooKeeper的CP架构,保证一致性
临时节点自动清理,不会死锁
会话断开自动释放锁
顺序性保证:
节点顺序保证公平锁,先请求的先获得锁。
可监听:
Watch机制实时感知锁状态变化。
3.4 ZooKeeper锁的劣势
性能较低:
每次锁操作需要多次ZooKeeper交互:
创建节点
查询节点列表
判断顺序
设置监听
高并发下性能不如Redis。
依赖ZooKeeper集群:
需要维护ZooKeeper集群,增加运维成本。
四、数据库分布式锁
4.1 基于唯一索引
利用数据库唯一索引的互斥性:
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) PRIMARY KEY,
holder_id VARCHAR(64),
create_time TIMESTAMP
); public class DatabaseLock {
private DataSource dataSource;
public boolean tryLock(String lockKey, String holderId) {
try (Connection conn = dataSource.getConnection()) {
String sql = "INSERT INTO distributed_lock (lock_key, holder_id, create_time) " +
"VALUES (?, ?, NOW())";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, lockKey);
ps.setString(2, holderId);
ps.executeUpdate();
return true;
} catch (SQLException e) {
// 唯一索引冲突,说明锁已被占用
return false;
}
}
public void unlock(String lockKey, String holderId) {
try (Connection conn = dataSource.getConnection()) {
String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND holder_id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, lockKey);
ps.setString(2, holderId);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
}问题:锁没有过期机制,如果客户端崩溃,锁永远不释放。
解决:定时清理超时锁
-- 定时任务清理超过5分钟的锁
DELETE FROM distributed_lock WHERE create_time < NOW() - INTERVAL 5 MINUTE4.2 乐观锁
乐观锁不真正加锁,而是用版本号控制:
CREATE TABLE product (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
stock INT,
version INT DEFAULT 0
); public boolean reduceStock(Long productId) {
// 查询当前版本
Product product = productDao.getById(productId);
if (product.getStock() <= 0) {
return false;
}
// 更新时检查版本
int rows = productDao.updateWithVersion(
productId,
product.getStock() - 1,
product.getVersion()
);
return rows > 0; // 更新成功说明获取到锁
} UPDATE product
SET stock = ?, version = version + 1
WHERE id = ? AND version = ?如果版本不匹配,更新失败,说明有其他线程已修改。
乐观锁适用场景:
读多写少
冲突概率低
可以容忍失败重试
4.3 数据库锁的优缺点
优点:
实现简单,不需要额外组件
利用已有数据库基础设施
可靠性依赖数据库的事务特性
缺点:
性能较低,每次锁操作都要访问数据库
获取锁失败需要轮询重试
数据库压力增大
五、实战经验与踩坑总结
5.1 秒杀系统的锁优化
问题场景:
秒杀商品库存扣减,使用Redis分布式锁。高峰期每秒上万请求,锁竞争严重。
原始实现:
public boolean reduceStock(Long productId) {
RLock lock = redisson.getLock("stock:" + productId);
try {
lock.lock();
Product product = productDao.getById(productId);
if (product.getStock() > 0) {
product.setStock(product.getStock() - 1);
productDao.update(product);
return true;
}
return false;
} finally {
lock.unlock();
}
}问题分析:
每次请求都竞争锁,大量请求等待
锁内包含数据库查询,耗时较长
锁粒度太大,阻塞所有库存操作
优化方案:
public boolean reduceStock(Long productId) {
// 1. 先检查库存(不加锁)
Product product = productDao.getById(productId);
if (product.getStock() <= 0) {
return false; // 库存不足直接返回
}
// 2. Redis预扣库存(原子操作)
Long remain = redisTemplate.opsForValue().decrement("stock:cache:" + productId);
if (remain < 0) {
// 库存不足,回滚Redis
redisTemplate.opsForValue().increment("stock:cache:" + productId);
return false;
}
// 3. 分布式锁+数据库扣库存
RLock lock = redisson.getLock("stock:" + productId);
try {
lock.lock();
// 异步写数据库(不阻塞)
asyncUpdateStock(productId);
return true;
} finally {
lock.unlock();
}
}优化效果:
Redis预扣库存,减少锁竞争
异步写数据库,不阻塞请求
锁内操作减少,吞吐量提升10倍
5.2 分布式锁超时问题
问题场景:
任务执行时间超过锁过期时间,锁自动释放,另一个客户端获取锁,两个客户端同时执行。
踩坑经历:
// 锁过期30秒
RLock lock = redisson.getLock("process-lock");
lock.lock(30, TimeUnit.SECONDS);
try {
// 执行任务(实际耗时50秒)
longRunningTask();
} finally {
lock.unlock(); // 此时锁已过期,释放了别人的锁
}解决方案:
RLock lock = redisson.getLock("process-lock");
// 不指定过期时间,使用看门狗自动续期
lock.lock();
try {
longRunningTask();
} finally {
// 只释放自己持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}Redisson默认30秒过期,看门狗每10秒续期,保持锁不过期。
5.3 锁等待超时处理
问题场景:
高并发下,大量请求等待锁,等待超时后请求失败,用户体验差。
优化方案:
public boolean processWithLock(Long orderId) {
RLock lock = redisson.getLock("order:" + orderId);
try {
// 尝试获取锁,最多等待100ms
boolean acquired = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (acquired) {
return processOrder(orderId);
} else {
// 获取锁失败,降级处理
return fallbackProcess(orderId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 降级处理:记录请求,异步处理
private boolean fallbackProcess(Long orderId) {
asyncQueue.add(orderId);
return true; // 返回"已提交",后台异步处理
}关键点:
设置合理的等待时间(如100ms)
等待失败时降级处理,不阻塞用户
异步队列兜底,保证请求不丢失
5.4 锁粒度优化
问题场景:
锁粒度太大,不相关的操作也被阻塞。
原始实现:
// 锁整个订单处理
RLock lock = redisson.getLock("order-process");
lock.lock();
try {
// 处理订单(包括:库存扣减、优惠券核销、积分计算、日志记录)
processOrder();
} finally {
lock.unlock();
}优化方案:
// 分段锁:不同操作用不同的锁
public void processOrder(Order order) {
// 库存锁
reduceStockWithLock(order.getProductId());
// 优惠券锁
useCouponWithLock(order.getCouponId());
// 积分锁
calculatePointsWithLock(order.getUserId());
// 日志记录(不需要锁)
recordLog(order);
}
private void reduceStockWithLock(Long productId) {
RLock lock = redisson.getLock("stock:" + productId);
try {
lock.lock();
reduceStock(productId);
} finally {
lock.unlock();
}
}效果:
不同商品的库存扣减可以并行
不需要同步的操作不加锁
整体吞吐量提升
5.5 锁与事务的协调
问题场景:
锁在事务内获取,事务还没提交锁就释放了。
@Transactional
public void process(Long orderId) {
RLock lock = redisson.getLock("order:" + orderId);
lock.lock();
try {
updateOrder(orderId);
// 锁释放
} finally {
lock.unlock(); // 释放锁
}
// 事务提交(此时锁已释放,其他线程可以修改)
}另一个线程获取锁,但前一个线程的事务还没提交,读到的数据是旧值。
解决方案:
public void process(Long orderId) {
RLock lock = redisson.getLock("order:" + orderId);
lock.lock();
try {
// 在锁内调用事务方法
transactionalUpdate(orderId);
} finally {
lock.unlock(); // 事务提交后释放锁
}
}
@Transactional
private void transactionalUpdate(Long orderId) {
updateOrder(orderId);
}锁包裹事务,确保事务提交后才释放锁。
六、分布式锁最佳实践
6.1 选择合适的实现方案

6.2 锁的使用原则
原则1:锁范围最小化
// 不推荐
lock.lock();
try {
validateData(); // 不需要锁
processData(); // 需要锁
sendNotification(); // 不需要锁
} finally {
lock.unlock();
}
// 推荐
validateData();
lock.lock();
try {
processData();
} finally {
lock.unlock();
}
sendNotification();原则2:设置合理的过期时间
过期时间应该大于任务执行时间,但也不能太长(客户端崩溃后锁释放太慢)。
建议:
简单操作:10-30秒
复杂操作:使用看门狗自动续期
原则3:正确释放锁
finally {
// 只释放自己持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}原则4:锁失败降级处理
不要让用户长时间等待,获取锁失败时降级处理:
boolean acquired = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (!acquired) {
// 降级:记录请求,异步处理
asyncQueue.add(request);
return "已提交,请稍后查看结果";
}6.3 监控与告警
监控指标:
锁获取成功率
锁等待时间
锁持有时间
锁竞争次数
告警规则:
锁获取成功率 < 80%:告警
锁等待时间 > 1秒:告警
锁持有时间 > 预期时间2倍:告警
七、常见问题解答
Q1:Redis锁和ZooKeeper锁哪个好?
Redis性能好,适合高并发场景。ZooKeeper可靠性高,适合对一致性要求高的场景。根据需求选择,不是哪个绝对好。
Q2:Redlock算法必须用吗?
不是。Redlock解决主从切换丢锁问题,但增加了复杂度。如果业务允许偶尔锁失效(如非关键操作),不需要Redlock。
Q3:看门狗机制会一直续期吗?
不会。客户端主动释放锁,或者会话断开,看门狗停止。Redisson的实现是安全的。
Q4:分布式锁能保证绝对安全吗?
不能。网络分区、时钟问题等极端情况可能导致锁失效。关键业务要有兜底方案(如事后校验)。
Q5:如何测试分布式锁的正确性?
测试并发竞争、锁过期、客户端崩溃、网络延迟等场景。可以用JMeter模拟高并发,或写测试脚本随机杀死进程。
Q6:分布式锁可以用在数据库事务中吗?
可以,但要正确处理顺序:先获取锁,再开启事务,事务提交后释放锁。不要在事务内获取锁。
八、总结
分布式锁不是"加个锁就行",而是要考虑:
实现选择:
Redis:高性能,适合高并发
ZooKeeper:高可靠,适合一致性要求高
数据库:简单,适合已有基础设施
Redis关键点:
SET命令原子加锁
看门狗自动续期
Lua脚本安全释放
Redlock解决主从问题
ZooKeeper关键点:
顺序临时节点
自动清理,不死锁
Curator简化实现
实战经验:
锁粒度要小
过期时间要合理
释放锁要检查所有权
失败要有降级方案
监控告警要完善
用好分布式锁,能让分布式系统安全可靠。但关键是理解每种实现的优缺点,根据场景选择合适的方案,并做好兜底准备。
评论区