计数系统架构实践一次搞定 | 架构师之路

简介: 在数据量很大的情况下,频繁的变更数据库schema的结构显然是不可取的,有没有扩展性更好的方式呢?

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

一、需求缘起

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

image.png

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

关注了多少人的计数

粉丝的计数

发布博文的计数

image.png

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

转发计数

评论计数

点赞计数

甚至是浏览计数

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

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

image.png

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

调用层:处于端上的browser或者APP

站点层:拼装html或者json返回的web-server层

服务层:提供RPC调用接口的service层

数据层:提供固化数据存储的db,以及加速存储的cache

针对“缘起”里微博计数的例子,主要涉及“关注”业务,“粉丝”业务,“微博消息”业务,一般来说,会有相应的db存储相关数据,相应的service提供相关业务的RPC接口:

image.png

关注服务:提供关注数据的增删查改RPC接口

粉丝服务:提供粉丝数据的增删查改RPC接口

消息服务:提供微博消息数据的增删查改RPC接口,消息业务相对比较复杂,涉及微博消息、转发、评论、点赞等数据的存储

对关注、粉丝、微博业务进行了初步解析,那首页的计数需求应该如何满足呢?

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

image.png

例如,个人中心首页,需要展现博文数量这个计数,web层访问message-service的count接口,这个接口执行:

select count(*) from t_msg where uid = XXX

image.png

同理,也很容易拿到关注,粉丝的这些计数。

这个方案叫做“count”计数法,在数据量并发量不大的情况下,最容易想到且最经常使用的就是这种方法,但随着数据量的上升,并发量的上升,这个方法的弊端将逐步展现。

例如,微博首页有很多条微博消息,每条消息有若干计数,此时计数的拉取就成了一个庞大的工程:

image.png

整个拉取计数的伪代码如下:

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接口:

image.png

这样,在查询一条微博消息的若干个计数的时候,不用进行多次数据库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发送一条异步消息,通知计数系统计数发生了变化即可:

image.png

如上图:

用户新发布了一条微博

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的用户,其关注计数,粉丝计数,微博消息计数的缓存就可以设计为:

image.png

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

image.png

如此这般,多个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的用户,其关注计数,粉丝计数,微博消息计数的缓存就可以设计为:

image.png

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

memcache::get($uid1, $uid2, $uid3, …);

然后对获取的value进行分析,得到关注计数,粉丝计数,微博计数。

如果计数value能够事先预估一个范围,甚至可以用一个整数的不同bit来存储多个计数,用整数的与或非计算提高效率。

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

使用id作为key,使用同一个id的多个计数的拼接作为value

多个id的多个计数查询,一次搞定

六、计数扩展性优化

考虑完效率,架构设计上还需要考虑扩展性,如果uid除了关注计数,粉丝计数,微博计数,还要增加一个计数,这时系统需要做什么变更呢?

之前的数据库结构是:

t_user_count(uid, gz_count, fs_count, wb_count)

image.png

这种设计,通过列来进行计数的存储,如果增加一个XX计数,数据库的表结构要变更为:

t_user_count(uid, gz_count, fs_count, wb_count, XX_count)

image.png

在数据量很大的情况下,频繁的变更数据库schema的结构显然是不可取的,有没有扩展性更好的方式呢?

当当当当!

不要陷入思维定式,谁说只能通过扩展列来扩展属性,通过扩展行来扩展属性,在“架构师之路”的系列文章里也不是第一次出现了(具体请查阅《啥,又要为表增加一列属性?》《这才是真正的表扩展方案》《100亿数据1万属性数据架构设计》),完全可以这样设计表结构:

t_user_count(uid, count_key, count_value)

image.png

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

image.png

七、总结

小小的计数,在数据量大,并发量大的时候,其架构实践思路为:

计数外置:由“count计数法”升级为“计数外置法”

读多写少,甚至写多但一致性要求不高的计数,需要进行缓存优化,降低数据库压力

缓存kv设计优化,可以由[key:type]->[count],优化为[key]->[c1:c2:c3]

即:
image.png

优化为:
image.png

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

即:
image.png

优化为:
image.png

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

===【完】===

