|
| 1 | +## **高性能Mysql ** |
| 2 | + |
| 3 | +笔记来源于 极客时间 《Mysql实战45讲》—— 丁奇 |
| 4 | + |
| 5 | + 中华石杉 《从零开始带你成为MySQL实战优化高手》 ——救火队长 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +### 1.基础架构 |
| 10 | + |
| 11 | +<img src="Mysql 实战.assets/image-20210511153212957.png" alt="image-20210511153212957" style="zoom:50%;" /> |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +* Server 层 |
| 16 | + * 连接器 |
| 17 | + * 查询缓存 |
| 18 | + * 分析器 |
| 19 | + * 优化器 |
| 20 | + * 执行器 |
| 21 | +* 存储引擎层 |
| 22 | + * InnoDB |
| 23 | + * MyISAM |
| 24 | + * Memory |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +**不同的存储引擎共用一个 Server层** |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | +#### 1.1 连接器 |
| 33 | + |
| 34 | +第一步,你会先连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端**建立连接**、**获取权限**、**维持和管理连接**。连接命令一般是这么写的: |
| 35 | + |
| 36 | +```mysql |
| 37 | +mysql -h$ip -P$port -u$user -p |
| 38 | +``` |
| 39 | + |
| 40 | +在完成经典的 TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。 |
| 41 | + |
| 42 | +如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。 |
| 43 | + |
| 44 | +如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。 |
| 45 | + |
| 46 | + |
| 47 | + |
| 48 | +连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist 命令中看到它。其中的 Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。 |
| 49 | + |
| 50 | +<img src="Mysql 实战.assets/f2da4aa3a672d48ec05df97b9f992fed.png" alt="f2da4aa3a672d48ec05df97b9f992fed" style="zoom:67%;" /> |
| 51 | + |
| 52 | + 客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 **wait_timeout** 控制的,默认值是 8 小时。 |
| 53 | + |
| 54 | + **长连接**: 连接成功后,如果客户端持续有请求,则一直使用同一个连接 |
| 55 | + |
| 56 | + **短连接:** 每次执行完很少的几次查询就断开连接,下次查询再重新建立一个 |
| 57 | + |
| 58 | + 建立连接的过程比较复杂,尽量使用长连接。但是因为mysql执行过程中临时使用的内存是管理在连接对象中的,这些资源在 连接断开时才会释放,所以长连接不断累积,会导致内存占用太大,被系统强行杀掉(OOM),会看到mysql异常重启了。 |
| 59 | + |
| 60 | + 解决方法: |
| 61 | + |
| 62 | + 1. 定期断开长连接。使用一段时间或者执行过一次占用大内存的大查询后断开,要查询的话再重连 |
| 63 | + |
| 64 | + 2. Mysql 5.7 或更新的版本,可以在执行较大操作后,执行 **mysql_reset_connection** 重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完的状态。 |
| 65 | + |
| 66 | +<img src="Mysql 实战.assets/image-20210513150622316.png" alt="image-20210513150622316" style="zoom:80%;" /> |
| 67 | + |
| 68 | +连接池:c3p0 druid |
| 69 | + |
| 70 | +数据库端连接池与客户端连接池是一一对应的。 |
| 71 | + |
| 72 | +网络连接需要后台线程去监听端口和解析数据。 |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | +#### 1.2 查询缓存 |
| 77 | + |
| 78 | + |
| 79 | + |
| 80 | +MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。 |
| 81 | + |
| 82 | +之前执行过的语句及其结果可能会以 **key-value** 对的形式,被直接缓存在内存中。 |
| 83 | + |
| 84 | +key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。 |
| 85 | + |
| 86 | +如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。 |
| 87 | + |
| 88 | + |
| 89 | + |
| 90 | +**但是大多数情况下我会建议不要使用查询缓存** |
| 91 | + |
| 92 | +查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。 |
| 93 | + |
| 94 | +MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻底没有这个功能了。 |
| 95 | + |
| 96 | + |
| 97 | + |
| 98 | +#### 1.3 分析器 |
| 99 | + |
| 100 | +如果没有命中查询缓存,就要开始真正执行语句了。 |
| 101 | + |
| 102 | + |
| 103 | + |
| 104 | +* **词法分析**:MySQL 需要识别出里面的字符串分别是什么,代表什么。比如说:把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。 |
| 105 | + |
| 106 | +* **语法分析: ** 根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。 |
| 107 | + |
| 108 | + 如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容。 |
| 109 | + |
| 110 | + |
| 111 | + |
| 112 | +#### 1.4 优化器 |
| 113 | + |
| 114 | +经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。 |
| 115 | + |
| 116 | +优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 |
| 117 | + |
| 118 | + |
| 119 | + |
| 120 | +```mysql |
| 121 | + |
| 122 | +mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20; |
| 123 | + |
| 124 | +``` |
| 125 | + |
| 126 | + |
| 127 | + |
| 128 | +* 既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。 |
| 129 | +* 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。 |
| 130 | + |
| 131 | +这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。 |
| 132 | + |
| 133 | + |
| 134 | + |
| 135 | + |
| 136 | + |
| 137 | +#### 1.5 执行器 |
| 138 | + |
| 139 | + |
| 140 | + |
| 141 | +MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。 |
| 142 | + |
| 143 | + |
| 144 | + |
| 145 | +* 先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误。 |
| 146 | + |
| 147 | +* 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。 |
| 148 | + |
| 149 | +```mysql |
| 150 | + |
| 151 | +mysql> select * from T where ID=10; |
| 152 | + |
| 153 | +ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T' |
| 154 | +``` |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | +**执行器的执行流程:** |
| 161 | + |
| 162 | +1. 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中; |
| 163 | + |
| 164 | +2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。 |
| 165 | + |
| 166 | +3. 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。 |
| 167 | + |
| 168 | + |
| 169 | + |
| 170 | +在数据库的慢查询日志中看到一个 **rows_examined** 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。 |
| 171 | + |
| 172 | +执行器调用一次,在引擎内部则扫描了多行,因此**引擎扫描行数**跟 **rows_examined** 并不是完全相同的。 |
| 173 | + |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | + |
| 178 | +> 如果表 T 中没有字段 k,而你执行了这个语句 select * from T where k=1, 那肯定是会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。你觉得这个错误是在我们上面提到的哪个阶段报出来的呢? |
| 179 | +
|
| 180 | + |
| 181 | + |
| 182 | +分析器 |
| 183 | + |
| 184 | + |
| 185 | + |
| 186 | +总的流程分析 : |
| 187 | + |
| 188 | +1. java客户端程序通过 mysql数据库驱动与 mysql建立网络连接 (建立连接和关闭连接耗时较大,所以使用了连接池) |
| 189 | +2. 数据库后台线程接收到请求拿到数据,查询一下在不在缓存中。 |
| 190 | +3. 通过sql接口执行,分析器分析sql语句。识别一下表、变量,语法检查一下。 |
| 191 | +4. 优化器,生成最优的执行计划 |
| 192 | +5. 执行器判断有没有权限,调用存储引擎的接口去完成执行计划 (操作磁盘数据) |
| 193 | + |
| 194 | + |
| 195 | + |
| 196 | +### 2.日志系统 |
| 197 | + |
| 198 | + |
| 199 | + |
| 200 | +一条更新语句的执行流程又是怎样的呢?MySQL 可以恢复到半个月内任意一秒的状态,惊叹的同时,你是不是心中也会不免会好奇,这是怎样做到的呢? |
| 201 | + |
| 202 | + |
| 203 | + |
| 204 | +两个重要的日志模块: |
| 205 | + |
| 206 | +1. **redo log** (重做日志) |
| 207 | +2. **bin log** (归档日志) |
| 208 | + |
| 209 | + |
| 210 | + |
| 211 | +#### 2.1 redo log |
| 212 | + |
| 213 | +WAL 技术 Write-Ahead Logging 先写日志,再写磁盘。 |
| 214 | + |
| 215 | +具体来说,当有一条记录需要更新的时候,InnoDB 引擎会先把记录写到 redo log ,并更新内存,这个时候更新就算完成了,同时,InnoDB引擎会在适当的时候将这个操作记录更新到磁盘,而这个更新往往是在系统比较空闲的时候。 |
| 216 | + |
| 217 | + |
| 218 | + |
| 219 | +例子: 粉板(redo log) 账本(bin log) |
| 220 | + |
| 221 | +如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。 |
| 222 | + |
| 223 | + |
| 224 | + |
| 225 | +与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。 |
| 226 | + |
| 227 | + |
| 228 | + |
| 229 | +<img src="Mysql 实战.assets/16a7950217b3f0f4ed02db5db59562a7.png" alt="16a7950217b3f0f4ed02db5db59562a7" style="zoom:67%;" /> |
| 230 | + |
| 231 | + |
| 232 | + |
| 233 | + |
| 234 | + |
| 235 | +write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。 |
| 236 | + |
| 237 | + |
| 238 | + |
| 239 | +write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。 |
| 240 | + |
| 241 | + |
| 242 | + |
| 243 | +有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 **crash-safe** 。数据库重启了,**内存中的数据页没有同步到磁盘中**,可以通过redo log日志恢复。 |
| 244 | + |
| 245 | + |
| 246 | + |
| 247 | +#### 2.2 bin log |
| 248 | + |
| 249 | +MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。 |
| 250 | + |
| 251 | + |
| 252 | + |
| 253 | +上面的粉板 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。 |
| 254 | + |
| 255 | + |
| 256 | + |
| 257 | +> 为什么会有两份日志呢? |
| 258 | +
|
| 259 | +最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。 |
| 260 | + |
| 261 | + |
| 262 | + |
| 263 | +**redo log 和 bin log 的不同处:** |
| 264 | + |
| 265 | +1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。 |
| 266 | + |
| 267 | +2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。 |
| 268 | + |
| 269 | +3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。 |
| 270 | + |
| 271 | + |
| 272 | + |
| 273 | +执行器和 InnerDB引擎在执行这条 Update语句的流程: |
| 274 | + |
| 275 | +1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。 |
| 276 | +2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。 |
| 277 | +3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。 |
| 278 | +4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。 |
| 279 | +5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。 |
| 280 | + |
| 281 | + |
| 282 | + |
| 283 | +update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。 |
| 284 | + |
| 285 | +<img src="Mysql 实战.assets/2e5bff4910ec189fe1ee6e2ecc7b4bbe.png" alt="2e5bff4910ec189fe1ee6e2ecc7b4bbe" style="zoom:67%;" /> |
| 286 | + |
| 287 | + |
| 288 | + |
| 289 | +redo log 的写入拆成了两步: prepare commit 这就是 **两阶段提交** |
| 290 | + |
| 291 | +如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。 |
| 292 | + |
| 293 | +> 这个概率是不是很低,平时也没有什么动不动就需要恢复临时库的场景呀? |
| 294 | +
|
| 295 | + |
| 296 | + |
| 297 | +其实不是的,不只是误操作后需要用这个过程来恢复数据。当你需要扩容的时候,也就是需要再多搭建一些备库来增加系统的读能力的时候,现在常见的做法也是用**全量备份**加上应用 **binlog** 来实现的,这个“不一致”就会导致你的线上出现**主从数据库不一致**的情况。 |
| 298 | + |
| 299 | + |
| 300 | + |
| 301 | +简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。 |
| 302 | + |
| 303 | +redolog和binlog具有关联行,在恢复数据时,redolog用于恢复主机故障时的未更新的物理数据,binlog用于备份操作。 |
| 304 | + |
| 305 | +每个阶段的log操作都是记录在磁盘的,在恢复数据时,redolog 状态为commit则说明binlog也成功,直接恢复数据; |
| 306 | + |
| 307 | +如果redolog是prepare,则需要查询对应的binlog事务是否成功,决定是回滚还是执行。 |
| 308 | + |
| 309 | +**通过查看redo日志状态,手工决定后续操作,以此来保证数据一致性。** |
| 310 | + |
| 311 | + |
| 312 | + |
| 313 | + |
| 314 | + |
| 315 | +**两个重要参数** |
| 316 | + |
| 317 | +redo log 用于保证 crash-safe 能力。**innodb_flush_log_at_trx_commit** 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。 |
| 318 | + |
| 319 | +**sync_binlog** 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。 |
| 320 | + |
| 321 | + |
| 322 | + |
| 323 | + |
| 324 | + |
| 325 | +### 3.事务 |
| 326 | + |
| 327 | + |
| 328 | + |
| 329 | +#### 3.1 隔离性与隔离级别 |
| 330 | + |
| 331 | + |
| 332 | + |
| 333 | +脏读: 一个事务读取了另一个事务未提交的数据 |
| 334 | + |
| 335 | +不可重复读: 一个事务读取表中数据,多次读取结果不同 |
| 336 | + |
| 337 | +幻读: 一个事务读取到了别的事务插入的数据,导致前后读取不一致 |
| 338 | + |
0 commit comments