一、mysql事务基础概念

1. 基本概念

MySQL 事务主要用于处理操作量大,复杂度高的数据。
比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!

  • 在 MySQL 中,事务支持是在引擎层实现的。你现在知道,MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
  • 事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
  • 事务用来管理 insert,update,delete 语句

1.ACID原则

ACID的定义:

1.Atomic原子性—由undo log日志保证

一个事务的所有系列操作步骤被看成是一个动作,所有的步骤要么全部完成要么一个也不会完成,如果事务过程中任何一点失败,将要被改变的数据库记录就不会被真正被改变。

2. Consistent一致性-----由其他三种特性保证

定义:一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

场景1:
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

场景2:
比如说,在事务中执行一个充值 100 元的交易,先记录一条交易流水,流水号是 888,然后把账户余额从 100 元更新到200 元。

对应的 SQL 是这样的:

mysql 响应时间 吞吐量 mysql吞吐量有多大_sql

数据库中的数据总是从一个一致性状态(888 流水不存在,余额是 100
元)转换到另外一个一致性状态(888 流水存在,余额是 200 元)。对于其他事务来说,不存在任何中间状态(888 流水存在,但余额是 100元)。其他事务,在任何一个时刻,如果它读到的流水中没有 888 这条流水记录,它读出来的余额一定是 100 元,这是交易前的状态。如果它能读到 888 这条流水记录,它读出来的余额一定是 200 元,这是交易之后的状态。也就是说,事务保证我们读到的数据(交易和流水)总是一致的,这是事务的一致性 (Consistency)。

3. Isolated隔离性—由MVCC来保证

主要用于实现并发控制。隔离能够确保并发执行的事务能够顺序一个接一个执行,通过隔离,一个未完成事务不会影响另外一个未完成事务。 (在高并发下,一般会违背隔离性)

3. Durable持久性—由内存+redo log来保证:

一旦一个事务被提交,它应该持久保存,不会因为和其他操作冲突而取消这个事务。很多人认为这意味着事务是持久在磁盘上,但是规范没有特别定义这点。

2.ACID原则靠什么保证?

A原⼦性由undo log⽇志保证,它记录了需要回滚的⽇志信息,事务回滚时撤销已经执⾏成功的sql
C⼀致性⼀般由代码层⾯来保证
I隔离性由MVCC来保证
D持久性由内存+redo log来保证, mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复

1.2 并发事务处理带来的问题

相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高了数据库系统的事务吞吐量,从而可以支持更多的用户操作。

但并发事务处理也会带来一些问题,主要包括以下几种情况。

1.更新丢失(Lost Update)

丢失更新是另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。

例如:

  1. 事务T1将行记录r更新为v1,但是事务T1并未提交
  2. 与此同时,事务T2将行记录r更新为v2,事务T2未提交。
  3. 事务T1提交。
  4. 事务T2提交。

但是,在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。
这是因为,即使是 READ UNCOMMITTED的事务隔离级别,对于行的DML操作,需要对行或其他粗粒度级别的对象加锁。
因此在上述步骤2中,事务T2并不能对行记录r进行更新操作,其会被阻塞,直到事务T1提交。

注意:除隔离级别为串行化之外,其他隔离级别都可能产生更新丢失,特别是可重复读隔离级别下尤其注意。

虽然数据库能阻止丢失更新问题的产生,但是在生产应用中还有另一个逻辑意义的丢失更新问题,而导致该问题的并不是因为数据库本身的问题。

实际上,在所有多用户计算机系统环境下都有可能产生这个问题。
简单地说来,出现下面的情况时,就会发生丢失更新:

事务T1查询一行数据,放入本地内存,并显示给一个终端用户User1。
事务T2也查询该行数据,并将取得的数据显示给终端用户User2。
User1修改这行记录,更新数据库并提交。
User2修改这行记录,更新数据库并提交。

显然,这种覆盖式更新过程用户Uer1的修改更新操作“丢失”了,而这可能会导致一恐怖”的结果。

设想银行发生丢失更新现象,例如一个用户账号中有10000元人民币,他用两个网上银行的客户端分别进行转账操作。第一次转账9000.人民币,因为网络和数据的关系,这时需要等待。
但是这时用户操作另一个网上银行客户端,转账1元,如果最终两笔操作都成功了,用户的账号余款是9999人民币,第一次转的9000民币并没有得到更新,但是在转账的另一个账号却会收到这9000元,这导致的结果就是钱变多,而账不平。

也许有读者会说,不对,我的网银是绑定 USB Key的,不会发生这种情况。是的,通过 USB Key登录也许可以解决这个问题,但是更重要的是在数据库层解决这个问题,避免任何可能发生丢失更新的情况。

