在以前传统的web应用当中,一个项目基本一个war/jar包走天下,对于事务处理相信很多的项目基本是使用到的spring的事务处理。但是在当下流行的分布式微服务来说,普通的Spring事务处理已经无法满足场景,Spring事务也是基于jvm级别的,当多个服务系统之间进行调用,进行数据库操作,一旦失败就会发现事务会存在严重的问题,举个简单的例子
在上图中,为了减轻数据库的压力等,将数据库分成了2个不同的物理机数据库,在订单系统中需要对2个数据库有数据交互的情况下,在订单库操作操作mysql的时候,成功了,但是在进行库存操作时失败了,平时使用的Spring事务已经完全派不上用场了。
分布式事务-垮库事务
2PC:二阶段提交协议,即将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。事务的发起者称协调者,事务的执行者称参与者。
准备阶段
提交阶段
第一阶段==》准备阶段
协调者向参与者发起询问,询问是否可以提交事务等。然后等待参与者答复,参与者全部答复允许/yes后
参与者进行sql等一系列事务操作(没有提交事务),预提交
参与者都成功,给协调者返回yes,只要一方参与者执行失败,返回no,告诉协调者不可提交
第二阶段==》提交阶段
当都可以进行数据库操作之后,需要进行事务提交。
当参与者事务提交全部返回yes后即成功,若其中一个返回no即全部不可提交,进行回滚
提交事务流程:
协调者向参与者发起事务提交请求commit,参与者commit提交事务并释放所占用的系统资源,参与者向协调者返回提交事务的结果,协调者收到全部成功后即完成事务提交
市面上有的的框架比如jta atomikos,就能完成上面的2PC的功能,在本人第一次遇到上面的垮库场景时,就是用的jta atomikos。原理一样的,使用起来很简单,但可能会有性能一定的影响,如果在需求业务场景允许的条件下,可以尝试
JTA:java transaction api,是java两阶段提交协议的一种规范,java提供一套规范,atomikos框架来进行实现,和JPA一套规范,hibernate来进行实现一样的道理
代码实现:如果使用的Spring Boot项目
pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
application.yml:
spring:
datasource:
druid:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
driver-class-name: com.mysql.jdbc.Driver
platform: mysql
default:
url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/mypinyu?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: root
password: Goodlan@123
second:
url: jdbc:mysql://localhost:3306/mypinyu?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: root
password: admin
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT1FROMDUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
logSlowSql: true
logging.config:
classpath: log4j2.xml
mybatis:
typeAliasesPackage: com.pinyu.system.entity
mapper-locations: classpath:mapper/**/*Mapper.xml
#返回视图的前缀 目录对应src/main/webapp下
spring.mvc.view.prefix: /WEB-INF/jsp/
#返回的后缀
spring.mvc.view.suffix: .jsp
package com.pinyu.system.global.config.datasource;
public interface DataSourceNames {
public static final String DEFAULT = "default";
public static final String SECOND = "second";
}
package com.pinyu.system.global.config.datasource;
import javax.sql.DataSource;
import javax.transaction.UserTransaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.transaction.jta.JtaTransactionManager;
import com.alibaba.druid.filter.stat.StatFilter;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
@Configuration
public class DruidConfig {
@Bean(DataSourceNames.DEFAULT)
@Primary
@Autowired
@ConfigurationProperties(prefix = "spring.datasource.druid."+DataSourceNames.DEFAULT)
public DataSource systemDataSource(Environment env) {
return new AtomikosDataSourceBean();
}
@Autowired
@Bean(name = DataSourceNames.SECOND)
@ConfigurationProperties(prefix = "spring.datasource.druid."+DataSourceNames.SECOND)
public AtomikosDataSourceBean businessDataSource(Environment env) {
return new AtomikosDataSourceBean();
}
/**
* 注入事物管理器
* @return
*/
@Bean("transactionManager")
public JtaTransactionManager regTransactionManager () {
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
}
package com.pinyu.system.global.config.datasource;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
@Configuration
@MapperScan(basePackages = "com.pinyu.system.mapper",sqlSessionFactoryRef = "defaultSqlSessionFactory")
public class MybatisDataSourceDefaultConfig {
@Bean
public SqlSessionFactory defaultSqlSessionFactory(@Qualifier(DataSourceNames.DEFAULT)DataSource ds) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(ds);
//指定mapper xml目录
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:mapper/**/*Mapper.xml"));
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate defaultSqlSessionTemplate(@Qualifier("defaultSqlSessionFactory")SqlSessionFactory sf) throws Exception {
SqlSessionTemplate template = new SqlSessionTemplate(sf); // 使用上面配置的Factory
return template;
}
}
package com.pinyu.system.global.config.datasource;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
/**
* @author ypp
* 创建时间:2018年11月9日 上午10:18:03
* @Description: TODO(用一句话描述该文件做什么)
*/
@Configuration
@MapperScan(basePackages = "com.pinyu.system.mapper2",sqlSessionFactoryRef = "secondSqlSessionFactory")
public class MybatisDataSourceSecondConfig {
@Bean
public SqlSessionFactory secondSqlSessionFactory(@Qualifier(DataSourceNames.SECOND)DataSource ds) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(ds);
//指定mapper xml目录
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:mapper2/**/*Mapper.xml"));
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate secondSqlSessionTemplate(@Qualifier("secondSqlSessionFactory")SqlSessionFactory sf) throws Exception {
SqlSessionTemplate template = new SqlSessionTemplate(sf); // 使用上面配置的Factory
return template;
}
}
mapper和mapper2里面都是mapper接口,操作不同的数据库,在其他地方注入哪个mapper就是操作的哪个数据库,自行配置即可
在service注入就行,但是必须贴上注解@Transactional,
@Service
@Transactional
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Autowired
private UserMapper2 userMapper2;
}
有兴趣的同学可以试试,每个mapper配置不同的数据库操作,一个异常,全部会进行回滚。配置代码可以进一步优化
在以上的还是有一定的缺陷,比如突然一个数据库连接不上了,网络故障等。在以前某宝出现过网线被挖掘机挖断,导致很多脏数据。
缺陷:1、同步阻塞,一直会占用资源,对性能有一定的影响
2、脑裂,部分参与者提交commit请求,节点数据错乱
3、单点,协调者挂了,参与者一直处于锁定状态
3PC:三阶段提交协议,在2PC基础之上更进一步,如果出现网络故障等情况,会进行重试操作,超时连接等。
比较适合的框架有jta atomikos(三阶段提交协议收费,抛弃)、tcc transaction、spring-cloud-rest-tcc、支付宝内部也有一个框架(名字忘了)。
在实际应用中,要用分布式事务的话,建议还是使用分布式消息队列来实现分布式事务数据最终一致性,高可用方案。强一致性就是以性能高可用换数据一致性。有兴趣的同学可以去了解下分布式系统CAP理论,从本人理解来说jta atomikos这种就是一个CP解决方案。用消息队列实现最终一致性就是AP解决方案,也是很多公司选择的技术实现(大牛勿喷)
后续对分布式消息队列实现最终一致性进行整理,只要学不死,就往死里学