当前位置: 代码迷 >> 综合 >> 【Sofice小司笔记】3 MySQL,包含Mysql架构,InnoDB体系架构,redolog,MVCC,索引等
  详细解决方案

【Sofice小司笔记】3 MySQL,包含Mysql架构,InnoDB体系架构,redolog,MVCC,索引等

热度:33   发布时间:2024-01-12 00:39:37.0

概述

安装配置:https://www.cnblogs.com/winton-nfs/p/11524007.html

命名规则:小写字母,‘ _’分隔 (为避免不同操作系统上数据库表和字段名等名字区分大小写不一致的问题)

安装

centos7安装mysql5.7

# 下载
wget https://dev.mysql.com/get/mysql57-community-release-el7-9.noarch.rpm
# repo安装
rpm -ivh mysql57-community-release-el7-9.noarch.rpm
# 进入到 /etc/yum.repos.d/目录进行mysql安装
cd /etc/yum.repos.d/
yum -y install mysql-server
# 启动msyql
systemctl start mysqld
# 获取安装时的临时密码
# 倘若没有获取临时密码,则删除原来安装过的mysql残留的数据:rm -rf /var/lib/mysql,再启动
grep 'temporary password' /var/log/mysqld.log
# 进入
mysql -u root -p
# 修改密码
set global validate_password_policy=LOW;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'martin123';
# 远程连接
use mysql;
update mysql.user set Host='%' where User='root';
flush privileges;

SQL语句

新建表

create table SC(Sno char(9) not null,Cno char(9) not null,Grade smallint,primary key(Sno,Cno), # 主键foreign key(Sno) references Student(Sno), # 外键
);

CRUD

增 Create

INSERT INTO students (class_id, name, gender, score) VALUES (2, '大牛', 'M', 80);

删 Delete

DELETE FROM students WHERE id>=5 AND id<=7;

改 Update

UPDATE students SET name='大牛', score=66 WHERE id=1;

查 Retrieve

-- 投影查询,聚合查询 s. 指定表名;c.name class_name 列名重命名;AVG(s.score) 求平均值
SELECT s.class_id, c.name class_name, s.gender, AVG(s.score) avgScore 
-- 指定一个或多个表,可重命名
FROM students s
-- 连接查询 还有RIGHT OUTER JOIN,LEFT OUTER JOIN,FULL OUTER JOIN
INNER JOIN classes c
ON s.class_id = c.id
-- 条件查询
WHERE s.class_id=1 OR s.class_id=2 OR s.class_id=3 OR s.dept in ('CS','Math') OR (age between 20 and 33)
-- 分组 将相同内容合并,常与聚合查询一起使用
GROUP BY class_id,gender 
-- 排序 默认升序,desc为降序,按逗号顺序排序
ORDER BY avgScore DESC,class_id 
-- 分页查询 每页4条 从第100条数据开始
LIMIT 4 OFFSET 100

数据类型

img

varchar 和 char 的区别

  • CHAR是定长的,根据定义的字符串长度分配足够的空间。CHAR存储的内容超出设置的长度时,内容会被截断。
  • VARCHAR用于存储可变长字符串,它比定长类型更节省空间。VARCHAR存储的内容超出设置的长度时,内容同样会被截断。
  • 对于经常变更的数据来说,CHAR 比 VARCHAR 更好,因为 CHAR 不容易产生碎片。
  • 对于非常短的列,CHAR 比 VARCHAR 在存储空间上更有效率。

MySQL 逻辑架构概览

MySQL 最重要、最与众不同的特性就是它的可插拔存储引擎架构(pluggable storage engine architecture),这种架构的设计将查询处理及其他系统任务和数据的存储/提取分离开来。

MySQL 可插拔存储引擎架构使开发者能够为特定应用程序需求选择专门的存储引擎,同时完全无需管理任何特定应用程序编码要求。也就是说,尽管不同存储引擎具有不同的功能,但应用程序不受这些差异的影响。

如果应用程序更改带来了需要更改底层存储引擎的需求,或者需要添加一个或多个存储引擎来支持新需求,则无需进行重大的编码或流程更改即可使工作正常进行。 MySQL 服务器架构通过提供适用于跨存储引擎的一致且易于使用的 API,使应用程序免受存储引擎底层复杂性的影响。

image-20211108165552941

我们可以大致把 MySQL 的逻辑架构分成 Server 层和存储引擎层:

1)大多数 MySQL 的核心服务功能都在 Server 层,包括连接,查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。

值得一提的是,Server 最上面的服务也就是连接器,拥有管理 MySQL 连接、权限验证的功能。显然这并非 MySQL 所独有,大多数基于网络的客户端/服务器的工具或者服务都有类似的架构。

2)第二层就是存储引擎(支持 InnoDB、MyISAM、Memory 等多个存储引擎)。存储引擎负责 MySQL 中数据的存储和提取,响应上层服务器的请求。每个存储引擎自然是有它的优势和劣势,不同的存储引擎之间无法相互通信,所以我们需要根据不同的场景来选择合适的存储引擎。

服务器通过 API 与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎 API 包含几十个底层函数,用于执行诸如 “开始一个事务” 或者 “根据主键提取一行记录” 等操作。

需要注意的是,在 MySQL 5.1 及之前的版本,MyISAM 是默认的存储引擎,而在 MySQL 5.5.5 后,InnoDB 成为了默认的存储引擎。

连接器(Connector)

MySQL 连接器为客户端程序提供到 MySQL 服务器的连接。 说得更细节一点的话,连接器其实会做两个事情,一个是管理 MySQL 连接,一个是权限验证。

要连接到 MySQL 服务器,需要提供 MySQL 用户名和密码,并且如果服务器运行在我们登录的机器以外的机器上,还需要指定一个主机名比如 host;如果在运行 MySQL 的同一台机器上登录,就可以省略主机名。

shell> mysql -h host -u user -p
Enter password: ********

通过上述命令完成经典的 TCP 三次握手建立连接后,连接器就会根据你输入的用户名和密码来认证你的身份。

当然,它还会验证该用户是否具有执行某个特s定查询的权限(例如,是否允许该用户对 world 数据库的 Country 表执行 SELECT 语句)。之后,这个连接里面的所有权限判断逻辑,都将依赖于此时读到的权限。这意味着,当一个用户成功建立连接后,即使你在另一个终端用管理员账号对这个用户的权限做了修改,对当前已经存在连接的权限不会造成任何影响。并且当修改了用户权限后,只有再新建的连接才会使用新的权限设置。

当一个连接建立起来后,如果你没有后续的动作,那么这个连接就处于空闲状态(Sleep)。

对于一个 MySQL 连接来说(或者说一个线程),任何时刻都有一个状态,该状态表示了 MySQL 当前正在做什么。最简单的是使用 SHOW FULL PROCESSLIST 命令。

在一个查询的生命周期中,状态会变化很多次。这里就不详细列出来了,上图中的 Sleep 状态就是说当前连接正在等待客户端发送新的请求,Query 状态表示当前连接正在执行查询或者正在将结果发送给客户端。

在 MyQL 的默认设置中,如果一个连接处在 Sleep 状态 8 小时(就是超过 8 小时没有使用),服务器将断开这条连接,后续在该连接上进行的所有操作都将失败。这个时间是由参数 wait_timeout 控制的:

查询缓存(Query Cache)

连接建立完成后,就可以输入 select 语句进行查询了。

查询缓存存储了 SELECT 语句的文本以及响应给客户端的相应结果。这样,如果服务器稍后接收到相同的 SELECT 语句,服务器会先从查询缓存中检索结果,而不是再次解析和执行该语句。查询缓存在 session 之间共享,因此可以发送一个客户端生成的结果集以响应另一个客户端发出的相同查询。

如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前 MySQL 会检查一次用户权限。这仍然是无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。

当表被修改时,查询缓存中的任何相关条目都会被 flushed(清空),这解决了缓存一致性问题。

然而,很不幸的是,正是由于这个特性,从 MySQL 5.7.20 开始,官方不再推荐使用查询缓存,并在 MySQL 8.0 中直接删除了查询缓存!

其实不难理解,举个例子,对于一个流量很大的论坛项目来说,查询帖子表的需求每时每刻都存在,帖子也几乎每时每刻都在增加,那只要这个表一更新,这个表上所有的查询缓存都会被清空,这对于 MySQL 数据库的压力之大,可想而知了吧。费个劲把查询结果存起来,还没来得及使用呢,就被一个更新全清空了。

对于 MySQL 8.0 之前的版本来说,你可以将参数 query_cache_type 设置成 DEMAND,这样所有的 SQL 语句都不会再使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:

mysql> select SQL_CACHE * from t1 where id = 1;

解析器(Parser)

解析器需要解析 SQL预处理

MySQL 通过关键字对 SQL 语句进行解析,并生成一棵对应的 “解析树”,用于根据语法规则来验证语句是否正确。例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确匹配。

预处理则会进一步检查解析树是否合法,例如,检查数据表和数据列是否存在,检查表名和字段名是否正确等。

优化器(Optimizer)

现在,解析树是合法的了,MySQL 已经知道你要做什么了。不过,一条查询可以有很多种执行计划,最后都返回相同的结果,那到底该选择哪种执行计划呢?

举个简单的例子:

mysql> select * from t1 where id = 10 and name = "good";

对于上面这个语句,可以先查找 name = good 再查找 id = 10,也可以先查找 id = 10 再查找 name = good,这两种不同的执行计划可能耗费的时间成本是不一样的。

那么优化器的作用就是找到这其中最好的执行计划。需要注意的是,这里的执行计划是一个数据结构,而不是和很多其他的关系型数据库那样会生成对应的字节码。

另外,优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。

当优化器阶段完成后,这个语句的执行计划就确定下来了,就可以进入执行器阶段了。

执行器

和命中查询缓存一样,在开始执行 SQL 语句之前,执行器会先判断一下当前用户对这个表有没有执行查询的权限。

权限认证完成后,MySQL 就会根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口(“handler API”)来完成。

查询中的每一个表由一个 handler 的实例表示。实际上,MySQL 在优化阶段就为每个表创建了一个 handler 实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名、索引统计信息,等等。

举个例子:

mysql> select * from t1 where id = 10;

假设我们使用默认的 InnoDB 引擎,则执行器的执行流程大概是这样的(注意,如果 id 不是索引则会进行全表扫描,一行一行的查找,如果是索引则会在索引组织表中查询,比较负责。这里以非索引举例):

1)调用 InnoDB 引擎接口获取这个表的第一行记录,判断 id 值是不是 10,如果是则将这行记录存在一个集合中;如果不是则进入下一行的判断,直到取到这个表的最后一行

2)执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果返回给客户端

InnoDB 体系架构

InnoDB 存储引擎由内存池和一些后台线程组成:

内存池(缓冲池)

InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照的方式进行管理,因此可将其视为基于磁盘的数据库系统(Disk-base Database)。,由于 CPU 速度与磁盘速度之间的不匹配,通常会使用缓冲池技术来提高数据库的整体性能。

具体来说,缓冲池其实就是一块内存区域,在 CPU 与磁盘之间加入内存访问,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。

拥有了缓冲池后,“读取页” 操作的具体步骤就是这样的:

  • 首先将从磁盘读到的页存放在缓冲池中
  • 下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。

“修改页” 操作的具体步骤就是这样的:

  • 首先修改在缓冲池中的页;然后再以一定的频率刷新到磁盘上。

脏页:如果缓冲池中的页已经被修改了,但是还没有刷新到磁盘上,那么我们就称缓冲池中的这页是 ”脏页“,即缓冲池中的页的版本要比磁盘的新。

因此,缓冲池的大小直接影响着数据库的整体性能

后台线程

后台线程最大的作用就是用来完成 “将从磁盘读到的页存放在缓冲池中” 以及 “将缓冲池中的数据以一定的频率刷新到磁盘上” 这两个操作

另外,InnoDB 存储引擎是多线程的模型,也就是说它拥有多个不同的后台线程,负责处理不同的任务。这里简单列举下几种不同的后台线程:

  • Master Thread:主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性
  • IO Thread:在 InnoDB 存储引擎中大量使用了 AIO(Async IO)来处理写 IO 请求,这样可以极大提高数据库的性能。IO Thread 的工作主要是负责这些 IO 请求的回调(call back)处理
  • Purge Thread:回收已经使用并分配的 undo 页
  • Page Cleaner Thread:将之前版本中脏页的刷新操作都放入到单独的线程中来完成。其目的是为了减轻原 Master Thread 的工作及对于用户查询线程的阻塞,进一步提高 InnoDB 存储引擎的性能

数据库恢复技术

  • 原子性实现原理 - Undo Log

    为了实现原子性,需要通过日志:将所有对数据更新操作都写入日志,如果一个事务中的一部分已经操作成功,但以后的操作由于断电/系统崩溃/其他软硬件错误或者用户提交了rollback 导致无法进行,则通过回溯日志,将已经执行成功的操作撤销 undo,从而达到全部操作失败的目的,使得数据库恢复到一致性的状态,可以继续被使用。

  • 持久性实现原理 - Redo Log

    和Undo Log 相反,Redo(重做) Log 记录的是新数据的备份。在事务提交前,只是将Redo Log 持久化即可,不需要数据持久化。当系统崩溃时,虽然数据没有持久化,但Redo Log 已经持久化了。系统可以根据Redo Log 将数据更新到最新的状态。

  • 隔离性实现原理 - 锁

    当然,保证事务的隔离性,即并发控制不止可用封锁协议,还有时间戳、多版本控制等等。

① WAL 策略 与 redo log

redo log 是 InnoDB 存储引擎所特有的

脏页的数据会被定期刷新到磁盘上。倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。并且,如果热点数据都集中在某几个页中,那么数据库的性能将变得非常差。另外,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么这个数据就不能恢复了。

WAL(Write Ahead Log,预写日志):为了避免发生数据丢失的问题,当前事务数据库系统(并非 MySQL 所独有)普遍都采用了 WAL 策略:即当事务提交时,先写重做日志(redo log),再修改页(先修改缓冲池,再刷新到磁盘);当由于发生宕机而导致数据丢失时,通过 redo log 来完成数据的恢复。这也是事务 ACID 中 D(Durability 持久性)的要求。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe

每个 InnoDB 存储引擎至少有 1 个重做日志文件组( redo log group),每个文件组下至少有 2 个重做日志文件(redo log file),默认的话是一个 redo log group,其中包含 2 个 redo log file:ib_logfile0ib_logfile1

一般来说,为了得到更高的可靠性,用户可以设置多个镜像日志组(mirrored log groups),将不同的文件组放在不同的磁盘上,以此提高 redo log 的高可用性。在日志组中每个 redo log file 的大小一致,并以循环写入的方式运行:InnoDB 存储引擎会先写 redo log file 0,当 file 0 被写满的时候,会切换至 redo log file 1,当 file 1 也被写满时,会切换到 redo log file 2 中,而当 file 2 也被写满时,会再切换到 file 0 中。

可以看出,redo log file 的大小设置对于 InnoDB 存储引擎的性能有着非常大的影响:

  • redo log file 不能设置得太大,如果设置得很大,在恢复时可能需要很长的时间
  • redo log file 又不能设置得太小了,否则可能导致一个事务的日志需要多次切换重做日志文件

② CheckPoint

有了 redo log 就可以高枕无忧了吗?显然不是这么简单,我们仍然面临这样 3 个问题:

1)缓冲池不是无限大的,也就是说不能没完没了的存储我们的数据等待一起刷新到磁盘

2)redo log 是循环使用而不是无限大的(也许可以,但是成本太高,同时不便于运维),那么当所有的 redo log file 都写满了怎么办?

3)当数据库运行了几个月甚至几年时,这时如果发生宕机,重新应用 redo log 的时间会非常久,此时恢复的代价将会非常大。

因此 Checkpoint 技术的目的就是解决上述问题:

  • 缓冲池不够用时,将脏页刷新到磁盘
  • redo log 不可用时,将脏页刷新到磁盘
  • 缩短数据库的恢复时间

CheckPoint 技术简单来说其实就是在 redo log file 中找到一个位置,将这个位置前的页都刷新到磁盘中去,这个位置就称为 CheckPoint(检查点)。

针对上面这三点我们依次来解释下:

1)缩短数据库的恢复时间:当数据库发生宕机时,数据库不需要重做所有的日志,因为 Checkpoint 之前的页都已经刷新回磁盘。故数据库只需对 Checkpoint 后的 redo log 进行恢复就行了。这显然大大缩短了恢复的时间。

2)缓冲池不够用时,将脏页刷新到磁盘:所谓缓冲池不够用的意思就是缓冲池的空间无法存放新读取到的页,这个时候 InnoDB 引擎使用优化的LRU 算法:即最频繁使用的页在 LRU 列表(LRU List)的前端,最少使用的页在 LRU 列表的尾端;当缓冲池的空间无法存放新读取到的页时,将首先释放 LRU 列表中尾端的页。这个被释放出来(溢出)的页,如果是脏页,那么就需要强制执行 CheckPoint,将脏页刷新到磁盘中去。

3)redo log 不可用时,将脏页刷新到磁盘

所谓 redo log 不可用就是所有的 redo log file 都写满了。但那些已经不再需要的部分就称为 ”可以被重用的部分“,即当数据库发生宕机时,数据库恢复操作不需要这部分的 redo log,因此这部分就可以被覆盖重用(或者说被擦除)。

