随机瓜分百万红包
年关将近,各类促销活动即将上线,有个需求类似支付宝集五福的那种,用户凑齐卡片之后,可以瓜分百万红包。
因为这种瓜分活动集齐的人数肯定是很多的,直接随机之后再扣减,感觉不是很合适。
网上搜了下也没有搜到合适的方案,比如红包怎么拆分的实现这些。最后红包拆分的灵感来自于程序员小灰:http://www.sohu.com/a/229372464_115128。
大致思路如下:按照集齐卡片的用户数,比如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弹出数据)可以达到10000多点的QPS,本地起的单机服务,redis也是装在vmware虚拟机中的,10000+感觉还是将就了。
如果各位大佬有好的方案,希望论评交流。