InnoDB秘籍:MVCC机制与行锁的深度探索(2)

云掣YunChe2周前技术文章43




InnodDB 和 ACID 模型



事务 ACID 模型是一种数据库设计原则,InnoDB 引擎是 MySQL 默认且支持事务的存储引擎,它严格遵循 ACID 模型,结果也不会因软件崩溃和硬件故障等特殊情况而失真。依靠符合 ACID 标准的功能,就不需要重新设计一致性检查和崩溃恢复机制。当然,用户如果有额外的软件保障措施和超可靠的硬件,或者应用可容忍少量数据丢失或失去 ACID 模型保护,可根据 MySQL 提供的参数进行配置,从而换取更高的性能和更高的吞吐。


在 MySQL 中涉及 ACID 模型组件非常多,贯穿内存结构设计、磁盘结构设计以及事务和锁定模型等等。接下来,我们将介绍一些 InnoDB 引擎实现 ACID 模型的一些关键技术。

图片



01

Innodb Buffer  Pool

InnoDB 存储引擎将数据都存储在表空间中,表空间是存储引擎对文件系统上的一个文件或者几个文件的抽象。InnoDB 引擎在处理用户请求时,会把需要访问的页面加载到内存中,页大小一般为 16k ,即使只访问一个页面中的一行记录,依然需要加载整个页面。在访问结束后 InnoDB 引擎并不会立即释放掉页面,而是将其缓存起来,内存页面淘汰策略由 LRU 算法控制。下次需要访问时就不需要进行磁盘读,这样就可以提高数据库的读写性能。这部分内存的大小由 innodb_buffer_pool_size 控制。



show variables like 'innodb_buffer_pool_size';



02

Redo log

为了取得更好的读写性能,InnoDB 会将数据缓存在内存中(InnoDB Buffer Pool)对磁盘数据的修改也会落后于内存,这时如果进程或机器崩溃,会导致内存数据丢失,为了保证数据库本身的一致性和持久性,InnoDB 维护了 Redo log。


修改 Page 之前需要先将修改的内容记录到 Redo 中,并保证 Redo log 早于对应的 Page 落盘,也就是常说的 WAL(Write Ahead Log)日志优先写,Redo log 的写入是顺序 IO,可以获得更高的 IOPS 从而提升数据库的性能。当故障发生导致内存数据丢失后,InnoDB 会在重启时,通过重放 Redo,将 Page 恢复到崩溃前的状态。


图片

03

Undo log

undo 日志是 innodb 引擎非常重要的部分,它的设计贯穿事务管理、并发控制和故障恢复等。当开启一个事务涉及到数据变动的时候,就会为事务分配 undo 段以 “逻辑日志” 的形式保存事务的信息,用于回滚。事务的回滚分为三种情况:


  1. 用户事务管理 rollback 回滚。

  2. 锁等待及死锁回滚。

  3. Crash Recovery 故障恢复回滚。

数据库在运行过程中,随时可能会因为某种情况异常中断,比如软件 bug、硬件故障运维操作等等,这个时候可能已经有未提交的事务有部分已经刷新到磁盘中,如果不加以处理的话会违反事务的原子性,要么全部提交,要么全部回滚。针对这种问题,直观的想法是事务在提交后再进行刷盘。也就是 no-steal 策略。显而易见,这种方式会造成很大的内存空间压力,另一方面,事务提交时的随机 IO 会影响性能。


Innodb 事务处理采用的是 steal 策略,允许一个 uncommitted 的事务将修改刷新到磁盘。为了保障事务原子性,事务执行过程中会持续的写入 undo log 来记录事务变更的历史值。当 Crash 真正发生时,可以在 Recovery 过程中通过回放 Undo 将未提交事务的修改抹掉。


需要注意的是,因为 Innodb 事务执行过程中产生的 Undo log 也需要进行持久化操作,所以 Undo log 也会产生 Redo log。由于Undo log 的完整性和可靠性需要 Redo log 来保证,因此数据库崩溃时需要先做 Redo log 数据恢复,然后做 Undo log 回滚。


04

应用案例

MySQL 社区开源物理热备工具 Xtrabackup 就是基于 MySQL 故障恢复实现的。在 copy 数据文件阶段,会一直监听并备份 redo log。在备份恢复阶段 Xtrabackup 需要执行一个 prepare 命令,该阶段就是启动一个临时实例,依赖 innodb 恢复机制应用刚才备份的 redo 文件,相当于应用一遍备份期间的增量数据。





隔离级别和锁机制



MySQL 5.7 及以上的版本,默认的隔离级别是 RR 可通过下方 SQL 查看或修改隔离级别。










