Mysql 实战笔记 (一) 基础

一、mysql 基础架构

Mysql 实战笔记 (一) 基础_第1张图片
大体来说,MySQL 可以分为 Server 层和存储引擎层两部分。Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

连接器

一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了。
怎么解决这个问题呢?你可以考虑以下两种方案。

  1. 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
  2. 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

查询缓存

大多数情况下不要使用查询缓存

分析器

词法分析,错误时会收到 "You have an error in your SQL syntax",也会在这个阶段判断查询条件是否包含在这个表。在分析阶段判断语句是否正确,表是否存在,列是否存在等。

优化器

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

开始执行的时候,要先判断一下你对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误。
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
为什么不在执行器之前判断权限? 有些时候,SQL语句要操作的表不只是SQL字面上那些。比如如果有个触发器,得在执行器阶段(过程中)才能确定。优化器阶段前是无能为力的

二、日志系统

redo log (InnoDB 引擎特有的日志)

WAL技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log 里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。
时间轮
Mysql 实战笔记 (一) 基础_第2张图片

binlog(server层的日志)

这两种日志有以下三点不同。

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
  4. redo log 记录数据页 “做了什么改动”,Binlog有两种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有。

Mysql 实战笔记 (一) 基础_第3张图片
浅色框表示是在 InnoDB 内部执行的,深色框是在执行器执行

两阶段提交

将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
一般备份都是全量备份加上binlog
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。

三、事务隔离

隔离性与隔离级别

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。
串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
Mysql 实战笔记 (一) 基础_第4张图片
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念
而“串行化”隔离级别下直接用加锁的方式来避免并行访问。

事务隔离的实现

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
Mysql 实战笔记 (一) 基础_第5张图片
同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)
回滚日志的删除,当系统里没有比这个回滚日志更早的 read-view 的时候。
长事务意味着系统里面会存在很老的事务视图

四、索引

哈希表这种结构适用于只有等值查询的场景。
有序数组在等值查询和范围查询场景中的性能就都非常优秀。有序数组索引只适用于静态存储引擎。

基于主键索引和普通索引的查询有什么区别?
如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵B+ 树;
如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。
基于非主键索引的查询需要多扫描一棵索引树。
drop主键索引会导致其他索引失效,但drop普通索引不会

索引维护

插入数据的时候,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。

覆盖索引

如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,建立联合索引就有意义了(利用覆盖索引加速高频查询)。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。当然,索引字段的维护总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。

最左前缀原则

为什么会有最左前缀?
索引项是按照索引定义里面出现的字段顺序排序的
因为可以支持最左前缀,所以当已经有了 (a,b)这个联合索引后,一般就不需要单独在 a 上建立索引了。
在建立联合索引的时候,如何安排索引内的字段顺序?
第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
如果有 b 也必须要建索引,那么考虑的原则就是空间了,比如,name 字段是比age 字段大的 ,那我就建议你创建一个(name,age) 的联合索引和一个 (age) 的单字段索引

索引下推

MySQL 5.6 引入的索引下推优化(index condition pushdown),可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
select * from tuser where name like '张 %' and age=10
Mysql 实战笔记 (一) 基础_第6张图片
有索引下推
它们的区别是,InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。

重建索引合不合理?
索引可能因为删除,或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。

五、锁

全局锁

让mysql 只读,其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。
Flush tables with read lock (FTWRL)
set global readonly=true
一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。
二是,在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。

表级锁

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)
表锁的语法是 lock tables ... read/write。与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。
当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁
Mysql 实战笔记 (一) 基础_第7张图片
session C 会被 blocked,是因为 session A 的 MDL 读锁还没有释放,而 session C需要 MDL 写锁,因此只能被阻塞。
如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新session 再请求的话,这个库的线程很快就会爆满。
如何安全地给小表加字段?
首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。比较理想的机制是,在 alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。

行锁

在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
如果一个事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往一个事务最后放。因为这样冲突锁的时间会最少

死锁和死锁检测

一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行,然后通过重试死锁业务。 将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。如果要加锁访问的行上有锁,才会检测
怎么解决由这种热点行更新导致的性能问题呢? 问题的症结在于,死锁检测要耗费大量的 CPU 资源
1.控制并发度
2.也可以考虑通过将一行改成逻辑上的多行来减少锁冲突

深入事务隔离

consistent read view,即InnoDB 在实现 MVCC 时用到的一致性读视图,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id(row trx_id就是事务id)。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 rowtrx_id
Mysql 实战笔记 (一) 基础_第8张图片
语句更新会生成 undo log(回滚日志)
图中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。
**对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
对于读提交,查询只承认在语句启动前就已经提交完成的数据;**
而当前读,总是读取已经提交完成的最新版本。

你可能感兴趣的:(mysql)