《架构师之路》 学习笔记
https://www.w3cschool.cn/architectroad/architectroad-counting-system.html
一、需求缘起
很多业务都有**“计数”**需求,以微博为例:
微博个人中心
微博首页的个人中心部分,有三个重要的计数:
- 关注了多少人的计数
- 粉丝的计数
- 发布博文的计数
微博首页
微博首页的博文消息主体部分,也有有很多计数,分别是一条博文的:
- 转发计数
- 评论计数
- 点赞计数
- 甚至是浏览计数
在业务复杂,计数扩展频繁,数据量大,并发量大
的情况下,计数系统的架构演进与实践,是本文将要讨论的问题。
二、业务分析与计数初步实现
典型的互联网架构,常常分为这么几层:
- 调用层:处于端上的browser或者APP
- 站点层:拼装html或者json返回的web-server层
- 服务层:提供RPC调用接口的service层
- 数据层:提供固化数据存储的db,以及加速存储的cache
针对**“缘起”**里微博计数的例子,主要涉及“关注”业务,“粉丝”业务,“微博消息”业务
,
一般来说,会有相应的db存储相关数据,相应的service提供相关业务的RPC接口: - 关注服务:提供关注数据的增删查改RPC接口
- 粉丝服务:提供粉丝数据的增删查改RPC接口
- 消息服务:提供微博消息数据的增删查改RPC接口,消息业务相对比较复杂,涉及微博消息、转发、评论、点赞等数据的存储
微博个人中心-需求
对关注、粉丝、微博
业务进行了初步解析,那个人中心的计数需求应该如何满足呢?
例如,个人中心首页,需要展现
博文数量
这个计数,web层访问message-service
的count
接口,这个接口执行:select count(*) from t_msg where uid = XXX
同理,也很容易拿到关注,粉丝的这些计数。
count计数法–弊端
这个方案叫做**“count”计数法**,
在数据量并发量不大的情况下,最容易想到且最经常使用的就是这种方法,但随着数据量的上升,并发量的上升,这个方法的弊端将逐步展现。
例如,微博首页有很多条微博消息
,每条消息有若干计数
,此时计数的拉取就成了一个庞大的工程:
整个拉取计数的伪代码如下:
list<msg_id> = getHomePageMsg(uid);// 获取首页所有消息
for( msg_id in list<msg_id>){ // for-each每一条消息
getReadCount(msg_id); // 阅读计数
getForwordCount(msg_id); // 转发计数
getCommentCount(msg_id); // 评论计数
getPraiseCount(msg_id); // 赞计数
}
弊端
- 每一个微博消息的若干个计数,都对应4个后端服务访问
- 每一个访问,对应一条count的数据库访问(count要了老命了)
其效率之低,资源消耗之大,处理时间之长,可想而知。
“count”计数法方案,可以总结为:
- 多条消息多次查询,for循环进行
- 一条消息多次查询,多个计数的查询
- 一次查询一个count,每个计数都是一个count语句
那如何进行优化呢?
三、计数外置的架构设计
计数是一个通用的需求,有没有可能,这个计数的需求实现在一个通用的系统里
,而不是由关注服务、粉丝服务、微博服务来分别来提供相应的功能呢(否则扩展性极差)?
这样需要实现一个通用的计数服务。
通过分析,上述微博的业务可以抽象成两类:
用户(uid)维度的计数
:用户的关注计数,粉丝计数,发布的微博计数微博消息(msg_id)维度的计数
:消息转发计数,评论计数,点赞计数
模型设计
于是可以抽象出两个表,针对这两个维度来进行计数的存储:
t_user_count (uid, gz_count, fs_count, wb_count);
字段对应:关注,粉丝,微博数
t_msg_count (msg_id, forword_count, comment_count, praise_count);
甚至可以更为抽象,一个表搞定所有计数:
t_count(id, type, c1, c2, c3, …)
通过type来判断,id究竟是uid还是msg_id
,但并不建议这么做。
接口涉及
存储抽象完,再抽象出一个计数服务对这些数据进行管理,提供独立的RPC接口:
uid,c1,c2,c3
表示:uid用户
的关注数,粉丝数,微博数
例1:单条微博计数
这样,在查询一条微博
消息的若干个计数的时候,不用进行多次数据库count操作
,而会转变为一条数据的多个属性的查询:
for(msg_id in list<msg_id>) {
select forword_count, comment_count, praise_count
from t_msg_count
where msg_id=$msg_id;
}
例2:微博首页-批量微博计数
甚至,可以将微博首页所有消息的计数,转变为一条IN语句(不用多次查询了)
的批量查询:
select * from t_msg_count
where msg_id IN
($msg_id1, $msg_id2, $msg_id3, …);
IN查询可以命中msg_id聚集索引,效率很高。
计数变更
方案非常帅气,接下来,问题转化为:当有微博被转发、评论、点赞的时候,计数服务如何同步的进行计数的变更呢?
如果让业务服务来调用计数服务
,势必会导致业务系统与计数系统耦合。
MQ解耦
之前的文章介绍过,对于不关心下游结果的业务,可以使用MQ来解耦
,在业务发生变化的时候,向MQ发送一条异步消息,通知计数系统计数发生了变化即可:
如上图:
- 用户新发布了一条微博
- msg-service向MQ发送一条消息
- counting-service从MQ接收消息
- counting-service变更这个uid发布微博消息计数
总结
这个方案称为“计数外置”
,可以总结为:
- 通过
counting-service
单独保存计数 - MQ同步计数的变更
- 多条消息的多个计数,一个批量IN查询完成
计数外置,本质是数据的冗余,架构设计上,数据冗余必将引发数据的一致性问题
,需要有机制来保证计数系统里的数据与业务系统里的数据一致,常见的方法有:
- 对于一致性要求比较高的业务,要
有定期check并fix的机制
,例如关注计数,粉丝计数,微博消息计数等 - 对于一致性要求比较低的业务,
即使有数据不一致,业务可以接受
,例如微博浏览数,微博转发数等
四、计数外置缓存优化
计数外置很大程度上解决了计数存取的性能问题,但是否还有优化空间呢?
像关注计数,粉丝计数,微博消息计数,变化的频率很低,查询的频率很高
,这类读多些少的业务场景,非常适合使用缓存来进行查询优化
,减少数据库的查询次数,降低数据库的压力。
缓存key设计
但是,缓存是kv结构的
,无法像数据库一样,设置成t_uid_count(uid, c1, c2, c3)
这样的schema
,如何来对kv进行设计呢?
缓存kv结构的value是计数,看来只能在key上做设计,很容易想到,可以使用uid:type
来做key,存储对应type的计数。
例
对于uid=123
的用户,其关注计数,粉丝计数,微博消息计数
的缓存就可以设计为:
此时对应的counting-service架构变为:
如此这般,多个uid的多个计数
,又可能会变为多次缓存
的访问:
for(uid in list<uid>) {
memcache::get($uid:c1, $uid:c2, $uid:c3);
}
总结
这个“计数外置缓存优化”方案,可以总结为:
- 使用缓存来保存读多写少的计数(其实写多读少,一致性要求不高的计数,也可以先用缓存保存,然后定期刷到数据库中,以降低数据库的读写压力)
- 使用id:type的方式作为缓存的key,使用count来作为缓存的value
- 多次读取缓存来查询多个uid的计数
五、缓存批量读取优化
缓存的使用能够极大降低数据库的压力,但多次缓存交互依旧存在优化空间,有没有办法进一步优化呢?
缓存value设计
缓存kv结构的key是uid,value可以是多个计数同时存储
。
对于uid=123的用户,其关注计数,粉丝计数,微博消息计数的缓存就可以设计为:
这样多个用户,多个计数的查询就可以一次搞定:
memcache::get($uid1, $uid2, $uid3, …);
然后对获取的value进行分析,得到关注计数,粉丝计数,微博计数。
如果计数value能够事先预估一个范围,甚至可以用
一个整数的不同bit
来存储多个计数,用二进制的与或
非计算提高效率。
总结
这个“计数外置缓存批量优化”方案,可以总结为:
- 使用id作为key,使用同一个id的多个计数的拼接作为value
- 多个id的多个计数查询,一次搞定
六、计数扩展性优化
考虑完效率,架构设计上还需要考虑扩展性,如果uid除了关注计数,粉丝计数,微博计数,还要增加一个计数,这时系统需要做什么变更呢?
之前的数据库结构
t_user_count(uid, gz_count, fs_count, wb_count)
变更后的结构
这种设计,通过列来进行计数的存储,如果增加一个XX计数,数据库的表结构要变更为:
t_user_count(uid, gz_count, fs_count, wb_count, XX_count)
在数据量很大的情况下,频繁的变更数据库schema的结构显然是不可取的,有没有扩展性更好的方式呢?
行扩展
谁说只能通过扩展列来扩展属性,通过扩展行来扩展属性
变更前结构
t_user_count(uid, count_key, count_value)
变更后结构
如果需要新增一个计数XX_count
,只需要增加一行
即可,而不需要变更表结构:
七、总结
小小的计数,在数据量大,并发量大的时候,其架构实践思路为:
- 计数外置:
由“count计数法”升级为“计数外置法”
- 读多写少,甚至写多但一致性要求不高的计数,需要进行缓存优化,降低数据库压力
- 缓存kv设计优化,可以由
[key:type]->[count]
,优化为[key]->[c1:c2:c3]
- 数据库扩展性优化,可以由
列扩展优化为行扩展