Seata 分布式事务的使用和原理浅析

一、说明

  • 本博客首先会对 Seata 进行简单介绍,然后演示如何在项目中使用 Seata 分布式事务(仅 AT 模式),并结合监控工具简单分析它的工作原理,最后再谈谈博主在工作中关于 Seata 的使用感受。
    • Seata 简介
      • 介绍一些 Seata 的重要概念,和胖友们在一些事情上达成共识。
    • Seata 通用接入流程
      • 这部分内容也属于通用知识,和具体工作环境无关。
      • 介绍 Server 端和 Client 端的使用,尤其是 Client 端使用不同微服务框架时如何解决事务传播问题。
    • Seata 分布式事务使用演示
      • 可以看做是上面 “Seata 通用接入流程” 的实战版。
      • 由于博主目前工作的公司使用的是 SpringBoot 1.x 版本,在一些配置细节上可能和某些胖友不同,但万变不离其宗,只要能理解它的核心思想,相信这些小细节问题对于各位优秀的胖友们都是小意思。
    • Seata 工作原理简单分析
      • 首先会提供通过监控工具得到的工作流程截图,让大家有个直观的认知。
      • 然后介绍代码中关键的类。
  • 胖友们可以把本博客当成是一个快速上手的参考手册,但如果想要全面系统地了解 Seata 框架,建议查看 Seata 的官方文档以及 GitHub

二、Seata 简介

看到博主这篇博客的胖友,想必大多都是从业 n 年的同仁,所以一些基础概念的介绍和铺垫这里就省略了,而只介绍博主认为最核心的部分。

2.1、Seata 是什么?

  • Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

2.2、Seata 的整体架构

2.2.1、主要角色

  • TC (Transaction Coordinator) - 事务协调者
    • 维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器
    • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器
    • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
  • 事务回话信息存储
    • 非必须角色,理论上可以不引入额外组件。
    • 事务会话信息存储方式有:file本地文件(不支持HA),db数据库|redis(支持HA)
    • 但从生产实践角度来看,这个组件也是必须的,博主使用的是 MySQL
  • 注册中心
    • 非必须角色,理论上可以不引入额外组件。
    • 默认file,支持file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom
    • 但从生产实践角度来看,这个组件也是必须的,博主使用的是 zk.
  • 配置中心
    • 非必须角色,理论上可以不引入额外组件。
    • 默认file,支持file、nacos 、apollo、zk、consul、etcd3、custom
    • 从生产实践角度来看,这个组件也是必须的,博主使用的是 zk.

2.2.2、整体架构和工作流程图

  • 整体架构图

    • 在这里插入图片描述
  • 工作流程图

    • 在这里插入图片描述

2.3、Seata 的事务模式

  • AT
  • TCC
  • SAGA
  • XA 事务模式

2.3.1、Seata 的 AT 事务模式

  • 前提
    • 基于支持本地 ACID 事务的关系型数据库。
    • Java 应用,通过 JDBC 访问数据库。
  • 整体机制
    • 两阶段提交协议的演变:
      • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
      • 二阶段:
        • 提交异步化,非常快速地完成。
        • 回滚通过一阶段的回滚日志进行反向补偿。
  • 写隔离
    • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
    • 拿不到 全局锁 ,不能提交本地事务。
    • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁
  • 读隔离
    • 在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)
    • 如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

2.4、微服务框架支持

  • 从上面的 2.2.2、整体架构和工作流程图 的工作流程图中可以看到,整个事务过程中,需要有一个全局的事务ID,并且需要把这个全局事务ID在各个微服务间传播。

2.4.1、事务上下文

  • Seata 的事务上下文由 RootContext 来管理。
  • 应用开启一个全局事务后,RootContext 会自动绑定该事务的 XID,事务结束(提交或回滚完成),RootContext 会自动解绑 XID。
  • 应用可以通过 RootContext 的 API 接口(RootContext.getXID())来获取当前运行时的全局事务 XID
  • 应用是否运行在一个全局事务的上下文中,就是通过 RootContext 是否绑定 XID 来判定的

