Mysql相关
Mysql并发控制-锁
共享锁
共享锁也称为读锁,读锁允许多个连接可以同一时刻并发的读取同一资源,互不干扰。
排他锁
排他锁也称为写锁,一个写锁会阻塞其他的写锁或读锁,保证同一时刻只有一个连接可以写入数据,同时防止其他用户对这个数据的读写。
锁策略
table lock(表锁)
- 表锁是mysql最基本的锁策略,也是开销最小的锁,加锁快,会锁定整个表。
- 通常发生在DDL语句和不走索引的DML语句。
update table set columnA = "a" where columnB = "b";
如果上述sql语句中columnB字段不存在索引或者不是组合索引前缀,会进行锁表操作。
如果columnB字段创建索引后,那么会锁住满足where条件的行(行锁)。
row lock (行锁)
行锁可以最大限度的支持并发处理,当然也带来了最大开销,加锁慢,行锁的粒度在每一行数据。
- InnoDB行锁是通过给索引上的索引项加锁来实现的:只有通过索引条件检索数据,InnoDB才使用行级锁,否则将使用表锁。
- 不论是主键索引、唯一索引或者普通索引,InnoDB都会使用行锁来对数据加锁。
- 只有执行计划真正使用了索引,才能使用行锁,可以通过explain来检查SQL的执行计划。
- 如果多个session访问不同行的记录,使用了相同的索引键,是会出现索引冲突的。
InnoDB 间隙锁
当我们用范围条件而不是相等检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但是并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个间隙加锁,这种锁机制就是间隙锁(Next-Key锁)。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值得并发插入,造成严重的锁等等。所以应尽量使用相等条件访问更新数据。
间隙锁的目的:
- 防止幻读,以满足相关隔离级别的要求;
- 满足恢复和复制的需要。
显式锁 与 隐式锁
隐式锁:我们上文说的锁都属于不需要额外语句加锁的隐式锁。
显示锁:
SELECT ... LOCK IN SHARE MODE(加共享锁);
SELECT ... FOR UPDATE(加排他锁);
Mysql并发控制-事务
事务的特性ACID
- A: atomiciy 原子性
一个事务必须保证其中的操作要么全部执行,要么全部回滚。 - C: consistency 一致性
数据必须保证从一种一致性的状态转换为另一种一致性状态。 - I:isolation 隔离性
当一个事务未执行完成时,通常会保证其他session无法看到这个事务的执行结果。 - D:druability 持久性
事务一旦commit,则数据会保存下来,即使提交后系统崩溃,数据也不会丢失。
事务的隔离级别(依靠锁来实现)
- READ UNCOMMITTED(读未提交,可脏读):依靠不加锁实现,并发性能最好。事务中的修改,即使没有提交,其他会话也能读到-出现脏读。
- READ COMMITTED(读已提交,可幻读):采用MVCC实现读已提交,读取快照数据,每次执行语句的时候都要重新创建一次快照。保证了一个事务如果没有commit,事务中的操作对其他会话是不可见的。解决了脏读的问题,但是会对其他session产生两次不一致的读取结果-出现幻读。
- REPEATABLE READ (可重复读):采用MVCC实现可重复读,读取的是数据快照,仅在事务开始时创建一次快照。一个事务中多次执行同一读sql,返回结果是一样的,解决了幻读和脏读问题。InnoDB使用间隙锁对当前读进行加锁,锁住行以及可能产生幻读得插入位置,阻止新的数据插入产生幻读行。
- SERIALIZABLE(可串行化):读的时候加共享锁支持并发读,写的时候加排他锁,属于串行执行。最强的隔离级别,保证不产生幻读问题,但是会导致大量超时以及锁竞争问题。
Mysql多版本并发控制(MVCC)
MVCC是个行级锁变种,他在普通读情况下避免了加锁操作,因此开销更低。实现了非阻塞读,对于写操作只锁定必要的行。
一致性读(就是读取快照)
select * from table .....;-
当前读(就是读取实际的持久化的数据)
特殊的读操作,插入/更新/删除操作都属于当前读,处理的都是当前的数据,需要加锁。select * from table where ? lock in share mode; select * from table where ? for update; insert; update ; delete;
InnoDB通过在每行数据后边保存两个隐藏的列来实现(其实有三列,第三列用于事务回滚)
第一列保存了行的创建版本号,第二列保存了行的更新版本号。这个版本号是每个事务的版本号,递增的。
select无锁操作与维护版本号
Select (快照读,就是读取当前事务之前的数据)
1、InnoDB只select查找版本号早已当前版本号的数据行,这样保证了读取的数据要么是在这个事务开始之前commit的,要么是这个事务自身中执行创建操作的数据(等于当前版本号)
2、查找行的更新版本号要么未定义,要么大于当前版本号,保证了事务读取到在当前事务开始之后的未被更新的数据。Insert
InnoDB为这个事务中新插入的行,保存当前事务版本号的行作为行的行创建版本号。Delete
InnoDB为每一个删除的行保存当前事务版本号,作为行的删除标记。-
Update
将存在两条数据,保持当前版本号作为更新后的数据的新增版本号,同时保存当前版本号作为老数据行的更新版本号。Mysql - 死锁
死锁,就是产生了循环等待链条,我等待你释放锁,你却等待我释放锁,我们都相互等待,谁也不释放自己的锁,导致无限等待下去。比如:
//Session A
START TRANSACTION;
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";
COMMIT;
//Thread B
START TRANSACTION;
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";
COMMIT;
innodb_lock_wait_timeout 等待锁超时回滚事务:
直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另一个事务就能继续执行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout用来设置超时时间。
wait-for graph算法来主动进行死锁检测:
innodb还提供了wait-for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要并进入等待时,wait-for graph算法都会被触发。
如何避免死锁
- 以固定的顺序访问表和行。比如两个事务更新数据,事务A更新数据顺序为1、2,但是事务B更新数据顺序为2、1。这样可能会造成死锁。
- 大事务拆分成小事务,大事务发生死锁的概率更大一些。
- 在同一个事务中,尽可能一次锁定所需要的所有资源,减少死锁概率。
- 降低隔离级别,如果业务运行的情况下,比如将隔离级别从RR调整为RC,可以避免因为gap锁造成的死锁。
- 为表添加合理的索引,如果不走索引,将会为表的每一行记录添加锁,死锁的概率增大。
Mysql - 索引原理
索引目的
索引的目的在于提高查询效率,可以类比一本字典,如果要查询某个单词,可以先根据单词首字母查询字典目录,然后找到对应的页码,直接翻到指定页码在进行二分查找,提高查询效率。
磁盘IO与预读
磁盘IO是非常高昂的操作,计算机系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也会读取到内存缓冲区,这样可以方便计算机很快访问相邻的数据,这就是磁盘预读。每一次IO读取的数据称之为一页(page),page大小和操作系统有关,一般为4k或8k。
索引数据结构 B+Tree
为了每次查询数据的时候把磁盘IO次数控制在一个很小的数量级,我们需要一个高度可控的多路搜索树。这就是B+Tree。
浅蓝色为一个磁盘块,每个磁盘块包含几个数据项(深蓝色)和指针(黄色),如磁盘块1包含数据项17和35,包含指针P1、P2、P3表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块,真实的数据只存在于叶子节点。非叶子节点只存储指引搜索方向的数据项,而不存真实的数据。如17、35并不真实存在数据表中。
B+Tree的查找过程
比如要查找数据项29,需要先把磁盘块1加载到内存,发生一次IO,在内存中二分查找29在17和35之间,找到磁盘块1的P2指针;然后在通过P2指针在加载磁盘块3到内存,发生第二次IO,内存中二分查找29在26和30之间,锁定磁盘块3的P2指针;通过指针加载磁盘块8到内存,发生第三次IO,同时二分查找到29,结束查询,总计发生三次IO。
B+Tree性质
- 一次查询发生IO的次数取决于B+Tree的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=log(m+1)N,树的高度就是log以m+1为底N的对数,如果m等于1时,也是一个二叉树!当数据量N一定的情况下,m越大则h越小;比如m=1、N=8则h=log(2)8=3;而m=磁盘块大小/数据项的大小;磁盘块大小就是数据页大小,一般为4K或8K,是固定的。所以数据项占空间越小,每个磁盘块存储的数据项就越多,树的高度就越低,查询发生的磁盘ID次数就越少。所以索引字段尽量要小,int占4字节,要比bigint8字节少一半。所以真实的数据要放到叶子节点,如果放在中间节点,会导致树增高。
- 最左匹配特性,B+Tree是按照从左到右的顺序建立搜索树的,
联合索引数据结构
一个SQL只能利用到联合索引中的其中一列进行范围查找,因为B+Tree的每个叶子节点有一个指针指向下一个节点,把某一索引列的所有叶子节点串在了一起,只能根据单列的叶子节点进行范围查询。
Mysql - 索引原则
- 最左前缀匹配原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如 a=1 and b=2 and c>3 and d=4 如果建立了(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a、b、d的顺序可以任意调整。
- =和in可以乱序,比如 a=1 and b=2 and c=3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮助优化成索引可以识别的形式。
- 尽量选择区分度高的列作为索引,区分度公式count(distinct col)/count(*),表示字段不重复的比例。
- 索引列不能参与计算,否则索引会失效。
- 尽量扩展索引,不要新建索引。
Mysql - 索引类型
索引主要分为两大类,聚簇索引和非聚簇索引
聚簇索引 (clustered index)
聚簇索引的叶子节点存储行记录,InnoDB必须要有且只有一个聚簇索引:
- 如果表定义了主键,则主键索引就是聚簇索引;
- 如果没有定义主键,则第一个非空的唯一索引列是聚簇索引;
- 如果没有唯一索引,则创建一个隐藏的row-id列作为聚簇索引。主键索引查询非常快,可以直接定位行记录。
非聚簇索引 (secondary index)
InnoDB非聚簇索引的叶子节点存储的是行记录的主键值,而MyISAM叶子节点存储的是行指针。
通常情况下,需要先遍历非聚簇索引获得聚簇索引的主键ID,然后在遍历聚簇索引获取对应行记录。
非聚簇索引需要扫描两遍索引树:又叫回表查询
1、先扫描非聚簇索引根据where条件定位到主键值id=9;
2、再根据主键ID扫描聚簇索引定位到行记录。
回表查询
回表查询就是先定位主键值,在根据主键值定位行记录,需要扫描两遍索引。
解决方案:
只需要在一颗索引树上能够获取SQL所需要的所有列数据,则无需回表查询,速度更快。
常见方法:
将要查询的字段,建立到联合索引里去,这就是索引覆盖。
查询sql在进行explain解析时,Extra字段为Using Index时,则触发索引覆盖。
没有触发索引覆盖,发生了回表查询时,Extra字段为Using Index condition.
索引覆盖进行回表查询优化场景
select id, name, sex from table where name = 'tom'; #单列索引(name)联合索引(name,sex),可避免回表
select id, name, sex from table order by name limit 5 ,10 # 单列索引(name)联合索引(name,sex),可避免回表
索引条件下推 (Index Condition Pushdown)
mysql5.6引入了索引下推优化,默认开启,使用 SET optimizer_switch = 'index_condition_pushdown=off'; 可以关闭。
索引下推示例
数据库表中创建了联合索引 (a,b,c)
select * from table where a = 'a' and b like '%b%' and c like '%c%';
没有索引下推技术:
由于b和c使用了like,将导致索引匹配失效,所以只有a字段使用了索引,在索引中通过字段a查询到所有的数据,然后返回服务端,然后在服务端基于模糊匹配条件进行数据过滤。
使用索引下推技术:
可以在索引中首先通过字段a进行查询,然后在根据索引上的b和c判断是否符合模糊匹配条件,如果符合条件则根据该索引来定位对应数据,不符合则直接拒绝,有了索引下推技术,可以在有like条件的情况下减少回表次数,提高查询性能。
主从复制过程
- 主库把数据更改记录到二进制日志(Binary Log)中;
- 从库通过IO线程将主库上的二进制日志复制到自己的中继日志中(Relay Log);
-
从库通过sql线程读取中继日志中的事件,将其执行到从库之上。
主从复制模式
异步复制模式 (mysql async-mode) -默认模式
Mysql增删改查操作全部记录在binLog中,当salve节点连接master节点时,会主动从master节点处获取最新的binLog,并把binLog中的sql进行重新执行。
缺点:会造成主从复制延迟
半同步模式 (mysql semi-sync)
master需要接受到其中一台salve的确认信息,才会commit然后返回给用户;否则要等直到超时时间然后切换成异步模式再提交。可以使得主从复制延迟缩小,提高数据安全性,但是影响性能,响应时间变长。可以确保了事务提交后,binlog至少传输到一个slave,但不保证slave将此事务更新到db。
全同步模式
全同步模式是指master和salve节点全部执行了commit并确认才会向客户端返回成功。
Mysql - 主从复制方式
mysql复制主要有三种方式:基于SQL语句的复制(statement-based replication,SBR),基于行的复制(row-based replication,RBR),混合模式复制(mixed-based replication,MBR)。分别对应了binlog的三种格式:
STATEMENT,ROW,MIXED。
STATEMENT方式(SBR)
每一条会修改数据的SQL语句都会记录到binlog中。
优点:并不需要记录每一条sql语句和每一行的数据变化,减少了binlog的日志量,节约IO,提高性能。
缺点:在某些情况下会导致主从数据不一致(如sleep,last_insert_id,now函数会出现问题)。ROW方式 (RBR)
不记录每条SQL语句的上下文信息,仅需要记录哪条数据被修改,修改成什么样。不会出现函数调用问题,
缺点:会产生大量的日志,尤其是alter table的时候会让日志暴涨。-
MIXED方式 (MBR)
以上两种模式的混合,一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog,Mysql会根据执行的SQL语句选择日志保存方式。Mysql - GTID原理
GTID是MySQL 5.6的新特性,其全称是Global Transaction Identifier,可简化MySQL的主从切换以及Failover。GTID用于在binlog中唯一标识一个事务。当事务提交时,MySQL Server在写binlog的时候,会先写一个特殊的Binlog Event,类型为GTID_Event,指定下一个事务的GTID,然后再写事务的Binlog。主从同步时GTID_Event和事务的Binlog都会传递到从库,从库在执行的时候也是用同样的GTID写binlog,这样主从同步以后,就可通过GTID确定从库同步到的位置了。也就是说,无论是级联情况,还是一主多从情况,都可以通过GTID自动找点儿,而无需像之前那样通过File_name和File_position找点儿了。
在传统的复制里面,当发生故障,需要主从切换,需要找到binlog和pos点,然后将主节点指向新的主节点,相对来说比较麻烦,也容易出错。在MySQL 5.6里面,不用再找binlog和pos点,我们只需要知道主节点的ip,端口,以及账号密码就行,因为复制是自动的,MySQL会通过内部机制GTID自动找点同步。
多线程复制(基于库),在MySQL 5.6以前的版本,slave的复制是单线程的。一个事件一个事件的读取应用。而master是并发写入的,所以延时是避免不了的。唯一有效的方法是把多个库放在多台slave,这样又有点浪费服务器。在MySQL 5.6里面,我们可以把多个表放在多个库,这样就可以使用多线程复制。
基于GTID复制实现的工作原理
- 主节点更新数据时,会在事务前产生GTID,一起记录到binlog日志中。
- 从节点的I/O线程将变更的bin log,写入到本地的relay log中。
- SQL线程从relay log中获取GTID,然后对比本地binlog是否有记录(所以MySQL从节点必须要开启binary log)。
- 如果有记录,说明该GTID的事务已经执行,从节点会忽略。
- 如果没有记录,从节点就会从relay log中执行该GTID的事务,并记录到bin log。
- 在解析过程中会判断是否有主键,如果没有就用二级索引,如果有就用全部扫描。
Mysql - Explain
explain + 查询sql可以生成一个执行计划,可以通过执行计划来进行sql优化。
mysql> explain select * from table;
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | servers | ALL | NULL | NULL | NULL | NULL | 1 | NULL |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
row in set (0.03 sec)
id
表示sql执行顺序标识,sql从大到小按顺序执行。
select_type
- SIMPLE:简单select,不使用union或子查询等;
- PRIMARY:查询中若包含任何复杂的子部分,最外层select表标记为PRIMARY;
- UNION:UNION中的第二个或后面的SELECT语句;
- DEPENDENT UNION:UNION中第二个或后面的select语句,取决于外面的查询;
- UNION RESULT:union的结果;
- SUBQUERY:子查询中第一个select;
- DEPENDEND SUBQUERY:子查询中第一个select,取决于外面的查询;
- DERVED:派生表的select,from子句的子查询;
- UNCACHEABLE SUBQUERY:一个子查询的结果不能被缓存,必须重新评估外连接的第一行。
table
显示这一行的数据是关于哪张表的,有时不是真实的表名,看到的是derived+数字
type
访问类型:ALL index range ref eq_ref const system NULL 从左到右边,性能越好。
- ALL:Full table scan,遍历全表找到匹配的行;
- index:Full index scan,遍历索引树找到匹配的行;
- range:只检索给定范围的行,使用一个索引来选择行;
- ref:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值;
- eq_ref:类似ref,区别是使用的索引是唯一索引,对于每个索引键值,表示只有一条记录匹配,就是多表连接中使用主键或者唯一索引键作为关联条件;
- const、system:当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system;
- NULL:MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。
该列完全独立于EXPLAIN输出所示的表的次序。这意味着在possible_keys中的某些键实际上不能按生成的表次序使用。
如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查WHERE子句看是否它引用某些列或适合索引的列来提高你的查询性能。如果是这样,创造一个适当的索引并且再次用EXPLAIN检查查询
Key
key列显示MySQL实际决定使用的键(索引)
如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)不损失精确性的情况下,长度越短越好
ref
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
rows
表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数
Extra
该列包含MySQL解决查询的详细信息,有以下几种情况:
- Using where:列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤
- Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询
- Using filesort:MySQL中无法利用索引完成的排序操作称为“文件排序”
- Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
- Impossible where:这个值强调了where语句会导致没有符合条件的行。
- Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
- Using index:表示相应的select 操作中使用了覆盖索引(Covering index),避免访问了表的数据行,效果不错!如果同时出现Using where,表明索引被用来执行索引键值的查找。如果没有同时出现Using where,表示索引用来读取数据而非执行查找动作。
- Using index condition:在5.6版本后加入的新特性,优化器会在索引存在的情况下,通过符合RANGE范围的条数 和 总数的比例来选择是使用索引还是进行全表遍历。
- distinct:优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作。
Mysql - 日志文件
Mysql中一共有6种日志文件
- 重做日志 - redo log
- 回滚日志 - undo log
- 二进制日志 - bin log
- 中继日志 - relay log
- 错误日志 - error log
- 慢查询日志 - slow query log
重做日志 - redo log
- 用途:确保事务的持久性,防止在发生故障的时候,还有脏页未写入磁盘,在重启mysql的时候根据redolog进行重做,从而保证事务的持久性。
- 事务开始的时候就会记录redolog,当事务对应的page写入到磁盘后,redo日志占用的空间就可以释放,被覆盖重用。
- redolog也有日志缓冲区,默认每秒一次刷新到磁盘
回滚日志 - undo log
- 用途:保存事务发生之前的数据版本,主要用于事务回滚,也可以提供多版本并发控制读(MVCC)。
- 事务开始之前,将当期版本的数据生成undo log。
- 当事务提交后,undo log并不立即删除,而是放入待清理链表,由清理线程异步判断是否需要清理。
二进制日志 - bin log
- 用途:用于主从复制,从库利用主库上的binlog进行重播,实现主从同步功能,也可以实现数据基于时间点的数据还原。
- 事务提交的时候,一次性将事务中的sql语句按照一定的格式记录到binlog中。
中继日志 - relay log
- 用途:用于从服务器的sql线程读取relaylog日志内容同步主库数据。
- relaylog与binlog类似,它是从服务器IO线程将主服务器的binlog读取过来记录到从服务器的本地日志文件。
错误日志 - error log
- 用途:用于记录mysql服务的错误信息和服务进程的错误信息
慢查询日志 - slow query log
- 用途:用于记录sql语句执行时长超过配置阈值的sql语句日志