古越剑箫

学习是一种习惯

  :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: :: 管理 ::

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消息,后台中奖数据异步入库

 

posted on 2020-03-21 11:09  古越剑箫  阅读(1599)  评论(0编辑  收藏  举报