Wetts's blog

Stay Hungry, Stay Foolish.

0%

方差(Variance)

方差用于衡量随机变量或一组数据的离散程度,方差在在统计描述和概率分布中有不同的定义和计算公式。

  1. 概率论中方差用来度量随机变量和其数学期望(即均值)之间的偏离程度;
  2. 统计中的方差(样本方差)是每个样本值与全体样本均值之差的平方值的平均数,代表每个变量与总体均值间的离散程度。

概率论中计算公式

离散型随机变量的数学期望:
离散型随机变量的数学期望

连续型随机变量的数学期望:
连续型随机变量的数学期望

其中,pi是变量,xi发生的概率,f(x)是概率密度。

方差:
概率论方差

统计学中计算公式

总体方差

总体方差,也叫做有偏估计,其实就是我们从初高中就学到的那个标准定义的方差:

总体均值:
总体均值

其中,n表示这组数据个数,x1、x2、x3……xn表示这组数据具体数值。

总体方差:
总体方差

样本方差

样本方差,无偏方差,在实际情况中,总体均值是很难得到的,往往通过抽样来计算,于是有样本方差,计算公式如下:
样本方差

协方差(Covariance)

协方差在概率论和统计学中用于衡量两个变量的总体误差。而方差是协方差的一种特殊情况,即当两个变量是相同的情况。协方差表示的是两个变量的总体的误差,这与只表示一个变量误差的方差不同。 如果两个变量的变化趋势一致,也就是说如果其中一个大于自身的期望值,另外一个也大于自身的期望值,那么两个变量之间的协方差就是正值。 如果两个变量的变化趋势相反,即其中一个大于自身的期望值,另外一个却小于自身的期望值,那么两个变量之间的协方差就是负值。

协方差
协方差2
协方差3

其中,E[X]与E[Y]分别为两个实数随机变量X与Y的数学期望,Cov(X,Y)为X,Y的协方差。

标准差(Standard Deviation)

标准差也被称为标准偏差,在中文环境中又常称均方差,是数据偏离均值的平方和平均后的方根,用σ表示。标准差是方差的算术平方根。标准差能反映一个数据集的离散程度,只是由于方差出现了平方项造成量纲的倍数变化,无法直观反映出偏离程度,于是出现了标准差,标准偏差越小,这些值偏离平均值就越少,反之亦然。

样本标准差

总体标准差

均方误差(mean-square error, MSE)

均方误差是反映估计量与被估计量之间差异程度的一种度量,换句话说,参数估计值与参数真值之差的平方的期望值。MSE可以评价数据的变化程度,MSE的值越小,说明预测模型描述实验数据具有更好的精确度。

均方误差

均方根误差(root mean squared error,RMSE)

均方根误差亦称标准误差,是均方误差的算术平方根。换句话说,是观测值与真值(或模拟值)偏差(而不是观测值与其平均值之间的偏差)的平方与观测次数n比值的平方根,在实际测量中,观测次数n总是有限的,真值只能用最可信赖(最佳)值来代替。标准误差对一组测量中的特大或特小误差反映非常敏感,所以,标准误差能够很好地反映出测量的精密度。这正是标准误差在工程测量中广泛被采用的原因。因此,标准差是用来衡量一组数自身的离散程度,而均方根误差是用来衡量观测值同真值之间的偏差。

均方根误差

均方根值(root-mean-square,RMES)

均方根值也称作为方均根值或有效值,在数据统计分析中,将所有值平方求和,求其均值,再开平方,就得到均方根值。在物理学中,我们常用均方根值来分析噪声。

均方根值

有算术平均数、几何平均数、平方平均数(均方根平均数,rms)、调和平均数、加权平均数等

调和平均数

又称倒数平均数,是总体各统计变量倒数的算术平均数的倒数。调和平均数是平均数的一种。

简单调和平均数

简单调和平均数是算术平均数的变形,它的计算公式如下:
调和平均数

加权调和平均数

加权调和平均数是加权算术平均数的变形。它与加权算术平均数在实质上是相同的,而仅有形式上的区别,即表现为变量对称的区别、权数对称的区别和计算位置对称的区别。因而其计算公式为:
加权调和平均数

几何平均数

是对各变量值的连乘积开项数次方根。

简单几何平均数

几何平均数与算术平均数

加权几何平均数

加权几何平均数

算术平均数

又称均值,是统计学中最基本、最常用的一种平均指标,分为简单算术平均数、加权算术平均数。它主要适用于数值型数据,不适用于品质数据。

简单算术平均数

主要用于未分组的原始数据。设一组数据为X1,X2,…,Xn,简单的算术平均数的计算公式为:
简单算术平均数

加权算术平均数

主要用于处理经分组整理的数据。设原始数据为被分成K组,各组的组中的值为X1,X2,…,Xk,各组的频数分别为f1,f2,…,fk,加权算术平均数的计算公式为:
加权算术平均数

平方平均数

又名均方根(Root Mean Square),是指一组数据的平方的平均数的算术平方根。
平方平均数

转自:https://www.ruanyifeng.com/blog/2018/07/cap.html

分布式系统(distributed system)正变得越来越重要,大型网站几乎都是分布式的。

分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。

本文介绍该定理。它其实很好懂,而且是显而易见的。下面的内容主要参考了 Michael Whittaker 的文章。

分布式系统的三个指标

1

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。

  • Consistency
  • Availability
  • Partition tolerance

它们的第一个字母分别是 C、A、P。

Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。

Partition tolerance

先看 Partition tolerance,中文叫做”分区容错”。

大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

2

上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。

一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。

Consistency

Consistency 中文叫做”一致性”。意思是,写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。

3

接下来,用户的读操作就会得到 v1。这就叫一致性。

4

问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。

5

为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。

6

这样的话,用户向 G2 发起读操作,也能得到 v1。

7

Availability

Availability 中文叫做”可用性”,意思是只要收到用户的请求,服务器就必须给出回应。

用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。

Consistency 和 Availability 的矛盾

一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。

如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性不。

如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。

综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。

[更新 2018.7.17]

读者问,在什么场合,可用性高于一致性?

举例来说,发布一张网页到 CDN,多个服务器有这张网页的副本。后来发现一个错误,需要更新网页,这时只能每个服务器都更新一遍。

一般来说,网页的更新不是特别强调一致性。短时期内,一些用户拿到老版本,另一些用户拿到新版本,问题不会特别大。当然,所有人最终都会看到新版本。所以,这个场合就是可用性高于一致性。

转自:https://www.cnblogs.com/rjzheng/p/9950951.html

引言

大家在面试中有没遇到面试官问你下面六句Sql的区别呢

1
2
3
4
5
6
select * from table where id = ?
select * from table where id < ?
select * from table where id = ? lock in share mode
select * from table where id < ? lock in share mode
select * from table where id = ? for update
select * from table where id < ? for update

如果你能清楚的说出,这六句sql在不同的事务隔离级别下,是否加锁,加的是共享锁还是排他锁,是否存在间隙锁,那这篇文章就没有看的意义了。

之所以写这篇文章是因为目前为止网上这方面的文章太片面,都只说了一半,且大多没指明隔离级别,以及where后跟的是否为索引条件列。在此,我就不一一列举那些有误的文章了,大家可以自行百度一下,大多都是讲不清楚。

OK,要回答这个问题,先问自己三个问题

  • 当前事务隔离级别是什么
  • id列是否存在索引
  • 如果存在索引是聚簇索引还是非聚簇索引呢?

OK,开始回答

正文

本文假定读者,看过我的《MySQL(Innodb)索引的原理》。如果没看过,额,你记得三句话吧

  • innodb 一定存在聚簇索引,默认以主键作为聚簇索引
  • 有几个索引,就有几棵 B+ 树(不考虑 hash 索引的情形)
  • 聚簇索引的叶子节点为磁盘上的真实数据。非聚簇索引的叶子节点还是索引,指向聚簇索引B+树。

