MySQL InnoDB如何保证事务特性

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

先说结论:

  • 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层产生的
  • 重做日志是物理格式日志,记录的是对每个页的修改
  • 重做日志在事务进行中不断被写入,而且是顺序写入

MySQL的innoDB存储引擎,使用Redo log保证了事务的持久性。当事务提交时,必须先将事务的所有日志写入日志文件进行持久化,就是我们常说的WAL(write ahead log)机制。这样才能保证断电或宕机等情况发生后,已提交的事务不会丢失,这个能力称为 crash-safe。

Redo log包括两部分,重做日志缓冲(redo log buffer)和重做日志文件(redo log file),前者是易失的缓存,后者是持久化的文件。

举一个事务的例子:

步骤1:begin;

步骤2:insert into t1 …r

步骤3:insert into t2 …

步骤4:commit;

这个事务的写入过程实际拆解如下:

图片.png

重点关注在这个事务提交前,将redo log的写入拆成了两个步骤,prepare 和 commit,这就是"两阶段提交”。那么为什么要采用两阶段提交呢?

实际上,两阶段提交是分布式系统常用的机制。MySQL使用了两阶段提交后,也是为了保证事务的持久性。Redo log 和bingo 有一个共同的数据字段,叫 XID,崩溃恢复的时候,会按顺序扫描 redo log。

假设在写入binlog前系统崩溃,那么数据库恢复后顺序扫描 redo log,碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务,而且binlog也没写入,所以事务就直接回滚了。

假设在写入binlog之后,事务提交前数据库崩溃,那么数据库恢复后顺序扫描 redo log,碰到既有 prepare、又有 commit 的 redo log,就直接提交,保证数据不丢失。

这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先写入redo log buffer ,等到commit的时候,才真正把日志写到 redo log 文件。(当然,这里不绝对,因为redo log buffer可能因为其他原因被迫刷新到redo log)。

而为了确保每次日志都能写入日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次fsync操作,确保写入了磁盘。

对于redo log的持久化,可以如下图所示。


图片.png

1)先写入redo log buffer,在蓝色区域。

2)写入redo log file,但是还没有fsync,这时候是处于黄色的位置,处于系统缓存。

3)调用fsync,真正写入磁盘。

为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值:

设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;

设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;

设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。

binlog的写入和redo log一样,也是包括bingo cache和bingo file,同样跟上面的三色层次类似(当然,binlog是server层的,不是存储引擎层的),包括log buffer、文件系统page cache、hard disk。

写入page cache 和 fsync到disk 的时机,是由参数 sync_binlog 控制的:

sync_binlog=0 的时候,表示每次提交事务都只 写入文件系统的page cache,不 fsync;

sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;

sync_binlog=N(N>1) 的时候,表示每次提交事务都写入文件系统的page cache,但累积 N 个事务后才 fsync。(如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志)

通常我们说MySQL的“双 1”配置,指的就是sync_binlog和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。

回滚日志 undo log

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

回滚日志不同于重做日志,它是逻辑日志,对数据库的修改都逻辑的取消了。当事务回滚时,它实际上做的是与先前相反的工作。对于每个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种锁:行级锁,意向锁

行级锁:

  • 共享锁(读锁 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且查询的索引走的非唯一索引、主键索引。

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

sql

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执行如下语句:

sql

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

sql

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是无法插入成功的,这就从根本上解决了幻读问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容