注意:更新丢失对应特别注意是更新某一个状态,如并发更新状态(运输中->运输完成)可能会出现更新丢失问题,但若运用到了当前读如Update set num=num+1,则不会产生更新丢失问题,因为会读起到最新的值。

要避免丢失更新发生,需要让事务在这种情况下的操作变成串行化,而不是并行的操作。
即在上述四个步骤的1)中,对用户读取的记录加上一个排他X锁。同样,在步骤2)的操作过程中,用户同样也需要加一个排他X锁。通过这种方式,步骤2)就必须等待一步骤1)和步骤3)完成,最后完成步骤4)。下表所示的过程演示了如何避免这种逻辑上丢失更新问题的产生。

mysql 响应时间 吞吐量 mysql吞吐量有多大_数据库_02

解决更新丢失办法:

悲观锁:在更新前读数据时加锁,使其他事务无法对其数据更新,待当前事务更新后释放锁。
乐观锁:在数据行增加版本号,更新时更新条件传入版本号

2. 脏读(Dirty Reads)

脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。下表的例子显示了一个脏读的例子。

mysql 响应时间 吞吐量 mysql吞吐量有多大_mysql 响应时间 吞吐量_03


在上述例子中,事务的隔离级别进行了更换,由默认的 REPEATABLE READ换成了 READ UNCOMMITTED。因此在会话A中,在事务并没有提交的前提下,会话B中的两次 SELECT操作取得了不同的结果,并且2这条记录是在会话A中并未提交的数据,即产生了脏读,违反了事务的隔离性。

在理解脏读(Dirty Read)之前,需要理解脏数据的概念。

但是脏数据和之前所介绍的脏页完全是两种不同的概念。脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。

而所谓脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。

对于脏页的读取,是非常正常的。
脏页是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终会达到一致性,即当脏页都刷回到磁盘)。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。

脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。

3. 不可重复读(Non-Repeatable Reads)

一个事物读取到另一事物已提交的数据。一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变!这种现象就叫做“不可重复读”。

可以通过下面一个例子来观察不可重复读的情况,如下表所示。

mysql 响应时间 吞吐量 mysql吞吐量有多大_隔离级别_04


在会话A中开始一个事务,第一次读取到的记录是1,在另一个会话B中开始了另一个事务,插入一条为2的记录,在没有提交之前,对会话A中的事务进行再次读取时,读到的记录还是1,没有发生脏读的现象。

但会话B中的事务提交后,在对会话A中的事务进行读取时,这时读到是1和2两条记录。

这个例子的前提是,在事务开始前,会话A和会话B的事务隔离级别都调整为 READ COMMITTED。

一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库厂商(如 Oracle、 Microsoft SQL Server)将其数据库事务的默认隔离级别设置为READ COMMITTED,在这种隔离级别下允许不可重复读的现象。

在Read committed隔离级别下解决不可重复读

解决不可重复读的方法:在SQL语句中加读锁,也许是纪录锁,间隙锁,又或者是表锁。

这样可避免当前事务读起到其他事务修改的数据,因为其他事务已经被阻塞了,只有当前事务释放了锁,其他事务才能对其数据修改。

4.幻读(Phantom Problen)

Phantom Problen是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。

幻读的重点在于范围内的新增或者删除。

同样的条件, 第1次和第2次读出来的记录数不一样。幻读和不可重复读都是读取了另一条已经提交的事务(这点与脏读不同)

mysql 响应时间 吞吐量 mysql吞吐量有多大_sql_05

案例1

最后,我来简单说一下“幻读”。在实际业务中,很少能遇到幻读,即使遇到,也基本不会影响到数据准确性,所以你简单了解一下即可。在 RR 隔离级别下,我们开启一个事务,之后直到这个事务结束,在这个事务内其他事务对数据的更新是不可见的,这个我们刚刚讲过。

比如我们在会话 A 中开启一个事务,准备插入一条 ID 为 1000 的流水记录。查询一下当前流水,不存在 ID 为 1000 的记录,可以安全地插入数据。

mysql 响应时间 吞吐量 mysql吞吐量有多大_数据_06


这时候,另外一个会话抢先插入了这条 ID 为 1000 的流水记录。

mysql 响应时间 吞吐量 mysql吞吐量有多大_数据_07


然后会话 A 再执行相同的插入语句时,就会报主键冲突错误,但是由于事务的隔离性,它执行查询的时候,却查不到这条 ID 为 1000 的流水,就像出现了“幻觉”一样,这就是幻读。

