目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

数据库拆分

主从架构无法解决单节点容量问题

  • 部分解决高可用
  • 降低主库一部分读压力
  • 无法解决容量问题

主从结构解决了高可用,读扩展,但是单机容量不变,单机写性能无法解决。
提升容量-->分库分表,分布式,多个数据库,作为数据分片的集群提供服务。
降低单个节点的写压力。 提升整个系统的数据容量上限。

单机数据库已经无法适应互联网的发展

  传统的将数据集中存储至单一数据节点的解决方案,在容量性能可用性运维成本这三方面已经难于满足互联网的海量数据场景。我们在单库单表数据量超过一定容量水位的情况下,索引树层级增加,磁盘 IO 也很可能出现压力,会导致很多问题。

  • 性能:由于关系型数据库大多采用 B+树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
  • 可用性:从可用性的方面来讲,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。
  • 运维:当一个数据库实例中的数据达到阈值以上,数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。

1、无法执行 DDL,比如添加一列,或者增加索引,都会直接影响线上业务,导致长时间的数据库无响应。(14TB的数据加索引需要几个小时才能完成,DDL操作都会缩表,几个小时不能访问某张表线上业务是无法接受的)
2、无法备份,与上面类似,备份会自动线 lock 数据库的所有表,然后到处数据,量大了就没法执行了。
3、影响性能与稳定性,系统越来越慢,随时可能会出现主库延迟高,主从延迟很高,且不可控,对业务系统有极大的破坏性影响。
4、主从数据同步延迟较高

扩展立方体

  • X 轴:通过 clone 整个系统复制,集群 (完整扩展,水平扩展)
  • Y 轴:通过解耦不同功能复制,业务拆分 (按需扩展,垂直扩展)
  • Z 轴:通过拆分不同数据扩展,数据分片(VIP拆分)

垂直拆分

  垂直拆分是根据数据库里面的数据表的相关性进行拆分

  垂直分库分表 => 分布式服务化 => 微服务架构

垂直拆分:拆库

  垂直拆分(拆库):将一个数据库,拆分成多个提供不同业务数据处理能力的数据库,拆库比较常用,能解决大部分问题。

  例如拆分所有订单的数据和产品的数据,变成两个独立的库,这种方式对业务系统有极大的影响,因为数据结构本身发生了变化,SQL 和关联关系也必随之发生了改变。原来一个复杂 SQL 直接把一批订单和相关的产品都查了出来,现在这个 SQL 不能用了,得改写 SQL 和程序。先查询订单库数据,拿到这批订单对应的所有产品 id,再根据产品 id 集合去产品库查询所有的产品信息,最后再业务代码里进行组装。

垂直拆分:拆表

  垂直拆分(拆表):如果单表数据量过大,还可能需要对单表进行拆分。

  比如一个 200 列的订单主表,拆分成十几个子表:订单表、订单详情表、订单收件信息表、订单支付表、订单产品快照表等等。这个对业务系统的影响有时候可能会大到跟新作一个系统差不多。对于一个高并发的线上生产系统进行改造,就像是给心脑血管做手术,动的愈多,越核心,出现大故障的风险越高。所以,我们一般情况下,尽量少用这种办法。

垂直拆分的一般做法

  1. 梳理清楚拆分范围和影响范围
  2. 检查评估和重新影响到的服务
  3. 准备新的数据库集群复制数据
  4. 修改系统配置并发布新版上线

注意
1、先拆分系统,还是先拆分数据库?(先拆封系统,否则就会涉及到灰度的机制)
2、先拆分多大范围?(拆一个表出来?拆一个库出来?还是拆N个表?N个库?取决于对业务数据的认知度)

垂直拆分的优缺点

优点

  1. 单库(单表)变小,便于管理和维护
  2. 对性能和容量有提升作用
  3. 改造后,系统和数据复杂度降低(SQL复杂度降低,业务复杂度降低)
  4. 可以作为微服务改造的基础(不改造数据库的微服务是假的微服务)

缺点

  1. 库变多,管理变复杂
  2. 对业务系统有较强的侵入性(SQL、业务逻辑都需要改变)
  3. 改造过程复杂,容易出故障(广度和深度很大,容易出现问题)
  4. 拆分到一定程度就无法继续拆分(没有解决单表大数据量的问题)