相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
目录
相关文章
|
17天前
|
Kubernetes 安全 Java
构建高效微服务架构:从理论到实践
【4月更文挑战第9天】 在当今快速迭代与竞争激烈的软件市场中,微服务架构以其灵活性、可扩展性及容错性,成为众多企业转型的首选。本文将深入探讨如何从零开始构建一个高效的微服务系统,覆盖从概念理解、设计原则、技术选型到部署维护的各个阶段。通过实际案例分析与最佳实践分享,旨在为后端工程师提供一套全面的微服务构建指南,帮助读者在面对复杂系统设计时能够做出明智的决策,并提升系统的可靠性与维护效率。
|
1天前
|
消息中间件 持续交付 数据库
构建高效可靠的微服务架构:策略与实践
【4月更文挑战第25天】 随着现代软件开发的复杂性日益增加,传统的单体应用已难以满足快速迭代和灵活部署的需求。本文深入探讨了如何构建一个高效且可靠的微服务架构,包括关键的设计原则、技术选型以及实践中的挑战和应对策略。通过分析多个成功案例,我们总结了一系列最佳实践,并提出了一套可量化的性能优化方法。文章不仅为开发者提供了具体的技术指导,同时也强调了团队协作和持续学习在微服务转型过程中的重要性。
|
9天前
|
消息中间件 运维 监控
现代化软件开发中的微服务架构设计与实践
本文将深入探讨现代化软件开发中微服务架构的设计原则和实践经验。通过分析微服务架构的优势、挑战以及常见的设计模式,结合实际案例,帮助开发者更好地理解如何构建可靠、可扩展、高效的微服务系统。
|
9天前
|
负载均衡 Java 开发者
细解微服务架构实践:如何使用Spring Cloud进行Java微服务治理
【4月更文挑战第17天】Spring Cloud是Java微服务治理的首选框架,整合了Eureka(服务发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Zuul(API网关)和Config Server(配置中心)。通过Eureka实现服务注册与发现,Ribbon提供负载均衡,Hystrix实现熔断保护,Zuul作为API网关,Config Server集中管理配置。理解并运用Spring Cloud进行微服务治理是现代Java开发者的关键技能。
|
10天前
|
敏捷开发 监控 前端开发
深入理解自动化测试框架Selenium的架构与实践
【4月更文挑战第16天】 在现代软件开发过程中,自动化测试已成为确保产品质量和加快迭代速度的关键手段。Selenium作为一种广泛使用的自动化测试工具,其开源、跨平台的特性使得它成为业界的首选之一。本文旨在剖析Selenium的核心架构,并结合实际案例探讨其在复杂Web应用测试中的高效实践方法。通过详细解读Selenium组件间的交互机制以及如何优化测试脚本,我们希望为读者提供深入理解Selenium并有效运用于日常测试工作的参考。
15 1
|
13天前
|
Linux 数据安全/隐私保护
Linux基础与服务器架构综合小实践
【4月更文挑战第9天】Linux基础与服务器架构综合小实践
1242 8
|
22天前
|
消息中间件 监控 API
构建高性能微服务架构:从理论到实践
【4月更文挑战第4天】 在当今互联网应用的快速迭代和高并发需求下,传统的单体应用架构已不足以满足市场的灵活性与扩展性要求。微服务架构以其独立部署、弹性伸缩、技术多样性等优势,成为众多企业转型升级的首选方案。本文将深入探讨如何构建一个高性能的微服务系统,涵盖关键组件的选择、系统设计的考量以及性能优化的策略,旨在为开发者和架构师提供一套实用的指导思路和具体实践步骤。
|
24天前
|
机器学习/深度学习 人工智能 架构师
【架构师】AI时代架构师必备技能
【架构师】AI时代架构师必备技能
|
25天前
|
消息中间件 安全 API
构建高效微服务架构:策略与实践
【4月更文挑战第1天】在数字化转型的浪潮中,微服务架构已成为企业追求敏捷、可扩展和灵活部署的重要技术手段。本文将深入探讨如何通过合理的设计原则和先进的技术栈,构建一个高效的微服务系统。我们将剖析微服务设计的核心要点,包括服务的划分、通信机制、数据一致性以及安全性问题,并结合案例分析,展示如何在现实世界中应用这些策略以提升系统的可靠性和性能。
|
25天前
|
设计模式 API 持续交付
构建高效微服务架构:从理论到实践
在当今快速迭代和部署的软件开发环境中,微服务架构已成为一种流行的设计模式,它允许开发团队以模块化的方式构建、维护和扩展应用程序。本文将深入探讨微服务的核心概念,包括其定义、优势、挑战以及如何在实际项目中实施。我们将通过一个实际案例来展示如何将传统的单体应用拆分成一系列独立、松耦合的服务,并通过容器化、服务发现、API网关和持续集成/持续部署(CI/CD)等技术手段来管理这些服务。