写点什么

基于 Redis 实现基本抢红包算法

  • 2024-04-17
    北京
  • 本文字数:3978 字

    阅读完需:约 13 分钟

简介:

抢红包是我们生活常用的社交功能, 这个功能最主要的特点就是用户的并发请求高, 在系统设计上, 可以使用非常多的办法来扛住用户的高并发请求, 在本文中简要介绍使用 Redis 缓存中间件来实现抢红包算法, Redis 是一个在内存中基于 [key, value] 的缓存数据库, Redis 官方性能描述非常高, 所以面对高并发场景, 使用 Redis 来克服高并发压力是一个不错的手段, 本文主要基于 Redis 来实现基本的抢红包系统设计.

发红包模块:

1:发红包模块流程图如下:


用户首先输入红包金额和红包个数, 然后生成当前红包唯一标识, 并使用二倍均值算法生成随机金额的红包, 然后将生成的红包存入缓存 Redis 数据库中, Redis 数据库中会保存当前剩余的红包数量和每个红包的金额, 由于 Redis 数据库是作为临时存储的地方, 所以发红包记录需要持久化存储在数据库中, 这里为加快系统响应, 使用异步的方式, 将红包金额纪录存储入 Mysql 数据库中, 以上就是发红包模块的简要系统设计.

2:随机生成红包金额

对于抢红包来说, 生成红包金额是非常关键的, 这里有许多生成随机数方法, 在本文中介绍一种使用较多的二倍均值算法来随机生成红包金额.对于抢红包来说, 如果发送一个金额为 J 的红包, 那么对与抢红包的 N 个人来说, 公平的概率是: 每个人抢到 J / N 的金额的概率是相同的, 例如 100 元红包发给 10 个人,那么最公平的策略是使每个人抢到 10 元的概率相同, 二倍均值算法就是基于上面这个概率策略. 二倍均值算法流程如下: 首先设置红包金额为 J, 抢红包人数为 N, 接下来计算随机数区间上 U = J / N * 2, 得到随机数区间(0,U), 从而在这个区间里生成第一个随机数金额 M, 接下来继续生成第二个随机金额. 首先更新总红包金额为 J-M,总抢红包人数为 N-1, 然后生成第二个随机金额区间(0, (J-M) / (N-1) *2) , 从这个区间里面生成第二个随机金额 M2, 继续迭代, 直到生成最后一个红包金额, 下图是二倍均值算法的流程



二倍均值算法案例: 红包总金额 100 元, 总计 10 个人


计算第一个随机金额区间: 100/10X2 = 20, 第一个随机金额的区间是(0,20 ),区间均值为 10


假设第一个人抢到 10 元,剩余金额是 90 元


计算第二个随机金额区间: 90/9X2 = 20, 第一个随机金额的区间是(0,20 ),区间均值为 10


假设第二个人抢到 10 元,剩余金额是 80 元 计算第三个随机金额区间: 80/8X2 = 20, 第一个随机金额的区间是(0,20 ),区间均值为 10


...............


所以使用二倍均值算法能够在不论谁先抢的情况下, 都能公平保证每个人抢到平均金额的概率是相等的, 二倍均值算法生成红包金额的代码如下:


//这里输入的totalMoney单位是分,例如100元,totalMoney = 10000public List<Integer> getRedPackage(Integer totalMoney,Integer totalPeopleCount) {    List<Integer> moneyList = new ArrayList<>();    //暂存剩余金额为红包的总金额    Integer restMoney = totalMoney;    //暂存剩余的总人数-初始化时即为指定的总人数    Integer restPeopleCount = totalPeopleCount;        //随机数对象    Random random = new Random();    //开始循环迭代生成红包    for (int i =0;i< totalPeopleNum-1;i++){       //加1是为了至少抢到1分钱       int money = random.nextInt (restMoney / restPeopleCount * 2) + 1;       restMoney -= money;       restPeopleCount--;       moneyList.add(money);    }    //添加最后的一个红包金额    amountList.add(restAmount);    return amountList;}
复制代码

3: 红包存储

为了应对用户高并发的请求, 也就是需要频繁读取红包金额和数量, 所以将红包金额和数量存储在 Mysql 中是不行的, 所以只能借助基于内存的 Redis 数据库来支持高并发的读取操作.Redis 中有 5 种基本的数据结构分别是:String, List, Set, Sorted Set, Map 这五种, 红包金额数量是一个 List 集合, 所以使用 List 来存储最为合适,在发红包时, 我们先用二倍均值算法随机生成一定数量的红包金额, 然后将红包金额和红包数量存入 Redis 缓存中,等待用户抢红包


//随机生成全局唯一的红包idredId = getRedId();//首先生成红包金额List<Integer> moneyList = getRedPackage(totalMoney,totalPeopleCount);//放入redisredisClient.lpush(redId, moneyList);//redis中记录红包个数redisClient.set(redId, moneyList.size());//异步存储发红包记录到Mysql数据库//将红包id返回return redId;
复制代码

抢红包模块:

1:抢红包模块流程图如下:


首先判断用户是否已经抢过红包了, 是否还有剩余的红包, 如果抢过或者剩余红包数量小于等于 0, 则代表红包已经被抢完了, 直接结束用户本次抢红包流程. 如果还有剩余的红包数量, 则从 Redis 缓存列表中弹出一个红包金额, 然后将剩余红包数量减 1, 同时异步将用户抢红包记录存入 Mysql 数据库, 最后将抢到的红包金额返回给用户, 结束本次抢红包流程