水平拆分

  水平切分就是要把一个表按照某种规则把数据划分到同业务属性的不同表或数据库里。(表还是那么多,拆分出来的数据跟业务属性本身是没有关系的

例如:像计费系统和日志系统,通过按时间来划分表就比较合适。

常见的三种方式

  1. 分库
  2. 分表
  3. 分库分表(常用)

水平拆分(按主键分库分表)

  直接对数据进行分片,有分库和分表两个具体方式,但是都只是降低单个节点数据量,但不改变数据本身的结构。这样对业务系统本身的代码逻辑来说,就不需要做特别大的改动,甚至可以基于一些中间件做到透明

  比如把一个 10 亿条记录的订单单库单表(orderDB 库 t_order 表)
  我们按照用户 id 除以 32 取模,把单库拆分成 32 个库orderDB_00..31
  再按订单 id 除以 32 取模,每个库里再拆分成 32 个表t_order_00..31
  这样一共是 1024 个子表(32 * 32),
  单个表的数据量就只是 100万条(1000000000 / 1024 = 976,562.5)
  一个查询如果能够直接路由到某个具体的字表,比如:orderDB05.t_order_10 (6库11表),那么查询效率就会高很多

阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表

水平拆分(按时间分库分表)

  很多时候,数据是有时间属性的,所以自然可以按照时间维度来拆分。比如当前数据表和历史数据表,甚至按季度,按月,按天来划分不同的表。这样我们按照时间维度来查询数据时,就可以直接定位到当前的这个子表。

水平拆分(强制按条件指定分库分表)

  比如配置好某些用户的数据进入单独的库表,其他数据默认处理。

  vip客户的访问量特别大,可以为这些用户单独创建数据库

水平拆分(自定义条件分库分表)

  指定某些条件的数据进入到某些库或表。常见的一些分库分表的中间件或框架也支持自定义规则(一些判断条件和分表策略:精确分表、范围分表……)

例如:id等于0~500万的在一张表中,id等于5000001~1000万在第二张表中

水平拆分:分库?还是分表?

  为什么有些 DBA 不建议分表,只建议分库?
  为什么一些中间件,也只支持分库,不能分表?

选择当前状态说明DBA角度
分库数据本身的读写压力较大:磁盘IO、网络IO压力较大分表:数据库总数据量没有变化,只是单表的数据量小了而已(单表查询会快),整体的磁盘IO、网络IO减少;分库:不同的数据库运行在不同的节点上拥有自己独立的磁盘IO、网络IO,整体的IO吞吐能力要优于在单节点的分表操作表变多了,DBA的管理和维护变的复杂
分表磁盘IO、网络IO压力不大,但是数据查询时间慢单表的数据量多(10亿条数据)只能做分表,分库解决不了问题

水平分库分表的优缺点

优点

  1. 解决容量问题(单表10亿条数据)
  2. 比垂直拆分对系统影响小
  3. 部分提升性能和稳定性

缺点

  1. 集群规模大,管理复杂(单表10亿条数据拆分:1024个表的管理变的复杂)
  2. 复杂 SQL 支持问题(业务侵入性、性能:查询多个数据库再进行合并,中间件对复杂查询的支持功能不够完善)
  3. 数据迁移问题(数据库迁移,旧数据迁移到多个新的数据库)
  4. 一致性问题(一个操作需要跨两个数据库,数据库之间的事务是彼此独立的,为了保证一致性需要用到分布式事务:强一致、弱一致、最终一致)

分库分表注意点

### 库名、表名、表结构
create schema demo_ds_0;
create schema demo_ds_1;

CREATE TABLE IF NOT EXISTS demo_ds_0.t_order_0 (order_id BIGINT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, status VARCHAR(50), PRIMARY KEY (order_id));
CREATE TABLE IF NOT EXISTS demo_ds_0.t_order_1 (order_id BIGINT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, status VARCHAR(50), PRIMARY KEY (order_id));
CREATE TABLE IF NOT EXISTS demo_ds_1.t_order_0 (order_id BIGINT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, status VARCHAR(50), PRIMARY KEY (order_id));
CREATE TABLE IF NOT EXISTS demo_ds_1.t_order_1 (order_id BIGINT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, status VARCHAR(50), PRIMARY KEY (order_id));

### 按用户分库,按订单分表( 如果)
# 正确的分法
分库: user_id % 2	 ==>   ds_${user_id % 2}
分表: order_id % 2   ==>   t_order_${order_id % 2}

# 错误的分法(这样会导致有2张表始终没有数据)
分库: user_id % 2	==>   ds_${user_id % 2}
分库: user_id % 2	==>   dt_order_${user_id % 2}

# dataSource (单节点)
ds_0:
   url: jdbc:mysql

# dataSources
ds_0:
  url: jdbc:mysql://192.168.10.11:3306/demo_ds_0?serverTimezone=UTC&useSSL=false
ds_1:
  url: jdbc:mysql://192.168.10.11:3306/demo_ds_1?serverTimezone=UTC&useSSL=false

# dataSources(多节点)
ds_0:
  url: jdbc:mysql://192.168.10.11:3306/demo_ds_0?serverTimezone=UTC&useSSL=false
ds_1:
  url: jdbc:mysql://192.168.10.12:3306/demo_ds_0?serverTimezone=UTC&useSSL=false

数据的分类管理

  随着我们对业务系统、对数据本身的进一步了解,我们就会发现,很多数据对质量的要求是不同的
  比如,订单数据,肯定一致性要求最高,不能丢数据。而日志数据和一些计算的中间数据,我们则是可以不要那么高的一致性,丢了不要了,或者从别的地方找回来。

定期的清除
  同样地,我们对于同样一张表里的订单数据,也可以采用不同策略,无效订单如果比较多,可以定期的清除或者转移(一些交易系统里有 80%以上是的机器下单然后取消的无意义订单,没有人会去查询它,所以可以清理)。

具体场景具体优化

  1. 最近一周下单但是未支付的订单,被查询和支付的可能性较大,再长时间的订单,我们可以直接取消掉。
    定义一周内下单但未支付的数据为热数据,同时放到数据库和内存;
  2. 最近 3 个月下单的数据,被在线重复查询和系统统计的可能性最大。
    定义三个月内的数据为温数据,放到数据库,提供正常的查询操作;
  3. 超过 3 个月、3 年以内的数据,查询的可能性非常小,我们可以不提供在线查询。
    定义3个月到3年的数据,为冷数据,从数据库删除,归档到一些便宜的磁盘,用压缩的方式(比如MySQL 的 tokuDB 引擎,可以压缩到几十分之一)存储,用户需要邮件或者提交工单来查询,我们导出后发给用户
  4. 3 年以上的数据,我们可以直接不提供任何方式的查询。
    定义 3 年以上的数据为冰数据,备份到磁带之类的介质上,不提供任何查询操

框架和中间件

Java 框架层面

  • TDDL(淘宝,商业闭源)
  • Apache ShardingSphere-JDBC

中间件层面

  • DRDS(阿里云,商业闭源)
  • Apache ShardingSphere-Proxy
  • MyCat/DBLE
  • Cobar
  • Vitness(golang)
  • KingShard(golang)

数据库中间件的技术演进

  单核 --> 高主频(瓶颈) --> 多核(瓶颈) --> 分布式集群(大量廉价PC协同功能)VS CAP

  业务侧增强 --> 数据库中间件(模拟数据库) --> 分布式数据库 --> 数据网格

ShardingSphere

  Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款相互独立,却又能够混合部署配合使用的产品组成。 它们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。

三大组件对比

ShardingSphere-Sidecar(规划中,简单知道就行)

  • 定位为 Kubernetes 的云原生数据库代理,以 Sidecar 的形式代理所有对数据库的访问
  • 通过无中心、零侵入的方案提供与数据库交互的啮合层,即 Database Mesh,又可称数据库网格

ShardingSphere-JDBC

  • 它使用客户端直连数据库,以 jar 包形式提供服务
  • 无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架
  • 适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis,或直接使用 JDBC
  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, HikariCP 等;
  • 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 访问的数据库
  • 采用无中心化架构,与应用程序共享资源,适用于 Java 开发的高性能的轻量级 OLTP 应用

ShardingSphere-Proxy

  • 数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持
  • 向应用程序完全透明,可直接当做 MySQL/PostgreSQL
  • 它可以使用任何兼容 MySQL/PostgreSQL 协议的访问客户端(如:MySQL Command Client, MySQL Workbench, Navicat 等)操作数据

数据迁移

迁移是最容易出故障的一个点

  • 设计新系统容易,但是我们处理的都是老系统和历史数据
  • 怎么能更平滑的迁移旧数据到新的数据库和系统
  • 特别是在异构的数据库结构情况下
  • 达到数据准确,迁移速度快,减少停机,对业务影响小

数据迁移:全量

 全量数据导出和导入(直接复制的话,可以 msql-dump 后全量导入;如果是异构数据,需要用程序来处理)
1、业务系统停机,
2、数据库迁移,校验一致性,
3、然后业务系统升级,接入新数据库。

优点:简单
缺点:停机时间长

数据迁移:全量+增量

 依赖于数据本身的时间戳
1、先同步数据到最近的某个时间戳
2、然后在发布升级时停机维护,
3、再同步最后一段时间(通常是一天)的变化数据。
4、最后升级业务系统,接入新数据库。

优点:停机时间短(星期六之前的所有历史数据提前迁移好,不需要关闭服务)
缺点:增加目前数据库的读压力

数据迁移:binlog+全量+增量

 通过主库或者从库的 binlog 来解析和重新构造数据,实现复制。一般需要中间件等工具的支持(把自己模拟成一个MySQL的从库)。可以实现多线程,断点续传,全量历史和增量数据同步。中间件可以做到:
1、实现自定义复杂异构数据结构;
2、实现自动扩容和缩容,比如分库分表到单库单表,单库单表到分库分表,分4个库表到分64个库表。
优点:1、2
缺点:需要使用能够把自己模拟成从库去订阅业务库的binlog的这种中间件的支持,推荐迁移工具 shardingsphere-scaling

迁移工具 shardingsphere-scaling

  • 支持数据全量和增量同步。
  • 支持断点续传和多线程数据同步。
  • 支持数据库异构复制和动态扩容。
  • 具有 UI 界面,可视化配置。

作者:Soulboy