19 事务、连接池之间的关系与配置

这⼀讲我们来看下事务在 JPA 和 Spring ⾥⾯的详细配置和原理。

19.1 事务的基本原理

在学习 Spring 的事务之前,⾸先要了解数据库的事务原理,这里以 MySQL 8.0 为例,讲解⼀下数据库事务的基础知识。

当 MySQL 使⽤ InnoDB 数据库引擎的时候,数据库是对事务有⽀持的。⽽事务最主要的作⽤是保证数据 ACID 的特性,即原⼦性(Atomicity)、⼀致性(Consistency)、隔离性(Isolation)、持久性(Durability)

  • 原⼦性: 是指⼀个事务(Transaction)中的所有操作,要么全部完成,要么全部回滚,⽽不会有中间某个数据单独更新的操作。事务在执⾏过程中⼀旦发⽣错误,会被回滚(Rollback)到此次事务开始之前的状态,就像这个事务从来没有执⾏过⼀样。
  • ⼀致性: 是指事务操作开始之前,和操作异常回滚以后,数据库的完整性没有被破坏。数据库事务 Commit 之后,数据也是按照我们预期正确执⾏的。即要通过事务保证数据的正确性。
  • 持久性: 是指事务处理结束后,对数据的修改进⾏了持久化的永久保存,即便系统故障也不会丢失,其实就是保存到硬盘。
  • 隔离性: 是指数据库允许多个连接,同时并发多个事务,⼜对同⼀个数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时,由于交叉执⾏⽽导致数据不⼀致的现象。

⽽ MySQL ⾥⾯就是我们经常说的事务的四种隔离级别,即读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串⾏化(Serializable)。

19.1.1 四种 MySQL 事务的隔离级别
  • Read Uncommitted(读取未提交内容):此隔离级别,表示所有正在进⾏的事务都可以看到其他未提交事务的执⾏结果。不同的事务之间读取到其他事务中未提交的数据,通常这种情况也被称之为脏读(Dirty Read),会造成数据的逻辑处理错误,也就是我们在多线程⾥⾯经常说的数据不安全了。在业务开发中,⼏乎很少⻅到使⽤的,因为它的性能也不⽐其他级别要好多少。
  • Read Committed(读取提交内容): 此隔离级别是指,在⼀个事务相同的两次查询可能产⽣的结果会不⼀样,也就是第⼆次查询能读取到其他事务已经提交的最新数据。也就是我们常说的不可重复读(Nonrepeatable Read)的事务隔离级别。因为同⼀事务的其他实例在该实例处理期间,可能会对其他事务进⾏新的 commit,所以在同⼀个事务中的同⼀ select 上,多次执⾏可能返回不同结果。这是⼤多数数据库系统的默认隔离级别(但不是 MySQL 默认的隔离级别)。
  • Repeatable Read(可重读): 这是 MySQL 的默认事务隔离级别,它确保同⼀个事务多次查询相同的数据,能读到相同的数据。即使多个事务的修改已经 commit,本事务如果没有结束,永远读到的是相同数据,要注意它与Read Committed 的隔离级别的区别,是正好相反的。这会导致另⼀个棘⼿的问题:幻读 (Phantom Read),即读到的数据可能不是最新的。这个是最常⻅的,下面有例子说明。
  • Serializable(可串⾏化):这是最⾼的隔离级别,它保证了每个事务是串⾏执⾏的,即强制事务排序,所有事务之间不可能产⽣冲突,从⽽解决幻读问题。如果配置在这个级别的事务,处理时间⽐较⻓,并发⽐较⼤的时候,就会导致⼤量的 db 连接超时现象和锁竞争,从⽽降低了数据处理的吞吐量。也就是这个性能⽐较低,所以除了某些财务系统之外,⽤的⼈不是特别多。

这四种类型中,能清楚地知道 Read UncommittedRead Committed 就可以了,⼀般这两个⽤得是最多的。

19.1.2 可重复的例子

第一步:创建一个 user 表并插入数据,查询一下数据库的事务隔离级别

