数据库的事务
1. 数据库的事务
1.1 什么是事务
作为单个逻辑工作单位的一系列操作,要么完全成功,要么完全失败。
1.2 事务的四大特征ACID
A Atomic(原子性)
事务中包含的操作被看作一个整体的业务单元,这样业务单元要么完全成功,要么完全失败。最小的单元
C Consistency(一致性)
事务在完成时,必须使所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性
I Isolation(隔离性)
下面将会重点讲解隔离
D Durability(持久性)
事务结束后,所有的数据会固化到一个地方,如保存到磁盘中,即使断电后也可以提供给相应程序访问
1.3 事务的隔离级别
1.3.1 读未提交造成的脏读
是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险
的隔离级别,所以一般在我们实际的开发中应用不广,但是它的优点在于并发能力高,适合那些对数据
一致性没有要求而追求高并发的场景,它的最大坏处是出现脏读。让我们看看可能发生的脏读场景,
如表所示。
T3时刻,因为采用未提交读,所以事务2可以读取事务l未提交的库存数据为1,这里当它扣减
库存后则数据为0,然后它提交了事务,库存就变为了。,而事务l在TS时刻回滚事
务,因为第一类丢失更新已经被克服,所以它不会将库存回滚到2,那么最后的结果就变为了
0,这样就出现了错误。脏读一般是比较危险的隔离级别,在我们实际应用中采用得不多。
为了克服脏读的问题,数据库隔离级别还提供了读已提交(readcommited)的级别,
下面我们时论它。
1.3.2 读已提交
读已提交解决了读未提交造成的脏读问题,但读已提交依然造成了另外一个问题–不可重复读
读已提交: 是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。
在T3时刻事务2读取库存的时候,因为事务l未提交事务,所以读出的库存为l,于是事务2
认为当前可扣减库存;在T4时刻,事务l己经提交事务,所以在TS时刻,它扣减库存的时候
就发现库存为0,于是就无法扣减库存了。这里的问题在于事务2之前认为可以扣减,而到扣
减那一步却发现已经不可以扣减,于是库存对于事务2而言是一个可变化的值,这样的现象
我们称为不可重复读,这就是读写提交的一个不足。为了克服这个不足,数据库的隔离级别
还提出了可重复读的隔离级别,它能够消除不可重读的问题。
1.3.3 可重复读
可以看到,事务2在T3时刻尝试读取库存,但是此时这个库存己经被事务1事先读取,所以这
个时候数据库就阻塞它的读取,直至事务1提交,事务2才能读取库存的值。此时己经是TS时
刻,而读取到的值为0,这时就已经无法扣减了,显然在读写提交中出现的不可重复读的场
景被消除了。但是这样也会引发新的问题的出现,这就是幻读。
下面演示造成幻读的实战
1.3.4 序列化 (串行化)
串行化(Serializable):是数据库最高的隔离级别,它会要求所有的SQL都会按照顺序执
行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性。
2. Spring的事务的传播行为
2.1 具体的传播行为《java基础底层》中分析了Spring事务的传播行为
2.2 这里分析一个易错点,@Transactional失效的情景
首先编写一个测试service
/** * @author ZhangRongJun * @version 1.0 * @date 2020/9/5 10:03 * @description:TODO */ @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper = null; @Override @Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,rollbackFor = RuntimeException.class) public int inserts(List<Users> users) { int count = 0; for (Users u:users){ count += insert(u); } return 0; } @Override @Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRES_NEW,rollbackFor = RuntimeException.class) public int insert(Users users) { return userMapper.insert(users); } }
根据事务的传播,REQUIRED表示必须存在一个事务中,没有则新建事务,insert方法中调用了参数为Users的方法,而这个方法设置了新的传播行为REQUIRES_NEW,表示会为每一个执行该方法时创建新的事务,运行接口测试
在上图中,调用insert(Users users)方法时,使用的还是inserts(List users)方法创建的事务,造成了insert(Users users)的@Transactional注解失效
Spring数据库事务的约定,其实现原理是AOP,而AOP的原理是动态代理,在自调用的过程中,是类自身的调用,
而不是代理对象去调用,那么就不会产生AOP,这样Spring就不能把你的代码织入到约定的流程中,于是就产生
了现在看到的失败场景。为了克服这个问题,用一个Service去调用另一个Service,这样就是代理对象的调用,
Spring才会将你的代码织入事务流程。当然也可以从SpringIoC容器中获取代理对象去启用AOP,
自调用会造成@Transactional失效
2.3 解决自调用使@Transactional失效
2.3.1 更改上面service代码
/**
* @author ZhangRongJun
* @version 1.0
* @date 2020/9/5 10:03
* @description:TODO
*/
@Service
public class UserServiceImpl implements UserService,ApplicationContextAware {
@Autowired
private UserMapper userMapper = null;
ApplicationContext applicationContext;
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,rollbackFor = RuntimeException.class)
public int inserts(List<Users> users) {
UserService userService = applicationContext.getBean(UserService.class);
int count = 0;
for (Users u:users){
count += userService.insert(u);
}
return 0;
}
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRES_NEW,rollbackFor = RuntimeException.class)
public int insert(Users users) {
return userMapper.insert(users);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
- 在上面代码中新增实现接口ApplicationContextAware获取Spring Ioc容器
- 通过applicationContext获取Bean:UserService userService = applicationContext.getBean(UserService.class);
2.3.2 测试
如图执行insert(Users users)方法时,没执行一次,创建了一个事务。@Transactional注解生效
- 上一篇: SpringBoot优点
- 下一篇: 观察者模式