事物
基本概念
ACID
- Atomic(原子性):事物操作的原子性;指一个事物中的操作要么全部成功,要么全部失败;
-
redoLog
日志:记录事物操作,在事物异常中断时,如Mysql服务器宕机等情况下,保证对未完成事物的恢复; -
undoLog
日志:事物失败时,对已经成功的SQL进行回滚操作,保证失败情况下,事务的原子性;
- Consistency(一致性):数据状态的一致性;指事物执行前后,数据应该符合数据库的特定规则和约束;
比如:假设有个事物A(客户下单),它由「扣库存、新增订单」两个操作组成,当客户下单购买了100个商品,库存需要减少100、生成一条100个商品的订单数据,也就是【总库存 = 库存的商品数量+ 订单的商品数据】的等式需恒成立;若一个成功一个失败则数据库整体数据出现不一致;
-
undoLog
日志:事物失败时,对已经成功的SQL进行回滚操作,保证失败情况下,事务的一致性; -
redoLog
日志:记录事物操作,在事物异常中断时,如Mysql服务器宕机等情况下,保证对未完成事物的恢复; - 锁机制:保证事物对数据的独占性;
- 完整性约束:确保插入、更新或删除的数据满足特定条件;
- 实体完整性:保证关系中的每个元组都是可识别的和惟一的;例如:主键不能为空、允许重复等
- 引用完整性:保证实体与实体之间的引用关系必须满足一定约束;例如:外键引用需与对应的主键值一致,或者为空;
- 域完整性: 数据库中的数据必须满足定义的域约束条件;例如:数值必须在允许的范围内、日期格式必须正确、唯一性约束、非空约束等。
- Isolation(隔离性):事物之间的隔离性,指事物的执行相互之间互不影响;
- 锁机制:保证写操作时,事物对数据的独占性;
- MVCC机制:通过保存数据的多个版本,以及使用版本控制来实现并发事务之间的隔禽;
- Durability(持久性):数据的持久性;指一个事务执行完成后,它对数据库的修改应该是永久性的;即使发生系统崩溃或机器宕机,也不会导致数据的丢失。
-
redoLog
日志:记录事物操作,在事物异常中断时,如Mysql服务器宕机等情况下,保证对未完成事物的恢复;
隔离机制
- 丢失更新、脏读、幻读、不可重复读问题:
- 脏写(丢失更新):多个事物操作同一条数据,导致后提交事物的修改结果覆盖了先提交修改结果;
- 脏读:读未提及;一个事物读到了另一个事物还未提交的数据;
- 不可重复读:读已提交;同一个事物中,对某一个数据多次读取却得到了不同的结果;
- 幻读:同一个事物中,多次查询返回的结果集不同;
例:假设我们需要将数据库中的数据全部设置为软删(del_flag = 1),此时事物A开始更改数据库中的del_flag
字段,当负责执行事务A刚更改好最后一条数据时,此时事务B来了,正好向表中插入了一条「del_flag = 0
」的数据并提交了,然后事物A再次去查询数据时,发现还存在一条「del_flag = 0
」的数据,似乎产生幻觉一样;
原因:另外一个事务在第一个事务要处理的目标数据范围之内新增了数据,并且先于第一个事务提交;
隔离级别
- Read Uncommitted(读未提交):解决脏写,可能产生脏读、不可重复读、幻读;
- 实现:写操作加排他锁,读操作不加锁;当事物对一条数据进行写操作时,需先获取到锁资源,才允许对数据进行写操作,读操作不加锁;
- 例子:当存在两个事物T1、T2,T1在修改ID=1的数据时添加上排他锁,若这时T2也尝试修改ID=1的数据也需要获取排他锁,两个事物的写操作就会互斥,T2就需要阻塞等待;但读操作不会加锁,因此T2在尝试读取ID=1的数据时,自然可以读到;
- Read Committed(读以提交):解决脏写、脏读,可能产生不可重复读、幻读;
- 实现:写操作加排他锁,读操作采用MVCC多版本并发控制技术限制,在每一次查询数据时,生成一个
ReadView
读视图;
也可以通过读操作加共享锁实现,但是会导致并发事物串行化执行,影响效率;
- Repeatable Read(可重复读、默认):解决了脏写、脏读、不可重复读,可能产生幻读;
- 实现:对于写操作同样通过互斥锁限制;读操作同样通过MVCC限制,区别于读已提交级别,一个事务中只有首次查询时会生成
ReadView
快照;
也可以在查询时对目标数据加上行锁,即读操作执行时,不允许其他事物改动数据; - 幻读问题:Mysql通过采用MVCC机制已经极大程度的在RR级别下规避了幻读问题,但在极端情况下(离谱操作)还是存在幻读可能;
- Serializable(串行化):解决了脏写、脏读、不可重复读、幻读;
- 实现:所有写操作加临键锁,所有读操作加共享锁;这种情况下只有读-读场景可以并发执行。
Mysql的隔离机制:
- 查询命令:
SELECT @@tx_isolation;
或show variables like '%tx_isolation%';
- 设置命令:
-- 设置隔离级别为RU级别(当前连接生效)
set transaction isolation level read uncommitted;
-- 设置隔离级别为RC级别(全局生效)
set global transaction isolation level read committed;
-- 设置隔离级别为RR级别(当前连接生效)
set tx_isolation = 'repeatable-read';
-- 设置隔离级别为最高的serializable级别(全局生效)
set global tx_isolation = 'serializable';
设置全局生效需要加上global
关键字
Mysql的事物实现原理和执行流程:
- 事务是基于数据库连接的,而每个数据库连接在MySQL中,又会用一条工作线程来维护,也意味着一个事务的执行,本质上就是一条工作线程在执行;
- 基于锁和MVCC多版本并发控制机制实现,
MVCC
又主要通过隐藏字段、undo-log
日志、读诗图实现;
- InnoDB事物执行的流程:
-
start trasaction;
关闭事物自动提交机制; - SQL解析过程,经过SQL接口、解析器、执行器得到执行计划;
- 记录一条状态为
prepare
的redo-loge
日志; - 生成对应
undo-log
日志并记录; - 执行SQL,在缓冲区
BufferPool
中更改对应数据, 如果有多条sql则逐条依次做上述相同处理; - 直到碰到了
rollback、commit
命令时,再对前面的所有写SQL做相应处理;
-
commit
事物提交:将redo-log
日志改为commit
状态; -
rollback
事物回滚:根据前面已经执行了的写SQL的更改行数据的隐藏列roll_ptr
回滚指针,定位到undo-log
日志中的undo
记录,然后从xx.ibdata
共享表数据文件中拷贝到xx.ibd
表数据文件,覆盖掉原本改动过的数据(存在缓存优化,日志是写入undo_log_buffer
缓冲区,数据也是修改BufferPool
缓冲区中的数据);若是插入操作,则新插入的行数据隐藏列roll_ptr = null
,因此之间用null
覆盖插入的新纪录即可;
- Mysql进行刷盘处理,将缓冲区中的数据落入磁盘中;
Mysql中事物分组
Mysql在提交事物时内部会调用ordered_commit
函数来处理相关工作,具体流程为:
每一个事物提交时都会调用ordered_commit
函数,首先会将事务加入等待事务组,接着会经过三个核心步骤:FLUSH、SYNC、COMMIT
,对应的也会有三个队列,它们三者的工作原理都大致相同:
- 如果某个事务进入
FLUSH
队列时,该队列还是空的,则这个事务会担任“队长”的角色。 - 当后续其他事务进入队列时,发现队列不为空,则会将提交工作委托给队长来完成。
- 如上图中的「事务1」则是队长,后续的都是队员,但队长不会无限制等待队员到来:从队长加入的时间点开始,当超出
binlog_group_commit_sync_delay
规定的时间后,就会进行一次组提交;
同一时刻只允许一个事物组进行工作;
当事物组提交后会将当前事务组的内容记录到Bin-log
日志中,同时会将这组事务记录成一个GTID
(全局事务标识符,主从同步时使用),不同事务之间通过,
逗号分隔;
Spring中事务的使用
编程式事务
TransactionTemplate
- 使用:
// SpringBoot 注入对象
@Autowired
private TransactionTemplate transactionTemplate;
transactionTemplate.execute(transactionStatus -> {
// 业务代码 一个表删除不成功则回滚
try {
if (mapper1.delete(id) < 1) {
transactionStatus.setRollbackOnly();
return Boolean.FALSE;
}
if (mapper2.delete(id) < 1) {
transactionStatus.setRollbackOnly();
return Boolean.FALSE;
}
if (mapper3.delete(id) < 1) {
transactionStatus.setRollbackOnly();
return Boolean.FALSE;
}
return Boolean.TRUE;
} catch (Exception e) {
transactionStatus.setRollbackOnly();
return Boolean.FALSE;
}
});
DataSourceTransactionManager与TransactionDefinition
- 使用:
//DataSourceTransactionManager: 数据源事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
//TransactionDefinition:事务定义
@Autowired
private TransactionDefinition transactionDefinition;
TransactionStatus transactionStatus = null;
int result = 0;
try{
//开启事务
transactionStatus = transactionManager.getTransaction(transactionDefinition);
//业务操作,删除事务
result = userService.delete(id);
//提交事务
transactionManager.commit(transactionStatus);
}catch (Exception e){
//回滚事务
if(transactionStatus != null){
transactionManager.rollback(transactionStatus);
}
}
注意点:
- 通常只在单数据库操作多个表时使用;
- 事务只能保证数据库的一致,若存在
redis
、rpc
调用等三方操作,则不能保证数据库和三方的一致性; - 事务未提交前,程序抛出异常(未处理)时会自动回滚事务;
- 捕获异常需要手动回滚事务;
声明式事务
@Transactional
- 作用范围:可以加在方法上以及类上
- 当使用
@Transactional
注解修饰方法时,它只对public
的方法生效。 - 当使用
@Transactional
注解修饰类时,表示对该类中所有的public
方法生效。
- 原理:当一个类或者方法带有
@Transactional
注解时,Spring
将创建一个代理对象来管理事务。Spring
使用 AOP将事务管理逻辑织入到带有@Transactional
注解的方法周围。具体spring实现方法:TransactionInterceptor#invokeWithinTransaction
- 失效场景:
- 访问权限问题 (只有public方法会生效);原因:在
AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则TransactionAttribute
返回null
,即不支持事务; - 方法使用final、static修饰;原因:使用final、static修饰的方法无法被
spring AOP
生成的代理类重写; - 同一个类中的方法直接内部调用;原因:内部掉用使用的是
this
对象的方法;
解决办法:新加一个Service方法;在该Service类中注入自己;通过AopContent
获取到代理类掉用,((ServiceA)AopContext.currentProxy()).doSave(user);
- 多线程调用:在一个事务方法中,新开线程调用另一个事务方法;原因:事务是基于数据库连接的,在不同的线程,拿到的数据库连接是不一样的,所以是不同的事务;
- 类未被spring管理;
- 错误的传播特性;
- 异常自行捕获了却没有重新抛出;
传播特性
支持当前事务:
REQUIRED
(默认):如果当前存在事务,则加入到当前事务中,如果没有事务,则创建一个新的事务。
SUPPORTS
:如果当前存在事务,则加入到当前事务中,如果没有事务,则以非事务的方式执行。
MANDATORY
:必须在一个已存在的事务中执行,否则抛出异常。
不支持当前事务:
REQUIRES_NEW
:每次都会创建一个新的事务,如果当前存在事务,则将当前事务挂起。
NOT_SUPPORTED
:以非事务的方式执行操作,如果当前存在事务,则将当前事务挂起。
NEVER
:必须以非事务方式执行,如果当前存在事务,则抛出异常。
嵌套事务:
NESTED
:如果当前存在事务,则在嵌套事务内执行,如果没有事务,则创建一个新的事务。