下面啰嗦点基础知识

锁类型

  • 共享锁(S锁):假设事务T1对数据A加上共享锁,那么事务T2可以读数据A,不能修改数据A。
  • 排他锁(X锁):假设事务T1对数据A加上排他锁,那么事务T2不能读数据A,不能修改数据A。

我们通过 updatedelete 等语句加上的锁都是行级别的锁。只有 LOCK TABLE … READLOCK TABLE … WRITE 才能申请表级别的锁。

  • 意向共享锁(IS锁):一个事务在获取(任何一行/或者全表)S锁之前,一定会先在所在的表上加IS锁。
  • 意向排他锁(IX锁):一个事务在获取(任何一行/或者全表)X锁之前,一定会先在所在的表上加IX锁。

意向锁存在的目的?

OK,这里说一下意向锁存在的目的。假设事务T1,用X锁来锁住了表上的几条记录,那么此时表上存在IX锁,即意向排他锁。那么此时事务T2要进行 LOCK TABLE … WRITE 的表级别锁的请求,可以直接根据意向锁是否存在而判断是否有锁冲突。

加锁算法

我的说法是来自官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html。加上自己矫揉造作的见解得出。

ok,记得如下三种,本文就够用了

  • Record Locks:简单翻译为行锁吧。注意了,该锁是对索引记录进行加锁!锁是在加索引上而不是行上的。注意了,innodb一定存在聚簇索引,因此行锁最终都会落到聚簇索引上!
  • Gap Locks:简单翻译为间隙锁,是对索引的间隙加锁,其目的只有一个,防止其他事物插入数据。在Read Committed隔离级别下,不会使用间隙锁。这里我对官网补充一下,隔离级别比ReadCommitted低的情况下,也不会使用间隙锁,如隔离级别为Read Uncommited时,也不存在间隙锁。当隔离级别为Repeatable Read和Serializable时,就会存在间隙锁。
  • Next-Key Locks:这个理解为Record Lock+索引前面的Gap Lock。记住了,锁住的是索引前面的间隙!比如一个索引包含值,10,11,13和20。那么,间隙锁的范围如下
    1
    2
    3
    4
    5
    (negative infinity, 10]
    (10, 11]
    (11, 13]
    (13, 20]
    (20, positive infinity)

快照读和当前读

最后一点基础知识了,大家坚持看完,这些是后面分析的基础!

在 mysql 中 select 分为快照读和当前读,执行下面的语句

1
select * from table where id = ?;

执行的是快照读,读的是数据库记录的快照版本,是不加锁的。(这种说法在隔离级别为 Serializable 中不成立,后面我会补充。)

那么,执行

1
select * from table where id = ? lock in share mode;

会对读取记录加S锁(共享锁),执行

1
select * from table where id = ? for update

会对读取记录加X锁(排他锁),那么

加的是表锁还是行锁呢?

针对这点,我们先回忆一下事务的四个隔离级别,他们由弱到强如下所示:

  • Read Uncommited(RU):读未提交,一个事务可以读到另一个事务未提交的数据!
  • Read Committed (RC):读已提交,一个事务可以读到另一个事务已提交的数据!
  • Repeatable Read (RR):可重复读,加入间隙锁,一定程度上避免了幻读的产生!注意了,只是一定程度上,并没有完全避免!我会在下一篇文章说明!另外就是记住从该级别才开始加入间隙锁(这句话记下来,后面有用到)!
  • Serializable:串行化,该级别下读写串行化,且所有的select语句后都自动加上lock in share mode,即使用了共享锁。因此在该隔离级别下,使用的是当前读,而不是快照读。

那么关于是表锁还是行锁,大家可以看到网上最流传的一个说法是这样的,

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。 InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

这句话大家可以搜一下,都是你抄我的,我抄你的。那么,这句话本身有两处错误!

错误一:并不是用表锁来实现锁表的操作,而是利用了Next-Key Locks,也可以理解为是用了行锁+间隙锁来实现锁表的操作!
为了便于说明,我来个例子,假设有表数据如下,pId为主键索引

pId(int) name(varchar) num(int)
1 aaa 100
2 bbb 200
7 ccc 200

执行语句(name列无索引)

1
select * from table where name = `aaa` for update

那么此时在pId=1,2,7这三条记录上存在行锁(把行锁住了)。另外,在(-∞,1)(1,2)(2,7)(7,+∞)上存在间隙锁(把间隙锁住了)。因此,给人一种整个表锁住的错觉!

ps:对该结论有疑问的,可自行执行show engine innodb status;语句进行分析。

错误二:所有文章都不提隔离级别!

注意我上面说的,之所以能够锁表,是通过行锁+间隙锁来实现的。那么,RU和RC都不存在间隙锁,这种说法在RU和RC中还能成立么?

因此,该说法只在RR和Serializable中是成立的。如果隔离级别为RU和RC,无论条件列上是否有索引,都不会锁表,只锁行!

分析

下面来对开始的问题作出解答,假设有表如下,pId为主键索引

pId(int) name(varchar) num(int)
1 aaa 100
2 bbb 200
3 bbb 300
7 ccc 200

RC/RU+条件列非索引

  1. select * from table where num = 200
    • 不加任何锁,是快照读。
  2. select * from table where num > 200
    • 不加任何锁,是快照读。
  3. select * from table where num = 200 lock in share mode
    • 当num = 200,有两条记录。这两条记录对应的pId=2,7,因此在pId=2,7的聚簇索引上加行级S锁,采用当前读。
  4. select * from table where num > 200 lock in share mode
    • 当num > 200,有一条记录。这条记录对应的pId=3,因此在pId=3的聚簇索引上加上行级S锁,采用当前读。
  5. select * from table where num = 200 for update
    • 当num = 200,有两条记录。这两条记录对应的pId=2,7,因此在pId=2,7的聚簇索引上加行级X锁,采用当前读。
  6. select * from table where num > 200 for update
    • 当num > 200,有一条记录。这条记录对应的pId=3,因此在pId=3的聚簇索引上加上行级X锁,采用当前读。

RC/RU+条件列是聚簇索引

恩,大家应该知道pId是主键列,因此pId用的就是聚簇索引。此情况其实和RC/RU+条件列非索引情况是类似的。

  1. select * from table where pId = 2
    • 不加任何锁,是快照读。
  2. select * from table where pId > 2
    • 不加任何锁,是快照读。
  3. select * from table where pId = 2 lock in share mode
    • 在pId=2的聚簇索引上,加S锁,为当前读。
  4. select * from table where pId > 2 lock in share mode
    • 在pId=3,7的聚簇索引上,加S锁,为当前读。
  5. select * from table where pId = 2 for update
    • 在pId=2的聚簇索引上,加X锁,为当前读。
  6. select * from table where pId > 2 for update
    • 在pId=3,7的聚簇索引上,加X锁,为当前读。

这里,大家可能有疑问

为什么条件列加不加索引,加锁情况是一样的?

ok,其实是不一样的。在RC/RU隔离级别中,MySQL Server做了优化。在条件列没有索引的情况下,尽管通过聚簇索引来扫描全表,进行全表加锁。但是,MySQL Server层会进行过滤并把不符合条件的锁当即释放掉,因此你看起来最终结果是一样的。但是RC/RU+条件列非索引比本例多了一个释放不符合条件的锁的过程

RC/RU+条件列是非聚簇索引

我们在num列上建上非唯一索引。此时有一棵聚簇索引(主键索引,pId)形成的B+索引树,其叶子节点为硬盘上的真实数据。以及另一棵非聚簇索引(非唯一索引,num)形成的B+索引树,其叶子节点依然为索引节点,保存了num列的字段值,和对应的聚簇索引。

