秒杀系统超卖记录

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层改进

  1. 判断余量和减库存合并
    UPDATE seckill  
    SET number=number-1 
    WHERE seckill_id=#{seckillId} AND number>0
  2. 将判断库存的快照读改成当前读
    SELECT number 
    FROM seckill 
    WHERE seckill_id=#{seckillId} 
    FOR UPDATE

使用队列将发送请求和执行减库存的操作异步化

  1. 前端发送请求依次进入阻塞队列,后端将请求入队后就返回,由于只进行请求入队操作,所以速度很快;
  2. 服务开启的同时开一个后台消费线程用于消费队列里的请求(减库存),由于消费操作是单线程,不用加锁,不会有超卖问题。

图片说明

全部评论
哥,你这秒杀系统是哪儿的开源项目,或者教程~~~
点赞 回复 分享
发布于 2020-09-23 18:16

相关推荐

offer多多的六边形战士很无语:看了你的博客,感觉挺不错的,可以把你的访问量和粉丝数在简历里提一下,闪光点(仅个人意见)
点赞 评论 收藏
分享
11-04 14:10
东南大学 Java
_可乐多加冰_:去市公司包卖卡的
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务