baby sword‘s blog baby sword‘s blog
首页
  • java基础
  • java进阶
大数据
  • mysql

    • mysql索引
    • mysql日志
  • redis

    • 单机下的redis
    • 集群下的redis
  • Spring
  • springboot
  • RPC
  • netty
  • mybatis
  • maven
  • 消息队列
  • kafka
  • zookeeper
  • rocketmq
  • 七大设计原则
  • 创建型模式
  • 结构型模式
  • 行为型模式
  • SpringCloud

    • eureka
  • SpringCloud Alibaba

    • nacos
  • 计算机网络
  • 操作系统
  • 算法
  • 个人项目
  • 个人面试面经
  • 八股记忆
  • 工作积累
  • 逻辑题
  • 面试

    • 百度后端实习二面
GitHub (opens new window)

zhengjian

不敢承担失去的风险,是不可能抓住梦想的
首页
  • java基础
  • java进阶
大数据
  • mysql

    • mysql索引
    • mysql日志
  • redis

    • 单机下的redis
    • 集群下的redis
  • Spring
  • springboot
  • RPC
  • netty
  • mybatis
  • maven
  • 消息队列
  • kafka
  • zookeeper
  • rocketmq
  • 七大设计原则
  • 创建型模式
  • 结构型模式
  • 行为型模式
  • SpringCloud

    • eureka
  • SpringCloud Alibaba

    • nacos
  • 计算机网络
  • 操作系统
  • 算法
  • 个人项目
  • 个人面试面经
  • 八股记忆
  • 工作积累
  • 逻辑题
  • 面试

    • 百度后端实习二面
GitHub (opens new window)
  • 华仔聊技术

  • 业务设计

    • 微信红包实现方案
    • 分布式id
    • 数据库迁移设计
    • 单点登录相关知识
    • 风控平台累计量设置
    • 红包雨设计
      • 单机
        • 红包常见算法
  • 场景设计

  • 运维

  • 安全

  • 面试

  • mac相关工具推荐

  • 开发工具

  • 人工智能

  • 推荐

  • 阅读

  • 工具

  • 计划

  • 产品

  • 云原生

  • go

  • QVM

  • 软件设计师

  • 极客时间

  • 单元测试

  • 其他
  • 业务设计
xugaoyi
2023-07-22
目录

红包雨设计

# 红包基本业务

首先,在我们做一个红包系统之前,需要了解一个抢红包的基本业务

  1. 创建红包: 红包的发起者首先创建一个红包,设置红包的金额、数量以及其他限制条件(如单个红包金额上限、留言等)。
  2. 发放红包: 红包发起者将创建好的红包发送给参与抢红包的人,可以通过社交媒体、聊天应用、红包平台等进行发放。
  3. 领取红包: 参与抢红包的人收到红包后,可以点击打开红包进行领取。在某些情况下,为了增加趣味性,可能需要等待特定的时间或触发某些条件才能打开红包。
  4. 随机分配: 当参与者打开红包时,系统会进行随机分配,计算出该红包的具体金额,并将金额实时显示给领取者。
  5. 金额显示: 红包金额可能是固定的,也可能是随机分配的,参与者在打开红包后可以看到自己获得的具体金额。
  6. 金额入账: 红包金额入账到领取者的账户中,可以作为余额存储起来或者用于其他支付或提现等操作。
  7. 红包记录: 抢红包的过程会被记录下来,包括谁发起的红包、参与者名单以及每个人获得的金额等信息。
  8. **红包回退:**如果红包还有剩余的金额,需要回退给原发红包用户。

我们首先基于单机下看看如何设计红包系统。

image-20230722212402322

# 场景模拟

我们想象这样一个场景

  1. 老大发送了一个100元的红包,分成了10份,发送到群里供大家抢红包。(C2C)
  2. 某一个商家发起了一次红包雨,很多用户可以在一次红包雨中抢红包。红包雨以一定的速率下落。(B2C)