CREATE TABLE user
(
    id                    BIGINT                                 NOT NULL
        PRIMARY KEY,
    create_user_id        INT          DEFAULT 0                 NOT NULL,
    created_date          DATETIME     DEFAULT CURRENT_TIMESTAMP NOT NULL,
    last_modified_date    DATETIME     DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP,
    last_modified_user_id INT          DEFAULT 0                 NOT NULL,
    version               INT          DEFAULT 0                 NOT NULL,
    age                   INT          DEFAULT 0                 NOT NULL,
    email                 VARCHAR(255) DEFAULT ''                NOT NULL,
    name                  VARCHAR(255) DEFAULT ''                NOT NULL,
    sex                   VARCHAR(255) DEFAULT ''                NOT NULL
);

INSERT INTO `user` (id, `name`, email, age)
VALUES (1, 'zzn', 'zzn@qq.com', '18'),
       (2, 'zzn', 'zzn@qq.com', '18'),
       (3, 'zzn', 'zzn@qq.com', '18'),
       (4, 'zzn', 'zzn@qq.com', '18');

查询数据库的隔离级别

SELECT @@TRANSACTION_ISOLATION;

查询结果如下:

@@TRANSACTION_ISOLATION

REPEATABLE-READ

第二步:开启一个事务,查询 user 表

START TRANSACTION;
SELECT * FROM `user`;

## 等待另外一个事务执行完成后在执行
SELECT * FROM `user`;
COMMIT;

第三步:打开另外一个数据库连接,删除一条数据

START TRANSACTION;
DELETE FROM `user` WHERE id = 1;
COMMIT;

当删除执⾏成功之后,我们可以开启第三个连接,看⼀下数据库⾥⾯确实少了⼀条 ID=1 的数据。那么这个时候我们再返回第⼀个连接,第⼆次执⾏ select * from user,查到的还是四条数据。这就是我们经常说的可重复读。

19.1.3 MySQL 中事务与连接的关系

我们要搞清楚事务和连接池的关系,必须要先知道⼆者存在的前提条件。

  1. 事务必须在同⼀个连接⾥⾯的,离开连接没有事务可⾔;
  2. MySQL 数据库默认 autocommit=1,即每⼀条 SQL 执⾏完⾃动提交事务;
  3. 数据库⾥⾯的每⼀条 SQL 执⾏的时候必须有事务环境;
  4. MySQL 创建连接的时候默认开启事务,关闭连接的时候如果存在事务没有 commit 的情况,则⾃动执⾏ rollback 操作;
  5. 不同的 connect 之间的事务是相互隔离的。

知道了这些条件,我们就可以继续探索⼆者的关系了。在 connection 当中,操作事务的⽅式只有两种。

第⼀种:⽤ BEGIN、ROLLBACK、COMMIT 来实现。

  • BEGIN开始⼀个事务
  • ROLLBACK事务回滚
  • COMMIT事务确认

第⼆种:直接⽤ SET 来改变 MySQL 的⾃动提交模式。

  • SET AUTOCOMMIT=0禁⽌⾃动提交
  • SET AUTOCOMMIT=1开启⾃动提交

MySQL 数据库的最⼤连接数是什么?

⽽任何数据库的连接数都是有限的,受内存和 CPU 限制,你可以通过 show variables like 'max_connections' 查看此数据库的最⼤连接数、通过 show global status like 'Max_used_connections' 查看正在使⽤的连接数,还可以通过 set global max_connections=1500 来设置数据库的最⼤连接数。

除此之外,你可以在观察数据库的连接数的同时,通过观察 CPU 和内存的使⽤,来判断你⾃⼰的数据库中 server 的连接数最佳⼤⼩是多少。⽽既然是连接,那么肯定会有超时时间,默认是 8 ⼩时。

这⾥我只是列举了 MySQL 数据库的事务处理原理,你可以⽤相同的思考⽅式看⼀下你在⽤的数据源的事务是什么机制的。

19.2 Spring 事务的配置方法

由于我们使⽤的是 Spring Boot,所以会通过 TransactionAutoConfiguration.java 加载 @EnableTransactionManagement 注解帮我们默认开启事务,关键代码如下图所示。

postgre 连接池最大多少 jpa 连接池_mysql

