玖叶教程网

前端编程开发入门

高并发, 如何利用redis设计一个抢红包功能

业务描述

模拟微信设计开发一个发、抢红包的通用功能,提供给公司业务线使用。

需求分析

  1. 在特殊的活动日的时候,发红包+抢红包,要应对高并发的业务请求,所以不可以直接用MySQL来做,考虑使用redis 、异步处理的机制。
  2. 拆分红包的时候,如何做到尽量平均一点,不要出现100的红包经过拆分后其中一个红包到99元这种情况,会影响用户参与的活跃性。
  3. 每个用户只能抢一次,并且需要记录下那些人抢到了红包。
  4. 所有红包被抢完的总时间是否记录? 回显使用。
  5. 红包过期,剩余金额的退回。或者没人抢红包,金额原封不动的退回。

架构设计

抢红包其实主要有 2 个核心流程:红包金额拆分->抢红包

  • 红包金额拆分:是指将指定金额拆分为指定数目红包的过程,用来确定每个红包的金额数;
  • 抢红包:是用户抢红包的这个操作,典型的高并发场景,需要系统扛流量且避免红包超发的情况,重复抢的情况;

红包金额拆分

可选的方案

  • 实时拆分

实时拆分,指的是在抢红包时实时计算每个红包的金额,以实现红包的拆分过程,对系统性能和拆分算法要求较高,例如拆分过程要一直保证后续待拆分红包的金额不能为空,不容易做到拆分的红包金额服从正态分布规律。

  • 预先生成

预先生成,指的是在红包开抢之前已经完成了红包的金额拆分,抢红包时只是依次取出拆分好的红包金额,对拆分算法要求较低,可以拆分出随机性很好的红包金额,通常需要结合队列使用。

我们这次的场景对红包金额的随机性要求不高,但是对系统可靠性要求较高,所以我们选用了预先生成方式,使用 二倍均值法 的算法拆分红包金额。

拆分算法可以描述为:假设剩余拆分金额为 M,剩余待拆分红包个数为 N,红包最小金额为 1 元,那么定义当前红包的金额为:m=rand(1,floor(M/N?2))

代码实现

/**
     * 拆红包的算法--->二倍均值算法
     * @param totalMoney
     * @param redPackageNumber
     * @return
     */
    private Integer[] splitRedPackageAlgorithm(int totalMoney,int redPackageNumber) {
        Integer[] redPackageNumbers = new Integer[redPackageNumber];
        //已经被抢夺的红包金额,已经被拆分塞进子红包的金额
        int useMoney = 0;

        for (int i = 0; i < redPackageNumber; i++) {
            if(i == redPackageNumber - 1) {
                redPackageNumbers[i] = totalMoney - useMoney;
            }else{
                //二倍均值算法,每次拆分后塞进子红包的金额 = 随机区间(1,(剩余红包金额M ÷ 未被抢的剩余红包个数N) * 2)
                int avgMoney = ((totalMoney - useMoney) / (redPackageNumber - i)) * 2;
                redPackageNumbers[i] = 1 + new Random().nextInt(avgMoney - 1);
            }
            useMoney = useMoney + redPackageNumbers[i];
        }
        return redPackageNumbers;
    }

抢红包

我们使用的在发红包的时候使用的是redis的 list

/**
 * 发红包
 * @param totalMoney
 * @param redPackageNumber
 * @return
 */
@RequestMapping(value = "/send")
public String sendRedPackage(int totalMoney,int redPackageNumber) {
    //1 拆红包,将总金额totalMoney拆分为redPackageNumber个子红包
    Integer[] splitRedPackages = splitRedPackageAlgorithm(totalMoney,redPackageNumber);//拆分红包算法通过后获得的多个子红包数组
    //2 发红包并保存进list结构里面且设置过期时间
    String key = RED_PACKAGE_KEY+ IdUtil.simpleUUID();
    redisTemplate.opsForList().leftPushAll(key,splitRedPackages);
    redisTemplate.expire(key,1, TimeUnit.DAYS);
	//todo 异步记录当前用户以及当前红包的基本信息到DB
    //3 发红包OK,返回前台显示
    return key;
}

所以在抢红包整个过程先只操作 Redis,且都是简单高效的 Pop 和 Push 命令操作。

抢红包流程:先从红包队列中 Pop 占有红包,然后通过mq进一步做统计处理,例如:每一年你发出多少红包,抢到了多少红包,年度总结,打款处理等),并同步告知用户抢到红包的结果,抢红包流程就结束了。

/**
 * 抢红包
 * @param totalMoney
 * @param redPackageNumber
 * @return
 */
@RequestMapping(value = "/rob")
public String robRedPackage(String redPackageKey,String userId) {
    //1 验证某个用户是否抢过红包,不可以多抢
    Object redPackage = redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KEY + redPackageKey, userId);
    //2 没有抢过可以去抢红包,否则返回-2表示该用户抢过红包了
    if(null == redPackage) {
        //2.1 从大红包(list)里面出队一个作为该客户抢的红包,抢到了一个红包
        Object partRedPackage = redisTemplate.opsForList().leftPop(RED_PACKAGE_KEY + redPackageKey);
        if(partRedPackage != null) {
            //2.2 抢到红包后需要记录进入hash结构,表示谁抢到了多少钱的某个子红包
            redisTemplate.opsForHash().put(RED_PACKAGE_CONSUME_KEY + redPackageKey,userId, partRedPackage);
            //TODO 后续异步进mysql或者MQ进一步做统计处理,每一年你发出多少红包,抢到了多少红包,年度总结
            return String.valueOf(partRedPackage);
        }
        // 抢完了
        return "红包抢完了";
    }
    //3 某个用户抢过了,不可以作弊抢多次
    return "你已经抢过红包了,不能重新抢";
}

看上述的代码我们可以看出大致流程如下

  1. 我们首先验证了一下合格用户是否抢过红包,避免重复抢。
  2. 没有抢过我们就要从list中拿出一个红包,给到当前的用户,如果没有拿到就代表这当前红包已经被抢完了。
  3. 抢到红包后,我们要记下这条记录,谁抢到哪个红包中的多少钱。这个地方我们选用的是redis 的 hash 结构。

后续

当然上面只是一个抢红包功能简单的介绍,目的还是用于理解redis的数据结构该如何使用,真实的抢红包还是有太多的地方要考虑的,比如:限流,降级等等。

限流

前端限流

前端限制用户在 n 秒之内只能提交一次请求,虽然这种方式只能挡住小白(99% 的用户),所以也必须得做。

后端限流

常用的后端限流方法有 漏桶算法令牌桶算法。漏桶算法 主要目的是控制请求数据注入的速率,如果此时漏桶溢出,后续的请求数据会被丢弃。而 令牌桶算法 是以一个恒定的速度往桶里放入令牌,而如果请求数据需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌时,这些请求才被丢弃,令牌桶算法的一个好处是可以方便地改变应用接受请求的速率。

降级措施

在每个关键节点都增加开关,一但出现异常,可以通过配置中心人工介入做降级处理

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言