基于SpringBoot的电影票务系统座位锁定与解锁实现方案问询
电影票务预订系统座位锁定与解锁方案
一、多线程环境下的座位锁定实现
要解决并发场景下的座位抢占问题,核心是保证座位锁定操作的原子性,避免多个用户同时锁定同一批座位。推荐基于Redis的原子操作实现,具体思路如下:
- 单座位原子锁定:使用Redis的
SET命令带NX(仅不存在时设置)和EX(过期时间)参数,为每个座位生成唯一锁定键(如lock:show:{showId}:seat:{seatId}),值绑定当前预订ID,同时设置超时时间(比如15分钟,对应用户支付窗口期)。 - 批量座位原子锁定:若用户选中多个座位,需保证要么全锁成功、要么全失败,避免部分锁定的不一致状态。可通过Redis Lua脚本实现批量原子操作,脚本会先检查所有目标座位是否未被锁定,再统一执行锁定。
二、座位解锁的场景与实现
解锁分为两种触发场景:
- 主动解锁:用户取消选座、支付失败时,主动删除Redis中对应座位的锁定键,或更新状态。
- 被动解锁:锁定超时后,Redis自动删除锁定键,座位恢复可用;也可配合定时任务扫描数据库中过期的
Created状态预订,主动解锁对应座位,作为Redis过期机制的补充。
支付成功后,需将座位的永久锁定状态同步到数据库:将Booking的状态更新为Confirmed,同时可在数据库中维护座位与预订的关联关系,确保数据最终一致性。
三、Redis+数据库方案的可行性分析
该方案完全可行,是票务系统的行业常规实践,核心优势如下:
- 高性能:Redis作为内存存储,读写速度远高于数据库,能支撑高并发选座场景,避免数据库瓶颈。
- 原子性保障:Redis的原子命令和Lua脚本可彻底解决并发竞争问题。
- 自动超时释放:Redis的键过期机制无需额外复杂逻辑,即可自动回收用户超时未支付的锁定座位。
- 最终一致性:支付成功后同步数据库状态,若同步失败可通过重试、消息队列等补偿机制确保数据一致。
需注意的细节:
- 要处理Redis与数据库的一致性问题,比如支付成功后,需删除Redis临时锁或标记为永久锁定。
- 用户重复提交选座请求时,需先检查是否已有自身的锁定记录,避免重复操作。
四、代码示例
1. Redis Lua批量锁定脚本
-- 参数:KEYS[1..n] 为座位锁定键集合;ARGV[1]为预订ID;ARGV[2]为过期时间(秒) for i, key in ipairs(KEYS) do -- 检查座位是否已被锁定 if redis.call('EXISTS', key) == 1 then return 0 -- 存在已锁定座位,返回失败 end end -- 所有座位未被锁定,执行批量锁定 for i, key in ipairs(KEYS) do redis.call('SET', key, ARGV[1], 'EX', ARGV[2], 'NX') end return 1 -- 锁定成功
2. Java座位锁定服务实现
import redis.clients.jedis.Jedis; import java.util.ArrayList; import java.util.List; public class SeatLockService { private static final String LOCK_SCRIPT = "for i, key in ipairs(KEYS) do if redis.call('EXISTS', key) == 1 then return 0 end end for i, key in ipairs(KEYS) do redis.call('SET', key, ARGV[1], 'EX', ARGV[2], 'NX') end return 1"; private final Jedis jedis; private static final int LOCK_EXPIRE_SECONDS = 900; // 15分钟超时 public SeatLockService(Jedis jedis) { this.jedis = jedis; } // 批量锁定座位 public boolean lockSeats(String bookingId, String showId, List<String> seatIds) { List<String> keys = new ArrayList<>(); for (String seatId : seatIds) { keys.add(String.format("lock:show:%s:seat:%s", showId, seatId)); } Object result = jedis.eval(LOCK_SCRIPT, keys, List.of(bookingId, String.valueOf(LOCK_EXPIRE_SECONDS))); return result.equals(1L); } // 批量解锁座位 public void unlockSeats(String showId, List<String> seatIds) { List<String> keys = new ArrayList<>(); for (String seatId : seatIds) { keys.add(String.format("lock:show:%s:seat:%s", showId, seatId)); } jedis.del(keys.toArray(new String[0])); } // 检查单个座位是否被锁定 public boolean isSeatLocked(String showId, String seatId) { String key = String.format("lock:show:%s:seat:%s", showId, seatId); return jedis.exists(key); } }
3. 支付成功后的状态同步逻辑
public class BookingService { private final SeatLockService seatLockService; private final BookingRepository bookingRepository; public BookingService(SeatLockService seatLockService, BookingRepository bookingRepository) { this.seatLockService = seatLockService; this.bookingRepository = bookingRepository; } public boolean confirmBooking(String bookingId) { Booking booking = bookingRepository.findById(bookingId); if (booking == null || booking.getBookingStatus() != BookingStatus.Created) { return false; } // 更新数据库预订状态为已确认 booking.setBookingStatus(BookingStatus.Confirmed); bookingRepository.save(booking); // 解锁Redis中的临时锁定(或标记为永久锁定) List<String> seatIds = booking.getSeatsBooked().stream().map(Seat::getId).toList(); seatLockService.unlockSeats(booking.getShow().getId(), seatIds); return true; } }
4. 过期预订清理定时任务
import java.util.Date; import java.util.List; public class ExpiredBookingCleanupTask { private final BookingRepository bookingRepository; private final SeatLockService seatLockService; private static final int BOOKING_EXPIRE_MINUTES = 15; public ExpiredBookingCleanupTask(BookingRepository bookingRepository, SeatLockService seatLockService) { this.bookingRepository = bookingRepository; this.seatLockService = seatLockService; } public void run() { Date expireTime = new Date(System.currentTimeMillis() - BOOKING_EXPIRE_MINUTES * 60 * 1000); List<Booking> expiredBookings = bookingRepository.findByBookingStatusAndCreatedTimeBefore(BookingStatus.Created, expireTime); for (Booking booking : expiredBookings) { // 更新预订状态为已过期 booking.setBookingStatus(BookingStatus.Expired); bookingRepository.save(booking); // 解锁对应座位 List<String> seatIds = booking.getSeatsBooked().stream().map(Seat::getId).toList(); seatLockService.unlockSeats(booking.getShow().getId(), seatIds); } } }
内容的提问来源于stack exchange,提问作者user17312833
相关产品推荐
相关产品推荐

