LCN分布式事务

背景

项目采用Spring Cloud (Spring Boot 2.0.1)开发,Spring Cloud是一个微服务架构实施的综合性解决框架。

1.知识点概述

1.“微服务”

微服务架构的主旨是将原本独立的系统拆分成多个小型服务,这些小型服务在各自的进程中独立运行,服务之间基于HTTP的RESful API进行通信。被拆分的每一个小型服务都围绕着系统中某一项或一些耦合度较高的业务功能进行构建,
并且每个服务都维护这自身的数据存储、业务并发、自动化测试案例以及独立部署机制。
由于有了轻量级的通信协作基础,所有这些微服务可以使用不同的语言来编写

优点:独立部署、扩展性强(可根据不同模块的业务量分别部署相关服务)
缺点:运维的新挑战;接口的一致性;分布式的复杂性

2.事务的传播行为

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)

(1)REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)

(2)SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。

@Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)

(3)MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)

(4)REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。

@Transactional(propagation = Propagation.NOT_SUPPORTED, rollbackFor = Exception.class)

(5)NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

@Transactional(propagation = Propagation.NEVER, rollbackFor = Exception.class)

(6)NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。

@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)

(7)NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。

3.LCN分布式事务原理

由于采用微服务架构,各个模块相互独立,导致原先Spring中的@Transactional注解无法满足跨服务的事务处理。故而引入LCN分布式事务处理。

网上已经有很多关于LCN分布式事务的介绍,主要引用网上的两张图:

正常流程时序图.png
异常流程时序图.png

注:针对异常场景,若项目采用熔断机制,参与方B抛出异常回滚,若参与方A 未能获取到参与方B的异常时,参与方A不会回滚。

2.项目实例

https://github.com/codingapi/tx-lcn/wiki

TxManager源码
https://github.com/codingapi/tx-lcn/tree/master/tx-manager
TxClient使用说明
https://github.com/codingapi/tx-lcn/wiki/TxClient%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E

demo
https://github.com/codingapi/springcloud-lcn-demo

主要原理

核心步骤

(1)创建事务组
在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。
(2)添加事务组
参与方在执行完业务方法以后,将该模块的事务信息添加通知给TxManager的操作。
(3)关闭事务组
在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager的动作。当执行完关闭事务组的方法以后,TxManager将根据事务组信息来通知相应的参与模块提交或回滚事务。

主要实现类.png

问题:

git上的源码是1.5.4版本的Spring Boot,若使用2.0版本,项目启动会报错,主要原因是2.0版本移除了context.embedded包,导致EmbeddedServletContainerInitializedEvent.java对象找不到(该对象主要和Listener配合使用,“通过监听,在刷新上下文后将要发布的事件,用于获取A的本地端口运行服务器。”TxClient主要从该Event中获取服务器端口号。)

解决:

(1)改写springcloud项目下com.codingapi.tx.springcloud.listener包中的ServerListener.java

@Component
public class ServerListener implements ApplicationListener<ApplicationEvent> {

    private Logger logger = LoggerFactory.getLogger(ServerListener.class);

    private int serverPort;

    @Value("${server.port}")
    private String port;

    @Autowired
    private InitService initService;

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        //        logger.info("onApplicationEvent -> onApplicationEvent. "+event.getEmbeddedServletContainer());
        //        this.serverPort = event.getEmbeddedServletContainer().getPort();
        //TODO Spring boot 2.0.0没有EmbeddedServletContainerInitializedEvent 此处写死;modify by young
        this.serverPort = Integer.parseInt(port);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 若连接不上txmanager start()方法将阻塞
                initService.start();
            }
        });
        thread.setName("TxInit-thread");
        thread.start();
    }

    public int getPort() {
        return this.serverPort;
    }

    public void setServerPort(int serverPort) {
        this.serverPort = serverPort;
    }
}

@Component注解会自动扫描配置文件中的server.port值;

(2)改写tx-manager项目下com.codingapi.tm.listener包中的ApplicationStartListener.java

@Component
public class ApplicationStartListener implements ApplicationListener<ApplicationEvent> {


    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        //TODO Spring boot 2.0.0没有EmbeddedServletContainerInitializedEvent 此处写死;modify by young
//        int serverPort = event.getEmbeddedServletContainer().getPort();
        String ip = getIp();
        Constants.address = ip+":48888";//写死端口号,反正TxManager端口也是配置文件配好的(●′ω`●)
    }

    private String getIp(){
        String host = null;
        try {
            host = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return host;
    }
}

(3)改写tx-manager项目下com.codingapi.tm.manager.service.impl包中MicroServiceImpl.java类的getState()方法

