You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

100 KiB

Database

Introduction:收纳技术相关的数据库知识 事务索引SQL优化 等总结!

[TOC]

数据库范式

  • 1NF:所有字段仅包含单值(即单个字段不可在分割使用)

  • 2NF:非键字段必须全完依赖于主键(不能是主键的部分)

  • 3NF:非键字段不能依赖于非键字段(禁止传递依赖)

  • BCNF:并且主属性不依赖于主属性

  • 第四范式4NF:要求把同一表内的多对多关系删除

  • 第五范式5NF:从最终结构重新建立原始结构

四种范式之间的关系:

1NF→2NF消去非主属性对键的部分函数依赖

2NF→3NF消去非主属性对键的传递函数依赖

3NF→BCNF消去主属性对键的传递函数依赖

第一范式(1NF)

列都是不可再分。即实体中的某个属性有多个值时,必须拆分为不同的属性。例如:

用户信息表

编号 姓名 年龄 地址
1 小王 23 浙江省杭州市拱墅区湖州街51号

当实际需求对地址没有特定的要求下,这个用户信息表的每一列都是不可分割的。但是当实际需求对省份或者城市有特别要求时,这个用户信息表中的地址就是可以分割的,改为:

用户信息表

编号 姓名 年龄 省份 城市 详细地址
1 小王 23 浙江省 杭州市 拱墅区湖州街51号

好处

  • 表结构相对清晰
  • 易于查询

第二范式(2NF)

