文章目录
- 一、分布式全局id
- 1、分库分表引发的id问题
- 2、解决id问题
- (1)UUID
- (2)统一ID序列生成(使用mycat生成id)
- (3)雪花算法
- 二、分布式事物
- 1、基本理论
- (1)CAP原理
- (2)ACID原理
- (3)BASE原理
- 2、分布式事物的解决方案
- (1)XA协议的两段提交
- 1)Atomikos实现两阶段事物
- 2)MyCat分布式事物
- 3)Sharding-JDBC分布式事物
- (2)事务补偿机制(不推荐使用,对程序员压力很大!!!)
- 1)配置数据源
- 2)编制转账服务
- (3)本地消息表的最终一致性方案
- 1)配置数据源
- 2)编写支付接口
- 3)订单操作接口
- 4)编写定时任务,将支付接口和订单接口起来
- 5)进行测试
- (4)MQ的最终一致性方案
- 1)安装rocketmq,其他mq也一样
- 2)配置生产者和消费者
- 3)编写代码
- 4)进行测试
如果不会mycat和sharding-jdbc一定要看,不然文章看不懂
mycat和sharding-jdbc详解:https://blog.csdn.net/qq_34886352/article/details/104458171
一、分布式全局id
1、分库分表引发的id问题
在正常的单库系统下,为了效率id通常采用自增的方式,但是分库分表的情况,依旧采用这种方法,那么每张表每个库的id都是从0开始的,id就是去了唯一标识的作用,不同的表中会存在相同的id,导致业务数据严重混乱
2、解决id问题
(1)UUID
UUID是通用的唯一识别码(Universally Unique Identifier),使用UUID为每一条数据生成id,可以保证所有的id都是不同的,但是UUID有较为明显的缺点,只是一个单纯的32位无规则字符串,没有实际意义,长度过长(数据匹配的时候就会慢),生成较慢。
mycat不支持UUID,sharding-jdbc支持UUID
表结构,t_order表:
字段 | 中文解释 |
---|---|
order_id | 订单id |
total_amount | 价格 |
order_status | 订单状态 |
user_id | 用户id |
1)sharding-jdbc使用UUID自动添加id
如果主键不是分库分表的关键字段,直接在springboot的配置文件中添加两个属性就可以完成配置
,在插入数据的时候就不要在填写主键字段的值了
#指定主建字段,其中t_order是逻辑表名,order_id是字段名称
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
#指定主键生成规则
spring.shardingsphere.sharding.tables.t_order.key-generator.type=UUID
如果主键是分库分表的关键字段,例如:order表使用order_id作为分表关键字段,但是order_id同时也是主键需要用UUID,这时候就不能用简单的分表分表规则,来录入数据。
同样的还是需要先指定主键字段,和主键生成规则
#指定主建字段,其中t_order是逻辑表名,order_id是字段名称
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
#指定主键生成规则
spring.shardingsphere.sharding.tables.t_order.key-generator.type=UUID
然后指定分表字段,和分表规则
#指定分表字段
spring.shardingsphere.sharding.tables.t_order.table-standard.sharding-column=order_id
#指定分表规则的具体类
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.example.shardingdemo.MySharding
创建一个类MySharding
public class MySharding implements PreciseShardingAlgorithm<String>{/*** availableTargetNames:可用的分片表(这条数据对应的多有的分片表)* ShardingValue:当前的分片值(也就是分表分库的关键字段的值)*/@Overridepublic String doSharding(Collection<String> availableTargetNames,PreciseShardingValue<String> shardingValue){String id = shardingValue.getValue();int mode = id.hashCode()%availableTargetNames.size();Stirng[] strings=availableTargetNames.toArray(new String[0]);return string[mode];}
}
这样就完成了,可用正常的使用了
附:springxml的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://shardingsphere.apache.org/schema/shardingsphere/shardinghttp://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsd"><!-- 添加数据源 --><bean id="ds0" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"><!-- 数据库驱动 --><property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /><property name="username" value="用户名"/><property name="password" value="密码"/><property name="jdbcUrl" value="jdbc:mysql://192.168.85.200:3306/sharding_order?serverTimezone=Asia/Shanghai&useSSL=false"/></bean><!-- 第二个数据源 --><bean id="ds1" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"><!-- 数据库驱动 --><property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /><property name="username" value="用户名"/><property name="password" value="密码"/><property name="jdbcUrl" value="jdbc:mysql://192.168.85.201:3306/sharding_order?serverTimezone=Asia/Shanghai&useSSL=false"/></bean><!-- 配置sharding-jdbc --><sharding:data-source id="sharding-data-source"><!-- 配置数据源 --><sharding:sharding-rule data-source-name="ds0,ds1"><sharding:table-rules><!-- logic-table :分片表的逻辑表名 --><!-- atcual-data-nodes :实际的数据节点 ds$->{0..1}:分为两个部分ds是数据源的前缀,$->{0..1}是占位符,等同于${} --><!-- database-strategy-ref :库的分片策略 --><!-- table-strategy-ref :表的分片策略 --><!-- 重点:key-generator-ref:主键规则--><sharding:table-rule logic-table="t_order" atcual-data-nodes="ds$->{0..1}.t_order_$->{1..2}"database-strategy-ref="databaseStrategy"table-strategy-ref="standard" key-generator-ref="uuid"/></sharding:table-rules></sharding:sharding-rule></sharding:data-source><!-- 重点:指定主键和生成规则 --><sharding:key-generator id="uuid" column="order_id" type="UUID"><!-- 数据库的分片规则 --><!-- sharding-column:分库使用的字段 --><!-- algorithm-expression:分片规则,对user_id取模 --><sharding:inline-strategy id="databaseStrategy" sharding-column="user_id" algorithm-expression="ds$->{user_id%2}"/><bean id="myShard" class="com.example.shardingdemo.MySharding"/><!-- 表的分片规则 --><sharding:standard-strategy id="standard" sharding-column="order_id" precise-algorithm-ref="myShard"/><bean class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="sharding-data-source"/><property name="mapperLocations" value="classpath*:/mybatis/*.xml"/></bean>
</beans>
(2)统一ID序列生成(使用mycat生成id)
再次提醒如果不会mycat和sharding-jdbc一定要看,不然文章看不懂
https://blog.csdn.net/qq_34886352/article/details/104458171
ID的值统一的从一个集中的ID序列生成器中获取,例如:每一个mysql都连接mycat,mycat生成所有的id分发给mysql。
MyCat有两种方式生成id:
- 本地文件方式:将id序列通过零时文件的方式存在本地的文件中,每次使用都从文件中读取,
但是mycat重启之后id就会重新计算
,一般用于测试环境 - 数据库方式:将id存放在数据库中,mycat 重启后不会重新计算,一般用于生存环境
统一id生成器有个非常明显的缺点,当并发量大的时候,id生成器压力就会非常巨大,如果生成器宕机,整个数据库就可能无法使用
表结构,o_order表:
字段 | 中文解释 |
---|---|
id | id |
total_amount | 价格 |
order_status | 订单状态 |
1)修改mycat的server.xml
#修改checkSQLschema的值为false,这里是个bug,如果不修改为false,在执行分表id插入的时候就会报sql语句语法错误,mycat应该在后期会修复
checkSQLschema="false"#找到sequnceHandlerType,整个是控制id生成方式的
#0:本地文件方式
#1:数据库方式
#2:本地时间戳的方式(雪花算法)
<property name="sequnceHandlerType">0</property>
2)修改mycat的schema.xml(可选)
指定表的主键,并且是自增的
#autoIncrement:属性是否自增
#primaryKey:指定主键的字段
<table name="o_order" autoIncrement="true" primaryKey="id" dataNode="dn200,nd201" rule="auto-sharding-long">
3)修改mycat的sequence_conf.properties
- sequence_conf.properties:本地文件方式的配置文件
- sequence_db_conf.properties:数据库方式的配置文件
- sequence_distributed_conf.properties:分布式方式的配置文件,基于zookeeper
- sequence_time_conf.properties:本地时间戳的方式的配置文件
#default global sequence
#GLOBAL是全局的配置#历史的id
GLOBAL.HISIDS=
#最小的id数
GLOBAL.MINID=10001
#最大的id数
GLOBAL.MAXID=20000
#当前的id
GLOBAL.CURID=10000#O_ORDER是表的名称,这里一定要和数据的表对应上,多个表就要写多组
O_ORDER.HISIDS=
O_ORDER.MINID=10001
O_ORDER.MAXID=20000
O_ORDER.CURID=10000
4)启动mycat,并插入数据
insert into o_order(id,total_amount,order_status)values(--这句话就是从mycat里面取id,mycatseq_是固定的 后面跟上在sequence_conf.properties中配置的前缀next value FOR mycatseq_O_ORDER,88,3
)
如果配置了步骤2,可以不需要写id
insert into o_order(total_amount,order_status)values(88,3);
如果是文件方式的配置到这里就结束了,下面就是数据库方式的配置,这里接上第2部,3不用做了,别忘记第二步sequnceHandlerType要设置为1
5)在mycat的conf文件夹下找到dbseq.sql
dbseq.sql里面有数据库方式需要的建表语句,在任意的一个库中执行语句,新建出表MYCAT_SEQUENCE,同时还会创建出4个函数:mycat_seq_currval、mycat_seq_nextval、mycat_seq_nextvals、mycat_seq_setval
在表中添加,一条数据,之前的不要删
--配置的表前缀,当前id值,间隔数
insert into MYCAT_SEQUENCE(name,current_value,increment)values(O_ORDER,1,1);
6)修改mycat的sequence_db_conf.properties
#dn200是配置在schema.xml中数据节点(dataNode),就是MYCAT_SEQUENCE所在的数据库,
GLOBAL=dn200
O_ORDER=dn200
配置好后重启mycat,这样就配置好了,使用参看步骤4
(3)雪花算法
mycat的配置参考使用mycat生成id,需要修改的配置都在下面,重复的步骤就不写了
同样的sharding-jdbc参考上面的uuid的配置
雪花算法(SnowFlake)是由Twitter剔除的分布式id算法,一个64bit(64bit是指2进制)的long型数字,引入了时间戳,保证了id的递增性质
以下为每位的解释
- 第一位为0,表示是正数,固定为0
- 接着是41位的时间戳,记录的是一段时间,雪花算法要求填写一个开始时间,用开始时间减去当前时间,得到时间戳
- 跟着5位机房id
- 跟着5位机器id,可以和之前的5位组合在一起使用(做多可以配置1024个机器编码,让不同机器使用同一个机器码,就不保证绝对不重复了)
- 最后12位不规则序列,保证在同一时间点上,有2^12次方个并发量
在使用雪花算法的时候需要注意时间回调的问题,之前说过雪花算法需要设定一个开始时间,如果系统已经运行一段实际生产了id后,调整了设定的时间,就有可能出现重复的数据,
在使用雪花算法之前,要注意数据库表的主键是否是bigint类型的,长度是否有19位
1)mycat设置雪花算法
- 修改server.xm
#设置为2
<property name="sequnceHandlerType">0</property>
- 修改schema.xml
修改分片表的分片规则
<!-- mod-long:取模的方式分表 -->
<table name="o_order" autoIncrement="true" primaryKey="id" dataNode="dn200,nd201" rule="mod-long>
- 修改sequence_time_conf.properties
#这两个值都要小于32
#机房id
WORKIN=01
#机器id
DATAACENTERID=01
其他的按照使用mycat生成id的方法做就行了
刷新配置,或者重启mycat
2)sharding-jdbc设置雪花算法
1.修改springboot的配置文件
#指定主建字段,其中t_order是逻辑表名,order_id是字段名称
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
#指定主键生成规则
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
#有可能会报红 没有关系,可以使用
#worker.id:机房id+机器id一共10位数的2进制,转换成10进制最大为1024,这个值不能超过1024
spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=234
#最大回调时间,单位毫秒
spring.shardingsphere.sharding.tables.t_order.key-generator.props.max.tolerate.time.difference.milliseconds=10
2.修改MySharding类
public class MySharding implements PreciseShardingAlgorithm<Long>{/*** availableTargetNames:可用的分片表(这条数据对应的多有的分片表)* ShardingValue:当前的分片值(也就是分表分库的关键字段的值)*/@Overridepublic String doSharding(Collection<String> availableTargetNames,PreciseShardingValue<Long> shardingValue){Long id = shardingValue.getValue();Long mode = id%availableTargetNames.size();Stirng[] strings=availableTargetNames.toArray(new String[0]);return string[(int)mode];}
}
设置完成,正常插入数据就可以了
附:springxml的配置方法
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://shardingsphere.apache.org/schema/shardingsphere/shardinghttp://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsd"><!-- 添加数据源 --><bean id="ds0" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"><!-- 数据库驱动 --><property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /><property name="username" value="用户名"/><property name="password" value="密码"/><property name="jdbcUrl" value="jdbc:mysql://192.168.85.200:3306/sharding_order?serverTimezone=Asia/Shanghai&useSSL=false"/></bean><!-- 第二个数据源 --><bean id="ds1" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"><!-- 数据库驱动 --><property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /><property name="username" value="用户名"/><property name="password" value="密码"/><property name="jdbcUrl" value="jdbc:mysql://192.168.85.201:3306/sharding_order?serverTimezone=Asia/Shanghai&useSSL=false"/></bean><!-- 配置sharding-jdbc --><sharding:data-source id="sharding-data-source"><!-- 配置数据源 --><sharding:sharding-rule data-source-name="ds0,ds1"><sharding:table-rules><!-- logic-table :分片表的逻辑表名 --><!-- atcual-data-nodes :实际的数据节点 ds$->{0..1}:分为两个部分ds是数据源的前缀,$->{0..1}是占位符,等同于${} --><!-- database-strategy-ref :库的分片策略 --><!-- table-strategy-ref :表的分片策略 --><!-- 重点:key-generator-ref:主键规则--><sharding:table-rule logic-table="t_order" atcual-data-nodes="ds$->{0..1}.t_order_$->{1..2}"database-strategy-ref="databaseStrategy"table-strategy-ref="standard" key-generator-ref="snow"/></sharding:table-rules></sharding:sharding-rule></sharding:data-source><!-- 重点:指定主键和生成规则 --><sharding:key-generator id="snow" column="order_id" type="SNOWFLAKE" props-ref="snowprop"><!-- 重点:配置雪花算法的参数 --><bean:properties id="snowprop"><!-- worker.id:机房id+机器id一共10位数的2进制,转换成10进制最大为1024,这个值不能超过1024 --><prop key="worker.id">678</prop><!-- 可以容忍的最大回调时间,单位毫秒 --><prop key="max.tolerate.time.difference.milliseconds">10</prop></bean><!-- 数据库的分片规则 --><!-- sharding-column:分库使用的字段 --><!-- algorithm-expression:分片规则,对user_id取模 --><sharding:inline-strategy id="databaseStrategy" sharding-column="user_id" algorithm-expression="ds$->{user_id%2}"/><bean id="myShard" class="com.example.shardingdemo.MySharding"/><!-- 表的分片规则 --><sharding:standard-strategy id="standard" sharding-column="order_id" precise-algorithm-ref="myShard"/><bean class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="sharding-data-source"/><property name="mapperLocations" value="classpath*:/mybatis/*.xml"/></bean>
</beans>
二、分布式事物
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
1、基本理论
(1)CAP原理
一致性(Consistency)
访问所有的节点得到的数据应该是一样的。注意,这里的一致性指的是强一致性,也就是数据更新完,访问任何节点看到的数据完全一致,要和弱一致性,最终一致性区分开来。
可用性(Availability)
所有的节点都保持高可用性。注意,这里的高可用还包括不能出现延迟,比如如果节点B由于等待数据同步而阻塞请求,那么节点B就不满足高可用性。也就是说,任何没有发生故障的服务必须在有限的时间内返回合理的结果集。
分区容忍性(Partiton tolerence)
这里的分区是指网络意义上的分区,由于网络是不可靠的,所有节点之间很可能出现无法通讯的情况,在节点不能通信时,要保证系统可以继续正常服务。
CAP原理说,一个数据分布式系统不可能同时满足C和A和P这3个条件。所以系统架构师在设计系统时,不要将精力浪费在如何设计能满足三者的完美分布式系统,而是应该进行取舍。由于网络的不可靠性质,大多数开源的分布式系统都会实现P,也就是分区容忍性,之后在C和A中做抉择。
(2)ACID原理
原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误
(3)BASE原理
基本可用(Basically Available)
基本可用指分布式系统在出现故障时,系统允许损失部分可用性,即保证核心功能或者当前最重要功能可用。对于用户来说,他们当前最关注的功能或者最常用的功能的可用性将会获得保证,但是其他功能会被削弱。
软状态(Soft-state)
软状态允许系统数据存在中间状态,但不会影响系统的整体可用性,即允许不同节点的副本之间存在暂时的不一致情况。
最终一致性(Eventually Consistent)
最终一致性要求系统中数据副本最终能够一致,而不需要实时保证数据副本一致。例如,银行系统中的非实时转账操作,允许 24 小时内用户账户的状态在转账前后是不一致的,但 24 小时后账户数据必须正确。
最终一致性是 BASE 原理的核心,也是 NoSQL 数据库的主要特点,通过弱化一致性,提高系统的可伸缩性、可靠性和可用性。而且对于大多数 Web 应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是多数分布式数据库产品的方向。
2、分布式事物的解决方案
(1)XA协议的两段提交
XA是由X/Open组织提出的分布式事物的规范,由一个事物管理器(TM)和多个资源管理器(RM)组成,提交分为两个阶段:prepare和commit
1)prepare第一阶段,准备阶段
在第一阶段中,事物管理器(TM)会告诉资源管理器(RM)需要做什么,当资源管理器(RM)完成之后,告诉事务管理器已经完成,此时每个资源管理器(RM)的事务并没有提交。
2)commit第二阶段,提交阶段
所有的资源管理器(RM)都告诉事物管理器(TM)任务已经完成并成功之后,事物管理器(TM)再次发出通知,让所有的资源管理器(RM)提交事物,完成一次分布式事物管理。如果有一个资源管理器(RM)告诉事物管理器(TM)执行失败,事物管理器(TM)就会通知所有的资源管理器(RM)回滚。
如果commit阶段出现问题,事务出现不一致,需要人工处理
两阶段提交的方式效率低下,性能与本地事务相差10倍多
MySql5.7及以上均支持XA协议,Mysql Connector/J 5.0以上支持XA协议,java系统中数据源一般采用Atomikos
1)Atomikos实现两阶段事物
Springboot官方文档中对Atomikos集成:https://docs.spring.io/spring-boot/docs/2.2.4.RELEASE/reference/html/spring-boot-features.html#boot-features-jta
数据库说明:
两个数据库
数据库地址:192.168.85.200:
库名:xa_200
表名:xa_200
字段:id,name
数据库地址:192.168.85.201
库名:xa_201
表名:xa_201
字段:id,name
- 添加maven
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
别忘了,mysql、mybatis的jar包也要引入
- 使用javabean的方式创数据源
创建一个ConfigDB200类,我这里放在com.example.xa.config包下
@Configuration
//dao(mapper类的位置)
@MapperScan(value="com.example.xa.dao200",sqlSessionFactoryRef="sqlSessionFactoryBean200")
public class ConfigDB200{/** 配置数据源*/@Bean("db200")public DataSource db200(){//这里使用的是mysql的XA数据源,因为Atomikos是XA协议的MysqlXADataSource xaDataSource = new MysqlXADataSource();xaDataSource.setUser("用户名");xaDataSource.setPassword("密码");xaDataSource.setUrl("jdbc:mysql://192.168.85.200:3306/xa_200");//使用Atomikos统一的管理数据源AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();atomikosDataSourceBean.setXaDataSource(xaDataSource);return atomikosDataSourceBean;}/** 配置SqlSessionFactoryBean*/@Bean("sqlSessionFactoryBean200")public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db200")DataSource dataSource)throws IOException{SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource);//配置mybatis的xml位置ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db200/*.xml"))return sqlSessionFactoryBean;}/** 配置事物管理器,这个只需要配置一个就可以了*/@Bean("xaTransaction")public JtaTransactionManager jtaTransactionManager(){UserTransaction userTransaction = new UserTransactionImp();UserTranSactionManager userTransactionManager = new UserTransactionManager();return new JtaTransactionManager(userTransaction,userTransactionManager);}
}
再创建另外一个数据库的配置ConfigDB201类
@Configuration
//dao(mapper类的位置)
@MapperScan(value="com.example.xa.dao201",sqlSessionFactoryRef="sqlSessionFactoryBean201")
public class ConfigDB201{/** 配置数据源*/@Bean("db201")public DataSource db201(){//这里使用的是mysql的XA数据源,因为Atomikos是XA协议的MysqlXADataSource xaDataSource = new MysqlXADataSource();xaDataSource.setUser("用户名");xaDataSource.setPassword("密码");xaDataSource.setUrl("jdbc:mysql://192.168.85.201:3306/xa_201");//使用Atomikos统一的管理数据源AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();atomikosDataSourceBean.setXaDataSource(xaDataSource);return atomikosDataSourceBean;}/** 配置SqlSessionFactoryBean*/@Bean("sqlSessionFactoryBean201")public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db201")DataSource dataSource)throws IOException{SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource);//配置mybatis的xml位置ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db201/*.xml"))return sqlSessionFactoryBean;}
}
测试配置是否成功,mybatis的内容就不写了,不然太多了 看上去反而容易乱,如果mybatis都不会!速度补习
@Service
public class XAService{//重点就在这里 设置事物管理器为xa的事务管理器@Transactional(transactionManager = "xaTransaction")public void testXA(){XA200 xa200 = new XA200();xa200.setId(1);xa200.setName("xa_200");xa200Mapper.insert(xa200);XA201 xa201 = new XA201();xa201.setId(1);xa201.setName("xa_201");xa201Mapper.insert(xa201);}
}
2)MyCat分布式事物
修改mycat的server.xml配置文件
vim server.xml
找到handleDistributedTransactions的配置项
<!--- 分布式事务开关,0为不过滤分布式事物 1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤) 2为不过滤分布式事物,但是记录分布式事物日志 -->
<property name="handleDistributedTransactions">0</property>
测试类
@Transactional(rollbackFor = Exception.class)
public void testUser(){//插入服务器200的数据库User user1= new User();user1.setId();user1.setUsername("奇数");userMapper.insert(user1);//插入服务器201的数据库//username字段长度只有2,所以这条数据会插入失败,最后结果是两个数据库都会回滚User user2= new User();user2.setId();user2.setUsername("奇数11111");userMapper.insert(user2);
}
3)Sharding-JDBC分布式事物
Sharding-JDBC如果正常的配置,默认就支持分布式事物,直接使用就可以使用,和正常的单机事务使用方法一致加上@Transactional(rollbackFor = Exception.class)就能开启事务
(2)事务补偿机制(不推荐使用,对程序员压力很大!!!)
事务补偿即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须事务链中的每一个业务服务或操作都有对应的可逆服务。当事务中一个阶段发生异常,事务中的其他阶段就需要调用对应的逆向操作进行补偿。
- 优点:逻辑清晰、流程简单
- 缺点:数据一致性比XA还要差,可能出错的点比较多,TCC属于应用层的一种补偿方法,程序员需要写大量代码
数据库说明:
两个数据库
数据库地址:192.168.85.200:
库名:xa_200
表名:accout_a
字段:id,name,balance(余额)
数据库地址:192.168.85.201
库名:xa_201
表名:accout_b
字段:id,name,balance(余额)
1)配置数据源
数据源200
@Configuration
//dao(mapper类的位置)
@MapperScan(value="com.example.xa.dao200",sqlSessionFactoryRef="sqlSessionFactoryBean200")
public class ConfigDB200{/** 配置数据源*/@Bean("db200")public DataSource db200(){MysqlDataSource dataSource = new MysqlDataSource();dataSource.setUser("用户名");dataSource.setPassword("密码");dataSource.setUrl("jdbc:mysql://192.168.85.200:3306/xa_200");return dataSource;}/** 配置SqlSessionFactoryBean*/@Bean("sqlSessionFactoryBean200")public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db200")DataSource dataSource) throws IOException{SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource);//配置mybatis的xml位置ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db200/*.xml"))return sqlSessionFactoryBean;}/** 配置事务管理器*/@Bean("tm200")public PlatformTransactionManager transactionManager(@Qualifier("db200")DataSource dataSource){return new DataSourceTransactionManager(dataSource);}
}
数据源201
@Configuration
//dao(mapper类的位置)
@MapperScan(value="com.example.xa.dao201",sqlSessionFactoryRef="sqlSessionFactoryBean201")
public class ConfigDB201{/** 配置数据源*/@Bean("db201")public DataSource db201(){MysqlDataSource dataSource = new MysqlDataSource();dataSource.setUser("用户名");dataSource.setPassword("密码");dataSource.setUrl("jdbc:mysql://192.168.85.200:3306/xa_201");return dataSource;}/** 配置SqlSessionFactoryBean*/@Bean("sqlSessionFactoryBean201")public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db201")DataSource dataSource) throws IOException{SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource);//配置mybatis的xml位置ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db201/*.xml"))return sqlSessionFactoryBean;}/** 配置事务管理器*/@Bean("tm201")public PlatformTransactionManager transactionManager(@Qualifier("db201")DataSource dataSource){return new DataSourceTransactionManager(dataSource);}
}
2)编制转账服务
将数据库200的用户的钱,转给数据库201的用户账户上
@Servicepublic class AccountService{@Resourceprivate AccountAmapper accountAmapper;@Resourceprivate AccountBmapper accountBmapper;@Transactional(transactionManager="tm200" ,rollbackFor = Exception.class)public void transferAccount(){//查询数据库200中 id为1的数据AccountA accountA=accountAMapper.selectByPrimaryKey(1);//将用户accountA的余额扣除200accountA.setBalance(accountA.getBalance.subtract(new BigDecimal(200)));accountAmapper.updateById(accountA);//查询数据库201中 id为2的数据AccountB accountB=AccountBmapper.selectByPrimaryKey(2);//将用户accountB的余额加上200accountB.setBalance(accountB.getBalance.add(new BigDecimal(200)));accountBmapper.updateById(accountB);try{//模拟抛出异常,不能在更新accountB之前就检测异常,不然余额还没有增加200元,这里又扣了200元int i = 1/0}catch (Exception e){//这里就是补偿机制,如果这里还是出现了异常,补偿机制就会失败!!!,需要人工接入//报错的话,需要将accountB减去200元//查询数据库201中 id为2的数据AccountB accountB=AccountBmapper.selectByPrimaryKey(2);//将用户accountB的余额加上200accountB.setBalance(accountB.getBalance.subtract(new BigDecimal(200)));accountBmapper.updateById(accountB);//异常还需要重新抛出,不然程序就正常结束了throw e;}}}
(3)本地消息表的最终一致性方案
采用BASE原理,保证事物最终一致,在一致性方面,允许一段时间内的不一致,但最终会一致。基于本地消息表中的方案,将本事务外操作,记录在消息表中,其他的事务提供接口,定时任务轮询本地消息表,将未执行的消息发送给操作接口。例如:银行从A账户转账给B账户200元,我们先将A账户减去200元,然后向本地事务表中添加一条记录,记录需要把B账户增加200元,定时任务会轮询的扫描本地事务表,发现新增了一条记录,根据要求调用接口将B账户增加200元,如果调用接口失败,不会取消掉本地消息表的事务,下次轮询的时候再次执行。
- 优点:将分布式事务,拆分成多个单独的事务,实现最终一致性
- 缺点:要注意重试时的幂等性操作
数据库说明:
两个数据库
数据库地址:192.168.85.200:
库名:xa_200
表名:accout_a(用户账户表)
字段:id,name,balance(余额)
表名:payment_msg(本地消息表)
字段:id,order_id,status(0:未发送,1:发送成功,2:超过最大失败次数),falure_cnt(失败次数)
数据库地址:192.168.85.201
库名:xa_201
表名:t_order(订单表)
字段:id,order_status,order_amount
这里模拟 用户已经下单了,但是还没有付款的步骤,从用户库中扣除商品金额,然后再去订单库中,修改订单状态
所以我们在订单表中预先就插入一条数据
insert into t_order values (1, 0,200);
还要预先插入一条用户的数据
insert into accout_a values (1, "用户A",1000);
1)配置数据源
参考上面的,完全一样
2)编写支付接口
@Service
public class PaymentService{@Resourceprivate AccountAMapper accountAMapper;@Resourceprivate PaymentMsgMapper paymentMsgMapper;/** userId:用户id* orderId:订单id* amount:订单金额*/@Transactional(transactionManager = "tm200")public int pament(int userId,int orderId, BigDecimal amount){//支付操作AccountA accountA=accountAMapper.selectByPrimaryKey(userId);//用户不存在if(accountA == null)return 1;//余额不足if(accountA.getBalance().compareTo(amount)<0)return 2;accountA.setBalance(accountA.getBalance().subtract(amount));accountAMapper.updateByPrimaryKey(accountA);//记录本地消息表PaymentMsg paymentMsg = new PaymentMsg();paymentMsg.setOrderId(orderId);paymentMsg.setStatus(0);//未发送paymentMsg.setFalureCnt(0);//失败次数paymentMsgMapper.insertSelective(paymentMsg);//成功return 0;}
}
Controller层提供接口
@RestController
public class PaymentController{@Autowireprivate PaymentService paymentService;/** userId:用户id* orderId:订单id* amount:订单金额*/@RequestMapping("/pament")public String pament(int userId,int orderId, BigDecimal amount){int result = paymentService.pament(userId,orderId,amount);return "支付结果:"+result;}
}
3)订单操作接口
@Service
public class OrderService{@Resourceprivate OrderMapper orderMapper;/** orderId:订单id*/@Transactional(transactionManager = "tm201")public int handleOrder(int orderId){//查询出订单,之前预先插入的那条信息Order order=OrderMapper.selectByPrimaryKey(orderId);//订单不存在if(order == null)return 1;//修改订单支付状态order.setOrderStatus(1); //已支付orderMapper.updateByPrimaryKey(order);//成功return 0;}
}
Controller层提供接口
@RestController
public class OrderController{@Autowireprivate OrderService orderService;/** orderId:订单id*/@RequestMapping("/handleOrder")public String handleOrder(int orderId){try{int result = orderService.handleOrder(orderId);if(result == 0){return "success";}else{return "fail";}}catch(Exception e){//发生异常return "fail";}}
}
4)编写定时任务,将支付接口和订单接口起来
@Service
public class OrderScheduler(){//十秒执行一次@Scheduled(cron="0/10 * * * * ?")private void orderNotify()throws IOException{//查询所有未发送的本地消息表,可以用其他方式实现,效果一样就行了,查询消息表里面所有未发送的消息PaymentMsgExample paymentMsgExample = new PaymentMsgExample();paymentMsgExample.createCriteria().andStatusEqualTo(0);//未发送List<PaymentMsg> paymentMsgs = paymentMsgMapper.selectByExample(paymentMsgExample);for(PaymentMsg paymentMsg : paymentMsgs){int order = paymentMsg.getOrderId();//处理调用订单接口的参数 这里用的是httpClient,别忘记引入jar包CloseableHttpClient httpClient = HttpClientBuilder.create().build();HttpPost httpPost= new HttpPost("http://localhost:8080/handleOrder");NameValuePair orderIdPair = new BasicNameValuePair("orderId",order+"");List<NameValuePair> list = new ArrayList<>();list.add(orderIdPair);HttpEntity httpEntity = new UrlEncodeFormEntity(list);httpPost.setEntity(httpEntity);//调用订单接口,并且得到返回值CloseableHttpResponse execute = httpClient.execute(httpPost);String s= EntityUtils.toString(response.getEntity());if("success".equals(s)){paymentMsg.setStatus(1);//发送成功}else{Integer falureCnt = paymentMsg.getFalureCnt();falureCnt++;//重试超过5次,就失败if(falureCnt>5){paymentMsg.setStatus(2);//失败}}paymentMsgMapper.updateByPrimaryKey(paymentMsg);}}}
5)进行测试
执行一下支付的接口
localhost:8080/payment?userId=1&orderId=10010&amount=200
(4)MQ的最终一致性方案
原理、流程与本地消息表类似,主要的不同点是将本地消息表改为MQ、定时任务改为MQ的消费者
依旧采用之前的示例做演示,依旧采用消息表的库,同意也要准备数据
1)安装rocketmq,其他mq也一样
这里在windows下安装,需要安装jdk1.8及以上版本
下载地址:http://rocketmq.apache.org/dowloading/releases/
- 选择Binary的包下载,解压压缩包
- 设置环境变量,新建环境变量ROCKETMQ_HOME,指向rocketmq的解压目录(D:\rocketmq-all-4.5.2-bin-release)
- 启动nameserver,在rocketmq的bin目录下执行mqnamesrv.cmd
- 启动broker(队列),在rocketmq的bin目录下执行mqbroker.cmd -n localhost:9876
2)配置生产者和消费者
- 引入maven
<dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-client</artifactId><version>4.5.2</version >
</dependency>
- 配置mq
使用spring统一管理mq的生命周期,这里消费者和生产者配置在一起,真实的工作中,多数情况是分开的
@Configuration
public class RocketMQConfig{@Bean(initMethod = "start",destroyMethod = "shutdown")public DefaultMQProducer producer(){DefaultMQProducer producer = new DefaultMQProducer("paymentGroup");producer.setNamesrvAddr("localhost:9876");return producer;}@Bean(initMethod = "start",destroyMethod = "shutdown")public DefaultMQPushConsumer consumer(@Qualifier("messageListener")MessageListenerConcurrently messageListener) throws MQClientException{DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("paymentConsumerGroup");consumer.setNamesrvAddr("localhost:9876");consumer.subscribe("payment","*");consumer.registerMessageListener(messageListener);return consumer;}
}
3)编写代码
- 支付接口
@Service
public class PaymentService{@Resourceprivate AccountAMapper accountAMapper;@Autowireprivate DefaultMQProducer producer;/** userId:用户id* orderId:订单id* amount:订单金额*/@Transactional(transactionManager = "tm200",rollbackFor = Exception.class)public int pament(int userId,int orderId, BigDecimal amount) throws Exception{//支付操作AccountA accountA=accountAMapper.selectByPrimaryKey(userId);//用户不存在if(accountA == null)return 1;//余额不足if(accountA.getBalance().compareTo(amount)<0)return 2;accountA.setBalance(accountA.getBalance().subtract(amount));accountAMapper.updateByPrimaryKey(accountA);//放入消息队列Message message = new Message();message.setTopic("payment");message.setKeys(orderId+"");message.setBody("订单已支付".getBytes());producer.send(message);try{SendResult result = producer.send(message);if(result.getSendStatus() == SendStatus.SEND_OK){//成功return 0;}else{//消息发送失败,抛出异常让事务回滚throw new Exception("消息发送失败!");}}cath(Exception e){e.printStackTrace();throw e;}}
}
Controller层提供接口
@RestController
public class PaymentController{@Autowireprivate PaymentService paymentService;/** userId:用户id* orderId:订单id* amount:订单金额*/@RequestMapping("/pament")public String pament(int userId,int orderId, BigDecimal amount){int result = paymentService.pament(userId,orderId,amount);return "支付结果:"+result;}
}
- 实现mq消费者
@Component
public class ChangeOrderStatus implements MessageListenerConcurrently{@Autowiredprivate OrderMapper orderMapper;@Overridepublic ConsumeConcurrentlyStatus consumeMessage(List<MessageExt list,ConsumeConcurrentlyContext consumeConcurrentlyContext>){if(list == null || list.size ==) {//消费成功return CONSUME_SUCCESS;}//实际上 如果不修改默认配置,这个list只会有一个消息for(MessagesExt messageExt: list){String orderId = messageExt.getKeys();String msg =new String(messageExt.getBody()); System.out.println("msg="+msg);Order order = orderMapper.selectByPrimaryKey(Integer.parseInt(orderId));//订单不存在,再次消费if(order == null)return RECONSUME_LATER;try{//修改订单支付状态order.setOrderStatus(1); //已支付orderMapper.updateByPrimaryKey(order);}catch(Exception e){e.printStackTrace();return RECONSUME_LATER;}}return CONSUME_SUCCESS;}
}
4)进行测试
执行一下支付的接口
localhost:8080/payment?userId=1&orderId=10010&amount=200
···