事务处理和隔离级别

@Transactional

上一章节我们提到了Spring的AOP,其中所举的例子就是数据库事务处理。原本的数据库事务操作是繁杂而充斥冗余的,但Spring为我们引入了“切面”的思想,从而分离了变与不变的部分。

当然,像上一节那样自己定义一个MyAspect仍然显得不够简洁,为此,Spring提供了一个注解@Transactional。你只需要在service类的业务方法上增加该注解,Spring就能自动地帮我们完成该方法的事务操作。

是不是很简单呢?如果不信,可以来验证一下。由于引入MyBatis依赖,SpringBoot可以自动地创建事务管理、MyBatis的SqlSessionFactory和SqlSessionTemplate等内容,他们作为bean存储在IoC容器中。我们可以在SpringBoot的启动类中创建一个PlatformTransactionManager类型的事务管理器对象,通过监控该对象来查看事务处理机制。具体操作这里就不详细阐述了。

四种隔离级别

为了应付实际应用中更复杂的并发问题,我们要讨论事务的隔离级别。我们知道,事务有四个特性:ACID,分别是原子性、一致性、隔离性、持久性。

其中,隔离性是在高并发场景下需要关注的重点内容。例如,电商系统中有一款商品被疯狂抢购,这时会出现多个事务同时访问商品库存的场景,这样会产生丢失更新。

我们只讨论一种情况,即两个事务同时提交时,某个提交的结果会使其余提交的结果丢失。如下图所示,两个事务都提交后,库存仅从100降至99。
事务处理和隔离级别
为了在不同程度上压制丢失更新问题,数据库提出了4类隔离级别的概念:未提交读、读写提交、可重复读、串行化。首先需要明白,我们分级别地压制而不是彻底解决丢失更新,是因为要在性能和一致性之间做tradeoff,尽量保证用户体验。在现实中,我们往往只需要将隔离级别设置在读写提交(Oracle默认)或者可重复读(Mysql默认)。

未提交读

未提交读,允许一个事务读取另一个事务尚未提交的数据,这就会出现“脏读”,是一种危险的隔离级别。A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。它只适用于对数据一致性没有要求而单纯追求高并发的场景。

读写提交

读写提交,是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。读写提交避免了脏读,但还是有所缺陷,那就是“不可重复读”。

比如说,某活动还剩最后一个名额,报名事务A和事务B都在执行报名流程,最初两事务都检测到还剩一个名额,但是当事务A提交结束后,还在执行中的事务B发现剩余名额为0。也就是说,前后读取的数据内容不一致,这就是不可重复读。

可重复读

为了解决不可重复读的问题,隔离级别又增加了可重复读。

以上述活动报名为例,事务A先行读取报名名额,即阻塞事务B的执行。唯有当事务A提交完毕,事务B才可获取剩余名额。这样,就避免了两次读取值不同的问题,也即“不可重复读”的问题。

但是,可重复读会可能会引发“幻读”。所谓幻读,针对的是多条数据,事务A之前读取某个表中记录的总数是N,后事务B插入了一条记录,导致实际输出的记录变成了N+1,仿佛是产生幻觉一般。

串行化

串行化是数据库隔离的*别,它要求每条SQL都按顺序执行,从而避免了“脏读”“不可重复读”“幻读”的问题,保证了数据的一致性。

隔离级别与@Transactional

回到Spring Boot,我们如何在实际开发中设置隔离级别呢?
方法一:只要在Transactional的参数isolation中设置即可。
方法二:在application.properties文件中加入默认配置。