年关将近,各类促销活动即将上线,有个需求类似支付宝集五福的那种,用户凑齐卡片之后,可以瓜分百万红包。

因为这种瓜分活动集齐的人数肯定是很多的,直接随机之后再扣减,感觉不是很合适。

网上搜了下也没有搜到合适的方案,比如红包怎么拆分的实现这些。最后红包拆分的灵感来自于程序员小灰: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+感觉还是将就了。

如果各位大佬有好的方案,希望论评交流。

版权声明:本文为nijunyang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/nijunyang/p/12037705.html