CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
每秒100W次的计数,架构原来可以这样设计!
0
架构设计
小笨蛋
发布于:2021年07月09日
更新于:2021年07月09日
104
#custom-toc-container
> 转载自 架构师之路 今天和大家聊聊计数系统。 计数系统解决什么业务问题? 很多业务都有“计数”需求,以微博为例: ![图片alt](/uploads/images/20210709/205831-0f585d4417f24eceab1f07a5bdfd1992.png ''图片title'') 微博首页的个人中心部分,有三个重要的计数: (1)关注了多少人的计数; (2)粉丝的计数; (3)发布博文的计数; ![图片alt](/uploads/images/20210709/205848-50d88684ac87485f890feec02d93d5ed.png ''图片title'') 微博首页的博文消息主体部分,也有有很多计数,分别是一条博文的: (1)转发计数; (2)评论计数; (3)点赞计数; (4)甚至是浏览计数; > 画外音:浏览计数有点难,每秒100W次PV,就有100W次计数。 在业务复杂,计数扩展频繁,数据量大,并发量大的情况下,**计数系统的架构演进与实践**,是本文将要分享的问题。 如何最快速的实现业务计数呢? count计数法。 ![图片alt](/uploads/images/20210709/205928-df3a3c2acad5410390da2b225b8e42f3.png ''图片title'') 典型的互联网架构,常常分为这么几层: (1)调用层:处于端上的browser或者APP; (2)站点层:拼装html或者json返回的web-server层; (3)服务层:提供RPC调用接口的service层; (4)数据层:提供固化数据存储的db,以及加速存储的cache; 针对上文微博计数的例子,主要涉及“关注”业务,“粉丝”业务,“微博消息”业务,一般来说,会有相应的db存储相关数据,相应的service提供相关业务的RPC接口: ![图片alt](/uploads/images/20210709/205948-de186387e48e49d6bb174478904dcd18.png ''图片title'') (1)关注服务:提供关注数据的增删查改RPC接口; (2)粉丝服务:提供粉丝数据的增删查改RPC接口; (3)消息服务:提供微博消息数据的增删查改RPC接口,消息业务相对比较复杂,涉及微博消息、转发、评论、点赞等数据的存储; 对关注、粉丝、微博业务进行了初步解析,**那首页的计数需求应该如何满足呢**? 很容易想到,**关注服务+粉丝服务+消息服务均提供相应接口,就能拿到相关计数数据。** ![图片alt](/uploads/images/20210709/210102-bfddb3c09d9d4d3b9209540e51d5f83f.png ''图片title'') 例如,个人中心首页,需要展现博文数量这个计数,web层访问message-service的count接口,这个接口执行: `select count(*) from t_msg where uid = XXX` ![图片alt](/uploads/images/20210709/205831-0f585d4417f24eceab1f07a5bdfd1992.png ''图片title'') 同理,也很容易拿到关注,粉丝的这些计数。 这个方案叫做“**count**”计数法,在数据量并发量不大的情况下,最容易想到且最经常使用的就是这种方法。 **count计数法存在什么问题呢?** 随着数据量的上升,并发量的上升,这个方法的弊端将逐步展现。 例如,微博首页有很多条微博消息,每条消息有若干计数,此时计数的拉取就成了一个庞大的工程: ![图片alt](/uploads/images/20210709/210214-a5816107d35a4b08b8101e17ce057053.png ''图片title'') 整个拉取计数的伪代码如下: ```csharp list
= getHomePageMsg(uid);// 获取首页所有消息 for( msg_id in list
){ // 对每一条消息 getReadCount(msg_id); // 阅读计数 getForwordCount(msg_id); // 转发计数 getCommentCount(msg_id); // 评论计数 getPraiseCount(msg_id); // 赞计数 } ``` 其中: (1)每一个微博消息的若干个计数,都对应4个后端服务访问; (2)每一个访问,对应一条count的数据库访问(count要了老命了); 其效率之低,资源消耗之大,处理时间之长,可想而知。 “count”计数法方案,可以总结为: (1)多条消息多次查询,for循环进行; (2)一条消息多次查询,多个计数的查询; (3)一次查询一个count,每个计数都是一个count语句; **那如何进行优化呢? 计数外置法。** 计数是一个通用的需求,有没有可能,这个计数的需求实现在一个通用的系统里,而不是由关注服务、粉丝服务、微博服务来分别来提供相应的功能呢? > 画外音:各个业务系统具备的通用痛点,应该下沉统一解决。 > 可以抽象一个通用的计数服务。 通过分析,上述微博的业务可以抽象成两类: (1)**用户(uid)维度**的计数:用户的关注计数,粉丝计数,发布的微博计数; (2)**微博消息(msg_id)维度**的计数:消息转发计数,评论计数,点赞计数; 于是可以抽象出两个表,**针对这两个维度来进行计数的存储:** ```csharp 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接口:** ![图片alt](/uploads/images/20210709/210457-aa06f852c0754d459701239ac86ee203.png ''图片title'') 这样,在查询一条微博消息的若干个计数的时候,**不用进行多次数据库count操作,而会转变为一条数据的多个属性的查询:** ```csharp for(msg_id in list
) { select forword_count, comment_count, praise_count from t_msg_count where msg_id=$msg_id; } ``` 甚至,可以将微博首页所有消息的计数,**转变为一条IN语句**(不用多次查询了)的批量查询: ```csharp select * from t_msg_count where msg_id IN ($msg_id1, $msg_id2, $msg_id3, …); ``` **IN查询可以命中msg_id聚集索引**,效率很高。 **那么,当有微博被转发、评论、点赞的时候,计数服务如何同步的进行计数的变更呢?** 如果让业务服务来调用计数服务,势必会导致业务系统与计数系统耦合。可以通过**MQ来解耦**,在业务发生变化的时候,向MQ发送一条异步消息,通知计数系统计数发生了变化即可: ![图片alt](/uploads/images/20210709/210619-8087b1cb8f914aa08b62e42004a795c9.png ''图片title'') 如上图: (1)用户新发布了一条微博; (2)msg-service向MQ发送一条消息; (3)counting-service从MQ接收消息; (4)counting-service变更这个uid发布微博消息计数; > 画外音:其实发送一条微博本来就会发MQ消息,计数系统只是新增一个订阅方。 这个方案称为“**计数外置**”,可以总结为: (1)通过counting-service单独保存计数; (2)MQ同步计数的变更; (3)多条消息的多个计数,一个批量IN查询完成; **计数外置可能存在什么新的问题呢? ** **计数外置,本质是数据的冗余**,架构设计上,**数据冗余必将引发数据的一致性问题**,需要有机制来保证计数系统里的数据与业务系统里的数据一致,常见的方法有: (1)对于一致性要求比较高的业务,要有定期check并fix的机制,例如关注计数,粉丝计数,微博消息计数等; (2)对于一致性要求比较低的业务,即使有数据不一致,业务可以接受,例如微博浏览数,微博转发数等; **计数外置很大程度上解决了计数存取的性能问题,但是否还有优化空间呢?** 像关注计数,粉丝计数,微博消息计数,变化的频率很低,查询的频率很高,这类读多些少的业务场景,非常适合使用缓存来进行查询优化,减少数据库的查询次数,降低数据库的压力。 但是,缓存是kv结构的,无法像数据库一样,设置成 `t_uid_count(uid, c1, c2, c3)` 这样的schema,**如何来对kv进行设计呢**? 缓存kv结构的value是计数,看来只能在key上做设计,很容易想到,可以使用uid:type来做key,存储对应type的计数。 对于uid=123的用户,其关注计数,粉丝计数,微博消息计数的缓存就可以设计为: ![图片alt](/uploads/images/20210709/210750-8b4710de93864908b64682122a7d8a95.png ''图片title'') 此时对应的counting-service架构变为: ![图片alt](/uploads/images/20210709/210800-9a4700fd6a4c4b3c81ee90112c924074.png ''图片title'') 如此这般,多个uid的多个计数,**又可能会变为多次缓存的访问**: ```csharp for(uid in list
) { memcache::get($uid:c1, $uid:c2, $uid:c3); } ``` 这个“**计数外置缓存优化**”方案,可以总结为: (1)使用缓存来保存读多写少的计数; > 画外音:其实写多读少,一致性要求不高的计数,也可以先用缓存保存,然后定期刷到数据库中,以降低数据库的读写压力。 (2)使用id:type的方式作为缓存的key,使用count来作为缓存的value; (3)多次读取缓存来查询多个uid的计数; 缓存的使用能够极大降低数据库的压力,**但多次缓存交互依旧存在优化空间,有没有办法进一步优化呢?** 不要陷入思维定式,谁说value一定只能是一个计数,难道不能多个计数存储在一个value中么? 缓存kv结构的key是uid,value可以是多个计数同时存储。 对于uid=123的用户,其关注计数,粉丝计数,微博消息计数的缓存就可以设计为: ![图片alt](/uploads/images/20210709/210902-f652bd8db0cc44e282b35b40187ee107.png ''图片title'') 这样多个用户,多个计数的查询就可以一次搞定: `memcache::get($uid1, $uid2, $uid3, …);` 然后对获取的value进行分析,得到关注计数,粉丝计数,微博计数。 如果计数value能够事先预估一个范围,甚至可以用一个整数的不同bit来存储多个计数,用整数的与或非计算提高效率。 这个“**计数外置缓存批量优化**”方案,可以总结为: (1)使用id作为key,使用同一个id的多个计数的拼接作为value; (2)多个id的多个计数查询,一次搞定; 考虑完效率,架构设计上还需要考虑**扩展性**,如果uid除了关注计数,粉丝计数,微博计数,还要**增加一个计数,这时系统需要做什么变更呢?** 之前的数据库结构是: `t_user_count(uid, gz_count, fs_count, wb_count)` ![图片alt](/uploads/images/20210709/210941-412a30b6f9f94d2baf19ee5e7ae88008.png ''图片title'') 这种设计,**通过列来进行计数的存储**,如果增加一个XX计数,数据库的表结构要变更为: `t_user_count(uid, gz_count, fs_count, wb_count, XX_count)` ![图片alt](/uploads/images/20210709/211000-d0fe222e92dd49c8894ef2f5f8d3bfe7.png ''图片title'') 在数据量很大的情况下,**频繁的变更数据库schema的结构显然是不可取的**,有没有扩展性更好的方式呢? 不要陷入思维定式,谁说只能通过扩展列来扩展属性,能不能通过扩展行来扩展属性呢? 答案是肯定的,完全可以这样设计表结构: `t_user_count(uid, count_key, count_value)` ![图片alt](/uploads/images/20210709/211026-0c05045cf65643deb0635c3e0ab8837a.png ''图片title'') 如果需要新增一个计数XX_count,只需要增加一行即可,而不需要变更表结构: ![图片alt](/uploads/images/20210709/211036-d89ef60902bc45968125258f6e40450f.png ''图片title'') **总结** 小小的计数,在数据量大,并发量大的时候,其架构实践思路为: (1)**计数外置**:由“count计数法”升级为“计数外置法”; (2)读多写少,甚至写多但一致性要求不高的计数,需要进行**缓存优化**,降低数据库压力; (3)缓存kv设计优化,可以由`[key:type]->[count],优化为[key]->[c1c2:c3]` 即: ![图片alt](/uploads/images/20210709/211133-a0dfd51f5d864e6e8e63e8f48351589c.png ''图片title'') 优化为: ![图片alt](/uploads/images/20210709/211141-ce36f30a52154b47a9c347d465dac8c4.png ''图片title'') (4)**数据库扩展性优化**,可以由列扩展优化为行扩展 即: ![图片alt](/uploads/images/20210709/211154-0ad88337ff4d4b449efd78194c239ca9.png ''图片title'') 优化为: ![图片alt](/uploads/images/20210709/211205-e7c2eb1181994f96b5e9904b1e4b2a1b.png ''图片title'')
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024