Spring框架声明式事务

发布 | 2024-09-08 | JAVA

声明式事务是Spring框架提供的一种管理事务的方式

允许在不修改业务代码的情况下,通过注解或XML配置来管理事务。

【声明式】VS【编程式】

声明式:通过注解等方式,告诉框架,我要做什么,框架会帮我做什么。
优点:代码量小。
缺点:封装太多。排错不容易
编程式:通过代码的方式,告诉框架,我要做什么,需要自己写代码实现。
优点:排错容易
缺点:代码量多

操作数据库

1、导入包:spring-boot-starter-data-jdbc、mysql-connector-java
2、配置数据库连接信息:在appLication.properties中spring.datasource.*
3、可以直接使用DataSource/JdbcTemplate

@Transactional 是 Spring 框架中用于声明式事务管理的一个重要注解。它可以应用于类或者方法上,帮助开发者在不编写大量手动事务控制代码的情况下实现对数据库事务的管理。以下是关于 @Transactional 注解的详细笔记:

事务管理

@Transactional

可以应用于接口、接口方法、类以及类方法上。

当应用于类上时,表示该类的所有公共方法都需要被事务管理。

当应用于方法上时,表示该方法需要被事务管理。

  • 类级别:该类中的所有方法都会被事务管理。
  • 方法级别:仅该方法会被事务管理。
@Service
@Transactional
public class BookService {
    // 类中的所有方法都具有事务性
}

@Service
public class BookService {
    @Transactional
    public void updateBook(Book book) {
        // 只有这个方法具有事务性
    }
}
事务的基本概念

在使用 @Transactional 之前,了解事务的基本特性非常重要。事务具有以下四个基本属性(ACID):

  • 原子性(Atomicity):事务要么完全执行,要么不执行。
  • 一致性(Consistency):事务完成后,数据库必须处于一致的状态。
  • 隔离性(Isolation):事务的执行彼此独立,避免相互干扰。
  • 持久性(Durability):一旦事务提交,对数据库的更改是永久的。
