用户/帖子/好友/订单中心如何进行数据库水平切分

简介: 用户/帖子/好友/订单中心如何进行数据库水平切分

本文主要节选和总结自沈剑大佬的四篇文章

必备,前台与后台分离的架构实践

单KEY业务,数据库水平切分架构实践 | 架构师之路

1对多业务,数据库水平切分架构一次搞定 | 架构师之路

多对多业务,数据库水平切分架构一次搞定

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也有对应的用户,如果不是同一个用户,不让查。


相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
4月前
|
小程序 JavaScript 安全
【微信小程序-原生开发】转发给好友/群,分享到朋友圈(含单页模式访问云开发数据库的方法)
【微信小程序-原生开发】转发给好友/群,分享到朋友圈(含单页模式访问云开发数据库的方法)
176 0
|
4月前
|
关系型数据库 MySQL 数据库
生成订单的过程------支付系统21------支付宝支付----统一收单下单并支付页面接口----创建订单,下订单,我们要在我们数据库的订单表中,设置订单,订单表常用数据库设置格式
生成订单的过程------支付系统21------支付宝支付----统一收单下单并支付页面接口----创建订单,下订单,我们要在我们数据库的订单表中,设置订单,订单表常用数据库设置格式
|
存储 SQL NoSQL
01MyCat - 数据库切分概述
01MyCat - 数据库切分概述
50 0
|
存储 前端开发 中间件
应用数据库常见的数据切分方式
应用数据库常见的数据切分方式
148 0
|
JSON JavaScript 小程序
小程序里显示附近的人,云开发数据库实现附近的人,按照位置远近排序,附近多少公里内的好友
小程序里显示附近的人,云开发数据库实现附近的人,按照位置远近排序,附近多少公里内的好友
178 0
|
存储 SQL NoSQL
MyCat:第一章:数据库切分概述
MyCat:第一章:数据库切分概述
|
SQL 数据库
47-网上商城数据库-订单数据操作
47-网上商城数据库-订单数据操作
238 0
|
数据库
LeetCode(数据库)- 每件商品的最新订单
LeetCode(数据库)- 每件商品的最新订单
105 0
|
数据库
LeetCode(数据库)- 最近的三笔订单
LeetCode(数据库)- 最近的三笔订单
123 0
|
数据库
LeetCode(数据库)- 好友申请II:谁有最多的好友
LeetCode(数据库)- 好友申请II:谁有最多的好友
91 0