2.4.2、事务传播

  • 服务内部的事务传播
    • 默认的,RootContext 的实现是基于 ThreadLocal 的,即 XID 绑定在当前线程上下文中
    • 所以服务内部的 XID 传播通常是天然的通过同一个线程的调用链路串连起来的。默认不做任何处理,事务的上下文就是传播下去的
    • 如果希望挂起事务上下文,则需要通过 RootContext 提供的 API 来实现:
      •   // 挂起(暂停)
          String xid = RootContext.unbind();
        
          // TODO: 运行在全局事务外的业务逻辑
        
          // 恢复全局事务上下文
          RootContext.bind(xid);
        
  • 跨服务调用的事务传播
    • 跨服务调用场景下的事务传播,本质上就是要把 XID 通过服务调用传递到服务提供方,并绑定到 RootContext 中去。
    • 只要能做到这点,理论上 Seata 可以支持任意的微服务框架。

2.5、ORM 框架支持

  • Seata 虽然是保证数据一致性的组件,但对于 ORM 框架并没有特殊的要求,像主流的Mybatis,Mybatis-Plus,Spring Data JPA, Hibernate等都支持。这是因为ORM框架位于JDBC结构的上层,而 Seata 的 AT,XA 事务模式是对 JDBC 标准接口操作的拦截和增强

2.6、数据库类型支持

  • AT模式支持的数据库有:MySQL、Oracle、PostgreSQL和 TiDB
  • TCC模式不依赖数据源(1.4.2版本)

2.7、SQL 参考

  • 完整 SQL 参考传送阵
  • 需要特别注意它的使用限制:
    • 不支持 SQL 嵌套
    • 不支持多表复杂 SQL
    • 不支持存储过程、触发器
    • 不支持批量更新 SQL

三、Seata 通用接入流程

3.1、Seata Server 端(TC)

  1. 下载程序包并解压
    1. 当前最新版本:seata-server-1.4.2
  2. 配置事务回话信息存储方式
    1. 配置文件:seata-server-1.4.2\conf\file.conf
    2. 事务会话信息存储方式有:file本地文件(不支持HA),db数据库|redis(支持HA),这里配置为 db.
      1. ## transaction log store, only used in seata-server
        store {
        ## store mode: file、db、redis
        mode = "db"
        ## rsa decryption public key
        publicKey = ""
        
        
        ## database store property
         db {
          ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) 	etc.
            datasource = "druid"
            ## mysql/oracle/postgresql/h2/oceanbase etc.
            dbType = "mysql"
            driverClassName = "com.mysql.cj.jdbc.Driver"
            ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
            url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true"
            user = "root"
            password = "Andy6666"
            minConn = 5
            maxConn = 100
            globalTable = "global_table"
            branchTable = "branch_table"
            lockTable = "lock_table"
            queryLimit = 100
            maxWait = 5000
          }
        
        }
        
        
    3. 在 MySQL 数据库创建事务回话信息表
      1. -- -------------------------------- The script used when storeMode is 'db' --------------------------------
        -- the table to store GlobalSession data
        CREATE TABLE IF NOT EXISTS `global_table`
        (
        	`xid`                       VARCHAR(128) NOT NULL,
        	`transaction_id`            BIGINT,
        	`status`                    TINYINT      NOT NULL,
        	`application_id`            VARCHAR(32),
        	`transaction_service_group` VARCHAR(32),
        	`transaction_name`          VARCHAR(128),
        	`timeout`                   INT,
        	`begin_time`                BIGINT,
        	`application_data`          VARCHAR(2000),
        	`gmt_create`                DATETIME,
        	`gmt_modified`              DATETIME,
        	PRIMARY KEY (`xid`),
        	KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
        	KEY `idx_transaction_id` (`transaction_id`)
        ) ENGINE = InnoDB
        DEFAULT CHARSET = utf8;
        
        -- the table to store BranchSession data
        CREATE TABLE IF NOT EXISTS `branch_table`
        (
        	`branch_id`         BIGINT       NOT NULL,
        	`xid`               VARCHAR(128) NOT NULL,
        	`transaction_id`    BIGINT,
        	`resource_group_id` VARCHAR(32),
        	`resource_id`       VARCHAR(256),
        	`branch_type`       VARCHAR(8),
        	`status`            TINYINT,
        	`client_id`         VARCHAR(64),
        	`application_data`  VARCHAR(2000),
        	`gmt_create`        DATETIME(6),
        	`gmt_modified`      DATETIME(6),
        	PRIMARY KEY (`branch_id`),
        	KEY `idx_xid` (`xid`)
        ) ENGINE = InnoDB
        DEFAULT CHARSET = utf8;
        
        -- the table to store lock data
        CREATE TABLE IF NOT EXISTS `lock_table`
        (
        	`row_key`        VARCHAR(128) NOT NULL,
        	`xid`            VARCHAR(128),
        	`transaction_id` BIGINT,
        	`branch_id`      BIGINT       NOT NULL,
        	`resource_id`    VARCHAR(256),
        	`table_name`     VARCHAR(32),
        	`pk`             VARCHAR(36),
        	`gmt_create`     DATETIME,
        	`gmt_modified`   DATETIME,
        	PRIMARY KEY (`row_key`),
        	KEY `idx_branch_id` (`branch_id`)
        ) ENGINE = InnoDB
        DEFAULT CHARSET = utf8;
        
        
  3. 配置注册中心和配置中心
    1. 配置文件:seata-server-1.4.2\conf\registry.conf。这里博主把注册中心和配置中心都设置为 zk(胖友们可以根据情况设置成其他的,配置大同小异)。
      1.  registry {
         # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
         type = "zk"
         
         zk {
         	cluster = "default"
         	serverAddr = "127.0.0.1:2181"
         	sessionTimeout = 6000
         	connectTimeout = 2000
         	username = ""
         	password = ""
         }
         
         }
         
         config {
         # file、nacos 、apollo、zk、consul、etcd3
         type = "zk"
         
         zk {
         	cluster = "default"
         	serverAddr = "127.0.0.1:2181"
         	sessionTimeout = 6000
         	connectTimeout = 2000
         	username = ""
         	password = ""
         }
         
         }
        
        
  4. 启动 Server 端
    1. 点击 seata-server-1.4.2\bin\seata-server.bat(或 seata-server.sh)

3.2、Seata Client 端(TM和RM)

3.2.1、业务系统集成 Seata Client

  1. 业务数据库创建回滚日志表
    1.  -- for AT mode you must to init this sql for you business database. the seata server not need it.
       CREATE TABLE IF NOT EXISTS `undo_log`
       (
       	`branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
       	`xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
       	`context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
       	`rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
       	`log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
       	`log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
       	`log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
       	UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
       ) ENGINE = InnoDB
       AUTO_INCREMENT = 1
       DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
      
      
  2. 业务系统添加 seata 依赖
    1.  <dependency>
           <groupId>io.seata</groupId>
           <artifactId>seata-spring-boot-starter</artifactId>
           <version>1.3.0</version>
       </dependency>
      
  3. 业务系统配置文件中配置注册中心和配置中心、事务分组名称
    1. 这里注册中心和配置中心都选择 zk(以 account-service 为例)
    2.  seata:
       	registry:
       		type: zk
       		zk:
       		server-addr: localhost:2181
       	config:
       		type: zk
       		zk:
       		server-addr: localhost:2181
       	txServiceGroup:  account-service-g
      
  4. 在 Seata 配置中心配置事务分组和TC集群的关联关系
    1. 在 zk 添加以下配置(其他配置中心大同小异):
      1. 节点名称:/seata/service.vgroupMapping.account-service-g,节点内容:default
        1. 其中 account-service-g 是事务分组名称
        2. default 是 TC 集群名称
        3. 即配置所有事务分组名称为 account-service-g 的 TM 和 RM 注册到集群名称为 default 的 TC 集群

3.2.2、不同微服务框架解决 Seata 事务传播问题

3.2.2.1、事务传播的原理
  • 2.4.2、事务传播 可以知道,跨服务调用场景下的事务传播,本质上就是要把 XID 通过服务调用传递到服务提供方,并绑定到 RootContext 中去。
3.2.2.2、事务传播的解决方案
  • 远程服务调用方:
    • 发起远程服务调用时,需要把全局事务XID包含到请求信息中
  • 远程服务提供方:
    • 处理请求前,解析获取 XID 并绑定到 RootContext 中
    • 处理请求后,将 XID 从 RootContext 中解绑
3.2.2.3、常用微服务框架的事务传播问题
(1)、Seata + Dubbo 分布式事务
  • 如何实现 Dubbo 请求的前置和后置处理呢? 熟悉 Dubbo 的胖友可能就会说,扩展它的 Filter。
  • 没错,我们就是要扩展 Dubbo 的 Filter,在服务调用方发起请求时,设置全局事务 XID;在服务提供方处理请求前,解析获取XID,并绑定到 RootContext,处理完请求后,清理 XID。
  • 幸运的是,Seata 框架中已经默认提供了这样的 Filter,它基于 SPI 机制自动注册,项目中也可以通过 ServiceLoader.load 查看。
  • 也就是说,Seata 天然支持 Dubbo 框架的事务传播,我们什么都不需要做。
(2)、Seata + Spring Cloud OpenFeign 分布式事务
  • Spring Cloud OpenFeign 是基于 Http 协议的微服务框架。远程服务调用方需要在 Header 中设置全局事务XID;远程服务提供方在处理请求前需要从 Http Header 中获取全局事务XID 并绑定到 RootContext,处理完请求后清理 XID。
  • 推荐的做法:
    • 远程服务调用方:通过 feign 提供的 RequestInterceptor 设置 Header。但由于 OpenFeign 默认集成了 Hystrix,通信时使用的是异步多线程通信,所以还需要自定义配置 HystrixConcurrencyStrategy,处理线程间全局事务XID传递的问题。
      • 配置 HystrixConcurrencyStrategy:
        •   @Slf4j
            @Configuration
            public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
            
            	private HystrixConcurrencyStrategy delegate;
            
            	public FeignHystrixConcurrencyStrategy() {
            		try {
            			this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
            			if (this.delegate instanceof FeignHystrixConcurrencyStrategy) {
            				return;
            			}
            			HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins
            					.getInstance().getCommandExecutionHook();
            			HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance()
            					.getEventNotifier();
            			HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance()
            					.getMetricsPublisher();
            			HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance()
            					.getPropertiesStrategy();
            
            
            			HystrixPlugins.reset();
            			HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
            			HystrixPlugins.getInstance()
            					.registerCommandExecutionHook(commandExecutionHook);
            			HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
            			HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
            			HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
            		}
            		catch (Exception ex) {
            			log.error("Failed to register Seata Hystrix Concurrency Strategy", ex);
            		}
            	}
            
            	@Override
            	public <T> Callable<T> wrapCallable(Callable<T> callable) {
            		if (callable instanceof SeataContextCallable) {
            			return callable;
            		}
            		return new SeataContextCallable<>(callable,
            				RequestContextHolder.getRequestAttributes());
            	}
            
            	private static class SeataContextCallable<K> implements Callable<K> {
            
            		private final Callable<K> actual;
            
            		private final String xid;
            
            		private final RequestAttributes requestAttributes;
            
            		SeataContextCallable(Callable<K> actual, RequestAttributes requestAttribute) {
            			this.actual = actual;
            			this.requestAttributes = requestAttribute;
            			this.xid = RootContext.getXID();
            		}
            
            		@Override
            		public K call() throws Exception {
            			try {
            				RequestContextHolder.setRequestAttributes(requestAttributes);
            				if (!StringUtils.isEmpty(xid)) {
            					RootContext.bind(xid);
            				}
            				return actual.call();
            			}
            			finally {
            				if (!StringUtils.isEmpty(xid)) {
            					RootContext.unbind();
            				}
            				RequestContextHolder.resetRequestAttributes();
            			}
            		}
            
            	}
            
            }
          
      • 添加 feign RequestInterceptor 设置 Header
        •   @Slf4j
            public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {
            
            	@Override
            	public void apply(RequestTemplate template) {
            		try {
            			// 支持 seata 事务传播
            			String xid = RootContext.getXID();
            			if (!StringUtils.isEmpty(xid)) {
            				template.header(RootContext.KEY_XID, xid);
            			}
            		} catch (Exception e) {
            			log.error("FeignBasicAuthRequestInterceptor apply fail。", e);
            		}
            
            	}
            }
          
          
    • 远程服务调用方:基于 WEB 拦截器做前置、后置处理。例如,基于 Spring Web 项目,可以通过 HandlerInterceptor 实现:
      •   public class SeataHandlerInterceptor implements HandlerInterceptor {
          
          	private static final Logger log = LoggerFactory
          			.getLogger(SeataHandlerInterceptor.class);
          
          	@Override
          	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
          			Object handler) {
          		String xid = RootContext.getXID();
          		String rpcXid = request.getHeader(RootContext.KEY_XID);
          		if (log.isDebugEnabled()) {
          			log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);
          		}
          
          		if (StringUtils.isBlank(xid) && rpcXid != null) {
          			RootContext.bind(rpcXid);
          			if (log.isDebugEnabled()) {
          				log.debug("bind {} to RootContext", rpcXid);
          			}
          		}
          
          		return true;
          	}
          
          	@Override
          	public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
          
          	}
          
          	@Override
          	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
          			Object handler, Exception e) {
          		if (StringUtils.isNotBlank(RootContext.getXID())) {
          			String rpcXid = request.getHeader(RootContext.KEY_XID);
          
          			if (StringUtils.isEmpty(rpcXid)) {
          				return;
          			}
          
          			String unbindXid = RootContext.unbind();
          			if (log.isDebugEnabled()) {
          				log.debug("unbind {} from RootContext", unbindXid);
          			}
          			if (!rpcXid.equalsIgnoreCase(unbindXid)) {
          				log.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid);
          				if (unbindXid != null) {
          					RootContext.bind(unbindXid);
          					log.warn("bind {} back to RootContext", unbindXid);
          				}
          			}
          		}
          	}
          
          }
        
(3)、Seata + RestTemplate 分布式事务
  • RestTemplate 同样基于 Http 协议,处理方式和 Spring Cloud OpenFeign 类似。推荐的做法:
    • 远程服务调用方:
      • 定义拦截器:
        •   public class SeataRestTemplateInterceptor implements ClientHttpRequestInterceptor {
            
            	@Override
            	public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,
            			ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
            		HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);
            
            		String xid = RootContext.getXID();
            
            		if (!StringUtils.isEmpty(xid)) {
            			requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);
            		}
            		return clientHttpRequestExecution.execute(requestWrapper, bytes);
            	}
            
            }
          
      • 给 RestTemplate 设置拦截器
        • restTemplate.setInterceptors(Collections.singletonList(new SeataRestTemplateInterceptor()));
    • 远程服务提供方:基于 WEB 拦截器做前置、后置处理。处理方式完全和 Spring Cloud OpenFeign一样,即可以复用上面的 SeataHandlerInterceptor
(3)、Seata + 其他微服务框架
  • 实现思路参考:3.2.2.2、事务传播的解决方案
  • 在动手实现前,先查看 seata 包中是否已经默认提供相关方案。例如,motan、grpc 框架也享受和 Dubbo 框架同样的待遇,Seata 同样也默认提供了一个拥有传播事务XID的过滤器。
  • 实在找不到现成方案,我想根据上面的思路,自己实现一套事务传播方案,相信对于各位能看到这里的胖友来说,应该是小意思。

四、Seata 分布式事务使用演示

  • 创建 3 个项目,order-service、storage-service、account-service

  • order-service 开启事务,并通过 Spring Cloud OpenFeign 调用 storage-service 服务。

    • 在这里插入图片描述
  • storage-service 通过 RestTemplate 调用 account-service 提供的服务。

    • 在这里插入图片描述
  • account-service 提供的服务。

    • 在这里插入图片描述
  • 通过 postman 调用 order-service 的接口,可以分别看到以下日志:

    • 在这里插入图片描述

    • 在这里插入图片描述

    • 在这里插入图片描述

  • 通过日志,可以看到全局事务最终是提交的状态。而要使全局事务的回滚,只需要让其中一个分支事务失败即可,这里就不演示了。

五、Seata 工作原理简单分析

5.1、工作流程

这里使用 SkyWalking 进行链路追踪,给大家展示 四、Seata 分布式事务使用演示 的完整流程

  1. 完整链路追踪:

    1. 在这里插入图片描述
  2. 对 Postman 请求的链路追踪

    1. order-service

      1. 在这里插入图片描述
    2. storage-service

      1. 在这里插入图片描述
    3. account-service

      1. 在这里插入图片描述
  3. 全局事务提交后,异步删除 undo_log

    1. 在这里插入图片描述

5.2、主要的类

  • 2.5、ORM 框架支持 ,我们知道:
    • Seata 虽然是保证数据一致性的组件,但对于 ORM 框架并没有特殊的要求,像主流的Mybatis,Mybatis-Plus,Spring Data JPA, Hibernate等都支持。这是因为ORM框架位于JDBC结构的上层,而 Seata 的 AT,XA 事务模式是对 JDBC 标准接口操作的拦截和增强。
  • Seata 是如何对 JDBC 标准接口操作的拦截和增强???答案在 DataSourceProxy。
    • 它对 DataSource 进行了增强
    • 它的 getConnection 方法返回的是:ConnectionProxy
      • ConnectionProxy 对 Connection 进行了增强
      • 它的 prepareStatement 方法返回的是同样是一个 Proxy 代理类
  • 从 DataSource 开始,Seata 对 JDBC 的标准接口进行了层层增强。而具体每一个增强类实现做了哪些事情,本博客就不深入探讨了。
  • 已经有不少关于 DataSourceProxy 源码解读的博客,各位胖友们可以移步观看。
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值