分布式事务
基础概念
1.分布式系统中,每个系统都对应着一个数据库。数据库是相互独立的。
2.每个数据库事务执行期间是不知道别的数据库的执行情况的。所以就需要一个协调者,来统一管理
3.分布式系统会把一个应用系统拆分成多个可独立部署的服务,因此需要服务之间远程协作才能完成整个服务操作,这种分布式环境下的由不同服务之间通过网络远程协议协作完成的事务称为分布式事务.
例子:用户用户注册送积分事务i,创建订单减库存事务,银行转账事务都是分布式事务
用到分布式事务情况(场景):
1.多个服务多个数据库(跨jvm,mysql)
2.单个服务多个数据库(mysql分布式)
3.多个服务一个数据库(服务分布式)
基础理论
CAP理论
Consistency(一致性),Availability(可用性),Partition tolerance(分区容忍性)
流程:
1.商品服务请求主数据库写入商品信息(添加商品,修改商品,删除商品)
2.主数据库向商品服务响应写入成功
3.商品服务的请求从数据库读取商品信息
一致性
指的是写操作后可以读取到数据的最新状态,当数据分布在多个节点上,从任意节点读取到的数据都是最新状态.
实现一致性就要解决下面问题:
1.商品服务写入主数据库,则向从数据库查询新数据也要成功
2.商品服务写入主数据库失败,则向从数据库查询新数据也失败
实现思路:
1.写入主数据库后将数据同步到从数据库
2.写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完后再释放锁,以免在新的数据写入
数据库成功后,向从数据库查询到旧的数据.
分布式系统一致性的特点:
1.由于数据库同步过程,会有读数据会有延时
2.为保证数据的一致性对资源暂时锁定,待数据同步完成释放锁定资源
可用性
可用性指的是:任何事务都可以的到相应结果,且不会出现超时或者响应错误.
上图中如果想要实现可用性需要:
1.从数据库中查询的数据的请求可以立即得到响应结果
2.从数据库不能出现响应超时或者响应错误
如何实现:
1.写入主数据库后将数据同步到从数据库
2.由于要实现可用性从数据库中的资源不可锁定
3.即使没有同步过来数据,也能返回旧的数据,不能返回超时或者错误码
特点:
所有请求都有相应,不可出现超时或者错误
分区容忍性
是指:分布式系统的各个节点部署到不同的节点,这就是网络分区,不可避免的会出现网络问题导致节点通信失败,此时仍可对外提供服务,这叫做分区的容忍性.
思路:
1.主数据库向从数据库同步数据失败,不影响读写操作
2.其中一个节点挂了不影响另外的节点提供服务.
实现:
1.异步取代同步操作,异步将数据同步到从数据库,实现松耦合
2.一个从节点挂了,它可以从其他节点提供服务
特点:
1.分区容忍性是分布式系统具备的基本能力
CAP组合原理
分布式系统下,不会捅死具备cap三个特性,因为在具备p的前提下c和a是不能共存的
下图满足了p即满足了分区容忍性:
从上面的可以看到,p(分区容忍性)是分布式系统的基本能力,c是一致性,为保持数据的一致性,写数据时要把从数据库锁住,而a时可用性,要保证从读取数据都要有响应,要求从数据库不能上锁,不能超时和出现异常.此时C和A就出现矛盾,怎么解决呢?
1.AP组合方式:放弃一致性,可用性和分区容忍性,就是一定会查到数据,可能时旧数据,就是一定会查到数据.不会出现异常,这是可以接受的,因为用户一定会再从数据后更新数据得到新的数据,等于说新的数据一定会copy到从的数据库,这是公司常用的策略
例子:如用户转账,用户可以查到数据,查到的数据可能是旧数据,这是可以接受的,因为用户知道还没到账,接受一定时间内接受新的数据.
2.CP组合:一致性和分区容忍性,放弃可用性,我们的zookeeper追求的就是强一致性,比如说跨行转账,他就要求我们必须两个银行数据都处理完了,才算事务成功
3.CA:放弃分区容忍性,不考虑网络原因或节点挂掉,则我们可以实现一致性和可用性,那么不是一个标准的分布式系统,关系型数据库就满足CA
总结
在我们分布式开发中,实践证明我们只能用CAP中的两种,其中我们最常用的是AP,牺牲掉一致性,保证数据的最终一致性.
BASE理论:柔性事务
Basically Available (基本可以),Soft state(软状态),Eventually consistence(最终一致性),BASE是对CAP中Ap的扩展,通过牺牲强一致性来获取可用性,当出现故障部分可不用,但是核心功能可用,允许数据一定时间内不一致,以达到最终的一致性,满足BASE理论,称为"柔性事务"
强一致性和最终一致性
CAP告诉我们,我们只能用其中的两个进行组合,我们最常用的是A(Availability)和P(Partition Tolerance),放弃一致性,实现最终一致性.
XA方案
XA/JTA规范能解决分布式事务吗
分布式事务是不能100%解决事务问题的,只能说是提高成功率.
一阶段 1PC
假如有多个分布式服务,一个协调者
当执行一部分事务的时候,需要多个分布式服务共同执行。所以当每个分布式服务执行成功的时候,就向协调者发送执行成功的信息。当协调者发现所有的服务都执行成功的时候,就发送信息告诉每个分布式服务去commit
。这样就完成了。
一旦有任何一个分布式事务执行失败,并告诉协调者。协调者就给每个分布式服务发送信息进行rollback。这样就可以保持数据的一致性
二阶段 2PC
老张,老王,老刘,老韩四个人长时间不见面,准备去聚餐,来到了饭店,老板说你们的先结账,老张,老刘,老韩先付款了,结果老王说有事来不了了,尴尬了一批
1.准备:老板要求老张,老韩,老刘先付款,他们付了款
2.提交:老板发票,三人落座
老王说来不了了,四人餐三人不够,没有多余的钱,老板不给票,把三人的款都退了,聚餐失败.
准备,协调者开启事务,协调者向所有的参与者发出Prepare请求,参与者(数据库)收到请求之后,将请求放在log中。逐步执行。
当执行成功的时候,就返回协调者执行成功,当协调者接收到所有的服务都执行成功之后,就告诉所有的服务进行commit
缺点:
- 协调者出现单点故障
- 可能会出现数据不一致的情况,假如协调者发起commit。发送给1都正常,但是发给2的时候出现网络异常。导致2并没有提交。这个时候就会出现问题
- 把整个资源再准备阶段都锁定,执行完才释放锁.
开源框架框架:atomikos
一个简单的例子:
加入依赖
<dependencies>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>4.0.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
然后分布式事务设计方面
public class SoaLession {
public static void main(String[] args) {
//将数据源交给AtomikosDataSourceBean管理
AtomikosDataSourceBean ds1 = createAtomikosDataSourceBean("day05");
AtomikosDataSourceBean ds2 = createAtomikosDataSourceBean("day03");
//创建事务管理器,即中间管理者
UserTransaction transactionImp = new UserTransactionImp();
Connection conn1 = null;
Connection conn2 = null;
PreparedStatement ps1 = null;
PreparedStatement ps2 = null;
try {
transactionImp.begin();
conn1 = ds1.getConnection();
conn2 = ds2.getConnection();
String day05Sql = "insert into king(k_id,k_name) value(3,'金莲')";
ps1 = conn1.prepareStatement(day05Sql);
ps1.executeUpdate();
String dayd03Sql = "insert into user(user_name) value('张三')";
ps2 = conn2.prepareStatement(dayd03Sql);
int i = 2/0;
ps2.executeUpdate();
transactionImp.commit();
} catch (Exception e ){
e.printStackTrace();
}
}
private static AtomikosDataSourceBean createAtomikosDataSourceBean(String dbName) {
Properties p = new Properties();
p.setProperty("url", "jdbc:mysql://localhost:3306/"+dbName+"?useSSL=false&serverTimezone=UTC");
p.setProperty("user","root");
p.setProperty("password","123456");
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName(dbName);
ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
ds.setXaProperties(p);
return ds;
}
}
参与者:
AP:及应用
TM:事务管理器,控制全局的事务
RM:事务的参与者(数据库),每一个分支事务
柔性事务
TCC(try-Confirm-Cancle):两阶段补偿阶段
解决方案两阶段(2PC)解决方案
1.准备(预留资源–try)
2.提交(确认资源—confirm/cancle)
准备;
提交:
Seata方案
Seata方案是alibab中间件团队发起的开源的项目Fescar,后来更名为Seata,它是一个开源的分布式框架,传统的2PC的问题再Seata方案中得到了解决,它通过本地关系型数据库的分支事务的协调来驱动完整全局的事务,是工作应用层的中间件,主要的优点是性能号,且不长时间占用资源,它的搞笑并且对业务的0侵入的方式来解决分布式下的事务问题.
seata的设计思想:
Transaction Coordinator(TC):事务协调器,它是独立的中间件,需要独立的部署运行,它维护全局事务的运行状态,接受TM指令发起的全局事务的提交和回滚,负责与RM通信协调各个分支事务的提交和回滚
Transaction Manager™:事务管理器,嵌入到应用程序中工作,它负责开启一个事务的全局事务,并且最终让TXC发起全局事务的提交和回滚
Resource Manager(RM):控制分支事务,负责分支注册,状态汇报,并且接受事务协调器的指令.
TC是事务的具体实施者决定事务的提交和回滚;TM收集每个分支事务的结果,交给TC去执行,开启一个全局事务.
Seata实现2PC和传统的2PC差别
1.传统的2PC方案是基于数据库连接管理实现的分布式事务,它的第一阶段(准备阶段)并没有提交事务,而是预提交数据,第一阶段如果没有问题,然后由事务管理器决定Commit/Rollback,可能存在的问题也是网络问题,解决方案是重试,或者定时任务,或者是人工服务处理.
2.Seata实现2PC方案,它是先提交数据,每个事务成功否交给TM然后由TC来执行Commit/Rollback,TM是以jar包的形式用于应用,然后每个RM执行成功才Commit或者是有一个执行失败Rollback,回滚是删除原来执行成功的插入的数据.
案例(实践章节)
TCC方案
什么是TCC事务
TCC:try,Confirm,Cancel三个单词
try:表示预处理阶段
Confirm:确认
Cancel:回滚
三个阶段:
1.try阶段:是做业务的检查(一致性)以及资源的预留(隔离),此阶段仅仅是一个初步操作,它和后面的Confirm才在一起才能真正的构成一个完整的业务逻辑.
2.Confirm阶段是做确认提交,Try阶段所有的分支事务执行成功后开始Confirm.通常情况下,采用TCC则认为Confirm是不会出错的,即:只要Try成功,Confirm一定成功,若是Confirm阶段真的出错了,需要引入重试机制或者人工处理的.
3.Cancel阶段是在业务执行错误需要回滚的状态下执行分支事务的业务的业务取消,预留资源释放.通常情况下,采用TCC则认为Cancel阶段一定是成功的.若是Cancel阶段真的出错了,需要引入重试机制或者人工处理的.
4.TM事务管理器
TM事务管理器是可以独立的实现为独立的服务,也可以让全局事务发起方充当Tm的角色,TM独立出来是为了成为公用组件,是为了考虑系统结构和软件的复用.
TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链,用来记录事务上下文,追踪和记录状态,由于Confirm和Cancel失败需要进行重试,因此需要实现幂等,幂等是指同一个操作无论请求多少次,其结果是一样的.
Tcc框架Hmily
Seata分布式框架也是支持TCC的(在Seatajar包中有tcc的包),但是Seata的TCC模式是不支持Spring Cloud.
Hmily是一个高性能的分布式开源TCC框架,基于Java语言开发(JDK8),
支持Dubbo,spring cloud等rpc远程调用服务分布式框架.它目前支持一以下的特性:
1.支持嵌套事务
2.采用disruptor框架进行事务日志的异步读写,与rpc的性能毫无差别
3.支持SpringBoot-starter项目的启动,使用简单
4.rpc框架:dubbo,motan,springcloud
5.本地事务存储支持:redis,mongdb,zookeeper,file,mysql
6.事务日志序列化:支持:javahessian,kryo.protostuff
7.采用AOP切面思想与spring无缝集成,天然支持集群
TCC需要注意的三种异常处理分别是空回滚,幂等,悬挂
1.空回滚:
在没有调用TCC资源方法的情况下.调用第二阶段的Cancel方法,CAncel需要来一个空的回滚,然后直接返回成功
出现的原因是当一个分支事务所在的服务宕机或者异常,分支事务调用记录为失败,这个时候其实是没有执行try阶段的,当故障恢复后,分布式什么hi五进行回滚则会调用第二阶段的Cancel方法,从而形成空回滚.
解决的思路:关键是要识别出这个空回滚.思路很简单就是需要知道一阶段是否执行,日过执行,就正常回滚,如果没有,那就空回滚,前面已经说过TM在/发起全局事务时会生成全局事务记录,全局事务ID贯穿整个事务的调用链条,再额外增加一张分支事务记录表,其中全局事务ID和分支 事务ID,第一阶段Try方法里插入一条记录,则表示第一阶段执行了,CAncel接口的读取该记录,如果该记录存在正常回滚,不存在就空回滚.
简单的小结:就是针对cancel的问题,try没执行就不要cancel了,避免造成数据的混乱
幂等
通过前面的介绍了解到,为了保证TCC二阶段提交重试机制不会引发数据的不一致,要求TCC二阶段Try,Confirm和Cancel要保证幂等没这样不会重复使用或者释放资源,如果幂等没有做好,很可能出现数据的不一致问题.
解决思路:在上述"分支事务记录"中增加执行状态,每次执行前都查询状态.
注意:难点
悬挂
悬挂就是对一个分布式事务,其二阶段Cancel接口比try接口先执行.
出现原因是rpc调用分支事务try时,先注册分支事务,在执行rpc调用的网络拥堵,通常rpc调用是有超时时间的,rpc超时时,TM就会通知RM回滚分支事务,可能回滚完成后,rpc才到达参与者真正执行,而一个try方法预留的业务资源.只有该分布式才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理,对于这种情况,我们就称为悬挂,即业务资源预留后没办法继续处理.
解决思路:如果二阶段执行 完了,那一阶段就不能在执行了,在第一阶段事务时判断在该全局事务下,"分支事务记录"表中是否有第二阶段的事务记录,如果有不在执行try.
小结:在执行try时需要判断cancel和confirm执行没,如果cancel和confirm没执行再执行try
例子
cancel:
减少30元
存在的问题:
1.对于账户A:如果try没执行,则会出现空回滚问题,导致账户A多出来30块钱
解决办法:判断try执行没,没有执行就不要执行cancel
2.try和cancel还有confirm都是由单独的线程调用的,且会出现重复调用,所以都需要实现幂等.
3.账号B再try阶段增加元,当try执行完后其他线程把他给消耗了
4.账户B的try没有执行再cancel多减少元
优化方案:
解决幂等的关键步骤
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bj8Hj0ep-1577502884859)(D:\总结\分布式事务和锁\img\分布式事务\幂等的关键.png)]
三个阶段的数据库日志表.
具体的实现步骤
案例使用mybatis逆向工程来解决
1.首先是依赖搭配问题s
1>springcloud和springboot的搭配
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dtx.tcc</groupId>
<artifactId>dtx-tcc-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>dtx-tcc-parent</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2>dtx-tcc-demo搭配
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-core</artifactId>
<version>2.0.6-RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
</dependencies>
2.配置问题
1>bank1配置
server:
port: 8081
spring:
application:
name: dtx-tcc-demo-bank1
profiles:
active: local
2>application-local.yml的配置
spring:
datasource:
ds0:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/bank1?characterEncoding=utf8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 5
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+ss
eureka:
client:
eureka-server-port: 8761
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://${eureka.instance.hostname}:${eureka.client.eureka-server-port}/eureka/
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.application.instance_id:${server.port}}
feign:
client:
config:
default:
ConnectTimeout: 2000
ReadTimeout: 2000
hystrix:
enabled: true
hystrix:
command:
default:
executon:
isolation:
thread:
timeoutInMilliseconds: 1000 #设置熔断时间
timeout:
enabled: true
org:
dromara:
hmily:
serializer: kryo
recoverDelayTime: 30
retryMax: 30
scheduleDelay: 30
scheduledThreadMax: 10
RepositorySupport: db
started: true
hmilyDbConfig:
driverClassNane: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hmily?characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
mybatis:
mapper-locations: classpath*:/mappers/*.xml
3.配置类
package com.soa.tcc.lession.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.dromara.hmily.common.config.HmilyConfig;
import org.dromara.hmily.common.config.HmilyDbConfig;
import org.dromara.hmily.core.bootstrap.HmilyTransactionBootstrap;
import org.dromara.hmily.core.service.HmilyInitService;
import org.dromara.hmily.core.service.impl.HmilyInitServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.env.Environment;
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DatabaseConfiguration {
private final ApplicationContext applicationContext;
public DatabaseConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Autowired
private Environment env;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.ds0")
public DruidDataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* org:
* dromara:
* hmily:
* serializer: kryo
* recoverDelayTime: 30
* retryMax: 30
* scheduleDelay: 30
* scheduledThreadMax: 10
* RepositorySupport: db
* started: true
* hmilyDbConfig:
* driverClassMane: com.mysql.cj.jdbc.Driver
* url: jdbc:mysql://localhost:3306/hmily?characterEncoding=utf8&serverTimezone=UTC
* username: root
* password: 123456
*
* hmilyDbConfig:
* driverClassNane: com.mysql.cj.jdbc.Driver
* url: jdbc:mysql://localhost:3306/hmily?characterEncoding=utf8&serverTimezone=UTC
* username: root
* password: 123456
* @param hmilyInitService
* @return
*/
@Bean
public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){
HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
hmilyTransactionBootstrap.setRecoverDelayTime(30);
hmilyTransactionBootstrap.setRetryMax(30);
hmilyTransactionBootstrap.setScheduledDelay(30);
hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("db"));
hmilyTransactionBootstrap.setStarted(true);
HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassNane"));
hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url"));
hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username"));
hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password"));
hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
return hmilyTransactionBootstrap;
}
}
4.业务逻辑代码实现思路
1>逆向工程操作
A.插件
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
B.逆向工程文件:修改一下就能用
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 引入第三方依赖包 -->
<!-- <classPathEntry location=".\lib\mysql-connector-java-8.0.12.jar" />-->
<!--
targetRuntime常用值:
MyBatis3Simple(只生成基本的CRUD和少量的动态SQL)
MyBatis3(生成完整的CRUD,包含CriteriaAPI方法Example后缀的方法)
-->
<context id="localhost_mysql" targetRuntime="MyBatis3">
<!-- 不生成注释 -->
<commentGenerator><!--注解编辑器-->
<property name="suppressAllComments" value="true" />
</commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/bank1?characterEncoding=utf8&serverTimezone=UTC"
userId="root"
password="123456">
<property name="nullCatalogMeansCurrent" value="true"/>
</jdbcConnection>
<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<!-- 生成实体类 -->
<javaModelGenerator targetPackage="com.xxx.pojo" targetProject="../taotao-pojo/src/main/java">
<property name="enableSubPackages" value="false" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- 生成XML Mapper -->
<sqlMapGenerator targetPackage="src/main/resources/mappers" targetProject=".">
<property name="enableSubPackages" value="false" />
</sqlMapGenerator>
<!-- 生成Mapper接口 -->
<!-- 生成的Mapper类型:ANNOTATEDMAPPER(注解)、MIXEDMAPPER(混合)、XMLMAPPER(XML) -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.taotao.mapper" targetProject="src/main/java">
<!-- 是否将数据库中的schema作为包名的一部分,默认就是false -->
<property name="enableSubPackages" value="false" />
</javaClientGenerator>
<!--<table schema="xxx" tableName="message" domainObjectName="Message">
<!– 是否用数据库中的字段名作为POJO属性名(不自动转小驼峰),默认值是false –>
<!–
<property name="useActualColumnNames" value="true"/>
–>
<!– 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 –>
<generatedKey column="id" sqlStatement="mysql" identity="true" />
<!– 此标签用于在生成代码时忽略数据库中的某个字段 –>
<!–
<ignoreColumn column="FRED" />
–>
<!– 通过此标签重写mybatis从数据库读到的元信息,自定义列相关配置,包括(名称、类型) –>
<!–
<columnOverride column="aa" property="sname" />
–>
</table>-->
<table schema="" tableName="tb_content">
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_content_category">
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_item" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_item_cat" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_item_desc" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_item_param" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_item_param_item" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_order" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_order_item" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_order_shipping" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
<table schema="" tableName="tb_user" >
<!-- 生成代码时支持获取插入数据后自增的ID, 需要通过sqlStatement配置数据库类型。 -->
<generatedKey column="id" sqlStatement="mysql" identity="true" />
</table>
</context>
</generatorConfiguration>
上面的操作主要是对数据库bank1的操作,即try,confirm,cancel,还有账户表的操作,主要的操作通过API来操作,不用写数据库语句,然后就是主要的操作是插入和更新
5.业务逻辑层
一:转账人
try,confirm,cancel的具体操作
@Hmily指的就是try方法需要指定Cancel和Confirm,在本类中指定就行
三个方法:本地事务控制
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Hmily {
String confirmMethod() default "";
String cancelMethod() default "";
PatternEnum pattern() default PatternEnum.TCC;
}
1.幂等判断
判断日志中是否有执行的记录,如果有执行记录则不再执行,需要有全局事务的ID
String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
然后根据全局事务id查找对应的执行记录,有则不执行,没有则执行try
2.悬挂处理
如果confirm和cancel执行了,try则不在执行,直接从数据库查找就行.判断一下,如果有执行记录,try不执行return,如果没有,则执行
3.try执行完后插入执行记录
4.远程调用另一个接口转账.即增加目的人存款金额.
5.空回滚
cancel即rollback()方法中:
1>.幂等的判断:从数据库中查询cancel有没有执行记录,有执行记录,则不执行,没有执行记录则不执行.
2>.预防空回滚
是在cancel中做的,拿到全局事务id查找try执行没,有执行日志,执行cancel,没有执行没日志则不会滚return.
3>业务逻辑处理
二:收账人
1.结构还用
2.直在confirm中收账
1>做幂等
2>收账
3>插入执行日志