mysql 响应时间 吞吐量 mysql吞吐量有多大_数据库_08

案例2

幻读自己手写演示:

DROP TABLE IF EXISTS `tag`;
CREATE TABLE `tag` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '租户ID',
  `biz_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '标签ID',
  `category_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '类目ID',
  `tag_name` varchar(64) DEFAULT NULL COMMENT '标签名称',
  `tag_desc` varchar(255) NOT NULL DEFAULT '' COMMENT '标签描述',
  `release_status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '发布状态 0:未发布 1:已发布',
  `create_user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建人ID',
  `update_user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '更新人ID',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '删除标识',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uk_biz_id` (`biz_id`) USING BTREE,
  KEY `idx_tenant_tag_id` (`tenant_id`,`biz_id`) USING BTREE COMMENT '标签ID索引'
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COMMENT='标签';

-- ----------------------------
-- Records of tag
-- ----------------------------
BEGIN;
INSERT INTO `tag` VALUES (1, 1101, 100000058001, 100000057004, 'dwtest', '戴维test', 1, 1, 1, '2020-12-09 19:22:20', '2021-02-23 10:29:46', 0);
INSERT INTO `tag` VALUES (2, 1102, 100000060001, 100000057004, 'dwtest2', '戴维test', 1, 1, 1, '2020-12-10 11:38:58', '2021-02-23 10:48:17', 0);
INSERT INTO `tag` VALUES (5, 1102, 100000060004, 100000057005, '戴维test', '戴维test', 1, 1, 12345678, '2020-12-10 11:47:50', '2021-02-23 10:48:17', 0);
INSERT INTO `tag` VALUES (6, 1102, 100000062001, 100000057005, 'FFFFEEEE', 'fsdfsdfsf', 0, 1, 1, '2020-12-10 16:13:44', '2021-02-23 10:48:17', 1);
INSERT INTO `tag` VALUES (7, 1102, 100000062002, 100000057005, 'ASW', 'RRR', 0, 1, 1, '2020-12-10 16:59:55', '2021-02-23 10:48:17', 1);
INSERT INTO `tag` VALUES (8, 1102, 100000062003, 100000057005, '福建省打飞机水电费福', '福建省打飞机水电费福建省打飞机水电费福建省打飞机水电费福建省打飞机水电费', 0, 1, 1, '2020-12-10 17:29:24', '2021-02-23 10:48:17', 1);
INSERT INTO `tag` VALUES (9, 1102, 100000064001, 100000057005, 'dwtest4', '戴维test', 1, 1, 1, '2020-12-10 17:31:25', '2021-02-23 10:48:17', 0);
INSERT INTO `tag` VALUES (10, 1102, 100000079001, 100000057005, '水电费两居室的弗兰克', '', 1, 1, 1, '2020-12-11 17:04:59', '2021-02-23 10:48:17', 0);
INSERT INTO `tag` VALUES (11, 1102, 100000079002, 100000057005, '123', '1231', 1, 1, 1, '2020-12-11 17:06:28', '2021-02-23 10:48:17', 0);
INSERT INTO `tag` VALUES (12, 1102, 100000079003, 100000057005, '1231', '123', 1, 1, 1, '2020-12-11 17:19:38', '2021-02-23 10:48:17', 0);
COMMIT;

#查看 MySQL 隔离级别
SELECT @@tx_isolation

#查看是否自动提交
show variables like ‘autocommit’;

#修改autocommit
set autocommit=off;

会话A:

SELECT @@tx_isolation;

show variables like 'autocommit';

set autocommit=off;

start transaction;

SELECT * FROM `test`.`tag` WHERE `id` = '1' ;

UPDATE `test`.`tag` SET `tenant_id` = 1102 WHERE `id` > 1;

SELECT * FROM `test`.`tag` WHERE `id` >1 AND id<5;
COMMIT;



会话B:

UPDATE `test`.`tag` SET `tenant_id` = 1101 WHERE `id` = 1;
SELECT * FROM `test`.`tag` WHERE `id` = '1' ;

INSERT INTO `test`.`tag`(`id`, `tenant_id`, `biz_id`, `category_id`, `tag_name`, `tag_desc`, `release_status`, `create_user_id`, `update_user_id`, `create_time`, `update_time`, `deleted`) 
VALUES (4, 1100, 100000070001, 100000057004, 'dwtest2', '戴维test', 1, 1, 1, '2020-12-10 11:38:58', '2020-12-19 13:52:58', 0);

注意:

1.如果会话A不执行:

UPDATE `test`.`tag` SET `tenant_id` = 1102 WHERE `id` > 1;