-- 查看隔离级别show variables like '%transaction_isolation%';
-- 修改全局隔离级别 global 替换为 session 表示修改当前会话隔离级别set global transaction_isolation = 'READ-UNCOMMITTED' |    'READ-COMMITTED' |    'REPEATABLE-READ' |    'SERIALIZABLE';


ISO SQL标准基于事务是否可能出现的几种异象来定义隔离级别,4 种隔离级别的定义见下表:


级别脏读不可重复读幻读

读未提交(READ-UNCOMMITTED)




不可重复读(READ-COMMITTED)




可重复读(REPEATABLE-READ)




串行化(SERIALIZABLE)






隔离级别越严格,事务的并发度就越小,因此很多时候,业务都需要在二者之间选择一个平衡点。


01

事务隔离级别

以下是测试隔离级别需要用的演示数据。










-- 创建测试表create table account(    id bigint auto_increment primary key ,    name varchar(20) not null ,    balance int not null);-- 插入测试数据insert into account(name, balance) VALUES ('张三', 300),('李四', 400),('王五', 500);


1 READ UNCOMMITTED


RU 隔离级别也叫读未提交,会读取到其他会话中,未提交的事务修改的数据。


调整参数,修改数据库隔离级别为 RU。



set global transaction_isolation = 'READ-UNCOMMITTED';


Session 1:在 RU 隔离级别下,开启一个事务:













-- 开启事务begin;-- 查询数据select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     300 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 2:在 RU 隔离级别下,开启一个事务:






-- 开启事务begin;-- 给张三账户余额加 1000000update account set balance = balance + 100 where id = 1;


Session 1:查询张三余额:











-- 查询余额select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     400 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 2:因为某种原因需要回滚事务:



rollback;


Session 1:张三余额减少 100:














-- 张三余额减少 100 并提交事务update account set balance = balance - 100 where id = 1;commit;
-- 查询余额select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     200 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 1 中,查询张三余额为 400 时,对张三的余额减 100,事务提交后余额又变成了 200,因为程序中并不知道其他会话回滚过事务,这就是脏读的案例,一般生产环境没有理由使用 RU 作为隔离级别。


2 READ COMMITTED


RC 隔离级别也叫读已提交,事务中会读取到其他事务已提交到数据,这种隔离级别下会有不可重复读和幻读的问题。


调整参数,修改数据库隔离级别为 RC:



set global transaction_isolation = 'READ-COMMITTED';

Session 1:在 RC 隔离级别下,开启一个事务:











-- 开启事务begin;-- 查询数据select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     300 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 2:开启一个事务,给张三余额加 100。






-- 开启事务begin;-- 修改张三余额update account set balance = balance + 100 where id = 1;


Session 1:再次查询张三余额,发现余额依然为 300,在 RC 隔离级别下,不会读取到未提交到事务。











-- 查询数据select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     300 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 2:提交事务:



commit;


Session 1:再次查询张三余额:











-- 查询数据select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     400 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


可以看到 Session 1 在事务中不同时刻读取了张三的余额,得到的结果不一样,这是因期间 Session 2 对张三的余额进行了改动并提交事务,产生不可重复读的问题。


3 REPEATABLE READ


RR 隔离级别也叫可重复读,指在一个事务内,无论何时查询到的数据都与开始查询到的数据一致,是 InnoDB 引擎默认的隔离级别,通过更严格的锁机制,一定程度上可以避免幻读。


调整参数,修改数据库隔离级别为 RR:



set global transaction_isolation = 'REPEATABLE-READ';


Session 1:在 RR 隔离级别下,开启一个事务:













-- 开启事务begin;-- 查询数据select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     300 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 2:开启事务,给张三余额添加 100 并提交事务:















-- 开启事务begin;update account set balance = balance + 100 where id = 1;commit;-- 张三余额被修改为 400select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     400 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+
Session 1:查询表中数据:








select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     300 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 2 的事务已经提交,Session 1 再次读取,前后的数据没有发生变化,不存在不可重复读的问题。


Session 1:为张三余额加 100 提交事务。














-- 为张三加 100update account set balance = balance + 100 where id = 1;
-- 查询select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     500 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


发现最终张三的余额为 500,而不是 400 元,数据的一致性没有被破坏。RR 隔离级别下,使用了 MVCC 多版本并发控制的机制 Select 查询不会更新更新版本号,是快照读,而 DML 语句操作表中的数据,会更新版本号,是当前读。


Session 2:不显式开启事务,插入一条记录。



insert into account(name, balance) VALUES ('赵六', 600);


Session 1:再次查询。










select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     500 ||  2 | 李四   |     400 ||  3 | 王五   |     500 |+----+--------+---------+


Session 2 已经插入一条记录并提交,Session 1 没有读取到 Session 2 插入的记录,没有发生幻读。


Session 1:更新赵六的记录,触发一次当前读:














-- 为赵六余额增加 100update account set balance = balance + 100 where id = 4;-- 再次查询select * from account;+----+--------+---------+| id | name   | balance |+----+--------+---------+|  1 | 张三   |     500 ||  2 | 李四   |     400 ||  3 | 王五   |     500 ||  4 | 赵六   |     700 |+----+--------+---------+


Session 1 未提交的状态下,对 Session 2 插入的新纪录触发一次当前读,行版本发生了更新,出现了幻读的情况。


4 SERIALIZABLE


SERIALIZABLE 可串行化隔离级别下,所有的读写操作都需要加锁,并发度很小,避免了幻读。




-- 设置隔离级别为 SERIALIZABLEset global transaction_isolation = 'SERIALIZABLE';
Session 1:开启事务,读取一行数据。


begin;select * from account where id = 5;

Session 2:尝试修改刚才读取的记录。





update account set balance = balance + 100 where id = 5;-- 锁超时ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction



02

多版本并发控制

数据库的核心方向就是高并发,Innodb 引擎通过并发控制技术来维护高并发的环境下数据的一致性和数据安全。Innodb 并发控制有两种技术方案,锁机制(Locking) 和 多版本并发控制 (MVCC) 。


并发场景分为四类 读-读读-写写-读写-写 通过锁机制可以帮助我们解决 写-写 场景下的并发问题,而 MVCC 侧重优化 读-写 和 写-读 业务的高并发场景,可以解决写操作时堵塞读操作的并发问题。


Innodb 引擎使用 MVCC 机制来保障事务的隔离性,实现数据库的隔离级别。


1 两种读取方式


  • 快照读(Snapshot Read) 也叫一致性非锁定读取,简称 “一致性读”。一种读操作,指 innodb 引擎通过多版本并发控制的方式来读取,使用 Read View 基于某个时间点呈现查询结果。读取正在进行 update 或 delete 操作的行,不会等待锁释放,而是会读取该行的快照数据。快照就是指该行之前的版本数据,主要靠 undo 日志来实现,而 undo 是用于数据库回滚,因此快照读本身是没有开销的。后台 Purge 线程也会自动清理一些不需要的 undo 数据。

  • 当前读(Current Read) 如果 select 语句后加上 for share、for update,或者执行的是 update、delete、insert into t2 select from t1 这类语句(这几种类型的 SQL 需要读取最新版本的数据,被称为当前读)MySQL会根据 where 条件和 SQL 的执行计划,加上相应的锁,阻止其他会话修改满足过滤条件的记录。


2 Read View


Read View 是实现 MySQL 一致性读的一种机制,对于一条查询来讲,不同隔离级别可以读取到的版本是不同的。RC 和 RR 隔离级别在实现上最根本的区别就是创建 READ VIEW 的时间不一样。


对于 RC 隔离级别的事务,每次执行一致性查询时,都会创建一个新的 Read View,这样 SELECT 就能获取系统中当前已经提交的最新数据。对于 RR 隔离级别的事务,在第一次执行一致性读取时创建 READ VIEW。事务中后续再次执行 SELECT 时,会重用已经创建好的 Read View,这样同一个 SQL 后续重复执行时就能获取到相同的数据。



相关文章

PG的多版本并发控制(三)

三、多版本并发控制3.1 常见多版本并发的实现方式第一种方式是,数据库仅保存最新版本数据,将发生变更的旧行版本数据写到其他地方如undo,当需要读取旧版本数据时,通过undo重构。oracle和MyS...

Elasticsearch数据生命周期如何规划

Elasticsearch中的open状态的索引都会占用堆内存来存储倒排索引,过多的索引会导致集群整体内存使用率多大,甚至引起内存溢出。所以需要根据自身业务管理历史数据的生命周期,如近3个月的数据op...

MySQL创建用户授权

创建用户授权一、创建用户1.创建管理用户create user 'test'@'%' identified by 'Test123@'create user 'test'@'localhost' id...

Hive与HBase整合文档

Hive与HBase整合文档

1.Hive整合HBase配置1.拷贝hbase 相关jar包将hbase 相关jar包拷贝到hive lib目录下hbase-client-0.98.13-hadoop2.jar hbase-co...

MySQL排障实战(一)—— 连接异常中断

MySQL排障实战(一)—— 连接异常中断

问题背景数栈数据质量模块,接入客户的数据源后,一执行就报错。报错信息:{"logInfo": {{"jobid":"1a4ebbbd&quo...

ORC、Parquet 等列式存储的优点

ORC 和Parquet 都是高性能的存储方式,这两种存储格式总会带来存储和性能上的提升Parquet:1. Parquet 支持嵌套的数据模型,类似于Protocol Buffers,每一个数据模型...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。