网上购电影票系统面试题:从入门到精通的5个核心考点
官方文档太长抓不住重点?别慌。很多应届生做网上购电影票项目,看了一天文档还是懵的,根本分不清哪段代码是核心,哪句注释是废话。今天不熬鸡汤,直接拆解这个经典场景在面试中的高频陷阱。网上购电影票看似简单,实则涵盖了并发控制、数据一致性、状态机等后端核心难点。想要从入门到精通,必须把这些底层逻辑吃透,而不是只会调API。
考点梳理:别把业务当玩具
很多同学在简历上写“实现网上购电影票功能”,面试官一听就皱眉。为什么?因为购票系统最核心的矛盾是超卖和状态一致性。如果不懂这两点,你的系统就是玩具。
面试中常见的错误认知有三个。第一,认为只要加个if判断库存大于0就行。这在单线程下没问题,高并发下直接崩溃。第二,认为数据库事务能解决所有问题。事务只能保证原子性,不能解决性能瓶颈,锁表会导致请求堆积。第三,忽略支付回调的幂等性。用户支付后网络抖动,回调发两次,你的系统扣两次票还是退一次款?
核心考点清单:高并发下的库存扣减:如何防止超卖?
分布式锁的应用:Redis锁还是数据库锁?
状态机设计:订单状态如何流转?
幂等性设计:如何防止重复支付?
数据一致性:最终一致性方案。面试官问网上购电影票,本质是在问分布式系统基础。你答的是业务,他听的是架构能力。
标准答法:结构化输出逻辑
回答这类问题,切忌东一榔头西一棒子。建议采用**“现状-方案-兜底”**三段式。
第一步:描述场景与痛点。
“网上购电影票系统面临高并发秒杀场景,主要痛点是库存超卖和订单状态不一致。传统数据库行锁在高并发下性能急剧下降,无法满足峰值流量。”
第二步:给出核心解决方案。
“我采用了Redis预扣减库存 + 数据库异步落库的方案。预扣减:将库存预热到Redis,利用Lua脚本原子性执行decr和判断,快速拦截无效请求。
异步落库:Redis扣减成功后,发送消息到MQ,消费者异步调用数据库接口创建订单并扣减DB库存。
补偿机制:如果数据库扣减失败,发送重试消息,并回滚Redis库存。”第三步:补充兜底策略。
“为了应对Redis宕机或MQ丢失,我设计了定时对账任务。每小时比对Redis库存与DB库存,差异超过阈值则告警并人工介入。同时,支付回调接口通过唯一订单号保证幂等性,避免重复退款。”
加分项:提及官方源码仓库。
可以提到:“在实现Redis Lua脚本时,我参考了Redis官方源码仓库中t_clue.c关于原子操作的实现逻辑,确保脚本在执行过程中不会被中断,保证了原子性。”这句话能体现你不仅会用,还懂底层。
代码实现:Lua脚本与幂等设计
光说不练假把式。这里给出最核心的Redis Lua脚本和Java幂等处理代码。这是面试必考代码。
1. Redis Lua脚本:原子扣减库存
为什么用Lua?因为Redis执行Lua脚本时是单线程的,整个脚本执行期间不会被其他命令插入,天然具备原子性。
-- 文件: deduct_stock.lua
-- 参数: KEYS[1] = 电影场次库存key, ARGV[1] = 购买数量
local stock_key = KEYS[1]
local buy_count = tonumber(ARGV[1])-- 1. 检查库存是否存在
if redis.call('exists', stock_key) == 0 thenreturn -1 -- 库存key不存在,返回-1
end-- 2. 获取当前库存
local current_stock = tonumber(redis.call('get', stock_key))-- 3. 判断库存是否充足
if current_stock buy_count thenreturn 0 -- 库存不足,返回0
end-- 4. 扣减库存
redis.call('decrby', stock_key, buy_count)-- 5. 返回扣减后的剩余库存
return current_stock - buy_count代码解析:原子性:exists、get、decrby在一个脚本内执行,中间不会插入其他线程操作,彻底解决check-then-act竞态条件。
返回值设计:返回剩余库存,方便后续逻辑判断。返回-1表示key异常,0表示库存不足,正数表示成功。2. Java端:幂等性处理
支付回调是最容易出Bug的地方。用户支付成功,微信/支付宝回调通知。如果网络超时,对方会重试。你的接口必须能处理重复请求。
@Service
public class OrderService {@Autowiredprivate RedisTemplateString, String redisTemplate;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate TransactionTemplate transactionTemplate;/*** 处理支付回调* @param orderNo 订单号* @param payAmount 支付金额* @return 处理结果*/public boolean handlePayCallback(String orderNo, BigDecimal payAmount) {// 1. 幂等性检查:使用Redis SETNX,有效期1天String idempotentKey = pay:callback: + orderNo;Boolean isProcessed = redisTemplate.opsForValue().setIfAbsent(idempotentKey, 1, 1, TimeUnit.DAYS);if (Boolean.FALSE.equals(isProcessed)) {// 已经处理过,直接返回成功,防止重复退款log.warn(订单 {} 支付回调重复,忽略处理, orderNo);return true;}// 2. 业务处理:开启事务return transactionTemplate.execute(status - {try {// 查询订单,加锁Order order = orderMapper.selectForUpdate(orderNo);if (order == null) {throw new BusinessException(订单不存在);}// 校验状态:只有待支付状态才能转已支付if (order.getStatus() != OrderStatus.PENDING_PAY) {throw new BusinessException(订单状态错误);}// 校验金额if (order.getAmount().compareTo(payAmount) != 0) {throw new BusinessException(支付金额不一致);}// 更新订单状态为已支付order.setStatus(OrderStatus.PAID);order.setPayTime(new Date());orderMapper.update(order);// 扣减数据库真实库存(如果Redis是预扣减,这里做最终一致性校验或扣减)// 注意:实际生产中,DB库存扣减应在创建订单时做,或者通过MQ异步做// 这里假设是最终扣减boolean stockDeducted = cinemaMapper.deductStock(order.getCinemaId(), order.getSeatCount());if (!stockDeducted) {throw new BusinessException(库存扣减失败);}return true;} catch (Exception e) {// 3. 异常处理:删除幂等键,允许重试redisTemplate.delete(idempotentKey);status.setRollbackOnly();throw e;}});}
}代码解析:SETNX幂等:利用Redis的SETNX(Set If Not Exist)特性。第一次请求成功设置,后续重复请求返回false,直接跳过业务逻辑。
事务回滚与幂等键清除:如果业务处理失败(如DB扣库存失败),必须删除Redis中的幂等键。否则,后续重试也会因为键存在而被拦截,导致数据不一致。
悲观锁:selectForUpdate对订单行加锁,防止并发修改同一订单。追问与延伸:进阶技巧与避坑
面试官不会只问基础。以下是三个高频追问,答不好直接挂。
追问1:Redis挂了怎么办?
错误回答:“重启Redis,数据丢了就丢了,从DB同步。”
正确思路:持久化:开启RDB+AOF混合持久化,确保宕机重启后数据尽量恢复。
降级策略:如果Redis集群不可用,网关层直接拒绝新请求,返回“系统繁忙”,保护DB不被打垮。
数据修正:恢复后,运行对账脚本,以DB库存为准,修正Redis库存。追问2:如果MQ消息丢失呢?
错误回答:“MQ不可能丢消息。”
正确思路:生产端:使用同步发送+重试机制,或者事务消息(RocketMQ)。
消费端:手动ACK,处理成功后再确认。失败则重试,超过最大重试次数进入死信队列。
死信处理:人工介入或定时任务扫描死信队列,补偿执行。追问3:为什么不用数据库乐观锁?
对比分析:乐观锁:update set stock = stock - 1 where id = ? and stock 0。优点:无锁,实现简单。
缺点:高并发下失败率高,大量请求重试,DB压力大。Redis+Lua:优点:内存操作,速度极快,能扛住高并发。
缺点:需要处理Redis与DB的一致性,架构复杂度略高。结论:秒杀场景(如电影票开售瞬间),流量极大,必须用Redis削峰。普通场景(如日常购票),DB乐观锁足够,无需过度设计。
记忆口诀:快速回顾核心
为了面试前快速回忆,记住这个口诀:“一预二异三对账,幂等锁表别忘详”。一预:Redis预扣减库存,拦截无效流量。
二异:异步MQ落库,解耦高并发。
三对账:定时任务比对Redis与DB,兜底数据一致性。
幂等:支付回调用SETNX,防重复。
锁表:DB更新加悲观锁,防并发冲突。
别忘详:异常回滚要删幂等键,细节决定成败。晋升与职业发展路径思考:
网上购电影票项目虽然经典,但只是入门。在简历中,不要只写“实现了购票功能”。要写“设计了高并发下的库存扣减方案,通过Redis Lua脚本将QPS从1k提升到50k,并引入MQ异步落库,系统可用性达到99.99%”。
对于应届生,这个项目是敲门砖。对于1-3年工程师,要能讲清楚为什么这么选,而不是怎么实现。面试官考察的是你的技术选型能力和权衡(Trade-off)能力。
关于证书变更与注销流程、证书补办流程,这属于行政或特定行业(如PMP、软考)范畴,与技术面试无直接关联。但在工程类面试中,“流程思维”很重要。比如,你如何设计订单取消流程?如何设计退款流程?这些流程的状态机设计,才是面试官想看的。
确保你的系统状态流转清晰:待支付 - 已支付 - 已出票 - 已退票,每个状态变更都有明确的前置条件和后置操作。
网上购电影票系统看似简单,实则涵盖了分布式系统的核心难题。从入门到精通,关键在于理解并发和保证一致性。不要为了用技术而用技术,要基于业务场景做合理选型。
还有什么不懂的?评论区留言挨个回。
