微服务分布式事务详解

在以前传统的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解决方案,也是很多公司选择的技术实现(大牛勿喷)

后续对分布式消息队列实现最终一致性进行整理,只要学不死,就往死里学

发布了288 篇原创文章 · 获赞 88 · 访问量 43万+

猜你喜欢

转载自blog.csdn.net/ypp91zr/article/details/89856277