MySQL性能优化学习——(三) 数据库事务与锁

一、什么是数据库的事务?

1.1 事务的定义

维基百科的定义:

事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,
由一个有限的数据库操作序列构成。

这里面有两个关键点,第一个,它是数据库最小的工作单元,是不可以再分的。
第二个,它可能包含了一个或者一系列的 DML 语句,包括 insert delete update。
(单条 DDL(create drop)和 DCL(grant revoke)也会有事务)
 

1.2 事务的典型场景

在项目里面,什么地方会开启事务,或者配置了事务?

无论是在方法上加注解,还是配置切面。

<tx:advice id="txAdvice" transaction-manager="transactionManager">

<tx:attributes>
<tx:method name = "save*" rollback-for = "Throwable" />
<tx:method name = "add*" rollback-for = "Throwable" />
<tx:method name = "send*" rollback-for = "Throwable" />
<tx:method name = "insert*" rollback-for = "Throwable" />
</tx:attributes>
</tx:advice>
 
比如下单,会操作订单表,资金表,物流表等等,这个时候我们需要让这些操作都在一个事务里面完成。
当一个业务流程涉及多个表的操作的时候,我们希望它们要么是全部成功的,要么都不成功,这个时候我们会启用事务。

1.3 哪些存储引擎支持事务?

InnoDB 支持事务,这个也是它成为默认的存储引擎的一个重要原因:
https://dev.mysql.com/doc/refman/5.7/en/storage-engines.html
另一个是 NDB。

1.4 事务的四大特性

事务的四大特性:ACID

1)原子性(Atomicity)

指我们对数据库一系列的操作,要么都是成功,要么都是失败,不可能有部分成功或部分失败的情况。
例如转账,一个账户余额减少,对应一个账户增加,这两个账户一定是同时成功或者同时失败的。
如果前面一个操作已经成功了,后面的操作失败了,怎么让它全部失败呢?这个时候我们必须要回滚。
原子性,在InnoDB中是通过undo log来实现的,它记录数据修改前的值,一旦异常,就通过undo log来实现回滚操作。

2)一致性(consistent)

一致性指的是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
比如主键必须是唯一的,字段长度符合要求。
 
除了数据库自身的完整性约束,还有一个是用户自定 义的完整性:
例如转账,A 账户余额减少 1000,B 账户余额只增加了 500,
这个时候因为两个操作都成功了,按照我们对原子性的定义,它是满足原子性的,
但是它没有满足一致性,因为它导致了会计科目的不平衡。

用户自定义的完整性通常要在代码中控制。

3)隔离性(Isolation

多个并发事物同时操作同一张表或同一行数据,必然会引发一些并发和干扰的问题。
隔离性就是指多个事物对表或行的并发操作,应该是透明的互不干扰的。
通过这种规则,也是为了保证数据的一致性。

4)持久性(Durable

我们对数据库的任意的操作,增删改,只要事务提交成功,那么结果就是永久性的。
不可能因为我们系统宕机或者重启了数据库的服务器,它又恢复到原来的状态了。
 
持久性以及数据库崩溃恢复(crash-safe)是通过什么实现的?
持久性是通过redo log 和 double write 来实现的,操作数据时,
会先写到内存的buffer pool中,同时记录redo log,如果在刷入磁盘前出现异常,
在重启后就可以读取redolog中的内容,写入磁盘,保证数据的持久性。
但是恢复成功的前提是数据页本身没有被破坏,是完整的,这个通过双写缓冲(double write)保证。
 
原子性、隔离性、持久性最终目的都是为了实现一致性。
 

1.5 数据库事物的开启

无论是在 Navicat 的这种工具里面去操作,还是在我们的 Java 代码里面通过API 去操作,
还是加上@Transactional 的注解或者 AOP 配置,其实最终都是发送一个指令到数据库去执行,
Java 的 JDBC 只不过是把这些命令封装起来了。
 
操作环境为版本(5.7),存储引擎(InnnoDB),事务隔离级别(RR)。
select version();
show variables like '%engine%' ;
show global variables like "tx_isolation" ;
 
执行这样一条更新语句的时候,它有事务吗?
update user  set name = 'admin2 ' where id= 2 ;
 
实际上,它自动开启了一个事物,并且提交了,所以最终写入了磁盘。
 
这个是开启事务的第一种方式,自动开启和自动提交。
InnoDB 里有一个 autocommit 的参数(分成两个级别, session 级别和 global级别)。
show variables like 'autocommit' ;
 
它的默认值是 ON。autocommit 代表是否自动提交。如果它的值是 true/on 的话,
在操作数据的时候,会自动开启一个事务,和自动提交事务。
否则,如果把 autocommit 设置成 false/off,那么数据库的事务就需要手动地开启和手动地结束。
手动开启事务也有几种方式,一种是用 begin;一种是用 start transaction。
 
怎么结束一个事务呢?我们结束也有两种方式,第一种就是提交一个事务, commit;
还有一种就是 rollback,回滚的时候,事务也会结束。
还有一种情况,客户端的连接断开的时候,事务也会结束。

1.6 事物并发会带来什么问题?

假如没有隔离性,并发事物会产生什么问题呢?

1)脏读


