MySQL 事务简述
# 一、事务特性
# 1,原子性(Atomicity)
- 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节;
- 事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样;
# 2,一致性(Consistency)
- 事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态;
- 事务必须使数据库从一个一致性状态变换到另一个一致性状态;
- 比如,AB之间转账,不管怎么转,最后总额一定是不变的;
# 3,隔离性(Isolation)
- 数据库允许多个并发事务同时对其数据进行读写和修改;
- 隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致;
- 多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的;
# 4,持久性(Durability)
- 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失;
# 5,特性的实现
- 持久性是通过 redo log (重做日志)来保证的;
- 原子性是通过 undo log(回滚日志) 来保证的;
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证;
# 二、并行事务带来的问题
# 1,脏读
- 如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象;
- A事务读取了数据并进行了更改,还未提交;B读取了更改后提交前的数据;A事务触发了回滚;
- 此时B读到的就是过期的诗句,这种现象就被称为脏读;
# 2,不可重复读
- 在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象;
- A事务先读取了数据;B事务再对数据进行了更改,并提交了事务;A事务再次读取数据;即B事务的更改与提交发生在A事务中间;
- 此时A事务前后两次读到的数据是不一致的,这种现象就被称为不可重复读;
# 3,幻读
- 在一个事务内多次查询符合某个条件的「记录数量」,如果前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象;
- A事务先查询得到了n条记录;B事务增删了数据,并提交了事务;A事务再查询得到m条记录;
- 现和前一次读到的记录数量不一样了,这种现象就被称为幻读;
# 4,严重性
- 脏读 > 不可重复度 > 幻读;
# 三、事务的隔离
# 1,隔离级别
- 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
- 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;
- 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,** MySQL InnoDB 引擎的默认隔离级别 **;
- 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
# 2,等级与性能
- 等级由高到低:串行化、可重复读、读提交、读未提交;
- 性能由低到高:串行化、可重复读、读提交、读未提交;
# 3,不同隔离级别解决的问题
- 在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;
- 在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
- 在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象;
- 在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生;
# 四、并行事务问题的解决
# 1,MVCC(多版本并发控制)
- 通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC;
- Read View 的四个组成字段:
- creator_trx_id :创建该 Read View 的事务的事务 id;
- m_ids :在创建 Read View 时,当前数据库中「活跃但还没提交事务」的 id 列表;
- min_trx_id :在创建 Read View 时,当前数据库中「活跃但还没提交事务」列表中 id 最小的事务,也就是 m_ids 的最小值;
- max_trx_id :创建 Read View 时,当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
- 聚簇索引记录中的两个隐藏列:
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录;
- trx_id 在 Read View 中的情况:
- 如果 trx_id 小于 Read View 中的 min_trx_id ,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见;
- 如果 trx_id 大于或等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见;
- 如果 trx_id 在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
- 如果 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务还没提交,所以该版本的记录对当前事务不可见;
- 如果 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经提交,所以该版本的记录对当前事务可见;
# 2,隔离级别实现方式
- 「读未提交」:因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 「串行化」:通过加读写锁的方式来避免并行访问;
- 「读提交」:在「每个语句执行前」都会重新生成一个 Read View,期间如果有事务提交,新的 Read View 中的 m_ids 会被更新,会导致此期间提交事务的 trx_id 不在 m_ids列表中,通过数据库中最新 Read View(即最大的trx_id但不一定是提交了的)就可以在 undo log 版本链找到最新提交的数据,所以事务过程中每次查询的数据都是最新的,不会出现脏读,但会出现不可重复读和幻读;
- 「可重复读」:在「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View,期间即使有事务提交,m_ids 也不会被更新,通过最新 Read View(即最大的trx_id但不一定是提交了的)就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,此期间提交的事务对数据的增删改不会造成当前事务前后读取的差异,能解决脏读、不可重复读以及针对快照读的幻读;
# 3,幻读的解决方式
- MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生,所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能;
- 普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作;
- 针对快照读(普通 select 语句),通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题;
- 针对当前读(select ... for update 等语句),通过 next-key lock(记录锁+间隙锁)方式解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在锁范围内插入记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题;
# 4,「可重复读」隔离级别下幻读示例
- 示例1:对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读;
- 示例2:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读;
- 要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录;
# 5,事务的启动
- begin/start transaction 只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机;
- start transaction with consistent snapshot 执行这个命令后,就会马上启动事务;
编辑 (opens new window)
上次更新: 2024/08/12, 17:02:28