news 2026/4/17 18:32:42

仿 12306 售票系统:Spring Boot + Redisson 分布式锁解决“超卖”与“余票缓存一致性”难题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
仿 12306 售票系统:Spring Boot + Redisson 分布式锁解决“超卖”与“余票缓存一致性”难题

😱 前言:一张票引发的血案

在单机应用中,防止超卖很简单,加个synchronized关键字就行。
但在微服务集群下,synchronized只能锁住当前机器的线程,锁不住由于负载均衡分发到其他机器的请求。

超卖场景还原:
库存剩 1 张。

  1. 服务器 A的线程读库存:stock = 1
  2. 服务器 B的线程同时读库存:stock = 1
  3. A 卖出:stock = 0
  4. B 卖出:stock = 0
    结果:卖出了 2 张票,实际上只有 1 张。这在铁路系统里意味着有人要站票或者上不了车,属于 P0 级事故。

我们需要一把**“分布式锁”**。


🔒 一、 为什么选 Redisson?

Redis 自带的setnx虽然能实现锁,但有巨大缺陷:

  1. 死锁风险:如果服务宕机,锁没释放怎么办?(需要加过期时间)
  2. 过期时间难定:业务执行了 10s,锁 5s 就过期了,导致锁失效(锁误删)。
  3. 不可重入:复杂的业务逻辑调用链无法多次拿锁。

Redisson是 Redis 的 Java 驻内存数据网格,它完美解决了上述问题,特别是它的**“看门狗(Watch Dog)”**机制。

Redisson 锁流程图 (Mermaid):

Redisson 内部机制

1. tryLock()

加锁成功

开启后台线程

每 10s 续期

业务结束

停止看门狗

加锁失败

客户端请求

Redis Master

执行业务逻辑

🐶 看门狗 (Watch Dog)

重置锁过期时间 (默认 30s)

unlock() 释放锁

自旋等待 / 放弃


💻 二、 实战:Redisson 锁住高铁票

1. 引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.0</version></dependency>
2. 抢票核心逻辑 (OrderService)

我们不仅要加锁,还要锁得细粒度。如果锁整个G1024车次,那吞吐量太低。我们应该锁具体座次车厢