万变不离其中,上面两种情况都有一个共有的业务。就是拆红包、发红包、抢红包

疑惑点:

用什么数据结构本次的发红包记录

1.1 发红包。100元,分10个红包发送,用什么数据结构

1.2 分红包。要求大家分的金额都差不多。即金额拆分范围(1个人99远,其他10远分1元,这肯定是不行的)【即拆分红包算法是什么】

1.3 抢红包。如何在高并发下抢红包。

要求:数据一致性+高并发+不能加锁(效率)

1.4 记录本次红包活动的数据争抢情况。

  • 每个人分别抢到了多少,记录本次短期的。

  • 长期的统计并记录年度的红包总结(一年收发了多少红包)

  • 发送的红包,没人抢夺,没有任何一个红包被抢到,24小时候必须回退给红包发送者?如何回退?(一个人都没有抢,全部回退;部分人抢了红包,还有剩余,剩下的进行回退)

    记录分为:短期记录+长期记录+没有抢到的红包进行回退

# 解决方案

细化一下以上的业务需求即解决方案:

  1. 各种节假日,发红包+抢红包,100%高并发业务要求,不能用mysql来做 首先,这种有高并发量的任务,一般的核心操作都是在缓存redis中进行,然后将数据异步保存到mysql进行一个落盘操作。所以,抢红包我们用到的是redis进行操作。

  2. 一个总的大红包,有可能被拆分成多个小红包,总金额=分金额1+分金额2+……+分金额N 这里会用到一个红包领域通用的算法:二倍均值法 这是一种较为常见的红包分配算法。在每次分配红包时,随机生成一个红包金额,但要保证剩余红包金额的均值是前一次的一半。这样可以保证每个红包获得的金额较为均衡。

