秒杀系统超卖记录

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

相关推荐

一共一个小时,面试难度以及自己的回答算是最近的面试压力比较大的,实习问了30分钟,中间穿插八股。1.redis数据结构2.redis持久化机制3.mysql索引底层4.聚簇索引与非聚簇索引5.索引优化6.索引失效7.mysql执行一条sql8.那么多索引mysql怎么选(不会)9.tcp与udp区别10.tcp为什么可靠11.消息队列作用12.kafka怎么保证消息有序性13.mcp是什么?14.skills是什么?15.jvm内存分配与回收过程(我讲了从创建对象到判断垃圾对象到垃圾回收我全说了一遍,是这个吗?)16.fullgc触发机制17.tcp的拥塞控制流程(不会了)18.分布式事务解决方案,说了2pc,3pc,tcc。算法是反转双向链表,没有按格式输出,但是面试官没让继续写了,面完以为挂了,结果晚上秒过,看看复试什么情况吧。今天百度打电话准备发offer了,业务跟在手子的差不多,很垂,并且说不分日常暑期,只看表现,会有转正机会,但是考虑再三还是拒绝了,百度实习薪资确实有点低,title也不如之前了,但是面试的二位业务老师我很喜欢,对我的评价也不错,希望之后能有机会共事。从三月份到现在一共面了六家,面试次数总共是8场,情况如下:脉脉二面(无答复,默认挂)百度二面已oc美团一面过,下周一二面shein一面过直接HR面游族一面过直接HR面腾讯一面过等待约二面滴滴明天一面面试通过率还是蛮高的,但是大部分都是日常,感觉对我现在的加成不大,大概率不会去,不知道暑期会是什么情况呢唉,希望能有面试吧,继续加油。字节被无hc直接取消了,现在还没人捞,有没有字节HR救救我
不管什么都不想跳动了:本人美团百度快手都待过,建议肯定是直接留快手多一点产出后转正or直接冲字节腾讯暑期吧。一是快手从福利到基建都吊打另外两家。美团现在这个业务比较惨,本来毛利就很低,亏损严重,今年很可能要优化人力降低成本,去了别说日常,就算暑期后面都很可能被优化。百度其实实习生权限挺高的,可以接触到一些含金量高的项目,但是现在的风评不如之前了,薪资也不高。二是转正概率和薪资是跟产出挂钩的,你都在手子已经积累产出了,去其他家日常实习产出都是从0开始,肯定不可能有你在手子转正可能性大啊,现在日常压根没必要去,而且我有两个师弟都是在快手日常转正的,不用太担心,安心留在手子一边多做一点产出然后一边冲字节腾讯暑期,字节腾讯今年实习岗位非常多的,不如好好把握这个,加油。
查看18道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务