如图,有A,B两个事物,A事物读取一条记录name值是Jack,B事物修改这条数据,把name修改成Rose,没有提交

此时A事物又进行了一次查询,查到这条记录中name变成了Rose。

这种在一个事物中,前后两次读到的数据不一致,读到了其他事物没有提交的数据的情况,就被称为脏读。

2)不可重复读

A,B两个事务,A事务读取到的name是Jack,B事务修改后进行了提交,A事务再次读取到的name是Rose。

由于A事务再次读取时,读到了其他事务提交后的数据,读取到了两个不一致的数据。name到底时Jack还是Rose呢?

这种一个事务读取到了其他事务已提交的数据导致前后两次读取数据不一致的情况,被称为 不可重复读。
 

3)幻读

在A事物中进行条件查询,满足条件的数据只有1条,在B事物中插入一条数据,并且提交

在A事物再次查询时发现多了一条数据。

一个事务前后两次读取数据数据不一致,是由于其他事务插入数据造成的,这种情况我们把它叫做幻读。
 
不可重复读和幻读的区别是什么?
不可重复读是修改或者删除,幻读是插入。
注意:只有insert操作导致的数据不一致才是幻读,修改或删除则是不可重复读。
 
 
无论是脏读,还是不可重复读,还是幻读,它们都是数据库的 读一致性 的问题,
都是在一个事务里前后两次读取出现了不一致的情况。
针对读一致性的问题,数据库提供了一定的事物隔离机制来解决。
 

1.7 隔离级别

就有很多的数据库专家联合制定了一个标准,也就是说建议数据库厂商都按
照这个标准,提供一定的事务隔离级别,来解决事务并发的问题,这个就是 SQL92 标准。
 
SQL92 标准的官网:
http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt
这里面有一张表格( 搜索isolation_levels ),里面定义了四个隔离级别。
P1 P2 P3 代表事务并发的 3 个问题,脏读,不可重复读,幻读。
Possible 代表在这个隔离级别下, 这个问题有可能发生,没有解决这个问题。
Not Possible 代表解决了这个问题。

1)Read Uncommitted(未提交读)

一个事物可以读取到其他事物未提交的数据,会出现脏读,没有解决任何问题

2)Read Committed(已提交读)

一个事物智能读取到其他事物已提交的数据,不能读取到其他未提交的数据,

它解决了脏读的问题,但会出现不可重复读的问题。

3)Repeatable Read(可重复读)

解决了不可重复读的问题,但没有定义解决幻读的问题

4)Serializable(串行化)

所有事物都串行执行,也就是事物的操作需要排队,已经不存在事物的并发操作了,所以解决了所有的问题。

这是SQL92标准,但不同的数据库厂商或存储引擎的实现有差异。

如Oracle中就只有两种RC(已提交读)和Serializable(串行化)。

那么 InnoDB 的实现又是怎么样的呢?

1.8 MySQL InnoDB 对隔离级别的支持

MySQL InnoDB 里面,不需要使用串行化的隔离级别去解决所有问题。

InnoDB 支持的四个隔离级别和 SQL92定义的唯一区别是

InnoDB 在 RR 的级别就解决了幻读的问题。