将不会出现幻读,即查询的行数还是原来的,不会增加id=4的行数。
因为mysql底层已经使用mvcc机制解决了幻读,通过使用当前读才能读取到其他事务的数据

幻读和不可重复读的边界

mysql官网并没有明确定义两者的不同,如果实在要区分,正如大部分博客中所说:

不可重复读更注重的是具体某行数据两次查询对应的值不一样了,
而幻读针对的是某一范围下,两次查询对应的纪录数不一样了。

在可重复读隔离级别下,对于某一行的数据的修改,当前事务不会受其他事务影响,但对这一行的其他行进行增加或者删除,当前事务是会读取到其他事务新增或者删除的行的结果的。

如果你从控制的角度来看:

对于不可重复读, 只需要锁住满足条件的记录。
对于幻读, 要锁住满足条件及其相近的记录。

思考

1.解决幻读的方法

1.mysql底层使用了mvcc机制,解决了幻读的大多数场景。

但当前事务如果使用了更新语句且恰恰满足其他事务插入的那条数据所在的行,此时mysql采用的是当前读,会读到其他事务提交的数据。此时再次使用同范围的查询,会出现其他事务插入的数据,即出现幻读。此时可以使用间隙锁解决幻读,使其他事务无法读所在间隙插入新的数据。

2.使用间隙锁解决幻读

InnoDB存储引擎采用 Next-Key Locking的算法避免 Phantom Problem。对于上述的SQL语句 SELECT * FROM t WHERE a>2 FOR UPDATE,其锁住的不是5这单个值,而是对(2,+∞)这个范围加了X锁。因此任何对于这个范围的插入都是不被允许的,从而避免 Phantom Problem。

InnoDB存储引擎默认的事务隔离级别是 REPEATABLE READ,在该隔离级别下,其采用 Next-Key Locking的方式来加锁。

而在事务隔离级别 READ COMMITTED下,其仅采用 Record Lock

2.长事务系列问题

为什么建议你尽量不要使用长事务。

1.磁盘空间维度
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。

2.长事务占用锁资源

长事务还占用锁资源,也可能拖垮整个库。

如下面场景
1.当出现并发调用到配置事务的接口时,会不断占有连接池资源而不释放,可能造成其他接口获取不到数据库链接和发生超时错误等,系统的吞吐量就大大下降甚至不可用。

如何避免写出长事务
从应用开发端来看
1.读写业务逻辑分离。

确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。我见过有些是业务并没有这个需要,但是也把好几个 select 语句放到了事务中。这种只读事务可以去掉。

如何降低长事务的影响

从应用开发端来看
1.设置事务超时时间

从数据库端来看:

1.监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;

2.Percona 的 pt-kill 这个工具不错,推荐使用;

3.在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;

4.如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。

三、数据库隔离级别

1.数据库的四种隔离级别

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

在谈隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。

在高并发的情况下,要完全保证其ACID特性是非常困难的,除非把所有的事物串行化执行,但带来的负面的影响将是性能大打折扣。
很多时候我们有些业务对事物的要求是不一样的,所以数据库中设计了四种隔离级别,供用户基于业务进行选择。

mysql 响应时间 吞吐量 mysql吞吐量有多大_数据_09

下面我逐一为你解释:

  • 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

Read committed 违背了I,即隔离性。
Repeatable read 遵守了I,即隔离性。

其存在的意义是为了保证即使在并发情况下也能正确的执行crud操作。

主流数据库的默认隔离级别:

Oracle中默认级别是 Read committed
mysql 中默认级别 Repeatable read。

查看mysql 的默认隔离级别命令:SELECT @@tx_isolation

因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为“读提交”。

实例

假设存在一张表,里面只有一个字段和一条记录,值是 1,现在发生以下的操作

mysql 响应时间 吞吐量 mysql吞吐量有多大_数据_10

