很多业务都有“计数”需求,在业务复杂,计数扩展频繁,数据量大,并发量大的情况下,计数系统的架构演进与实践。

初始架构

我们可以很容易想到,关注服务+粉丝服务+消息服务均提供相应接口,就能拿到相关计数数据。

这样将所有的数据记录入表,然后对某个属性进行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操作,而会转变为一条数据的多个属性的查询。但是当有微博被转发、评论、点赞的时候,计数服务如何同步的进行计数的变更呢?如果让业务服务来调用计数服务,势必会导致业务系统与计数系统耦合。

对于不关心下游结果的业务,可以使用MQ来解耦,在业务发生变化的时候,向MQ发送一条异步消息,通知计数系统计数发生了变化即可

计数外置,本质是数据的冗余,架构设计上,数据冗余必将引发数据的一致性问题,需要有机制来保证计数系统里的数据与业务系统里的数据一致,常见的方法有:

  • 对于一致性要求比较高的业务,要有定期check并fix的机制,例如关注计数,粉丝计数,微博消息计数等
  • 对于一致性要求比较低的业务,即使有数据不一致,业务可以接受,例如微博浏览数,微博转发数等

计数外置缓存优化

计数外置很大程度上解决了计数存取的性能问题,但是否还有优化空间呢?像关注计数,粉丝计数,微博消息计数,变化的频率很低,查询的频率很高,这类读多些少的业务场景,非常适合使用缓存来进行查询优化,减少数据库的查询次数,降低数据库的压力。

但是,缓存是kv结构的,无法像数据库一样,设置成t_uid_count(uid, c1, c2, c3)这样的schema,如何来对kv进行设计呢?缓存kv结构的value是计数,看来只能在key上做设计,很容易想到,可以使用uid:type来做key,存储对应type的计数。

对于uid=123的用户,其关注计数,粉丝计数,微博消息计数的缓存就可以设计为:

此时对应的counting-service架构变为:

这个“计数外置缓存优化”方案,可以总结为:

  • 使用缓存来保存读多写少的计数(其实写多读少,一致性要求不高的计数,也可以先用缓存保存,然后定期刷到数据库中,以降低数据库的读写压力)
  • 使用id:type的方式作为缓存的key,使用count来作为缓存的value
  • 多次读取缓存来查询多个uid的计数

缓存批量读取优化

缓存的使用能够极大降低数据库的压力,但多次缓存交互依旧存在优化空间,有没有办法进一步优化呢?

不要陷入思维定式,谁说value一定只能是一个计数,难道不能多个计数存储在一个value中么?缓存kv结构的key是uid,value可以是多个计数同时存储。

对于uid=123的用户,其关注计数,粉丝计数,微博消息计数的缓存就可以设计为:

这样多个用户,多个计数的查询就可以一次搞定。

这个“计数外置缓存批量优化”方案,可以总结为:

  • 使用id作为key,使用同一个id的多个计数的拼接作为value
  • 多个id的多个计数查询,一次搞定

计数扩展性优化

考虑完效率,架构设计上还需要考虑扩展性,如果uid除了关注计数,粉丝计数,微博计数,还要增加一个计数,就需要变更表结构,频繁的变更数据库schema的结构显然是不可取的。

我们可以这样设计表结构来通过行来扩展属性

t_user_count(uid, count_key, count_value)

如果需要新增一个计数XX_count,只需要增加一行即可,而不需要变更表结构: