从这里开始,跟我一起搞懂 MySQL!
提前申明:《MySQL 基础实战》系列是学习极客时间林晓斌的《MySQL实战45讲》的整理和总结,希望大家仅做为学习使用!
- 架构示意图
- Server 层:包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
- binlog(归档日志)
- Server层日志;
- 逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- 追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
- binlog(归档日志)
- 存储引擎层:负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。
- redo log
- 引擎层 InnoDB 特有的日志;
- 物理日志,记录的是“在某个数据页上做了什么修改”
- 循环写的,空间固定会用完 (比喻 擦除粉板记录到账本)
- redo log
- Server 层:包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
- WAL(Write Ahead Log ,译为进行写操作前先写日志。)
- 逻辑:
- 它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
- 具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成(返回给客户端成功)了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。
- redo log:
- 擦除粉板记录到账本
- write pos 是当前记录的位置
- checkpoint 是当前要擦除的位置,也是往后推移并且循环的,如果 write pos 追上 checkpoint,表示“粉板”满了,擦除记录前要把记录更新到数据文件
- crash-safe:即保证即使数据库发生异常重启,之前提交的记录都不会丢失
只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
- 两阶段提交:执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程
mysql> create table T(ID int primary key, c int);
mysql> update T set c=c+1 where ID=2;
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
- 为什么一定要两阶段提交?
- 由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
- 先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1(应该是)。 但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。 然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
- 先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0(应该是)。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
- 简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
- 恢复:redolog和binlog具有关联行,在恢复数据时,redolog用于恢复主机故障时的未更新的物理数据,binlog用于备份操作。每个阶段的log操作都是记录在磁盘的,在恢复数据时
- redolog还没到prepare:就认为该事务没有执行完
如果redolog是prepare,则需要查询对应的
binlog事务是否成功(是/否),决定是回滚还是执行。
- binlog 成功:恢复
- binlog 失败:回滚(通过 undo log 回滚。脏页一直在刷,更新了脏页,但事务没提交就宕机了,所以需要回滚)。
- redolog 状态为commit则说明binlog也成功,直接恢复数据;
- 扩容:数据库的扩容,即增加备份库来提高系统读数据库的能力的时候,常采取“全量备份 + binlog”实现。假如binlog和redo log记录的事务的逻辑状态不一致,则会导致严重的主从数据库数据不一致问题。
- 恢复:redolog和binlog具有关联行,在恢复数据时,redolog用于恢复主机故障时的未更新的物理数据,binlog用于备份操作。每个阶段的log操作都是记录在磁盘的,在恢复数据时
- 由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
- 为什么一定要两阶段提交?
- 事务隔离
- 事务特性:提到事务,你肯定会想到 ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),今天我们就来说说其中 I,也就是“隔离性”。
隔离性与隔离级别:当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。
- 问题 (好例子)
- 脏读:脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。
- 不可重复读:通常针对数据更新(UPDATE)操作。
- 可重复读:可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。
- 不可重复读:
- 是指在事务1内,读取了一个数据,事务1还没有结束时
- 事务2也访问了这个数据,修改了这个数据(UPDATE),并提交。
- 紧接着,事务1又读这个数据。由于事务2的修改,那么事务1两次读到的的数据可能是不一样的,因此称为是不可重复读。
- 幻读:幻读是针对数据插入(INSERT)操作来说的。
- 事务A 在读取某个范围内的记录时 (事务1查询id<10的记录时,返回了2条记录)
- 此时事务B 又在该范围内插入(INSERT)了新的记录 (事务2插入了一条id为3的记录,并提交)
- 再次读取该范围的记录时,会产生幻行,这就叫幻读。(接着事务1查询id<10的记录时,返回了3条记录)
- 什么是幻读解释?
- 例子1:
--样例,先建表和插入语句,InnoDB 的默认事务隔离级别是可重复读(下面的解释也基于此)
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB; insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);下面先来看一下这个场景(注意:这是我假设的一个场景):图 1 假设只在 id=5 这一行加行锁
- 可以看到,session A 里执行了三次查询,分别是 Q1、Q2 和 Q3。它们的 SQL 语句相同,都是 select * from t where d=5 for update。这个语句的意思你应该很清楚了,查所有 d=5 的行,而且使用的是当前读,并且加上写锁。现在,我们来看一下这三条 SQL 语句,分别会返回什么结果。
- Q1 只返回 id=5 这一行;
- 在 T2 时刻,session B 把 id=0 这一行的 d 值改成了 5,因此 T3 时刻 Q2 查出来的是 id=0 和 id=5 这两行;
- 在 T4 时刻,session C 又插入一行(1,1,5),因此 T5 时刻 Q3 查出来的是 id=0、id=1 和 id=5 的这三行。
- 其它注意
- 其中,Q3 读到 id=1 这一行的现象,被称为“幻读”。也就是说,幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
- 这里,我需要对“幻读”做一个说明:
- 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
- 上面 session B 的修改结果,被 session A 之后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。
- 问题 (好例子)
- 解决方案
- 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交(read committed):一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化(serializable ):顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
- 解决方案
4 种隔离级别对这三个问题的解决程度。
可能问题(多个事务执行时) 隔离级别(解决方案)
脏读 不可重复读 幻读 读未提交 可能 可能 可能 读提交 不可能 可能 可能 可重复读 不可能 不可能 可能 串行化 不可能 不可能 不可能 只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。
- 在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- 在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
- MVCC视图实在开始事务的时候就创建好了,这个视图会一直使用,直到该事务结束
- 在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
- MVCC视图会在每一个语句前创建一个,所以在RC级别下,一个事务是可以看到另外一个事务已经提交的内容,因为它在每一次查询之前都会重新给予最新的数据创建一个新的MVCC视图。
- 这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;(直接返回最小行数据)
- 而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
- 在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
- 不同数据库的默认隔离级别,从不同数据库迁移过来的时候注意
- Oracle:读提交
- MySQL:可重复读(阿里:读提交,查看另一篇文章)show variables like 'transaction_isolation';
- 事务隔离的实现是由多版本并发控制(MVCC),通过undo log版本链和read-view实现事务隔离(MVCC)。
- 另外在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。即除了记录变更记录,还会记录一条变更相反的回滚操作记录,前者记录在redo log,后者记录在undo log。
- 假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
这一点其实就是一个版本迭代记录,类似功能的实现的还有GIT等等这种版本管理工具,只有将每次修改都记录下来,才能方便回滚到指定的每次提交。
当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。
【在可重复读隔离级别中,表中的数据其实已经改变,在前面的视图里,需要查找某条记录时,是通过取当前数据,再取视图对应的回滚段回滚到该视图的值。】
同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的
【因为有行锁保证不会出现一个事务把4改到5,然后另外一个事务回滚的情况】
- 你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。
- 什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。当没有比回滚日志更早的读视图(可重复读隔离级别中,read-view读视图在事务开启时创建,事务提交的时候删除)的时候,这个数据不会再有谁驱使它回滚了,这个回滚日志也就失去了用武之地,可以删除了
- 基于上面的说明,我们来讨论一下为什么建议你尽量不要使用长事务。
- 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。
- 除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个我们会在后面讲锁的时候展开。
- 如前面所述,长事务有这些潜在风险,我当然是建议你尽量避免。其实很多时候业务开发同学并不是有意使用长事务,通常是由于误用所致。MySQL 的事务启动方式有以下几种:
- 1. 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
- 2. set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
- 有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。因此,我会建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
- autocommit=1;表示MySQL自动开启和提交事务。 比如执行一个update语句,语句只完成后就事务自动提交了。不需要显示的使用begin、commit来开启和提交事务。 所以,当我们需要对某些操作使用事务的时候,手动的用begin、commit来开启和提交事务。阿里的数据库 show variables like 'autocommit';
- 有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。因此,我会建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
- 有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。因此,我会建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
- 任何隔离级别,数据操作都有两种:快照读(普通select)和当前读(select for update, update 等)。
- 快照读采用MVCC
- 当前读采用锁。不同隔离级别下同个手段的实现也各有差异,比如同样当前读采用锁,读已提交隔离级别下,写加排它锁,事务结束释放,读加共享锁,读完立刻释放,这样解决了脏读问题,但存在不可重复读问题;而在可重复读隔离级别下,写加排它锁,事务结束释放,读加共享锁,事务结束释放,这样解决了不可重复读问题,但存在幻读问题。
- for update, lock in share mode,update,delete,insert都是当前读的规则,就是读取最新的已经提交的数据。 update先查询再修改,这里的查询就是使用的当前读。 delete要先查询再删除,这里的查询也是要当前读。 插入的时候,要判断主键是否已经存在、是否违反唯一约束,此时查看主键是否存在的查询也是当前读。
- 事务到底是隔离的还是不隔离的?
- 事务ID:InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
- 每行数据也都是有多个版本的,每次事务更新该行数据的时候,都会生成一个新的数据版本,
- 新的数据版本包含(以如下V4为例):图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。
- 本次数据的值
- 事务id
- 还有一个引用(指向上一个数据版本) -- redolog
- 语句更新会生成 undo log(回滚日志)吗?那么,undo log 在哪呢?undo log可以看成是一个回滚日志, 记录redo log的反操作.
- 实际上,图 2 中的三个虚线箭头,就是 undo log;
- 而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
- 如图:就是一个记录被多个事务连续更新后的状态)
- 新的数据版本包含(以如下V4为例):图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。
- 事务的启动方式:
- 如前面所述,长事务有这些潜在风险,我当然是建议你尽量避免。其实很多时候业务开发同学并不是有意使用长事务,通常是由于误用所致。MySQL 的事务启动方式有以下几种:
- 显式启动事务语句:
- begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。 (不管autocommit 是1还是0 。 只有当commit数据才会生效,ROLLBACK后就会回滚)
- begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动
- 一致性视图:是在执行第一个快照读语句时创建的
- start transaction with consistent snapshot; 配套的提交语句是 commit,回滚语句是 rollback。(不管autocommit 是1还是0 。 只有当commit数据才会生效,ROLLBACK后就会回滚)
- 马上启动事务
- 一致性视图:是在执行 start transaction with consistent snapshot 时创建的
- 这里需要说明一下,“start transaction with consistent snapshot; ”的意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于普通的 start transaction。
- 自动启动事务:set autocommit=0,这个命令会将这个线程的自动提交关掉。
- 意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
- 有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。
- begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。 (不管autocommit 是1还是0 。 只有当commit数据才会生效,ROLLBACK后就会回滚)
- 隐式启动事务语句:set autocommit=1(mysql默认为1,可以设置会话级别(session级别)为0) 通过显式语句的方式来启动事务。
- autocommit=1;表示MySQL自动开启和提交事务。 比如执行一个update语句,语句只完成后就自动提交了。不需要显示的使用begin、commit来开启和提交事务。
- 所以,当我们需要对某些操作使用事务的时候,手动的用begin、commit来开启和提交事务。
- 显式启动事务语句:
- “快照”在MVCC里是如何工作的
- 在 MySQL 里,有两个“视图”的概念:
- 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。
- 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。(逻辑结构是:就是通过高低水位,数据版本号,undo日记来进行判断数据可见不可见,达到MVCC目的)
- 一致性读视图:InnoDB 为每个事务构造了一个数组(一致性视图),用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。
- 高低水位:
- 低水位:该数组里面事务ID的最小值(即该事务启动瞬间,当前活跃事务ID的最小值)
- 高水位:即该事务启动瞬间,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
- 高低水位:
- 在 MySQL 里,有两个“视图”的概念:
- 以“可重复读”隔离级别为例:
- 通俗理解:
- 一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;
- 如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。 当然,如果“上一个版本”也不可见,那就得继续往前找。
- 还有,如果是这个事务自己更新的数据,它自己还是要认的。
- 通俗理解2:一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
- 一致性读视图数组:[96, 98, 99, 101];对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
- 如果落在黄色部分,那就包括两种情况
- a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
- b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
- 通俗理解:
- 一致性读视图(隔离级别涉及 可重复读)、读提交,行锁 区别,如下例子解读:
- 图1、图2:都是一致性读视图的例子
- 假设:
- 事务 A 开始前,系统里面只有一个活跃事务 ID 是 99;
- 事务 A、B、C 的ID分别是 100、101、102,且当前系统里只有这四个事务;
- 三个事务开始前,(1,1)这一行数据的 row trx_id 是 90。
- 图1:事务A 最终读到;好,现在事务 A 要来读数据了,它的视图数组是[99,100]。当然了,读数据都是从当前版本读起的。所以,事务 A 查询语句的读数据流程是这样的:
- 找到 (1,3) 的时候,判断出 row trx_id=101,比高水位大,处于红色区域,不可见;
- 接着,找到上一个历史版本,一看 row trx_id=102,比高水位大,处于红色区域,不可见;
- 再往前找,终于找到了(1,1),它的 row trx_id=90,比低水位小,处于绿色区域,可见。这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。
- 图2:事务A 最终读到2、事务B读到3
- 这时,事务 A 的查询语句的视图数组是在执行这个语句的时候创建的,时序上 (1,2)、(1,3) 的生成时间都在创建这个视图数组的时刻之前。
- 但是,在这个时刻:(1,3) 还没提交,属于情况 1,不可见;(1,2) 提交了,属于情况 3,可见。
- 所以,这时候事务 A 查询语句返回的是 k=2。显然地,事务 B 查询结果 k=3。
- 假设:
- 图3:当前读:事务 B 的视图数组是先生成的,之后事务 C 才提交,不是应该看不见 (1,2) 吗,怎么能算出 (1,3) 来?
- 是的,如果事务 B 在更新之前查询一次数据,这个查询返回的 k 的值确实是 1。但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务 C 的更新就丢失了。因此,事务 B 此时的 set k=k+1 是在(1,2)的基础上进行的操作。
- 所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。因此,在更新的时候,当前读拿到的数据是 (1,2),更新后生成了新版本的数据 (1,3),这个新版本的 row trx_id 是 101。
- 图4:行锁
- 事务 C’的不同是,更新后并没有马上提交,在它提交前,事务 B 的更新语句先发起了。前面说过了,虽然事务 C’还没提交,但是 (1,2) 这个版本也已经生成了,并且是当前的最新版本(??有疑问??)。那么,事务 B 的更新语句会怎么处理呢?
- 分析
- 事务 C’没提交,也就是说 (1,2) 这个版本上的写锁还没释放。
- 而事务 B 是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到事务 C’释放这个锁,才能继续它的当前读。
- 行锁的 “两阶段锁协议”:在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放
- 分析
- 事务 C’的不同是,更新后并没有马上提交,在它提交前,事务 B 的更新语句先发起了。前面说过了,虽然事务 C’还没提交,但是 (1,2) 这个版本也已经生成了,并且是当前的最新版本(??有疑问??)。那么,事务 B 的更新语句会怎么处理呢?
- 图1、图2:都是一致性读视图的例子
- 索引的常见模型
- 常见模型:哈希表、有序数组、搜索树(二叉树、平衡二叉树、红黑树、B树、B+树),其它有跳表、LSM 树等
- 哈希表
- 图中四个 ID_card_n 的值并不是递增的(非有序)
- 这样做的好处是增加新的 User 时速度会很快,只需要往后追加
- 但缺点是,因为不是有序的,所以哈希索引做区间查询的速度是很慢的。
- 例子:如果你现在要找身份证号在[ID_card_X, ID_card_Y]这个区间的所有用户,就必须全部扫描一遍了。
- 哈希表这种结构适用于只有等值查询的场景,比如 Memcached 及其他一些 NoSQL 引擎。
- 等值查询就是用等号来匹配查询结果,分为单条件查询、多条件查询,
- 与等值查询对应的是模糊查询、范围查询
- 你要查 ID_card_n2 对应的名字是什么,处理步骤就是:
- 首先,将 ID_card_n2 通过哈希函数算出 N;
- 然后,按顺序遍历,找到 User2。
- 图中四个 ID_card_n 的值并不是递增的(非有序)
- 有序数组
- 等值查询和范围查询:性能就都非常优秀。这里我们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。
- 这时候如果你要查 ID_card_n2 对应的名字,用二分法就可以快速得到,这个时间复杂度是 O(log(N))。
- 同时很显然,这个索引结构支持范围查询。你要查身份证号在[ID_card_X, ID_card_Y]区间的 User,可以先用二分法找到 ID_card_X(如果不存在 ID_card_X,就找到大于 ID_card_X 的第一个 User),然后向右遍历,直到查到第一个大于 ID_card_Y 的身份证号,退出循环。
- 更新:在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。
- 所以,有序数组索引只适用于静态存储引擎,比如你要保存的是 2017 年某个城市的所有人口信息,这类不会再修改的数据。
- 等值查询和范围查询:性能就都非常优秀。这里我们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。
- 搜索树
- 二叉树、平衡二叉树、红黑树、B树、B+树 不断演进和优化
- 平衡二叉树:你可以想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。
- 问题:为什么 树高20的话,就要遍历20个块 ?
- 解答:因为是二叉树结构,每次指针查找很大概率是触发随机磁盘读(比如很难刚好碰上一个节点和他的左右儿子刚好相邻)
- N叉树:(N是多少 取决于数据块大小)
- 以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200(1200叉树)。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。
- 考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。
- 其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。
- N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。
- 1200怎么来的?
MySql默认一个节点的长度为16K,一个整数(bigint)字段索引的长度为 8B,另外每个索引还跟着6B的指向其子树的指针;所以16K/14B ≈ 1170
以每一层只进行一次磁盘IO,也就是在『一页』的大小16K内进行计算,一个节点14Byte,16KB * 1024 / 14Byte = 1170没毛病 (约等于1200)
- 以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200(1200叉树)。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。
- InnoDB 的索引模型
- 1.在 InnoDB 中,每一张表其实就是多个 B+ 树,即一个主键索引树和多个非主键索引树。
- 例子:我们有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引。
mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index (k))engine=InnoDB; - 表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
- 从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
- 主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
- 非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
- 基于主键索引和普通索引的查询有什么区别?
- 如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
- 如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
- 从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
- 索引维护
- B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护
- 以下面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂 (逆向过程:合并)
既然InnoDB对数据的存储必须依赖于主键,那么对于没有创建主键的表,InnoDB对聚簇索引如何处理?InnoDB对聚簇索引处理如下:
- - 如果定义了主键,那么InnoDB会使用主键作为聚簇索引
- - 如果没有定义主键,那么会使用第一非空的唯一索引(NOT NULL and UNIQUE INDEX)作为聚簇索引
- - 如果既没有主键也找不到合适的非空索引,那么InnoDB会自动生成一个不可见的名为ROW_ID的列名为GEN_CLUST_INDEX的聚簇索引,该列是一个6字节的自增数值,随着插入而自增
- 你可能在一些建表规范里面见到过类似的描述,要求建表语句里一定要有自增主键。当然事无绝对,我们来分析一下哪些场景下应该使用自增主键,而哪些场景下不应该。
- 自增主键:
- 确保有序:插入性能更佳
- 自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。也就是说,自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
- 有业务逻辑的字段做主键:则往往不容易保证有序插入,这样写数据成本相对较高。
- 节约存储空间:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小
- 假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
- 由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。
- 假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
- 确保有序:插入性能更佳
- 有没有什么场景适合用业务字段直接做主键的呢?
- 比如,有些业务的场景需求是这样的:只有一个索引;该索引必须是唯一索引。
- 你一定看出来了,这就是典型的 KV 场景。由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
- 这时候我们就要优先考虑上一段提到的“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
- 你一定看出来了,这就是典型的 KV 场景。由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
- 比如,有些业务的场景需求是这样的:只有一个索引;该索引必须是唯一索引。
- 自增主键:
联合索引在B+树上的存储结构及数据查找方式(查看)
构造例子
1 CREATE TABLE `t1` (
2 `a` int(11) NOT NULL AUTO_INCREMENT,
3 `b` int(11) NULL DEFAULT NULL,
4 `c` int(11) NULL DEFAULT NULL,
5 `d` int(11) NULL DEFAULT NULL,
6 `e` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
7 PRIMARY KEY (`a`) USING BTREE,
8 INDEX `index_bcd`(`b`, `c`, `d`) USING BTREE
9 ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
10
11 -- ----------------------------
12 -- Records of t1
13 -- ----------------------------
14 INSERT INTO `t1` VALUES (1, 13, 12, 4, 'dll');
15 INSERT INTO `t1` VALUES (2, 1, 5, 4, 'doc');
16 INSERT INTO `t1` VALUES (3, 13, 16, 5, 'img');
17 INSERT INTO `t1` VALUES (4, 12, 14, 3, 'xml');
18 INSERT INTO `t1` VALUES (5, 1, 1, 4, 'txt');
19 INSERT INTO `t1` VALUES (6, 13, 16, 1, 'exe');
20 INSERT INTO `t1` VALUES (7, 5, 3, 6, 'pdf');T1表的表数据如下:(a是自增列, (`b`, `c`, `d`) 是联合索引)
-
- bcd联合索引在B+树上的结构图:联合索引(b,c,d)也会生成一个索引树,同样是B+树的结构,只不过它的data部分存储的是联合索引所在行的主键值(下图叶子节点紫色背景部分);存储规则如下
- 存储引擎会首先根据第一个索引列排序,如上图我们可以单看第一个索引列,横着看,如,1 1 5 12 13....他是单调递增的;
- 如果第一列相等则再根据第二列排序
- 依次类推就构成了上图的索引树,上图中的b列都等于1时,则根据c排序,此时c列也相等则按d列排序,如:1 1 4 ,1 1 5,c=4在c=5前面,以及13 12 4,13 16 1,13 16 5就可以说明这种情况。
- 联合合索引的查找方式
- 比如 select * from T1 where b = 12 and c = 14 and d = 3; 也就是T1表中a列为4的这条记录。
- 存储引擎首先从根节点(一般常驻内存)开始查找,第一个索引的第一个索引列为1,12大于1,第二个索引的第一个索引列为56,12小于56,于是从这俩索引的中间读到下一个节点的磁盘文件地址,从磁盘上Load这个节点,通常伴随一次磁盘IO,然后在内存里去查找。当Load叶子节点的第二个节点时又是一次磁盘IO,比较第一个元素,b=12,c=14,d=3完全符合,于是找到该索引下的data元素即ID值,再从主键索引树上找到最终数据。
- 比如 select * from T1 where b = 12 and c = 14 and d = 3; 也就是T1表中a列为4的这条记录。
- 最左前缀匹配原则
- 我们创建的index_bcd(b,c,d)索引,相当于创建了(b)、(b、c)(b、c、d)三个索引 (没有(b、d) 索引 ),看完下面你就知道为什么相当于创建了三个索引。
- 我们看,联合索引是首先使用多列索引的第一列构建的索引树,用上面idx_t1_bcd(b,c,d)的例子就是优先使用b列构建,当b列值相等时再以c列排序,若c列的值也相等则以d列排序。我们可以取出索引树的叶子节点看一下
索引的第一列也就是b列可以说是从左到右单调递增的
但我们看c列和d列并没有这个特性,它们只能在b列值相等的情况下这个小范围内递增,
如第一叶子节点的第1、2个元素和第二个叶子节点的后三个元素。
由于联合索引是上述那样的索引构建方式及存储结构,所以联合索引只能从多列索引的第一列开始查找。所以如果你的查找条件不包含b列如(c,d)、(c)、(d)是无法应用缓存的,以及跨列也是无法完全用到索引如(b,d),只会用到b列索引。
- 我们创建的index_bcd(b,c,d)索引,相当于创建了(b)、(b、c)(b、c、d)三个索引 (没有(b、d) 索引 ),看完下面你就知道为什么相当于创建了三个索引。
- 例子:电话本一样,有姓和名以及电话,姓和名就是联合索引。在姓可以以姓的首字母排序,姓的首字母相同的情况下,再以名的首字母排序。如:
- 我们知道姓和名是很快就能够
- 从姓的首字母索引定位到姓,然后定位到名,进而找到电话号码,因为所有的姓从上到下按照既定的规则(首字母排序)是有序的,而名是在姓的首字母一定的条件下也是按照名的首字母排序的,
- 但是整体来看,所有的名放在一起是无序的,所以如果只知道名查找起来就比较慢,因为无法用已排好的结构快速查找。到这里大家是否明白了为啥会有最左前缀匹配原则了吧。
- 较快查找
- 姓、名 ——> 电话
- 姓——> 电话
- 其它例子
(select * from T1 where b = 12 and c = 14 and d = 3;-- 全值索引匹配 三列都用到
select * from T1 where b = 12 and c = 14 and e = 'xml';-- 应用到两列索引
select * from T1 where b = 12 and e = 'xml';-- 应用到一列索引
select * from T1 where b = 12 and c >= 14 and e = 'xml';-- 应用到bc两列列索引及索引条件下推优化
select * from T1 where b = 12 and d = 3;-- 应用到一列索引 因为不能跨列使用索引 没有c列 连不上
select * from T1 where c = 14 and d = 3;-- 无法应用索引,违背最左匹配原则 -- 市民表:建立了联合索引(name, age)
select * from tuser where name like '张%' and age=10 and ismale=1; -- 只能用到name索引,用不了(name, age索引)- 联合索引,范围查询后,后面的字段不走索引 (查看)
底层B+树叶子节点的键值是通过链表连接的,而组合索引是从左向右按照索引字段的顺序进行查找,只有当前一个索引列是确定值的时候,下一个索引列的值才是递增的状态,才能通过该组合索引的B+树遍历得到,而一旦前一个索引列是范围匹配,则之后的索引列的值不存在递增关系,那么就没办法通过B+树遍历得到。
例如,以下联合索引。A>=2,B=4的查询过程。
a=2时,B是有序的
2<=a<=3 时,b是无序的,就不能继续向下查找了。必须在前一个是定值的情况下,才能继续向下查找。
下推优化:mysql> select * from tuser where name like '张%' and age=10 and ismale=1;
- 图 3 中,在 (name,age) 索引里面我特意去掉了 age 的值,这个过程 InnoDB 并不会去看 age 的值,只是按顺序把“name 第一个字是’张’”的记录一条条取出来回表。因此,需要回表 4 次。
- 图 4 跟图 3 的区别是,InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。
- 二叉树、平衡二叉树、红黑树、B树、B+树 不断演进和优化
- 锁
- 锁的分类:
- 设计初衷:数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。
- 根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类
- 全局锁
- 概念:全局锁就是对整个数据库实例加锁。
- 使用场景:全局锁的典型使用场景是,做全库逻辑备份(在备份过程中整个库完全处于只读状态)。也就是把整库每个表都 select 出来存成文本。
- 加锁方法:
- 传统全局读锁方法:
- 命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令
- 之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
- 若引擎支持可重复读(底层是MVCC 即一致性读视图)—— 事务引擎InnoDB:
- 官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
- 你一定在疑惑,有了这个功能,为什么还需要 FTWRL 呢?一致性读是好,但前提是引擎要支持这个隔离级别。
- 比如,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用 FTWRL 命令了。
- 所以,single-transaction 方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。
- 既然要全库只读,为什么不使用 set global readonly=true 的方式呢?确实 readonly 方式也可以让全库进入只读状态,但我还是会建议你用 FTWRL 方式,主要有两个原因:
- 一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。
- 二是,在异常处理机制上有差异。
- 如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。
- 而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
- 传统全局读锁方法:
- 表级锁
- 类型:MySQL中的表级锁有两种:
- 1.表锁: lock tables ... read/write
- 2.元数据锁(MDL metadata lock): 不需要显式使用 ,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。
- 1.表锁:lock tables ... read/write
- 例子:如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,
- 本线程与其它线程
- 则其他线程写 t1、读写 t2 的语句都会被阻塞。
- 同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。
- 读锁与写锁
- 读锁(线程A lock tables t1 read)共享锁:本线程只能读t1、其它线程也可以读t1;都不能写t1
- 写锁(线程A lock table t2 write)排他锁:本线程可以读写t2,其它线程不能读也不能写
- 本线程与其它线程
- 在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。
- 例子:如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,
- 概念:指的是DML 和 DDL 之间的锁
- 详情:当对一个表做增删改查操作的时候,加 MDL 读锁(自动加);当要对表做结构变更操作的时候,加 MDL 写锁(自动加)。
- 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
- 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。
- 增删改查的时候(加了MDL 读锁),这个时候不能做表结构变更(加MDL 写锁);反之亦然
- 因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
- 给小表增加字段也会导致连接池(线程池)打爆;(也知道了,事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。)
- 如何安全地给小表加字段?
- 首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
- 但考虑一下这个场景。如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?这时候 kill 可能未必管用,因为新的请求马上就来了
- 首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
- 比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
2.元数据锁(metadata lock)
- 类型:MySQL中的表级锁有两种:
- 行锁
- 概念:行锁就是针对数据表中行记录的锁(是引擎层实现的,不是所有引擎都支持)。这很好理解,比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新
- 实现原理:行锁的实现是通过给索引上的索引项添加锁实现的,故只有当执行的脚本走索引时,innodb才会使用行锁,否则innodb只能使用元数据锁MDL,两种锁均无需显示添加。故此时id必须是主键(聚集索引)或创建了辅助索引。
- 如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放(减少一次操作中锁住共享数据的时间,进而提升效率)。
- 反例:
- 正例:
- 假设你负责实现一个电影票在线交易业务,顾客 A 要在影院 B 购买电影票。我们简化一点,这个业务需要涉及到以下操作。也就是说,要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。当然,为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?(解决:如果你把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时)
- 1.从顾客 A 账户余额中扣除电影票价;
- 2.给影院 B 的账户余额增加这张电影票价;(容易锁冲突)
- 3.记录一条交易日志。
- 如果这个影院做活动,可以低价预售一年内所有的电影票,而且这个活动只做一天。于是在活动时间开始的时候,你的 MySQL 就挂了。你登上服务器一看,CPU 消耗接近 100%,但整个数据库每秒就执行不到 100 个事务。这是什么原因呢?这里,我就要说到死锁和死锁检测了。
- 假设你负责实现一个电影票在线交易业务,顾客 A 要在影院 B 购买电影票。我们简化一点,这个业务需要涉及到以下操作。也就是说,要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。当然,为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?(解决:如果你把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时)
- 死锁和死锁检测
- 概念:当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。这里我用数据库中的行锁举个例子。
- 例子:
- 解决策略:当出现死锁以后,有两种策略:
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。(默认值是 50s,对于在线服务来说,这个等待时间往往是无法接受的。但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。)
- 另一种策略是主动死锁检测,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on(默认值就是on),表示开启这个逻辑。
- 主动死锁检测引发的CPU大量占用问题
- 整体死锁检测的耗费代价是 O(n2)级别的。 并发更新同一行的1000个线程,整体耗费的死锁检测操作为1000*1000=100万。但是这期间要消耗大量的 CPU 资源。因此,你就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。
- 怎么解决由这种热点行更新导致的性能问题呢?
- 1,头痛医头:就是如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。
- 2,控制并发度
- 根据上面的分析,你会发现如果并发能够控制住,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。
- 一个直接的想法就是,在客户端做并发控制。但是,你会很快发现这个方法不太可行,因为客户端很多。我见过一个应用,有 600 个客户端,这样即使每个客户端控制到只有 5 个并发线程,汇总到数据库服务端以后,峰值并发数也可能要达到 3000。
- 因此,这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;
- 你可以考虑通过将一行改成逻辑上的多行来减少锁冲突。还是以影院账户为例,可以考虑放在多条记录上,比如 10 个记录,影院的账户总额等于这 10 个记录的值的总和。
- 这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的 1/10,可以减少锁等待个数,也就减少了死锁检测的 CPU 消耗。
- 这个方案看上去是无损的,但其实这类方案需要根据业务逻辑做详细设计。如果账户余额可能会减少,比如退票逻辑,那么这时候就需要考虑当一部分行记录变成 0 的时候,代码要有特殊处理。
- 根据上面的分析,你会发现如果并发能够控制住,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。
- 发
- 主动死锁检测引发的CPU大量占用问题
- 行锁:好文章 https://www.cnblogs.com/jian0110/p/12721924.html
- 锁的分类:
从这里开始,跟我一起搞懂 MySQL!的更多相关文章
- 彻底搞懂MySQL为什么要使用B+树索引
目录 MySQL的存储结构 表存储结构 B+树索引结构 B+树页节点结构 为什么要用B+树索引 二叉树 多叉树 B树 B+树 搞懂这个问题之前,我们首先来看一下,MySQL表的存储结构 MySQL的存 ...
- 搞懂MySQL InnoDB B+树索引
一.InnoDB索引 InnoDB支持以下几种索引: B+树索引 全文索引 哈希索引 本文将着重介绍B+树索引.其他两个全文索引和哈希索引只是做简单介绍一笔带过. 哈希索引是自适应的,也就是说这个不能 ...
- 搞懂MySQL GTID原理
从MySQL 5.6.5 开始新增了一种基于 GTID 的复制方式.通过 GTID 保证了每个在主库上提交的事务在集群中有一个唯一的ID.这种方式强化了数据库的主备一致性,故障恢复以及容错能力. GT ...
- 一本彻底搞懂MySQL索引优化EXPLAIN百科全书
1.MySQL逻辑架构 日常在CURD的过程中,都避免不了跟数据库打交道,大多数业务都离不开数据库表的设计和SQL的编写,那如何让你编写的SQL语句性能更优呢? 先来整体看下MySQL逻辑架构图: M ...
- 五分钟搞懂MySQL索引下推
大家好,我是老三,今天分享一个小知识点--索引下推. 如果你在面试中,听到MySQL5.6"."索引优化" 之类的词语,你就要立马get到,这个问的是"索引下推 ...
- 一文彻底搞懂MySQL分区
一个执着于技术的公众号 一.InnoDB逻辑存储结构 首先要先介绍一下InnoDB逻辑存储结构和区的概念,它的所有数据都被逻辑地存放在表空间,表空间又由段,区,页组成. 段 段就是上图的segment ...
- 一文搞懂mysql索引底层逻辑,干货满满!
一.什么是索引 在mysql中,索引是一种特殊的数据库结构,由数据表中的一列或多列组合而成,可以用来快速查询数据表中有某一特定值的记录.通过索引,查询数据时不用读完记录的所有信息,而只是查询索引列即可 ...
- 搞懂MySQL InnoDB事务ACID实现原理
前言 说到数据库事务,想到的就是要么都做修改,要么都不做.或者是ACID的概念.其实事务的本质就是锁和并发和重做日志的结合体.那么,这一篇主要讲一下InnoDB中的事务到底是如何实现ACID的. 原子 ...
- 搞懂MySQL分区
一.InnoDB逻辑存储结构 首先要先介绍一下InnoDB逻辑存储结构和区的概念,它的所有数据都被逻辑地存放在表空间,表空间又由段,区,页组成. 段 段就是上图的segment区域,常见的段有数据段. ...
- 一文快速搞懂MySQL InnoDB事务ACID实现原理(转)
这一篇主要讲一下 InnoDB 中的事务到底是如何实现 ACID 的: 原子性(atomicity) 一致性(consistency) 隔离性(isolation) 持久性(durability) 隔 ...
随机推荐
- .Net 一套接口多实现
.Net 一套接口多实现 接口(interface)可理解为规范.标准.协议.接口是用来约束各方都在同一组规范下工作. 电脑外设USB接口,各个品牌商家生产的U盘.鼠标都能够被电脑主板识别并工作,这是 ...
- UE源码分析:Slate 类鼠标输入处理
序言 UE4.25版本 本章看一下UE的 "类鼠标"输入处理(其实就是鼠标和触摸的输入,我也没找到专门的术语来代指,英文UE用的是PointerXXX),主要是按下.移动.抬起等相 ...
- Python类与面向对象
Python类与面向对象 一.面向对象 1.1 面向对象概述 面向对象与面向过程? 面向过程编程的基本思想是:分析解决问题的步骤,使用函数实现每步对应的功能,按照步骤的先后顺序依次调用函数.面向过程只 ...
- 聊聊 RocketMQ 名字服务
NameServer 是专为 RocketMQ 设计的轻量级名字服务,它的源码非常精简,八个类 ,少于1000行代码. 这篇文章, 笔者会从基础概念.Broker发送心跳包.NameServer 维护 ...
- [nginx]编译安装openresty
前言 OpenResty是一个基于Nginx和Lua的高性能Web平台,其内部集成了大量精良的Lua库.第三方模块以及大多数的依赖项.用于方便地搭建能够处理超高并发.扩展性极高的动态 Web 应用.W ...
- 使用wrk对api接口进行性能测试
安装 yum install -y openssl git git clone https://github.com/wg/wrk.git wrk cd wrk make cp wrk /usr/lo ...
- 【go笔记】标准库-strconv
前言 标准库strconv提供了字符串类型与其他常用数据类型之间的转换. strconv.FormatX()用于X类型转字符串,如strconv.FormatFloat()用于浮点型转字符串. str ...
- 「Go笔记-02」变量、数据类型、数据类型间转换、进制转换...看这一篇就Go了
前言 一个程序就是一个世界,不论是使用哪种高级程序语言编写程序, 变量都是其程序的基本组成单位, 变量 在 go 中 变量是用于存储数据的命名空间(内存位置),它可以表示一个值,这个值在程序执行过程中 ...
- phpexcel图片获取
phpexcel图片获取常见的两种方式: require_once dirname(__FILE__) . '/../Jn/PHPExcel.php'; $filePath = "123.x ...
- Spark RDD惰性计算的自主优化
原创/朱季谦 RDD(弹性分布式数据集)中的数据就如final定义一般,只可读而无法修改,若要对RDD进行转换或操作,那就需要创建一个新的RDD来保存结果.故而就需要用到转换和行动的算子. Spark ...