ShardingSphere JDBC
一、数据分片
1 搭建基础环境
1.1 安装环境
- jdk: 要求jdk必须是1.8版本及以上
- MySQL: 推荐mysql5.7版本
- 搭建两台MySQL服务器
1.2 创建数据库和表
- 在mysql01服务器上, 创建数据库 ivy_payorder_db,并创建表pay_order
CREATE DATABASE ivy_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
- 在mysql02服务器上, 创建数据库 ivy_user_db,并创建表users
CREATE DATABASE ivy_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='用户表'
1.3 创建SpringBoot程序
环境说明:
SpringBoot2.3.7
+MyBatisPlus
+ShardingSphere-JDBC 5.1
+Hikari
+MySQL 5.7
1) 创建项目
项目名称: sharding-jdbc-demo
Spring脚手架: http://start.aliyun.com
2) 引入依赖
<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) 创建实体类
@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
@Mapper
public interface PayOrderMapper extends BaseMapper<PayOrder> {
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
2 实现垂直分库
2.1 配置文件
使用sharding-jdbc 对数据库中水平拆分的表进行操作,通过sharding-jdbc对分库分表的规则进行配置,配置内容包括:数据源、主键生成策略、分片策略等。
application.properties
-
基础配置
# 应用名称 spring.application.name=sharding-jdbc
-
数据源
# 定义多个数据源 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.52.10:3306/ivy_payorder_db?characterEncoding=UTF-8&useSSL=false spring.shardingsphere.datasource.db1.username = root spring.shardingsphere.datasource.db1.password = QiDian@666 #数据源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.52.11:3306/ivy_user_db?characterEncoding=UTF-8&useSSL=false spring.shardingsphere.datasource.db2.username = root spring.shardingsphere.datasource.db2.password = QiDian@666
-
配置数据节点
# 标准分片表配置 # 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 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输出日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.2 垂直分库测试
@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);
}
}
3 实现水平分表
3.1 数据准备
需求说明:
- 在mysql-server01服务器上, 创建数据库 ivy_course_db
- 创建表 t_course_1 、 t_course_2
- 约定规则:如果添加的课程 id 为偶数添加到 t_course_1 中,奇数添加到 t_course_2 中。
水平分片的id需要在业务层实现,不能依赖数据库的主键自增
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
3.2 配置文件
1) 基础配置
# 应用名称
spring.application.name=sharding-jdbc
# 打印SQl
spring.shardingsphere.props.sql-show=true
2) 数据源配置
#===============数据源配置
#配置真实的数据源
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.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_course_db?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=QiDian@666
3) 数据节点配置
#1.配置数据节点
#指定course表的分布情况(配置表在哪个数据库,表名是什么)
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_1 #先插入一张表试试
#spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_$->{1..2}
3.3 测试
- course类
@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
@Mapper
public interface CourseMapper extends BaseMapper<Course> {
}
- 测试:保留上面配置中的一个分片表节点分别进行测试,检查每个分片节点是否可用
# 测试t_course_1表插入
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_1
# 测试t_course_2表插入
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db1.t_course_2
//水平分表测试
@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);
}
}
3.4 行表达式
对上面的配置操作进行修改, 使用inline表达式,灵活配置数据节点
行表达式的使用: https://shardingsphere.apache.org/document/5.1.1/cn/features/sharding/concept/inline-expression/)
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
3.5 配置分片策略
分片策略包括分片键和分片算法。
分片规则,约定cid值为偶数时,添加到t_course_1表,如果cid是奇数则添加到t_course_2表
- 配置分片策略
#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}
3.6 分布式序列算法
雪花算法:
https://shardingsphere.apache.org/document/5.1.1/cn/features/sharding/concept/key-generator/
水平分片需要关注全局序列,因为不能简单的使用基于数据库的主键自增。
这里有两种方案:一种是基于MyBatisPlus的id策略;一种是ShardingSphere-JDBC的全局序列配置。
- 基于MyBatisPlus的id策略:将Course类的id设置成如下形式
@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的策略二选一
#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策略修改成以下形式:
//当配置了shardingsphere-jdbc的分布式序列时,自动使用shardingsphere-jdbc的分布式序列
//当没有配置shardingsphere-jdbc的分布式序列时,自动依赖数据库的主键自增策略
@TableId(type = IdType.AUTO)
4 实现水平分库
水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。接下来看一下如何使用Sharding-JDBC实现水平分库
4.1 数据准备
- 创建数据库
在mysql-server01服务器上, 创建数据库 ivy_course_db0, 在mysql-server02服务器上, 创建数据库 ivy_course_db1
- 创建表
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
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
-
实体类
原有的Course类添加一个
corder_no
即可.
@TableName("t_course")
@Data
@ToString
public class Course implements Serializable {
// @TableId(value = "cid",type = IdType.ASSIGN_ID)
//是否配置sharding-jdbc的分布式序列 ? 是:使用ShardingJDBC的分布式序列,否:自动依赖数据库的主键自增策略
@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;
}
4.2 配置文件
1) 数据源配置
#===============数据源配置
#配置真实的数据源
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.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_course_db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
#数据源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.jdbc-url=jdbc:mysql://192.168.52.11:3306/ivy_course_db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=QiDian@666
2) 数据节点配置
先测试水平分库, 数据节点中数据源是动态的, 数据表固定为t_course_0, 方便测试
#配置数据节点
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_0
#spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_$->{1..2}
3) 水平分库之分库策略配置
分库策略: 以 user_id
为分片键,分片策略为 user_id % 2
,user_id为偶数操作db0数据源,否则操作db1数据源。
#===============水平分库-分库策略==============
#----分片列名称----
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}
4) 分布式主键自增
#4.分布式序列配
#4.1 分布式序列-列名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.column=cid
#4.2 分布式序列-算法名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.key-generator-name=alg-snowflake
#4.3 分布式序列-算法类型
spring.shardingsphere.rules.sharding.key-generators.alg-snowflake.type=SNOWFLAKE
3) 测试
/**
* 水平分库 --> 分库插入数据
*/
@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);
}
}
4) 水平分库之分表策略配置
分表规则:t_course
表中 cid
的哈希值为偶数时,数据插入对应服务器的 t_course_0
表,cid
的哈希值为奇数时,数据插入对应服务器的 t_course_1
。
- 修改数据节点配置,数据落地到dn0或db1数据源的 t_course_0表 或者 t_course_1表.
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_$->{0..1}
- 分表策略配置 (对id进行哈希取模)
#===============水平分库-分表策略==============
#----分片列名称----
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}
官方提供分片算法配置
https://shardingsphere.apache.org/document/current/cn/dev-manual/sharding/

