计数服务设计

在Feed流的推荐信息里,一般都会有点赞数收藏数评论数转发数 的功能,假设让你设计这套计数系统,你会怎么设计?

(1) 背景

以微信、抖音、快手、微博 为例,推荐的朋友圈、视频或文章里会有点赞数收藏数评论数转发数,假如要设计,一般设计成一个简单的kv查询。

考虑因素如下:
1、预估用户 14亿
2、预估活跃用户数 0.01 ~ 6亿。
3、假设每个用户每天发1~3个信息/动态, 每天大概有 0.01亿 ~ 18亿条信息(动态)。 每条信息(动态)上都有点赞数收藏数评论数转发数

备注: 一条信息(动态)代表一个朋友圈动态或一个抖音视频或一篇微博动态。


(2) 信息Id设计

如果只考虑用户信息的ID查询,只要保证动态Id唯一即可;考虑到要按照用户维度去展示用户的信息列表,信息ID里最好包含用户ID,这样方便查询某个用户发布的所有的信息(动态)。
类似于电商场景查询某个用户的所有订单。

信息有个唯一表示,用rec_id表示
根据推荐信息唯一标识rec_id获取点赞数收藏数评论数转发数 是一个简单查询。

(2.1) 用数字表示信息ID

int类型占用4字节,32位,最多可以表示 2^32个数字,考虑上符号,以及0,正数最多有 2^31-1 = 21 4748 3647 个,大概21亿多。

long类型占用8字节,64位,最多可以表示 2^64个数字,考虑到id都是正数,而且一般不用0,所以最多有 2^63-1 = 922 3372 0368 5477 5807 个,大概922亿亿个。

可以用8字节的Long类型表示信息ID(msg_id),msg_id的设计可以参考雪花算法的思想, 64位可以按照 符号位(1位)、城市(6位)、用户(8-10位)、时间戳()、随机字符()等来设计。


(3) 计数器设计

(3.1) 使用数据库做计数器

在刚开始阶段,用户较少并且用户发布的信息/动态较少时,可以使用数据库来存储。

CREATE TABLE msg_count (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`msg_id` bigint(11) NOT NULL COMMENT '用户id',
`like_num` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数',
`comments_num` int(11) NOT NULL DEFAULT '0' COMMENT '评论数',
`favor_count` int(11) NOT NULL DEFAULT '0' COMMENT '收藏数',
`forwards_count` int(11) NOT NULL DEFAULT '0' COMMENT '转发数',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_idx_msg_id` (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='信息(动态)计数表';

按信息ID(msg_id)取模,把数据拆分到N个表(N是2的幂次方)。
如果要扩容,把N个表拆成 N * 2^x,迁移数据时只需要把 1个表拆到 2^x个表即可。

访问量太大怎么办?
使用缓存+数据库,先查缓存,缓存查不到再查数据库。

问题:
1、空数据也得Cache(有一半以上的微博是没有转发也没有评论的,但是依然有大量的访问会查询数据库);
2、Cache频繁失效(由于计数更新非常快,所以经常需要失效Cache再重新缓存,还会导致数据不一致);

更好的硬件解决。 上FusionIO + HandleSocket + 大内存 优化。

总的来说,MySQL分库分表 + Cache加速的方案 对于数据规模和访问量不是特别巨大的情况下,是非常不错的解决方案。


(3.2) 使用Redis做缓存

Redis作为一个简单的内存数据库,提供了多种数据类型,可以使用string类型的 incr 来计数。
具体命令参考 https://redis.io/commands/incr/

简单的来估算一下数据存储量,按照Redis 6.0.0的实现,在64位系统,指针为8字节来估算

假设 key 为8字节,value为 4字节,通过incr存储的话
key value 会存储在 dictEntry里,详细的结构: redisServer -> db0 -> dict -> ht[0] -> dictht -> dictEntry

redis整体数据结构
放到db->dict->ht[0]->table中存储dictEntry的指针,需要8个字节;
存储一个kv首先需要一个dictEntry,dictEntry里面有3个指针,每个指针占用8字节,占用 3*8字节=24字节
key的指针指向一个RedisObject(16字节),RedisObject的ptr又指向一个SDS,一个Key(msg_id 8字节)通过sds存储,需要 8+

(4) 系统安全问题

(4.1) 系统怎么应对爬虫?

网关+风控处理

参考资料

[1] [WeiDesign]微博计数器的设计(上)
[2] [WeiDesign]微博计数器的设计(下)