接下来分析开始

  1. select * from table where num = 200
    • 不加任何锁,是快照读。
  2. select * from table where num > 200
    • 不加任何锁,是快照读。
  3. select * from table where num = 200 lock in share mode
    • 当num = 200,由于num列上有索引,因此先在 num = 200的两条索引记录上加行级S锁。接着,去聚簇索引树上查询,这两条记录对应的pId=2,7,因此在pId=2,7的聚簇索引上加行级S锁,采用当前读。
  4. select * from table where num > 200 lock in share mode
    • 当num > 200,由于num列上有索引,因此先在符合条件的 num = 300的一条索引记录上加行级S锁。接着,去聚簇索引树上查询,这条记录对应的pId=3,因此在pId=3的聚簇索引上加行级S锁,采用当前读。
  5. select * from table where num = 200 for update
    • 当num = 200,由于num列上有索引,因此先在 num = 200的两条索引记录上加行级X锁。接着,去聚簇索引树上查询,这两条记录对应的pId=2,7,因此在pId=2,7的聚簇索引上加行级X锁,采用当前读。
  6. select * from table where num > 200 for update
    • 当num > 200,由于num列上有索引,因此先在符合条件的 num = 300的一条索引记录上加行级X锁。接着,去聚簇索引树上查询,这条记录对应的pId=3,因此在pId=3的聚簇索引上加行级X锁,采用当前读。

RR/Serializable+条件列非索引

RR级别需要多考虑的就是gap lock,他的加锁特征在于,无论你怎么查都是锁全表。如下所示

接下来分析开始

  1. select * from table where num = 200
    • 在RR级别下,不加任何锁,是快照读。
    • 在Serializable级别下,在pId = 1,2,3,7(全表所有记录)的聚簇索引上加S锁。并且在聚簇索引的所有间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
  2. select * from table where num > 200
    • 在RR级别下,不加任何锁,是快照读。
    • 在Serializable级别下,在pId = 1,2,3,7(全表所有记录)的聚簇索引上加S锁。并且在聚簇索引的所有间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
  3. select * from table where num = 200 lock in share mode
    • 在pId = 1,2,3,7(全表所有记录)的聚簇索引上加S锁。并且在聚簇索引的所有间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
  4. select * from table where num > 200 lock in share mode
    • 在pId = 1,2,3,7(全表所有记录)的聚簇索引上加S锁。并且在聚簇索引的所有间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
  5. select * from table where num = 200 for update
    • 在pId = 1,2,3,7(全表所有记录)的聚簇索引上加X锁。并且在聚簇索引的所有间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
  6. select * from table where num > 200 for update
    • 在pId = 1,2,3,7(全表所有记录)的聚簇索引上加X锁。并且在聚簇索引的所有间隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock

RR/Serializable+条件列是聚簇索引

恩,大家应该知道pId是主键列,因此pId用的就是聚簇索引。该情况的加锁特征在于,如果where后的条件为精确查询(=的情况),那么只存在record lock。如果where后的条件为范围查询(>或<的情况),那么存在的是record lock+gap lock。

  1. select * from table where pId = 2
    • 在RR级别下,不加任何锁,是快照读。
    • 在Serializable级别下,是当前读,在pId=2的聚簇索引上加S锁,不存在gap lock。
  2. select * from table where pId > 2
    • 在RR级别下,不加任何锁,是快照读。
    • 在Serializable级别下,是当前读,在pId=3,7的聚簇索引上加S锁。在(2,3)(3,7)(7,+∞)加上gap lock
  3. select * from table where pId = 2 lock in share mode
    • 是当前读,在pId=2的聚簇索引上加S锁,不存在gap lock。
  4. select * from table where pId > 2 lock in share mode
    • 是当前读,在pId=3,7的聚簇索引上加S锁。在(2,3)(3,7)(7,+∞)加上gap lock
  5. select * from table where pId = 2 for update
    • 是当前读,在pId=2的聚簇索引上加X锁。
  6. select * from table where pId > 2 for update
    • 在pId=3,7的聚簇索引上加X锁。在(2,3)(3,7)(7,+∞)加上gap lock
  7. select * from table where pId = 6 [lock in share mode|for update]
    • 注意了,pId=6是不存在的列,这种情况会在(3,7)上加gap lock。
  8. select * from table where pId > 18 [lock in share mode|for update]
    • 注意了,pId>18,查询结果是空的。在这种情况下,是在(7,+∞)上加gap lock。

RR/Serializable+条件列是非聚簇索引

这里非聚簇索引,需要区分是否为唯一索引。因为如果是非唯一索引,间隙锁的加锁方式是有区别的。

先说一下,唯一索引的情况。如果是唯一索引,情况和RR/Serializable+条件列是聚簇索引类似,唯一有区别的是:这个时候有两棵索引树,加锁是加在对应的非聚簇索引树和聚簇索引树上!大家可以自行推敲!

下面说一下,非聚簇索引是非唯一索引的情况,他和唯一索引的区别就是通过索引进行精确查询以后,不仅存在record lock,还存在gap lock。而通过唯一索引进行精确查询后,只存在record lock,不存在gap lock。老规矩在num列建立非唯一索引

  1. select * from table where num = 200
    • 在RR级别下,不加任何锁,是快照读。
    • 在Serializable级别下,是当前读,在pId=2,7的聚簇索引上加S锁,在num=200的非聚集索引上加S锁,在(100,200)(200,300)加上gap lock。
  2. select * from table where num > 200
    • 在RR级别下,不加任何锁,是快照读。
    • 在Serializable级别下,是当前读,在pId=3的聚簇索引上加S锁,在num=300的非聚集索引上加S锁。在(200,300)(300,+∞)加上gap lock
  3. select * from table where num = 200 lock in share mode
    • 是当前读,在pId=2,7的聚簇索引上加S锁,在num=200的非聚集索引上加S锁,在(100,200)(200,300)加上gap lock。
  4. select * from table where num > 200 lock in share mode
    • 是当前读,在pId=3的聚簇索引上加S锁,在num=300的非聚集索引上加S锁。在(200,300)(300,+∞)加上gap lock。
  5. select * from table where num = 200 for update
    • 是当前读,在pId=2,7的聚簇索引上加S锁,在num=200的非聚集索引上加X锁,在(100,200)(200,300)加上gap lock。
  6. select * from table where num > 200 for update
    • 是当前读,在pId=3的聚簇索引上加S锁,在num=300的非聚集索引上加X锁。在(200,300)(300,+∞)加上gap lock
  7. select * from table where num = 250 [lock in share mode|for update]
    • 注意了,num=250是不存在的列,这种情况会在(200,300)上加gap lock。
  8. select * from table where num > 400 [lock in share mode|for update]
    • 注意了,pId>400,查询结果是空的。在这种情况下,是在(400,+∞)上加gap lock。

转自:https://www.cnblogs.com/rjzheng/p/9721765.html

引言

为什么写这篇文章?

大家当年在学 MySQL 的时候,为了能够迅速就业,一般是学习一下 MySQL 的基本语法,差不多就出山找工作了。水平稍微好一点的童鞋呢还会懂一点存储过程的编写,又或者是懂一点索引的创建和使用。但是呢,基本上大家都忽略了对底层知识的学习。为什么呢?因为工作中很少用到嘛。然后呢,市面上流传的大部分这种底层的知识,又比较偏运维,研发懂这么多意义也不是太大,很多知识可能这辈子都不会用到。

因此,我整理了一部分相关的知识,希望大家有所收获。

研发究竟要懂哪些?

主要分为两个部分

  • binlog 的相关概念
  • 怎么解析 binlog

