分布式锁4大实现方案详解:数据库/Redis对比
分布式锁主流实现方案深度解析
分布式锁是分布式系统中保证资源互斥访问的核心机制。本文将深入分析四种主流实现方案的技术原理、适用场景及最佳实践。
一、基于数据库的实现方案
1. 唯一索引/主键冲突
实现原理:利用数据库唯一约束特性,通过插入相同键值实现锁竞争。
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) PRIMARY KEY,
expire_time DATETIME NOT NULL
);
加锁操作:
INSERT INTO distributed_lock(lock_key, expire_time)
VALUES ('order_lock', NOW() + INTERVAL 30 SECOND);
解锁操作:
DELETE FROM distributed_lock WHERE lock_key = 'order_lock';
实践建议:
- 必须设置过期时间避免死锁
- 添加定时任务清理过期锁
- 适合并发量较低的场景(QPS < 500)
2. 乐观锁(版本号机制)
实现原理:通过版本号或时间戳实现CAS操作。
UPDATE resource_table
SET resource = new_value, version = version + 1
WHERE id = #{id} AND version = #{old_version};
适用场景:
- 读多写少环境
- 冲突概率低的业务场景
3. 悲观锁(SELECT FOR UPDATE)
实现原理:利用数据库行锁机制阻塞其他事务。
// 事务方法
@Transactional
public void updateWithLock(Long id) {
// 获取行锁
Resource res = jdbcTemplate.queryForObject(
"SELECT * FROM resources WHERE id = ? FOR UPDATE",
rowMapper, id);
// 业务处理
updateResource(res);
}
注意事项:
- 必须与事务注解@Transactional配合使用
- 可能导致死锁,需设置超时时间
- 高并发场景下性能较差
二、基于Redis的实现方案
1. SETNX + 过期时间
基础实现:
public boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
关键问题:
- 非原子性操作风险(SETNX + EXPIRE应使用Lua脚本)
- 误删其他客户端锁(需验证requestId)
2. RedLock算法
实现步骤:
- 获取当前毫秒级时间戳
- 依次向N个Redis节点申请锁
- 当获得多数节点锁时,计算获取锁总耗时
- 若总耗时小于锁有效期,则获取成功
实践建议:
- 至少部署5个Redis主节点
- 网络延迟需稳定在锁有效期的1/3以内
- 官方建议仅在没有其他方案时使用
3. Redisson实现
核心特性:
- Watchdog自动续期机制
- 可重入锁支持
- 多种锁类型(公平锁、联锁等)
// 获取锁
RLock lock = redisson.getLock("orderLock");
try {
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
// 业务逻辑
}
} finally {
lock.unlock();
}
三、基于ZooKeeper的实现方案
1. 临时顺序节点
实现原理:
- 在锁节点下创建临时顺序节点
- 获取所有子节点并排序
- 判断自己是否是最小节点
- 若不是则监听前一个节点
Curator实现示例:
InterProcessMutex lock = new InterProcessMutex(client, "/order_lock");
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
// 业务处理
}
} finally {
lock.release();
}
2. 对比优势
特性 | Redis方案 | ZK方案 |
---|---|---|
性能 | 高 | 中 |
可靠性 | 依赖持久化 | 高 |
实现复杂度 | 简单 | 复杂 |
锁自动释放 | 依赖TTL | 会话结束自动释放 |
公平锁 | 需额外实现 | 原生支持 |
四、其他中间件方案
1. Etcd实现
核心机制:
# 获取锁(租约10秒)
etcdctl lease grant 10
# 写入键值(关联租约ID)
etcdctl put --lease=1234abcd /lock/resource1 client1
特点:
- 基于Raft协议强一致性
- 租约自动过期
- 适合Kubernetes环境
2. Consul实现
Session机制:
// 创建Session(TTL 10秒)
String sessionId = consulClient.sessionCreate(
NewSession.request().ttl("10s"));
// 获取锁
boolean locked = consulClient.setKVValue(
"lock/resource1", "client1",
new QueryParams().acquire(sessionId));
五、选型建议
- 简单场景:Redis SETNX(需处理续期问题)
- 高一致性要求:ZooKeeper/Etcd
- 云原生环境:Etcd/Consul
- 已有技术栈:优先选择团队熟悉的方案
性能对比:
- Redis:10,000+ QPS
- ZooKeeper:1,000-5,000 QPS
- 数据库:500-1,000 QPS
六、常见陷阱
锁续期问题:业务执行时间超过锁有效期
- 解决方案:Watchdog机制或合理设置超时
锁误释放:客户端A释放了客户端B的锁
- 解决方案:每个客户端设置唯一标识
GC停顿导致锁失效:
- 解决方案:ZK通过会话检测,Redis需额外处理
时钟漂移问题:
- 解决方案:避免依赖多节点时间,使用TTL机制
最佳实践
- 始终设置锁超时时间
- 实现锁的可重入性
- 添加锁获取失败的重试机制
- 记录锁竞争情况监控
- 考虑锁降级策略(如从分布式锁降级到本地锁)
// 锁模板示例
public <T> T executeWithLock(String lockKey, int waitTime, int leaseTime, Supplier<T> supplier) {
RLock lock = redisson.getLock(lockKey);
try {
if (lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)) {
return supplier.get();
}
throw new LockAcquireException("Get lock failed");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
通过深入理解各方案的实现原理和适用场景,开发者可以构建出既可靠又高效的分布式锁体系。