1.背景
1.1.关系链业务
社交系统(微信、QQ,支付宝)需要解决的一个工程问题是如何完成海量用户关系存储,并高效查询。典型代表系统是微信好友、QQ好友、蚂蚁森林游戏中的好友关系、微博粉丝、知乎粉丝偶像列表等等。这类业务其实就是关系链业务,而关系链分弱好友关系和强好友关系。
弱好友关系不需要彼此的同意,比如用户A关注用户B这类关注和粉丝的关系。而强好友关系需要经过彼此同意,比如用户A请求添加B为好友,用户B同意后,则AB就互为好友。
1.2.举例
举个例子:假设需要你设计蚂蚁森林这个系统,系统假设有1亿用户,每个用户平均有100个好友,如果是你需要如何设计数据库表结构呢?
如果进行了数据库表容量评估你会发现:如果只用一张表来存储将会产生100亿条记录,这个规模已经算超大表了。使用超大表一般会产生2个问题:
- 这个数据规模即使建立索引,查询性能也很受单台资源的影响,
- 互联网系统需求总是在不断变动新增字段锁表时间会特别长。基于性能考虑,单库单表很难满足需求。
基于以上分析,面对海量用户关系,一般需要分库分表,那么问题来了,怎么进行分区呢(即分区策略是什么)?
2.基本设计
2.1.弱好友关系
弱好友关系一般需要两张表:attention(关注表)、fans(粉丝表)
CREATE TABLE `attention` (
`id` int NOT NULL AUTO_INCREMENT,
`uid` int NOT NULL,
`attention_uid` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_uid` (`uid`)
) ENGINE=InnoDB;
CREATE TABLE `fans` (
`id` int NOT NULL AUTO_INCREMENT,
`uid` int NOT NULL,
`fans_uid` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_uid` (`uid`)
) ENGINE=InnoDB;
关注表我们称为正表,粉丝表称为反表。
2.2.强好友关系
**方案1:**强好友关系即frient表
CREATE TABLE `friend` (
`id` int NOT NULL AUTO_INCREMENT,
`uid` int NOT NULL,
`friend_uid` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_uid` (`uid`),
KEY `idx_friend_uid` (`friend_uid`)
) ENGINE=InnoDB;
uid = 1的用户加uid=2好友成功,那么插入的记录是{1,2}还是{2,1}呢,其实都可以,但为了需要约定一个规则,比如uid < friend_id。
此时,如果查询uid = 2的好友,则为:
select * from friend where uid = 2
UNION
select * from friend where friend_uid = 2;
**方案2:**强好友关系肯定属于弱好友关系(但弱好友关系推到不出是强好友关系),怎么理解呢?如果1,2两个用户互相是好友,那么1,2两用户互为关注同时互为粉丝。这样方案会导致持久化1,2好友的记录数是4条。产生了冗余。初步看好像没怎么好处,但在数据量很大需要做分库分表时,冗余的价值就产生了,具体体现在:
- Friend表在数据量大时很难做分库分表,比如如果根据uid来分库,那么通过friend_uid查询就需要一次全量遍历所有库。
- attention和fans表实现分区就没那么困难,即都通过uid分库。
3.分库分表基本原则
从好友基本设计中我们了解到在进行水平分库分表时需要把握两个基本原则:
- 避免扫库扫表:
- 避免跨库事务;
基于此,我们的对应策略是:
- 分析业务查询场景,做数据冗余:按用户做分区,同一用户在一个DB,如果可能涉及跨库查询,可以通过冗余字段来满足大部分常见,比如保留好友用户_id时同时保留好友名称。
- **使用搜索引擎:**避免扫库扫表的查询压力。
- **跨库事务:**跨库的数据一致性属于分布式数据一致性范畴,有很多方案可以保证数据一致性,我们后面分析。
4.跨库事务
**方案1:**异步消息
采用数据冗余方案可通过异步消息来写入冗余数据,为此需要多引入一个消息组件(比如消息队列)和一个数据复制服务(用于接收消息并同步冗余数据)。
采用异步消息存在短暂的不一致窗口。
**方案2:**异步复制
比如通过数据库操作日志(比如MySQL的BInlog)完成异步操作。数据服务接收并解析Binlog完成数据同步。
跨库事务就到这,关于数据一致性的解决方案有机会我们单独分析分析。
The end.