事务在整个开发框架中是一个非常常用的功能,特别涉及到数据库操作。像Mysql,就有4个数据库级别:
(1) READ UNCOMMITTED(读未提交):允许读取未提交的数据。这种级别的事务可以读取到其他事务未提交的数据,可能会导致脏读、不可重复读和幻读等问题。
(2)READ COMMITTED(读已提交):只能读取已经提交的数据。这种级别的事务可以避免脏读,但可能会出现不可重复读和幻读等问题。
(3)REPEATABLE READ(可重复读):保证在同一个事务中多次读取同一份数据时,得到的结果是一致的。这种级别的事务可以避免脏读和不可重复读,但可能会出现幻读等问题。
(4)SERIALIZABLE(串行化):强制事务串行执行,避免了所有并发问题。这种级别的事务可以避免所有并发问题,但可能会导致性能问题。
事务级别从低到高,性能越来越差。通过不同的应用场景,选择不同的事务。
(1) READ UNCOMMITTED(读未提交):对数据一致性要求不高的,比如 统计分析
(2)READ COMMITTED(读已提交):对数据一致性要求较高,比如,金融计费系统
(3)REPEATABLE READ(可重复读):对数据一致性要求非常高,比如银行
(4)SERIALIZABLE(串行化):对数据一致性要求最高,这个也是银行应用比较多
接下来我们介绍,脏读,不可重复读,幻像读,第一类更新丢失,第二类更新丢失
脏读
时间序 | 转账事务甲 | 取款事务乙 |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询到账户有100元 | |
4 | 取出50,余额修改为50元 | |
5 | 查询到余额为50元 | |
6 | 撤销事务,余额改回100元 | |
7 | 存入10元,余额为60元 | |
8 | 事务提交 |
发生在第5步的就是脏读,由于甲读到乙未有提交的数据,造成脏读,后面乙又撤回了事务,把本该取的50元退了回去,但是甲由于读到了脏数据,并且根据脏数据存入了10元,这样就造成了账户白白丢失了50元。
不可重复读
时间序 | 取款事务甲 | 转账事务乙 |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询到账户有100元 | |
4 | 查询到账户有100元 | |
5 | 取出10元,余额为90元 | |
6 | 提交事务 | |
7 | 查询余额为90元,与第4步的不一致 |
在同一个事务中,4,7时间序读出的余额都不一样。
幻像读
时间序 | 统计事务甲 | 转账事务乙 |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 统计总账户有100元 | |
4 | 现在增加一个账户,存入10元 | |
5 | 提交事务 | |
6 | 再次统计是110元 |
3,6查询出来的数据金额不一致。这样就出现了幻像读。幻像读和不可重复读感觉上好像一样,但是不可重复读是由于数据的更新造成的,要避免的话,直接加上行锁就可以。但是幻像读是由于数据新增造成的,对于这种问题,需要加上表锁。
第一类更新丢失
时间序 | 取款事务甲 | 转账事务乙 |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询到账户有100元 | |
4 | 查询到账户有100元 | |
5 | 汇入10元,余额为110元 | |
6 | 事务提交 | |
7 | 取出10元,把余额设为90元 | |
8 | 事务撤销 | |
9 | 余额恢复为100元(丢失更新) |
甲在撤销事务的时候,把乙存入的10元给抹掉了。
第二类更新丢失
时间序 | 转账事务甲 | 取款事务乙 |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询到账户有100元 | |
4 | 查询到账户有100元 | |
5 | 取出10元,余额为90元 | |
6 | 事务提交 | |
7 | 存入10元 | |
8 | 事务提交 | |
9 | 把余额设为110元(丢失更新) |
转账事务甲把取款业务乙的提交事务更新了,这样造成银行可能损失了10元。
Mysql的4个事务隔离级别能处理的业务并发
隔离级别 | 脏读 | 不可重复读 | 幻象读 | 第一类丢失更新 | 第二类丢失更新 |
---|---|---|---|---|---|
READ UNCOMMITTED | yes | yes | yes | no | yes |
READ COMMITTED | no | yes | yes | no | yes |
REPEATABLE READ | no | no | yes | no | no |
SERIALIZABLE | no | no | no | no | no |
在Springboot中,我们先引入事务依赖包,加入了mybatis这个依赖后,springboot就已经自动注入了DataSourceTransactionManager,这样就可以直接使用@Transactional 注解进行事务使用,Transactional 既可以注解在方法上,也可以注解在类上,注解在类上代表该类的public方法都开启事务的,如果类和方法都开启了Transactional ,类级别的注解会重载方法级的注解。
我们紧接上一篇的代码,在DAO层增加insert 代码
@Insert("insert into welcome_info(info) values('transaction test')")
@DS("test")
public void insertintoWelcomeinfo();
在Service层增加@Transactional注解,并且手动抛出异常
@Transactional
public void insertWelcomeInfo(){
//插入一条记录
welcomDao.insertintoWelcomeinfo();
//手动抛出异常
throw new RuntimeException();
}
在Test用例代码中,增加测试用例方法,来测试上面的Service方法,看数据是否会回滚
@Test
void testTransactional(){
welcomeService.insertWelcomeInfo();
}
运行之后,会直接抛出异常,数据库并没有增加记录,这样异常就生效了。
现在如果我们注释掉throw new RuntimeException();这行代码,那么记录就会插入数据库。
有时候有些异常没有被捕获到,而造成事务没有回滚,这是由于Springboot的默认事务是遇到RuntimeException和程序错误Error造成事务回滚。如果要捕获这些通用的异常,只要在注解中增加rollbackfor选项。比如: @Transactional(rollbackFor = Exception.class)
我们来认识一下Transactional注解的主要配置选项
属性值 | 具体意义 |
---|---|
propagationtiong | 主要定义事务周期 |
isolation | 设置事务隔离级别 |
timeout | 指定事务过期时间,默认为数据库事务过期时间 |
readonly | 指定事务是否是只读事务 |
rollbackFor | 指定哪些异常可以引起事务回滚 |
noRollbackFor | 指定哪些异常不引起事务回滚 |
propagationtiong选项:
REQUIRED,方法A调用时没有事务就新建,当调用另一个方法B时,方法B将使用与A相同的事务,如果B发生异常,这个事务都回滚回去。
REQUIRES_NEW,方法A和B,在方法调用的时候无论是否有开启一个新事务,如方法B有异常不会导致方法A的数据回滚。
NESTED 和上面的类似,但支持JDBC,不支持JPA或Hibernate
SUPPORT 方法调用时有事物就用事务,没事务就不用事务
NOT SUPPORT 强制不在事务中执行,在方法调用到结束阶段事务都会被挂起
NEVER 强制方法不在事务中执行,若有事务抛出异常
MANDATORY 强制方法在事务中,没有事务就抛出异常
Isolation选项
READ UNCOMMITTED
READ COMMITTED
REPEATABLE_READ
SERIALIZABLE
DEFAULT 使用数据库的默认是隔离级别,ORACLE,SQL SERVER是READ COMMITTED ,Mysql是REPEATABLE_READ
程序的源码在这里可以下载到链接:链接: https://pan.baidu.com/s/1jL04s4huVnTBOaQyg68QIA 提取码: vgut