Redis+Lua解决 万/s 级别 抢红包方案

https://github.com/ZhuBaker/redis-lua

一、技术方案

说到抢红包,我们肯定会想到拆红包和抢红包两个业务场景,拆红包是指将制定金额拆分为指定数目的红包的过程,即用来确定每个红包的金额,抢红包是典型的高并发场景,需要放置红包超发的情况。

红包拆分:

1.实施拆分

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

2.预先拆分

预先生成,指的是红包在开抢之前已经完成了红包的拆分工作,抢红包的过程实际上是依次取出拆分好的红包金额,对拆分算法要求较低,可以拆分出随机性较好的红包金额,通常需要结合队列来进行使用。

拆分算法

现在还并没有找到业界通用的算法,但红包拆分算法应该是拆分金额要看起来随机,最好能够服从正态分布,可以参考微信或者@lcode提供的红包拆分算法。

微信拆分算法的优点是算法较为简单,拆分效率高,同事由于该算法天然的特性,能够保证后续红包金额一定不为空,特别适合实时拆分的场景,但缺点是会导致大额红包较大概率地在拆分的最后出现。@lcode拆分算法的有点是拆分金额基本符合正态分布,适合随机性要求较高的红包拆分场景。

我们的方案

红åå ææµç¨å¾

如果对红包拆分的随机性要求不高,但是对系统的可靠性要求较高,我们选用预生成方式,使用二倍均值的 红包拆分算法,作为我们的红包拆分方案。

采用预生成方式,我们先把预生成的红包放入Redis的List中,当抢红包时只是Pop List即可,具体实现将在下面介绍。

拆分算法可以描述为:假设剩余拆分金额为M , 剩余拆分红包个数为N , 红包最小金额为1元,红包最小单位为元, 那么定义当前红包的金额为:

m=rand(1,floor(M/N∗2))

其中floor向下取整, rand(min,max)表示从[min,max] 区间随机一个值,M/N*2 标识剩余平均金额的两倍,因为N >=2 ,所以 M/N*2 < M , 表示一定能保证后续的红包拆分到金额。

Java实现:

package com.qf58.exec.redpackage;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * Created with IntelliJ IDEA.
 *
 * @author: zhubo
 * @description: 红包拆分工具类
 * @time: 2018年06月07日
 * @modifytime:
 */
public class RadPackageUtils {

    private static final Random random = new Random();

    /**
     * 拆分核心算法
     * @param num 拆分红包个数
     * @param sumMoney 总金额
     * @return
     */
    public static List<Integer> getPackage(Integer num , Integer sumMoney){
        List<Integer> pak = new ArrayList<>();
        while(num > 1){
            Integer m = getMoney(num,sumMoney);
            num -- ;
            sumMoney -= m;
            pak.add(m);
        }
        pak.add(sumMoney);
        return pak;
    }

    private static Integer getMoney(Integer num , Integer restMoney){
        int floor = (int)Math.floor(restMoney * 1.0d / num * 2);
        return getRandom(1,floor);
    }

    public static Integer getRandom(Integer min,Integer max){
        int i = random.nextInt(max - min + 1) + min;
        return i;
    }

    public static void main(String[] args) {
        List<Integer> aPackage = getPackage(1000, 50000000);
        System.out.println(aPackage.size());
        Integer j = 0;
        for (Integer i : aPackage){
            j = j + i;
        }
        System.out.println(j);
        System.out.println(aPackage);
    }
}

值得一提的是,我们为了保证红包金额差异尽量小,先将总金额平均拆分成 N+1 份,将第 N+1 份红包按照上述的红包拆分算法拆分成 N 份,这 N 份红包加上之前的平均金额才作为最终的红包金额。

红包占有流程图如下:

红åå ææµç¨å¾

