MySQL InnoDB如何保证事务特性

若是有人问你“数据库事务有哪些特性”?你可能会很快回答出原子性、一致性、隔离性、持久性即ACID特性。那么你知道InnoDB如何保证这些事务特性的吗?若是知道的话这篇文章就能够直接跳过不看啦(#^.^#)算法

先说结论:sql

  • redo log重作日志用来保证事务的持久性
  • undo log回滚日志保证事务的原子性
  • undo log+redo log保证事务的一致性
  • 锁(共享、排他)用来保证事务的隔离性

重作日志 redo log

重作日志 redo log 分为两部分:一部分是内存中的重作日志缓冲(redo log buffer),是易丢失的;二部分是重作日志文件(redo log file),是持久的。InnoDB经过Force Log at Commit机制来实现持久性,当commit时,必须先将事务的全部日志写到重作日志文件进行持久化,待commit操做完成才算完成。
InnoDB在下面状况下会将重作日志缓冲的内容写入重作日志文件:数据库

  • master thread 每一秒将重作日志缓冲刷新到重作日志文件;
  • 每一个事务提交时
  • 当重作日志缓冲池剩余空间小于1/2时

为了确保每第二天志都写入重作日志文件,在每次将日志缓冲写入重作日志文件后,InnoDB存储引擎都须要调用一次fsync(刷盘)操做。但这也不是绝对的。用户能够经过修改innodb_flush_log_at_trx_commoit参数来控制重作日志刷新到磁盘的策略,这个能够做为大量事务提交时的优化点。缓存

  • 1参数默认值,表示事务提交时必须调用一次fsync操做。
  • 0表示事务提交时,重作日志缓存并不当即写入重作日志文件,而是随着Master Thread的间隔进行fsync操做。
  • 2表示事务提交时将重作日志写入重作日志文件,但仅写入文件系统的缓存中,不进行fsync操做。

fsync的效率取决于磁盘的性能,所以磁盘的性能决定了事务提交的性能,也就是数据库的性能。因此若是有人问你如何优化Mysql数据库的时候别忘了有硬件这一条,让他们提高硬盘配置,换SSD固态硬盘
重作日志都是以512字节进行存储的,称之为重作日志块,与磁盘扇区大小一致,这意味着重作日志的写入能够保证原子性,不须要doublewrite技术。它有如下3个特性:微信

  • 重作日志是在InnoDB层产生的
  • 重作日志是物理格式日志,记录的是对每一个页的修改
  • 重作日志在事务进行中不断被写入,并且是顺序写入

回滚日志 undo log

为了知足事务的原子性,在操做任何数据以前,首先将数据备份到一个地方(这个存储数据备份的地方称为Undo Log),而后进行数据的修改。若是出现了错误或者用户执行了 ROLLBACK语句,系统能够利用Undo Log中的备份将数据恢复到事务开始以前的状态。
undo log实现多版本并发控制(MVCC)来辅助保证事务的隔离性。session

回滚日志不一样于重作日志,它是逻辑日志,对数据库的修改都逻辑的取消了。当事务回滚时,它实际上作的是与先前相反的工做。对于每一个INSERT,InnoDB存储引擎都会完成一个DELETE;对于每一个UPDATE,InnoDB存储引擎都会执行一个相反的UPDATE。并发

事务提交后并不能立刻删除undo log,这是由于可能还有其余事务须要经过undo log 来获得行记录以前的版本。故事务提交时将undo log 放入一个链表中,是否能够删除undo log 根据操做不一样分如下2种状况:性能

  • Insert undo log: insert操做的记录,只对事务自己可见,对其余事务不可见(这是事务隔离性的要求),故该undo log能够在事务提交后直接删除。不须要进行 purge操做。
  • update undo log:记录的是对 delete和 update操做产生的 undo log。该undo log可能须要提供MVCC机制,所以不能在事务提交时就进行删除。提交时放入undo log链表,等待 purge线程进行最后的删除。

事务的隔离性的实现原理就是锁,于是隔离性也能够称为并发控制、锁等。事务的隔离性要求每一个读写事务的对象对其余事务的操做对象能互相分离。再者,好比操做缓冲池中的LRU列表,删除,添加、移动LRU列表中的元素,为了保证一致性那么就要锁的介入。优化

锁的类型

InnoDB主要有2种锁:行级锁,意向锁ui

行级锁:

  • 共享锁(读锁 S),容许事务读一行数据。事务拿到某一行记录的共享S锁,才能够读取这一行,并阻止别的事务对其添加X锁。共享锁的目的是提升读读并发。
  • 排它锁(写锁 X),容许事务删除一行数据或者更新一行数据。事务拿到某一行记录的排它X锁,才能够修改或者删除这一行。排他锁的目的是为了保证数据的一致性。

行级锁中,除了S和S兼容,其余都不兼容。

意向锁:

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

解释一下意向锁

The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.

意向锁的主要用途是为了表达某个事务正在锁定一行或者将要锁定一行数据。e.g:事务A要对一行记录r进行上X锁,那么InnoDB会先申请表的IX锁,再锁定记录r的X锁。在事务A完成以前,事务B想要来个全表操做,此时直接在表级别的IX就告诉事务B须要等待而不须要在表上判断每一行是否有锁。意向排它锁存在的价值在于节约InnoDB对于锁的定位和处理性能。另外注意了,除了全表扫描之外意向锁都不会阻塞。

锁的算法

InnoDB有三种行锁的算法:

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,而非记录自己
  • Next-Key Lock:结合Gap Lock和Record Lock,锁定一个范围,而且锁定记录自己。主要解决的问题是REPEATABLE READ隔离级别下的幻读。能够参考文章了解事务隔离级别的相关知识点。

这里主要讲一下Next-Key Lock,利用Next-key Lock锁定的不是单个值而是一个范围,他的目的就是为了阻止多个事务将记录插入到同一范围内从而致使幻读。

注意了,若是走惟一索引,那么Next-Key Lock会降级为Record Lock,即仅锁住索引自己,而不是范围。也就是说Next-Key Lock前置条件为事务隔离级别为RR且查询的索引走的非惟一索引、主键索引。

下面咱们用个例子详细说一下。
首先创建一张表:

CREATE TABLE T (id int ,f_id int,PRIMARY KEY (id), KEY(f_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into T SELECT 1,1;
insert into T SELECT 3,1;
insert into T SELECT 5,3;
insert into T SELECT 7,6;
insert into T SELECT 10,8;

事务A执行以下语句:

SELECT * FROM T WHERE f_id = 3 FOR UPDATE

这时SQL语句走非惟一索引,所以使用Next-Key Locking加锁,而且有2个索引,其须要分别进行锁定。
对于汇集索引,其仅对id等于5的索引加上Record Lock。而对于辅助索引,其加上Next-Key Lock,锁定了范围(1,3),特别须要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上Gap Lock,即范围(3.6)的锁。
因此若是在新session中执行以下语句都会报错[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

select * from T where id = 5 lock in share MODE -- 不能执行,由于事务A已经给id=5的值加上了X锁,执行会被阻塞
INSERT INTO T SELECT 4,2  -- 不能执行,辅助索引的值为2,在(1,3)的范围内,执行阻塞
INSERT INTO T SELECT 6,5  -- 不能执行,gap锁会锁住(3,6)的范围,执行阻塞

此时想象一下,事务A锁定了f_id =5 的记录, 正常会有个gap lock,锁住(5,6),那么若是没有(5,6)的gap锁,那么用户能够插入索引 f_id 为5的记录,这样事务A再次查询就会返回一个不一样的记录,也就致使了幻读的产生。

同理,若是咱们事务A执行的是select * from T where f_id = 10 FOR UPDATE,在表里查不到数据,可是基于Next-Key Lock会锁住(8,+∞),咱们执行INSERT INTO T SELECT 6,11是没法插入成功的,这就从根本上解决了幻读问题。

更多内容请关注公众号:JAVA日知录
微信公众号

相关文章
相关标签/搜索