每个表只描述一件事情。满足第一范式( 1NF的情况下每行必须有主键且主键与非主键之间是完全函数依赖关系消除部分子函数依赖。即数据库表中的每一列都和主键的所有属性相关不能只和主键的部分属性相关。完全函数依赖有属性集 XY通过 X 中的所有属性能够推出 Y 中的任意属性,但是 X 的任何真子集,都不能推出 Y 中的任何属性。例如:

学生课程表

学生编号 课程编号 学生名称 课程名称 所在班级 班主任
S1 C1 小王 计算机导论 计算机3班 陈老师
S1 C2 小王 数据结构 计算机3班 陈老师
S2 C1 小马 计算机导论 软件1班 李老师

将学生编号和课程编号作为主键,能确定唯一一条数据,但是学生名称只跟学生编号有关,跟课程编号无关,即不满足完全函数依赖,改为:

学生表

学生编号 学生名称 所在班级 班主任
S1 小王 计算机3班 陈老师
S2 小马 软件1班 李老师

课程表

课程编号 课程名称
C1 计算机导论
C2 数据结构

学生课程关系表

学生编号 课程编号
S1 C1
S1 C2
S2 C1

好处

  • 相对节约空间,当学生表和课程表属性越多,效果越明显
  • 解决插入异常,当新增一门课程时,原表因为没有学生选课,导致无法插入数据
  • 解决更新繁琐,当更改一门课程名称时,原表要更改多条数据
  • 解决删除异常,当学生学完一门课,原表若要清空学生上课信息,课程编号与课程名称的关系可能会丢失

第三范式(3NF)

不存在对非主键列的传递依赖。满足第二范式( 2NF的情况下任何非主属性不依赖于其它非主属性消除传递函数依赖。例如

学生表

学生编号 学生名称 班级编号 班级名字
S1 小王 001 计算机1班
S2 小马 003 计算机3班

学生编号作为主键满足第二范式2NF。通过学生编号 ⇒⇒ 班级编号 ⇒⇒ 班级名字,所以班级编号和班级名字之间存在依赖关系,改为:

学生表

学生编号 学生名称 班级编号
S1 小王 001
S2 小马 003

班级表

班级编号 班级名称
001 计算机1班
003 计算机3班

好处

  • 相对节约空间
  • 解决更新繁琐
  • 解决插入异常,当班级分配了老师,还没分配学生的时候,原表将不可插入数据
  • 解决删除异常,当学生毕业后,若要清空学生信息,班级和老师的关系可能会丢失

巴斯-科德范式BCNF

在3NF基础上消除主属性之间的传递函数依赖,即存在多个主属性时,主属性不依赖于主属性。例如:

配件表

仓库号 配件号 职工号 配件数量
W1 P1 E1 10
W1 P2 E1 10
W2 P1 E2 20

有以下约束:

  • 一个仓库有多个职工
  • 一个职工只在一个仓库
  • 一种配件可以放多个仓库
  • 一个仓库,一个职工管理多个配件,一种配件由唯一一个职工管理

由此,将(仓库号,配件号)作为主键,满足 3NF但是仓库号配件号⇒⇒ 职工号 ⇒⇒ 仓库号,造成传递函数依赖,改为:

仓库表

仓库号 职工号
W1 E1
W2 E2

工作表

职工号 配件号 配件数量
E1 P1 10
E1 P2 10
E2 P1 20

好处

  • 解决一些冗余和一些异常情况

不足

  • 丢失一些函数依赖,如丢失(仓库号,配件号)⇒⇒ 职工号,无法通过单表来确定一个职工号

第四范式4NF

在满足第三范式3NF的前提下表中不能包含一个实体的两个或多个互相独立的多值因子。

当一个表中非主属性相互独立时(即在 3NF 基础上),这些非主属性不应该有多值。即表中不能包含一个实体的两个或多个互相独立的多值因子。例如:

客户联系方式

客户编号 固定电话 移动电话
10 88-123 151
10 80-123 183

一个用户拥有多个固定电话和移动电话,给表的维护带来很多麻烦。比如增加一个固定电话,那么移动电话这一栏就较难维护,改为:

客户电话表

客户编号 电话号码 电话类型
10 88-123 固定电话
10 80-123 固定电话
10 151 移动电话
10 183 移动电话

好处

  • 解决一些异常,使表结构更加合理

第五范式5NF

在关系模式 R 中,每一个连接依赖均有 R 的候选码所隐含。即连接时,所连接的属性都是候选码,例如:

销售表

销售人员 供应商 产品
S1 V1 P1
S2 V2 P2
S1 V1 P1
S2 V2 P2

要想找到某一条数据,必须以(销售人员,供应商,产品)为主键,改为:

销售人员_供应商表

销售人员 供应商
S1 V1
S2 V2

销售人员_产品表

销售人员 产品
S1 P1
S2 P2

供应商_产品表

供应商 产品
V1 P1
V2 P2

好处

  • 解决某些异常操作

连接方式

内连接INNER JOIN

INNER JOIN 一般被译作内连接。内连接查询能将左表(表 A和右表表 B中能关联起来的数据连接后返回。

文氏图

内连接(INNER-JOIN)

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
INNER JOIN Table_B B
ON A.PK = B.PK;

查询结果

+------+------+---------+---------+
| A_PK | B_PK | A_Value | B_Value |
+------+------+---------+---------+
|    1 |    1 | both ab | both ab |
+------+------+---------+---------+
1 row in set (0.00 sec)

注意其中A为Table_A的别名B为Table_B的别名下同。

左连接LEFT JOIN

LEFT JOIN 一般被译作左连接,也写作 LEFT OUTER JOIN。左连接查询会返回左表表 A中所有记录不管右表表 B中有没有关联的数据。在右表中找到的关联数据列也会被一起返回。

文氏图

左连接(LEFT-JOIN)

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
LEFT JOIN Table_B B
ON A.PK = B.PK;

查询结果

+------+------+---------+---------+
| A_PK | B_PK | A_Value | B_Value |
+------+------+---------+---------+
|    1 |    1 | both ab | both ba |
|    2 | NULL | only a  | NULL    |
+------+------+---------+---------+
2 rows in set (0.00 sec)

右连接RIGHT JOIN

RIGHT JOIN 一般被译作右连接,也写作 RIGHT OUTER JOIN。右连接查询会返回右表表 B中所有记录不管左表表 A中有没有关联的数据。在左表中找到的关联数据列也会被一起返回。

文氏图

右连接(RIGHT-JOIN)

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
RIGHT JOIN Table_B B
ON A.PK = B.PK;

查询结果

+------+------+---------+---------+
| A_PK | B_PK | A_Value | B_Value |
+------+------+---------+---------+
|    1 |    1 | both ab | both ba |
| NULL |    3 | NULL    | only b  |
+------+------+---------+---------+
2 rows in set (0.00 sec)

全连接FULL OUTER JOIN

FULL OUTER JOIN 一般被译作外连接、全连接,实际查询语句中可以写作 FULL OUTER JOIN 或 FULL JOIN。外连接查询能返回左右表里的所有记录其中左右表里能关联起来的记录被连接后返回。MySQL不支持FULL OUTER JOIN

文氏图

全连接(FULL-OUTER-JOIN)

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
FULL OUTER JOIN Table_B B
ON A.PK = B.PK;

查询结果

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FULL OUTER JOIN Table_B B
ON A.PK = B.PK' at line 4

注意:我当前示例使用的 MySQL 不支持FULL OUTER JOIN。应当返回的结果使用UNION模拟

mysql> SELECT * 
    -> FROM Table_A
    -> LEFT JOIN Table_B 
    -> ON Table_A.PK = Table_B.PK
    -> UNION ALL
    -> SELECT *
    -> FROM Table_A
    -> RIGHT JOIN Table_B 
    -> ON Table_A.PK = Table_B.PK
    -> WHERE Table_A.PK IS NULL;
+------+---------+------+---------+
| PK   | Value   | PK   | Value   |
+------+---------+------+---------+
|    1 | both ab |    1 | both ba |
|    2 | only a  | NULL | NULL    |
| NULL | NULL    |    3 | only b  |
+------+---------+------+---------+
3 rows in set (0.00 sec)

SQL常见缩影

SQL常用JOIN

LEFT JOIN EXCLUDING INNER JOIN

返回左表有但右表没有关联数据的记录集。

文氏图

LEFT-JOIN-EXCLUDING-INNER-JOIN

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
LEFT JOIN Table_B B
ON A.PK = B.PK
WHERE B.PK IS NULL;

查询结果

+------+------+---------+---------+
| A_PK | B_PK | A_Value | B_Value |
+------+------+---------+---------+
|    2 | NULL | only a  | NULL    |
+------+------+---------+---------+
1 row in set (0.01 sec)

RIGHT JOIN EXCLUDING INNER JOIN

返回右表有但左表没有关联数据的记录集。

文氏图

RIGHT-JOIN-EXCLUDING-INNER-JOIN

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
RIGHT JOIN Table_B B
ON A.PK = B.PK
WHERE A.PK IS NULL;

查询结果

+------+------+---------+---------+
| A_PK | B_PK | A_Value | B_Value |
+------+------+---------+---------+
| NULL |    3 | NULL    | only b  |
+------+------+---------+---------+
1 row in set (0.00 sec)

FULL OUTER JOIN EXCLUDING INNER JOIN

返回左表和右表里没有相互关联的记录集。

文氏图

FULL-OUTER-JOIN-EXCLUDING-INNER-JOIN

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
FULL OUTER JOIN Table_B B
ON A.PK = B.PK
WHERE A.PK IS NULL
OR B.PK IS NULL;

因为使用到了 FULL OUTER JOINMySQL 在执行该查询时再次报错。

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FULL OUTER JOIN Table_B B
ON A.PK = B.PK
WHERE A.PK IS NULL
OR B.PK IS NULL' at line 4

应当返回的结果(用 UNION 模拟):

mysql> SELECT * 
    -> FROM Table_A
    -> LEFT JOIN Table_B
    -> ON Table_A.PK = Table_B.PK
    -> WHERE Table_B.PK IS NULL
    -> UNION ALL
    -> SELECT *
    -> FROM Table_A
    -> RIGHT JOIN Table_B
    -> ON Table_A.PK = Table_B.PK
    -> WHERE Table_A.PK IS NULL;
+------+--------+------+--------+
| PK   | Value  | PK   | Value  |
+------+--------+------+--------+
|    2 | only a | NULL | NULL   |
| NULL | NULL   |    3 | only b |
+------+--------+------+--------+
2 rows in set (0.00 sec)

SQL所有JOIN

SQL所有JOIN

CROSS JOIN

返回左表与右表之间符合条件的记录的迪卡尔集。

图示

CROSS-JOIN

示例查询

SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value
FROM Table_A A
CROSS JOIN Table_B B;

查询结果

+------+------+---------+---------+
| A_PK | B_PK | A_Value | B_Value |
+------+------+---------+---------+
|    1 |    1 | both ab | both ba |
|    2 |    1 | only a  | both ba |
|    1 |    3 | both ab | only b  |
|    2 |    3 | only a  | only b  |
+------+------+---------+---------+
4 rows in set (0.00 sec)

上面讲过的几种 JOIN 查询的结果都可以用 CROSS JOIN 加条件模拟出来,比如 INNER JOIN 对应 CROSS JOIN ... WHERE A.PK = B.PK。

SELF JOIN

返回表与自己连接后符合条件的记录,一般用在表里有一个字段是用主键作为外键的情况。比如 Table_C 的结构与数据如下:

+--------+----------+-------------+
| EMP_ID | EMP_NAME | EMP_SUPV_ID |
+--------+----------+-------------+
|   1001 | Ma       |        NULL |
|   1002 | Zhuang   |        1001 |
+--------+----------+-------------+
2 rows in set (0.00 sec)

EMP_ID 字段表示员工 IDEMP_NAME 字段表示员工姓名EMP_SUPV_ID 表示主管 ID。

示例查询

现在我们想查询所有有主管的员工及其对应的主管 ID 和姓名,就可以用 SELF JOIN 来实现。

SELECT A.EMP_ID AS EMP_ID, A.EMP_NAME AS EMP_NAME, 
    B.EMP_ID AS EMP_SUPV_ID, B.EMP_NAME AS EMP_SUPV_NAME
FROM Table_C A, Table_C B
WHERE A.EMP_SUPV_ID = B.EMP_ID;

查询结果

+--------+----------+-------------+---------------+
| EMP_ID | EMP_NAME | EMP_SUPV_ID | EMP_SUPV_NAME |
+--------+----------+-------------+---------------+
|   1002 | Zhuang   |        1001 | Ma            |
+--------+----------+-------------+---------------+
1 row in set (0.00 sec)

补充说明

  • 文中的图使用 Keynote 绘制
  • 个人的体会是 SQL 里的 JOIN 查询与数学里的求交集、并集等很像SQLite 不支持 RIGHT JOIN 和 FULL OUTER JOIN可以使用 LEFT JOIN 和 UNION 来达到相同的效果
  • MySQL 不支持 FULL OUTER JOIN可以使用 LEFT JOIN 和 UNION 来达到相同的效果

事务

什么叫事务?

事务是一系列对系统中数据进行访问与更新的操作组成的一个程序逻辑单元。即不可分割的许多基础数据库操作。

事务特性ACID

原子性Atomicity

事务包含的操作要么全部成功,要么全部失败回滚。

一致性Consistency

一个事务执行前后应该使数据库从一个一致性状态转换为另一个一致性状态。比方说假设A、B两个人共有5000元。那么无论A给B转多少钱转多少次总数仍然是5000没有改变。

隔离性Isolation

多个用户并发访问数据库时,比如操作同一张表,数据库为每一个用户开启的事务,不能被其他事务的操作干扰。多个并发事务间需要隔离。

持久性Durability

一旦一个事务被提交,对数据库中的数据改变是永久的,即使数据库系统故障,也不会丢失提交事务操作。

隔离级别

Oracle数据库中仅有Serializable串行化和Read Committed读已提交两种隔离方式默认选择读已提交的方式。不做隔离操作则会出现

  • 脏读:读到未提交更新的数据
  • 第一类丢失更新A事务撤销时把已经提交的B事务的更新数据覆盖了
  • 第二类丢失更新A事务提交时把已经提交的B事务的更新数据覆盖了
  • 不可重复读:读到已经提交更新的数据,但一个事务范围内两个相同的查询却返回了不同数据
  • 幻读 事物A在用一个表此时事物B在表中增加或删除了一条数据A发现多了/少了一条数据,即为幻读

InnoDB存储引擎下的四种隔离级别发生问题的可能性如下:

隔离级别 第一类丢失更新 第二类丢失更新 脏读 不可重复读 幻读
SERIALIZABLE (串行化) × × × × ×
REPEATABLE READ可重复读 × × × ×
READ COMMITTED (读已提交) × ×
READ UNCOMMITTED读未提交 ×

Serializable串行化

指一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。

特点:避免脏读、不可重复读、幻读

可序列化的数据库锁情况

  • 事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放
  • 事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放

Repeatable Read可重复读

由于提交读隔离级别会产生不可重复读的读现象所以比提交读更高一个级别的隔离级别就可以解决不可重复读的问题这种隔离级别就叫可重复读Repeatable reads

特点MYSQL默认选择为可重复读。避免脏读、不可重复读

可重复读的数据库锁情况

  • 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放

Read Committed读已提交

提交读Read committed也可以翻译成“读已提交”通过名字也可以分析出在一个事务修改数据过程中如果事务还没提交其他事务不能读该数据。

特点:避免脏读

提交读的数据库锁情况

  • 事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放

Read Committed隔离级别下的加锁分析

隔离级别的实现与锁机制密不可分所以需要引入锁的概念首先我们看下InnoDB存储引擎提供的两种标准的行级锁

  • 共享锁(S Lock)又称为读锁可以允许多个事务并发的读取同一资源互不干扰。即如果一个事务T对数据A加上共享锁后其他事务只能对A再加共享锁不能再加排他锁只能读数据不能修改数据
  • 排他锁(X Lock): 又称为写锁如果事务T对数据A加上排他锁后其他事务不能再对A加上任何类型的锁获取排他锁的事务既能读数据也能修改数据

注意 共享锁和排他锁是不相容的。

Read uncommitted读未提交

未提交读Read uncommitted是最低的隔离级别。通过名字咱们就可以知道在这种事务隔离级别下一个事务可以读到另外一个事务未提交的数据。

特点:最低级别,任何情况都无法保证

未提交读的数据库锁情况

  • 事务在读数据的时候并未对数据加锁
  • 事务在修改数据的时候只对数据增加行级共享锁

SpringBoot Transaction

查看 mysql 事务隔离级别:show variables like 'tx_iso%';

事务管理方式

在Spring中事务有两种实现方式分别是编程式事务管理和声明式事务管理两种方式。

  • 编程式事务管理 编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理spring推荐使用TransactionTemplate
  • 声明式事务管理 建立在AOP之上的。其本质是对方法前后进行拦截然后在目标方法开始之前创建或者加入一个事务在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理不需要入侵代码通过@Transactional就可以进行事务操作更快捷而且简单推荐使用

事务提交方式

默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。 对于正常的事务管理是一组相关的操作处于一个事务之中因此必须关闭数据库的自动提交模式。不过这个我们不用担心spring会将底层连接的自动提交特性设置为false。也就是在使用spring进行事物管理的时候spring会将是否自动提交设置为false等价于JDBC中的 connection.setAutoCommit(false);,在执行完之后在进行提交,connection.commit();

事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • TransactionDefinition.ISOLATION_DEFAULT这是默认值表示使用底层数据库的默认隔离级别。对大部分数据库而言通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读不可重复读和幻读因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别
  • TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值
  • TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读
  • TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别

事务传播行为

所谓事务的传播行为是指如果在开始当前事务之前一个事务上下文已经存在此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量

  • TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
  • TransactionDefinition.PROPAGATION_NESTED如果当前存在事务则创建一个事务作为当前事务的嵌套事务来运行如果当前没有事务则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

事务回滚规则

指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常然后依据规则决定是否回滚抛出异常的事务。 默认配置下spring只有在抛出的异常为运行时unchecked异常时才回滚该事务也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚)而抛出checked异常则不会导致事务回滚。 可以明确的配置在抛出那些异常时回滚事务包括checked异常。也可以明确定义那些异常抛出时不回滚事务。

事务常用配置

  • readOnly该属性用于设置当前事务是否为只读事务设置为true表示只读false则表示可读写默认值为false。例如@Transactional(readOnly=true)
  • rollbackFor 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
  • rollbackForClassName 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})
  • noRollbackFor:该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})
  • noRollbackForClassName:该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”})
  • propagation 该属性用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
  • isolation:该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置
  • timeout:该属性用于设置事务的超时秒数,默认值为-1表示永不超时

