【MySQL 架构师视角 数据库并发】

文章目录

  • 什么是事务
  • 事务的ACID特性
  • 并发带来的问题
    • 脏读
    • 不可重复读
    • 幻读
  • INNODB的几种事务隔离级别
    • serializable事务隔离级别
    • repeatable read事务隔离级别
    • read commited 事务隔离级别
    • read uncommited事务隔离级别
  • 事务阻塞的产生
    • 产生阻塞的原因
      • 排它锁与共享锁
      • 悲观锁与乐观锁
    • 如何检测阻塞
      • 什么是阻塞
      • 如何发现阻塞
      • 如何处理阻塞
    • 另一并发问题——死锁
      • 如何发现死锁
      • 如何检测死锁
      • 如何处理死锁

什么是事务

  • 事务是数据库执行操作的最小逻辑单元
  • 事务可以由一个SQL或多个SQL组成
  • 组成事务的SQL要么全部执行成功,要么全部执行失败
START TRANSACTION / BEGIN

SELECT ...
UPDATE ...
INSERT ...

COMMIT / ROLLBACK

ROLLBACK:回滚到事务开始之前的状态
COMMIT: 提交事务,执行事务中的操作

事务的ACID特性

【MySQL 架构师视角 数据库并发】_第1张图片

特征 说明
原子性(A 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节
一致性(C 在事务开始之前和事务结束以后,事务的完整性没有被破坏
隔离性(I 该事务提交前对其他事务不可见
持久性(D 事务一旦提交了,其结果就是永久性的,即使发生宕机,数据库也能恢复

并发带来的问题

脏读

脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
【MySQL 架构师视角 数据库并发】_第2张图片
事务1先读取id为59的课程的分值,接着事务2访问并更新分值并且回滚,事务1再次读取就会读到“脏数据”,无效的数据。

不可重复读

不可重复读:指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

【MySQL 架构师视角 数据库并发】_第3张图片
不可重复读与脏读的区别是不可重复读的事务2提交了事务,所以事务1读取到的数据是有效的。

幻读

幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

【MySQL 架构师视角 数据库并发】_第4张图片
幻读与不可重复读的区别在于:
不可重复读的重点是修改,而幻读的重点是新增或删除

例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 2000的操作还没完成,事务2中的B先生就修改了A的工资为3000,导 致A再读自己的工资时工资变为 3000;这就是不可重复读。

例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读.

INNODB的几种事务隔离级别

【MySQL 架构师视角 数据库并发】_第5张图片

serializable事务隔离级别

SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。在这个级别中,读操作加共享锁,写操作加排它锁,所以会产生读写互斥,写写互斥,所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读
我们可以通过下面的命令来设置隔离级别

SET [SESSION|GLOBAL] 
TRANSACTION ISOLATION LEVEL
 [READ UNCOMMITTED
 |READ COMMITTED
 |REPEATABLE READ
 |SERIALIZABLE]

下面来举个例子来理解一下,我们可以通过查看serializable级别两个事务并发的情况:

第一个命令行窗口:
先设置隔离级别为serializable:

set session transaction isolation level serializable;

可通过下面这个命令查看当前隔离级别是否设置成功:

SELECT @@transaction_isolation;

接着输入下面命令显式开启一个事务:

START TARNSACTION;

然后输入需要执行的查询语句:
【MySQL 架构师视角 数据库并发】_第6张图片
接着开启第二个命令行窗口,再开启一个事务,然后输入更新语句,
更新语句是满足第一个事务的条件的,会发现当前事务在等待,超过等待时间后就会自动结束语句:
【MySQL 架构师视角 数据库并发】_第7张图片
当第一个事务提交或者回滚后,第二个事务才会继续执行
在这里插入图片描述
在这里插入图片描述
除了在命令行,也可以在图形化工具中查看,如下用Navicat中:
第一个事务开启,
【MySQL 架构师视角 数据库并发】_第8张图片
开启第二个事务并且更新,可以看到右上角处显示正在处理,说明在等待第一个事务提交或回滚,才可执行:
【MySQL 架构师视角 数据库并发】_第9张图片

repeatable read事务隔离级别

可重复读(Repeated Read):可重复读。在同一个事务内对同一字段的多次读取结果都是一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了脏读和不可重复读,但是还存在幻读。

与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下,允许应用使用 Next-Key Lock 锁算法来避免幻读的产生。这与其他数据库系统(如 SQL Server)是不同的。

所以说虽然 InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读),但是可以通过应用加锁读来保证不会产生幻读,而这个加锁度使用到的机制就是 Next-Key Lock 锁算法。从而达到了 SQL 标准的 SERIALIZABLE(可串行化) 隔离级别。

以下我们再通过一个例子来理解可重复读:
先使用命令查看隔离级别:

SELECT @@transaction_isolation;

在这里插入图片描述
这里默认为REPEATABLE-READ,若不是则设置:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

接下来在第一个事务中执行查询语句:
【MySQL 架构师视角 数据库并发】_第10张图片
打开第二个事务,修改一门课程的学习人数为9600,并提交修改
【MySQL 架构师视角 数据库并发】_第11张图片
可以看到,这里事务2并不会阻塞,与串行化隔离级别不同,那我们来看看事务1中的数据是否有被修改呢?
【MySQL 架构师视角 数据库并发】_第12张图片
可以看到事务1中的数据并未发生变化,所以可重复读在该sql第一次读取到数据后,就将这些数据加锁(悲观锁),其它事务无法修改这些数据,前后多次对该字段查询,得到的结果都是一致的,
再举个例子来看插入一条数据:

事务2插入成功:
【MySQL 架构师视角 数据库并发】_第13张图片
【MySQL 架构师视角 数据库并发】_第14张图片
事务1查询到的数据还是没有变化,说明可重复读也可避免幻读。

read commited 事务隔离级别

READ-COMMITTED(读已提交)
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

同样以一个例子来理解,
1.设置事务隔离级别为READ COMMITTED:
【MySQL 架构师视角 数据库并发】_第15张图片
2.启用事务1,并执行查询语句:
【MySQL 架构师视角 数据库并发】_第16张图片
3.启动事务2,并更新其中一门课程的人数为9600,并提交事务2
【MySQL 架构师视角 数据库并发】_第17张图片
4.回到事务1,再次查询人数大于9500的课程发现多了id为100的:
【MySQL 架构师视角 数据库并发】_第18张图片

read uncommited事务隔离级别

READ UNCOMMITTED(读未提交):
最低的隔离级别,允许读取尚未提交的数据变更可能会导致脏读、幻读或不可重复读

同样我们还是以一个例子理解,
1.设置隔离级别:
【MySQL 架构师视角 数据库并发】_第19张图片
2.启动事务1并查询学习人数大于9500的课程:
【MySQL 架构师视角 数据库并发】_第20张图片
3.启动事务2,更新一门课程的学习人数为9800,但不提交
【MySQL 架构师视角 数据库并发】_第21张图片
4.回到事务1,再次查询发现读取到了未提交的修改值:
【MySQL 架构师视角 数据库并发】_第22张图片

事务阻塞的产生

我们先来通过一个例子理解阻塞的产生:
在事务1中执行更新语句
【MySQL 架构师视角 数据库并发】_第23张图片
在事务2中执行相同的更新语句,发现事务2被阻塞了:
【MySQL 架构师视角 数据库并发】_第24张图片
只有事务1提交了,事务2才可以执行。
所以为什么会产生阻塞呢?

产生阻塞的原因

排它锁与共享锁

为了不产生阻塞,INNODB引入了锁的机制,这里主要介绍两种锁:

  • 查询时需要对资源加共享锁(S)
  • 数据修改时需要对资源加排它锁(x)

【MySQL 架构师视角 数据库并发】_第25张图片
首先说明:数据库中的增删改都会默认加排它锁,而查询不加任何锁
共享锁:对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。语法为:

select * from table lock in share mode

|–排他锁:对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。语法为:

select * from table for update

排它锁与排它锁,排它锁与共享锁之间是不兼容的,而对于共享锁来说,由于查询并不会修改数据,所以共享锁可以并发查询,我们在上面一个例子中也可以看到,由于第一个事务的更新操作加了排它锁,由于锁的不兼容,所以第二个事务需要等第一个事务释放排它锁,它才可执行操作,这也是为什么可重复读事务隔离级别下会产生阻塞的原因。

悲观锁与乐观锁

上面提及到的排它锁与共享锁都是悲观锁的实现形式,悲观锁与乐观锁都是抽象出来的,不真实存在这种锁。

悲观锁:指的就是在数据处理过程中加锁使得数据处于锁定状态,使得数据不被修改,悲观锁的实现,往往依靠数据库的锁机制,正如上面的排它锁和共享锁都是依靠数据库的锁机制实现的,保证了事务的隔离性,但是加锁对性能消耗很大,特别对于长事务,带来的开销是往往不能接受的,所以有了乐观锁。

乐观锁:相对于悲观锁而言,乐观锁采取了宽松的锁机制,其基于数据版本实现的,就是为数据库表增加一个“version”字段来实现,读取数据的时候将版本号一同读出,在之后的每更新一次,版本号加一,然后将提交的版本号与系统的版本号比较,若大于系统版本号则更新,否则过期,在INNODB中通过MVCC实现了乐观锁。

如何检测阻塞

什么是阻塞

阻塞就是:由于不同锁之间的兼容关系,造成一个事务需要等待另一事务释放其所占用的资源的现象。

如何发现阻塞

我们可以在上面两个例子阻塞后,新开一个连接执行以下语句

SELECT waiting_pid AS 'blocked pid',
       waiting_query AS 'blocked SQL',
       blocking_pid AS 'running pid',
       blocking_query AS 'running SQL',
       wait_age AS 'blocked time',
       sql_kill_blocking_query AS 'info'
FROM sys.innodb_lock_waits

在这里插入图片描述
可以得到上面的信息,第一列是阻塞的连接ID,第二列是阻塞的操作,第三列是引发阻塞的ID,第四列无法查询到正在执行的SQL是哪一个,第五列是阻塞的时间,第六列是建议是杀掉ID为14的进程。

如何处理阻塞

  • 手动终止占用资源的事务
  • 优化占用资源的SQL,避免长时间占用资源

另一并发问题——死锁

死锁:并行执行的多个事务之间相互占用对方的资源
在MySQL中会自动结束占用资源少的一方,让占用资源多的一方继续执行,解决了死锁的问题,但我们还是要对死锁进行监控,降低对我们业务的影响。

如何发现死锁

开启日志记录死锁的产生信息:

set global innodb_print_all_deadlocks=on;

【MySQL 架构师视角 数据库并发】_第26张图片

如何检测死锁

第一个事务:
在这里插入图片描述
第二个事务:会发现死锁自动发现并处理了
【MySQL 架构师视角 数据库并发】_第27张图片

如何处理死锁

  • 数据库会自行回滚占用资源少的事务
  • 并行事务应该按相同顺序占用资源

你可能感兴趣的:(数据库)