分布式锁
为什么需要分布式锁
在单体应用中,synchronized 或 ReentrantLock 即可保证线程安全。但在分布式系统中,多个 JVM 进程并发操作共享资源时,本地锁无法跨进程互斥。
text
单体应用: JVM 1 ─→ synchronized → 互斥
分布式系统: JVM 1 ─→ ??? ×
JVM 2 ─→ ??? × ← 各自 JVM 的锁无效
JVM 3 ─→ ??? ×实现方案对比
| 方案 | 一致性 | 可靠性 | 性能 | 运维成本 |
|---|---|---|---|---|
| Redis | 最终一致(AP) | ★★★ | ★★★★★ | 低 |
| RedLock | 弱一致(有争议) | ★★★ | ★★★★ | 低 |
| Zookeeper | 强一致(CP) | ★★★★★ | ★★★ | 中 |
| 数据库 | 强一致 | ★★★★ | ★ | 无 |
方案一:Redis 分布式锁
基础实现(SET NX)
java
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 尝试获取锁
* @param key 锁的 key
* @param requestId 请求标识(用于安全释放,避免误删他人锁)
* @param expireMs 锁自动过期时间(毫秒)
* @return true 获取成功
*/
public boolean tryLock(String key, String requestId, long expireMs) {
// SET key value NX PX milliseconds
// NX: key 不存在才设置成功(互斥)
// PX: 设置过期时间(防止死锁)
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(key, requestId, Duration.ofMillis(expireMs))
);
}
/**
* 释放锁(使用 Lua 脚本保证原子性)
*/
public boolean unlock(String key, String requestId) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), requestId);
return Long.valueOf(1).equals(result);
}
}核心要点:
- SET NX + PX — 原子操作(不能分开执行)
- requestId — 使用 UUID 标识锁持有者,防止误删
- Lua 脚本释放 — 校验持有者再删除,原子性
- 过期时间 — 防止死锁
使用示例
java
@Service
public class InventoryService {
@Autowired
private RedisDistributedLock lock;
public boolean deductStock(Long skuId, Integer quantity) {
String lockKey = "lock:stock:" + skuId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁(等待 3 秒)
if (!lock.tryLock(lockKey, requestId, 5000)) {
throw new ServiceException("系统繁忙,请稍后重试");
}
// 查询库存
Stock stock = stockMapper.selectBySkuId(skuId);
if (stock.getQuantity() < quantity) {
return false;
}
// 扣减库存
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock);
return true;
} finally {
lock.unlock(lockKey, requestId);
}
}
}存在的问题
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 锁超时 | 业务执行时间 > 锁过期时间,锁自动释放 | 看门狗(Watch Dog)续期 |
| 主从切换 | Redis 主节点写入锁后宕机,从节点还未同步 | RedLock(但存在争议) |
| 时钟跳跃 | 服务器时间回拨 | 避免依赖系统时间 |
方案二:Redisson(生产推荐)
Redisson 是 Redis 官方推荐的 Java 客户端,提供了成熟的分布式锁实现。
依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>使用
java
@Service
public class InventoryService {
@Autowired
private RedissonClient redisson;
public boolean deductStock(Long skuId, Integer quantity) {
String lockKey = "lock:stock:" + skuId;
RLock lock = redisson.getLock(lockKey);
try {
// 1. 等待 3 秒,锁 30 秒自动释放
if (!lock.tryLock(3, 30, TimeUnit.SECONDS)) {
throw new ServiceException("系统繁忙");
}
// 2. 看门狗自动续期(默认每 10 秒续期一次)
// 只要业务未完成,锁就不会过期
return doDeduct(skuId, quantity);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("操作中断");
} finally {
// 3. 释放锁(同时取消看门狗)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}看门狗机制:
text
获取锁成功 → 启动 Watch Dog
→ 默认每 10 秒检查一次
→ 如果锁还存在,续期到 30 秒
→ 业务完成释放锁 → 取消 Watch Dog
→ 业务宕机 → 锁在 30 秒后自动释放(防止死锁)公平锁、读写锁
java
// 公平锁(排队)
RLock fairLock = redisson.getFairLock("lock:fair");
// 读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("lock:rw");
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();
// 联锁(同时锁多个 key 再执行)
RLock lock1 = redisson.getLock("lock:1");
RLock lock2 = redisson.getLock("lock:2");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);
multiLock.tryLock(3, 30, TimeUnit.SECONDS);方案三:Zookeeper 分布式锁
原理
利用 Zookeeper 的临时顺序节点 + Watcher 机制实现。
text
/locks/my_lock/
├── lock_0000000001 ← 客户端 A 创建(最小序号,获得锁)
├── lock_0000000002 ← 客户端 B 创建(监听前一个节点)
└── lock_0000000003 ← 客户端 C 创建(监听前一个节点)
加锁: 创建临时顺序节点 → 判断是否序号最小
→ 是 → 获得锁
→ 否 → 监听前一个节点
解锁: 删除自己的节点
→ 下一个节点收到 Watcher 通知
→ 检查自己是否最小 → 获得锁Curator 实现
java
@Service
public class ZkDistributedLock {
@Autowired
private CuratorFramework client;
public void executeWithLock(String lockPath, Runnable task) {
// 创建可重入锁
InterProcessLock lock = new InterProcessSemaphoreMutex(client, lockPath);
try {
// 等待锁(最多等 10 秒)
if (!lock.acquire(10, TimeUnit.SECONDS)) {
throw new RuntimeException("获取锁超时");
}
task.run();
} catch (Exception e) {
throw new RuntimeException("分布式锁异常", e);
} finally {
try {
lock.release();
} catch (Exception e) {
log.error("释放锁失败", e);
}
}
}
}与 Redis 方案对比
| 特性 | Redis | Zookeeper |
|---|---|---|
| 一致性 | 最终一致 | 强一致(ZAB 协议) |
| 自动释放 | 过期时间 | 临时节点 + Session 超时 |
| 性能 | 极高(内存操作) | 中等(磁盘写入 + Leader 选举) |
| 监控 | 需额外实现 | Watcher 机制内置 |
| 客户端 | Redisson(成熟) | Curator(成熟) |
| 运维 | 低 | 中(需维护 ZK 集群) |
方案四:数据库分布式锁
基于唯一约束
sql
CREATE TABLE `t_distributed_lock` (
`lock_key` varchar(64) NOT NULL,
`holder` varchar(64) NOT NULL,
`expire_time` datetime NOT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`lock_key`)
) ENGINE=InnoDB;java
@Transactional
public boolean tryLock(String lockKey, String holder, long expireMs) {
try {
lockMapper.insert(new LockRecord(lockKey, holder,
LocalDateTime.now().plus(expireMs, ChronoUnit.MILLIS)));
return true;
} catch (DuplicateKeyException e) {
// 已存在,检查是否过期
LockRecord record = lockMapper.selectByKey(lockKey);
if (record.getExpireTime().isBefore(LocalDateTime.now())) {
// 锁已过期,抢锁
int rows = lockMapper.renewIfExpired(lockKey, holder, LocalDateTime.now().plusSeconds(30));
return rows > 0;
}
return false;
}
}
public void unlock(String lockKey, String holder) {
lockMapper.deleteByKeyAndHolder(lockKey, holder);
}不推荐生产环境使用:性能差、连接池压力大、死锁风险。
选型建议
text
Redis 分布式锁(Redisson)← 首选
- 性能高,能满足绝大多数场景
- 看门狗机制解决锁超时问题
- 适用于高并发下的秒杀、库存扣减
Zookeeper 分布式锁 ← 强一致性场景
- 对数据一致性要求极高
- 适合分布式调度、配置管理
- 性能低于 Redis
数据库分布式锁 ← 不推荐
- 无额外组件即可实现
- 性能差,仅适合低并发场景最佳实践
text
1. 锁的粒度尽可能小(lock:stock:SkuId 而非 lock:stock)
2. 设置合理的超时时间(至少 3 倍于平均执行时间)
3. 释放锁放在 finally 块中
4. 使用 requestId 标识锁持有者,防止误删
5. 监控锁获取失败率和等待时间
6. 锁未获取到时快速失败而非无限等待