转载:MySQL 三万字精华总结 + 面试100 问,和面试官扯皮绰绰有余(收藏系列) (juejin.cn)
一个事物由一批操作组合而成,要么全部成功,要么全部失败。
事务四要素:
(1)并发事务会引起下面的问题:
更新丢失(Lost Update): 事务A和事务B选择同一行,然后基于最初选定的值更新该行时,由于两个事务都不知道彼此的存在,就会发生丢失更新的问题。
脏读(Dirty Reads):事务A读取了事务B更新的数据,然后事务B回滚了,那么A读取到的数据是脏数据。
不可重复读(Non-Repeatable Reads):事务 A 多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
幻读(Phantom Reads):幻读与不可重复读类似。它发生在一个事务A读取了几行数据,接着另一个并发事务B插入了一些数据时。在随后的查询中,事务A就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
(2)不可重复读 和 幻读 的区别:
(3)并发事务问题的解决办法:
数据库事务的隔离级别有4种,由低到高分别为:
数据库的事务隔离越严格,并发副作用越小,但付出的代价就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化” 进行,这显然与 “并发” 是矛盾的。
MySQL的默认事务隔离级别是:REPEATABLE-READ。
这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-Key Lock 算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别,而且保留了比较好的并发性能。
MySQL的大多数事务型存储引擎实现都不是简单的行级锁。基于提升并发性考虑,一般都同时实现了多版本并发控制(MVCC)。
可以认为 MVCC 是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。 虽然实现机制有所不同,但大都 实现了非阻塞的读操作,写操作也只是锁定必要的行。
MVCC 的实现是通过保存数据在某个时间点的快照来实现的。也就是说不管需要执行多长时间,每个事物看到的数据都是一致的。
典型的MVCC实现方式,分为 乐观(optimistic)并发控制 和 悲观(pressimistic)并发控制。
InnoDB 的 MVCC,是通过在每行记录后面保存 两个隐藏的列 来实现。这两个列,一个保存了行的创建时间,一个保存 行的过期时间(删除时间)。当然存储的并不是真实的时间,而是 系统版本号(system version number)。每开始一个新的事务,系统版本号都会 自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
REPEATABLE READ(可重读)隔离级别下MVCC如何工作:
SELECT 操作
InnoDB 会根据以下两个条件检查每行记录:
只有符合上述两个条件的才会被查询出来。
INSERT 操作
InnoDB 为新插入的每一行保存当前系统版本号作为行版本号。
DELETE 操作
InnoDB 为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE 操作
InnoDB 为插入的一行新纪录保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识。
注意:
保存这两个额外系统版本号,使大多数操作都不用加锁。使数据操作简单,性能很好,并且也能保证只会读取到符合要求的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。
MVCC 只在 COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作。
事务的隔离性(I)是通过锁实现;而事务的原子性(A)、一致性(C)和持久性(D)则是通过事务日志实现 。
InnoDB 使用日志来减少提交事务时的开销。因为日志中已经记录了事务,就无须在每个事务提交时把缓冲池的脏块刷新(flush)到磁盘中。
一旦日志安全写到磁盘,事务就持久化了,即使断电了,InnoDB可以重放日志并且恢复已经提交的事务。
InnoDB 使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。
事务日志可以帮助提高事务效率:
目前来说,大多数存储引擎都是这样实现的,我们通常称之为 预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘(一次写日志,一次写数据)。
事务日志包括:redo log(重做日志)和 undo log(回滚日志)。
redo log(重做日志):实现持久性和原子性
事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲里,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的 “预写式日志”(Write-Ahead Logging)。当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据 redo log 中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。
在系统启动的时候,就已经为 redo log 分配了一块连续的存储空间,以顺序追加的方式记录redo Log,通过顺序IO来改善性能。所有的事务共享redo log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起。
undo log(回滚日志):实现一致性
undo log 主要为事务的回滚服务。在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
undo记录的是已部分完成并且写入硬盘的未完成的事务,默认情况下回滚日志是记录下表空间中的(共享表空间或者独享表空间)。
二种日志均可以视为一种恢复操作,redo_log是恢复提交事务修改的页操作,而undo_log是回滚行记录到特定版本。二者记录的内容也不同,redo_log是物理日志,记录页的物理修改操作,而undo_log是逻辑日志,根据每行记录进行记录。