news 2026/5/1 10:29:48

仿 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/29 11:11:50

lora-scripts与其他LoRA训练脚本的功能对比表

LoRA训练工具的进化&#xff1a;从脚本拼接到工程化实践 在生成式AI席卷各行各业的今天&#xff0c;一个现实问题摆在许多开发者和创意工作者面前&#xff1a;如何用有限的资源&#xff0c;让大模型学会“说我们的话”&#xff1f;无论是设计师想让Stable Diffusion理解自己的艺…

作者头像 李华
网站建设 2026/4/28 7:45:40

微PE官网安全提醒:避免误下病毒软件影响lora-scripts开发环境

微PE官网安全提醒&#xff1a;避免误下病毒软件影响LoRA开发环境 在AI模型微调日益平民化的今天&#xff0c;越来越多个人开发者借助LoRA技术定制专属的图像或语言模型。工具链的简化让“训练一个风格化AI”变得像安装普通软件一样简单——只需几条命令、一份配置文件&#xff…

作者头像 李华
网站建设 2026/5/1 9:47:09

C++26并发编程重大升级(std::execution内存模型全曝光)

第一章&#xff1a;C26并发编程的重大变革C26 标准在并发编程领域引入了多项突破性改进&#xff0c;显著提升了开发者编写高效、安全多线程程序的能力。核心变化包括对执行器&#xff08;executor&#xff09;模型的标准化、协作式中断机制的引入&#xff0c;以及更简洁的异步任…

作者头像 李华
网站建设 2026/5/1 7:18:26

Ubuntu系统下Cursor编辑器完全指南 (安装与配置详细教程)

Ubuntu系统下Cursor编辑器完全指南 (安装与配置详细教程) 欢迎来到本教程&#xff01;本文将详细介绍在Ubuntu系统下安装和配置Cursor编辑器的完整步骤。无论你是初学者还是经验丰富的开发者&#xff0c;这篇指南都将帮助你快速上手Cursor编辑器在Linux环境中的使用。Cursor编…

作者头像 李华
网站建设 2026/4/24 5:04:38

MyBatisPlus在AI后台管理系统中的应用:存储lora-scripts训练日志

MyBatisPlus在AI后台管理系统中的应用&#xff1a;存储lora-scripts训练日志 在当前生成式AI快速落地的背景下&#xff0c;越来越多企业开始构建自己的垂直领域微调平台。以LoRA&#xff08;Low-Rank Adaptation&#xff09;为代表的轻量级微调技术&#xff0c;因其对算力要求低…

作者头像 李华
网站建设 2026/4/26 15:00:40

揭秘C++多线程死锁根源:3步精准识别并预防死锁的实战方法

第一章&#xff1a;C多线程死锁的本质与危害在C多线程编程中&#xff0c;死锁是一种严重的运行时错误&#xff0c;它发生在两个或多个线程相互等待对方持有的资源而无法继续执行的情况。死锁的本质源于资源竞争与同步机制的不当使用&#xff0c;通常涉及互斥锁&#xff08;mute…

作者头像 李华