摘要:活动规模既然公司对自如客这么阔,那对我们员工也得够意思,所以年底我们共准备了个活动。拆分后,红包占有只需操作,响应性能已不是问题。
首发于 樊浩柏科学院
2017 年是自如快速增长的一年,自如客突破 100 万,管理资产达到 50 万间,在年底成功获得了 40 亿 A 轮融资,而这些都要感谢广大的自如客,公司为了回馈自如客,在六周年活动时就发放了 6000 万租住基金,当然年底散币活动也够疯狂。
活动规模既然公司对自如客这么阔,那对我们员工也得够意思,所以年底我们共准备了 3 个活动。
1、针对 自如客 的服务费减免活动;
2、针对 自如客 的 1000 万现金礼包;
3、25 万的 员工 红包活动;
散币活动 2 和 3 是通过微信红包形式进行,想散币就散吧,可微信告诉我们,想散币还得交税(>﹏<)。员工红包来说,25 万要交掉 10 多万税,此时心疼我的钱。好了,下面开始说点正事。
技术方案说到红包,我们肯定会想到红包拆分和抢红包两个场景。红包拆分是指将指定金额拆分为指定数目红包的过程,即是用来确定每个红包的金额数;而抢红包就是典型的高并发场景,需要避免红包超发的情况。
红包拆分 可选的方案拆分方式
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 ast 2$ 表示剩余待拆分金额平均金额的 2 倍,因为 N >= 2,所以 $M/N ast 2 <= M$,表示一定能保证后续红包能拆分到金额。
代码实现为:
for ($i = 0; $i < $N - 1; $i++) { $max = (int)floor($M / ($N - $i)) * 2; $m[$i] = $max ? mt_rand(1, $max) : 0; $M -= $m[$i]; } $m[] = $M;
值得一提的是,我们为了保证红包金额差异尽量小,先将总金额平均拆分成 N+1 份,将第 N+1 份红包按照上述的红包拆分算法拆分成 N 份,这 N 份红包加上之前的平均金额才作为最终的红包金额。
抢红包 可选的方案限流
1、前端限流
前端限制用户在 n 秒之内只能提交一次请求,虽然这种方式只能挡住小白,不过这是 99% 的用户哟,所以也必须得做。
2、后端限流
常用的后端限流方法有 漏桶算法 和 令牌桶算法。漏桶算法 主要目的是控制请求数据注入的速率,如果此时漏桶溢出,后续的请求数据会被丢弃。而 令牌桶算法 是以一个恒定的速度往桶里放入令牌,而如果请求数据需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌时,这些请求才被丢弃,令牌桶算法的一个好处是可以方便地改变应用接受请求的速率。
防超发
1、库存加锁
可以通过加锁的方式解决资源抢占问题,但是加锁会增加系统开销,大流量下更容易拖垮系统,不过可以尝试一下基于版本号的乐观锁。
2、通过高速队列串行化请求
之所会出现超发问题,是因为并发时会出现多个进程同时获取同一资源的现象,如果使用高速队列将并行请求串行化,那么问题就不存在了。高速队列可以使用 Redis 缓存服务器来实现,当然光使用队列还不够,必要保证整个流程调用链要短、要快,否则队列会积压严重,甚至会拖垮整个服务。
在限流方面,由于我们预估的请求量还在系统承受范围,所以没有考虑引入后端限流方案。我们的抢红包系统流程图如下:
我们将抢红包拆分为 红包占有(流程①,同步) 和 红包发放 (流程②,异步)这两个过程,首先采用高速队列串行化请求,红包发放逻辑由一组 Worker 异步去完成。高速队列只是完成红包占有的过程,实现库存的控制,Worker 则处理耗时较长的红包发放过程。
当然,在实际应用中,红包占用过程还需要加上一些前置规则校验,比如用户是否已经领取过,领取次数是否已经达到上限等?红包占有流程图如下:
其中,red::list为 List 结构,存放预先生成的红包金额(流程①中的红包队列);red::task 也为 List 结构,红包异步发放队列(流程②中的任务队列);red::draw为 Hash 结构,存放红包领取记录,field为用户的 openid,value为序列化的红包信息;red::draw_count:u:openid为 k-v 结构,用户领取红包计数器。
下面,我将以以下 3 个问题为中心,来说说我们设计出的抢红包系统。
1、怎么保证不超发
我们需要关注的是红包占有过程,从红包占有流程图可看出,这个过程是很多 Key 操作的组合,那怎么保证原子性?可以使用 Redis 事务,但我们选用了 Lua 方案,一方面是因为首先要保证性能,而 Lua 脚本嵌入 Redis 执行不存在性能瓶颈,另一方面 Lua 脚本执行时本身就是原子性的,满足需求。
红包占有的 Lua 脚本实现如下:
-- 领取人的openid为xxxxxxxxxxx local openid = "xxxxxxxxxxx" 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://xxxxxxx local red = {money=number,name="Fhb",pic="https://xxxxxxx"} -- 领取记录 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 处理能力较弱,那么红包发放的延迟就会很高,体验较差。
如抢红包流程图中所示,我们采用一组 Worker 去消费任务队列,并调用红包支付 API,以及数据持久化操作(后续对账)。尽管红包发放调用链又长又慢,但是注意到这些 Worker 是 无状态 的,所以可以通过增加 Worker 数量,以横向扩展提高系统的处理能力。
4、怎么保证数据一致性
其实,红包发放延时我们可以做到用户无感知,但是若红包发放(流程②)失败了,已经告知用户抢到红包,但是却木有发,估计他杀人的心都有了。根据 CAP 原理,我们无法同时满足数据一致性、数据可用性、分区耐受性,通常只需做到数据最终一致性。
为了达到数据最终一致性,我们就引入了重试机制,生成一个全局唯一的外部订单号,当某单红包发放失败,就会放回任务队列,使得有机会进行发放重试,当然这一切都需要 API 做幂等处理。
Worker可靠性保障这里必须将 Worker 可靠性多带带说,因为它实在太重要了。Worker 的实现如下:
$maxTask = 1000; $sleepTime = 1000; while (true) { while ($red = RedLogic::getTask()) { RedLogic::doTask($red); //处理多少个任务主动退出 $maxTask--; if ($maxTask < 0) { return EXIT_CODE_NORMAL; } } //等待任务 usleep($sleepTime); }
这里使用 LPOP 命令获取任务,所以使用了 while 结构,并且无任务时需要等待,可以用阻塞命令 BLPOP 来改进。
由于 Worker 需要常驻内存运行,难免会出现异常退出的情况(也有主动退出), 所以需要保持 Worker 一直处于运行状态。我们使用进程管理工具 Supervisor 来监控 Worker 的运行状态,同时管理 Worker 的数量,当任务队列出现堆积时,增加 Worker 数量即可。Supervisor 的监控后台如下:
员工系统号散列公司员工都用唯一一个系统号 emp_code(自增字段)标识,登录成功后返回 emp_code,系统后续所有交互流程都基于 emp_code,分享出去的红包也会携带 emp_code,为了保护员工敏感信息和防止恶意碰撞攻击,我们不能直接将 emp_code 暴露给前端,需要借助一个 token(无规律)的中间者来完成交互。
可选的方案1、储存映射关系,时时查询
预先生成一个随机串 token,然后跟 emp_code 绑定,每次请求都根据 token 时时查询 emp_code。优点是可以定期更新,相对安全,缺点是性能不高。
2、建立映射关系函数,实时计算
建立一个映射关系函数,如 hash 散列或者加密解密算法,能够根据 emp_code 生成一个无规律的字符串 token,并且要能够根据 token 反映射出 emp_code。优点是需要存储介质存储关系,性能较高,缺点是很难做到定期失效并更新。
由于我们的红包活动只进行几天,所以我们选用了方案 2。对 emp_code 做了 hashids 散列算法,暴露的只是一串无规律的散列字符串。
hashids 是一个开源且轻量的唯一 id 生成器,支持 Java、PHP、C/C++、Python 等主流语言,PHP 想使用 hashids,只需composer require hashids/hashids命令安装即可。
然后,如下方式使用:
use HashidsHashids; $hashids = new Hashids("salt", 6, "abcdefghijk1234567890"); $hashids->encode(11002); //994k2kk $hashids->decode("994k2kk"); //[11002]
需要说明的是,其中salt是非常重要的散列加密盐串,6表示散列值最小长度,abcde...7890为散列字典,太长影响效率,太短不安全。由于默认的散列字典比较长,decode 效率并不高,所以这里移除了大写字母部分。
语音点赞语音点赞就是用户以语音的形式助力好友,核心技术其实是语音识别,而我们一般都会使用第三方语音识别服务。
可选的方案1、客户端调用第三方服务识别
客户端直接调用第三方语音识别服务,如微信提供了 JS-SDK 的语音识别 API ,返回识别的语音文本的信息,并且已经经过语义化。优点是识别较快,且不许关注语音存储问题,缺点是不安全,识别结果提交到服务端之前可能被恶意篡改。
2、服务端调用第三方服务识别
先将录制的语音上传至存储平台,然后服务端调用第三方语音识别服务,第三方语音识别服务去获取语音信息并识别,返回识别的语音文本的信息。优点是识别结果较安全,缺点是系统交互较多,识别效率不高。
我们业务场景的特殊性,存在用户可助力次数的限制,所以无需担心恶意刷赞的情况,因此可以选用方案 1,语音识别的交互流程如下:
此时,整个语音识别流程如下:
当然中国文字博大精深,语音识别的文本在匹配时,需要考虑容错处理,可以将文本转化为拼音,然后匹配拼音,或者设置一个匹配百分比,达到匹配值则认为语音口令正确。
需要注意的是,微信只提供 3 天的语音存储服务,若语音播放周期较长,则要考虑实现语音的存储。其他 红包发放测试
我们使用了线上公账号进行红包发放测试,为了让线上公众号能够授权到测试环境,在线上的微信授权回调地址新增一个参数,将带有to=feature参数的请求引流到测试环境,其他线上流量还是保持不变,匹配规则如下:
# Nginx不支持if嵌套,所以就这样变通实现 set $auth_redirect ""; if ($args ~* "r=auth/redirect") { set $auth_redirect "prod"; } if ($args ~* "to=feature") { set $auth_redirect "feature"; } if ($auth_redirect ~ "feature") { rewrite ^(.*)$ http://wx.t.ziroom.com/index.php last; } if ($auth_redirect ~ "prod") { rewrite ^(.*)$ http://wx.ziroom.com/index.php last; }CDN缓存
由于本次活动力度较大,预估流量会比以往增加不少(不能再出现机房带宽打满的情况了,不然 >﹏<),静态页面占流量的很大一部分,所以静态页面在发布时都会放置一份在 CDN 上,这样回源的流量就很小了。
灾备方案尽管做了很多准备,还是无法确保万无一失,我们在每个关键节点都增加了开关,一点出现异常,通过配置中心可以人工介入做降级处理。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/11989.html
摘要:活动规模既然公司对自如客这么阔,那对我们员工也得够意思,所以年底我们共准备了个活动。拆分后,红包占有只需操作,响应性能已不是问题。 首发于 樊浩柏科学院 2017 年是自如快速增长的一年,自如客突破 100 万,管理资产达到 50 万间,在年底成功获得了 40 亿 A 轮融资,而这些都要感谢广大的自如客,公司为了回馈自如客,在六周年活动时就发放了 6000 万租住基金,当然年底散币活...
摘要:总共邀请全球观众参与共同瓜分了亿现金红包大奖。春晚红包战背后暗暗较劲的正是云计算技术。此一役后,安全容灾性能成了每个春节红包团队需要长期考虑的问题。2007年,国内情报史专家高金虎出版过一本《看不见的第二战场》,讲述无线电情报与战争的关系。看不见的第二战场,这段话拿来形容BAT春晚红包战背后的云计算技术战再合适不过了。每年的春晚红包战似乎成了BAT的正面战场,三巨头呼风唤雨,在短时间内把红包...
摘要:老薛主机官方网站点击进入商家官方网站优惠详情抢红包活动每位用户都可以参与,可抢得元元随机红包,抢得红包直接充值到账户余额,可用于购买或续费老薛主机任意产品。新购主机活动新购买主机年付享折优惠,以官网标注原价新购买主机,买年送年,买年送年。老薛主机怎么样,老薛主机好不好,老薛主机是国内成立时间比较长的主机商,最早商家主要销售虚拟主机,对于稳定性的保证还是比较给力的,有宕机的时候都会补偿时间,商...
摘要:新购主机活动新购买主机年付享折优惠,以官网标注原价新购买主机,买年送年,买年送年。幸运大转盘可抽得主机折优惠券香港号主机年美国号主机年现金券等奖品。美国套餐机房美国洛杉矶,虚拟架构,支持系统。老薛主机怎么样?老薛主机是国内成立时间比较长的主机商,最早商家主要销售虚拟主机,对于稳定性的保证还是比较给力的,有宕机的时候都会补偿时间,商家现在开启了十三周年庆活动,活动力度非常大,不管你需要虚拟主机...
摘要:国产片的理想之作,国产科幻片的先行者,未来可期活动预告月日,将参加在举办的活动月日,受邀参加由碳链价值主办的年首次线下活动碳话主题为论区块链共识机制关注我们官网论坛这是双周报的第期,如有任何建议或者想法,欢迎大家来讨论留言哦 showImg(https://segmentfault.com/img/bVbokFM?w=1080&h=460); 今年的朋友圈突然变得不那么活跃了?大家是否...
阅读 2308·2021-09-29 09:42
阅读 536·2021-09-06 15:02
阅读 2536·2021-09-02 15:40
阅读 2087·2019-08-30 14:23
阅读 1799·2019-08-30 13:48
阅读 1244·2019-08-26 12:01
阅读 945·2019-08-26 11:53
阅读 2111·2019-08-23 18:31