@ServicepublicclassTicketService{@AutowiredprivateRedissonClientredissonClient;@AutowiredprivateStringRedisTemplateredisTemplate;publicbooleanbuyTicket(StringtrainNumber,StringseatType){// 关键点:锁的粒度。这里锁住特定车次的特定席别StringlockKey="lock:ticket:"+trainNumber+":"+seatType;RLocklock=redissonClient.getLock(lockKey);try{// 1. 尝试获取锁// waitTime: 等待获取锁的时间,leaseTime: -1 表示开启看门狗自动续期booleanisLocked=lock.tryLock(5,-1,TimeUnit.SECONDS);if(isLocked){// 2. 双重检查 (Double Check) - 防止拿到锁之前库存被扣光// 这里不仅要查 Redis,最稳妥是查数据库或预加载的缓存intstock=getStockFromCache(trainNumber,seatType);if(stock>0){// 3. 扣减库存 (操作数据库 + 更新缓存)decreaseStock(trainNumber,seatType);createOrder();returntrue;}else{returnfalse;// 没票了}}else{returnfalse;// 系统繁忙 (获取锁失败)}}catch(InterruptedExceptione){returnfalse;}finally{// 4. 释放锁 (必须放在 finally 中)if(lock.isHeldByCurrentThread()){lock.unlock();}}}}

🔄 三、 难点攻克:余票缓存一致性

抢票时,用户疯狂刷新查看余票。如果每次都查数据库,数据库必死。我们必须查 Redis。
但这就引出了经典问题:数据库扣减了库存,Redis 里的缓存还没更新,怎么办?

在 12306 这种场景下,我们通常采用Cache-Aside Pattern (旁路缓存模式)的变种,并配合Lua 脚本

方案 A:先更库,再删缓存 (延时双删)

这是通用方案,但在极端高并发下依然有脏数据风险。

方案 B:Redis 预扣减 (12306 推荐)

真正的余票其实是以Redis 为准的。

  1. 初始化:将数据库库存预热到 Redis。
  2. 扣减:直接在 Redis 中扣减 (decr)。
  3. 异步同步:通过 MQ 异步将扣减结果同步回 MySQL,做最终持久化。

Redis Lua 脚本实现 (保证原子性):

-- keys[1]: 库存 key-- argv[1]: 扣减数量localstock=tonumber(redis.call('get',KEYS[1]))if(stock==nil)thenreturn-1endif(stock>=tonumber(ARGV[1]))thenredis.call('decrby',KEYS[1],tonumber(ARGV[1]))return1elsereturn0end

Java 调用:

// 这样就不需要 Redisson 锁住“读”操作,只需要锁住“写”操作// 或者完全依赖 Redis 单线程特性,连分布式锁都可以省去(针对纯扣减逻辑)Longresult=redisTemplate.execute(script,Collections.singletonList(key),"1");if(result==1){// Redis 扣减成功,发送 MQ 消息去异步更新 MySQLsendToMQ(orderInfo);}

🚀 四、 性能优化:分段锁 (Segment Lock)

如果 G1024 次列车只有一把锁,那么全中国想买这趟车的人都要排队。
我们可以借鉴ConcurrentHashMap的思想,将库存分段

假设二等座有 1000 张票:

  • Key1:stock:G1024:second:part1(0-100)
  • Key2:stock:G1024:second:part2(101-200)

用户请求进来时,随机路由到一个分段库存 Key 上。

  • 如果 Key1 有票,直接扣。
  • 如果 Key1 没票,尝试去 Key2 扣。

这样,并发度瞬间提升了 10 倍!


🎯 总结

开发一个简易版的 12306,核心就在于对“共享资源”(库存)的争抢控制。

  1. Redisson 看门狗:解决了锁过期导致的并发安全问题。
  2. Redis 预扣减 + MQ:解决了数据库的性能瓶颈和缓存一致性问题。
  3. 分段锁:解决了热点商品的单点瓶颈。

Next Step:
思考一下,12306 还有一个极其复杂的逻辑:区间票
比如北京 -> 上海的车,中间经停南京。如果我买北京 -> 南京,那么北京 -> 上海的全程票库存也要减 1。
这涉及到Bitmap (位图)技术。下一篇,我们挑战用 Redis Bitmap 实现区间库存管理!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 14:13:06

提示词语法详解:在SD中正确调用lora-scripts生成的LoRA模型

提示词语法详解&#xff1a;在SD中正确调用lora-scripts生成的LoRA模型 在数字内容创作日益个性化的今天&#xff0c;如何让AI真正“理解”你的风格&#xff0c;成为每一位创作者关心的问题。无论是想复现某位艺术家的笔触、还原某个虚拟角色的形象&#xff0c;还是打造专属品…

作者头像 李华
网站建设 2026/4/15 12:49:10

C++多线程编程避坑宝典(死锁预防的8个黄金法则)

第一章&#xff1a;C多线程死锁问题的根源剖析在C多线程编程中&#xff0c;死锁是导致程序停滞不前的常见问题。其根本原因在于多个线程对共享资源的竞争访问缺乏合理的同步控制&#xff0c;导致彼此相互等待对方释放锁&#xff0c;从而陷入永久阻塞状态。死锁的四大必要条件 互…

作者头像 李华
网站建设 2026/4/15 12:52:14

C++26契约编程新特性:如何利用静态/动态检查提升代码健壮性

第一章&#xff1a;C26契约编程概述C26 引入的契约编程&#xff08;Contract Programming&#xff09;机制旨在提升代码的可靠性与可维护性&#xff0c;通过在函数接口中显式声明前置条件、后置条件和断言&#xff0c;使程序逻辑更加清晰&#xff0c;并为编译器和运行时系统提供…

作者头像 李华
网站建设 2026/4/15 14:33:29

C++内核优化实战案例:一个循环优化让系统吞吐量提升7倍

第一章&#xff1a;C内核性能优化的挑战与机遇在现代高性能计算、实时系统和资源受限环境中&#xff0c;C 内核的性能优化成为决定系统成败的关键因素。尽管 C 提供了对硬件的精细控制和高效的执行能力&#xff0c;但充分发挥其潜力仍面临诸多挑战&#xff0c;同时也蕴藏着巨大…

作者头像 李华
网站建设 2026/4/15 14:34:08

【C++26任务队列深度解析】:揭秘新标准中队列大小控制的5大核心机制

第一章&#xff1a;C26任务队列大小控制的演进与意义随着并发编程在现代软件系统中的广泛应用&#xff0c;任务调度机制的可控性与稳定性成为关键设计考量。C26标准在并发设施方面引入了对任务队列大小的显式控制机制&#xff0c;标志着标准库在线程池与异步执行模型上的进一步…

作者头像 李华
网站建设 2026/4/15 14:35:44

C++26反射即将上线:5个代码示例带你提前掌握未来标准

第一章&#xff1a;C26反射特性概览C26 正在为现代 C 引入原生反射支持&#xff0c;这标志着语言在元编程能力上的重大飞跃。通过编译时反射&#xff0c;开发者能够直接查询和操作类型、变量、函数等程序结构的信息&#xff0c;而无需依赖宏或复杂的模板技巧。核心目标与设计原…

作者头像 李华