举个例子来具体解释下:一组 4 个文件,每个文件的大小是 1GB,那么总共就有 4GB 的 redo log file 空间。write pos 是当前 redo log 记录的位置,随着不断地写入磁盘,write pos 也不断地往后移,就像我们上文说的,写到 file 3 末尾后就回到 file 0 开头。CheckPoint 是当前要擦除的位置(将 Checkpoint 之前的页刷新回磁盘),也是往后推移并且循环的。

write pos 和 CheckPoint 之间的就是 redo log file 上还空着的部分,可以用来记录新的操作。如果 write pos 追上 CheckPoint,就表示 redo log file 满了,这时候不能再执行新的更新,得停下来先覆盖(擦掉)一些 redo log,把 CheckPoint 推进一下。

综上所述,Checkpoint 所做的事情无外乎是将缓冲池中的脏页刷新到磁盘。不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发 Checkpoint。在 InnoDB 存储引擎内部,有两种 Checkpoint,分别为:

  • Sharp Checkpoint:发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,参数 innodb_fast_shutdown=1
  • Fuzzy Checkpoin:InnoDB 存储引擎内部使用这种模式,只刷新一部分脏页,而不是刷新所有的脏页回磁盘。

③ bin log 和 redo log

redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,包括错误日志(error log)、二进制日志(binlog)、慢查询日志(slow query log)、查询日志(log)。

binlog(二进制日志,binary log)记录了对 MySQL 数据库执行更改的所有操作,但是不包括 SELECTSHOW 这类操作,因为这类操作对数据本身并没有修改。也就是说,binlog 是逻辑日志,记录的是这个语句的原始逻辑。

binlog 日志只能用于归档,因此 binlog 也被称为归档日志,显然如果 MySQL 只依靠 binlog 等这四种日志是没有 crash-safe 能力的,所以为了弥补这种先天的不足,得益于 MySQL 可插拔的存储引擎架构,InnoDB 开发了另外一套日志系统 — 也就是 redo log 来实现 crash-safe 能力。

redo log 是物理日志,记录的是 “在某个数据页上做了什么修改”。

binlog 是追加写入的,就是说 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志;而 redo log 是循环写入的。

并发控制技术(InnoDB 中的锁)

“锁" 是数据库系统区别于文件系统的一个关键特性,其对象是事务,用来锁定的是数据库中的对象,如表、页、行等。需要注意的是,每种数据库对于锁的实现都是不同的,并且对于 MySQL 来说,每种存储引擎都可以实现自己的锁策略和锁粒度,比如 InnoDB 引擎支持行锁和表锁,而 MyISAM 引擎只支持表锁

表锁与行锁

表锁 (Table Lock)就是会锁定整张表,它是 MySQL 中最基本的锁策略,不依赖于存储引擎,并且表锁是开销最小的策略(因为粒度比较大)。

由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。当然,锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣。

行锁(Row Lock)也称为记录锁,就是锁住某一行(某条记录 row)。需要的注意的是,MySQL 服务器层并没有实现行锁机制,行级锁只在存储引擎层实现

读锁和写锁

首先说明一点,对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上

对于并发读和并发写的问题,可以通过共享锁(Shared Lock,S Lock,读锁) 和 **排他锁(Exclusive Lock,X Lock,写锁)**实现:

意向锁

InnoDB 存储引擎支持多粒度锁定,允许事务在行级上的锁和表级上的锁同时存在。意向锁 IS/IX 是一个表级锁,其作用就是指明接下来的事务将会用到哪种锁。

如何加锁

InnoDB 采用的是两阶段锁协议:即在事务执行过程中,随时都可以执行加锁操作,但是只有在事务执行 COMMIT 或者 ROLLBACK 的时候才会释放锁,并且所有的锁是在同一时刻被释放。

意向锁:由 InnoDB 引擎自己维护,用户无法手动操作。在为数据行加读写锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

表级锁:

1)隐式锁定:对于常见的 DDL 语句(如 ALTERCREATE 等),InnoDB 会自动给相应的表加表级锁

2)显示锁定:在执行 SQL 语句时,也可以明确显示指定对某个表进行加锁(lock table user read(write)

lock table user read; # 加表级读锁
unlock tables; # 释放表级锁

行级锁:

1)对于常见的 DML 语句(如 UPDATEDELETEINSERT ),InnoDB 会自动给相应的记录行加写锁

