一、事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功。
二、事务的分类
1、编程式事务管理:
将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式管理事务中,必须在每个事务操作中包含额外的事务管理代码。
2、声明式事务管理(常用):
大多数情况下比编程式事务管理更好用,它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理,Spring声明式事务管理建立在AOP基础之上,是一个典型的横切关注点,通过环绕增强来实现,其原理是对方法前后进行拦截,然后在目标方法开始之前创建或加入一个事务,在执行完毕之后根据执行情况提交或回滚事务。
代码模型:
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//开启事务
return joinPoint.proceed();
//提交事务
} catch (Throwable e) {
//回滚事务
throw e;
}finally {
//释放资源
}
}
三、spring中的事务
①.spring事务的配置
1、配置spring事务管理器
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
2.启用spring事务注解
<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
3.在相应的service层的public方法上部配置注解(即@Transactional)
注:1.此处需要用到spring-aspects-4.3.10.RELEASE.jar包。2.一个类含有@Transactional注解修饰的方法,则Spring框架自动为该类创建代理对象,默认使用JDK创建代理对象,可以通过添加<aop:aspectj-autoproxy proxy-target-class="true"/>使用CGLib创建代理对象,此时需要添加aspectjweaver-x.x.x.jar包,具体代码参见《@Transactional注解》Java工程。3.不能在protected、默认或者private的方法上使用@Transactional注解,否则无效。
②.@Transactional注解属性
1.rollbackFor和rollbackForClassName
用于指定对哪些异常进行回滚,一般来说在spring中对于运行时异常进行回滚,但是若出现了检查时异常则不进行回滚。这种处理方式是大多数开发者希望的处理方式,也是 EJB 中的默认处理方式;但可以根据需要人为控制事务在抛出某些运行时异常时仍然提交事务,或者在抛出某些检查时异常时回滚事务。
示例:
|
书籍表中有50本书籍,每本书10元,一个人钱包有1元,欲买50本,则该行代码抛出MoneyException异常,但由于该异常为运行时异常,所以回滚事务,即左边图片“bookDao.update(bookId, count);”行代码执行失效! |
示例:
|
书籍表中有50本书籍,每本书10元,一个人钱包有1元,欲买50本,则该行代码抛出MoneyException异常,但由于该异常为检查时异常,所以依然提交事务,即左边图片“bookDao.update(bookId, count);”行代码执行生效! |
示例:
|
书籍表中有50本书籍,每本书10元,一个人钱包有1元,欲买50本,则该行代码抛出MoneyException异常,尽管该异常为检查时异常,但由于@Transactional注解中添加了rollbackFor=MoneyException.class,所以回滚事务,即左边图片“bookDao.update(bookId, count);”行代码执行失效! |
注意:上例红框中代码不能try-catch处理异常,否则即便@Transactional注解中添加了rollbackFor=MoneyException.class,事务也不会回滚,如下代码:
|
书籍表中有50本书籍,每本书10元,一个人钱包有1元,欲买50本,则该行代码抛出MoneyException异常,尽管该异常为检查时异常,且@Transactional注解中添加了rollbackFor=MoneyException.class,但由于红框代码已经通过try-catch处理了异常,所以事务不回滚,即左边图片“bookDao.update(bookId, count);”行代码执行生效! |
2.readonly,即事务只读
通俗的来说就是只允许在被加上含有readonly=true的方法上进行进行读取数据的操作,而修改数据的操作将被禁止。下面看一下定义。事务只读,指对事务性资源进行只读操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源、 JMS 资源,以及自定义的事务性资源等等。如果确定只对事务性资源进行只读操作,那么可以将事务标志为只读的,以提高事务处理的性能。在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
|
@Transactional注解中添加了readOnly=true,但@Transactional注解修饰的方法涉及数据的修改,因此抛出如下异常: Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed |
3.timeout,设置事务最长执行时间单位为秒,如果超过这个时间,则事务自动回滚,并出现org.springframework.transaction.TransactionTimedOutException异常
注意:1.事务的开始往往会发生数据库的表锁或者被数据库优化为行锁,如果允许时间过长,那么这些数据会一直被锁定,最终影响系统的并发性,因此可以给这些事务设置超时时间以规避该问题。2.由于超时是在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
4.propagation:指定事务传播行为,一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播,例如:方法可能继承在现有事务中运行,也可能开启一个新事物,并在自己的事务中运行。Spring定义了如下7种事务传播行为:1.REQUIRED:默认值,如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行,如果一个加有事务注解的方法运行两个都加有事务注解(propagation为默认值)的方法,则按照一个事务来处理 2.REQUIRES_NEW:当前方法必须启动新事务,并在它自己的事务内运行,如果有事务在运行,则把当前事务挂起,直到新的事务提交或者回滚才恢复执行,也就是说如果一个加有事务注解的方法中运行两个及两个以上的加有(propagation=REQUIRES_NEW)的方法则会生成两个及两个以上的事务来运行。3.SUPPORTS:如果有事务在运行,当前的方法就在这个事务内运行,否则以非事务的方式运行;4.NOT_SUPPORTED:当前的方法不应该运行在事务中,如果有运行的事务,则将它挂起;5.NEVER:当前方法不应该运行在事务中,否则将抛出异常;6.MANDATORY(强制的):当前方法必须运行在事务内部,否则将抛出异常;7.NESTED(嵌套):如果有事务在运行,当前的方法在这个事务的嵌套事务内运行,否则就启动一个新的事务,并在它自己的事务内运行,此时等价于REQUIRED。注意:对于NESTED内层事务而言,内层事务独立于外层事务,可以独立递交或者回滚,如果内层事务抛出的是运行异常,外层事务进行回滚,内层事务也会进行回滚。
5.isolation
指定事务隔离级别,Spring定义了如下5种事务隔离级别:
DEFAULT:默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常为READ_COMMITTED。
READ_UNCOMMITTED:表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别可能出现脏读、不可重复读或幻读,因此很少使用该隔离级别。
READ_COMMITTED:表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,但可能出现不可重复读或幻读,这也是大多数情况下的推荐值。
REPEATABLE_READ:表示一个事务在整个过程中可以多次重复执行某个查询,且每次返回的记录都相同,除非数据被当前事务自生修改。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读,但可能出现幻读。
SERIALIZABLE:表示所有的事务依次逐个执行,事务之间互不干扰,该级别可以防止脏读、不可重复读和幻读,但是这将严重影响程序的性能,因此通常情况下也不会用到该级别。
下面对于这些属性值进行解释
1.READ_UNCOMMITTED即读取还未提交的数据,也就是说若事务中对数据进行多次读写操作,那么在READ_UNCOMMITTED状态下,读取的数据是会随着事务操作数据变更的状态而改变,与事务是否提交无关。
2.READ_COMMITTED即读取已经提交的数据,也就是说无论数据在事务中怎样变更,只有当事务提交后,读取到的数据才会发生变更,事务没有提交,读取的一直是之前的值。
3.REPEATABLE_READ表示一个事务在整个过程中可以多次重复执行某个查询,且每次返回的记录都相同,除非数据被当前事务自生修改。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读,但可能出现幻读。
4.SERIALIZABLE:表示所有的事务依次逐个执行,事务之间互不干扰,该级别可以防止脏读、不可重复读和幻读,但是这将严重影响程序的性能,因此通常情况下也不会用到该级别。
下面给出一个表形象的表示这些属性值的功能:
|
给出几个例子来解释这些属性:
脏读(Drity Read): 已知有两个事务A和B, A读取了已经被B更新但还没有被提交的数据,之后,B回滚事务,A读取的数据就是脏数据。
场景:公司发工资了,领导把5000元打到Tom的账号上,但是该事务并未提交,而Tom正好去查看账户,发现工资已经到账,账户多了5000元,非常高兴,可是不幸的是,领导发现发给Tom的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,Tom再次查看账户时发现账户只多了2000元,Tom空欢喜一场,从此郁郁寡欢,走上了不归路…... 分析:上述情况即为脏读,两个并发的事务:“事务B:领导给Tom发工资”、“事务A:Tom查询工资账户”,事务A读取了事务B尚未提交的数据。 |
不可重复读(Non-repeatable read):已知有两个事务A和B,A 多次读取同一数据,B 在A多次读取的过程中对数据作了修改并提交,导致A多次读取同一数据时,结果不一致,例子:
场景:Tom拿着工资卡去消费,酒足饭饱后在收银台买单,服务员告诉他本次消费1000元,Tom将银行卡给服务员,服务员将银行卡插入POS机,POS机读到卡里余额为3000元,就在Tom磨磨蹭蹭输入密码时,他老婆以迅雷不及掩耳盗铃之势把Tom工资卡的3000元转到自己账户并提交了事务,当Tom输完密码并点击“确认”按钮后,POS机检查到Tom的工资卡已经没有钱,扣款失败,Tom十分纳闷,明明卡里有钱,于是怀疑POS有鬼,和收银小姐姐大打出手,300回合之后终因伤势过重而与世长辞,Tom老婆痛不欲生,郁郁寡欢,从此走上了不归路...... 分析:上述情况即为不可重复读,两个并发的事务,“事务A:POS机扣款”、“事务B:Tom的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新数据并提交了事务,而事务A再次读取该数据扣款时,数据已经发生了改变。
场景:Tom拿着工资卡去消费时,一旦POS机读取工资卡信息(即事务开始),Tom老婆即便进行了转账,待Tom输入密码并点击“确认”按钮后,POS机检查到Tom工资卡上余额没有变化,最终扣款成功。 分析:上述情况即为重复读 |
场景:Tom的老婆工作在银行部门,她时常通过银行内部系统查看Tom的工资卡消费记录。2019年5月的某一天,她查询到Tom当月工资卡的总消费额(select sum(amount) from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05')为80元,Tom的老婆非常吃惊,心想“老公真是太节俭了,嫁给他真好!”,而Tom此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录并提交了事务,沉浸在幸福中的老婆查询了Tom当月工资卡消费明细(select amount from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05')一探究竟,可查出的结果竟然发现有一笔1000元的消费,Tom的老婆瞬间怒气冲天,外卖订购了一个大号的榴莲,傍晚降临,Tom生活在了水深火热之中,只感到膝盖针扎的痛...... 分析:上述情况即为幻读,两个并发的事务,“事务A:获取事务B消费记录”、“事务B:添加了新的消费记录”,事务A获取事务B消费记录时数据多出了一条。 场景:教师A执行SQL语句将数据库中所有学生的成绩从具体分数改为ABCDE等级制,该SQL语句执行后事务提交前,教师B插入了一条具体分数的记录并提交了事务,教师A事务提交并执行查询SQL语句,此时教师A发现还有一条记录没有改过来,就好像发生了幻觉一样。 |
幻读(Phantom Read): 已知有两个事务A和B,A从一个表中读取了数据,然后B在该表中插入了一些新数据,导致A再次读取同一个表, 就会多出几行,简单地说,一个事务中先后读取一个范围的记录,但每次读取的纪录数不同,称之为幻象读,例子:
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。