分布式事务框架 Seata

前言

写当前文章时,分支最新为 v0.7.1。

seata 的设计想法是将分布式事务看作一个有多个分支事务组成的全局事务,通常分支事务是我们所常用的本地事务。


三大基本组件:

TC(事务协调器)、TM(事务管理器)、RM(资源管理器)

seata 管理分布式事务典型的生命周期:

  1. TM 向 TC 请求开始全局事务, TC 生成一个代表全局性事务的 XID .
  2. XID 通过微服务的调用链传播.
  3. RM 向 TC 注册本地事务,作为 XID 对应的全局事务的分支.
  4. TM 要求 TC 提交或者回滚 XID 相应的全局事务.
  5. TC 驱动 XID 相应的全局事务下的分支事务完成分支的提交或者回滚.

全局事务:一次性操作多个资源管理器的事务就是全局事务。

分支事务:在全局事务中,每一个资源管理器有自己独立的任务,这些任务的集合是这个资源管理器的分支任务。

值得一读的 API GuideSeata AT ModelSeata MT Modelfescar锁设计和隔离级别的理解

Transaction Coordinator(TC) : 事务协调器


维护全局和分支事务的状态,用于全局性事务的提交和回滚。

Transaction Manager™ : 事务管理器


定义全局事务域:开启全局事务、提交或者回滚全局事务。向事务指定标识,监视它们的进场,并负责处理事务的完成和失败。事务分支标识(称为XID) 由 TM 指定,以标识一个 RM 内的全局事务和特定分支。

Resource Manager(RM) : 资源管理器


管理分支事务所使用的资源,向TC注册分支事务并报告分支事务的状态,接受TC的命令来提交或者回滚分支事务。

demo搭建

demo 来自官网提供的seata-samples中的dubbo,按照官网的使用是没有问题的,先整理搭建步骤,再解决心中的疑问;

搭建步骤
  1. undo_log.sql 表的创建,(每个物理库)

    需要修改每个项目下的 file.conf 文件:

    store.mode = db
    
  2. 配置数据源代理:

    // 配置数据源代理
    <bean id="accountDataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
            <constructor-arg ref="accountDataSource" />
    </bean>
    
    // 使用代理数据源创建连接
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="accountDataSourceProxy" />
    </bean>
    
  3. Seata GlobalTransactionScanner 配置:

<bean class="io.seata.spring.annotation.GlobalTransactionScanner">
    	// 程序标识 applicationId ,单机部署多微服务需要保证 applicationId 唯一
        <constructor-arg value="dubbo-demo-account-service"/>
    	// 来自file.conf 文件的 vgroup_mapping.my_test_tx_group = "default"
    	// Seata 事务服务逻辑分组
        <constructor-arg value="my_test_tx_group"/>
</bean>
  1. 启动 seata-server:

    github 下载最新稳定版本,执行解压后 bin 目录下的启动脚本。

    创建 seata 数据库, 根据conf 下的 db_store.sql 建表,修改file.conf 如下:

    store.mode = db
    store.db.datasource=dbcp
    store.db.db-type=mysql
    store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
    store.db.user=mysql
    store.db.password=mysql
    lock.mode=remote
    
  2. 在需要分布式事务的业务逻辑代码的方法上添加注解:

    @GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx")
    
  3. 如果你在 file.conf 文件中使用的 transaction 的 undo.log.serialization 值为 jackson (不配置时,默认也为该值),你需要配置额外的依赖包,否则 undo_log 表并不能插入值,至少我这里是这样;

    		<jackson.version>2.7.4</jackson.version>		
    		<dependency>
              	<groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>
    