2)默认情况下对于普通 SELECT 语句,InnoDB 不会加任何锁,但是在 Serializable 隔离级别下会加行级读锁

上面两种是隐式锁定,InnoDB 也支持通过特定的语句进行显式锁定,不过这些语句并不属于 SQL 规范:

3)SELECT * FROM table_name WHERE ... FOR UPDATE,加行级写锁

4)SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE,加行级读锁

InnoDB 存储引擎的行级锁是基于索引的,也就是说当索引失效或者说根本没有用索引的时候,行锁就会升级成表锁

举个例子,有数据库如下,id 是主键索引:

CREATE TABLE `test` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

新建两个事务,先执行事务 T1 的前两行,也就是不要执行 commit。我们试图使用 select ... for update 给 username = “user_three” 的记录行加上记录锁,但是由于 username 并非主键也并非索引,所以实际上这里事务 T1 锁住的是整张表:

begin;
SELECT * FROM user where name = "司展宇" for update;
COMMIT;

由于没有执行 commit,所以这个时候事务 T1 没有释放锁,并且锁住了整张表。此时再来执行事务 2 试图申请 id = 5 的记录锁,你会发现事务 T2 会卡住,最后超时关闭事务:

begin;
UPDATE user set name = "大可爱" where id = 1;
COMMIT;

多版本并发控制(Multi-Version Concurrency Control, MVCC)

多版本并发控制是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

基本思想

在封锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的,而 MVCC 利用了多版本的思想**,不会阻塞住数据的查询操作,而是会查询出数据被修改之前的备份,写操作更新最新的版本快照**,这一点和 CopyOnWrite 类似。

在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照。

脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算是脏读。

版本号

  • 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号 TRX_ID :事务开始时的系统版本号。

Undo 日志

MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。

例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。

INSERT INTO t(id, x) VALUES(1, "a");
UPDATE t SET x="b" WHERE id=1;
UPDATE t SET x="c" WHERE id=1;

因为没有使用 START TRANSACTION 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。

img

INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。

ReadView

MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, …},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。

img

在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用:

  • TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。
  • TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。
  • TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
    • 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
    • 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。

在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。

快照读与当前读

  1. 快照读

    MVCC 的 SELECT 操作是快照中的数据,不需要进行加锁操作。

    SELECT * FROM table ...;
    
  2. 当前读

    MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。

    INSERT;
    UPDATE;
    DELETE;
    

    在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S 锁,第二个需要加 X 锁。

    SELECT * FROM table WHERE ? lock in share mode;
    SELECT * FROM table WHERE ? for update;
    

Next-Key Locks(行锁三种实现)

行级锁只在存储引擎层实现。行锁锁住的是索引,而不是一条记录。

Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。

MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。

Record Locks 记录锁

锁定一个记录上的索引,而不是记录本身。

如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用:

update user set name = "NULL" where id = 1;

如果 SQL 语句中的条件导致索引失效(比如使用 or) 或者说条件根本就不涉及索引或者主键,行级锁就将退化为表锁:

update user set name = "NULL" where name = "szy";

Gap Locks 间隙锁

锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:

(-, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +)

在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的算法就是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。

由于主键也是一种唯一索引,所以我们可以这么说:Record Lock 是基于唯一索引的,而 Next-Key Lock 是基于非唯一索引的

当操作的索引为非唯一索引时,InnoDB 会先用 Record Lock 锁住对应的唯一索引,再用 Next-Key Lock 和 Gap Lock 对这个非唯一索引进行处理,而不仅仅是锁住这个非唯一索引。

Next-Key Lock 示例

假设我们为上面 test 表中新增一个字段,并设置为非唯一索引:

CREATE TABLE `test` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(255) DEFAULT NULL,`class` int(11) NOT NULL,PRIMARY KEY (`id`),KEY `index_class` (`class`) USING BTREE COMMENT '非唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

插入一些数据:

image-20210802225225160

开启一个事务 1 执行如下的操作语句:

select * from test where class = 3 for update;

image-20210802225348249

在这种情况下,InnoDB 事实上会加上三种行锁(select * ... from update 加的是行级写锁即 X 锁):

1)给主键索引 id = 105 加上 Record Lock

2)对于非唯一索引 class = 3,其加上的是 Next-Key Lock,锁定的范围是 (1,3]

3)另外,特别需要注意的是,InnoDB 存储引擎还会对非唯一索引 class 的下一个键值加上 Gap Lock(表中 class = 3 的下个键值是 6),所以还有一个 class 索引范围为 (3,6) 的间隙锁

总结下 2)和 3),对于这条 SQL 语句,InnoDB 存储引擎锁定地 class 索引范围是 (1, 6)

下面我们用实践来验证理论,再开启一个事务 2,执行下述的语句:

image-20210802225636814

不出所料,由于在事务 1 中执行的 SQL 语句已经对主键索引中列 a=105 的记录加上了 X 锁,所以此处再去获取 这个记录的 X 锁会被阻塞住。

再用一个事务来执行下述 SQL 语句:

image-20210802230358942

主键插入 104 没有任何问题,但是插入的 class 索引值 2 在被锁定的范围 (1,6) 中,因此执行同样会被阻塞住。

经过上面的分析,大家一定能够知道下面的 SQL 语句是可以正常执行的:

image-20210802230542969

Attention

需要注意的是,Next-Key Lock 降级为 Record Lock 仅存在于操作所有的唯一索引列的情况。若唯一索引由多个列组成,而操作的仅是多个唯一索引列中的其中一个,那么 InnoDB 存储引擎依然使用 Next-Key Lock 进行锁定

一致性非锁定读与一致性锁定读

  • 一致性非锁定读

    非一致性锁定读适用于对数据一致性要求不是很高的情况,比如在 READ COMMITTED 隔离级别下,即使行被锁定了,非一致性读也可以读到该行版本的最新一个快照。也即,非锁定读机制极大地提高了数据库的并发性

    一致性非锁定读就是读快照。只有在事务隔离级别 READ COMMITTED 和 REPEATABLE READ(默认)下,InnoDB 存储引擎才会使用非锁定的一致性读。并且,即使都是使用非锁定的一致性读,它俩对于快照数据的定义也各不相同:

    • READ COMMITTED:总是读取行的最新版本;如果行被锁定了,一致性非锁定读不会因此去等待行上锁的释放,而是去读取该行版本的最新一个快照
    • REPEATABLE READ:对于快照数据,一致性非锁定读总是读取事务开始时的行数据版本
  • 一致性锁定读

    一致性锁定读适用于对数据一致性要求比较高的情况,这个时候我们需要对读操作进行加锁以保证数据逻辑的一致性(就是给行记录加 X 锁或 S 锁)

    InnoDB 存储引擎对读操作支持两种一致性锁定读方式,或者说对读操作支持两种加锁方式:

    • SELECT ... FOR UPDATE,对于读取的行记录加一个 X 排它锁,其他事务不能对锁定的行加任何锁
    • SELECT ... LOCK IN SHARE MODE,对于读取的行记录添加一个 S 共享锁。其它事务可以向被锁定的行加 S 锁,但是不允许添加 X 锁,否则会被阻塞住

InnoDB 中的索引

对于 InnoDB 存储引擎来说,每张表都一定有个主键(Primary Key)。表采用的存储方式称为索引组织表(index organizedtable),也即表都是根据主键的顺序来进行组织存放的

如果在创建表时没有显式地定义主键,InnoDB 存储引擎会按如下方式选择或创建主键:

  • 首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键(主键的选择根据的是非空唯一索引定义的顺序,而不是建表时列的顺序)。
  • 如果不符合上述条件,InnoDB 存储引擎自动创建一个 6 字节大小的指针 _rowid 作为主键

InnoDB 存储引擎支持以下几种常见的索引:

  • B+ 树索引

    InnoDB 中一棵 B+ 树索引一般有 2 ~ 3 层,可以存放约 两千万行 的数据。

  • 覆盖索引

    在下面三个场景中,可以使用覆盖索引来进行优化 SQL 语句:

    1)列查询回表优化(如上面讲的例子,将单列索引 age 升级为联合索引(age, name))

    2)全表 count 查询

    举个例子,假设 user 表中现在只有一个索引即主键 id:

    select count(age) from user;
    

    可以用 explain 分析下这条语句,如果 Extra 字段为 Using index 时,就表示触发索引覆盖:

    显然现在是没有触发覆盖索引的,我们来优化下:将 age 列设置为索引 create index idx_age on user(age),这样只需要查一遍 age 索引的 B+ 树即可得到结果:

    3)分页查询

    select id, age, name from user order by username limit 500, 100;
    

    对于这条 SQL,因为 name 字段不是索引,所以在分页查询需要进行回表查询。

    Using filesort 表示没有使用索引的排序,或者说表示在索引之外,需要额外进行外部的排序动作。看到这个字段就应该意识到你需要对这条 SQL 进行优化了。

    使用索引覆盖优化:将 (age, name) 设置为联合索引,这样只需要查一遍 (age, name) 联合索引的 B+ 树即可得到结果。

  • 全文索引

  • 哈希索引

    InnoDB 中哈希索引是自适应的,也就是说 InnoDB 存储引擎会根据表的使用情况自动为表生成哈希索引,不能人为干预是否在一张表中生成哈希索引