因此InnoDB默认的隔离级别就是Repeatable Read(可重复读),即保证了数据一致性,也支持较高的并发。

1.9 一致性的两大实现方案

LBCC

既然要保证前后两次读取数据一致,那么读取数据的时候,锁定要操作的数据,
不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制 Lock
Based Concurrency Control(LBCC)。
如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那
就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地
影响操作数据的效率。
 

MVCC

所以还有另一种解决方案,如果要让一个事务前后两次读取的数据一致,
可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了。
这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control(MVCC)。
 
MVCC 的核心思想是:
我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。
在我这个事务之后新增的数据,我是查不到的。
 
MVCC是如何实现的?
DB 为每行记录都实现了两个隐藏字段:
DB_TRX_ID(创建版本号):
插入或更新行的最后一个事务的事务 ID,事务编号是自动递增的。
(我们把它理解为 创建版本号 ,在数据新增或者修改为新数据的时候,记录当前事务 ID)。
DB_ROLL_PTR(删除版本号):
回滚指针
(我们把它理解为 删除版本号 ,数据被删除或记录为旧数据的时候,记录当前事务 ID)。
 
我们把这两个事务 ID 理解为版本号。

下面是MVCC的演示:

第一个事务,初始化数据,
Transaction 1
begin;
insert into product values(NULL,'电脑') ;
insert into product values(NULL,'空调') ;
commit;
此时的数据,创建版本是当前事务 ID:1,删除版本为空:
id name 创建版本 删除版本
1 电脑 1 undefined
2 空调 1 undefined
第二个事务,执行第 1 次查询,读取这两条数据,第二个事务的 ID 是 2:
Transaction 2
begin;
select * from product ; 
第三个事务,插入一条数据:
Transaction 3
begin;
insert into product values(NULL,'电冰箱') ;
commit;

此时的数据,多了一条数据,它的创建版本号是当前事务编号,3: 

id name 创建版本 删除版本
1 电脑 1 undefined
2 空调 1 undefined
3 电冰箱 3 undefined
第二个事务,执行第 2 次查询:
Transaction 2
begin;
select * from product ; 

查询结果,只查到了开始的两条数据,并没有查到事物三新创建的数据。

因此:
MVCC 的查找规则:
只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事务 ID 的行(或未删除)。
也就是不能查到在我的事务开始之后插入的数据,tom 的创建 ID 大于 2,所以还是只能查到两条数据。

再来看看删除,

第四个事务,删除数据,删除了 id=2 的记录:
 
Transaction 4
begin;
delete from product where id=2;
commit;

此时的数据,id为2这条数据的删除版本被记录为当前事务 ID:4,其他数据不变: 

id name 创建版本 删除版本
1 电脑 1 undefined
2 空调 1 4
3 电冰箱 3 undefined
在第二个事务中,执行第 3 次查询:
Transaction 2
select * from product; (3) 第三次查询

查询结果,还是查到了开始的两条数据,事物四删除的数据仍能被事物二查到。

查找规则:
只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事务 ID 的行(或未删除)。
也就是,在我事务开始之后删除的数据,所以 id为2的数据依然可以查出来。所以还是这两条数据。
 
 
然后是更新,
第五个事务,执行更新操作,这个事务事务 ID 是 5:
Transaction 4
begin;
update product set name ='洗衣机 ' where id=1;
commit;
此时的数据,更新数据的时候,旧数据的删除版本被记录为当前事务 ID 5,
产生了一条新数据,创建 ID 为当前事务 ID 5:
id name 创建版本 删除版本
1 电脑 1 5
2 空调 1 4
3 电冰箱 3 undefined
1 洗衣机 5 undefined
第二个事务,执行第 4 次查询:
Transaction 2
select * from mvcctest ; (4) 第四次查询

查询结果,仍然可以查出更新前的"电脑",查不出更新后的“洗衣机”。这是为什么?

查找规则:
只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事务 ID 的行(或未删除)。
 
因为更新后 “洗衣机”  数据的创建版本大于 2,代表是在事务之后增加的,查不出来。
而旧数据 “电脑” 的删除版本大于 2,代表是在事务之后删除的,可以查出来。
 
通过以上演示可以发现,通过版本号的控制,
无论其他事务是插入、修改、删除, 第一个事务查询到的数据都没有变化。
 