其中,red::list为 List 结构,存放预先生成的红包金额(流程①中的红包队列);red::task也为 List 结构,红包异步发放队列(流程②中的任务队列);red::draw为 Hash 结构,存放红包领取记录,key 为用户的 openid,value为序列化的红包信息;red::draw_count:u:openid为 k-v 结构,用户领取红包计数器。

下面,我将以以下 3 个问题为中心,来说说我们设计出的抢红包系统。

1.如何保证不超发

我们需要保证红包的占有过程,从红包占有的流程图中可以看出,这个过程是很多key操作的组合 , 那么怎么保证原子性? 可以使用redis事物,但我们选用了lua方案,一方面是因为首先要保证性能,而lua脚本嵌入 redis执行不存在性能瓶颈,另一方面lua脚本在执行时本身就是原子的,满足需求,并且 lua脚本内部可以写逻辑。

红包占有的Lua 脚本实现如下:

-- 抢红包脚本
--[[
--red:list 为 List 结构,存放预先生成的红包金额
red:draw_count:u:openid 为 k-v 结构,用户领取红包计数器
red:draw为 Hash 结构,存放红包领取记录
red:task 也为 List 结构,红包异步发放队列
openid 为用户的openid
]]--
local openid = KEYS[1]
local isDraw = redis.call("HEXISTS","red:draw",openid)
-- 已经领取
if isDraw ~= 0 then
    return true
end
-- 领取太多次了
local times = redis.call("INCR","red:draw_count:u:"..openid)
if times and tonumber(times) > 9 then
    return 0
end
local number = redis.call("RPOP","red:list")
-- 没有红包
if not number then
    return {}
end
-- 领取人昵称为Fhb,头像为 https:// xxxxxx
local red = {money=number,name=KEYS[2] , pic = KEYS[3] }
-- 领取记录
redis.call("HSET","red:draw",openid,cjson.encode(red))
-- 处理队列
red["openid"] = openid
redis.call("RPUSH","red:task",cjson.encode(red))

return true

需要注意Lua 脚本执行过程中并不是事物的,脚本中的操作命令在执行时是有先后顺序的,当某个操作执行失败时并不会回滚执行成功的操作,它的原子性是通过单线程模型来实现的

2.怎么提高系统响应速度

如红包占有流程图所示,当用户发起抢红包请求时,若有红包则直接完成红包占有操作,同步告知用户是否已经抢到红包,这个过程要求快速响应。

但由于微信红包支付属于第三方调用,若抢到红包后同步调用红包支付,系统调用链又长又慢,所以红包占有和红包发放异步拆分是必然的。拆分后,红包占有只需操作Redis , 响应已不是问题。

3.怎么提高系统处理能力

从上述分析可知,目前系统压力都集中在红包发放的环节,因为用户抢到红包时,我们只是告知用户已抢到红包,然后异步发放红包,因此用户并不会立即受到红包(受到 红包发放Worker处理能力和微信服务压力的制约)。若红包发放的Worker处理能力较弱, name红包发放的延迟就会较高。 体验较差。

如抢红包流程图中所示,我们采用一组 Worker 去消费任务队列,并调用红包支付 API,以及数据持久化操作(后续对账)。尽管红包发放调用链又长又慢,但是注意到这些 Worker 是 无状态 的,所以可以通过增加 Worker 数量,以横向扩展提高系统的处理能力。

4.怎么保证数据一致性

其实,红包发放延时我们可以做到用户无感知,但是若红包发放(流程②)失败了,已经告知用户抢到红包,但是却木有发,估计杀人的心都有了。 根据 CAP 原则,我们无法同时满足数据一致性, 数据可用性, 分区容错性, 通常我们需要做到数据一致性。

为了达到数据一致性,我们引入了重试机制, 生成一个全剧唯一的外部订单号, 当某单红包发放失败,就会放回任务队列,使得有机会进行发放重试,当然需要 API 做幂等处理。

 

源码参考:https://github.com/ZhuBaker/redis-lua

赞赏支持

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

转载于:https://my.oschina.net/LucasZhu/blog/1827116

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值