目录导航

 

前言

性能优化专题共计四个部分,分别是:

本节是性能优化专题第二部分 —— MySql 性能优化篇,共计四个小节,分别是:

  1. MySql索引机制
  2. MySql运行机理
  3. 深入理解InnoDB
  4. MySql调优

MySql事务

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。

​ 事务的ACID特性,事务并发带来了哪些特性,事务的四种隔离级别。

事务

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。

  • 典型事务场景(转账):
update user_account set balance = balance - 1000 where userID = 3;
update user_account set balance = balance +1000 where userID = 1;

mysql中如何开启事务

  • SQL编程
  1. 手工
begin / start transaction 
# 开启事务
commit / rollback
# 事务提交或回滚
  1. 自动
set session autocommit = on/off;
# 设定事务是否自动开启
  • JDBC 编程
connection.setAutoCommit(boolean);
  • Spring 事务AOP编程
expression=execution(com.xxx.service.*.*(..))

事务的ACID特性

  • 原子性(Atomicity)

​ 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性(Consistency)

​ 一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。

​ 也就是说:如果事务是并发多个,系统也必须如同串行事务一样操作。其主要特征是保护性和不变性(Preserving an Invariant),以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性。

  • 隔离性(Isolation)

一个事务所操作的数据在提交之前,对其他事务的可见性设定(一般设定为不可见)

  • 持久性(Durability)

事务所做的修改就会永久保存,不会因为系统意外导致数据的丢失

事务并发带来了哪些问题

如下图,事务A和事务B 同时操作id为1的user

脏读(dirty read)

  1. 事务B 修改id为1的用户age由16 --> 18
  2. 事务A 查询id为1的用户,获取到age为18
  3. 事务B 此时因为某些意外原因,rollback

(要理解1和3为同一个事务(最小执行单元))
此时数据库中的id为1的记录age还是16,而事务B并不之情,以为age是18,此时就出问题了,所谓的脏读

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql锁

不可重复读(nonrepeatableread)

  1. 事务A 查询id为1的用户,获取到age为16
  2. 事务B 修改id为1的用户,age由16 --> 18
  3. 事务B commit
  4. 事务A 查询id为1的用户,获取到age为18

(1和4一个事务 2和3一个事务,要理解成不可分割的最小执行单元)
此时事务A两次查询不一样,在一个事务重复读数据内容不一样,所谓的 不可重复读

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql锁_02

幻读(Phantom read)

  1. 事务A 查询id为age > 15的用户,获取到一个用户,id为1,age为16的用户
  2. 事务B 新增id为2,name为‘Bob’,age为22的用户
  3. 事务A 再次查询age > 15的用户,获取到两个用户

(1和3一个事务 ,要理解成不可分割的最小执行单元)
此时事务A两次查询不一样,在一个事务重复读数据的数量一样,产生了幻觉,所谓的 幻读

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql优化_03

  • 脏读:很好理解,事务中,读取到脏数据。
  • 不可重复读:事务中,多次读取同一个数据的内容不一样。(针对update)。
  • 幻读:事务中,多次读取同一个条件数据的数量不一样。(针对的是insert、delete)

如何解决上面三种问题呢?往下看

事务四种隔离级别

SQL92,是数据库的一个ANSI/ISO标准。它定义了一种语言(SQL)以及数据库的行为(事务、隔离级别等)。

SQL92 ANSI/ISO标准:
http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

四种隔离级别

  • Read Uncommitted(未提交读) --未解决并发问题

事务未提交对其他事务也是可见的,脏读(dirty read)

  • Read Committed(提交读) --解决脏读问题

一个事务开始之后,只能看到自己提交的事务所做的修改,不可重复读(nonrepeatableread)

  • Repeatable Read (可重复读) --解决不可重复读问题

在同一个事务中多次读取同样的数据结果是一样的,这种隔离级别未定义解决幻读(Phantom read)的问题

  • Serializable(串行化) --解决所有问题

最高的隔离级别,通过强制事务的串行执行

Innodb引擎对隔离级别的支持程度

事务隔离级别 脏读 不可重复读 幻读 并发能力
Read Uncommitted(未提交读) 可能 可能 可能 ☆☆☆☆
Read Committed(提交读) 不可能 可能 可能 ☆☆☆
Repeatable Read(可重复读) 不可能 可能 不可能 对Innodb不可能 ☆☆
Serializable(串行化) 不可能 不可能 不可能

MySql锁

​ MySQL为什么要提供锁机制?锁能解决什么问题?

​ 如何保证数据并发访问的一致性、有效性是所在有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

理解表锁、行锁

锁是用于管理不同事务对共享资源的并发访问

表锁与行锁的区别:

锁定粒度:表锁 > 行锁

加锁效率:表锁 > 行锁

冲突概率:表锁 > 行锁

并发性能:表锁 < 行锁

InnoDB存储引擎支持行锁和表锁(另类的行锁)

MySQL Innodb锁类型

MySQL Innodb锁类型一共有3种类型

  1. 行锁
  • 共享锁(行锁):Shared Locks

  • 排它锁(行锁):Exclusive Locks

  1. 表锁
  • 意向锁共享锁(表锁):Intention Shared Locks

  • 意向锁排它锁(表锁):Intention Exclusive Locks

  1. 自增锁
  • AUTO-INC Locks

下面3种是行锁的算法

  • 记录锁 Record Locks

  • 间隙锁 Gap Locks

  • 临键锁 Next-key Locks

行锁的算法https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

共享锁(Share Locks)vs 排它锁(Exclusive Locks)

事务共享锁 :又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改;

加锁释锁方式:

select * from users WHERE id=1 LOCK IN SHARE MODE;
commit/rollback

实例测试Share Locks

会话A autocommit关闭

##1、当前会话A autocommit关闭
mysql> set session autocommit = OFF;
Query OK, 0 rows affected (0.00 sec)

mysql> show VARIABLES like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.01 sec)

##2、查询select
mysql> select * from users WHERE id=1 LOCK IN SHARE MODE\G
*************************** 1. row ***************************
        id: 1
     uname: 李二狗
 userLevel: 2
       age: 19
  phoneNum: 13666666666
createTime: 2021-01-23 15:39:46
lastUpdate: 2021-01-23 15:39:50
1 row in set (0.00 sec)
##3、当前事务还没提交或者回滚

会话B autocommit采用默认的,未关闭

mysql> select * from users where id =1\G
*************************** 1. row ***************************
        id: 1
     uname: 李二狗
 userLevel: 2
       age: 19
  phoneNum: 13666666666
createTime: 2021-01-23 15:39:46
lastUpdate: 2021-01-23 15:39:50
1 row in set (0.00 sec)

mysql> update users set age=19 where id =1;
##这里修改会阻塞。。。
##。。。等一会显示
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

事务排他锁:又称为写锁,简称X锁,排他锁不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的锁(共享锁、排他锁),只有该获取了排他锁的事务是可以对数据行进行读取和修改,(其他事务要读取数据可来自于快照//TODO 快照后面会将,待补充链接)

加锁释锁方式:

delete / update / insert 
# 默认加上X锁

SELECT * FROM table_name WHERE ... FOR UPDATE
commit / rollback 

实例测试
会话A

mysql> set session autocommit = OFF;
Query OK, 0 rows affected (0.01 sec)

mysql> show VARIABLES like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.01 sec)

mysql> select * from users where id =1 for update\G
*************************** 1. row ***************************
        id: 1
     uname: 李二狗
 userLevel: 2
       age: 19
  phoneNum: 13666666666
createTime: 2021-01-23 15:39:46
lastUpdate: 2021-01-23 15:39:50
1 row in set (29.50 sec)
##此时会话A拿到排它锁

会话B

mysql> show VARIABLES like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.02 sec)

mysql> select * from users where id =1 for update\G
## 会阻塞。。。
## 然后过一段时间超时
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