在 InnoDB 中,MVCC 是通过 Undo log 实现的。
Oracle、Postgres 等等其他数据库都有 MVCC 的实现。
需要注意,在 InnoDB 中,MVCC 和锁是协同使用的,这两种方案并不是互斥的
 
第一大类解决方案是锁,锁又是怎么实现读一致性的呢?

二、InnoDB的锁

官网把锁分成了 8 类。
所以我们把行级别的锁(Shared and Exclusive Locks),和表级别的锁(Intention Locks)称为锁的基本模式。
把 Record Locks、Gap Locks、Next-Key Locks叫做锁的算法, 也就是分别在什么情况下锁定什么范围。
 
 

2.1 锁的粒度

表锁,顾名思义,是锁住一张表;行锁就是锁住表里面的一行数据。
 
锁定粒度:表锁的锁定粒度肯定是大于行锁的。
加锁效率:表锁的加锁效率大于行锁。
                 表锁只需要直接锁住这张表就行了,而行锁,还需要在表里面去检索这一行数据,所以表锁的加锁效率更高。
冲突概率:表锁的冲突概率比行锁大
                 当我们锁住一张表的时候,其他任何一个事务都不能操作这张表。
                 但我们锁住了表里的一行数据时,其他事务还可以操作表里其他没有被锁定的行,所以表锁的冲突概率更大。
冲突概率:表锁的冲突概率更大,所以并发性能更低。
 

2.2 共享锁(Shared Locks)

行级别的锁有两种,第一种叫共享锁。

共享锁又称为读锁,简称S锁。

共享锁就是多个事物对同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

加锁释锁方式:
可以用 select …… lock in share mode; 的方式手工加上一把读锁。
释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。
 
 
测试一下,创建两个会话。
第一个会话查询时加锁,第二个会话查询时仍可以加锁,也能查出加了读锁的数据。
说明了共享锁是可以重复获取的:
 

2.3 排它锁

第二种行锁叫排它锁。

又称为写锁,简称X锁,排它锁不能与其他锁并存。

如果一个事物获取了一个数据行的排它锁,其他事物事务就不能再获取该行的锁(包含共享锁),只有这个获取了排它锁的事物可以对数据行进行读写。

加锁释锁方式:

自动:delete/update/insert 默认加上排它锁

手动:select * form student where id=1 FOR UPDATE;

释放: commit;\rollback;

测试一下,一个会话修改数据,自动对该行施加写锁,另一个会话无法将获得共享锁和排它锁:

这个是两个行锁,接下来是两个表锁。
 

2.4 意向锁

意向锁是由数据库自己维护的。
意向共享锁:当我们给一行数据加上共享锁之前,数据库会自动在这张表上加一个意向共享锁。
意向排它锁:当我们给一行数据加上排它锁之前,数据库会自动在这张表上加一个意向排它锁。
 
意向锁有什么用呢?
如果没有意向锁,当我们准备给一张表加上表锁时,数据库要先判断有没有其他事物锁定其中某些行。
如果有,就无法加上表锁。
这时需要扫描整张表才能确定能不能成功加上一个表锁,如果数据量大,加表锁的效率就很低了。
 
但是有了意向锁,只要判断这张表上有没有意向锁,如果有,就直接返回失败。
如果没有,就可以加锁成功。
因此InnoDB中的表锁,可以把它理解成一个标志。可以用来提高加锁的效率。
 
以上就是 MySQL 里面的 4 种基本的锁的模式,或者叫做锁的类型。
 
到这里我们需要思考两个问题。
首先,锁的作用是什么?
它跟Java里的锁是一样的,为了解决资源竞争的问题。所以锁是用来解决事物对数据的并发访问的问题的。
锁到底锁住了什么?
当一个事物锁住了一行数据时,其他的事物不能操作这一行数据,
到底是锁住了一行数据,还是锁住了一个字段,还是锁住了别的?
 
 

三、行锁的原理

首先我们有三张表,一张没有索引的 t1,一张有主键索引的 t2,一张有唯一索引的 t3。
 

3.1 没有索引的表(假设锁住的是行)

先假设InnoDB的锁是锁住了一行数据或一条记录。
 
