一, 为什么要分库分表
软件时代, 传统应用都有这样一个特点: 访问量, 数据量都比较小, 单库单表都完全可以支撑整个业务. 随着互联网的发展和用户规模的迅速扩大, 对系统的要求也越来越高. 因此传统的 MySQL 单库单表架构的性能问题就暴露出来了. 而有下面几个因素会影响数据库性能:
数据量
MySQL 单库数据量在 5000 万以内性能比较好, 超过阈值后性能会随着数据量的增大而变弱. MySQL 单表的数据量是 500w-1000w 之间性能比较好, 超过 1000w 性能也会下降.
磁盘
因为单个服务的磁盘空间是有限制的, 如果并发压力下, 所有的请求都访问同一个节点, 肯定会对磁盘 IO 造成非常大的影响.
数据库连接
数据库连接是非常稀少的资源, 如果一个库里既有用户, 商品, 订单相关的数据, 当海量用户同时操作时, 数据库连接就很可能成为瓶颈.
为了提升性能, 所以我们必须要解决上述几个问题, 那就有必要引进分库分表, 当然除了分库分表, 还有别的解决方案, 就是 NoSQL 和 NewSQL,NoSQL 主要是 MongoDB 等, NewSQL 则以 TiDB 为代表.
二, 分区分库分表的原理
1, 什么是分区, 分表, 分库
(1)分区
就是把一张表的数据分成 N 个区块, 在逻辑上看最终只是一张表, 但底层是由 N 个物理区块组成的, 分区实现比较简单, 数据库 MySQL,oracle 等很容易就可支持.
(2)分表
就是把一张表按一定的规则分解成 N 个具有独立存储空间的实体表. 系统读写时需要根据定义好的规则得到对应的字表明, 然后操作它.
(3)分库
一旦分表, 一个库中的表会越来越多, 将整个数据库比作图书馆, 一张表就是一本书. 当要在一本书中查找某项内容时, 如果不分章节, 查找的效率将会下降. 而同理, 在数据库中就是分区.
2, 什么时候使用分区?
一张表的查询速度已经慢到影响使用的时候.
sql 经过优化
数据量大
表中的数据是分段的
对数据的操作往往只涉及一部分数据, 而不是所有的数据
最常见的分区方法就是按照时间进行分区, 分区一个最大的优点就是可以非常高效的进行历史数据的清理.
(1)分区的实现方式
mysql5 自 5.1 开始对分区 (Partition) 有支持.
(2)分区类型
目前 MySQL 支持范围分区 (RANGE), 列表分区(LIST), 哈希分区(HASH) 以及 KEY 分区四种.
(3)RANGE 分区实例
基于属于一个给定连续区间的列值, 把多行分配给分区. 最常见的是基于时间字段. 基于分区的列最好是整型, 如果日期型的可以使用函数转换为整型. 本例中使用 to_days 函数.
- CREATE TABLE my_range_datetime(
- id INT,
- hiredate DATETIME
- )
- PARTITION BY RANGE (TO_DAYS(hiredate) ) (
- PARTITION p1 VALUES Less THAN ( TO_DAYS('20171202') ),
- PARTITION p2 VALUES Less THAN ( TO_DAYS('20171203') ),
- PARTITION p3 VALUES Less THAN ( TO_DAYS('20171204') ),
- PARTITION p4 VALUES Less THAN ( TO_DAYS('20171205') ),
- PARTITION p5 VALUES Less THAN ( TO_DAYS('20171206') ),
- PARTITION p6 VALUES Less THAN ( TO_DAYS('20171207') ),
- PARTITION p7 VALUES Less THAN ( TO_DAYS('20171208') ),
- PARTITION p8 VALUES Less THAN ( TO_DAYS('20171209') ),
- PARTITION p9 VALUES Less THAN ( TO_DAYS('20171210') ),
- PARTITION p10 VALUES Less THAN ( TO_DAYS('20171211') ),
- PARTITION p11 VALUES Less THAN (MAXVALUE)
- );
3, 什么时候分表?
一张表的查询速度已经慢到影响使用的时候.
sql 经过优化
数据量大
当频繁插入或者联合查询时, 速度变慢
分表后, 单表的并发能力提高了, 磁盘 I/O 性能也提高了, 写操作效率提高了
(1)分表的实现方式
需要结合相关中间件, 需要业务系统配合迁移升级, 工作量较大.
三, 分库分表后引入的问题
1, 分布式事务问题
如果我们做了垂直分库或者水平分库以后, 就必然会涉及到跨库执行 SQL 的问题, 这样就引发了互联网界的老大难问题 -"分布式事务". 那要如何解决这个问题呢?
1. 使用分布式事务中间件 2. 使用 MySQL 自带的针对跨库的事务一致性方案(XA), 不过性能要比单库的慢 10 倍左右. 3. 能否避免掉跨库操作(比如将用户和商品放在同一个库中)
2, 跨库 join 的问题
分库分表后表之间的关联操作将受到限制, 我们无法 join 位于不同分库的表, 也无法 join 分表粒度不同的表, 结果原本一次查询能够完成的业务, 可能需要多次查询才能完成. 粗略的解决方法: 全局表: 基础数据, 所有库都拷贝一份. 字段冗余: 这样有些字段就不用 join 去查询了. 系统层组装: 分别查询出所有, 然后组装起来, 较复杂.
3, 横向扩容的问题
当我们使用 HASH 取模做分表的时候, 针对数据量的递增, 可能需要动态的增加表, 此时就需要考虑因为 reHash 导致数据迁移的问题.
4, 结果集合并, 排序的问题
因为我们是将数据分散存储到不同的库, 表里的, 当我们查询指定数据列表时, 数据来源于不同的子库或者子表, 就必然会引发结果集合并, 排序的问题. 如果每次查询都需要排序, 合并等操作, 性能肯定会受非常大的影响. 走缓存可能一条路!
四, 分库分表中间件设计
分表又分为单库分表 (表名不同) 和多库分表(表名相同), 不管使用哪种策略都还需要自己去实现路由, 制定路由规则等, 可以考虑使用开源的分库分表中间件, 无侵入应用设计, 例如淘宝的 tddl 等.
分库分表中间件全部可以归结为两大类型:
CLIENT 模式;
PROXY 模式;
CLIENT 模式代表有阿里的 TDDL, 开源社区的 sharding-jdbc(sharding-jdbc 的 3.x 版本即 sharding-sphere 已经支持了 proxy 模式).
架构如下:
PROXY 模式代表有阿里的 cobar, 民间组织的 MyCAT. 架构如下:
无论是 CLIENT 模式, 还是 PROXY 模式. 几个核心的步骤是一样的: SQL 解析, 重写, 路由, 执行, 结果归并.
五, 分库分表常用中间件
目前应用比较多的基本有以下几种,
- TDDL
- Sharding-jdbc
- Mycat
- Cobar
- 1,TDDL
淘宝团队开发的, 属于 client 层方案. 支持基本的 crud 语法和读写分离, 但不支持 join, 多表查询等语法.
2,Sharding-jdbc
当当开源的, 属于 client 层方案, 目前已经更名为 ShardingSphere.SQL 语法支持也比较多, 没有太多限制, 支持分库分表, 读写分离, 分布式 id 生成, 柔性事务(最大努力送达型事务, TCC 事务).
3,Cobar
阿里 b2b 团队开发和开源的, 属于 proxy 层方案, 就是介于应用服务器和数据库服务器之间. 应用程序通过 JDBC 驱动访问 Cobar 集群, Cobar 根据 SQL 和分库规则对 SQL 做分解, 然后分发到 MySQL 集群不同的数据库实例上执行.
4,Mycat
基于 Cobar 改造的, 属于 proxy 层方案, 支持的功能完善, 社区活跃.
六, 常见分表, 分库常用策略
平均进行分配 hash(object)%N(适用于简单架构).
按照权重进行分配且均匀轮询.
按照业务进行分配.
按照一致性 hash 算法进行分配(适用于集群架构, 在集群中节点的添加和删除不会造成数据丢失, 方便数据迁移).
七, 全局 ID 生成策略
1, 自动增长列
优点: 数据库自带功能, 有序, 性能佳.
缺点: 单库单表无妨, 分库分表时如果没有规划, ID 可能重复.
解决方案, 一个是设置自增偏移和步长.
假设总共有 10 个分表
级别可选: SESSION(会话级), GLOBAL(全局)
- SET @@SESSION.auto_increment_offset = 1; ## 起始值, 分别取值为 1~10
- SET @@SESSION.auto_increment_increment = 10; ## 步长增量
如果采用该方案, 在扩容时需要迁移已有数据至新的所属分片.
另一个是全局 ID 映射表.
在全局 Redis 中为每张数据表创建一个 ID 的键, 记录该表当前最大 ID;
每次申请 ID 时, 都自增 1 并返回给应用;
Redis 要定期持久至全局数据库.
2,UUID(128 位)
在一台机器上生成的数字, 它保证对在同一时空中的所有机器都是唯一的. 通常平台会提供生成 UUID 的 API.
UUID 由 4 个连字号 (-) 将 32 个字节长的字符串分隔后生成的字符串, 总共 36 个字节长. 形如: 550e8400-e29b-41d4-a716-446655440000.
UUID 的计算因子包括: 以太网卡地址, 纳秒级时间, 芯片 ID 码和许多可能的数字.
UUID 是个标准, 其实现有几种, 最常用的是微软的 GUID(Globals Unique Identifiers).
优点: 简单, 全球唯一;
缺点: 存储和传输空间大, 无序, 性能欠佳.
3,COMB(组合)
组合 GUID(10 字节) 和时间(6 字节), 达到有序的效果, 提高索引性能.
4,Snowflake(雪花) 算法
Snowflake 是 Twitter 开源的分布式 ID 生成算法, 其结果为 long(64bit) 的数值.
其特性是各节点无需协调, 按时间大致有序, 且整个集群各节点单不重复.
该数值的默认组成如下(符号位之外的三部分允许个性化调整):
1bit: 符号位, 总是 0(为了保证数值是正数).
41bit: 毫秒数(可用 69 年);
10bit: 节点 ID(5bit 数据中心 + 5bit 节点 ID, 支持 32 * 32 = 1024 个节点)
12bit: 流水号(每个节点每毫秒内支持 4096 个 ID, 相当于 409 万的 QPS, 相同时间内如 ID 遇翻转, 则等待至下一毫秒)
八, 优雅实现分库分表的动态扩容
优雅的设计扩容缩容的意思就是 进行扩容缩容的代价要小, 迁移数据要快.
可以采用逻辑分库分表的方式来代替物理分库分表的方式, 要扩容缩容时, 只需要将逻辑上的数据库, 表改为物理上的数据库, 表.
第一次进行分库分表时就多分几个库, 一个实践是利用 32 * 32 来分库分表, 即分为 32 个库, 每个库 32 张表, 一共就是 1024 张表, 根据某个 id 先根据先根据数据库数量 32 取模路由到库, 再根据一个库的表数量 32 取模路由到表里面.
刚开始的时候, 这个库可能就是逻辑库, 建在一个 MySQL 服务上面, 比如一个 MySQL 服务器建了 16 个数据库.
如果后面要进行拆分, 就是不断的在库和 MySQL 实例之间迁移就行了. 将 MySQL 服务器的库搬到另外的一个服务器上面去, 比如每个服务器创建 8 个库, 这样就由两台 MySQL 服务器变成了 4 台 MySQL 服务器. 我们系统只需要配置一下新增的两台服务器即可.
比如说最多可以扩展到 32 个数据库服务器, 每个数据库服务器是一个库. 如果还是不够? 最多可以扩展到 1024 个数据库服务器, 每个数据库服务器上面一个库一个表. 因为最多是 1024 个表么.
这么搞, 是不用自己写代码做数据迁移的, 都交给 dba 来搞好了, 但是 dba 确实是需要做一些库表迁移的工作, 但是总比你自己写代码, 抽数据导数据来的效率高得多了.
哪怕是要减少库的数量, 也很简单, 其实说白了就是按倍数缩容就可以了, 然后修改一下路由规则.
参考文档
https://shardingsphere.apache.org/
深度认识 Sharding-JDBC https://juejin.im/entry/5905ac37a22b9d0065e1199c
来源: https://www.cnblogs.com/binyue/p/12312734.html