手机买电影票系统实战:面试必问的并发陷阱

发布时间:2026/9/23 7:07:31
手机买电影票系统实战:面试必问的并发陷阱 手机买电影票系统实战:面试必问的并发陷阱 刚接手劳务班组管理项目,第一周就让我崩溃了。 为了演示“手机买电影票”功能,我花了一整天配环境。 结果跑起来全是报错,面试被问得哑口无言。 这不仅是配置问题,更是底层逻辑没搞清。 今天把这套坑全填平,全是实战经验。 概念速懂:为什么手机买票会卡死 很多人觉得买票就是简单的 SELECT 和 UPDATE。 错了。这是典型的高并发竞态条件问题。 想象一下,热门电影《流浪地球3》开售。 10000人同时点击“确认支付”。 你的数据库如果没做好锁,会出现两种灾难:超卖:座位只剩1个,卖出了5张票。 少卖:库存还有100个,但系统提示已售罄。在劳务班组场景中,这和**“抢工单”**一模一样。 班组负责人要在APP上抢下个月的施工任务。 如果系统响应慢或数据不一致,直接导致收入损失。 面试官问“手机买电影票”,其实是在考你的分布式一致性思维。 这不是前端按钮的事,是后端架构的事。 核心矛盾:高并发下的数据一致性 vs 系统吞吐量。 解决思路只有三条路:数据库悲观锁(简单,性能差) Redis 原子操作(快,需处理过期) 消息队列削峰(复杂,最终一致性)初学者建议从Redis预扣减入手。 这也是目前90%电商系统的标准做法。 环境准备:别再卡在依赖冲突 别再问我 npm install 报错了。 那是前端的事,我们今天讲后端核心。 但环境不对,代码写得再漂亮也是白搭。 1. 基础栈选择 为了演示清晰,我们使用最通用的技术栈:语言:Java 17 (LTS版本,稳定) 框架:Spring Boot 3.0+ 缓存:Redis 7.0 (必须) 数据库:MySQL 8.02. 关键依赖配置 pom.xml 里这几个包缺一不可。 特别注意版本兼容性,这是新手翻车重灾区。 dependencies!-- Spring Boot Web --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- Spring Data Redis --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency!-- Lettuce 连接池 (默认) --dependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependency!-- MySQL Driver --dependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactIdscoperuntime/scope/dependency /dependencies3. 配置文件避坑 application.yml 中,Redis 连接超时时间必须调短。 默认配置在弱网环境下会卡死线程。 spring:redis:host: 127.0.0.1port: 6379timeout: 200ms # 关键:快速失败,别傻等lettuce:pool:max-active: 200max-idle: 50min-idle: 10max-wait: 100msdatasource:url: jdbc:mysql://localhost:3306/movie_ticket?useSSL=falseserverTimezone=UTCusername: rootpassword: 123456hikari:maximum-pool-size: 20connection-timeout: 3000重点:max-wait 设为 100ms。 如果获取连接超过 100ms,直接抛异常。 这是保护系统不雪崩的第一道防线。 在掘金技术社区看过的架构分享都强调:快速失败是稳定性的基石。 核心语法:Lua脚本才是王道 很多新手喜欢用 DECR 命令扣减库存。 这是错的。 DECR 是原子操作,但判断库存是否充足不是原子的。 如果你先 GET 判断 0,再 DECR,中间就有时间差。 高并发下,这个时间差就是超卖的根源。 正确做法:Lua 脚本。 Redis 执行 Lua 脚本是原子性的。 整个脚本执行期间,其他命令无法插入。 1. 扣减库存脚本 将以下脚本保存为 decr_stock.lua。 -- key: 电影场次ID, 如 stock:movie_001 local key = KEYS[1] local stock = tonumber(redis.call('get', key))-- 如果库存不存在,返回 -1 if stock == nil thenreturn -1 end-- 如果库存充足,扣减并返回新库存 if stock 0 thenredis.call('decr', key)return stock - 1 elsereturn 0 end2. Java 端调用 Spring Boot 3.0 中,使用 RedisScript 接口。 import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Service; import org.springframework.core.io.ClassPathResource;import java.util.Collections;@Service public class TicketService {private final StringRedisTemplate redisTemplate;private final RedisScriptLong decrStockScript;public TicketService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;// 加载 Lua 脚本this.decrStockScript = new DefaultRedisScript(new ClassPathResource(lua/decr_stock.lua).toString(), Long.class);}/*** 尝试扣减库存* @param movieId 电影ID* @return 是否扣减成功*/public boolean tryDecrStock(String movieId) {String key = stock: + movieId;try {Long result = redisTemplate.execute(decrStockScript,Collections.singletonList(key));// 返回 -1 表示 key 不存在// 返回 0 表示库存不足// 返回 0 表示扣减成功return result != null result 0;} catch (Exception e) {// 记录日志,但不抛出异常,返回 falselog.error(Redis 扣减库存异常: + movieId, e);return false;}} }关键行解释:ClassPathResource:从类路径加载 Lua 文件。 Collections.singletonList:Lua 脚本需要 Key 列表参数。 try-catch:Redis 可能宕机,必须捕获异常,防止线程池被占满。完整代码示例:从点击到出票 光扣减库存不够,还要落库。 如果扣减成功,但写数据库失败,用户钱扣了,票没了。 这叫数据不一致。 解决方案:本地消息表 或 事务消息。 为了演示简单,我们用**“Redis扣减 + 异步落库”**模式。 1. 控制器入口 @RestController @RequestMapping(/api/ticket) public class TicketController {@Autowiredprivate TicketService ticketService;@Autowiredprivate OrderService orderService;@PostMapping(/buy)public ResultString buyTicket(@RequestBody BuyTicketRequest req) {// 1. 参数校验if (req.getMovieId() == null || req.getUserId() == null) {return Result.fail(参数错误);}// 2. 尝试扣减 Redis 库存boolean success = ticketService.tryDecrStock(req.getMovieId());if (!success) {return Result.fail(手慢了,票卖光了);}try {// 3. 异步创建订单 (关键点)orderService.createOrderAsync(req);// 4. 返回成功 (实际是“受理成功”,非“出票成功”)return Result.success(订单创建中,请刷新查看);} catch (Exception e) {// 5. 业务异常,回滚 Redis 库存ticketService.rollbackStock(req.getMovieId());return Result.fail(系统繁忙,请重试);}} }2. 异步落库服务 使用 Spring 的 @Async 注解,避免阻塞主线程。 @Service public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate RedisTemplateString, String redisTemplate;@Async(taskExecutor) // 需要配置线程池@Transactional // 开启事务public void createOrderAsync(BuyTicketRequest req) {try {// 1. 创建订单记录 (状态: 待支付)Order order = new Order();order.setUserId(req.getUserId());order.setMovieId(req.getMovieId());order.setStatus(OrderStatus.PENDING);orderMapper.insert(order);// 2. 记录操作日志 (用于对账)log.info(订单创建成功: {}, order.getId());} catch (Exception e) {// 数据库写入失败log.error(订单落库失败, e);// 注意:这里不能直接回滚 Redis// 需要人工介入或延迟队列补偿// 简单场景下,可以抛出异常让上层回滚throw new RuntimeException(DB Error, e);}} }避坑指南:@Async 必须配合线程池使用,否则默认用 SimpleAsyncTaskExecutor,不可控。 @Transactional 在异步方法中可能失效,需确保代理生效。常见报错:这些坑我全踩过 1. Redis 连接池耗尽 现象:Connection reset 或 GetConnectionTimeoutException。 原因:高并发下,所有连接都被占用,新请求等待超时。 解决:调大 max-active 连接数。 检查是否有长事务占用连接。 核心:确保 Redis 操作在毫秒级完成,不要做复杂计算。2. Lua 脚本执行超时 现象:SCRIPT ERROR 或响应慢。 原因:脚本逻辑复杂,或 Key 热点导致单线程阻塞。 解决:简化 Lua 脚本逻辑。 如果 Key 是热点(如爆款电影),考虑分桶。将 stock:movie_001 拆分为 stock:movie_001_1 到 stock:movie_001_10。 请求随机访问其中一个桶。 这样可以将单点压力分散到 10 个 Key 上。3. 订单重复创建 现象:用户点击一次,生成多个订单。 原因:前端重试机制 + 后端幂等性缺失。 解决:前端:点击后禁用按钮,显示 Loading。 后端:幂等性 Token。用户进入页面时,生成唯一 token 存入 Session/Redis。 提交订单时,必须携带 token。 后端先删除 token,删除成功才处理请求。// 幂等性校验示例 public boolean checkIdempotent(String token) {String key = idempotent: + token;// 原子操作:删除 keyBoolean deleted = redisTemplate.delete(key);return Boolean.TRUE.equals(deleted); }小结:从买票到职业发展 讲完代码,聊聊职业。 手机买电影票 这个场景,在面试中属于**“中等难度”**。 它能考察你对并发、缓存、数据库的综合理解。 但对于劳务班组负责人或初级后端来说,这还不够。 真正的挑战在于:如何监控库存准确性?(对账系统) 如何处理 Redis 宕机?(降级到数据库,限流) 如何防止恶意刷单?(风控规则)在劳务行业,这些对应着:工单分配公平性:算法是否透明? 系统可用性:抢工单时APP崩了,工人会骂娘。 数据安全:工人信息泄露,法律责任巨大。晋升路径:初级:能写出扣减库存的 Lua 脚本。 中级:能设计完整的防超卖方案,包含幂等、对账、降级。 高级:能主导分布式事务架构,处理跨服务数据一致性。你现在的代码,可能只能应付初级面试。 想拿大厂 Offer,必须把**“为什么”**讲清楚。 为什么用 Redis?为什么用 Lua?为什么异步落库? 每一个技术选型,都要有成本收益分析。 你公司项目里是怎么处理的?欢迎评论。 是用了消息队列?还是直接数据库行锁? 或者有没有遇到过更诡异的并发 Bug? 留言区见,我会挑几个典型问题单独拆解。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询