进行测试,有这样一张没有设置主键的表:
现在我们在两个会话里面手工开启两个事务。
在第一个事务里面,我们通过 where id =1 锁住第一行数据。
在第二个事务里面,我们尝试给 id=3 的这一行数据加锁。
两个事物分别锁不同的行,能否都锁住呢?
可以发现,加锁的操作被阻塞了。
 
再尝试事物二插入一条id=5的数据:

发现这个操作也被阻塞了,实际上这里整张表都被锁住了。
所以,我们的第一个猜想被推翻了, InnoDB 的锁锁住的应该不是行记录
 
 

3.2 有主键索引的表(假设锁住的是列)

我们看一下 t2 的表结构。字段是一样的,不同的地方是 id 上创建了一个主键索引。
里面的数据是 1、4、7、10。
 
使用相同的 id 值去加锁,冲突;
使用不同的 id 加锁,可以加锁成功。
那么,既然不是锁定一行数据,有没有可能是 锁住了 id 的这个字段呢
 

3.3 有唯一索引的表(假设锁住的是字段)

 t3 的表结构。字段还是一样的。
 id 上创建了一个主键索引,name 上 创建了一个唯一索引。里面的数据是 1、4、7、10。
 
在第一个事务中,使用name字段锁定值为4的这行数据。
第二个事务中,尝试获取相同的排他锁,结果当然是失败的。
 
在这里我们怀疑 InnoDB 锁住的是字段,所以这次换一个字段,
在第二个事务中用 id=4 去给这行数据加锁,是否成功呢?
可以发现 又被阻塞了,说明锁住的是字段的这个推测也是错的,
否则就不会出现第一个事务中锁住了 name,第二个事务中锁 id 失败的情况。
 
既然 锁住的不是 record,也不是 column ,InnoDB 里面锁住的到底是什么呢?
 
分析一下这三个案例的差异在哪里,也就是这三张表的结构,是什么区别导致了加锁的行为的差异?
其实答案就是索引。InnoDB 的行锁,就是通过锁住索引来实现的。

还有两个问题没有解决:

1、为什么表里面没有索引的时候,锁住一行数据会导致锁表? 如果锁住的是索引,一张表没有索引怎么办?
1)如果我们定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键作为聚集索引。
2)如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索
引作为主键索引。
3)如果也没有这样的唯一索引,则 InnoDB 会选择内置的 ROWID 作为隐藏的聚集索引,
它会随着行记录的写入而主键递增。
所以,为什么锁表,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了。
 
为什么出现案例三中,通过唯一索引给数据行加锁,主键索引也会被锁住?
回顾在说索引原理的时候,在InnoDB中,辅助索引中存储的是二级索引和主键的值。
比如name=4,存储的是name的索引和主键id的值4。
所以通过辅助索引锁定一行数据时,它和检索数据的步骤是一样的,会通过主键值找到主键索引,然后也锁定。
 
现在已经搞清楚 4 个锁的基本类型和锁的原理了。
在官网上,还有 3 种锁,我们把它理解为锁的算法。我们也来看下 InnoDB 在什么时候分别锁住什么范围。
 
 

四、锁的算法

4.1 记录锁(Record Locks)

第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询, 精准匹配到一条记录的时候,这个时候使用的就是记录锁。

比如 where id = 1 4 7 10 。
这个演示在前面3.2小节 已经看过了。我们使用不同的 key 去加锁,不会冲突,它只锁住这个 record。
 
 
4.2 间隙锁(Gap Locks)
第二种情况,当我们查询的记录不存在,没有命中任何一个 record,
无论是用等值查询还是范围查询的时候,它使用的都是间隙锁。
 
例如,表t2中只有id为1,4,7,10的四条数据:
Transcation1 Transcation2

begin;

select * form t2 where id=6 for update;

//id=6不存在时,同时会对(4,7)这个不存在数据的区间施加间隙锁

 
 
INSERT INTO `t2` (`id`, `name`) VALUES (5, '5'); // 被阻塞
INSERT INTO `t2` (`id`, `name`) VALUES (6, '6'); // 被阻塞
select * from t2 where id =6 for update; // OK
select * from t2 where id >20 for update;
//对大于最后一个存在的值的某值作为条件,会对(10,+∞)的区间施加间隙锁,因此并不是从20往后开始锁
 
 
INSERT INTO `t2` (`id`, `name`) VALUES (11, '11'); // 被阻塞

