黑马点评 秒杀业务「一人一单」并发安全
在秒杀优惠券的业务中,我们需要保证同一个用户只能下单一次,避免出现一人多单、库存超卖的情况。在高并发与集群环境下,这个问题会被无限放大,因此需要从单机锁逐步优化到分布式锁。订单服务接口(IVoucherOrderService.java)该接口定义了秒杀优惠券的核心方法,是业务的入口。public interface IVoucherOrderService extends IServiceVoucherOrder { /** * 秒杀优惠券 * @param voucherId 优惠券ID * @return 订单编号 / 错误提示 */ Result seckillVoucher(Long voucherId); }订单服务实现类(单机锁版本)最开始我们使用synchronized保证单机环境下的并发安全,只允许同一个用户串行执行。@Service public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService { // 优惠券服务,用于扣减库存 @Resource private IVoucherService voucherService; // Redis全局唯一ID生成器,用于生成订单号 @Resource private RedisIdWorker redisIdWorker; /** * 秒杀入口方法(单机锁版本) */ @Override public Result seckillVoucher(Long voucherId) { // 获取当前登录的用户ID Long userId = UserHolder.getUser().getId(); // 对用户ID字符串加锁,保证同一用户同时只有一个请求执行 // intern() 确保相同userId使用常量池中的同一个字符串对象,锁才能真正生效 synchronized (userId.toString().intern()) { // 获取当前类的AOP代理对象,事务必须通过代理调用才生效 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } } /** * 真正创建订单的方法(带事务) * 必须被代理对象调用 */ @Transactional public Result createVoucherOrder(Long voucherId) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); // 1. 判断用户是否已经下单(一人一单核心) Integer count = query() .eq("user_id", userId) // 根据用户ID查询 .eq("voucher_id", voucherId) // 根据优惠券ID查询 .count(); // 统计订单数量 // 若数量大于0,说明已经买过了,直接返回失败 if (count 0) { return Result.fail("您已经购买过此优惠券,无法重复购买"); } // 2. 扣减库存(乐观锁机制,只有库存大于0时才扣减) boolean success = voucherService.update() .setSql("stock = stock - 1") // 库存减1 .eq("voucher_id", voucherId) // 匹配对应优惠券 .gt("stock", 0) // 库存必须大于0 .update(); // 扣减失败,说明库存不足 if (!success) { return Result.fail("库存不足,秒杀失败"); } // 3. 创建订单 VoucherOrder order = new VoucherOrder();