本文主要节选和总结自沈剑大佬的四篇文章
1、前台后台分离架构
1.1 前后台用户访问的特点
用户侧,前台访问的特点是:
- 访问模式有限
- 访问量较大,DAU不达到百万都不好意思说是互联网C端产品
- 对访问时延敏感,用户如果访问慢,立马就流失了
- 对服务可用性要求高,系统经常用不了,用户还会再来么
- 对数据一致性的要求高,关乎用户体验的事情就是大事
运营侧,后台访问的特点是:
- 访问模式多种多样,运营销售可能有各种奇形怪状的,大批量分页的,查询需求
- 用户量小,访问量小
- 访问延时不这么敏感,大批量分页,几十秒能出结果,也能接受
- 对可用性能容忍,系统挂了,10分钟之内重启能回复,也能接受
- 对一致性的要求始终,晚个30秒的数据,也能接受
1.2 存在的问题
- 后台的低性能访问,对前台用户产生巨大的影响,本质还是耦合
- 随着数据量变大,为了保证前台用户的低时延,质量,做一些类似与分库分表的升级,数据库一旦变化,可能很多后台的需求难以满足
1.3 优化思路
冗余数据,前台与后台服务与数据分离,解耦。后台使用ES或者hive在进行数据存储,用以满足“各种奇形怪状的,大批量分页的,查询需求”
所谓的“1对1”,“1对多”,“多对多”,来自数据库设计中的“实体-关系”ER模型,用来描述实体之间的映射关系。
2、单 key 类业务(用户中心)
用户中心一个典型的“单KEY”类业务,一对一,一个用户只有一个用户名,主要提供用户注册、登录、信息查询与修改的服务,其核心元数据为:
User(uid, login_name, passwd, sex, age, nickname, …)
水平切分算法:哈希法和范围法,优缺点见“数据库高并发和高可用方案”
水平切分后碰到的问题:
- 通过uid属性查询能直接定位到库,通过非uid属性查询不能定位到库
2.1 前后台分离
系统两类用户,普通用户和后台运营同学。这两类用户由于访问模式、访问量和时延容忍度上有比较大的区别,所以一般采用前后台分离的架构。
前台用户侧的非主属性查找:
非分片键属性上的查找,遍历分库扫描法、映射索引法、基因法、用属性生成key法
后台运营侧的非主属性查找:
索引外置(元数据跟索引数据分离,一般元数据存在数据库/hive,索引用 ES )
3、一对多业务(帖子中心)
帖子中心是一个典型的1对多业务。一个用户可以发布多个帖子,一个帖子只对应一个发布者。
帖子中心主要提供的功能是:帖子搜索、帖子的增删改、查询用户发布的帖子列表
帖子搜索是个query 搜索需求,所以应该用 ES 来实现。其他非query的查询一般直接查数据库。
3.1 如何分库
如果选择 tiezi_id 作为分片键,那么一个用户发布的所有帖子可能会落到不同的库上,通过uid来查询会比较麻烦
如果选择用 uid 来水平切分,uid 的查询可以直接定位到分片库,但是通过 tiezi_id 的查询就得遍历所有的分片库
3.1.1 优化
- 通过基因法把 uid 融入到tiezi_id 中,这样能同时根据 uid 和 tiezi_id 确定分片库;
- 使用 uid 来分片,同一个用户发布的帖子落在同一个库上,需要通过索引表或者缓存来记录tid与uid的映射关系,通过tid来查询时,先查到uid,再通过uid定位库
- 或者使用 tiezi_id 来分片,但是建立tiezi_id 跟 uid 的映射索引表,通过 uid 查询就需要先查索引表拿到 tiezi_id,在用 tiezi_id 去查帖子表。
3、多对多业务(好友中心)
一个学生可以选修多个课程,一个课程可以被多个学生选修,这里学生与课程时间的关系,就是多对多关系。
弱好友关系的建立,不需要双方彼此同意:关注和粉丝(被关注)
强好友关系的建立,需要好友双方彼此同意:好友
3.1 弱好友关系实现
核心表关注表和粉丝表
- guanzhu(uid, guanzhu_uid); 用户记录uid 关注的所有用户guanzhu_uid
- fensi(uid, fensi_uid); 用来记录 uid 所有粉丝用户fensi_uid
需要强调的是,一条弱关系的产生,会产生两条记录,一条关注记录,一条粉丝记录。
3.2 强好友关系实现
方式一
- friend(uid1, uid2);
每次互相添加为好友,只新增一条记录到 friend 表,约定 uid 小的作为 uid1,另一个作为 uid2
查询 uid=2 的所有好友
select * from friend where uid1=2
union
select * from friend where uid2=2
缺点:
查询用户的好友需要同时通过 uid1 和 uid2 查询,如果使用uid1来分库,那么uid2上的查询就需要遍历多库
方式二
- friend(uid1, uid2); 冗余存储
每次互相添加为好友,新增 2 条记录到 friend 表,一条记录以用户 A 作为uid1,另一条记录以用户 B 作为 uid1。
好处
- 这样查询 uid=2 的记录就变成了:
select * from friend where uid1=2
- 分库方便,用 uid1 分库即可把同个用户所有的好友放在同个分库里
坏处
多存储了一条冗余数据
方式三
- guanzhu(uid, guanzhu_uid); 用户记录uid 关注的所有用户guanzhu_uid
- fensi(uid, fensi_uid); 用来记录 uid 所有粉丝用户fensi_uid
强好友关系是弱好友关系中的一种特殊情况,所以也可以用 粉丝和关注的形式来实现,但是每次用户AB 互相添加好友,需要个每个用户添加一个粉丝和关注,这样每个好友关系需要存 4 条记录。
优点是分库方便。缺点是多存储了 3 条冗余数据。
3.3 如何实现数据冗余
以强好友关系中的方式二数据冗余为例,假设用户 A 的好友数据在 T1 分库,用户 B 的好友数据在 T2 分库。
方式一:服务同步冗余
由好友中心服务同步写冗余数据。服务先插入T1数据,服务再插入T2数据,都插入成功后服务返回业务方新增数据成功
优点:
- 不复杂,服务层由单次写,变两次写
- 数据一致性相对较高(因为双写成功才返回)
缺点:
- 请求的处理时间增加(要插入2次,时间加倍)
- 数据仍可能不一致,例如第二步写入T1完成后服务重启,则数据不会写入T2
方式二:MQ 异步冗余
服务层写完 T1 数据后,异步发送一个MQ 消息,专门的数据复制服务来写入冗余数据 T2。
优点:
- 请求处理时间短(只插入1次)
缺点:
- 系统的复杂性增加了,多引入了一个组件(消息总线)和一个服务(专用的数据复制服务)
- 因为返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)
- 在消息总线丢失消息时,冗余表数据会不一致
方式三:Binlog 异步冗余
服务层写完 T1 数据后,专门的数据复制服务监听 T1 库的 binlog,完成 T2 数据的插入。
优点:
- 数据双写与业务完全解耦
- 请求处理时间短(只插入1次)
缺点:
- 返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)
- 数据的一致性依赖于线下服务或者任务的可靠性
3.4 如何保证数据一致性
上一节的讨论可以看到,不管哪种方案,因为两步操作不能保证原子性,总有出现数据不一致的可能,高吞吐分布式事务是业内尚未解决的难题,此时的架构优化方向,并不是完全保证数据的一致,而是尽早的发现不一致,并修复不一致。
最终一致性,是高吞吐互联网业务一致性的常用实践。 更具体的,保证数据最终一致性的方案有三种。
方法一:线下扫面正反冗余表全部数据
线下启动一个离线的定时扫描工具,不停的比对正表T1和反表T2(查询每条uid1=1,uid2=2的记录是否存在一条,uid1=2, uid2=1的记录),如果发现数据不一致,就进行补偿修复。
优点:
- 比较简单,开发代价小
- 线上服务无需修改,修复工具与线上服务解耦
缺点:
- 扫描效率低,会扫描大量的“已经能够保证一致”的数据
- 由于扫描的数据量大,扫描一轮的时间比较长,即数据如果不一致,不一致的时间窗口比较长
方法二:线下增量扫描增量数据
启动离线定时扫描工具,只扫描过去某段时间增量的数据,如果发现数据不一致,就进行补偿修复。
其实沈剑大佬这里写的是T1 和 T2 数据写完后都新增一条数据库log记录,离线对比增量 log记录。
> 如果每次写t1和t2都插入一条log记录,那岂不是又回到了服务同步冗余,那岂不是延迟又会增加???
优点:
- 比较简单,开发代价小
- 线上服务无需修改,修复工具与线上服务解耦
缺点:
- 虽然比方法一更实时,但时效性还是不高,不一致窗口取决于扫描的周期
方法三:线上实时检测法
订阅服务订阅数据库的 binlog,假设正常情况下,msg1和msg2的binlog接收时间应该在3s以内,如果检测服务在收到msg1后没有收到msg2,就尝试检测数据的一致性,不一致时进行补偿修复。
具体实现就是:监听到某个正表或者反表的Binlog后,等待几秒钟,查另一个表的记录,如果没查到,就进行不一致补偿修复。
优点:
- 效率高,实时性高
缺点:
- 方案比较复杂,上线引入了消息总线这个组件
- 线下多了一个订阅总线的检测服务
4、多key业务(订单中心),数据库水平切分
所谓的“多key”,是指一条元数据中,有多个属性上存在前台在线查询需求。
4.1 业务介绍
订单中心是一个非常常见的“多key”业务,主要提供订单的查询与修改的服务,其核心元数据为:
Order(oid, buyer_uid, seller_uid, time,money, detail…);
其中:oid为订单ID,主键;buyer_uid为买家uid;seller_uid为卖家uid;time, money, detail, …等为订单属性
首先肯定需要采用前后台分离架构。其次前台对订单id,卖家id和买家id都有查询需求。
4.2 分库后存在的问题
一般来说在业务初期,单库单表就能够搞定这个需求,随着订单量的越来越大,数据库需要进行水平切分,由于存在多个key上的查询需求,用哪个字段进行切分,成了需要解决的关键技术问题:
- 如果用oid来切分,buyer_uid和seller_uid上的查询则需要遍历多库
- 如果用buyer_uid或seller_uid来切分,其他属性上的查询则需要遍历多库
4.3 实现方案
方法一
新增一张订单冗余表,Order_copy,表结构跟 Order表一样。每次插入订单表时,也在Order_copy冗余表插入一条冗余数据。
Order表使用订单id作为分片键,但是订单id使用基因法融入了买家id,所以可以同时实现通过订单id和买家id定位到库
Order_copy 冗余表使用卖家id作为分片键,用于满足通过卖家id的查询需求。
方法二
新加一个数据库,卖家库,把订单表分别在订单库和卖家库里建一份。
订单库里的订单表使用订单id作为分片键,但是订单id使用基因法融入了买家id,所以可以同时实现通过订单id和买家id定位到库。
卖家库里的订单表使用卖家id作为分片键,用于满足通过卖家id的查询需求。
4.4 冗余方式和一致性校验方式
见好友中心的三种冗余方式和三种一致性校验方式。
数据冗余的方法有很多种:
- 服务同步双写
- MQ 异步双写
- Binlog 异步双写
保证数据最终一致性的方案有三种:
- 冗余数据全量定时扫描
- 冗余数据增量日志扫描
- 冗余数据线上消息实时检测
5、问题:
问:既然数据库容易成为系统的瓶颈,为什么不采用hadoop等大数据框架代替mysql
hadoop 和 MySQL 的适用场景不一样,一个实时访问+关系型,一个离线访问大数据存储,解决的不是一个问题。
问:如果前后台数据分离,如果运营平台修改数据,是通过调用服务来实时修改用户中心的数据吗?
通过 MQ 同步数据
问:如果后面业务增长,部分库的用户发帖量明显高于其它库,出现单库/单表性能问题吗
只要不是一个uid发帖很多,例如1000个uid发帖很多,这1000个uid一定也是均匀分布的,数据不会不均匀
问:强好友关系使用关注和粉丝方式来实现时,能不能每个关系只插入 2 条数据,即在粉丝和关注表插入一条数据
不行,这样的话可能用户A的好友数据一部分在粉丝表,一部分在关注表,跟 friend表有一样的问题。需要同时查粉丝表和关注表才能拿到用户 A 的所有好友列表。
> 关注表只能查到我主动关注别人的列表,别人主动关注我的列表需要在粉丝表里查,这样查询我所有好友需要同时查两个表。所以不管我主动关注别人,还是别人主动关注我,都需要在关注表里给我插入一条关注记录,这样查关注表就能获得所有好友列表。
问:强好友关系中的实现方式一,能用 or 来代替union吗,即 select * from friend uid1=2 or uid2=2
union 能分别命中uid1和uid2的索引,效率很高,union好像有临时表
or 的左右两个key都有索引时,会转换成 union;否则无法利用索引,发生全表扫描。
问:文中一致性检查的方法三只是针对同步冗余这一场景吗
其他几种冗余方案也可以,只是两个消息的间隔时间稍微要设置长一点。
问:前端通过js调用后端接口,如何控制用户访问权限
sso,用户有自己的token,查询订单信息,token能得到用户,订单ID也有对应的用户,如果不是同一个用户,不让查。