内容简介:计数系统架构实践一次搞定
提醒,本文较长,可提前收藏/转发。
一、需求缘起
很多业务都有“计数”需求,以微博为例:
微博首页的 个人中心 部分,有三个重要的计数:
-
关注了多少人的计数
-
粉丝的计数
-
发布博文的计数
微博首页的 博文消息主体 部分,也有有很多计数,分别是一条博文的:
-
转发计数
-
评论计数
-
点赞计数
-
甚至是浏览计数
在 业务复杂,计数扩展频繁,数据量大,并发量大 的情况下, 计数系统的架构演进与实践 ,是本文将要讨论的问题。
二、业务分析与计数初步实现
典型的互联网架构,常常分为这么几层:
-
调用层:处于端上的 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” 计数法 ,在数据量并发量不大的情况下,最容易想到且最经常使用的就是这种方法,但随着数据量的上升,并发量的上升,这个方法的弊端将逐步展现。
例如,微博首页有很多条微博消息,每条消息有若干计数,此时计数的拉取就成了一个庞大的工程:
整个拉取计数的伪代码如下:
list<msg_id> = getHomePageMsg(uid);// 获取首页所有消息
for( msg_id in list<msg_id>){ // 对每一条消息
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 接口 :
这样,在查询一条微博消息的若干个计数的时候, 不用进行多次数据库 count 操作,而会转变为一条数据的多个属性的查询 :
for(msg_id in list<msg_id>) {
select forword_count, comment_count, praise_count
from t_msg_count
where msg_id=$msg_id;
}
甚至,可以将微博首页所有消息的计数, 转变为一条 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 的机制,例如关注计数,粉丝计数,微博消息计数等
-
对于一致性要求比较低的业务,即使有数据不一致,业务可以接受,例如微博浏览数,微博转发数等
四、计数外置缓存优化
计数外置很大程度上解决了计数存取的性能问题,但是否还有优化空间呢?
像关注计数,粉丝计数,微博消息计数, 变化的频率很低,查询的频率很高,这类读多些少的业务场景 , 非常适合使用 缓存 来进行查询优化 ,减少数据库的查询次数,降低数据库的压力。
但是,缓存是 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 一定只能是一个计数,难道不能多个计数存储在一个 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 的结构显然是不可取的 ,有没有扩展性更好的方式呢?
当当当当!
不要陷入思维定式,谁说只能通过扩展列来扩展属性, 通过扩展行来扩展属性 ,在“架构师之路”的系列文章里也不是第一次出现了(具体请查阅《 啥,又要为表增加一列属性? 》《 这才是真正的表扩展方案 》《 100亿数据1万属性数据架构设计 》),完全可以这样设计表结构:
t_user_count(uid, count_key, count_value)
如果需要新增一个计数 XX_count ,只需要增加一行即可,而不需要变更表结构:
七、总结
小小的计数,在数据量大,并发量大的时候,其架构实践思路为:
-
计数外置 :由“ count 计数法”升级为 “计数外置法”
-
读多写少,甚至写多但一致性要求不高的计数,需要进行 缓存优化 ,降低数据库压力
-
缓存 kv 设计优化 ,可以 由 [key:type]->[count] ,优化为 [key]->[c1:c2:c3]
即:
优化为:
-
数据库扩展性优化 ,可以由 列扩展优化为行扩展
即:
优化为:
计数系统架构先聊到这里,希望大家有收获。
===【完】===
相关阅读:
本文值得读第二遍,值得 转发 ,谢谢。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Numerical Methods and Methods of Approximation in Science and En
Karan Surana / CRC Press / 2018-10-31
ABOUT THIS BOOK Numerical Methods and Methods of Approximation in Science and Engineering prepares students and other readers for advanced studies involving applied numerical and computational anal......一起来看看 《Numerical Methods and Methods of Approximation in Science and En》 这本书的介绍吧!