You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于SpringBoot的电影票务系统座位锁定与解锁实现方案问询

电影票务预订系统座位锁定与解锁方案

一、多线程环境下的座位锁定实现

要解决并发场景下的座位抢占问题,核心是保证座位锁定操作的原子性,避免多个用户同时锁定同一批座位。推荐基于Redis的原子操作实现,具体思路如下:

  1. 单座位原子锁定:使用Redis的SET命令带NX(仅不存在时设置)和EX(过期时间)参数,为每个座位生成唯一锁定键(如lock:show:{showId}:seat:{seatId}),值绑定当前预订ID,同时设置超时时间(比如15分钟,对应用户支付窗口期)。
  2. 批量座位原子锁定:若用户选中多个座位,需保证要么全锁成功、要么全失败,避免部分锁定的不一致状态。可通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 09:33:31