这是一篇与 (下文用"neptunezx"代替) 交流的心得。
简单限制下讨论的范围: 1、编程语言为 PHP; 2、操作系统为主流 linux 服务器操作系统; 3、不使用 kafka、rabbitMq 等消息队列;
Copy neptunezx 问:weizeng ,rti 系统那边的红包怎么实现的?
weizeng 答:这个我懂,语言栈是 java,根据 HTTP Request 打过来的积分总数和红包个数字段,rand 函数搞定红包分配,再放到 java 的内存队列里,然后等待用户 pop 即可!
neptunezx 问:有没有更好的实现方式?
weizeng 答:应该有,emmmmm~
上面一段对话后面省略一万字,根据当时的讨论,发现抢红包问题里面的门道真是多!接下来主要分析下列问题:
1、发红包的数据结构应该怎么设计?
2、根据1,红包算法如何实现?(只考虑随机红包)
3、如果优雅的记录用户抢红包的记录?
4、如果使用内存型数据库(redis、mongoDb等),如何解决抢红包过程的负载问题?
5、随着业务增长,未来可能出现分布式架构,如何解决分布式架构下的抢红包问题?
挨个儿讨论,问题1:
Copy 首先考虑当前的业务量,如果业务量比较小,Redis 资源上比较充裕,若是恰好时间比较紧张,可以选择如下简单设计:
设计1:根据红包总数和红包金额,用 rand 函数计算每一个红包应该放多少钱,组成队列存入 Redis,抢红包过程中直接 POP 操作即可,相信我,普通情况请直接选它;
优化:
设计2:将上下文获得的红包总数和红包总金额存入 Redis,在抢红包的过程中一边计算一边抢红包,这种方法的瓶颈在抢红包的过程中需要实时计算本次红包应该抢多少?
对比一下,1 在设计上需要更多的存储空间,只是将红包的计算逻辑提前到红包创建过程中而已,所以设计2 当选
问题2: 贴一段简单的代码,这里时间问题先不做过多的优化,回头有时间这里专门聊聊!
Copy // emmmm,对,不要惊讶,就是 java 实现的!!!
public void grapRedEnvelope(double total_money,int total_people) {
double minPoint = 0.01;
for (int i = 0; i < total_people - 1; i++) {
int j = i + 1;
double safeMoney = (total_money - (total_people - j) * minPoint) / (total_people - j);
double tmp_money = (Math.random() * (safeMoney * 100 - minPoint * 100) + minPoint * 100) / 100;
total_money = total_money - tmp_money;
System.out.format("第 %d 个红包: %.2f 元,剩下: %.2f 元\n", j, tmp_money, total_money);
}
System.out.format("第 %d 个红包: %.2f 元,剩下: 0 元\n", total_people,
total_money);
}
问题3:
Copy 这个问题的实现方式有很多,按照实现难易程度排列:
1、direct insert into mysql or MongoDB;
2、add redis list queue,微服务或独立线程实时消费;
3、push to ELK cluster;
...
实现方式很多,根据约束条件的限制,当前状态下 2 应该是比较优雅的方式,理由见最上方'讨论范围'的第三条。
问题4:
Copy 这是个典型的 Redis 优化问题啊,但是脱离业务的优化全是耍流氓啊,那就随便列举几个吧:
1、Redis 搭建为分布式主从结构,提升 Redis 的性能;
2、Redis 主从结构中,机器尽量在同一个局域网内,保证主从同步复制的性能;
3、Redis Master 节点不要开启内存快照、AOF 日志等;
...
emmmm~ 问题答到这里并不算完整,抢红包问题可以简单的分析下请求的模型结构,Request 打到服务器上再到达应用程序(App),App 再与 Redis 交互,我们可以做的还有不少!
1、Redis 和 App 交互的优化!
2、App 内部的优化,作为一个 PHP 的 Application,有一些硬伤需要解决!
第一点,红包的个数和总金额是固定的,但是不一定每一次用户的抢红包行为都需要和 Redis 直接交互啊!可以用一些设计上的优化节省 Redis 的请求次数,缓解 Redis 的压力:
在红包申请的过程中,将红包的总数和金额记录到 App 的内存中,接下来每一次请求直接与 App 本身的内存交互即可,独立一个子进程同步 App 的内存和 Redis 数据即可!
第二点:根据第一点的优化建议,有两个比较致命的硬伤,大家都知道 PHP 是多进程的,通过 PHP-FPM 分配 Request 请求,常规 PHP App 没有应用级别的内存。
大家也都晓得 PHP 进程在每一次 Request 结束都会释放一次空间,所以这里需要安装几个扩展:APC、Yac 等都可以,见仁见智。
另外一个问题:常规的 PHP App 通过 php-fpm 管理,存在 max-request 限制,万一某个进程直接重启了,那么上述过程的同步操作简直就是爆炸,
岂不是徒增问题的复杂性,可以选择 pcntl 扩展,建立一个独立的子进程,拿到资源句柄,进行同步操作,不过这种方式我都没试过。
综上:建议直接安装 Swoole,解决共享内存问题和子进程 fork 问题!
Copy 容我插一句话,我们成功把原先的一个基础问题提升到高并发、高可用的高度了~~~ 然后问题也变得超级复杂!!!
// TODO
问题4,扩展问题: App 与 redis 同步在优化之后可能会出现一个非常尴尬的情况,如何保证稳定性?
举个例子完善下上面的问题,App 在内存中操作红包数据,按照之前的方案,需要有一个独立的进程实现 App 数据到 Redis 数据的同步, 大家也都比较清楚写程序最困难的部分是错误的处理,如何优雅的保证极端情况(网络抖动过于剧烈、程序crash等等)下 App 和 Redis 的一致性?
问题5:
Copy 好问题,基本上到分布式的程度,就不能只依赖内存型数据库(Redis)了,还需要一些脱耦合型的数据存储方案,比如说消息队列,kafka 就能很好的处理这个问题,所以我还是打破了上述的约束条件,
"不使用消息队列"。先说下如果使用消息队列,其实很简单,抢红包数据先行达到消息队列上,按照业务级别,可以选择是否在消息队列前面套一层 Redis 限流,不过不重要,按照 kafka 的吞吐,机器给到位,
基本没什么可能性能干掉 kafka!那么分布式情况下只需要在同一个 group 下消费 同一个 topic 即可简单搞定这个问题,感觉找到了一个简直完美的方案啊!!!
---- 对,你没看错,最后我还是用了消息队列。