一 数据库事务四个特性
数据库的事务指的是一种机制,一系列的操作指令集合,是并发的系统上的最小控制单元
事务把一系列的的命令作为一个整体,一同向数据库进行提交或者回滚,一个事务内的命令要么全部成功,要么全部失败
事务的四个特性 ACID
- 原子性Atomicity:一个事务内的所有操作是一个整体,要么全部成功,要么全部失败
- 一致性Consistency:当事务完成时,数据库内的数据必须要处于一致的状态;在事务进行过程中,有可能修改了一条记录,但是另外一条记录还没有修改完成,此时就属于不一致的状态。例如,银行的转账,转账的事务完成之后,两个账户里的钱加起来的总数不变
- 隔离性Isolation:不同的事务之间的操作互相不能影响。事务必须是独立的,不能依赖于或者影响其他事务,1给2转账,2又给3转账,则两个操作不能同时进行,必须等其中一个事务结束之后才能访问2的数据,否则有可能会出现问题。
- 持久性Durability:事务一旦执行完成,那么就保存到了硬盘上,不管系统是否发生故障,数据都不会再发生改变,事务对数据的操作是永久性的
事务的ACID原则保证了事务要么全部执行成功后提交,要么失败全部回滚,使数据恢复到执行前的状态,不会影响数据
二 数据库事务的四个隔离等级以及并发的操作中可能出现的问题
2.1 四种隔离等级及并发操作引起的三种问题
数据库事务有四个隔离等级,从低到高分别是 Read uncommitted、 Read committed、Repeatable read、 Serializable
读未提交、读已提交、可重复读、序列化
不同的隔离等级在并发的环境下可能会出现不同的问题:脏读,不可重复读、幻读
读未提交:老板发工资(事务A),原本发一万,手抖多打了个0,同时程序员查工资(事务B开启),查到10万开心坏了;结果老板发现输错了就回滚了或者又多打了个0提交了,反正就是程序员看到的工资不对,这个时候事务B读到了A还没有提交的结果,这个就是读未提交,导致了脏读。
由于读未提交的隔离等级,对于查询的操作是不加锁的,所以这种隔离等级的一致性是最差的,几乎没有人试用这种隔离等级
读已提交:Oracle,SqlServer的默认隔离等级,程序员去买单(事务A),第一次检测卡里余额还有一万,这时候他老婆转出来了一万(事务B),然后买单事务A要扣款检测的时候发现余额不足了,这就是在A事务中进行两次查询,查到的结果不一样,导致了不可重复读。
读已提交和读未提交相同,在查询的时候都没有加锁,但是读已提交使用了快照读的机制,使得读已提交避免了由于事务B加了写锁导致事务A无法获取读锁而阻塞大大降低性能的问题
读已提交会导致不可重复读的问题
可重复读:MySql的默认隔离等级,**程序员去买单开始检查卡里的余额(事务A开启),这个时候他老婆(事务B)就取钱执行更新update的操作了,但是程序员(事务A)在这个过程中看到的余额始终是事务A开启之初创建的视图,在一次事务内看到的一直是事务开启之初的视图,这样子就避免了不可重复读的问题
程序员开始第一次查败家娘们最近一个月的账单即消费记录(事务A开启),但是这个时候败家娘们(事务B)又产生了一笔消费,在账单上又新增了一笔记录,然后程序员发现第二次查询多了一条记录,这个就是幻读,在A开启了查询的事务,可重复读的隔离等级会禁止事务B更新的操作,但是无法阻止插入或删除的操作,这个就是可重复读,会导致幻读,多或者少出来的行叫做幻行
序列化:最高的隔离等级,在这种情况下,所有的事务都是一个一个排着队等待执行的,这种效率最低性能开销也很大,但是可以避免脏读、不可重复读、幻读
- 脏读:假如事务A开始执行更新,同时B也开始执行查询,在A更新之前,B读到了还没有更新的数据,此时就是脏读。由于A还没有提交,B读到的属于脏数据,此时就是脏读
- 不可重复读:事务A中要进行两次读取同一条数据的操作,A执行完第一次查询后,B开启事务,开始更新这条数据,导致A第二次读到的数据跟第一次读到的数据不一样,这就是不可重复读。**不可重复读对应的是更新update的操作。**读已提交是只能读到提交后的事务,A的第二次查询必须要等到B更新的事务提交后才能执行
- 幻读:事务A要读取两次同一范围内的数据,A读完第一次之后,B往里插入或者删除了几条数据,导致A读到的记录数与第一次不一样,这就导致了幻读。幻读对应的是插入insert或者删除delete的操作,多出来或者少的那些行记录叫做幻行
2.2 快照读
了解快照读
这里指的读,要抛开读写分离的思想,这里的读既包含了select还包含了insert、update等语句中的处理逻辑,读分为两种:当前读和快照读
- 当前读:读当前时刻已经提交的数据
- 快照读:为数据库创建一个快照,进行读的操作的时候从快照中进行读取
快照读可以理解为在进行读的操作的时候,为数据库创建的一个视图;那么快照是什么时候生成的呢?不同的隔离等级下快照的创建时间是不同的。接下来先复习一下隔离等级:
- 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到
- 读已提交:一个事务提交之后,它做的变更才会被其他事务看到
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。未提交的数据对其他事务不可见
- 串行化。对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
那么接下来继续看快照时间的创建时间:
- 读未提交下,不创建快照
- 读已提交下,在每次sql语句开始执行的时候创建快照
- 可重复读下,在事务开启的时候创建快照
2.3 如何知道当前是快照读还是当前读
1. 默认隔离等级下,如果不显示加锁就是快照读
select a from t where id = 1
2. 加锁就是当前读
# 共享锁
select a from t where id = 1 lock in share mode;
#排他锁
select a from t where id = 1 for update;
3. update操作是当前读
update t set a = a + 1;
关于数据库创建快照的更多可以参考网址:https://blog.csdn.net/qq_34679704/article/details/106161807
2.4 总结
- 为什么会出现脏读?因为读到了没有提交的数据
- 为什么会出现不可重复读?因为创建了多次快照,读到了已经提交的数据
- 为什么会出现幻读?insert和delete可以将快照中的记录删除,而且可以不提交就可以影响快照
三 如何设置
3.1 启动类
在启动类上添加@EnableTransationManageMent注解
对于系统需要提供默认事务管理的情况下,实现接口 TransactionManagementConfigurer 指定
为了避免不必要的问题,如果在业务中必须要明确指定 @Transactional 的value的情况下,不建议实现接口 TransactionManagementConfigurer,这样控制台会明确抛出异常,开发人员就不会忘记主动指定
@SpringBootApplication @EnableTransactionManagement
public class AppMain {
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
}
3.2 在Service层类或者方法上使用@Transactional注解
如果没有实现TransactionalManagementConfigurer接口,没有配置默认隔离等级,则必须要在注解中添加Isolation属性配置隔离等级,否则将会抛出异常
@Transactional(isolation = Isolation.DEFAULT,propagation = Propagation.REQUIRED)
public class DefaultFooService implements FooService {
public void getFoo(Foo foo) {
// do something
}
//方法上注解属性会覆盖类注解上的相同属性
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void updateFoo(Foo foo) {
// do something
}
}
3.3 属性配置
3.3.1 隔离等级Isolation
隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读
我们可以看
org.springframework.transaction.annotation.Isolation
枚举类中定义了五个表示隔离级别的值:
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
- DEFAULT:默认值,对应的使用数据源的默认的隔离等级,Oracle、SqlServer默认隔离等级是读已提交,会导致不可重复读和幻读的问题;MySql默认隔离等级是可重复读,会导致幻读的问题
- READ_UNCOMMITTED:读未提交
- READ_COMMITTED:读已提交
- REPEATABLE_READ:可重复读
- SERIALIZABLE:序列化
3.3.2 七种传播行为
事务的传播行为是针对嵌套事务而言
我们可以看
org.springframework.transaction.annotation.Propagation
枚举类中定义了7个表示传播行为的枚举值:
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
}
- REQUIRED :如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- MANDATORY :如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- NOT_SUPPORTED :以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- NEVER :以非事务方式运行,如果当前存在事务,则抛出异常。
- NESTED :如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED
3.4 其他属性
- value:指定事务管理器的名称
- readOnly:是否只读
- rollbackFor:指定回滚的异常的类型
- rollbackForClassname:指定回滚的异常类名字
- noRollbackFor:
- noRollbackForClassname:
四 事务的七种传播行为
事务的传播机制主要是针对的嵌套事务而言
@Transactional(propagation = Propagation.REQUIRED)
4.1 REQUIRED
spring默认的事务传播行为就是这个
支持事务,如果方法执行的时候已经在一个事务中,那么加入进去;如果没有事务,那么创建一个事务。
外层事务提交后,内层才会提交
内/外层如果抛出了异常,那么将会一起回滚;只要内层事务抛出了异常,那么就会回滚,无论外层是否有try-catch
因为内外层方法在同一个事务中,内层只要抛出了异常,这个事务就会被设置成rollback-only,即使外层try-catch内层的异常,该事务也会回滚
例子
外层方法在调用内层方法的时候包裹住try-catch,内层方法报错抛出异常。
外层:
@Override
@Transactional
public int addUser(User user) {
int i = userMapper.insertSelective(user);
Student student = new Student();
student.setCourse("cs");
student.setName("sid");
try {
studentService.addStudent(student);
}catch (Exception e){
//不抛出
}
return i;
}
内层:
@Override
@Transactional//(propagation = Propagation.NESTED)
public int addStudent(Student student) {
int i = studentMapper.insertSelective(student);
int j = 10/ 0; // 内层报错抛出异常
return i;
}
如果内层抛出异常,则尽管外层catch了异常,没有跑出去,但是外层还是会跟着回滚,因为他们在同一个事务中,会一起失败
4.2 SUPPORTS
支持事务,如果当前有事务就加入,没有就算了
如果外层没有事务,就没有事务,不会开启事务;如果外层有事务,那么就加入事务。
如果有事务的话还是会一起提交一起回滚
有事务的话与REQUIRE一样
示例
内层:
@Override
@Transactional(propagation = Propagation.SUPPORTS)// 这个addStudent方法的事务传播行为是SUPPORTS。
public int addStudent(Student student) {
int i = studentMapper.insertSelective(student);
return i;
}
外层:
@Override
@Transactional //有事务
public int addUser(User user) {
int i = userMapper.insertSelective(user);
Student student = new Student();
student.setCourse("cs");
student.setName("sid");
studentService.addStudent(student);// 调用addStudent方法,addStudent方法的事务传播机制是SUPPORTS
return i;
}
结果:
如果外层没有事务,则内层被调用之后数据直接就被插入到表里了;如果外层有事务,则要等到外层事务提交后内层才会提交
4.3 MANDATORY
mandatory:强制的
如果存在事务,则加入;否则,如果不存在事务,则抛出异常
实例
内层:
@Override
@Transactional(propagation = Propagation.MANDATORY)
public int addStudent(Student student) {
int i = studentMapper.insertSelective(student);
return i;
}
外层:
@Override
@Transactional //有事务
public int addUser(User user) {
int i = userMapper.insertSelective(user);
Student student = new Student();
student.setCourse("cs");
student.setName("sid");
studentService.addStudent(student);// 调用addStudent方法
return i;
}
如果外层有事务,则加入;如果外层没有事务,则抛出IllegalTransactionStateException:No existing transaction found for transaction marked with propagation 'mandatory’
4.4 REQUIRES_NEW
如果外层没有事务,则创建一个事务
如果外层有事务,则创建一个新的事务!内部执行完就提交了,与外部没有关系!
如果内层抛出异常,则内层回滚;外层如果catch了这个异常,则外层不会回滚;如果外层抛出了异常,不会影响到内层
4.5 NOT_SUPPORTED
不支持事务,如果外层有事务,那么内层执行的时候会挂起外层事务,内层执行完毕后,外层事务恢复执行
如果内层抛出了异常,则外层有catch的话不会影响到外层
4.6 NEVER
不支持事务,如果外层有事务,则直接抛出异常IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never’
4.7 NESTED
nested:嵌套的
如果外层有事务,加入事务;如果外层没有事务,则内层开启事务
内层事务要等到外层提交才能提交,如果外层回滚,则内层也会滚,如果内层回滚不影响到外层,则外层正常提交
使用该事务内层回滚不影响外层是有前提的!!!
- JDK版本在1.4以上
- 事务管理器的nestedTransactionAllowed属性需要设置为true
- 外层try-catch内层的异常,因为这样子内层异常就不会影响到内层