事物注意事项

  • 要根据实际的需求来决定是否要使用事物,最好是在编码之前就考虑好,不然到以后就难以维护
  • 如果使用了事物,请务必进行事物测试,因为很多情况下以为事物是生效的,但是实际上可能未生效
  • 事物@Transactional的使用要放再类的公共(public)方法中,需要注意的是在 protected、private 方法上使用 @Transactional 注解,它也不会报错(IDEA会有提示),但事务无效
  • 事物@Transactional是不会对该方法里面的子方法生效也就是你在公共方法A声明的事物@Transactional但是在A方法中有个子方法B和C其中方法B进行了数据操作但是该异常被B自己处理了这样的话事物是不会生效的反之B方法声明的事物@Transactional但是公共方法A却未声明事物的话也是不会生效的如果想事物生效需要将子方法的事务控制交给调用的方法在子方法中使用rollbackFor注解指定需要回滚的异常或者将异常抛出交给调用的方法处理。一句话就是在使用事物的异常由调用者进行处理
  • 事物@Transactional由spring控制的时候它会在抛出异常的时候进行回滚。如果自己使用catch捕获了处理了是不生效的如果想生效可以进行手动回滚或者在catch里面将异常抛出比如throw new RuntimeException();

失效场景

  • @Transactional 应用在非 public 修饰的方法上
  • 数据库引擎要不支持事务
  • 由于propagation 设置错误,导致注解失效
  • rollbackFor 设置错误,@Transactional 注解失效
  • 方法之间的互相调用也会导致@Transactional失效
  • 异常被你的 catch“吃了”导致@Transactional失效

select for update

for update是一种行级锁,又叫排它锁。一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行。

  • 修改sql:在 selectsql 尾部添加 for update。如:select * from job_info where id = 1 for update;
  • 启用事务:为 service 添加注解 @Transactional

只有当出现如下之一的条件,才会释放共享更新锁:

  1. 执行提交COMMIT语句
  2. 退出数据库LOG OFF
  3. 程序停止运行

假设有个表单products 里面有id 跟name 二个栏位id 是主键。

-- 例1: 明确指定主键并且有此数据row lock
SELECT * FROM products WHERE id='3' FOR UPDATE;
-- 例2: 明确指定主键若查无此数据无lock
SELECT * FROM products WHERE id='-1' FOR UPDATE;
-- 例2: 无主键table lock
SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
-- 例3: 主键不明确table lock
SELECT * FROM products WHERE id<>'3' FOR UPDATE;
-- 例4: 主键不明确table lock
SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;

注意

  • FOR UPDATE 仅适用于InnoDB且必须在事务区块(start sta/COMMIT)中才能生效
  • 要测试锁定的状况可以利用MySQL 的Command Mode ,开二个视窗来做测试

索引

建立索引的目的是加快对表中记录的查找或排序。索引只是提高效率的一个因素如果你的MySQL有大数据量的表就需要花时间研究建立最优秀的索引或优化查询语句。因此应该只为最经常查询和最经常排序的数据列建立索引。MySQL里同一个数据表里的索引总数限制为16个。

优点

  • 索引大大减小了服务器需要扫描的数据量
  • 索引可以帮助服务器避免排序和临时表
  • 索引可以将随机IO变成顺序IO
  • 索引对于InnoDB对索引支持行级锁非常重要因为它可以让查询锁更少的元组
  • 关于InnoDB、索引和锁InnoDB在二级索引上使用共享锁读锁但访问主键索引需要排他锁写锁

缺点

  • 虽然索引大大提高了查询速度同时却会降低更新表的速度如对表进行INSERT、UPDATE和DELETE。因为更新表时MySQL不仅要保存数据还要保存索引文件
  • 建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快
  • 如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果
  • 对于非常小的表,大部分情况下简单的全表扫描更高效

索引规范

  • 索引的数量要控制

    • 单张表 中索引数量不超过 5
    • 单个索引 中的字段数不超过 5字段超过5个时实际已经起不到有效过滤数据的作用了
    • 对字符串使⽤ 前缀索引,前缀索引长度不超过 8 个字符,必要时可添加伪列并建立索引
  • 禁止在 更新十分频繁区分度不高 的属性上建立索引

    • 更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能
    • “性别”这种区分度不大的属性,建立索引是没有意义的,其不能有效过滤数据,性能与全表扫描类似
  • 不在索引列进行 数学运算函数运算

  • 建立组合索引:必须 把区分度高的字段放在前面(能够更加有效的过滤数据)

  • 重要的SQL必须被索引比如

    • UPDATEDELETE 语句的 WHERE 条件列
    • ORDER BYGROUP BYDISTINCT 的字段
  • 多表JOIN 的字段注意以下(优化准则)

    • 区分度最大 的字段放在前面
    • 核⼼SQL优先考虑 覆盖索引
    • 避免 冗余重复 索引
    • 索引要综合评估数据 密度分布以及考虑 查询更新 比例
  • 索引命名

    • 索引名称必须 全部小写
    • 唯一所以必须以 uniq _ 字段1 _ 字段2 命名
    • 非唯一索引必须以 idx _ 字段1 _ 字段2 命名
  • 新建的 唯一索引 必须不能和主键重复

  • 索引字段的默认值不能为 NULL NULL非常影响索引的查询效率

  • 反复查看与表相关的SQL符合 最左前缀 的特点建立索引

    多条件字段重复的语句,要修改语句条件字段的顺序,为其建立一条 联合索引,减少索引数量

  • 优先使用唯一索引:能使用唯一索引就要使用唯一索引,提高查询效率

  • 研发要经常使用 explain如果发现索引选择性差必须让他们学会使用hint

索引结构

二叉树

特点

  1. 左子节点值 < 节点值
  2. 右子节点值 > 节点值
  3. 当数据量非常大时,要查找的数据又非常靠后,和没有索引相比,那么二叉树结构的查询优势将非常明显

存在问题

如下图,可以看出,二叉树出现单边增长时,二叉树变成了“链”,这样查找一个数的时候,速度并没有得到很大的优化。

索引结构-二叉树

红黑树

特点

  1. 节点是红色或者黑色
  2. 根节点是黑色
  3. 每个叶子的节点都是黑色的空节点NULL
  4. 每个红色节点的两个子节点都是黑色的
  5. 从任意节点到其每个叶子的所有路径都包含相同的黑色节点

索引结构-红黑树

存在的问题

红黑树虽然和二叉树相比,一定程度上缓解了单边过长的问题,但是它依旧存储高度问题。 

假设现在数据量有100万那么红黑树的高度大概为 100,0000 = 2^n n大概为 20。那么至少要20次的磁盘IO这样性能将很受影响。如果数据量更大IO次数更多性能损耗更大。所以红黑树依旧不是最佳方案。

思考:针对上面的红黑树结构,我们能否优化一下呢?

上述红黑树默认一个节点就存了一个 (索引+磁盘地址),我们设想一个节点存多个 (索引+磁盘地址),这样就可以降低红黑树的高度了。 实际上我们设想的这种结构就是 B-Tree

Hash

