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

回滚日志 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且查询的索引走的非唯一索引、主键索引。

下面我们用个例子详细说一下。

首先建立一张表:

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • MySQL中Innodb的事务隔离级别和锁的关系的讲解教程

    前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式.同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力.所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在.这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么. 一次封锁or两段锁? 因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会

  • MySQL InnoDB之事务与锁详解

    引题:为何引入事务? 1>.数据完整性 2>.数据安全性 3>.充分利用系统资源,提高系统并发处理的能力 1. 事务的特征 事务具有四个特性:原子性(Atomiocity).一致性(Consistency).隔离性(Isolation)和持久性(Durability),这四个特性简称ACID特性. 1.1原子性 事务是数据库的逻辑工作单位,事务中包括的所有操作要么都做,要么都不做. 1.2 一致性 事务执行的结果必须是使数据库从一个一致性的状态变到另外一个一致性状态. 1.3 隔离性 一

  • MySQL InnoDB如何保证事务特性示例详解

    前言 如果有人问你"数据库事务有哪些特性"?你可能会很快回答出原子性.一致性.隔离性.持久性即ACID特性.那么你知道InnoDB如何保证这些事务特性的吗?如果知道的话这篇文章就可以直接跳过不看啦(#^.^#) 先说结论: redo log重做日志用来保证事务的持久性 undo log回滚日志保证事务的原子性 undo log+redo log保证事务的一致性 锁(共享.排他)用来保证事务的隔离性 重做日志 redo log 重做日志 redo log 分为两部分:一部分是内存中的重做

  • MySQL InnoDB 二级索引的排序示例详解

    排序问题 最近看了极客时间上 <MySQL实战45讲>,纠正了一直以来对 InnoDB 二级索引的一个理解不到位,正好把相关内容总结下. PS:本文的所有测试基于 MySQL 8.0.13 . 先把问题抛出来,下面的 SQL 所创建的表,有两个查询语句,哪个索引是非必须的? CREATE TABLE `geek` ( `a` int(11) NOT NULL, `b` int(11) NOT NULL, `c` int(11) NOT NULL, `d` int(11) NOT NULL, P

  • MySQL教程数据定义语言DDL示例详解

    目录 1.SQL语言的基本功能介绍 2.数据定义语言的用途 3.数据库的创建和销毁 4.数据库表的操作(所有演示都以student表为例) 1)创建表 2)修改表 3)销毁表 如果你是刚刚学习MySQL的小白,在你看这篇文章之前,请先看看下面这些文章.有些知识你可能掌握起来有点困难,但请相信我,按照我提供的这个学习流程,反复去看,肯定可以看明白的,这样就不至于到了最后某些知识不懂却不知道从哪里下手去查. <MySQL详细安装教程> <MySQL完整卸载教程> <这点基础都不懂

  • MySQL实现数据插入操作的示例详解

    目录 一.方法分类 二.具体方法 三.实例 (1)常规插入 (2)从另一个表导入 (3)插入时数据重复 四.注意事项 (1)不写字段名,需要填充自增ID (2)按字段名填充,可以不录入id 其余注意事项 使用MySQL插入数据时,可以根据需求场景选择合适的插入语句,例如当数据重复时如何插入数据,如何从另一个表导入数据,如何批量插入数据等场景.本文通过给出每个使用场景下的实例来说明数据插入的实现过程和方法. 一.方法分类 二.具体方法 使用场景 作用 语句 注意 常规插入 忽略字段名 insert

  • Mysql联表update数据的示例详解

    1.MySQL UPDATE JOIN语法 在MySQL中,可以在 UPDATE语句 中使用JOIN子句执行跨表更新.MySQL UPDATE JOIN的语法如下: UPDATE T1, T2, [INNER JOIN | LEFT JOIN] T1 ON T1.C1 = T2. C1 SET T1.C2 = T2.C2, T2.C3 = expr WHERE condition 更详细地看看MySQL UPDATE JOIN语法: 首先,在UPDATE子句之后,指定主表(T1)和希望主表连接表

  • Go1.20 arena新特性示例详解

    目录 正文 快速背景 最新进展 总结 正文 大概半年前,我写过一篇文章<Go 要违背初心吗?新提案:手动管理内存>.有兴趣了深入解的同学,可以再回顾一下. 当时我们还想着 Go 团队应该不会接纳,至少不会那么快: 懒得翻也可以看我再次道来,本文提到的提案<proposal: arena: new package providing memory arenas>,这其中的 Arena 将会是一个突破项. 快速背景 Arena 指的是一种从一个连续的内存区域分配一组内存对象的方式.优点

  • MySql学习笔记之事务隔离级别详解

    背景 说的事务,大家应该都不陌生,开发用到 MySql 数据库的时候,通常会用到事务.其中比较经典的例子就是转账,比如你要给小明转 50 块钱,而此时你的银行卡也就只有 50 块钱. 对于转账过程在代码程序里会有一系列的操作,比如查询账户余额.余额加减.更新余额等,这些操作必须保证是一起处理的,不然等程序查完之后,如果账号 50 块钱还在,然后再给另外一个朋友转账,如果银行也处理的话,没有保证整个流程数据一致性的话,这不就乱套了吗?这时就要用到"事务"了. 事务介绍 简单来说,事务就是

  • mysql查看死锁与去除死锁示例详解

    1.查询进程 show processlist 2. 查询到相对应的进程,然后 kill id 验证(kill后再看是否还有锁) 2.查询是否锁表 show OPEN TABLES where In_use > 0; 示例: 新建一个会话执行如下的显示锁示例 LOCK TABLES account_data.account READ; SELECT SLEEP(160); UNLOCK TABLES account_data.account; 另开启一个会话检查锁表情况: mysql> sho

  • MySQL中执行计划explain命令示例详解

    前言 explain命令是查看查询优化器如何决定执行查询的主要方法. 这个功能有局限性,并不总会说出真相,但它的输出是可以获取的最好信息,值得花时间去了解,因为可以学习到查询是如何执行的. 调用EXPLAIN 在select之前添加explain,mysql会在查询上设置一个标记,当执行查询计划时,这个标记会使其返回关于执行计划中每一步的信息,而不是执行它. 它会返回一行或多行信息,显示出执行计划中的每一部分和执行次序. 这是一个简单的explain效果: 在查询中每个表在输出只有一行,如果查询

  • MySQL中的行级锁定示例详解

    前言 锁是在执行多线程时用于强行限定资源访问的同步机制,数据库锁根据锁的粒度可分为行级锁,表级锁和页级锁 行级锁 行级锁是mysql中粒度最细的一种锁机制,表示只对当前所操作的行进行加锁,行级锁发生冲突的概率很低,其粒度最小,但是加锁的代价最大.行级锁分为共享锁和排他锁. 特点: 开销大,加锁慢,会出现死锁:锁定粒度最小,发生锁冲突的概率最大,并发性也高: 实现原理: InnoDB行锁是通过给索引项加锁来实现的,这一点mysql和oracle不同,后者是通过在数据库中对相应的数据行加锁来实现的,

随机推荐