计划分上下两个部分来叙述。上部分讲述 binlog 的相关概念这部分的知识,我们不需要像运维懂的那么深,我会列举一些常见概念和常见配置,大家匆匆扫一眼,有个概念即可。这样大家以后和运维讨论问题的时候,也不会一脸的懵逼。正所谓

懵逼树上懵逼果,懵逼树下你和我。

懵逼树前排排坐,一人一个懵逼果。

博主一个人默默的把懵逼果收走独享就好,各位读者还是懂点基本概念,以后方便和运维沟通。下半部分讲怎么解析 binlog。

另外,这篇文章是给研发大大看的,可能有些概念我理解的也不对,请运维大大轻喷。

正文

记得我的“一个定义,两个误解,三个用途,四个常识”

一个定义

先从定义开始讲起

binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。

binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看 MySQL 执行过的所有语句。

多说一句,如果 update 操作没有造成数据变化,也是会记入 binlog。

两个误解

误解一:binlog 只是一类记录操作内容的日志文件

因为 binlog 称之为二进制日志,很多研发会把这个二进制日志和我们平时在代码里写的代码日志联系在一起。因为我们的代码日志,只有一类记录操作容的文件,并不包含索引文件。然而,这个二进制日志包括两类文件:

  • 索引文件(文件名后缀为.index)用于记录哪些日志文件正在被使用
  • 日志文件(文件名后缀为.00000*)记录数据库所有的 DDL 和 DML(除了数据查询语句)语句事件。

这么说可能还有一点抽象,假设文件 my.cnf 中有这么三条配置

1
2
3
4
5
log_bin:on 打开binlog日志

log_bin_basename:bin文件路径及名前缀(/var/log/mysql/mysql-bin)

log_bin_index:bin文件index(/var/log/mysql/mysql-bin.index)

那么你会在文件目录 /var/log/mysql/ 下面发现两个文件 mysql-bin.000001 和 mysql-bin.index。

mysql-bin.index 就是我们所说的索引文件,打开瞅瞅,内容是下面这样,记录哪些文件是日志文件。

1
./mysql-bin.000001

那么说到日志文件。在 innodb 里其实又可以分为两部分,一部分在缓存中,一部分在磁盘上。这里业内有一个词叫做刷盘,就是指将缓存中的日志刷到磁盘上。跟刷盘有关的参数有两个:sync_binlog 和 binlog_cache_size。这两个参数作用如下

1
2
3
binlog_cache_size: 二进制日志缓存部分的大小,默认值32k

sync_binlog=[N]: 表示写缓冲多少次,刷一次盘,默认值为0

注意两点:

  1. binlog_cache_size 设过大,会造成内存浪费。binlog_cache_size 设置过小,会频繁将缓冲日志写入临时文件。具体怎么设,有兴趣自行查询,我觉得研发大大根本没机会去设这个值的,了解即可。
  2. sync_binlog=0:表示刷新 binlog 时间点由操作系统自身来决定,操作系统自身会每隔一段时间就会刷新缓存数据到磁盘,这个性能最好。sync_binlog=1,代表每次事务提交时就会刷新 binlog 到磁盘。sync_binlog=N,代表每 N 个事务提交会进行一次 binlog 刷新。

另外,这里存在一个一致性问题,sync_binlog=N,数据库在操作系统宕机的时候,可能数据并没有同步到磁盘,于是再次重启数据库,会带来数据丢失问题。

当sync_binlog=1,事务在 commit 的时候,数据写入 binlog,但是还没写入事务日志(redo log和undo log)。此时宕机,重启数据库,数据被回滚。但是 binlog 里已经记录,这里存在不一致问题。这个事务日志和 binlog 一致性的问题,大家可以查询 mysql 的内部 XA 协议,该协议就是解决这个一致性问题的。

误解二:binlog 是 InnoDb 独有的

binlog 是以事件形式记录的,这句话通俗点说,就是 binlog 的内容都是一个个的事件。这块具体的我会在下一篇讲,这篇记住 binlog 的内容就是一个个事件就行。

注意了,这里的用词,是一个个事件,而不是事务。大家应该知道 Innodb 和 mysiam 最显著的区别就是一个支持事务,一个不支持事务。

因此你可以说,binlog 是基于事务来记录二进制日志,比如 sync_binlog=1,每提交一次事务,就写入 binlog。你却不能说 binlog 是事务日志,binlog 不仅记录 innodb 日志,在 myisam 中,也一样存在 binlog。

三个用途

这三个用途,出自《MySQL技术内幕 InnoDB存储引擎》一书,分别为 恢复、复制、审计。这三个用途,研发大大们了解一下即可,比如数据恢复,你碰到同事删库的机会实在太少。假如真的有同事舍己为人,冒着离职的风险给你提供做数据恢复的机会,大把运维工程师待命在那,轮不到你的。所以,这三个功能了解即可。

恢复:这里网上有大把的文章指导你,如何利用 binlog 日志恢复数据库数据。如果你真的觉得自己很有时间,就自己去创建个库,然后删了,再去恢复一下数据,练练手吧。

复制: 如图所示(图片不是自己画的,偷懒了)
1

主库有一个 log dump 线程,将 binlog 传给从库

从库有两个线程,一个 I/O 线程,一个 SQL 线程,I/O 线程读取主库传过来的 binlog 内容并写入到 relay log,SQL 线程从 relay log 里面读取内容,写入从库的数据库。

审计:用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入攻击。

四个常识

常识一:binlog 常见格式

这块知识我用一个表格来表示,没必要啰嗦一大堆。

format 定义 优点 缺点
statement 记录的是修改SQL语句 日志文件小,节约IO,提高性能 准确性差,对一些系统函数不能准确复制或不能复制,如now()、uuid()等
row 记录的是每行实际数据的变更 准确性强,能准确复制数据的变更 日志文件大,较大的网络IO和磁盘IO
mixed statement和row模式的混合 准确性强,文件大小适中 有可能发生主从不一致问题

业内目前推荐使用的是 row 模式,准确性高,虽然说文件大,但是现在有 SSD 和万兆光纤网络,这些磁盘 IO 和网络 IO 都是可以接受的。

那么,大家一定想问,为什么不推荐使用 mixed 模式,理由如下
假设 master 有两条记录,而 slave 只有一条记录。

master的数据为

1
2
3
4
5
6
+----+------------------------------------------------------+
| id | n |
+----+------------------------------------------------------+
| 1 | d24c2c7e-430b-11e7-bf1b-00155d016710 |
| 2 | ddd |
+----+------------------------------------------------------+

slave的数据为

1
2
3
4
5
+----+-------------------------------------------------------+
| id | n |
+----+-------------------------------------------------------+
| 1 | d24c2c7e-430b-11e7-bf1b-00155d016710 |
+----+-------------------------------------------------------+

当在 master上 更新一条从库不存在的记录时,也就是 id=2 的记录,你会发现 master 是可以执行成功的。而 slave 拿到这个 SQL 后,也会照常执行,不报任何异常,只是更新操作不影响行数而已。并且你执行命令 show slave status,查看输出,你会发现没有异常。但是,如果你是 row 模式,由于这行根本不存在,是会报1062错误的。

常识二:怎查看 binlog

binlog 本身是一类二进制文件。二进制文件更省空间,写入速度更快,是无法直接打开来查看的。

因此 mysql 提供了命令 mysqlbinlog 进行查看。

一般的 statement 格式的二进制文件,用下面命令就可以

1
mysqlbinlog mysql-bin.000001 

如果是 row 格式,加上 -v 或者 -vv 参数就行,如

1
mysqlbinlog -vv mysql-bin.000001 

常识三:怎么删 binlog