原理

  1. 事先将索引通过 hash算法后得到的hash值(即磁盘文件指针存到hash表中
  2. 在进行查询时将索引通过hash算法得到hash值与hash表中的hash值比对。通过磁盘文件指针只要一次磁盘IO就能找到要的值

例如在第一个表中要查找col=6的值。hash(6) 得到值比对hash表就能得到89。性能非常高。

存在问题

但是hash表索引存在问题如果要查询带范围的条件时hash索引就歇菜了。

select *from t where col1>=6;

B-Tree

特点

B-Tree索引能很好解决红黑树中遗留的高度问题B-Tree 是一种平衡的多路查找又称排序在文件系统中和数据库系统有所应用主要用作文件的索引其中的B就表示平衡Balance

为了描述B-Tree首先定义一条数据记录为一个二元组 [key, data]key为记录的键值key对于不同数据记录key是互不相同的data为数据记录除以key外的数据 (这里指的是聚集索引)。那么B-Tree是满足下列条件的数据结构

  1. d 为大于1的一个正整数称为BTree的度
  2. h为一个正整数称为BTree的高度
  3. key和指针互相间隔节点两端是指针
  4. 叶子节点具有相同的深度,叶子节点的指针为空,节点中数据索引(下图中的key)从左往右递增排列

说明:下图都是以主键索引为例,至于非主键索引(非聚集索引)无非就是data里存的内容不同。

索引结构-B-Tree指针

索引结构-B-Tree

分析

模拟下查找key为29的data的过程

  1. 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作第1次
  2. 磁盘块1存储1735和三个指针数据。我们发现17<29<35因此我们找到指针p2
  3. 根据p2指针我们定位并读取磁盘块3。【磁盘IO操作2次
  4. 磁盘块3存储2630和三个指针数据。我们发现26<29<30因此我们找到指针p2
  5. 根据p2指针我们定位并读取磁盘块8。【磁盘IO操作3次
  6. 磁盘块8中存储2829。我们找到29获取29所对应的数据data

存在问题

  1. 比如,下面查询语句,那么不但需要叶子节点>20的值也需要非叶子节点在右边节点的值。即下图画圈的两部分 B-Tree似乎在范围查找没有更简便的方法为了解决这一问题。我们可以用B+Tree。

    select *from t where col1 > 20; 
    

    索引结构-B-Tree

  2. 深度问题

    从图上可以看到每个节点中不仅包含数据的key值还有data值。而每一个节点的存储空间是有限的(mysql默认设置一个节点的大小为16K)如果data中存放的数据较大时将会导致每个节点即一个页能存储的key的数量索引的数量很小所以当数据量很多且每行数据量很大的时候同样会导致B-Tree的深度较大增大查询时的磁盘I/O次数进而影响查询效率。所以引入B+Tree

B+Tree

特点

B+Tree是在B-Tree基础上的一种优化使其更适合实现外存储索引结构。在B+Tree中所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上而非叶子节点上只存储key值信息这样可以大大加大每个节点存储的key值数量降低B+Tree的高度。

  1. 非叶子节点不存储data只存储索引可以存放更多索引
  2. 叶子节点不存储指针
  3. 顺序访问指针,提高区间访问性能
  4. 非叶子节点中的索引最终还是会在叶子节点上存储一份,也就是叶子节点会包含非叶子节点上的所有索引
  5. 一个父节点,它的左侧子节点都小于父节点的值,右侧的子节点都大于等于父节点的值
  6. 每一层节点从左往右都是递增排列,无论是数值型还是字符型

注意MySQL索引默认的存储结构使用的就是B+Tree。

索引结构-B+Tree指针

索引结构-B+Tree

剖析如上图在叶子节点上注意是MySQL已经有成双向箭头原生B+Tree是单向的而且从左到右是递增顺序的所以很好的解决了 > 和 < 这类查找问题。

分析

假如:以一个高度为3的B+Tree为例B+Tree的表都存满了能存储多少数据

**首先,**查看MySQL默认一个节点页的大小

SHOW GLOBAL STATUS like 'Innodb_page_size';

如下图大小为16K。

索引结构-B+Tree案例

然后假设主键Id为bigint类型那么长度就是8B指针在Innodb源码中大小为6B所以一共就是14B再假设最后一层存放的数据data为1k 大小(能存很多内容了),那么:

  1. 第一层最大节点数为: 16k / (8B + 6B) = 1170 (个)
  2. 第二层最大节点数也应为1170个
  3. 第三层最大节点数为16k / 1k = 16 (个)

一张B+Tree的表最多存放 1170 * 1170 * 16 = 21902400 ≈ 2千万。所以通过分析我们可以得出B+Tree结构的表可以容纳千万数据量的查询。而且一般来说MySQL会把 B+Tree 根节点放在内存中,那只需要**两次磁盘IO第二层1次第三层1次**就行。

扩展

数据库中的B+Tree索引可以分为聚集索引clustered index也叫主键索引和辅助索引secondary index也叫非聚集索引。上面的B+Tree示例图在数据库中的实现对应的是聚集索引聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据(除主键以外的所有数据)辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据而是存储相应行数据的对应的聚集索引键即主键。当通过辅助索引来查询数据时InnoDB存储引擎会遍历辅助索引找到主键然后再通过主键在聚集索引中找到完整的行记录数据。

索引类型

普通索引

普通索引(单列索引):单列索引是最基本的索引,它没有任何限制。

  • 直接创建索引
CREATE INDEX index_name ON table_name(col_name);
  • 修改表结构的方式添加索引
ALTER TABLE table_name ADD INDEX index_name(col_name);
  • 创建表的时候同时创建索引
CREATE TABLE `news` (
    `id` int(11) NOT NULL AUTO_INCREMENT ,
    `title` varchar(255)  NOT NULL ,
    `content` varchar(255)  NULL ,
    `time` varchar(20) NULL DEFAULT NULL ,
    PRIMARY KEY (`id`),
    INDEX index_name (title(255))
)
  • 删除索引
DROP INDEX index_name ON table_name;
# 或
alter table `表名` drop index 索引名;

复合索引

复合索引(组合索引):复合索引是在多个字段上创建的索引。复合索引遵守“最左前缀”原则****即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。

  • 创建一个复合索引
create index index_name on table_name(col_name1,col_name2,...);
  • 修改表结构的方式添加索引
alter table table_name add index index_name(col_name,col_name2,...);

唯一索引

唯一索引:唯一索引和普通索引类似,主要的区别在于,唯一索引限制列的值必须唯一,但允许存在空值(只允许存在一条空值)。如果在已经有数据的表上添加唯一性索引的话:

  • 如果添加索引的列的值存在两个或者两个以上的空值,则不能创建唯一性索引会失败。(一般在创建表的时候,要对自动设置唯一性索引,需要在字段上加上 not null
  • 如果添加索引的列的值存在两个或者两个以上null值还是可以创建唯一性索引只是后面创建的数据不能再插入null值 并且严格意义上此列并不是唯一的因为存在多个null值

对于多个字段创建唯一索引规定列值的组合必须唯一。比如在order表创建orderId字段和 productId字段 的唯一性索引,那么这两列的组合值必须唯一:

“空值” 和”NULL”的概念: 
1:空值是不占用空间的 .
2: MySQL中的NULL其实是占用空间的.

长度验证:注意空值的之间是没有空格的。

> select length(''),length(null),length(' ');
+------------+--------------+-------------+
| length('') | length(null) | length(' ') |
+------------+--------------+-------------+
|          0 |         NULL |           1 |
+------------+--------------+-------------+
  • 创建唯一索引
# 创建单个索引
CREATE UNIQUE INDEX index_name ON table_name(col_name);
# 创建多个索引
CREATE UNIQUE INDEX index_name on table_name(col_name,...);
  • 修改表结构
# 单个
ALTER TABLE table_name ADD UNIQUE index index_name(col_name);
# 多个
ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...);
  • 创建表的时候直接指定索引
CREATE TABLE `news` (
    `id` int(11) NOT NULL AUTO_INCREMENT ,
    `title` varchar(255)  NOT NULL ,
    `content` varchar(255)  NULL ,
    `time` varchar(20) NULL DEFAULT NULL ,
    PRIMARY KEY (`id`),
    UNIQUE index_name_unique(title)
)

主键索引

主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:

  • 主键索引(创建表时添加)
CREATE TABLE `news` (
    `id` int(11) NOT NULL AUTO_INCREMENT ,
    `title` varchar(255)  NOT NULL ,
    `content` varchar(255)  NULL ,
    `time` varchar(20) NULL DEFAULT NULL ,
    PRIMARY KEY (`id`)
)
  • 主键索引(创建表后添加)
alter table tbl_name add primary key(col_name);
CREATE TABLE `order` (
    `orderId` varchar(36) NOT NULL,
    `productId` varchar(36)  NOT NULL ,
    `time` varchar(20) NULL DEFAULT NULL
)
alter table `order` add primary key(`orderId`);

全文索引

在一般情况下,模糊查询都是通过 like 的方式进行查询。但是,对于海量数据,这并不是一个好办法,在 like "value%" 可以使用索引,但是对于 like "%value%" 这样的方式,执行全表查询,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情,所以 like 进行模糊匹配性能很差。

这种情况下,需要考虑使用全文搜索的方式进行优化。全文搜索在 MySQL 中是一个 FULLTEXT 类型索引。FULLTEXT 索引在 MySQL 5.6 版本之后支持 InnoDB而之前的版本只支持 MyISAM 表

全文索引主要用来查找文本中的关键字而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同它更像是一个搜索引擎而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用而不是一般的where语句加like。目前只有char、varchartext 列上可以创建全文索引。

小技巧 在数据量较大时候先将数据放入一个没有全局索引的表中然后再用CREATE index创建fulltext索引要比先为一张表建立fulltext然后再将数据写入的速度快很多。

  • 创建表的适合添加全文索引
CREATE TABLE `news` (
    `id` int(11) NOT NULL AUTO_INCREMENT ,
    `title` varchar(255)  NOT NULL ,
    `content` text  NOT NULL ,
    `time` varchar(20) NULL DEFAULT NULL ,
     PRIMARY KEY (`id`),
    FULLTEXT (content)
)
  • 修改表结构添加全文索引
ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(col_name)
  • 直接创建索引
CREATE FULLTEXT INDEX index_fulltext_content ON table_name(col_name)

注意 默认MySQL不支持中文全文检索MySQL 全文搜索只是一个临时方案,对于全文搜索场景,更专业的做法是使用全文搜索引擎,例如 ElasticSearch 或 Solr。

  • 索引的查询和删除
-- 查看:
show indexes from `表名`;
-- 或
show keys from `表名`;

-- 删除
alter table `表名` drop index 索引名;

注意MySQl的客户端工具也可以进索引的创建、查询和删除如 Navicat Premium!

索引规范

  • 【强制】业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。

    说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明 显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必 然有脏数据产生。

  • 【强制】超过三个表禁止join。join字段数据类型必须绝对一致多表关联查询时 保证被关联字段需要有索引。

    说明:即使双表 join 也要注意表索引、SQL 性能。

  • 强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度即可。

    说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分 度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度 来确定。

  • 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。

    说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索 引。

  • 【推荐】如果有 order by 的场景请注意利用索引的有序性。order by 最后的字段是组合 索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。

    正例where a=? and b=? order by c; 索引a_b_c

    反例索引中有范围查找那么索引有序性无法利用WHERE a>10 ORDER BY b; 索引 a_b 无法排序。

  • 【推荐】利用覆盖索引来进行查询操作,避免回表。

    说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览 一下就好,这个目录就是起到覆盖索引的作用。 正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查 询的一种效果,用 explain 的结果extra 列会出现using index。

  • 【推荐】利用延迟关联或者子查询优化超多分页场景。

    说明MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过 特定阈值的页数进行 SQL 改写。 正例:先快速定位需要获取的 id 段,然后再关联: SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id

  • 【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。

    说明:

    1consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据

    2ref 指的是使用普通的索引normal index

    3range 对索引进行范围检索

    反例explain 表的结果type=index索引物理文件全扫描速度非常慢这个 index 级 别比较 range 还低,与全表扫描是小巫见大巫。

  • 【推荐】建组合索引的时候,区分度最高的在最左边。

    正例:如果 where a=? and b=? ,如果 a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。

    说明存在非等号和等号混合时在建索引时请把等号条件的列前置。如where c>? and d=? 那么即使 c 的区分度更高,也必须把 d 放在索引的最前列,即索引 idx_d_c。

  • 【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。

  • 【参考】创建索引时避免有如下极端误解:

    • 认为一个查询就需要建一个索引
    • 认为索引会消耗空间、严重拖慢更新和新增速度
    • 认为业务的惟一性一律需要在应用层通过“先查后插”方式解决

SQL优化

SQL优化步骤

第1步通过慢查日志等定位那些执行效率较低的SQL语句

第2步explain分析SQL的执行计划

需要重点关注typerowsfilteredextra

  • type:由上至下,效率越来越高

    • ALL:全表扫描
    • index:索引全扫描
    • range:索引范围扫描,常用语<<=>=betweenin等操作
    • ref:使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中
    • eq_ref类似ref区别在于使用的是唯一索引使用主键的关联查询
    • const/system:单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询
    • nullMySQL不访问任何表或索引直接返回结果

    虽然上至下效率越来越高但是根据cost模型假设有两个索引idx1(a, b, c),idx2(a, c)SQL为select * from t where a = 1 and b in (1, 2) order by c;如果走idx1那么是type为range如果走idx2那么type是ref当需要扫描的行数使用idx2大约是idx1的5倍以上时会用idx1否则会用idx2

  • Extra

    • Using filesortMySQL需要额外的一次传递以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序并按排序顺序检索行。
    • Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化
    • Using index:表示相应的 select 操作中使用了覆盖索引Coveing Index,避免访问了表的数据行,效率不错!如果同时出现 using where意味着无法直接通过索引查找来查询到符合条件的数据。
    • Using index conditionMySQL5.6之后新增的ICPusing index condtion就是使用了ICP索引下推在存储引擎层进行数据过滤而不是在服务层过滤利用索引现有的数据减少回表的数据。

第3步show profile 分析

了解SQL执行的线程的状态及消耗的时间。默认是关闭的开启语句“set profiling = 1;”

SHOW PROFILES ;
SHOW PROFILE FOR QUERY  #{id};

第4步trace

trace分析优化器如何选择执行计划通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。

set optimizer_trace="enabled=on";
set optimizer_trace_max_mem_size=1000000;
select * from information_schema.optimizer_trace;

第5步确定问题并采用相应的措施

  • 优化索引
  • 优化SQL语句修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤
  • 改用其他实现方式ES、数仓等
  • 数据碎片处理

特殊需求

批量去重插入

问题MySQL批量插入如何不插入重复数据

解决方案1insert ignore into

当插入数据时如出现错误时如重复数据将不返回错误只以警告形式返回。所以使用ignore请确保语句本身没有问题否则也会被忽略掉。例如

INSERT IGNORE INTO user (name) VALUES ('telami') 

这种方法很简便,但是有一种可能,就是插入不是因为重复数据报错,而是因为其他原因报错的,也同样被忽略了~

解决方案2on duplicate key update

当primary或者unique重复时则执行update语句,如update后为无用语句,如id=id则同1功能相同但错误不会被忽略掉。在公众号顶级架构师后台回复“架构整洁”获取一份惊喜礼包。例如为了实现name重复的数据插入不报错可使用一下语句

INSERT INTO user (name) VALUES ('telami') ON duplicate KEY UPDATE id = id 

这种方法有个前提条件,就是,需要插入的约束,需要是主键或者唯一约束(在你的业务中那个要作为唯一的判断就将那个字段设置为唯一约束也就是unique key)。

解决方案3insert … select … where not exist

根据select的条件判断是否插入可以不光通过primaryunique来判断,也可通过其它条件。例如:

INSERT INTO user (name) SELECT 'telami' FROM dual WHERE NOT EXISTS (SELECT id FROM user WHERE id = 1) 

这种方法其实就是使用了MySQL的一个临时表的方式,但是里面使用到了子查询,效率也会有一点点影响,如果能使用上面的就不使用这个。

解决方案4replace into

如果存在primary or unique相同的记录,则先删除掉。再插入新记录。

REPLACE INTO user SELECT 1, 'telami' FROM books 

这种方法就是不管原来有没有相同的记录,都会先删除掉然后再插入。选择的是第二种方式

<insert id="batchSaveUser" parameterType="list">
    insert into user (id,username,mobile_number)
    values
    <foreach collection="list" item="item" index="index" separator=",">
        (
            #{item.id},
            #{item.username},
            #{item.mobileNumber}
        )
    </foreach>
    ON duplicate KEY UPDATE id = id
</insert>

这里用的是Mybatis批量插入的一个操作mobile_number已经加了唯一约束。这样在批量插入时,如果存在手机号相同的话,是不会再插入了的。

场景分析

案例1最左匹配

索引

KEY `idx_shopid_orderno` (`shop_id`,`order_no`)

SQL语句

select * from _t where orderno='xxx';

查询匹配从左往右匹配,要使用order_no走索引,必须查询条件携带shop_id或者索引(shop_id,order_no)调换前后顺序。

案例2隐式转换

索引

KEY `idx_mobile` (`mobile`)

SQL语句

select * from _user where mobile=12345678901;

隐式转换相当于在索引上做运算会让索引失效。mobile是字符类型使用了数字应该使用字符串匹配否则MySQL会用到隐式替换导致索引失效。

案例3大分页

索引

KEY `idx_a_b_c` (`a`, `b`, `c`)

SQL语句

select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10;

对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式:

  • 把上一次的最后一条数据也即上面的c传过来然后做“c < xxx”处理但是这种一般需要改接口协议并不一定可行
  • 采用延迟关联的方式进行处理减少SQL回表但是要记得索引需要完全覆盖才有效果SQL改动如下
SELECT t1.* FROM _t t1, (SELECT id FROM _t WHERE a=1 AND b=2 ORDER BY c DESC LIMIT 10000,10) t2  WHERE t1.id=t2.id;

案例4in+order by

索引

KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`)

SQL语句

SELECT * FROM _order WHERE shop_id = 1 AND order_status IN ( 1, 2, 3 ) ORDER BY created_at DESC LIMIT 10

in查询在MySQL底层是通过n*m的方式去搜索类似union但是效率比union高。in查询在进行cost代价计算时代价 = 元组数 * IO平均值是通过将in包含的数值一条条去查询获取元组数的因此这个计算过程会比较的慢所以MySQL设置了个临界值(eq_range_index_dive_limit)5.6之后超过这个临界值后该列的cost就不参与计算了。

因此会导致执行计划选择不准确。默认是200即in条件超过了200个数据会导致in的代价计算存在问题可能会导致Mysql选择的索引不准确。处理方式可以(order_status, created_at)互换前后顺序并且调整SQL为延迟关联。

案例5范围查询索引失效

范围查询阻断,后续字段不能走索引。

索引

KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`)