`属性配置

@Transactional 注解支持多种属性配置,用于更精细地控制事务行为:

  • propagation(传播行为):定义事务方法如何与当前事务关联。常见的传播行为有:

    • REQUIRED(默认):如果有现成的事务,就加入这个事务;如果没有,则创建一个新事务。
    • REQUIRES_NEW:无论是否有现成的事务,都创建一个新的事务。
    • SUPPORTS:如果有现成的事务,就加入;如果没有,就以非事务方式执行。
    • NOT_SUPPORTED:不支持事务,如果有现成的事务,则暂停它。
    • MANDATORY:必须在现成事务中执行,否则抛出异常。
    • NEVER:必须在没有事务的环境中执行,否则抛出异常。
    • NESTED:如果有现成的事务,则在嵌套的事务中执行。
  • isolation(隔离级别):定义事务与其他事务之间的隔离程度。常见的隔离级别有:

    • DEFAULT:使用数据库默认的隔离级别。
    • READ_UNCOMMITTED:允许脏读、不可重复读和幻读。
    • READ_COMMITTED:不允许脏读,但允许不可重复读和幻读。
    • REPEATABLE_READ:不允许脏读和不可重复读,但允许幻读。
    • SERIALIZABLE:完全串行化的读,防止所有事务问题。
  • timeout(超时):事务的超时时间(以秒为单位)。如果超过这个时间,事务将自动回滚。
  • readOnly(只读):是否只读事务。如果设置为 true,则该事务不允许对数据库进行修改操作,优化查询性能。
  • rollbackForrollbackForClassName:指定哪些异常会导致事务回滚。
  • noRollbackFornoRollbackForClassName:指定哪些异常不会导致事务回滚。
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    timeout = 30,
    readOnly = false,
    rollbackFor = {SQLException.class, DataAccessException.class}
)
public void updateBook(Book book) {
    // 业务逻辑
}
使用场景与注意事项
  • 适用场景@Transactional 通常用于数据库操作较多的服务层(Service Layer),以确保数据操作的原子性和一致性。
  • 事务传播:选择合适的传播行为非常重要。例如,某些情况需要新建事务(REQUIRES_NEW),而有些情况需要加入现有事务(REQUIRED)。
  • 异常处理:默认情况下,Spring 仅对未检查的异常(运行时异常)进行事务回滚。如果需要对检查异常(如 SQLException)进行回滚,必须在 @Transactional 中指定 rollbackFor
  • 懒加载问题:在事务外访问懒加载的实体属性可能导致 LazyInitializationException。因此,确保事务管理的范围足够大,以覆盖所有需要的数据访问操作。
实现原理

@Transactional 的实现依赖于 Spring AOP(面向切面编程)。当一个带有 @Transactional 注解的方法被调用时,Spring 会创建一个代理对象,并在代理对象中添加事务管理逻辑:

  • 方法调用前:打开一个新的数据库连接,启动事务。
  • 方法执行中:执行实际的业务逻辑。
  • 方法调用后

    • 如果没有异常:提交事务。
    • 如果有异常:根据配置回滚事务。
常见问题与解决
  • No qualifying bean of type 错误:确保相关类(如 DataSourcePlatformTransactionManager)已正确配置,并在 Spring 容器中注册。
  • LazyInitializationException 错误:增加事务范围,确保事务在访问懒加载数据时依然活跃。
  • 事务失效问题@Transactional 注解的生效范围仅限于外部方法调用,因此不能在同一个类中直接调用带有 @Transactional 的方法。解决办法是通过代理类或者将事务方法提取到单独的类中。

示例代码

@Service
public class BookService {
    
    @Autowired
    private BookDao bookDao;

    @Transactional
    public void updateBookStock(int bookId, int quantity) {
        Book book = bookDao.getBookById(bookId);
        if (book.getStock() < quantity) {
            throw new RuntimeException("库存不足");
        }
        book.setStock(book.getStock() - quantity);
        bookDao.updateBook(book);
    }

    @Transactional(readOnly = true)
    public Book findBook(int id) {
        return bookDao.getBookById(id);
    }
}
包含所有 @Transactional 属性的调用演示
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BookService {

    // 自动注入DAO对象,用于数据库操作
    @Autowired
    private BookDao bookDao;

    /**
     * 演示使用所有属性的事务方法
     */
    @Transactional(
        propagation = Propagation.REQUIRED,          // 事务传播行为:如果有现存的事务,则加入;否则新建一个事务
        isolation = Isolation.REPEATABLE_READ,       // 事务隔离级别:防止脏读和不可重复读
        timeout = 30,                                // 事务超时时间:30秒
        readOnly = false,                            // 是否只读事务:否,允许数据修改
        rollbackFor = {RuntimeException.class, SQLException.class},  // 指定哪些异常会导致事务回滚
        noRollbackFor = {IllegalArgumentException.class}             // 指定哪些异常不会导致事务回滚
    )
    public void updateBookStock(int bookId, int quantity) {
        // 获取指定ID的图书对象
        Book book = bookDao.getBookById(bookId);
        
        // 如果库存不足,抛出异常,触发事务回滚
        if (book.getStock() < quantity) {
            throw new RuntimeException("库存不足");
        }
        
        // 更新图书库存
        book.setStock(book.getStock() - quantity);
        bookDao.updateBook(book);
    }

    /**
     * 演示只读事务
     */
    @Transactional(
        readOnly = true,               // 设置为只读事务,不允许对数据库进行修改操作
        propagation = Propagation.SUPPORTS, // 支持现有事务,如果没有事务则以非事务方式执行
        isolation = Isolation.READ_COMMITTED // 允许读取已提交的数据,防止脏读
    )
    public Book findBook(int id) {
        // 查找图书信息
        return bookDao.getBookById(id);
    }

    /**
     * 演示事务超时和特定异常回滚
     */
    @Transactional(
        timeout = 10,  // 设置事务超时时间为10秒
        rollbackFor = {CustomException.class},  // 仅在遇到自定义异常时回滚
        propagation = Propagation.REQUIRES_NEW, // 每次调用都创建一个新的事务
        isolation = Isolation.SERIALIZABLE      // 串行化隔离级别,最严格的隔离方式
    )
    public void processTransaction() throws CustomException {
        // 执行一些数据库操作
        try {
            // 假设进行某些复杂操作
            bookDao.someComplexOperation();
        } catch (SQLException e) {
            // 抛出自定义异常以触发回滚
            throw new CustomException("操作失败", e);
        }
    }
}

标签
没有标签

© 著作权归作者所有

本文由 趣代码Blog 创作,采用 知识共享署名4.0 国际许可协议进行许可,本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。

评论关闭