《系统掌握 ShardingSphere-JDBC:分库分表、读写分离、分布式事务一网打尽》

目录

  • [Apache ShardingSphere实战](#Apache ShardingSphere实战)
  • 1.数据库架构演变与分库分表介绍
    • [1.1 海量数据存储问题及解决方案](#1.1 海量数据存储问题及解决方案)
    • [1.2 项目架构的演进](#1.2 项目架构的演进)
      • [1.2.1 理财平台 - V1.0](#1.2.1 理财平台 - V1.0)
      • [1.2.2 理财平台 - V1.x](#1.2.2 理财平台 - V1.x)
      • [1.2.3 理财平台-V2.0 版本](#1.2.3 理财平台-V2.0 版本)
      • [1.2.4 理财平台-V2.x 版本](#1.2.4 理财平台-V2.x 版本)
    • [1.5 分库分表](#1.5 分库分表)
      • [1.5.1 什么是分库分表](#1.5.1 什么是分库分表)
      • [1.5.2 分库分表的方式](#1.5.2 分库分表的方式)
        • [1.5.2.1 垂直分库](#1.5.2.1 垂直分库)
        • [1.5.2.2 垂直分表](#1.5.2.2 垂直分表)
        • [1.5.2.3 水平分库](#1.5.2.3 水平分库)
        • [1.5.2.4 水平分表](#1.5.2.4 水平分表)
      • [1.5.3 分库分表的规则](#1.5.3 分库分表的规则)
      • [1.5.4 分库分表带来的问题及解决方案](#1.5.4 分库分表带来的问题及解决方案)
  • 2.ShardingSphere实战
    • [2.1 什么是ShardingSphere](#2.1 什么是ShardingSphere)
    • [2.2 Sharding-JDBC介绍](#2.2 Sharding-JDBC介绍)
    • [2.3 数据分片详解与实战](#2.3 数据分片详解与实战)
      • [2.3.1 核心概念](#2.3.1 核心概念)
        • [2.3.1.1 表概念](#2.3.1.1 表概念)
        • [2.3.1.2 分片键](#2.3.1.2 分片键)
        • [2.3.1.3 分片算法](#2.3.1.3 分片算法)
        • [2.3.1.4 分片策略](#2.3.1.4 分片策略)
        • [2.3.1.5 分布式主键](#2.3.1.5 分布式主键)
      • [2.3.2 搭建基础环境](#2.3.2 搭建基础环境)
        • [2.3.2.1 安装环境](#2.3.2.1 安装环境)
        • [2.3.2.2 创建数据库和表](#2.3.2.2 创建数据库和表)
        • [2.3.2.3 创建SpringBoot程序](#2.3.2.3 创建SpringBoot程序)
      • [2.3.3 实现垂直分库](#2.3.3 实现垂直分库)
        • [2.3.3.1 配置文件](#2.3.3.1 配置文件)
        • [2.3.3.2 垂直分库测试](#2.3.3.2 垂直分库测试)
      • [2.3.4 实现水平分表](#2.3.4 实现水平分表)
        • [2.3.4.1 数据准备](#2.3.4.1 数据准备)
        • [2.3.4.2 配置文件](#2.3.4.2 配置文件)
        • [2.3.4.3 测试](#2.3.4.3 测试)
        • [2.3.4.4 行表达式](#2.3.4.4 行表达式)
        • [2.3.4.5 配置分片策略](#2.3.4.5 配置分片策略)
        • [2.3.4.6 分布式序列算法](#2.3.4.6 分布式序列算法)
      • [2.3.5 实现水平分库](#2.3.5 实现水平分库)
        • [2.3.5.1 数据准备](#2.3.5.1 数据准备)
        • [2.3.5.2 配置文件](#2.3.5.2 配置文件)
        • [2.3.5.3 水平分库测试](#2.3.5.3 水平分库测试)
        • [2.3.5.4 水平分库查询](#2.3.5.4 水平分库查询)
        • [2.3.5.5 分片算法HASH_MOD和MOD](#2.3.5.5 分片算法HASH_MOD和MOD)
        • [2.3.5.6 水平分库总结](#2.3.5.6 水平分库总结)
      • [2.3.6 实现绑定表](#2.3.6 实现绑定表)
        • [2.3.6.1 数据准备](#2.3.6.1 数据准备)
        • [2.3.6.2 创建实体类](#2.3.6.2 创建实体类)
        • [2.3.6.3 创建mapper](#2.3.6.3 创建mapper)
        • [2.3.6.4 配置多表关联](#2.3.6.4 配置多表关联)
        • [2.3.6.5 测试插入数据](#2.3.6.5 测试插入数据)
        • [2.3.6.6 配置绑定表](#2.3.6.6 配置绑定表)
        • [2.3.6.7 总结](#2.3.6.7 总结)
      • [2.3.7 实现广播表(公共表)](#2.3.7 实现广播表(公共表))
        • [2.3.7.1 公共表介绍](#2.3.7.1 公共表介绍)
        • [2.3.7.2 代码编写](#2.3.7.2 代码编写)
        • [2.3.7.3 广播表配置](#2.3.7.3 广播表配置)
        • [2.3.7.4 测试广播表](#2.3.7.4 测试广播表)
        • [2.3.7.5 总结](#2.3.7.5 总结)
    • [2.4 读写分离详解与实战](#2.4 读写分离详解与实战)
      • [2.4.1 读写分离架构介绍](#2.4.1 读写分离架构介绍)
        • [2.4.1.1 读写分离原理](#2.4.1.1 读写分离原理)
        • [2.4.1.2 读写分离应用方案](#2.4.1.2 读写分离应用方案)
      • [2.4.2 CAP 理论](#2.4.2 CAP 理论)
        • [2.4.2.1 CAP理论介绍](#2.4.2.1 CAP理论介绍)
        • [2.4.2.2 CAP理论特点](#2.4.2.2 CAP理论特点)
        • [2.4.2.3 分布式数据库对于CAP理论的实践](#2.4.2.3 分布式数据库对于CAP理论的实践)
      • [2.4.3 MySQL主从同步](#2.4.3 MySQL主从同步)
        • [2.4.3.1 主从同步原理](#2.4.3.1 主从同步原理)
        • [2.4.3.2 一主一从架构搭建](#2.4.3.2 一主一从架构搭建)
      • [2.4.4 Sharding-JDBC实现读写分离](#2.4.4 Sharding-JDBC实现读写分离)
        • [2.4.4.1 数据准备](#2.4.4.1 数据准备)
        • [2.4.4.2 环境准备](#2.4.4.2 环境准备)
        • [2.4.4.3 配置读写分离](#2.4.4.3 配置读写分离)
        • [2.4.4.4 读写分离测试](#2.4.4.4 读写分离测试)
        • [2.4.4.5 事务读写分离测试](#2.4.4.5 事务读写分离测试)
      • [2.4.5 负载均衡算法](#2.4.5 负载均衡算法)
        • [2.4.5.1 一主两从架构](#2.4.5.1 一主两从架构)
        • [2.4.5.2 负载均衡测试](#2.4.5.2 负载均衡测试)
    • [2.5 强制路由详解与实战](#2.5 强制路由详解与实战)
      • [2.5.1 强制路由介绍](#2.5.1 强制路由介绍)
      • [2.5.2 强制路由的使用](#2.5.2 强制路由的使用)
        • [2.5.2.1 环境准备](#2.5.2.1 环境准备)
        • [2.5.2.2 代码编写](#2.5.2.2 代码编写)
        • [2.5.2.3 配置文件](#2.5.2.3 配置文件)
        • [2.5.2.4 强制路由到库到表测试](#2.5.2.4 强制路由到库到表测试)
        • [2.5.2.5 强制路由到库到表查询测试](#2.5.2.5 强制路由到库到表查询测试)
        • [2.5.2.6 强制路由走主库查询测试](#2.5.2.6 强制路由走主库查询测试)
        • [2.5.2.7 SQL执行流程剖析](#2.5.2.7 SQL执行流程剖析)
    • [2.6 数据加密详解与实战](#2.6 数据加密详解与实战)
      • [2.6.1 数据加密介绍](#2.6.1 数据加密介绍)
      • [2.6.2 整体架构](#2.6.2 整体架构)
      • [2.6.3 加密规则](#2.6.3 加密规则)
      • [2.6.4 脱敏处理流程](#2.6.4 脱敏处理流程)
      • [2.6.5 数据加密实战](#2.6.5 数据加密实战)
        • [2.6.5.1 环境搭建](#2.6.5.1 环境搭建)
        • [2.6.5.2 加密策略解析](#2.6.5.2 加密策略解析)
        • [2.6.5.3 默认AES加密算法实现](#2.6.5.3 默认AES加密算法实现)
        • [2.6.5.4 MD5加密算法实现](#2.6.5.4 MD5加密算法实现)
    • [2.7 分布式事务详解与实战](#2.7 分布式事务详解与实战)
      • [2.7.1 什么是分布式事务](#2.7.1 什么是分布式事务)
        • [2.7.1.1 本地事务介绍](#2.7.1.1 本地事务介绍)
        • [2.7.1.2 事务日志undo和redo](#2.7.1.2 事务日志undo和redo)
        • [2.7.1.3 分布式事务介绍](#2.7.1.3 分布式事务介绍)
      • [2.7.2 分布式事务理论](#2.7.2 分布式事务理论)
        • [2.7.2.1 CAP (强一致性)](#2.7.2.1 CAP (强一致性))
        • [2.7.2.2 BASE(最终一致性)](#2.7.2.2 BASE(最终一致性))
      • [2.7.3 分布式事务模式(大概了解)](#2.7.3 分布式事务模式(大概了解))
        • [2.7.3.1 DTP模型与XA协议](#2.7.3.1 DTP模型与XA协议)
        • [2.7.3.2 2PC模式 (强一致性)](#2.7.3.2 2PC模式 (强一致性))
        • [2.7.3.3 TCC模式 (最终一致性)](#2.7.3.3 TCC模式 (最终一致性))
        • [2.7.3.4 消息队列模式(最终一致性)](#2.7.3.4 消息队列模式(最终一致性))
        • [2.7.3.5 AT模式 (最终一致性)](#2.7.3.5 AT模式 (最终一致性))
        • [2.7.3.6 Saga模式(最终一致性)](#2.7.3.6 Saga模式(最终一致性))
      • [2.7.4 Sharding-JDBC分布式事务实战](#2.7.4 Sharding-JDBC分布式事务实战)
        • [2.7.4.1 Sharding-JDBC分布式事务介绍](#2.7.4.1 Sharding-JDBC分布式事务介绍)
        • [2.7.4.2 环境准备](#2.7.4.2 环境准备)
        • [2.7.4.3 案例实现](#2.7.4.3 案例实现)
        • [2.7.4.4 案例测试](#2.7.4.4 案例测试)
    • [2.8 ShardingProxy实战](#2.8 ShardingProxy实战)
      • [2.8.1 使用二进制发布包安装ShardingSphere-Proxy](#2.8.1 使用二进制发布包安装ShardingSphere-Proxy)
      • [2.8.2 proxy实现读写分离](#2.8.2 proxy实现读写分离)
      • [2.8.3 使用应用程序连接proxy](#2.8.3 使用应用程序连接proxy)
      • [2.8.4 Proxy实现垂直分片](#2.8.4 Proxy实现垂直分片)
      • [2.8.5 Proxy实现水平分片](#2.8.5 Proxy实现水平分片)
      • [2.8.6 Proxy实现广播表](#2.8.6 Proxy实现广播表)
      • [2.8.7 Proxy实现绑定表](#2.8.7 Proxy实现绑定表)
      • [2.8.8 总结](#2.8.8 总结)

Apache ShardingSphere实战

1.数据库架构演变与分库分表介绍

1.1 海量数据存储问题及解决方案

如今随着互联网的发展,数据的量级也是成指数的增长,从GB到TB到PB。对数据的各种操作也是愈加的困难,传统单体的关系性数据库已经无法满足快速查询与插入数据的需求。

阿里数据中心内景( 阿里、百度、腾讯这样的互联网巨头,数据量据说已经接近EB级)

事务安全性,NOSQL数据库对事务的支持不完善. MySQL不可替代.

遇到的问题

  • 用户请求量大
  • 单库的数据量过大
  • 单表的数据量过大

解决方案

  • 单机数据库 --> 主从架构 ---> 分库分表

1.2 项目架构的演进

1.2.1 理财平台 - V1.0

此时项目是一个单体应用架构 (一个归档包(可以是JAR、WAR、EAR或其它归档格式)包含所有功能的应用程序,通常称为单体应用)

这个阶段是公司发展的早期阶段,系统架构如上图所示。我们经常会在单台服务器上运行我们所有的程序和软件。

在项目运行初期,User表、Order表、等等各种表都在同一个数据库中,每个表都包含了大量的字段。在用户量比较少,访问量也比较少的时候,单库单表不存在问题。

这个阶段一般是属于业务规模不是很大的公司使用,因为机器都是单台的话,随着我们业务规模的增长,慢慢的我们的网站就会出现一些瓶颈和隐患问题

1.2.2 理财平台 - V1.x

随着访问量的继续不断增加,单台应用服务器已经无法满足我们的需求。所以我们通过增 加应用服务器的方式来将服务器集群化。

存在的问题

采用了应用服务器高可用集群的架构之后,应用层的性能被我们拉上来了,但是数据库的负载也在增加,随着访问量的提高,所有的压力都将集中在数据库这一层.

1.2.3 理财平台-V2.0 版本

应用层的性能被我们拉上来了,但数据库的负载也在逐渐增大,那如何去提高数据库层面的性能呢?

  • 数据库主从复制、读写分离

写10000 -> 3分钟, 读10000->5秒, 读操作占整体操作8成,写操作2成

读写分离的数据节点中的数据内容是一致。

复制代码
		![](https://cdn.nlark.com/yuque/0/2025/jpg/29416702/1765370704333-9ee67f08-2d54-4c36-a399-ebc3c808aaab.jpg) 

使用主从复制+读写分离一定程度上可以解决问题,但是随着用户量的增加、访问量的增加、数据量的增加依然会带来大量的问题.

1.2.4 理财平台-V2.x 版本

随着访问量的持续不断增加,慢慢的我们的系统项目会出现许多用户访问同一内容的情况,比如秒杀活动,抢购活动等。

那么对于这些热点数据的访问,没必要每次都从数据库重读取,这时我们可以使用到缓存技术,比如 redis、memcache 来作为我们应用层的缓存。

  • 数据库主从复制、读写分离 +缓存技术

存在的问题

  1. 缓存只能缓解读取压力,数据库的写入压力还是很大
  2. 且随着数据量的继续增大,性能还是很缓慢

我们的系统架构从单机演变到这个阶,所有的数据都还在同一个数据库中,尽管采取了增加缓存,主从、读写分离的方式,但是随着数据库的压力持续增加,数据库的瓶颈仍然是个最大的问题。因此我们可以考虑对数据的垂直拆分和水平拆分。就是今天所讲的主题,分库分表。

1.5 分库分表

1.5.1 什么是分库分表

简单来说,就是指通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)上面,以达到分散单台设备负载的效果。

  1. 分库分表解决的问题 ?

  2. 什么情况下需要分库分表

1.5.2 分库分表的方式

分库分表包括: 垂直分库、垂直分表、水平分库、水平分表 四种方式。

1.5.2.1 垂直分库
  • 数据库中不同的表对应着不同的业务,垂直切分是指按照业务的不同将表进行分类,分布到不同的数据库上面
    • 将数据库部署在不同服务器上,从而达到多个服务器共同分摊压力的效果

      复制代码
        ![](https://i-blog.csdnimg.cn/img_convert/a8a76ebafe2e69ca391bce338f087085.jpeg)      
1.5.2.2 垂直分表

表中字段太多且包含大字段的时候,在查询时对数据库的IO、内存会受到影响,同时更新数据时,产生的binlog文件会很大,MySQL在主从同步时也会有延迟的风险

  • 将一个表按照字段分成多表,每个表存储其中一部分字段。

  • 对职位表进行垂直拆分, 将职位基本信息放在一张表, 将职位描述信息存放在另一张表

    复制代码
      	![](https://i-blog.csdnimg.cn/img_convert/6cd19bbf3741bd92eeb470ff57bf4665.jpeg)  
  • 垂直拆分带来的一些提升

    • 解决业务层面的耦合,业务清晰
    • 能对不同业务的数据进行分级管理、维护、监控、扩展等
    • 高并发场景下,垂直分库一定程度的提高访问性能
  • 垂直拆分没有彻底解决单表数据量过大的问题

1.5.2.3 水平分库
  • 将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈.
  • 简单讲就是根据表中的数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库(主机)上面, 例如将订单表 按照id是奇数还是偶数, 分别存储在不同的库中。
1.5.2.4 水平分表
  • 针对数据量巨大的单张表(比如订单表),按照规则把一张表的数据切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。
  • 总结

水平根据数据进行拆分

复制代码
- **垂直分表**: 将一个表按照字段分成多表,每个表存储其中一部分字段。
- **垂直分库**: 根据表的业务不同,分别存放在不同的库中,这些库分别部署在不同的服务器.
- **水平分库**: 把一张表的数据按照一定规则,分配到**不同的数据库**,每一个库只有这张表的部分数据.
- **水平分表**: 把一张表的数据按照一定规则,分配到**同一个数据库的多张表中**,每个表只有这个表的部分数据.

1.5.3 分库分表的规则

1) 水平分库规则

  • 不跨库、不跨表,保证同一类的数据都在同一个服务器上面。
  • 数据在切分之前,需要考虑如何高效的进行数据获取,如果每次查询都要跨越多个节点,就需要谨慎使用。

2) 水平分表规则

  • RANGE
    • 时间:按照年、月、日去切分。
    • 地域:按照省或市去切分。
    • 大小:从0到1000000一个表。
  • HASH
    • 用户ID取模

3) 不同的业务使用的切分规则是不一样,就上面提到的切分规则,举例如下:

  • 用户表
    • 范围法:以用户ID为划分依据,将数据水平切分到两个数据库实例,如:1到1000W在一张表,1000W到2000W在一张表,这种情况会出现单表的负载较高
    • 按照用户ID HASH尽量保证用户数据均衡分到数据库中
  • 流水表
    • 时间维度:可以根据每天新增的流水来判断,选择按照年份分库,还是按照月份分库,甚至也可以按照日期分库

1.5.4 分库分表带来的问题及解决方案

关系型数据库在单机单库的情况下,比较容易出现性能瓶颈问题,分库分表可以有效的解决这方面的问题,但是同时也会产生一些 比较棘手的问题.

1) 事务一致性问题

  • 当我们需要更新的内容同时分布在不同的库时, 不可避免的会产生跨库的事务问题. 原来在一个数据库操作, 本地事务就可以进行控制, 分库之后 一个请求可能要访问多个数据库,如何保证事务的一致性,目前还没有简单的解决方案.

2) 跨节点关联的问题

  • 在分库之后, 原来在一个库中的一些表,被分散到多个库,并且这些数据库可能还不在一台服务器,无法关联查询.解决这种关联查询,需要我们在代码层面进行控制,将关联查询拆开执行,然后再将获取到的结果进行拼装.

3) 分页排序查询的问题

  • 分库并行查询时,如果用到了分页 每个库返回的结果集本身是无序的, 只有将多个库中的数据先查出来,然后再根据排序字段在内存中进行排序,如果查询结果过大也是十分消耗资源的.

4) 主键避重问题

  • 在分库分表的环境中,表中的数据存储在不同的数据库, 主键自增无法保证ID不重复, 需要单独设计全局主键.

5) 公共表的问题

  • 不同的数据库,都需要从公共表中获取数据. 某一个数据库更新看公共表其他数据库的公共表数据需要进行同步.

上面我们说了分库分表后可能会遇到的一些问题, 接下来带着这些问题,我们就来一起来学习一下Apache ShardingSphere !

2.ShardingSphere实战

2.1 什么是ShardingSphere

Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。

官网: https://shardingsphere.apache.org/document/current/cn/overview/

Apache ShardingSphere 设计哲学为 Database Plus,旨在构建异构数据库上层的标准和生态。 它关注如何充分合理地利用数据库的计算和存储能力,而并非实现一个全新的数据库。 它站在数据库的上层视角,关注它们之间的协作多于数据库自身。

Apache ShardingSphere它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(规划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。

复制代码
	![](https://cdn.nlark.com/yuque/0/2025/jpg/29416702/1765370705110-e439ab23-9c01-444e-a355-8425bfc9314e.jpg) 
  • Sharding-JDBC:被定位为轻量级Java框架,在Java的JDBC层提供的额外服务,以jar包形式使用。

  • Sharding-Proxy:被定位为透明化的数据库代理端,向应用程序完全透明,可直接当做 MySQL 使用;

  • Sharding-Sidecar:被定位为Kubernetes(K8S)的云原生数据库代理,以守护进程的形式代理所有对数据库的访问(只是计划在未来做)。

    复制代码
      ![](https://i-blog.csdnimg.cn/img_convert/805a42b479766da015a80b9fef7bfcc5.jpeg) 

Sharding-JDBC、Sharding-Proxy之间的区别如下:

Sharding-JDBC Sharding-Proxy
数据库 任意 MySQL/PostgreSQL
连接消耗数
异构语言 仅Java 任意
性能 损耗低 损耗略高
无中心化
静态入口

异构是继面向对象编程思想又一种较新的编程思想,面向服务编程,不用顾虑语言的差别,提供规范的服务接口,我们无论使用什么语言,就都可以访问使用了,大大提高了程序的复用率。

Sharding-Proxy的优势在于对异构语言的支持,以及为DBA提供可操作入口。它可以屏蔽底层分库分表的复杂度,运维及开发人员仅面向proxy操作,像操作单个数据库一样操作复杂的底层MySQL实例

很显然ShardingJDBC只是客户端的一个工具包,可以理解为一个特殊的JDBC驱动包,所有分库分表逻辑均有业务方自己控制,所以他的功能相对灵活,支持的 数据库也非常多,但是对业务侵入大,需要业务方自己定义所有的分库分表逻辑.

而ShardingProxy是一个独立部署的服务,对业务方无侵入,业务方可以像用一个普通的MySQL服务一样进行数据交互,基本上感觉不到后端分库分表逻辑的存在,但是这也意味着功能会比较固定,能够支持的数据库也比较少,两者各有优劣.

ShardingSphere项目状态如下:

ShardingSphere定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。

2.2 Sharding-JDBC介绍

Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架的使用。

  • 适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。

  • 基于任何第三方的数据库连接池,如:DBCP, C3P0, Druid, HikariCP等。

  • 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。

    复制代码
      	![](https://i-blog.csdnimg.cn/img_convert/3c6181c64c57d912e0528c0b77f527a5.jpeg) 

Sharding-JDBC主要功能

  • 数据分片
    • 分库分表
    • 读写分离
    • 分片策略
    • 分布式主键
  • 分布式事务
    • 标准化事务接口
    • XA强一致性事务
    • 柔性事务
  • 数据库治理
    • 配置动态化
    • 编排治理
    • 数据脱敏
    • 可视化链路追踪

Sharding-JDBC 内部结构

  • 图中黄色部分表示的是Sharding-JDBC的入口API,采用工厂方法的形式提供。 目前有ShardingDataSourceFactory和MasterSlaveDataSourceFactory两个工厂类。
    • ShardingDataSourceFactory支持分库分表、读写分离操作
    • MasterSlaveDataSourceFactory支持读写分离操作
  • 图中蓝色部分表示的是Sharding-JDBC的配置对象,提供灵活多变的配置方式。 ShardingRuleConfiguration是分库分表配置的核心和入口,它可以包含多个TableRuleConfiguration和MasterSlaveRuleConfiguration。
    • TableRuleConfiguration封装的是表的分片配置信息,有5种配置形式对应不同的Configuration类型。
    • MasterSlaveRuleConfiguration封装的是读写分离配置信息。
  • 图中红色部分表示的是内部对象,由Sharding-JDBC内部使用,应用开发者无需关注。Sharding-JDBC通过ShardingRuleConfiguration和MasterSlaveRuleConfiguration生成真正供ShardingDataSource和MasterSlaveDataSource使用的规则对象。ShardingDataSource和MasterSlaveDataSource实现了DataSource接口,是JDBC的完整实现方案。

2.3 数据分片详解与实战

2.3.1 核心概念

对于数据库的垂直拆分一般都是在数据库设计初期就会完成,因为垂直拆分与业务直接相关,而我们提到的分库分表一般是指的水平拆分,数据分片就是将原本一张数据量较大的表t_order拆分生成数个表结构完全一致的小数据量表t_order_0、t_order_1...,每张表只保存原表的部分数据.

2.3.1.1 表概念
  • 逻辑表水平拆分的数据库(表)的相同逻辑和数据结构表的总称。 比如的订单表 t_order ---> t_order_0 ...t_order _9.拆分后t_order表 已经不存在了,这个时候t_order表就是上面拆分的表单的逻辑表.
  • 真实表数据库中真实存在的物理表。 t_order_0 ...t_order _9
  • 数据节点在分片之后,由数据源和数据表组成。比如: t_order_db1.t_order_0
  • 绑定表绑定表是指具有相同分片规则的一组关联表(如主表与子表),例如 t_ordert_order_item 均按 order_id 进行分片。当这些表中 order_id 相同的数据落在相同的分片上时,它们即构成绑定表关系。绑定表之间的多表关联查询不会产生笛卡尔积,从而显著提升查询效率。
sql 复制代码
# t_order:t_order0、t_order1
# t_order_item:t_order_item0、t_order_item1

select * from t_order o join t_order_item i on o.order_id=i.order_id where o.order_id in (10,11);

由于分库分表以后这些表被拆分成N多个子表。如果不配置绑定表关系,会出现笛卡尔积关联查询,将产生如下四条SQL。

sql 复制代码
select * from t_order0 o join t_order_item0 i on o.order_id=i.order_id
where o.order_id in (10,11);

select * from t_order0 o join t_order_item1 i on o.order_id=i.order_id
where o.order_id in (10,11);

select * from t_order1 o join t_order_item0 i on o.order_id=i.order_id
where o.order_id in (10,11);

select * from t_order1 o join t_order_item1 i on o.order_id=i.order_id
where o.order_id in (10,11);

如果配置绑定表关系后再进行关联查询时,只要对应表分片规则一致产生的数据就会落到同一个库中,那么只需 t_order_0和 t_order_item_0 表关联即可。

sql 复制代码
select * from t_order0 o join t_order_item0 i on o.order_id=i.order_id
where o.order_id in (10,11);

select * from t_order1 o join t_order_item1 i on o.order_id=i.order_id
where o.order_id in (10,11);
  • 广播表在使用中,有些表没必要做分片,例如字典表、省份信息等,因为他们数据量不大,而且这种表可能需要与海量数据的表进行关联查询。广播表会在不同的数据节点上进行存储,存储的表结构和数据完全相同。
  • 单表指所有的分片数据源中只存在唯一一张的表。适用于数据量不大且不需要做任何分片操作的场景。
2.3.1.2 分片键

用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。

例:将订单表中的订单主键取模分片,则订单主键为分片字段。 SQL 中如果无分片字段,将执行全路由(去查询所有的真实表),性能较差。 除了对单分片字段的支持,Apache ShardingSphere 也支持根据多个字段进行分片。

2.3.1.3 分片算法

由于分片算法(ShardingAlgorithm) 和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。目前提供4种分片算法。

  • 精确分片算法

用于处理使用单一键作为分片键的=与IN进行分片的场景。

  • 范围分片算法

用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。

  • 复合分片算法

用于处理使用多键作为分片键进行分片的场景,多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。

  • Hint分片算法

Hint 分片算法适用于分片键无法从 SQL 语句中直接获取,而需依赖外部上下文(如用户身份、会话信息等)动态确定的场景。当数据库表结构中不包含实际用于分片的字段时,可通过 SQL Hint 在执行时显式传递分片值,从而实现精确的数据路由。

典型应用场景包括:内部系统按员工登录 ID 进行分库,但业务表中并未存储该字段。此时,借助 Hint 机制,可在不修改表结构的前提下完成分片逻辑。

2.3.1.4 分片策略

分片策略(ShardingStrategy) 包含分片键和分片算法,真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。

  • 标准分片策略 StandardShardingStrategy

只支持单分片键,提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。

PreciseShardingAlgorithm是必选的,RangeShardingAlgorithm是可选的。但是SQL中使用了范围操作,如果不配置RangeShardingAlgorithm会采用全库路由扫描,效率低。

  • 复合分片策略 ComplexShardingStrategy

支持多分片键。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

  • 行表达式分片策略 InlineShardingStrategy

只支持单分片键。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发。如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。

  • Hint分片策略HintShardingStrategy

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

  • 不分片策略NoneShardingStrategy

不分片的策略。

2.3.1.5 分布式主键

数据分片后,不同数据节点生成全局唯一主键是非常棘手的问题,同一个逻辑表(t_order)内的不同真实表(t_order_n)之间的自增键由于无法互相感知而产生重复主键。

尽管可通过设置自增主键初始值和步长的方式避免ID碰撞,但这样会使维护成本加大,缺乏完整性和可扩展性。如果后期需要增加分片表的数量,要逐一修改分片表的步长,运维成本非常高,所以不建议这种方式。

ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。

内置主键生成器:

  • UUID采用UUID.randomUUID()的方式产生分布式主键。
  • SNOWFLAKE在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法,生成64bit的长整型数据。

2.3.2 搭建基础环境

2.3.2.1 安装环境
  1. jdk: 要求jdk必须是1.8版本及以上
  2. MySQL: 推荐mysql5.7版本
  3. 搭建两台MySQL服务器
plain 复制代码
mysql-server1 192.168.116.128
mysql-server2 192.168.116.129
2.3.2.2 创建数据库和表
  1. 在mysql01服务器上, 创建数据库 payorder_db,并创建表pay_order
sql 复制代码
CREATE DATABASE payorder_db CHARACTER SET 'utf8';

CREATE TABLE `pay_order` (
  `order_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `product_name` varchar(128) DEFAULT NULL,
  `COUNT` int(11) DEFAULT NULL,
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=12345679 DEFAULT CHARSET=utf8
  1. 在mysql02服务器上, 创建数据库 user_db,并创建表users
sql 复制代码
CREATE DATABASE user_db CHARACTER SET 'utf8';

CREATE TABLE `users` (
  `id` int(11) NOT NULL,
  `username` varchar(255) NOT NULL COMMENT '用户昵称',
  `phone` varchar(255) NOT NULL COMMENT '注册手机',
  `PASSWORD` varchar(255) DEFAULT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表'
2.3.2.3 创建SpringBoot程序

1) 创建项目

环境说明:SpringBoot2.3.7+ MyBatisPlus + ShardingSphere-JDBC 5.1 + Hikari+ MySQL 5.7

Spring脚手架: http://start.aliyun.com

2) 引入依赖
xml 复制代码
    <parent>
        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-parent</artifactId>

        <version>2.3.7.RELEASE</version>

        <relativePath/>
    </parent> 
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>

            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>

            <version>5.1.1</version>

        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>

            <artifactId>mybatis-plus-boot-starter</artifactId>

            <version>3.3.1</version>

        </dependency>

        <dependency>
            <groupId>mysql</groupId>

            <artifactId>mysql-connector-java</artifactId>

            <scope>runtime</scope>

        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>

            <artifactId>lombok</artifactId>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>

                    <artifactId>junit-vintage-engine</artifactId>

                </exclusion>

            </exclusions>

        </dependency>

    </dependencies>
3) 创建实体类
java 复制代码
@TableName("pay_order") //逻辑表名
@Data
@ToString
public class PayOrder {

    @TableId
    private long order_id;

    private long user_id;

    private String product_name;

    private int count;

}

@TableName("users")
@Data
@ToString
public class User {

    @TableId
    private long id;

    private String username;

    private String phone;

    private String password;

}
4) 创建Mapper
java 复制代码
@Mapper
public interface PayOrderMapper extends BaseMapper<PayOrder> {
}

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

2.3.3 实现垂直分库

2.3.3.1 配置文件

使用sharding-jdbc 对数据库中水平拆分的表进行操作,通过sharding-jdbc对分库分表的规则进行配置,配置内容包括:数据源、主键生成策略、分片策略等。

application.properties

  • 基础配置
properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
  • 数据源
properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table


# 定义多个数据源
spring.shardingsphere.datasource.names = db1,db2

#数据源1
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.128:3306/payorder_db?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

#数据源2
spring.shardingsphere.datasource.db2.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db2.url = jdbc:mysql://192.168.116.129:3306/user_db?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db2.username = root
spring.shardingsphere.datasource.db2.password = 123456


#配置数据节点
# 标准分片表配置
# 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
spring.shardingsphere.rules.sharding.tables.pay_order.actual-data-nodes=db1.pay_order
spring.shardingsphere.rules.sharding.tables.users.actual-data-nodes=db2.users


mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
  • 配置数据节点
properties 复制代码
# 标准分片表配置
# 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
spring.shardingsphere.rules.sharding.tables.pay_order.actual-data-nodes=db1.pay_order
spring.shardingsphere.rules.sharding.tables.users.actual-data-nodes=db2.users
  • 打开sql输出日志
properties 复制代码
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.3.3.2 垂直分库测试
java 复制代码
@SpringBootTest
class ShardingJdbcApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PayOrderMapper payOrderMapper;

    @Test
    public void testInsert(){
        User user = new User();
        user.setId(1002);
        user.setUsername("大远哥");
        user.setPhone("15612344321");
        user.setPassword("123456");
        userMapper.insert(user);

        PayOrder payOrder = new PayOrder();
        payOrder.setOrder_id(12345679);
        payOrder.setProduct_name("猕猴桃");
        payOrder.setUser_id(user.getId());
        payOrder.setCount(2);
        payOrderMapper.insert(payOrder);
    }

    @Test
    public void testSelect(){

        User user = userMapper.selectById(1001);
        System.out.println(user);
        PayOrder payOrder = payOrderMapper.selectById(12345678);
        System.out.println(payOrder);
    }

}

数据插入情况:

可以看到User数据就插入到了129的服务器,PayOrder数据就插入到了128服务器。这就实现了一个简单的垂直分库的情况,不同的表分布在不同的服务器上

2.3.4 实现水平分表

2.3.4.1 数据准备

需求说明:

  1. 在mysql-server01服务器上, 创建数据库 course_db
  2. 创建表 t_course_1 、 t_course_2
  3. 约定规则:如果添加的课程 id 为偶数添加到 t_course_1 中,奇数添加到 t_course_2 中。

水平分片的id需要在业务层实现,不能依赖数据库的主键自增

sql 复制代码
CREATE TABLE t_course_1 (
  `cid` BIGINT(20) NOT NULL,
  `user_id` BIGINT(20) DEFAULT NULL,
  `cname` VARCHAR(50) DEFAULT NULL,
  `brief` VARCHAR(50) DEFAULT NULL,
  `price` DOUBLE DEFAULT NULL,
  `status` INT(11) DEFAULT NULL,
  PRIMARY KEY (`cid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8


CREATE TABLE t_course_2 (
  `cid` BIGINT(20) NOT NULL,
  `user_id` BIGINT(20) DEFAULT NULL,
  `cname` VARCHAR(50) DEFAULT NULL,
  `brief` VARCHAR(50) DEFAULT NULL,
  `price` DOUBLE DEFAULT NULL,
  `status` INT(11) DEFAULT NULL,
  PRIMARY KEY (`cid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
2.3.4.2 配置文件

1) 基础配置

properties 复制代码
# 应用名称
spring.application.name=sharding-jdbc
# 打印SQl
spring.shardingsphere.props.sql-show=true

2) 数据源配置

properties 复制代码
#===============数据源配置
#配置真实的数据源
spring.shardingsphere.datasource.names=db1

#数据源1
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.128:3306/course_db?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

3) 数据节点配置

先指定t_course_1表试试

properties 复制代码
#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_1     

4) 完整配置文件

properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
# 打印SQl
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names = db1

#数据源1
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.128:3306/course_db?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

#配置数据节点
# 标准分片表配置
# 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_1
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.3.4.3 测试
  • course类
java 复制代码
@TableName("t_course")
@Data
@ToString
public class Course implements Serializable {

    @TableId
    private Long cid;

    private Long userId;

    private String cname;

    private String brief;

    private double price;

    private int status;
}
  • CourseMapper
java 复制代码
@Mapper
public interface CourseMapper extends BaseMapper<Course> {
}
java 复制代码
    //水平分表测试
    @Autowired
    private CourseMapper courseMapper;

    @Test
    public void testInsertCourse(){

        for (int i = 0; i < 3; i++) {
            Course course = new Course();
            course.setCid(10086L+i);
            course.setUserId(1L+i);
            course.setCname("Java经典面试题讲解");
            course.setBrief("课程涵盖目前最容易被问到的10000道Java面试题");
            course.setPrice(100.0);
            course.setStatus(1);

            courseMapper.insert(course);
        }
    }

插入数据情况

2.3.4.4 行表达式

对上面的配置操作进行修改, 使用inline表达式,灵活配置数据节点

行表达式的使用: https://shardingsphere.apache.org/document/5.1.1/cn/features/sharding/concept/inline-expression/)

properties 复制代码
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_$->{1..2}

表达式 db1.t_course_$->{1..2}

复制代码
$  会被 大括号中的 `{1..2}` 所替换, ` ${begin..end}` 表示范围区间

会有两种选择:  **db1.t_course_1** 和  **db1.t_course_2**
2.3.4.5 配置分片策略

分片策略包括分片键和分片算法.

分片规则,约定cid值为偶数时,添加到t_course_1表,如果cid是奇数则添加到t_course_2表

  • 配置分片策略
properties 复制代码
#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_$->{1..2}

##2.配置分片策略(分片策略包括分片键和分片算法)
#2.1 分片键名称: cid
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid
#2.2 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=table-inline
#2.3 分片算法类型: 行表达式分片算法(标准分片算法下包含->行表达式分片算法)
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.type=INLINE
#2.4 分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.props.algorithm-expression=t_course_$->{cid % 2 + 1}

测试:

完整配置文件

properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
# 打印SQl
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names = db1

#数据源1
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.128:3306/course_db?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456




#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_$->{1..2}

##2.配置分片策略(分片策略包括分片键和分片算法)
#2.1 分片键名称: cid
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid
#2.2 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=table-inline
#2.3 分片算法类型: 行表达式分片算法(标准分片算法下包含->行表达式分片算法)
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.type=INLINE
#2.4 分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.props.algorithm-expression=t_course_$->{cid % 2 + 1}



mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
java 复制代码
    @Test
    public void testInsertCourse(){

        for (int i = 0; i < 20; i++) {
            Course course = new Course();
            course.setCid(10086L+i);
            course.setUserId(1L+i);
            course.setCname("Java经典面试题讲解");
            course.setBrief("课程涵盖目前最容易被问到的10000道Java面试题");
            course.setPrice(100.0);
            course.setStatus(1);

            courseMapper.insert(course);
        }
    }

数据分布情况:

2.3.4.6 分布式序列算法

在水平分表中由于数据会存储到多个表中,每个表有独立的主键,也就是说有可能会发生主键重复的情况,所以就不能使用MySQL默认的主键自增应该使用分布式ID作为主键值,确保每个主键都是唯一的

雪花算法:

https://shardingsphere.apache.org/document/5.1.1/cn/features/sharding/concept/key-generator/

水平分片需要关注全局序列,因为不能简单的使用基于数据库的主键自增。

这里有两种方案:一种是基于MyBatisPlus的id策略;一种是ShardingSphere-JDBC的全局序列配置。

  • 基于MyBatisPlus的id策略:将Course类的id设置成如下形式
java 复制代码
@TableName("t_course")
@Data
@ToString
public class Course imp {

    @TableId(value = "cid",type = IdType.ASSIGN_ID)
    private Long cid;

    private Long userId;

    private String cname;

    private String brief;

    private double price;

    private int status;
}
  • 基于ShardingSphere-JDBC的全局序列配置:和前面的MyBatisPlus的策略二选一
properties 复制代码
#3.分布式序列配置
#3.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.column=cid
#3.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.key-generator-name=alg_snowflake
#3.3 分布式序列-算法类型
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE

# 分布式序列算法属性配置,可以先不配置
#spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.props.xxx=

此时,需要将实体类中的id策略修改成以下形式:

java 复制代码
//当配置了shardingsphere-jdbc的分布式序列时,自动使用shardingsphere-jdbc的分布式序列
//当没有配置shardingsphere-jdbc的分布式序列时,自动依赖数据库的主键自增策略
@TableId(type = IdType.AUTO)

测试:

java 复制代码
    @Test
    public void testInsertCourse(){

        for (int i = 0; i < 20; i++) {
            Course course = new Course();
            course.setUserId(1L+i);
            course.setCname("Java经典面试题讲解");
            course.setBrief("课程涵盖目前最容易被问到的10000道Java面试题");
            course.setPrice(100.0);
            course.setStatus(1);

            courseMapper.insert(course);
        }
    }

2.3.5 实现水平分库

水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。接下来看一下如何使用Sharding-JDBC实现水平分库

2.3.5.1 数据准备
  1. 创建数据库

    在mysql-server01服务器上, 创建数据库 course_db0, 在mysql-server02服务器上, 创建数据库 course_db1

  1. 分别在course_db0和course_db1中创建表t_course_0
sql 复制代码
CREATE TABLE `t_course_0` (
  `cid` bigint(20) NOT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  `corder_no` bigint(20) DEFAULT NULL,
  `cname` varchar(50) DEFAULT NULL,
  `brief` varchar(50) DEFAULT NULL,
  `price` double DEFAULT NULL,
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  1. 实体类原有的Course类添加一个 corder_no 即可.

如果使用了ShardingJDBC的分布式序列,ShardingJDBC会自动生成id,如果没有配置就自动依赖mybatisplus设置的主键自增IdType.AUTO

java 复制代码
@TableName("t_course")
@Data
@ToString
public class Course implements Serializable {

    @TableId(value = "cid",type = IdType.AUTO)
    private Long cid;

    private Long userId;

    private Long corderNo;

    private String cname;

    private String brief;

    private double price;

    private int status;
}
2.3.5.2 配置文件

1) 数据源配置

properties 复制代码
# 定义多个数据源
spring.shardingsphere.datasource.names = db0,db1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/course_db0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/course_db1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

2) 数据节点配置

先测试水平分库, 数据节点中数据源是动态的, 数据表固定为t_course_0, 方便测试

  • db$->{0...1}.t_course_0,表示数据库是动态的,db由db0、db1组成,表就是t_course_0一个表
properties 复制代码
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_0

3) 水平分库之分库策略配置

分库策略: 以user_id为分片键,分片策略为user_id % 2,user_id为偶数操作db0数据源,否则操作db1数据源。

properties 复制代码
#===============水平分库-分库策略==============
##2.配置分片策略(分片策略包括分片键和分片算法)
#2.1 分片键名称: user_id
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-column=user_id
#2.2 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-algorithm-name=table-inline
#2.3 分片算法类型: 行表达式分片算法(标准分片算法下包含->行表达式分片算法)
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.type=INLINE
#2.4 分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.props.algorithm-expression=db$->{user_id % 2}

4) 分布式主键自增

properties 复制代码
#3.分布式序列配置
#3.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.column=cid
#3.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.key-generator-name=alg_snowflake
#3.3 分布式序列-算法类型
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE

5) 测试

完成配置文件

properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
# 打印SQl
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names = db0,db1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/course_db0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/course_db1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456



#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_0

##2.配置分片策略(分片策略包括分片键和分片算法)
#2.1 分片键名称: user_id
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-column=user_id
#2.2 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-algorithm-name=table-inline
#2.3 分片算法类型: 行表达式分片算法(标准分片算法下包含->行表达式分片算法)
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.type=INLINE
#2.4 分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.props.algorithm-expression=db$->{user_id % 2}

#3.分布式序列配置
#3.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.column=cid
#3.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.key-generator-name=alg_snowflake
#3.3 分布式序列-算法类型
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
java 复制代码
    /**
     * 水平分库 --> 分库插入数据
     */
    @Test
    public void testInsertCourseDB(){

        for (int i = 0; i < 10; i++) {
            Course course = new Course();
            course.setUserId(1001L+i);
            course.setCname("Java经典面试题讲解");
            course.setBrief("课程涵盖目前最容易被问到的10000道Java面试题");
            course.setPrice(100.0);
            course.setStatus(1);
            courseMapper.insert(course);
        }
    }

数据分布情况:可以看到偶数都分布到了course_db0上

6) 水平分库之分表策略配置

可以分库和分表的分片策略同时设置

分库规则:以user_id为分片键,分片策略为user_id % 2,user_id为偶数操作db0数据源,否则操作db1数据源。

分表规则:t_course 表中 cid 的哈希值为偶数时,数据插入对应服务器的t_course_0表,cid 的哈希值为奇数时,数据插入对应服务器的t_course_1

  1. 修改数据节点配置,数据落地到dn0或db1数据源的 t_course_0表 或者 t_course_1表.
properties 复制代码
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_$->{0..1}
  1. 分别在两个库中再创建一个t_course_1表
sql 复制代码
CREATE TABLE `t_course_1` (
  `cid` bigint(20) NOT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  `corder_no` bigint(20) DEFAULT NULL,
  `cname` varchar(50) DEFAULT NULL,
  `brief` varchar(50) DEFAULT NULL,
  `price` double DEFAULT NULL,
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  1. 分表策略配置 (对id进行哈希取模)
properties 复制代码
#===============水平分库-分表策略==============
#----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid
##----分片算法配置----
##分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=inline-hash-mod
#分片算法类型
spring.shardingsphere.rules.sharding.sharding-algorithms.inline-hash-mod.type=INLINE
#分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.inline-hash-mod.props.algorithm-expression=t_course_$->{Math.abs(cid.hashCode()) % 2}

完整配置文件

properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
# 打印SQl
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names = db0,db1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/course_db0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/course_db1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456



#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_$->{0..1}

##2.配置分片策略(分片策略包括分片键和分片算法)
#2.1 分片键名称: user_id
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-column=user_id
#2.2 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-algorithm-name=table-inline
#2.3 分片算法类型: 行表达式分片算法(标准分片算法下包含->行表达式分片算法)
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.type=INLINE
#2.4 分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.props.algorithm-expression=db$->{user_id % 2}

#3.分布式序列配置
#3.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.column=cid
#3.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.key-generator-name=alg_snowflake
#3.3 分布式序列-算法类型
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE

#4===============水平分库-分表策略==============
#4.1----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid
##----分片算法配置----
##4.2分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=inline-hash-mod
#4.3分片算法类型
spring.shardingsphere.rules.sharding.sharding-algorithms.inline-hash-mod.type=INLINE
#4.4分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.inline-hash-mod.props.algorithm-expression=t_course_$->{Math.abs(cid.hashCode()) % 2}

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

官方提供分片算法配置

https://shardingsphere.apache.org/document/current/cn/dev-manual/sharding/

properties 复制代码
#----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid

#----分片算法配置----
#分片算法名称 -> 取模分片算法
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=table-hash-mod
#分片算法类型
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.type=HASH_MOD
#分片算法属性配置-分片数量,有两个表值设置为2
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.props.sharding-count=2
2.3.5.3 水平分库测试
  1. 测试插入数据
java 复制代码
    //水平分库 --> 分表策略
    @Test
    public void testInsertCourseTable(){

        for (int i = 100; i < 150; i++) {
            Course course = new Course();
            course.setUserId(1L+i);
            course.setCname("Java面试题详解");
            course.setCorderNo(1000L+i);
            course.setBrief("经典的10000道面试题");
            course.setPrice(100.00);
            course.setStatus(1);

            courseMapper.insert(course);
        }
    }

    @Test
    public void testHashMod(){
        //cid的hash值为偶数时,插入对应数据库的t_course_0表,为奇数插入对应数据库的t_course_1
        Long cid = 1175196313105465345L;  //获取到cid
        int hash = cid.hashCode();
        System.out.println(hash);
        System.out.println("===========" + Math.abs(hash  % 2) );  //获取针对cid进行hash取模后的值
    }

两台服务器中的两个表都已经有数据

2.3.5.4 水平分库查询

下边来测试查询,看看shardingjdbc是怎么把多个表的数据汇总查询出来的

java 复制代码
    //查询所有记录
    @Test
    public void testShardingSelectAll(){
        List<Course> courseList = courseMapper.selectList(null);
        courseList.forEach(System.out::println);
    }
  • 查看日志: 查询了两个数据源,每个数据源中使用UNION ALL连接两个表
java 复制代码
    //根据user_id进行查询
    @Test
    public void testSelectByUserId(){
        QueryWrapper<Course> courseQueryWrapper = new QueryWrapper<>();
        courseQueryWrapper.eq("user_id",2L);
        List<Course> courses = courseMapper.selectList(courseQueryWrapper);

        courses.forEach(System.out::println);
    }
  • 查看日志: 查询了一个数据源,使用UNION ALL连接数据源中的两个表
2.3.5.5 分片算法HASH_MOD和MOD

HASH_MOD和MOD是shardingjdbc中自带的分片算法

上边水平分库使用的是**t_course_ -\>{Math.abs(cid.hashCode()) % 2}\*\*取cid的hash值取模进行分库,shardingjdbc有个集成的分片规律HASH_MOD,就和t_course_ ->{Math.abs(cid.hashCode()) % 2}分片规律差不多

分表的策略db$->{user_id % 2}可以用shardingjdbc中的MOD来替代,效果都是一样

HASH_MOD配置文件内容:

properties 复制代码
#----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid

#----分片算法配置----
#分片算法名称 -> 取模分片算法
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=table-hash-mod
#分片算法类型
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.type=HASH_MOD
#分片算法属性配置-分片数量,有两个表值设置为2
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.props.sharding-count=2

MOD配置文件内容:

properties 复制代码
###2.配置分片策略(分片策略包括分片键和分片算法)
##2.1 分片键名称: user_id
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-column=user_id
##2.2 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-algorithm-name=table-mod
#2.3 --分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.type=MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.props.sharding-count=2

完整配置文件内容:

properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
# 打印SQl
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names = db0,db1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/course_db0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/course_db1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456



#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_$->{0..1}


###2.配置分片策略(分片策略包括分片键和分片算法)
##2.1 分片键名称: user_id
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-column=user_id
##2.2 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-algorithm-name=table-mod
#2.3 --分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.type=MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.props.sharding-count=2
#3.分布式序列配置
#3.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.column=cid
#3.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.key-generator-name=alg_snowflake
#3.3 分布式序列-算法类型
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE


#----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid

#4===============水平分库-分表策略==============
#4.1----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=table-hash-mod
###----分片算法配置----
#4.2分片算法类型
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.type=HASH_MOD
#4.3分片算法属性配置-分片数量,有两个表值设置为2
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.props.sharding-count=2

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

测试:

java 复制代码
    @Test
    public void testInsertCourseTable() throws InterruptedException {

        for (int i = 100; i < 150; i++) {
            Thread.sleep(10);
            Course course = new Course();
            course.setUserId(1L+i);
            course.setCname("Java面试题详解");
            course.setCorderNo(1000L+i);
            course.setBrief("经典的10000道面试题");
            course.setPrice(100.00);
            course.setStatus(1);

            courseMapper.insert(course);
        }
    }
2.3.5.6 水平分库总结

水平分库包含了分库策略和分表策略.

  • 分库策略 ,目的是将一个逻辑表 , 映射到多个数据源
properties 复制代码
#===============水平分库-分库策略==============
#----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-column=user_id

#----分片算法配置----
#分片算法名称 -> 行表达式分片算法
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.standard.sharding-algorithm-name=table-inline

#分片算法类型
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.type=INLINE

#分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.props.algorithm-expression=db$->{user_id % 2}
  • 分表策略, 如何将一个逻辑表 , 映射为多个 实际表
properties 复制代码
#===============水平分库-分表策略==============
#----分片列名称----
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=cid

##----分片算法配置----
#分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=inline-hash-mod

#分片算法类型
spring.shardingsphere.rules.sharding.sharding-algorithms.inline-hash-mod.type=INLINE

#分片算法属性配置
spring.shardingsphere.rules.sharding.sharding-algorithms.inline-hash-mod.props.algorithm-expression=t_course_$->{Math.abs(cid.hashCode()) % 2}

2.3.6 实现绑定表

先来回顾一下绑定表的概念: 指的是分片规则一致的关系表(主表、子表),例如t_order和t_order_item,均按照order_id分片,则此两个表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,可以提升关联查询效率。

注: 绑定表是建立在多表关联的基础上的.所以我们先来完成多表关联的配置

2.3.6.1 数据准备

先在两个服务器中分别创建shardingjdbc这个数据库,具体库和表如下

server01:

server02:

  1. 创建表在server01服务器上的 shardingjdbc0 数据库 和 server02服务器上的 shardingjdbc1 数据库分别创建 t_ordert_order_item表 ,表结构如下:
sql 复制代码
CREATE TABLE `t_order_0` (
  `order_id` bigint NOT NULL COMMENT '订单ID',
  `user_id` int NOT NULL COMMENT '用户ID',
  `product_name` varchar(255) NOT NULL COMMENT '商品名称',
  `total_price` decimal(10,2) NOT NULL COMMENT '总价',
  `status` varchar(50) DEFAULT 'CREATED' COMMENT '订单状态',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`order_id`)
);
CREATE TABLE `t_order_1` (
  `order_id` bigint NOT NULL COMMENT '订单ID',
  `user_id` int NOT NULL COMMENT '用户ID',
  `product_name` varchar(255) NOT NULL COMMENT '商品名称',
  `total_price` decimal(10,2) NOT NULL COMMENT '总价',
  `status` varchar(50) DEFAULT 'CREATED' COMMENT '订单状态',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`order_id`)
);


CREATE TABLE `t_order_item_0` (
  `item_id` bigint NOT NULL COMMENT '订单项ID',
  `order_id` bigint NOT NULL COMMENT '订单ID',
  `product_name` varchar(255) NOT NULL COMMENT '商品名称',
  `price` decimal(10,2) NOT NULL COMMENT '单价',
  `user_id` int NOT NULL COMMENT '用户ID',
  `quantity` int NOT NULL COMMENT '数量',
  `total_price` decimal(10,2) NOT NULL COMMENT '小计',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`item_id`)
);
CREATE TABLE `t_order_item_1` (
  `item_id` bigint NOT NULL COMMENT '订单项ID',
  `order_id` bigint NOT NULL COMMENT '订单ID',
  `product_name` varchar(255) NOT NULL COMMENT '商品名称',
  `price` decimal(10,2) NOT NULL COMMENT '单价',
  `user_id` int NOT NULL COMMENT '用户ID',
  `quantity` int NOT NULL COMMENT '数量',
  `total_price` decimal(10,2) NOT NULL COMMENT '小计',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`item_id`)
);
2.3.6.2 创建实体类
java 复制代码
@TableName("t_order")
@Data
@ToString
public class TOrder {

    @TableId
    private Long orderId;

    private Integer userId;

    private String productName;

    private BigDecimal totalPrice;

    private String status;

    private LocalDateTime createTime;
}
java 复制代码
@TableName("t_order_item")
@Data
@ToString
public class TOrderItem {
    @TableId
    private Long itemId;

    private Long orderId;

    private String productName;

    private BigDecimal price;

    private Integer quantity;

    private Integer userId;

    private BigDecimal totalPrice;

    private LocalDateTime createTime;
}
2.3.6.3 创建mapper
java 复制代码
@Mapper
public interface TOrderMapper extends BaseMapper<TOrder> {
}
java 复制代码
@Mapper
public interface TOrderItemMapper extends BaseMapper<TOrderItem> {
}
2.3.6.4 配置多表关联

t_order的分片表、分片策略、分布式序列策略和t_order_item保持一致

  • 数据源
properties 复制代码
# 定义多个数据源
spring.shardingsphere.datasource.names = db0,db1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/shardingjdbc1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456
  • 数据节点
properties 复制代码
#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=db$->{0..1}.t_order_$->{0..1}
spring.shardingsphere.rules.sharding.tables.t_order_item.actual-data-nodes=db$->{0..1}.t_order_item_$->{0..1}
  • 分库策略
properties 复制代码
#2.=========水平分库-分库策略========
##     t_order 分库策略
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-column=user_Id
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-algorithm-name=table-mod

# ----t_course_section分库策略
spring.shardingsphere.rules.sharding.tables.t_order_item.database-strategy.standard.sharding-column=user_Id
spring.shardingsphere.rules.sharding.tables.t_order_item.database-strategy.standard.sharding-algorithm-name=table-mod
  • 分表策略
properties 复制代码
#================水平分库-分表策略
##     t_order 分表策略
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=order_Id
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=table-hash-mod
##     t_order_item 分表策略
spring.shardingsphere.rules.sharding.tables.t_order_item.table-strategy.standard.sharding-column=order_Id
spring.shardingsphere.rules.sharding.tables.t_order_item.table-strategy.standard.sharding-algorithm-name=table-hash-mod
  • 分片算法
properties 复制代码
#3.=========分片算法========
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.type=MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.props.sharding-count=2

spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.type=HASH_MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.props.sharding-count=2

完整配置文件

properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
# 打印SQl
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names = db0,db1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/shardingjdbc1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456



#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=db$->{0..1}.t_order_$->{0..1}
spring.shardingsphere.rules.sharding.tables.t_order_item.actual-data-nodes=db$->{0..1}.t_order_item_$->{0..1}


#2.=========水平分库-分库策略========
##     t_order 分库策略
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-column=user_Id
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-algorithm-name=table-mod

# ----t_course_section分库策略
spring.shardingsphere.rules.sharding.tables.t_order_item.database-strategy.standard.sharding-column=user_Id
spring.shardingsphere.rules.sharding.tables.t_order_item.database-strategy.standard.sharding-algorithm-name=table-mod

#================水平分库-分表策略
##     t_order 分表策略
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=order_Id
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=table-hash-mod
##     t_order_item 分表策略
spring.shardingsphere.rules.sharding.tables.t_order_item.table-strategy.standard.sharding-column=order_Id
spring.shardingsphere.rules.sharding.tables.t_order_item.table-strategy.standard.sharding-algorithm-name=table-hash-mod


#3.=========分片算法========
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.type=MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.props.sharding-count=2

spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.type=HASH_MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.table-hash-mod.props.sharding-count=2

#3.分布式序列配置
#3.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.column=cid
#3.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.key-generator-name=alg_snowflake

#3.分布式序列配置
#3.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_order_item.key-generate-strategy.column=item_id
#3.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_order_item.key-generate-strategy.key-generator-name=alg_snowflake_item

#3.3 分布式序列-算法类型
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE




mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

spring.shardingsphere.rules.sharding.binding-tables[0]=t_order,t_order_item
2.3.6.5 测试插入数据
java 复制代码
@Test
    public void createOrderAndItems() {
        for (int j = 0; j < 10; j++) {
            // 1. 创建订单主表记录
            TOrder order = new TOrder();
            order.setUserId(1+j);
            order.setProductName("iPhone 15 Pro");
            order.setTotalPrice(new BigDecimal("8999.00"));
            order.setStatus("CREATED");
            order.setCreateTime(LocalDateTime.now());

            // 插入主表
            orderMapper.insert(order);
            System.out.println("主单已插入: " + order);

            // 2. 创建订单项(多个商品)
            for (int i = 1; i <= 2; i++) {
                TOrderItem item = new TOrderItem();
                item.setOrderId(order.getOrderId());
                item.setProductName("配件" + i);
                item.setPrice(new BigDecimal("99.00"));
                item.setQuantity(1);
                item.setUserId(order.getUserId());
                item.setTotalPrice(new BigDecimal("99.00"));
                item.setCreateTime(LocalDateTime.now());

                orderItemMapper.insert(item);
                System.out.println("订单项已插入: " + item);
            }
        }

    }
2.3.6.6 配置绑定表

需求说明: 查询每个订单的订单号和订单名称和购买数量

  1. 根据需求编写SQL
sql 复制代码
SELECT t_order.order_Id,t_order.product_name,t_order_item.quantity FROM t_order INNER join t_order_item on t_order.order_Id = t_order_item.order_Id
  1. 创建DTO类
java 复制代码
@Data
public class TOrderDTO {

    @TableId
    private Long orderId;
    private String productName;
    private Integer quantity;
}
  1. 添加Mapper方法
java 复制代码
@Mapper
public interface TOrderMapper extends BaseMapper<TOrder> {

    @Select("SELECT t_order.order_Id,t_order.product_name,t_order_item.quantity FROM t_order INNER join t_order_item on t_order.order_Id = t_order_item.order_Id ")
    List<TOrderDTO> findItemNamesByOrderId();
}
  1. 进行关联查询
java 复制代码
    @Test
    public void findItemNamesByOrderId(){
        List<TOrderDTO> tOrderDTOS = orderMapper.findItemNamesByOrderId();
        tOrderDTOS.forEach(System.out::println);

    }
  • **如果不配置绑定表:测试的结果为8个SQL。**多表关联查询会出现笛卡尔积关联。
  1. 配置绑定表

https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/spring-boot-starter/rules/sharding/

properties 复制代码
#======================绑定表
spring.shardingsphere.rules.sharding.binding-tables[0]=t_course,t_course_section
  • 如果配置绑定表:测试的结果为4个SQL。 多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
2.3.6.7 总结

order 表和 order_item 表为例,它们在每个数据库中都存在多个分片表。如果两者的分片策略完全一致(即使用相同的分片键和分片算法),必须将它们配置为绑定表(Binding Tables)。否则,在执行多表 JOIN 查询时,ShardingSphere-JDBC 无法确定哪些具体的分片表之间存在对应关系,只能对所有分片组合进行笛卡尔积式的关联,导致查询性能急剧下降,甚至引发不必要的全节点扫描。

2.3.7 实现广播表(公共表)

2.3.7.1 公共表介绍

公共表属于系统中数据量较小,变动少,而且属于高频联合查询的依赖表。参数表、数据字典表等属于此类型。

可以将这类表在每个数据库都保存一份,所有更新操作都同时发送到所有分库执行。接下来看一下如何使用Sharding-JDBC实现公共表的数据维护。

2.3.7.2 代码编写

1) 创建表

分别在 msb_course_db0 , msb_course_db1 ,msb_user_db 都创建 t_district

sql 复制代码
-- 区域表
CREATE TABLE t_district  (
  id BIGINT(20) PRIMARY KEY COMMENT '区域ID',
  district_name VARCHAR(100) COMMENT '区域名称',
  LEVEL INT COMMENT '等级'
);

2) 创建实体类

java 复制代码
@TableName("t_district")
@Data
public class District {

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String districtName;

    private int level;
}

3) 创建mapper

java 复制代码
@Mapper
public interface DistrictMapper extends BaseMapper<District> {
}
2.3.7.3 广播表配置
  • 数据源
properties 复制代码
# 应用名称
spring.application.name=shardingsphere-jdbc-table
# 打印SQl
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names = db0,db1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/shardingjdbc1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

#数据节点可不配置,默认情况下,向所有数据源广播
spring.shardingsphere.rules.sharding.tables.t_district.actual-data-nodes=db$->{0..1}.t_district

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
  • 广播表配置
properties 复制代码
#------------------------广播表配置
# 广播表规则列表
spring.shardingsphere.rules.sharding.broadcast-tables[0]=t_district
2.3.7.4 测试广播表
java 复制代码
    //广播表: 插入数据 两个数据源中都会插入
    @Test
    public void testBroadcast(){
        District district = new District();
        district.setDistrictName("昌平区");
        district.setLevel(1);

        districtMapper.insert(district);
    }



    //查询操作,只从一个节点获取数据, 随机负载均衡规则
    @Test
    public void testSelectBroadcast(){

        List<District> districtList = districtMapper.selectList(null);
        districtList.forEach(System.out::println);
    }
2.3.7.5 总结

由于 ShardingSphere-JDBC 不支持跨数据库实例(即跨数据源)的 JOIN 操作 ,所有关联查询必须在同一个数据库节点内完成。当 SQL 中涉及分片表与非分片表(如字典表)的 JOIN 时,若该非分片表未被配置为 广播表(Broadcast Table) ,ShardingSphere 将无法确定其所在的数据节点,导致路由失败并抛出异常。因此,此类公共表必须显式配置为广播表,使其在每个数据库实例中都存在完整副本,从而确保 JOIN 查询能在单个节点内正确执行。

2.4 读写分离详解与实战

2.4.1 读写分离架构介绍

2.4.1.1 读写分离原理

**读写分离原理:**读写分离就是让主库处理事务性操作,从库处理select查询。数据库复制被用来把事务性查询导致的数据变更同步到从库,同时主库也可以select查询。

注意: 读写分离的数据节点中的数据内容是一致。

读写分离的基本实现:

  • 主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
  • 读写分离是根据 SQL 语义的分析将读操作和写操作分别路由至主库与从库。
  • 通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。
  • 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行

将用户表的写操作和读操路由到不同的数据库

复制代码
				![](https://cdn.nlark.com/yuque/0/2025/jpg/29416702/1765370706319-6ef58fb7-77e1-4766-bd33-85cbc1a7c0b5.jpg)  
2.4.1.2 读写分离应用方案

在数据量不是很多的情况下,我们可以将数据库进行读写分离,以应对高并发的需求,通过水平扩展从库,来缓解查询的压力。如下:

分表+读写分离

在数据量达到500万的时候,这时数据量预估千万级别,我们可以将数据进行分表存储。

分库分表+读写分离

在数据量继续扩大,这时可以考虑分库分表,将数据存储在不同数据库的不同表中,如下:

读写分离虽然可以提升系统的吞吐量和可用性,但同时也带来了数据不一致的问题,包括多个主库之间的数据一致性,以及主库与从库之间的数据一致性的问题。 并且,读写分离也带来了与数据分片同样的问题,它同样会使得应用开发和运维人员对数据库的操作和运维变得更加复杂。

透明化读写分离所带来的影响,让使用方尽量像使用一个数据库一样使用主从数据库集群,是ShardingSphere读写分离模块的主要设计目标。

主库、从库、主从同步、负载均衡

  • 核心功能
    • 提供一主多从的读写分离配置。仅支持单主库,可以支持独立使用,也可以配合分库分表使用
    • 独立使用读写分离,支持SQL透传。不需要SQL改写流程
    • 同一线程且同一数据库连接内,能保证数据一致性。如果有写入操作,后续的读操作均从主库读取。
    • 基于Hint的强制主库路由。可以强制路由走主库查询实时数据,避免主从同步数据延迟。
  • 不支持项
    • 主库和从库的数据同步
    • 主库和从库的数据同步延迟
    • 主库双写或多写
    • 跨主库和从库之间的事务的数据不一致。建议在主从架构中,事务中的读写均用主库操作。

2.4.2 CAP 理论

2.4.2.1 CAP理论介绍

CAP 定理(CAP theorem)又被称作布鲁尔定理(Brewer's theorem),是加州大学伯克利分校的计算机科学家埃里克·布鲁尔(Eric Brewer)在 2000 年的 ACM PODC 上提出的一个猜想。对于设计分布式系统的架构师来说,CAP 是必须掌握的理论。

在一个分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

  • C 一致性(Consistency):等同于所有节点访问同一份最新的数据副本

在分布式环境中,数据在多个副本之间能够保持一致的特性,也就是所有的数据节点里面的数据要是一致的

  • A 可用性(Availability):每次请求都能够获取到非错的响应(不是错误和超时的响应) , 但是不能够保证获取的数据为最新的数据.

意思是只要收到用户的请求,服务器就必须给出一个成功的回应. 不要求数据是否是最新的.

  • P 分区容错性(Partition Tolerance):以实际效果而言,分区相当于对通信的时限要求. 系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况 , 必须对当前操作在C和A之间作出选择.

更简单的理解就是: 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败(可能是丢包,也可能是连接中断,还可能是拥塞) ,但是系统能够继续"履行职责" 正常运行.

一般来说,分布式系统,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。根据CAP 定理,剩下的 C 和 A 无法同时做到。

2.4.2.2 CAP理论特点

CAP如何取舍

  • CAP理论的C也就是一致性,不等于事务ACID中的C(数据的一致性), CAP理论中的C可以理解为副本的一致性.即所有的副本的结果都是有一致的.
  • 在没有网络分区的单机系统中可以选择保证CA, 但是在分布式系统中存在网络通信环节,网络通信在多机中是不可靠的,P是必须要选择的,为了 保证P就需要在C和A之间作出选择

假设有三个副本,写入时有下面两个方案

方案一: W=1, 一写,向三个副本写入,只要一个副本写入成功,即认为成功

一写的情况下,只要写入一个副本成功即可返回写入成功,出现网络分区后,三台机器的数据就有可能出现不一致, 无法保证C. (比如server1与其他节点的网络中断了,那S1与S2 S3 就不一致的了), 但是因为可以正常返回写入成功,A依旧可以保证.

方案二: W=2, 三写,向三个副本写入,三个副本写入成功,才认为是成功

在三写的情况下,要三个副本都写入成功,才可以返回成功,出现网络分区后,无法实现这一点,最终会返回报错,所以没有保证A,但是保证了C.

2.4.2.3 分布式数据库对于CAP理论的实践

从上面的分析我们可以总结出来: 在分布式环境中,P是一定存在的,一旦出现了网络分区,那么一致性和可用性就一定要抛弃一个.

  • 对于NoSQL数据库,更加注重可用性,所以会是一个AP系统.
  • 对于分布式关系型数据库,必须要保证一致性,所以会是一个CP系统.

分布式关系型数据库仍有高可用性需求,虽然达不到CAP理论中的100%可用性,单一般都具备五个9(99.999%) 以上的高可用.

  • **计算公式: A表示可用性; MTBF表示平均故障间隔; MTTR表示平均恢复时间 **

  • 高可用有一个标准,9越多代表越容错, 可用性越高.

假设系统一直能够提供服务,我们说系统的可用性是100%。如果系统每运行100个时间单位,会有1个时间单位无法提供服务,我们说系统的可用性是99%。很多公司的高可用目标是4个9,也就是99.99%

我们可以将分布式关系型数据库看做是CP+HA的系统.由此也产生了两个广泛的应用指标.

  • **RPO(Recovery PointObjective): ** 恢复点目标,指数据库在灾难发生后会丢失多长时间的数据.分布式关系型数据库RPO=0.
  • RTO(Recovery Time Objective): 恢复时间目标,指数据库在灾难发生后到整个系统恢复正常所需要的时间.分布式关系型数据库RTO < 几分钟(因为有主备切换,所以一般恢复时间就是几分钟).

总结一下: CAP理论并不是让我们选择C或者选择A就完全抛弃另外一个, 这样极端显然是不对的,实际上在设计一个分布式系统时,P是必须的,所以要在AC中取舍一个"降级"。根据不同场景来取舍A或者C.

2.4.3 MySQL主从同步

2.4.3.1 主从同步原理

读写分离是建立在MySQL主从复制基础之上实现的,所以必须先搭建MySQL的主从复制架构。

主从复制的用途

  • 实时灾备,用于故障切换
  • 读写分离,提供查询服务
  • 备份,避免影响业务

主从部署必要条件

  • 主库开启binlog日志(设置log-bin参数)
  • 主从server-id不同
  • 从库服务器能连通主库

主从复制的原理

  • Mysql 中有一种日志叫做 binlog日志(二进制日志)。这个日志会记录下所有修改了数据库的SQL 语句(insert,update,delete,create/alter/drop table, grant 等等)。
  • 主从复制的原理其实就是把主服务器上的 binlog日志复制到从服务器上执行一遍,这样从服务器上的数据就和主服务器上的数据相同了。
  1. 主库db的更新事件(update、insert、delete)被写到binlog
  2. 主库创建一个binlog dump thread,把binlog的内容发送到从库
  3. 从库启动并发起连接,连接到主库
  4. 从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log
  5. 从库启动之后,创建一个SQL线程,从relay log里面读取内容,执行读取到的更新事件,将更新内容写入到slave的db
2.4.3.2 一主一从架构搭建

Mysql的主从复制至少是需要两个Mysql的服务,当然Mysql的服务是可以分布在不同的服务器上,也可以在一台服务器上启动多个服务。

准备:

主机 角色 用户名 密码
192.168.116.129 master root 123456
192.168.116.128 slave root 123456

第一步 master中和slave创建数据库

sql 复制代码
-- 创建数据库
CREATE DATABASE itcast;

主库中配置

① 修改配置文件 /etc/my.cnf

plain 复制代码
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 -- 232-1,默认为1
server-id=1
#是否只读,1 代表只读, 0 代表读写
read-only=0
#指定同步的数据库
binlog-do-db=itcast

② 重启MySQL服务器

plain 复制代码
systemctl restart mysqld

③ 登录mysql,创建远程连接的账号,并授予主从复制权限

bash 复制代码
#创建itcast用户,并设置密码,该用户可在任意主机连接该MySQL服务
create user 'itcast'@'%' IDENTIFIED WITH mysql_native_password BY 'Itcast@123456';
#为 'itcast'@'%' 用户分配主从复制权限
GRANT REPLICATION SLAVE ON *.* TO 'itcast'@'%';

④ 通过指令,查看二进制日志坐标

sql 复制代码
show master status;

字段含义说明:

  • file : 从哪个日志文件开始推送日志文件
  • position : 从哪个位置开始推送日志
  • binlog_ignore_db : 指定不需要同步的数据库

从库配置

① 修改配置文件 /etc/my.cnf

bash 复制代码
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 -- 2^32-1,和主库不一样即可
server-id=2
#是否只读,1 代表只读, 0 代表读写
read-only=1

② 重新启动MySQL服务

sql 复制代码
systemctl restart mysqld

③ 登录mysql,设置主库配置

SOURCE_LOG_FILE和SOURCE_LOG_POS设置的是主库中刚才查询出来的

sql 复制代码
 CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.116.129', SOURCE_USER='itcast',SOURCE_PASSWORD='Itcast@123456',SOURCE_LOG_FILE='binlog.000014', SOURCE_LOG_POS=479;

上述是8.0.23中的语法。如果mysql是 8.0.23 之前的版本,执行如下SQL:

sql 复制代码
CHANGE MASTER TO MASTER_HOST='192.168.116.129', MASTER_USER='itcast', 
MASTER_PASSWORD='Itcast@123456', MASTER_LOG_FILE='binlog.000014', 
MASTER_LOG_POS=479;
参数名 含义 8.0.23之前
SOURCE_HOST 主库IP地址 MASTER_HOST
SOURCE_USER 连接主库的用户名 MASTER_USER
SOURCE_PASSWORD 连接主库的密码 MASTER_PASSWORD
SOURCE_LOG_FILE binlog日志文件名 MASTER_LOG_FILE
SOURCE_LOG_POS binlog日志文件位置 MASTER_LOG_POS

④ 开启同步操作

sql 复制代码
start replica ; #8.0.22之后
start  slave ;  #8.0.22之前

⑤ 查看主从同步状态

sql 复制代码
show replica  status ;  #8.0.22之后
show  slave  status ;   #8.0.22之前

Replica_IO_Running: Yes和Replica_SQL_Running: Yes说明配置成功

测试:

在主库中itcast数据库中执行如下:

sql 复制代码
-- 创建表
CREATE TABLE users (
  id INT(11) PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(20) DEFAULT NULL,
  age INT(11) DEFAULT NULL
); 

-- 插入数据
INSERT INTO users VALUES(NULL,'user1',20);
INSERT INTO users VALUES(NULL,'user2',21);
INSERT INTO users VALUES(NULL,'user3',22);

查看从库是否已经将users表和数据同步过来

2.4.4 Sharding-JDBC实现读写分离

Sharding-JDBC读写分离则是根据SQL语义的分析,将读操作和写操作分别路由至主库与从库。它提供透明化读写分离,让使用方尽量像使用一个数据库一样使用主从数据库集群。

2.4.4.1 数据准备

为了实现Sharding-JDBC的读写分离,首先,要进行mysql的主从同步配置。在上面的课程中我们已经配置完成了.

  • 在主服务器中的 itcast数据库 创建商品表
sql 复制代码
CREATE TABLE `products` (
  `pid` bigint(32) NOT NULL AUTO_INCREMENT,
  `pname` varchar(50) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  `flag` varchar(2) DEFAULT NULL,
  PRIMARY KEY (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 主库新建表之后,从库会根据binlog日志,同步创建.

主库:

从库:

2.4.4.2 环境准备
1) 创建实体类
java 复制代码
@TableName("products")
@Data
public class Products {

    @TableId(value = "pid",type = IdType.AUTO)
    private Long pid;

    private String pname;

    private int  price;

    private String flag;

}
2) 创建Mapper
java 复制代码
@Mapper
public interface ProductsMapper extends BaseMapper<Products> {
}
2.4.4.3 配置读写分离

https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/spring-boot-starter/rules/readwrite-splitting/

application.properties:

properties 复制代码
# 应用名称
spring.application.name=shardingjdbc-table-write-read

#===============数据源配置
# 配置真实数据源
spring.shardingsphere.datasource.names=master,slave

#数据源1
spring.shardingsphere.datasource.slave.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave.url = jdbc:mysql://192.168.116.128:3306/itcast?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.slave.username = root
spring.shardingsphere.datasource.slave.password = 123456

#数据源2
spring.shardingsphere.datasource.master.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master.url = jdbc:mysql://192.168.116.129:3306/itcast?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.master.username = root
spring.shardingsphere.datasource.master.password = 123456


# 读写分离类型,如: Static,Dynamic, ms1 包含了  m1 和 s1
spring.shardingsphere.rules.readwrite-splitting.data-sources.ms1.type=Static

# 写数据源名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.ms1.props.write-data-source-name=master

# 读数据源名称,多个从数据源用逗号分隔
spring.shardingsphere.rules.readwrite-splitting.data-sources.ms1.props.read-data-source-names=slave


# 打印SQl
spring.shardingsphere.props.sql-show=true

负载均衡相关配置

2.4.4.4 读写分离测试
java 复制代码
    //插入测试
    @Test
    public void testInsertProducts(){

        Products products = new Products();
        products.setPname("电视机");
        products.setPrice(100);
        products.setFlag("0");

        productsMapper.insert(products);
    }
java 复制代码
    @Test
    public void testSelectProducts(){

        QueryWrapper<Products> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("pname","电视机");
        List<Products> products = productsMapper.selectList(queryWrapper);

        products.forEach(System.out::println);
    }
2.4.4.5 事务读写分离测试

为了保证主从库间的事务一致性,避免跨服务的分布式事务,ShardingSphere-JDBC的主从模型中,事务中的数据读写均用主库。

  • 不添加@Transactional:insert对主库操作,select对从库操作
  • 添加@Transactional:则insert和select均对主库操作
  • **注意:**在JUnit环境下的@Transactional注解,默认情况下就会对事务进行回滚(即使在没加注解@Rollback,也会对事务回滚)
java 复制代码
//事务测试
@Transactional  //开启事务
@Test
public void testTrans(){

    Products products = new Products();
    products.setPname("洗碗机");
    products.setPrice(2000);
    products.setFlag("1");
    productsMapper.insert(products);

    QueryWrapper<Products> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("pname","洗碗机");
    List<Products> list = productsMapper.selectList(queryWrapper);
    list.forEach(System.out::println);
}

2.4.5 负载均衡算法

2.4.5.1 一主两从架构

上边再搭建一主一从时用到过129和128两个服务器,再操作之前先还原两个服务器的主从架构,分别在两个服务器上执行下边的sql

sql 复制代码
STOP SLAVE;
RESET SLAVE ALL;

准备:

主机 角色 用户名 密码
192.168.116.129 master root 123456
192.168.116.128 slave root 123456
192.168.116.130 slave root 123456

第一步 master中和slave创建数据库

sql 复制代码
-- 创建数据库
CREATE DATABASE itcast;

主库中配置

① 修改配置文件 /etc/my.cnf

plain 复制代码
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 -- 232-1,默认为1
server-id=1
#是否只读,1 代表只读, 0 代表读写
read-only=0
#指定同步的数据库
binlog-do-db=itcast

② 重启MySQL服务器

plain 复制代码
systemctl restart mysqld

③ 登录mysql,创建远程连接的账号,并授予主从复制权限

bash 复制代码
#创建itcast用户,并设置密码,该用户可在任意主机连接该MySQL服务
create user 'itcast'@'%' IDENTIFIED WITH mysql_native_password BY 'Itcast@123456';
#为 'itcast'@'%' 用户分配主从复制权限
GRANT REPLICATION SLAVE ON *.* TO 'itcast'@'%';

④ 通过指令,查看二进制日志坐标

sql 复制代码
show master status;

字段含义说明:

  • file : 从哪个日志文件开始推送日志文件
  • position : 从哪个位置开始推送日志
  • binlog_ignore_db : 指定不需要同步的数据库

从库192.168.116.128配置

① 修改配置文件 /etc/my.cnf

bash 复制代码
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 -- 2^32-1,和主库不一样即可
server-id=2
#是否只读,1 代表只读, 0 代表读写
read-only=1

② 重新启动MySQL服务

sql 复制代码
systemctl restart mysqld

③ 登录mysql,设置主库配置

SOURCE_LOG_FILE和SOURCE_LOG_POS设置的是主库中刚才查询出来的

sql 复制代码
 CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.116.129', SOURCE_USER='itcast',SOURCE_PASSWORD='Itcast@123456',SOURCE_LOG_FILE='binlog.000031', SOURCE_LOG_POS=156;

上述是8.0.23中的语法。如果mysql是 8.0.23 之前的版本,执行如下SQL:

sql 复制代码
CHANGE MASTER TO MASTER_HOST='192.168.116.129', MASTER_USER='itcast', 
MASTER_PASSWORD='Itcast@123456', MASTER_LOG_FILE='binlog.000031', 
MASTER_LOG_POS=156;
参数名 含义 8.0.23之前
SOURCE_HOST 主库IP地址 MASTER_HOST
SOURCE_USER 连接主库的用户名 MASTER_USER
SOURCE_PASSWORD 连接主库的密码 MASTER_PASSWORD
SOURCE_LOG_FILE binlog日志文件名 MASTER_LOG_FILE
SOURCE_LOG_POS binlog日志文件位置 MASTER_LOG_POS

④ 开启同步操作

sql 复制代码
start replica ; #8.0.22之后
start  slave ;  #8.0.22之前

⑤ 查看主从同步状态

sql 复制代码
show replica  status ;  #8.0.22之后
show  slave  status ;   #8.0.22之前

Replica_IO_Running: Yes和Replica_SQL_Running: Yes说明配置成功

从库192.168.116.130配置

① 修改配置文件 /etc/my.cnf

bash 复制代码
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 -- 2^32-1,和主库不一样即可
server-id=3
#是否只读,1 代表只读, 0 代表读写
read-only=1

② 重新启动MySQL服务

sql 复制代码
systemctl restart mysqld

③ 登录mysql,设置主库配置

SOURCE_LOG_FILE和SOURCE_LOG_POS设置的是主库中刚才查询出来的

sql 复制代码
 CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.116.129', SOURCE_USER='itcast',SOURCE_PASSWORD='Itcast@123456',SOURCE_LOG_FILE='binlog.000031', SOURCE_LOG_POS=156;

上述是8.0.23中的语法。如果mysql是 8.0.23 之前的版本,执行如下SQL:

sql 复制代码
CHANGE MASTER TO MASTER_HOST='192.168.116.129', MASTER_USER='itcast', 
MASTER_PASSWORD='Itcast@123456', MASTER_LOG_FILE='binlog.000031', 
MASTER_LOG_POS=156;
参数名 含义 8.0.23之前
SOURCE_HOST 主库IP地址 MASTER_HOST
SOURCE_USER 连接主库的用户名 MASTER_USER
SOURCE_PASSWORD 连接主库的密码 MASTER_PASSWORD
SOURCE_LOG_FILE binlog日志文件名 MASTER_LOG_FILE
SOURCE_LOG_POS binlog日志文件位置 MASTER_LOG_POS

④ 开启同步操作

sql 复制代码
start replica ; #8.0.22之后
start  slave ;  #8.0.22之前

⑤ 查看主从同步状态

sql 复制代码
show replica  status ;  #8.0.22之后
show  slave  status ;   #8.0.22之前

Replica_IO_Running: Yes和Replica_SQL_Running: Yes说明配置成功

测试:

在主库中执行如下:

sql 复制代码
drop database if exists itcast;
create database itcast;
use itcast;
-- 创建表
CREATE TABLE `products` (
  `pid` bigint(32) NOT NULL AUTO_INCREMENT,
  `pname` varchar(50) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  `flag` varchar(2) DEFAULT NULL,
  PRIMARY KEY (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into products values(1,'拖鞋',20,'1');
insert into products values(2,'拖鞋',20,'1');
insert into products values(3,'拖鞋',20,'1');

查看从库是否已经将products表同步过来

2.4.5.2 负载均衡测试

负载均衡算法就是用在如果有多个从库的时候决定查询哪个从库的数据,一共有如下的算法:

  • **轮询算法(ROUND_ROBIN)**原理 :按照配置的数据源列表顺序,依次轮流 地将请求分发到每一个可用的数据源上例如,有 2个读库:read_ds_0read_ds_1,则请求的分发顺序为:
plain 复制代码
请求1 → read_ds_0
请求2 → read_ds_1
请求3 → read_ds_0
请求4 → read_ds_1
  • 随机访问算法(RANDOM)每次请求时,从所有可用的读数据源中随机选择一个进行访问
  • 权重访问算法(WEIGHT)
    • 为每个数据源配置一个权重值(如 read_ds_0=3, read_ds_1=1)。
    • 使用加权随机算法(如轮盘赌算法),权重越高的数据源被选中的概率越大。
    • 例如:read_ds_0 被选中的概率是 75%,read_ds_1 是 25%

测试WEIGHT算法配置文件如下

properties 复制代码
# 应用名称
spring.application.name=shardingjdbc-table-write-read

#===============数据源配置
# 配置真实数据源
spring.shardingsphere.datasource.names=master,slave

#数据源1
spring.shardingsphere.datasource.master.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master.url = jdbc:mysql://192.168.116.128:3306/itcast?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.master.username = root
spring.shardingsphere.datasource.master.password = 123456

#数据源2
spring.shardingsphere.datasource.slave1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave1.url = jdbc:mysql://192.168.116.129:3306/itcast?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.slave1.username = root
spring.shardingsphere.datasource.slave1.password = 123456
#数据源3
spring.shardingsphere.datasource.slave2.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave2.url = jdbc:mysql://192.168.116.130:3306/itcast?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.slave2.username = root
spring.shardingsphere.datasource.slave2.password = 123456

# 打印SQl
spring.shardingsphere.props.sql-show=true
# 读写分离类型,如: Static,Dynamic, ms2 包含了  m1 和 s1 s2
spring.shardingsphere.rules.readwrite-splitting.data-sources.ms2.type=static

# 写数据源名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.ms2.props.write-data-source-name=master

# 读数据源名称,多个从数据源用逗号分隔
spring.shardingsphere.rules.readwrite-splitting.data-sources.ms2.props.read-data-source-names=slave1,slave2

# 负载均衡算法名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.ms2.load-balancer-name=alg_weight

# 负载均衡算法配置
# 负载均衡算法类型
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.type=WEIGHT
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.props.slave1=1
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.props.slave2=3
java 复制代码
    @Test
    public void testSelectProducts2(){
        for (int i = 0; i < 12; i++) {
            List<Products> products = productsMapper.selectList(null);
        }

    }

查询结果中有8次从slave2查询4次从slave1查询

RANDOM和ROUND_ROBIN配置方式如下

2.5 强制路由详解与实战

2.5.1 强制路由介绍

https://shardingsphere.apache.org/document/4.1.0/cn/manual/sharding-jdbc/usage/hint/

在一些应用场景中,分片条件并不存在于SQL,而存在于外部业务逻辑。因此需要提供一种通过在外部业务代码中指定路由配置的一种方式,在ShardingSphere中叫做Hint。如果使用Hint指定了强制分片路由,那么SQL将会无视原有的分片逻辑,直接路由至指定的数据节点操作。

Hint使用场景:

  • 数据分片操作,如果分片键没有在SQL或数据表中,而是在业务逻辑代码中
  • 读写分离操作,如果强制在主库进行某些数据操作

2.5.2 强制路由的使用

基于 Hint 进行强制路由的设计和开发过程需要遵循一定的约定,同时,ShardingSphere 也提供了专门的 HintManager 来简化强制路由的开发过程.

2.5.2.1 环境准备
  1. shardingjdbc0shardingjdbc1中创建 t_course表.
sql 复制代码
CREATE TABLE `t_course` (
  `cid` bigint(20) NOT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  `corder_no` bigint(20) DEFAULT NULL,
  `cname` varchar(50) DEFAULT NULL,
  `brief` varchar(50) DEFAULT NULL,
  `price` double DEFAULT NULL,
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2.5.2.2 代码编写
java 复制代码
@TableName("t_course")
@Data
@ToString
public class Course implements Serializable {

    @TableId(type = IdType.ASSIGN_ID)
    private Long cid;

    private Long userId;

    private Long corderNo;

    private String cname;

    private String brief;

    private double price;

    private int status;
}

CourseMapper

java 复制代码
@Repository
public interface CourseMapper extends BaseMapper<Course> {
}

自定义MyHintShardingAlgorithm类

在该类中编写分库或分表路由策略,实现HintShardingAlgorithm接口,重写doSharding方法

java 复制代码
// 泛型Long表示传入的参数是Long类型
public class MyHintShardingAlgorithm implements HintShardingAlgorithm<Long> {
    public MyHintShardingAlgorithm() {
        System.out.println("MyHintShardingAlgorithm 被创建了!");
    }

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                         HintShardingValue<Long> shardingValue) {
        Collection<String> result = new ArrayList<>();
        for (String target : availableTargetNames) {
            for (Long value : shardingValue.getValues()) {
                if (target.endsWith(String.valueOf(value % 2))) {
                    result.add(target);
                }
            }
        }
        return result;
    }

    @Override
    public void init() {
    }

    @Override
    public String getType() {
        // CLASS_BASED 模式下不会用到此 type,但实现返回值无妨
        return "MY_HINT";
    }
}

参数解析:

参数 含义
availableTargetNames 当前所有可用的数据节点名称,例如:["db0", "db1"]["t_course_0", "t_course_1"]
shardingValue 通过 hintManager.addDatabaseShardingValue(...)addTableShardingValue(...) 传入的值
Collection 实际要路由到的目标
2.5.2.3 配置文件

application.properties

properties 复制代码
# 启用 debug 日志
logging.level.org.apache.shardingsphere=DEBUG
# 应用名称
spring.application.name=shardingsphere-jdbc-table

# 打印 SQL
spring.shardingsphere.props.sql-show=true

# 定义多个数据源
spring.shardingsphere.datasource.names=db0,db1

# 数据源1
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=123456

# 数据源2
spring.shardingsphere.datasource.db1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db1.jdbc-url=jdbc:mysql://192.168.116.129:3306/shardingjdbc1?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=123456

# 默认数据源(建议保留)
spring.shardingsphere.rules.sharding.default-data-source-name=db0

# t_course 表实际数据节点
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db${0..1}.t_course_${0..1}



# 数据库分片策略 - Hint
spring.shardingsphere.rules.sharding.tables.t_course.database-strategy.hint.sharding-algorithm-name=myHint

# 表分片策略 - Hint
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.hint.sharding-algorithm-name=myHint

# 分片算法定义 - MY_HINT
spring.shardingsphere.rules.sharding.sharding-algorithms.myHint.type=CLASS_BASED
spring.shardingsphere.rules.sharding.sharding-algorithms.myHint.props.strategy=HINT
spring.shardingsphere.rules.sharding.sharding-algorithms.myHint.props.algorithmClassName=com.zhp.hint.MyHintShardingAlgorithm
2.5.2.4 强制路由到库到表测试

通过设置addDatabaseShardingValue 决定路由到哪个数据库,addTableShardingValue决定路由到哪张表

java 复制代码
@Test
    public void testHintInsert(){
        HintManager hintManager = HintManager.getInstance();
        hintManager.addDatabaseShardingValue("db", 1L);
        hintManager.addTableShardingValue("t_course", 0L);
        for (int i = 1; i < 9; i++) {
            Course course = new Course();
            course.setCid(Long.parseLong(String.valueOf(i)));
            course.setUserId(1001L+i);
            course.setCname("Java经典面试题讲解");
            course.setBrief("课程涵盖目前最容易被问到的10000道Java面试题");
            course.setPrice(100.0);
            course.setStatus(1);
            courseMapper.insert(course);
        }
    }
2.5.2.5 强制路由到库到表查询测试
java 复制代码
//测试查询
    @Test
    public void testHintSelectTable() {
        HintManager hintManager = HintManager.getInstance();
        //强制路由到db1数据库
        hintManager.addDatabaseShardingValue("db", 1L);
        //强制路由到t_course_1表
        hintManager.addTableShardingValue("t_course",0L);
        List<Course> courses = courseMapper.selectList(null);
        courses.forEach(System.out::println);
    }
2.5.2.6 强制路由走主库查询测试

在读写分离结构中,为了避免主从同步数据延迟及时获取刚添加或更新的数据,可以采用强制路由走主库查询实时数据,使用hintManager.setMasterRouteOnly设置主库路由即可。

  1. 配置文件
sql 复制代码
# 应用名称
spring.application.name=sharding-jdbc-hint01

# 定义多个数据源
spring.shardingsphere.datasource.names = m1,s1

#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/shardingjdbc1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

#主库与从库的信息
spring.shardingsphere.sharding.master-slave-rules.ms1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.ms1.slave-data-source-names=s1

#配置数据节点
spring.shardingsphere.sharding.tables.products.actual-data-nodes = ms1.products

# 打印SQl
spring.shardingsphere.props.sql-show=true
  1. 测试
java 复制代码
//强制路由走主库
@Test
public void testHintReadTableToMaster() {
    HintManager hintManager = HintManager.getInstance();
    hintManager.setMasterRouteOnly();

    List<Products> products = productsMapper.selectList(null);
    products.forEach(System.out::println);
}
2.5.2.7 SQL执行流程剖析

ShardingSphere 3个产品的数据分片功能主要流程是完全一致的,如下图所示。

  • SQL解析SQL解析分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。 Sharding-JDBC采用不同的解析器对SQL进行解析,解析器类型如下:
    • MySQL解析器
    • Oracle解析器
    • SQLServer解析器
    • PostgreSQL解析器
    • 默认SQL解析器
  • 查询优化
    负责合并和优化分片条件,如OR等。
  • SQL路由根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
  • SQL改写将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。
  • SQL执行通过多线程执行器异步执行SQL。
  • 结果归并将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。

2.6 数据加密详解与实战

2.6.1 数据加密介绍

数据加密(数据脱敏) 是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、卡号、客户号等个人信息按照规定,都需要进行数据脱敏。

数据加密模块属于ShardingSphere分布式治理这一核心功能下的子功能模块。

  • Apache ShardingSphere 通过对用户输入的 SQL 进行解析,并依据用户提供的加密规则对 SQL 进行改写,从而实现对原文数据进行加密,并将原文数据(可选)及密文数据同时存储到底层数据库。
  • 在用户查询数据时,它仅从数据库中取出密文数据,并对其解密,最终将解密后的原始数据返回给用户。

Apache ShardingSphere自动化&透明化了数据脱敏过程,让用户无需关注数据脱敏的实现细节,像使用普通数据那样使用脱敏数据。

2.6.2 整体架构

ShardingSphere提供的Encrypt-JDBC和业务代码部署在一起。业务方需面向Encrypt-JDBC进行JDBC编程。

加密模块将用户发起的 SQL 进行拦截,并通过 SQL 语法解析器进行解析、理解 SQL 行为,再依据用户传入的加密规则,找出需要加密的字段和所使用的加解密算法对目标字段进行加解密处理后,再与底层数据库进行交互。

Apache ShardingSphere 会将用户请求的明文进行加密后存储到底层数据库;并在用户查询时,将密文从数据库中取出进行解密后返回给终端用户。

通过屏蔽对数据的加密处理,使用户无需感知解析 SQL、数据加密、数据解密的处理过程,就像在使用普通数据一样使用加密数据。

2.6.3 加密规则

脱敏配置主要分为四部分:数据源配置,加密器配置,脱敏表配置以及查询属性配置,其详情如下图所示:

  • 数据源配置:指DataSource的配置信息
  • 加密器配置:指使用什么加密策略进行加解密。目前ShardingSphere内置了两种加解密策略:AES/MD5
  • 脱敏表配置:指定哪个列用于存储密文数据(cipherColumn)、哪个列用于存储明文数据(plainColumn)以及用户想使用哪个列进行SQL编写(logicColumn)
  • 查询属性的配置:当底层数据库表里同时存储了明文数据、密文数据后,该属性开关用于决定是直接查询数据库表里的明文数据进行返回,还是查询密文数据通过Encrypt-JDBC解密后返回。

2.6.4 脱敏处理流程

下图可以看出ShardingSphere将逻辑列与明文列和密文列进行了列名映射。

下方图片展示了使用Encrypt-JDBC进行增删改查时,其中的处理流程和转换逻辑,如下图所示。

2.6.5 数据加密实战

2.6.5.1 环境搭建
  1. 创建数据库及表
sql 复制代码
CREATE TABLE `t_Account` (
  `user_id` bigint(11) NOT NULL,
  `user_name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL COMMENT '密码明文',
  `password_encrypt` varchar(255) DEFAULT NULL COMMENT '密码密文',
  `password_assisted` varchar(255) DEFAULT NULL COMMENT '辅助查询列',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  1. 创建实体类
java 复制代码
@TableName("t_Account")
@Data
public class Account {

    @TableId(value = "user_id",type = IdType.ASSIGN_ID)
    private Long userId;

    private String userName;

    private String password;

    private String passwordEncrypt;

    private String passwordAssisted;

}
  1. 创建Mapper
java 复制代码
@Repository
public interface AccountMapper extends BaseMapper<User> {

    @Insert("insert into t_user(user_id,user_name,password) " +
            "values(#{userId},#{userName},#{password})")
    void insetUser(User users);

    @Select("select * from t_user where user_name=#{userName} and password=#{password}")
    @Results({
            @Result(column = "user_id", property = "userId"),
            @Result(column = "user_name", property = "userName"),
            @Result(column = "password", property = "password"),
            @Result(column = "password_assisted", property = "passwordAssisted")
    })
 List<Account> getUserInfo(@Param("userName") String userName,@Param("password") String password);
}
  1. 配置文件
properties 复制代码
# 启用 debug 日志
logging.level.org.apache.shardingsphere=DEBUG
# 应用名称
spring.application.name=shardingsphere-jdbc-encryption

# 打印 SQL
spring.shardingsphere.props.sql-show=true

# 定义数据源
spring.shardingsphere.datasource.names=db0

# 数据源1
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=123456
  1. 测试插入与查询
java 复制代码
package com.zhp;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zhp.entity.*;
import com.zhp.mapper.*;
import com.zhp.utils.SnowFlakeUtil;
import org.apache.shardingsphere.infra.hint.HintManager;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@SpringBootTest
class ShardingsphereJdbcTableApplicationTests {
    @Autowired
    private AccountMapper accountMapper;
    @Test
    public void testInsertAccount(){

        Account account = new Account();
        account.setUserName("user2022");
        account.setPassword("123456");

        accountMapper.insetUser(account);
    }
    @Test
    public void testSelectAccount(){
        List<Account> accountList = accountMapper.getUserInfo("user2022", "123456");
        accountList.forEach(System.out::println);
    }
}
2.6.5.2 加密策略解析

https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/spring-boot-starter/rules/encrypt/

ShardingSphere提供了两种加密策略用于数据脱敏,该两种策略分别对应ShardingSphere的两种加解密的接口,即Encryptor和QueryAssistedEncryptor。

  • Encryptor: 该解决方案通过提供encrypt(), decrypt()两种方法对需要脱敏的数据进行加解密。
    • 在用户进行INSERT, DELETE, UPDATE时,ShardingSphere会按照用户配置,对SQL进行解析、改写、路由,并会调用encrypt()将数据加密后存储到数据库, 而在SELECT时,则调用decrypt()方法将从数据库中取出的脱敏数据进行逆向解密,最终将原始数据返回给用户。
    • 当前,ShardingSphere针对这种类型的脱敏解决方案提供了两种具体实现类,分别是MD5(不可逆),AES(可逆),用户只需配置即可使用这两种内置的方案。
  • QueryAssistedEncryptor: 相比较于第一种脱敏方案,该方案更为安全和复杂。
    • 它的理念是:即使是相同的数据,如两个用户的密码相同,它们在数据库里存储的脱敏数据也应当是不一样的。这种理念更有利于保护用户信息,防止撞库成功。
    • 当前,ShardingSphere针对这种类型的脱敏解决方案并没有提供具体实现类,却将该理念抽象成接口,提供给用户自行实现。ShardingSphere将调用用户提供的该方案的具体实现类进行数据脱敏。
2.6.5.3 默认AES加密算法实现

数据加密默认算法支持 AES 和 MD5 两种

  • AES 对称加密: 同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密
plain 复制代码
加密:明文 + 密钥 -> 密文
解密:密文 + 密钥 -> 明文
  • MD5算是一个生成签名的算法,引起结果不可逆.MD5的优点:计算速度快,加密速度快,不需要密钥;MD5的缺点: 将用户的密码直接MD5后存储在数据库中是不安全的。很多人使用的密码是常见的组合,威胁者将这些密码的常见组合进行单向哈希,得到一个摘要组合,然后与数据库中的摘要进行比对即可获得对应的密码。https://www.tool.cab/decrypt/md5.html

配置文件

注意shardingJdbc每个版本的配置文件配置方式变动很大,如果使用和本教程使用的shardingjdbc版本不一样,自己去shardingjdbc官网找对应版本配置文件配置方式,思想都是一样的。

properties 复制代码
# 启用 debug 日志
logging.level.org.apache.shardingsphere=DEBUG
# 应用名称
spring.application.name=shardingsphere-jdbc-encryption

# 打印 SQL
spring.shardingsphere.props.sql-show=true

# 定义数据源
spring.shardingsphere.datasource.names=db0

# 数据源1
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=123456

# 采用AES对称加密策略
spring.shardingsphere.rules.encrypt.encryptors.encryptor_aes.type=aes
# ASE算法的密钥
spring.shardingsphere.rules.encrypt.encryptors.encryptor_aes.props.aes-key-value=123456abc

# password为逻辑列,password.plainColumn为数据表明文列,password.cipherColumn为数据表密文列
spring.shardingsphere.rules.encrypt.tables.t_Account.columns.password.plain-column=password
spring.shardingsphere.rules.encrypt.tables.t_Account.columns.password.cipher-column=password_encrypt
spring.shardingsphere.rules.encrypt.tables.t_Account.columns.password.encryptor-name=encryptor_aes

# 查询是否使用密文列
spring.shardingsphere.rules.encrypt.query-with-cipher-column=true
# 打印SQl
spring.shardingsphere.props.sql.show=true

测试插入数据

java 复制代码
    @Test
    public void testInsertAccount(){

        Account account = new Account();
        account.setUserName("user2022");
        account.setPassword("123456");

        accountMapper.insetUser(account);
    }
  1. 设置了明文列和密文列,运行成功,新增时逻辑列会改写成明文列和密文列
  1. 仅设置明文列,运行直接报错,所以必须设置加密列

  2. 仅设置密文列,运行成功,明文会进行加密,数据库实际插入到密文列

  3. 设置了明文列和密文列, spring.shardingsphere.rules.encrypt.query-with-cipher-columntrue 时,查询通过密文列查询,返回数据为明文.

  4. 设置了明文列和密文列, spring.shardingsphere.rules.encrypt.query-with-cipher-column 为false时,查询通过明文列执行,返回数据为明文列.

2.6.5.4 MD5加密算法实现

配置文件

properties 复制代码
# 启用 debug 日志
logging.level.org.apache.shardingsphere=DEBUG
# 应用名称
spring.application.name=shardingsphere-jdbc-encryption

# 打印 SQL
spring.shardingsphere.props.sql-show=true

# 定义数据源
spring.shardingsphere.datasource.names=db0

# 数据源1
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=123456


# 采用md5
spring.shardingsphere.rules.encrypt.encryptors.encryptor_md5.type=MD5
# password为逻辑列,password.plainColumn为数据表明文列,password.cipherColumn为数据表密文列
spring.shardingsphere.rules.encrypt.tables.t_Account.columns.password.plain-column=password
spring.shardingsphere.rules.encrypt.tables.t_Account.columns.password.cipher-column=password_encrypt
spring.shardingsphere.rules.encrypt.tables.t_Account.columns.password.encryptor-name=encryptor_md5

# 查询是否使用密文列
spring.shardingsphere.rules.encrypt.query-with-cipher-column=false
# 打印SQl
spring.shardingsphere.props.sql.show=true

测试插入数据

  1. 新增时,可以看到加密后的数据和AES的有所区别
  2. 查询时,spring.shardingsphere.rules.encrypt.query-with-cipher-columntrue 时,通过密文列查询,由于MD5加密是非对称的,所以返回的是密文数据
  3. 查询时,spring.shardingsphere.rules.encrypt.query-with-cipher-columnfalse 时,通过明文列查询,返回明文数据

2.7 分布式事务详解与实战

2.7.1 什么是分布式事务

2.7.1.1 本地事务介绍

本地事务,是指传统的单机数据库事务,必须具备ACID原则

  • 原子性(A) 所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。
  • **一致性(C)**事务的执行必须保证系统的一致性,在事务开始之前和事务结束以后,数据库的完整性没有被破坏,就拿转账为例,A有500元,B有500元,如果在一个事务里A成功转给B500元,那么不管发生什么,那么最后A账户和B账户的数据之和必须是1000元。
  • **隔离性(I)**所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。数据库保证隔离性包括四种不同的隔离级别:
    • Read Uncommitted(读取未提交内容)
    • Read Committed(读取提交内容)
    • Repeatable Read(可重读)
    • Serializable(可串行化)
  • **持久性(D)**所谓的持久性,就是说一旦事务提交了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。

因为在传统项目中,项目部署基本是单点式:即单个服务器和单个数据库。这种情况下,数据库本身的事务机制就能保证ACID的原则,这样的事务就是本地事务。

2.7.1.2 事务日志undo和redo

单个服务与单个数据库的架构中,产生的事务都是本地事务。其中原子性和持久性其实是依靠undo和redo 日志来实现。

InnoDB的事务日志主要分为:

  • undo log(回滚日志,提供回滚操作)
  • redo log(重做日志,提供前滚操作)

1) undo log日志介绍

Undo Log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到Undo Log。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。

Undo Log 记录了此次事务**「开始前」** 的数据状态,记录的是更新之 **「前」**的值

undo log 作用:

  1. 实现事务原子性,可以用于回滚
  2. 实现多版本并发控制(MVCC), 也即非锁定读

Undo log 产生和销毁

  1. Undo Log在事务开始前产生
  2. 当事务提交之后,undo log 并不能立马被删除,而是放入待清理的链表
  3. 会通过后台线程 purge thread 进行回收处理

Undo Log属于逻辑日志,记录一个变化过程。例如执行一个delete,undolog会记录一个insert;执行一个update,undolog会记录一个相反的update。

2) redo log日志介绍

和Undo Log相反,Redo Log记录的是新数据的备份。在事务提交前,只要将Redo Log持久化即可,不需要将数据持久化,减少了IO的次数。

Redo Log: 记录了此次事务**「完成后」** 的数据状态,记录的是更新之 **「后」**的值

Redo log的作用:

  • 比如MySQL实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。

Redo Log 的工作原理

Undo + Redo事务的简化过程

假设有A、B两个数据,值分别为1,2

plain 复制代码
 A. 事务开始.
 B. 记录A=1到undo log buffer.
 C. 修改A=3.
 D. 记录A=3到redo log buffer.
 E. 记录B=2到undo log buffer.
 F. 修改B=4.
 G. 记录B=4到redo log buffer.
 H. 将undo log写入磁盘
 I. 将redo log写入磁盘
 J. 事务提交

安全和性能问题

  • 如何保证原子性?如果在事务提交前故障,通过undo log日志恢复数据。如果undo log都还没写入,那么数据就尚未持久化,无需回滚
  • 如何保证持久化?大家会发现,这里并没有出现数据的持久化。因为数据已经写入redo log,而redo log持久化到了硬盘,因此只要到了步骤I以后,事务是可以提交的。
  • 内存中的数据库数据何时持久化到磁盘?因为redo log已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存中与磁盘不一致),事务提交后也会将内存数据刷入磁盘(也可以按照固设定的频率刷新内存数据到磁盘中)。
  • redo log何时写入磁盘redo log会在事务提交之前,或者redo log buffer满了的时候写入磁盘

总结一下:

  • undo log 记录更新前数据,用于保证事务原子性
  • redo log 记录更新后数据,用于保证事务的持久性
  • redo log有自己的内存buffer,先写入到buffer,事务提交时写入磁盘
  • redo log持久化之后,意味着事务是可提交
2.7.1.3 分布式事务介绍

分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务:

  • 跨数据源的分布式事务
  • 跨服务的分布式事务
  • 综合情况

1)跨数据源

随着业务数据规模的快速发展,数据量越来越大,单库单表逐渐成为瓶颈。所以我们对数据库进行了水平拆分,将原单库单表拆分成数据库分片,于是就产生了跨数据库事务问题。

2)跨服务

在业务发展初期,"一块大饼"的单业务系统架构,能满足基本的业务需求。但是随着业务的快速发展,系统的访问量和业务复杂程度都在快速增长,单系统架构逐渐成为业务发展瓶颈,解决业务系统的高耦合、可伸缩问题的需求越来越强烈。

如下图所示,按照面向服务(SOA)的架构的设计原则,将单业务系统拆分成多个业务系统,降低了各系统之间的耦合度,使不同的业务系统专注于自身业务,更有利于业务的发展和系统容量的伸缩。

3)分布式系统的数据一致性问题

在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。在分布式网络环境下,我们无法保障所有服务、数据库都百分百可用,一定会出现部分服务、数据库执行成功,另一部分执行失败的问题。

当出现部分业务操作成功、部分业务操作失败时,业务数据就会出现不一致。

例如电商行业中比较常见的下单付款案例,包括下面几个行为:

  • 创建新订单
  • 扣减商品库存
  • 从用户账户余额扣除金额

完成上面的操作需要访问三个不同的微服务和三个不同的数据库。

在分布式环境下,肯定会出现部分操作成功、部分操作失败的问题,比如:订单生成了,库存也扣减了,但是 用户账户的余额不足,这就造成数据不一致。

订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。

但是当我们把三件事情看做一个事情事,要满足保证"业务"的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。

此时ACID难以满足,这是分布式事务要解决的问题.

2.7.2 分布式事务理论

2.7.2.1 CAP (强一致性)
  • CAP 定理,又被叫作布鲁尔定理。对于共享数据系统,最多只能同时拥有CAP其中的两个,任意两个都有其适应的场景。

    复制代码
      ![](https://i-blog.csdnimg.cn/img_convert/8c9ff0f6b82376f9b6978465394623d6.jpeg) 
  • 怎样才能同时满足CA?除非是单点架构

  • 何时要满足CP?对一致性要求高的场景。例如我们的Zookeeper就是这样的,在服务节点间数据同步时,服务对外不可用。

  • 何时满足AP?对可用性要求较高的场景。例如Eureka,必须保证注册中心随时可用,不然拉取不到服务就可能出问题。

2.7.2.2 BASE(最终一致性)

BASE 是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。它的核心思想是即使无法做到强一致性(CAP 就是强一致性),但应用可以采用适合的方式达到最终一致性。

  • BA指的是基本业务可用性,支持分区失败;
  • S表示柔性状态,也就是允许短时间内不同步;
  • E表示最终一致性,数据最终是一致的,但是实时是不一致的。

原子性和持久性必须从根本上保障,为了可用性、性能和服务降级的需要,只有降低一致性和隔离性的要求。BASE 解决了 CAP 理论中没有考虑到的网络延迟问题,在BASE中用软状态和最终一致,保证了延迟后的一致性。

还以上面的下单减库存和扣款为例:

订单服务、库存服务、用户服务及他们对应的数据库就是分布式应用中的三个部分。

  • CP方式:现在如果要满足事务的强一致性,就必须在订单服务数据库锁定的同时,对库存服务、用户服务数据资源同时锁定。等待三个服务业务全部处理完成,才可以释放资源。此时如果有其他请求想要操作被锁定的资源就会被阻塞,这样就是满足了CP。这就是强一致,弱可用
  • AP方式:三个服务的对应数据库各自独立执行自己的业务,执行本地事务,不要求互相锁定资源。但是这个中间状态下,我们去访问数据库,可能遇到数据不一致的情况,不过我们需要做一些后补措施,保证在经过一段时间后,数据最终满足一致性。这就是高可用,但弱一致(最终一致)。

由上面的两种思想,延伸出了很多的分布式事务解决方案:

  • XA
  • TCC
  • 可靠消息最终一致
  • AT

2.7.3 分布式事务模式(大概了解)

了解了分布式事务中的强一致性和最终一致性理论,下面介绍几种常见的分布式事务的解决方案。

下面内容大概了解就行,主要是了解分布式事务的思想

2.7.3.1 DTP模型与XA协议

1) DTP介绍

X/Open DTP(Distributed Transaction Process)是一个分布式事务模型。这个模型主要使用了两段提交(2PC - Two-Phase-Commit)来保证分布式事务的完整性。

1994 年,X/Open 组织(即现在的 Open Group )定义了分布式事务处理的DTP 模型。该模型包括这样几个角色:

  • 应用程序( AP ):我们的微服务
  • 事务管理器( TM ):全局事务管理者
  • 资源管理器( RM ):一般是数据库
  • 通信资源管理器( CRM ):是TM和RM间的通信中间件

在该模型中,一个分布式事务(全局事务)可以被拆分成许多个本地事务,运行在不同的AP和RM上。每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每一个本地事务都能同时成功,若有一个本地事务失败,则所有其它事务都必须回滚。但问题是,本地事务处理过程中,并不知道其它事务的运行状态。因此,就需要通过CRM来通知各个本地事务,同步事务执行的状态。

因此,各个本地事务的通信必须有统一的标准,否则不同数据库间就无法通信。XA 就是 X/Open DTP中通信中间件与TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。

2) XA介绍

XA是由X/Open组织提出的分布式事务的规范,是基于两阶段提交协议。 XA规范主要定义了全局事务管理器(TM)和局部资源管理器(RM)之间的接口。目前主流的关系型数据库产品都是实现了XA接口。

XA之所以需要引入事务管理器,是因为在分布式系统中,从理论上讲两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。由全局事务管理器管理和协调的事务,可以跨越多个资源(数据库)和进程。

事务管理器用来保证所有的事务参与者都完成了准备工作(第一阶段)。如果事务管理器收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL 在这个XA事务中扮演的是参与者的角色,而不是事务管理器。

2.7.3.2 2PC模式 (强一致性)

二阶提交协议就是根据这一思想衍生出来的,将全局事务拆分为两个阶段来执行:

  • 阶段一:准备阶段,各个本地事务完成本地事务的准备工作。
  • 阶段二:执行阶段,各个本地事务根据上一阶段执行结果,进行提交或回滚。

这个过程中需要一个协调者(coordinator),还有事务的参与者(voter)。

1)正常情况

投票阶段 :协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行成功的信息(agree

提交阶段 :协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务。

2)异常情况

当然,也有异常的时候:

投票阶段 :协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行结果,但只要有一个参与者返回的是Disagree,则说明执行失败。

提交阶段 :协调组发现有一个或多个参与者返回的是Disagree,认为执行失败。于是向各个事务参与者发出abort指令,各个事务参与者回滚事务。

3)二阶段提交的缺陷

缺陷1: 单点故障问题

  • 2PC的缺点在于不能处理fail-stop形式的节点failure. 比如下图这种情况.
  • 假设coordinator和voter3都在Commit这个阶段c挂掉了, 而voter1和voter2没有收到commit消息. 这时候voter1和voter2就陷入了一个困境. 因为他们并不能判断现在是两个场景中的哪一种: (1)上轮全票通过然后voter3第一个收到了commit的消息并在commit操作之后crash了 (2)上轮voter3反对所以干脆没有通过.

缺陷2: 阻塞问题

  • 在准备阶段、提交阶段,每个事物参与者都会锁定本地资源,并等待其它事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就比较低了。

3)二阶段提交的使用场景

  • 对事务有强一致性要求,对事务执行效率不敏感,并且不希望有太多代码侵入。

面对二阶段提交的上述缺点,后来又演变出了三阶段提交,但是依然没有完全解决阻塞和资源锁定的问题,而且引入了一些新的问题,因此实际使用的场景较少。

2.7.3.3 TCC模式 (最终一致性)

TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate's Opinion》的论文提出。

TCC 是服务化的两阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现:

1) TCC的基本原理

它本质是一种补偿的思路。事务运行过程包括三个方法,

  • Try:资源的检测和预留;
  • Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
  • Cancel:预留资源释放。

执行分两个阶段:

  • 准备阶段(try):资源的检测和预留;

  • 执行阶段(confirm/cancel):根据上一步结果,判断下面的执行方法。如果上一步中所有事务参与者都成功,则这里执行confirm。反之,执行cancel

    复制代码
      ![](https://i-blog.csdnimg.cn/img_convert/534f880a471186c8f013595eab34d6d4.jpeg) 

    粗看似乎与两阶段提交没什么区别,但其实差别很大:

  • try、confirm、cancel都是独立的事务,不受其它参与者的影响,不会阻塞等待它人

  • try、confirm、cancel由程序员在业务层编写,锁粒度有代码控制

2) TCC的具体实例

我们以之前的下单业务中的扣减余额为例来看下三个不同的方法要怎么编写,假设账户A原来余额是100,需要余额扣减30元。如图:

  • 一阶段(Try):余额检查,并冻结用户部分金额,此阶段执行完毕,事务已经提交
    • 检查用户余额是否充足,如果充足,冻结部分余额
    • 在账户表中添加冻结金额字段,值为30,余额不变
  • 二阶段
    • 提交(Confirm):真正的扣款,把冻结金额从余额中扣除,冻结金额清空
      • 修改冻结金额为0,修改余额为100-30 = 70元
    • 补偿(Cancel):释放之前冻结的金额,并非回滚
      • 余额不变,修改账户冻结金额为0

3) TCC模式的优势和缺点

  • 优势TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其它事务的执行结果。而如果其它事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。
  • 缺点
    • 代码侵入:需要人为编写代码实现try、confirm、cancel,代码侵入较多
    • 开发成本高:一个业务需要拆分成3个步骤,分别编写业务实现,业务编写比较复杂
    • 安全性考虑:cancel动作如果执行失败,资源就无法释放,需要引入重试机制,而重试可能导致重复执行,还要考虑重试时的幂等问题

4) TCC使用场景

  • 对事务有一定的一致性要求(最终一致)
  • 对性能要求较高
  • 开发人员具备较高的编码能力和幂等处理经验
2.7.3.4 消息队列模式(最终一致性)

消息队列的方案最初是由 eBay 提出,基于TCC模式,消息中间件可以基于 Kafka、RocketMQ 等消息队列。

此方案的核心是将分布式事务拆分成本地事务进行处理,将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或MQ中间件,再通过业务规则人工发起重试。

1) 事务的处理流程:

  • 步骤1:事务主动方处理本地事务。事务主动方在本地事务中处理业务更新操作和MQ写消息操作。例如: A用户给B用户转账,主动方先执行扣款操作
  • 步骤 2:事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B。例如: 告知被动方生增加银行卡金额事务主动方主动写消息到MQ,事务消费方接收并处理MQ中的消息。
  • 步骤 3:事务被动方通过MQ中间件,通知事务主动方事务已处理的消息,事务主动方根据反馈结果提交或回滚事务。例如: 订单生成成功,通知主动方法,主动放即可以提交.

为了数据的一致性,当流程中遇到错误需要重试,容错处理规则如下:

  • 当步骤 1 处理出错,事务回滚,相当于什么都没发生。
  • 当步骤 2 处理出错,由于未处理的事务消息还是保存在事务发送方,可以重试或撤销本地业务操作。
  • 如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。
  • 如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。

那么问题来了,我们如何保证消息发送一定成功?如何保证消费者一定能收到消息?

2) 本地消息表

为了避免消息发送失败或丢失,我们可以把消息持久化到数据库中。实现时有简化版本和解耦合版本两种方式。

  • 事务发起者:
    • 开启本地事务
    • 执行事务相关业务
    • 发送消息到MQ
    • 把消息持久化到数据库,标记为已发送
    • 提交本地事务
  • 事务接收者:
    • 接收消息
    • 开启本地事务
    • 处理事务相关业务
    • 修改数据库消息状态为已消费
    • 提交本地事务
  • 额外的定时任务
    • 定时扫描表中超时未消费消息,重新发送

3) 消息事务的优缺点

总结上面的几种模型,消息事务的优缺点如下:

  • 优点:
    • 业务相对简单,不需要编写三个阶段业务
    • 是多个本地事务的结合,因此资源锁定周期短,性能好
  • 缺点:
    • 代码侵入
    • 依赖于MQ的可靠性
    • 消息发起者可以回滚,但是消息参与者无法引起事务回滚
    • 事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况

针对事务无法回滚的问题,有人提出说可以再事务参与者执行失败后,再次利用MQ通知消息服务,然后由消息服务通知其他参与者回滚。那么,恭喜你,你利用MQ和自定义的消息服务再次实现了2PC 模型,又造了一个大轮子

2.7.3.5 AT模式 (最终一致性)

2019年 1 月份,Seata 开源了 AT 模式。AT 模式是一种无侵入的分布式事务解决方案。可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。

在 AT 模式下,用户只需关注自己的"业务 SQL",用户的 "业务 SQL" 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

可以参考Seata的官方文档

1) AT模式基本原理

先来看一张流程图:

有没有感觉跟TCC的执行很像,都是分两个阶段:

  • 一阶段:执行本地事务,并返回执行结果
  • 二阶段:根据一阶段的结果,判断二阶段做法:提交或回滚

但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编写,全部有Seata自己实现了。也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务。

那么,AT模式如何实现无代码侵入,如何帮我们自动实现二阶段代码的呢?

一阶段

  • 在一阶段,Seata 会拦截"业务 SQL",首先解析 SQL 语义,找到"业务 SQL"要更新的业务数据,在业务数据被更新前,将其保存成"before image",然后执行"业务 SQL"更新业务数据,在业务数据更新之后,再将其保存成"after image",最后获取全局行锁,提交事务。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
  • 这里的before imageafter image类似于数据库的undo和redo日志,但其实是用数据库模拟的。

update t_stock set stock = stock - 2 where id = 1

select * from t_stock where id = 1 ,保存元快照 before image ,类似undo日志.

放行执行真实SQL,执行完成,再次查询,获取到最新的库存数据,再将数据保存到镜像after image 类似redo.

提交业务如果成功,就清楚快照信息,失败,则根据redo 中的数据与数据库的数据进行对比,如果一致就回滚,如果不一致 出现脏数据,就需要人工介入.

AT模式最重要的一点就是 程序员只需要关注业务处理的本身即可,不需要考虑回滚补偿等问题.代码写的跟以前一模一样.

二阶段提交

  • 二阶段如果是提交的话,因为"业务 SQL"在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

二阶段回滚

  • 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的"业务 SQL",还原业务数据。回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比"数据库当前业务数据"和 "after image",如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

不过因为有全局锁机制,所以可以降低出现脏写的概率。

AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写"业务 SQL",便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

AT模式优缺点

优点:

  • 与2PC相比:每个分支事务都是独立提交,不互相等待,减少了资源锁定和阻塞时间
  • 与TCC相比:二阶段的执行操作全部自动化生成,无代码侵入,开发成本低

缺点:

  • 与TCC相比,需要动态生成二阶段的反向补偿操作,执行性能略低于TCC
2.7.3.6 Saga模式(最终一致性)

Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。

其理论基础是Hector & Kenneth 在1987年发表的论文Sagas

Seata官网对于Saga的指南:https://seata.io/zh-cn/docs/user/saga.html

1) 基本模型

在分布式事务场景下,我们把一个Saga分布式事务看做是一个由多个本地事务组成的事务,每个本地事务都有一个与之对应的补偿事务。在Saga事务的执行过程中,如果某一步执行出现异常,Saga事务会被终止,同时会调用对应的补偿事务完成相关的恢复操作,这样保证Saga相关的本地事务要么都是执行成功,要么通过补偿恢复成为事务执行之前的状态。(自动反向补偿机制)。

Saga是一种补偿模式,它定义了两种补偿策略:

  • 向前恢复(forward recovery):对应于上面第一种执行顺序,发生失败进行重试,适用于必须要成功的场景(一定会成功)。
  • 向后恢复(backward recovery):对应于上面提到的第二种执行顺序,发生错误后撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。

2) 适用场景

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

3) 优势

  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现

3) 缺点

2.7.4 Sharding-JDBC分布式事务实战

2.7.4.1 Sharding-JDBC分布式事务介绍

1) 分布式内容回顾

  • 本地事务
  • 本地事务提供了 ACID 事务特性。基于本地事务,为了保证数据的一致性,我们先开启一个事务后,才可以执行数据操作,最后提交或回滚就可以了。
  • 在分布式环境下,事情就会变得比较复杂。假设系统中存在多个独立的数据库,为了确保数据在这些独立的数据库中保持一致,我们需要把这些数据库纳入同一个事务中。这时本地事务就无能为力了,我们需要使用分布式事务。
  • 分布式事务
    • 业界关于如何实现分布式事务也有一些通用的实现机制,例如支持两阶段提交的 XA 协议以及以 Saga 为代表的柔性事务。针对不同的实现机制,也存在一些供应商和开发工具。
    • 因为这些开发工具在使用方式上和实现原理上都有较大的差异性,所以开发人员的一大诉求在于,希望能有一套统一的解决方案能够屏蔽这些差异。同时,我们也希望这种解决方案能够提供友好的系统集成性。

2) ShardingJDBC事务

ShardingJDBC支持的分布式事务方式有三种 LOCAL, XA , BASE,这三种事务实现方式都是采用的对代码无侵入的方式实现的

java 复制代码
//事务类型枚举类
public enum TransactionType {
    //除本地事务之外,还提供针对分布式事务的两种实现方案,分别是 XA 事务和柔性事务
    LOCAL, XA, BASE
} 
  • LOCAL本地事务
    • 这种方式实际上是将事务交由数据库自行管理,可以用Spring的@Transaction注解来配置。这种方式不具备分布式事务的特性。
  • XA 事务
    • XA 事务提供基于两阶段提交协议的实现机制。所谓两阶段提交,顾名思义分成两个阶段,一个是准备阶段,一个是执行阶段。在准备阶段中,协调者发起一个提议,分别询问各参与者是否接受。在执行阶段,协调者根据参与者的反馈,提交或终止事务。如果参与者全部同意则提交,只要有一个参与者不同意就终止。
    • 目前,业界在实现 XA 事务时也存在一些主流工具库,包括 Atomikos、Narayana 和 Bitronix。ShardingSphere 对这三种工具库都进行了集成,并默认使用 Atomikos 来完成两阶段提交。
  • BASE 事务
    • XA 事务是典型的强一致性事务,也就是完全遵循事务的 ACID 设计原则。与 XA 事务这种"刚性"不同,柔性事务则遵循 BASE 设计理论,追求的是最终一致性。这里的 BASE 来自基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventual Consistency)这三个概念。
    • 关于如何实现基于 BASE 原则的柔性事务,业界也存在一些优秀的框架,例如阿里巴巴提供的 Seata。ShardingSphere 内部也集成了对 Seata 的支持。当然,我们也可以根据需要,集成其他分布式事务类开源框架.

2) 分布式事务模式整合流程

ShardingSphere 作为一款分布式数据库中间件,势必要考虑分布式事务的实现方案。在设计上,ShardingSphere整合了XA、Saga和Seata模式后,为分布式事务控制提供了极大的便利,我们可以在应用程序编程时,采用以下统一模式进行使用。

  1. JAVA编码方式设置事务类型
java 复制代码
@ShardingSphereTransactionType(TransactionType.XA) // Sharding-jdbc一致性性事务
@ShardingSphereTransactionType(TransactionType.BASE) // Sharding-jdbc柔性事务
2.7.4.2 环境准备

下面主要演示XA事务,如下图:

1) 创建数据库及表

shardingjdbc0shardingjdbc1 中分别创建职位表和职位描述表.

sql 复制代码
-- 职位表
CREATE TABLE `position` (
  `Id` bigint(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(256) DEFAULT NULL,
  `salary` varchar(50) DEFAULT NULL,
  `city` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 职位描述表
CREATE TABLE `position_detail` (
  `Id` bigint(11) NOT NULL AUTO_INCREMENT,
  `pid` bigint(11) NOT NULL DEFAULT '0',
  `description` text,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

shardingjdbc0:

shardingjdbc1:

配置文件内容:

properties 复制代码
# 应用名称
spring.application.name=sharding-jdbc-trans

# 打印SQl
spring.shardingsphere.props.sql-show=true

# 端口
server.port=8081

#===============数据源配置
#配置真实的数据源
spring.shardingsphere.datasource.names=db0,db1

#数据源1
#数据源1
spring.shardingsphere.datasource.db0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.url = jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db0.username = root
spring.shardingsphere.datasource.db0.password = 123456

#数据源2
spring.shardingsphere.datasource.db1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.url = jdbc:mysql://192.168.116.129:3306/shardingjdbc1?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.db1.username = root
spring.shardingsphere.datasource.db1.password = 123456

#分库策略
spring.shardingsphere.rules.sharding.tables.position.database-strategy.standard.sharding-column=id
spring.shardingsphere.rules.sharding.tables.position.database-strategy.standard.sharding-algorithm-name=table-mod

spring.shardingsphere.rules.sharding.tables.position_detail.database-strategy.standard.sharding-column=id
spring.shardingsphere.rules.sharding.tables.position_detail.database-strategy.standard.sharding-algorithm-name=table-mod

#分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.type=MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.table-mod.props.sharding-count=2

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.7.4.3 案例实现

1) entity

java 复制代码
@TableName("position")
@Data
public class Position {

    @TableId()
    private long id;

    private String name;

    private String salary;

    private String city;
}

@TableName("position_detail")
@Data
public class PositionDetail {

    @TableId()
    private long id;

    private long pid;

    private String description;

}

3) mapper

java 复制代码
@Repository
public interface PositionMapper extends BaseMapper<Position> {

}

@Repository
public interface PositionDetailMapper extends BaseMapper<PositionDetail> {

}

4) controller

java 复制代码
@RestController
@RequestMapping("/position")
public class PositionController {

    @Autowired
    private PositionMapper positionMapper;

    @Autowired
    private PositionDetailMapper positionDetailMapper;

    @RequestMapping("/show")
    public String show() {
        return "SUCCESS";
    }

    @RequestMapping("/add")
    public String savePosition() throws InterruptedException {
        Position position = new Position();
        position.setId(2);
        position.setName("root");
        position.setSalary("1000000");
        position.setCity("beijing");
        positionMapper.insert(position);
        PositionDetail positionDetail = new PositionDetail();
        positionDetail.setId(1);
        positionDetail.setPid(position.getId());
        positionDetail.setDescription("root");
        positionDetailMapper.insert(positionDetail);
        return "SUCCESS";
    }
}
2.7.4.4 案例测试

测试1: 访问在PositionController的add方法 , 注意: 方法不添加任何事务控制

java 复制代码
 @RequestMapping("/add")
 public String savePosition()

http://localhost:8081/position/add

访问结果:

插入position的数据落在服务器1上:

插入position_detail的数据落在服务器2上:

测试2: 在方法中创造异常,并在add 方法上添加@Transactional本地事务控制,继续测试

java 复制代码
@RequestMapping("/add")
    @Transactional
    public String savePosition() throws InterruptedException {
        Position position = new Position();
        position.setId(2);
        position.setName("root");
        position.setSalary("1000000");
        position.setCity("beijing");
        positionMapper.insert(position);
        PositionDetail positionDetail = new PositionDetail();
        positionDetail.setId(1);
        positionDetail.setPid(position.getId());
        positionDetail.setDescription("root");
        positionDetailMapper.insert(positionDetail);
        int a = 1 / 0;
        return "SUCCESS";
    }

清除数据库中的数据,再次进行测试

查看数据库发现,使用@Transactional注解 ,竟然实现了跨库插入数据, 出现异常也能回滚.

@Transactional注解可以解决分布式事务问题, 这其实是个假象

接下来我们说一下为什么@Transactional不能解决分布式事务

问题1: 为什么会出现回滚操作 ?

  • Sharding-JDBC中的本地事务在以下两种情况是完全支持的:

    • 支持非跨库事务,比如仅分表、在单库中操作
    • 支持因逻辑异常导致的跨库事务(这点非常重要),比如上述的操作,跨两个库插入数据,插入完成后抛出异常
  • 本地事务不支持的情况:

    • 不支持因网络、硬件异常导致的跨库事务;例如:同一事务中,跨两个库更新,更新完毕后、未提交之前,第一个库宕机,则只有第二个库数据提交.

对于因网络、硬件异常导致的跨库事务无法支持很好理解,在分布式事务中无论是两阶段还是三阶段提交都是直接或者间接满足以下两个条件:

1.有一个事务协调者

2.事务日志记录

本地事务并未满足上述条件,自然是无法支持

为什么逻辑异常导致的跨库事务能够支持?

  • 首先Sharding-JDBC中的一条SQL会经过改写 ,拆分成不同数据源 的SQL,比如一条select语句,会按照其中分片键拆分成对应数据源的SQL,然后在不同数据源中的执行,最终会提交或者回滚.
  • 下面是Sharding-JDBC自定义实现的事务控制类ShardingConnection 的类关系图

可以看到ShardingConnection继承了java.sql.Connection,Connection是数据库连接对象,也可以对数据库的本地事务进行管理.

找到ShardingConnection的rollback方法

rollback的方法中区分了本地事务分布式事务,如果是本地事务将调用父类的rollback方法,如下:

ShardingConnection父类:AbstractConnectionAdapter#rollback

ForceExecuteTemplate#execute()方法内部就是遍历数据源去执行对应的rollback方法

java 复制代码
public void execute(Collection<T> targets, ForceExecuteCallback<T> callback) throws SQLException {
    Collection<SQLException> exceptions = new LinkedList();
    Iterator var4 = targets.iterator();

    while(var4.hasNext()) {
        Object each = var4.next();

        try {
            callback.execute(each);
        } catch (SQLException var7) {
            exceptions.add(var7);
        }
    }

    this.throwSQLExceptionIfNecessary(exceptions);
}

总结: 依靠Spring的本地事务@Transactional是无法保证跨库的分布式事务

rollback 在各个数据源中回滚且未记录任何事务日志,因此在非硬件、网络的情况下都是可以正常回滚的,一旦因为网络、硬件故障,可能导致某个数据源rollback失败,这样即使程序恢复了正常,也无undo日志继续进行rollback,因此这里就造成了数据不一致了。

3)测试3: 实现XA事务

首先要在项目中导入对应的依赖包

xml 复制代码
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>

            <artifactId>shardingsphere-transaction-xa-core</artifactId>

            <version>5.1.1</version>

        </dependency>

        <dependency>
            <groupId>com.atomikos</groupId>

            <artifactId>transactions-jta</artifactId>

            <version>4.0.4</version>

        </dependency>

创建配置类

java 复制代码
@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {

    @Bean
    public PlatformTransactionManager txManager(final DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public JdbcTemplate jdbcTemplate(final DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

我们知道,ShardingSphere 提供的事务类型有三种,分别是 LOCAL、XA 和 BASE,默认使用的是 LOCAL。所以如果需要用到分布式事务,需要在业务方法上显式的添加这个注解 @ShardingTransactionType(TransactionType.XA)

java 复制代码
@RequestMapping("/add")
    @ShardingSphereTransactionType(TransactionType.XA)
    @Transactional
    public String savePosition() throws InterruptedException {
        Position position = new Position();
        position.setId(2);
        position.setName("root");
        position.setSalary("1000000");
        position.setCity("beijing");
        positionMapper.insert(position);
        PositionDetail positionDetail = new PositionDetail();
        positionDetail.setId(1);
        positionDetail.setPid(position.getId());
        positionDetail.setDescription("root");
        positionDetailMapper.insert(positionDetail);
        int a = 1 / 0;
        return "SUCCESS";
    }

执行测试代码,结果是数据库的插入全部被回滚了.

2.8 ShardingProxy实战

Sharding-Proxy是ShardingSphere的第二个产品,定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前先提供MySQL版本,它可以使用任何兼容MySQL协议的访问客户端(如:MySQL Command Client, MySQL Workbench等操作数据,对DBA更加友好。

  • 向应用程序完全透明,可直接当做MySQL使用
  • 适用于任何兼容MySQL协议的客户端

2.8.1 使用二进制发布包安装ShardingSphere-Proxy

目前 ShardingSphere-Proxy 提供了 3 种获取方式:

  • 二进制发布包
  • Docker
  • Helm

这里我们使用二进制包的形式安装ShardingProxy, 这种安装方式既可以Linux系统运行,又可以在windows系统运行,步骤如下:

1) 解压二进制包

shell 复制代码
tar -zxvf apache-shardingsphere-5.1.1-shardingsphere-proxy-bin.tar.gz

2) 上传MySQL驱动

下载地址:

plain 复制代码
https://mvnrepository.com/artifact/mysql/mysql-connector-java/8.0.22
复制代码
`mysql-connector-java-8.0.22.jar` ,将MySQl驱动放至`ext-lib`目录 ,该ext-lib目录需要自行创建,创建位置如下图:

![](https://i-blog.csdnimg.cn/img_convert/10890aa3aa860f8acc4aa6fd0fea0646.png)

3) 修改配置conf/server.yaml

yaml 复制代码
# 配置用户信息 用户名密码,赋予管理员权限
rules:
  - !AUTHORITY
    users:
      - root@%:root
    provider:
      type: ALL_PRIVILEGES_PERMITTED
#开启SQL打印
props:
  sql-show: true

4) 启动ShardingSphere-Proxy

  • Linux 操作系统请运行 bin/start.sh
  • Windows 操作系统请运行 bin/start.bat
  • 指定端口号和配置文件目录:bin/start.bat ${proxy_port} ${proxy_conf_directory}

5) 远程连接ShardingSphere-Proxy

  • 远程访问,默认端口3307
shell 复制代码
mysql -h192.168.116.129 -P3307 -uroot -p

6) 访问测试

sql 复制代码
show databases;

可以看到一些基础的表信息

2.8.2 proxy实现读写分离

1) 修改配置config-readwrite-splitting.yaml

yaml 复制代码
#schemaName用来指定->逻辑表名
schemaName: readwrite_splitting_db

dataSources:
  write_ds:
    url: jdbc:mysql://192.168.116.129:3306/shardingjdbc1?characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  read_ds_0:
    url: jdbc:mysql://192.168.116.128:3306/shardingjdbc0?characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
    
rules:
- !READWRITE_SPLITTING
  dataSources:
    readwrite_ds:
      type: Static
      props:
        write-data-source-name: write_ds
        read-data-source-names: read_ds_0

2) 命令行测试

sql 复制代码
C:\Users\86187>mysql -h192.168.116.129 -P3307 -uroot -p

mysql> show databases;
+------------------------+
| schema_name            |
+------------------------+
| readwrite_splitting_db |
| mysql                  |
| information_schema     |
| performance_schema     |
| sys                    |
+------------------------+
5 rows in set (0.03 sec)


mysql> use readwrite_splitting_db;
Database changed
mysql> show tables;
+----------------------------------+------------+
| Tables_in_readwrite_splitting_db | Table_type |
+----------------------------------+------------+
| t_course_0                       | BASE TABLE |
| t_order_item_0                   | BASE TABLE |
| t_order_item_1                   | BASE TABLE |
| t_order_0                        | BASE TABLE |
| position                         | BASE TABLE |
| t_order_1                        | BASE TABLE |
| t_district                       | BASE TABLE |
| position_detail                  | BASE TABLE |
| t_course_1                       | BASE TABLE |
| products                         | BASE TABLE |
+----------------------------------+------------+
10 rows in set (0.00 sec)



mysql> select * from t_order_0;
+--------------------+---------+---------------+-------------+---------+---------------------+
| order_id           | user_id | product_name  | total_price | status  | create_time         |
+--------------------+---------+---------------+-------------+---------+---------------------+
| 378522667419959296 |       2 | iPhone 15 Pro |     8999.00 | CREATED | 2025-09-20 20:33:57 |
| 378522667549982720 |       4 | iPhone 15 Pro |     8999.00 | CREATED | 2025-09-20 20:33:57 |
| 378522667696783360 |       6 | iPhone 15 Pro |     8999.00 | CREATED | 2025-09-20 20:33:57 |
| 378522667839389696 |       8 | iPhone 15 Pro |     8999.00 | CREATED | 2025-09-20 20:33:57 |
| 378522667935858688 |      10 | iPhone 15 Pro |     8999.00 | CREATED | 2025-09-20 20:33:57 |
+--------------------+---------+---------------+-------------+---------+---------------------+
5 rows in set (0.07 sec)

3) 动态查看日志

plain 复制代码
tail -f /opt/apache-shardingsphere-5.1.1-shardingsphere-proxy-bin/logs/stdout.log

2.8.3 使用应用程序连接proxy

1) 创建项目

项目名称: shardingproxy

Spring脚手架: http://start.aliyun.com

2) 添加依赖

xml 复制代码
    <parent>
        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-parent</artifactId>

        <version>2.3.7.RELEASE</version>

        <relativePath/>
    </parent>

    <groupId>com.zhp</groupId>

    <artifactId>shardingproxy</artifactId>

    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

        <dependency>
            <groupId>mysql</groupId>

            <artifactId>mysql-connector-java</artifactId>

            <scope>runtime</scope>

        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>

            <artifactId>mybatis-plus-boot-starter</artifactId>

            <version>3.3.1</version>

        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>

            <artifactId>lombok</artifactId>

            <optional>true</optional>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>

                    <artifactId>junit-vintage-engine</artifactId>

                </exclusion>

            </exclusions>

        </dependency>

    </dependencies>

3) 创建实体类

java 复制代码
@TableName("products")
@Data
public class Products {

    @TableId(value = "pid",type = IdType.AUTO)
    private Long pid;

    private String pname;

    private int  price;

    private String flag;
}

4) 创建Mapper

java 复制代码
@Mapper
public interface ProductsMapper extends BaseMapper<Products> {
    
}

5) 配置数据源

properties 复制代码
# 应用名称
spring.application.name=sharding-proxy-demo

#mysql数据库 (实际连接的是proxy)
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.116.129:3307/readwrite_splitting_db?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

6) 启动类

java 复制代码
@SpringBootApplication
public class ShardingproxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShardingproxyApplication.class, args);
    }

}

7) 测试

java 复制代码
@SpringBootTest
class ShardingproxyDemoApplicationTests {

    @Autowired
    private ProductsMapper productsMapper;

    /**
     * 读数据测试
     */
    @Test
    public void testSelect(){
        productsMapper.selectList(null).forEach(System.out::println);
    }
    
    @Test
    public void testInsert(){
        Products products = new Products();
        products.setPname("洗碗机");
        products.setPrice(1000);
        products.setFlag("1");

        productsMapper.insert(products);
    }
}

日志:

2.8.4 Proxy实现垂直分片

1) 修改配置config-sharding.yaml

yaml 复制代码
schemaName: sharding_db
#
dataSources:
  ds_0:
    url: jdbc:mysql://192.168.116.128:3306/payorder_db?characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  ds_1:
    url: jdbc:mysql://192.168.116.129:3306/user_db?characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !SHARDING
  tables:
    pay_order:
      actualDataNodes: ds_0.pay_order
    users:
      actualDataNodes: ds_1.users

重新启动proxy

2) 动态查看日志

plain 复制代码
tail -f /opt/apache-shardingsphere-5.1.1-shardingsphere-proxy-bin/logs/stdout.log

3) 远程访问

sql 复制代码
mysql -h192.168.116.129 -P3307 -uroot -p

mysql> show databases;
+------------------------+
| schema_name            |
+------------------------+
| sharding_db            |
| readwrite_splitting_db |
| mysql                  |
| information_schema     |
| performance_schema     |
| sys                    |
+------------------------+
6 rows in set (0.00 sec)

mysql> use sharding_db;
Database changed

mysql> show tables;
+-----------------------+------------+
| Tables_in_sharding_db | Table_type |
+-----------------------+------------+
| pay_order             | BASE TABLE |
| users                 | BASE TABLE |
+-----------------------+------------+
2 rows in set (0.01 sec)


mysql> select * from pay_order;
+----------+---------+--------------+-------+
| order_id | user_id | product_name | COUNT |
+----------+---------+--------------+-------+
|     2001 |    1003 | 电视         |     0 |
+----------+---------+--------------+-------+
1 row in set (0.09 sec)

2.8.5 Proxy实现水平分片

1) 修改配置config-sharding.yaml

yaml 复制代码
schemaName: sharding_db

dataSources:
  course_db0:
    url: jdbc:mysql://192.168.116.128:3306/course_db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  course_db1:
    url: jdbc:mysql://192.168.116.129:3306/course_db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !SHARDING
  tables:
    t_course:
      actualDataNodes: course_db${0..1}.t_course_${0..1}
      databaseStrategy:
        standard:
          shardingColumn: user_id
          shardingAlgorithmName: alg_mod
      tableStrategy:
        standard:
          shardingColumn: cid
          shardingAlgorithmName: alg_hash_mod
      keyGenerateStrategy:
        column: cid
        keyGeneratorName: snowflake

  shardingAlgorithms:
    alg_mod:
      type: MOD
      props:
        sharding-count: 2
    alg_hash_mod:
      type: HASH_MOD
      props:
        sharding-count: 2
  
  keyGenerators:
    snowflake:
      type: SNOWFLAKE

重新启动proxy

2) 远程访问

sql 复制代码
mysql> use sharding_db;
Database changed
mysql> show tables;
+-----------------------+------------+
| Tables_in_sharding_db | Table_type |
+-----------------------+------------+
| t_district            | BASE TABLE |
| t_course              | BASE TABLE |
+-----------------------+------------+
2 rows in set (0.00 sec)

mysql> select * from t_course;

3) 动态查看日志

plain 复制代码
tail -f /opt/apache-shardingsphere-5.1.1-shardingsphere-proxy-bin/logs/stdout.log

2.8.6 Proxy实现广播表

环境准备:

1) 修改配置config-sharding.yaml

yaml 复制代码
schemaName: sharding_db

dataSources:
  course_db0:
    url: jdbc:mysql://192.168.116.128:3306/course_db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  course_db1:
    url: jdbc:mysql://192.168.116.129:3306/course_db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !SHARDING
  tables:
    t_course:
      actualDataNodes: course_db${0..1}.t_course_${0..1}
      databaseStrategy:
        standard:
          shardingColumn: user_id
          shardingAlgorithmName: alg_mod
      tableStrategy:
        standard:
          shardingColumn: cid
          shardingAlgorithmName: alg_hash_mod
      keyGenerateStrategy:
        column: cid
        keyGeneratorName: snowflake
  broadcastTables:
    - t_district	
  shardingAlgorithms:
    alg_mod:
      type: MOD
      props:
        sharding-count: 2
    alg_hash_mod:
      type: HASH_MOD
      props:
        sharding-count: 2
  
  keyGenerators:
    snowflake:
      type: SNOWFLAKE

重启proxy

2) 测试广播表

sql 复制代码
INSERT INTO t_district (id,district_name,`LEVEL`) VALUES (1,'北京',1);

2.8.7 Proxy实现绑定表

环境准备:

server01:

server02:

1) 修改配置config-sharding.yaml

yaml 复制代码
schemaName: sharding_db

dataSources:
  db0:
    url: jdbc:mysql://192.168.116.128:3306/shardingjdbc0?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  db1:
    url: jdbc:mysql://192.168.116.129:3306/shardingjdbc1?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !SHARDING
  tables:
    t_order:
      actualDataNodes: db$->{0..1}.t_order_$->{0..1}
      databaseStrategy:
        standard:
          shardingColumn: user_id
          shardingAlgorithmName: alg_mod
      tableStrategy:
        standard:
          shardingColumn: order_Id
          shardingAlgorithmName: alg_hash_mod
      keyGenerateStrategy:
        column: cid
        keyGeneratorName: snowflake
    t_order_item:
      actualDataNodes: db$->{0..1}.t_order_item_$->{0..1}
      databaseStrategy:
        standard:
          shardingColumn: user_Id
          shardingAlgorithmName: alg_mod
      tableStrategy:
        standard:
          shardingColumn: order_Id
          shardingAlgorithmName: alg_hash_mod
      keyGenerateStrategy:
        column: id
        keyGeneratorName: snowflake
  bindingTables:
    - t_order,t_order_item
  shardingAlgorithms:
    alg_mod:
      type: MOD
      props:
        sharding-count: 2
    alg_hash_mod:
      type: HASH_MOD
      props:
        sharding-count: 2
  
  keyGenerators:
    snowflake:
      type: SNOWFLAKE

重启proxy

2) 测试绑定表

sql 复制代码
SELECT t_order.order_Id,t_order.product_name,t_order_item.quantity FROM t_order INNER join t_order_item on t_order.order_Id = t_order_item.order_Id;

2.8.8 总结

  • Sharding-Proxy的优势在于对异构语言的支持(无论使用什么语言,就都可以访问),以及为DBA提供可操作入口。
  • Sharding-Proxy 默认不支持hint,如需支持,请在conf/server.yaml中,将props的属性proxy.hint.enabled设置为true。在Sharding-Proxy中,HintShardingAlgorithm的泛型只能是String类型。
  • Sharding-Proxy默认使用3307端口,可以通过启动脚本追加参数作为启动端口号。如: bin/start.sh 3308
  • Sharding-Proxy使用conf/server.yaml配置注册中心、认证信息以及公用属性。
  • Sharding-Proxy支持多逻辑数据源,每个以"config-"做前缀命名yaml配置文件,即为一个逻辑数据源。
相关推荐
Dxy12393102162 小时前
MySQL快速入门
数据库·mysql
SMF19192 小时前
解决在 Linux 系统中,当你尝试以 root 用户登录时遇到 “Access denied“ 的错误
java·linux·服务器
ByNotD0g2 小时前
Golang Green Tea GC 原理初探
java·开发语言·golang
NaiLuo_452 小时前
MySQL表的约束
数据库·sql·mysql
kkkkkkkkl242 小时前
彻底讲清 MySQL InnoDB 锁机制:从 Record 到 Next-Key 的全景理解
数据库·mysql
9号达人2 小时前
Jackson序列化让验签失败?破解JSON转义陷阱
java·后端·面试
Evan芙2 小时前
使用inotify + rsync和sersync实现文件的同步,并且总结两种方式的优缺点
java·服务器·网络
爱笑的眼睛113 小时前
PyTorch自动微分:超越基础,深入动态计算图与工程实践
java·人工智能·python·ai
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 小时前
java实现登录:多点登录互踢,30分钟无操作超时
java·前端