计数系统架构实践一次搞定

栏目: 后端 · 发布时间: 6年前

内容简介:计数系统架构实践一次搞定

提醒,本文较长,可提前收藏/转发。

一、需求缘起

很多业务都有“计数”需求,以微博为例:

计数系统架构实践一次搞定

微博首页的 个人中心 部分,有三个重要的计数:

  • 关注了多少人的计数

  • 粉丝的计数

  • 发布博文的计数

计数系统架构实践一次搞定

微博首页的 博文消息主体 部分,也有有很多计数,分别是一条博文的:

  • 转发计数

  • 评论计数

  • 点赞计数

  • 甚至是浏览计数

业务复杂,计数扩展频繁,数据量大,并发量大 的情况下, 计数系统的架构演进与实践 ,是本文将要讨论的问题。

二、业务分析与计数初步实现

计数系统架构实践一次搞定

典型的互联网架构,常常分为这么几层:

  • 调用层:处于端上的 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]

即:

计数系统架构实践一次搞定

优化为:

计数系统架构实践一次搞定

  • 数据库扩展性优化 ,可以由 列扩展优化为行扩展

即:

计数系统架构实践一次搞定

优化为:

计数系统架构实践一次搞定

计数系统架构先聊到这里,希望大家有收获。

===【完】===

相关阅读:

到底什么时候该使用MQ?

啥,又要为表增加一列属性?

这才是真正的表扩展方案

100亿数据1万属性数据架构设计

本文值得读第二遍,值得 转发 ,谢谢。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming Rust

Programming Rust

Jim Blandy / O'Reilly Media / 2016-8-25 / GBP 47.99

This practical book introduces systems programmers to Rust, the new and cutting-edge language that’s still in the experimental/lab stage. You’ll learn how Rust offers the rare and valuable combination......一起来看看 《Programming Rust》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具