前言
在互联网数据库的使用中,对于那些电商和金融网站,最关注的内容毫无疑问就是数据库事务,因为对于人们商品的交易和库存以及金融产品的金额,是不允许发生错误的。但是它们面临的问题是,热门产品或金融产品上线销售瞬间可能面对的高并发的场景。那么在Spring中采用哪些事务机制处理这些高并发场景的呢?
Spring事务管理
常见的Spring事务管理有以下两种:
1. 编程式事务管理
- 编程式事务管理需要显示的调用beginTransaction()、rollback()、commit()等相关事务的处理方法,操作比较复杂,不推荐。
2.声明式事务管理
- 基于AOP技术实现的声明式事务管理,实质就是:在方法执行前后进行拦截,然后在目标方法执行前创建并加入事务,在目标方法执行后根据结果进行事务的回滚或者提交。
- 声明式事务管理的实现有两种:
- 基于XML配置文件的实现;
2.在业务方法上加上@Transactional注解,将事务规则应用到业务逻辑中。
对于声明式事务,是使用@Transactional进行标注的,这个注解可以标注在类或方法上,当它标注在类上时,代表这个类的所有公共的(public)非静态的方法都将启动事务功能。在@Transactional中,还允许配置许多的属性,如事务的传播行为和隔离级别,以及异常类型,从而确定方法发生什么异常下回滚事务什么异常下不回滚事务等。这些配置内容,是在Spring IoC容器在加载时就会将这些配置信息解析出来,然后把这些信息存到事务定义器(TransactionDefinition接口的实现类)里,记录哪些类或者方法需要启动事务功能,采取什么策略去执行事务。在这个过程中,我们需要做的就是给需要事务的类或方法中加上@Transactional注解和配置其属性而已。
有了@Transactional的配置,Spring就会知道在哪里启动事务机制,其约定流程如下图所示:
3.隔离级别
当前互联网应用时刻面临着高并发的环境,如商品库存,时刻都是多个线程共享的数据,这样就会在多线程的环境中扣减商品库存。对于数据库而言,就会出现多个事务同时操作同一记录的情况,这样会引起数据出现不一致的情况,便是数据的丢失更新(Lost Update)问题。
数据库事务的知识
数据库事务具有以下4个基本特性,也就是著名的ACID。
- Atomic(原子性):事务中包含的操作被看做一个整体的业务单元,这个业务单元的操作要么全部成功,要么全部失败,不会出现部分成功,部分失败的现象。
- Consistency(一致性):事务在完成时,必须使所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性,
- Isolation(隔离性):可能多个应用程序线程同时访问同一数据,这样数据库同样的数据在各个不同的事务中被访问,这样会产生丢失更新,为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制数据丢失更新的产生,因为互联网的应用常常面对高并发的场景,所以隔离性是需要掌握的重点内容。
- Durability(持久性):事务结束后,所有的数据都会固定到一个地方,如保存到磁盘中,即使断电重启后也可以提供给应用程序访问。
第一类丢失更新
时刻 | 事 务 1 | 事 务 2 |
---|---|---|
T1 | 初始库存100 | 初始库存100 |
T2 | 扣减库存,余99 | —— |
T3 | —— | 扣减库存,余99 |
T4 | 提交事务,库存变为99 | |
T5 | 回滚事务,库存100 |
可以看到,T5时刻事务回滚,导致原本库存为99的变为了100,显然事务2的结果就丢失了,这就是一个错误的值,类似的,对于这样一个事务提交而引发的数据不一致的情况,我们称为第一类丢失更新。
第二类丢失
时刻 | 事 务 1 | 事 务 2 |
---|---|---|
T1 | 初始库存100 | 初始库存100 |
T2 | 扣减库存,余99 | —— |
T3 | —— | 扣减库存,余99 |
T4 | —— | 提交事务,库存为99 |
T5 | 提交事务,库存变为99 | —— |
注意在T5时刻提交的事务。因为在事务1中,无法感知事务2 的操作,这样它就不知道事务2已经做了修改,因此它依旧认为这只是发生了一笔业务,所以库存更新变成了99,而这个结果又是一个错误的结果。这样T5时刻事务1提交的事务,就会引发事务2提交结果的丢失,我们把这样的多个事务的提交引发丢失更新的称为第二类丢失更新。
为处理第二类丢失更新引发的错误,提出了事务的隔离级别,常见的隔离级别有以下4种:
- 未提交读
- 读写提交
- 可重读
- 串行化
下面我们来一一细说这四个隔离级别的作用
1.未提交读
未提交读(read uncommitted)是最低的得力级别,其含义是允许一个事务读取另一个事务没有提交的数据。未提交读是一种危险的隔离级别。所以我们一般在实际的开发中应用不广,但是它的优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景,它的最大坏处是出现脏读。
脏读现象
时刻 | 事 务 1 | 事 务 2 | 备 注 |
---|---|---|---|
T0 | ....... | ........ | 商品库存初始化为2 |
T1 | 读取库存为2 | ||
T2 | 扣减库存 | 库存为1 | |
T3 | 扣减库存 | 库存为0,读取事务1为提交的数据 | |
T4 | 提交事务 | 库存保存为0 | |
T5 | 回滚事务 | 因为第一类丢失更新已经克服,所以不会回滚为2,库存为0,结果错误 |
因为采用未提交读,所以事务2可以读取事务1为提交的库存数据为1,这里当它扣减库存后则数据为0,然后它提交了事务,库存就变成了0,而事务1在T5时刻回滚事务,因为第一类丢失更新已经被克服,所以它不会将库存回滚到2,那么最后的结果就变成了0,所以就出现了这样的错误,脏读一般是比较危险的隔离级别,在我们实际应用中采用的不多。