年關將近,各類促銷活動即將上線,類似支付寶集五福的那種,用戶湊齊卡片之后,可以瓜分百萬紅包。
因為這種瓜分活動集齊的人數肯定是很多的,直接隨機之后再扣減,感覺不是很合適。
參考:https://www.cnblogs.com/canglong/p/canglong001.html?utm_source=itdadao&utm_medium=referral
大致思路如下:因為是集卡截止后再進行紅包瓜分的,集齊的用戶可能幾十萬上百萬,所以就先根據集齊卡片的用戶數,先將紅包隨機瓜分好,存放起來,等到瓜分的時候,直接領取就行了。
假設5個人集齊,等長度生成一個隨機數數組[2,5,9,8,6]。根據這個隨機數數組里面的值所占整個數組元素和(30)的比例來計算每個紅包的大小,如果是瓜分10快錢。生成的真實紅包數組[(2/30)*10,(5/30)*10,...],最后一個不要按比例計算,直接就是是剩余的錢,這樣就有可能出現最后的這個金額最大,我們再把生成這個數組重新洗牌一下,這樣以保證更好的隨機性。最后將這些已經生成好的紅包放到redis的list中。等到瓜分紅包的時候,每個用戶進來直接從list中彈出一個元素就行了,因為這個list本來就是隨機生成的。這樣也正好滿足了隨機性了。
紅包數組生成(一些細節都在代碼注釋里面):
package com.nijunyang.algorithm.redpackage; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; /** * Description: * Created by nijunyang on 2019/12/12 22:03 */ public class RedPackageUtils { /** * 按人數隨機分配紅包 * @param moneyTotal 紅包總額 * @param number 人數 * @return 隨機紅包集合 */ public static List<BigDecimal> shareMoney(BigDecimal moneyTotal, int number) { if (moneyTotal.compareTo(new BigDecimal(number).multiply(new BigDecimal("0.01"))) < 0) { throw new RuntimeException("每人至少一分錢."); } // 按分計算,錢轉換成分 long money = moneyTotal.multiply(BigDecimal.valueOf(100)).longValue(); //生成一個和人數一樣的數組,分布隨機數,然后計算隨機數占比,根據對應占比分錢。 double randomCount = 0; double[] randomArr = new double[number]; Random random = new Random(); for (int i = 0; i < number; i++) { int r = random.nextInt(number * 100) + 1; //避免出現0 randomArr[i] = r; randomCount += r; } // 根據每個隨機數占比計算每份紅包金額 long alreadyShare = 0; List<BigDecimal> moneyList = new ArrayList<>(number); for (int i = 0; i < number; i++) { // 每份占比 double ratio = randomArr[i] / randomCount; /** * 向下取整,如果用round,可能導致多個向上舍入之后,最后還沒分完,卻沒錢了,向下取整可以保證正能分完 * 這樣可能導致最后,最后剩余的那份相對而言多一點,最后再將整個集合重新洗牌shuffle */ long shareMoney = (long) Math.floor(ratio * money); // 幾率太小,總數太少,向下取整可能出現0,處理最少1分錢 if (shareMoney == 0) { shareMoney = 1; } alreadyShare += shareMoney; if (i < number - 1) { moneyList.add(new BigDecimal(shareMoney).divide(new BigDecimal(100))); } else { // 最后一份直接把剩余的錢分過去 moneyList.add(new BigDecimal(money - alreadyShare + shareMoney).divide(new BigDecimal(100))); } } //洗牌 Collections.shuffle(moneyList); return moneyList; } }
在往redis里面放的時候 發現有兩個比較坑的地方
1.ListOperations的 leftPushAll(K var1, Collection<V> var2) 這個方法 是以整個集合為一個元素去放的,等於說使用這個方法push之后,redis的list里面之后一個元素。不知道這個本來就是個bug,還是我對這個方法的理解和寫這個方法的人不一樣。(spring-data-redis版本2.1.10)
2..ListOperations的 leftPushAll(K var1, V... var2) 這個方法 數組長度過大無法添加,會報IO異常,因為不知道會有多少集齊,所以我從幾萬,幾十萬都沒問題,百萬就會報IO異常了:(java.io.IOException: 遠程主機強迫關閉了一個現有的連接)。測試了下長度100萬可以加入,110萬長度就會報錯了,暫時沒有去深入研究,應該代碼里面有長度限制的,如果長度太長的話,建議成幾個數組,依次添加進去。
redis代碼:方便測試都是用的get請求
@GetMapping("/push/redpackage/{money}/{number}") public ResponseEntity<Long> pushRedPackage(@PathVariable Integer money, @PathVariable Integer number) { List<BigDecimal> redPackageList = RedPackageUtils.shareMoney(BigDecimal.valueOf(money), number); /** * leftPushAll(K var1, Collection<V> var2) 以整個集合為一個元素形式存放 並不是單個元素存放 * leftPushAll(K var1, V... var2) 數組長度過大無法添加,會報IO異常,測試了下長度100萬可以110萬長度就會報錯了 */ String[] redPackages = new String[redPackageList.size()]; for (int i = 0; i < redPackages.length; i++) { redPackages[i] = redPackageList.get(i).toString(); } Long length = listOperations.leftPushAll(SHARE_RED_PACKAGE_KEY, redPackages); return new ResponseEntity<>(length, HttpStatus.OK); } @GetMapping("/share/redpackage") public ResponseEntity<Object> share() { Object money = listOperations.leftPop(SHARE_RED_PACKAGE_KEY); if (money == null) { return new ResponseEntity<>("紅包已瓜分完畢", HttpStatus.OK); } return new ResponseEntity<>(money, HttpStatus.OK); }
用Jmeter試了下,瓜分紅包的接口(從redis的list彈出數據)可以達到2000多點的QPS,本地起的單機服務,redis也是裝在vmware虛擬機中的1核2G,2000+感覺還是將就了。