1)如例,当Transcation1中 执行查询where id=6 for update时,会对(4,7)之前的开区间施加间隙所,锁住(4,7)之间不存在数据的间隙。
当其他事物想对这个间隙(区间)内进行插入时,就会被阻塞,无法获取锁。

2)如例,对大于最后一个存在的值(10)的某值(20)作为条件,会对(10,+∞)的区间施加间隙锁,
因此并不是从20往后开始锁,因此插入id为11时也被阻塞。

注意,间隙锁主要是阻塞插入 insert。 相同的间隙锁之间不冲突
重复一遍,当查询的记录不存在的时候,才会施加间隙锁。
 
间隙锁只在 RR 中存在。如果要关闭间隙锁,就是把事务隔离级别设置成 RC,
并且把 innodb_locks_unsafe_for_binlog 设置为 ON。
这种情况下除了外键约束和唯一性检查会加间隙锁,其他情况都不会用间隙锁。
 
4.3 临键锁
当我们使用了范围查询,不仅仅命中了 Record 记录,还包含了 Gap 间隙,在这种情况下我们使用的就是临键锁。
临键锁是 MySQL 里面默认的行锁算法,相当于记录锁加上间隙锁。
 
临键锁退化的两种情况:
唯一性索引,等值查询匹配到一条记录的时候,就会退化成记录锁。
没有匹配到任何记录的时候,就会退化成间隙锁。
 
例如,表t3中只有id为1,4,7,10的四条数据:
我们使用>5 <9作为条件进行select....for update, 它包含了记录不存在的区间,也包含了一个 Record 7。
也就是会锁住(4,7]+(7,10]这个范围。
也就是锁住了5,6,7,8,9,10这些数据。正好是这个查询事物条件所包含的范围。
 
查询条件不包含10,那为什么10也会被被锁?
临键锁的规则是会锁住范围内存在keys中的最后一个key(此处也就是7) 的下一个左开右闭的区间(7,10],
 
 
select * from t2 where id > 5 and id <= 7 for update ; -- 锁住 (4,7] (7,10]
select * from t2 where id > 8 and id <= 10 for update ; -- 锁住 (7,10] (10,+∞)

临键锁解决了什么问题?

临建锁的这种设计正是解决了幻读的问题。

4.4 锁与隔离级别的实现

再回过头来看下这张图片,为什么 InnoDB 的 Repeatable Read级别能够解决幻读的问题,就是用临键锁实现的。
临键锁锁住了查询条件中最后一个存在的key之后的区间,使该不存在数据的区间无法被insert。自然就解决了幻读的问题。
 
最后总结一下四个事务隔离级别的实现:
 
Read Uncommited:不加锁
 
Read Commited:
Read Commited的隔离级别下,普通的select都是快照读,使用MVCC实现。加锁的 select 都使用记录锁。
除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用 间隙锁 封锁区间。所以 RC 会出现 幻读 的问题。
 
Repeatable Read:
RR 隔离级别下,普通的 select 使用 快照读(snapshot read) ,底层使用 MVCC 来实现。
加锁的 select(select ... in share mode / select ... for update)以及更新操作 update, delete 等语句使用 当前读(current read) ,底层使用 记录锁、或者间隙锁、临键锁
 
Serializable:
Serializable 所有的 select 语句都会被隐式的转化为 select ... in share mode,会
和 update、delete 互斥。

五、事物的隔离级别如何选择

RU 和 Serializable 肯定不能用。为什么有些公司要用 RC,或者说网上有些文章推荐使用 RC?
 
RC和RR主要的区别:
1.RR的间隙锁会导致锁定范围扩大
2.条件列未使用到索引时,RR锁表,RC锁行。
3.RC的“半一致性”(semi-consistent)读可以增加update操作的并发性。
在RC中,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本。
由MySQL上层判断此版本是否满足update的where条件。如果满足(需要更新),则MySQL会重新发起一次读操作,
此时会读取行的最新版本(并加锁)。
 