InnoDB 存储引擎中,所有数据都被逻辑地存放在一个空间中,称之为 表空间(tablespace)。表空间又由 段(segment)区(extent)页(page) 组成。页是 InnoDB 磁盘管理的最小单位,默认每个页的大小为 16KB(show variables like 'innodb_page_size'

MyISAM

设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。

提供了大量的特性,包括压缩表、空间数据索引等。

不支持事务。

不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。

可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。

如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。

比较 InnoDB & MyISAM

  • 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
  • 并发:MyISAM 只支持表级锁,而 InnoDB 同时支持行级锁和表级锁
  • 外键:InnoDB 支持外键
  • 备份:InnoDB 支持在线热备份。
  • 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
  • 其它特性:MyISAM 支持压缩表和空间数据索引。

连接C#

配置

NuGet中添加MySql.Data

using MySql.Data.MySqlClient;

连接

string s = "server=localhost;database=test1;user=root;password=1234;pooling=true;charset=utf8;";
MySqlConnection connection = new MySqlConnection(s);try{connection.Open();Console.WriteLine("已经建立连接");//执行命令  connection.Close();
}
catch (MySqlException ex){Console.WriteLine(ex.Message);
}

【server】=服务器IP地址;
【database】=数据库名称;
【user】=数据库用户名;
【password】=数据库密码;
【pooling】=是否放入连接池;
【charset】=编码方式;

执行命令

https://blog.csdn.net/yellowman2/article/details/88724686?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control&dist_request_id=1328576.13056.16146765333312607&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control

string sql = "select count(*) from tb_User where ID=123";
MySqlCommand command = new MySqlCommand(sql, connection);
MySqlDataReader res = command.ExecuteReader();
reader.Read();//读取DataReader对象单行数据
reader.GetInt32(0); reader.GetString(1);
reader.GetInt32("userid");reader.GetString("username");//获取单行字段数据//获取数据
DataSet GetDataSet(string sql)
{// 创建数据适配器,传入SQL语句和链接对象MySqlDataAdapter myAd = new MySqlDataAdapter(sql, connection);// 创建数据存放集合;DataSet data = new DataSet();// 通过数据适配器读取数据并导入到数据集合;myAd.Fill(data);//返回数据集return data;                                              
}
if (data.Tables[0].Rows.Count > 0)                  //判断是否存在
{// 循环表for (int i = 0; i < data.Tables.Count;i++ ){// 循环该表的行for (int j = 0; j < data.Tables[i].Rows.Count;j++ ){// 循环该表的列for (int k = 0; k < data.Tables[i].Columns.Count;k++ ){// 打印数据Console.Write(data.Tables[i].Rows[j][k]+"\t");}Console.WriteLine();}Console.WriteLine();}
}

ExcuteScalar:执行单行查询,返回查询结果的首行数据
ExcuteReader:执行多行查询,返回DataReader对象
ng sql)
{
// 创建数据适配器,传入SQL语句和链接对象
MySqlDataAdapter myAd = new MySqlDataAdapter(sql, connection);
// 创建数据存放集合;
DataSet data = new DataSet();
// 通过数据适配器读取数据并导入到数据集合;
myAd.Fill(data);
//返回数据集
return data;
}
if (data.Tables[0].Rows.Count > 0) //判断是否存在
{
// 循环表
for (int i = 0; i < data.Tables.Count;i++ )
{
// 循环该表的行
for (int j = 0; j < data.Tables[i].Rows.Count;j++ )
{
// 循环该表的列
for (int k = 0; k < data.Tables[i].Columns.Count;k++ )
{
// 打印数据
Console.Write(data.Tables[i].Rows[j][k]+"\t");
}
Console.WriteLine();
}
Console.WriteLine();
}
}

ExcuteScalar:执行单行查询,返回查询结果的首行数据
ExcuteReader:执行多行查询,返回DataReader对象
ExcuteNonQuery:执行【insert(增)】、【updata(改)】、【delete(删)】语句
  相关解决方案