微信抢红包模拟实现

2023-11-17 18:15:12 浏览数 (2)

微信抢红包模拟实现

1、抢红包介绍

微信抢红包基本流程:

  1. 发红包(拼手气红包)

需要发红包用户输入红包总个数、总金额,然后发红包。

2.抢红包

需要满足规则:

  1. 所有人抢到金额之和要等于红包总金额
  2. 每个人至少抢到一分钱
  3. 要保证所有人抢到金额的几率相等

2、二倍均值法

目前市面上主流实现是二倍均值算法(听说微信的红包实现是用的这个,应该是改良过的)

设剩余红包金额为 M,剩余人数为 N,每次抢到的金额 = 随机区间(0,M / N * 2)

分析:这样保证了每个随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。

假设有 10 个人,红包总金额 100,第一个人的随机范围是(0,100/10 * 2)=(0,20),平均金额 = 10;

假设第一个人随机到 10 元,第二个人的随机范围就是(0,90/9 * 2)=(0,20),平均金额 = 10;

假设第二个人随机到 10 元,第三个人的随机范围就是(0,80/8 * 2)=(0,20),平均金额 = 10。

以此类推,每一次的随机范围都相同,平均值也相同。二倍均值法保证了抢红包的公平性,但不能保证真正的随机性。因为除了最后一个人,前面任何一个人抢到的金额都一定小于当前人均金额的两倍,并不是真正的随机。

算法核心逻辑实现(拆分红包)

代码语言:javascript复制
    /**
     * 红包分割方法
     *
     * @param amount 总金额
     * @param min    每个红包最小值
     * @param num    红包数
     */
    public static List<BigDecimal> splitRedPackage(BigDecimal amount, BigDecimal min, BigDecimal num){
        List<BigDecimal> split = new ArrayList<>();
        BigDecimal remain = amount.subtract(min.multiply(num));
        final Random random = new Random();
        final BigDecimal hundred = new BigDecimal("100");
        final BigDecimal two = new BigDecimal("2");
        BigDecimal sum = BigDecimal.ZERO;
        BigDecimal redpeck;
        for (int i = 0; i < num.intValue(); i  ) {
            final int nextInt = random.nextInt(100);
            if(i == num.intValue() -1){
                redpeck = remain;
            }else{
                //RoundingMode.CEILING:取右边最近的整数
                //RoundingMode.FLOOR:取左边最近的正数
                redpeck = new BigDecimal(nextInt).multiply(remain.multiply(two).divide(num.subtract(new BigDecimal(i)),2, RoundingMode.CEILING)).divide(hundred,2, RoundingMode.FLOOR);
            }
            if(remain.compareTo(redpeck) > 0){
                remain = remain.subtract(redpeck);
            }else{
                remain = BigDecimal.ZERO;
            }
            sum = sum.add(min.add(redpeck));
            // 添加到List
            split.add(min.add(redpeck));
        }
        Assert.isTrue(compare(amount, sum), "切分红包出现异常");
        return split;
    }

    private static boolean compare(BigDecimal a, BigDecimal b){
        if(a.compareTo(b) == 0){
            return true;
        }
        return false;
    }

3、流程模拟实现

3.1 发红包接口

  • SendRedPackageDto
代码语言:javascript复制
/**
 * @program: red-package
 * @description: 发红包dto
 * @author: yuanshuai
 * @create: 2023-07-19 18:46
 **/
@Data
public class SendRedPackageDto {

    /**
     * 总金额
     */
    @NotBlank(message = "总金额不能为空")
    private String totalAmount;

    /**
     * 拆分红包个数
     */
    @NotBlank(message = "拆红包个数不能为空")
    private String redPackageNums;

    /**
     * 每个红包最小金额
     */
    @NotBlank(message = "红包最小金额不能为空")
    private String minAmount;

}
  • RedPackageV1Controller
代码语言:javascript复制
    /**
     * 发红包
     *
     * @param dto DTO 发红包参数
     * @return {@link Result}<{@link String}>
     */
    @PostMapping(value = "/send")
    public Result sendRedPackage(@Valid @RequestBody SendRedPackageDto dto) {
        try {
            // 验证红包个数不能小于1
            int redNums = Integer.parseInt(dto.getRedPackageNums());
            if (redNums <= 0) {
                return Result.build(null, ResultCodeEnum.PARAMS_ERROR);
            }
            // 验证总金额 和 最小金额 不能小于0
            BigDecimal total = new BigDecimal(dto.getTotalAmount());
            BigDecimal min = new BigDecimal(dto.getMinAmount());
            if (total.compareTo(BigDecimal.ZERO) < 0 || min.compareTo(BigDecimal.ZERO) < 0) {
                return Result.build(null, ResultCodeEnum.PARAMS_ERROR);
            }
            log.info("开始发红包, redNums:{}, total:{}, min:{}", redNums, total, min);
            return redPackageV1Service.sendRedPackage(redNums, total, min);
        }catch (Exception e) {
            log.error("请求发红包接口发生异常, e:{}", e.toString());
            return Result.build(null, ResultCodeEnum.PARAMS_ERROR);
        }

    }
  • RedPackageV1Service
代码语言:javascript复制
  /**
     * 发红包
     *
     * @param redNums 红包个数
     * @param total   总金额
     * @param min     每个红包最小金额
     * @return {@link Result}
     */
    public Result sendRedPackage(int redNums, BigDecimal total, BigDecimal min) {
        //1 拆红包,将总金额total拆分为redNums个子红包
        List<BigDecimal> splitRedPackages = RedPackageUtil.splitRedPackage(total, min, new BigDecimal(String.valueOf(redNums)));
        log.info("拆红包结果: {}", new Gson().toJson(splitRedPackages));
        //2 发红包并保存进list结构里面且设置过期时间
        String key = RedPackageUtil.generateUUID();
        // 设置红包拆分缓存 由于这里是发红包不存在并发 理论上可以写成两行
        redisTemplate.opsForList().leftPushAll(Constant.RED_PACKAGE_KEY   key, splitRedPackages);
        // 设置过期时间  默认24小时退回
        redisTemplate.expire(Constant.RED_PACKAGE_KEY   key, 1, TimeUnit.DAYS);

        //3 发红包OK,返回前台显示
        return Result.build(key, ResultCodeEnum.SUCCESS);
    }

3.2 抢红包

  • RedPackageV1Controller
代码语言:javascript复制
    /**
     * 抢红包
     *
     * @param redPackageKey 红包密钥
     * @param token         token
     * @return {@link Result}
     */
    @GetMapping(value = "/rob")
    public Result robRedPackage(@RequestParam("redPackageKey") String redPackageKey, @RequestHeader("token") String token) {
        log.info("开始抢红包, redPackageKey:{}, token:{}", redPackageKey, token);
        return redPackageV1Service.robRedPackage(redPackageKey, token);
    }
  • RedPackageV1Service
代码语言:javascript复制
    /**
     * 抢红包
     *
     * @param redPackageKey 红包密钥
     * @param token         token
     * @return {@link Result}
     */
    public Result robRedPackage(String redPackageKey, String token) {
        //1 验证某个用户是否抢过红包,不可以多抢
        Object redPackage = redisTemplate.opsForHash().get(Constant.RED_PACKAGE_CONSUME_KEY   redPackageKey, token);
        //2 没有抢过可以去抢红包,否则返回-2表示该用户抢过红包了
        if (null == redPackage) {
            //2.1 从红包池(list)里面出队一个作为该客户抢的红包,抢到了一个红包
            Object partRedPackage = redisTemplate.opsForList().leftPop(Constant.RED_PACKAGE_KEY   redPackageKey);
            if (partRedPackage != null) {
                //2.2 抢到红包后需要记录进入hash结构,表示谁抢到了多少钱的某个子红包
                redisTemplate.opsForHash().put(Constant.RED_PACKAGE_CONSUME_KEY   redPackageKey, token, partRedPackage);
                log.info("用户:{} 抢到了多少钱的红包:{}", token, partRedPackage);
                //TODO 后续异步进mysql或者MQ进一步做统计处理,每一年你发出多少红包,抢到了多少红包,年度总结
                return Result.build(partRedPackage, ResultCodeEnum.SUCCESS);
            }
            // 抢完了
            log.info("红包池已抢空,红包标识:{}", redPackageKey);
            return Result.build(null, ResultCodeEnum.RED_PACKAGE_FINISHED);
        }
        //3 某个用户抢过了,不可以作弊抢多次
        return Result.build(null, ResultCodeEnum.RED_PACKAGE_REAPT);

    }

3.3 模拟测试

3.3.1 先发红包 获取到一个uuid 作为本次红包的一个标识

发100块,分成三个包,每个包最低金额为0.01元

获取到uuid:f4860538-1bd6-4f83-bc4a-d0af8cf1ee9d

3.3.2 抢红包 注意一个用户是一个token 且只能抢一次

第一次抢(header的token一致就认为是一个用户)

同一个用户第二次抢

提示不能重复抢

当红包抢完后

这样基本模拟了简单的抢红包流程。

0 人点赞