实际上,如果能正确的使用锁(避免不适用索引去加锁),只锁定需要的数据,用默认的RR级别就可以了。
 
在使用锁的时候,有一个问题是需要注意和避免的。
排它锁有互斥的特性,一个事物或者说一个线程持有锁的时候,会阻止其他线程获取锁,
这个时候会造成阻塞等待,如果循环等待,就有可能造成死锁。
 
 

六、死锁

6.1 锁的释放和阻塞

Q : 锁什么时候会被释放?

A : 事物结束(commit,rollback);客户端连接断开。

如果一个事物一直未释放锁,其他事物会被阻塞多久?会不会一直等待下去?

如果是,在并发访问高的情况下,打了的事物因无法立即获得需要的锁而挂起,会造成严重的性能问题,升值拖垮数据库。

MySQL中有一个参数来控制获取锁的等待时间,默认50 秒:

show VARIABLES like 'innodb_lock_wait_timeout' ;

对于死锁,无论等多久都不能获取到锁,这种情况也会要等待50秒吗?

先看一下什么时候会发生死锁。

6.2 死锁的发生和检测

死锁演示:

Session 1
Session 2
begin;
select * from t2 where id =1 for update;
 
 
begin;
delete from t2 where id =4 ;
update t2 set name= '4d' where id =4 ;
 
 
delete from t2 where id =1 ;

第一个事物获取了id=1这行数据的排它锁,第二个事物获取了id=4这行数据的排它锁。

之后第一个事物想获取已经被第二个事物获取的id=4这行数据的排它锁,获取不到,保持阻塞。

第二个事物又想获取已经被第一个事物获取的id=1这行数据的排它锁,也后去不到,保持阻塞。

由于两个事物都获取不到向下执行所需要的锁,于是都无法向下执行结束事物,也就释放不了自身已经获得的锁,于是互相等待,形成死锁。

执行过程确实形成了死锁。但是最终结果是在第一个事务中,检测到了死锁,马上退出了,第二个事务获得了锁,不需要等待50 秒:
[Err] 1213 - Deadlock found when trying to get lock; try restarting transaction

这是因为在发生死锁时,InnoDB 一般都能通过算法(wait-for graph)自动检测到。

那么产生死锁的条件是什么?

因为锁本身是互斥的,同一时刻只有一个事物持有这把锁,其他事物需要在这个事物释放锁之后才能获得锁,而不可以强行剥夺,

当多个事物形成等待环路的时候,就会发生死锁。

6.3 查看锁信息(日志)

show status命令中包含了一些行锁的信息:

innodb_row_lock_current_waits: 当前正在等待锁定的数量

innodb_row_lock_time: 从系统启动到现在锁定的总时长(ms)

innodb_row_lock_time_avg: 每次等待所化平均时间

innodb_row_lock_time_max: 从系统启动到现在等待最长的一次所花的时间

innodb_row_lock_waits: 从系统启动到现在总共等待的次数

InnoDB还提供了三张表来分析事物和锁的情况:

SELECT * FROM information_schema.INNODB_TRX;-- 当前运行的所有事物,还有具体的语句

select * from information_schema.INNODB_LOCKS; -- 当前出现的锁
select * from information_schema.INNODB_LOCK_WAITS; -- 锁等待的对应关系
 
找出持有锁的事务之后呢?
如果一个事务长时间持有锁不释放,可以 kill 事务对应的线程 ID,也就是 INNODB_TRX 表中的 trx_mysql_thread_id,
例如执行 kill 4,kill 7,kill 8。
当然,死锁的问题不能每次都靠 kill 线程来解决,这是治标不治本的行为。
我们应该尽量在应用端,也就是在编码的过程中避免。

6.4 死锁的避免

1.操作多张表的时候,尽量以相同的顺序来访问(避免形成等待环路);

2,批量操作单张表的时候,先对数据进行排序(避免形成等待环路);

3,申请足够级别的锁,如果要操作数据,就申请排它锁;

4,尽量使用索引访问数据,避免没有where条件的操作,避免锁表;

5,使用等值查询而不是范围查询,避免间隙锁对并发的影响; 

发布了25 篇原创文章 · 获赞 4 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_41570691/article/details/105127163