什么是基于注解的声明式事务
基于注解的声明式事务是Spring框架提供的一种事务管理机制,它允许开发者以声明的方式指定哪些方法应该在事务边界内执行,而不是通过编程方式显式地管理事务开始和结束。这种机制极大地简化了事务管理代码,使得业务逻辑更加清晰,同时也提高了代码的可维护性和可读性。
在Spring中,声明式事务主要通过@Transactional
注解来实现。这个注解可以被应用在以下几个地方:
- 方法级:直接将
@Transactional
注解添加到方法签名上,这样每次调用该方法时,Spring都会确保在一个事务上下文中执行该方法。 - 类级:如果一个类的所有公共方法都需要相同的事务属性,可以在类级别上应用
@Transactional
。这样,类中的所有方法都会继承这些事务属性,除非某个特定方法上另有声明。 - 接口级:类似于类级别的应用,如果接口上的所有方法都应具有相同的事务行为,可以在接口上应用
@Transactional
。
@Transactional
注解支持多种属性,包括但不限于:
- propagation:指定事务的传播行为,比如是否支持现有事务或创建新的事务。
- isolation:指定事务的隔离级别,如读未提交、读已提交、可重复读或序列化。
- readOnly:指定事务是否只读,这可以影响数据库的优化策略。
- timeout:指定事务的超时时间。
- rollbackFor:指定哪些类型的异常会导致事务回滚。
- noRollbackFor:指定哪些类型的异常不会导致事务回滚。
当使用基于注解的声明式事务时,还需要确保Spring配置中启用了事务管理器,并且将@EnableTransactionManagement
注解添加到配置类中,或者在XML配置中使用<tx:annotation-driven>
元素。这样,Spring才能识别和处理@Transactional
注解。
准备工作
导入相关依赖
创建jdbc.properties
配置Spring的配置文件,tx-annotation.xml
创建数据库
创建Service接口
创建Service实现层
创建dao接口
创建dao的是实现层
控制类
创建一个测试类
不加事务的时候进行
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书用户余额为50,而图书价格为80,注意这里,用户的额度只有50,但是一本书需要80, 50 - 80 = -30 ,由于数据库余额字段设置为无符号,会报错。
原因,购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段此时执行sql语句会抛出SQLException。
但是我们去看数据库,这个时候数据库会有一定的问题。
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新,显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败。
完善上述操作:
在tx-annotation.xml文件中添加事务的启动,注意添加事务的时候,需要看你插入的事务需要注意下,这里避免出现事情,故而我给出完整的xml配置。
启用了事务,我们就需要service层佳航我们的事务注解。
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理在BookServiceImpl的buybook()添加注解@Transactional
@Transactional
标识在方法上,咋只会影响该方法@Transactional
标识的类上,咋会影响类中所有的方法
看这是我们之前没加事务之前的结果,数据库的stock减少了1,如果我们没有加事务之前,那么这个结果stock会变成98,但是我们加上事务之后,正常情况想,只有在上述代码所有成功才能完成修改,否则不予修改。
我们在执行一次测试类查看,结果如下:
数据无变动,说明事务生效了。由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
事务的属性
本篇开始之前我们讲了事务的属性有如下:@Transactional
注解支持多种属性,包括但不限于:
- propagation:指定事务的传播行为,比如是否支持现有事务或创建新的事务。
- isolation:指定事务的隔离级别,如读未提交、读已提交、可重复读或序列化。
- readOnly:指定事务是否只读,这可以影响数据库的优化策略。
- timeout:指定事务的超时时间。
- rollbackFor:指定哪些类型的异常会导致事务回滚。
- noRollbackFor:指定哪些类型的异常不会导致事务回滚。
ReadOnly属性
超时timeout
事务的回滚策略
可以通过@Transactional中相关属性设置回滚策略
- rollbackFor属性:需要设置一个Class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名
- noRollbackFor属性:需要设置一个Class类型的对象
- rollbackFor属性:需要设置一个字符串类型的全类名
使用方式
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行.
事务的隔离级别(isolation)
- DEFAULT(默认):使用数据库默认的事务隔离级别,通常为数据库的默认级别(如MySQL的REPEATABLE READ)。
- READ_UNCOMMITTED(读未提交):允许事务读取未提交的数据更改,可能导致脏读、不可重复读和幻读问题。
- READ_COMMITTED(读已提交):确保一个事务只能读取到已提交的数据,可以避免脏读,但仍可能出现不可重复读和幻读问题。
- REPEATABLE_READ(可重复读):确保事务可以多次读取相同的数据,并且在事务执行期间其他事务对数据的修改不会影响到该事务,可以避免脏读和不可重复读,但仍可能出现幻读问题。
- SERIALIZABLE(串行化):最高的隔离级别,确保事务可以完全隔离,避免脏读、不可重复读和幻读,但可能导致性能下降。
各个隔离级别解决并发问题的能力见下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
READ | UNCOMMITTED | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
各种数据库产品对事务隔离级别的支持程度:
隔离级别 | Oracle | MySQL |
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
事务的传播行为(propagation)
- REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式执行。
- MANDATORY:强制要求当前存在事务,如果没有事务,则抛出异常。
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则将当前事务挂起。
- NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在事务,则将当前事务挂起。
- NEVER:以非事务的方式执行操作,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新的事务。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
创建接口CheckoutService:
创建实现类CheckoutServiceImpl:
在BookController中添加方法:
在数据库中将用户的余额修改为100元
可以通过@Transactional
中的propagation
属性设置事务传播行为修改BookServiceImpl
中buyBook()
上,注解@Transactional
的propagation
属性。
此时运行代码如下:
@Transactional(propagation = Propagation.REQUIRED)
,默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。
经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了。@Transactional(propagation = Propagation.REQUIRES_NEW)
,表示不管当前线程上是否有已经开启的事务,都要开启新事务。
同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。