分布式锁实现方案深度解析:数据库与Redis对比
分布式锁主流实现方案深度解析
分布式锁是分布式系统中保证资源互斥访问的核心机制,本文将系统性地介绍当前主流的分布式锁实现方案,包括技术原理、实现细节和最佳实践。
一、基于数据库的实现方案
1. 唯一索引/主键冲突
实现原理:利用数据库唯一键约束特性,通过插入相同键值实现互斥。
CREATE TABLE distributed_lock (
lock_name VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(255),
expire_time DATETIME
);
加锁操作:
public boolean tryLock(String lockName, String ownerId, long expireSeconds) {
try {
return jdbcTemplate.update(
"INSERT INTO distributed_lock VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND))",
lockName, ownerId, expireSeconds) == 1;
} catch (DuplicateKeyException e) {
// 检查是否为过期锁
int updated = jdbcTemplate.update(
"UPDATE distributed_lock SET owner_id=?, expire_time=DATE_ADD(NOW(), INTERVAL ? SECOND) " +
"WHERE lock_name=? AND expire_time <= NOW()",
ownerId, expireSeconds, lockName);
return updated == 1;
}
}
实践建议:
- 适合并发量不高的简单场景
- 必须设置合理的过期时间避免死锁
- 需要定时任务清理过期锁
2. 乐观锁(版本号机制)
实现原理:通过版本号或时间戳实现CAS操作
CREATE TABLE optimistic_lock (
resource_id BIGINT PRIMARY KEY,
version INT DEFAULT 0,
data VARCHAR(1000)
);
-- 更新时检查版本
UPDATE optimistic_lock
SET data = 'new_value', version = version + 1
WHERE resource_id = ? AND version = ?;
实践建议:
- 适合读多写少场景
- 需要处理更新失败的重试逻辑
- 长时间冲突会导致大量重试
3. 悲观锁(SELECT FOR UPDATE)
实现原理:利用数据库行锁机制阻塞其他事务
@Transactional
public void doWithLock(Long resourceId) {
// 获取行锁
Resource res = jdbcTemplate.queryForObject(
"SELECT * FROM resources WHERE id = ? FOR UPDATE",
rowMapper, resourceId);
// 业务处理
process(res);
}
实践建议:
- 事务要保持短小精悍
- 需要合理设置事务超时时间
- 注意锁升级问题(如MySQL的间隙锁)
二、基于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);
}
解锁脚本(避免误删):
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
2. RedLock算法
算法步骤:
- 获取当前毫秒级时间戳
- 依次尝试从N个独立Redis实例获取锁
- 计算获取锁总耗时,当且仅当大多数节点获取成功且耗时小于锁有效期时认为成功
- 如果失败,向所有节点发起释放锁请求
实践建议:
- 官方推荐5个独立实例
- 时钟同步问题可能导致锁失效
- 性能开销较大,适合关键业务
3. Redisson实现
特性:
- Watchdog自动续期机制
- 可重入锁支持
- 多种锁类型(公平锁、联锁等)
// 获取锁
RLock lock = redisson.getLock("myLock");
try {
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
// 业务逻辑
}
} finally {
lock.unlock();
}
三、基于ZooKeeper的实现
1. 临时顺序节点实现
核心流程:
- 在锁节点下创建临时顺序节点
- 获取所有子节点并排序
- 判断自己是否为最小节点,是则获取锁
- 否则监听前一个节点的删除事件
Curator实现示例:
InterProcessMutex lock = new InterProcessMutex(client, "/my_lock");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 业务处理
}
} finally {
lock.release();
}
2. 监听机制(Watcher)
特性:
- 事件一次性触发
- 会话失效自动释放锁
- 避免羊群效应(只监听前驱节点)
实践建议:
- 适合对可靠性要求高的场景
- 注意ZooKeeper的watch数量限制
- 会话超时时间要合理设置
四、其他中间件方案
1. Etcd实现
核心机制:
# 获取锁(租约10秒)
etcdctl lease grant 10
# 使用租约PUT键值
etcdctl put --lease=1234abcd /lock/resource1 owner1
特性:
- 基于租约的自动过期
- 事务比较支持
- 线性一致性保证
2. Consul实现
Session机制:
// 创建Session
NewSession session = NewSession.builder()
.name("service-lock")
.ttl("10s")
.lockDelay("0s")
.build();
// 获取锁
KVOperation operation = KVOperation.builder()
.verb(KVVerb.LOCK)
.key("locks/resource1")
.session(sessionId)
.build();
五、方案对比与选型建议
方案 | 性能 | 可靠性 | 实现复杂度 | 适用场景 |
---|---|---|---|---|
数据库 | 低 | 中 | 简单 | 低并发、已有数据库依赖 |
Redis | 高 | 中高 | 中等 | 高并发、允许偶尔失效 |
ZooKeeper | 中 | 高 | 复杂 | 强一致性要求、可靠性优先 |
Etcd | 中高 | 高 | 中等 | 云原生环境、需要线性一致性 |
选型建议:
- 单数据中心首选Redis(Redisson实现)
- 金融级场景考虑ZooKeeper
- Kubernetes环境可优先考虑Etcd
- 简单场景可用数据库方案
六、最佳实践
锁粒度控制:按资源ID分片,避免全局锁
String lockKey = "order_lock:" + orderId;
超时设置:业务操作超时应小于锁超时
// 业务超时5秒,锁超时10秒 lock.tryLock(5, 10, TimeUnit.SECONDS);
容错处理:
int maxRetries = 3; while (maxRetries-- > 0) { if (tryLock()) { try { doBusiness(); return; } finally { unlock(); } } Thread.sleep(100); } throw new LockAcquireFailedException();
监控指标:
- 锁等待时间
- 锁持有时间
- 锁获取失败率
通过合理选择和正确实现分布式锁,可以确保分布式系统中的数据一致性和业务正确性。建议根据实际业务场景和技术栈选择最适合的方案。