第一次接触分布式锁是在2016年做电商秒杀系统时。当时我们的库存扣减在高峰期经常出现超卖,查看日志发现多个节点同时处理了同一个商品的请求。这个看似简单的并发问题,在分布式环境下却成了棘手的难题。
分布式锁本质上是一种跨进程的互斥机制,用来控制多个服务节点对共享资源的访问。与单机锁不同,它需要解决网络延迟、节点故障等分布式环境特有的问题。想象一下多个仓库管理员同时操作同一本库存台账的场景——没有可靠的锁机制,数据混乱将不可避免。
在任何时刻,锁只能被一个客户端持有。这是锁存在的根本意义。我们曾用Redis的SETNX命令简单实现,直到遇到锁过期但业务未执行完的坑。
锁必须设置过期时间,即使客户端崩溃也能自动释放。但设置多久合适?我们通过统计历史执行时间百分位(P99)来确定,比如订单服务通常需要200ms,那就设置500ms的TTL。
当部分节点宕机时,锁服务仍应可用。这要求采用多节点部署,比如Redis的RedLock算法需要至少3个主节点。不过在实践中,我们更倾向使用ZooKeeper的临时顺序节点方案。
同一个线程多次获取同一把锁不应被阻塞。我们在Java客户端使用ThreadLocal存储持有计数,配合锁的UUID标识实现。注意要在finally块中正确释放锁。
java复制// 典型Redis锁实现
public boolean tryLock(String key, long expireMs) {
String uuid = UUID.randomUUID().toString();
String result = jedis.set(key, uuid, "NX", "PX", expireMs);
return "OK".equals(result);
}
看似简单的几行代码藏着多个陷阱:
lua复制if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
基于临时顺序节点的实现更符合锁的语义:
我们曾用Curator框架的InterProcessMutex,其内置了:
基于唯一索引或版本号的实现:
sql复制-- 乐观锁实现
UPDATE inventory SET stock=stock-1, version=version+1
WHERE product_id=1001 AND version=123;
适合低频场景,但存在连接池耗尽风险。我们曾在促销时因此导致整个数据库雪崩。
某次线上事故中,Redis节点时间不同步导致:
解决方案:
对于长任务,需要定期延长锁有效期。我们开发了看门狗线程:
java复制private void renewLock() {
while (!Thread.interrupted() && isLocked.get()) {
try {
Thread.sleep(lockTimeout / 3);
jedis.expire(lockKey, lockTimeout);
} catch (Exception e) {
log.error("续约失败", e);
break;
}
}
}
简单的while循环重试会导致:
我们最终采用:
错误示范:整个订单系统用一把大锁
优化方案:
某次压测发现锁竞争激烈,实际是:
解决方案:
完善的监控应包括:
code复制distributed_lock_wait_seconds_bucket{le="0.1"} 1234
distributed_lock_hold_seconds_sum 5678
根据我们的经验,给出以下决策路径:
特别提醒:Redis集群切换时可能出现脑裂,此时RedLock也不安全。对于金融级场景,我们会在业务层增加二次确认。
锁丢失:任务未完成锁已过期
误释放:删除其他客户端的锁
锁堆积:大量线程等待同一把锁
GC停顿:STW导致锁过期
网络分区:客户端与锁服务失联
在下一篇文章中,我们将深入剖析分布式锁在Kubernetes环境下的特殊实现,以及如何基于Raft协议构建高可用的锁服务。