在分布式系统中,锁的本质是协调多个进程对共享资源的访问顺序。与单机环境下的锁不同,分布式锁需要解决网络延迟、节点故障等特有挑战。我曾在一个电商秒杀系统中亲历过分布式锁的重要性——当库存仅剩最后一件时,如果没有可靠的锁机制,超卖问题会直接导致平台资损。
根据分布式系统理论,一个合格的分布式锁必须满足以下核心要求:
互斥性:这是锁的基本属性。在任意时刻,只能有一个客户端持有锁。我们在实际测试中发现,某些Redis集群在脑裂情况下可能短暂违反此原则,需要特殊处理。
死锁预防:必须确保即使锁持有者崩溃,锁最终也能被释放。常见做法是设置合理的TTL(Time-To-Live)。我在早期项目中曾因未设置TTL导致系统死锁,最终只能手动清理数据库。
容错能力:当部分节点故障时,锁服务仍应可用。ZooKeeper要求半数以上节点存活,而Redis哨兵模式需要至少一个主节点可用。
性能保障:锁操作不能成为系统瓶颈。实测数据显示,Redis锁的吞吐量可达10万QPS,而数据库锁通常不足1千QPS。
数据库锁最直观的实现是利用唯一约束:
sql复制CREATE TABLE distributed_lock (
id INT PRIMARY KEY AUTO_INCREMENT,
lock_name VARCHAR(64) UNIQUE,
owner VARCHAR(64),
expire_time DATETIME
);
获取锁的SQL:
sql复制INSERT INTO distributed_lock(lock_name, owner, expire_time)
VALUES ('order_lock', 'service_01', NOW() + INTERVAL 30 SECOND)
ON DUPLICATE KEY UPDATE
owner = IF(expire_time < NOW(), VALUES(owner), owner),
expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time);
连接池耗尽:在高并发场景下,大量连接可能被锁操作占用。建议:
时钟同步问题:各节点时钟不同步会导致过期时间判断错误。解决方案:
CURRENT_TIMESTAMP替代应用传入的时间性能优化技巧:
Redisson是目前最成熟的Redis分布式锁实现。以下是典型用法:
java复制RLock lock = redissonClient.getLock("orderLock");
try {
// 尝试加锁,最多等待100ms,锁持有时间30s
if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) {
// 业务逻辑
}
} finally {
lock.unlock();
}
看门狗机制:
Config.setLockWatchdogTimeout()调整红锁算法:
java复制RLock lock1 = redissonClient1.getLock("lock");
RLock lock2 = redissonClient2.getLock("lock");
RLock lock3 = redissonClient3.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();
红锁需要至少N/2+1个节点获取成功才算加锁成功,能容忍部分节点故障。
避免主从切换问题:
Lua脚本优化:
Redisson使用Lua脚本保证原子性。自定义实现时应注意:
lua复制if redis.call("exists",KEYS[1])==0 then
redis.call("hset",KEYS[1],ARGV[2],1)
redis.call("pexpire",KEYS[1],ARGV[1])
return nil
end
性能压测数据:
ZooKeeper通过临时顺序节点实现分布式锁:
java复制public void lock() throws Exception {
// 创建临时顺序节点
currentPath = zk.create(basePath + "/lock-",
null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点并排序
List<String> children = zk.getChildren(basePath, false);
Collections.sort(children);
// 判断是否获得锁
if (currentPath.endsWith(children.get(0))) {
return;
}
// 监听前一个节点
String previousPath = basePath + "/" + children.get(Collections.binarySearch(children,
currentPath.substring(currentPath.lastIndexOf('/') + 1)) - 1);
CountDownLatch latch = new CountDownLatch(1);
zk.exists(previousPath, event -> {
if (event.getType() == EventType.NodeDeleted) {
latch.countDown();
}
});
latch.await();
}
连接复用:
监听器优化:
CuratorFramework的InterProcessMutex节点清理:
| 维度 | 数据库 | Redis | ZooKeeper |
|---|---|---|---|
| 一致性保证 | 强一致 | 最终一致 | 强一致 |
| 吞吐量(QPS) | <1,000 | >50,000 | 2,000-10,000 |
| 最小延迟 | 5-10ms | 0.1-2ms | 2-5ms |
| 可重入性 | 需自行实现 | 原生支持 | 需自行实现 |
| 公平性 | 无 | 可配置 | 原生公平 |
| 运维复杂度 | 低 | 中 | 高 |
| 典型适用场景 | 低频管理任务 | 高并发业务 | 金融交易 |
yaml复制redisson:
lock:
watchdog-timeout: 30000
retry-interval: 100
retry-attempts: 3
sql复制SELECT * FROM job_lock WHERE job_name='report' FOR UPDATE NOWAIT;
错误示范:
java复制// 锁整个订单系统
lock.lock("order_system");
正确做法:
java复制// 只锁特定订单
lock.lock("order:" + orderId);
Redis锁续期:
java复制// Redisson自动处理
RLock lock = redisson.getLock("lock");
lock.lock(30, TimeUnit.SECONDS); // 看门狗自动续期
ZK锁续期:
java复制// 通过会话心跳维持
CuratorFramework client = CuratorFrameworkFactory.newClient(...);
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
Redis跨语言:
ZK跨语言:
云原生锁服务:
混合锁策略:
java复制// 先尝试Redis锁,失败降级ZK锁
try {
redisLock.tryLock();
} catch (Exception e) {
zkLock.lock();
}
无锁化设计:
在实际项目中选择分布式锁时,需要综合考虑团队技术栈、业务场景和运维能力。我曾见过一个团队为了追求理论完美而过度设计,最终系统复杂度反而成为故障根源。记住:没有最好的锁,只有最适合的锁。