针对不同的隔离级别,V1、V2、V3 读到的值不同。

  • 在「读未提交」的隔离级别下,由于 t4 时刻事务 B 将值改成了 2,虽然 B 还没提交事务,但是此时的修改对其他事务是可见的,所以V1、V2、V3 查询到的值都是 2。
  • 在「读提交」的隔离级别下,t4 时刻修改了值,但是在 t5 时刻,事务 B 还没有提交,此时事务 A 读取到的值还是老的值,所以 V1 是1,而在 t7 时刻,由于事务 B 已经在 t6 时刻提交了,此时事务 B 所做的修改对其他的事务都可见,所以事务 A 在 t7时刻能看到事务 B 的修改,此时 V2 的值为 2,当然 V3 的值也为 2。
  • 在「可重复读」的隔离级别下,遵循 “事务在执行期间看到的数据必须是前后一致” 的要求,所以无论事务 B 是否修改值,也无论事务 B是否提交,事务 A 在没提交前读到的值都是相同的,即 V1 和 V2 的值都是 1,当 A 事务提交后,再次查询时,事务 B 的修改就能被A 看到了,所以 V3 的值为 2。
  • 在「串行化」的隔离级别下,当事务 B 在 t4 时刻执行更新时,由于与事务 A 操作的是同一行,且出现读写冲突,此时事务 B 被会阻塞,等待事务 A 执行完毕后,再执行事务 B,所以 V1 和 V2 的值是 1,V3 的值是 2。

2.数据库的事务隔离实现原理

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。

这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;
而“串行化”隔离级别下直接用加锁的方式来避免并行访问。

1 可重复读的实现原理

2 可重复读解决了哪些问题?

1.可重复读的核心就是一致性读(consistent read);保证多次读取同一个数据时, 其值都和事务开始时候的内容是一致, 禁止读取到别的事务未提交的数据, 但会造成幻读。
2. 查询只承认在事务启动前就已经提交完成的数据。而事务更新数据的时候, 只能用当前读。 如果当前的记录的行锁被其他事务占用的话, 就需要进入锁等待。
3.可重复读解决的是重复读的问题, 可重复读在快照读的情况下是不会有幻读, 但当前读的时候会有幻读。

1.4 事务分类

从事务理论的角度来看,可以把事务分为以下五种类型:

  1. 扁平事务(Flat Transactions):全部回滚
  2. 带有保存点的扁平事务(Flat Transactions with Savepoints):事务可以回滚到某个保存点,保存点之前的不会回滚,保存点之后的需要回滚
  3. 链事务(Chained Transactions):下一个事务可以直接读取上一个事务的结果,就好像一个事务中进行的一样。
  4. 嵌套事务(Nested Transactions):即事务中又嵌入到了子事务。
  5. 分布式事务(Distributed Transactions):即只会在分布式环境下存在的事务

扁平事务 是事务类型中最简单的一种,在实际生产环境中,也是使用最频繁的事务。

带有保存点的扁平事务和链事务的区别:带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限当前事务,即只能恢复到最近的一个保存点。

二、mysql事务底层实现

redo log保证事务的原子性和持久性,undo log保证事务的一致性

2.1 redo log(重做日志)

redo log主要用来记录事务提交后的信息状态。

在innoDB的存储引擎中,事务日志通过重做(redo)日志和innoDB存储引擎的日志缓冲(InnoDB Log Buffer)实现。

事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的“日志先行”(Write-Ahead Logging)。

当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。

此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。

mysql 响应时间 吞吐量 mysql吞吐量有多大_mysql 响应时间 吞吐量_11

注意:

1.当事务提交时,会调用fsync接口对redo log进行刷盘,如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。

2.既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

思考

1.Buffer Pool的意义?

InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool)。Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲。

当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool(与缓存和磁盘的读思想一致);

当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中

2.redo log 有什么作用呢?

因为 MySQL 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存储到 Buffer Pool 中,然后通过后台线程去做缓冲池和磁盘之间的同步。

那么问题来了

如果还没来得及同步,发生了崩溃或者宕机,还没来得及执行上图中红色的操作,这样就会导致丢失部分已提交事务的修改信息!当然对于未提及的事务信息,如果发生崩溃,redo log也无能为力。

所以引入了 redo log 来记录已经成功提交事务的修改信息,并且会把 redo log 持久化到磁盘,系统重启之后再读取 redo log 恢复最新数据。

3.redo log与binlog的区别?

1.层次不同 :redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。

2.作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;
binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。

3.内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。

4.写入时机不同:binlog在事务提交时写入;redo log并非事务提交时写入,它是支持并发的,故并不会按照事务的顺序写入:

2.2 undo log

undo log主要为事务的回滚服务。

在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。

undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作

mysql 响应时间 吞吐量 mysql吞吐量有多大_数据库_12

所以每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。
假如由于系统错误或者 rollback 操作回滚的话,可以根据 undo log 的信息进行回滚到没被修改前的状态。

参考文献

1.MySQL事务(Transaction)及其ACID属性、并发事务处理带来的问题讲解
https://www.2cto.com/database/201803/726901.html

2.书籍:《MySQL技术内幕:InnoDB存储引擎(第2版)》

3.MySQL实战45讲 事务到底是隔离的还是不隔离的? https://time.geekbang.org/column/article/70562