秒杀系统超卖记录
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
使用队列将发送请求和执行减库存的操作异步化
- 前端发送请求依次进入阻塞队列,后端将请求入队后就返回,由于只进行请求入队操作,所以速度很快;
- 服务开启的同时开一个后台消费线程用于消费队列里的请求(减库存),由于消费操作是单线程,不用加锁,不会有超卖问题。
字节跳动公司福利 1297人发布
查看4道真题和解析