#----分片列名称----
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
4.3 水平分库测试
- 测试插入数据
/**
* 水平分库 --> 分表插入数据
*/
@Test
public void testInsertCourseTable(){
for (int i = 101; i < 130; i++) {
Course course = new Course();
//userId为偶数数时插入到 ivy_course_0数据库,为奇数时插入到ivy_course_1数据库
course.setUserId(1L+i);
course.setCname("Java经典面试题讲解");
course.setBrief("课程涵盖目前最容易被问到的10000道Java面试题");
course.setPrice(100.0);
course.setStatus(1);
courseMapper.insert(course);
}
}
//验证Hash取模分片是否正确
@Test
public void testHashMod(){
//cid的hash值为偶数时,插入对应数据库的t_course_0表,为奇数插入对应数据库的t_course_1
Long cid = 797197529904054273L; //获取到cid
int hash = cid.hashCode();
System.out.println(hash);
System.out.println("===========" + Math.abs(hash % 2) ); //获取针对cid进行hash取模后的值
}
- 测试查询数据
//查询所有记录
@Test
public void testShardingSelectAll(){
List<Course> courseList = courseMapper.selectList(null);
courseList.forEach(System.out::println);
}
- 查看日志: 查询了两个数据源,每个数据源中使用UNION ALL连接两个表
//根据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连接数据源中的两个表
4.4 水平分库总结
水平分库包含了分库策略和分表策略.
- 分库策略 ,目的是将一个逻辑表 , 映射到多个数据源
#===============水平分库-分库策略==============
#----分片列名称----
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}
- 分表策略, 如何将一个逻辑表 , 映射为多个 实际表
#===============水平分库-分表策略==============
#----分片列名称----
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}
5 实现绑定表
先来回顾一下绑定表的概念: 指的是分片规则一致的关系表(主表、子表),例如t_order和t_order_item,均按照order_id分片,则此两个表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,可以提升关联查询效率。
注: 绑定表是建立在多表关联的基础上的.所以我们先来完成多表关联的配置
5.1 数据准备
-
创建表
在
mysql-server01
服务器上的ivy_course_db0
数据库 和mysql-server02
服务器上的ivy_course_db1
数据库分别创建t_course_section_0
和t_course_section_1
表 ,表结构如下:CREATE TABLE `t_course_section_0` ( `id` bigint(11) NOT NULL, `cid` bigint(11) DEFAULT NULL, `corder_no` bigint(20) DEFAULT NULL, `user_id` bigint(20) DEFAULT NULL, `section_name` varchar(50) DEFAULT NULL, `status` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 CREATE TABLE `t_course_section_1` ( `id` bigint(11) NOT NULL, `cid` bigint(11) DEFAULT NULL, `corder_no` bigint(20) DEFAULT NULL, `user_id` bigint(20) DEFAULT NULL, `section_name` varchar(50) DEFAULT NULL, `status` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
5.2 创建实体类
@TableName("t_course_section")
@Data
@ToString
public class CourseSection {
@TableId(type = IdType.AUTO)
private Long id;
private Long cid; //课程id
private Long corderNo;
private Long userId;
private String sectionName;
private int status;
}
5.3 创建mapper
@Mapper
public interface CourseSectionMapper extends BaseMapper<CourseSection> {
}
5.4 配置多表关联
t_course_section的分片表、分片策略、分布式序列策略和t_course保持一致
- 数据源
#===============数据源配置
#配置真实的数据源
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.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_course_db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
#数据源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.jdbc-url=jdbc:mysql://192.168.52.11:3306/ivy_course_db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=QiDian@666
- 数据节点
#配置数据节点
spring.shardingsphere.rules.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_$->{0..1}
spring.shardingsphere.rules.sharding.tables.t_course_section.actual-data-nodes=db$->{0..1}.t_course_section_$->{0..1}
- 分库策略
#===============分库策略==============
# 用于单分片键的标准分片场景
#t_course与t_course_section表 都使用user_id作为分库的分片键,这样就能够保证user_id相同的数据落入到同一个库中
# 分片列名称
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-mod
# 分片列名称
spring.shardingsphere.rules.sharding.tables.t_course_section.database-strategy.standard.sharding-column=user_id
# 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course_section.database-strategy.standard.sharding-algorithm-name=table-mod
- 分表策略
#====================分表策略===================
#t_course与t_course_section表都使用corder_no作为分表的分片键,这样就能够保证corder_no相同的数据落入到同一个表中
# 用于单分片键的标准分片场景
# 分片列名称
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-column=corder_no
# 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course.table-strategy.standard.sharding-algorithm-name=table-hash-mod
# 分片列名称
spring.shardingsphere.rules.sharding.tables.t_course_section.table-strategy.standard.sharding-column=corder_no
# 分片算法名称
spring.shardingsphere.rules.sharding.tables.t_course_section.table-strategy.standard.sharding-algorithm-name=table-hash-mod
- 分片算法
#=======================分片算法配置==============
# 取模分片算法
# 分片算法类型
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
- 分布式主键
#========================布式序列策略配置====================
# t_course表主键生成策略
# 分布式序列列名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.column=cid
# 分布式序列算法名称
spring.shardingsphere.rules.sharding.tables.t_course.key-generate-strategy.key-generator-name=snowflake
# t_course_section 表主键生成策略
# 分布式序列列名称
spring.shardingsphere.rules.sharding.tables.t_course_section.key-generate-strategy.column=id
# 分布式序列算法名称
spring.shardingsphere.rules.sharding.tables.t_course_section.key-generate-strategy.key-generator-name=snowflake
#------------------------分布式序列算法配置
# 分布式序列算法类型
spring.shardingsphere.rules.sharding.key-generators.snowflake.type=SNOWFLAKE
5.5 测试插入数据
//测试关联表插入
@Test
public void testInsertCourseAndCourseSection(){
//userID为奇数 --> ivy_course_db1数据库
for (int i = 0; i < 3; i++) {
Course course = new Course();
course.setUserId(1L);
//CorderNo为偶数 --> t_course_0, 为奇数t_course_1
course.setCorderNo(1000L + i);
course.setPrice(100.0);
course.setCname("ShardingSphere实战");
course.setBrief("ShardingSphere实战-直播课");
course.setStatus(1);
courseMapper.insert(course);
Long cid = course.getCid();
for (int j = 0; j < 3; j++) { //每个课程 设置三个章节
CourseSection section = new CourseSection();
section.setUserId(1L);
//CorderNo为偶数 --> t_course_0, 为奇数t_course_1
section.setCorderNo(1000L + i);
section.setCid(cid);
section.setSectionName("ShardingSphere实战_" + i);
section.setStatus(1);
courseSectionMapper.insert(section);
}
}
//userID为偶数 --> ivy_course_db0
for (int i = 3; i < 5; i++) {
Course course = new Course();
course.setUserId(2L);
//CorderNo为偶数 --> t_course_0, 为奇数t_course_1
course.setCorderNo(1000L + i);
course.setPrice(100.0);
course.setCname("ShardingSphere实战");
course.setBrief("ShardingSphere实战-直播课");
course.setStatus(1);
courseMapper.insert(course);
Long cid = course.getCid();
for (int j = 0; j < 3; j++) {
CourseSection section = new CourseSection();
//CorderNo为偶数 --> t_course_section_0, 为奇数t_course_section_1
section.setCorderNo(1000L + i);
section.setCid(cid);
section.setUserId(2L);
section.setSectionName("ShardingSphere实战_" + i);
section.setStatus(1);
courseSectionMapper.insert(section);
}
}
}
5.6 配置绑定表
需求说明: 查询每个订单的订单号和课程名称以及每个课程的章节的数量.
- 根据需求编写SQL
SELECT
c.corder_no,
c.cname,
COUNT(cs.id) num
FROM t_course c INNER JOIN t_course_section cs ON c.corder_no = cs.corder_no
GROUP BY c.corder_no,c.cname;
- 创建VO类
@Data
public class CourseVo {
private long corderNo;
private String cname;
private int num;
}
- 添加Mapper方法
@Mapper
public interface CourseMapper extends BaseMapper<Course> {
@Select({"SELECT \n" +
" c.corder_no,\n" +
" c.cname,\n" +
" COUNT(cs.id) num\n" +
"FROM t_course c INNER JOIN t_course_section cs ON c.corder_no = cs.corder_no\n" +
"GROUP BY c.corder_no,c.cname"})
List<CourseVo> getCourseNameAndSectionName();
}
- 进行关联查询
//测试关联表查询
@Test
public void testSelectCourseNameAndSectionName(){
List<CourseVo> list = courseMapper.getCourseNameAndSectionName();
list.forEach(System.out::println);
}
- 如果不配置绑定表:测试的结果为8个SQL。多表关联查询会出现笛卡尔积关联。
- 配置绑定表
https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/spring-boot-starter/rules/sharding/
#======================绑定表
spring.shardingsphere.rules.sharding.binding-tables[0]=t_course,t_course_section
- 如果配置绑定表:测试的结果为4个SQL。 多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
6 实现广播表(公共表)
6.1 公共表介绍
公共表属于系统中数据量较小,变动少,而且属于高频联合查询的依赖表。参数表、数据字典表等属于此类型。
可以将这类表在每个数据库都保存一份,所有更新操作都同时发送到所有分库执行。接下来看一下如何使用Sharding-JDBC实现公共表的数据维护。
6.2 代码编写
1) 创建表
分别在 ivy_course_db0, ivy_course_db1,ivy_user_db 都创建 t_district表
-- 区域表
CREATE TABLE t_district (
id BIGINT(20) PRIMARY KEY COMMENT '区域ID',
district_name VARCHAR(100) COMMENT '区域名称',
LEVEL INT COMMENT '等级'
);
2) 创建实体类
@TableName("t_district")
@Data
public class District {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String districtName;
private int level;
}
3) 创建mapper
@Mapper
public interface DistrictMapper extends BaseMapper<District> {
}
6.3 广播表配置
- 数据源
#===============数据源配置
#配置真实的数据源
spring.shardingsphere.datasource.names=db0,db1,user_db
#数据源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.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_course_db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
#数据源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.jdbc-url=jdbc:mysql://192.168.52.11:3306/ivy_course_db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=QiDian@666
#数据源3
spring.shardingsphere.datasource.user_db.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.user_db.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.user_db.url = jdbc:mysql://192.168.52.11:3306/ivy_user_db?characterEncoding=UTF-8&useSSL=false
spring.shardingsphere.datasource.user_db.username = root
spring.shardingsphere.datasource.user_db.password = QiDian@666
- 广播表配置
#数据节点可不配置,默认情况下,向所有数据源广播
spring.shardingsphere.rules.sharding.tables.t_district.actual-data-nodes=db$->{0..1}.t_district,user_db.t_district
#------------------------广播表配置
# 广播表规则列表
spring.shardingsphere.rules.sharding.broadcast-tables[0]=t_district
6.4 测试广播表
//广播表: 插入数据
@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);
}
二、读写分离
1 MySQL主从同步
1.1 主从同步原理
读写分离是建立在MySQL主从复制基础之上实现的,所以必须先搭建MySQL的主从复制架构。一般默认写操作在主机,读操作在从机。
主从复制的用途
- 实时灾备,用于故障切换
- 读写分离,提供查询服务
- 备份,避免影响业务
主从部署必要条件
- 主库开启binlog日志(设置log-bin参数)
- 主从server-id不同
- 从库服务器能连通主库
主从复制的原理
- Mysql 中有一种日志叫做 bin 日志(二进制日志)。这个日志会记录下所有修改了数据库的SQL 语句(insert,update,delete,create/alter/drop table, grant 等等)。
- 主从复制的原理其实就是把主服务器上的 bin 日志复制到从服务器上执行一遍,这样从服务器上的数据就和主服务器上的数据相同了。
- 主库db的更新事件(update、insert、delete)被写到binlog
- 主库创建一个binlog dump thread,把binlog的内容发送到从库
- 从库启动并发起连接,连接到主库
- 从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log
- 从库启动之后,创建一个SQL线程,从relay log里面读取内容,执行读取到的更新事件,将更新内容写入到slave的db
1.2 主从复制架构搭建
Mysql的主从复制至少是需要两个Mysql的服务,当然Mysql的服务是可以分布在不同的服务器上,也可以在一台服务器上启动多个服务。
1) 第一步 master中创建数据库和表
-- 创建数据库
CREATE DATABASE test CHARACTER SET utf8;
-- 创建表
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);
2) 第二步 修改主数据库的配置文件my.cnf
vim /etc/my.cnf
插入下面的内容
lower_case_table_names=1
log-bin=mysql-bin
server-id=1
binlog-do-db=test
binlog_ignore_db=mysql
- server-id=1 中的1可以任定义,只要是唯一的就行。
- log-bin=mysql-bin 表示启用binlog功能,并制定二进制日志的存储目录,
- binlog-do-db=test 是表示只备份test 数据库。
- binlog_ignore_db=mysql 表示忽略备份mysql。
- 不加binlog-do-db和binlog_ignore_db,那就表示备份全部数据库。
3) 第三步 重启MySQL
service mysqld restart
4) 第四步 在主数据库上, 创建一个允许从数据库来访问的用户账号.
用户: slave
密码:123456
主从复制使用 REPLICATION SLAVE
赋予权限
-- 创建账号
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'192.168.52.11' IDENTIFIED BY 'Qwer@1234';
5) 第五步 停止主数据库的更新操作, 并且生成主数据库的备份
-- 执行以下命令锁定数据库以防止写入数据。
FLUSH TABLES WITH READ LOCK;
6) 导出数据库,恢复写操作
使用SQLYog导出,主数据库备份完毕,恢复写操作
unlock tables;
7) 将刚才主数据库备份的test.sql导入到从数据库
导入后, 主库和从库数据会追加相平,保持同步!此过程中,若主库存在业务,并发较高,在同步的时候要先锁表,让其不要有修改!等待主从数据追平,主从同步后在打开锁!
8) 接着修改从数据库的 my.cnf
- 增加server-id参数,保证唯一.
server-id=2
-- 重启
service mysqld restart
10) 在从数据库设置相关信息
- 执行以下SQL
STOP SLAVE;
CHANGE MASTER TO MASTER_HOST='192.168.52.10',
MASTER_USER='slave',
MASTER_PASSWORD='Qwer@1234',
MASTER_PORT=3306,
MASTER_LOG_FILE='mysql-bin.000015',
MASTER_LOG_POS=442,
MASTER_CONNECT_RETRY=10;
11) 修改auto.cnf中的UUID,保证唯一
-- 编辑auto.cnf
vim /var/lib/mysql/auto.cnf
-- 修改UUID的值
server-uuid=a402ac7f-c392-11ea-ad18-000c2980a208
-- 重启
service mysqld restart
12) 在从服务器上,启动slave 进程
start slave;
-- 查看状态
SHOW SLAVE STATUS;
-- 命令行下查看状态 执行
SHOW SLAVE STATUS \G;
13) 现在可以在我们的主服务器做一些更新的操作,然后在从服务器查看是否已经更新
-- 在主库插入一条数据,观察从库是否同步
INSERT INTO users VALUES(NULL,'user4',23);
1.3 常见问题解决
启动主从同步后,常见错误是 Slave_IO_Running: No 或者 Connecting
的情况
解决方案1:
- 首先停掉Slave服务
-- 在从机停止slave
stop slave;
- 到主服务器上查看主机状态, 记录File和Position对应的值
-- 在主机查看mater状态
SHOW MASTER STATUS;
- 然后到slave服务器上执行手动同步:
-- MASTER_LOG_FILE和MASTER_LOG_POS与主库保持一致
CHANGE MASTER TO MASTER_HOST='192.168.52.10',
MASTER_USER='slave',
MASTER_PASSWORD='Qwer@1234',
MASTER_PORT=3306,
MASTER_LOG_FILE='mysql-bin.000015',
MASTER_LOG_POS=442,
MASTER_CONNECT_RETRY=10;
解决方案2
- 程序可能在slave上进行了写操作
- 也可能是slave机器重起后,事务回滚造成的.
- 一般是事务回滚造成的,解决办法
mysql> slave stop;
mysql> set GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
mysql> slave start;
2 实现读写分离
2.1 数据准备
为了实现Sharding-JDBC的读写分离,首先,要进行mysql的主从同步配置。在上面的课程中我们已经配置完成。了。
- 在主服务器中的 test数据库 创建商品表
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.2 创建SpringBoot程序
环境说明:
SpringBoot2.3.7
+MyBatisPlus
+ShardingSphere-JDBC 5.1
+Hikari
+MySQL 5.7
1) 创建项目
项目名称: sharding-jdbc-write-read
Spring脚手架: http://start.aliyun.com
2) 引入依赖
<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) 创建实体类
@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
@Mapper
public interface ProductsMapper extends BaseMapper<Products> {
}
2.3 配置读写分离
https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/spring-boot-starter/rules/readwrite-splitting/
application.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.jdbc-url=jdbc:mysql://192.168.52.10:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.master.username=root
spring.shardingsphere.datasource.master.password=QiDian@666
#数据源2
spring.shardingsphere.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave.jdbc-url=jdbc:mysql://192.168.52.11:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.slave.username=root
spring.shardingsphere.datasource.slave.password=QiDian@666
# 读写分离类型,如: 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
负载均衡相关配置
https://shardingsphere.apache.org/document/current/cn/dev-manual/readwrite-splitting/
# 负载均衡算法名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.load-balancer-name=alg_round
# 负载均衡算法配置
# 负载均衡算法类型
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_round.type=ROUND_ROBIN # 轮询
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_random.type=RANDOM # 随机
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=2
2.4 读写分离测试
//插入测试
@Test
public void testInsert(){
Products products = new Products();
products.setPname("电视机");
products.setPrice(100);
products.setFlag("0");
productsMapper.insert(products);
}
@Test
public void testSelect(){
QueryWrapper<Products> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("pname","电视机");
List<Products> products = productsMapper.selectList(queryWrapper);
products.forEach(System.out::println);
}
2.5 读写分离测试
为了保证主从库间的事务一致性,避免跨服务的分布式事务,ShardingSphere-JDBC的主从模型中,事务中的数据读写均用主库。
- 不添加@Transactional:insert对主库操作,select对从库操作
- 添加@Transactional:则insert和select均对主库操作
- **注意:**在JUnit环境下的@Transactional注解,默认情况下就会对事务进行回滚(即使在没加注解@Rollback,也会对事务回滚)
//事务测试
@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);
}
三、强制路由
1 强制路由是什么?
https://shardingsphere.apache.org/document/4.1.0/cn/manual/sharding-jdbc/usage/hint/
在一些应用场景中,分片条件并不存在于SQL,而存在于外部业务逻辑。因此需要提供一种通过在外部业务代码中指定路由配置的一种方式,在ShardingSphere中叫做Hint。如果使用Hint指定了强制分片路由,那么SQL将会无视原有的分片逻辑,直接路由至指定的数据节点操作。
Hint使用场景:
- 数据分片操作,如果分片键没有在SQL或数据表中,而是在业务逻辑代码中
- 读写分离操作,如果强制在主库进行某些数据操作
2 强制路由的使用
基于 Hint 进行强制路由的设计和开发过程需要遵循一定的约定,同时,ShardingSphere 也提供了专门的 HintManager 来简化强制路由的开发过程.
2.1 环境准备
-
在
ivy_course_db0
和ivy_course_db1
中创建 t_course表.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
-
创建一个maven项目,直接下一步即可
-
创建完成后,引入依赖 (注意: 在这里我们使用ShardingSphere4.1版本演示强制路由)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>shardingjdbc-hint</artifactId>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis plus 代码生成器 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- ShardingSphere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-core-common</artifactId>
<version>4.1.0</version>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.8</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 代码编写
- 启动类: ShardingSphereDemoApplication
@SpringBootApplication
@MapperScan("com.mashibing.mapper")
public class ShardingSphereDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ShardingSphereDemoApplication.class, args);
}
}
- Course
@TableName("t_course")
@Data
@ToString
public class Course {
@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
@Repository
public interface CourseMapper extends BaseMapper<Course> {
}
-
自定义MyHintShardingAlgorithm类
在该类中编写分库或分表路由策略,实现HintShardingAlgorithm接口,重写doSharding方法
// 泛型Long表示传入的参数是Long类型
public class MyHintShardingAlgorithm implements HintShardingAlgorithm<Long> {
/**
* collection: 代表分片目标,对哪些数据库、表分片.比如这里如果是对分库路由,表示db0.db1
* hintShardingValue: 代表分片值,可以通过 HintManager 设置多个分片值,所以是个集合
*/
@Override
public Collection<String> doSharding(Collection<String> collection,
HintShardingValue<Long> hintShardingValue) {
// 添加分库或分表路由逻辑
Collection<String> result = new ArrayList<>();
for (String actualDb : collection){
for (Long value : hintShardingValue.getValues()){
//分库路由,判断当前节点名称结尾是否与取模结果一致
if(actualDb.endsWith(String.valueOf(value % 2))){
result.add(actualDb);
}
}
}
return result;
}
}
2.3 配置文件
application.properties
# 应用名称
spring.application.name=sharding-jdbc-hint
#===============数据源配置
# 命名数据源 这个是自定义的
spring.shardingsphere.datasource.names=db0,db1
# 配置数据源db0
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_course_db0?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
## 配置数据源db1
spring.shardingsphere.datasource.db1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.jdbc-url=jdbc:mysql://192.168.52.11:3306/ivy_course_db1?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=QiDian@666
# 配置默认数据源db0
spring.shardingsphere.sharding.default-data-source-name=db0
# Hint强制路由
# 使用t_course表测试强制路由到库
spring.shardingsphere.sharding.tables.t_course.database-strategy.hint.algorithm-class-name=com.mashibing.hint.MyHintShardingAlgorithm
# 打印SQl
spring.shardingsphere.props.sql.show=true
2.4 强制路由到库测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShardingSphereDemoApplication.class)
public class TestHintAlgorithm {
@Autowired
private CourseMapper courseMapper;
//测试强制路由,在业务代码中执行查询前使用HintManager指定执行策略值
@Test
public void testHintInsert(){
HintManager hintManager = HintManager.getInstance();
//如果只是针对库路由,就调用setDatabaseShardingValue方法
hintManager.setDatabaseShardingValue(1L); //添加数据源分片键值,强制路由到db$->{1%2} = db1
for (int i = 1; i < 9; 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);
}
}
//测试查询
@Test
public void testHintSelect(){
HintManager hintManager = HintManager.getInstance();
hintManager.setDatabaseShardingValue(1L);
List<Course> courses = courseMapper.selectList(null);
System.out.println(courses);
}
}
2.5 强制路由到库到表测试
- 配置文件
# 应用名称
spring.application.name=sharding-jdbc-hint
#===============数据源配置
# 命名数据源 这个是自定义的
spring.shardingsphere.datasource.names=db0,db1
# 配置数据源ds-0
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_course_db0?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
## 配置数据源ds-1
spring.shardingsphere.datasource.db1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db1.jdbc-url=jdbc:mysql://192.168.52.11:3306/ivy_course_db1?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=QiDian@666
# 配置默认数据源ds-0
spring.shardingsphere.sharding.default-data-source-name=db0
# Hint强制路由
# 使用t_course表测试强制路由到库
#spring.shardingsphere.sharding.tables.t_course.database-strategy.hint.algorithm-class-name=com.mashibing.hint.MyHintShardingAlgorithm
# 使用t_course表测试强制路由到库和表
spring.shardingsphere.sharding.tables.t_course.database-strategy.hint.algorithm-class-name=com.mashibing.hint.MyHintShardingAlgorithm
spring.shardingsphere.sharding.tables.t_course.table-strategy.hint.algorithm-class-name=com.mashibing.hint.MyHintShardingAlgorithm
spring.shardingsphere.sharding.tables.t_course.actual-data-nodes=db$->{0..1}.t_course_$->{0..1}
# 打印SQl
spring.shardingsphere.props.sql.show=true
- 测试
@Test
public void testHintSelectTable() {
HintManager hintManager = HintManager.getInstance();
//强制路由到db1数据库
hintManager.addDatabaseShardingValue("t_course", 1L);
//强制路由到t_course_1表
hintManager.addTableShardingValue("t_course",1L);
List<Course> courses = courseMapper.selectList(null);
courses.forEach(System.out::println);
}
2.6 强制路由走主库查询测试
在读写分离结构中,为了避免主从同步数据延迟及时获取刚添加或更新的数据,可以采用强制路由走主库查询实时数据,使用hintManager.setMasterRouteOnly设置主库路由即可。
- 配置文件
# 应用名称
spring.application.name=sharding-jdbc-hint01
# 定义多个数据源
spring.shardingsphere.datasource.names = m1,s1
#读写分离数据源
spring.shardingsphere.datasource.m1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.m1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.jdbc-url=jdbc:mysql://192.168.52.10:3306/test_rw?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=QiDian@666
spring.shardingsphere.datasource.s1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.s1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s1.jdbc-url=jdbc:mysql://192.168.52.10:3306/test_rw?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.s1.username=root
spring.shardingsphere.datasource.s1.password=QiDian@666
#主库与从库的信息
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
- 测试
//强制路由走主库
@Test
public void testHintReadTableToMaster() {
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
List<Products> products = productsMapper.selectList(null);
products.forEach(System.out::println);
}
2.6 SQL执行流程剖析
ShardingSphere 3个产品的数据分片功能主要流程是完全一致的,如下图所示。
-
SQL解析
SQL解析分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。
Sharding-JDBC采用不同的解析器对SQL进行解析,解析器类型如下:
- MySQL解析器
- Oracle解析器
- SQLServer解析器
- PostgreSQL解析器
- 默认SQL解析器
-
查询优化
负责合并和优化分片条件,如OR等。 -
SQL路由
根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
-
SQL改写
将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。
-
SQL执行
通过多线程执行器异步执行SQL。
-
结果归并
将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。
四、数据加密(数据脱敏)
1 数据加密(数据脱敏)是什么
数据加密(数据脱敏) 是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、卡号、客户号等个人信息按照规定,都需要进行数据脱敏。
数据加密模块属于ShardingSphere分布式治理这一核心功能下的子功能模块。
- Apache ShardingSphere 通过对用户输入的 SQL 进行解析,并依据用户提供的加密规则对 SQL 进行改写,从而实现对原文数据进行加密,并将原文数据(可选)及密文数据同时存储到底层数据库。
- 在用户查询数据时,它仅从数据库中取出密文数据,并对其解密,最终将解密后的原始数据返回给用户。
Apache ShardingSphere自动化&透明化了数据脱敏过程,让用户无需关注数据脱敏的实现细节,像使用普通数据那样使用脱敏数据。
2 整体架构
ShardingSphere提供的Encrypt-JDBC和业务代码部署在一起。业务方需面向Encrypt-JDBC进行JDBC编程。
加密模块将用户发起的 SQL 进行拦截,并通过 SQL 语法解析器进行解析、理解 SQL 行为,再依据用户传入的加密规则,找出需要加密的字段和所使用的加解密算法对目标字段进行加解密处理后,再与底层数据库进行交互。
Apache ShardingSphere 会将用户请求的明文进行加密后存储到底层数据库;并在用户查询时,将密文从数据库中取出进行解密后返回给终端用户。
通过屏蔽对数据的加密处理,使用户无需感知解析 SQL、数据加密、数据解密的处理过程,就像在使用普通数据一样使用加密数据。
3 加密规则
脱敏配置主要分为四部分:数据源配置,加密器配置,脱敏表配置以及查询属性配置,其详情如下图所示:
- 数据源配置:指DataSource的配置信息
- 加密器配置:指使用什么加密策略进行加解密。目前ShardingSphere内置了两种加解密策略:AES/MD5
- 脱敏表配置:指定哪个列用于存储密文数据(cipherColumn)、哪个列用于存储明文数据(plainColumn)以及用户想使用哪个列进行SQL编写(logicColumn)
- 查询属性的配置:当底层数据库表里同时存储了明文数据、密文数据后,该属性开关用于决定是直接查询数据库表里的明文数据进行返回,还是查询密文数据通过Encrypt-JDBC解密后返回。
4 脱敏处理流程
下图可以看出ShardingSphere将逻辑列与明文列和密文列进行了列名映射。
下方图片展示了使用Encrypt-JDBC进行增删改查时,其中的处理流程和转换逻辑,如下图所示。
5 数据脱敏实战
5.1 环境搭建
- 创建数据库及表
CREATE TABLE `t_user` (
`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
- 创建maven项目,并引入依赖
与强制路由的项目创建方式相同,引入依赖也相同.
- 启动类
@SpringBootApplication
@MapperScan("com.mashibing.mapper")
public class ShardingSphereApplication {
public static void main(String[] args) {
SpringApplication.run(ShardingSphereApplication.class, args);
}
}
- 创建实体类
@TableName("t_user")
@Data
public class User {
@TableId(value = "user_id",type = IdType.ASSIGN_ID)
private Long userId;
private String userName;
private String password;
private String passwordEncrypt;
private String passwordAssisted;
}
- 创建Mapper
@Repository
public interface UserMapper 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<User> getUserInfo(String userName, String password);
}
- 配置文件
# 应用名称
spring.application.name=sharding-jdbc-encryption
#===============数据源配置
# 命名数据源 这个是自定义的
spring.shardingsphere.datasource.names=db0
# 配置数据源ds0
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_encryption_db?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
# 打印SQl
spring.shardingsphere.props.sql.show=true
- 测试插入与查询
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShardingSphereApplication.class)
public class TestShardingEncryption {
@Autowired
private UserMapper usersMapper;
@Test
public void testInsertUser(){
User users = new User();
users.setUserName("user2022");
users.setPassword("123456");
usersMapper.insetUser(users);
}
@Test
public void testSelectUser(){
List<User> userList = usersMapper.getUserInfo("user2022", "123456");
userList.forEach(System.out::println);
}
}
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将调用用户提供的该方案的具体实现类进行数据脱敏。
5.3 默认AES加密算法实现
数据加密默认算法支持 AES 和 MD5 两种
-
AES 对称加密: 同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密
加密:明文 + 密钥 -> 密文 解密:密文 + 密钥 -> 明文
-
MD5算是一个生成签名的算法,引起结果不可逆.
MD5的优点:计算速度快,加密速度快,不需要密钥;
MD5的缺点: 将用户的密码直接MD5后存储在数据库中是不安全的。很多人使用的密码是常见的组合,威胁者将这些密码的常见组合进行单向哈希,得到一个摘要组合,然后与数据库中的摘要进行比对即可获得对应的密码。
https://www.tool.cab/decrypt/md5.html
配置文件
# 应用名称
spring.application.name=sharding-jdbc-encryption
#===============数据源配置
# 命名数据源 这个是自定义的
spring.shardingsphere.datasource.names=db0
# 配置数据源ds0
spring.shardingsphere.datasource.db0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.db0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.db0.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_encryption_db?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
# 采用AES对称加密策略
spring.shardingsphere.encrypt.encryptors.encryptor_aes.type=aes
spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value=123456abc
# password为逻辑列,password.plainColumn为数据表明文列,password.cipherColumn为数据表密文列
spring.shardingsphere.encrypt.tables.t_user.columns.password.plainColumn=password
spring.shardingsphere.encrypt.tables.t_user.columns.password.cipherColumn=password_encrypt
spring.shardingsphere.encrypt.tables.t_user.columns.password.encryptor=encryptor_aes
# 查询是否使用密文列
spring.shardingsphere.props.query.with.cipher.column=true
# 打印SQl
spring.shardingsphere.props.sql.show=true
测试插入数据
- 设置了明文列和密文列,运行成功,新增时逻辑列会改写成明文列和密文列
-
仅设置明文列,运行直接报错,所以必须设置加密列
-
仅设置密文列,运行成功,明文会进行加密,数据库实际插入到密文列
-
设置了明文列和密文列,
spring.shardingsphere.props.query.with.cipher.column
为true时,查询通过密文列查询,返回数据为明文. -
设置了明文列和密文列,
spring.shardingsphere.props.query.with.cipher.column
为false时,查询通过明文列执行,返回数据为明文列.
5.4 MD5加密算法实现
配置文件
# 采用MD5加密策略
spring.shardingsphere.encrypt.encryptors.encryptor_md5.type=MD5
# password为逻辑列,password.plainColumn为数据表明文列,password.cipherColumn为数据表密文列
spring.shardingsphere.encrypt.tables.t_user.columns.password.plainColumn=password
spring.shardingsphere.encrypt.tables.t_user.columns.password.cipherColumn=password_encrypt
spring.shardingsphere.encrypt.tables.t_user.columns.password.encryptor=encryptor_md5
# 查询是否使用密文列
spring.shardingsphere.props.query.with.cipher.column=true
测试插入数据
-
新增时,可以看到加密后的数据和AES的有所区别
-
查询时,
spring.shardingsphere.props.query.with.cipher.column
为true时,通过密文列查询,由于MD5加密是非对称的,所以返回的是密文数据 -
查询时,
spring.shardingsphere.props.query.with.cipher.column
为false时,通过明文列查询,返回明文数据
五、分布式事务
1 本地事务
单个服务与单个数据库的架构中,产生的事务都是本地事务。其中原子性和持久性其实是依靠undo和redo 日志来实现。
InnoDB的事务日志主要分为:
- undo log(回滚日志,提供回滚操作)
- redo log(重做日志,提供前滚操作)
1) undo log日志介绍
Undo Log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到Undo Log。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。
Undo Log 记录了此次事务**「开始前」** 的数据状态,记录的是更新之 **「前」**的值
undo log 作用:
- 实现事务原子性,可以用于回滚
- 实现多版本并发控制(MVCC), 也即非锁定读
MVCC:undo log + Read View + 隐式字段
Undo log 产生和销毁
- Undo Log在事务开始前产生
- 当事务提交之后,undo log 并不能立马被删除,而是放入待清理的链表
- 会通过后台线程 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 的工作原理
MySQL独有缓存,也是为什么MySQL比其他关系型数据性能高的原因。
Undo + Redo事务的简化过程
假设有A、B两个数据,值分别为1,2
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 记录更新前数据,用于保证事务原子性
- redo log 记录更新后数据,用于保证事务的持久性
- redo log有自己的内存buffer,先写入到buffer,事务提交时写入磁盘
- redo log持久化之后,意味着事务是可提交的
2 环境与配置文件准备
在今天的案例中,我们将演示如何在分库环境下实现分布式事务。
ShardingJDBC支持的分布式事务方式有三种 LOCAL, XA , BASE,这三种事务实现方式都是采用的对代码无侵入的方式实现的
//事务类型枚举类
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 的支持。当然,我们也可以根据需要,集成其他分布式事务类开源框架.
分布式事务模式整合流程
ShardingSphere 作为一款分布式数据库中间件,势必要考虑分布式事务的实现方案。在设计上,ShardingSphere整合了XA、Saga和Seata模式后,为分布式事务控制提供了极大的便利,我们可以在应用程序编程时,采用以下统一模式进行使用。
- 引入maven依赖
<!--XA模式-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
<version>4.1.0</version>
</dependency>
- JAVA编码方式设置事务类型
@ShardingSphereTransactionType(TransactionType.XA) // Sharding-jdbc一致性性事务
@ShardingSphereTransactionType(TransactionType.BASE) // Sharding-jdbc柔性事务
-
参数配置
ShardingSphere默认的XA事务管理器为Atomikos,通过在项目的classpath中添加jta.properties来定制化Atomikos配置项。
XA模式具体的配置规则如下:
#指定是否启动磁盘日志,默认为true。在生产环境下一定要保证为true,否则数据的完整性无法保证 com.atomikos.icatch.enable_logging=true #JTA/XA资源是否应该自动注册 com.atomikos.icatch.automatic_resource_registration=true #JTA事务的默认超时时间,默认为10000ms com.atomikos.icatch.default_jta_timeout=10000 #事务的最大超时时间,默认为300000ms。这表示事务超时时间由 UserTransaction.setTransactionTimeout()较大者决定。4.x版本之后,指定为0的话则表示不设置超时时间 com.atomikos.icatch.max_timeout=300000 #指定在两阶段提交时,是否使用不同的线程(意味着并行)。3.7版本之后默认为false,更早的版本默认为true。如果为false,则提交将按照事务中访问资源的顺序进行。 com.atomikos.icatch.threaded_2pc=false #指定最多可以同时运行的事务数量,默认值为50,负数表示没有数量限制。在调用 UserTransaction.begin()方法时,可能会抛出一个”Max number of active transactions reached”异常信息,表示超出最大事务数限制 com.atomikos.icatch.max_actives=50 #是否支持subtransaction,默认为true com.atomikos.icatch.allow_subtransactions=true #指定在可能的情况下,否应该join 子事务(subtransactions),默认值为true。如果设置为false,对于有关联的不同subtransactions,不会调用XAResource.start(TM_JOIN) com.atomikos.icatch.serial_jta_transactions=true #指定JVM关闭时是否强制(force)关闭事务管理器,默认为false com.atomikos.icatch.force_shutdown_on_vm_exit=false #在正常关闭(no-force)的情况下,应该等待事务执行完成的时间,默认为Long.MAX_VALUE com.atomikos.icatch.default_max_wait_time_on_shutdown=9223372036854775807 ========= 日志记录配置======= #事务日志目录,默认为./。 com.atomikos.icatch.log_base_dir=./ #事务日志文件前缀,默认为tmlog。事务日志存储在文件中,文件名包含一个数字后缀,日志文件以.log为扩展名,如tmlog1.log。遇到checkpoint时,新的事务日志文件会被创建,数字增加。 com.atomikos.icatch.log_base_name=tmlog #指定两次checkpoint的时间间隔,默认为500 com.atomikos.icatch.checkpoint_interval=500 =========日志恢复配置============= #指定在多长时间后可以清空无法恢复的事务日志(orphaned),默认86400000ms com.atomikos.icatch.forget_orphaned_log_entries_delay=86400000 #指定两次恢复扫描之间的延迟时间。默认值为与com.atomikos.icatch.default_jta_timeout相同 com.atomikos.icatch.recovery_delay=${com.atomikos.icatch.default_jta_timeout} #提交失败时,再抛出一个异常之前,最多可以重试几次,默认值为5 com.atomikos.icatch.oltp_max_retries=5 #提交失败时,每次重试的时间间隔,默认10000ms com.atomikos.icatch.oltp_retry_interval=10000
1) 创建数据库及表
在 ivy_position_db0
和 ivy_position_db1
中分别创建职位表和职位描述表.
-- 职位表
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;
2) 创建一个maven项目
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis plus 代码生成器 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- ShardingSphere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-core-common</artifactId>
<version>4.1.0</version>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.8</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
<scope>compile</scope>
</dependency>
<!-- XA模式-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3) 配置文件
分库环境下实现分布式事务,配置文件
# 应用名称
spring.application.name=sharding-jdbc-trans
# 打印SQl
spring.shardingsphere.props.sql-show=true
# 端口
server.port=8081
#===============数据源配置
#配置真实的数据源
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.jdbc-url=jdbc:mysql://192.168.52.10:3306/ivy_position_db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db0.username=root
spring.shardingsphere.datasource.db0.password=QiDian@666
#数据源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.jdbc-url=jdbc:mysql://192.168.52.11:3306/ivy_position_db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.db1.username=root
spring.shardingsphere.datasource.db1.password=QiDian@666
#分库策略
spring.shardingsphere.sharding.tables.position.database-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.position.database-strategy.inline.algorithm-expression=db$->{id % 2}
spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.sharding-column=pid
spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.algorithm-expression=db$->{pid % 2}
#分布式主键生成
spring.shardingsphere.sharding.tables.position.key-generator.column=id
spring.shardingsphere.sharding.tables.position.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.position_detail.key-generator.column=id
spring.shardingsphere.sharding.tables.position_detail.key-generator.type=SNOWFLAKE
3 案例实现
1) 启动类
@EnableTransactionManagement //开启声明式事务
@SpringBootApplication
@MapperScan("com.test.mapper")
public class ShardingTransApplication {
public static void main(String[] args) {
SpringApplication.run(ShardingTransApplication.class,args);
}
}
2) entity
@TableName("position")
@Data
public class Position {
@TableId(type = IdType.AUTO)
private long id;
private String name;
private String salary;
private String city;
}
@TableName("position_detail")
@Data
public class PositionDetail {
@TableId(type = IdType.AUTO)
private long id;
private long pid;
private String description;
}
3) mapper
@Repository
public interface PositionMapper extends BaseMapper<Position> {
}
@Repository
public interface PositionDetailMapper extends BaseMapper<PositionDetail> {
}
4) controller
@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(){
for (int i=1; i<=3; i++){
Position position = new Position();
position.setName("root"+i);
position.setSalary("1000000");
position.setCity("beijing");
positionMapper.insert(position);
if (i==3){
throw new RuntimeException("人为制造异常");
}
PositionDetail positionDetail = new PositionDetail();
positionDetail.setPid(position.getId());
positionDetail.setDescription("root" + i);
positionDetailMapper.insert(positionDetail);
}
return "SUCCESS";
}
}
4 案例测试
测试1: 访问在PositionController的add方法 , 注意: 方法不添加任何事务控制
@RequestMapping("/add")
public String savePosition()
http://localhost:8081/position/add
提示出现: 人为制造异常
检查数据库, 会发现数据库的数据插入了,但是不是完整的
测试2: 在add 方法上添加@Transactional本地事务控制,继续测试
@Transactional
@RequestMapping("/add")
public String savePosition()
查看数据库发现,使用@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方法
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事务
首先要在项目中导入对应的依赖包
<!--XA模式-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
<version>4.1.0</version>
</dependency>
我们知道,ShardingSphere 提供的事务类型有三种,分别是 LOCAL、XA 和 BASE,默认使用的是 LOCAL。所以如果需要用到分布式事务,需要在业务方法上显式的添加这个注解 @ShardingTransactionType(TransactionType.XA)
@ShardingTransactionType(TransactionType.XA)
@RequestMapping("/add")
public String savePosition(
执行测试代码,结果是数据库的插入全部被回滚了.