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不会回滚

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,679评论 6 342
  • 佛遗教经 (佛垂般涅槃略说教诫经) 《佛遗教经》:简称《遗教经》,又名《佛垂般涅槃略说教诫经》,一卷,姚秦·鸠摩罗...
    江南莫之阅读 847评论 13 22
  • 一不小心flag差点倒了,按理说6号就得更新了,赶紧来扶一把要倒的flag,今天更新两篇哈,勉强交差。 第一篇本来...
    李轩若阅读 157评论 0 0