SQL语句

SELECT * FROM _order WHERE shop_id=1 AND created_at > '2021-01-01 00:00:00' AND order_status=10;

范围查询还有“IN、between”。

案例6避免使用非快速索引

不等于、不包含不能用到索引的快速搜索。

select * from _order where shop_id=1 and order_status not in (1,2);
select * from _order where shop_id=1 and order_status != 1;

在索引上,避免使用NOT!=<>!<!>NOT EXISTSNOT INNOT LIKE等。

案例7优化器选择索引失效

如果要求访问的数据量很小则优化器还是会选择辅助索引但是当访问的数据占整个表中数据的蛮大一部分时一般是20%左右),优化器会选择通过聚集索引来查找数据。

select * from _order where  order_status = 1

查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引。

案例8复杂查询

select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01';
select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10;

如果是统计某些数据可能改用数仓进行解决如果是业务上就有那么复杂的查询可能就不建议继续走SQL了而是采用其他的方式进行解决比如使用ES等进行解决。

案例9asc和desc混用

select * from _t where a=1 order by b desc, c asc

desc 和asc混用时会导致索引失效

案例10大数据

对于推送业务的数据存储可能数据量会很大如果在方案的选择上最终选择存储在MySQL上并且做7天等有效期的保存。那么需要注意频繁的清理数据会照成数据碎片需要联系DBA进行数据碎片处理。

存储引擎

InnoDB引擎

InnoDB 是一个事务安全的存储引擎它具备提交、回滚以及崩溃恢复的功能以保护用户数据。InnoDB 的行级别锁定保证数据一致性提升了它的多用户并发数以及性能。InnoDB 将用户数据存储在聚集索引中以减少基于主键的普通查询所带来的 I/O 开销。为了保证数据的完整性InnoDB 还支持外键约束。默认使用B+TREE数据结构存储索引。

特点

  • 支持事务支持4个事务隔离ACID级别
  • 行级锁定(更新时锁定当前行)
  • 读写阻塞与事务隔离级别相关
  • 既能缓存索引又能缓存数据
  • 支持外键
  • InnoDB更消耗资源读取速度没有MyISAM快
  • 在InnoDB中存在着缓冲管理通过缓冲池将索引和数据全部缓存起来加快查询的速度
  • 对于InnoDB类型的表其数据的物理组织形式是聚簇表。所有的数据按照主键来组织。数据和索引放在一块都位于B+数的叶子节点上

业务场景

  • 需要支持事务的场景(银行转账之类)
  • 适合高并发,行级锁定对高并发有很好的适应能力,但需要确保查询是通过索引完成的
  • 数据修改较频繁的业务

InnoDB引擎调优

  • 主键尽可能小否则会给Secondary index带来负担
  • 避免全表扫描,这会造成锁表
  • 尽可能缓存所有的索引和数据减少IO操作
  • 避免主键更新,这会造成大量的数据移动

MyISAM引擎

MyISAM既不支持事务、也不支持外键、其优势是访问速度快但是表级别的锁定限制了它在读写负载方面的性能因此它经常应用于只读或者以读为主的数据场景。默认使用B+TREE数据结构存储索引。

特点

  • 不支持事务
  • 表级锁定(更新时锁定整个表)
  • 读写互相阻塞(写入时阻塞读入、读时阻塞写入;但是读不会互相阻塞)
  • 只会缓存索引通过key_buffer_size缓存索引但是不会缓存数据
  • 不支持外键
  • 读取速度快

业务场景

  • 不需要支持事务的场景(像银行转账之类的不可行)
  • 一般读数据的较多的业务
  • 数据修改相对较少的业务
  • 数据一致性要求不是很高的业务

MyISAM引擎调优

  • 设置合适索引
  • 启用延迟写入,尽量一次大批量写入,而非频繁写入
  • 尽量顺序insert数据让数据写入到尾部减少阻塞
  • 降低并发数,高并发使用排队机制
  • MyISAM的count只有全表扫描比较高效带有其它条件都需要进行实际数据访问

MySQL原理

架构设计

MySQL架构设计

从上面的示意图可以看出MySQL从上到下包含了客户端、Server层和存储引擎层

  • 客户端可以是我们常用的MySQL命令行窗口或者是Java的客户端程序等
  • Server层连接器、查询缓存、分析器、优化器和执行器等。大部分MySQL对用户提供的功能都在这一层实现包括了内置函数的实现存储过程、触发器、视图等
  • 存储层存储引擎层负责数据的存储和提取存储引擎的实现是插件式的。也就是说用户可以选择自己所需要的存储引擎如InnoDB、MyISAM等

连接器

连接器是MySQL服务端对外的门户当我们使用命令行黑窗口或者JDBC的Connection.connect()连接到MySQL Server端时会校验用户名和密码然后会查询用户对应的权限列表。当连接建立后后续的权限范围就在此时确定了如果连接没有断开的情况下更改了用户的权限此时对于该连接也不生效。

查询缓存

当连接建立完成后执行select 语句的时候就会来到查询缓存。MySQL会将Select 语句为 KEY将查询结果为VALUE 的形式保存在内存中。如果匹配到对应的 KEY 就会直接从内存中返回结果。

但是常我们不会使用MySQL自身的查询缓存因为当有一条Update 或 Insert 的改表语句时,就会清空对该表的所有查询缓存。缓存的粒度比较大,可以考虑类似 Redis 的分布式缓存做业务数据的缓存。在MySQL 8.0 中,查询缓存直接被移除了。

分析器

如果在查询缓存中没有查到数据就要真正的开始执行SQL语句了。分析器首先会做“词法分析”。词法分析就是识别上面字符串id、name 是表的字段名T 是表的名称等等。之后就是语法分析如果SQL有语法错误在此时就会报错。

优化器

当分析器处理过之后MySQL就知道SQL 要干什么了但是此时还需要优化器对待执行的SQL 进行优化。当然MySQL 提供的优化器相比其他几款商用收费的数据库来说还是比较弱的。当然MySQL 的优化器还是可以对 join 操作,表达式计算等等进行优化,本篇不做过多的介绍。

执行器

执行阶段,首先会检查当前用户有没有权限操作该 SQL 语句。如果有,则继续执行后续的操作。

日志系统

生产优化

  • 在生产上,建议 innodb_flush_log_at_trx_commit 设置成 1可以让每次事务的 redo log 都持久化到磁盘上。保证异常重启后redo log 不丢失
  • 建议 sync_binlog 设置成 1可以让每次事务的 binlog 都持久化到磁盘上。保证异常重启后binlog 不丢失

IO性能优化

  • binlog_group_commit_sync_delay:表示延迟多少微秒后,再执行 fsync
  • binlog_group_commit_sync_no_delay_count:表示累计多少次后,在调用 fsync

MySQL 出现了 IO 的性能问题,可以考虑下面的优化策略:

  • 设置 binlog_group_commit_sync_delaybinlog_group_commit_sync_no_delay_count。可以使用故意等待来减少,binlog 的写盘次数,没有数据丢失的风险,但是会有客户端响应变慢的风险
  • 设置 sync_binlog 设置为 100~1000 之间的某个值。这样做存在的风险是可能造成 binlog 丢失
  • 设置 innodb_flush_log_at_trx_commit = 2,可能会丢数据

redo log(重做日志)

在MySQL里如果我们要执行一条更新语句。执行完成之后数据不会立马写入磁盘因为这样对磁盘IO的开销比较大。MySQL里面有一种叫做WALWrite-Ahead Logging就是先写日志在写磁盘。就是当有一条记录需要更新的时候InnoDB 会先写redo log 里面并更新内存这个时候更新的操作就算完成了。之后MySQL会在合适的时候将操作记录 flush 到磁盘上面。当然 flush 的条件可能是系统比较空闲,或者是 redo log 空间不足时。redo log 文件的大小是固定的比如可以是由4个1GB文件组成的集合。如下图所示

redolog位置指针

write pos 是当前要写入日志的位置当写到末尾时会重新到文件头部开始写入。checkpoint 是当前待擦除的位置,以此循环反复利用这 4GB 的空间。有了 redo log即时数据异常宕机重启时也不会丢失已经提交的数据这个能力叫做 crash-safe。

redo log写入流程

前面介绍过了 redo log 的写入首先会写入 redo log cache其详细的状态如下所示

redolog写入流程

redo log 对应上面的 3 种状态分别是:

  • 在 MySQL 应用的 redo log buffer 中
  • write 到文件系统的 page cache 中但是没有进行实际的写盘操作fsync
  • 执行 fsync 之后,写盘结束

InnoDB 有一个后台线程,每个 1 秒钟 就会将 redo log buffer 中的日志,调用 write 写入到 文件系统的 page cache 中,然后再调用 fsync 持久化到磁盘中。redo log buffer 是共享的,因此一些正在执行中的事务的 redo log 也有可能被持久化到磁盘中。

通常我们说的 MySQL 的 “双1” 操作,指的是 sync_binlog = 1 AND innodb_flush_log_at_trx_commit = 1innodb_flush_log_at_trx_commit 设置成 1 表示 redo log 在 prepare 阶段就需要持久化一次,那么 “双1” 配置 每个事务提交的时候都会刷盘 2 次,一次是 binlog,一次是 redo log

为了控制 redo log 的写入策略innodb_flush_log_at_trx_commit 会有下面 3 中取值:

  • 0每次提交事务只写在 redo log buffer 中
  • 1每次提交事务持久化到磁盘
  • 2每次提交事务写到 文件系统的 page cache 中

redo log 实际的触发 fsync 操作写盘包含以下几个场景:

  • 后台每隔 1 秒钟的线程轮询
  • innodb_flush_log_at_trx_commit 设置成 1 时,事务提交时触发
  • innodb_log_buffer_size 是设置 redo log 大小的参数。当 redo log buffer 达到 innodb_log_buffer_size / 2 时,也会触发一次 fsync

binlog(归档日志)

通过MySQL的架构可以看出MySQL服务端主要分为2大块Server层 和 引擎层。redo log 本身是 InnoDB所特有的日志而Server 层也有自己的日志那就是binlog。至于为什么会有两种日志这就是历史原因了。最开始MySQL原生的存储引擎是MyISAM。它本身不支持事务的特性而InnoDB 是另外一家公司以插件的形式开发的,为了支持事务等特性,引入了 redo log。两者主要有以下区别

binlog和redolog区别

结合到update 语句中对ID=2的这一行的c 值进行更改,执行流程如下所示:

update的binlog执行流程

从上面的执行流程可以看出对于redo log 的写入拆成了 2 个步骤,这就是两阶段提交

binlog写入流程

事务执行过程中binlog 首先会被写到 binlog cache 中事务提交的时候再讲binlog cache 写到 binlog 文件中。一个事务的 binlog 是原子的,无论多大都需要保证完整性。

系统为每个客户端线程分配一个 binlog cache其大小由 binlog_cache_size 控制。如果binlog cache 超过阀值,就会临时持久化到磁盘。当事务提交的时候,再将 binlog cache 中完整的事务持久化到磁盘中,并清空 binlog cache。

binlog写入流程

从上面可以看出,每个客户端线程都有自己独立的 binlog cache但是会共享一份 binlog files。上面的 write 是指把binlog cache 写到文件系统的 page cache并没有写入到磁盘中因此速度较快。fsync 是实际的写盘操作,占用磁盘的 IOPS。write 和 fsync 的写入时机是由sync_binlog 控制的:

  • sync_binlog=0每次事务提交都只 write不 fsync
  • sync_binlog=1每次事务提交都会fsync
  • sync_binlog=NN>1每次提交事务都会 write累计N 个后再执行 fsync

在出现 IO 瓶颈的情况下,可以考虑将 sync_binlog 设置成一个大的值。比较常见的是将 N设置为 100~1000。但是存在的风险是当主机异常重启时会丢失 N 个最近提交的事务 binlog。

查询过程

MySQL查询过程

全局锁表锁&行锁

全局锁

FTWRL

全局锁就是对整个数据库实例加锁MySQL 提供了 flush tables with read lock (FTWRL) 的方式去加全局锁。当你需要让整个库处于只读状态的时候,就可以使用这个命令了,之后所有线程的更改操作都会被阻塞。

mysqldump

mysqldump 是官方提供的备份工具,可以通过 --single-transaction 参数来启用可重复读隔离级别,从而可以拿到一个一致性视图。

set global readonly = true

通过上述命令可以让全库进入只读状态,但是在开发当中,事务框架往往会利用这个参数来处理读写分离。所以通常情况下,还是不建议使用这种方式。

表级锁

MySQL 的表级锁有 2 种:表锁和元数据锁。

表锁

表锁可以使用 lock tables T read/write , 可以使用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放锁。

MDL (metadata lock)

MDL 没有显示的命令当执行改表语句时MDL 会保证读写的正确性。MySQL 在 5.5 版本以后引入了 MDL 锁,当对一个表做增删改查的时候,加 MDL 读锁;当要多表结构做变更的时候,加 MDL 写锁。

  • MDL 读锁之间不互斥,因此可以有多个线程同时对一张表 增删改查
  • MDL 读-写、写-写之间是互斥的,因此如果同时有 2 个线程给表加字段,则需要顺序执行

行锁

两阶段锁

当使用update 更新数据时,会对 where 条件扫描到的行加行锁。在 InnoDB 事务中,行锁是在需要的时候才加上的,并且在事务提交后释放的,这就是两阶段锁协议。所以我们在更新数据时,应尽量把容易产生并发更新的行放在事务末端执行。

死锁

在事务对不同行加锁的时候,就很有可能出现死锁的情况。

MySQL 有 2 中策略去解决死锁:

  • 超时等待:可以通过 innodb_lock_wait_timeout 来设置,默认 50S。一般不采用因为 50S 对应用来说是不可接受的,并且这个值的设置也没有合适的估算值)
  • 死锁检测:发现死锁后,主动回滚其中一个事务。可以通过 innodb_deadlock_detect 设置为 on

上面 2 种死锁的解决方法都是MySQL 本身提供的。我们实际开发的过程当中,往往是需要自己从业务的角度去考虑,如何规避死锁和解决死锁的问题:

  • 按规则加锁:如 A 转账给 B同时 B 也转账给 A此时就很可能出现死锁。但是如果我们根据 userId 的升序规则去加锁,就不会产生死锁的问题了
  • 控制并发度:如支付系统中的账户系统,可以将总账户拆分成子账户,然后每个子账户是一个独立的锁实体

mysql复制原理

基于语句的复制

基于语句的复制模式下主库会记录那些造成数据更改的查询当备库读取并重放这些事件时实际上只把主库上执行过的SQL再执行一遍。

  • 优点

    最明显的好处是实现相当简单。理论上讲简单地记录和执行这些语句能够让备库保持同步。另外好处是binlog日志里的事件更加紧凑所以相对而言基于语句的模式不会使用太多带宽。一条更新好几兆数据的语句在二进制日志里可能只占用几十字节。

  • 缺点

    有些数据更新语句可能依赖其他因素。例如同一条sql在主库和备库上执行的时间可能稍微或很不相同因此在传输的binlog日志中除了查询语句还包括一些元数据信息如当前的时间戳。即便如此还存在着一些无法被正确复制的SQL例如使用CURRENT_USER()函数语句。存储过程和触发器在使用基于语句的复制模式时也可能存在问题。另外一个问题是更新必须是串行的。这需要更多的锁。并且不是所有的存储引擎都支持这种复制模式。

基于行的复制

MySQL5.1开始支持基于行的复制,这种方式会将实际数据记录在二进制日志中,跟其他数据库的实现比较相像。

  • 优点

    最大的好处是可以正确的复制每一行,一些语句可以呗更加有效地复制。由于无需重放更新主库数据的查询,使用基于行的复制模式能够更高效地复制数据。重放一些查询的代价会很高

  • 缺点

    全表更行,使用基于行复制开销会大很多,因为每一行数据都会呗记录到二进制日志中,这使得二进制日志时间非常庞大

高可用方案

我们在考虑MySQL数据库的高可用架构时主要考虑如下几方面

  • 如果数据库发生了宕机或者意外中断等故障,能尽快恢复数据库的可用性,尽可能的减少停机时间,保证业务不会因为数据库的故障而中断
  • 用作备份、只读副本等功能的非主节点的数据应该和主节点的数据实时或者最终保持一致
  • 当业务发生数据库切换时,切换前后的数据库内容应当一致,不会因为数据缺失或者数据不一致而影响业务

关于对高可用的分级我们暂不做详细的讨论,这里只讨论常用高可用方案的优缺点以及选型。

随着人们对数据一致性要求不断的提高越来越多的方法被尝试用来解决分布式数据一致性的问题如MySQL自身的优化、MySQL集群架构的优化、Paxos、Raft、2PC算法的引入等。

而使用分布式算法用来解决MySQL数据库数据一致性问题的方法也越来越被人们所接受一系列成熟的产品如PhxSQL、MariaDB Galera Cluster、Percona XtraDB Cluster等越来越多的被大规模使用。

随着官方MySQL Group Replication的GA使用分布式协议来解决数据一致性问题已经成为了主流的方向。期望越来越多优秀的解决方案被提出MySQL高可用问题也可以被更好的解决。

主从或主主半同步复制

使用双节点数据库搭建单向或者双向的半同步复制。在5.7以后的版本中由于lossless replication、logical多线程复制等一些列新特性的引入使得MySQL原生半同步复制更加可靠。常见架构如下

主从或主主半同步复制

通常会和Proxy、Keepalived等第三方软件同时使用即可以用来监控数据库的健康又可以执行一系列管理命令。如果主库发生故障切换到备库后仍然可以继续使用数据库。

优点

  • 架构比较简单,使用原生半同步复制作为数据同步的依据
  • 双节点,没有主机宕机后的选主问题,直接切换即可
  • 双节点,需求资源少,部署简单

缺点

  • 完全依赖于半同步复制,如果半同步复制退化为异步复制,数据一致性无法得到保证
  • 需要额外考虑HAProxy、Keepalived的高可用机制

半同步复制优化

半同步复制机制是可靠的。如果半同步复制一直是生效的,那么可以认为数据是一致的。但是由于网络波动等一些客观原因,导致半同步复制发生超时而切换为异步复制,这时便不能保证数据的一致性。所以尽可能的保证半同步复制,就可以提高数据的一致性。

该方案同样使用双节点架构,但是在原有半同复制的基础上做了功能上的优化,使半同步复制的机制变得更加可靠。可参考的优化方案如下:

双通道复制

双通道复制

半同步复制由于发生超时后,复制断开,当再次建立起复制时,同时建立两条通道,其中一条半同步复制通道从当前位置开始复制,保证从机知道当前主机执行的进度。另外一条异步复制通道开始追补从机落后的数据。当异步复制通道追赶到半同步复制的起始位置时,恢复半同步复制。

binlog文件服务器

binlog文件服务器

搭建两条半同步复制通道,其中连接文件服务器的半同步通道正常情况下不启用,当主从的半同步复制发生网络问题退化后,启动与文件服务器的半同步复制通道。当主从半同步复制恢复后,关闭与文件服务器的半同步复制通道。

优点

  • 双节点,需求资源少,部署简单
  • 架构简单,没有选主的问题,直接切换即可
  • 相比于原生复制,优化后的半同步复制更能保证数据的一致性

缺点

  • 需要修改内核源码或者使用MySQL通信协议。需要对源码有一定的了解并能做一定程度的二次开发
  • 依旧依赖于半同步复制,没有从根本上解决数据一致性问题

高可用架构优化

将双节点数据库扩展到多节点数据库,或者多节点数据库集群。可以根据自己的需要选择一主两从、一主多从或者多主多从的集群。由于半同步复制,存在接收到一个从机的成功应答即认为半同步复制成功的特性,所以多从半同步复制的可靠性要优于单从半同步复制的可靠性。并且多节点同时宕机的几率也要小于单节点宕机的几率,所以多节点架构在一定程度上可以认为高可用性是好于双节点架构。

但由于数据库数量较多所以需要数据库管理软件来保证数据库的可维护性。可以选择MMM、MHA或者各个版本的Proxy等等。常见方案如下

MHA+多节点集群

MHA+多节点集群

MHA Manager会定时探测集群中的master节点当master出现故障时它可以自动将最新数据的slave提升为新的master然后将所有其他的slave重新指向新的master整个故障转移过程对应用程序完全透明。MHA Node运行在每台MySQL服务器上主要作用是切换时处理二进制日志确保切换尽量少丢数据。MHA也可以扩展到如下的多节点集群

MHA-Manager

优点

  • 可以进行故障的自动检测和转移
  • 可扩展性较好可以根据需要扩展MySQL的节点数量和结构
  • 相比于双节点的MySQL复制三节点/多节点的MySQL发生不可用的概率更低

缺点

  • 至少需要三节点,相对于双节点需要更多的资源
  • 逻辑较为复杂,发生故障后排查问题,定位问题更加困难
  • 数据一致性仍然靠原生半同步复制保证,仍然存在数据不一致的风险
  • 可能因为网络分区发生脑裂现象。
  • 在此我向大家推荐一个架构学习交流群。交流学习群号575745314 里面会分享一些资深架构师录制的视频录像有SpringMyBatisNetty源码分析高并发、高性能、分布式、微服务架构的原理JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源目前受益良多

ZooKeeper+Proxy

ZooKeeper使用分布式算法保证集群数据的一致性使用ZooKeeper可以有效的保证Proxy的高可用性可以较好地避免网络分区现象的产生。

ZooKeeper+Proxy

优点

  • 较好的保证了整个系统的高可用性包括Proxy、MySQL
  • 扩展性较好,可以扩展为大规模集群

缺点

  • 数据一致性仍然依赖于原生的mysql半同步复制
  • 引入ZK整个系统的逻辑变得更加复杂

共享存储

共享存储实现了数据库服务器和存储设备的解耦不同数据库之间的数据同步不再依赖于MySQL的原生复制功能而是通过磁盘数据同步的手段来保证数据的一致性。

SAN共享储存

SAN的概念是允许存储设备和处理器服务器之间建立直接的高速网络与LAN相比连接通过这种连接实现数据的集中式存储。常用架构如下

SAN共享储存

使用共享存储时MySQL服务器能够正常挂载文件系统并操作如果主库发生宕机备库可以挂载相同的文件系统保证主库和备库使用相同的数据。

优点

  • 两节点即可,部署简单,切换逻辑简单
  • 很好的保证数据的强一致性
  • 不会因为MySQL的逻辑错误发生数据不一致的情况

缺点

  • 需要考虑共享存储的高可用
  • 价格昂贵

DRBD磁盘复制

DRBD是一种基于软件、基于网络的块复制存储解决方案主要用于对服务器之间的磁盘、分区、逻辑卷等进行数据镜像当用户将数据写入本地磁盘时还会将数据发送到网络中另一台主机的磁盘上这样的本地主机(主节点)与远程主机(备节点)的数据就可以保证实时同步。常用架构如下:

DRBD磁盘复制

当本地主机出现问题远程主机上还保留着一份相同的数据可以继续使用保证了数据的安全。DRBD是Linux内核模块实现的快级别的同步复制技术可以与SAN达到相同的共享存储效果。

优点

  • 两节点即可,部署简单,切换逻辑简单
  • 相比于SAN储存网络价格低廉
  • 保证数据的强一致性

缺点

  • 对IO性能影响较大
  • 从库不提供读操作

分布式协议

分布式协议可以很好地解决数据一致性问题。比较常见的方案如下:

MySQL Cluster

MySQL Cluster是官方集群的部署方案通过使用NDB存储引擎实时备份冗余数据实现数据库的高可用性和数据一致性。

MySQL-Cluster

优点

  • 全部使用官方组件,不依赖于第三方软件
  • 可以实现数据的强一致性

缺点

  • 国内使用的较少
  • 配置较复杂需要使用NDB储存引擎与MySQL常规引擎存在一定差异
  • 至少三节点

Galera

基于Galera的MySQL高可用集群 是多主数据同步的MySQL集群解决方案使用简单没有单点故障可用性高。常见架构如下

MySQL-Galera

优点

  • 多主写入,无延迟复制,能保证数据强一致性
  • 有成熟的社区,有互联网公司在大规模的使用
  • 自动故障转移,自动添加、剔除节点

缺点

  • 需要为原生MySQL节点打wsrep补丁
  • 只支持innodb储存引擎
  • 至少三节点

Paxos

Paxos算法解决的问题是一个分布式系统如何就某个值决议达成一致。这个算法被认为是同类算法中最有效的。Paxos与MySQL相结合可以实现在分布式的MySQL数据的强一致性。常见架构如下

MySQL-Paxos

优点

  • 多主写入,无延迟复制,能保证数据强一致性
  • 有成熟理论基础
  • 自动故障转移,自动添加、剔除节点

