一.简单介绍
1.什么是分库分表?
分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
分库分表包括分库和分表两个部分,在生产中通常包括四种方式:
- 垂直分库
- 垂直分表
- 水平分库
- 水平分表
2.垂直分库
垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。
它带来的提升是:
- 解决业务层面的耦合,业务清晰
- 能对不同业务的数据进行分级管理、维护、监控、扩展等
- 高并发场景下,垂直分库一定程度的提升 IO、数据库连接数、降低单机硬件资源的瓶颈
垂直分库通过将表按业务分类,然后分布在不同数据库,并且可以将这些数据库部署在不同服务器上,从而达到多个服务器共同分摊压力的效果,但是依然没有解决单表数据量过大的问题。
3.垂直分表
垂直分表定义:将一个表按照字段分成多表,每个表存储其中一部分字段。
它带来的提升是:
- 为了避免 IO 争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响
- 充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累。
一般来说,某业务实体中的各个数据项的访问频次是不一样的,部分数据项可能是占用存储空间比较大的 BLOB 或是 TEXT。例如上例中的商品描述。所以,当表数据量很大时,可以将表按字段切开,将热门字段、冷门字段分开放置在不同库中,这些库可以放在不同的存储设备上,避免 IO 争抢。垂直切分带来的性能提升主要集中在热门数据的操作效率上,而且磁盘争用情况减少。
4.水平分库
水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。
它带来的提升是:
- 解决了单库大数据,高并发的性能瓶颈。
- 提高了系统的稳定性及可用性。
5.水平分表
水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中。
它带来的提升是:
- 优化单一表数据量过大而产生的性能问题
- 避免 IO 争抢并减少锁表的几率
库内的水平分表,解决了单一表数据量过大的问题,分出来的小表中只包含一部分数据,从而使得单个表的数据量变小,提高检索性能。
6.分库分表总结
垂直分表:
可以把一个宽表的字段按访问频次、是否是大字段的原则拆分为多个表,这样既能使业务清晰,还能提升部分性能。拆分后,尽量从业务角度避免联查,否则性能方面将得不偿失。垂直分库:
可以把多个表按业务耦合松紧归类,分别存放在不同的库,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题。水平分库:
可以把一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能。它不仅需要解决跨库带来的所有复杂问题,还要解决数据路由的问题(数据路由问题后边介绍)。水平分表:
可以把一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化。
一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。
7.分库分表带来的问题
- 事务一致性问题
- 跨节点关联查询
- 跨节点分页、排序函数
- 主键避重
- 公共表
二.Sharding-JDBC
1.什么是 Sharding-JDBC
Sharding-JDBC是当当网研发的开源分布式数据库中间件,从 3.0 开始 Sharding-JDBC 被包含在 Sharding-Sphere 中,之后该项目进入进入 Apache 孵化器,4.0 版本之后的版本为 Apache 版本。
Sharding-JDBC,定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
Sharding-JDBC 的核心功能为数据分片和读写分离
,通过 Sharding-JDBC,应用可以透明的使用 jdbc 访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
- 适用于任何基于 Java 的 ORM 框架,如: Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
- 基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
- 支持任意实现 JDBC 规范的数据库。目前支持 MySQL,Oracle,SQLServer 和 PostgreSQL。
2.架构
下图展示了 Sharding-Jdbc 的工作方式,使用 Sharding-Jdbc 前需要人工对数据库进行分库分表,在应用程序中加入 Sharding-Jdbc 的 Jar 包,应用程序通过 Sharding-Jdbc 操作分库分表后的数据库和数据表,由于 Sharding-Jdbc 是对 Jdbc 驱动的增强,使用 Sharding-Jdbc 就像使用 Jdbc 驱动一样,在应用程序中是无需指定具体要操作的分库和分表的。
3.与 jdbc 性能对比
- 性能损耗测试:服务器资源充足、并发数相同,比较 JDBC 和 Sharding-JDBC 性能损耗,Sharding-JDBC 相对 JDBC 损耗不超过 7%。
- 性能对比测试:服务器资源使用到极限,相同的场景 JDBC 与 Sharding-JDBC 的吞吐量相当。
- 性能对比测试:服务器资源使用到极限,Sharding-JDBC 采用分库分表后,Sharding-JDBC 吞吐量较 JDBC 不分表有接近 2 倍的提升。
4.流程分析
通过日志分析,Sharding-JDBC 在拿到用户要执行的 sql 之后干了哪些事儿:
- 解析 sql,获取片键值,在本例中是 order_id。
- Sharding-JDBC 通过规则配置 torder$ -> {order_id % 2 + 1},知道了当 order_id 为偶数时,应该往 t_order_1 表插数据,为奇数时,往 t_order_2 插数据。
- 于是 Sharding-JDBC 根据 order_id 的值改写 sql 语句,改写后的 SQL 语句是真实所要执行的 SQL 语句。
- 执行改写后的真实 sql 语句。
- 将所有真正执行 sql 的结果进行汇总合并,返回。
5.基本概念
逻辑表:
水平拆分的数据表的总称。例:订单数据表根据主键尾数拆分为 10 张表,分别是 t_order_0、t_order_1 到 t_order_9 ,他们的逻辑表名为 t_order 。
真实表:
在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9 。
数据节点:
数据分片的最小物理单元。由数据源名称和数据表组成,例: ds_0.t_order_0 。
绑定表:
指分片规则一致的主表和子表。例如: t_order 表和 t_order_item 表,均按照 order_id 分片,绑定表之间的分区键完全相同,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
广播表:
指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
分片键:
用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL 中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingJdbc 也支持根据多个字段进行分片。
分片算法:
通过分片算法将数据分片,支持通过 = 、 BETWEEN 和 IN 分片。分片算法需要应用方开发者自行实现,可实现的灵活度非常高。包括:精确分片算法 、范围分片算法 ,复合分片算法
等。
- where order_id = ? 将采用精确分片算法,
- where order_id in (?,?,?)将采用精确分片算法,
- where order_id BETWEEN ? and ? 将采用范围分片算法,
- 复合分片算法用于分片键有多个复杂情况。
分片策略:
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法
,也就是分片策略。内置的分片策略大致可分为尾数取模、哈希、范围、标签、时间等。由用户方配置的分片策略则更加灵活,常用的使用行表达式配置分片策略,它采用 Groovy 表达式表示,如:tuser$->{u_id % 8}表示 t_user 表根据 u_id 模 8,而分成 8 张表,表名称为 t_user_0 到 t_user_7。
自增主键生成策略:
通过在客户端生成自增主键替换以数据库原生自增主键的方式,做到分布式主键无重复。
6.SQL 解析
当 Sharding-JDBC 接受到一条 SQL 语句时,会陆续执行 SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 => 结果归并
,最终返回执行结果。
SQL 解析过程分为词法解析和语法解析。 词法解析器用于将 SQL 拆解为不可再分的原子符号,称为 Token。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将 SQL 转换为抽象语法树。
例如:
SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18
为了便于理解,抽象语法树中的关键字的 Token 用绿色表示,变量的 Token 用红色表示,灰色表示需要进一步拆分。最后,通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要 SQL 改写(后边介绍)的位置。 供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。
7.SQL 路由
SQL 路由就是把针对逻辑表的数据操作映射到对数据结点操作的过程。
根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的 SQL,根据分片键操作符不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是 IN)和范围路由(分片键的操作符是 BETWEEN),不携带分片键的 SQL 则采用广播路由。根据分片键进行路由的场景可分为直接路由、标准路由、笛卡尔路由等。
标准路由:
标准路由是 Sharding-Jdbc 最为推荐使用的分片方式,它的适用范围是不包含关联查询或仅包含绑定表之间关联查询的 SQL。 当分片运算符是等于号时,路由结果将落入单库(表),当分片运算符是 BETWEEN 或 IN 时,则路由结果不一定落入唯一的库(表),因此一条逻辑 SQL 最终可能被拆分为多条用于执行的真实 SQL。
笛卡尔路由:
笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛卡尔积组合执行。
全库表路由:
对于不携带分片键的 SQL,则采取广播路由的方式。根据 SQL 类型又可以划分为全库表路由、全库路由、全实例路由、单播路由和阻断路由这 5 种类型。其中全库表路由用于处理对数据库中与其逻辑表相关的所有真实表的操作,主要包括不带分片键的 DQL(数据查询)和 DML(数据操纵),以及 DDL(数据定义)等。
8.SQL 改写
工程师面向逻辑表书写的 SQL,并不能够直接在真实的数据库中执行,SQL 改写用于将逻辑 SQL 改写为在真实数据库中可以正确执行的 SQL。
由于原始 SQL 中并不包含需要在结果归并中需要获取的 user_id,因此需要对 SQL 进行补列改写。补列之后的 SQL 是:
SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;
9.SQL 执行
Sharding-JDBC 采用一套自动化的执行引擎,负责将路由和改写完成之后的真实 SQL 安全且高效发送到底层数据源执行。 它不是简单地将 SQL 通过 JDBC 直接发送至数据源执行;也并非直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是自动化的平衡资源控制与执行效率,他能在以下两种模式自适应切换:
内存限制模式:
使用此模式的前提是,Sharding-JDBC 对一次操作所耗费的数据库连接数量不做限制。 如果实际执行的 SQL 需要对某数据库实例中的 200 张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。
连接限制模式:
使用此模式的前提是,Sharding-JDBC 严格控制对一次操作所耗费的数据库连接数量。 如果实际执行的 SQL 需要对某数据库实例中的 200 张表做操作,那么只会创建唯一的数据库连接,并对其 200 张表串行处理。 如果一次操作中的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。
内存限制模式适用于 OLAP 操作,可以通过放宽对数据库连接的限制提升系统吞吐量; 连接限制模式适用于 OLTP 操作,OLTP 通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用,是明智的选择。
10.结果归并
将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。Sharding-JDBC 支持的结果归并从功能上可分为遍历、排序、分组、分页和聚合5 种类型,它们是组合而非互斥的关系。
结果归并从结构划分可分为流式归并、内存归并和装饰者归并
。流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。
内存归并很容易理解,他是将所有分片结果集的数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回。
流式归并是指每一次从数据库结果集中获取到的数据,都能够通过游标逐条获取的方式返回正确的单条数据,它与数据库原生的返回结果集的方式最为契合。
装饰者归并是对所有的结果集归并进行统一的功能增强,比如归并时需要聚合 SUM 前,在进行聚合计算前,都会通过内存归并或流式归并查询出结果集。因此,聚合归并是在之前介绍的归并类型之上追加的归并能力,即装饰者模式。
三.分库分表
1.水平分库
水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。
# 定义多个数据源
spring.shardingsphere.datasource.names = m1,m2
spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/order_db_1?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = root
spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url = jdbc:mysql://localhost:3306/order_db_2?useUnicode=true
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = root
...
# 分库策略,以user_id为分片键,分片策略为user_id % 2 + 1,user_id为偶数操作m1数据源,否则操作m2。
spring.shardingsphere.sharding.tables.t_order.database‐strategy.inline.sharding‐column = user_id
spring.shardingsphere.sharding.tables.t_order.database‐strategy.inline.algorithm‐expression = m$‐>{user_id % 2 + 1}
分库逻辑
#分库策略,如何将一个逻辑表映射到多个数据源
spring.shardingsphere.sharding.tables.<逻辑表名称>.database‐strategy.<分片策略>.<分片策略属性名>= #分片策略属性值
#分表策略,如何将一个逻辑表映射为多个实际表
spring.shardingsphere.sharding.tables.<逻辑表名称>.table‐strategy.<分片策略>.<分片策略属性名>= #分片策略属性值
2.分片策略
Sharding-JDBC 支持以下几种分片策略:不管理分库还是分表,策略基本一样。
standard
:标准分片策略,对应 StandardShardingStrategy。提供对 SQL 语句中的=, IN 和 BETWEEN AND 的分片操作支持。StandardShardingStrategy 只支持单分片键,提供 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。PreciseShardingAlgorithm 是必选的,用于处理=和 IN 的分片。RangeShardingAlgorithm 是可选的,用于处理 BETWEEN AND 分片,如果不配置 RangeShardingAlgorithm,SQL 中的 BETWEEN AND 将按照全库路由处理。complex
:复合分片策略,对应 ComplexShardingStrategy。复合分片策略。提供对 SQL 语句中的=, IN 和 BETWEEN AND 的分片操作支持。ComplexShardingStrategy 支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。inline
:行表达式分片策略,对应 InlineShardingStrategy。使用 Groovy 的表达式,提供对 SQL 语句中的=和 IN 的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的 Java 代码开发,如:tuser$->{u_id % 8} 表示 t_user 表根据 u_id 模 8,而分成 8 张表,表名称为 t_user_0 到 t_user_7 。hint
:Hint 分片策略,对应 HintShardingStrategy。通过 Hint 而非 SQL 解析的方式分片的策略。对于分片字段非 SQL 决定,而由其他外置条件决定的场景,可使用 SQL Hint 灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint 支持通过 Java API 和 SQL 注释(待实现)两种方式使用。none
:不分片策略,对应 NoneShardingStrategy。不分片的策略。
3.垂直分库
垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。
# 新增m0数据源,对应user_db
spring.shardingsphere.datasource.names = m0,m1,m2
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3306/user_db?useUnicode=true
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = root
# t_user分表策略,固定分配至m0的t_user真实表
spring.shardingsphere.sharding.tables.t_user.actual‐data‐nodes = m$‐>{0}.t_user
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
4.公共表
公共表属于系统中数据量较小,变动少,而且属于高频联合查询的依赖表。参数表、数据字典表等属于此类型。可以将这类表在每个数据库都保存一份
,所有更新操作都同时发送到所有分库执行。接下来看一下如何使用 Sharding-JDBC 实现公共表。
# 指定t_dict为公共表
spring.shardingsphere.sharding.broadcast‐tables=t_dict
5.公共表更新
要确保共享表的数据在多个分片之间保持一致,你可以采用以下方法:
- 定时数据同步:定期将共享表的数据从一个分片同步到其他分片,以保持数据一致性。这可以通过 ETL(Extract, Transform, Load)作业或者数据库复制来完成。
- 触发器或消息队列:当一个分片上的共享表数据发生变化时,可以使用触发器或消息队列来通知其他分片,以便它们也更新相应的数据。
6.读写分离
通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。
读写分离的数据节点中的数据内容是一致的,而水平分片的每个数据节点的数据内容却并不相同。将水平分片和读写分离联合使用,能够更加有效的提升系统的性能。
Sharding-JDBC 读写分离则是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库。它提供透明化读写分离,让使用方尽量像使用一个数据库一样使用主从数据库集群。
Sharding-JDBC 提供一主多从的读写分离配置,可独立使用,也可配合分库分表使用,同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。Sharding-JDBC 不提供主从数据库的数据同步功能,需要采用其他机制支持。
配置文件:
# 增加数据源s0,使用上面主从同步配置的从库。
spring.shardingsphere.datasource.names = m0,m1,m2,s0
...
spring.shardingsphere.datasource.s0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s0.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s0.url = jdbc:mysql://localhost:3307/user_db?useUnicode=true
spring.shardingsphere.datasource.s0.username = root
spring.shardingsphere.datasource.s0.password = root
# 主库从库逻辑数据源定义 ds0为user_db
spring.shardingsphere.sharding.master‐slave‐rules.ds0.master‐data‐source‐name=m0
spring.shardingsphere.sharding.master‐slave‐rules.ds0.slave‐data‐source‐names=s0
# t_user分表策略,固定分配至ds0的t_user真实表
spring.shardingsphere.sharding.tables.t_user.actual‐data‐nodes = ds0.t_user
7.分库分表数据聚合
在使用 sharding-jdbc 进行分库分表的时候,查询数据在不同的库和表中,数据是如何聚合整理的?底层的原理是什么?
在使用 Sharding-JDBC 进行分库分表时,查询数据在不同的库和表中,Sharding-JDBC 会根据分片规则将查询语句分发到对应的库和表中进行查询,并将结果进行聚合整理
,最终返回给用户。
Sharding-JDBC 的底层原理是通过对 SQL 语句进行解析,获取到对应的表和字段信息,然后根据分片规则将查询语句分发到对应的库和表中进行查询。在查询完成后,Sharding-JDBC 会将查询结果进行聚合整理,并根据分片规则将数据进行合并,最终返回给用户。
具体来说,Sharding-JDBC 会根据分片规则将查询条件拆分成多个子条件,然后将这些子条件分发到对应的库和表中进行查询。在查询完成后,Sharding-JDBC 会将查询结果进行聚合整理,根据分片规则将数据进行合并,并将结果返回给用户。
总的来说,Sharding-JDBC 通过分片规则将查询语句分发到对应的库和表中进行查询,并将结果进行聚合整理,最终返回给用户,从而实现了分库分表的功能。
Sharding-JDBC 支持多种分片规则,包括按照范围、按照取模、按照哈希等方式进行分片。不同的分片规则对应不同的分片策略,例如按照范围进行分片的策略是 RangeShardingStrategy,按照取模进行分片的策略是 InlineShardingStrategy,按照哈希进行分片的策略是 HintShardingStrategy 等。
在使用 Sharding-JDBC 进行分库分表时,需要配置对应的分片规则和分片策略,并将其与数据源进行绑定。这样,当用户进行查询时,Sharding-JDBC 就会根据分片规则将查询语句分发到对应的库和表中进行查询,并将结果进行聚合整理,最终返回给用户。
需要注意的是,分库分表虽然可以提高数据库的性能和扩展性,但也会带来一些问题,例如跨库事务、分布式锁等问题需要额外处理。因此,在使用 Sharding-JDBC 进行分库分表时,需要仔细考虑业务需求和实际情况,避免出现问题。
8.分库分表跨库事务是如何处理的
在分库分表的场景下,跨库事务是一个比较复杂的问题。因为不同的事务可能会涉及到多个库和多个表,需要保证所有操作都是原子性的,即要么全部成功,要么全部失败。
Sharding-JDBC 提供了两种跨库事务的解决方案:XA 事务和基于 TCC(Try-Confirm-Cancel)的事务。
XA 事务:
XA 事务是一种分布式事务协议,可以保证多个数据库之间的事务一致性。在使用 XA 事务时,Sharding-JDBC 会将所有涉及到的数据库都加入到同一个 XA 事务中,从而保证所有操作的原子性。
但是,XA 事务的性能比较差,会影响整个系统的性能。而且,XA 事务需要数据库支持,不是所有数据库都支持 XA 事务。
TCC 事务:
TCC 事务是一种基于补偿机制的分布式事务解决方案。在使用 TCC 事务时,Sharding-JDBC 会将整个事务拆分成三个阶段:Try 阶段、Confirm 阶段和 Cancel 阶段。
在 Try 阶段,Sharding-JDBC 会尝试执行所有操作,但不会立即提交事务。如果所有操作都执行成功,就进入 Confirm 阶段,将所有操作提交到数据库中。如果有任何一个操作执行失败,就进入 Cancel 阶段,回滚所有操作。
TCC 事务相对于 XA 事务来说,性能更好,而且不需要数据库支持。但是,TCC 事务的实现比较复杂,需要开发人员自己实现补偿机制,而且可能会出现数据不一致的情况。
总的来说,分库分表的跨库事务是一个比较复杂的问题,需要根据实际情况选择合适的解决方案。如果数据库支持 XA 事务,可以选择使用 XA 事务;如果需要更好的性能,可以选择使用 TCC 事务。
9.分库分表总结
- 分库分表方式:垂直分表、垂直分库、水平分库、水平分表
- 分库分表带来问题:由于数据分散在多个数据库,服务器导致了事务一致性问题、跨节点 join 问题、跨节点分页、排序、函数,主键需要全局唯一,公共表。
- Sharding-JDBC 基础概念:逻辑表,真实表,数据节点,绑定表,广播表,分片键,分片算法,分片策略,主键生成策略
- Sharding-JDBC 核心功能:数据分片,读写分离
- Sharding-JDBC 执行流程:SQL 解析 => 查询优化 => SQL 路由 => SQL 改写 => SQL 执行 => 结果归并
- 最佳实践:系统在设计之初就应该对业务数据的耦合松紧进行考量,从而进行垂直分库、垂直分表,使数据层架构清晰明了。若非必要,无需进行水平切分,应先从缓存技术着手降低对数据库的访问压力。如果缓存使用过后,数据库访问量还是非常大,可以考虑数据库读、写分离原则。若当前数据库压力依然大,且业务数据持续增长无法估量,最后可考虑水平分库、分表,单表拆分数据控制在 1000 万以内。
四.Groovy
1.什么是 Groovy
Groovy 是一种基于 Java 的动态语言,支持 Java 语言的所有特性,并且在语法和功能上进行了扩展和增强。在 Groovy 中,表达式是指一个或多个值、变量、操作符和函数调用组成的语句,能够产生一个值或结果。
2.Groovy 类型
Groovy 支持各种类型的表达式,包括算术表达式、逻辑表达式、关系表达式、位运算表达式、三元表达式、正则表达式、闭包表达式等等。
3.Groovy 示例
下面是一些常见的 Groovy 表达式示例:
- 算术表达式:支持加、减、乘、除、取模等运算符,例如:
2 + 3 * 4
。 - 逻辑表达式:支持与、或、非等运算符,例如:
a && b || !c
。 - 关系表达式:支持等于、不等于、大于、小于、大于等于、小于等于等运算符,例如:
x == y
。 - 位运算表达式:支持按位与、按位或、按位异或、左移、右移等运算符,例如:
a & b
。 - 三元表达式:支持类似于 Java 中的三元运算符,例如:
x > y ? x : y
。 - 正则表达式:支持使用正则表达式进行字符串匹配和替换,例如:
"hello world".replaceAll(/world/, "Groovy")
。 - 闭包表达式:支持定义匿名函数,例如:
{ a, b -> a + b }
。
以上只是 Groovy 表达式的一些示例,Groovy 还支持更多的表达式类型和功能。
4.分库分表中应用
Sharding-JDBC 是一款基于 JDBC 的分库分表中间件,可以帮助 Java 应用快速地实现分库分表功能。在 Sharding-JDBC 中,Groovy 被广泛应用于 SQL 解析和路由规则的编写。
具体来说,Sharding-JDBC 使用了 Antlr4 作为 SQL 解析器,Antlr4 可以通过 Groovy DSL(领域特定语言)来编写 SQL 解析规则,同时支持动态 SQL 和静态 SQL 的解析。在 Groovy DSL 中,可以定义 SQL 解析规则、SQL 语句的语法结构、SQL 语句的类型等信息,从而实现对 SQL 语句的解析和分析。
除了 SQL 解析之外,Sharding-JDBC 还使用 Groovy 来编写路由规则。在 Groovy 中,可以定义路由规则、分表策略、分库策略等信息,从而实现对 SQL 语句的路由和分发。通过 Groovy 的灵活性和动态性,Sharding-JDBC 可以支持各种复杂的分库分表场景,包括水平分表、垂直分库、读写分离等。
五.快速入门
1.建表语句
CREATE DATABASE `order_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
DROP TABLE IF EXISTS `t_order_1`;
CREATE TABLE `t_order_1` (
`order_id` bigint(20) NOT NULL COMMENT '订单id',
`price` decimal(10, 2) NOT NULL COMMENT '订单价格',
`user_id` bigint(20) NOT NULL COMMENT '下单用户id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `t_order_2`;
CREATE TABLE `t_order_2` (
`order_id` bigint(20) NOT NULL COMMENT '订单id',
`price` decimal(10, 2) NOT NULL COMMENT '订单价格',
`user_id` bigint(20) NOT NULL COMMENT '下单用户id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2.依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding‐jdbc‐spring‐boot‐starter</artifactId>
<version>4.0.0‐RC1</version>
</dependency>
3.properties 配置文件
server.port=56081
spring.application.name = sharding‐jdbc‐simple‐demo
server.servlet.context‐path = /sharding‐jdbc‐simple‐demo
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF‐8
spring.http.encoding.force = true
spring.main.allow‐bean‐definition‐overriding = true
mybatis.configuration.map‐underscore‐to‐camel‐case = true
# 以下是分片规则配置
# 定义数据源
spring.shardingsphere.datasource.names = m1
spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/order_db?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = root
# 指定t_order表的数据分布情况,配置数据节点
spring.shardingsphere.sharding.tables.t_order.actual‐data‐nodes = m1.t_order_$‐>{1..2}
# 指定t_order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key‐generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key‐generator.type=SNOWFLAKE
# 指定t_order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.t_order.table‐strategy.inline.sharding‐column = order_id
spring.shardingsphere.sharding.tables.t_order.table‐strategy.inline.algorithm‐expression =
t_order_$‐>{order_id % 2 + 1}
# 打开sql输出日志
spring.shardingsphere.props.sql.show = true
swagger.enable = true
logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding = debug
logging.level.druid.sql = debug
说明:
- 首先定义数据源 m1,并对 m1 进行实际的参数配置。
- 指定 t_order 表的数据分布情况,他分布在 m1.t_order_1,m1.t_order_2
- 指定 t_order 表的主键生成策略为 SNOWFLAKE,SNOWFLAKE 是一种分布式自增算法,保证 id 全局唯一
- 定义 torder 分片策略,order_id 为偶数的数据落在 t_order_1,为奇数的落在 t_order_2,分表策略的表达式为 t_order$->{order_id % 2 + 1}
4.操作
@Mapper
@Component
public interface OrderDao {
/**
* 新增订单
* @param price 订单价格
* @param userId 用户id
* @param status 订单状态
* @return
*/
@Insert("insert into t_order(price,user_id,status) value(#{price},#{userId},#{status})")
int insertOrder(@Param("price") BigDecimal price, @Param("userId") Long userId
, @Param("status") String status);
/**
* 根据id列表查询多个订单
* @param orderIds 订单id列表
* @return
*/
@Select({
"<script>" +
"select " +
" * " +
" from t_order t" +
" where t.order_id in " +
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>" +
" #{id} " +
"</foreach>" +
"</script>"
})
List < Map > selectOrderbyIds(@Param("orderIds") List < Long > orderIds);
}
5.测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes= {
ShardingJdbcSimpleDemoBootstrap.class
})
publicclassOrderDaoTest {
@Autowired
privateOrderDaoorderDao;
@Test
publicvoidtestInsertOrder() {
for (inti=0; i<10; i++) {
orderDao.insertOrder(newBigDecimal((i+1) *5), 1L, "WAIT_PAY");
}
}
@Test
publicvoidtestSelectOrderbyIds() {
List<Long>ids=newArrayList<> ();
ids.add(373771636085620736L);
ids.add(373771635804602369L);
List<Map>maps=orderDao.selectOrderbyIds(ids);
System.out.println(maps);
}
}