• Spring系列文章:Spring事务


    一、事务简述

    1、什么是事务( Transaction(tx))

    在⼀个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这 多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。

    多条DML要么同时成功,要么同时失败,这叫做事务。

    2、事务的四个处理过程

    第⼀步:开启事务 (start transaction)

    第⼆步:执⾏核⼼业务代码

    第三步:提交事务(如果核⼼业务处理过程中没有出现异常)(commit transaction)

    第四步:回滚事务(如果核⼼业务处理过程中出现异常)(rollback transaction)

    3、事务的四个特性:

    A 原⼦性:事务是最⼩的⼯作单元,不可再分。

    C ⼀致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变。

    I 隔离性:事务和事务之间因为有隔离性,才可以保证互不⼲扰。

    D 持久性:持久性是事务结束的标志

    二、Spring事务

    1、简述

    Spring实现事务的两种⽅式

    第一种:编程式事务

            事务功能的相关操作全部通过自己编写代码来实现:

    第二种:声明式事务

    既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出 来,进行相关的封装。 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。有基于注解⽅式 和基于XML配置⽅式

    Spring对事务的管理底层实现⽅式是基于AOP实现的。采⽤AOP的⽅式进⾏了封装。所以Spring专⻔针 对事务开发了⼀套API,API的核⼼接⼝如下:

    • PlatformTransactionManager接⼝:spring事务管理器的核⼼接⼝。在Spring6中它有两个实现:
    • DataSourceTransactionManager:⽀持JdbcTemplate、MyBatis、Hibernate等事务管理。 JtaTransactionManager:⽀持分布式事务管理。

    如果要在Spring6中使⽤JdbcTemplate,就要使⽤DataSourceTransactionManager来管理事务。 (Spring内置写好了,可以直接⽤。) 

    2、编程式事务

    事务功能的相关操作全部通过自己编写代码来实现:

    1. Connection conn = ...;
    2. try {
    3. // 开启事务:关闭事务的自动提交
    4. conn.setAutoCommit(false);
    5. // 核心操作
    6. // 提交事务
    7. conn.commit();
    8. }catch(Exception e){
    9. // 回滚事务
    10. conn.rollBack();
    11. }finally{
    12. // 释放数据库连接
    13. conn.close();
    14. }

    编程式的实现方式存在缺陷:

    细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。

    代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复 用。 

    3、声明式事务之注解方式

    3.1、配置

    依赖

    1. <dependency>
    2. <groupId>org.springframeworkgroupId>
    3. <artifactId>spring-contextartifactId>
    4. <version>5.3.1version>
    5. dependency>
    6. <dependency>
    7. <groupId>org.springframeworkgroupId>
    8. <artifactId>spring-ormartifactId>
    9. <version>5.3.1version>
    10. dependency>

    ⼀定要集成Log4j2⽇志框架,在⽇志信息中可以看到更加详细的信息。

     jdbc配置文件

    1. jdbc.user=root
    2. jdbc.password=root
    3. jdbc.url=jdbc:mysql://localhost:3306/ssm
    4. jdbc.driver=com.mysql.cj.jdbc.Driver

    spring配置文件

    1. <context:component-scan base-package="com.demo.spring.tx.annotation">
    2. context:component-scan>
    3. <context:property-placeholder location="classpath:jdbc.properties" />
    4. <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    5. <property name="url" value="${jdbc.url}"/>
    6. <property name="driverClassName" value="${jdbc.driver}"/>
    7. <property name="username" value="${jdbc.username}"/>
    8. <property name="password" value="${jdbc.password}"/>
    9. bean>
    10. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    11. <property name="dataSource" ref="druidDataSource"/>
    12. bean>
    13. <bean id="transactionManager"
    14. class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    15. <property name="dataSource" ref="dataSource">property>
    16. bean>
    17. <tx:annotation-driven transaction-manager="transactionManager" />

    添加注解 

    因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层添加注解@Transactional 

    @Transactional标识在方法上,只会影响该方法

    @Transactional标识的类上,会影响类中所有的方法

    接口BookDao:

    1. public interface BookDao {
    2. Integer getPriceByBookId(Integer bookId);
    3. void updateStock(Integer bookId);
    4. void updateBalance(Integer userId, Integer price);
    5. }

    实现类BookDaoImpl:

    1. @Repository
    2. public class BookDaoImpl implements BookDao {
    3. @Autowired
    4. private JdbcTemplate jdbcTemplate;
    5. @Override
    6. public Integer getPriceByBookId(Integer bookId) {
    7. String sql = "select price from t_book where book_id = ?";
    8. return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
    9. }
    10. @Override
    11. public void updateStock(Integer bookId) {
    12. String sql = "update t_book set stock = stock - 1 where book_id = ?";
    13. jdbcTemplate.update(sql, bookId);
    14. }
    15. @Override
    16. public void updateBalance(Integer userId, Integer price) {
    17. String sql = "update t_user set balance = balance - ? where user_id =
    18. ?";
    19. jdbcTemplate.update(sql, price, userId);
    20. }
    21. }

    接口BookService:

    1. public interface BookService {
    2. void buyBook(Integer bookId, Integer userId);
    3. }

    实现类BookServiceImpl:

    1. @Service
    2. public class BookServiceImpl implements BookService {
    3. @Autowired
    4. private BookDao bookDao;
    5. @Transactional
    6. @Override
    7. public void buyBook(Integer bookId, Integer userId) {
    8. //查询图书的价格
    9. Integer price = bookDao.getPriceByBookId(bookId);
    10. //更新图书的库存
    11. bookDao.updateStock(bookId);
    12. //更新用户的余额
    13. bookDao.updateBalance(userId, price);
    14. }
    15. }

    3.2、事务属性

    事务属性包括哪些

     事 务 中 的 重 点 属 性:

    • 事务传播⾏为
    • 事务隔离级别
    • 事务超时
    • 只读事务
    • 设置出现哪些异常 回 滚 事 务
    • 设 置 出 现 哪 些异常 不 回 滚 事 务
    事务 传 播 ⾏ 为

    什么是事务的传播⾏为? 在service类中有a()⽅法和b()⽅法,a()⽅法上有事务,b()⽅法上也有事务,当a()⽅法执⾏过程中调⽤了 b()⽅法,事务是如何传递的?合并到⼀个事务⾥?还是开启⼀个新的事务?这就是事务传播⾏为。

    事务传播⾏为在spring框架中被定义为枚举类型:

     ⼀共有七种传播⾏为:

    • REQUIRED:⽀持当前事务,如果不存在就新建⼀个(默认)【没有就新建,有就加⼊】
    • SUPPORTS:⽀持当前事务,如果当前没有事务,就以⾮事务⽅式执⾏【有就加⼊,没有就不管 了】
    • MANDATORY:必须运⾏在⼀个事务中,如果当前没有事务正在发⽣,将抛出⼀个异常【有就 加⼊,没有就抛异常】
    • REQUIRES_NEW:开启⼀个新的事务,如果⼀个事务已经存在,则将这个存在的事务挂起 【不管有没有,直接开启⼀个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务 被挂起】
    • NOT_SUPPORTED:以⾮事务⽅式运⾏,如果有事务存在,挂起当前事务【不⽀持事务,存在 就挂起】
    • NEVER:以⾮事务⽅式运⾏,如果有事务存在,抛出异常【不⽀持事务,存在就抛异常】
    • NESTED:如果当前正有⼀个事务在进⾏中,则该⽅法应当运⾏在⼀个嵌套式事务中。被嵌套 的事务可以独⽴于外层事务进⾏提交或回滚。如果外层事务不存在,⾏为就像REQUIRED⼀ 样。【有事务的话,就在这个事务⾥再嵌套⼀个完全独⽴的事务,嵌套的事务可以独⽴的提交 和回滚。没有事务就和REQUIRED⼀样。】

    在代码中设置事务的传播⾏为:

    @Transactional(propagation = Propagation.REQUIRED)

    事务隔离级别 

    数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事 务与其他事务隔离的程度称为隔离级别。

    SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同 的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

    数据库中读取数据存在的三⼤问题:(三⼤读问题)

    • 脏读:读取到没有提交到数据库的数据,叫做脏读。
    • 不可重复读:在同⼀个事务当中,第⼀次和第⼆次读取的数据不⼀样。
    • 幻读:读到的数据是假的。

    隔离级别一共有四种:

    • 读未提交:READ UNCOMMITTED、 允许Transaction01读取Transaction02未提交的修改。这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的 数据
    • 读已提交:READ COMMITTED、 要求Transaction01只能读取Transaction02已提交的修改。解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
    • 可重复读:REPEATABLE READ、 确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它 事务对这个字段进行更新。解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据⼀直都 是⼀样的。但存在幻读问题。
    • 串行化:SERIALIZABLE 、确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它 事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。解决了幻读问题,事务排队执⾏。不⽀持并发。

    各个隔离级别解决并发问题的能力

    各种数据库产品对事务隔离级别的支持程度

    使用,隔离级别在spring中以枚举类型存在:

    代码使用

    1. @Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
    2. @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
    3. @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
    4. @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
    5. @Transactional(isolation = Isolation.SERIALIZABLE)//串行化

    只读

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

    1. @Transactional(readOnly = true)
    2. public void buyBook(Integer bookId, Integer userId) {
    3. //查询图书的价格
    4. Integer price = bookDao.getPriceByBookId(bookId);
    5. //更新图书的库存
    6. bookDao.updateStock(bookId);
    7. //更新用户的余额
    8. bookDao.updateBalance(userId, price);
    9. //System.out.println(1/0);
    10. }

    对增删改操作设置只读会抛出下面异常: Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed 

    该特性的作⽤是:启动spring的优化策略。提⾼select语句执⾏效率。

    如果该事务中确实没有增删改操作,建议设置为只读事务

    超时

    事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间 占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。

    此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常 程序可以执行。

    概括来说就是一句话:超时回滚,释放资源。

    1. @Transactional(timeout = 3)
    2. public void buyBook(Integer bookId, Integer userId) {
    3. try {
    4. TimeUnit.SECONDS.sleep(5);
    5. } catch (InterruptedException e) {
    6. e.printStackTrace();
    7. }
    8. //查询图书的价格
    9. Integer price = bookDao.getPriceByBookId(bookId);
    10. //更新图书的库存
    11. bookDao.updateStock(bookId);
    12. //更新用户的余额
    13. bookDao.updateBalance(userId, price);
    14. //System.out.println(1/0);
    15. }

    执行过程中抛出异常: org.springframework.transaction.TransactionTimedOutException

    以上代码表示设置事务的超时时间为3秒。 表示超过3秒如果该事务中所有的DML语句还没有执⾏完毕的话,最终结果会选择回滚。

    默认值-1,表示没有时间限制。

    这⾥有个坑,事务的超时时间指的是哪段时间? 在当前事务当中,最后⼀条DML语句执⾏之前的时间。如果最后⼀条DML语句后⾯很有很多业务逻辑, 这些业务代码执⾏的时间不被计⼊超时时间。

    以下代码的超时不会被计⼊超时时间

    1. @Transactional(timeout = 10) // 设置事务超时时间为10秒。
    2. public void save(Account act) {
    3. accountDao.insert(act);
    4. // 睡眠⼀会
    5. try {
    6. Thread.sleep(1000 * 15);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. }

    当然,如果想让整个⽅法的所有代码都计⼊超时时间的话,可以在⽅法最后⼀⾏添加⼀⾏⽆关紧要的 DML语句。 

    设置哪些异常回滚事务、哪些异常不回滚事务

    声明式事务默认只针对运行时异常回滚,编译时异常不回滚。 可以通过@Transactional中相关属性设置回滚策略

    rollbackFor属性:需要设置一个Class类型的对象

    rollbackForClassName属性:需要设置一个字符串类型的全类名

    noRollbackFor属性:需要设置一个Class类型的对象

    rollbackFor属性:需要设置一个字符串类型的全类名

    1. @Transactional(noRollbackFor = ArithmeticException.class)
    2. //@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
    3. public void buyBook(Integer bookId, Integer userId) {
    4. //查询图书的价格
    5. Integer price = bookDao.getPriceByBookId(bookId);
    6. //更新图书的库存
    7. bookDao.updateStock(bookId);
    8. //更新用户的余额
    9. bookDao.updateBalance(userId, price);
    10. System.out.println(1/0);
    11. }

    虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当 出现ArithmeticException不发生回滚,因此购买图书的操作正常执行 

    表示只有发⽣RuntimeException异常或该异常的⼦类异常才回滚。

    @Transactional(rollbackFor = RuntimeException.class)

    3.3、 事务的全注解式开发

    编写⼀个类来代替配置⽂件,代码如下:

    1. @Configuration
    2. @ComponentScan("com.demo.bank")
    3. @EnableTransactionManagement
    4. public class Spring6Config {
    5. @Bean
    6. public DataSource getDataSource(){
    7. DruidDataSource dataSource = new DruidDataSource();
    8. dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    9. dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
    10. dataSource.setUsername("root");
    11. dataSource.setPassword("root");
    12. return dataSource;
    13. }
    14. @Bean(name = "jdbcTemplate")
    15. public JdbcTemplate getJdbcTemplate(DataSource dataSource){
    16. JdbcTemplate jdbcTemplate = new JdbcTemplate();
    17. jdbcTemplate.setDataSource(dataSource);
    18. return jdbcTemplate;
    19. }
    20. @Bean
    21. public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
    22. DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
    23. dataSourceTransactionManager.setDataSource(dataSource);
    24. return dataSourceTransactionManager;
    25. }
    26. }

    4、声明式事务之xml方式

    基于xml实现的声明式事务,必须引入aspectJ的依赖

    1. <dependency>
    2. <groupId>org.springframeworkgroupId>
    3. <artifactId>spring-aspectsartifactId>
    4. <version>5.3.1version>
    5. dependency>

    applicationContext.xml配置 

    1. "1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xmlns:context="http://www.springframework.org/schema/context"
    5. xmlns:tx="http://www.springframework.org/schema/tx"
    6. xmlns:aop="http://www.springframework.org/schema/aop"
    7. xsi:schemaLocation="http://www.springframework.org/schema/beans
    8. http://www.springframework.org/schema/beans/spring-beans.xsd
    9. http://www.springframework.org/schema/context
    10. http://www.springframework.org/schema/context/spring-context.xsd
    11. http://www.springframework.org/schema/tx
    12. http://www.springframework.org/schema/tx/spring-tx.xsd
    13. http://www.springframework.org/schema/aop
    14. http://www.springframework.org/schema/aop/spring-aop.xsd">
    15. <context:component-scan base-package="com.demo"/>
    16. <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    17. <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    18. <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
    19. <property name="username" value="root"/>
    20. <property name="password" value="root"/>
    21. bean>
    22. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    23. <property name="dataSource" ref="dataSource"/>
    24. bean>
    25. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    26. <property name="dataSource" ref="dataSource"/>
    27. bean>
    28. <tx:advice id="txAdvice" transaction-manager="transactionManager">
    29. <tx:attributes>
    30. <tx:method name="get*" read-only="true"/>
    31. <tx:method name="query*" read-only="true"/>
    32. <tx:method name="find*" read-only="true"/>
    33. <tx:method name="save*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Throwable"/>
    34. <tx:method name="del*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Throwable"/>
    35. <tx:method name="update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Throwable"/>
    36. <tx:method name="transfer*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Throwable"/>
    37. tx:attributes>
    38. tx:advice>
    39. <aop:config>
    40. <aop:pointcut id="txPointcut" expression="execution(* com.demo.service..*(..))"/>
    41. <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    42. aop:config>
    43. beans>

    事务注解 @Transactional 失效的3种场景及解决办法

    事务注解 @Transactional 失效的3种场景及解决办法_Hollis Chuang的博客-CSDN博客

  • 相关阅读:
    1.vue3脚手架在vscode下面建立
    东南亚电商指南,卖家如何布局东南亚市场?
    学生管理系统详细架构
    【培训课程专用】中断路由代码导读:当cpu运行在TEE来了一个Non secure Group1中断
    backtrace输出任意线程栈信息
    嵌入式开发笔记:STM32的外设GPIO知识学习
    详解风控模型中的逻辑回归评分卡与模型评估内容
    vue+elementUI 使用腾讯地图
    软件项目管理期末复习---项目立项
    Node.js躬行记(20)——KOA源码分析(下)
  • 原文地址:https://blog.csdn.net/qq_34491508/article/details/132693025