
12306铁路客户服务中心手写实现保姆级教程:面试原理避坑指南
面试被问“12306高并发抢票怎么实现”,结果你只能答出“加锁”,面试官脸色当场就变了。别慌,今天这篇保姆级教程,带你从底层原理到代码实现,彻底搞懂这类高并发场景的常见坑。
坑的现象:库存超卖与死锁频发
在模拟12306铁路客户服务中心核心业务时,新手最容易踩的两个坑就是库存超卖和分布式死锁。
现象一:并发测试时,同一趟车次的剩余票量从10张,在100个并发请求后变成了-5张。这就是典型的超卖。
现象二:在多节点部署下,系统突然卡死,线程栈显示大量线程在等待 ReentrantLock,形成环形等待。
很多应届生以为只要加上 synchronized 或者 Redis SETNX 就能解决,结果在压测时直接崩盘。
根本原因:单机锁失效与状态不一致
坑1:单机锁在分布式环境下失效。
12306铁路客户服务中心是典型的多节点集群架构。如果你在应用层使用 Java 的 synchronized 或 ReentrantLock,这些锁只在单台 JVM 内有效。节点A拿到了锁,节点B依然可以并发访问数据库,导致锁形同虚设。
坑2:检查与更新非原子操作。
传统的“先查库存,再扣减”两步操作,在并发下存在时间窗口。线程A查到库存0,还没执行扣减,线程B也查到库存0,两个线程同时扣减,导致超卖。
坑3:Redis与数据库状态不同步。
如果只把库存放在 Redis 里,Redis 挂了或者重启,数据就丢了。如果只放在数据库里,高并发下数据库连接池会被打满。
正确写法对比:原子操作与分布式锁
错误写法:非原子操作 + 单机锁
// 错误示例:典型的竞态条件
public class TicketServiceWrong {private int stock = 10;private final Object lock = new Object();public boolean buyTicket() {synchronized (lock) { // 坑点1:单机锁,多节点无效if (stock 0) { // 坑点2:检查与扣减非原子,且未处理异常回滚stock--;// 假设这里调用数据库更新,如果超时,内存中已扣减,数据库未扣减updateDBStock(stock); return true;}return false;}}
}正确写法:Redis Lua 原子扣减 + 数据库兜底
// 正确示例:Redis Lua 脚本保证原子性
public class TicketServiceCorrect {private final String stockKey = ticket:stock:G1001;private static final String LUA_SCRIPT = local stock = tonumber(redis.call('get', KEYS[1])) +if (stock ~= 0) and (stock = 1) then + return redis.call('decr', KEYS[1]) +else + return -1 +end;public boolean buyTicket(String userId) {// 1. 通过 Redis Lua 脚本原子扣减,保证多节点下的一致性Object result = redisTemplate.execute(new DefaultRedisScript(LUA_SCRIPT, Long.class), Collections.singletonList(stockKey));if (result != null (Long) result 0) {// 2. 异步或同步扣减数据库,并发送MQ消息进行最终一致性补偿try {int rows = dbMapper.decreaseStock(1);if (rows == 0) {// 数据库扣减失败,回滚RedisredisTemplate.opsForValue().increment(stockKey);return false;}return true;} catch (Exception e) {// 异常回滚,防止状态不一致redisTemplate.opsForValue().increment(stockKey);throw new RuntimeException(数据库扣减失败, e);}}return false;}
}核心区别:Lua 脚本:Redis 执行 Lua 脚本是原子的,彻底解决了并发下的竞态条件。
分布式一致性:不再依赖单机锁,而是依赖 Redis 集群的中心化状态。
兜底机制:数据库作为最终数据源,Redis 作为高性能缓存,两者通过事务或补偿机制保持一致。复现与修复代码:压测验证
为了验证上述方案,我们使用 JMeter 或 Gatling 进行压测。
复现步骤:初始化 Redis 库存为 100。
启动 1000 个并发线程,模拟用户抢票。
检查 Redis 库存与数据库库存是否一致。修复后的关键配置:
在 Spring Boot 中,需要配置 Redis 的 Lua 脚本支持:
@Configuration
public class RedisConfig {@Beanpublic DefaultRedisScriptLong stockDeductScript() {DefaultRedisScriptLong script = new DefaultRedisScript();script.setLocation(new ClassPathResource(lua/stock_deduct.lua));script.setResultType(Long.class);return script;}
}Lua 脚本文件 (stock_deduct.lua):
local stock = tonumber(redis.call('get', KEYS[1]))
if (stock ~= 0) and (stock = 1) thenreturn redis.call('decr', KEYS[1])
elsereturn -1
end避坑提醒:不要直接在 Java 代码里写 Lua 字符串,容易出错且难维护。
务必处理 Redis 连接池耗尽的情况,建议配置 lettuce 客户端的超时与重试策略。
数据库扣减建议使用乐观锁:UPDATE ticket SET stock = stock - 1 WHERE train_id = ? AND stock 0。规避建议:架构设计与监控预扣减 + 异步落库:
在极高并发场景下,不要同步等待数据库响应。先通过 Redis 扣减,成功后发送 MQ 消息,由消费者异步落库。这能极大降低数据库压力。限流与熔断:
在网关层使用 Sentinel 或 Hystrix 进行限流,保护后端服务。当 QPS 超过阈值时,直接返回“排队中”,避免雪崩。监控告警:
监控 Redis 的 KEYS 数量、内存使用率,以及数据库的连接池活跃数。一旦库存为负数或连接池满,立即触发告警。缓存穿透保护:
对于不存在的车次,缓存空值(TTL 短一些),防止恶意请求直接打到数据库。数据一致性校验:
定时任务对比 Redis 与数据库的库存,发现不一致时自动修正,并记录日志。关于 NPM/PyPI 官方包的提示:
如果你使用 Python 进行原型开发,建议使用 redis-py 官方包,其 StrictRedis.evalsha 方法能高效执行 Lua 脚本。在 Java 生态中,Spring Data Redis 是最标准的选择,但要注意版本兼容性,Spring Boot 2.x 与 3.x 在 Redis 配置上有差异,建议查阅官方文档。
岗位日常职责边界:
在12306铁路客户服务中心这类核心系统中,开发人员的职责不仅是写业务代码,更包括性能调优、故障排查和监控体系建设。面试中,能清晰说出“如何通过监控发现异常”、“如何设计降级方案”,往往比单纯炫技更重要。
考试科目与题型:
这类岗位的技术面试通常分为三轮:基础轮:Java/Go 基础、数据结构、算法(LeetCode Medium 难度)。
系统设计轮:高并发架构、分布式锁、消息队列、缓存策略。
场景题:模拟真实故障,如“Redis 主节点宕机,如何保证数据不丢失?”与其他岗位证书的区别:
与纯后端开发相比,12306这类高可用系统更看重稳定性和一致性。普通的 CRUD 经验在这里不够用,必须有处理过千万级 QPS 或高并发抢购的经验,或者至少有深入的压测和调优经历。
这个知识点你面试被问过吗?留言说说你当时是怎么答的,或者有没有踩过更深的坑?