删 binlog 的方法很多,有三种是常见的

  1. 使用 reset master,该命令将会删除所有日志,并让日志文件重新从 000001 开始。
  2. 使用命令
    1
    PURGE { BINARY | MASTER } LOGS { TO 'log_name' | BEFORE datetime_expr }
    例如
    1
    purge master logs to "binlog_name.00000X" 
    将会清空 00000X 之前的所有日志文件。
  3. 使用 –expire_logs_days=N 选项指定过了多少天日志自动过期清空。

常识四:binlog 常见参数

常见参数,列举如下,有个印象就好。

参数名 含义
log_bin = {on off
sql_log_bin ={ on off }
expire_logs_days 指定自动删除二进制日志的时间,即日志过期时间
log_bin_index 指定mysql-bin.index文件的路径
binlog_format = { mixed row
max_binlog_size 指定二进制日志文件最大值
binlog_cache_siz 指定事务日志缓存区大小
max_binlog_cache_size 指定二进制日志缓存最大大小
sync_binlog = { 0 n }

思考题

请问,我说的

  • 一个定义
  • 两个误解
  • 三个用途
  • 四个常识

说的是什么呢?

另外,我会在下一篇进行介绍,怎么用代码解析binlog日志。


转自:https://www.cnblogs.com/rjzheng/p/9745551.html

引言

这篇是《研发应该懂的binlog知识(上)》的下半部分。在本文,我会阐述一下 binlog 的结构,以及如何使用 java 来解析 binlog。

不过,话说回来,其实严格意义上来说,研发应该还需要懂如何监听 binlog 的变化。我本来也想写这块的知识,但是后来发现,这块讲起来篇幅过长,需要从mysql的通讯协议开始讲起,实在是不适合放在这篇文章讲,所以改天抽时间再写一篇监听 binlog 变化的文章。

说到这里,大家可能有一个疑问:

研发为什么要懂得如何解析 binlog?

说句实在话,如果在实际项目中遇到,我确实推荐使用现成的jar包来解析,比如 mysql-binlog-connector-java 或者 open-replicator 等。但是呢,这类 jar 包解析 binlog 的原理都是差不多的。因为我有一个怪癖,我用一个 jar 包,都会去溜几眼,看一下大致原理,所以想在这个部分把如何解析 binlog 的实质性原理讲出来,希望大家有所收获。大家懂一个大概的原理即可,不需要自己再去造轮子。另外,注意了,本文教你的是解析 binlog 的方法,不可能每一个事件带你解析一遍。能达到举一反三的效果,就是本文的目的。

什么,你还没碰到过解析 binlog 的需求?没事,那先看着,就当学习一下,将来一定会遇到。

正文

先说一下,binlog 的结构。

文件头由一个四字节 Magic Number 构成,其值为 1852400382,在内存中就是”0xfe,0x62,0x69,0x6e”。这个 Magic Number 就是来验证这个 binlog 文件是否有效 。

引一个题外话

java 里头的 class 文件,头四个字节 的Magic Number 是多少?

回答:”0xCAFEBABE。”这个数字可能比较难记,记(咖啡宝贝)就好。

下面写个程序,读一份 binlog 文件,给大家 binlog 看看头四个字节是否为”0xfe,0x62,0x69,0x6e”,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MagicParser {

public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};

public static void main(String[] args)throws Exception {
String filePath = "D:\\mysql-bin.000001";
File binlogFile = new File(filePath);
ByteArrayInputStream inputStream = null;
inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));
byte[] magicHeader = inputStream.read(4);
System.out.println("魔数\\xfe\\x62\\x69\\x6e是否正确:"+Arrays.equals(MAGIC_HEADER, magicHeader));
}
}

输出如下

1
魔数\xfe\x62\x69\x6e是否正确:true

在文件头之后,跟随的是一个一个事件依次排列。在《binlog二进制文件解析》一文中,将其分为三个部分:通用事件头(common-header)、私有事件头(post-header)和事件体(event-body)。本文修改了一下,只用两个 Java 类来修饰 binlog 中的事件,即 EventHeader 和 EventData。可以理解为下述的对应关系:

1
2
EventHeader --> 通用事件头(common-header)
EventData ---> 私有事件头(post-header)和事件体(event-body)

于是,你们可以把 Binlog 的文件结构像下面这么理解
2

说一下这个 Checksum,在获取 event 内容的时候,会增加 4 个额外字节做校验用。mysql5.6.5 以后的版本中binlog_checksum=crc32,而低版本都是 binlog_checksum=none。如果不想校验,可以使用 set 命令设置 set binlog_checksum=none。说得再通俗一点,Checksum 要么为 4 个字节,要么为 0 个字节。

下面说一下通用事件头的结构,如下所示

属性 字节数 含义
timestamp 4 包含了该事件的开始执行时间
eventType 1 事件类型
serverId 4 标识产生该事件的MySQL服务器的server-id
eventLength 4 该事件的长度(Header+Data+CheckSum)
nextPosition 4 下一个事件在binlog文件中的位置
flags 2 标识产生该事件的MySQL服务器的server-id。

从上表可以看出,EventHeader 固定为 19 个字节,为此我们构造下面的类,来解析这个通用事件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class EventHeader {
private long timestamp;
private int eventType;
private long serverId;
private long eventLength;
private long nextPosition;
private int flags;
//省略setter和getter方法
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("EventHeader");
sb.append("{timestamp=").append(timestamp);
sb.append(", eventType=").append(eventType);
sb.append(", serverId=").append(serverId);
sb.append(", eventLength=").append(eventLength);
sb.append(", nextPosition=").append(nextPosition);
sb.append(", flags=").append(flags);
sb.append('}');
return sb.toString();
}
}

OK,接下来,我们来一段代码试着解析一下第一个事件的 EventHeader,代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HeaderParser {

public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};

public static void main(String[] args)throws Exception {
String filePath = "D:\\mysql-bin.000001";
File binlogFile = new File(filePath);
ByteArrayInputStream inputStream = null;
inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));
byte[] magicHeader = inputStream.read(4);
if(!Arrays.equals(MAGIC_HEADER, magicHeader)){
throw new RuntimeException("binlog文件格式不对");
}
EventHeader eventHeader = new EventHeader();
eventHeader.setTimestamp(inputStream.readLong(4) * 1000L);
eventHeader.setEventType(inputStream.readInteger(1));
eventHeader.setServerId(inputStream.readLong(4));
eventHeader.setEventLength(inputStream.readLong(4));
eventHeader.setNextPosition(inputStream.readLong(4));
eventHeader.setFlags(inputStream.readInteger(2));
System.out.println(eventHeader);

}
}

输出如下

1
EventHeader{timestamp=1536487335000, eventType=15, serverId=1, eventLength=119, nextPosition=123, flags=1}

注意看,两个参数

1
2
eventLength=119
nextPosition=123

下一个事件从 123 字节开始。这是怎么算的呢,当前事件长度是是 119 字节,算上最开始 4 个字节的魔数占位符,那么下一个事件自然是,119+4=123,从 123 字节开始。再强调一次,这个 119 字节,是包含EventHeader、EventData、Checksum,三个部分的长度为 119。

最重要的一个参数

1
eventType=15

我们去下面的地址

https://dev.mysql.com/doc/internals/en/binlog-event-type.html

查询一下,15 对应的事件类型为 FORMAT_DESCRIPTION_EVENT。我们接下来,需要知道 FORMAT_DESCRIPTION_EVENT 所对应 EventData 的结构。在下面的地址

https://dev.mysql.com/doc/internals/en/format-description-event.html

查询得到 EventData 的结构对应如下表所示

属性 字节数 含义
binlogVersion 2 binlog版本
serverVersion 50 服务器版本
timestamp 4 该字段指明该binlog文件的创建时间。
headerLength 1 事件头长度,为19
headerArrays n 一个数组,标识所有事件的私有事件头的长度