##在尝试拿共享锁(会话A要重新拿一次排它锁,因为我用的linux,这边超时了,会话A的事务无效了)
mysql> select * from users where id =1 lock in share mode\G
## 会阻塞。。。
## 然后过一段时间超时
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Innodb到底锁了什么?

InnoDB的行锁是通过给索引上的索引项加锁来实现的。

只有通过索引条件进行数据检索,InnoDB才使用行级锁,否则,InnoDB将使用表锁(锁住索引的所有记录)

意向共享锁(IS)& 意向排他锁

  • 意向共享锁(IS)

​ 表示事务准备给数据行加入共享锁,即一个数据行加共享锁前必须先取得该表的IS锁,意向共享锁之间是可以相互兼容的

  • 意向排它锁(IX)

​ 表示事务准备给数据行加入排他锁,即一个数据行加排他锁前必须先取得该表的IX锁,意向排它锁之间是可以相互兼容的

**意向锁(IS、IX)**是InnoDB数据操作之前自动加的,不需要用户干预

意义:
当事务想去进行锁表时,先尝试拿意向锁,意向拿不到,就不用去拿共享锁、排他锁

例如生活中的案例

​ 一节火车车厢上的卫生间WC会有一个指示灯,提示有人、无人,其他乘客只需要通过指示灯可以判断卫生间能否进入,获取使用权。这个指示灯就相当于意向锁,只是一个标识。

​ 乘客要获取使用权卫生间,不用进入卫生间查看是否有人,只需要看指示灯就行了。

​ 事务要获取一个数据行的锁,要先获取意向锁。如果意向所获取不到,就没必要继续获取其共享锁或排他锁了,提高获取锁的性能。

自增锁 AUTO-INC Locks

针对自增列自增长的一个特殊的表级别锁

mysql> show variables like 'innodb_autoinc_lock_mode';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 1     |
+--------------------------+-------+
1 row in set (0.00 sec)

默认取值1,步长为1,代表连续,事务未提交ID永久丢失

临键锁(Next-key)&间隙锁(Gap)&记录锁(Record)

Gap locks:

锁住数据不存在的区间(左开右开)
​ 当sql执行按照索引进行数据的检索时,查询条件的数据不存在,这时SQL语句加上的锁即为Gap locks,锁住索引不存在的区间(左开右开)

Record locks:

锁住具体的索引项
​ 当sql执行按照唯一性(Primary key、Unique key)索引进行数据的检索时,查询条件等值匹配且查询的数据是存在,这时SQL语句加上的锁即为记录锁Record locks,锁住具体的索引项

下面结合实例详细介绍

数据准备,比如数据库中有一个表t,表结构和数据如下:

mysql> desc t;
+-------+---------+------+-----+---------+----------------+
| Field | Type    | Null | Key | Default | Extra          |
+-------+---------+------+-----+---------+----------------+
| id    | int(11) | NO   | PRI | NULL    | auto_increment |
| value | int(11) | NO   |     | NULL    |                |
+-------+---------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

mysql> select * from t;
+----+-------+
| id | value |
+----+-------+
|  1 |     1 |
|  4 |     7 |
|  7 |     7 |
| 10 |    10 |
+----+-------+
4 rows in set (0.00 sec)

很简单的数据,只有4条

临键锁(Next-key)

Next-key locks
​ 锁住记录+区间(左开右闭)
​ 当sql执行按照索引进行数据的检索时,查询条件为范围查找(between and、<、>等)并有数据命中则此时SQL语句加上的锁为Next-key locks,锁住索引的记录+区间(左开右闭)

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql_04
为什么Innodb选择临键锁Next-key作为行锁的默认算法?

防止幻读,同时Innodb的默认引擎是B+树,其数据结构特点就是连续递增,且左开右闭,所以使用Next-key策略

间隙锁(Gap)

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql_05
Gap只在RR事务隔离级别存在

记录锁(Record)

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql_06

怎么利用锁解决脏读、不可重复读、幻读

解决脏读用x锁:
性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql事务_07
解决不可重复读用s锁:
性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql事务_08
解决幻读用Next-key锁:
性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql优化_09

死锁介绍

多个并发事务(2个或者以上);

每个事务都持有锁(或者是已经在等待锁);

每个事务都需要再继续持有锁;

事务之间产生加锁的循环等待,形成死锁。

小结:我在等你、你在等我。

死锁如何避免

  1. 类似的业务逻辑以固定的顺序访问表和行。
  2. 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
  3. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  4. 降低隔离级别,如果业务允许,将隔离级别调低也是较好的选择
  5. 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁(或者说是表锁)

MVCC

先思考一个问题

## 伪代码
## 查看mysql的设置的事务隔离级别
select @@tx_isolation;
ex1:
	tx1: set session autocommit=off;
		 update users set lastUpdate=now() where id =1;
		 ## 在未做commit/rollback操作之前
		 ## 在其他的事务我们能不能进行对应数据的查询(特别是加上了X锁的数据)
	tx2: select * from users where id > 1;
			 select * from users where id = 1;
ex2:
	tx1: begin
		 select * from users where id =1 ;
	tx2: begin
		 update users set lastUpdate=now() where id =1;
	tx1:
		 select * from users where id =1;

这两个案例从结果上来看是一致的!底层实现是怎样的呢?是一样的吗?他们的底层实现跟MVCC有什么关系么?

MVCC
​ Multiversion concurrency control (多版本并发控制)

普通话解释:
​ 并发访问(读或写)数据库时,对正在事务内处理的数据做多版本的管理。以达到用来避免写操作的堵塞,从而引发读操作的并发问题。

MVCC实现
​ MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 实现是不同的,典型的有乐观并发控制和悲观并发控制.

MVCC的具体实现分析
下面,我们通过InnoDB的MVCC实现来分析MVCC是怎样进行并发控制的

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间(DB_TRX_ID),一个保存的是行的删除时间(DB_ROLL_PT)。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),每开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID.下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的.

MySQL中MVCC逻辑流程

插入

假设系统的全局事务ID号从1开始;

begin;  -- 拿到系统的事务ID=1;
insert into teacher(name,age) value ('sever',18);
insert into teacher(name,age) value ('qingshan',19);
commit;

如下图,数据插入成功后,表后面两列保存相应的版本号

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql_10

删除

假设系统的全局事务ID号目前到了22

begin; -- 拿到系统的事务ID=22;
delete teacher where id = 1;
commit;

如下图,id为2的数据行,删除版本号设置为当前事务ID(22)

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql优化_11

修改

假设系统的全局事务ID号目前到了33

begin; -- 拿到系统的事务ID=33;
update teacher set age = 19 where id = 1;
commit;

修改操作是先做命中的数据行的copy,将原行数据的删除版本号的值设置为当前事务ID(33)

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql锁_12

查询

数据行查询规则

  1. 查找数据行版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
  2. 查找删除版本号要么为null,要么大于当前事务版本号的记录,确保取出来的行记录在事务开启之前没有被删除
    只有1,2同时满足的记录,才能返回作为查询结果

假设系统的全局事务ID号目前到了44

begin; -- 拿到系统的事务ID=44;
select * from teacher;
commit;

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql优化_13

MySQL中版本控制案例

数据准备:

insert into teacher(name,age) value ('seven',18) ;
insert into teacher(name,age) value ('qingshan',20) ;
# tx1:
begin; 									-- --------1
select * from users ; 					-- --------2
commit;
# tx2:
begin; 									-- --------3
update teacher set age =28 where id =1; -- --------4
commit;

案例1
按顺序执行 1,2,3,4,2

案例2
按顺序执行 3,4,1,2

案例一(1,2,3,4,2)

tx1 先执行1,2

tx2 再执行3,4

tx1 再执行2

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql锁_14

案例二(3、4、1、2)

tx2 先执行3,4

tx1 再执行1,2

性能优化专题 - MySql 性能优化 - 03 - 深入理解InnoDB_mysql_15
案例二查询结果不是我们想要的,mysql的Innodb也不是这样做的。

写在最后

更多架构知识,欢迎关注本套系列文章Java架构师成长之路