「高併發秒殺」微信搶紅包實戰案例

「高併發秒殺」微信搶紅包實戰案例

前言

群裡有小夥伴諮詢微信紅包的架構,對於我來說,顯然是不知道的,但是寫一個相對高併發的搶紅包案例還是完全可以的。

架構流程

「高併發秒殺」微信搶紅包實戰案例

架構設計

  • 老闆發紅包,此時緩存初始化紅包個數,紅包金額(單位分),並異步入庫。
  • 搶紅包,判斷緩存剩餘紅包金額,剩餘金額大於零則搶到紅包,否則手慢了,紅包派完了
  • 拆紅包,根據 redPacketId 獲取分佈式鎖,如果獲取到鎖,紅包個數減一,如果剩餘紅包個數大於零搶紅包成功、否則失敗。成功則計算紅包金額,緩存總紅包金額減去搶到的紅包金額,異步入庫、異步到賬。
  • 若獲取分佈式鎖失敗,使用 Redis的 decr命令對紅包個數加一。

數據庫設計

  • 紅包信息表
<code>CREATE TABLE `red_racket` (   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',   `red_packet_id` bigint(20) NOT NULL COMMENT '紅包唯一ID',   `total_amount` int(11) NOT NULL COMMENT '紅包金額單位分',   `total_packet` int(11) NOT NULL COMMENT '紅包個數',   `type` int(11) NOT NULL COMMENT '紅包類型',   `create_time` datetime DEFAULT NULL COMMENT '創建時間',   `version` int(11) NOT NULL COMMENT '版本號',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 CHECKSUM=1 DELAY_KEY_WRITE=1 ROW_FORMAT=DYNAMIC COMMENT='紅包信息表'/<code>
  • 搶紅包記錄表
<code>CREATE TABLE `red_packet_record` (   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',   `amount` int(11) NOT NULL COMMENT '搶到紅包的金額',   `red_packet_id` bigint(20) NOT NULL COMMENT '紅包ID',   `uid` int(11) NOT NULL COMMENT '搶到紅包用戶的用戶標識',   `create_time` datetime DEFAULT NULL COMMENT '創建時間',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 CHECKSUM=1 DELAY_KEY_WRITE=1 ROW_FORMAT=DYNAMIC COMMENT='搶紅包記錄表'/<code>

代碼案例

老闆發了10個紅包一共200人民幣,100個人同時搶紅包,偽代碼分別為拆紅包和搶紅包相關業務邏輯。模擬搶紅包偽代碼:

<code>   /**     * 搶紅包 拆紅包 搶到不一定能拆到     * @param redPacketId     * @return     */    @ApiOperation(value="搶紅包二",nickname="爪哇筆記")    @PostMapping("/startTwo")    public Result startTwo(long redPacketId){        int skillNum = 100;        final CountDownLatch latch = new CountDownLatch(skillNum);//N個搶紅包        /**         * 初始化紅包數據,搶紅包攔截         */        redisUtil.cacheValue(redPacketId+"-num",10);        /**         * 初始化紅包金額,單位為分         */        redisUtil.cacheValue(redPacketId+"-money",20000);        /**         * 模擬100個用戶搶10個紅包         */        for(int i=1;i<=skillNum;i++){            int userId = i;            Runnable task = () -> {                /**                 * 搶紅包 判斷剩餘金額                 */                Integer money = (Integer) redisUtil.getValue(redPacketId+"-money");                if(money>0){                    /**                     * 雖然能搶到 但是不一定能拆到                     * 類似於微信的 點擊紅包顯示搶的按鈕                     */                    Result result = redPacketService.startTwoSeckil(redPacketId,userId);                    if(result.get("code").toString().equals("500")){                        LOGGER.info("用戶{}手慢了,紅包派完了",userId);                    }else{                        Double amount = DoubleUtil.divide(Double.parseDouble(result.get("msg").toString()), (double) 100);                        LOGGER.info("用戶{}搶紅包成功,金額:{}", userId,amount);                    }                }else{                    /**                     * 直接顯示手慢了,紅包派完了                     */                    //LOGGER.info("用戶{}手慢了,紅包派完了",userId);                }                latch.countDown();            };            executor.execute(task);        }        try {            latch.await();            Integer restMoney = Integer.parseInt(redisUtil.getValue(redPacketId+"-money").toString());            LOGGER.info("剩餘金額:{}",restMoney);        } catch (InterruptedException e) {            e.printStackTrace();        }        return Result.ok();    }/<code>

業務層拆紅包:

<code>    @Override    @Transactional    public Result startTwoSeckil(long redPacketId, int userId) {        Integer money = 0;        boolean res=false;        try {            /**             * 獲取鎖 保證紅包數量和計算紅白金額的原子性操作             */            res = RedissLockUtil.tryLock(redPacketId+"", TimeUnit.SECONDS, 3, 10);            if(res){                long restPeople = redisUtil.decr(redPacketId+"-num",1);                if(restPeople>0){                    /**                     * 如果是最後一人                     */                    if(restPeople==1){                        money = Integer.parseInt(redisUtil.getValue(redPacketId+"-money").toString());                    }else{                        Integer restMoney = Integer.parseInt(redisUtil.getValue(redPacketId+"-money").toString());                        Random random = new Random();                        //隨機範圍:[1,剩餘人均金額的兩倍]                        money = random.nextInt((int) (restMoney / (restPeople+1) * 2 - 1)) + 1;                    }                    redisUtil.decr(redPacketId+"-money",money);                    /**                     * 異步入庫                     */                    RedPacketRecord record = new RedPacketRecord();                    record.setMoney(money);                    record.setRedPacketId(redPacketId);                    record.setUid(userId);                    record.setCreateTime(new Timestamp(System.currentTimeMillis()));                    saveRecord(record);                    /**                     * 異步入賬                     */                }else{                    return Result.error("手慢了,紅包派完了");                }            }else{                /**                 * 獲取鎖失敗相當於搶紅包失敗,紅包個數加一                 */                redisUtil.incr(redPacketId+"-num",1);            }        } catch (Exception e) {            e.printStackTrace();        }finally {            if(res){//釋放鎖                RedissLockUtil.unlock(redPacketId+"");            }        }        return Result.ok(money);    }/<code>

演示

在 Application中有接口演示說明,你可以在搶紅包 Red Packet Controller接口中輸入任何參數進行測試,也可以配合數據庫稍加修改即可作為生產環境的搶紅包功能模塊。

源碼

https://gitee.com/52itstyle/spring-boot-seckill

鏈接:https://mp.weixin.qq.com/s/YJZvE1SdSPjddgZBBkacSQ


分享到:


相關文章: