Sharding 您所在的位置:网站首页 shardingjdbc扩容方案 Sharding

Sharding

2024-05-30 00:06| 来源: 网络整理| 查看: 265

文章很长,而且持续更新,建议收藏起来,慢慢读! Java 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源:

免费赠送 经典图书 : 极致经典 + 社群大片好评 《 Java 高并发 三部曲 》 面试必备 + 大厂必备 + 涨薪必备免费赠送 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 大厂必备 +涨薪必备 (加尼恩领取)免费赠送 经典图书 : 《SpringCloud、Nginx高并发核心编程》 面试必备 + 大厂必备 + 涨薪必备 (加尼恩领取)免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 (加尼恩领取) Sharding-JDBC 实战(史上最全)

在开始 Sharding-JDBC分库分表具体实战之前,

必要先了解分库分表的一些核心概念。

分库分表的背景:

传统的将数据集中存储⾄单⼀数据节点的解决⽅案,在性能、可⽤性和运维成本这三⽅⾯已经难于满⾜互联⽹的海量数据场景。

随着业务数据量的增加,原来所有的数据都是在一个数据库上的,网络IO及文件IO都集中在一个数据库上的,因此CPU、内存、文件IO、网络IO都可能会成为系统瓶颈。

当业务系统的数据容量接近或超过单台服务器的容量、QPS/TPS接近或超过单个数据库实例的处理极限等,

此时,往往是采用垂直和水平结合的数据拆分方法,把数据服务和数据存储分布到多台数据库服务器上。

容量瓶颈:

从性能⽅⾯来说,由于关系型数据库⼤多采⽤ B+ 树类型的索引,

数据量超过一定大小,B+Tree 索引的高度就会增加,而每增加一层高度,整个索引扫描就会多一次 IO 。

在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;

一般的存储容量是多少呢? 请参见 3 高架构秒杀部分内容。

吞吐量瓶颈:

同时,⾼并发访问请求也使得集中式数据库成为系统的最⼤瓶颈。

一般的吞吐量是多少呢? 请参见 3 高架构秒杀部分内容。

在传统的关系型数据库⽆法满⾜互联⽹场景需要的情况下,将数据存储⾄原⽣⽀持分布式的 NoSQL 的尝试越来越多。

但 NoSQL 并不能包治百病,而关系型数据库的地位却依然不可撼动。

如果进行sql、nosql数据库的选型呢? 请参见 推送中台架构部分的内容。

分治模式在存储领域的落地

分治模式在存储领域的使用:数据分⽚

数据分⽚指按照某个维度将存放在单⼀数据库中的数据, 分散地存放⾄多个数据库或表中以达到提升性能瓶颈以及可⽤性的效果。

数据分⽚的有效⼿段是对关系型数据库进⾏分库和分表。

分库能够⽤于有效的分散对数据库单点的访问量;

分库的合理的时机, 请参见 3 高架构秒杀部分内容。

分表能够⽤于有效的数据量超过可承受阈值而产⽣的查询瓶颈, 解决MySQL 单表性能问题

分表的合理的时机, 请参见 3 高架构秒杀部分内容。

使⽤多主多从的分⽚⽅式,可以有效的避免数据单点,从而提升数据架构的可⽤性。

通过分库和分表进⾏数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进⾏疏导应对⾼访问量,是应对⾼并发和海量数据系统的有效⼿段。

数据分⽚的拆分⽅式⼜分为垂直分⽚和⽔平分⽚。

分库分表的问题 分库导致的事务问题

不过,由于目前采用柔性事务居多,实际上,分库的事务性能也是很高的,有关柔性事务,请参见疯狂创客圈的专题博文:

分布式事务面试题 (史上最全、持续更新、吐血推荐)

Sharding-JDBC简介

Sharding-JDBC 是当当网开源的适用于微服务的分布式数据访问基础类库,完整的实现了分库分表,读写分离和分布式主键功能,并初步实现了柔性事务。

从 2016 年开源至今,在经历了整体架构的数次精炼以及稳定性打磨后,如今它已积累了足够的底蕴。

官方的网址如下:

http://shardingsphere.apache.org/index_zh.html

ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar这3款相互独立的产品组成。

他们均提供标准化的数据分片、分布式事务 和 数据库治理功能,可适用于如Java同构、异构语言、云原生等各种多样化的应用场景。

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

Apache ShardingSphere 定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。 它通过关注不变,进而抓住事物本质。关系型数据库当今依然占有巨大市场,是各个公司核心业务的基石,未来也难于撼动,我们目前阶段更加关注在原有基础上的增量,而非颠覆。

Apache ShardingSphere 5.x 版本开始致力于可插拔架构,项目的功能组件能够灵活的以可插拔的方式进行扩展。 目前,数据分片、读写分离、数据加密、影子库压测等功能,以及 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 与协议的支持,均通过插件的方式织入项目。 开发者能够像使用积木一样定制属于自己的独特系统。Apache ShardingSphere 目前已提供数十个 SPI 作为系统的扩展点,仍在不断增加中。

ShardingSphere 已于2020年4月16日成为 Apache 软件基金会的顶级项目。

Sharding-JDBC的优势

Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:

可适用于任何基于Java的ORM框架,如JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC。可基于任何第三方的数据库连接池,如DBCP、C3P0、 BoneCP、Druid等。理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle、SQLServer等数据库的计划。

Sharding-JDBC定位为轻量Java框架,使用客户端直连数据库,以jar包形式提供服务,无proxy代理层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。

Sharding-JDBC分片策略灵活,可支持等号、between、in等多维度分片,也可支持多分片键。

SQL解析功能完善,支持聚合、分组、排序、limit、or等查询,并支持Binding Table以及笛卡尔积表查询。

与常见开源产品对比

下表仅列出在数据库分片领域非常有影响力的几个项目:

img

通过以上表格可以看出,Cobar(MyCat)属于中间层方案,在应用程序和MySQL之间搭建一层Proxy。

中间层介于应用程序与数据库间,需要做一次转发,而基于JDBC协议并无额外转发,直接由应用程序连接数据库,性能上有些许优势。这里并非说明中间层一定不如客户端直连,除了性能,需要考虑的因素还有很多,中间层更便于实现监控、数据迁移、连接管理等功能。

Cobar-Client、TDDL和Sharding-JDBC均属于客户端直连方案。

此方案的优势在于轻便、兼容性、性能以及对DBA影响小。其中Cobar-Client的实现方式基于ORM(Mybatis)框架,其兼容性与扩展性不如基于JDBC协议的后两者。

img

目前常用的就是Cobar(MyCat)与Sharding-JDBC两种方案

MyCAT

MyCAT是社区爱好者在阿里cobar基础上进行二次开发,解决了cobar当时存 在的一些问题,并且加入了许多新的功能在其中。目前MyCAT社区活 跃度很高,

目前已经有一些公司在使用MyCAT。

总体来说支持度比 较高,也会一直维护下去,发展到目前的版本,已经不是一个单纯的MySQL代理了,

它的后端可以支持MySQL, SQL Server, Oracle, DB2, PostgreSQL等主流数据库,也支持MongoDB这种新型NoSQL方式的存储,未来还会支持更多类型的存储。

MyCAT是一个强大的数据库中间件,不仅仅可以用作读写分离,以及分表分库、容灾管理,而且可以用于多租户应用开发、云平台基础设施,让你的架构具备很强的适应性和灵活性,

借助于即将发布的MyCAT只能优化模块,系统的数据访问瓶颈和热点一目了然,

根据这些统计分析数据,你可以自动或手工调整后端存储,将不同的表隐射到不同存储引擎上,而整个应用的代码一行也不用改变。

MyCAT是在Cobar基础上发展的版本,两个显著提高:

后端由BIO改为NIO,并发量有大幅提高;

增加了对Order By, Group By, Limit等聚合功能

(虽然Cobar也可以支持Order By, Group By, Limit语法,但是结果没有进行聚合,只是简单返回给前端,聚合功能还是需要业务系统自己完成, 适用于有专门团队维护的大型企业、或者大团队。)

Sharding-JDBC

Sharding-JDBC定位为轻量Java框架,使用客户端直连数据库,以jar包形式提供服务,无proxy代理层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。

所以 ,适用于中小企业、或者中小团队。

Sharding-JDBC分片策略灵活,可支持等号、between、in等多维度分片,也可支持多分片键。

SQL解析功能完善,支持聚合、分组、排序、limit、or等查询,并支持Binding Table以及笛卡尔积表查询。

Sharding-JDBC 功能列表 分库 & 分表读写分离分布式主键 高并发数据分片的两大工作

一般情况下,开发维度的数据分片,大多是以水平切分模式(水平分库、分表)为基础来说的,

垂直分片主要在于 运维维度,或者 或者做存储的深级改造的时候。

数据分片的工作

简单来说,数据分片的工作分为两大工作 :

第一大工作:分片的拆分 es 的数据分片的背后原理

参见视频

rediscluster的数据分片的背后原理

表的拆分:

将一张大表 t_order ,拆分生成数个表结构完全一致的小表 t_order_0、t_order_1、···、t_order_n,

每张小表,只存储大表中的一部分数据,

第二大工作:分片的路由

当执行一条SQL时,会通过 路由策略 , 将数据**route(路由)**到不同的分片内。

面临的问题:

分片建的选择

分片策略的选择

分片算法的选择

什么是数据分片?

按照分片规则把数据分到若干个shard、partition当中

在这里插入图片描述

主要的分片算法 range 分片

一种是按照 range 来分,就是每个片,一段连续的数据,这个一般是按比如时间范围/数据范围来的,但是这种一般较少用,因为很容易发生数据倾斜,大量的流量都打在最新的数据上了。

比如,安装数据范围分片,把1到100个数字,要保存在3个节点上

按照顺序分片,把数据平均分配三个节点上

1号到33号数据保存到节点1上34号到66号数据保存到节点2上67号到100号数据保存到节点3上

在这里插入图片描述

ID取模分片

此种分片规则将数据分成n份(通常dn节点也为n),从而将数据均匀的分布于各个表中,或者各节点上。

扩容方便。

ID取模分片常用在关系型数据库的设计

具体请参见 秒杀视频的 亿级库表架构设计

hash 哈希分布

使用hash 算法,获取key的哈希结果,再按照规则进行分片,这样可以保证数据被打散,同时保证数据分布的比较均匀

哈希分布方式分为三个分片方式:

哈希取余分片一致性哈希分片虚拟槽分片 哈希取余模分片

例如1到100个数字,对每个数字进行哈希运算,然后对每个数的哈希结果除以节点数进行取余,余数为1则保存在第1个节点上,余数为2则保存在第2个节点上,余数为0则保存在第3个节点,这样可以保证数据被打散,同时保证数据分布的比较均匀

比如有100个数据,对每个数据进行hash运算之后,与节点数进行取余运算,根据余数不同保存在不同的节点上

在这里插入图片描述

哈希取余分片是非常简单的一种分片方式

哈希取模分片有一个问题

即当增加或减少节点时,原来节点中的80%的数据会进行迁移操作,对所有数据重新进行分布

哈希取余分片,建议使用多倍扩容的方式,例如以前用3个节点保存数据,扩容为比以前多一倍的节点即6个节点来保存数据,这样只需要适移50%的数据。

数据迁移之后,第一次无法从缓存中读取数据,必须先从数据库中读取数据,然后回写到缓存中,然后才能从缓存中读取迁移之后的数据

img

哈希取余分片优点:

配置简单:对数据进行哈希,然后取余

哈希取余分片缺点:

数据节点伸缩时,导致数据迁移迁移数量和添加节点数据有关,建议翻倍扩容 一致性哈希分片

一致性哈希原理:

将所有的数据当做一个token环,

token环中的数据范围是0到2的32次方。

然后为每一个数据节点分配一个token范围值,这个节点就负责保存这个范围内的数据。

img

对每一个key进行hash运算,被哈希后的结果在哪个token的范围内,则按顺时针去找最近的节点,这个key将会被保存在这个节点上。

img

一致性哈希分片的节点扩容

在下面的图中:

有4个key被hash之后的值在在n1节点和n2节点之间,按照顺时针规则,这4个key都会被保存在n2节点上

如果在n1节点和n2节点之间添加n5节点,当下次有key被hash之后的值在n1节点和n5节点之间,这些key就会被保存在n5节点上面了

下图的例子里,添加n5节点之后:

数据迁移会在n1节点和n2节点之间进行n3节点和n4节点不受影响数据迁移范围被缩小很多

同理,如果有1000个节点,此时添加一个节点,受影响的节点范围最多只有千分之2。所以,一致性哈希一般用在节点比较多的时候,节点越多,扩容时受影响的节点范围越少

img

分片方式:哈希 + 顺时针(优化取余)

一致性哈希分片优点:

一致性哈希算法解决了分布式下数据分布问题。比如在缓存系统中,通过一致性哈希算法把缓存键映射到不同的节点上,由于算法中虚拟节点的存在,哈希结果一般情况下比较均匀。节点伸缩时,只影响邻近节点,但是还是有数据迁移

“但没有一种解决方案是银弹,能适用于任何场景。所以实践中一致性哈希算法有哪些缺陷,或者有哪些场景不适用呢?”

一致性哈希分片缺点:

一致性哈希在大批量的数据场景下负载更加均衡,但是在数据规模小的场景下,会出现单位时间内某个节点完全空闲的情况出现。

虚拟槽分片 (范围分片的变种)

Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;

虚拟槽分片是Redis Cluster采用的分片方式.

虚拟槽分片 ,可以理解为范围分片的变种, hash取模分片+范围分片, 把hash值取余数分为n段,一个段给一个节点负责

在这里插入图片描述

es的数据分片两大工作 Shards

代表索引分片,es可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。

分片的数量只能在索引创建前指定,并且索引创建后不能更改。(why,大家可以独立思考一下!)

分片配置建议:

每个分片大小不要超过30G,硬盘条件好的话,不建议超过100G.

(官方推荐,每个shard的数据量应该在20GB - 50GB)。

总而言之,每个分片都是一个Lucene实例,当查询请求打到ES后,ES会把请求转发到每个shard上分别进行查询,最终进行汇总。

这时候,shard越少,产生的额外开销越少

路由机制

一条数据是如何落地到对应的shard上的?

当索引一个文档的时候,文档会被存储到一个主分片中。

Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?

img

es的路由过程是根据下面这个算法决定的:

shard_num = hash(_routing) % num_primary_shards 其中 _routing是一个可变值,默认是文档的 _id 的值 ,也可以设置成一个自定义的值。Elasticsearch文档的ID(类似于关系数据库中的自增ID), _routing 通过 hash 函数生成一个数字,然后这个数字再除以 num_of_primary_shards (主分片的数量)后得到余数 。 这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:

因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

假设你有一个100个分片的索引。当一个请求在集群上执行时会发生什么呢?

1. 这个搜索的请求会被发送到一个节点 2. 接收到这个请求的节点,将这个查询广播到这个索引的每个分片上(可能是主分片,也可能是复本分片) 3. 每个分片执行这个搜索查询并返回结果 4. 结果在通道节点上合并、排序并返回给用户

img

rediscluster的数据分片两大工作 虚拟槽分片 ( hash取模分片+范围分片的混血)

Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;

虚拟槽分片是Redis Cluster采用的分片方式.

在该分片方式中:

首先 预设虚拟槽,每个槽为一个hash值,每个node负责一定槽范围。每一个值都是key的hash值取余,每个槽映射一个数据子集,一般比节点数大

Redis Cluster中预设虚拟槽的范围为0到16383

在这里插入图片描述

3个节点的Redis集群虚拟槽分片结果: [root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001 172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves. 172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves. 172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves. [OK] 0 keys in 3 masters. 0.00 keys per slot on average. >>> Performing Cluster Check (using node 172.18.8.164:6001) M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001 slots:[0-5460] (5461 slots) master 1 additional replica(s) S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006 slots: (0 slots) slave replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002 slots:[5461-10922] (5462 slots) master 1 additional replica(s) S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004 slots: (0 slots) slave replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005 slots: (0 slots) slave replicates c15a7801623ee5ebe3cf952989dd5a157918af96 M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003 slots:[10923-16383] (5461 slots) master 1 additional replica(s) [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. 虚拟槽分片的路由机制:

1.把16384槽按照节点数量进行平均分配,由节点进行管理 2.对每个key按照CRC16规则进行hash运算 3.把hash结果对16383进行取模 4.把余数发送给Redis节点 5.节点接收到数据,验证是否在自己管理的槽编号的范围

如果在自己管理的槽编号范围内,则把数据保存到数据槽中,然后返回执行结果如果在自己管理的槽编号范围外,则会把数据发送给正确的节点,由正确的节点来把数据保存在对应的槽中

需要注意的是:Redis Cluster的节点之间会共享消息,每个节点都会知道是哪个节点负责哪个范围内的数据槽

虚拟槽分布方式中,由于每个节点管理一部分数据槽,数据保存到数据槽中。

当节点扩容或者缩容时,对数据槽进行重新分配迁移即可,数据不会丢失。

shardingjdbc的数据分片两大工作 第一大工作:分片的拆分

表的拆分:

将一张大表 t_order ,拆分生成数个表结构完全一致的小表 t_order_0、t_order_1、···、t_order_n,

每张小表,只存储大表中的一部分数据,

例子:user表的数据分片

在这里插入图片描述

例子:order表的数据分片

在这里插入图片描述

第二大工作:分片的路由

当执行一条SQL时,会通过 路由策略 , 将数据**route(路由)**到不同的分片内。

数据源的路由

表的路由

面临的问题:

分片key的选择分片策略的选择分片算法的选择 核心概念 分⽚键

⽤于分⽚的字段,是将数据库(表)⽔平拆分的关键字段。

在对表中的数据进行分片时,首先要选出一个分片键(Shard Key),即用户可以通过这个字段进行数据的水平拆分。

例:

将订单表中的订单主键的尾数取模分⽚,则订单主键为分⽚字段。

执行表的选择

我们将 t_order 表分片以后,当执行一条 SQL 时,通过对字段 order_id 取模的方式来决定要执行的表, 这条数据该在哪个数据库中的哪个表中执行,此时 order_id 字段就是分片健。

在这里插入图片描述

执行库的选择(数据源的选择)

这样以来同一个订单的相关数据就会存在同一个数据库表中,大幅提升数据检索的性能,

说明

除了使用单个字段作为分片件, sharding-jdbc 还支持根据多个字段作为分片健进行分片。

SQL 中如果⽆分⽚字段,将执⾏全路由,性能较差。

数据节点

数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成,

例如上图中 ds1.t_user_0 就表示一个数据节点。

逻辑表

逻辑表是指一组具有相同逻辑和数据结构表的总称。

比如我们将订单表 t_order 拆分成 t_order_0 ··· t_order_9 等 10 张表。

此时我们会发现分库分表以后数据库中已不在有 t_order 这张表,取而代之的是 t_order_n,但我们在代码中写 SQL 依然按 t_order 来写。

此时 t_order 就是这些拆分表的逻辑表。

例如上图中 t_user 就表示一个数据节点。

真实表(物理表)

真实表也就是上边提到的 t_order_n 数据库中真实存在的物理表。

例如上图中 t_user _0就表示一个真实表。

分片策略

分片策略是一种抽象的概念,实际分片操作的是由分片算法和分片健来完成的。

真正可⽤于分⽚操作的是分⽚键 + 分⽚算法,也就是分⽚策略。

在这里插入图片描述

为什么要这么设计,是出于分⽚算法的独⽴性,将其独⽴抽离。

ShardingSphere-JDBC考虑更多的灵活性,把分片算法单独抽象出来,方便开发者扩展;

标准分片策略

标准分片策略适用于单分片键,此策略支持 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。

其中 PreciseShardingAlgorithm 是必选的,用于处理 = 和 IN 的分片。

RangeShardingAlgorithm 用于处理BETWEEN AND, >, =,, =, {t_order_id % 4} 代表 t_order 对其字段 t_order_id取模,拆分成4张表,而表名分别是t_order_0 到 t_order_3。

强制分片策略(Hint 暗示分片策略)

Hint 分片策略,通过指定分片健而非从 SQL 中提取分片健的方式进行分片的策略。

对于分⽚值⾮ SQL 决定,不是来自于分片建,甚至连分片建都没有 ,而由其他外置条件决定的场景,可使⽤Hint 分片策略 。

前面的分片策略都是解析 SQL 语句, 提取分片建和分片值,并根据设置的分片算法进行分片。

Hint 分片算法 指定分⽚值而⾮从 SQL 中提取,而是手工设置的⽅式,进⾏分⽚的策略。

例:内部系统,按照员⼯登录主键分库,而数据库中并⽆此字段。

不分⽚策略

对应 NoneShardingStrategy。不分⽚的策略。

这种严格来说不算是一种分片策略了。

只是ShardingSphere也提供了这么一个配置。

分片算法

上边我们提到可以用分片健取模的规则分片,但这只是比较简单的一种,

在实际开发中我们还希望用 >=、、, =, 、=、{ expression } 标识 行表达式即可。

⽬前⽀持数据节点和分⽚算法这两个部分的配置。

行表达式的内容使⽤的是 Groovy 的语法,Groovy 能够⽀持的所有操作, 行表达式均能够⽀持。例如: ${begin…end} 表⽰范围区间 ${[unit1, unit2, unit_x]} 表⽰枚举值

行表达式中如果出现连续多个 ${ expression } 或 $->{ expression } 表达式,整个表达式最终的结果将会根据每个表达式的结果进笛卡尔组合。 例如,以下⾏表达式: [ ′ o n l i n e ′ , ′ o f f l i n e ′ ] t a b l e {['online', 'offline']}_table [′online′,′offline′]t​able{1…3} 最终会解析为: online_table1, online_table2, online_table3, offline_table1, offline_table2,offline_table3

配置数据节点时对于均匀分布的数据节点,如果数据结构如下:

db0 ├── t_order0 └── t_order1 db1 ├── t_order0 └── t_order1

用行表达式可以简化为: db 0..1. t o r d e r {0..1}.t_order 0..1.to​rder{0…1} 或者 db − > 0..1. t o r d e r ->{0..1}.t_order −>0..1.to​rder->{0…1} 对于⾃定义的数据节点,如果数据结构如下:

db0 ├── t_order0 └── t_order1 db1 ├── t_order2 ├── t_order3 └── t_order4

用行表达式可以简化为: db0.t_order 0..1 , d b 1. t o r d e r {0..1},db1.t_order 0..1,db1.to​rder{2…4} 或者 db0.t_order − > 0..1 , d b 1. t o r d e r ->{0..1},db1.t_order −>0..1,db1.to​rder->{2…4}

配置分片规则

配置分片规则ShardingRuleConfiguration,包括多种配置规则:

表规则配置、绑定表配置、广播表配置、默认数据源名称、默认数据库分片策略、默认表分片策略、默认主键生成策略、主从规则配置、加密规则配置;

表规则配置 tableRuleConfigs:也就是上面配置的库分片策略和表分片策略,也是最常用的配置;绑定表配置 bindingTableGroups:指分⽚规则⼀致的主表和⼦表;绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将⼤⼤提升;广播表配置 broadcastTables:所有的分⽚数据源中都存在的表,表结构和表中的数据在每个数据库中均完全⼀致。适⽤于数据量不⼤且需要与海量数据的表进⾏关联查询的场景;默认数据源名称 defaultDataSourceName:未配置分片的表将通过默认数据源定位;默认数据库分片策略 defaultDatabaseShardingStrategyConfig:表规则配置可以设置数据库分片策略,如果没有配置可以在这里面配置默认的;默认表分片策略 defaultTableShardingStrategyConfig:表规则配置可以设置表分片策略,如果没有配置可以在这里面配置默认的;默认主键生成策略 defaultKeyGeneratorConfig:表规则配置可以设置主键生成策略,如果没有配置可以在这里面配置默认的;内置UUID、SNOWFLAKE生成器;主从规则配置 masterSlaveRuleConfigs:用来实现读写分离的,可配置一个主表多个从表,读面对多个从库可以配置负载均衡策略;加密规则配置 encryptRuleConfig:提供了对某些敏感数据进行加密的功能,提供了⼀套完整、安全、透明化、低改造成本的数据加密整合解决⽅案; 实战:数据插入

以上准备好,就可以操作数据库了,这里执行插入操作:

/** * 新增测试. * */ @Test public void testInsertUser() throws SQLException { /* * 1. 需要到DataSource * 2. 通过DataSource获取Connection * 3. 定义一条SQL语句. * 4. 通过Connection获取到PreparedStament. * 5. 执行SQL语句. * 6. 关闭连接. */ // * 2. 通过DataSource获取Connection Connection connection = dataSource.getConnection(); // * 3. 定义一条SQL语句. // 注意:******* sql语句中 使用的表是 上面代码中定义的逻辑表 ******* String sql = "insert into t_user(name) values('name-0001')"; // * 4. 通过Connection获取到PreparedStament. PreparedStatement preparedStatement = connection.prepareStatement(sql); // * 5. 执行SQL语句. preparedStatement.execute(); sql = "insert into t_user(name) values('name-0002')"; preparedStatement = connection.prepareStatement(sql); preparedStatement.execute(); // * 6. 关闭连接. preparedStatement.close(); connection.close(); }

通过以上配置的真实数据源、分片规则以及属性文件创建分片数据源ShardingDataSource;

接下来就可以像使用单库单表一样操作分库分表了,sql中可以直接使用逻辑表,分片算法会根据具体的值就行路由处理;

经过路由最终:奇数入ds1.t_user_1,偶数入ds0.t_user_0;

实战:数据查询

以上准备好,就可以操作数据库了,这里执行查询操作:

/** * 新增测试. * */ @Test public void testSelectUser() throws SQLException { /* * 1. 需要到DataSource * 2. 通过DataSource获取Connection * 3. 定义一条SQL语句. * 4. 通过Connection获取到PreparedStament. * 5. 执行SQL语句. * 6. 关闭连接. */ // * 2. 通过DataSource获取Connection Connection connection = dataSource.getConnection(); // * 3. 定义一条SQL语句. // 注意:******* sql语句中 使用的表是 上面代码中定义的逻辑表 ******* String sql = "select * from t_user where user_id=10000"; // * 4. 通过Connection获取到PreparedStament. PreparedStatement preparedStatement = connection.prepareStatement(sql); // * 5. 执行SQL语句. ResultSet resultSet= preparedStatement.executeQuery(); // * 6. 关闭连接. preparedStatement.close(); connection.close(); } 实战:Properties配置InlineShardingStrategy 实战

通过 Properties 配置来使用 InlineShardingStrategy

配置参数:

inline.shardingColumn 分片键;

inline.algorithmExpression 分片表达式

配置实例 spring.shardingsphere.datasource.names=ds0,ds1 spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds0.filters=com.alibaba.druid.filter.stat.StatFilter,com.alibaba.druid.wall.WallFilter,com.alibaba.druid.filter.logging.Log4j2Filter spring.shardingsphere.datasource.ds0.url=jdbc:mysql://cdh1:3306/sharding_db1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC spring.shardingsphere.datasource.ds0.password=123456 spring.shardingsphere.datasource.ds0.username=root spring.shardingsphere.datasource.ds0.maxActive=20 spring.shardingsphere.datasource.ds0.initialSize=1 spring.shardingsphere.datasource.ds0.maxWait=60000 spring.shardingsphere.datasource.ds0.minIdle=1 spring.shardingsphere.datasource.ds0.timeBetweenEvictionRunsMillis=60000 spring.shardingsphere.datasource.ds0.minEvictableIdleTimeMillis=300000 spring.shardingsphere.datasource.ds0.validationQuery=SELECT 1 FROM DUAL spring.shardingsphere.datasource.ds0.testWhileIdle=true spring.shardingsphere.datasource.ds0.testOnBorrow=false spring.shardingsphere.datasource.ds0.testOnReturn=false spring.shardingsphere.datasource.ds0.poolPreparedStatements=true spring.shardingsphere.datasource.ds0.maxOpenPreparedStatements=20 spring.shardingsphere.datasource.ds0.connection-properties=druid.starggSql=ture;druid.stat.slowSqlMillis=5000 spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds1.filters=com.alibaba.druid.filter.stat.StatFilter,com.alibaba.druid.wall.WallFilter,com.alibaba.druid.filter.logging.Log4j2Filter spring.shardingsphere.datasource.ds1.url=jdbc:mysql://cdh1:3306/sharding_db2?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC spring.shardingsphere.datasource.ds1.password=123456 spring.shardingsphere.datasource.ds1.username=root spring.shardingsphere.datasource.ds1.maxActive=20 spring.shardingsphere.datasource.ds1.initialSize=1 spring.shardingsphere.datasource.ds1.maxWait=60000 spring.shardingsphere.datasource.ds1.minIdle=1 spring.shardingsphere.datasource.ds1.timeBetweenEvictionRunsMillis=60000 spring.shardingsphere.datasource.ds1.minEvictableIdleTimeMillis=300000 spring.shardingsphere.datasource.ds1.validationQuery=SELECT 1 FROM DUAL spring.shardingsphere.datasource.ds1.testWhileIdle=true spring.shardingsphere.datasource.ds1.testOnBorrow=false spring.shardingsphere.datasource.ds1.testOnReturn=false spring.shardingsphere.datasource.ds1.poolPreparedStatements=true spring.shardingsphere.datasource.ds1.maxOpenPreparedStatements=20 spring.shardingsphere.datasource.ds1.connection-properties=druid.starggSql=ture;druid.stat.slowSqlMillis=5000 spring.shardingsphere.sharding.tables.t_user.actual-data-nodes=ds$->{0..1}.t_user_$->{0..1} spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.sharding-column=user_id spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.algorithm-expression=t_user_$->{user_id % 2} spring.shardingsphere.sharding.tables.t_user.database-strategy.inline.sharding-column=user_id spring.shardingsphere.sharding.tables.t_user.database-strategy.inline.algorithm-expression=ds$->{user_id % 2} spring.shardingsphere.sharding.tables.t_user.key-generator.column=user_id spring.shardingsphere.sharding.tables.t_user.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.t_user.key-generator.props.worker.id=123 spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..1} spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=user_id spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{user_id % 2} spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2} spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=123 spring.shardingsphere.sharding.binding-tables[0]=t_order,t_user # 配置公共表 spring.shardingsphere.sharding.broadcast-tables=t_config spring.shardingsphere.sharding.tables.t_config.key-generator.column=id spring.shardingsphere.sharding.tables.t_config.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.t_config.key-generator.props.worker.id=123 行表达式分片策略的测试用例 @Test public void testAddSomeUser() { for (int i = 0; i < 10; i++) { User dto = new User(); dto.setName("user_" + i); //增加用户 entityService.addUser(dto); } } @Test public void testSelectAllUser() { //增加用户 List all = entityService.selectAllUser(); System.out.println(all); } @Test public void testSelectAll() { entityService.selectAll(); } 行表达式分片策略的问题

行表达式分片策略(InlineShardingStrategy),在配置中使用 Groovy 表达式,提供对 SQL语句中的 = 和 IN 的分片操作支持,它只支持单分片健。

行表达式分片策略适用于做简单的分片算法,无需自定义分片算法,省去了繁琐的代码开发,是几种分片策略中最为简单的。

它的配置相当简洁,这种分片策略利用inline.algorithm-expression书写表达式。

比如:ds-$->{order_id % 2} 表示对 order_id 做取模计算,$ 是个通配符用来承接取模结果,最终计算出分库ds-0 ··· ds-n,整体来说比较简单。

spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..1} spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=user_id spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{user_id % 2}

优势:

相当简洁

行表达式分片策略的问题:

不能支持 范围分片

范围分片 用于处理含有 BETWEEN AND 、>,>=, =, =, =, 、=、 10000

参考的代码如下(以下代码,视频中有详细介绍):

public final class RouteInfinityRangeShardingAlgorithm implements RangeShardingAlgorithm { @Override public Collection doSharding(final Collection availableTargetNames, final RangeShardingValue shardingValue) { Collection result = new HashSet(); result.addAll(availableTargetNames); return result; } } Properties配置StandardShardingStrategy 实战

通过 Properties 配置来使用 StandardShardingStrategy

配置参数:

standard.sharding-column 分片键;

standard.precise-algorithm-class-name 精确分片算法类名;

standard.range-algorithm-class-name 范围分片算法类名

参数standard.precise-algorithm-class-name 说明:

standard.precise-algorithm-class-name 指向一个实现了PreciseShardingAlgorithm接口的java实现类,

io.shardingsphere.api.algorithm.sharding.standard.PreciseShardingAlgorithm

此java实现类提供按照 = 或者 IN 逻辑的精确分片

参数standard.range-algorithm-class-name 说明:

指向一个实现了 io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm接口的java类名,

此java实现类提供按照 Between 条件进行的范围分片。

示例: com.crazymaker.springcloud.message.core.PreciseShardingAlgorithm 参数补充说明:

StandardShardingStrategy的两大内嵌算法中:精确分片算法是必须提供的,而范围分片算法则是可选的。

配置实例 spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..1} #spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=user_id #spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{user_id % 2} spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.sharding-column=user_id spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.crazymaker.springcloud.sharding.jdbc.demo.core.TablePreciseShardingAlgorithmDemo spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=123 spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.crazymaker.springcloud.sharding.jdbc.demo.core.DsPreciseShardingAlgorithmDemo spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.sharding-column=user_id #spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id #spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2}

就写这么多,更加详细的内容,请参见视频

ComplexShardingStrategy复合分片策略实战 内联分片、标准分片 策略的不足:

只有一个分片建

问题: 多个分片键参与分片路由,咋整?

ComplexSharding分片策略

分片策略基本和上面的分片算法对应,包括:标准分片策略、复合分片策略、Hint分片策略、内联分片策略、不分片策略;

标准分片策略:对应StandardShardingStrategy类,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法,PreciseShardingAlgorithm是必须的,RangeShardingAlgorithm可选的;

public final class StandardShardingStrategy implements ShardingStrategy { private final String shardingColumn; private final PreciseShardingAlgorithm preciseShardingAlgorithm; private final RangeShardingAlgorithm rangeShardingAlgorithm; }

复合分片策略:对应ComplexShardingStrategy类,提供ComplexKeysShardingAlgorithm分片算法;

public final class ComplexShardingStrategy implements ShardingStrategy { @Getter private final Collection shardingColumns; private final ComplexKeysShardingAlgorithm shardingAlgorithm; }

可以发现支持多个分片键;

Hint分片策略:对应HintShardingStrategy类,通过 Hint 指定分片值而非从 SQL 中提取分片值的方式进行分片的策略;提供HintShardingAlgorithm分片算法;

public final class HintShardingStrategy implements ShardingStrategy { @Getter private final Collection shardingColumns; private final HintShardingAlgorithm shardingAlgorithm; }

内联分片策略:对应InlineShardingStrategy类,没有提供分片算法,路由规则通过表达式来实现;

不分片策略:对应NoneShardingStrategy类,不分片策略;

ComplexSharding分片策略配置类

在使用中我们并没有直接使用上面的分片策略类,ShardingSphere-JDBC分别提供了对应策略的配置类包括:

StandardShardingStrategyConfigurationComplexShardingStrategyConfigurationHintShardingStrategyConfigurationInlineShardingStrategyConfigurationNoneShardingStrategyConfiguration /** * Complex sharding strategy configuration. */ @Getter public final class ComplexShardingStrategyConfiguration implements ShardingStrategyConfiguration { private final String shardingColumns; private final ComplexKeysShardingAlgorithm shardingAlgorithm; public ComplexShardingStrategyConfiguration( final String shardingColumns, final ComplexKeysShardingAlgorithm shardingAlgorithm) { Preconditions.checkArgument(!Strings.isNullOrEmpty(shardingColumns), "ShardingColumns is required."); Preconditions.checkNotNull(shardingAlgorithm, "ShardingAlgorithm is required."); this.shardingColumns = shardingColumns; this.shardingAlgorithm = shardingAlgorithm; } } ComplexSharding分片算法

提供了抽象分片算法类:ShardingAlgorithm,根据类型又分为:精确分片算法、区间分片算法、复合分片算法以及Hint分片算法;

精确分片算法:对应PreciseShardingAlgorithm类,主要用于处理 = 和 IN的分片;区间分片算法:对应RangeShardingAlgorithm类,主要用于处理 BETWEEN AND, >, =, , =, SQL 路由 -> SQL 改写 -> SQL 执⾏ -> 结果归并 六步组成,一起瞅瞅每个步骤做了点什么。

img

SQL解析

接着语法解析会将拆分后的SQL转换为抽象语法树,通过对抽象语法树遍历,提炼出分片所需的上下文,

上下文包含查询字段信息(Field)、表信息(Table)、查询条件(Condition)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit)等,并标记出 SQL中有可能需要改写的位置。

例如,以下 SQL:

SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18 SQL 解析引擎

相对于其他编程语⾔,SQL 是⽐较简单的。不过,它依然是⼀⻔完善的编程语⾔,因此对 SQL 的语法进⾏解析,与解析其他编程语⾔(如:Java 语⾔、C 语⾔、Go 语⾔等)并⽆本质区别。

功能点

• 提供独立的 SQL 解析功能

• 可以非常方便的对语法规则进行扩充和修改 (使用了 ANTLR)

• 支持多种方言的 SQL 解析

数据库支持状态MySQL支持,完善PostgreSQL支持,完善SQLServer支持Oracle支持SQL92支持 历史

SQL 解析作为分库分表类产品的核心,其性能和兼容性是最重要的衡量指标。ShardingSphere 的 SQL 解 析器经历了 3 代产品的更新迭代。

第一代 SQL 解析器为了追求性能与快速实现,在 1.4.x 之前的版本使用 Druid 作为 SQL 解析器。经实际 测试,它的性能远超其它解析器。

第二代 SQL 解析器从 1.5.x 版本开始,ShardingSphere 采用完全自研的 SQL 解析引擎。由于目的不同, ShardingSphere 并不需要将 SQL 转为一颗完全的抽象语法树,也无需通过访问器模式进行二次遍历。它 采用对 SQL 半理解的方式,仅提炼数据分片需要关注的上下文,因此 SQL 解析的性能和兼容性得到了进 一步的提高。

第三代 SQL 解析器从 3.0.x 版本开始,尝试使用 ANTLR 作为 SQL 解析引擎的生成器,并采用 Visit 的方 式从 AST 中获取 SQL Statement。从 5.0.x 版本开始,解析引擎的架构已完成重构调整,同时通过将第一 次解析的得到的 AST 放入缓存,方便下次直接获取相同 SQL 的解析结果,来提高解析效率。

因此官方建 议用戶采用 PreparedStatement 这种 SQL 预编译的方式来提升性能。

抽象语法树

解析过程分为词法解析和语法解析。词法解析器⽤于将 SQL 拆解为不可再分的原⼦符号,称为 Token。并根据不同数据库⽅⾔所提供的字典,将其归类为关键字,表达式,字⾯量和操作符。再使⽤语法解析器将 SQL 转换为抽象语法树。

shardingjdbc 的SQL 路由原理

SQL 路由通过解析分片上下文,匹配到用户配置的分片策略,并生成路由路径。

简单点理解就是可以根据我们配置的分片策略计算出 SQL该在哪个库的哪个表中执行,

而SQL路由又根据有无分片健区分出 分片路由 和 广播路由。

img

有分⽚键的路由叫分片路由,细分为直接路由、标准路由和笛卡尔积路由这3种类型。

直接路由(暗示路由)

直接路由是通过使用 HintAPI 直接将 SQL路由到指定⾄库表的一种分⽚方式,而且直接路由可以⽤于分⽚键不在SQL中的场景,还可以执⾏包括⼦查询、⾃定义函数等复杂情况的任意SQL。

比如根据 t_order_id 字段为条件查询订单,此时希望在不修改SQL的前提下,加上 user_id作为分片条件就可以使用直接路由。

直接路由需要通过 Hint(使用 HintAPI 直接指定路由至库表)方式指定分片值,

不需要提取分片键值,并且 是只分库不分表的前提下,则可以避免 SQL 解析。

因此它的兼容性最好,可以执行包 括子查询、自定义函数等复杂情况的任意 SQL。直接路由还可以用于分片键不在 SQL 中的场景。例如,设 置用于数据库分片的值为 3

hintManager.setDatabaseShardingValue(3);

假如路由算法为 value % 2,当一个逻辑库 t_order 对应 2 个真实库 t_order_0 和 t_order_1 时, 路由后 SQL 将在 t_order_1 上执行。

标准路由

标准路由是最推荐也是最为常⽤的分⽚⽅式,它的适⽤范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。

当 SQL分片健的运算符为 = 时,路由结果将落⼊单库(表),路由策略返回的是单个的目标。

当分⽚运算符是BETWEEN 或IN 等范围时,路由结果则不⼀定落⼊唯⼀的库(表),因此⼀条逻辑SQL最终可能被拆分为多条⽤于执⾏的真实SQL。

如果按照 order_id 的奇数和偶数进行数据分片,一个单表查询的 SQL 如下:

SELECT * FROM t_order where t_order_id in (1,2)

SQL路由处理后

SELECT * FROM t_order_0 where t_order_id in (1,2) SELECT * FROM t_order_1 where t_order_id in (1,2)

绑定表的关联查询与单表查询复杂度和性能相当。

举例说明,如果一个包含绑定表的关联查询的 SQL 如 下:

SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_ id IN (1, 2);

那么路由的结果应为:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);

可以看到,SQL 拆分的数目与单表是一致的。

笛卡尔积路由

笛卡尔路由是由非绑定表之间的关联查询产生的,查询性能较低尽量避免走此路由模式。

笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需 要拆解为笛卡尔积组合执行。

如果上个示例中的 SQL 并未配置绑定表关系,那么路由的结果应为:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);

笛卡尔路由查询性能较低,需谨慎使用。

广播路由

无分⽚键的路由又叫做广播路由,可以划分为全库表路由、全库路由、 全实例路由、单播路由和阻断路由这 5种类型。

全库表路由

全库表路由针对的是数据库 DQL和 DML,以及 DDL等操作,

当我们执行一条逻辑表 t_order SQL时,在所有分片库中对应的真实表 t_order_0 ··· t_order_n 内逐一执行。

全库路由

全库路由主要是对数据库层面的操作,比如数据库 SET 类型的数据库管理命令,以及 TCL 这样的事务控制语句。

对逻辑库设置 autocommit 属性后,所有对应的真实库中都执行该命令。

SET autocommit=0;

全实例路由

全实例路由是针对数据库实例的 DCL 操作(设置或更改数据库用户或角色权限),比如:创建一个用户 order ,这个命令将在所有的真实库实例中执行,以此确保 order 用户可以正常访问每一个数据库实例。

CREATE USER [email protected] identified BY '程序员内点事';

单播路由

单播路由用来获取某一真实表信息,比如获得表的描述信息:

DESCRIBE t_order;

t_order 的真实表是 t_order_0 ···· t_order_n,他们的描述结构相完全同,我们只需在任意的真实表执行一次就可以。

阻断路由

⽤来屏蔽SQL对数据库的操作,例如:

USE order_db;

这个命令不会在真实数据库中执⾏,因为 ShardingSphere 采⽤的是逻辑 Schema(数据库的组织和结构) ⽅式,所以无需将切换数据库的命令发送⾄真实数据库中。

SQL 改写

将基于逻辑表开发的SQL改写成可以在真实数据库中可以正确执行的语句。比如查询 t_order 订单表,我们实际开发中 SQL是按逻辑表 t_order 写的。

SELECT * FROM t_order

但分库分表以后真实数据库中 t_order 表就不存在了,而是被拆分成多个子表 t_order_n 分散在不同的数据库内,还按原SQL执行显然是行不通的,这时需要将分表配置中的逻辑表名称改写为路由之后所获取的真实表名称。

SELECT * FROM t_order_n SQL执行

将路由和改写后的真实 SQL 安全且高效发送到底层数据源执行。但这个过程并不是简单的将 SQL 通过JDBC 直接发送至数据源执行,而是平衡数据源连接创建以及内存占用所产生的消耗,它会自动化的平衡资源控制与执行效率。

结果归并

将从各个数据节点获取的多数据结果集,合并成一个大的结果集并正确的返回至请求客户端,称为结果归并。

而我们SQL中的排序、分组、分页和聚合等语法,均是在归并后的结果集上进行操作的。

问题:分库的join怎么解决

在这里插入图片描述

首先看是那种 join。

JOIN的含大致分为左连接,右连接,内连接,外连接,自然连接。

img

笛卡尔积

JOIN首先要理解笛卡尔积。

笛卡尔积就是将A表的每一条记录与B表的每一条记录强行拼在一起。所以,如果A表有n条记录,B表有m条记录,笛卡尔积产生的结果就会产生n*m条记录。

内连接:INNER JOIN

内连接INNER JOIN是最常用的连接操作。从数学的角度讲就是求两个表的交集,从笛卡尔积的角度讲就是从笛卡尔积中挑出ON子句条件成立的记录。

左连接:LEFT JOIN

左连接LEFT JOIN的含义就是求两个表的交集外加左表剩下的数据。

从笛卡尔积的角度讲,就是先从笛卡尔积中挑出ON子句条件成立的记录,然后加上左表中剩余的记录

右连接:RIGHT JOIN

同理右连接RIGHT JOIN就是求两个表的交集外加右表剩下的数据。

从笛卡尔积的角度描述,右连接就是从笛卡尔积中挑出ON子句条件成立的记录,然后加上右表中剩余的记录

常用的是左外连接 /** * 根据用户查询 order * * @return */ @Query(nativeQuery = true, value = "SELECT a.* FROM `t_order` a left join `t_user` b on a.user_id=b.user_id where a.user_id=?1") List selectOrderOfUserId(long userId); /** * 根据用户查询 order * * @return */ @Query(nativeQuery = true, value = "SELECT a.* FROM `t_order` a left join `t_user` b on a.user_id=b.user_id ") List selectOrderOfUser(); 回答:分库的join怎么解决:

就是一般用左外连接,

两个表用相同的分片建,

并且进行表绑定,防止产生数据源实例内的笛卡尔积路由。

使得join的时候,一个分片内部的数据,在分片内部完成 join操作,再由shardingjdbc完成 结果的归并。

从而得到最终的结果。

连环问:分库分表后,模糊条件查询怎么处理?

上面提到的都是条件中有sharding column的SQL执行。

但是,总有一些查询条件是不包含sharding column的,同时,我们也不可能为了这些请求量并不高的查询,无限制的冗余分库分表。

那么这些查询条件中没有sharding column的SQL怎么处理?

而在移动互联网时代,海量的用户每天产生海量的数量,这些海量数据远不是一张表能Hold住的。

比如

用户表:支付宝8亿,微信10亿。CITIC对公140万,对私8700万。

订单表:美团每天几千万,淘宝历史订单百亿、千亿。

目前绝大部分公司的核心数据都是:以RDBMS存储为主,NoSQL/NewSQL存储为辅!

RDBMS互联网公司又以MySQL为主

NoSQL比较具有代表性的是MongoDB,es

NewSQL比较具有代表性的是TiDB。

但是,MySQL单表可以存储10亿级数据,具体的原因,前面视频已经具体分析

但是,行业认可的,MySQL单表容量在1KW以下, 所以必然要分库分表

回顾一下,sharding 核心的步骤是:

SQL解析,重写,路由,执行,结果归并。

以sharding-jdbc为例,有多少个分库分表,就要并发路由到多少个分库分表中执行,然后对结果进行合并。

更有甚者,尤其是有些模糊条件查询,或者上十个条件筛选。

这种条件查询相对于有sharding column的条件查询性能很明显会下降很多。

多sharding column最好不要使用,建议采用 单sharding column + es + HBase的索引与存储隔离的架构。

索引与存储隔离的架构

例如有sharding column的查询走分库分表,一些模糊查询,或者多个不固定条件筛选则走es,海量存储则交给HBase。

在这里插入图片描述

HBase特点:

所有字段的全量数据保存到HBase中,Hadoop体系下的HBase存储能力是海量的,

rowkey查询速度快,快如闪电(可以优化到50Wqps甚至更高)。

es特点:

es的多条件检索能力非常强大。可能参与条件检索的字段索引到ES中。

这个方案把es和HBase的优点发挥的淋漓尽致,同时又规避了它们的缺点,可以说是一个扬长避免的最佳实践。

这就是经典的ES+HBase组合方案,即索引与数据存储隔离的方案。

它们之间的交互大概是这样的:

先根据用户输入的条件去es查询获取符合过滤条件的rowkey值,

然后用rowkey值去HBase查询

交互图如下所示:

在这里插入图片描述

对于海量数据,且有一定的并发量的分库分表,绝不是引入某一个分库分表中间件就能解决问题,而是一项系统的工程。

需要分析整个表相关的业务,让合适的中间件做它最擅长的事情。

例如有sharding column的查询走分库分表,

一些模糊查询,或者多个不固定条件筛选则走es,海量存储则交给HBase。

biglog同步保障数据一致性的架构

在很多业务情况下,我们都会在系统中加入redis缓存做查询优化, 使用es 做全文检索。

如果数据库数据发生更新,这时候就需要在业务代码中写一段同步更新redis的代码。

这种数据同步的代码跟业务代码糅合在一起会不太优雅,能不能把这些数据同步的代码抽出来形成一个独立的模块呢,答案是可以的。

在这里插入图片描述

数据的冷热分离

做了这么多事情后,后面还会有很多的工作要做,比如数据同步的一致性问题,

还有运行一段时间后,某些表的数据量慢慢达到单表瓶颈,这时候还需要做冷数据迁移。

问题:广播表是不是公共表

在这里插入图片描述

可以这么理解。

广播表,就是更新操作,覆盖所有分片, 查询操作,查一个分片即可。

分布式主键

数据分⽚后,不同数据节点⽣成全局唯⼀主键是非常棘⼿的问题,

同⼀个逻辑表(t_order)内的不同真实表(t_order_n)之间的⾃增键由于无法互相感知而产⽣重复主键。

尽管可通过设置⾃增主键 初始值 和 步长 的⽅式避免 ID 碰撞,但这样会使维护成本加大,乏完整性和可扩展性。

如果后去需要增加分片表的数量,要逐一修改分片表的步长,运维成本非常高,所以不建议这种方式。

为了让上手更加简单,ApacheShardingSphere 内置了 UUID、SNOWFLAKE 两种分布式主键⽣成器,

默认使⽤雪花算法(snowflake)⽣成 64bit 的⻓整型数据。

不仅如此它还抽离出分布式主键⽣成器的接口,⽅便我们实现⾃定义的⾃增主键⽣成算法。

实现动机

传统数据库软件开发中,主键⾃动⽣成技术是基本需求。而各个数据库对于该需求也提供了相应的⽀持,⽐如 MySQL 的⾃增键,Oracle 的⾃增序列等。

数据分⽚后,不同数据节点⽣成全局唯⼀主键是⾮常棘⼿的问题。

同⼀个逻辑表内的不同实际表之间的⾃增键由于⽆法互相感知而产⽣重复主键。

虽然可通过约束⾃增主键初始值和步⻓的⽅式避免碰撞,但需引⼊额外的运维规则,使解决⽅案缺乏完整性和可扩展性。

⽬前有许多第三⽅解决⽅案可以完美解决这个问题,如 UUID 等依靠特定算法⾃⽣成不重复键,或者通过引⼊主键⽣成服务等。

为了⽅⽤⼾使⽤、满⾜不同⽤⼾不同使⽤场景的需求,Apache ShardingSphere不仅提供了内置的分布式主键⽣成器,

例如 UUID 、SNOWFLAKE,

还抽离出分布式主键⽣成器的接口,⽅便⽤⼾⾃⾏实现⾃定义的⾃增主键⽣成器。

内置的主键⽣成器

UUID

采⽤ UUID.randomUUID() 的⽅式产⽣分布式主键。

SNOWFLAKE

在分⽚规则配置模块可配置每个表的主键⽣成策略,默认使⽤雪花算法(snowfl ake )⽣成 64bit 的⻓整型数据。 雪花算法是由 Twitter 公布的分布式主键⽣成算法,它能够保证不同进程主键的不重复性,以及相同进程主键的有序性。

实现原理

在同⼀个进程中,它⾸先是通过时间位保证不重复,如果时间相同则是通过序列位保证。同时由于时间位是单调递增的,且各个服务器如果⼤体做了时间同步,那么⽣成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插⼊的⾼效性。

例如 MySQL 的 Innodb 存储引擎的主键。 使⽤雪花算法⽣成的主键,⼆进制表⽰形式包含 4 部分,从⾼位到低位分表为:

1bit 符号位、41bit 时间戳位、10bit ⼯作进程位以及12bit 序列号位。 符号位(1bit)

预留的符号位,恒为零。

时间戳位(41bit)

41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,⼀年所使⽤的毫秒数是:365 * 24 * 60 * 60 *1000。

通过计算可知: Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L); 结果约等于 69.73 年。

Apache ShardingSphere 的雪花算法的时间纪元从 2016 年 11 ⽉ 1 ⽇零点开始,可以使⽤到 2086 年,

相信能满⾜绝⼤部分系统的要求。

⼯作进程位(10bit)

该标志在 Java 进程内是唯⼀的,如果是分布式应⽤部署应保证每个⼯作进程的 id 是不同的。该值默认为0,可通过属性设置。

序列号位(12bit)

该序列是⽤来在同⼀个毫秒内⽣成不同的 ID。

如果在这个毫秒内⽣成的数量超过 4096 (2 的 12 次幂),那么⽣成器会等待到下个毫秒继续⽣成。 雪花算法主键的详细结构⻅下图。

在这里插入图片描述

时钟回拨

服务器时钟回拨会导致产⽣重复序列,因此默认分布式主键⽣成器提供了⼀个最⼤容忍的时钟回拨毫秒数。

如果时钟回拨的时间超过最⼤容忍的毫秒数阈值,则程序报错;

如果在可容忍的范围内,默认分布式主键⽣成器会等待时钟同步到最后⼀次主键⽣成的时间后再继续⼯作。

最⼤容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。

步长不均衡

导致数据倾斜

Shardingjdbc SPI与自定义主键 Java SPI是什么

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

系统设计的各个抽象,往往有很多不同的实现方案,

在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。

一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。

为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。

有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

整体机制图如下:

在这里插入图片描述

这里写图片描述

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。所以SPI的核心思想就是解耦。

Java SPI的约定 Java SPI使用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

比较常见的例子:

数据库驱动加载接口实现类的加载 JDBC加载不同类型数据库的驱动日志门面接口实现类加载 SLF4J加载不同提供商的日志实现类Spring Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等Dubbo Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口 Java SPI使用约定

要使用Java SPI,需要遵循如下约定:

1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;2、接口实现类所在的jar包放在主程序的classpath中;3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;4、SPI的实现类必须携带一个不带参数的构造方法; JavaSPI实战

首先,我们需要定义一个接口,SPI Service 接口

package com.crazymaker.springcloud.sharding.jdbc.demo.generator; public interface IdGenerator { /** * Next id long. * * @return the nextId */ Long nextId(); }

然后,定义两个实现类,也可以定义两个实现类

// 单机版 AtomicLong 类型的ID生成器 @Data public class AtomicLongShardingKeyGeneratorSPIDemo implements IdGenerator { private AtomicLong atomicLong = new AtomicLong(0); @Override public Long nextId() { return atomicLong.incrementAndGet(); } }

最后,要在ClassPath路径下配置添加一个文件:

文件名字是接口的全限定类名内容是实现类的全限定类名多个实现类用换行符分隔。

SPI配置文件位置,文件路径如下:

在这里插入图片描述

内容就是实现类的全限定类名:

com.crazymaker.springcloud.sharding.jdbc.demo.generator.AtomicLongShardingKeyGeneratorSPIDemo

测试

然后我们就可以通过ServiceLoader.load或者Service.providers方法拿到实现类的实例。

Service.providers包位于sun.misc.Service,ServiceLoader.load包位于java.util.ServiceLoader。 @Test public void testGenIdByProvider() { Iterator providers = Service.providers(IdGenerator.class); while (providers.hasNext()) { IdGenerator generator = providers.next(); for (int i = 0; i


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有