Spring ⾥⾯的事务有两种使⽤⽅式,常⻅的是直接通过 @Transaction 的⽅式进⾏配置,⽽我们打开 SimpleJpaRepository 源码类的话,会看到如下代码。

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    // ...
    @Override
	@Transactional
	public void deleteAllById(Iterable<? extends ID> ids) {

		Assert.notNull(ids, "Ids must not be null!");

		for (ID id : ids) {
			deleteById(id);
		}
	}
    // ...

}

我们仔细看源码的时候就会发现,默认情况下,所有 SimpleJpaRepository ⾥⾯的⽅法都是只读事务,⽽⼀些更新的⽅法都是读写事务。

所以每个 Respository 的⽅法是都是有事务的,即使我们没有使⽤任何加 @Transactional 注解的⽅法,按照上⾯所讲的 MySQL 的 Transactional 开启原理,也会有数据库的事务。

那么我们就来看下 @Transactional 的具体⽤法。

19.2.1 默认 @Transactional 注解式事务

注解式事务⼜称显式事务,需要⼿动显式注解声明,那么我们看看如何使⽤。

按照惯例,我们打开 @Transactional 的源码,如下所示。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    @AliasFor("transactionManager")
    String value() default "";

    /**
     * 指定 transactionManager,当有多个数据源的时候使用
     */
    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    /**
     * 该属性用于设置事务的传播行为。
     */
    Propagation propagation() default Propagation.REQUIRED;

    /**
     * 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,
     * 通常使用数据库的默认隔离级别即可,基本上不需要设置
     */    
    Isolation isolation() default Isolation.DEFAULT;

    /**
     * 该属性用于设置事务的超时秒数,默认值为 -1 表示永不超时
     */ 
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    String timeoutString() default "";

    /**
     * 该属性用于设置当前事务是否为只读事务。
     * 设置为 true 表示只读,false 表示可读写,默认值为 false
     * 例如: @Transactional(readOnly = true)
     */
    boolean readOnly() default false;

    /**
     * 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定数组中的异常时,则进行事务的回滚。
     * 例如:指定一个异常类:@Transactional(rollbackFor = RuntimeException.class)
     * 指定多个异常类:@Transactional(rollbackFor = {RuntimeException.class, Exception.class})
     */
    Class<? extends Throwable>[] rollbackFor() default {};

    /**
     * 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务的回滚。
     * 例如:指定一个异常类:@Transactional(rollbackForClassName = "RuntimeException")
     */
    String[] rollbackForClassName() default {};

    /**
     * 该属性用于设置不需要回滚的异常类数组,当方法中抛出指定异常类数组中的异常时,不进行事务的回滚。
     * 例如: @Transactional(noRollbackFor = RuntimeException.class)
     */ 
    Class<? extends Throwable>[] noRollbackFor() default {};

    /**
     * 该属性用于设置不需要回滚的异常类名称数组,当方法中抛出指定异常类名称数组中的异常时,不进行事务的回滚。
     * 例如: @Transactional(noRollbackForClassName = "RuntimeException")
     */ 
    String[] noRollbackForClassName() default {};

}

其他属性你基本上都可以知道是什么意思,下⾯重点说⼀下隔离级别和事务的传播机制。

隔离级别 Isolation isolation() default Isolation.DEFAULT:默认采⽤数据库的事务隔离级别。其中,Isolation 是个枚举值,基本和我们上⾯讲解的数据库隔离级别是⼀样的,如下所示。

public enum Isolation {

	/**
	 * 使用底层数据存储的默认隔离级别。所有其他级别对应于 JDBC 隔离级别。
	 */
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

	/**
	 * 读未提交
	 * 指示可能发生脏读、不可重复读和幻读的常量。此级别允许在提交该行中的任何更改之前由另一个事务读取由一个事务更改的行(“脏读”)。
	 * 如果回滚任何更改,则第二个事务将检索到无效行。
	 */
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

	/**
	 * 读已提交
	 * 指示防止脏读的常量;可能发生不可重复读取和幻读。此级别仅禁止事务读取其中包含未提交更改的行。
	 */
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

	/**
	 * 可重复读
	 * 指示防止脏读和不可重复读的常量;可能发生幻读。该级别禁止一个事务读取其中未提交更改的行,同时也禁止一个事务读取一行,第二个事务更改该行,第一个事务重新读取该行,第二次得到不同值的情况(“不可重复读取”)。
	 */
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

	/**
	 * 串行化
	 */
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);


	private final int value;


	Isolation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}

}

propagation:代表的是事务的传播机制,这个是 Spring 事务的核⼼业务逻辑,是 Spring 框架独有的,它和 MySQL 数据库没有⼀点关系。所谓事务的传播⾏为是指在同⼀线程中,在开始当前事务之前,需要判断⼀下当前线程中是否有另外⼀个事务存在,如果存在,提供了七个选项来指定当前事务的发⽣⾏为。我们可以看 org.springframework.transaction.annotation.Propagation 这类的枚举值来确定有哪些传播⾏为。7 个表示传播⾏为的枚举值如下所示:

public enum Propagation {

	/**
	 * 如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。这个值是默认的。
	 */
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

	/**
	 * 如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。
	 */
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

	/**
	 * 如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。
	 */
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

	/**
	 * 创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

	/**
	 * 以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。
	 */
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

	/**
	 * 以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
	 */
	NEVER(TransactionDefinition.PROPAGATION_NEVER),

	/**
	 * 如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于 REQUIRED。
	 */
	NESTED(TransactionDefinition.PROPAGATION_NESTED);


	private final int value;


	Propagation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}

}

设置⽅法:通过使⽤ propagation 属性设置,例如下⾯这⾏代码

@Transactional(propagation = Propagation.REQUIRES_NEW)

虽然⽤法很简单,但是也有使⽤ @Transactional 不⽣效的时候,那么在哪些场景中是不可⽤的呢?

19.2.2 @Transactional 的局限性

这⾥列举的是⼀个当前对象调⽤对象⾃⼰⾥⾯的⽅法不起作⽤的场景。

我们在 UserServiceImpl 的 save ⽅法中调⽤了带事务的 calculate ⽅法,代码如下。

@Component
public class UserServiceImpl implements UserService {
    @Autowired
    private UserRepository userRepository;
    /**
     * 根据UserId产⽣的⼀些业务计算逻辑
     */
    @Override
    @Transactional(transactionManager = "masterTransactionManager")
    public User calculate(Long userId) {
        User user = userRepository.findById(userId).get();
        user.setAge(userInfo.getAge() + 1);
        //.....等等⼀些复杂事务内的操作
        user.setTelephone(Instant.now().toString());
        return userRepository.saveAndFlush(user);
    }
    
    
    /**
     * 此⽅法调⽤⾃身对象的⽅法,就会发现 calculate ⽅法上⾯的事务是失效的
     */
    public User save(Long userId) {
        return this.calculate(userId);
    }
}

当在 UserServiceImpl 类的外部调⽤ save ⽅法的时候,此时 save ⽅法⾥⾯调⽤了⾃身的 calculate ⽅法,你就会发现 calculate ⽅法上⾯的事务是没有效果的,这个是 Spring 的代理机制的问题。那么我们应该如何解决这个问题呢?可以引⼊⼀个类 TransactionTemplate,我们看下它的⽤法

19.2.3 TransactionTemplate 的用法

此类是通过 TransactionAutoConfiguration 加载配置进去的,如下图所示。

postgre 连接池最大多少 jpa 连接池_mysql_02

我们通过源码可以看到此类提供了⼀个关键 execute ⽅法,如下图所示。

postgre 连接池最大多少 jpa 连接池_mysql_03

这⾥⾯会帮我们处理事务开始、rollback、commit 的逻辑,所以我们⽤的时候就⾮常简单,把上⾯的⽅法做如下改动

public User save(Long userId) {
    return transactionTemplate.execute(status -> this.calculate(userId));
}

此时外部再调⽤我们的 save ⽅法的时候,calculate 就会进⼊事务管理⾥⾯去了。当然了,这⾥举的例⼦很简单,你也可以通过下⾯代码中的⽅法设置隔离级别和传播机制,以及超时时间和是否只读。

transactionTemplate = new TransactionTemplate(transactionManager);
//设置隔离级别
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
//设置传播机制
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
//设置超时时间
transactionTemplate.setTimeout(1000);
//设置是否只读
transactionTemplate.setReadOnly(true);

我们也可以根据 transactionTemplate 的实现原理,⾃⼰实现⼀个 TransactionHelper,⼀起来看⼀下。

19.2.4 自定义 TransactionHelper

第⼀步:新建⼀个 TransactionHelper 类,进⾏事务管理,代码如下。

@Component
public class TransactionHelper {

    /**
     * 利⽤ spring 的机制和 jdk8 的 supplier 机制实现事务
     */
    @Transactional(rollbackFor = Exception.class)
    public <T> T transactional(Supplier<T> supplier) {
        return supplier.get();
    }

}

第⼆步:直接在 service 中就可以使⽤了,代码如下。

@Autowired
private TransactionHelper transactionHelper;

/**
 * 调⽤外部的 transactionHelper 类,利⽤ transactionHelper ⽅法上⾯的 @Transaction 注解使事务⽣效
 */
public User save(Long userId) {
    return transactionHelper.transactional(() -> this.calculate(userId));
}

上⾯我介绍了显式事务,都是围绕 @Transactional 的显式指定的事务,我们也可以利⽤ AspectJ 进⾏隐式的事务配置。

19.2.5 AspectJ 事务配置

只需要在我们的项⽬中新增⼀个类 AspectjTransactionConfig 即可,代码如下。

@Configuration
@EnableTransactionManagement
public class AspectjTransactionConfig {
    /**
     * 指定拦截器作⽤的包路径
     */
    public static final String TRANSACTION_EXECUTION = "execution (*com.example..service.*.*(..))";

    @Autowired
    private PlatformTransactionManager transactionManager;


    @Bean
    public DefaultPointcutAdvisor defaultPointcutAdvisor() {
        // 指定⼀般要拦截哪些类
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(TRANSACTION_EXECUTION);
        // 配置 advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(pointcut);
        // 根据正则表达式,指定上⾯的包路径⾥⾯的⽅法的事务策略
        Properties attributes = new Properties();
        attributes.setProperty("get*", "PROPAGATION_REQUIRED,-Exception");
        attributes.setProperty("add*", "PROPAGATION_REQUIRED,-Exception");
        attributes.setProperty("save*", "PROPAGATION_REQUIRED,-Exception");
        attributes.setProperty("update*", "PROPAGATION_REQUIRED,-Exception");
        attributes.setProperty("delete*", "PROPAGATION_REQUIRED,-Exception");
        // 创建 Interceptor
        TransactionInterceptor txAdvice = new TransactionInterceptor();
        txAdvice.setTransactionManager(transactionManager);
        txAdvice.setTransactionAttributes(attributes);
        advisor.setAdvice(txAdvice);
        return advisor;
    }
}

上⾯的⽅法介绍完了,那么⼀个⽅法经历的 SQL 和过程都有哪些呢?我们通过⽇志分析⼀下。

19.2.6 通过日志分析配置方法的过程

第⼀步,我们在数据连接中加上 logger=Slf4JLogger&profileSQL=true,⽤来显示 MySQL 执⾏的 SQL ⽇志,如图所示。

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&logger=Slf4JLogger&profileSQL=true

第⼆步,打开 Spring 的事务处理⽇志,⽤来观察事务的执⾏过程,代码如下。

logging:
  level:
    # 监控事务的情况
    org.springframework.orm.jpa: debug
    org.springframework.transaction: trace
    org.hibernate.engine.transaction.internal.TransactionImpl: debug
    # 监控连接的情况
    org.hibernate.resource.jdbc: trace
    com.zaxxer.hikari: debug

第三步,我们执⾏⼀个 save 的操作,通过⽇志可以发现,我们执⾏⼀个 saveUserInfo 的动作,由于在其中配置了⼀个事务,所以可以看到 JpaTransactionManager 获得事务的过程,图上⻩⾊的部分是同⼀个连接⾥⾯执⾏的 SQL 语句,其执⾏的整体过程如下所示。

  1. get connection:从事务管理⾥⾯,获得连接就 begin 开始事务了。我们没有看到显示的 begin 的 SQL,基本上可以断定它利⽤了 MySQL 的 connection 初始化事务的特性。
  2. set autocommit=0:关闭⾃动提交模式,这个时候必须要在程序⾥⾯ commit 或者 rollback。
  3. select user:看看 user 数据库⾥⾯是否存在我们要保存的数据。
  4. update user:发现数据库⾥⾯存在,执⾏更新操作。
  5. commit:执⾏提交事务。
  6. set autocommit=1:事务执⾏完,改回 autocommit 的默认值,每条 SQL 是独⽴的事务。

我们这⾥采⽤的是数据库默认的隔离级别,如果我们通过下⾯这⾏代码,改变默认隔离级别的话,再观察我们的⽇志

@Transactional(isolation = Isolation.READ_COMMITTED)

你会发现在开始事务之前,它会先改变默认的事务隔离级别,⽽在事务结束之后,它还会还原此链接的事务隔离级别。

如果你明⽩了 MySQL 的事务原理的话,再通过⽇志分析可以很容易地理解 Spring 的事务原理。我们在⽇志⾥⾯能看到 MySQL 的事务执⾏过程,同样也能看到 Spring 的 TransactionImpl 的事务执⾏过程。这是什么原理呢?我们来详细分析⼀下。

19.3 Spring 事务的实现原理

这⾥我重点介绍⼀下 @Transactional 的⼯作机制,这个主要是利⽤ Spring 的 AOP 原理,在加载所有类的时候,容器就会知道某些类需要对应地进⾏哪些 Interceptor 的处理。

例如我们所讲的 TransactionInterceptor,在启动的时候是怎么设置事务的、是什么样的处理机制,默认的代理机制⼜是什么样的呢?

19.3.1 Spring 事务源码分析

我们在 TransactionManagementConfigurationSelector ⾥⾯设置⼀个断点,就会知道代理的加载类 ProxyTransactionManagementConfiguration 对事务的处理机制。关键源码如下图所示

postgre 连接池最大多少 jpa 连接池_postgre 连接池最大多少_04

⽽我们打开 ProxyTransactionManagementConfiguration 的话,就会加载 TransactionInterceptor 的处理类,关键源码如下图所示。

postgre 连接池最大多少 jpa 连接池_java_05

如果继续加载的话,⾥⾯就会加载带有 @Transactional 注解的类或者⽅法。关键源码如下图所示。

postgre 连接池最大多少 jpa 连接池_java_06

加载期间,通过 @Trnsactional 注解来确定哪些⽅法需要进⾏事务处理。

JpaTransactionManager ⾥⾯通过 getTransaction ⽅法创建的事务,然后再通过 debuger 模式的 IDEA 线程栈进⾏分析,就能知道创建事务的整个过程。

那么还需要知道它是在什么时间释放连接到连接池⾥⾯的。我们在 org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl#releaseConnection 这个方法可以了解到。

所以,Spring 中的事务和连接的关系是,开启事务的同时获取 DB 连接;事务完成的时候释放 DB 连接。通过 MySQL 的基础知识可以知道数据库连接是有限的,那么当我们给某些⽅法加事务的时候,都需要注意哪些内容呢?

19.3.2 事务和连接池在 JPA 中的注意事项

数据源的连接池不能配置过⼤,否则连接之前切换就会⾮常耗费应⽤内部的 CPU 和内存,从⽽降低应⽤对外提供 API 的吞吐量。

所以当我们使⽤事务的时候,需要注意如下 ⼏个事项:

  1. 事务内的逻辑不能执⾏时间太⻓,否则就会导致占⽤ db 连接的时间过⻓,会造成数据库连接不够⽤的情况;
  2. 跨应⽤的操作,如 API 调⽤等,尽量不要在有事务的⽅法⾥⾯进⾏;
  3. 如果在真实业务场景中有耗时的操作,也需要带事务时(如扣款环节),那么请注意增加数据源配置的连接池数;
  4. 我们通过 MVC 的应⽤请求连接池数量,也要根据连接池的数量和事务的耗时情况灵活配置;⽽ tomcat 默认的请求连接池数量是 200 个,可以根据实际情况来增加或者减少请求的连接池数量,从⽽减少并发处理对事务的依赖。