微信紅包實現原理


  我們平時在用微信的時候,經常會用到‘搶紅包’的功能。那么這樣一個需求給我們的話,具體又應該怎么實現呢?

  

需求分析

  1 發紅包:在db、cache各新增一條記錄
  2 搶紅包:有人發紅包之后,肯定很多人同時去搶,所以應該請求訪問cache,剩余紅包個數大於0就可以點擊拆開紅包;反之提醒紅包已經被搶完了
  3 拆紅包:總金額每次都是遞減,可以用redis的decreby來做。
  4 查看紅包記錄:用戶直接查db即可。
  這里面就會涉及到2個問題:
    我只發了100個紅包,並發下如何保證搶到紅包的人數不會超過100.
    紅包總金額1w元,如何分配才能讓金額不超出這個數,如何保證最后一個人一定能搶到錢.

  

數據庫表設計

紅包信息表主要字段:    誰發的紅包,發紅包時間,紅包總個數、總金額、剩余紅包信息、最后一次被搶紅包時間

CREATE TABLE `red_packet_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT, 
  `red_packet_id` bigint(11) NOT NULL DEFAULT  0 COMMENT '紅包id,采用timestamp+5位隨機數', 
  `total_amount` int(11) NOT NULL DEFAULT 0 COMMENT '紅包總金額,單位分',
  `total_packet` int(11) NOT NULL DEFAULT 0 COMMENT '紅包總個數',
  `remaining_amount` int(11) NOT NULL DEFAULT 0 COMMENT '剩余紅包金額,單位分',
  `remaining_packet` int(11) NOT NULL DEFAULT 0 COMMENT '剩余紅包個數',
  `uid` int(20) NOT NULL DEFAULT 0 COMMENT '新建紅包用戶的用戶標識',
  `create_time` timestamp  COMMENT '創建時間',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='紅包信息表,新建一個紅包插入一條記錄';

搶紅包記錄主要字段:    紅包信息、搶紅包人信息,搶紅包時間    (主要都是查看搶紅包記錄列表的那些字段)

CREATE TABLE `red_packet_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT, 
  `amount` int(11) NOT NULL DEFAULT '0' COMMENT '搶到紅包的金額',
  `nick_name` varchar(32) NOT NULL DEFAULT '0' COMMENT '搶到紅包的用戶的用戶名',
  `img_url` varchar(255) NOT NULL DEFAULT '0' COMMENT '搶到紅包的用戶的頭像',
  `uid` int(20) NOT NULL DEFAULT '0' COMMENT '搶到紅包用戶的用戶標識',
  `red_packet_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '紅包id,采用timestamp+5位隨機數', 
  `create_time` timestamp  COMMENT '創建時間',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='搶紅包記錄表,搶一個紅包插入一條記錄';

編碼實現

發紅包

發紅包之后,肯定立馬會有很多人來搶,如果直接操作數據庫會有很大的壓力,所以我們把數據放到緩存里面去。

    /***
     * 發紅包
     * @param uid 發紅包的用戶id
     * @param totalNum 紅包金額
     * @param totalAmount 紅包總個數
     * @return
     */
    @GetMapping("/addPacket")
    public String saveRedPacket(Integer uid, Integer totalNum, Integer totalAmount) {
        // 組裝數據
        RedPacketInfo record = new RedPacketInfo();
        record.setUid(uid);
        record.setTotalAmount(totalAmount);
        record.setTotalPacket(totalNum);
        record.setCreateTime(new Date());
        record.setRemainingAmount(totalAmount);
        record.setRemainingPacket(totalNum);
        // 雪花算法生成唯一id
        long redPacketId = new SnowflakeDistributeId(0, 0).nextId();
        record.setRedPacketId(redPacketId);
        // 紅包保存到數據庫
        redPacketInfoMapper.insert(record);
        // 紅包個數和總金額存入緩存
        redisService.set(redPacketId + "_totalNum", totalNum + "");
        redisService.set(redPacketId + "_totalAmount", totalAmount + "");
        return "success";
    }

搶紅包

用戶點擊紅包之后,就查看紅包數量,如果為0的話,點擊拆紅包就提示紅包被搶完了;反之獲取到紅包金額數量

    /**
     * 搶紅包
     * @param redPacketId 紅包id
     * @param uid 用戶id
     * @return
     */
    @GetMapping("/getPacket")
    public String getRedPacket(long redPacketId, Integer uid) {
        Object record = redisService.get(uid + RECORD + redPacketId);
        // 如果用戶已經搶過紅包了,那點擊搶紅包就應該是查看搶紅包的詳細記錄
        if (StringUtils.isNotBlank((String)record)){
            return "紅包詳細記錄";
        }
        // 查詢紅包剩余個數
        String redPacketName = redPacketId + TOTAL_NUM;
        String num = (String) redisService.get(redPacketName);
        if (StringUtils.isNotBlank(num)) {
            return num;
        }
        return "0";
    }

拆紅包(核心)

這是重點也是難點,我們要保證領取紅包的人數不能超過設置的紅包個數,還要保證每一個人的紅包都能搶到錢、還不能超過總金額。這就會涉及到線程安全問題。現在我們就來來想想,如何合理的生成紅包隨機金額數量。
  1. 剩余總金額/剩余總個數 = 紅包金額平均數
  2. 由於紅包是隨機金額,我們的紅包金額可以在這個平均值左右浮動,總和不變即可
  這樣設計,才能真正保證每個人拆開都能領到錢,而且總金額不會超支

    /**
     * 拆紅包
     * @param redPacketId 紅包id
     * @param uid 用戶id
     * @return
     */
    @GetMapping("/getRedPacketMoney")
    public String getRedPacketMoney(int uid, long redPacketId) {
        // 搶到的紅包金額
        Integer randomAmount = 0;
        String redPacketName = redPacketId + TOTAL_NUM;
        String totalAmountName = redPacketId + TOTAL_AMOUNT;
        // 預減獲取紅包剩余數量,decr原子減來防止領取人數超過紅包個數
        long decr = redisService.decr(totalAmountName, 1);
        if (decr<0){
            System.out.println(uid+":  抱歉!紅包已經搶完了");
            return "抱歉!紅包已經搶完了";
        }
        // 下面就開始隨機分配金額了,並發下可能領取人數的業務邏輯同時走到了這里,
        // 下面算法最后計算出來的金額就會和總金額有偏差,所以我們可以通過對紅包
        // id進行路由,放入同一個隊列里面,從而保證順序消費,
        // 這樣金額總和就和總金額不會有偏差

        // 剩余總金額(后面所有邏輯,都由下游業務去隊列里面執行)
        Integer totalAmountInt = Integer.parseInt((String) redisService.get(redPacketName));
        // 剩余金額 / 剩余紅包個數 * 2 = 最大紅包金額
        Integer maxMoney = (int) (totalAmountInt / (decr + 1) * 2);
        Random random = new Random();
        // 紅包取值隨機數,不超過最大金額(如果是最后一個紅包,金額就是剩下的所有錢)
        randomAmount = random.nextInt(maxMoney);
        System.out.println(uid+":  搶到了  "+randomAmount+"   分錢");
        // 紅包剩余個數減1,同時剩余金額也要減少
        redisService.decr(redPacketName,randomAmount);  //redis decreby功能
        redisService.set(uid + RECORD + redPacketId,randomAmount.toString());
        // 數據庫插入搶紅包記錄
        updateRacketInDB(uid, redPacketId,randomAmount);
        return randomAmount + "";
    }

    public void updateRacketInDB(int uid, long redPacketId, int amount) {
        // 數據庫插入搶紅包記錄
        RedPacketRecord redPacketRecord = new RedPacketRecord();
        redPacketRecord.setUid(uid);
        redPacketRecord.setRedPacketId(redPacketId);
        redPacketRecord.setAmount(amount);
        redPacketRecord.setCreateTime(new Date());
        redPacketRecordMapper.insertSelective(redPacketRecord);
        // 查詢到紅包信息
        RedPacketInfoExample example = new RedPacketInfoExample ();
        RedPacketInfoExample.Criteria criteria = example.createCriteria();
        criteria.andRedPacketIdEqualTo(redPacketId);
        RedPacketInfo redPacketInfo = redPacketInfoMapper.selectByExample(example).get(0);
        // 修改紅包剩余信息
        redPacketInfo.setRemainingPacket(redPacketInfo.getRemainingPacket()-amount);
        redPacketInfo.setRemainingAmount(redPacketInfo.getRemainingAmount()-1);
        redPacketInfo.setCreateTime(new Date());
        redPacketInfoMapper.updateByPrimaryKey(redPacketInfo);
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM