事务概念

编程式事务

是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager)来实现编程式事务。

编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。

例如

Connection conn = ...;
  
try {
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    // 核心操作
    // 业务代码
    // 提交事务
    conn.commit();
  
}catch(Exception e){
  
    // 回滚事务
    conn.rollBack();
  
}finally{
  
    // 释放数据库连接
    conn.close();
  
}

声明式事务

是指使用注解或 XML 配置的方式来控制事务的提交和回滚。

开发者只需要添加配置即可, 具体事务的实现由第三方框架实现,避免我们直接进行事务操作!

使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。

区别:

  • 编程式事务需要手动编写代码来管理事务
  • 而声明式事务可以通过配置文件或注解来控制事务

Spring事务管理器

Spring声明式事务对应依赖

  • spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)
  • spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager
  • spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等

Spring声明式事务对应事务管理器接口

使用的事务管理器 org.springframework.jdbc.datasource.DataSourceTransactionManager

将来整合 JDBC方式、JdbcTemplate方式、Mybatis方式的事务实现!

DataSourceTransactionManager 类中的主要方法:

  • doBegin:开启事务
  • doSuspend:挂起事务
  • doResume:恢复挂起的事务
  • doCommit:提交事务
  • doRollback:回滚事务

基于注解的声明式事务

导入 spring 依赖

<!--spring context依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>6.0.6</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>6.0.6</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>jakarta.annotation</groupId>
  <artifactId>jakarta.annotation-api</artifactId>
  <version>2.1.1</version>
</dependency>

<!-- 数据库驱动 和 连接池-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.25</version>
</dependency>

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.2.8</version>
</dependency>

<!-- spring-jdbc -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>6.0.6</version>
</dependency>

<!-- 声明式事务依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-tx</artifactId>
  <version>6.0.6</version>
</dependency>

<!-- AOP-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>6.0.6</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.6</version>
</dependency>
</dependencies>

配置数据库连接信息

新建 jdbc.properties

jdbc.url=jdbc:mysql://localhost:3306/spring
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.username=root
jdbc.password=root

spring 配置类

@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:jdbc.properties")
public class JavaConfig {

  @Value("${jdbc.driver}")
  private String driver;
  @Value("${jdbc.url}")
  private String url;
  @Value("${jdbc.username}")
  private String username;
  @Value("${jdbc.password}")
  private String password;
  //druid连接池
  @Bean
  public DataSource dataSource(){
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
  }
  @Bean
  //jdbcTemplate
  public JdbcTemplate jdbcTemplate(DataSource dataSource){
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(dataSource);
    return jdbcTemplate;
  }

}

组件基本代码

dao

@Repository
public class StudentDao {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public void updateNameById(String name,Integer id){
    String sql = "update students set name = ? where id = ? ;";
    int rows = jdbcTemplate.update(sql, name, id);
  }

  public void updateAgeById(Integer age,Integer id){
    String sql = "update students set age = ? where id = ? ;";
    jdbcTemplate.update(sql,age,id);
  }
}

service

@Service
public class StudentService {

  @Autowired
  private StudentDao studentDao;

  public void changeInfo(){
    studentDao.updateAgeById(100,1);
    
    studentDao.updateNameById("test1",1);
  }
}

测试代码

@SpringJUnitConfig(JavaConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class TxTest {

  @Autowired
  private StudentService studentService;

  @Test
  public void  testTx(){
    studentService.changeInfo();
  }
}

基本事务管理

配置事务管理器

数据库配置信息

package com.example;
@Configuration
@ComponentScan("com.example")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {
  /**
     * 实例化dataSource加入到ioc容器
     * @param url
     * @param driver
     * @param username
     * @param password
     * @return
     */
  @Bean
  public DataSource dataSource(@Value("${jdbc.url}")String url,
                               @Value("${jdbc.driver}")String driver,
                               @Value("${jdbc.username}")String username,
                               @Value("${jdbc.password}")String password){
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);

    return dataSource;
  }

  /**
     * 实例化JdbcTemplate对象,需要使用ioc中的DataSource
     * @param dataSource
     * @return
     */
  @Bean
  public JdbcTemplate jdbcTemplate(DataSource dataSource){
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(dataSource);
    return jdbcTemplate;
  }

  /**
     * 装配事务管理实现对象
     * @param dataSource
     * @return
     */
  @Bean
  public TransactionManager transactionManager(DataSource dataSource){
    return new DataSourceTransactionManager(dataSource);
  }

}

在组件类上使用声明事务注解@Transactional

@Service
public class StudentService {

  @Autowired
  private StudentDao studentDao;

  @Transactional
  public void changeInfo(){
    studentDao.updateAgeById(200,1);
    int i = 1/0; // 手动出错抛异常 ,数据回滚
    studentDao.updateNameById("test1",1);
  }
}

@Transactional 事务注解

@Transactional注解放在类上

  1. 生效原则

    如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。

    对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。

  2. 用法举例

    在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。

    然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。

事务属性:只读

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

// readOnly = true把当前事务设置为只读 默认是false!
@Transactional(readOnly = true)

事务属性:超时时间

如果事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,占用数据库连接时间过长,可设置超时时间,回滚程序,即超时回滚,释放资源

/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
*/
@Transactional(readOnly = false,timeout = 3)

事务属性:事务异常

  • rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
  • noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!

例如

@Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class)

事务属性:事务隔离级别

常见的事务隔离级别

  1. 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。

  2. 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。

  3. 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。

  4. 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。

不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整

语法:

isolation = 设置事务的隔离级别, mysql 默认是 repeatable read!

例如

@Transactional(readOnly = false,
               timeout = 3,
               rollbackFor = Exception.class,
               noRollbackFor = FileNotFoundException.class,
               isolation = Isolation.REPEATABLE_READ)

事务属性:事务传播行为

@Transactional 注解通过 propagation 属性设置事务的传播行为

默认值: Propagation propagation() default Propagation.REQUIRED

propagation 属性的可选值由 org.springframework.transaction.annotation.Propagation 枚举类提供:

名称 含义
REQUIRED 默认值 如果父方法有事务,就加入,如果没有就新建自己独立!
REQUIRES_NEW 不管父方法是否有事务,我都新建事务,都是独立的!

注意:

在同一个类中,对于 @Transactional 注解的方法调用,事务传播行为不会生效。这是因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此 @Transactional 注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果。


参考资料