在分布式系统中,当多个服务实例需要同时访问共享资源时,如何保证数据一致性是个经典难题。去年我们团队重构点评系统时就遇到了这个问题:秒杀场景下超卖问题频发,库存扣减经常出现负数。这就是典型的分布式环境下的竞态条件问题。
分布式锁的本质是通过一个全局可见的互斥标志,让多个节点对共享资源的访问从并行变为串行。想象一下医院挂号窗口的叫号系统 - 无论有多少患者在等待,系统每次只允许一个人办理业务。分布式锁在电商秒杀、票务系统、支付结算等场景中都是刚需。
最简单的实现方式是使用数据库唯一索引:
sql复制CREATE TABLE `distributed_lock` (
`lock_key` varchar(64) NOT NULL,
`expire_time` datetime NOT NULL,
PRIMARY KEY (`lock_key`)
);
获取锁时执行INSERT,释放锁时删除记录。这种方案虽然实现简单,但存在致命缺陷:没有自动过期机制可能导致死锁,且数据库性能会成为瓶颈。我们在压测时发现QPS超过2000后系统就开始报警。
Redis的SETNX命令是更优的选择:
bash复制SET lock_key unique_value NX PX 30000
这个原子操作能同时解决锁获取和过期时间设置。但要注意几个关键点:
我们在生产环境使用Redisson客户端时,发现其内置的看门狗机制能自动续期,非常适合长任务场景。
通过创建临时顺序节点可以实现更严谨的锁:
java复制public void lock() {
path = zk.create("/lock/seq-", EPHEMERAL_SEQUENTIAL);
while(!checkMinNode(path)) {
wait();
}
}
Zookeeper的Watcher机制能精准控制锁释放事件,但网络开销较大。实测发现其吞吐量只有Redis方案的1/5,更适合CP场景。
Spring Boot集成Redisson的典型配置:
yaml复制spring:
redis:
host: 192.168.1.100
port: 6379
password: redis123
加锁代码示例:
java复制RLock lock = redissonClient.getLock("orderLock");
try {
if(lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 业务逻辑
reduceStock();
}
} finally {
lock.unlock();
}
我们采用Redis Cluster模式部署,配合如下配置提升可用性:
java复制Config config = new Config();
config.useClusterServers()
.setScanInterval(2000)
.addNodeAddress("redis://node1:6379")
.addNodeAddress("redis://node2:6379");
现象:库存出现超卖,日志显示锁在业务执行期间被释放
原因分析:Redis主从切换导致锁状态不一致
解决方案:
java复制config.setMinimumReplicationSize(2);
案例:凌晨定时任务卡死,监控显示锁持续持有超过2小时
排查步骤:
TTL lock_key对于热点商品秒杀,我们采用库存分段方案:
java复制// 将商品1000个库存分为10段
int segment = itemId.hashCode() % 10;
RLock segmentLock = redisson.getLock("stock_" + segment);
实测QPS从1500提升到8500,关键是不需要额外基础设施投入。
在商品详情页的缓存重建场景,使用读写锁提升并发:
java复制RReadWriteLock rwLock = redisson.getReadWriteLock("cacheLock");
rwLock.readLock().lock(); // 多个读并发
rwLock.writeLock().lock(); // 写独占
对于非关键路径,可以使用异步方式获取锁:
java复制RFuture<Boolean> future = lock.tryLockAsync(5, 10, TimeUnit.SECONDS);
future.onComplete((res, e) -> {
if(res) {
// 获取锁成功
}
});
完善的监控是分布式锁稳定性的保障,我们采用以下方案:
java复制redisson.getMetrics().register(meterRegistry);
Grafana监控看板重点指标:
关键报警规则:
在支付场景下,我们采用Redisson的XA模式:
java复制TransactionOptions options = TransactionOptions.defaults()
.syncSlaves(2, 5, TimeUnit.SECONDS);
Transaction transaction = redisson.createTransaction(options);
当Java服务需要与Python服务互斥时,我们设计了一套通用协议:
service:resource:action{"clientId":"py-service1","timestamp":1630000000}大促前我们通过脚本预热锁:
bash复制for i in {1..100}; do
redis-cli SET "preheat_lock_$i" 1 NX PX 60000
done
这个技巧帮助我们平稳度过了去年双11的流量洪峰。