在Mycat 基础_moneywenxue的博客-CSDN博客 这篇文章中我们已经实践过一部分分片测试,这一节我们在它的基础上作补充。
略
三个节点 imall 库创建 sharding_by_month 表(上节课的单库分表也是)
CREATE TABLE `sharding_by_month` (
`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`db_nm` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
逻辑表——schema.xml:
分片规则——rule.xml:
create_time
qs-partbymonth
分片算法——rule.xml:
yyyy-MM-dd
2025-10-01
2025-12-31
注意: 节点个数要大于月份的个数
测试语句:
truncate table sharding_by_month;
INSERT INTO sharding_by_month (create_time,db_nm) VALUES ('2024-10-16', database());
INSERT INTO sharding_by_month (create_time,db_nm) VALUES ('2025-10-27', database());
INSERT INTO sharding_by_month (create_time,db_nm) VALUES ('2026-11-04', database());
INSERT INTO sharding_by_month (create_time,db_nm) VALUES ('2027-11-11', database());
INSERT INTO sharding_by_month (create_time,db_nm) VALUES ('2029-12-25', database());
INSERT INTO sharding_by_month (create_time,db_nm) VALUES ('2030-12-31', database());
执行完情况:
10月份在imall_1节点;11月份在imall_2节点;12月份的在imall_3节点。
注意:以上跟年度没有关系,只跟月度有关。
问题:不在 10 月和 12 月之间的数据,路由到哪个节点?结果不一定。可以在本地调试一下。
另外还有按天分片(可以指定多少天一个分片)、按小时分片。
略。
将所有可能出现的值列举出来,指定分片。例如:全国 34 个省,要将不同的省的数
据存放在不同的节点,可用枚举的方式。
略
一致性 hash 有效解决了分布式数据的扩容问题。
建表语句:
CREATE TABLE `sharding_by_murmur` (
`id` int(10) DEFAULT NULL,
`db_nm` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
逻辑表——schema.xml:
分片规则——rule.xml:
id
qs-murmur
分片算法——rule.xml:
0
3
160
测试语句(mycat库-imall):
truncate table sharding_by_murmur;
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (1, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (2, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (3, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (4, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (5, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (6, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (7, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (8, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (9, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (10, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (11, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (12, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (13, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (14, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (15, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (16, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (17, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (18, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (19, database());
INSERT INTO `sharding_by_murmur` (id,db_nm) VALUES (20, database());
测试结果:
逻辑表:
物理表节点:
特点:可以一定程度减少数据的迁移。
这是先求模得到逻辑分片号,再根据逻辑分片号直接映射到物理分片的一种散列算法。
建表语句:
CREATE TABLE `sharding_by_long` (
`id` int(10) DEFAULT NULL,
`db_nm` varchar(20) DEFAULT NULL
) ;
逻辑表——schema.xml:
分片规则——rule.xml:
id
qs-sharding-by-long
平均分成 8 片(%1024 的余数,1024=128*8):
8
128
第二个例子(实践中是这个例子):
两个数组,分成不均匀的 3 个节点(%1024 的余数,1024=2*256+1*512):
2,1
256,512
3 个节点,对 1024 取模余数的分布:
测试语句:
truncate table sharding_by_long;
INSERT INTO `sharding_by_long` (id,db_nm) VALUES (222, database());
INSERT INTO `sharding_by_long` (id,db_nm) VALUES (333, database());
INSERT INTO `sharding_by_long` (id,db_nm) VALUES (666, database());
测试结果:
mycat表:
物理表节点:
特点:在一定范围内 id 是连续分布的。
逻辑表-schema.xml:
建表语句:
CREATE TABLE `sharding_by_pattern` (
`id` varchar(20) DEFAULT NULL,
`db_nm` varchar(20) DEFAULT NULL
) ;
分片规则——rule.xml:
id
sharding-by-pattern
分片算法——rule.xml:
100
0
partition-pattern.txt
patternValue 取模基数,这里设置成 100
partition-pattern.txt,一共 3 个节点:
partitin-pattern.txt:
# id partition range start-end ,data node index
###### first host configuration
1-20=0
21-70=1
71-100=2
测试语句:
INSERT INTO `sharding_by_pattern` (id,db_nm) VALUES (19, database());
INSERT INTO `sharding_by_pattern` (id,db_nm) VALUES (222, database());
INSERT INTO `sharding_by_pattern` (id,db_nm) VALUES (371, database());
特点:可以调整节点的数据分布。
建表语句:
CREATE TABLE `sharding_by_rang_mod` (
`id` bigint(20) DEFAULT NULL,
`db_nm` varchar(20) DEFAULT NULL
);
逻辑表——schema.xml:
分片规则——rule.xml:
id
qs-rang-mod
分片算法——rule.xml:
partition-range-mod.txt
partition-range-mod.txt:
# range start-end ,data node group size
0-20000=1
20001-40000=2
解读:先范围后取模。Id 在 20000 以内的,全部分布到 dn1。Id 在 20001-40000的,%2 分布到 dn2,dn3。
插入数据:
INSERT INTO `sharding_by_rang_mod` (id,db_nm) VALUES (666, database());
INSERT INTO `sharding_by_rang_mod` (id,db_nm) VALUES (6667, database());
INSERT INTO `sharding_by_rang_mod` (id,db_nm) VALUES (16666, database());
INSERT INTO `sharding_by_rang_mod` (id,db_nm) VALUES (21111, database());
INSERT INTO `sharding_by_rang_mod` (id,db_nm) VALUES (22222, database());
INSERT INTO `sharding_by_rang_mod` (id,db_nm) VALUES (23333, database());
INSERT INTO `sharding_by_rang_mod` (id,db_nm) VALUES (24444, database());
特点:扩容的时候旧数据无需迁移。
连续分片优点:
连续分片缺点:
离散分片优点:
离散分片缺点:
步骤:
例如:按照什么递增?序号还是日期?主键是否有业务意义?
一般来说,分片数要比当前规划的节点数要大。
总结:根据业务场景,合理地选择分片规则。
举例:
老师:3.7 亿的数据怎么分表?我是不是分成 3 台服务器?
1、一年内到达多少?两年内到达多少?(数据的增长速度)?
答:一台设备每秒钟往 3 张表各写入一条数据,一共 4 台设备。每张表一天
86400*4=345600 条。每张表一个月 10368000 条。
分析:增长速度均匀,可以用日期切分,每个月分一张表。
2、什么业务?所有的数据都会访问,还是访问新数据为主?
答:访问新数据为主,但是所有的数据都可能会访问到
3、表结构和表数据是什么样的?一个月消耗多少空间
答:字段不多,算过了,三年数据量有 3.7 亿,30G。
分析:30G 没必要分库,浪费磁盘空间。
4、访问量怎么样?并发压力大么?
答:并发有一点吧
分析:如果并发量不大,不用分库,只需要在单库分表。不用引入 Mycat 中间件
了。如果要自动路由的话可以用 Sharding-JDBC,否则就是自己拼装表名。
5、3 张表有没有关联查询之类的操作?
答:没有。
分析:还是拼装表名简单一点。
如果从单库变成分库分表,或者节点数的增加和减少,都会涉及到数据迁移的问
题。数据迁移有两种,一种是在线不停机的迁移,还有一种是停机的。
在线扩容的流程:
如果数据已经超过了单个节点的存储上线,或者需要下线节点的时候,就需要对数
据重新分片。
系统第一次上线,把单张表迁移到 Mycat,可以用 mysqldump。
MySQL 导出:
mysqldump -uroot -p123456 -h127.0.0.1 -P3306 -c -t --skip-extended-insert imall > mysql-1107.sql
Mycat 导入:
mysql -uroot -p123456 -h127.0.0.1 -P8066 imall < mysql-1107.sql
如果是已有分片表,可以用 mycat 自带的工具,实际上是对 mysqldump 进行了包
装。
笔者也按照步骤做了一遍,但是报错了,也不知道是因为我是window版本的mycat,还是哪些配置没配对,报错了,报错内容如下:
虽然没操作成功,但是我步骤也贴上来:Mycat生产实践---数据迁移与扩容实践_蓑衣客的博客-CSDN博客
以取模分片表 sharding-by-mod 缩容为例。
注意:
只有节点变化的表才会进行迁移。仅分片配置变化不会迁移。
newSchema.xml:
改成(减少了一个节点):
newRule.xml 修改 count 个数:
2
3、修改 conf 目录下的 migrateTables.properties 配置文件,告诉工具哪些表需
要进行扩容或缩容,没有出现在此配置文件的 schema 表不会进行数据迁移,格式:
注意,
1)不迁移的表,不要修改 dn 个数,否则会报错。
2)ER 表,因为只有主表有分片规则,子表不会迁移
imall=sharding-by-mod
4、dataMigrate.sh 中这个必须要配置
通 过 命 令 "find / -name mysqldump" 查 找 mysqldump 路 径 为
"/usr/bin/mysqldump",指定#mysql bin 路径为"/usr/bin/"
#mysql bin 路径
RUN_CMD="$RUN_CMD -mysqlBin= /usr/bin/"
5、停止 mycat 服务
6、执行执行 bin/dataMigrate.sh 脚本
注意:必须要配置 Java 环境变量,不能用 openjdk
7、脚本执行完成,如果最后的数据迁移验证通过,就可以将之前的 newSchema.xml
和 newRule.xml 替换之前的 schema.xml 和 rule.xml 文件,并重启 mycat 即可。
注意事项:
- 保证分片表迁移数据前后路由规则一致(取模——取模)。
- 保证分片表迁移数据前后分片字段一致。
- 全局表将被忽略。
- 不要将非分片表配置到 migrateTables.properties 文件中。
- 暂时只支持分片表使用 MySQL 作为数据源的扩容缩容。migrate 限制比较多,还可以使用 mysqldump。
总结:离线或者在线,主要看数据量,和对于业务的影响程度决定。
上一节课我们已经用 mycat 实现了 MySQL 数据的分片存储,第一个可以实现负载
均衡,不同的读写发生在不同的节点上。第二可以实现横向扩展,如果数据持续增加,
加机器就 OK 了。
当然,一个分片只有一台机器还不够。为了防止节点宕机或者节点损坏,都要用副
本机制来实现。MySQL 数据库同样可以集群部署,有了多个节点之后,节点之间数据又
是个大问题。
这里我们来学习一下 MySQL 是怎么实现节点数据同步的。
MySQL 主从复制配置:
https://gper.club/articles/7e7e7f7ff3g5bgc3g6c
在 MySQL 多服务器的架构中,主节点,也就是产生数据的节点叫 master 节点。其
他的副本,向主节点同步数据的节点,叫做 slave(默认是异步的,客户端的数据在 master
刷盘就返回)。一个集群里面至少要有一个 master。slave 可以有多个。
主从复制的架构可以有多种。
一主一从/一主多从:
双主复制(互为主从):
级联复制:
不过在,MySQL 自身并没有自动选举和故障转移的功能,需要依赖其他的中间件或
者架构实现,比如 MMM,MHA,percona,mycat。
主从复制是怎么实现的呢?
客户端对 MySQL 数据库进行操作的时候,包括 DDL 和 DML 语句,服务端会在日志文件中用事件的形式记录所有的操作记录,这个文件就是 binlog 文件(属于逻辑日志,、跟 Redis 的 AOF 文件类似)。Binary log,二进制日志。
基于 binlog,我们可以实现主从复制和数据恢复。
binlog 默认是不开启的,需要在服务端手动配置。注意有一定的性能损耗。
编辑 /etc/my.cnf:
log-bin=/var/lib/mysql/mysql-bin
server-id=1
重启 MySQL 服务:
service mysqld stop
service mysqld start
## 如果出错查看日志
vi /var/log/mysqld.log
cd /var/lib/my
是否开启 binlog
show variables like 'log_bin%';
查看 binlog 格式:
show global variables like '%binlog_format%';
Binlog 文件超过一定大小就会产生一个新的,查看 binlog 列表:
show binary logs;
大小:
show variables like 'max_binlog_size';
查看 binlog 内容:
show binlog events in 'mysql-bin.000001';
用 mysqlbinlog 工具,基于时间查看 binlog
(注意这个是 Linux 命令, 不是 SQL)
/usr/bin/mysqlbinlog --start-datetime='2025-08-22 13:30:00' --stop-datetime='2025-08-22 14:01:01' -d gupao
/var/lib/mysql/mysql-bin.000001
1、主库开启 binlog,设置 server-id
这一步查看:3.4.1 binlog 配置
2、在主库创建具有复制权限的用户,允许从库连接
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'192.168.44.128' IDENTIFIED
BY '123456';
FLUSH PRIVILEGES;
3、从库/etc/my.cnf 配置,重启数据库
server-id=2
log-bin=mysql-bin
relay-log=mysql-relay-bin
read-only=1
log-slave-updates=1
开启 log-slave-updates 参数后,从库从主库复制的数据会写入 log-bin 日志文件里,这样可以实现互为主备或者级联复制(它自己也可以作为一个 master 节点)。
4、在从库执行
stop slave;
change master to
master_host='192.168.44.121',master_user='repl',master_password='123456',master_log_file='mysql-bin.00000
1', master_log_pos=4;
start slave;
5、查看同步状态
SHOW SLAVE STATUS;
Slave_IO_Running 和 Slave SQL Running 都为 yes 为正常。
为什么需要 relay log?为什么不把接收到的 binlog 数据直接写入从库?
——Relay log 相当于一个中转站,也记录了 master 和 slave 的同步信息。
添加
select user()
balance:负载的配置,决定 select 语句的负载
值 | 作用 |
0 | 不开启读写分离机制, 所有读操作都发送到当前可用的 writeHost 上。 |
1 | 所有读操作都随机发送到当前的 writeHost 对应的 readHost 和备用的 writeHost |
2 | 所有的读操作都随机发送到所有的 writeHost,readHost 上 |
3 | 所有的读操作都只发送到 writeHost 的 readHost 上 |
writeType:读写分离的配置,决定 update、delete、insert 语句的负载
值 | 作用 |
0 | 所有写操作都发送到可用的 writeHost 上(默认第一个, 第一个挂了以后发到第二个) |
1 | 所有写操作都随机的发送到 writeHost |
switchType:主从切换配置
值 | 作用 |
-1 | 表示不自动切换 |
1 | 默认值, 表示自动切换 |
2 | 基于 MySQL 主从同步的状态决定是否切换,心跳语句为 show slave status |
3 | 基于 MySQL galera cluster 的切换机制(适合集群) (1.4.1), 心跳语句为 show status like 'wsrep%'。 |
当关联的数据不在同一个节点的时候,Mycat 是无法实现跨库 join 的。
举例:
如果直接在 122-imall 插入主表数据,123-imall 插入明细表数据(本来应该在 122),此时关联查询无法查询出来。
模拟mycat ER分片不在同一节点不能链表查询的情况:
-- 122 节点插入
INSERT INTO `order_info` (`order_id`, `uid`, `nums`, `state`, `create_time`, `update_time`) VALUES (9,
1000003, 3, 1, '2019-9-25 11:35:49', '2019-9-25 11:35:49');
-- 123 节点插入
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (9,
20250001, 85114752, 19.99, 1, 1, 1);
在 mycat 数据库查询,直接查询没有结果:
select a.order_id,b.goods_id from order_info a, order_detail b where a.order_id = b.order_id;
Mycat 作为一个中间件,有很多自身不支持的 SQL 语句,比如存储过程,但是这些
语句在实际的数据库节点上是可以执行的。有没有办法让 Mycat 做一层透明的代理转发,
直接找到目标数据节点去执行这些 SQL 语句呢?
那我们必须要有一种方式告诉 Mycat 应该在哪个节点上执行。这个就是 Mycat 的注
解。我们在需要执行的 SQL 语句前面加上一段代码,帮助 Mycat 找到我们的目标节点。
注解的形式是 :
/*!mycat: sql=注解 SQL 语句*/
注解的使用方式是 :
/*!mycat: sql=注解 SQL 语句*/ 真正执行的 SQL
使用时将 = 号后的 "注解 SQL 语句" 替换为需要的 SQL 语句即可。
注解中的语句有一些限制,或者注意的地方:
原始 SQL | 注解 SQL |
select | 如果需要确定分片, 则使用能确定分片的注解, 比如/*!mycat: sql=select * from users where user_id=1*/ 如果要在所有分片上执行则可以不加能确定分片的条件 |
insert | 使用 insert 的表作为注解 SQL, 必须能确定到某个分片 原始 SQL 插入的字段必须包括分片字段 非分片表(只在某个节点上) : 必须能确定到某个分片 |
delete | 使用 delete 的表作为注解 SQL |
update | 使用 update 的表作为注解 SQL |
使用注解并不额外增加 MyCat 的执行时间;从解析复杂度以及性能考虑,注解SQL 应尽量简单,因为它只是用来做路由的。
注解可以帮我们解决什么问题呢?
注意(表里面必须有三个节点的数据):
https://gper.club/articles/7e7e7f7ff7g59gc1g68
注意, 跨库关联查询使用是有限制的。
/*!mycat:catlet=io.mycat.catlets.ShareJoin */
select a.order_id,b.goods_id from order_info a, order_detail b where a.order_id = b.order_id;
customer.id=1 全部路由到 122
-- 表
/*!mycat: sql=select * from customer where id =1 */ CREATE TABLE test2(id INT);
-- 存储过程
/*!mycat: sql=select * from customer where id =1 */ CREATE PROCEDURE test_proc() BEGIN END ;
Mycat 本身不支持 insert select,通过注解支持。
122-imall 创建测试表:
CREATE TABLE `test2` (
`id` int(11) NOT NULL,
PRIMARY KEY (`id`)
);
【先确认 order_detail 表有数据】
INSERT INTO `order_info` (`order_id`, `uid`, `nums`, `state`, `create_time`, `update_time`) VALUES (1,
1000001, 1, 2, '2025-9-23 14:35:37', '2025-9-23 14:35:37');
INSERT INTO `order_info` (`order_id`, `uid`, `nums`, `state`, `create_time`, `update_time`) VALUES (2,
1000002, 1, 2, '2025-9-24 14:35:37', '2025-9-24 14:35:37');
INSERT INTO `order_info` (`order_id`, `uid`, `nums`, `state`, `create_time`, `update_time`) VALUES (3,
1000003, 3, 1, '2025-9-25 11:35:49', '2025-9-25 11:35:49');
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (3,
20180001, 85114752, 19.99, 1, 1, 1);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (1,
20180002, 25411251, 1280.00, 1, 1, 0);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (1,
20180003, 62145412, 288.00, 1, 1, 2);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (2,
20180004, 21456985, 399.00, 1, 1, 2);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (2,
20180005, 21457452, 1680.00, 1, 1, 2);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (2,
20180006, 65214789, 9999.00, 1, 1, 3);
/*!mycat: sql=select * from customer where id =1 */ INSERT INTO test2(id) SELECT id FROM order_detail;
读写分离 : 配置 Mycat 读写分离后,默认查询都会从读节点获取数据,但是有些场
景需要获取实时数据,如果从读节点获取数据可能因延时而无法实现实时,Mycat 支持
通过注解 /*balance*/ 来强制从写节点(write host)查询数据。
/*balance*/ select a.* from customer a where a.id=6666;
/*!mycat: db_type=master */ select * from customer;
/*!mycat: db_type=slave */ select * from customer;
/*#mycat: db_type=master */ select * from customer;
/*#mycat: db_type=slave */ select * from customer;
注解支持的'! '不被 mysql 单库兼容
注解支持的'#'不被 MyBatis 兼容
随着 Mycat 的开发,更多的新功能正在加入。
Mycat 在执行 SQL 之前会先解析 SQL 语句,在获得分片信息后再到对应的物理节
点上执行。如果 SQL 语句无法解析,则不能被执行。如果语句中有注解,则会先解析注
解的内容获得分片信息,再把真正需要执行的 SQL 语句发送到对应的物理节点上。
所以我们在使用注解的时候,应该清楚地知道目标 SQL 应该在哪个节点上执行,注
解的 SQL 也指向这个分片,这样才能使用。如果注解没有使用正确的条件,会导致原始
SQL 被发送到所有的节点上执行,造成数据错误。
作为一个服务端的软件,或者说一个伪装成 MySQL 数据库的代理层,怎么实现
对分布式事务的支持呢?
我们看到 Mycat 的官方介绍,从 1.6.5 版本开始支持 XA 分布式事务。XA 是一
种两阶段提交的实现,我们先看看什么是两阶段提交。
所谓的二阶段提交分为两个阶段:
第一阶段分为两个步骤:
第二阶段也分为两个步骤:
事务管理器接受消息后,事务结束,应用程序继续执行。
为什么要分两步执行?
一是因为分两步,就有了事务管理器统一管理的机会;二尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作,这样,最后的提交阶段将是耗时极短,耗时极短意味着操作失败的可能性也就降低。
在 XA 的分布式事务处理模型里面涉及到三个角色 AP(应用程序)、RM(数据
库)、TM(事务管理器)。
XA 协议主要规定了了 TM 与 RM 之间的交互。
注意:通过实现 XA 的接口,只是提供了对 XA 分布式事务的支持,并不是说数
据库本身有分布式事务的能力。
XA 是一种两阶段提交的实现。数据库本身必须要提供被协调的接口,比如事务
开启,准备,事务结束,事务提交,事务回滚。
https://dev.mysql.com/doc/refman/5.7/en/xa.html
MySQL 单节点运行 XA 事务演示:
use gupao;
-- 开启 XA 事务
xa start 'xid';
-- 插入数据
insert into teacher(tid,tname,tcid) values(100,'qingshan',1);
-- 结束一个 XA 事务
xa end 'xid';
-- 准备提交
xa prepare 'xid';
-- 列出所有处于 PREPARE 阶段的 XA 事务
xa recover
-- 提交
xacommit'xid';
工程:ssm-mycat,代码:com.qingshan.xa.XaTest:
package com.qingshan.xa;
import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlXid;
import javax.sql.XAConnection;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.Statement;
public class XaTest {
public static MysqlXADataSource getDataSource(String connStr, String user, String pwd) {
try {
MysqlXADataSource ds = new MysqlXADataSource();
ds.setUrl(connStr);
ds.setUser(user);
ds.setPassword(pwd);
return ds;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] arg) {
String connStr1 = "jdbc:mysql://localhost:3306/imall_1";
String connStr2 = "jdbc:mysql://localhost:3306/imall_2";
try {
//从不同数据库获取数据库数据源
MysqlXADataSource ds1 = getDataSource(connStr1, "root", "123456");
MysqlXADataSource ds2 = getDataSource(connStr2, "root", "123456");
//数据库1获取连接
XAConnection xaConnection1 = ds1.getXAConnection();
XAResource xaResource1 = xaConnection1.getXAResource();
Connection connection1 = xaConnection1.getConnection();
Statement statement1 = connection1.createStatement();
//数据库2获取连接
XAConnection xaConnection2 = ds2.getXAConnection();
XAResource xaResource2 = xaConnection2.getXAResource();
Connection connection2 = xaConnection2.getConnection();
Statement statement2 = connection2.createStatement();
//创建事务分支的xid
Xid xid1 = new MysqlXid(new byte[] { 0x01 }, new byte[] { 0x02 }, 100);
Xid xid2 = new MysqlXid(new byte[] { 0x011 }, new byte[] { 0x012 }, 100);
try {
//事务分支1关联分支事务sql语句
xaResource1.start(xid1, XAResource.TMNOFLAGS);
int update1Result = statement1.executeUpdate("update order_info set nums=2674 where order_id=3");
xaResource1.end(xid1, XAResource.TMSUCCESS);
//事务分支2关联分支事务sql语句
xaResource2.start(xid2, XAResource.TMNOFLAGS);
int update2Result = statement2.executeUpdate("update order_info set nums=2674 where order_id=1");
xaResource2.end(xid2, XAResource.TMSUCCESS);
// 两阶段提交协议第一阶段
int ret1 = xaResource1.prepare(xid1);
int ret2 = xaResource2.prepare(xid2);
// 两阶段提交协议第二阶段
if (XAResource.XA_OK == ret1 && XAResource.XA_OK == ret2) {
xaResource1.commit(xid1, false);
xaResource2.commit(xid2, false);
System.out.println("reslut1:" + update1Result + ", result2:" + update2Result);
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
记住流程,帮助我们理解xa二段提交,实际开发不会用这个。
package com.qingshan.xa;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* @author Qian
* @Since JDK1.8
* @Date 2021/10/27 22:31
*/
public class MyCATXAClientDemo {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 1. 获得数据库连接
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:8066/imall", "root", "root");
conn.setAutoCommit(false);
// 2. 开启 MyCAT XA 事务
conn.prepareStatement("set xa=on").execute();
// 3. 插入 SQL
// 3.1 SQL1 A库
String sql1 = String.format("update order_info set nums=%d where order_id=%d", 2675, 3);
conn.prepareStatement(sql1).execute();
// 3.2 SQL2 B库
String sql2 = String.format("update order_info set nums=%d where order_id=%d", 2675, 1);
conn.prepareStatement(sql2).execute();
// 4. 提交 XA 事务
conn.commit();
}
}
set xa=on
MyCAT 开启 XA 事务。conn.commit
提交 XA 事务。MyCAT XA分布式事务_weixin_34253539的博客-CSDN博客
下面是springboot支持分布式事务的链接,不涉及mycat,也不是xa:
SpringBoot+Mybatis+atomikos实现动态切换数据源+分布式事务_有趣的灵魂_不世俗的心的博客-CSDN博客
分布式事务实战--一个完整的xa例子 - dongfuye - 博客园
Mycat 在 1.6.5 版本以后已经完全支持 XA 分布式强事务类型了。
对前端是资源管理器,对后端是事务管理器。
Mycat 中 XA 的用法:
用户应用侧(AP)的使用流程如下:
第三步如下:
INSERT INTO`customer`(`id`,`name`)VALUES(6667,'赵先生');
INSERT INTO`customer`(`id`,`name`)VALUES(7778,'钱先生');
INSERT INTO`customer`(`id`,`name`)VALUES(16667,'孙先生');
流程图如下:
在这里 mycat 起到了事务协调者的角色。
不过 XA 两阶段提交存在一些问题:
其他的分布式实现方案,参考微服务架构-Spring Cloud Alibaba-分布式事务,后续再研究。
最后我们来看一下 Mycat 的原理。它是怎么帮我们实现分库分表和解决相关的问题的。
官网的架构图:
git clone https://github.com/MyCATApache/Mycat-Server
schema.xml 放在:src\main\resources
select user()
本地数据库创建 db1、db2、db3 数据库:
create database db1;
create database db2;
create database db3;
全部执行建表脚本:
CREATE TABLE `company` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT '',
`market_value` bigint(20) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `hotnews` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(64) DEFAULT '',
`content` varchar(512) DEFAULT '0',
`time` varchar(8) DEFAULT '',
`cat_name` varchar(10) DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `travelrecord` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`city` varchar(32) DEFAULT '',
`time` varchar(8) DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
travelrecord 表,根据 id 范围分片:
travelrecord 表分片规则:
id
rang-long
travelrecord 表分片算法:
autopartition-long.txt
autopartition-long.txt:
1-10000=0
10000-20000=1
20000-30000=2
hotnews 表,根据 id 取模分片,模以 3:
id
mod-long
3
company 表,全局表,没有分片规则:
debug 方式启动 main 方法
\src\main\java\io\mycat\MycatStartup.java
测试语句:
-- 范围分片
insert into travelrecord(`id`, `city`, `time`) values(1, '长沙', '20251020');
-- 取模分片
insert into hotnews(`id`, `title`, `content`) values(1,'咕泡', '盆鱼宴');
-- 全局表
insert into company(`name`, `market_value`) values('spring', 100);
连接入口:
io.mycat.net.NIOAcceptor#accept
SQL 入口:
io.mycat.server.ServerQueryHandler#query
调试 DDL:
truncate table travelrecord;
普通 Select,这三条语句的路由都不一样:
select * from hotnews where id =1;
select * from hotnews where id =2;
select * from hotnews where id =3;
insert:
insert into travelrecord(`id`, `city`, `time`) values(10000, '长沙', '20251020');
insert into travelrecord(`id`, `city`, `time`) values(20000, '长沙', '20251020');
insert into travelrecord(`id`, `city`, `time`) values(30000, '长沙', '20251020');
Step Out(Shift+F8)可以看到上一层的调用。
目前 Mycat 没有实现对多 Mycat 集群的支持。集群之前最麻烦的是数据同步和选举。
可以暂时使用 HAProxy+Keepalived 实现。
参考:https://gper.club/articles/7e7e7f7ff3g5bgc5g6c
HAProxy+Keepalived 搭建 RabbitMQ 高可用集群。