目录
前言
1. 一条select是怎么执行的?
1.1. 连接器
1.2. 查询缓存
1.3. 优化器
1.4. 执行器
2. 一条update语句是怎么执行的
2.1. update执行步骤
2.2. binlog、redolog
2.3. update时,两阶段提交2PC
2.4. 双1 \ 组提交
2.4.1. 双1
2.4.2. 组提交
2.5. 恢复数据之“删库跑路”
3. 索引
3.1. 索引是什么?
3.2. 索引采用那些数据结构?
3.2.1. HASH索引和B+Tree索引对比
3.2.2. 为什么底层使用B+Tree,不使用二叉树、BST、AVL、RBT
3.3. 索引种类
3.3.1. 聚簇/主键索引、非聚簇/非主键索引
3.3.2. 几个小问题
3.3.3. MySQL中innodb表主键设计原则
3.3.4. 覆盖索引
3.3.5. 全文索引
3.3.6. 索引下推
3.3.7. 优化索引口诀
3.3.8. 小案例: 索引分析
4. 存储引擎 innodb/myisam
5. 范式
6. 事务
6.1. 特性: ACID
6.2. 事务并发问题
6.2.1. MVCC与幻读 TODO
6.3. 四种隔离级别
6.4. 事务原理
6.4.1. 事务日志redo/undo
6.4.2. 二进制日志Binlog
7. 锁
7.1. 共享锁(读锁)/ 独占锁(写锁)
7.2. 乐观锁 / 悲观锁
7.3. 表锁 / 行锁
7.4. 总结
7.5. 间隙锁
7.6. 死锁
8. SQL优化
8.1. 单机优化 explain
8.1.1. 慢查询
8.1.2. SQL语句优化
8.1.3. 没建立索引,就建立索引
8.1.4. 对于已经建立的索引,可能存在索引失效:
8.1.5. 数据插入优化
8.1.6. 连接池
8.1.7. 分页查询 limit
8.1.8. 关联查询 join
8.2. 集群优化
8.2.1. SQL/Redis主从复制
8.2.2. 主从复制 + 读写分离
8.2.3. 分库分表(水平/垂直)
8.2.4. 缓存redis
9. mysql不常用的能力
9.1. 存储过程
9.2. 触发器
10. 场景问题分析
10.1. select * 与 select全部字段
10.2. varchar/char区别
10.3. count(*)count(1) count(字段)
10.4. drop、truncate、delete
11. [极客时间] “脏页”导致查询速度变慢
11.1. 为什么mysql突然变慢了?
11.2. innodb刷写脏页的控制策略
12. [极客时间] 为什么我只查一行的语句,也执行这么慢?
12. [极客时间] 问题: 重建索引
13. [极客时间] 普通索引、唯一索引应该怎么选择?
13.1. 查询过程: 二者差别很小
13.2. insert过程
13.2.1. 先说结论
13.2.2. 插入过程
13.2.3. 实际举例
13.3. change buffer的使用场景
14. [极客时间] 为什么表数据delete删掉一半,表文件大小不变?
15. SQL注入
15.1. 什么是SQL注入
15.2. SQL注入方式
15.3. 防止SQL注入,我们需要注意以下几个要点:
15.4. SQL注入特殊字符处理
15.5. SQL注入--解决方案
16.生产环境配置
17.sql命令
18. 查漏补缺
\ \ \ \ 写在最前,本文主要以知识框架为主,根据自己对知识掌握的情况,进行知识点的梳理(有的知识点实际上篇幅很大,但是由于自己理解,就没有详细叙述)。
select [ALL|DISTANCE] <目标列表达式> from <表名或视图名> where <条件表达式> group by <列名> having <条件表达式> order by <列名> ASC|DESC
书写顺序: select...from...where...group by...having...order by..
执行顺序: from...where...group by...having...select...order by...
select * from A join B on A.id = B.id select * from A left join B on A.id = B.id select * from A right join B on A.id = B.id
select 列a,聚合函数 from 表明 where 过滤条件 group by 列a [DESC/ACE] order by 列名
客户端 ==> 连接器(管理链接,权限验证)==> 查询缓存(命中,直接返回结果)==> 分析器(词法分析,语法分析)==> 优化器(执行计划生成,索引选择)==> 执行器(操作引擎,返回结果)==> 存储引擎(存储数据,提供读写接口)
长连接: 连接建立成功后,如果客户端持续有请求,则一直使用同一个链接
短连接: 每次执行完很少的几次查询,就断开连接,下次查询重新再建立一个连接
建议: 尽量减少连接的建立(因为连接建立过程是比较复杂的),即尽可能的使用长连接
但是: 全部使用长连接,SQL占用内存会增长的非常快(连接是占用资源的,内存无限增长会导致OOM)
解决方案:
① 定期断开长连接
② 如果使用的是MySQL5.7或更新版本,每次在执行一个比较大的操作后,通过执行mysql_reset_connection来重新初始化连接资源(该过程不会重连和重新做权限验证,但是会将连接恢复到刚刚创建完成的状态)
查询请求到来时,先去查询缓存中查找(查询缓存中是否存在该条执行语句,它是以key-value形式,直接存在内存中),命中直接返回
注意: 不建议mysql开启查询缓存(因为查询缓存失效非常频繁: 只要对一个表更新,这个表上所有的查询缓存都会被清空==>对于更新压力大的数据库来说,查询缓存的命中率非常低)(MySQL 8.0 版本直接将查询缓存的整块功能删掉了)
决定使用哪个索引
在多表关联join时,决定各表的连接顺序
判断你对这张表有没有查询权限
根据引擎的定义,去使用该引擎提供的读写接口
[问题]: 如果表 T 中没有字段 k,而你执行了这个语句 select * from T where k=1, 那肯定是会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。你觉得这个错误是在我们上面提到的哪个阶段报出来的呢?
答案: 分析器
与查询流程一致,都会经过(连接器、查询缓存、分析器、优化器、执行器、存储引擎)
与查询流程不一样的是,更新流程还会涉及到2个日志,即: redolog重做日志、binlog归档日志
[举例说明] update table set val=2 where id=1; 执行过程
先查询到id=2,执行器拿到引擎的数据行,给val设置为2
prepare: 执行器调用接口,将该行数据更新到内存,同时将更新操作记录到redolog,此时redolog的状态处于prepare
执行器生成这个操作的binlog,将binlog写入磁盘
commit: 执行器调用引擎的提交事务的接口,引擎把刚刚写入的redolog状态改为commit状态,更新完成
可以看到,一个事务完整提交前,此时redolog、binlog都已经刷入磁盘
写方式方式
redolog 是循环写的(环形内存队列),空间固定会用完(WAL技术,全程是Write-Ahead-Log,关键点是先写环形内存队列,再写磁盘)。环形内存队列的数据刷新到磁盘的时机
当环形内存队列未被写满时,后台空闲的时候刷写日志
当环形内存队列被写满时,这时候不能在执行新的更新,得停下来先刷新写入磁盘中
binlog 是可以追加顺序写入的
对比
binlog(归档日志):
binlog是SQL的server层实现的,所有存储引擎都可以使用
binlog是逻辑日志(记录所有的逻辑操作),记录的是“某个sql语句”或者“某行的值”
归档日志,用于: 数据恢复、主从同步
binlog是可以追加写的(追加写指的是binlog文件写到一定大小后,会切换到下一个,并不会覆盖以前的日志)
redolog(事务日志)
innodb引擎独有,用于崩溃恢复(当数据库发生异常宕机重启后,之前提交的记录不会丢失,即crash-safe)
物理日志
redolog是有空间大小限制的,空间固定会写满,是循环写的(类比于尺寸固定的环形队列)
2.2.1. Mysql的checkpoint机制
极客时间mysql45讲,有一张图,详细请看02 | 日志系统:一条SQL更新语句是如何执行的?-极客时间
简介
redolog环形队列,包含4个ib_logfile,即ib_logfile0、ib_logfile1、ib_logfile2、ib_logfile3
包含2个指针,一个是write_pos,另外一个是checkpoint。可以理解为:
write_pos = write_pos:每次写write_pos都是向后移动(写的场景实际上是有mysql更新操作,记录该操作的redolog)
checkpoint=read_pos:每次读都是向后移动(读的场景实际上是消费,即将redolog刷写入磁盘)===> 每次checkpoint向后移动,都是刷写磁盘的操作
目的
checkpoint机制,是为了防止redolog频繁刷新缓存提出的机制,防止每次mysql有更新操作都刷写磁盘
checkpoint的2种机制
Fuzzy checkpoint:进行部分脏页的刷新,有效循环利用Redo日志
根据设置的“刷新比率”、“环形队列空闲大小”、“时间周期”,刷写磁盘
Sharp checkpoint:发生在关闭数据库时,将所有脏页刷回磁盘
当mysql关闭时,强制刷新
写redolog(处于prepare阶段) ==>写binlog==>提交事务,处于commit状态(将redolog的状态设置为commit)
上面将redlog的写入拆成2个步骤:prepare和commit,这就是2PC
Q: 为什么要用2PC?如果不用2PC会存在什么问题?请举例说明。
举例:假设update table set val=val+1 where id=1(假设现在val=1,修改后val=2)
场景1:先写redolog,后写binlog
redolog写入成功,binlog未写入
崩溃重启后,val已经被更新为2,但是binlog存放的val仍然为1
此时,binlog主从同步后,将val=1同步给从库,此时就出现主从数据不一致
binlog写入成功,redolog未写入
如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0
但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同
[补充一个Q/A]
Q1:如何实现ACID中的D持久化的,可否直接写硬盘(内存写 + WAL?不行,因为效率的原因)
A1:D持久化由redolog保证;不能直接写入磁盘,原因是直接写入磁盘效率太低。写入Redolog的过程上面已经介绍了,即先写入redolog内存环形队列,然后刷写磁盘
下面两个参数都建议设置成1
sync_binlog = 1
每次事务的binlog都持久化到磁盘 ==> 保证每次事务的binlog都持久化到磁盘,sql重启后binlog不会丢失
innodb_flash_log_at_commit =1
每次事务redolog都持久化到磁盘 ==> 保证mysql异常重启后数据不会丢失
目的: 组提交(降低redo-buffer刷写磁盘的次数)
日志逻辑序列号(log sequence number,LSN): LSN是单调递增的,用来对应redolog的一个个写入点。每次写入长度length的redolog,LSN的值都加上length。
举例: 存在三个并发事务 (trx1, trx2, trx3) ,都在 prepare 阶段,都写完 redo log buffer,持久化到磁盘的过程,对应的 LSN 分别是 50、120 和 160
trx1是第一个到达的,会被选为这个组的leader
假设trx2、trx3完成的比较快,那么: 等trx1去flush磁盘的时候,此时这个组里已经有3个事务了,这个时候LSN变成了160
trx1 去写盘的时候,带的就是 LSN=160,因此等 trx1 返回时,所有 LSN 小于等于 160 的 redo log,都已经被持久化到磁盘(这时候 trx2 和 trx3 就可以直接返回了)
组提交参数设置
binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync
binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync
使用场景: 比如某天下午两点发现中午12点有一次误删表操作,需要找回数据,应该怎么做?
找到最近一次全量备份,如果你运气好,可能就在昨晚备份了一个
从这个备份的时间点开始,将备份的binlog依次取出来,重放到中午误删表之前的时刻(先恢复到一个临时表)
把表数据从临时表取出来,按照需要恢复到线上表
加快SQL查询速度的数据结构,引来的缺点(降低更新表的速度,保存索引占用空间)
HASH索引: 底层是哈希表,存储KV,在进行查找时,O(1)就可以找到相应的键值
B+Tree索引: B+树,多路平衡查找树,每次查询都是从根节点出发,查找到叶子节点就可以获得所查的值
MySQL中的数据一般是放在磁盘中的,读取数据的时候肯定会有访问磁盘的操作, B+Tree是专门为磁盘IO设计的一种多路平衡查找树,它的高度远远小于其他数据结构,因此访问磁盘的数量极小,磁盘IO所花的时间少。
B+树为什么比B树更适合?
1)B+树的内部节点不存放数据,只有叶子节点存放数据 ==> 其内部节点相对B树更小
2)B+树的叶子节点形成链表,便于范围查询
3)B+树的查询效率更加稳定: B+树的数据都存储在叶子结点中,所以任何关键字的查找必须走一条从根结点到叶子结点的路,所有关键字查询的路径长度相同,导致每一个数据的查询效率相当
HASH索引等值查询更快O(1),但是不支持采用key排序/范围查询/最左匹配原则/模糊查询
HASH函数选择不好时,会发生HASH碰撞,导致查询效率降低
HASH索引任何时候都避免不了回表查询,B+索引在覆盖查询时,可以避免回表查询
HASH索引是存放在内存中的,占用内存资源太大
这个知识点很基本,就不详细阐述了,这是由数据结构的时间复杂度决定的
二叉树/BST会退化成链表
AVL树旋转代价太高
RBT树太高
普通索引: 最基本的索引,没有任何限制
唯一索引: 不允许具有索引值相同的行,从而禁止重复索引或键值,“唯一”: 假如在name上建立唯一索引,那么,整个表就不能有两个行name相同的情况
问题: 唯一索引允许为NULL么?出现NULL会造成什么影响?
答: 允许为NULL。NULL表示的是未知,因此两个NULL比较的结果既不相等,也不不等,所以结果仍然是未知
组合索引: 又叫联合索引,一个索引包含多个列。(最左前缀匹配原则)
全文索引: FULLTEXT,它是通过关键字语义匹配分析方式来进行过滤查询,仅适用于MYISAM引擎的数据表
聚簇索引、非聚簇索引,并不是一种索引类型,而是一种数据存储方式。
❓: 二者的核心区别
答: 核心区别是【索引的位置存放的是真实数据?还是主键值?】
① 主键索引的叶子节点存放的是要查找的真实数据,主键索引也被称为聚簇索引
② 非聚簇索引的叶子节点存放的不是真实的数据,而是主键值,它也被称为二级索引)
聚簇索引: 索引B+Tree的叶子节点上存放了数据行的物理地址
非聚簇索引: 非聚簇索引B+Tree树的叶子节点存储的不再是行的物理位置,而是主键值;索引数据时,需要两次查询(通过非聚簇索引查找到主键值,再通过主键值在主键索引中查找到该主键对应的真实数据)
[Q1] ❓: 可以存在多个聚簇索引么?
[A1]: 聚簇索引的顺序就是数据的物理存储顺序,正式因为如此,所以一个表最多只能有一个聚簇索引。
二者的查询方式案例,并引出什么是“回表”
如果查询语句是select * from table where ID=100,即主键查询方式,则只需要搜索ID=100的B+树的叶子节点
如果查询语句是select * from table where score=30,即非主键查询方式,则先通过非聚簇索引查找score=30的主键索引(即ID索引),再经过主键索引搜索一次才查找到数据行。这个操作也被称之为回表查询
参考链接: 为什么选用自增量作为主键索引,InnoDB中没有主键是如何运转的,总结下个人理解。
为什么innodb插入时,一般按照主键增加的方式,而不选用uuid
答: 这与索引的B+树数据结构有关。innodb的主键索引上存放的是主键值,采用递增的方式向B+Tree中插入数据,可以最少的降低B+Tree分裂合并次数。如果采用乱序插入,创建B+Tree的过程性能就比较低下了。
如果innodb没有主键,会发生什么?
先说结论: 每个InnoDB引擎的表必须有一个“聚簇索引”
答: 如果InnoDB引擎的表没有主键or没有不为NULL的唯一索引,innodb内部会合成一个隐藏的聚簇/主键索引,该索引一般采用行ID,它是6个字节(48bit)的。
innodb的几个小知识
innodb能不能不显示设置主键?可以,如果不设值
主键可以为空么?允许,但是尽可能不要这么做
聚集索引可以有多个么? 不能
主键设置的原则
一定要显示定义主键
采用自增列,数据类型采用int,尽可能小
将主键放在表的第一列
采用与业务无关的单独列
innode的聚集索引 and 主键
聚集索引只能有一个
聚集索引的叶子节点上存放的是真实的数据
有主键时会自动将主键设置为聚集索引
若没显示定义主键
① 会选择第一个没有null值的唯一索引作为聚集索引
② 如果①不符合,会自动添加一个不可见的6byte的rowid作为聚集索引
【1】上面提到了非聚簇索引的回表查询,会查找主键索引,进而查到数据,那么,请问: 所有的非聚簇索引都一定会经过回表查询么?
答: 不是,覆盖索引解决了该问题。
覆盖索引: 当sql语句的所求查询字段(select列)和查询条件字段(where子句属性列)全都包含在一个索引中,就可以直接使用索引查询而不需要回表!
【2】怎么通过覆盖索引优化回表查询?
答: 建立联合索引,使要查找的列都在索引中,避免回表查询。即: select (查找项) from where (条件项),查找项 in 条件项 && 查找项 == 索引
区别: 全文索引、模糊匹配like%
二者有本质的区别
全文索引是以语法分析的方式来分词的,而like是带通配符的匹配
使用场景: 全文索引(如,可以对特定词的同义词形式来进行查询,一般用于电子商务网站),like模糊匹配(由常规字符串和通配符组成,要符合匹配准则)
目的:减少回表次数
英文: Index Condition PushDown
select * from where name like 'zhang%' and age>18
因为是select * ,所以一定会触发回表查询,以下有2种查法
查找zhang开头的主键,然后回表查询所有的记录,再过滤age>18的行
查找zhang开头的数据,再筛选出age>18的记录,再回表查询所有数据
优化器会选择第2中,因为2先通过两个条件过滤会得到更少的信息,再回表查询(这样先通过过滤筛选掉一批数据,使得回表次数减少)
全值匹配我最爱,最左前缀要遵守
带头大哥不能丢,中间兄弟不能断
索引列上少计算,范围之后全失效
like百分写最右, 覆盖索引不写*
不空值还有or,索引失效要少用
var引号不能丢,SQL高级也不难
下面,列出的几条,就是上面口诀的具体展现
查询频率高的列、经常需要排序、分组、联合的字段建立索引
创建索引的数目不宜过多,过多会占用空间,且影响表的更新速度
选择唯一性索引(如学生的学号)
不在索引上做运算符操作
范围条件放最后: 因为范围条件后的索引都会失效
字符类型要加双引号: 隐式转换,索引会失效
条件字段函数操作,导致索引失效
隐式字符编码转换,索引会失效
or替换为union: A or B,如果A建立了索引,B没有建立索引,则索引通通不走
like查询要当心: like %keyword索引失效,like keyword%索引有效
不等于!=要慎用: 索引失效
考虑在where或order by 或 group by涉及的列建立索引
innodb无索引or索引失效时,行锁会升级为表锁d
之前听分享课,mysql加锁其实是对索引进行加锁,如果没有索引,锁会退化成表锁
建立联合索引(a,b,c),然后在where条件是: a=xxx and b>xxx and c=xxx,这个时候联合索引是否被用到?
答: 索引会在>后面停止
建立联合索(a,b,c),where条件是: c=xxx and b>xxx and a=xxx,索引是否被用到?
答: 这个where条件跟第一个是一致的,数据库会进行优化。
表并不存在索引,查询条件是where a=xxx order by b,这个时候应该如何建立索引?
答: 建立一个ab的联合索引,会先过滤a索引,在排序b索引
区别
innodb/myisam最主要的差别: Innodb 支持事务处理与外键和行级锁,而MyISAM不支持
InnoDB | myisam | |
---|---|---|
事务 | 支持(可靠性要求高) | 不支持事务 |
锁级别 | 行锁(适用于表更新较频繁) | 表锁(适用于查询多,插入和删除少) |
是否支持外键 | 支持 | |
查询 | 更快 | |
全文索引 | 支持 | |
适用场景 | (1)可靠性要求比较高,或者要求事务 (2)表更新和查询都相当的频繁,并且行锁定的机会比较大的情况 | (1) 做很多count的计算 (2) 查询非常频繁,插入不频繁 |
innodb支持事务、外键、行锁(默认)/表锁,不支持全文索引
innodb必须有主键,没有显示指定主键,mysql会默认创建主键_rowid;而myisam可以没有主键
innodb是主键索引/聚集索引,myisam是非主键索引/非聚集索引
存储文件
innodb: frm表结构文件、ibd数据文件(包括索引/数据)
Myisam: frm表结构文件、MYD数据文件、MYI索引文件
出现原因: 数据冗余、插入/删除/更新异常
采用范式对表进行拆分
第一范式: 属性列不可拆分(保证属性列的原子性)
第二范式: 消除了非主属性部分依赖
于候选码
(sno,cno,姓名,score,系名,系主任)
候选码是(sno,cno)
“姓名”部分依赖于“sno”,因此不满足2NF,应该拆表,即(sno,姓名,系名,系主任)+(sno,cno,score)
第三范式: 消除了非主属性传递依赖
于候选码
(sno,姓名,系名,系主任)
候选码是(sno)
“系主任”依赖于“系名”(非候选码),“系名”依赖于“sno”(候选码),那么,“系主任”(非主属性)传递依赖于“sno”(候选码),不符合3NF,应该拆表,即: (sno,姓名,系名)+(系名,系主任)
引出事务的原因: 多用户/多程序/多线程,存在同时对表中一个元组进行DML操作,如果不进行控制,就会造成数据不一致性
原子性: 最小单元,整个事务的所有操作要么做,要么都不做 (undo log)
一致性: 从一种一致性状态转换为另一种一致性状态,事务开始/结束都保证完整性。 ==> 原子性/持久性/隔离性,保证了一致性
一致性不好理解,和CAP里面的一致性不一样(CAP的一致性是强/弱一致性/最终一致性)
反过来看,数据库的不一致性指的是:丢失修改、脏读、不可重复读、幻读,反过来就是一致性
隔离性: 并发执行的各个事务之间不相互干扰
隔离级别:读未提交、读提交、可重复读、串行化
持久性: 事务一旦提交,结果将永久保存在数据库中(redo log)
賍读(读取未提交数据): 事务B修改某个数据后,未提交,被事务A读到;之后事务B回滚修改数据操作,事务A之前读到的数据就是脏数据
不可重复读(在一个事务中前后读取的数据不一致): 事务A读取同一个数据经历的时间很长,第一次读时,该数据为Val1,之后,该数据被事务B修改,之后事务A再去读该数据,结果为Val2,这就叫做不可重复读
幻读(前后多次读取,数据总量不一致): 与不可重复读类似,都是在一个事务中,两次读取结果不一样。区别在于幻读是在一个事务中读取到数据的条数不一致,如: 事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,称为幻读
mvcc是为了解决什么问题而衍生的
读未提交: 所有事务都能够读取其他事务未提交的数据,会导致賍读、不可重复读、幻读
读已提交: 所有事务只能读取其他事务已经提交的数据,但是还会出现在一个事务中前后读取内容不一致的问题。
可重复读: 在一个事务中,不允许Update操作,允许Add操作,因此能保证在一个事务中读取数据内容是一致的(能解决【脏读】,但是不能保证读取到数据条目数一致(会发生幻读)
可串行化: 所有的事务都顺序串行执行,不存在冲突
innodb默认隔离级别不是最高的,而是倒数第二高的,即可重复读级别(RR)
之前已经说过,innodb支持事务,它具有事务日志redo/undo,而myisam不支持事务,它没有这两种事务日志。
在事务提交之前,会先写入redo/undo日志
redo log 重做日志,保证事务持久性
保证: 所有已经提交的事务的数据仍然存在
redo_log作用: 用于记录事务的变化,记录的是数据修改之后的值,不管事务是否提交都会记录。如果某时刻系统宕机,重启后,可以通过redo log恢复之前的数据
数据库宕机恢复过程: 先从redo log中把未落盘的脏页数据恢复回来,重新写入磁盘,保证用户数据不丢失
redo_log写入磁盘过程: 先写入redo log buffer(redo log缓存),之后调用fsync,刷新写入redo log物理磁盘(它的写入是可进行参数配置的)
undo log 回滚日志,保证事务原子性
保证: 所有没有提交的事务的数据自动回滚
数据更新时,会写入undo log(该操作和数据更新执行操作相反,即如果是插入数据,则undo log是删除数据)
回滚: 当事务失败或回滚时,根据undo log,把未提交的事务回滚到更新前的状态
为什么要有Binlog
不管SQL使用那种存储引擎,都有Binlog,它是Server层的(而redo log是innodb层的)
SQL引入二阶段提交,保证主从数据的一致性!
SQL会为每一个事务,分配一个事务ID(XID)
commit被分为2个阶段: papare/commit
Binlog会被当作事务协调者
① 准备阶段(papare)
此时SQL已经成功执行,生成XID信息以及Redo/Undo的内存日志
然后调用papare方法,将事务状态设置为TRX_PREPARED,并将Redo log刷入磁盘
② 提交阶段(commit)
(1)提交 or 回滚
如果事务涉及的所有存储引擎的papare都执行成功,则将SQL语句写入Binlog,调用fsync写入磁盘
如果事务涉及的所有存储引擎的papare都执行失败,则SQL语句不会写入Binlog,此时事务回滚
(2)告诉引擎进行commit
(假设papare成功,完成事务提交)会清除undo信息,调用fsync刷redo日志到磁盘,将事务设置为TRX_NOT_STARTED状态
由上面的二阶段提交流程可以看出
一旦步骤②中的操作完成,就确保了事务的提交。此外需要注意的是,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。
步骤2的fsync参数由sync_binlog=1控制,步骤3的fsync由参数innodb_flush_log_at_trx_commit=1控制,俗称“双1”,是保证CrashSafe的根本。
6.4.1.1. 事务启动方式有一下几种
显示启动事务: begin/start transaction,commit/roolback
set autocommit=0 该命令会将这个线程的自动提交关闭掉,即:
只要你执行一个select语句,事务就启动了,而且并不会自动提交(直到你主动执行commit\rollback语句,或者断开连接)
建议: 总是使用set autocommit=1
为什么尽量避免长事务?
长事务意味着系统里面会存在很老的事务视图,在这个事务提交之前,回滚记录都要保留,这会导致占用大量的存储空间
长事务还占用锁资源,可能会拖垮库
长事务,commit后才会写入binlog,会造成主从延时问题
binlog与redo log类似,它记录了对数据库执行更新的所有操作,但是二者还是有本质的区别
binlog - 主要用作: 主从复制\即时点恢复
redo log | binlog | |
---|---|---|
场景 | crash-recovery宕机恢复(保证事务持久性),事务安全 | point-time-recovery恢复某个时间点(即时点恢复);主从复制 |
层次 | 只有innodb支持事务的存储引擎有(innodb层) | 所有SQL都支持(Server层) |
写入时机 | 在事务进行中不断地写入,并日志不是随事务提交而顺序写入的 | 只在事务完成后,进行一次写入 |
功能 | 保证事务一致性 | 记录数据库DML操作,能够实现主从复制 |
当数据库有并发事务时,可能会产生数据不一致,锁可以保证访问次序
(1)共享锁(读锁): 可以被多个事务同时读,但是加了读锁,不允许加写锁
(2)独占锁(写锁): 加了写锁,不允许加读锁or写锁
(1)悲观锁: 只允许一个锁进入
(2)乐观锁: MySql最经常使用的乐观锁是进行version版本控制,也就是在数据库表中增加一列,记为version。
① 当将数据读出时,将版本号一并读出;当数据进行更新时,会对这个版本号进行加1;
② 当提交数据时,会判断数据库表中当前的version列值和当时读出的version是否相同;
③ 若相同,说明没有进行更新的操作,不然,则取消这次的操作。
数据库锁: 处理并发问题(作为多用户共享的资源,当出现并发访问时,数据库需要合理的控制资源访问规则)
加锁范围划分: 库锁(全局锁)、表锁、行锁
全局锁
应用场景: 全库逻辑备份(就是把整个表select出来的数据保存成文本)
以前有一种做法,是通过 FTWRL 确保不会有其他线程对数据库做更新,然后对整个库做备份。注意,在备份过程中整个库完全处于只读状态。
但是让整个库只读,听起来就很危险
1. 在主库上备份,备份期间不能更新,业务就要停摆 2. 在从库上备份,备份期间从库不能从主库中同步binlog,会造成主从延迟问题
表级锁
SQL两种表级别的锁: 表锁、元数据锁(meta-data lock)
表锁: lock tables ... read/write
MDL(metadata lock)
对表增删改查时,加MDL读锁
读锁之间不互斥,因此,可以多个线程同时对一张表增删改查
对表结构变更时,加MDL写锁
备注: MDL作用是防止DDL、DML并发的冲突,个人感觉应该写清楚,一开始理解为select和update之间的并发。
行锁
行锁就是针对数据表中行记录的锁。
这很好理解,比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。
行锁: 在事务结束(commit)后,才释放
幻读、间隙锁
幻读的现象: 隔离级别一般设为可重复读,导致在一个事务中查询行数时,可能会不一致。
间隙锁(Gap-lock)是用来解决幻读的! 新插入记录时,要更新的记录之间的”间隙“,会被加间隙锁。(间隙锁,锁的区域是两个值之间的间隙。被lock的间隙,不能被插入)
间隙锁的特点:
间隙锁,可以被重复加。即: 事务A对某个间隙加了锁,事务B同样能对间隙加锁成功(原则是保护间隙,不允许插入值。但是,加间隙锁的操作是不会冲突的)
对某个间隙加锁了,依然能对该间隙内的数据执行查询操作
资源循环依赖
当出现死锁以后,有两种策略解决:
锁超时时间(innodb_lock_wait_timeout)
锁超时时间设置过小: 误伤不是死锁的事务
如果该事务不是死锁,超过1s,自动释放锁,会造成误伤
锁超时时间设置过大:
当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。
对于在线服务来说,这个等待时间往往是无法接受的。
发现死锁后,主动回滚锁链条中的一个事务,让其他事务得以继续执行(innodb_deadlock_detect设置为on)==>常用方式
但是该方式会带来大量的CPU资源消耗
你可以想象一下这个过程: 每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,你就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。
(1) 慢查询配置: slow_query_log、slow_query_log_file、long_query_time
(2) 慢查询日志分析工具mysqldumpslow: 捕获前10条查询较慢的 mysqldumpslow -s at -t 5 xxx.log
(1)使用join来代替子查询
(2)拆分大的delete或insert语句
(3)可通过开启慢查询日志来找出较慢的SQL
(4)OR改写成IN: OR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内
explain查看执行计划:type、key、extra
① type:访问类型
all:full table scan,全表扫描
index:full index scan,只遍历索引树
range:索引范围扫描。对索引的扫描开始于某一个点,返回匹配值域的行,常见于between、>、<等查询
ref:非唯一索引扫描,返回匹配某个单独值的所有行
eq_ref:唯一性索引扫描,对于每一个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描
const,system:当mysql对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量
NULL:MySQL在优化过程中分解语句,执行时甚至不用访问表或索引
② possible_keys:可能使用到的索引
③ key:实际使用的索引,若没有使用到索引,显示为NULL
④ key_len:表示索引中使用的字节数,可以通过该列计算查询中使用的索引的长度
⑤ ref:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
⑥rows:估算查询到所需记录需要读取的行数
⑦extra:包含不适合在其他列中显示,但是十分重要的额外信息
using index:使用到覆盖索引
using where:
表示mysql服务器在存储引擎受到记录后进行“后过滤”
如果查询未能使用索引,using where作用是提醒我们mysql将使用where子句来过滤结果集
using temporary:表示mysql需要使用临时表来存储结果集,常见于order by和group by
using filesort:mysql无法使用索引完成的排序操作称为“文件排序”
插入前,禁用索引
修改事务的提交方式(变多次提交为一次提交)
插入前,禁用索引 insert into test values(1,2); insert into test values(1,3); insert into test values(1,4); ===> insert into test values(1,2),(1,3),(1,4) // 合并多条为一条,批量插入
插入后,不禁用索引
8.1.7.1. 介绍limit随着数据量大,导致低性能的本质
limit语法支持两个参数,offset/limit,其中: offset=偏移量开始查找,limit=返回limit条元组
本质: limit 10000,10的语法,实际上是mysql查找到前10010条数据,之后丢弃前面的10000行,返回10行数据。(可以看到,查找的前10000行数据是十分消耗资源且没有必要的)
8.1.7.2. 用id优化
先找到上次分页的最大id
然后利用id上的索引来查询
类似于: select * from user where id >1000000 limit 100.
这样的效率非常快,因为主键上是有索引的;但是这样有个缺点,就是ID必须是连续的,并且查询不能有where语句,因为where语句会造成过滤数据。
淘宝等商品页,就采用该方式: 连续页面查询(上一页/下一页)
8.1.7.3. 用覆盖索引杜绝回表查询 — 子查询
select * from (select id from table_name limit 1000000,100) a join table_name b on a.id = b.id;
先查出索引id,然后根据id查询数据
自身连接
select FIRST.Cno,SECOND.Cpon form course FIRST, course SECOND where FIRST.Cpon=SECOND.Cno
左连接/右连接
from A left join B on (连接条件) #以A的行为主行, B没有的补NULL
from A right join B on (连接条件) #以B的行为主行, A没有的补NULL
内连接
from A inner join B on (连接条件) #A和B的交集
一致性 ==> 半同步复制(从库将数据拷贝到中继日志,就返回;只有一个从库)
可靠性 ==> 读写同时存在时,强制性读主库
定义: 将一台主服务器的数据,同步复制到从服务器(数据的复制是单向的,只能由主节点到从节点)
作用:
① 数据冗余: 从节点保存了和主节点一样的数据。
② 故障恢复: 主节点出现问题时,从节点可以提供服务,实现故障恢复。
③ 负载均衡: 在主从复制的基础上,配合读写分离,主节点提供写服务,从节点提供读服务
④ 高可用基石: 哨兵、集群实现高可用
8.2.1.1. 主从同步过程
① 从数据库开启IO线程,【主动拉取】binlog,保存为中继日志Relay log
② 从数据库开启SQL线程,将中继日志Relay log在从服务器上重新执行(执行完成后,主从数据库数据一致)
8.2.1.2. 主从复制中延迟问题+解决方案
延迟问题产生原因: 从服务器的两个线程执行速度不一致,可能会造成延迟问题。
① IO线程从主服务器读取日志速度很快(顺序读),而SQL线程重放SQL速度慢,这就会造成从服务器同步数据远远落后于主服务器,导致从服务器数据远远落后于主服务器,(主从数据库长期处于不一致的状态),这种现象就是延迟更新。
②(主库经常会开多个线程去写,从库只有一个线程在工作,导致从库效率 << 主库效率)。
延迟问题-解决方案: (MTS) 从服务器的数据重放过程采用多线程
MTS: 要遵循两个规则
① 同一个事务中的MDL,必须分发到同一个worker线程
② MDL同一行的多个事务,必须分发到同一个worker
核心原则: 主库只进行更新写操作,从库进行查询读操作
(1)主库: 增删改更新操作,即: 更新操作,一直在主服务器
(2)从库: 查询操作,即: 查询操作,一直在从服务器
为什么要进行分库分表?==> 数据量太大,负荷太高;提高性能,读写分离,冷热分离,系统解耦
读写分离
分区: 指定分区表达式,把记录拆分到不同的区域中(必须是同一个服务器,可以是不同硬盘),应用看来还是同一张表,没有变化
分库: 业务垂直切分;冷热数据
分表: 单表数据太大(属性列过多;行数过多)
拆分方式: 水平/垂直分库\分表
分库
垂直: 现在微服务基本已经实现了垂直分库,例如: 订单库、会员库、商品库
水平: 单库数据量太大,例如: 会员库过大,将其按照时间维度进行冷热拆分,一般最近3个月的是热数据,放在热数据库
一般按照冷热数据分库
分表
垂直: 一个表的属性列过多,拆成多个表
水平: 单表数据量过大水平拆分成多个表
涉及区域
等可枚举字段查询的可进行分区
涉及时间
的可按照月年的时间分表
实现方式 无论是client模式,还是proxy模式,核心实现步骤都一样: sql解析,重写,路由,执行,结果合并
client模式: 处于业务层和JDBC层中间,是以Jar包方式提供给应用调用,对代码有侵入星
client模式代表作有阿里的TDDL: 2012年关闭了维护通道,不建议使用
开源社区的sharding-jdbc: 仍在活跃使用
Golang的client,封装访问主/从的函数
proxy模式: 部署一台代理服务器伪装成Mysql服务器,代理服务器负责与真实Mysql服务器节点连接,应用层程序只和代理服务器对接,对应用层程序是透明的
阿里的cobar
民间组织的mycat
引发的问题
分库: 本地事务 ==> 分布式事务(引出新知识点: 分布式事务解决方案)
跨库join查询 ==> 对于多库join性能太低,不建议使用sql自带的join,解决方案为
全局表: 稳定的共用表,在各个数据库都存储一份
字段冗余: 一些常用的共用字段,在各个表中都存储一份
代码组装: 查询两次,代码业务逻辑对结果进行聚合
分布式全局唯一ID: 雪花算法
分库分表线上平滑扩容方案 数据库秒级平滑扩容架构方案 - 知乎
停服迁移
双写迁移(修改配置)
详谈分库分表
什么是好的分库分表方案?
方案可持续性
业务数据量级和业务流量未来进一步升高达到新的量级的时候,我们的分库分表方案可以持续使用
一个通俗的案例,假定当前我们分库分表的方案为10库100表,那么未来某个时间点,若10个库仍然无法应对用户的流量压力,或者10个库的磁盘使用即将达到物理上限时,我们的方案能够进行平滑扩容
数据倾斜问题
一般定义分库分表最大数据偏斜率为 :(数据量最大样本 - 数据量最小样本)/ 数据量最小样本。一般来说,如果我们的最大数据偏斜率在5%以内是可以接受的
数据应该是需要比较均匀的分散在各个库表中的
避免以下问题
某个数据库实例中,部分表的数据很多,而其他表中的数据却寥寥无几,业务上的表现经常是延迟忽高忽低,飘忽不定
数据库集群中,部分集群的磁盘使用增长特别块,而部分集群的磁盘增长却很缓慢。每个库的增长步调不一致,这种情况会给后续的扩容带来步调不一致,无法统一操作的问题
常见分库分表方案
Range
实现原理:根据数据范围划分数据的存放位置
举例:将订单表按照年限为单位,每年的数据存放在单独的库/表
缺点
数据热点问题:例如上面案例中的订单表,很明显当前年度所在的库表(相比于往年)属于热点数据,需要承载大部分的IO和计算资源
新库和新表的追加问题:一般线上运行的应用程序,没有访问新库新表的权限,因此需要提前将新库新表创建好,防止线上事故(这一点非常容易被遗忘)
业务上的交叉范围内数据的处理。举例,订单模块无法避免一些中间状态的数据补偿逻辑,即需要定时任务到订单表中扫描那些T+1处于待支付确认等状态的订单(这里就需要注意了,因为通过年份进行分库分表,那么元旦的那一天,你的定时任务很有可能会漏掉上一年最后一天的数据扫描)
HASH分库分表(最普遍最大众的解决方案)
基因法
防止每次请求都发到数据库上,使用缓存,降低连接数据库操作、数据库处理操作次数,提高数据库性能
定义: 多个SQL语句的集合,就像是函数,但是它没返回值
优点: 一次连接,执行存储过程中所有的SQL语句,效率高
只在创建时编译一次,之后不编译;可以重复使用,提高开发效率
安全性高: 可以设定某个用户是否具有某个存储过程的使用权限
create procedure insert_student_process(name varchar(50),age int,out_id ing) //创建存储过程 begin: insert into student value(null,name,age) select max(stuId) into id from studentend; call insert_student_process('Jamed',26,\@id); //调用存储过程select \@id;
触发器: 需要有触发条件,当条件满足以后做什么操作
例如1: 校内网,开心网,facebook,你发一个日志,自动通知好友,其实就是增加日志时做的一个后触发,再向"通知表"写入条目。==> 触发器的效率高
select * | select 全部字段 | |
---|---|---|
是否需要解析数据字典 | 是 | 否 |
结果输出顺序 | 与建表列顺序相同 | 按指定字段顺序 |
表字段改名 | 无需修改 | 需要修改 |
可读性 | 低 | 高 |
是否可以建立索引优化 | 否 | 是 |
(1) 定长/变长: 是否由实际存储内容决定
char是定长字段,假如申请了char(10)的空间,那么无论实际存储多少内容,该字段都占用10 个字符
varchar是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间.
举例:
char,变长的,假设定义char(10),如果存了“ABC”,那实际占用的空间就是10
varchar,变长的,假设定义varchar(10),如果存了“ABC”,那实际占用的空间就是3+1=4
(2) char查询效率更快
count(*) 包括了所有的列,相当于行数,在统计结果的时候,不会忽略字段值为NULL的列
count(1) 包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略字段值为NULL的列
count(列名) 只包括列名那一列,在统计结果的时候,会忽略字段值为值为NULL的列(这里的空不是指 空字符串“” 或者 0,而是表示null)的计数,即某个字段值为NULL时,不统计
执行效率上看
列名为主键,count(列名)会比count(1)快 且 select count(主键)的执行效率是最优的;
列名不为主键,count(1)会比count(列名)快 ;
如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*) ;
如果表只有一个字段,则 select count(*)最优。
DROP
将表所占用的空间全释放掉(会删除整个表的结构)
将删除表的结构被依赖的约束(constrain),触发器(trigger),索引(index),即: 依赖于该表的存储过程/函数将的状态会变为: invalid(无效)
TRUNCATE
一次性地从表中删除所有的数据,并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且,在删除的过程中不会激活与表有关的删除触发器。执行速度快。
删除后,(表结构及其列、约束、索引等保持不变),这个表和索引所占用的空间会恢复到初始大小
应用范围: 只能对TABLE
DELETE
执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。
DELETE语句为DML,该操作会被放到 rollback segment中,事务提交后才生效。如果有相应的tigger,执行的时候将被触发。
不会减少表或索引所占用的空间
应用范围: TABLE、VIEW
类比
掌柜记忆(内存)、记账粉板(redolog)、账本(数据文件)
掌柜要把账本更新一下,即: 内存中的数据写入到磁盘中,flush操作
脏页
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
你的SQL语句为什么变慢了?
当出现大量脏页,数据要从内存中刷入磁盘(内存==>redolog==>磁盘)
什么情况下会出现flush操作?
掌柜记忆满了(系统内存满了)
对应的就是系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。
粉板满了(redolog满了)
空闲时,掌柜闲着更新账本
店铺打样后,更新账本(即: sql关闭后)
以上4种,
① 3-4是正常情况。
② 1也是常态,内存满了,要先将脏页刷入磁盘(因为innodb的策略是尽可能的使用内存,避免频繁刷写磁盘)
③ 2不是常态,应该尽可能避免 ==> 引出,innodb刷写脏页的控制策略
需要正确的告诉innodb所在主机的IO能力==>决定需刷脏页的速度
主机IO能力(主机IOPS): fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
参数innodb_io_capacity: innodb刷脏页的速度,建议设置为主机IOPS
平时要多关注脏页比例,不要让它经常接近 75%
参数innodb_max_dirty_pages_pct: 脏页比例上线,默认值是75%
参数innodb_flush_neighbors: 控制邻居的行为
值1: 刷写自己页面的时候,邻居的页面也会刷写(减少随机IO)
值0: 只刷写自己的页面
全表扫描导致查询慢
表级别锁
等MDL锁(waiting for table metadata lock): 对表执行DDL操作,导致表被锁死
等flush
脏页过多,此时正在刷新内存中的脏页到磁盘
等行锁
sessionA: 开启了事务,在事务中更新某行,则该行被加上写锁
sessionB: 查询该行(将会被阻塞)
问题描述
最后,我给你留下一个问题吧。对于上面例子中的 InnoDB 表 T,如果你要重建索引 k,你的两个 SQL 语句可以这么写:
alter table T drop index k; alter table T add index(k);
如果你要重建主键索引,也可以这么写:
alter table T drop primary key; alter table T add primary key(id);
我的问题是,对于上面这两个重建索引的作法,说出你的理解。如果有不合适的,为什么,更好的方法是什么?
答案: 重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。这两个语句,你可以用这个语句代替 : alter table T engine=InnoDB。
为什么要重建索引?
我们文章里面有提到,索引可能因为删除,或者页分裂等原因,导致数据页有空洞
重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。
谁的性能更高呢?先说结论
读,唯一索引略高一点点,但是基本差不多
写,insert场景下,唯一索引的change buffer会失效 ==> 普通索引 >> 唯一索引
假设,执行查询的语句是 select id from T where k=5。
对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
当要被更新的数据在内存时,直接更新
当数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了
虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上
如果使用的是唯一索引,那么所有insert操作都会判断这个操作是否违反唯一性约束,即: 在插入数据时,唯一索引必须要先从磁盘读取数据,判断数据是否存在,然后再根据是否唯一性,返回结果
比如,要插入(4,400)这个记录,就要先判断现在的表中是否存在k=4记录,而这必须要将数据从磁盘读入到内存中才能判断。(如果此时都已经将数据读入到内存了,那么直接更新内存更快,就没必要使用change buffer了==>因此,在更新时,只有普通索引才会使用到change buffer)==> 因此,更新时,普通索引更快
实际例子: 有个 DBA 的同学跟我反馈说,他负责的某个业务的库内存命中率突然从 99% 降低到了 75%,整个系统处于阻塞状态,更新语句全部堵住。而探究其原因后,我发现这个业务有大量插入数据的操作,而他在前一天把其中的某个普通索引改成了唯一索引。
change buffer适用于写多读少的业务。(页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统)
反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。
结论: 索引占据大量的空间
在索引树上删除数据,是标记删除,看起来是”空洞“(标记删除的好处是为了复用)==> 要想完全释放表的数据,应该(重建表)
重建表命令: alter table A engine=InnoDB
根本原因: 没有校验【不可信的外部输入】,导致修改了原SQL语句的处理逻辑。
SQL注入时一种将恶意的SQL代码插入或添加到应用的输入参数的攻击,攻击者探测出开发编程过程中的漏洞,利用这些漏洞,巧妙的构造SQL语句,对数据库系统的内容直接进行检索或修改。
用户输入可控,代码对用户输入进行了拼接,带入SQL语句,产生SQL注入漏洞。
判断是否存在
SQL注入
报错注入: 在URL或表单中输入一个单引号
或者其他特殊符号
,页面出现错误说明此页面存在SQL注入。如果页面正常显示,说明有字符被过滤或不存在SQL注入。
登录注入攻击
免账号登录
‘or 1=1 --
‘or 1=1 #
String sql = " select * from user_table where username=' "userName+" ' and password=' "password" ' "; --当输入了上面的用户名和密码,上面的SQL语句变成: SELECT * FROM user_table WHERE username=' 'or 1 = 1 -- and password=' ' """ 分析SQL语句: username='' or 1=1 用户名等于'' or 1=1 ,那么, 这个条件一定会成功 后面加两个--,这意味着注释,它将后面的语句注释,让它们不起作用,用户轻易骗过系统,获取合法身份。 --这还是比较温柔的,如果是执行 SELECT * FROM user_table WHERE username=''; DROP DATABASE (DB Name) --' and password='' 其后果可想而知… """
联合union注入攻击
'union select 1,2#
SELECT first_name, last_name FROM users WHERE user_id = '$id'; 用户输入的字符串存在$id变量中,可以看到,上面没有任何处理用户输入的字符串的函数。因此,可以肯定这里存在SQL注入。 我们仍然可以输入'or 1#,使得SQL语句变为: SELECT first_name, last_name FROM users WHERE user_id = '' or 1#' ==> 从而查询到所有的first_name和last_name
' union select 1,2#; ' union select user(),database()#;
like语句中的注入
程序中sql语句拼装:
$sql = 'student_name like '"%'.$name.'%"';
貌似正常的sql语句
SELECT * FROM tblStudent WHERE unit_name like "%aaa%" order by create_time desc limit 0, 30 ;
倘若想要借此进行sql注入,input输入框中输入aaa %" or "1%" = "1 ,则sql语句被拼接为
SELECT * FROM tblStudent WHERE unit_name like "%aaa %" or "1%" = "1%" order by create_time desc limit 0, 30 显示所有的列.
这似乎无关痛痒,倘若input输入框换成aaa%";drop table tbl_test;# ,sql语句成为
SELECT * FROM tblStudent WHERE unit_name like "%aaa%";drop table tbl_test;#%" order by create_time desc limit 0, 30;
解决方法很简单:
$binName = bin2hex("%$name%"); $arrConds[] = " course_name like unhex('$binName')";
sql: SELECT * FROM tblStudent WHERE unit_name like hex('2520636f7572736525223b64726f70207461626c652074626c5f746573743b2325') order by create_time desc limit 0, 30;
1.永远不要信任用户的输入。对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和 双"-"进行转换等。
2.永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。
3.永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。
4.不要把机密信息直接存放,加密或者hash掉密码和敏感的信息。
5.应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装
6.sql注入的检测方法一般采取辅助软件或网站平台来检测,软件一般采用sql注入检测工具jsky,网站平台就有亿思网站安全平台检测工具。MDCSOFT SCAN等。采用MDCSOFT-IPS可以有效的防御SQL注入,XSS攻击等。
单引号 ‘
注释符号 -- # /**/
分号 ;
通配符 下划线_ 百分号%
下划线_ : 表示任意一个字符
百分号%: 表示任意多个字符
方括弧 []
如果是没有配成对的单个左方括号,查询时这个左方括号会被忽略
WHERE T2.name like (%+ [ + %)
等价于下面这个语句 WHERE T2.name like (%+ + %)
==> 这将导致查询结果中包含表中的全部记录,就像没有任何过滤条件一样。
开启预编译: 参数校验(对输入的参数进行校验)
过滤: 白名单 + 正则匹配
连接池
最小: 常驻连接数
最大(某个时间刻,允许最大的连接数): 2000
慢查询时间:500ms
dump线上数据
mysqldump -h主机 -P端口 -u用户 -p密码 --databases 数据库 --tables 表名 --single-transaction > /tmp/xx.sql
mysql -h主机 -P端口 -u用户 -p密码 --database 数据库 < /tmp/xx.sql
连接数据库
mysql -h 主机 -P 端口 -u 用户 -p密码; use 数据库
TIMESTAMP在UPDATE CURRENT_TIMESTAMP数据类型上做什么
答:只要表中的其他字段发生更改,UPDATE CURRENT_TIMESTAMP修饰符就将时间戳字段更新为当前时间
列设置为auto increment时,如果在表中达到最大值,会发生什么?
它会停止递增,任何插入操作都会返回错误
Mysql事务相关
在自动提交模式下,如果没有start transaction显式地开始一个事务,那么每个sql语句都会被当做一个事务执行提交操作:该模式下,不支持事务(因为会自动提交)
set autocommit=0,关闭自动提交(但是,autocommit参数是针对连接的,在一个连接中修改了该参数,不会对其他连接产生影响)
如果关闭了autocommit,则start transaction后,所有的sql语句都在一个事务中,直到执行了commit或者rollback,该事务才结束
特殊操作:在mysql中,存在一些特殊命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务,如:
DDL语句:create table / drop table / alter
Lock tables
记录货币使用什么字段好?DECIMAL(precision,scale)
precision:代表将被用于存储值的总的位数
scale:代表将被用于存储小数点后的位数
字符串类型可以是什么?
set、char/varchar、blob/text
enum:enum是一个字符串对象,用于指定一组预定义的值
NULL是什么意思?
NULL表示UNKNOWN(未知),它不表示“空字符串”
不要把任何值与NULL进行比较:NULL与任何一个值比较,结果都是NULL
使用IS NULL来进行NULL判断
唯一索引比普通索引快么?
查询:唯一索引更快
唯一索引查询到一个,就返回了
普通索引查询到一个,还会继续查询
更新:唯一索引慢
普通索引,将记录放到change buffer中语句就执行完毕了
唯一索引,必须要唯一性校验,因此必须将数据页读入内存确实没有冲突,然后才能继续操作
limit优化
select * from table where id > #{ID} limit #{LIMIT}
订单表数据量越来越大导致查询缓慢,如何处理?
分库分表:由于历史订单使用率并不高,高频的可能只是近期订单。因此,将订单表按照时间进行拆分,根据数据量的大小考虑按照月份分表或者按照年份分表
订单ID最好包含时间(根据雪花算法生成),此时,既能根据订单ID直接获取到订单记录,也能按照时间进行查询
ACID特性
前言
按照严格的标准,只有同时满足ACID特性的才是事务,但是在各大数据库厂商的实现中,真正满足ACID的事务少之又少,例如:
mysql的NDB cluster事务不满足持久性和隔离性
mysql的innodb的默认隔离级别是可重复读,不满足隔离性
oracle默认的事务隔离级别是read commit,不满足隔离性
因此,与其说ACID是事务必须满足的条件,不如说它是衡量事务的4个维度
依次介绍ACID
原子性
定义:一个事务是不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中的一个sql语句执行失败,则已经执行的语句也必须回滚,数据库退回到事务之前的状态
实现原理:undo-log
当事务执行更新操作时,innodb会生成对应的undo-log(undo-log属于逻辑日志,它记录的是sql执行相关的信息)
如果事务执行失败或者调用了rollback,会发生回滚,innodb会根据undo-log的内容做与之前相反的操作
持久性
定义:持久性指事务一旦提交,它会对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响
实现原理:redo-log
一致性
隔离性
定义:事务之间是隔离的,并发执行的各个事务之间不能互相干扰
实现原理:隔离性的探讨,可以分为2个方面
(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性
先讨论下写操作对读操作的影响
脏读:读取到未提交的数据(脏数据)
不可重复读:在一个事务中,先后两次读取的数据结果不一样
幻读:在一个事务中,先后两次读取的数据条数不一样
事务的隔离级别
读未提交:存在脏读、不可重复读、幻读
读已提交:存在不可重复读、幻读
可重复读RR:存在幻读。innodb的隔离级别是RR,需要注意的是,在sql标准中,RR是无法避免幻读的,但是针对快照读,innodb采用MVCC实现的RR避免了幻读问题(但是并没有完全解决幻读问题,存在特例)
针对当前读,是通过next-key lock(记录锁+间隙锁)的方式解决了幻读,因为当执行select...for update语句的时候,会加上next-key lock,如果有其他事务在next-key lock锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好的避免了幻读问题
串行化:都不存在
MVCC多版本并发控制
分析三种并发场景:读读、写写、读写
读读:不会存在任何问题,无需并发控制
写写:有线程安全问题,可能会存在更新丢失问题
读写:有线程安全问题,可能会造成事务隔离性问题,可能会遇到脏读、不可重复读、幻读,需要MVCC控制
MVCC解决读写冲突(无锁并发控制)
RC读已提交、RR可重复读,基于MVCC进行并发事务控制
MVCC是基于“数据版本”对并发事务进行访问
当前读、快照读
在学习MVCC之前,必须先了解一下,什么是mysql innodb下的当前读、快照读
当前读:就是它读取的是记录的最新版本,会对读取的记录进行加锁(以下操作都是当前读)。当前读实际上是一种加锁的操作,是悲观锁的实现。
select lock in share mode 共享锁
select for update 排他锁
update\insert\delete 增删改
快照读:像不加锁的select操作就是快照读,即不加锁的非阻塞读
快照读的前提是隔离级别不是串行化
串行化级别的快照读会退化成当前读
快照读的实现是基于MVCC
当前读、快照读、MVCC之间的关系
MVCC(维持一个数据的多个版本,是的读写操作没有冲突),它只是一个抽象概念,而非实现
快照读是mysql实现MVCC的手段
特点:
在同一个时刻,不同的事务读取到的数据可能是不同的(即多版本):既然是基于多版本,快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
可以认为MVCC是行锁的一个变种,为了实现读写冲突不加锁,而这个读就是快照读,而非当前读
实现原理
在数据行的后面增加了3个隐藏字段
DB_TRX_ID:记录最近更新该行的事务ID
DB_ROLL_PTR:回滚指针,用于配合undo-log,指向上一个旧版本
DB_ROW_ID:行标识(隐藏单调自增ID),如果表没有主键,innodb会自动生成一个隐藏主键
ReadView
作用:ReadView是“快照读”SQL执行MVCC提取数据的依据(快照读就是最普通的select查询语句)
用于判断可见性的
ReadView是一个数据结构,包含4个字段
m_ids:当前活跃的事务编号集合(活跃=未被提交的事务)
min_trx_id:最小活跃事务编号
max_trx_id:预分配事务编号,当前最大事务编号+1
creator_trx_id:ReadView创建者的事务编号
示例:
先看版本链的生成规则
上面4个操作,(事务D是查询操作),事务ABC是更新操作,因此,更新操作会记录undo-log版本链(DB_ROLL_PTR会链接成一个版本链),事务ID(TRX_ID)会自增
case1 读已提交(RC):在每一次执行快照读时,都生成ReadView
分析:
①在事务D中第1次执行select快照读时,会生成一个ReadView,生成过程见下:
1. 事务ABCD都执行了begin开启了事务
2. 事务A已经执行了commit,此时事务A已经不是活跃的事务;事务BCD未执行commit,它们都是活跃的事务 ==> 因此,m_ids={2,3,4},min_trx_id=2,max_trx_id=事务D的trx_id+1=5,creator_trx_id=事务D的trx_id=4
生成了上面的ReadView之后,后面就是进行“数据提取”了.....
“数据提取”执行过程:将每一个undo-log版本链上的数据,带入到右边的判断规则中,①如果右侧的判断规则满足,就将undo-log中的当前版本返回 ②如果右侧的判断规则不满足,就沿着undo-log版本链,依次尝试,直到获得满足条件的结果
②在事务D中第2次执行select快照读时,也会生成一个ReadView
1. 事务ABCD都执行了begin开启了事务
2. 事务AB已经执行了commit,此时事务AB已经不是活跃的事务;事务CD未执行commit,它们都是活跃的事务 ==> 因此,m_ids={3,4},min_trx_id=3,max_trx_id=事务D的trx_id+1=5,creator_trx_id=事务D的trx_id=4
生成了上面的ReadView之后,后面就是进行“数据提取”了..... ==> “数据提取”执行过程同上
case2 可重复读(RR):仅在第一次执行快照读时生成ReadView,后续快照读复用(有例外,后面会说)
还是上面的过程,两次生成的ReadView见下
分析,因为2次select快照读,使用的ReadView和undo-log版本链全部是一致的,因此,两次读取的结果都是一致的
总结:经过上面的分析,RR级别下可以使用MVCC解决不可重复读
case2 引出:那么,RR级别下,可以使用MVCC避免幻读么?答案:能,但是不完全能!存在特例!
因为,作为MVCC,并不是使用锁的方式完全的对事务来进行隔离,而是通过版本控制的方式变相的实现了解决幻读的功能。
连续多次select快照读,ReadView会产生复用,没有幻读问题。
特例:当两次快照读之间存在当前读,ReadView会重新生成,导致产生幻读
举例说明:
1. 原始数据,有1088一行数据
2. 按照时间顺序
2.1. 事务B,执行select快照读,select * from stu where name="张三",此时会生成ReadView1,结果为1088一条数据
2.2. 事务A,执行insert 2001新数据
2.3. 事务B,执行update set age=25,这个update操作就是一个典型的“当前读”
2.4. 事务B,执行select快照读,select * from stu where name="张三",此时又会重新生成ReadView2(不再复用之前的ReadView1),结果为1088和2001两条数据 ===> 在同一个事务B中,执行了两次相同的select查询操作,读取到的数据条数不一样,这样就会发生“幻读”