分布式锁4大实现方案详解与选型指南
分布式锁主流实现方案深度解析
分布式锁是分布式系统中保证资源互斥访问的关键技术。本文将详细介绍四种主流实现方案及其核心机制,帮助开发者根据业务场景选择最适合的方案。
一、基于数据库的实现方案
1. 唯一索引/主键冲突
实现原理:利用数据库唯一键约束特性,通过插入相同数据触发唯一键冲突来实现锁竞争。
CREATE TABLE distributed_lock (
lock_name VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(64),
expire_time TIMESTAMP
);
加锁操作:
INSERT INTO distributed_lock(lock_name, owner_id, expire_time)
VALUES ('order_lock', 'server1', NOW() + INTERVAL 30 SECOND);
解锁操作:
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND owner_id = 'server1';
实践建议:
- 适合并发量不高的场景
- 必须设置合理的过期时间防止死锁
- 需要定时任务清理过期锁
2. 乐观锁(版本号机制)
实现原理:通过版本号或时间戳实现CAS(Compare-And-Swap)操作。
-- 初始化数据
CREATE TABLE resource_with_lock (
id BIGINT PRIMARY KEY,
version INT DEFAULT 0,
data VARCHAR(255)
);
-- 更新时检查版本
UPDATE resource_with_lock
SET data = 'new_value', version = version + 1
WHERE id = 123 AND version = 5;
实践建议:
- 适合读多写少场景
- 需要处理更新失败的重试逻辑
- 不保证严格的互斥性
3. 悲观锁(SELECT FOR UPDATE)
实现原理:利用数据库行锁机制阻塞其他事务。
// Spring事务中使用
@Transactional
public void updateWithLock(Long id) {
// 获取行锁
Resource res = entityManager.createQuery(
"SELECT r FROM Resource r WHERE r.id = :id", Resource.class)
.setParameter("id", id)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.getSingleResult();
// 业务处理
updateResource(res);
}
实践建议:
- 事务结束后会自动释放锁
- 注意锁等待超时设置
- 可能引起死锁,需要合理设计获取顺序
二、基于Redis的实现方案
1. SETNX + 过期时间
基本命令:
SET lock_key unique_value NX PX 30000
Java实现:
public boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
public boolean unlock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return result.equals(1L);
}
实践建议:
- 必须设置过期时间防止死锁
- 解锁操作需要Lua脚本保证原子性
- 考虑锁续期问题(看门狗机制)
2. RedLock算法
算法步骤:
- 获取当前时间(毫秒)
- 依次向N个Redis节点获取锁
- 计算获取锁总耗时,当且仅当大多数节点获取成功且耗时小于锁有效期时认为成功
- 如果获取失败,向所有节点发起释放锁请求
Redisson实现:
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7000")
.addNodeAddress("redis://127.0.0.1:7001");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
try {
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
// 业务逻辑
}
} finally {
lock.unlock();
}
实践建议:
- 至少部署5个Redis主节点提高可靠性
- 需要考虑时钟同步问题
- 性能比单节点方案低,适合高可靠性要求场景
3. Redisson高级特性
看门狗机制:
实践建议:
- 默认看门狗检查间隔是锁过期时间的1/3
- 业务执行时间不可预测时特别有用
- 注意正确处理锁释放避免资源泄漏
三、基于ZooKeeper的实现方案
1. 临时顺序节点实现
核心原理:
- 在锁节点下创建临时顺序子节点
- 判断自己是否是最小序号节点
- 如果不是,则监听前一个节点的删除事件
- 前一个节点删除后重新检查顺序
Curator框架实现:
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
// 业务处理
}
} finally {
lock.release();
}
节点结构:
/locks
/order
/_c_8623f016-634b-4a85-b905-5d8a61f885d6-0000000000
/_c_8623f016-634b-4a85-b905-5d8a61f885d6-0000000001
2. 监听机制(Watcher)
工作原理:
实践建议:
- 适合对可靠性要求极高的场景
- 性能比Redis方案低
- 需要处理"羊群效应"(Curator已优化)
四、其他中间件方案
1. Etcd实现
核心机制:
// 获取锁(租约机制)
resp, err := client.Grant(context.Background(), 10) // 10秒租约
_, err = client.Put(context.Background(), "lock_key", "value", clientv3.WithLease(resp.ID))
// 保持心跳保持锁
ka, err := client.KeepAlive(context.Background(), resp.ID)
实践建议:
- 基于Raft协议,强一致性保证
- 适合需要严格一致性的场景
- 提供事务支持,可实现更复杂锁逻辑
2. Consul实现
Session机制:
# 创建Session(TTL 15秒)
SESSION_ID=$(curl -X PUT 'http://localhost:8500/v1/session/create' \
-d '{"Name": "order_lock", "TTL": "15s"}')
# 获取锁
curl -X PUT "http://localhost:8500/v1/kv/lock/order?acquire=${SESSION_ID}" \
-d "server1"
实践建议:
- Session失效时自动释放关联的锁
- 支持健康检查集成
- 适合服务发现与锁结合的场景
五、方案选型对比
特性 | 数据库 | Redis | ZooKeeper | Etcd |
---|---|---|---|---|
性能 | 低 | 高 | 中 | 中 |
可靠性 | 中 | 依赖部署方式 | 高 | 高 |
实现复杂度 | 简单 | 中 | 高 | 中 |
自动续期 | 不支持 | 支持(Redisson) | 支持 | 支持 |
适用场景 | 低并发简单场景 | 高并发场景 | 高可靠场景 | 强一致性场景 |
终极选型建议:
- 简单场景:数据库方案足够
- 高并发:Redis + Redisson
- 高可靠:ZooKeeper/Etcd
- 已有中间件:优先使用现有基础设施
希望本文能帮助您在分布式系统开发中做出合理的锁方案选择。每种方案都有其适用场景,理解其核心原理才能发挥最大价值。