<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    實現高并發秒殺的七種方式

    VSole2023-01-09 09:32:44

    1.引言

    高并發場景在現場的日常工作中很常見,特別是在互聯網公司中,這篇文章就來通過秒殺商品來模擬高并發的場景。文章末尾會附上文章的所有代碼、腳本和測試用例。

    • 本文環境: SpringBoot 2.5.7 + MySQL 8.0 X + MybatisPlus + Swagger2.9.2
    • 模擬工具: Jmeter
    • 模擬場景: 減庫存->創建訂單->模擬支付

    2.商品秒殺-超賣

    在開發中,對于下面的代碼,可能很熟悉:在Service里面加上@Transactional事務注解和Lock鎖

    控制層:Controller

    @ApiOperation(value="秒殺實現方式——Lock加鎖")
    @PostMapping("/start/lock")
    public Result startLock(long skgId){
        try {
            log.info("開始秒殺方式一...");
            final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
            Result result = secondKillService.startSecondKillByLock(skgId, userId);
            if(result != null){
                log.info("用戶:{}--{}", userId, result.get("msg"));
            }else{
                log.info("用戶:{}--{}", userId, "哎呦喂,人也太多了,請稍后!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
        return Result.ok();
    }
    

    業務層:Service

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSecondKillByLock(long skgId, long userId) {
        lock.lock();
        try {
            // 校驗庫存
            SecondKill secondKill = secondKillMapper.selectById(skgId);
            Integer number = secondKill.getNumber();
            if (number > 0) {
                // 扣庫存
                secondKill.setNumber(number - 1);
                secondKillMapper.updateById(secondKill);
                // 創建訂單
                SuccessKilled killed = new SuccessKilled();
                killed.setSeckillId(skgId);
                killed.setUserId(userId);
                killed.setState((short) 0);
                killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
                successKilledMapper.insert(killed);
                // 模擬支付
                Payment payment = new Payment();
                payment.setSeckillId(skgId);
                payment.setSeckillId(skgId);
                payment.setUserId(userId);
                payment.setMoney(40);
                payment.setState((short) 1);
                payment.setCreateTime(new Timestamp(System.currentTimeMillis()));
                paymentMapper.insert(payment);
            } else {
                return Result.error(SecondKillStateEnum.END);
            }
        } catch (Exception e) {
            throw new ScorpiosException("異常了個乖乖");
        } finally {
            lock.unlock();
        }
        return Result.ok(SecondKillStateEnum.SUCCESS);
    }
    

    對于上面的代碼應該沒啥問題吧,業務方法上加事務,在處理業務的時候加鎖。

    但上面這樣寫法是有問題的,會出現超賣的情況,看下測試結果:模擬1000個并發,搶100商品

    Jmeter不了解的,可以參考這篇文章:

    • https://blog.csdn.net/zxd1435513775/article/details/106372446

    這里在業務方法開始加了鎖,在業務方法結束后釋放了鎖。但這里的事務提交卻不是這樣的,有可能在事務提交之前,就已經把鎖釋放了,這樣會導致商品超賣現象。所以加鎖的時機很重要!

    3. 解決商品超賣

    對于上面超賣現象,主要問題出現在事務中鎖釋放的時機,事務未提交之前,鎖已經釋放。(事務提交是在整個方法執行完)。如何解決這個問題呢,就是把加鎖步驟提前

    • 可以在controller層進行加鎖
    • 可以使用Aop在業務方法執行之前進行加鎖

    3.1 方式一(改進版加鎖)

    @ApiOperation(value="秒殺實現方式——Lock加鎖")
    @PostMapping("/start/lock")
    public Result startLock(long skgId){
        // 在此處加鎖
        lock.lock();
        try {
            log.info("開始秒殺方式一...");
            final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
            Result result = secondKillService.startSecondKillByLock(skgId, userId);
            if(result != null){
                log.info("用戶:{}--{}", userId, result.get("msg"));
            }else{
                log.info("用戶:{}--{}", userId, "哎呦喂,人也太多了,請稍后!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 在此處釋放鎖
            lock.unlock();
        }
        return Result.ok();
    }
    

    上面這樣的加鎖就可以解決事務未提交之前,鎖釋放的問題,可以分三種情況進行壓力測試:

    • 并發數1000,商品100
    • 并發數1000,商品1000
    • 并發數2000,商品1000

    對于并發量大于商品數的情況,商品秒殺一般不會出現少賣的請況,但對于并發數小于等于商品數的時候可能會出現商品少賣情況,這也很好理解。

    對于沒有問題的情況就不貼圖了,因為有很多種方式,貼圖會太多

    3.2 方式二(AOP版加鎖)

    對于上面在控制層進行加鎖的方式,可能顯得不優雅,那就還有另一種方式進行在事務之前加鎖,那就是AOP

    自定義AOP注解

    @Target({ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public  @interface ServiceLock {
        String description()  default "";
    }
    

    定義切面類

    @Slf4j
    @Component
    @Scope
    @Aspect
    @Order(1) //order越小越是最先執行,但更重要的是最先執行的最后結束
    public class LockAspect {
        /**
         * 思考:為什么不用synchronized
         * service 默認是單例的,并發下lock只有一個實例
         */
        private static  Lock lock = new ReentrantLock(true); // 互斥鎖 參數默認false,不公平鎖
        // Service層切點     用于記錄錯誤日志
        @Pointcut("@annotation(com.scorpios.secondkill.aop.ServiceLock)")
        public void lockAspect() {
        }
        @Around("lockAspect()")
        public  Object around(ProceedingJoinPoint joinPoint) {
            lock.lock();
            Object obj = null;
            try {
                obj = joinPoint.proceed();
            } catch (Throwable e) {
                e.printStackTrace();
       throw new RuntimeException();
            } finally{
                lock.unlock();
            }
            return obj;
        }
    }
    

    在業務方法上添加AOP注解

    @Override
    @ServiceLock // 使用Aop進行加鎖
    @Transactional(rollbackFor = Exception.class)
    public Result startSecondKillByAop(long skgId, long userId) {
        try {
            // 校驗庫存
            SecondKill secondKill = secondKillMapper.selectById(skgId);
            Integer number = secondKill.getNumber();
            if (number > 0) {
                //扣庫存
                secondKill.setNumber(number - 1);
                secondKillMapper.updateById(secondKill);
                //創建訂單
                SuccessKilled killed = new SuccessKilled();
                killed.setSeckillId(skgId);
                killed.setUserId(userId);
                killed.setState((short) 0);
                killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
                successKilledMapper.insert(killed);
                //支付
                Payment payment = new Payment();
                payment.setSeckillId(skgId);
                payment.setSeckillId(skgId);
                payment.setUserId(userId);
                payment.setMoney(40);
                payment.setState((short) 1);
                payment.setCreateTime(new Timestamp(System.currentTimeMillis()));
                paymentMapper.insert(payment);
            } else {
                return Result.error(SecondKillStateEnum.END);
            }
        } catch (Exception e) {
            throw new ScorpiosException("異常了個乖乖");
        }
        return Result.ok(SecondKillStateEnum.SUCCESS);
    }
    

    控制層:

    @ApiOperation(value="秒殺實現方式二——Aop加鎖")
    @PostMapping("/start/aop")
    public Result startAop(long skgId){
        try {
            log.info("開始秒殺方式二...");
            final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
            Result result = secondKillService.startSecondKillByAop(skgId, userId);
            if(result != null){
                log.info("用戶:{}--{}", userId, result.get("msg"));
            }else{
                log.info("用戶:{}--{}", userId, "哎呦喂,人也太多了,請稍后!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Result.ok();
    }
    

    這種方式在對鎖的使用上,更高階、更美觀!

    3.3 方式三(悲觀鎖一)

    除了上面在業務代碼層面加鎖外,還可以使用數據庫自帶的鎖進行并發控制。

    悲觀鎖,什么是悲觀鎖呢?通俗的說,在做任何事情之前,都要進行加鎖確認。這種數據庫級加鎖操作效率較低。

    使用for update一定要加上事務,當事務處理完后,for update才會將行級鎖解除

    如果請求數和秒殺商品數量一致,會出現少賣

    @ApiOperation(value="秒殺實現方式三——悲觀鎖")
    @PostMapping("/start/pes/lock/one")
    public Result startPesLockOne(long skgId){
        try {
            log.info("開始秒殺方式三...");
            final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
            Result result = secondKillService.startSecondKillByUpdate(skgId, userId);
            if(result != null){
                log.info("用戶:{}--{}", userId, result.get("msg"));
            }else{
                log.info("用戶:{}--{}", userId, "哎呦喂,人也太多了,請稍后!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Result.ok();
    }
    

    業務邏輯

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSecondKillByUpdate(long skgId, long userId) {
        try {
            // 校驗庫存-悲觀鎖
            SecondKill secondKill = secondKillMapper.querySecondKillForUpdate(skgId);
            Integer number = secondKill.getNumber();
            if (number > 0) {
                //扣庫存
                secondKill.setNumber(number - 1);
                secondKillMapper.updateById(secondKill);
                //創建訂單
                SuccessKilled killed = new SuccessKilled();
                killed.setSeckillId(skgId);
                killed.setUserId(userId);
                killed.setState((short) 0);
                killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
                successKilledMapper.insert(killed);
                //支付
                Payment payment = new Payment();
                payment.setSeckillId(skgId);
                payment.setSeckillId(skgId);
                payment.setUserId(userId);
                payment.setMoney(40);
                payment.setState((short) 1);
                payment.setCreateTime(new Timestamp(System.currentTimeMillis()));
                paymentMapper.insert(payment);
            } else {
                return Result.error(SecondKillStateEnum.END);
            }
        } catch (Exception e) {
            throw new ScorpiosException("異常了個乖乖");
        } finally {
        }
        return Result.ok(SecondKillStateEnum.SUCCESS);
    }
    

    Dao層

    @Repository
    public interface SecondKillMapper extends BaseMapper<SecondKill> {
        /**
         * 將此行數據進行加鎖,當整個方法將事務提交后,才會解鎖
         * @param skgId
         * @return
         */
        @Select(value = "SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
        SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);
    }
    

    上面是利用for update進行對查詢數據加鎖,加的是行鎖

    3.4 方式四(悲觀鎖二)

    悲觀鎖的第二種方式就是利用update更新命令來加表鎖

    /**
     * UPDATE鎖表
     * @param skgId  商品id
     * @param userId    用戶id
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSecondKillByUpdateTwo(long skgId, long userId) {
        try {
            // 不校驗,直接扣庫存更新
            int result = secondKillMapper.updateSecondKillById(skgId);
            if (result > 0) {
                //創建訂單
                SuccessKilled killed = new SuccessKilled();
                killed.setSeckillId(skgId);
                killed.setUserId(userId);
                killed.setState((short) 0);
                killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
                successKilledMapper.insert(killed);
                //支付
                Payment payment = new Payment();
                payment.setSeckillId(skgId);
                payment.setSeckillId(skgId);
                payment.setUserId(userId);
                payment.setMoney(40);
                payment.setState((short) 1);
                payment.setCreateTime(new Timestamp(System.currentTimeMillis()));
                paymentMapper.insert(payment);
            } else {
                return Result.error(SecondKillStateEnum.END);
            }
        } catch (Exception e) {
            throw new ScorpiosException("異常了個乖乖");
        } finally {
        }
        return Result.ok(SecondKillStateEnum.SUCCESS);
    }
    

    Dao層

    @Repository
    public interface SecondKillMapper extends BaseMapper<SecondKill> {
        /**
         * 將此行數據進行加鎖,當整個方法將事務提交后,才會解鎖
         * @param skgId
         * @return
         */
        @Select(value = "SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
        SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);
        @Update(value = "UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")
        int updateSecondKillById(@Param("skgId") long skgId);
    }
    

    3.5 方式五(樂觀鎖)

    樂觀鎖,顧名思義,就是對操作結果很樂觀,通過利用version字段來判斷數據是否被修改

    樂觀鎖,不進行庫存數量的校驗,直接做庫存扣減

    這里使用的樂觀鎖會出現大量的數據更新異常(拋異常就會導致購買失敗)、如果配置的搶購人數比較少、比如120:100(人數:商品) 會出現少買的情況,不推薦使用樂觀鎖。

    @ApiOperation(value="秒殺實現方式五——樂觀鎖")
    @PostMapping("/start/opt/lock")
    public Result startOptLock(long skgId){
        try {
            log.info("開始秒殺方式五...");
            final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
            // 參數添加了購買數量
            Result result = secondKillService.startSecondKillByPesLock(skgId, userId,1);
            if(result != null){
                log.info("用戶:{}--{}", userId, result.get("msg"));
            }else{
                log.info("用戶:{}--{}", userId, "哎呦喂,人也太多了,請稍后!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Result.ok();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSecondKillByPesLock(long skgId, long userId, int number) {
        // 樂觀鎖,不進行庫存數量的校驗,直接
        try {
            SecondKill kill = secondKillMapper.selectById(skgId);
            // 剩余的數量應該要大于等于秒殺的數量
            if(kill.getNumber() >= number) {
                int result = secondKillMapper.updateSecondKillByVersion(number,skgId,kill.getVersion());
                if (result > 0) {
                    //創建訂單
                    SuccessKilled killed = new SuccessKilled();
                    killed.setSeckillId(skgId);
                    killed.setUserId(userId);
                    killed.setState((short) 0);
                    killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
                    successKilledMapper.insert(killed);
                    //支付
                    Payment payment = new Payment();
                    payment.setSeckillId(skgId);
                    payment.setSeckillId(skgId);
                    payment.setUserId(userId);
                    payment.setMoney(40);
                    payment.setState((short) 1);
                    payment.setCreateTime(new Timestamp(System.currentTimeMillis()));
                    paymentMapper.insert(payment);
                } else {
                    return Result.error(SecondKillStateEnum.END);
                }
            }
        } catch (Exception e) {
            throw new ScorpiosException("異常了個乖乖");
        } finally {
        }
        return Result.ok(SecondKillStateEnum.SUCCESS);
    }
    @Repository
    public interface SecondKillMapper extends BaseMapper<SecondKill> {
        /**
         * 將此行數據進行加鎖,當整個方法將事務提交后,才會解鎖
         * @param skgId
         * @return
         */
        @Select(value = "SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
        SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);
        @Update(value = "UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")
        int updateSecondKillById(@Param("skgId") long skgId);
        @Update(value = "UPDATE seckill  SET number=number-#{number},version=version+1 WHERE seckill_id=#{skgId} AND version = #{version}")
        int updateSecondKillByVersion(@Param("number") int number, @Param("skgId") long skgId, @Param("version")int version);
    }
    

    樂觀鎖會出現大量的數據更新異常(拋異常就會導致購買失敗),會出現少買的情況,不推薦使用樂觀鎖

    3.6 方式六(阻塞隊列)

    利用阻塞隊類,也可以解決高并發問題。其思想就是把接收到的請求按順序存放到隊列中,消費者線程逐一從隊列里取數據進行處理,看下具體代碼。

    阻塞隊列:這里使用靜態內部類的方式來實現單例模式,在并發條件下不會出現問題。

    // 秒殺隊列(固定長度為100)
    public class SecondKillQueue {
        // 隊列大小
        static final int QUEUE_MAX_SIZE = 100;
        // 用于多線程間下單的隊列
        static BlockingQueue blockingQueue = new LinkedBlockingQueue(QUEUE_MAX_SIZE);
        // 使用靜態內部類,實現單例模式
        private SecondKillQueue(){};
        private static class SingletonHolder{
            // 靜態初始化器,由JVM來保證線程安全
            private  static SecondKillQueue queue = new SecondKillQueue();
        }
        /**
         * 單例隊列
         * @return
         */
        public static SecondKillQueue getSkillQueue(){
            return SingletonHolder.queue;
        }
        /**
         * 生產入隊
         * @param kill
         * @throws InterruptedException
         * add(e) 隊列未滿時,返回true;隊列滿則拋出IllegalStateException(“Queue full”)異常——AbstractQueue
         * put(e) 隊列未滿時,直接插入沒有返回值;隊列滿時會阻塞等待,一直等到隊列未滿時再插入。
         * offer(e) 隊列未滿時,返回true;隊列滿時返回false。非阻塞立即返回。
         * offer(e, time, unit) 設定等待的時間,如果在指定時間內還不能往隊列中插入數據則返回false,插入成功返回true。
         */
        public  Boolean  produce(SuccessKilled kill) {
            return blockingQueue.offer(kill);
        }
        /**
         * 消費出隊
         * poll() 獲取并移除隊首元素,在指定的時間內去輪詢隊列看有沒有首元素有則返回,否者超時后返回null
         * take() 與帶超時時間的poll類似不同在于take時候如果當前隊列空了它會一直等待其他線程調用notEmpty.signal()才會被喚醒
         */
        public  SuccessKilled consume() throws InterruptedException {
            return blockingQueue.take();
        }
        /**
         * 獲取隊列大小
         * @return
         */
        public int size() {
            return blockingQueue.size();
        }
    }
    

    消費秒殺隊列:實現ApplicationRunner接口

    // 消費秒殺隊列
    @Slf4j
    @Component
    public class TaskRunner implements ApplicationRunner{
        @Autowired
        private SecondKillService seckillService;
        @Override
        public void run(ApplicationArguments var){
            new Thread(() -> {
                log.info("隊列啟動成功");
                while(true){
                    try {
                        // 進程內隊列
                        SuccessKilled kill = SecondKillQueue.getSkillQueue().consume();
                        if(kill != null){
                            Result result = seckillService.startSecondKillByAop(kill.getSeckillId(), kill.getUserId());
                            if(result != null && result.equals(Result.ok(SecondKillStateEnum.SUCCESS))){
                                log.info("TaskRunner,result:{}",result);
                                log.info("TaskRunner從消息隊列取出用戶,用戶:{}{}",kill.getUserId(),"秒殺成功");
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    @ApiOperation(value="秒殺實現方式六——消息隊列")
    @PostMapping("/start/queue")
    public Result startQueue(long skgId){
        try {
            log.info("開始秒殺方式六...");
            final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
            SuccessKilled kill = new SuccessKilled();
            kill.setSeckillId(skgId);
            kill.setUserId(userId);
            Boolean flag = SecondKillQueue.getSkillQueue().produce(kill);
            // 雖然進入了隊列,但是不一定能秒殺成功 進隊出隊有時間間隙
            if(flag){
                log.info("用戶:{}{}",kill.getUserId(),"秒殺成功");
            }else{
                log.info("用戶:{}{}",userId,"秒殺失敗");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Result.ok();
    }
    
    注意:在業務層和AOP方法中,不能拋出任何異常, throw new RuntimeException()這些拋異常代碼要注釋掉。因為一旦程序拋出異常就會停止,導致消費秒殺隊列進程終止!

    使用阻塞隊列來實現秒殺,有幾點要注意:

    • 消費秒殺隊列中調用業務方法加鎖與不加鎖情況一樣,也就是seckillService.startSecondKillByAop()seckillService.startSecondKillByLock()方法結果一樣,這也很好理解
    • 當隊列長度與商品數量一致時,會出現少賣的現象,可以調大數值
    • 下面是隊列長度1000,商品數量1000,并發數2000情況下出現的少賣

    3.7.方式七(Disruptor隊列)

    Disruptor是個高性能隊列,研發的初衷是解決內存隊列的延遲問題,在性能測試中發現竟然與I/O操作處于同樣的數量級,基于Disruptor開發的系統單線程能支撐每秒600萬訂單。

    // 事件生成工廠(用來初始化預分配事件對象)
    public class SecondKillEventFactory implements EventFactory<SecondKillEvent> {
        @Override
        public SecondKillEvent newInstance() {
            return new SecondKillEvent();
        }
    }
    // 事件對象(秒殺事件)
    public class SecondKillEvent implements Serializable {
        private static final long serialVersionUID = 1L;
        private long seckillId;
        private long userId;
     // set/get方法略
    }
    // 使用translator方式生產者
    public class SecondKillEventProducer {
        private final static EventTranslatorVararg translator = (seckillEvent, seq, objs) -> {
            seckillEvent.setSeckillId((Long) objs[0]);
            seckillEvent.setUserId((Long) objs[1]);
        };
        private final RingBuffer ringBuffer;
        public SecondKillEventProducer(RingBuffer ringBuffer){
            this.ringBuffer = ringBuffer;
        }
        public void secondKill(long seckillId, long userId){
            this.ringBuffer.publishEvent(translator, seckillId, userId);
        }
    }
    // 消費者(秒殺處理器)
    @Slf4j
    public class SecondKillEventConsumer implements EventHandler<SecondKillEvent> {
        private SecondKillService secondKillService = (SecondKillService) SpringUtil.getBean("secondKillService");
        @Override
        public void onEvent(SecondKillEvent seckillEvent, long seq, boolean bool) {
            Result result = secondKillService.startSecondKillByAop(seckillEvent.getSeckillId(), seckillEvent.getUserId());
            if(result.equals(Result.ok(SecondKillStateEnum.SUCCESS))){
                log.info("用戶:{}{}",seckillEvent.getUserId(),"秒殺成功");
            }
        }
    }
    public class DisruptorUtil {
        static Disruptor disruptor;
        static{
            SecondKillEventFactory factory = new SecondKillEventFactory();
            int ringBufferSize = 1024;
            ThreadFactory threadFactory = runnable -> new Thread(runnable);
            disruptor = new Disruptor<>(factory, ringBufferSize, threadFactory);
            disruptor.handleEventsWith(new SecondKillEventConsumer());
            disruptor.start();
        }
        public static void producer(SecondKillEvent kill){
            RingBuffer ringBuffer = disruptor.getRingBuffer();
            SecondKillEventProducer producer = new SecondKillEventProducer(ringBuffer);
            producer.secondKill(kill.getSeckillId(),kill.getUserId());
        }
    }
    @ApiOperation(value="秒殺實現方式七——Disruptor隊列")
    @PostMapping("/start/disruptor")
    public Result startDisruptor(long skgId){
        try {
            log.info("開始秒殺方式七...");
            final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
            SecondKillEvent kill = new SecondKillEvent();
            kill.setSeckillId(skgId);
            kill.setUserId(userId);
            DisruptorUtil.producer(kill);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Result.ok();
    }
    

    經過測試,發現使用Disruptor隊列隊列,與自定義隊列有著同樣的問題,也會出現超賣的情況,但效率有所提高。

    4. 小結

    對于上面七種實現并發的方式,做一下總結:

    • 一、二方式是在代碼中利用鎖和事務的方式解決了并發問題,主要解決的是鎖要加載事務之前
    • 三、四、五方式主要是數據庫的鎖來解決并發問題,方式三是利用for upate對表加行鎖,方式四是利用update來對表加鎖,方式五是通過增加version字段來控制數據庫的更新操作,方式五的效果最差
    • 六、七方式是通過隊列來解決并發問題,這里需要特別注意的是,在代碼中不能通過throw拋異常,否則消費線程會終止,而且由于進隊和出隊存在時間間隙,會導致商品少賣

    上面所有的情況都經過代碼測試,測試分一下三種情況:

    • 并發數1000,商品數100
    • 并發數1000,商品數1000
    • 并發數2000,商品數1000

    思考:分布式情況下如何解決并發問題呢?下次繼續試驗。

    源碼地址:

    • https://github.com/Hofanking/springboot-second-skill-example
    disruptor使用場景數據庫事務
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    文章末尾會附上文章的所有代碼、腳本和測試用例。本文環境: SpringBoot 2.5.7 + MySQL 8.0 X + MybatisPlus + Swagger2.9.2模擬工具: Jmeter模擬場景: 減庫存->創建訂單->模擬支付2.商品秒殺-超賣在開發中,對于下面的代碼,可能很熟悉:在Service里面加上@Transactional事務注解和Lock鎖控制層:Controller@ApiOperation. "哎呦喂,人也太多了,請稍后!
    隨著網絡的發展,傳輸控制協議/網際協議(Transmission Control Protocol/Internet Protocol,TCP/IP)架構已經不能適應現實的通信需求,存在諸多弊端。命名數據網絡(Named Data Network,NDN)在內容分發、移動性支持以及內生安全等方面具有獨特優勢,成為未來網絡架構方案中極具代表性的一種。
    2021年數字安全大事記
    2022-01-01 19:38:39
    2021年可謂是數字安全時代的開啟元年。習近平總書記在2021年世界互聯網大會烏鎮峰會開幕的賀信中強調,“筑牢數字安全屏障,讓數字文明造福各國人民,推動構建人類命運共同體。” 中央網絡安全和信息化委員會在2021年11月印發了《提升全民數字素養與技能行動綱要》,綱要在部署的第六個主要任務中明確了“提高數字安全保護能力”的要求。
    大家還記得今年的春晚么?在春晚中有一個搶紅包的環節。同時作為一名微信后端工程師,看完以后又會思考,學習了這樣的文章以后,是否能給自己的工作帶來一些實際的經驗呢?否則讀完以后腦子里能剩下的東西不過就是100億 1400萬QPS整流這樣的字眼,剩下的文章將展示作者是如何以此過程為目標,在本地環境的模擬了此過程。實現的目標:單機支持100萬連接,模擬了搖紅包和發紅包過程,單機峰值QPS 6萬,平穩支持了業務。背景知識QPS:Queries per second。
    近日,微軟數字犯罪部門查獲了越南網絡犯罪團伙Storm-1152使用的多個域名,該團伙注冊了超過 7.5 億個欺詐賬戶,并通過在網上向其他網絡犯罪分子出售這些賬戶賺取了數百萬美元。
    微軟已獲得法院命令,以扣押這家Windows巨頭所使用的 41 個域名,該組織稱這是一個伊朗網絡犯罪組織,該組織針對美國、中東和印度的組織開展魚叉式網絡釣魚活動。 微軟數字犯罪部門表示,該團伙被稱為Bohrium,對那些在技術、交通、政府和教育部門工作的人特別感興趣:其成員會假裝是招聘人員,以引誘標記在他們的 PC 上運行惡意軟件。
    2021年將成為網絡安全并購活動異常活躍的一年。SecBI公司以其自動化威脅檢測和響應能力而聞名,9月1日,LogPoint宣布已與SecBI方達成收購協議。企業安全提供商Check Point Software Technologies已收購云電子郵件安全公司Avanan。此次收購不僅擴大了 HackerU的地理覆蓋范圍,還將其產品范圍擴大到了所有安全職業點。
    本周二,美國司法部宣布FBI成功搗毀了ALPHV(BlackCat、黑貓)勒索軟件組織的服務器,并通過前期監控繳獲了大量解密密鑰。但ALPHV發動反攻并聲稱FBI的圍剿給數千個受害者帶來了災難性的后果。
    聯邦特工“破壞”了對一家未具名電信公司服務器的明顯網絡攻擊,該服務器與負責夏威夷和該地區互聯網、有線電視服務和手機連接的海底光纜相關。
    剛剛過去的2021年,是“十四五”開局之年,恰逢建黨100周年,砥礪奮斗百年路,揚帆啟航新征程。在百年變局和疫情交織背景下,我國經濟實現快速復蘇和持續增長。抗擊疫情是場持久戰,捍衛國家網絡空間安全也同樣不能放松警惕,信息安全、網絡安全是發展的保障,沒有網絡安全,就沒有國家安全。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类