接口開發說明
發紅包功能接口開發
- 新增一條紅包記錄
- 往 mysql 里面添加一條紅包記錄
- 往 redis 里面添加一條紅包數量記錄
- 往 redis 里面添加一條紅包金額記錄
搶紅包功能接口開發
- 在搶紅包這里並不能保證用戶已經能領到這個紅包
- 搶紅包只是做了一個判斷,判斷當前是否還有紅包
- 有紅包則返回可以領
- 沒紅包則返回不可以領
拆紅包功能接口開發
- 拆紅包才是用戶能領到紅包
- 這時候要先減 redis 里面的金額和紅包數量 decr decreby
- 減完金額再入庫
微信紅包設計算法分析
玩法:微信金額是拆的時候實時算出來,不是預先分配的,采用的是純內存計算,不需要預算空間存儲
分配:
- 發100塊錢,總共10個紅包,那么平均值是10塊錢一個,那么發出來的紅包的額度在0.01元~20元之間波動
- 當前面4個紅包總共被領了30塊錢時,剩下70塊錢,總共6個紅包,那么這7個紅包的額度在:0.01~(70➗6✖️2)=23.33之間波動
- 這樣算下去,可能會超過最開始的全部金額,因此到了最后面如果不夠這么算,那么會采取如下算法:保證剩余用戶能拿到最低1分錢即可
存儲:數據庫會累加已經領取的個數與金額,插入一條領取記錄。入賬則是后台異步操作
轉賬:通過財付通往紅包所得者賬戶轉賬,過程通過是異步操作
數據表設計

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='搶紅包記錄表,搶一個紅包插入一條記錄';
代碼實現
@RestController public class RedPacketController { @Autowired private RedisService redisService; @Autowired private RedPacketInfoMapper redPacketInfoMapper; @Autowired private RedPacketRecordMapper redPacketRecordMapper; private static final String TOTAL_NUM = "_totalNum"; private static final String TOTAL_AMOUNT = "_totalAmount"; /*** * 發紅包 * @param uid * @param totalNum * @return */ @ResponseBody @RequestMapping("/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); long redPacketId = System.currentTimeMillis(); //此時無法保證紅包id唯一,最好是用雪花算法進行生成分布式系統唯一鍵 record.setRedPacketId(redPacketId); redPacketInfoMapper.insert(record); redisService.set(redPacketId + "_totalNum", totalNum + ""); redisService.set(redPacketId + "_totalAmount", totalAmount + ""); return "success"; } /** * 搶紅包 * * @param redPacketId * @return */ @ResponseBody @RequestMapping("/getPacket") public Integer getRedPacket(long redPacketId) { String redPacketName = redPacketId + TOTAL_NUM; String num = (String) redisService.get(redPacketName); if (StringUtils.isNotBlank(num)) { return Integer.parseInt(num); } return 0; } /** * 拆紅包 * * @param redPacketId * @return */ @ResponseBody @RequestMapping("/getRedPacketMoney") public String getRedPacketMoney(int uid, long redPacketId) { Integer randomAmount = 0; String redPacketName = redPacketId + TOTAL_NUM; String totalAmountName = redPacketId + TOTAL_AMOUNT; String num = (String) redisService.get(redPacketName); if (StringUtils.isBlank(num) || Integer.parseInt(num) == 0) { return "抱歉!紅包已經搶完了"; } String totalAmount = (String) redisService.get(totalAmountName); if (StringUtils.isNotBlank(totalAmount)) { Integer totalAmountInt = Integer.parseInt(totalAmount); Integer totalNumInt = Integer.parseInt(num); Integer maxMoney = totalAmountInt / totalNumInt * 2; Random random = new Random(); randomAmount = random.nextInt(maxMoney); } //課堂作業:lua腳本將這兩個命令一起請求 redisService.decr(redPacketName, 1); redisService.decr(totalAmountName,randomAmount); //redis decreby功能 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(1111); redPacketRecord.setCreateTime(new Date()); redPacketRecordMapper.insertSelective(redPacketRecord); //這里應該查出RedPacketInfo的數量,將總數量和總金額減去 } }

@Service public class RedisService { @Autowired private RedisTemplate redisTemplate; private static double size = Math.pow(2, 32); /** * 寫入緩存 * * @param key * @param offset 位 8Bit=1Byte * @return */ public boolean setBit(String key, long offset, boolean isShow) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.setBit(key, offset, isShow); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入緩存 * * @param key * @param offset * @return */ public boolean getBit(String key, long offset) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.getBit(key, offset); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入緩存 * * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入緩存 * * @param key * @return */ public Object get(final String key) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); return operations.get(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 寫入緩存 * * @param key * @param value * @return */ public boolean decr(final String key, int value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.increment(key,-value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入緩存設置時效時間 * * @param key * @param value * @return */ public boolean set(final String key, Object value, Long expireTime) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 批量刪除對應的value * * @param keys */ public void remove(final String... keys) { for (String key : keys) { remove(key); } } /** * 刪除對應的value * * @param key */ public void remove(final String key) { if (exists(key)) { redisTemplate.delete(key); } } /** * 判斷緩存中是否有對應的value * * @param key * @return */ public boolean exists(final String key) { return redisTemplate.hasKey(key); } /** * 讀取緩存 * * @param key * @return */ public Object genValue(final String key) { Object result = null; ValueOperations<String, String> operations = redisTemplate.opsForValue(); result = operations.get(key); return result; } /** * 哈希 添加 * * @param key * @param hashKey * @param value */ public void hmSet(String key, Object hashKey, Object value) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); hash.put(key, hashKey, value); } /** * 哈希獲取數據 * * @param key * @param hashKey * @return */ public Object hmGet(String key, Object hashKey) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); return hash.get(key, hashKey); } /** * 列表添加 * * @param k * @param v */ public void lPush(String k, Object v) { ListOperations<String, Object> list = redisTemplate.opsForList(); list.rightPush(k, v); } /** * 列表獲取 * * @param k * @param l * @param l1 * @return */ public List<Object> lRange(String k, long l, long l1) { ListOperations<String, Object> list = redisTemplate.opsForList(); return list.range(k, l, l1); } /** * 集合添加 * * @param key * @param value */ public void add(String key, Object value) { SetOperations<String, Object> set = redisTemplate.opsForSet(); set.add(key, value); } /** * 集合獲取 * * @param key * @return */ public Set<Object> setMembers(String key) { SetOperations<String, Object> set = redisTemplate.opsForSet(); return set.members(key); } /** * 有序集合添加 * * @param key * @param value * @param scoure */ public void zAdd(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.add(key, value, scoure); } /** * 有序集合獲取 * * @param key * @param scoure * @param scoure1 * @return */ public Set<Object> rangeByScore(String key, double scoure, double scoure1) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); redisTemplate.opsForValue(); return zset.rangeByScore(key, scoure, scoure1); } //第一次加載的時候將數據加載到redis中 public void saveDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); boolean availableUsers = setBit("availableUsers", indexLong, true); } //第一次加載的時候將數據加載到redis中 public boolean getDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); return getBit("availableUsers", indexLong); } /** * 有序集合獲取排名 * * @param key 集合名稱 * @param value 值 */ public Long zRank(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.rank(key,value); } /** * 有序集合獲取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start,long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key,start,end); return ret; } /** * 有序集合添加 * * @param key * @param value */ public Double zSetScore(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.score(key,value); } /** * 有序集合添加分數 * * @param key * @param value * @param scoure */ public void incrementScore(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.incrementScore(key, value, scoure); } /** * 有序集合獲取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start,long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key,start,end); return ret; } /** * 有序集合獲取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end); return ret; } }
搶紅包功能擴展設計
- 將紅包 ID 的請求放入請求隊列中,如果發現超過紅包的個數,直接返回
- 類推出 token 令牌和秒殺設計原理
注意點
- 搶到紅包不代表能拆成功
- 2014 年的紅包一點開就知道金額,分兩次操作,先搶到金額,然后再轉賬。2015 年后的紅包的拆和搶是分離的,需要點兩次,因此會出現搶到紅包了,但點開后告知紅包已經被領完的狀況。進入到第一個頁面不代表搶到,只表示當時紅包還有。