目录
Sharding JDBC (二)分库分表关键问题与解决方案
Sharing JDBC (三) Mysql Replication主从复制 、环境搭建 、读写分离配置
Sharding JDBC(四) 分片策略一:标准分片策略StandardShardingStrategy
Sharding JDBC(五) 分片策略二:Inline行表达式分片策略InlineShardingStrategy
Sharding JDBC(六) 分片策略三:复合分片策略ComplexShardingStrategy
Sharding JDBC(七) 分片策略四:Hint强制路由HintShardingStrategy
Sharding JDBC (一)数据库分库、分表
一、什么是分库分表
分库分表就是按照一定的规则,对原有的数据库和表进行拆分,把原本存储于一个库的数据分玦存储到多 个库上,把原本存储于一个表的数据分块存储到多个表上。目的就是为了缓解数据库的压力,最大限度提高数据操作的效率。
分表
就是把一张表按一定的规则分解成N个具有独立存储空间的实体表。系统读写时需要根据定义好的规则得到对应的字表明,然后操作它。
分库
一旦分表,一个库中的表会越来越多
二、为什么要分库分表
问题描述:
单个表数据量越大,读写锁,插入操作重新建立索引效率越低。
单个库数据量太大(一个数据库数据量到1T-2T就是极限)
单个数据库服务器压力过大
读写速度遇到瓶颈(并发量几百)
mysql单表经验
300W Mysql 可以轻松抗住
600 W 数据开始卡,优化可以解决(表结构,索引设计)
800W ~ 1000W 牛逼的DBA 优化都会遇到瓶颈
一般来说 MySQL 数据库单表记录最好控制在 500 万条(这是个经验数字)
分库分表目的:分担单台设备负载
随着时间和业务的发展,数据库中的数据量增长是不可控的,库和表中的数据会越来越大,随之带来的是更高的磁盘、IO、系统开销,甚至性能上的瓶颈,而一台服务的资源终究是有限的,因此需要对数据库和表进行拆分,从而更好的提供数据服务。
三、分库分表有哪些形式
分库分表的基本思想:对数据切分,分而治之
3.1、垂直(纵向)拆分
将不同的表(或者 Schema)拆分到不同的数据库(主机)之上,垂直划分数据库是横据业务进行划分,例如将shop中涉及商品、订单、用户的表分别划分出成商品库、订单库、用户库,通过降低单库(表)的大小来提高性能,但这种方式并没有解决高数据量带来的性能提耗(并没有减少表的数据量)
分表的情况就是将一个大表根据业务功能拆分成一个个子表。常见是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联。
表垂直拆分是对列的拆分,拆分完成后每张单表相对于原表row 不变,column减少(可以理解A4纸左右对折切割)
优点:
拆分后业务清晰,拆分规则明确
降低单节点数据库的负载,按照业务将表进行分类,分布到不同的数据库上面,这样也就将数据或者说压力分担到不同的库上,
可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
数据维护相对简单
缺点:
表拆分主键出现冗余,需要管理冗余例
会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力
依然存在单表数据量过大的问题(需要水平拆分)
事务处理复杂
3.2、水平(横向)拆分
根据表中的数据的逻辑关系,将同一张表中的数据按照某种条件拆分到多台数据库(主机)上。水平划分是根据一定规则,例如时间或者id序列值等进行数据的拆分。相对于垂直拆分,水平拆分不是将表做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表包含一部分数据。
水平切分是对数据行的切分,拆分完成后每张单表相对于原表column不变,row减少(可以理解A4纸上下对折切割)
优点:
不存在单库大数据和高并发的性能瓶颈
应用端改造较少
提高了系统的稳定性和负载能力
缺点:
分片事务一致性难以解决
跨节点Join性能差,逻辑复杂
数据多次扩展难度跟维护量极大
四、分库分表组件有哪些
1. Atlas https://github.com/Qihoo360/Atlas
Atas是由 Qihoo360,Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目,它是在mysql- proxy0.8.2版本的基础上,对其进行了优化,增加了一些新的功能特性。Atlas是一个位于应用程序与MySQL之间,它实现了MySQL的客户端与服务端协议,作为服务端与应用程序通讯,同时作为客户端与 MySQL通讯。它对应用程序屏蔽了DB的细节,同时为了降低 MySQL负担,它还维护了连接池。
Atlas 相当于nginx 代理服务器,业务后端连接Atlas 服务器,将sql 发送给Arlas 服务器,Atlas 会负责返回拼接数据
2. Cobar https://github.com/alibaba/cobar
阿里巴巴(B2B)部门开发的一种关系型数据库的分布式处理系统,它可以在分布式的环境下看上去像传统数据库一样为您提供海量数据服务。
3. Sharding JDBC https://shardingsphere.apache.org
当当应用框架 ddframe中,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数 据库分库分表访问。
4. TDDL https://github.com/alibaba/tb_tddl
淘宝根据自己的业务特点开发了TDDL框架,主要解决了分库分表对应用的透明化以及异构数据库之间的数据复制,它是一个基于集中式配置的jdbc datasource实现,具有主备,读写分离,动态数据库配置等能。
5 Mycat http://www.mycat.io/
一个开源的分布式数据库系统,实现了 MySQL协议的服务器。前端用户可以把它看作是一个数据库代理,用 MySQL客户端工具和命令行访问,而其后端可以用 MySQL原生协议与多个 MySQL服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信
五、分库分表组件对比
Sharding JDBC (二)分库分表关键问题与解决方案
一、分库分表产生问题的原因
当系统数据库达到一定的量级,单数据库实例已经无法支撑的时候,我们就要考虑采用分库分表的策略了.那么进行数据分库分表后会,带来哪些影响呢?
1.1 垂直拆分的影响
- 单机ACID打破了
- 一些Join操作变得困难
- 外键约束的场景受影响
1.2 水平拆分的影响
- 单机ACID打破了
- 一些Join操作变得困难
- 外键约束的场景受影响
- 依赖单库的自增序列生成id受影响
- 逻辑意义上的单表查询可能要跨库
二、分库分表带来的难点问题
2.1 分布式全局唯一ID
在很多中小项目中,我们往往直接使用数据库自增特性来生成主键ID,这样确实比较简单。而在分库分表的环境中,数据分布在不同的分片上,如果每个分片都采用原有的主键自增策略。不可避免的会造成主键ID重复。简单介绍下使用和了解过的几种ID生成算法。
方案一 :设置主键自增id 的起始和步长
ds0.t_user 设置主键id 自动增长 起始1 步长2 -->> 1,3,5,7,9...
ds1.t_user 设置主键id 自动增长 起始2 步长2 -->> 2,4,6,8,10...
多少个库步长就为分库的数量
缺点:
不利于扩展,新增库之后要维护所有表的自动增长步长,
中小型使用自增序列,但是大型项目不使用
方案二 :UUID/GUID
优点:好处就是本地生成,不要基于数据库
缺点:
UUID 使用字符串形式存储、太长占用空间大,作为主键性能太差了
UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显
采用无意义字符串,区别于SKUID,没有任何业务上体现(淘宝,京东等商品的SKUID包含商品的信息)
方案三 :Twitter的分布式自增ID算法Snowflake
雪花算法概述,有一种说法,自然界中并不存在两片完全一样的雪花的。每一片雪花都拥有自己漂亮独特的形状、独一无二。雪花算法也表示生成的ID如雪花般独一无二。
组成结构
大致由:首位无效符、时间戳差值,机器(进程)编码,序列号四部分组成,雪花算法生成的ID是纯数字且具有时间顺序的。
- 时间位:可以根据时间进行排序,有助于提高查询速度。
- 机器id位:适用于分布式环境下对多节点的各个节点进行标识,可以具体根据节点数和部署情况设计划分机器位10位长度,如划分5位表示进程位等
- 序列号位:是一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号
优点:
- 时间自增排序
- 适合分布式场景,整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分)
- 效率较高,每毫秒4096个ID序号
缺点:
雪花算法在单机系统上ID是递增的,但是在分布式系统多节点的情况下,所有节点的时钟并不能保证不完全同步,所以有可能会出现不是全局递增的情况
方案四:Redis Increment incr可以用作计数器模式,它是原子自增操作
方案五:Zookeeper 分布式事务锁
方案六:MongoDB ObjectID(类似于UUID)
方案七:Ticket Server (数据库生成方式,Flickr采用这种方式)
2.2 常见分片规则
存储的规则
- 随机分片:hashcode 取模 (user_id % 片数量 ,取模结果为 0 ,insert t_user0,取模结果为 1,insert t_user1)
- 连续分片: 时间范围,int ,可能会造成数据倾斜(每个库数据量不平衡)
根据业务需求来决定分片键 ,在业务之初分片键确定之后一般情况不更换,如果更换,对数据迁移,数据维护 非常的麻烦
2.3 常见分片策略
Sharding-JDBC中的分片策略有两个维度:
- 数据源分片策略(DatabaseShardingStrategy):数据被分配的目标数据源
- 表分片策略(TableShardingStrategy):数据被分配的目标表
- 两种分片策略API完全相同,但是表分片策略是依赖于数据源分片策略的(即:先分库,然后才有分表)
Sharding分片策略继承自ShardingStrategy,提供了5种分片策略。
io.shardingsphere.core.routing.strategy.ShardingStrategy
--io.shardingsphere.core.routing.strategy.standard.StandardShardingStrategy
--io.shardingsphere.core.routing.strategy.standard.ComplexShardingStrategy
--io.shardingsphere.core.routing.strategy.standard.InlineShardingStrategy
--io.shardingsphere.core.routing.strategy.standard.HintShardingStrategy
--io.shardingsphere.core.routing.strategy.standard.NoneShardingStrategy
由于分片算法和业务实现紧密相关,因此Sharding-JDBC并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
分策策略一:标准分片策略StandardShardingStrategy
简单配置实现链接:https://blog.csdn.net/womenyiqilalala/article/details/106113983
- 提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持
- StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm(精准分片)和RangeShardingAlgorithm(范围分片)两个分片算法
- PreciseShardingAlgorithm是必选的,用于处理=和IN的分片
- RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理
- 如果需要使用RangeShardingAlgorithm,必须和PreciseShardingAlgorithm配套使用
分片策略二:复合分片策略ComplexShardingStrategy
简单配置实现链接:https://blog.csdn.net/womenyiqilalala/article/details/106115560
- 提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持
- ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此Sharding-JDBC并未做过多的封装,而是直接将分片键值组合以及分片操作符交于算法接口,完全由应用开发者实现,提供最大的灵活度
分片策略三:Inline表达式分片策略InlineShardingStrategy
简单配置实现链接:https://blog.csdn.net/womenyiqilalala/article/details/106115295
- 使用Groovy的Inline表达式,提供对SQL语句中的=和IN的分片操作支持。
- 只支持单分片键
- InlineShardingStrategy只支持单分片键,对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: tuser${user_id % 8} 表示t_user表按照user_id按8取模分成8个表,表名称为t_user_0到t_user_7
分片策略四:Hint分片HintShardingStrategy
简单配置实现链接:https://blog.csdn.net/womenyiqilalala/article/details/106115831
在分库分区中,有些特定的SQL,Sharding-jdbc、Mycat、Vitess都不支持(可以查看相关文档各自对哪些SQL不支持),例如:insert into table1 select * from table2 where ....这种SQL 路由很麻烦,需要解析table2的路由(是在ds0 /ds1 table2_0/table_1),结果集归并,insert 语句也需要同样的路由解析。这种情况Sharding-jdbc可以使用Hint分片策略来实现各种Sharding-jdbc不支持语法的限制
- 通过Hint而非SQL解析的方式分片的策略。对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段
- Hint分片策略是绕过SQL解析的,所以对于这些比较复杂的需要分片的查询,采用Hint分片策略性能可能会更好
- 在读写分离数据库中,Hint 可以通过HintManager.setMasterRouteOnly()方法,强制读主库(主从复制存在一定延时,但在某些特定的业务场景中,可能更需要保证数据的实时性)
分片策略五:NoneShardingStrategy 不分片的策略与直接不使用Sharding-JDBC 效果相同
2.4 分片算法
Sharding提供了以下4种算法接口:
PreciseShardingAlgorithm:精确分片算法,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。
RangeShardingAlgorithm:范围分片算法用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。需要配合StandardShardingStrategy使用。如果需要使用RangeShardingAlgorithm,必须和PreciseShardingAlgorithm配套使用,否则会报错
HintShardingAlgorithm:Hint分片算法
ComplexKeysShardingAlgorithm:复合分片算法
2.5 跨分片技术问题
2.5.1 跨分页的排序分页
跨节点多库进行查询时,会出现limit分页、order by排序等问题
分页需要按照指定字段进行排序当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片。
当排序字段非分片字段时,就变得比较复杂。各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前N页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。
例如:现有db0.t_user0 ,db1.t_user1 ,db2.t_user2 三张表
select id from t__user where id < 10 order by id limit 0,2
每张表的数据:
db0.t_user0 1,4,7
db1.t_user1 5,2,8
db2.t_user2 9,6,3
必须要对db0.t_user0 ,db1.t_user1 ,db2.t_user2 三张表进行排序,然后合并为结果 1,4,7,2,5,8,3,6,9 ,再对合并的结果排序分页
2.5.2 跨分页的函数处理
在使用Max、Min、Sum、Count之类的函数进行统计和计算的时候,需要先在每个分片数据源上执行相应的函数处理,然后再将各个结果集进行二次处理
2.5.3 跨分片join
Join是关系型数据库中最常用的特性,但是在分片集群中,join也变得非常复杂(这种场景,比上面的跨分片分页更加复杂,而且对性能的影响很大),应该采取尽量避免跨分片的join查询。在分库分表中,尽量减少跨库的join,基本上所有的组件都不提供跨库join的功能,目前没有好的办法去解决
a)全局表
全局表 (等同于维度表,例如省份表,城市表,名族表等),使用的是空间换时间的思想,每个db数据库上都存在一张相同数据的表,维护同步
b) ER表
在关系型数据库中,表之间往往存在一些关联的关系。如果我们可以先确定好关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能很好的避免跨分片join问题
2.4 跨分片事务问题
跨分片事务也叫分布式事务,想要了解分布式事务,就需要了解“XA接口”和“两阶段提交”。值得提到的是,MySQL5.5x和5.6x中的xa支持是存在问题的,会导致主从数据不一致。直到5.7x版本中才得到修复。Java应用程序可以采用Atomikos框架来实现XA事务(J2EE中JTA)
三、Sharding JDBC 概念
真实表
如果t_user 表分库分表之后四张表(ds0.t_user0、ds0.t_user1、ds1.t_user0、ds1.t_user1)
在sharding-jdbc 查询时:直接select * from t_user where age =18 ,shardig-jdbc回去查询四张表中符合的数据,对于程序代码sql 中t_user 是逻辑表,并不存在,而 ds0.t_user0、ds0.t_user1、ds1.t_user0、ds1.t_user1 是存放了数据并且存在的真实表(可以参考接口和实现类,接口不能不能被实例化)
广播表
也就是上面的 2.3.3中的全局表
绑定表
sharding-jdbc使用绑定表名称,Mycat使用ER表名称
例如
主表-> t_person_main(id,name,age,qq_level);
子表-> t_person_detail (id,gender,address,phone_num,e-mail,city_id);
如果主表id = 1 分表时在ds0,子表id=1 在ds1 。
Select * from t_person_main as mian left join t_person_detail as detail on mian .id = detail .id where mian .id=1;查询时就需要跨库join.
sharding-jdbc 使用绑定表解决跨库join ,判断相关联的数据插入时是否在同一个库,例如 t_person_main id=1在ds0上,那么t_person_detail id=1 就不会插入到ds1上,保障主表和子表相同数据在同一个库中。出现上面id相同不同库的情况, join时也不会去其他的库上查找数据。避免跨库join
Sharing JDBC (三) Mysql Replication主从复制 、环境搭建 、读写分离配置
一、什么Mysql的主从复制
MySQL数据库自身提供的主从复制功能就可以方便的实现数据的多处自动备份,实现数据库的拓展。多个数据备份不仅可以加强数据的安全性,通过实现读写分离还能进一步提升数据库的负载性能。
二、Mysql 主从复制分类
2.1 从硬件实现主从复制架构区分
1. 一主一从 最简单的主从复制架构,一台主机一台从机
2. 一主多从 在一主多从的数据库体系中,多个从服务器默认采用异步的方式更新主数据库的变化,业务服务器在执行写或者相关修改数据库的操作是在主服务器上进行的,读操作则是在各从服务器上进行(多从需要考虑相应的负载均衡)
3. 多主无从 多个master,所有的master全部会负责读+写,master和master之间会建立一个主主复制,就不在是简单的主从复制,一般情况下只在解决"高可用性"(High availability, HA)使用多主无从
多主涉及到2中模式:
- 主备:active-passive模式,active服务器对外提供正常服务,当active宕机时,passive中进行选举一个升级为新的active对外提供服务,active 和 passive 进行主主复制保证数据一致性
- 互为主从:active-active 模式,也可以理解为互为主从,所有的maste都是activer全部会负责读+写,业务场景更多的是写多读少,master对于代码来说是透明的,请求经过负载均衡之后分发到相应的某一个master上,master和master之间也是通过主主复制保证数据一致性。
无论是active-active还是active-passive, 都是为了解决"高可用性"(High availability, HA)问题. 就是说尽量不要让你的服务挂掉.
4. 主到从到从 链式复制:主从从模式,maste 和 slave1、slave2 进行主从复制,但是 slave1、slave2两台服务器既做主又做从,需要与 slave3、slave4同步数据
5. 环形多主 服务器互为主从,业互为主从结构只不过是在master1于master2上互相设置主服务器为对方。务场景更多的是写多读少,master对于代码来说是透明的,请求经过负载均衡之后会发到相应的某一个master上,master之间再进行主主复制。属于active-active 模式
7. 多主多从
主从之间主从复制,主主互为主从进行主主复制
2.2 复制方式区分
1. 异步复制 My SQL复制默认是异步复制, Master:将事件写入 bingo,提交事务,自身并不知道save是否接收是否处理;这样就会有一个问题,主如果 crash掉了,此时主上已经提交的事务可能并没有传到从上,如果此时强行将从提升为主,可能导致新主上的数据丢失
缺点:牺牲了强一致性,不能保证所有事务都被所有save接收
2. 同步复制 所有的从库都执行了该事务才返回给客户端,也就是说 Maste提交事务,直到事务在所有save都已提交,才会返回客户端事务执行完毕信息;
缺点:需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会受到严重的影响
3. 半同步复制 当 Master上开启半同步复制功前时,至少有一个save开启其功能。当 Master向 slave提交事务,且事务已写入 relay-og中并刷新到磁盘上,Save才会告知 Maste已收到;若 Master提交事务受到阻塞,出现等待超时,在一定时间内 Master没被告知已收到,此时 Master自动转换为异步复制机制。
介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给套户端,而是等待至少一个从库接收到并写到 relay log中才返回给客户端。
缺点:
此时,客户端会收到事务提交失败的信息,客户端会重新提交该事务到新的主上,当岩机的主库重新
启动后,以从库的身份重新加入到该主从结构中,会发现,该事务在从库中被提交了两次,一次是之前作为主的时候,一次是被新主同步过来
此时,从库已经收到并应用了该事务,但是客户端仍然会收到事务提交失败的信息,重新提交该事务到新的主机上
半同步复制(mysql5.5以上支持),5.7 中半同步复制已经解决了2次写入的缺点
三、主从复制实现原理
复制过程:
Master必须启用二进制日志,将任何修改了数据库数据的事件(insert,alert,create…)记录到二进制日志( binary log)中
Slave开启一个线程(I/O Thread)把自己扮演成 mysql 的客户端,通过 mysql 协议,请求Master的二进制日志文件中的事件
Master启动一个线程(dump Thread),检查自己二进制日志中的事件,跟对方请求的位置对比,如果不带请求位置参数,则Master就会从第一个日志文件中的第一个事件一个一个发送给Slave。
Slave接收到Master发送过来的数据把它放置到中继日志(Relay log)文件中。并记录该次请求到Master的具体哪一个二进制日志文件内部的哪一个位置(主节点中的二进制文件会有多个)
Slave启动另外一个线程(sql Thread ),把 Relay log 中的事件读取出来,并在本地再执行一次。
四、Mysql 主从复制简单实现 MySQL主从同步-CSDN博客
五、Sharding JDBC 读写分离配置
Sharding-JDBC、Mycat 目前版本都是不负责主从同步,需要自己运维搭建Mysql 主从复制集群
引入Sharing JDBC 相关依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
读写分离配置:
# 数据源名称定义
sharding.jdbc.datasource.names=ds0,ds1
# 主数据源
sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8
sharding.jdbc.datasource.ds0.username=root
sharding.jdbc.datasource.ds0.password=root
# 从数据源
sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:4306/test?characterEncoding=utf-8
sharding.jdbc.datasource.ds1.username=root
sharding.jdbc.datasource.ds1.password=root
# 读写分离配置
# dataSource 随意给masterslave 设置一个名字
sharding.jdbc.config.masterslave.name=dataSource
# 负载均衡算法 round_robin随机(内置算法)
sharding.jdbc.config.masterslave.load-balance-algorithm-type=round_robin
# 主库
sharding.jdbc.config.masterslave.master-data-source-name=ds0
# 从库
sharding.jdbc.config.masterslave.slave-data-source-names=ds1
# 日志打印SQL语句
sharding.jdbc.config.props.sql.show=true
分库分表配置
#分库分表
spring:
shardingsphere:
datasource:
names: db1,db2
db1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/its_order1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: root123
db2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/its_order2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: root123
#分库策略,以user_id为分片键,分片策略为user_id % 2 + 1,user_id为偶数操作db1数据源,否则操作db2。
sharding:
default-data-source-name: db1 #设置默认数据源名称为db1-没有配置分片规则的情况下,所有的数据库请求将使用默认数据源
broadcast-tables: t_config #广播表:存在于所有的分片数据源中的表,表结构和表中的数据在每个数据库中均完全一致。一般是为字典表或者配置表 t_config,某个表一旦被配置为广播表,只要修改某个数据库的广播表,所有数据源中广播表的数据都会跟着同步。
binding-tables:
- t_order,t_order_item #绑定表
tables:
t_order:
#指定逻辑表t_order对应到的真实表节点
actual-data-nodes: db$->{1..2}.t_order_$->{1..2}
#主键生成策略-解决分库分表后主键生成冲突
key-generator:
column: order_id
type: SNOWFLAKE
#配置分表策略-按order_id字段 % 2+1
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_$->{order_id % 2 + 1}
#配置分库策略-按user_id字段 % 2+1
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: db$->{user_id % 2 + 1}
t_order_item:
#指定逻辑表t_order对应到的真实表节点
actual-data-nodes: db$->{1..2}.t_order_item_$->{1..2}
#主键生成策略-解决分库分表后主键生成冲突
#key-generator:
# column: order_id
# type: SNOWFLAKE
#配置分表策略-按order_id字段 % 2+1
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_item_$->{order_id % 2 + 1}
#配置分库策略-按user_id字段 % 2+1
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: db$->{user_id % 2 + 1}
props:
sql:
show: true
mybatis:
configuration:
map-underscore-to-camel-case: true
六、Mysql 高可用方案比较
Sharding JDBC(四) 分片策略一:标准分片策略StandardShardingStrategy
一、标准分片策略StandardShardingStrategy
- 提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持
- StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm(精准分片)和RangeShardingAlgorithm(范围分片)两个分片算法
- PreciseShardingAlgorithm是必选的,用于处理=和IN的分片
- RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理
- 如果需要使用RangeShardingAlgorithm,必须和PreciseShardingAlgorithm配套使用
二、StandardShardingStrategy配置实现
Sharding -jdbc 在使用分片策略的时候,与分片算法是成对出现的,每种策略都对应一到两种分片算法(不分片策略NoneShardingStrategy除外)
分库分表最核心的两点SQL 路由、SQL 改写
路由:解析原生SQL,确定需要使用哪些数据库,哪些数据表
Route (路由)引擎:为什么要用Route 引擎呢?
在实际查询当中,数据可能不只是存在一台MYSQL服务器上,
SELECT * FROM t_order WHERE order _id IN(1,3,6)
数据分布:
ds0.t_order0 (1,3,5,7)
ds1.t_order0(2,4,6)
这个SELECT 查询就需要走2个database,如果这个SQL原封不动的执行,肯定会报错(表不存在),Sharding-jdbc 必须要对这个sql进行改写,将库名和表名 2个路由加上
SELECT * FROM ds0.t_order0 WHERE order _id IN(1,3)
SELECT * FROM ds0.t_order1 WHERE order _id IN(6)
SQL 改写:将SQL 按照一定规则,重写FROM 的数据库和表名(Route 返回路由决定需要去哪些库表中执行SQL)
application.properties 配置, 配置主要分为三个部分
- 配置数据源
- 分库配置
- 分表配置
# standard.precise-algorithm 标准策略 + 精准分片f算法 SQL 就是 = in
# standard.range-algorithm 标准策略 + 范围分片算法 (主要是between and )
sharding.jdbc.datasource.names=ds0,ds1
#分库配置
sharding.jdbc.config.sharding.default-database-strategy.standard.sharding-column=user_id
sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds0.username=root
sharding.jdbc.datasource.ds0.password=root
sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds1.username=root
sharding.jdbc.datasource.ds1.password=root
# standard.precise-algorithm 标准策略下分片算法包含2个 precise + range,range是可选的,但是如果使用 range 就必须同 precise 配套一起使用
# 买precise赠送 range,可以选择不要赠品,但是你不能不买还想白嫖赠品
sharding.jdbc.config.sharding.default-database-strategy.standard.precise-algorithm-class-name=ai.yunxi.sharding.config.PreciseShardingDBAlgorithm
sharding.jdbc.config.sharding.default-database-strategy.standard.range-algorithm-class-name=ai.yunxi.sharding.config.RangeShardingDBAlgorithm
# 设置绑定表
sharding.jdbc.config.sharding.binding-tables=t_order,t_order_item
# t_order分表配置
# 如果分片键相同,可以直接在后面凭拼接 例如 :ds$->{0..1}.t_order$->{0..1},ds$->{0..1}.t_order_item$->{0..1}
sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1}
sharding.jdbc.config.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id
sharding.jdbc.config.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=ai.yunxi.sharding.config.PreciseShardingDBAlgorithm
sharding.jdbc.config.sharding.tables.t_order.table-strategy.standard.range-algorithm-class-name=ai.yunxi.sharding.config.RangeShardingDBAlgorithm
# t_order_item分表配置
sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds$->{0..1}.t_order_item$->{0..1}
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.standard.sharding-column=order_id
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.standard.precise-algorithm-class-name=ai.yunxi.sharding.config.PreciseShardingDBAlgorithm
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.standard.range-algorithm-class-name=ai.yunxi.sharding.config.RangeShardingDBAlgorithm
精准分库PreciseShardingDBAlgorithm
import io.shardingsphere.api.algorithm.sharding.PreciseShardingValue;
import io.shardingsphere.api.algorithm.sharding.standard.PreciseShardingAlgorithm;
import java.util.Collection;
/**
* 自定义实现 精准分片算法(PreciseShardingAlgorithm)接口
* 数据库DB的精准分片
* @author Peng zhizhong
* @version 1.0
* fileName PreciseShardingDBAlgorithm1
* createTime 2020/5/11 19:21
*/
public class PreciseShardingDBAlgorithm implements PreciseShardingAlgorithm<Integer> {
/**
*
* @param databaseNames 有效的数据源 或者 表 的名字 databaseNames 就为配置文件中的 配置的数据源信息 -> ds0 , ds1
* @param shardingValue SQL 分片列 对应的实际值
* @return
*/
@Override
public String doSharding(Collection<String> databaseNames,
PreciseShardingValue<Integer> shardingValue) {
/*
* 作用:散列到具体的哪个库里面去
* shardingValue : SQL -> SELECT * FROM t_order WHERE order _id IN(1,3,6)
* shardingValue = [1,3,6]
* */
for (String each : databaseNames) {
/**
* 此方法如果参数所表示的字符序列是由该对象表示的字符序列的后缀返回true, 否则为false;
* 请注意,如果参数是空字符串或等于此String对象由equals(Object)方法确定结果为 true。
* String Str = new String("This is really not immutable!!"); retVal = Str.endsWith( "immutable!!" )
* 为true
* ds0.endsWith("0") -> true ;
*/
if (each.endsWith(String.valueOf(shardingValue.getValue() % databaseNames.size()))) {
//返回相应的数据库
System.out.println("each"+each);
return each;
}
}
throw new UnsupportedOperationException();
}
}
范围分库RangeShardingDBAlgorithm
import io.shardingsphere.api.algorithm.sharding.RangeShardingValue;
import io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 自定义实现 范围分片算法(RangeShardingAlgorithm)接口
* 数据库DB的范围分片
* @author Peng zhizhong
* @version 1.0
* fileName RangeShardingDBAlgorithm
* createTime 2020/5/11 19:21
*/
public class RangeShardingDBAlgorithm implements RangeShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(final Collection<String> databaseNames,
final RangeShardingValue<Integer> shardingValue) {
/**
* 自定义SQL -> SELECT * FROM t_order WHERE order _id Between 2000 and 4000
* ds0.t_order: 1000 ~ 3000
* ds1.t_order: 3001 ~ 5000
* ds2.t_order: 5001 ~ 7000
*
* 执行路由后的SQL 应为:
* SELECT * FROM ds0.t_order WHERE order _id Between 2000 and 3000
* SELECT * FROM ds1.t_order WHERE order _id Between 3001 and 4000
*/
Set<String> result = new LinkedHashSet<>();
// 从sql 中获取 Between 2000 and 4000 的值,将2000 赋值给 lower, 4000 赋值给 upper
int lower = shardingValue.getValueRange().lowerEndpoint();
int upper = shardingValue.getValueRange().upperEndpoint();
for (int i = lower; i <= upper; i++) {
for (String each : databaseNames) { //ds0,ds1
if (each.endsWith(i % databaseNames.size() + "")) {
result.add(each);
}
}
}
return result;
}
}
精准分表PreciseShardingTableAlgorithm
import com.google.common.collect.Range;
import io.shardingsphere.api.algorithm.sharding.RangeShardingValue;
import io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 自定义实现 范围分片算法(RangeShardingAlgorithm)接口
* 数据表 table 的范围分片
* @author Peng zhizhong
* @version 1.0
* fileName RangeShardingTableAlgorithm
* createTime 2020/5/11 19:21
*/
public class RangeShardingTableAlgorithm implements RangeShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(final Collection<String> tableNames,
final RangeShardingValue<Integer> shardingValue) {
Set<String> result = new LinkedHashSet<>();
// 如果between 2000000 and 7000000
if (Range.closed(2000000,
7000000).encloses(shardingValue.getValueRange())) {
for (String each : tableNames) {
if (each.endsWith("0")) {
result.add(each);
}
}
} else {
throw new UnsupportedOperationException();
}
return result;
}
}
范围分表RangeShardingTableAlgorithm:
import com.google.common.collect.Range;
import io.shardingsphere.api.algorithm.sharding.RangeShardingValue;
import io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 自定义实现 范围分片算法(RangeShardingAlgorithm)接口
* 数据表 table 的范围分片
* @author Peng zhizhong
* @version 1.0
* fileName RangeShardingTableAlgorithm
* createTime 2020/5/11 19:21
*/
public class RangeShardingTableAlgorithm implements RangeShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(final Collection<String> tableNames,
final RangeShardingValue<Integer> shardingValue) {
Set<String> result = new LinkedHashSet<>();
// 如果between 2000000 and 7000000
if (Range.closed(2000000,
7000000).encloses(shardingValue.getValueRange())) {
for (String each : tableNames) {
if (each.endsWith("0")) {
result.add(each);
}
}
} else {
throw new UnsupportedOperationException();
}
return result;
}
}
Sharding JDBC(五) 分片策略二:Inline行表达式分片策略InlineShardingStrategy
一、Inline行表达式分片策略InlineShardingStrategy
使用Groovy的Inline表达式,提供对SQL语句中的=和IN的分片操作支持。
InlineShardingStrategy只支持单分片键
对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: tuser${user_id % 8} 表示t_user表按照user_id按8取模分成8个表,表名称为t_user_0到t_user_7
二、StandardShardingStrategy配置实现
Sharding -jdbc 在使用分片策略的时候,与分片算法是成对出现的,每种策略都对应一到两种分片算法(不分片策略NoneShardingStrategy除外)
application.properties 配置
#数据源配置,有多少个数据库,就配置多少个数据源(库多的时候比较繁琐,可以采用数据治理),相比于Mycat 配置还是简单很多
#数据源名字随意,但是配置数据源时必须名字能对应
sharding.jdbc.datasource.names=ds0,ds1
sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds0.username=root
sharding.jdbc.datasource.ds0.password=root
sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds1.username=root
sharding.jdbc.datasource.ds1.password=root
# ----------------------分库配置--------------------------
# database-strategy.inline 库分片策略 + 指定分库的分片键
sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id
# database-strategy.inline.algorithm-expression 分片算法表达式
sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
# 设置绑定表 t_order,t_order_item
sharding.jdbc.config.sharding.binding-tables=t_order,t_order_item
# ---------------------- t_order分表配置----------------------
# t_order 分库分表后真实的数据节点(逻辑表 -> 真实表)
sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1}
# 分片键设置
sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order$->{order_id % 2}
# 主键生成 sharding jdbc 默认主键算法是 64位雪花算法
# sharding.jdbc.config.sharding.tables.t_order.key-generator-class-name=io.shardingsphere.core.keygen.DefaultKeyGenerator
# sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=id
# ---------------------- 绑定表t_order_item分表配置 ----------------------
sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds$->{0..1}.t_order_item$->{0..1}
# 分片键设置
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.sharding-column=order_id
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.algorithm-expression=t_order_item$->{order_id % 2}
# 定义广播表
sharding.jdbc.config.sharding.broadcast-tables=t_province
sharding.jdbc.config.props.sql.show=true
Sharding JDBC(六) 分片策略三:复合分片策略ComplexShardingStrategy
一、复合分片策略ComplexShardingStrategy
- 提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持
- ComplexShardingStrategy支持多分片键
- 由于多分片键之间的关系复杂,因此Sharding-JDBC并未做过多的封装,而是直接将分片键值组合以及分片操作符交于算法接口,完全由应用开发者实现,提供最大的灵活度
二、ComplexShardingStrategy配置实现
Sharding -jdbc 在使用分片策略的时候,与分片算法是成对出现的,每种策略都对应一到两种分片算法(不分片策略NoneShardingStrategy除外)
application.properties 配置
# 复合分片
sharding.jdbc.datasource.names=ds0,ds1
sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds0.username=root
sharding.jdbc.datasource.ds0.password=root
sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds1.username=root
sharding.jdbc.datasource.ds1.password=root
# 分库配置 (行表达式分片策略 + 行表达式分片算法)
sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id
sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
sharding.jdbc.config.sharding.binding-tables=t_order,t_order_item
# t_order分表配置 (复合分片策略)
sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1}_$->{0..1}
sharding.jdbc.config.sharding.tables.t_order.table-strategy.complex.sharding-columns=user_id,order_id
sharding.jdbc.config.sharding.tables.t_order.table-strategy.complex.algorithm-class-name=ai.yunxi.sharding.config.ComplexShardingAlgorithm
# t_order_item分表配置 (复合分片策略)
sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds$->{0..1}.t_order_item$->{0..1}_$->{0..1}
# 标准 和 inline 都是单分片键 ,复合分片策略可以配置则多分片键
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.complex.sharding-columns=user_id,order_id
# 自定义算法,让使用者根据业务自定义实现(开发性接口更灵活方便)
sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.complex.algorithm-class-name=ai.yunxi.sharding.config.ComplexShardingAlgorithm
# 定义广播表
sharding.jdbc.config.sharding.broadcast-tables=t_province
sharding.jdbc.config.props.sql.show=true
自定义ComplexShardingAlgorithm
import io.shardingsphere.api.algorithm.sharding.ListShardingValue;
import io.shardingsphere.api.algorithm.sharding.ShardingValue;
import io.shardingsphere.api.algorithm.sharding.complex.ComplexKeysShardingAlgorithm;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
public class ComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm {
/**
*
* @param collection 在加载配置文件时,会解析表分片规则。将结果存储到 collection中,doSharding()参数使用
* @param shardingValues SQL中对应的
* @return
*/
@Override
public Collection<String> doSharding(Collection<String> collection, Collection<ShardingValue> shardingValues) {
System.out.println("collection:" + collection + ",shardingValues:" + shardingValues);
Collection<Integer> orderIdValues = getShardingValue(shardingValues, "order_id");
Collection<Integer> userIdValues = getShardingValue(shardingValues, "user_id");
List<String> shardingSuffix = new ArrayList<>();
// user_id,order_id分片键进行分表
for (Integer userId : userIdValues) {
for (Integer orderId : orderIdValues) {
String suffix = userId % 2 + "_" + orderId % 2;
for (String s : collection) {
if (s.endsWith(suffix)) {
shardingSuffix.add(s);
}
}
}
}
return shardingSuffix;
}
/**
* 例如: SELECT * FROM T_ORDER user_id = 100000 AND order_id = 1000009
* 循环 获取SQL 中 分片键列对应的value值
* @param shardingValues sql 中分片键的value值 -> 1000009
* @param key 分片键列名 -> user_id
* @return shardingValues 集合 -> [1000009]
*/
private Collection<Integer> getShardingValue(Collection<ShardingValue> shardingValues, final String key) {
Collection<Integer> valueSet = new ArrayList<>();
Iterator<ShardingValue> iterator = shardingValues.iterator();
while (iterator.hasNext()) {
ShardingValue next = iterator.next();
if (next instanceof ListShardingValue) {
ListShardingValue value = (ListShardingValue) next;
// user_id,order_id分片键进行分表
if (value.getColumnName().equals(key)) {
return value.getValues();
}
}
}
return valueSet;
}
}
Sharding JDBC(七) 分片策略四:Hint强制路由HintShardingStrategy
一、Hint强制路由HintShardingStrategy
在分库分区中,有些特定的SQL,Sharding-jdbc、Mycat、Vitess都不支持(可以查看相关文档各自对哪些SQL不支持),例如:insert into table1 select * from table2 where ....这种SQL 路由很麻烦,需要解析table2的路由(是在ds0 /ds1 table2_0/table_1),结果集归并,insert 语句也需要同样的路由解析。这种情况Sharding-jdbc可以使用Hint分片策略来实现各种Sharding-jdbc不支持语法的限制
- 通过Hint而非SQL解析的方式分片的策略。对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段
- Hint分片策略是绕过SQL解析的,所以对于这些比较复杂的需要分片的查询,采用Hint分片策略性能可能会更好
- 在读写分离数据库中,Hint 可以通过HintManager.setMasterRouteOnly()方法,强制读主库(主从复制存在一定延时,但在某些特定的业务场景中,可能更需要保证数据的实时性)
- 在读写分离中,Hint 可以强制读主库(主从复制是存在一定延时,但在业务场景中,可能更需要保证数据的实时性)
二、Hint强制路由HintShardingStrategy配置实现
Sharding -jdbc 在使用分片策略的时候,与分片算法是成对出现的,每种策略都对应一到两种分片算法(不分片策略NoneShardingStrategy除外)
application.properties 配置
# Hint 强制路由分片策略
sharding.jdbc.datasource.names=ds0,ds1
sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds0.username=root
sharding.jdbc.datasource.ds0.password=root
sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8
sharding.jdbc.datasource.ds1.username=root
sharding.jdbc.datasource.ds1.password=root
# 分库配置
sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id
sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
# t_order强制分片配置
sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1}
# 和其他3种不同的是,Hint 需要指定分片表 的数据库分片算法 + 表分片算法
sharding.jdbc.config.sharding.tables.t_order.database-strategy.hint.algorithm-class-name=ai.yunxi.sharding.config.HintShardingKeyAlgorithm
sharding.jdbc.config.sharding.tables.t_order.table-strategy.hint.algorithm-class-name=ai.yunxi.sharding.config.HintShardingKeyAlgorithm
sharding.jdbc.config.props.sql.show=true
自定义HintShardingKeyAlgorithm
import ai.yunxi.sharding.service.OrderService;
import io.shardingsphere.api.HintManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VipShardingApplication.class)
public class HintApplicationTests {
@Autowired
private OrderService orderService;
@Test
public void test() {
// Hint分片策略必须要使用 HintManager工具类
HintManager hintManager = HintManager.getInstance();
//
hintManager.addDatabaseShardingValue("t_order", 0);
hintManager.addTableShardingValue("t_order", 1);
// 直接指定对应具体的数据库
//hintManager.setDatabaseShardingValue(1);
//在读写分离数据库中,Hint 可以强制读主库(主从复制是存在一定延时,但在业务场景中,可能更需要保证数据的实时性)
//hintManager.setMasterRouteOnly();
System.out.println(orderService.findHint());
}
}
编写测试类HintApplicationTests
import ai.yunxi.sharding.service.OrderService;
import io.shardingsphere.api.HintManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VipShardingApplication.class)
public class HintApplicationTests {
@Autowired
private OrderService orderService;
@Test
public void test() {
// Hint分片策略必须要使用 HintManager工具类
HintManager hintManager = HintManager.getInstance();
//
hintManager.addDatabaseShardingValue("t_order", 0);
hintManager.addTableShardingValue("t_order", 1);
// 直接指定对应具体的数据库
//hintManager.setDatabaseShardingValue(1);
//在读写分离数据库中,Hint 可以强制读主库(主从复制是存在一定延时,但在业务场景中,可能更需要保证数据的实时性)
//hintManager.setMasterRouteOnly();
System.out.println(orderService.findHint());
}
}
Sharding JDBC (八) 分布式唯一主键
一、 Sharding-JDBC主键
软件项目开发中,主键自动生成是基本需求。而各个数据库对于该需求也提供了相应的支持,比如:数据库自增( MySQL, Oracle等)。但在分布式环境中,分库分表之后,不同表生成全局唯一的Id是非常棘手的问题。因为同一个逻辑表内的不同实际表之间的自增键是无法互相感知的, 这样会造成重复Id的生成。我们当然可以通过约束表生成键的规则(设置不同的起始和步长)来达到数据的不重复,但是这需要引入额外的运维力量来解决重复性问题,如果数据库节点变更会使框架缺乏扩展性。
目前有许多第三方解决方案可以完美解决这个问题,如:
- UUID/GUID(一般应用程序和数据库均支持)
- Redis| increment
- Mongo DB ObjectID(类似UUID的方式)
- Zookeeper分布式锁
- Twitter的 Snowflake(又名“雪花算法”)
- Ticket server(数据库生存方式, Flick采用的就是这种方式)
而 ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、 Snowflake。还抽离出分布式主键生成器的接口(io.shardingsphere.core.keygen.KeyGenerator),方便用户自行实现自定义的自增主键生成器。
二、Twitter的分布式自增ID算法Snowflake
雪花算法概述
有这么一种说法,自然界中并不存在两片完全一样的雪花的。每一片雪花都拥有自己漂亮独特的形状、独一无二。雪花算法也表示生成的ID如雪花般独一无二
snowflake算法是一款本地生成的(ID生成过程不依赖任何中间件,无网络通信),保证ID全局唯一,并且ID总体有序递增,
组成结构
大致由:首位无效符、时间戳差值,机器(进程)编码,序列号四部分组成,雪花算法生成的ID是纯数字且具有时间顺序的。
- 1 bit:不用,因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0
- 时间位:可以根据时间进行排序,有助于提高查询速度。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。
- 机器id位:适用于分布式环境下对多节点的各个节点进行标识,可以具体根据节点数和部署情况设计划分机器位10位长度,如划分5位表示进程位等,这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。
- 序列号位:是一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号
优点
1.时间自增排序
2.适合分布式场景,整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分)效率较高,一个节点每毫秒4096个ID序号,服务最大每毫秒409.6万个序列号
缺点:
1.雪花算法在单机系统上ID是递增的,但是在分布式系统多节点的情况下不是绝对递增,所有节点的时钟(System.currentTimeMillis())并不能保证不完全同步,所以有可能会出现不是全局递增的情况
2.不能在一台服务器上部署多个分布式ID服务;
3.时钟回拨问题;
三、Sharding JDBC 使用Snowflake生成唯一主键ID
ShardingSphere 在分片规则配置模块可配置每个表的主键生成策略,默认使用为雪花算法(io.shardingsphere.core.keygen.DefaultKeyGenerator)
# 主键生成 sharding jdbc 默认主键算法是 64位雪花算法
sharding.jdbc.config.sharding.tables.t_order.key-generator-class-name=io.shardingsphere.core.keygen.DefaultKeyGenerator
sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=id
使用DefaultKeyGenerator 类获取
DefaultKeyGenerator generator = new DefaultKeyGenerator();
generator.generateKey();
对Snowflake时钟回拨问题处理
private boolean waitTolerateTimeDifferenceIfNeed(long currentMilliseconds) throws Throwable {
try {
//lastMilliseconds 最后一次生成序列时间
if (this.lastMilliseconds <= currentMilliseconds) {
//如果lastMilliseconds
return false;
} else {
long timeDifferenceMilliseconds = this.lastMilliseconds - currentMilliseconds;
Preconditions.checkState(timeDifferenceMilliseconds < (long) maxTolerateTimeDifferenceMilliseconds,
"Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds",
new Object[]{this.lastMilliseconds, currentMilliseconds});
Thread.sleep(timeDifferenceMilliseconds);
return true;
}
} catch (Throwable var5) {
throw var5;
}
}
服务器时钟回拨会导致产生重复序列,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待(Thread.sleep)时钟同步到最后一次主键生成的时间后再继续工作。最大咨忍的时钟回拨室秒数的默认值为0,可通过调用静态方法 Defaultkey Generator setMaxTolerate Time DifferenceMilliseconds设置
生成主键实现理解
public synchronized Number generateKey() {
long currentMilliseconds = timeService.getCurrentMillis();
if (this.waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
currentMilliseconds = timeService.getCurrentMillis();
}
if (this.lastMilliseconds == currentMilliseconds) {
if (0L == (this.sequence = this.sequence + 1L & 4095L)) {
currentMilliseconds = this.waitUntilNextTime(currentMilliseconds);
}
} else {
this.vibrateSequenceOffset();
this.sequence = (long)this.sequenceOffset;
}
this.lastMilliseconds = currentMilliseconds;
return currentMilliseconds - EPOCH << 22 | workerId << 12 | this.sequence;
}
核心代码如下,几个实现的关键点:
- synchronized保证线程安全;
- 如果出现时间回拨,判断时钟回拨的时间是否超过最大容忍的毫秒数值,如果超过抛出异常;
- 如果当前时间和上一次是同一秒时间,那么sequence自增。如果同一秒内sequence自增值超过2^13-1,那么就会自旋等待下一秒(getNextSecond);
- 如果是新的一秒,那么sequence重新从0开始;
Sharding-JDBC 核心之解析引擎
一、解析引擎
如果在大学期间学习过计算机编程原理课程,SQL的解析是比较简单的。 不过,它依然是一门完善的编程语言,因此对SQL的语法进行解析,与解析其他编程语言(如:Java语言、C语言、Go语言等)并无本质区别。
SQL解析引擎在 parsing包下:Lexer:词法解析器、Parser:SQL解析器
两者都是解析器,区别在于 Lexer只做词法的解析,不关注上下文。讲字符串拆解成N个词法,而Perser在 Lexer的基础上,还需要理解SQL再进行解析
1.1 语法树
解析过程分为词法解析和语法解析。 词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将SQL转换为抽象语法树。
例如,以下SQL:SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18
抽象语法树中的关键字的Token用绿色表示,变量的Token用红色表示,灰色表示需要进一步拆分。
最后,通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要改写的位置。 供分片使用的解析上下文包含:
- 查询选择项(Select Items)
- 表信息(Table)
- 分片条件(Sharding Condition)
- 自增主键信息(Auto increment Primary Key)
- 排序信息(Order By)
- 分组信息(Group By)
- 分页信息(Limit、Rownum、Top)
SQL的一次解析过程是不可逆的,一个个Token的按SQL原本的顺序依次进行解析,性能很高。
二、Lexer词法解析器
Lexer 会按照循序解析SQL,将sql字符串分解成 N 个分词(token),且这个过程是不可逆的
Lexer类继承图
可以看出,当前sharding-jdbc支持的数据库就是H2、Oracle、PostgreSQL、Mysql、SQLServer
2.1 Token token用于描述当前分解出的词法,包含3个属性:
- TokenType type :词法标记类型
- String literals :当前词法字面量
- int endPosition :literals 在 SQL 字符串中的位置(去除所有空格和注释)
TokenType 用于描述当前token的类型,分成 4 大类:
- DefaultKeyword :词法关键词已经定义了数据库的关键字,例如:SELECT 、FROM、WHERE、AND
- Literals :词法字面量标记 sql分解出来的字符串实际值,例如关键字SELECT,table表名
- Symbol :词法符号标记Sql中的符号。例如 * 号,table1.name 中的点号,age,name中的逗号分隔符
- Assist :词法辅助标记 Assist枚举只有2个属性,END和ERROR,END表示分解结束
Literals词法字面量标记,一共分成6种:
- IDENTIFIER: 词法关键词
- VARIABLE: 变量
- CHARS: 字符串
- HEX: 十六进制
- INT: 整数
- FLOAT: 浮点数
2.2 Tokenizer分词器
由于不同数据库遵守的 SQL 规范有所不同,所以不同的数据库对应存在不同的 Lexer实现,并且维护了各自对应的dictionary。
Sharding 会根据连接数据库的类型,选择相对应的Lexer实现类,并将对应的数据库词典覆盖父类Lexer的dictionary属性。Lexer内部根据相应数据库的dictionary与sql语句生成一个Tokenizer分词器进行分词。
public final class Tokenizer {
//SQL
private final String input;
//不同数据库对应的字典
private final Dictionary dictionary;
//偏移量
private final int offset;
}
分词器具体的方法如下:
2.3 Lexer分词核心实现代码
public class Lexer {
private final String input;
private final Dictionary dictionary;
private int offset;
private Token currentToken;
public final void nextToken() {
this.skipIgnoredToken();
if (this.isVariableBegin()) {
this.currentToken = (new Tokenizer(this.input, this.dictionary, this.offset)).scanVariable();
} else if (this.isNCharBegin()) {
this.currentToken = (new Tokenizer(this.input, this.dictionary, ++this.offset)).scanChars();
} else if (this.isIdentifierBegin()) {
this.currentToken = (new Tokenizer(this.input, this.dictionary, this.offset)).scanIdentifier();
} else if (this.isHexDecimalBegin()) {
this.currentToken = (new Tokenizer(this.input, this.dictionary, this.offset)).scanHexDecimal();
} else if (this.isNumberBegin()) {
this.currentToken = (new Tokenizer(this.input, this.dictionary, this.offset)).scanNumber();
} else if (this.isSymbolBegin()) {
this.currentToken = (new Tokenizer(this.input, this.dictionary, this.offset)).scanSymbol();
} else if (this.isCharsBegin()) {
this.currentToken = (new Tokenizer(this.input, this.dictionary, this.offset)).scanChars();
} else {
if (!this.isEnd()) {
throw new SQLParsingException(this, Assist.ERROR);
}
this.currentToken = new Token(Assist.END, "", this.offset);
}
this.offset = this.currentToken.getEndPosition();
}
总结:Lexer主要的执行逻辑就是 nextToken() 方法,不断解析出当前 Token。Lexer的nextToken()方法里,使用 skipIgnoredToken() 方法跳过空格和注释的部分,通过 isXxx() 方法判断好下一个 Token 的类型,交给 Tokenizer 进行分词并跟新偏移量后返回 Token。
2.4 示例
@Test
public void lexerTest() {
String sql = "SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18";
MySQLLexer mySQLLexer = new MySQLLexer(sql);
boolean bool = true;
Token token;
do {
mySQLLexer.nextToken();
token = mySQLLexer.getCurrentToken();
System.out.println(JSONObject.toJSONString(token));
if (mySQLLexer.getCurrentToken().getType().toString().equals("END")) {
bool = false;
}
} while (bool);
}
三、Parser 语法解析器
Parser有三个组件
- SQLParsingengine: SQL解析引擎(掉用 StatementParser解析SQL)
- SQLParser:SQL解析器(调用 SQLParser解析SQL表达式)
- StatementParser:SQL语句解析(调用 Lexer解析SQL词法解析器)
3.1 SQLParser 语法解析器
SQLParser 语法解析器,根据不同类型的语句有不同的语法解析器去解析成SQLStatement
可以看到,不同类型的sql,不同厂商的数据库,存在不同的处理解析器去解析,解析完成之后,会将SQL解析成SQLStatement。
SQLParsingEngine类 ,sql的解析引擎,其 parse() 方法作为 SQL 解析入口,本身不带复杂逻辑,通过调用对应的 SQLParser 进行 SQL 解析,返回SQLStatement
public SQLStatement parse(boolean useCache) {
//ShardingSphere将使用PreparedStatement的SQL解析的语法树放入缓存。 因此建议采用PreparedStatement这种SQL预编译的方式提升性能。
Optional<SQLStatement> cachedSQLStatement = this.getSQLStatementFromCache(useCache);
if (cachedSQLStatement.isPresent()) {
return (SQLStatement)cachedSQLStatement.get();
} else {
//词法解析
LexerEngine lexerEngine = LexerEngineFactory.newInstance(this.dbType, this.sql);
//语法解析
SQLStatement result = SQLParserFactory.newInstance(this.dbType, this.shardingRule, lexerEngine, this.shardingTableMetaData, this.sql).parse();
if (useCache) {
ParsingResultCache.getInstance().put(this.sql, result);
}
return result;
}
}
SQLStatement对象是个超类,具体实现类有很多。按照不同的语句,解析成不同的SQLStatement。
SQLStatement api:
不同的语句,ddl,dml,tcl等,有不同的语法解析器SQLParser去解析,与词法分析器一样使用工厂模式,词法分析器Lexer在解析Sql的时候,第一个分词就是SQL的具体类型(select,update),所以在执行sql的时候,首先调用词法分析器解析第一个分词,再按照不同类型的SQL选择不同的语法解析器。根据数据库类型,DB类型分词解析器获取语法解析器。
public final class SQLParserFactory {
public static SQLParser newInstance(DatabaseType dbType, ShardingRule shardingRule, LexerEngine lexerEngine, ShardingTableMetaData shardingTableMetaData, String sql) {
lexerEngine.nextToken();
TokenType tokenType = lexerEngine.getCurrentToken().getType();
if (DQLStatement.isDQL(tokenType)) {
return (SQLParser)(DatabaseType.MySQL == dbType ? new AntlrParsingEngine(dbType, sql, shardingRule, shardingTableMetaData) : getDQLParser(dbType, shardingRule, lexerEngine, shardingTableMetaData));
} else if (DMLStatement.isDML(tokenType)) {
return getDMLParser(dbType, tokenType, shardingRule, lexerEngine, shardingTableMetaData);
} else if (TCLStatement.isTCL(tokenType)) {
return new AntlrParsingEngine(dbType, sql, shardingRule, shardingTableMetaData);
} else if (DALStatement.isDAL(tokenType)) {
return getDALParser(dbType, (Keyword)tokenType, shardingRule, lexerEngine);
} else {
lexerEngine.nextToken();
TokenType secondaryTokenType = lexerEngine.getCurrentToken().getType();
if (DCLStatement.isDCL(tokenType, secondaryTokenType)) {
return new AntlrParsingEngine(dbType, sql, shardingRule, shardingTableMetaData);
} else if (DDLStatement.isDDL(tokenType, secondaryTokenType)) {
return new AntlrParsingEngine(dbType, sql, shardingRule, shardingTableMetaData);
} else if (TCLStatement.isTCLUnsafe(dbType, tokenType, lexerEngine)) {
return new AntlrParsingEngine(dbType, sql, shardingRule, shardingTableMetaData);
} else if (DefaultKeyword.SET.equals(tokenType)) {
return SetParserFactory.newInstance();
} else {
throw new SQLParsingUnsupportedException(tokenType);
}
}
}
}
3.2 SQLParsingEngine
一条sql在执行的时候,如何知道是什么类型的语句??
词法分析器Lexer在解析Sql的时候,第一个分词就是SQL的具体类型(select,update),所以在执行sql的时候,首先调用词法分析器解析第一个分词,获取语句类型,然后选择具体的语法解析器解析。和分词器引擎一样,SQL语句解析器也有自己的解析引擎
Sharding-JDBC 核心之路由引擎
无论是分库分表、还是读写分离,一个SQL在DB上执行前都需要经过特定规则运算获得运行的目标库表信息。路由引擎的职责定位根据分片规则计算SQL应该在哪个数据库、哪个表上执行。前者结果会传给后续执行引擎,然后根据其数据库标识获取对应的数据库连接;后者结果则会传给改写引擎在SQL执行前进行表名的改写,即替换为正确的物理表名。计算哪个数据库依据的算法是要用户配置的库路由规则,计算哪个表依据的算法是用户配置的表路由规则。目前在ShardingSphere中需要进行路由的功能模块有两个:先处理分库分表sharding再处理读写分离master-slave
根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的SQL,根据分片键的不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN)。不携带分片键的SQL则采用广播路由。
路由引擎的整体结构划分如下图。
一、分片路由
用于原生SQL中有包含有分片键的场景,又细分为直接路由、标准路由和笛卡尔积路由这3种类型。执行的性能也依次减弱。
满足直接路由的条件相对苛刻,它需要通过Hint强制路由的(使用HintAPI直接指定路由至库表)方式分片,并且是只分库不分表的前提下,则可以避免SQL解析和之后的结果归并。 因此它的兼容性最好,可以执行包括子查询、自定义函数等复杂情况的任意SQL。直接路由还可以用于分片键不在SQL中的某些特殊场景。例如,设置用于数据库分片的键为3
hintManager.setDatabaseShardingValue(3);
假如路由算法为 order_id % 2,当一个逻辑库t_order对应2个真实库ds_0和ds_1时,路由后SQL将在ds_1上执行。下方是使用API的代码样例:
String sql = "SELECT * FROM t_order";
try (
HintManager hintManager = HintManager.getInstance();
Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
hintManager.setDatabaseShardingValue(3);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
//...
}
}
}
1.2 标准路由
标准路由是ShardingSphere最为推荐使用的分片方式,它的适用范围是不包含关联查询或仅包含绑定表和广播表之间关联查询的SQL。 当分片运算符是等于号时,路由结果将落入单库(表),当分片运算符是BETWEEN或IN时,则路由结果不一定落入唯一的库(表),因此一条逻辑SQL最终可能被拆分为多条用于执行的真实SQL。 举例说明,如果按复合分片规则按照order_id的奇数和偶数进行分库,user_id 的奇数和偶数进行分表时,一个单表查询的SQL如下
SELECT * FROM t_order WHERE order_id IN (1, 2) AND user_id IN (1,2) ;
那么路由的结果应为:
SELECT * FROM t_order WHERE ds0.order_id_0 IN (1, 2) AND user_id IN (1,2) ;
SELECT * FROM t_order WHERE ds0.order_id_1 IN (1, 2) AND user_id IN (1,2) ;
SELECT * FROM t_order WHERE ds1.order_id_0 IN (1, 2) AND user_id IN (1,2) ;
SELECT * FROM t_order WHERE ds1.order_id_1 IN (1, 2) AND user_id IN (1,2) ;
绑定表和广播表的join关联查询与单表查询复杂度和性能相当
- 广播表在每个分库中都有相同的一份数据,例如原生SQL jion t_order_item,每个分库都存在一张t_order_item表,不需要特殊处理就可以查询 ;
- 绑定表之间的数据是一一对应的,order_id 为奇数的数据只会存储在相同库的一个分表中,有很强的关联性,SQL拆分后的数目与单表是一致的;
笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛卡尔积组合执行。
不管是mycat还是sharding,所有的分库分表对非绑定表和广播表之间的关联查询都无法做到有效优化支持,无法根据分片规则计算出SQL应该在哪个数据库、哪个表上执行,那么结果就是把所有分库分表中关联使用到的表交叉查询。笛卡尔路由查询性能较低,需谨慎使用。
示例如下:
SELECT * FROM t_order AS t1 LFTE JION t_user AS t2 ON t1.user_id = t2.user_id
WHERE t1.order_id IN (1, 2)
二、广播路由
对于不携带分片键的SQL,则采取广播路由的方式。根据SQL类型又可以划分为全库表路由、全库路由、全实例路由、单播路由和阻断路由这5种类型。
2.1 全库表路由
全库表路由用于处理对数据库中与其逻辑表相关的所有真实表的操作,主要包括不带分片键的DQL和DML,以及DDL等。例如:
SELECT * FROM t_order WHERE good_prority IN (1, 10);
则会遍历所有数据库中的所有表,逐一匹配逻辑表和真实表名,能够匹配得上则执行。路由后成为
SELECT * FROM ds0.t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM ds0.t_order_1 WHERE good_prority IN (1, 10);
SELECT * FROM ds1.t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM ds1.t_order_1 WHERE good_prority IN (1, 10);
2.2 全库路由
全库路由用于处理对数据库的操作,包括用于库设置的SET类型的数据库管理命令,以及TCL这样的事务控制语句。 在这种情况下,会根据逻辑库的名字遍历所有符合名字匹配的真实库,并在真实库中执行该命令,例如:
SET autocommit=0;
在t_order中执行时有2个真实库。则实际会在 ds0 和 ds1 上都执行这个命令。
2.3 全实例路由
全实例路由用于DCL操作,授权语句针对的是数据库的实例。无论一个实例中包含多少个Schema,每个数据库的实例只执行一次。例如:
CREATE USER customer@127.0.0.1 identified BY '123';
这个命令将在所有的真实数据库实例中执行,以确保customer用户可以访问每一个实例。
2.4 单播路由
单播路由用于获取某一真实表信息的场景,它仅需要从任意库中的任意真实表中获取数据即可。例如:
DESCRIBE t_order;
ds0 和 但ds1 上的的两个真实表t_order_0,t_order_1的描述结构相同,所以这个命令在任意真实表上选择执行一次。
阻断路由用于屏蔽SQL对数据库的操作,例如:
USE order_db;
这个命令不会在真实数据库中执行,因为ShardingSphere采用的是逻辑Schema的方式,无需将切换数据库Schema的命令发送至数据库中。
Sharding-JDBC 核心之归并引擎
ShardingSphere支持的结果归并从功能上分为遍历、排序、分组、分页和聚合5种类型,它们是组合而非互斥的关系。 从结构划分,可分为流式归并、内存归并和装饰者归并。流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。
- 流式归并是指每一次从结果集中获取到的数据,都能够通过逐条获取的方式返回正确的单条数据,它与数据库原生的返回结果集的方式最为契合。遍历、排序以及流式分组都属于流式归并的一种。
- 内存归并则是需要将结果集的所有数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回。
- 装饰者归并是对所有的结果集归并进行统一的功能增强,目前装饰者归并有分页归并和聚合归并这2种类型。
归并引擎主要源码如下
MergeEngineFactory-归并引擎工厂类:
//创建归并引擎的实例 ,根据SQL语句的类型不同,创建DQLMergeEngine或者DALMergeEngine
public static MergeEngine newInstance(final DatabaseType databaseType, final ShardingRule shardingRule,
final SQLStatement sqlStatement, final ShardingTableMetaData shardingTableMetaData, final List<QueryResult> queryResults) throws SQLException {
if (sqlStatement instanceof SelectStatement) {
// 创建DQLMergeEngine实例
return new DQLMergeEngine(databaseType, (SelectStatement) sqlStatement, queryResults);
}
if (sqlStatement instanceof DALStatement) {
return new DALMergeEngine(shardingRule, queryResults, (DALStatement) sqlStatement, shardingTableMetaData);
}
throw new UnsupportedOperationException(String.format("Cannot support type '%s'", sqlStatement.getType()));
}
DQLMergeEngine引擎:
public MergedResult merge() throws SQLException {
if (1 == this.queryResults.size()) {
// 如果只有一个归并的结果集,使用遍历归并(方法内部也只是调用了List.iterator()方法和将返回的结果集赋值给变量)
return new IteratorStreamMergedResult(this.queryResults);
} else {
this.selectStatement.setIndexForItems(this.columnLabelIndexMap);
return this.decorate(this.build());
}
}
private MergedResult build() throws SQLException {
// 判断原生sql 中是否 包含分组语句 + 聚合函数
if (this.selectStatement.getGroupByItems().isEmpty() && this.selectStatement.getAggregationSelectItems().isEmpty()) {
//原生sql 含有排序语句,则使用排序归并,否则使用遍历归并,因为最终返回客户端的结构不需要排序,所以只需多个结果集组合起来
return (MergedResult)(!this.selectStatement.getOrderByItems().isEmpty() ? new OrderByStreamMergedResult(this.queryResults, this.selectStatement.getOrderByItems()) : new IteratorStreamMergedResult(this.queryResults));
} else {
// 分组排序又分为流式分组排序 和 内存分组排序 2种
return this.getGroupByMergedResult();
}
}
private MergedResult getGroupByMergedResult() throws SQLException {
// isSameGroupByAndOrderByItems()方法: retrun !this.getGroupByItems().isEmpty() && this.getGroupByItems().equals(this.getOrderByItems());
return (MergedResult)(this.selectStatement.isSameGroupByAndOrderByItems() ? new GroupByStreamMergedResult(this.columnLabelIndexMap, this.queryResults, this.selectStatement) : new GroupByMemoryMergedResult(this.columnLabelIndexMap, this.queryResults, this.selectStatement));
}
//使用装饰器模式对结果集进行分页归并
private MergedResult decorate(MergedResult mergedResult) throws SQLException {
Limit limit = this.selectStatement.getLimit();
if (null != limit && 1 != this.queryResults.size()) {
//根据数据库类型的不同执行相应的分页结果集归并
if (DatabaseType.MySQL != this.databaseType && DatabaseType.PostgreSQL != this.databaseType && DatabaseType.H2 != this.databaseType) {
if (DatabaseType.Oracle == this.databaseType) {
return new RowNumberDecoratorMergedResult(mergedResult, this.selectStatement.getLimit());
} else {
return (MergedResult)(DatabaseType.SQLServer == this.databaseType ? new TopAndRowNumberDecoratorMergedResult(mergedResult, this.selectStatement.getLimit()) : mergedResult);
}
} else {
return new LimitDecoratorMergedResult(mergedResult, this.selectStatement.getLimit());
}
} else {
return mergedResult;
}
}
一、遍历归并
它是最为简单的归并方式。在返回的结果集只有一个或者没有使用到排序条件的场景中使用,因为不涉及到排序, 只需将多个数据结果集合并为一个单向链表即可。在遍历完成链表中当前数据结果集之后,将链表元素后移一位,继续遍历下一个数据结果集即可。
二、排序归并
在 查询SQL中,使用order by 但是没有group by + 聚合函数 的情况下使用,由于在SQL中存在ORDER BY语句,因此每个数据结果集自身是有序的,但各个数据结果集之间是无序的。因此只需要将数据结果集当前游标指向的数据值进行排序即可。 这相当于对多个有序的数组进行排序,归并排序是最适合此场景的排序算法。
ShardingSphere在对排序的查询进行归并时,将每个结果集的当前数据值进行比较(通过实现Java的Comparable接口完成),并将其放入优先级队列。 每次获取下一条数据时,只需将队列顶端结果集的游标下移,并根据新游标重新进入优先级排序队列找到自己的位置即可。
例如当前有三个数据结果集,如下所示:
将各个数据结果集的当前游标指向的数据值进行排序,并放入优先级队列。t_score_0的第一个数据值最大,t_score_2的第一个数据值次之,t_score_1的第一个数据值最小,因此优先级队列根据t_score_0,t_score_2和t_score_1的方式排序队列。结果如下所示:
调用next()方法,排在优先级队列首位的t_score_0将会被弹出队列,并且将当前游标指向的数据值返回至查询客户端,并且将游标下移一位之后重新放入优先级队列从新进行优先级队列排序
可以看到,对于每个数据结果集中的数据有序,而多数据结果集整体无序的情况下,ShardingSphere无需将所有的数据都加载至内存即可排序。 它使用的是流式归并的方式,每次next仅获取唯一正确的一条数据,极大的节省了内存的消耗。
从另一个角度来说,ShardingSphere的排序归并,是在维护数据结果集的纵轴和横轴这两个维度的有序性。 纵轴是指每个数据结果集本身,它是天然有序的,它通过包含ORDER BY的SQL所获取。 横轴是指每个数据结果集当前游标所指向的值,它需要通过优先级队列来维护其正确顺序。 每一次数据结果集当前游标的下移,都需要将该数据结果集重新放入优先级队列排序,而只有排列在队列首位的数据结果集才可能发生游标下移的操作。
三、分组归并
分组归并的情况最为复杂,它分为流式分组归并和内存分组归并。 流式分组归并要求SQL的排序项与分组项的字段以及排序类型(ASC或DESC)必须保持一致,否则只能通过内存归并才能保证其数据的正确性。
private MergedResult getGroupByMergedResult() throws SQLException {
return (MergedResult)(this.selectStatement.isSameGroupByAndOrderByItems() ? new GroupByStreamMergedResult(this.columnLabelIndexMap, this.queryResults, this.selectStatement) : new GroupByMemoryMergedResult(this.columnLabelIndexMap, this.queryResults, this.selectStatement));
}
public boolean isSameGroupByAndOrderByItems() {
return !this.getGroupByItems().isEmpty() && this.getGroupByItems().equals(this.getOrderByItems());
}
3.1 流式分组归并
在分组项与排序项完全一致的情况下,取得的数据是连续的,分组所需的数据全数存在于各个数据结果集的当前游标所指向的数据值,因此可以采用流式归并。
举例说明,假设根据科目分片,表结构中包含考生的姓名(为了简单起见,不考虑重名的情况)和分数。通过SQL获取每位考生的总分,可通过如下SQL:
SELECT name, SUM(score) FROM t_score GROUP BY name ORDER BY name;
进行归并时,逻辑与排序归并类似,下图展现了进行next调用的时候,流式分组归并是如何进行的。
通过图中我们可以看到,当进行第一次next调用时,排在队列首位的t_score_java将会被弹出队列,并且将分组值同为“Jetty”的其他结果集中的数据一同弹出队列。 在获取了所有的姓名为“Jetty”的同学的分数之后,进行累加操作,那么,在第一次next调用结束后,取出的结果集是“Jetty”的分数总和。 与此同时,所有的数据结果集中的游标都将下移至数据值“Jetty”的下一个不同的数据值,并且根据数据结果集当前游标指向的值进行重排序。 因此,包含名字顺着第二位的“John”的相关数据结果集则排在的队列的前列。
流式分组归并与排序归并的区别仅仅在于两点:
- 它会一次性的将多个数据结果集中的分组项相同的数据全数取出。
- 它需要根据聚合函数的类型进行聚合计算。
3.2 内存分组归并
对于分组项与排序项不一致的情况,由于需要获取分组的相关的数据值并非连续的,因此无法使用流式归并,需要将所有的结果集数据加载至内存中进行分组和聚合。 例如,若通过以下SQL获取每位考生的总分并按照分数从高至低排序,是无法进行流式归并的,只能将结果集的所有数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回:
注意:当SQL中只包含分组语句时,根据不同数据库的实现,其排序的顺序不一定与分组顺序一致。 但由于排序语句的缺失,则表示此SQL并不在意排序顺序。 因此,ShardingSphere通过SQL优化的改写,自动增加与分组项一致的排序项,使其能够从消耗内存的内存分组归并方式转化为流式分组归并方案。
四、聚合归并
聚合归并是在之前介绍的归并类的之上追加的归并能力,即装饰者模式的一种
无论是流式分组归并还是内存分组归并,对聚合函数的处理都是一致的。 除了分组的SQL之外,不进行分组的SQL也可以使用聚合函数。聚合函数可以归类为比较、累加和求平均值这3种类型
- 比较类型的聚合函数是指MAX和MIN。它们需要对每一个同组的结果集数据进行比较,并且直接返回其最大或最小值即可。
- 累加类型的聚合函数是指SUM和COUNT。它们需要将每一个同组的结果集数据进行累加。
- 求平均值的聚合函数只有AVG。
五、分页归并
上文所述的所有归并类型都可能进行分页。如源码部分所示,merge()归并方法中会调用decorate()进行分页处理。 分页也是追加在其他归并类型之上的装饰器,ShardingSphere通过装饰者模式来增加对数据结果集进行分页的能力。 分页归并负责将无需获取的数据过滤掉。
ShardingSphere 执行分页的处理是通过对SQL的改写来实现的。例如:
SELECT id FROM t_user WHERE age > 18 LIMIT 10000,10
为了保证返回数据的准确性,在SQL改写阶段修改为:SELECT id FROM t_user WHERE age > 18 LIMIT 0,10000
归并引擎的整体结构划分如下图。
Sharding-JDBC 核心之执行引擎
ShardingSphere采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。 它不是简单地将SQL通过JDBC直接发送至数据源执行;也并非直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是自动化的平衡资源控制与执行效率。
一、连接模式
ShardingSphere提供了一种解决思路。 它提出了连接模式(Connection Mode)的概念,将其划分为内存限制模式(MEMORY_STRICTLY)和连接限制模式(CONNECTION_STRICTLY)这两种类型。
1.1 内存限制模式
使用此模式的前提是,ShardingSphere对一次操作所耗费的数据库连接数量不做限制。 如果实际执行的SQL需要对某数据库实例中的200张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。 并且在SQL满足条件情况下,优先选择流式归并,以防止出现内存溢出或避免频繁垃圾回收情况。
注释:以结果集游标下移进行结果归并的方式,称之为流式归并,它无需将结果数据全数加载至内存,可以有效的节省内存资源,进而减少垃圾回收的频次。可以参考:Sharding-JDBC 核心之归并引擎
1.2 连接限制模式
使用此模式的前提是,ShardingSphere严格控制对一次操作所耗费的数据库连接数量。 如果实际执行的SQL需要对某数据库实例中的200张表做操作,那么只会创建唯一的数据库连接,并对其200张表串行处理。 如果一次操作中的分片散落在不同的数据库,采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。 这样即可以防止对一次请求对数据库连接占用过多所带来的问题。该模式始终选择内存归并。
注释:连接限制模式串行处理,只持有一个独立数据库连接时,复用该数据库连接获取下一张分表的查询结果集之前,会将当前的查询结果集全数加载至内存再进行一下次查询,然后将所有的分表结果在内存中归并处理。可以参考:Sharding-JDBC 核心之归并引擎
内存限制模式适用于OLAP操作,可以通过放宽对数据库连接的限制提升系统吞吐量; 连接限制模式适用于OLTP操作,OLTP通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用。
二、自动化执行引擎
ShardingSphere最初将使用何种模式的决定权交由用户配置,让开发者依据自己业务的实际场景需求选择使用内存限制模式或连接限制模式。
这种一分为二的处理方案,将两种模式的切换交由静态的初始化配置,是缺乏灵活应对能力的。在实际的使用场景中,面对不同SQL以及占位符参数,每次的路由结果是不同的。 这就意味着某些操作可能需要使用内存归并,而某些操作则可能选择流式归并更优,具体采用哪种方式不应该由用户在Shard ingSphere启动之前配置好,而是应该根据SQL和占位符参数的场景,来动态的决定连接模式。
自动化执行引擎将连接模式的选择粒度细化至每一次SQL的操作。 针对每次SQL请求,自动化执行引擎都将根据其路由结果,进行实时的演算和权衡,并自主地采用恰当的连接模式执行,以达到资源控制和效率的最优平衡。 针对自动化的执行引擎,用户只需配置maxConnectionSizePerQuery即可,该参数表示一次查询时每个数据库所允许使用的最大连接数。
执行引擎分为准备和执行两个阶段:
准备阶段
执行阶段
2.1 准备阶段
顾名思义,此阶段用于准备执行的数据。它分为结果集分组和执行单元创建两个步骤
2.1.1 结果集分组
结果集分组是实现内化连接模式概念的关键。执行引擎根据maxConnectionSizePerQuery配置项,结合当前路由结果,选择恰当的连接模式。 具体步骤如下:
- 将SQL的路由结果按照数据源的名称进行分组。
- 通过下图的公式,可以获得每个数据库实例在maxConnectionSizePerQuery的允许范围内,每个连接需要执行的SQL路由结果组,并计算出本次请求的最优连接模式。
每一次的连接模式的选择,是针对每一个物理数据库的。也就是说,在同一次查询中,如果路由至一个以上的数据库,每个数据库的连接模式不一定一样,它们可能是混合存在的形态。
例如路由和改写之后的sql为:
SELECT * FROM t_order WHERE ds0.order_id_0 IN (1, 2, 3) ;
SELECT * FROM t_order WHERE ds0.order_id_1 IN (1, 2, 3) ;
SELECT * FROM t_order WHERE ds0.order_id_2 IN (1, 2, 3) ;
SELECT * FROM t_order WHERE ds1.order_id_0 IN (1, 2, 3) ;
SELECT * FROM t_order WHERE ds1.order_id_1 IN (1, 2, 3) ;
将SQL的路由结果按照数据源的名称进行分组得到ds0 和 ds1 ,假如maxConnectionSizePerQuery设置为 2 ,那么 ds0 连接需要执行的请求数量为 3/2 >1,意味着ds0 的数据库连接将使用连接限制模式,只会创建唯一的数据库连接,复用数据库连接执行下一次的查询操作时,必须将当前得到的结果集加载到内存,则必须采用内存归并。反之 ds1 连接需要执行的请求数量为 2/2 = 1,采用内存限制模式,则可以采用流式归并
2.1.2 执行单元创建
通过上一步骤获得的路由分组结果创建执行的单元。 当数据源使用数据库连接池等控制数据库连接数量的技术时,在获取数据库连接时,如果不妥善处理并发,则有一定几率发生死锁。 在多个请求相互等待对方释放数据库连接资源时,将会产生饥饿等待,造成交叉的死锁问题。
举例说明,假设一次查询需要在某一数据源上获取两个数据库连接,并路由至同一个数据库的两个分表查询。 则有可能出现查询A已获取到该数据源的1个数据库连接,并等待获取另一个数据库连接;而查询B也已经在该数据源上获取到的一个数据库连接,并同样等待另一个数据库连接的获取。 如果数据库连接池的允许最大连接数是2,那么这2个查询请求将永久的等待下去。下图描绘了死锁的情况。
ShardingSphere为了避免死锁的出现,在获取数据库连接时进行了同步处理。 它在创建执行单元时,以原子性的方式一次性获取本次SQL请求所需的全部数据库连接,杜绝了每次查询请求获取到部分资源的可能。 由于对数据库的操作非常频繁,每次获取数据库连接时时都进行锁定,会降低ShardingSphere的并发。因此,ShardingSphere在这里进行了2点优化:
- 避免锁定一次性只需要获取1个数据库连接的操作。因为每次仅需要获取1个连接,则不会发生两个请求相互等待的场景,无需锁定。对于大部分OLTP的操作,都是使用分片键路由至唯一的数据节点,这会使得系统变为完全无锁的状态,进一步提升了并发效率。除了路由至单分片的情况,读写分离也在此范畴之内。
- 仅针对内存限制模式时才进行资源锁定。在使用连接限制模式时,所有的查询结果集将在装载至内存之后释放掉数据库连接资源,因此不会产生死锁等待的问题。
2.2 执行阶段
该阶段用于真正的执行SQL,它分为分组执行和归并结果集生成两个步骤。
分组执行将准备执行阶段生成的执行单元分组下发至底层并发执行引擎,并针对执行过程中的每个关键步骤发送事件。 如:执行开始事件、执行成功事件以及执行失败事件。执行引擎仅关注事件的发送,它并不关心事件的订阅者。 ShardingSphere的其他模块,如:分布式事务、调用链路追踪等,会订阅感兴趣的事件,并进行相应的处理。
ShardingSphere通过在执行准备阶段的获取的连接模式,生成内存归并结果集或流式归并结果集,并将其传递至结果归并引擎,以进行下一步的工作。可以参考:归并引擎
执行引擎的整体结构划分如下图所示。