2:首先判断是否已经抢过红包

通过在 Redis 中以用户 ID 构建一个唯一 Key 来判断是否抢过红包, Key 的构建规则是:业务前缀+红包 id+用户 id


redMoney = redisClient.get("rob" + redId + useId)//如果不为空,则说明已经抢过了,直接返回抢过的红包金额if (redMoney != null) {    return redMoney}
复制代码

3:判断是否还有红包

通过在 Redis 中以红包 id 记录一个数量来判断是否还有红包, key 的构建规则是:业务前缀+红包 id


totalNum = redisClient.get("totalNum" + redId)//如果为空或者小于等于0则代表没有了if (totalNum == null || totalNum <= 0) {    return null}
复制代码

4:弹出一个红包金额

因为我们是把红包金额存储到 Redis 的 List 列表中的, 所以直接使用列表的 Pop 操作就行了


money = redisClient.rpop(redId)//如果不为空,则说明抢到了if (money != null) {    ....    红包个数减1    存储抢红包记录    设置该用户已经抢过红包    ....    //返回抢到的金额    return money}//没抢到return null
复制代码

5:减少红包个数

红包总数是以一个[key, value] 键值对存储在 Redis 中的, 所以这里使用 Redis 的 DECR 命令就行了


money = redisClient.rpop(redId)//如果不为空,则说明抢到了if (money != null) {    //红包个数减1    redisClient.decr(redId)    ....    存储抢红包记录    设置该用户已经抢过红包    ....    //返回抢到的金额    return money}//没抢到return null
复制代码

6:异步记录抢红包记录

采用异步的方式将记录存入 Mysql 数据库, 异步的方式可以采用消息队列或者多线程的方式来实现


money = redisClient.rpop(redId)//如果不为空,则说明抢到了if (money != null) {    //红包个数减1    redisClient.decr(redId)    //异步存储抢红包记录    这里可以使用mq或者多线程的方式来实现    ....    设置该用户已经抢过红包    ....    //返回抢到的金额    return money}//没抢到return null
复制代码

7:设置该用户已经抢过红包

money = redisClient.rpop(redId)//如果不为空,则说明抢到了if (money != null) {    //红包个数减1    redisClient.decr(redId)    //异步存储抢红包记录    这里可以使用mq或者多线程的方式来实现    //设置该用户已经抢过红包    redisClient.set("rob" + redId + useId, money)    //返回抢到的金额    return money}//没抢到return null
复制代码

8: 整体的伪代码逻辑如下:

redMoney = redisClient.get("rob" + redId + useId)//如果不为空,则说明已经抢过了,直接返回抢过的红包金额if (redMoney != null) {    return redMoney}totalNum = redisClient.get("totalNum" + redId)//如果红包总数小于0, 则代表已经抢完了, 直接返回空if (totalNum == null || totalNum <= 0) {    return null}money = redisClient.rpop(redId)//如果不为空,则说明抢到了if (money != null) {    //红包个数减1    redisClient.decr(redId)    //异步存储抢红包记录    这里可以使用mq或者多线程的方式来实现    //设置该用户已经抢过红包    redisClient.set("rob" + redId + useId, money)    //返回抢到的金额    return money} //没抢到return null
复制代码

9:分布式锁

这里涉及到了同一个用户多次高并发来抢红包的情况, 并且代码逻辑中包含了下面这种逻辑: 判断条件成立然后进行业务操作,最后设置条件. 这种业务逻辑如果不防止并发的话, 就会产生重复操作, 所以需要使用锁来限制每一个用的访问频率, 加锁的方式是使用分布式锁, 这是因为我们抢红包服务不可能只在一台服务器上部署, 同时基于 Redis 也能很容易的实现分布式锁, 使用 Redis 命令 setNx 命令就可以实现简单分布式锁


redMoney = redisClient.get("rob" + redId + useId)//如果不为空,则说明已经抢过了,直接返回抢过的红包金额if (redMoney != null) {    return redMoney}totalNum = redisClient.get("totalNum" + redId)//如果红包总数小于0, 则代表已经抢完了, 直接返回空if (totalNum == null || totalNum <= 0) {    return null}//加分布式锁lockResut = redisClient.setNx(useId,redId,timeOut);//加锁失败,直接返回if(!lockResult){    return;}try{    money = redisClient.rpop(redId)    //如果不为空,则说明抢到了    if (money != null) {        //红包个数减1        redisClient.decr(redId)        //异步存储抢红包记录        这里可以使用mq或者多线程的方式来实现        //设置该用户已经抢过红包        redisClient.set("rob" + redId + useId, money)        //返回抢到的金额        return money    }     } finally {    //删除锁    redisClient.del(useId)}//没抢到return null
复制代码

总结

以上就是完整的抢红包伪代码流程, 可以基本实现发红包以及抢红包功能, 该方法基于 Redis 来实现红包的存储和抢红包的操作, 基于二倍均值算法来实现红包金额的随即生成, 在整体功能上还有很多不完善的地方, 可以基于整体框架进行扩展开发, 实现更加完整的算法

发布于: 刚刚阅读数: 5
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
基于Redis实现基本抢红包算法_京东科技开发者_InfoQ写作社区