mysql(三) mysql事务(InnoDB引擎时)实现原理

   这里所说的MySQL事务是指使用InnoDB引擎时的事务。MySQL在5.5版本之前默认的数据库引擎时MyISAM,虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。

redo log和undo log来保证事务的原子性、一致性和持久性,同时采用预写式日志(WAL)方式将随机写入变成顺序追加写入,提升事务性能。而隔离性是通过锁技术来保证的。

这里我们不妨先来了解一下redo log和undo log。redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作。undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志:

  • redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
  • undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。

预写式日志:
事务日志采用追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头。事务日志持久以后,内存中被修改的数据在后台可以慢慢的刷回到磁盘。我们通常称为预写式日志,修改数据需要写两次磁盘。
如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。

(一)redo log:

redo log 又称为重做日志,它包含两部分:

  • 一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;
  • 二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

当需要修改事务中的数据时,先将对应的redo log写入到redo log buffer中,然后才在内存中执行相关的数据修改操作。InnoDB通过“force log at commit”机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有redo log都写入到磁盘上的redo log file中,然后待事务的commit操作完成才算整个事务操作完成。

在每次将redo log buffer中的内容写入redo log file时,都需要调用一次fsync操作,以此确保redo log成功写入到磁盘上(参考下图,内容的流向为:用户态的内存->操作系统的页缓存->物理磁盘)。因此,磁盘的性能在一定程度上也决定了事务提交的性能。这里还可以通过innodb_flush_log_at_trx_commit来控制redo log刷磁盘的策略,这里就不做赘述了。

在这里插入图片描述

(二)undo log:

   undo log有2个功能:实现回滚和多版本并发控制(MVCC, Multi-Version Concurrency Control)。

在数据修改的时候,不仅记录了redo log,还记录了相对应的undo log,如果因为某些原因导致事务失败或回滚了,可以借助该undo log进行回滚。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

(三)MVCC:

   说到undo log,就不得不顺带提一下MVCC了,因为MVCC的实现依赖了undo log。当然,MVCC的实现还依赖了隐藏字段(DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID)、Read View等。

MVCC的全称是多版本并发控制,它使得在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。这是一个可以用来增强并发性的强大技术,因为这样的一来的话查询就不用等待另一个事务释放锁,使不同事务的读-写、写-读操作并发执行,从而提升系统性能。

这里的读指的是“快照读”。普通的SELECT操作就是快照读,有的地方也称之为“一致性读”或者“一致性无锁读”。它不会对表中的任何记录做加锁动作,即不加锁的非阻塞读。快照读的前提是隔离级别不是串行化级别,串行化级别下的快照读会退化成当前读。之所以出现快照读的情况,是基于提高并发性能的考虑,这里可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销。当然,既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

对应的还有“当前读”。类似UPDATE、DELETE、INSERT、SELECT…LOCK IN SHARE MODE、SELECT…FOR UPDATE这些操作就是当前读。为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

(四)锁技术:

   并发事务的读-读情况并不会引起什么问题(读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生),不过对于写-写、读-写或写-读这些情况可能会引起一些问题,需要使用MVCC或者加锁的方式来解决它们。

在使用加锁的方式解决问题时,既要允许读-读情况不受影响,又要使写-写、读-写或写-读情况中的操作相互阻塞。这里引入了两种行级锁:

  • 共享锁:英文名为Shared Locks,简称S锁。允许事务读一行数据。
  • 排它锁:也常称独占锁,英文名为Exclusive Locks,简称X锁。允许事务删除或更新一行数据。

假如事务A首先获取了一条记录的S锁之后,事务B接着也要访问这条记录:1) 如果事务B想要再获取一个记录的S锁,那么事务B也会获得该锁,也就意味着事务A和B在该记录上同时持有S锁;2) 如果事务B想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务A提交之后将S锁释放掉。

如果事务A首先获取了一条记录的X锁之后,那么不管事务B接着想获取该记录的S锁还是X锁都会被阻塞,直到事务A提交。

除了 S锁 和 S 锁兼容,其他都不兼容。

InnoDB存储引擎还支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为此,InnoDB存储引擎引入了意向锁(表级别锁):

  • 意向共享锁(IS 锁):事务想要获取一张表的几行数据的共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(IX 锁):事务想要获取一张表中几行数据的排它锁,事务在给一个数据行加排它锁前必须先取得该表的 IX 锁。

当我们在对使用InnoDB存储引擎的表的某些记录加S锁之前,那就需要先在表级别加一个IS锁,当我们在对使用InnoDB存储引擎的表的某些记录加X锁之前,那就需要先在表级别加一个IX锁。IS锁和IX锁的使命只是为了后续在加表级别的S锁和X锁时判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录。

锁粒度
MySQL中不同的存储引擎支持不同的锁机制:MyISAM与MEMORY存储引擎采用表级锁;BDB存储引擎采用的是页级锁,也支持表级锁;InnoDB存储引擎既支持行级锁,也支持表级锁,默认采用行级锁。
1:表级锁: MySQL中开销最小的策略,加锁速度快,锁定整张表,粒度大。不会出现死锁,发生锁竞争的概率最高,并发度最低,性能最差。
2:行级锁: 开销大,加锁速度慢,锁定一行数据,粒度小。会出现死锁,发生锁竞争的概率最低,并发读最高,性能高。
3:页级锁: 开销和加锁速度介于表锁和行锁之间,锁定一页数据。会出现死锁,锁竞争概率、并发性、性能均位于表锁和行锁之间。

下表展示了X、IX、S、IS锁的兼容性:
在这里插入图片描述

这里还要了解一下的是,在InnoDB中有 3 种行锁的算法:

  • Record Locks(记录锁):单个行记录上的锁。
  • Gap Locks(间隙锁):在记录之间加锁,或者在第一个记录之前加锁,亦或者在最后一个记录之后加锁,即锁定一个范围,而非记录本身。
  • Next-Key Locks:结合 Gap Locks 和 Record Locks,锁定一个范围,并且锁定记录本身。主要解决的是
    REPEATABLE READ 隔离级别下的幻读问题。

对于Next-Key Locks,如果我们锁定了一个行,且查询的索引含有唯一属性时(即有唯一索引),那么这个时候InnoDB会将Next-Key Locks优化成Record Locks,也就是锁定当前行,而不是锁定当前行加一个范围;如果我们使用的不是唯一索引锁定一行数据,那么此时InnoDB就会按照本来的规则锁定一个范围和记录。还有需要注意的点是,当唯一索引由多个列组成时,如果查询仅是查找其中的一个列,这时候是不会降级的。InnoDB存储引擎还会对辅助索引的下一个键值区间加Gap Locks(这么做也是为了防止幻读)。

(五)总结:

MySQL实现事务ACID特性的方式总结如下:

  • 原子性:使用 undo log来实现,如果事务执行过程中出错或者用户执行了rollback,系统通过undo
    log日志返回事务开始的状态。
  • 持久性:使用 redo log来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复。
  • 隔离性:通过锁以及MVCC来实现。
  • 一致性:通过回滚、恢复以及并发情况下的隔离性,从而实现一致性。
发布了246 篇原创文章 · 获赞 12 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41987908/article/details/105449536
今日推荐