MySQL事务
事务特性
- 原子性(Atomicity):事务是数据库的逻辑工作单位,事务中的操作要么都做,要么都不做,不允许部分执行。
- 一致性(Consistency):事务的执行结果必须是使数据库从一个一致性状态转移到另一个一致性状态,保证数据的一致性不被破坏。
- 隔离性(Isolation):数据库中一个事务的执行不能被其他事务干扰,即一个事务的内部操作及使用的数据对其他事务是隔离的,并发执行的事务不能相互干扰。
- 持久性(Durability):事务一旦提交,对数据库中数据的改变是永久的,即使数据库发生故障也不会丢失。
事务并发引发的问题
并发事务情况下会出现下列问题:
- 脏读:一个事务读取到其他事务未提交的数据。
- 不可重复读:一个事务查询一条数据两次,如果两次中间有其他事务修改当前数据且提交事务,则两次读取的数据不同。
- 幻读:一个事务查询表数据数量两次,如果两次中间有其他事务插入或删除数据,则两次读取的数据数量不同。
事务隔离级别
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 可串行化(SERIALIZABLE),事务单线程处理不会并发事务,性能差。
√ 表示解决了并发事务下的问题
SQL92标准中的隔离级别
隔离级别 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|
读未提交 | × | × | × |
读已提交 | √ | × | × |
可重复读 | √ | √ | × |
可串行化 | √ | √ | √ |
MySQL中的隔离级别
隔离级别 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|
读未提交 | × | × | × |
读已提交 | √ | × | × |
可重复读 | √ | √ | ? |
可串行化 | √ | √ | √ |
事务语法及事务常见问题
事务语法
- 查询当前使用的隔离级别:SELECT @@SESSION.TX_ISOLATION;
- 设置隔离级别:SET SESSION 或 GLOBAL TRANSACTION ISOLATION LEVEL xxx,xxx为(READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE),SESSION:当前会话生效,GLOBAL:全局生效。
- 开启事务:begin 或 start transaction
- 提交事务:commit
- 回滚:rollback
事务保存 点
begin; --开启事务
--设置三个保存点s1,s2,s3
insert into my_test values(4,4,4);
savepoint s1;
insert into my_test values(5,5,5);
savepoint s2;
insert into my_test values(6,6,6);
savepoint s3;
--回滚到s2保存点,只保存了两条数据
rollback to savepoint s2;
隐式提交
一些情况下会导致事务自动提交:
- 执行DDL语句
- 连续开启两个事务,上一个事务自动提交
MVVC实现事务机制
https://blog.csdn.net/m0_62436868/article/details/127202062 这篇文章太好了!
基础概念
- trx_id:事务的ID,每开启一个事务都会有一个事务ID。
- undo日志:记录每一条insert、update、delete的日志形成一个版本链信息,每个节点存储内容为:当前记录信息 + trx_id + roll_prt(指向上个版本的指针);
- ReadView:读视图,ReadView就是一个快照,保存着数据库某个时刻的数据信息,可重复读、读已提交、读未提交,这几个隔离级别都会使用Read View。里面有几个ID:
- creator_trx_id:创建这个 Read View 的事务 ID。只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
- trx_ids:在生成ReadView时当前系统中活跃的(未提交的)读写事务的事务id列表。
- up_limit_id:活跃的事务中最小的事务 ID。
- low_limit_id:ReadView时系统中即将分配给下一个事务的id值。low_limit_id 是系统最大的事务id值,类似计数器。
中心思想
每个事务操作数据记录undo日志,生成版本链,每个事务读取生成ReadView视图,对于不同的隔离级别:
- 可串行化:InnoDB使用枷锁的方式来访问记录
- 读未提交:每次读取最新的版本记录
- 读已提交和可重复读:必须保证只读取到其他事务已经提交的数据,核心就是通过ReadView判断版本链中哪个版本是对当前事务可见,判断规则如下:
- 如果版本链中的 trx_id = creator_trx_id,说明此次版本是当前事务操作的,则可见
- 如果版本链中的 trx_id < up_limit_id,说明当前版本在此次ReadView创建之前就已经提交事务,则可见
- 如果版本链中的 trx_id >= lo_limit_id,说明当前数据是ReadView创建之后的事务操作的数据,则不可见
- 如果版本连中的 trx_id > up_limit_id 且 trx_id < low_limit_id,则判断 trx_id是否在trx_ids中,如果在,证明事务还没提交,则不可见,如果不在,证明事务已经提交,则可见
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
读已提交和可重复读的区别: 读已提交每次查询数据之前都重新生成ReadView,这样还是无法避免不可重读问题,因为每次生成的ReadView不同;可重复读是事务内查询只有第一次查询之前生成ReadView,事务内每次读取都是同一个ReadView,就避免了不可重复读问题。
幻读
MySQL没有真正解决幻读问题
案例:
创建一个my_test空表,字段为id,a_id,b_id
事务1
begin;
select * from my_test;//此刻没有数据
事务2
begin;
insert into my_test values(1,1,1);
commit;
再次进入事务1
update my_test set a_id = 2 where id = 1;//此刻事务1还没有提交,但是事务2提交了
select * from my_test;//此刻就可以看到数据了
通过上述案例发现在事务1中第二次读取到了数据,而第一次没有读取到数据,这样就发生了幻读问题;
原因:
事务1在第一次查询时候生成了ReadView,没有查到数据,事务2插入一条数据并提交事务后事务1执行了update语句,所以会有版本链生成,且最新的版本的事务ID为事务1的trx_id,所以第二次读取的时候creator_trx_id = 事务1的trx_id所以可以看到数据了。