public static int[] splitRedPackageAlgorithm(int totalMoney, int redPackageNumber) {
        int[] redPackageNumbers = new int[redPackageNumber];
        int useMoney = 0;
        for (int i = 0; i < redPackageNumber; i++) {
            if (i == redPackageNumber - 1) {
                redPackageNumbers[redPackageNumber - 1] = totalMoney - useMoney;
            } else {
                int avgMoney = ((totalMoney - useMoney) / (redPackageNumber - i)) * 2;
                redPackageNumbers[i] = 1 + new Random().nextInt(avgMoney - 1);
            }
            useMoney = useMoney + redPackageNumbers[i];
        }
        return redPackageNumbers;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

其实除了这一个算法之外,还有其他的一些算法,也可以供参考文末的红包常见算法

以上,拆红包的逻辑就实现了,但是如何发红包呢?

我们可以把分好的红包放入一个队列中,对应抢红包的人从队列中拿红包就可以了。但是因为我们不想加锁,所以应该用什么队列呢?这时我们就可以使用redis队列。lpush、rpop。相应的代码案例如下:

@Operation(summary = "发红包")
    @GetMapping(value = "/send/{totalMoney}/{redPackageNumber}")
    public Result<String> sendRedPackage(@Parameter(name = "totalMoney", description = "红包金额", required = true) @PathVariable int totalMoney,
                                         @Parameter(name = "redPackageNumber", description = "红包个数", required = true) @PathVariable int redPackageNumber) {
        //1 拆红包,将总金额totalMoney拆分为redPackageNumber个子红包
        Integer[] splitRedPackages = RedPackageUtil.splitRedPackageAlgorithm(totalMoney, redPackageNumber);//拆分红包算法通过后获得的多个子红包数组
        log.info("拆红包: {}", JSON.toJSONString(splitRedPackages));
        //2 发红包并保存进list结构里面且设置过期时间
        String key = IdUtil.simpleUUID();
        redisTemplate.opsForList().leftPushAll(Constant.RED_PACKAGE_KEY + key, splitRedPackages);
        redisTemplate.expire(Constant.RED_PACKAGE_KEY + key, 1, TimeUnit.DAYS);

        //3 发红包OK,返回前台显示
        return Result.build(key, ResultCodeEnum.SUCCESS);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面的第二步,两个redisTemplate操作会发生并发问题吗?答案是不会的。

因为每一个线程会生成一个新的key,而redisTemplate也只会对一个key进行操作,相当于是只有一个用户进行发红包操作,是不会出现线程不安全的问题的。

  1. 若每个人只能抢一次红包(当然一个人抢对也可以,现在我们规定只能抢一个),你需要记录,比如100远,分拆分成10个红包发出去。总计有10个红包,抢一个少一个,总数显示(10/6)直到完,需要记录哪些人抢到了红包,重复抢作弊是不可以的

即当一个用户抢红包成功后,需要实时显示剩余多少,该用户是否已经抢过红包等,需要进行记录。本项目决定记录在redis当中,速率较快。且将抢的结果存放在redis hash中:

@Operation(summary = "抢红包")
    @GetMapping(value = "/rob1/{redPackageKey}")
    public Result robRedPackage1(@Parameter(name = "redPackageKey", description = "红包标识", required = true) @PathVariable String redPackageKey,
                                 @Parameter(name = "token", description = "用户标识", required = true) @RequestHeader("token") 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);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

上面会出现并发安全问题吗?

因为是redis,所有的操作在Object partRedPackage = redisTemplate.opsForList().leftPop(Constant.RED_PACKAGE_KEY + redPackageKey);这里都是串行执行的,最终不会出现数据一致性的问题。

  1. 有可能还需要计时,完整抢完,从出发到全部over,耗时多少

  2. 红包过期,或者群主人品极差,没人抢红包,原封不动退回

  3. 红包过期,剩余金额可能需要退回到红包主账户下

=====================================================================================================

以上是单机最小的一个微服务。如果并发量持续加到,应该怎么办。

1w->10w

集群:

image-20230722212439621

集群后可能会有的问题:

被薅羊毛了,发现后台有人盗刷,如何配置白名单和ip重点限制。gateway网管+ 隔离策略

削峰限流+服务熔断+服务降级+服务流控,分割+限流+软件负载

# 单机

前端界面

红包后台:配置界面中心

image-20230722214019956

image-20230722214041126

# 红包常见算法

  1. 随机分配算法: 最简单的算法是随机生成每个红包的金额。每个红包的金额是在一定范围内随机生成的,确保所有红包金额之和等于红包总金额。这种算法简单快捷,但可能导致某些红包金额过大或过小,不够公平。
import java.util.Random;

public class RedPacketAlgorithm {

    public static int[] randomDistribution(int totalAmount, int num) {
        Random random = new Random();
        int[] redPackets = new int[num];
        int remainingAmount = totalAmount;

        for (int i = 0; i < num - 1; i++) {
            int maxAmount = remainingAmount / (num - i);
            redPackets[i] = random.nextInt(maxAmount) + 1;
            remainingAmount -= redPackets[i];
        }

        redPackets[num - 1] = remainingAmount;
        return redPackets;
    }

    public static void main(String[] args) {
        int totalAmount = 1000; // 总金额,单位为分
        int num = 10; // 红包数量
        int[] result = randomDistribution(totalAmount, num);

        for (int i = 0; i < num; i++) {
            System.out.println("红包" + (i + 1) + ":" + result[i] + "分");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  1. 等额分配算法: 将红包总金额均分为n份,每个红包的金额都是相等的。这种算法保证了每个红包获得的金额一样,非常公平,但可能缺乏趣味性。
public class RedPacketAlgorithm {

    public static int[] equalDistribution(int totalAmount, int num) {
        int[] redPackets = new int[num];
        int amountPerPacket = totalAmount / num;

        for (int i = 0; i < num; i++) {
            redPackets[i] = amountPerPacket;
        }

        return redPackets;
    }

    public static void main(String[] args) {
        int totalAmount = 1000; // 总金额,单位为分
        int num = 10; // 红包数量
        int[] result = equalDistribution(totalAmount, num);

        for (int i = 0; i < num; i++) {
            System.out.println("红包" + (i + 1) + ":" + result[i] + "分");
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 随机金额分配算法: 在一定的范围内随机生成每个红包的金额,同时要保证所有红包金额之和等于红包总金额。与随机分配算法相比,这种算法可以更好地控制红包金额的分布,避免出现过大或过小的金额。
import java.util.Random;

public class RedPacketAlgorithm {

    public static int[] randomAmountDistribution(int totalAmount, int num) {
        Random random = new Random();
        int[] redPackets = new int[num];
        int remainingAmount = totalAmount;

        for (int i = 0; i < num - 1; i++) {
            int maxAmount = remainingAmount - (num - i - 1);
            int currentAmount = random.nextInt(maxAmount) + 1;
            redPackets[i] = currentAmount;
            remainingAmount -= currentAmount;
        }

        redPackets[num - 1] = remainingAmount;
        return redPackets;
    }

    public static void main(String[] args) {
        int totalAmount = 1000; // 总金额,单位为分
        int num = 10; // 红包数量
        int[] result = randomAmountDistribution(totalAmount, num);

        for (int i = 0; i < num; i++) {
            System.out.println("红包" + (i + 1) + ":" + result[i] + "分");
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  1. 二倍均值法: 这是一种较为常见的红包分配算法。在每次分配红包时,随机生成一个红包金额,但要保证剩余红包金额的均值是前一次的一半。这样可以保证每个红包获得的金额较为均衡。
import java.util.Random;

public class RedPacketAlgorithm {

    public static int[] doubleAverageDistribution(int totalAmount, int num) {
        Random random = new Random();
        int[] redPackets = new int[num];
        int remainingAmount = totalAmount;
        int remainingNum = num;

        for (int i = 0; i < num - 1; i++) {
            int maxAmount = remainingAmount / remainingNum * 2;
            int currentAmount = random.nextInt(maxAmount) + 1;
            redPackets[i] = currentAmount;
            remainingAmount -= currentAmount;
            remainingNum--;
        }

        redPackets[num - 1] = remainingAmount;
        return redPackets;
    }

    public static void main(String[] args) {
        int totalAmount = 1000; // 总金额,单位为分
        int num = 10; // 红包数量
        int[] result = doubleAverageDistribution(totalAmount, num);

        for (int i = 0; i < num; i++) {
            System.out.println("红包" + (i + 1) + ":" + result[i] + "分");
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  1. 线段切割法: 将红包总金额看作一条线段,随机在线段上选择n-1个点,然后将线段切割成n段,每段对应一个红包金额。这种算法也能实现较为均衡的红包分配。
import java.util.Random;

public class RedPacketAlgorithm {

    public static int[] segmentCuttingDistribution(int totalAmount, int num) {
        Random random = new Random();
        int[] redPackets = new int[num];
        int[] points = new int[num - 1];

        for (int i = 0; i < num - 1; i++) {
            points[i] = random.nextInt(totalAmount - num) + 1;
        }

        Arrays.sort(points);

        redPackets[0] = points[0];
        for (int i = 1; i < num; i++) {
            redPackets[i] = points[i - 1] - points[i];
        }
        redPackets[num - 1] = totalAmount - points[num - 2];
        return redPackets;
    }

    public static void main(String[] args) {
        int totalAmount = 1000; // 总金额,单位为分
        int num = 10; // 红包数量
        int[] result = segmentCuttingDistribution(totalAmount, num);

        for (int i = 0; i < num; i++) {
            System.out.println("红包" + (i + 1) + ":" + result[i] + "分");
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
编辑 (opens new window)
上次更新: 2024/02/22, 14:03:19
风控平台累计量设置
厂商分配问题

← 风控平台累计量设置 厂商分配问题→

最近更新
01
spark基础
02-22
02
mysql读写分离和分库分表
02-22
03
数据库迁移
02-22
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式