1. 分布式锁的核心价值与挑战
在分布式系统中,当多个服务实例需要访问共享资源时,如何保证操作的原子性成为一个关键问题。想象一下电商系统中的库存扣减场景:如果两个订单同时触发库存变更,没有互斥机制就会导致超卖。这就是分布式锁要解决的核心问题——在分布式环境下实现跨进程的互斥访问。
分布式锁与单机锁的最大区别在于其面临的挑战:
- 网络不可靠:锁服务与客户端之间的网络可能延迟或中断
- 时钟不同步:不同节点间可能存在时钟漂移
- 资源竞争:高并发场景下锁的获取可能成为性能瓶颈
- 死锁风险:客户端崩溃可能导致锁无法释放
2. 基于数据库的实现方案
2.1 唯一索引方案
这是最简单的实现方式,通过数据库的唯一键约束来实现互斥。我们创建一个lock表:
sql复制CREATE TABLE distributed_lock (
id INT PRIMARY KEY AUTO_INCREMENT,
lock_name VARCHAR(64) UNIQUE,
owner VARCHAR(128),
expire_time DATETIME
);
获取锁的SQL:
sql复制INSERT INTO distributed_lock(lock_name, owner, expire_time)
VALUES ('order_lock', 'service_1', NOW() + INTERVAL 30 SECOND);
释放锁:
sql复制DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND owner = 'service_1';
关键点:expire_time字段用于防止死锁,需要配合定时任务清理过期锁
2.2 乐观锁方案
适用于冲突较少的场景,通过版本号机制实现:
sql复制UPDATE inventory SET stock = stock - 1, version = version + 1
WHERE product_id = 1001 AND version = 5;
实际应用中需要配合重试机制:
java复制int retry = 3;
while(retry-- > 0) {
int version = getCurrentVersion(productId);
int rows = updateWithVersion(productId, version);
if(rows > 0) break;
Thread.sleep(100);
}
3. 基于Redis的实现方案
3.1 SETNX基础实现
Redis的SETNX命令天然适合实现分布式锁:
bash复制SETNX lock:order 1 # 获取锁
EXPIRE lock:order 30 # 设置过期时间
但这种方式存在原子性问题,改进方案是使用SET命令扩展参数:
bash复制SET lock:order <unique_value> NX PX 30000
释放锁时需要验证value值,避免误删:
lua复制if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
3.2 RedLock算法
Redis官方推荐的分布式锁算法,核心流程:
- 获取当前毫秒级时间戳
- 依次向N个独立的Redis实例申请锁
- 当在大多数(N/2+1)节点上获取成功,且总耗时小于锁有效期时才算成功
- 锁的实际有效时间 = 初始有效时间 - 获取锁总耗时
Java实现示例:
java复制public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
long start = System.currentTimeMillis();
int successCount = 0;
for (RedisNode node : redisNodes) {
if (tryAcquire(node, leaseTime)) {
successCount++;
}
if (successCount >= quorum &&
System.currentTimeMillis() - start < waitTime) {
return true;
}
}
// 获取失败要释放已获得的锁
unlock();
return false;
}
4. 基于ZooKeeper的实现方案
4.1 临时顺序节点方案
ZooKeeper通过临时节点和Watcher机制提供分布式锁支持:
java复制public void lock() throws Exception {
// 创建临时顺序节点
String lockPath = zk.create("/locks/order_",
null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点并排序
List<String> children = zk.getChildren("/locks", false);
Collections.sort(children);
// 判断自己是否是最小节点
if (lockPath.endsWith(children.get(0))) {
return; // 获取锁成功
} else {
// 监听前一个节点
String prevNode = children.get(Collections.binarySearch(children,
lockPath.substring(lockPath.lastIndexOf('/') + 1)) - 1);
CountDownLatch latch = new CountDownLatch(1);
zk.exists("/locks/" + prevNode, event -> {
if (event.getType() == EventType.NodeDeleted) {
latch.countDown();
}
});
latch.await(); // 阻塞等待
}
}
4.2 Curator框架实现
Apache Curator提供了现成的分布式锁实现:
java复制InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.release();
}
5. 三种方案的对比与选型建议
5.1 性能对比
| 指标 | 数据库 | Redis | ZooKeeper |
|---|---|---|---|
| 吞吐量 | 低(100-1000qps) | 高(10000+qps) | 中(1000-5000qps) |
| 延迟 | 10-100ms | 1-10ms | 10-50ms |
| 可靠性 | 高 | 中(需集群) | 高 |
5.2 适用场景建议
- 数据库锁:适合已有数据库依赖,并发量不高(100qps以下)的业务
- Redis锁:适合高性能要求的短期锁(秒级),需要处理网络分区问题
- ZooKeeper锁:适合需要高可靠性的长期锁(分钟级以上),对性能要求不极端
5.3 避坑指南
- 时钟漂移问题:所有基于超时的方案都可能因时钟不同步导致锁提前释放
- GC停顿风险:JVM的STW可能导致锁超时误判
- 锁重入问题:同一个线程多次获取锁需要特殊处理
- 锁续约机制:长时间操作需要自动延长锁有效期
6. 生产环境最佳实践
6.1 锁的粒度控制
- 太粗:降低系统并发度
- 太细:增加管理复杂度
- 建议:按业务实体ID分片,如
lock:order:{orderId}
6.2 超时时间设置
java复制// 推荐公式
long timeout = avgProcessingTime * 3 + networkLatencyBuffer;
6.3 监控与告警
关键监控指标:
- 锁等待时间
- 锁占用时间
- 获取锁失败率
- 死锁发生次数
6.4 降级方案
当分布式锁服务不可用时,可以:
- 本地锁降级(牺牲强一致性)
- 队列串行化处理
- 熔断业务操作
7. 常见问题排查
7.1 锁无法释放
可能原因:
- 客户端崩溃未执行释放逻辑
- 网络分区导致通信中断
- 锁服务持久化失败
排查步骤:
- 检查锁服务的存活状态
- 验证锁的过期时间设置
- 检查客户端最后活跃时间
7.2 锁被误删
典型现象:
- 客户端A的锁被客户端B释放
- 业务出现数据竞争
解决方案:
- 必须使用唯一token标识锁所有者
- 释放前验证owner信息(Redis的Lua脚本)
7.3 脑裂问题
在Redis集群中可能出现:
text复制Client1 -> Master1: 获取锁成功
网络分区
Master1故障转移 -> Master2: 锁状态丢失
Client2 -> Master2: 也能获取锁
应对措施:
- 使用RedLock多实例部署
- 设置合理的故障转移超时
8. 进阶优化方向
8.1 锁分段技术
将一个大锁拆分为多个小锁:
java复制// 传统方式
synchronized(wholeCache) {
// 操作全部数据
}
// 分段锁
int segment = key.hashCode() % 16;
synchronized(segments[segment]) {
// 只操作对应分段
}
8.2 读写锁分离
java复制// 读锁(共享)
RLock readLock = redisson.getReadWriteLock("order").readLock();
readLock.lock();
try {
// 并发读操作
} finally {
readLock.unlock();
}
// 写锁(排他)
RLock writeLock = redisson.getReadWriteLock("order").writeLock();
writeLock.lock();
try {
// 独占写操作
} finally {
writeLock.unlock();
}
8.3 锁性能优化技巧
- 尝试获取锁时使用异步非阻塞方式
- 实现锁的公平性避免线程饥饿
- 采用层级锁减少竞争范围
- 使用线程本地缓存减少锁争用
在实际项目中,我们最终选择了Redisson实现的Redis分布式锁方案,主要基于以下考虑:
- 团队已有Redis基础设施
- 业务对性能要求高于强一致性
- Redisson提供了完善的看门狗自动续期机制
- 支持可重入、公平锁等高级特性
部署后需要特别注意Redis的内存配置和持久化策略,我们遇到过因内存不足导致锁信息丢失的情况。建议设置maxmemory-policy为volatile-lru,并适当调大过期时间缓冲。