@Override
    public TxState getState() {
        TxState state = new TxState();
        String ipAddress = "";
        //TODO Spring boot 2.0.0没有discoveryClient.getLocalServiceInstance() 用InetAddress获取host;modify by young
        //String ipAddress = discoveryClient.getLocalServiceInstance().getHost();
        try {
            ipAddress = InetAddress.getLocalHost().getHostAddress();
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (!isIp(ipAddress)) {
            ipAddress = "127.0.0.1";
        }
        state.setIp(ipAddress);
        state.setPort(Constants.socketPort);
        state.setMaxConnection(SocketManager.getInstance().getMaxConnection());
        state.setNowConnection(SocketManager.getInstance().getNowConnection());
        state.setRedisSaveMaxTime(configReader.getRedisSaveMaxTime());
        state.setTransactionNettyDelayTime(configReader.getTransactionNettyDelayTime());
        state.setTransactionNettyHeartTime(configReader.getTransactionNettyHeartTime());
        state.setNotifyUrl(configReader.getCompensateNotifyUrl());
        state.setCompensate(configReader.isCompensateAuto());
        state.setCompensateTryTime(configReader.getCompensateTryTime());
        state.setCompensateMaxWaitTime(configReader.getCompensateMaxWaitTime());
        state.setSlbList(getServices());
        return state;
    }

注:有些服务器这个方法获取不到ip,注册到tx.manager的ip为127.0.0.1

  @Value("${spring.cloud.client.ip-address}")
    private String ipAddress;

 public TxState getState() {
        TxState state = new TxState();
  //      String ipAddress = "";
        //TODO Spring boot 2.0.0没有discoveryClient.getLocalServiceInstance() 用InetAddress获取host;modify by young
        //String ipAddress = discoveryClient.getLocalServiceInstance().getHost();
//        try {
//           ipAddress = InetAddress.getLocalHost().getHostAddress();
//        } catch (Exception e) {
//           e.printStackTrace();
//        }
        if (!isIp(ipAddress)) {
            ipAddress = "127.0.0.1";
        }
...
}

(4)改写tx-client项目下TransactionServerFactoryServiceImpl.java第60行;

//分布式事务已经开启,业务进行中 **/
        if (info.getTxTransactionLocal() != null || StringUtils.isNotEmpty(info.getTxGroupId())) {
            //检查socket通讯是否正常 (第一次执行时启动txRunningTransactionServer的业务处理控制,然后嵌套调用其他事务的业务方法时都并到txInServiceTransactionServer业务处理下)
            if (SocketManager.getInstance().isNetState()) {
                if (info.getTxTransactionLocal() != null) {
                    return txDefaultTransactionServer;
                } else {
//                    if(transactionControl.isNoTransactionOperation() // 表示整个应用没有获取过DB连接
//                        || info.getTransaction().readOnly()) { //无事务业务的操作
//                        return txRunningNoTransactionServer;
//                    }else {
//                        return txRunningTransactionServer;
//                    }
                    if(!transactionControl.isNoTransactionOperation()) { //TODO 有事务业务的操作 modify by young
                        return txRunningTransactionServer;
                    }else {
                        return txRunningNoTransactionServer;
                    }
                }
            } else {
                logger.warn("tx-manager not connected.");
                return txDefaultTransactionServer;
            }
        }
        //分布式事务处理逻辑*结束***********/

重新maven deploy打出jar包,上传自己的maven服务器;
就此Spring Boot2.0引入LCN分布式事务正常启动。

优化

git项目上引用tx.manager.url(txmanager服务器路径)需要在各自的微服务中添加TxManagerHttpRequestService.javaTxManagerTxUrlService.java的实现类;

可将两个方法继承到springcloud源码包中
(1)添加TxManagerConfiguration.java

@Configuration
@ConditionalOnProperty(value = "tx.manager.url")
public class TxManagerConfiguration {


    @Bean
    @RefreshScope
    @ConfigurationProperties(prefix = "tx.manager")
    public TxManagerProperity txManagerProperity(){
        return new TxManagerProperity();
    };


    @Bean
    public TxManagerTxUrlService txManagerTxUrlService(){
        return new TxManagerTxUrlServiceImpl(txManagerProperity());
    }

    @Bean
    public TxManagerHttpRequestService txManagerHttpRequestService(){
        return new TxManagerHttpRequestServiceImpl();
    }
}

(2) 添加TxManagerProperity .java

public class TxManagerProperity {

   private String url;

   public String getUrl() {
       return url;
   }

   public void setUrl(String url) {
       this.url = url;
   }
}

(3)修改TxManagerTxUrlServiceImpl.java

public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService {


    private TxManagerProperity property;

    public TxManagerTxUrlServiceImpl(TxManagerProperity property) {
        this.property = property;
    }

    @Override
    public String getTxUrl() {
        return property.getUrl();
    }
}

(4)自动配置扫描包添加新增的配置文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.codingapi.tx.TransactionConfiguration,\
  com.codingapi.longfor.TxManagerConfiguration

重新deploy jar包并被项目引用后,各个子服务无需再添加TxManagerHttpRequestService.javaTxManagerTxUrlService.java的实现类

测试

(1)参与方大于2个,参与方A分别调用参与方B和参与方C,只要参与方A捕获到Exception,3方均会回滚;
(2)参与方使用不同的事务传播行为,支持REQUIREDREQUIRES_NEW但是使用SUPPORTS时,A抛异常,B和C可以回滚,A不会回滚
(3)A既继承了ITxTransaction,又添加了@TxTransaction@Transactional注解,异常情况均会回滚

@TxTransaction(isStart = true)
@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)

(4)若项目采用熔断机制,参与方B抛出异常回滚,若参与方A 未能获取到参与方B的异常时,参与方A不会回滚

注:当前还未引入补偿机制,后续补充

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 121,090评论 16 134
  • 一不小心flag差点倒了,按理说6号就得更新了,赶紧来扶一把要倒的flag,今天更新两篇哈,勉强交差。 第一篇本来...
    李轩若阅读 59评论 0 0