电影票预订系统高效方案问询:并发座位冲突处理
你说得太对了——单纯用Java的synchronized这类进程内同步机制,在高并发场景下确实会很快碰到扩展性天花板。一旦请求量上来,单节点的锁会把所有并发请求堵成串行,不仅响应变慢,还没法通过横向扩容来缓解压力。下面是几个更适合高并发场景的可扩展方案,结合实际业务场景给你拆解:
1. 数据库乐观锁(推荐中小规模场景)
乐观锁的核心思路是不提前加锁,而是在提交更新时验证数据是否被修改过,非常适合冲突概率不是极端高的电影票场景(毕竟同一秒抢同一个座位的用户还是少数)。
实现方式很简单:
- 在座位表(比如
seat)里加一个版本号字段version,或者用座位的status状态+更新时间戳。 - 用户发起预订时,先查询座位的当前状态和版本号:
SELECT id, status, version FROM seat WHERE show_id = ? AND seat_no = ?; - 确认座位可用后,更新时带上版本号做校验:
UPDATE seat SET status = 'booked', user_id = ? WHERE id = ? AND version = ? AND status = 'available'; - 最后判断更新影响的行数,如果是0,说明座位已经被别人抢了,返回用户“座位已被预订”。
优点:完全无锁,支持横向扩容,数据库压力小;缺点:冲突频繁时会有较多失败请求,需要前端配合做友好的重试提示。
2. 数据库悲观锁(适合冲突极高的热门场次)
如果是热门电影的首映场,同一座位被几百人同时抢,乐观锁的重试次数会变多,这时候可以用数据库的行级悲观锁:
- 查询座位时直接加排他锁:
SELECT id, status FROM seat WHERE show_id = ? AND seat_no = ? FOR UPDATE; - 拿到锁后判断座位是否可用,再执行更新操作,最后提交事务释放锁。
注意:一定要控制事务的大小,查询和更新要在同一个事务里,而且事务要尽可能短,避免长时间占用锁导致其他请求阻塞。另外,MySQL的InnoDB引擎才支持行级锁,MyISAM是表锁,千万别用。
优点:冲突处理直接,不需要前端重试;缺点:锁粒度是行级,但高并发下还是会有排队,不过比进程内锁好,因为可以跨节点共享锁。
3. 分布式锁(适合分布式集群场景)
如果你的系统是多节点部署的,进程内的synchronized完全没用,因为每个节点的锁是独立的,这时候就得用分布式锁,比如基于Redis实现:
Redis分布式锁实现思路:
- 用户抢座时,先尝试用
SETNX(SET if Not eXists)命令获取锁,锁的key可以设为lock:show:{showId}:seat:{seatNo},过期时间设个10秒左右(防止锁持有者宕机导致锁永远不释放)。 - 拿到锁后,再去数据库查询座位状态,确认可用后执行更新,最后删除锁(注意要判断锁的持有者是自己,避免误删别人的锁,可以用Redis的
DEL配合Lua脚本)。 - 如果拿不到锁,直接返回用户“座位正在被预订,请稍后再试”。
优点:跨节点共享锁,支持大规模集群扩容;缺点:Redis锁可能存在过期时间设置不合理的问题,比如锁过期了但事务还没完成,这时候需要用“锁续约”机制(比如定时任务给锁续期)。
4. 座位预占机制(提升用户体验+降低冲突)
很多主流票务系统都会用预占机制,本质是把“预订”分成两步:预占座位和确认支付:
- 用户选座后,系统先把座位标记为“预占中”,并设置一个过期时间(比如15分钟),这段时间内只有该用户能支付锁定座位。
- 预占可以用Redis来做,把预占的座位信息存在Redis里,过期时间到了自动释放,不用操作数据库。
- 用户支付成功后,再把数据库里的座位状态改成“已预订”;如果超时未支付,自动释放预占座位。
这个机制的好处是:把大部分并发请求拦截在Redis层,不用频繁操作数据库,而且用户体验更好——选座后有充足的时间支付,不用急着抢。
总结一下选型建议:
- 中小规模单节点系统:优先乐观锁,简单易实现,扩展性好。
- 热门场次高冲突场景:悲观锁+预占结合,减少重试。
- 分布式集群系统:分布式锁+预占,配合Redis做快速拦截。
另外,不管用哪种方案,都要在前端做一些防重复提交的处理,比如按钮置灰、请求防抖,从源头减少无效的并发请求~
内容的提问来源于stack exchange,提问作者Chandan




