zoukankan      html  css  js  c++  java
  • 聊聊红包雨背后的逻辑

    2017年临近年关的时候接到开发年会红包雨的任务,时间紧任务重且之前没有这块经验。这个项目看似很简单,如果参加年会人数少,一个晚上可以搞出demo,如果是几万人则难度成指数级。领导说,当天现场有几百位参会,场外全集团估计有几万人,公司下属子公司从事的业务多,这次年会基本上每家子公司都有预算,如机票,饮料券,商场代金券,积分,现金,保养券,合作方赠送的礼品等全部以红包雨的形式发放出去,当天会有多场红包雨活动。

    经过和产品,技术一个晚上的需求讨论基本上把项目拆分完毕。

    系统划分大致如下

    1、后台管理服务,方便年会人员的工作,比如上架奖品;设置合适的活动时间,规则;开启活动;暂停活动;拉黑企业成员;禁止某些人抢红包;发布公告;统一推送消息;发送短信等。

    2、鉴权服务,所有进来抢红包的用户都需要经过这个服务,统一鉴权用户(是否登录,合法性,平衡性,活动状态等),如果流量太大或者中奖人数不平衡还要根据用户标签(司龄,中奖次数,所属子公司)进行限流。目的是尽可能的平均化各个子公司中奖人数。举个例子,A子公司人数有几千人,第一场红包雨奖品大部分被A子公司抢走,那么就要适当对A子公司人员进行管控,第二场活动可能大部分A子公司成员在鉴权时候直接踢掉,无法进入中奖服务。

    3、中奖服务,用户通过鉴权之后,开始进入真正的抢红包业务,所点即所得,如果中奖(log4j2+disruptor)会记录一条详细日志(这是最原始的中奖依据)。

    4、消息消费服务,统一处理用户中奖信息,每场红包雨活动的奖品数量和持续时间是不一致的,比如第一场活动1分钟,100000份奖品,如果中奖纪录直接入库则压力是很大的。所以这里使用消息中间件流量削峰,异步批量存入缓存,DB入库。

    5、定时服务,提前预热活动任务,中奖核对任务,过期作废任务,提现任务等。比如张三的中奖日志和中奖DB数据不一致,中奖核对任务需要清洗出类似这样的异常情况,及时通知技术,运营排查,防止出现不愉快。张三中了一张有期限的购物卡,过期了就需要作废掉不允许兑换。

    6、查询服务,活动进行中和结束后,大量用户会查看活动详情和自己中奖信息,这一块采取措施是H5本地缓存和服务端缓存,CND相结合,例如整场年会我就抢了3个红包,那么直接将这三个中奖信息写到本地缓存。活动未开始用户会看到公司相应的宣传片,公司领导讲话视频,banner等统一走CND。

    7、现金红包提现服务,因为集团下属有自己的银行,所以本次活动涉及钱的全部走自己的银行,用户提现,需要绑卡,开户。提现需要审核,确认无误会交给定时任务处理,一般在2个小时之内会打到绑卡的银行账户。

    服务按职责划分出来之后优点是按需分配节约了服务器资源,避免单点故障拖垮整个服务,缺点是增加运维负担。

    中奖逻辑

    首先中奖是无规律的,即用户点击的红包,谁也不确定是否中奖,谁也不确定奖品是什么,为了满足这个条件且为了增加用户体验,我们采取的是先预热活动也就是初始化活动数据。提前30分钟预热即将开始的活动数据到缓存队列。主要用到了redis,quartz

    例如以下表格(demo数据):

    红包场次 开始时间 结束时间 红包总数 现金总数 现金总金额 xx超市代金券总数 xx超市代金券总金额
    第一场 2017/12/25 13:30:00 2017/12/25 13:40:00 4 3 100000 1 5000
    第二场 2017/12/25 14:30:00 2017/12/25 13:35:00 10 9 50000 1 20000

    在2017/12/25 13:00定时任务会预热第一场活动数据,发送消息到钉钉群里提醒所有相关人员。

    首先sql根据时间纬度查到第一场活动,开始初始化数据,整个初始化后的数据就是一个令牌桶,每个奖就是一个令牌。它们整个对应关系如下:

    image

    这种令牌桶设计一般通用于秒杀,红包雨,红包等场景。整个桶使用redis的list数据结构存储,优点是拿了就走,不需要更新库存,效率高。

    代码逻辑

    1、初始化令牌的产生,结束时间戳-开始时间戳得到活动的时间区间,用开始时间戳+时间区间的随机数生成生成四个令牌(每个令牌对应一个奖品)并将令牌按照从小到大的顺序排序,rpush到redis中,并且设置key的expire为10分钟。这里尤其注意令牌不能重复,而按照这种设计如果时间短而奖品多的情况下可能就会重复,解决思路,如果1分钟,1000份奖品,则0-999,那就用时间戳*1000 + redis自增(incr)不够前面补上0,真正时间对比的时候令牌/1000 才是真正的时间戳。如下

    System.out.println(new Date().getTime());
    System.out.println(1584866007408L*1000+999);
    System.out.println(1584866007408999L/1000);

    2、令牌和奖品之间是k-v存储到redis,并且设置key的expire为10分钟

    3、用户经过前面的鉴权逻辑,进入中奖服务,lpop拿到令牌,我们设置的中奖逻辑是,拿到令牌并不代表中奖,比如令牌1时间戳对应时间是13:31,活动是13:30开始,用户在13:30 第10秒抢到这个令牌13:30:10 < 13:31:00(令牌)所以不算中奖,这时候用户需要把令牌lpush回去,这里需要注意的是抢令牌,对比令牌,还回令牌必须是原子操作,可以使用分布式锁或者lua脚本来完成,目的是防止并发情况下,拿到令牌和放回去的令牌顺序错乱。如果用户是13:31:01秒抢到令牌1 ,系统就认为中奖,整个逻辑就是当前时间小于令牌时间戳不中奖,大于令牌时间戳中奖。

    4、当前活动对应的活动策略,比如最大中奖次数,司龄中奖次数等,hset到redis中,并且设置key的expire为10分钟

    5、预热完成,更新活动状态,并发送消息到钉钉群提醒工作人员。用户可抢的红包一定是已经预热后的活动,如果预热环节出现问题管控平台立刻启用备用活动顶替。

    6、用户确定中奖,直接返回奖品信息,为了节省带宽返回的奖品信息越少越好。同时发送MQ消息,后台中奖数据异步入库

  • 相关阅读:
    模拟赛总结
    2018.04.06学习总结
    2018.04.06学习总结
    Java实现 LeetCode 672 灯泡开关 Ⅱ(数学思路问题)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 670 最大交换(暴力)
    Java实现 LeetCode 670 最大交换(暴力)
    Java实现 LeetCode 670 最大交换(暴力)
  • 原文地址:https://www.cnblogs.com/gyjx2016/p/12537186.html
Copyright © 2011-2022 走看看