秒杀系统超卖记录
Service流程:
1、检查商品余量大于零
2、检查是否存在重复下单;
3、执行减库存。
加了事务不加锁:
@Transactional public Excution executeSeckill(long seckillId, String md5, long userPhone) throws SeckillClosedException, SeckillRepeatException, SeckillException { //... Seckill seckill = seckillDao.queryById(seckillId); if (seckill != null && seckill.getNumber() > 0) { int count = seckilledDao.insertSeckilled(seckillId, userPhone); if (count > 0) { seckillDao.reduceNumber(seckillId, nowTime); Seckilled seckilled = seckilledDao.querySeckilled(seckillId, userPhone); return new Excution(seckillId, SeckillStatEnum.SUCCESS, seckilled); } else { throw new SeckillRepeatException("Seckill Repeat"); } } else { throw new SeckillClosedException("Seckill Closed"); } //... }
超卖
原因:
事务外加锁(结合spring AOP)
主要逻辑部分
@Override @ServiceLock @Transactional public Excution executeSeckillLock(long seckillId, String md5, long userPhone) throws SeckillClosedException, SeckillRepeatException, SeckillException { try { //... Seckill seckill = seckillDao.queryById(seckillId); if (seckill != null)System.out.println(seckill.getNumber()); if (seckill != null && seckill.getNumber() > 0) { int count = seckilledDao.insertSeckilled(seckillId, userPhone); if (count > 0) { seckillDao.reduceNumber(seckillId, nowTime); Seckilled seckilled = seckilledDao.querySeckilled(seckillId, userPhone); return new Excution(seckillId, SeckillStatEnum.SUCCESS, seckilled); } else { throw new SeckillRepeatException("Seckill Repeat"); } } else { throw new SeckillClosedException("Seckill Closed"); } } catch(Exception e) { throw new SeckillException(); } }
利用切面加锁
@Component @Scope @Aspect @Order public class ServiceLockAspect { private static Lock lock = new ReentrantLock(true); @Pointcut("@annotation(com.***.seckill.aop.annotation.ServiceLock)") public void pointCut(){} @Around("pointCut()") public Object addLock(ProceedingJoinPoint joinPoint) { lock.lock(); Object obj = null; try { obj = joinPoint.proceed(); } catch(Throwable e){ throw new SeckillException("inner error"); } finally { lock.unlock(); } return obj; } }
锁注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ServiceLock {}
不会产生超卖:
事务内加锁
@Override @Transactional public Excution executeSeckillLock(long seckillId, String md5, long userPhone) throws SeckillClosedException, SeckillRepeatException, SeckillException { try { lock.lock(); //... Seckill seckill = seckillDao.queryById(seckillId); if (seckill != null)System.out.println(seckill.getNumber()); if (seckill != null && seckill.getNumber() > 0) { int count = seckilledDao.insertSeckilled(seckillId, userPhone); if (count > 0) { seckillDao.reduceNumber(seckillId, nowTime); Seckilled seckilled = seckilledDao.querySeckilled(seckillId, userPhone); return new Excution(seckillId, SeckillStatEnum.SUCCESS, seckilled); } else { throw new SeckillRepeatException("Seckill Repeat"); } } else { throw new SeckillClosedException("Seckill Closed"); } } catch(Exception e) { throw new SeckillException(); } finally{ lock.unlock(); } }
超卖
原因:
另外一些做法是在DAO层改进
- 判断余量和减库存合并
UPDATE seckill SET number=number-1 WHERE seckill_id=#{seckillId} AND number>0
- 将判断库存的快照读改成当前读
SELECT number FROM seckill WHERE seckill_id=#{seckillId} FOR UPDATE
使用队列将发送请求和执行减库存的操作异步化
- 前端发送请求依次进入阻塞队列,后端将请求入队后就返回,由于只进行请求入队操作,所以速度很快;
- 服务开启的同时开一个后台消费线程用于消费队列里的请求(减库存),由于消费操作是单线程,不用加锁,不会有超卖问题。