缺点

  • 只支持InnoDB储存引擎
  • 至少三节点
  • 在此我向大家推荐一个架构学习交流群。交流学习群号575745314 里面会分享一些资深架构师录制的视频录像有SpringMyBatisNetty源码分析高并发、高性能、分布式、微服务架构的原理JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源目前受益良多

主从延迟

在实际的生产环境中由单台MySQL作为独立的数据库是完全不能满足实际需求的无论是在安全性高可用性以及高并发等各个方面。因此一般来说都是通过集群主从复制Master-Slave的方式来同步数据再通过读写分离MySQL-Proxy来提升数据库的并发负载能力进行部署与实施。总结MySQL主从集群带来的作用是

  • 提高数据库负载能力,主库执行读写任务(增删改),备库仅做查询
  • 提高系统读写性能、可扩展性和高可用性
  • 数据备份与容灾,备库在异地,主库不存在了,备库可以立即接管,无须恢复时间

MySQL主从集群

biglog

binlog是什么有什么作用

用于记录数据库执行的写入性操作(不包括查询)信息以二进制的形式保存在磁盘中。可以简单理解为记录的就是sql语句。binlog 是 mysql 的逻辑日志,并且由 Server层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志。在实际应用中, binlog 的主要使用场景有两个:

  • 用于主从复制在主从结构中binlog 作为操作记录从 master 被发送到 slaveslave服务器从 master 接收到的日志保存到 relay log 中
  • 用于数据备份在数据库备份文件生成后binlog保存了数据库备份后的详细信息以便下一次备份能从备份点开始

日志格式

binlog日志有三种格式分别为STATMENT 、 ROW 和 MIXED。在 MySQL 5.7.7 之前,默认的格式是 STATEMENT MySQL 5.7.7 之后,默认值是 ROW。日志格式通过 binlog-format 指定。

  • STATMENT :基于 SQL 语句的复制每一条会修改数据的sql语句会记录到 binlog 中
  • ROW :基于行的复制
  • MIXED :基于 STATMENT 和 ROW 两种模式的混合复制,比如一般的数据操作使用 row 格式保存,有些表结构的变更语句,使用 statement 来记录

我们还可以通过mysql提供的查看工具mysqlbinlog查看文件中的内容例如

mysqlbinlog mysql-bin.00001 | more

binlog文件大小和个数会不断的增加后缀名会按序号递增例如mysql-bin.00002等。

主从复制原理

主从复制原理

可以看到mysql主从复制需要三个线程masterbinlog dump thread、slaveI/O thread 、SQL thread

  • binlog dump线程 主库中有数据更新时根据设置的binlog格式将更新的事件类型写入到主库的binlog文件中并创建log dump线程通知slave有数据更新。当I/O线程请求日志内容时将此时的binlog名称和当前更新的位置同时传给slave的I/O线程
  • I/O线程 该线程会连接到master向log dump线程请求一份指定binlog文件位置的副本并将请求回来的binlog存到本地的relay log中
  • SQL线程 该线程检测到relay log有更新后会读取并在本地做redo操作将发生在主库的事件在本地重新执行一遍来保证主从数据同步

基本过程总结

  • 主库写入数据并且生成binlog文件。该过程中MySQL将事务串行的写入二进制日志即使事务中的语句都是交叉执行的
  • 在事件写入二进制日志完成后master通知存储引擎提交事务
  • 从库服务器上的IO线程连接Master服务器请求从执行binlog日志文件中的指定位置开始读取binlog至从库
  • 主库接收到从库IO线程请求后其上复制的IO线程会根据Slave的请求信息分批读取binlog文件然后返回给从库的IO线程
  • Slave服务器的IO线程获取到Master服务器上IO线程发送的日志内容、日志文件及位置点后会将binlog日志内容依次写到Slave端自身的Relay Log即中继日志文件的最末端并将新的binlog文件名和位置记录到master-info文件中以便下一次读取master端新binlog日志时能告诉Master服务器从新binlog日志的指定文件及位置开始读取新的binlog日志内容
  • 从库服务器的SQL线程会实时监测到本地Relay Log中新增了日志内容然后把RelayLog中的日志翻译成SQL并且按照顺序执行SQL来更新从库的数据
  • 从库在relay-log.info中记录当前应用中继日志的文件名和位置点以便下一次数据复制

并行复制

在MySQL 5.6版本之前Slave服务器上有两个线程I/O线程和SQL线程。I/O线程负责接收二进制日志SQL线程进行回放二进制日志。如果在MySQL 5.6版本开启并行复制功能那么SQL线程就变为了coordinator线程coordinator线程主要负责以前两部分的内容

并行复制

上图的红色框框部分就是实现并行复制的关键所在。这意味着coordinator线程并不是仅将日志发送给worker线程自己也可以回放日志但是所有可以并行的操作交付由worker线程完成。coordinator线程与worker是典型的生产者与消费者模型。

coordinator线程

不过到MySQL 5.7才可称为真正的并行复制这其中最为主要的原因就是slave服务器的回放与主机是一致的即master服务器上是怎么并行执行的slave上就怎样进行并行回放。不再有库的并行复制限制对于二进制日志格式也无特殊的要求。为了兼容MySQL 5.6基于库的并行复制5.7引入了新的变量slave-parallel-type,其可以配置的值有:

  • DATABASE:默认值,基于库的并行复制方式
  • LOGICAL_CLOCK:基于组提交的并行复制方式

按库并行

每个 worker 线程对应一个 hash 表用于保存当前正在这个worker的执行队列里的事务所涉及到的库。其中hash表里的key是数据库名用于决定分发策略。该策略的优点是构建hash值快只需要库名同时对于binlog的格式没有要求。但这个策略的效果只有在主库上存在多个DB且各个DB的压力均衡的情况下这个策略效果好。因此对于主库上的表都放在同一个DB或者不同DB的热点不同则起不到多大效果

按库并行

组提交优化

该特性如下:

  • 能够同一组里提交的事务,定不会修改同一行
  • 主库上可以并行执行的事务,从库上也一定可以并行执行

具体是如何实现的:

  • 在同一组里面一起提交的事务,会有一个相同的commit_id,下一组为commit_id+1,该commit_id会直接写到binlog中
  • 在从库使用时,相同commit_id的事务会被分发到多个worker并行执行直到这一组相同的commit_id执行结束后coordinator再取下一批

主从延迟

根据主从复制的原理可以看出,两者之间是存在一定时间的数据不一致,也就是所谓的主从延迟。导致主从延迟的时间点:

  • 主库 A 执行完成一个事务,写入 binlog该时刻记为T1
  • 传给从库B从库接受完这个binlog的时刻记为T2
  • 从库B执行完这个事务该时刻记为T3

那么所谓主从延迟就是同一个事务从库执行完成的时间和主库执行完成的时间之间的差值即T3-T1。我们也可以通过在从库执行show slave status,返回结果会显示seconds_behind_master,表示当前从库延迟了多少秒。

seconds_behind_master如何计算的

  • 每一个事务的binlog都有一个时间字段用于记录主库上写入的时间
  • 从库取出当前正在执行的事务的时间字段,跟当前系统的时间进行相减,得到的就是seconds_behind_master也就是前面所描述的T3-T1

为什么会主从延迟?

正常情况下如果网络不延迟那么日志从主库传给从库的时间是相当短所以T2-T1可以基本忽略。最直接的影响就是从库消费中转日志relaylog的时间段而造成原因一般是以下几种

  • 从库的机器性能比主库要差

    比如将20台主库放在4台机器把从库放在一台机器。这个时候进行更新操作由于更新时会触发大量读操作导致从库机器上的多个从库争夺资源导致主从延迟。不过目前大部分部署都是采取主从使用相同规格的机器部署

  • 从库的压力大

    按照正常的策略读写分离主库提供写能力从库提供读能力。将进行大量查询放在从库上结果导致从库上耗费了大量的CPU资源进而影响了同步速度造成主从延迟。对于这种情况可以通过一主多从分担读压力也可以采取binlog输出到外部系统比如Hadoop让外部系统提供查询能力

  • 大事务的执行

    一旦执行大事务那么主库必须要等到事务完成之后才会写入binlog。比如主库执行了一条insert … select非常大的插入操作该操作产生了近几百G的binlog文件传输到只读节点进而导致了只读节点出现应用binlog延迟。因此DBA经常会提醒开发不要一次性地试用delete语句删除大量数据尽可能控制数量分批进行

  • 主库的DDL(alter、drop、create)

    • 只读节点与主库的DDL同步是串行进行如果DDL操作在主库执行时间很长那么从库也会消耗同样的时间比如在主库对一张500W的表添加一个字段耗费了10分钟那么从节点上也会耗费10分钟
    • 从节点上有一个执行时间非常长的的查询正在执行那么这个查询会堵塞来自主库的DDL表被锁直到查询结束为止进而导致了从节点的数据延迟
  • 锁冲突

    锁冲突问题也可能导致从节点的SQL线程执行慢比如从机上有一些select .... for update的SQL或者使用了MyISAM引擎等

  • 从库的复制能力

    一般场景中,因偶然情况导致从库延迟了几分钟,都会在从库恢复之后追上主库。但若是从库执行速度低于主库,且主库持续具有压力,就会导致长时间主从延迟,很有可能就是从库复制能力的问题。

从库上的执行,即sql_thread更新逻辑在5.6版本之前是只支持单线程那么在主库并发高、TPS高时就会出现较大的主从延迟。因此MySQL自5.7版本后就已经支持并行复制了。可以在从服务上设置 slave_parallel_workers为一个大于0的数然后把slave_parallel_type参数设置为LOGICAL_CLOCK,这就可以了

mysql> show variables like 'slave_parallel%';
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| slave_parallel_type    | DATABASE |
| slave_parallel_workers | 0        |
+------------------------+----------+

怎么减少主从延迟?

主从同步问题永远都是一致性和性能的权衡,得看实际的应用场景,若想要减少主从延迟的时间,可以采取下面的办法:

  • 降低多线程大事务并发的概率,优化业务逻辑
  • 优化SQL避免慢SQL减少批量操作建议写脚本以update-sleep这样的形式完成
  • 提高从库机器的配置减少主库写binlog和从库读binlog的效率差
  • 尽量采用短的链路也就是主库和从库服务器的距离尽量要短提升端口带宽减少binlog传输的网络延时
  • 实时性要求的业务读强制走主库,从库只做灾备,备份