ShardingSphere之Sharding-JDBC分库分表、读写分离
一、数据切分概念
关系型数据库在单机存储容量、连接数、处理能力上都是有限的,容易成为整个系统的瓶颈。当单表的数据量达到一定的数量和容量时,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能也仍然会下降严重。
数据库分布式核心概念其实就是数据切分,以及切分后的数据定位、聚合归并。数据切分也就是将数据分散到多个表或多个库,从而使单库单表的数据量变小,通过对表或库的扩容(增加分表、增加分库、增加主机),来缓解单一数据库的性能压力,从而能提升对数据库操作的性能。
数据切分,可以分为垂直切分和水平切分。
1、垂直切分
垂直切分可分为 垂直分库和垂直分表。
垂直分库根据业务耦合度,将关联度低的不同表存储到不同的数据库中,每个库中的数据表是不同的,专库专用。与大系统拆分为小系统类似,按业务进行独立划分,像目前的微服务架构下,每个微服务都是负责不同的业务,可以使用单独的一个库, 公车项目中的系统管理支撑类业务库、工作流支撑类业务库、公车调度类业务库、GIS相关库,这就是属于垂直分库。
垂直分表是基于数据库表的”列”进行切分,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。在字段很多的情况下(例如一个大表有几十上百个字段),绝大部分字段如果都是存储后不会有改动,只有少数几个字段属于热点字段频繁的读写,此时就适合将热点字段拆分到扩展表中 与大表关联, 热点字段所在的表字段少,读写性能高。另外通过”大表拆小表”,也能避免跨页问题,MySQL底层是通过数据页或者叫数据块存储的,innodb存储引擎的页块大小默认为16K,但如果表中一行的数据长度超过了16k,就会出现行溢出,溢出的行是存放在另外的地方,存放该溢出数据的页叫uncompresse blob page。所以一条记录占用空间过大会导致跨页,读取时就会造成额外的性能开销。
关于垂直分表,在公车二期数据库评审时讨论过 申请单表字段比较多,将热点字段拆分到扩展表。
优点:
解决业务系统层面的耦合,业务清晰
高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈
缺点:
分库后表无法跨库join,只能通过接口聚合方式解决,提升了开发的复杂度
分布式事务处理复杂(借助第三方成熟的分布式事务框架处理,比如目前采用seata AT模式)
依然存在单表数据量过大的问题(需要水平切分) 公车二期垂直分库后,比如调度业务库中依然存在单表数据量 会随着系统运行快速增长,导致最终单表过大。
2、水平切分
当应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,此时就需要进行水平切分。
水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。
优点:
解决单库单表数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力
应用端改造较小,不需要拆分业务模块
缺点:
同样存在跨库的分布式事务问题
跨库的join关联查询问题
数据多次扩展难度和维护量极大,比如随着业务的增速,原来规划的分片数量不够,需要扩容,扩容后分片增加,那么就会影响原有分片策略,会造成数据需要进行迁移重新分片。
水平切分后原本在同一个库的同一个表的数据会被划分到不同库的不同表中,每个库和表的数据都不一样,但表结构一样。
水平切分规则:
A、根据数值范围
按照时间区间或ID区间来切分。例如:按日期将不同月份或年份的数据分散到不同的库和表中;再比如按照主键值为1~5000000的记录分到第一个库,5000000~10000000的分到第二个库,以此类推。这种就是按照范围分片。
还有一种方式叫”冷热数据分离”,就是将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是一种范围分片的实践。
数值范围优点:
单表大小可控
利于水平扩展,后期如果想对整个分片扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移
使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。
缺点:
热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询
B、根据数值取模(或哈希取模)
一般采用hash取模mod的切分方式,例如:将车辆调度表、司机表、出车任务表根据 车辆ID 或者申请单号 字段切分到多个库或表中,余数为0的放到第一个库的第一个表,余数为1的放到第二个库的第二个表,以此类推。这样同一个申请单 或者 同一个车辆的数据会分散到同一个库中或表中,如果查询条件带有车辆ID或申请单号字段时,就可以明确定位到相应库和表去查询。
优点:
数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈
缺点:
后期分片扩容时,需要迁移旧的数据
容易面临跨分片查询的复杂问题。如果频繁用到的查询条件中不带分片键时,将会导致无法精确定位数据库和表,就会导致全分片路由,向所有分片发起查询,再进行结果归并,取最小集返回给应用,分库反而成为拖累。
类比Oracle的分区表。
二、分库分表的问题汇总
1、事务一致性问题
解决方法:
分布式事务
当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,可采用成熟的第三方分布式事务解决方案,seata。
最终一致性
对于性能要求很高,但对一致性要求不高的系统,不要求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。是否可采用事务补偿还要结合业务系统对一致性的要求来考虑,如果必须要求强一致性,就不能使用事务补偿机制。
比如用户积分累计,如果对用户账户积分累计出现错误失败了,可以降级处理,先记录一个日志,事后跑批对累计失败的进行重新处理,这种处理方式就叫最终一致性。
2、跨节点关联查询 join 问题
切分之前,系统中很多列表和详情页所需的数据可以通过sql join来完成。而切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用join查询。
解决方案:
全局表
全部分库中存在相同的表和数据,全局表必须是数据量小的,增长速度微乎其微的。
字段冗余
ER关系
建立关联关系,使用相同的分片规则
采用关联关系,这样可以使用相同的分片键和策略
数据组装
应用层进行结果归并组装
3、全局主键重复问题
使用自增主键模式时,对于分片后的库表,各自自增,将会出现主键重复问题。
解决方案:
设定不同起始数值和步长;扩容时需要维护
UUID;字符串类型不利于聚簇索引
雪花分布式自增ID算法;强依赖时钟,时钟回拨时会造成ID重复
业界开源的雪花算法增强实现,比如美团的Leaf,百度的UIDGenerator
三、什么时候考虑切分
1、能不切分尽量不要切分
并不是所有表都需要进行切分,主要还是看数据的增长速度。切分后会在某种程度上提升业务的复杂度,数据库除了承载数据的存储和查询外,协助业务更好的实现需求也是其重要工作之一。
不到万不得已不用轻易使用分库分表这个大招,避免”过度设计”和”过早优化”。分库分表之前,不要为分而分,先尽力去做力所能及的事情,例如:升级硬件、升级网络、读写分离、索引优化等等。当数据量达到单表的瓶颈时候,再考虑分库分表。
2、数据量过大,正常运维影响业务访问
1)对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO。例如1T的数据,网络传输占50MB时候,需要20000秒才能传输完毕,整个过程的风险都是比较高的
2)对一个很大的表进行DDL修改时,MySQL会锁住全表,这个时间会很长,这段时间业务不能访问此表,影响很大。将数据表拆分,总量减少,有助于降低这个风险。
3)大表会经常访问与更新,就更有可能出现锁等待。将数据切分,用空间换时间,变相降低访问压力
3、数据量快速增长
以公车调度申请表为例,该表字段多,数据量如果随着业务增速,数据量增长过快的话,对该表的几个状态热点字段读写频繁,其他字段几乎不变,该表就会有压力,可遵循冷热分离的垂直切分,将热点字段拆分出去。
同样与调度申请关联,其他车辆调度表、司机反馈表、出车任务表 都会随着业务快速发展,数据量快速增长的情况,且数据量还大,就需要考虑切分。
4、安全性和可用性
在业务层面上垂直切分,将不相关的业务的数据库分隔,因为每个业务的数据量、访问量都不同,不能因为一个业务把数据库搞跨而牵连到其他业务。利用水平切分,当一个数据库出现问题时,不会影响到100%的用户,每个库只承担业务的一部分数据,这样整体的可用性就能提高。
四、分库分表实现方案
能提供分库分表的成熟开源解决方案的有两种形式,一种是基于中间件,一种是基于JDBC驱动层的增强代理。
ShardingSphere (Apache顶级项目)包含 ShardingJDBC、ShardingProxy、Sharding-Scaling
TSharding(蘑菇街)
Atlas(奇虎360)
Cobar(阿里巴巴)
MyCAT(基于Cobar)
Oceanus(58同城)
Vitess(谷歌)
ShardingJDBC定义为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。 像MyBatis、hibernate这类ORM框架 都是基于JDBC驱动之上进行封装更高级特性功能。
ShardingSphere官网 :https://shardingsphere.apache.org/
ShardingJDBC数据分片、读写分离
核心概念:
逻辑表:水平拆分的数据库(表)的相同逻辑和数据结构表的总称。
真实表:在分片的数据库中真实存在的物理表。
数据节点:数据分片的最小单元。由数据源名称和数据表组成,例:ds_0.t_order_0。
绑定表:指分片规则一致的主表和子表。
广播表:指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景。也就是全局表。
分片概念:
分片键:用于分片的数据库字段,将数据库(表)水平拆分的关键字段。
分片算法:通过分片算法将数据分片,支持通过=、>=、<=、>、<、BETWEEN和IN分片。包括 精确分片算法(=和in)、范围分片算法(>=、<=、>、<、BETWEEN)、复合分片算法(多分片键)、Hint分片算法
分片策略:
依据分片键 + 分片算法的结合,形成多种分片策略。行表达式分片策略(单分片键)、标准分片策略(单分片键,支持精确分片和范围分片算法)、复合分片策略(多分片键的分片算法)、Hint分片策略(通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略)、不分片策略
SQL Hint:对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。
内核:
解析引擎:对SQL进行词法解析和语法解析
路由引擎:
根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的SQL,根据分片键的不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN)。 不携带分片键的SQL则采用广播路由。
改写引擎:
应用层面向逻辑库与逻辑表书写的SQL,并不能够直接在真实的数据库中执行,SQL改写用于将逻辑SQL改写为在真实数据库中可以正确执行的SQL。 它包括正确性改写和优化改写两部分。
执行引擎:
负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。
归并引擎:
将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。分为遍历归并、排序归并、分组归并、聚合归并、分页归并。
读写分离概念:
主库
添加、更新以及删除数据操作所使用的数据库,支持单主库。
从库
查询数据操作所使用的数据库,支持多从库。
主从同步
将主库的数据异步的同步到从库的操作。由于主从同步的异步性,从库与主库的数据会短时间内不一致。主从同步需自行实现。
负载均衡策略
通过负载均衡策略将查询请求疏导至不同从库。
如何使用ShardingJDBC进行水平分片及读写分离
应用中集成ShardingSphere
第一步:引入依赖
第二步:加注解
启动类增加@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,DruidDataSourceAutoConfigure.class})注解,如果使用了druid数据的starter的话需要排除DruidDataSourceAutoConfigure.class自动装配,由ShardingSphere记性数据源的创建
第三步:配置参数
配置数据源、数据分片规则,参见以下几种数据分片方式的配置
1、单库水平分表(含自定义分片算法)
参数配置:
自定义精准分片算法:
自定义范围分片算法:
2、单库水平分表+绑定表
参数配置:
多一个 binding-tables: t_order,t_order_item 配置即可,但分片键和分片规则要一致。
先测无绑定表关系配置的路由结果,再测配置绑定表关系的路由结果。
3、单库水平分表+一主一从读写分离
参数配置:
shardingjdbc 同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。
先读后写路由结果:读操作路由至从库(读库),写操作路由至主库(写库);
先写后读路由结果:写操作路由至主库(写库),后续的读全部路由至主库(写库);
4、单库水平分表+一主多从读写分离,自定义从库负载均衡算法实现
水平分表+一主多从读写分离参数配置:
增加从库的数据源配置,并在主从规则中添加到从库数据源列表中即可,开启负载均衡,可以使用内置算法,分为 ROUND_ROBIN轮循,RANDOM随机 两种。
如想使用自定义负载均衡算法,通过下面的步骤来实现:
A、首先需要定义一个负载均衡算法实现类,实现MasterSlaveLoadBalanceAlgorithm接口,并实现getType和getDataSource方法;
B、工程中需增加SPI的配置,将自定义算法实现类加入到SPI配置文件中,由ShardingSphere启动时加载到负载均衡算法服务集合中,再通过配置的算法类型名称,找到对应的实现类。
应用配置文件中,配置
C、官网中提到 如果不使用load-balance-algorithm-type参数指定算法实现类,也可以使用load-balance-algorithm-class-name 来指定具体的实现类全限定名称,实测时没起作用。所以可以使用上述形式进行自定义负载算法实现及配置。
5、水平分库水平分表+一主多从读写分离
参数配置:
6、水平分库水平分表+一主多从读写分离+广播表+非分片表配置
当进行水平分库分表后,由于并不是所有的表都需要进行分片,对于非分片表又存在两种使用情况,一种是分片的表需要跟非分片表进行关联查询避免跨库关联,一种是非分片表只存储在其中一个主库即可,与分片表无直接关联查询。 这两种表的处理方式分别为 在各分库中维护相同的非分片表,即广播表,配置为广播表的表在CUD时将会路由所有库进行同样的操作,以保证数据的一致性。对于分片表关联广播表查询时,将根据分片表的策略路由至指定的库后使用指定库的表进行关联查询。
基于第5小节的水平分库分表+读写分离基础上配置广播表,在master-slave-rules相同层级增加一行配置即可:
对于第二种情况,数据量小的表,且增长速度缓慢,无需分库分表的话,可以采用配置一个默认数据源名称,对配置中未指明分片规则的表全部路由至默认数据源,对这些非分片表的CRUD将会始终路由至该默认数据源下。 配置默认数据源的表,同样基于第5小节的水平分库分表+读写分离基础上配置在 master-slave-rules相同层级增加一行配置即可:
ds0为读写分离规则中的逻辑数据源名称,代表非分片表将路由至ds0代表的主库和从库。
7、读写分离、数据脱敏在独立使用和结合分片使用时的配置参数差异性说明
A、仅读写分离 与 数据分片+读写分离
仅读写分离的配置参数,前缀为 spring.shardingsphere.masterslave
数据分片+读写分离时的配置参数,前缀为spring.shardingsphere.sharding.master-slave-rules
额外注意:同时使用时需由读写分离配置规则中定义的逻辑数据源名称作为 分片规则中的实际数据节点中的数据源名称,比如下面完整水平分表+读写分离的配置:
首先定义数据源时,spring.shardingsphere.datasource.names中包含主从数据源名称,master、slave
在配置分库分表规则时,配置actual-data-nodes 实际数据节点参数时,数据源名称需使用自定义主从逻辑数据源名称 ds0,比如分表规则配置中的数据节点的数据源名为读写分离规则中的逻辑数据源ds0
B、仅数据脱敏和 数据分片+数据脱敏
仅数据脱敏配置参数,前缀为spring.shardingsphere.encrypt
数据分片+数据脱敏配置参数,前缀为spring.shardingsphere.sharding.encrypt-rule