分布式事务理论到实战看这一篇就够了
分布式事务
一、概念
1. 何为分布式事务
一个事务包含多个操作,多个操作操作了多个数据源,这样的事务称为分布式事务。
2. CAP定理
画图举例说明:
例:用户在北京服务器下了单。
一致性:要想保证一致性,那么如果用户查询订单请求路由到了上海,因为上海没有这笔订单数据,所以只能报错来拒绝服务,让用户到北京服务器去查询
可用性:要想用户两个服务器都能访问,就只能不保证一致性,访问上海时就返回null,订单不存在。
这就是为什么CAP不能同时满足,一般都是满足AP或者CP的原因
3. BASE理论
BASE理论简单说就是,虽然CAP里面一般都只能满足CP和AP,但是这样满足不了大部分公司的要求,所以我们就降低了强一致性或者可用性,两者中和一下。
4. XA协议
X/Open 提出的分布式事务处理规范,分布式事务处理的工业标准。是一套跨语言的标准。
第一阶段:
第二阶段:
5. JTA
Java Transaction API
是Java根据XA规范提供的事务处理标准
二、事务处理方案
1. XA/JTA规范的两阶段提交
使用atomikos框架解决
传统的强一致性的方案。整个过程中的数据都会被锁住。
2. 可靠消息最终一致性方案
3. TCC(Try-confirm-cancel)两阶段补偿型方案
每个操作都需要实现预留方法,提交方法,取消方法,开发量比较大。它不依赖资源管理器(RM),通过对业务逻辑的分解来实现。
框架:
Atomikos,tcc-transaction,ByteTcc,支付宝GTS
4. 最大努力通知方案
类似银行的支付回调,会多次回调直到成功。
三、 代码实例
1. 跨库事务解决示例(不适用跨服务事务)
1.引入pom包,使用的是atomikos框架
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2.配置多个数据源
spring.jta.transaction-manager-id=txManager
spring.datasource.druid.system-db.name=system-db
spring.datasource.druid.system-db.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
spring.datasource.druid.system-db.username=root
spring.datasource.druid.system-db.password=0490218292
spring.datasource.druid.system-db.initialSize=5
spring.datasource.druid.system-db.minIdle=5
spring.datasource.druid.system-db.maxActive=20
spring.datasource.druid.system-db.maxWait=60000
spring.datasource.druid.system-db.timeBetweenEvictionRunsMillis=60000
spring.datasource.druid.system-db.minEvictableIdleTimeMillis=30000
spring.datasource.druid.system-db.validationQuery=SELECT 1
spring.datasource.druid.system-db.validationQueryTimeout=10000
spring.datasource.druid.system-db.testWhileIdle=true
spring.datasource.druid.system-db.testOnBorrow=false
spring.datasource.druid.system-db.testOnReturn=false
spring.datasource.druid.system-db.poolPreparedStatements=true
spring.datasource.druid.system-db.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.druid.system-db.filters=stat,wall
spring.datasource.druid.system-db.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
spring.datasource.druid.system-db.useGlobalDataSourceStat=true
spring.datasource.druid.business-db.name=business-db
spring.datasource.druid.business-db.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
spring.datasource.druid.business-db.username=root
spring.datasource.druid.business-db.password=0490218292
spring.datasource.druid.business-db.initialSize=5
spring.datasource.druid.business-db.minIdle=5
spring.datasource.druid.business-db.maxActive=20
spring.datasource.druid.business-db.maxWait=60000
spring.datasource.druid.business-db.timeBetweenEvictionRunsMillis=60000
spring.datasource.druid.business-db.minEvictableIdleTimeMillis=30000
spring.datasource.druid.business-db.validationQuery=SELECT 1
spring.datasource.druid.business-db.validationQueryTimeout=10000
spring.datasource.druid.business-db.testWhileIdle=true
spring.datasource.druid.business-db.testOnBorrow=false
spring.datasource.druid.business-db.testOnReturn=false
spring.datasource.druid.business-db.poolPreparedStatements=true
spring.datasource.druid.business-db.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.druid.business-db.filters=stat,wall
spring.datasource.druid.business-db.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
spring.datasource.druid.business-db.useGlobalDataSourceStat=true
3.java代码配置数据源
注意这里的PACKAGE路径,代表写在里面的mapper会使用这个数据源。
@Component
@ConfigurationProperties(prefix="spring.datasource.druid.business-db")
public class BusinessProperties {
private String url;
private String username;
private String password;
private Integer initialSize;
private Integer minIdle;
private Integer maxActive;
private Integer maxWait;
private Integer timeBetweenEvictionRunsMillis;
private Integer minEvictableIdleTimeMillis;
private String validationQuery;
private Integer validationQueryTimeout;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;
private boolean poolPreparedStatements;
private Integer maxPoolPreparedStatementPerConnectionSize;
private String connectionProperties;
private boolean useGlobalDataSourceStat;
private String filters;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getInitialSize() {
return initialSize;
}
public void setInitialSize(Integer initialSize) {
this.initialSize = initialSize;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
public Integer getMaxActive() {
return maxActive;
}
public void setMaxActive(Integer maxActive) {
this.maxActive = maxActive;
}
public Integer getMaxWait() {
return maxWait;
}
public void setMaxWait(Integer maxWait) {
this.maxWait = maxWait;
}
public Integer getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}
public void setTimeBetweenEvictionRunsMillis(Integer timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}
public Integer getMinEvictableIdleTimeMillis() {
return minEvictableIdleTimeMillis;
}
public void setMinEvictableIdleTimeMillis(Integer minEvictableIdleTimeMillis) {
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}
public String getValidationQuery() {
return validationQuery;
}
public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}
public Integer getValidationQueryTimeout() {
return validationQueryTimeout;
}
public void setValidationQueryTimeout(Integer validationQueryTimeout) {
this.validationQueryTimeout = validationQueryTimeout;
}
public boolean isTestWhileIdle() {
return testWhileIdle;
}
public void setTestWhileIdle(boolean testWhileIdle) {
this.testWhileIdle = testWhileIdle;
}
public boolean isTestOnBorrow() {
return testOnBorrow;
}
public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}
public boolean isTestOnReturn() {
return testOnReturn;
}
public void setTestOnReturn(boolean testOnReturn) {
this.testOnReturn = testOnReturn;
}
public boolean isPoolPreparedStatements() {
return poolPreparedStatements;
}
public void setPoolPreparedStatements(boolean poolPreparedStatements) {
this.poolPreparedStatements = poolPreparedStatements;
}
public Integer getMaxPoolPreparedStatementPerConnectionSize() {
return maxPoolPreparedStatementPerConnectionSize;
}
public void setMaxPoolPreparedStatementPerConnectionSize(Integer maxPoolPreparedStatementPerConnectionSize) {
this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
}
public String getConnectionProperties() {
return connectionProperties;
}
public void setConnectionProperties(String connectionProperties) {
this.connectionProperties = connectionProperties;
}
public boolean isUseGlobalDataSourceStat() {
return useGlobalDataSourceStat;
}
public void setUseGlobalDataSourceStat(boolean useGlobalDataSourceStat) {
this.useGlobalDataSourceStat = useGlobalDataSourceStat;
}
public String getFilters() {
return filters;
}
public void setFilters(String filters) {
this.filters = filters;
}
}
@Configuration
@MapperScan(basePackages = BusinessDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "businessSqlSessionFactory")
public class BusinessDataSourceConfig {
static final String PACKAGE = "com.mmc.transaction.mapper.business";
@Autowired
private BusinessProperties businessProperties;
@Bean(name = "businessDataSource")
public DataSource businessDataSource() {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setXaProperties(PojoUtil.obj2Properties(businessProperties));
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("businessProperties");
ds.setPoolSize(5);
ds.setTestQuery("SELECT 1");
return ds;
}
@Bean
public SqlSessionFactory businessSqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(businessDataSource());
return sqlSessionFactoryBean.getObject();
}
}
4.mapper文件
UserInfoMapper
public interface UserInfoMapper {
void insertUserInfo(UserInfo userInfo);
}
UserInfoMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mmc.transaction.mapper.business.UserInfoMapper">
<resultMap id="BaseResultMap" type="com.mmc.transaction.bean.UserInfo">
<id column="id" jdbcType="INTEGER" property="id" />
<id column="userid" jdbcType="INTEGER" property="userid" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="address" jdbcType="VARCHAR" property="address" />
</resultMap>
<insert id="insertUserInfo" parameterType="com.mmc.transaction.bean.UserInfo" >
insert into user_info(userid,email,address) values (#{userid},#{email},#{address})
</insert>
</mapper>
5.测试代码
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
public void insertUser(){
User u = new User();
u.setName("mmc");
u.setAge(29);
userMapper.insertUser(u);
UserInfo userInfo = new UserInfo();
userInfo.setUserid(11);
userInfo.setEmail("990974807@qq.com");
userInfo.setAddress("四川省");
userInfoMapper.insertUserInfo(userInfo);
// int i=30/0;
}
}
当打开int i=30/0的注解时,两个数据库的插入操作都失效。事务处理成功。
2. TCC代码示例