【快刷面试-高并发锁篇】- 基于票务系统在不同服务器,分布式场景中该如何解决

分布式场景下的锁解决方案
票务系统在多个服务器部署时,单机锁(如Java的synchronized或ReentrantLock)失效的原因很简单:这些锁只在单个JVM进程内有效 ,无法跨服务器协调。三台服务器同时读取i=100并各自执行i--,最终都写入i=99,导致超卖。
分布式锁的核心要求
实现分布式锁必须满足四个条件(互防可高):
- 互斥性:同一时刻只有一个客户端能持有锁
- 防死锁:锁必须有自动超时释放机制,防止客户端崩溃导致永久阻塞
- 可重入性(可选):同一个客户端可多次获取同一把锁
- 高可用:锁服务不能是单点故障源
主流解决方案对比
| 方案 | 实现原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 数据库悲观锁 | SELECT ... FOR UPDATE |
简单,无需额外组件 | 性能差,易死锁,无法优雅处理超时 | 并发量极低(<10 TPS) |
| Redis分布式锁 | SET NX PX + Lua脚本 |
性能极高(万级TPS),实现简洁 | 过期时间难评估,主从切换可能丢锁 | 绝大多数互联网业务 |
| Redisson框架 | Redis + WatchDog机制 | 自动续期,支持可重入,有成熟框架 | 依赖Redis集群稳定性 | 强烈推荐 |
| ZooKeeper临时节点 | 创建临时顺序节点 | 可靠性高,自动释放,无死锁 | 性能较低(千级TPS),需维护ZK集群 | 金融级强一致场景 |
| Etcd租约机制 | Lease + Revision | 强一致性,TTL自动过期 | 运维复杂,社区较小 | Kubernetes生态 |
实战推荐:Redisson分布式锁
对于票务系统这类高并发、性能敏感 的场景,Redisson是最佳实践:
java
// 1. 引入依赖
// Maven: <artifactId>redisson-spring-boot-starter</artifactId>
// 2. 配置Redis集群
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7001", "redis://127.0.0.1:7002");
return Redisson.create(config);
}
}
// 3. 业务代码改造
@Service
public class TicketService {
@Autowired
private RedissonClient redissonClient;
public boolean sellTicket(Long ticketId) {
// 锁的粒度要细,用ticketId作Key
RLock lock = redissonClient.getLock("ticket_lock:" + ticketId);
try {
// 尝试加锁,最多等待5秒,锁自动释放30秒
// WatchDog会自动续期(默认每10秒续一次)
boolean isLocked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!isLocked) {
throw new RuntimeException("系统繁忙,请重试");
}
// 执行业务:查询库存 → 减库存
Ticket ticket = ticketMapper.selectById(ticketId);
if (ticket.getStock() > 0) {
ticket.setStock(ticket.getStock() - 1);
ticketMapper.updateById(ticket);
return true;
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
// 必须释放锁,且只能释放自己持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
关键设计要点
- 锁粒度 :按
ticketId加锁,避免全局锁阻塞所有票种 - Key命名 :
业务:模块:资源ID,如ticket:stock:12345 - 防误删:Redisson的锁实例会存储线程ID,防止A线程释放B线程的锁
- 超时时间:必须设置,且要远大于业务执行时间(WatchDog会续期)
- 锁重试 :
tryLock支持等待时间,避免立即失败
为什么不能只用数据库?
sql
-- 悲观锁方案(不推荐)
BEGIN;
SELECT stock FROM ticket WHERE id = 123 FOR UPDATE; -- 阻塞其他事务
UPDATE ticket SET stock = stock - 1 WHERE id = 123;
COMMIT;
问题 :性能极差,TPS通常<50,且长事务会拖垮数据库。Redis方案性能是其100倍以上。
架构演进建议
如果你们的系统已经用MySQL,最小成本接入Redisson:
- 部署一个Redis集群(3主3从)
- 引入
redisson-spring-boot-starter - 改造核心库存接口
后续如果库存争抢极度激烈(如春运抢票),可升级为分段锁 或消息队列异步扣库存,但90%的场景下Redisson分布式锁已完全够用。
总结:分布式锁的本质是把多机并发转为单机排队,Redis+Redisson是目前工业界最成熟的方案,没有之一。
【详解-锁的4大条件】
我用春运抢票这一个完整故事,把四个条件串起来给你讲透:
故事背景
2025年春运,10万用户抢最后1000张火车票,系统部署在5台服务器上,库存服务共用MySQL。
条件1:互斥性(同一时间只有1个人能抢到最后票)
场景:用户张三和李四同时看到"最后1张票"。
- 无锁灾难 :两台服务器同时执行
SELECT stock=1→ 都判断有票 → 都执行stock-1→ 库存变成-1,超卖1张,两人付款成功,系统崩溃。 - 分布式锁介入 :Redisson在Redis中创建
ticket_lock:123键,只有一台服务器能SET成功,另一台等待。张三的请求先拿到锁,李四的请求阻塞等待。
口诀:锁就是"独木桥",一次只能过一人。
条件2:防死锁(服务器跪了,锁必须自动释放)
场景 :服务器A拿到锁后,刚要扣减库存,突然JVM OOM宕机了。
- 死锁灾难 :锁
ticket_lock:123永远留在Redis,其他服务器都以为"有人持锁",全部阻塞。票卖不出,系统卡死。 - Redisson方案 :加锁时设置30秒自动过期 + WatchDog每10秒续期。服务器A宕机后,WatchDog线程一起死,30秒后Redis自动删除锁,其他服务器恢复正常抢票。
口诀:锁带"定时炸弹",持有人死了就炸开锁。
条件3:可重入性(同一个用户多次操作同一资源)
场景 :张三付款成功后,系统要更新订单状态 + 发送短信通知,这两个方法都需要确认库存已扣减。
java
// 伪代码展示嵌套调用
public void processPayment(Long ticketId) {
lock.lock(); // 第一次获取锁
try {
deductStock(ticketId); // 扣库存
updateOrderStatus(orderId); // 内部也需要锁
sendSmsNotification(); // 内部也需要锁
} finally {
lock.unlock(); // 释放锁
}
}
- 不可重入灾难 :
updateOrderStatus内部再次尝试获取同一把锁,发现自己被"自己"阻塞,永远卡死。 - Redisson方案 :锁记录线程ID 和重入次数 。同线程可重复获取,每次
unlock()计数减1,计数到0才真正释放。
口诀:自己家的钥匙,可以反复开门,出门时关几次门才算真正锁上。
条件4:高可用(锁服务不能是单机)
场景 :春运高峰期,Redis主节点扛不住10万并发,突然挂了。
- 单点故障灾难:所有锁请求失败,抢票系统全面瘫痪,客服电话被打爆。
- Redisson方案 :部署3主3从Redis Cluster ,锁数据分片存储。主节点挂掉,从节点自动提升,客户端无感知切换。配合哨兵模式,故障恢复<10秒。
口诀:锁服务要像"双11客服",一个倒下,另一个立刻顶上。
四条件串联记忆模板(快刷用)
故事线:春运抢票 → 服务器宕机 → 嵌套调用 → Redis集群故障
互斥性:10万人抢1张票,锁保证只有1人成功
防死锁:服务器A拿到锁后OOM,30秒自动释放
可重入性:张三付款流程内,订单/短信方法复用同一把锁
高可用:Redis主节点挂了,从节点秒级接管
技术映射:
互斥 → SET NX原子命令
防死锁 → PX过期时间 + WatchDog续期
可重入 → Hash结构记录threadId + count
高可用 → Redis Cluster / Sentinel
一句话总结 :分布式锁就是给"跨服务器的临界资源"配一个带自动解锁、可重复进入、集群部署的"智能门卫"。
这个串联故事你可以直接用,面试时讲出来比背八股文生动10倍。
简单购票示例一句话说清楚
一句话版本:
分布式锁确保"最后1张票只能被1人成功购买,若买家服务器崩溃锁30秒自动释放防卡死,同买家付款流程可重复获取锁,锁服务集群部署避免单机故障导致系统瘫痪"。
极简关键词版:
分布式锁通过 互斥抢票、崩溃自愈、同用户可重入、集群高可用 四大机制,确保10万人抢最后1张票时绝不超卖。
【面试版本】
面试高分回答模板(总分总结构)
总起 :分布式锁的本质是解决多机环境下临界资源竞争,以春运抢票系统为例,分布式锁必须满足四个条件才能保障业务正确性:
1. 互斥性( Mutual Exclusion )
定义 :同一时刻,集群中仅有一个节点能持有锁操作库存。
场景 :10万用户抢最后1张票,5台服务器同时读到stock=1。若无互斥,每台都会执行stock-1,导致库存超卖为-4。
实现 :Redis的SET NX原子命令,只有唯一客户端能创建ticket_lock:123键。Redisson通过Lua脚本保证"判断存在+设置值+设置TTL"的原子性,避免SET+EXPIRE非原子导致的死锁风险。
细节 :锁的粒度必须精确到资源ID (ticket_lock:${ticketId}),而非全局锁,否则所有车次串行化,性能崩溃。
2. 防死锁( Deadlock-Free )
定义 :持有锁的节点崩溃时,锁必须自动释放,防止其他节点永久阻塞。
场景:服务器A获取锁后,JVM OOM宕机,锁未释放→所有服务器等待→系统卡死,1000张票无法售卖。
实现 :Redisson采用WatchDog机制:锁默认30秒过期,客户端启动后台线程每10秒续期。若客户端宕机,续期停止,30秒后Redis自动删锁。
对比 :ZooKeeper用临时节点,会话断开自动删除,可靠性更高但性能较差。数据库悲观锁无自动超时,需额外心跳表,实现复杂且低效。
3. 可重入性( Reentrancy )
定义 :同一客户端的同一线程可多次获取已持有的锁,避免自死锁。
场景 :用户下订单时,sellTicket()调用链嵌套updateOrder()→sendSms(),三者均需校验库存。若不可重入,第二次lock()会阻塞自己。
实现 :Redisson使用Hash结构 存储threadId和count。同线程重复获取时count++,每次unlock()时count--,计数归零才真正删除锁。
价值:提升业务封装灵活性,避免方法调用链因锁问题耦合。
4. 高可用( High Availability )
定义 :锁服务不能是单点,必须支持故障自动转移。
场景:Redis单机部署,主节点宕机→所有锁请求失败→春运高峰期系统瘫痪,引发P0级故障。
实现 :Redis Cluster集群(3主3从)+ Sentinel哨兵。锁数据分片存储,主节点故障时从节点秒级提升,Redisson客户端自动切换连接。
权衡 :ZooKeeper通过ZAB协议保证强一致,但写入性能仅为Redis的1/10 。Redis主从异步复制可能丢锁(主宕机从未同步),需根据业务容忍度选型:超卖零容忍 用ZK,性能优先用Redis。
总结升华
四个条件环环相扣:互斥性是目标,防死锁是兜底,可重入是工程友好,高可用是底线 。实际选型中,Redisson + Redis Cluster 是互联网业务标配,兼顾性能与可用性;金融场景可接受性能损耗则选ZooKeeper。回答时可主动提及Redlock算法争议(Redis官网已不推荐),体现技术视野深度。
一句话收尾 :分布式锁的本质是将"多机并行"转为"单机串行",而这四个条件是保证该转换正确、可靠、高效的基石。