ps:这个 n 其实我们可以推算出,为 39。事件长度为 119 字节,减去事件头 19 字节,减去末位的 4 字节(末位四个字节循环校验码),减去 2 个字节的 binlog 版本,减去 50 个字节的服务器版本号,减去 4 个字节的时间戳,减去 1 个字节的事件头长度。得到如下算式

$119−19−4−2−50−4−1=39$

不过,我们还是假装不知道n是多少吧。

根据上表结构 ,我们给出一个JAVA类如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FormatDescriptionEventData {
private int binlogVersion;
private String serverVersion;
private long timestamp;
private int headerLength;
private List headerArrays = new ArrayList<Integer>();
//省略setter和getter方法
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("FormatDescriptionEventData");
sb.append("{binlogVersion=").append(binlogVersion);
sb.append(", serverVersion=").append(serverVersion);
sb.append(", timestamp=").append(timestamp);
sb.append(", headerLength=").append(headerLength);
sb.append(", headerArrays=").append(headerArrays);
sb.append('}');
return sb.toString();
}
}

那如何解析呢,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class HeaderParser {

public static final byte[] MAGIC_HEADER = new byte[] { (byte) 0xfe,
(byte) 0x62, (byte) 0x69, (byte) 0x6e };

public static void main(String[] args) throws Exception {
String filePath = "D:\\mysql-bin.000001";
File binlogFile = new File(filePath);
ByteArrayInputStream inputStream = null;
inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));
byte[] magicHeader = inputStream.read(4);
if (!Arrays.equals(MAGIC_HEADER, magicHeader)) {
throw new RuntimeException("binlog文件格式不对");
}
EventHeader eventHeader = new EventHeader();
eventHeader.setTimestamp(inputStream.readLong(4) * 1000L);
eventHeader.setEventType(inputStream.readInteger(1));
eventHeader.setServerId(inputStream.readLong(4));
eventHeader.setEventLength(inputStream.readLong(4));
eventHeader.setNextPosition(inputStream.readLong(4));
eventHeader.setFlags(inputStream.readInteger(2));
System.out.println(eventHeader);
inputStream.enterBlock((int) (eventHeader.getEventLength() - 19 - 4));
FormatDescriptionEventData descriptionEventData = new FormatDescriptionEventData();
descriptionEventData.setBinlogVersion(inputStream.readInteger(2));
descriptionEventData.setServerVersion(inputStream.readString(50).trim());
descriptionEventData.setTimestamp(inputStream.readLong(4) * 1000L);
descriptionEventData.setHeaderLength(inputStream.readInteger(1));
int sums = inputStream.available();
for (int i = 0; i < sums; i++) {
descriptionEventData.getHeaderArrays().add(inputStream.readInteger(1));
}
System.out.println(descriptionEventData);
}
}

至于输出,就不给大家看了,没啥意思。大家看 headerArrays 的值即可,如下所示

1
headerArrays=[56, 13, 0, 8, 0, 18, 0, 4, 4, 4, 4, 18, 0, 0, 95, 0, 4, 26, 8, 0, 0, 0, 8, 8, 8, 2, 0, 0, 0, 10, 10, 10, 42, 42, 0, 18, 52, 0, 1]

其实他所输出的值,可以在地址 https://dev.mysql.com/doc/internals/en/format-description-event.html

查询到,该页有一个表格如下所示,其中我红圈的地方,就是私有事件头的长度,即
3

总结

关于其他事件的结构体,大家可以自行去网站查询,解析原理都是一样的。

转自:https://www.cnblogs.com/takumicx/p/9328459.html

在java并发包下各种同步组件的底层实现中,LockSupport的身影处处可见。JDK中的定义为用来创建锁和其他同步类的线程阻塞原语。

*Basic thread blocking primitives for creating locks and other
*synchronization classes.

我们可以使用它来阻塞和唤醒线程,功能和wait,notify有些相似,但是LockSupport比起wait,notify功能更强大,也好用的多。

阻塞唤醒线程例子

使用wait,notify阻塞唤醒线程

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class WaitNotifyTest {
private static Object obj = new Object();
public static void main(String[] args) {
new Thread(new WaitThread()).start();
new Thread(new NotifyThread()).start();
}
static class WaitThread implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("start wait!");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end wait!");
}
}
}
static class NotifyThread implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("start notify!");
obj.notify();
System.out.println("end notify");
}
}
}
}

结果
1

使用wait,notify来实现等待唤醒功能至少有两个缺点:

  1. 由上面的例子可知,wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,这限制了其使用场合:只能在同步代码块中。
  2. 另一个缺点可能上面的例子不太明显,当对象的等待队列中有多个线程时,notify只能随机选择一个线程唤醒,无法唤醒指定的线程。

而使用LockSupport的话,我们可以在任何场合使线程阻塞,同时也可以指定要唤醒的线程,相当的方便。

使用LockSupport阻塞唤醒线程

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LockSupportTest {

public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("开始线程唤醒");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒");

}

static class ParkThread implements Runnable{

@Override
public void run() {
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
}

结果
2

LockSupport.park(); 可以用来阻塞当前线程,park是停车的意思,把运行的线程比作行驶的车辆,线程阻塞则相当于汽车停车,相当直观。该方法还有个变体 LockSupport.park(Object blocker),指定线程阻塞的对象 blocker,该对象主要用来排查问题。方法 LockSupport.unpark(Thread thread) 用来唤醒线程,因为需要线程作参数,所以可以指定线程进行唤醒。

LockSupport的其他特色

可以先唤醒线程再阻塞线程

在阻塞线程前睡眠1秒中,使唤醒动作先于阻塞发生,看看会发生什么

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LockSupportTest {

public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("开始线程唤醒");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒");

}

static class ParkThread implements Runnable{

@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
}

结果
3

先唤醒指定线程,然后阻塞该线程,但是线程并没有真正被阻塞而是正常执行完后退出了。这是怎么回事?我们试着在改动下代码,先唤醒线程两次,在阻塞线程两次,看看会发生什么。

先唤醒线程两次再阻塞两次会发生什么

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LockSupportTest {

public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
for(int i=0;i<2;i++){
System.out.println("开始线程唤醒");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒");
}
}

static class ParkThread implements Runnable{

@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
}
}

结果
4

可以看到线程被阻塞导致程序一直无法结束掉。对比上面的例子,我们可以得出一个匪夷所思的结论,先唤醒线程,在阻塞线程,线程不会真的阻塞;但是先唤醒线程两次再阻塞两次时就会导致线程真的阻塞。那么这到底是为什么?

LockSupport阻塞和唤醒线程原理浅析

既然是浅析,那就不抠底层细节,只讲关键,细节可能有疏漏和不到位的地方。
每个线程都有Parker实例,如下面的代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}

LockSupport就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。

  • 当调用park()方法时,会将_counter置为0,同时判断前值,小于1说明前面被unpark过,则直接退出,否则将使该线程阻塞。
  • 当调用unpark()方法时,会将_counter置为1,同时判断前值,小于1会进行线程唤醒,否则直接退出。形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。
  • 为什么可以先唤醒线程后阻塞线程?因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。
  • 为什么唤醒两次后阻塞两次会阻塞线程。
  • 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。

总结

LockSupport是JDK中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。

JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。

常用版本号:

  • Alpha:软件或系统的内部测试版本,会有很多Bug,仅内部人员使用
  • Beta:软件或系统的测试版本,这一版本通常是在Alpha版本后,会有很多新功能,同时也有不少Bug
  • Gamma:软件或系统接近于成熟的版本,只需要做一些小的改进就能发行

微软常用的版本号:

  • RC(Release Candidate):候选版本,这一版本不会增加新功能,多要进行Debug
  • GA(General Available):正式发布版本,这个版本就是正式的版本
  • RTM(Release to Manufacture):给工厂大量生产的压片版本,与正式版内容一样
  • OEM(Original Entrusted Manufacture):给计算机厂商的出场销售版本,不零售只预装
  • RVL:号称是正式版,其实RVL根本不是版本的名称。它是中文版/英文版文档破解出来的
  • EVAL:而流通在网络上的EVAL版,与“评估版”类似,功能上和零售版没有区别
  • RTL(Retail):零售版是真正的正式版,正式上架零售版

苹果常用的版本号:

  • GM(Gold Master):正式版前最后一个测试版,其实也就是正式版

谷歌Chrome浏览器常用的版本号:

  • Chromium:开源版本,迭代速度极快,数小时就会有新版本,有很多新功能,等待验证后会移植到Chrome
  • Canary:迭代速度相对于Chromium版稍慢一些,功能非常新但未经过验证,同时崩溃的概率非常高
  • Dev:基于Chromium开发,每周出新功能,并且这些功能还有一定的筛选,另外还修复了一些Bug和不稳定因素
  • Beta:基于Dev版,Chrome会基于这一版本进行改进,一般按月更新,功能更加完善
  • Stable:稳定版本,也就是Chrome的正式版本,这一版本基于Beta版,已知Bug都被修复,一般情况下,更新比较慢

Ubuntu系统常用的版本号:

  • LTS(Long Term Support):长期演进版,Ubuntu会对这一版本的支持时间更长。目前Java也在运用这种方式

转自:https://www.jianshu.com/p/b8203d46895c

Unix网络编程中的五种IO模型

  • Blocking IO - 阻塞IO
  • NoneBlocking IO - 非阻塞IO
  • IO multiplexing - IO多路复用
  • signal driven IO - 信号驱动IO
  • asynchronous IO - 异步IO

由于signal driven IO在实际使用中并不常用,所以这里只讨论剩下的四种IO模型。

在讨论之前先说明一下IO发生时涉及到的对象和步骤,对于一个network IO,它会涉及到两个系统对象:

  • application 调用这个IO的进程
  • kernel 系统内核

那他们经历的两个交互过程是:

  • 阶段1 wait for data 等待数据准备
  • 阶段2 copy data from kernel to user 将数据从内核拷贝到用户进程中

之所以会有同步、异步、阻塞和非阻塞这几种说法就是根据程序在这两个阶段的处理方式不同而产生的。了解了这些背景之后,我们就分别针对四种IO模型进行讲解

Blocking IO - 阻塞IO

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概如下图:

1

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network IO来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

NoneBlockingIO - 非阻塞IO

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

2

从图中可以看出,当用户进程发出recvfrom这个系统调用后,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个结果(no datagram ready)。从用户进程角度讲 ,它发起一个操作后,并没有等待,而是马上就得到了一个结果。用户进程得知数据还没有准备好后,它可以每隔一段时间再次发送recvfrom操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,用户进程其实是需要不断的主动询问kernel数据好了没有。

IO multiplexing - IO多路复用

I/O多路复用(multiplexing)是网络编程中最常用的模型,像我们最常用的select、epoll都属于这种模型。以select为例:

3

看起来它与blocking I/O很相似,两个阶段都阻塞。但它与blocking I/O的一个重要区别就是它可以等待多个数据报就绪(datagram ready),即可以处理多个连接。这里的select相当于一个“代理”,调用select以后进程会被select阻塞,这时候在内核空间内select会监听指定的多个datagram (如socket连接),如果其中任意一个数据就绪了就返回。此时程序再进行数据读取操作,将数据拷贝至当前进程内。由于select可以监听多个socket,我们可以用它来处理多个连接。

在select模型中每个socket一般都设置成non-blocking,虽然等待数据阶段仍然是阻塞状态,但是它是被select调用阻塞的,而不是直接被I/O阻塞的。select底层通过轮询机制来判断每个socket读写是否就绪。

当然select也有一些缺点,比如底层轮询机制会增加开销、支持的文件描述符数量过少等。为此,Linux引入了epoll作为select的改进版本。

asynchronous IO - 异步IO

异步I/O在网络编程中几乎用不到,在File I/O中可能会用到:

4

这里面的读取操作的语义与上面的几种模型都不同。这里的读取操作(aio_read)会通知内核进行读取操作并将数据拷贝至进程中,完事后通知进程整个操作全部完成(绑定一个回调函数处理数据)。读取操作会立刻返回,程序可以进行其它的操作,所有的读取、拷贝工作都由内核去做,做完以后通知进程,进程调用绑定的回调函数来处理数据。

总结

我们来总结一下阻塞、非阻塞,同步和异步这两组概念。

先来说阻塞和非阻塞:

  • 阻塞调用会一直等待远程数据就绪再返回,即上面的阶段1会阻塞调用者,直到读取结束。
  • 而非阻塞无论在什么情况下都会立即返回,虽然非阻塞大部分时间不会被block,但是它仍要求进程不断地去主动询问kernel是否准备好数据,也需要进程主动地再次调用recvfrom来将数据拷贝到用户内存。

再说一说同步和异步:

  • 同步方法会一直阻塞进程,直到I/O操作结束,注意这里相当于上面的阶段1,阶段2都会阻塞调用者。其中 Blocking IO - 阻塞IO,Nonblocking IO - 非阻塞IO,IO multiplexing - IO多路复用,signal driven IO - 信号驱动IO 这四种IO都可以归类为同步IO。
  • 而异步方法不会阻塞调用者进程,即使是从内核空间的缓冲区将数据拷贝到进程中这一操作也不会阻塞进程,拷贝完毕后内核会通知进程数据拷贝结束。
    下面的这张图很好地总结了之前讲的这五种I/O模型(来自Unix Network Programming)

5

最后,在举个简单的例子帮助理解,比如我们怎样解决午饭问题:

A君喜欢下馆子吃饭,服务员点完餐后,A君一直坐在座位上等待厨师炒菜,什么事情也没有干,过了一会服务员端上饭菜后,A君就开吃了 – 【阻塞I/O】

B君也喜欢下馆子,服务员点完餐后,B君看这个服务员姿色不错,便一直和服务员聊人生理想,并时不时的打听自己的饭做好了没有,过了一会饭也做好了,B君也撩到了美女服务员的微信号 – 【非阻塞I/O 】顺便撩了个妹子☺

C君同样喜欢下馆子吃饭,但是C君不喜欢一个人下馆子吃,要呼朋唤友一起下馆子,但是这帮人到了饭店之后,每个人只点自己的,服务员一起给他们下单后,就交给后厨去做了,每做好一个人的,服务员就负责给他们端上来。做他们的服务员真滴好累😫 – 【IO多路复用】

D君比较宅,不喜欢下馆子,那怎么办呢?美团外卖啊(此处应有广告费:-D)手机下单后,自己啥也不用操心,只要等快递小哥上门就行了,这段时间可以撸好几把王者农药的了,嘿嘿 – 【异步IO】

转自:https://www.cnblogs.com/xckxue/p/8685675.html

介绍

synchronized是一种独占式的重量级锁,在运行到同步方法或者同步代码块的时候,让程序的运行级别由用户态切换到内核态,把所有的线程挂起,通过操作系统的指令,去调度线程。这样会频繁出现程序运行状态的切换,线程的挂起和唤醒,会消耗系统资源,为了提高效率,引入了偏向锁、轻量级锁、尽量让多线程访问公共资源的时候,不进行程序运行状态的切换。

synchronized实现原理

synchronized是在jvm中实现,是基于进入和退出Monitor对象来实现方法和代码块的同步

同步代码块:

monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

同步方法

synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,有一个ACC_SYNCHRONIZED标志,JVM就是通过该标志来判断是否需要实现同步的,具体过程为:当线程执行该方法时,会先检查该方法是否标志了ACC_SYNCHRONIZED,如果标志了,线程需要先获取monitor,获取成功后才能调用方法,方法执行完后再释放monitor,在该线程调用方法期间,其他线程无法获取同一个monitor对象。其实本质上和synchronized块相同,只是同步方法是用一种隐式的方式来实现,而不是显式地通过字节码指令。

synchronized 作用

  1. 确保线程互斥的访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决重排序问题

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象

自旋概念

互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能 带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如 果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有 锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

—>自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 1.6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的, 所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作, 反而会带来性能的浪费。因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次 数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。

—>在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象 上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间, 比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自 旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。

锁削除

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待, 认为它们是线程私有的,同步加锁自然就无须进行。

变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的 情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。比如:(只是说明概念,但实际情况并不一定如例子)在线程安全的环境中使用stringBuffer进行字符串拼加。则会在java文件编译的时候,进行锁销除。

锁粗化

我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快地拿到锁。

大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(锁粗化)到整个操作序列的外部。

锁的状态

锁一共有四种状态(由低到高的次序):无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态

锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得所得代价更低而引入了偏向锁,当一个线程访问同步代码块并获取锁时,会在线程的栈帧里创建lockRecord,在lockRecord里和锁对象的MarkWord里存储线程a的线程id.以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,说明是其他线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

轻量级锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果,完成自旋策略还是发现线程没有释放锁,或者让别的线程占用,则线程试图将轻量级锁升级为重量级锁。

轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

重量级锁

就是让争抢锁的线程从用户态转换成内核态。让cpu借助操作系统进行线程协调。

具体流程
每一个线程在准备获取共享资源时:

第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。

第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。

第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord.

第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋.

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败

第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己.

1

偏向锁,轻量级锁,重量级锁对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景(只有一个线程进入临界区)
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快(多个线程交替进入临界区)
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较慢(多个线程同时进入临界区)

如果上面的没看懂,可以先看看下面形象的例子:

打个比喻,假设你要回家上厕所,你要关上家里的大门,再关上自己房间的门,再关上房间厕所的门。如果你是一个人在家的话,那么其实只要关上家里的大门就好了,没人会来跟你抢上厕所(或者偷窥你~),就不用关上房间门和厕所门了,这就是偏向锁,只是你一个人的情况时才有用,即一个线程在获取锁的时候,重入的时候就不用做任何操作了,这不是很省事嘛。

那,如果家里有人,你就不能这么做了,他可能会跟你抢厕所呢,这就是偏向锁膨胀成轻量级锁。这就是多个线程交替获取锁的情况。那这个时候又要怎么做呢?比如说你哥上着厕所了,你也想上厕所,你猜你哥上厕所的时间不会太久,于是你就在厕所门口等一会(自旋),以前synchronized的方案就是让你回房间躺着等(阻塞),可能回房间的时间都比你拉尿的时间长(挂起线程的时间比执行同步方法中的时间还要长的情况)。这就是轻量级锁。

接着上面的故事,当你等的有点久了,你会觉得你哥可能这次要上很久了,所以你就回房间等了(阻塞),这时候就从轻量级锁升级到重量级锁了。

转自:https://www.cnblogs.com/tufujie/p/9413852.html

在日常工作中,我们会有时会开慢查询去记录一些执行时间比较久的SQL语句,找出这些 SQL 语句并不意味着完事了,些时我们常常用到 explain 这个命令来查看一个这些 SQL 语句的执行计划,查看该 SQL 语句有没有使用上了索引,有没有做全表扫描,这都可以通过 explain 命令来查看。所以我们深入了解 MySQL 的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,以及当运行 SQL 语句时哪种策略预计会被优化器采用。

1
2
3
4
-- 实际SQL,查找用户名为Jefabc的员工
select * from emp where name = 'Jefabc';
-- 查看SQL是否使用索引,前面加上explain即可
explain select * from emp where name = 'Jefabc';

1

expain 出来的信息有 10 列,分别是 id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra

概要描述:

  • id: 选择标识符
  • select_type: 表示查询的类型。
  • table: 输出结果集的表
  • partitions: 匹配的分区
  • type: 表示表的连接类型
  • possible_keys: 表示查询时,可能使用的索引
  • key: 表示实际使用的索引
  • key_len: 索引字段的长度
  • ref: 列与索引的比较
  • rows: 扫描出的行数(估算的行数)
  • filtered: 按表条件过滤的行百分比
  • Extra: 执行情况的描述和说明

下面对这些字段出现的可能进行解释:

id

SELECT 识别符。这是 SELECT 的查询序列号

我的理解是 SQL 执行的顺序的标识,SQL 从大到小的执行

  1. id 相同时,执行顺序由上至下
  2. 如果是子查询,id 的序号会递增,id 值越大优先级越高,越先被执行
  3. id 如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id 值越大,优先级越高,越先执行
    1
    2
    -- 查看在研发部并且名字以Jef开头的员工,经典查询
    explain select e.no, e.name from emp e left join dept d on e.dept_no = d.no where e.name like 'Jef%' and d.name = '研发部';

2

select_type

示查询中每个 select 子句的类型

  • SIMPLE(简单 SELECT,不使用 UNION 或子查询等)
  • PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的 select 被标记为 PRIMARY)
  • UNION(UNION 中的第二个或后面的 SELECT 语句)
  • DEPENDENT UNION(UNION 中的第二个或后面的 SELECT 语句,取决于外面的查询)
  • UNION RESULT(UNION 的结果,union 语句中第二个 select 开始后面所有 select)
  • SUBQUERY(子查询中的第一个 SELECT,结果不依赖于外部查询)
  • DEPENDENT SUBQUERY(子查询中的第一个 SELECT,依赖于外部查询)
  • DERIVED(派生表的 SELECT, FROM 子句的子查询)
  • UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)

table

显示这一步所访问数据库中表名称(显示这一行的数据是关于哪张表的),有时不是真实的表名字,可能是简称,例如上面的 e、d,也可能是第几步执行的结果的简称

type

对表访问方式,表示 MySQL 在表中找到所需行的方式,又称“访问类型”。

常用的类型有: ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)

  • ALL:Full Table Scan, MySQL 将遍历全表以找到匹配的行
  • index: Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树
  • range: 只检索给定范围的行,使用一个索引来选择行
  • ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
  • eq_ref: 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用 primary key 或者 unique key 作为关联条件
  • const、system: 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量,system 是 const 类型的特例,当查询的表只有一行的情况下,使用system
  • NULL: MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。

possible_keys

指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用(该查询可以利用的索引,如果没有任何索引显示 null)

该列完全独立于 EXPLAIN 输出所示的表的次序。这意味着在 possible_keys 中的某些键实际上不能按生成的表次序使用。

如果该列是 NULL,则没有相关的索引。在这种情况下,可以通过检查 WHERE 子句看是否它引用某些列或适合索引的列来提高你的查询性能。如果是这样,创造一个适当的索引并且再次用 EXPLAIN 检查查询

Key

key 列显示 MySQL 实际决定使用的键(索引),必然包含在 possible_keys 中

如果没有选择索引,键是 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 需要使用临时表来存储结果集,常见于排序和分组查询,常见 group by ; order by
  • Using filesort:当 Query 中包含 order by 操作,而且无法利用索引完成的排序操作称为“文件排序”
    1
    2
    -- 测试 Extra 的 filesort
    explain select * from emp order by name;
  • Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
  • Impossible where:这个值强调了where语句会导致没有符合条件的行(通过收集统计信息不可能存在结果)。
  • Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
  • No tables used:Query语句中使用 from dual 或不含任何 from 子句
    1
    -- explain select now() from dual;

总结:

  • EXPLAIN 不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
  • EXPLAIN 不考虑各种 Cache
  • EXPLAIN 不能显示 MySQL 在执行查询时所作的优化工作
  • 部分统计信息是估算的,并非精确值
  • EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划。