疑问
  1. undo_log 这个表需要放在哪个数据库?

    需要保证每个物理库都包含 undo_log 表

  2. 存在业务单元Unit包含了 A、B 2个分支事务的调用,当有请求执行到 B 时,其它请求是否能够在 A 正常提交还是直接阻塞直到失败?

    测试结果:如果是操作同样的数据,其它请求会在执行 A 时会抛出 LockConflictException ,该异常为运行时异常。如果是操作不同数据,这能够执行成功。


    测试代码:

    // BusinessServiceImpl	
    @Override
        @GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx2")
        public void test2(String userId, String commodityCode, int orderCount, Lock lock, CountDownLatch next){
            LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
            storageService.deduct(commodityCode, orderCount);
            System.out.println(Thread.currentThread().getName()+"storage complete!");
            lock.lock();
            try {
                next.await();
                orderService.create(userId, commodityCode, orderCount);
                System.out.println(Thread.currentThread().getName()+"orderService complete!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            //throw new RuntimeException("xxx");
        }
    
    // DubboBusinessTester
    // 静态成员变量
    private static ExecutorService executorService = new ThreadPoolExecutor(2,2,2000,
                TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(), r ->{
            Thread thread = new Thread(r);
            thread.setName("duofei "+ tCount.getAndDecrement() + " :");
            return thread;
        } );
    
    // main 方法
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                new String[]{"spring/dubbo-business.xml"});
            final BusinessService business = (BusinessService) context.getBean("business");
    
            Lock lock = new ReentrantLock();
            CountDownLatch next = new CountDownLatch(1);
    		
    		// 尝试着将线程池中的任务数据改为相同的值
            executorService.execute(()->{
                business.test2("U100002", "C00322", 2,lock,next);
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.execute(()->{
                business.test2("U100001", "C00321", 2,lock,next);
            });
    		try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("接着执行...");
            next.countDown();
    
            executorService.shutdown();
    
            new ApplicationKeeper(context).keep();
    
  3. 存在业务单元unit1包含了A、B、C 3个分支事务以及另一个业务单元unit2存在A、B、D 3个分支事务,当有请求在unit1的分支事务A 正常 commit 以后, unit 2 的分支事务 A 是否能够commit ?

    这和第二个问题反应到本质上,就是 seata 对表中的数据加锁范围,测试结果同二一样,无法操作相同的数据。


    测试代码:

    // BusinessServiceImpl
    @Override
        @GlobalTransactional(timeoutMills = 300000, name = "dubbo-unit1-tx")
        public void unit1(String userId, String commodityCode, int orderCount, Lock lock, CountDownLatch next) {
            LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
            storageService.deduct(commodityCode, orderCount);
            System.out.println(Thread.currentThread().getName()+" unit1 storage complete!");
            lock.lock();
            try {
                next.await();
                orderService.create(userId, commodityCode, orderCount);
                System.out.println(Thread.currentThread().getName()+" unit1 orderService complete!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        @Override
        @GlobalTransactional(timeoutMills = 300000, name = "dubbo-unit2-tx")
        public void unit2(String userId, String commodityCode, int orderCount, Lock lock, CountDownLatch next) {
            LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
            storageService.deduct(commodityCode, orderCount);
            System.out.println(Thread.currentThread().getName()+" unit2 storage complete!");
            lock.lock();
            try {
                next.await();
                orderService.create(userId, commodityCode, orderCount);
                System.out.println(Thread.currentThread().getName()+" unit2 orderService complete!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
    
    // DubboBusinessTester
    // 静态成员
    private static ExecutorService executorService = new ThreadPoolExecutor(2,2,2000,
                TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(), r ->{
            Thread thread = new Thread(r);
            thread.setName("duofei "+ tCount.getAndDecrement() + " :");
            return thread;
        } );
    
    // main 方法
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                new String[]{"spring/dubbo-business.xml"});
            final BusinessService business = (BusinessService) context.getBean("business");
    
            Lock lock = new ReentrantLock();
            CountDownLatch next = new CountDownLatch(1);
    		
    		// 尝试着将线程池中的任务数据改为相同的值
            executorService.execute(()->{
                business.unit1("U100002", "C00322", 3,lock,next);
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.execute(()->{
                business.unit2("U100001", "C00321", 2,lock,next);
            });*/
    
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("接着执行...");
            next.countDown();
    
            executorService.shutdown();
    
            new ApplicationKeeper(context).keep();
    
  4. 是否支持传递?(在其它节点的业务逻辑中进行远程调用)

    已支持,从原始例子中的 orderService 中的操作就可以看出。 这和 seata 的实现原理有关,在使用 dubbo 作为服务框架,能够保证全局事务等相关信息的传输,同时,服务本地的代理数据源能够保证数据的执行严格依赖着全局事务的相关信息。

  5. “脏读现象“验证

    逻辑上是会产生这种现象,如果需要解决,seata 默认是 “读未提交“, 它也支持 ”读已提交“(为查询语句加上 FOR UPDATE, 具体的还未研究)。

  6. 当分布式事务中的某个分支事务 commit 以后,通过其它操作修改该事务提交的数据(例如直接操作数据库),在后续的其它事务中抛出异常,验证回滚现象。

    会有SQLException 异常产生,异常信息为 “Has dirty records when undo.(回滚时有脏记录)”,该异常不会抛出,但会有线程一直尝试回滚,一直失败…


参考
大型网站系统与JAVA中间件实践书籍

如果你觉得我的文章对你有所帮助的话,欢迎关注我的公众号。赞!我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值