维基百科的定义:
事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由 一个有限的数据库操作序列构成。
这里面有两个关键点:
mysql 中的 InnoDB
1)原子性(Atomicity):意味着我们对数据库的一系列的操作,要么都是成功, 要么都是失败,不可能出现部分成功或者部分失败的情况。
以转账的场景为例,一个账户的余额减少,必然对应着另一个账户余额的增加。全部成功比较简单,问题是如果前面一个操作已经成功了,后面的操作失败了,怎 么让它全部失败呢?这个时候我们必须要回滚。
原子性,在InnoDB
里面是通过 undo log
来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用 undo log
来实现回滚操作。
2)隔离性(Isolation):有了事务的定义以后,在数据库里面会有很多的事务同时去操作我们的同一张表或者同一行数据,必然会产生一些并发或者干扰的操作。 我们对隔离性的定义,就是这些很多个的事务,对表或者行的并发操作,应该是透明的, 互相不干扰的。
比如两个人给我转账100,开启两个事务,都拿到了我账户的余额 1000,然后各自基于1000加100,最后结果是1100,就出现了数据混乱的问题。
3)持久性(Durability):操作,增删改,只要事务提交成功,那么结果就是永久性的,不可能因为数据库掉电、 宕机、意外重启,又变成原来的状态。这个就是事务的持久性。
持久性怎么实现呢?InnoDB崩溃恢复crash-safe
是通过什么实现的?
持久性是通过redo log
和double write buffer
(双写缓冲)来实现的,我们操作数据的时候,会先写到内存的buffer pool
里面,同时记录redo log
,如果在刷盘之前出现异常,在重启后就可以读取redo log
的内容,写入到磁盘,保证数据的持久性。
当然,恢复成功的前提是数据页本身没有被破坏,是完整的,这个通过双写缓冲保证。
需要注意的是,原子性,隔离性,持久性,最后都是为了实现一致性。
4)读一致性(Consistency)
增删改的语句会自动开启事务,当然是一条SQL 一 个事务。注意每个事务都是有编号的,这个编号是一个整数,有递增的特性。
如果要把多条SQL放在一个事务里面,就要手动开启事务。
手动开启事务有两种方 式:
begin
;start transactiono
那么怎么结束一个事务呢?结束也有两种方式:
rollback
,事务 结束。commit
,事务结束。InnoDB里面有一个autocommit
的参数(分为两个级别,session
级别和global
级别)。
show variables like 'autocommit';
它的默认值是ON。autocommit
这个参数是什么意思呢?是否自动提交。如果它的 值是true/on
的话,我们在操作数据的时候,会自动提交事务。
否则的话,如果我们把autocommit
设置成false/off
,那么数据库的事务就需要我 们手动地结束,用rollback
或者commit
。
还有一种情况,客户端的连接断开的时候,事务也会结束。
问题一:读到未提交的事务,脏读
问题二:同一个事务读到不同的数据(update/delete),不可重复读
问题三:同一个事务读到不同的数据(insert),幻读
总结:事务并发的三大问题其实都是数据读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
所以,美国国家标准协会(ANSI)制定了一个SQL标准,也就是说建议数据库厂商
都按照这个标准,提供一定的事务隔离级别,来解决事务并发的问题。这个SQL标准有 很多的版本,大家最熟悉的是SQL92标准。
我们来看一下 SQL92 标准的官网。
这里面有一张表格,里面定义了四个隔离级别,右边的 P1 P2 P3 就是代表事务并发的3个问题,脏读,不可重复读,幻读。Possible
代表在这个隔离级别下,
这个问题有可能发生,换句话说,没有解决这个问题。Not Possible
就是解决了这个问题。
InnoDB 支持的四个隔离级别和 SQL92
定义的完全一致,隔离级别越高,事务的并 发度就越低。唯一的区别就在于,InnoDB 在 RR 的级别就解决了幻读的问题。
也就是说,不需要使用串行化的隔离级别去解决所有问题,既保证了数据的一致性,又支持较高的并发度。
这个就是 InnoDB 默认使用 RR 作为事务隔离级别的原因。
如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?
总体上来说,我们有两大类的方案。
第一种,既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操 作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制Lock Based Concurrency Control (LBCC)
。
如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那 就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地 影响操作数据的效率。
所以我们还有另一种解决方案,如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的之前给它建立一个备份或者叫快照,后面再来读取这个快照 就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control (MVCC)
MVCC的原则:
一个事务能看到的数据版本:
一个事务不能看见的数据版本:
MVCC 的效果:我可以査到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。而在我这个事务之后新增的数据,我是查不到的。
所以我们才把这个叫做快照,不管别的事务做任何增删改查的操作,它只能看到第 一次查询时看到的数据版本。
我们有一个数据结构,把本事务心、活跃事务ID、当前系统最大事务ID存起来,这样才能实现判断。这个数据结构就叫 Read View
(可见性视图),每个事务都维护一个自己的Read View
有了这个数据结构以后,事务判断可见性的规则是这样的:
0、从数据的最早版本开始判断(undo log)
1、 数据版本的 trx_id = creator trx id,本事务修改’可以访问
2、 数据版本的 trx_id < min_trx_id (未提交事务的最小 ID ),说明这个版本在 生成 ReadView 已经提交,可以访问
3、 数据版本的 trx id > max_trx_id (下一个事务 ID ),这个版本是生成 ReadView 之后才开启的事务建立的,不能访问
4、 数据版本的 trx id在min trx id 和 max_trx_id 之间,看看是否在 m ids 中。 如果在,不可以。如果不在,可以。
5、 如果当前版本不可见,就找 undo log 链中的下一个版本。
注意:RR 中 Read View 是事务第一次査询的时候建立的。RC 的 Read View 是事务每次 查询的时候建立的。
需要注意,在InnoDB中,MVCC 和锁是协同使用的,这两种方案并不是互斥的。
第一大类解决方案是锁,锁又是怎么实现读一致性的呢?
InnoDB 和 MylSAM 支持的锁 的类型是不同的。MylSAM 只支持表锁,而 InnoDB 同时支持表锁和行锁。
表锁和行锁的区别:
我们可以看到,官网把锁分成了 8类。我们把前面的两个行级別的锁Shared and Exclusive Locks)
,和两个表级别的锁Intention Locks
称为锁的基本模式。
后面三个Record Locks
、Gap Locks
、Next-Key Locks
,我们把它们叫做锁的算法, 也就是分别在什么情况下锁定什么范围。
插入意向锁:是一个特殊的间隙锁。间隙锁不允许插入数据,但是插入意向锁允许 多个事务同时插入数据到同一个范围。比如(4,7),—个事务插入5, —个事务插入6,不 会发生锁等待。
**自增锁:**是一种特殊的表锁,用来防止自增字段重复,数据插入以后就会释放,不 需要等到事务提交才释放。如果需要选择更快的自增值生成速度或者更加连续的自增值, 就要通过修改自增锁的模式改变。
show variables like Innodb autoinc lock mode1;
Predicate Locks for Spatial Indexes 是5.7版本里面新增的一种数据类型的索引的 锁,
第一个行级别的锁就是我们在官网看到的Shared Locks
(共享锁),我们获取了一 行数据的读锁以后,可以用来读取数据,所以它也叫做读锁,注意不要在加上了读锁以 后去写数据,不然的话可能会出现死锁的情况。而且多个事务可以共享一把读锁。
共享锁的作用:因为共享锁会阻塞其他事务的修改,所以可以用在不允许其他事务 修改数据的情况
--- 手工加上一把读锁
select ............. ..... lock in share mode;
释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。
二个行级别的锁叫做Exclusive Locks
(排它锁),它是用来操作数据的,所以又 叫做写锁。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数 据的共享锁和排它锁。
排它锁的加锁方式有两种:
第一种是自动加排他锁,可能是同学们没有注意到的: 我们在操作数据的时候,包括增删改,都会默认加上一个排它锁。
还有一种是手工加锁,我们用一个 FOR UPDATE 给一行数据加上一个排它锁,这个 无论是在我们的代码里面还是操作数据的工具里面,都比较常用。
释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。释放锁的方式跟前面是一样的。
意向锁是什么呢?我们好像从来没有听过,也从来没有使用过,其实他们是由数据库自己维护的。
也就是说:
反过来:
意向锁跟意向锁是不冲突的,意向锁跟行锁也不冲突。
那么这两个表级别的锁存在的意义是什么呢?
如果说没有意向锁的话,当我们准备给一张表加上表锁的时候,我们首先要做什么? 是不是必须先要去判断有没其他的事务锁定了其中了某些行?如果有的话,肯定不能加 上表锁。那么这个时候我们就要去扫描整张表才能确定能不能成功加上一个表锁,如果 数据量特别大,比如有上千万的数据的时候,加表锁的效率特別低。
但是我们引入了意向锁之后就不一样了。我只要判断这张表上面有没有意向锁,如 果有,就直接返回失败。如果没有,就可以加锁成功。所以 InnoDB 里面的表锁,我们 可以把它理解成一个标志。就像火车上卫生间有没有人使用的灯,让你不用去推门,是 用来提高加锁的效率的。
锁是用来解决事务对数据的并发访问的问题的。
InnoDB 的行锁,就是通过锁住索 引来实现的。这个时候问题就来了,
1、为什么表里面没有索弓I的时候,锁住一行数据会导致锁表?或者说,如果锁住的是索引,一张表没有索引怎么办?所以,一张表有没有可能没有索引?
所以,为什么锁表,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索弓I都锁住了。
2、为什么通过唯一索引给数据行加锁,主键索引也会被锁住?
唯一索引是属于辅助索引。
主键索引里面除了索引之外,还存储了完整的数据。所以我们通过辅助索引锁定 一行数据的时候,它跟我们检索数据的步骤是一样的,会通过主键值找到主键索引,然 后也锁定。
本质上是因为锁定的是同一行数据,是相互冲突的。
主键索引:存储索引和数据
辅助索引:存储索引和主键值
假设 test 这张表有一个主键索引,前面我们已经见过了。
我们插入了 4行数据,主键id分别是1、4、7、*10。
因为我们用主键索弓I加锁,我们这里的划分标准就是主键索引的值。
1)这些数据库里面存在的主键值,我们把它叫做 Record,记录,那么这里我们就有4个 Record
2)根据主键,这些存在的 Record 隔开的数据不存在的区间,我们把它叫做 Gap 间隙,它是一个左开右开的区间。
假设我们有 N 个 Record,那么所有的数据会被划分成多少个 Gap 区间?
答案是N +1,就像我们把一条绳子砍N刀,它最后肯定是变成N +1段。
3)最后一个,间隙(Gap)连同它左边的记录(Record),我们把它叫做临键的区间,它是一个左开右闭的区间。再重复一次,是左开右闭。
整型的主键索引,它是可以排序,所以才有这种区间。如果我的主键索引不是整形,是字符怎么办呢?
任何一个字符集,都有相对应的排序规则:
第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,
精准匹配到一条记录的时候,这个时候使用的就是记录锁。
第二种情况,当我们査询的记录不存在,没有命中任何一个 record ,无论是用等值
査询还是范围查询的时候,它使用的都是间隙锁。
重复一遍,当査询的记录不存在的时候,使用间隙锁。
注意,间隙锁主要是阻塞插入 inserto 相同的间隙锁之间不冲突。
第三种情况,当我们使用了范围査询,不仅仅命中了 Record记录,还包含了 Gap 间隙,在这种情况下我们使用的就是临键锁,它是 MySQL 里面默认的行锁算法,相当于记录锁加上间隙锁。
唯一性索引,等值査询匹配到一条记录的时候,退化成记录锁。
没有匹配到任何记录的时候,退化成间隙锁。
Next-key Lock = Gap Lock + Record Lock
临键锁,锁住最后一个 key 的下一个左开右闭的区间。解决了幻读的问题。
为什么InnoDB的RR级别能够解决幻读的 问题,就是用临键锁实现的。
再回过头来看下这张图片,这个就是MySQL InnoDB里面事务隔离级别的实现。
最后我们来总结一下四个事务隔离级别的实现:
RU隔离级别:不加锁。
Serializable 所有的 select 语句都会被隐式的转化为 select… in share mode ,会和 updates delete 互斥。
这两个很好理解,主要是RR和RC的区别
RR隔离级别下,普通的 select 使用快照读(snapshot read),底层使用 MVCC 来实现。
加锁的 select(select … in share mode / select … for update) 以及更新操作 update, delete等语句使用当前读(current read),底层使用记录锁、或者间隙锁、 临键锁。
RC 隔离级别下,普通的 select 都是快照读,使用 MVCC 实现。 加锁的select都使用记录锁,因为没有 Gap Lock。
除了两种特殊情况:外键约束检査( foreign-key constraint checking )以及重复键检査(duplicate-key checking)时会使用间隙锁封锁区间。
所以RC会出现幻读的问题。
RU 和 Serializable 肯定不能用。有些公司要用 RC 呢?
RC 和 RR 主要有几个区别:
在 RC 中,一个 update 语句,如果读到一行已经加锁的记录,此时 InnoDB 返回记 录最近提交的版本,由 MySQL 上层判断此版本是否满足 update 的 where 条件。若满足(需要更新),则 MySQL 会重新发起一次读操作,此时会读取行的最新版本(并加锁)。
实际上,如果能够正确地使用锁(避免不使用索引去枷锁),只锁定需要的数据,用默认的RR级别就可以了。
在我们使用锁的时候,有一个问题是需要注意和避免的,我们知道,排它锁有互斥的特性。一个事务或者说一个线程持有锁的时候,会阻止其他的线程获取锁,这个时候 会造成阻塞等待,如果循环等待,会有可能造成死锁。
锁的释放:
如果一个事务一直未释放锁,其他事务会被阻塞 50 秒。MySQL有一个参数来控制获取锁的等待时间,默认是50秒。
show VARIABLES like 'Innodb_lock_wait_timeout';
对于死锁,是无论等多久都不能获取到锁的,这种情况,也需要等待50秒钟吗?那 不是白白浪费了 50秒钟的时间吗?
我们先来看一下什么时候会发生死锁。
死锁演示: 案例1
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; |
案例2
Session 1 | Session 2 |
---|---|
begin; select * from tl where id =1 lock in share mode; | |
begin; select * from tl where id =1 lock in share mode; | |
update tl set name= ‘la’ where id =1; | |
update tl set name= ‘la’ where id =1; |
我们看到:在第一个事务中,检测到了死锁,马上退岀了,第二个事务获得了锁, 不需要等待50秒:
[Err] 1213 ・ Deadlock found when trying to get lock; try restarting transaction
为什么可以直接检测到呢?是因为死锁的发生需要满足一定的条件,所以在发生死锁时,InnoDB —般都能通过算法wait-for graph
自动检测到。
那么死锁需要满足什么条件?死锁的产生条件,因为锁本身是互斥的:
理发店有两个总监。一个负责剪头的 Tony 老师,一个负责洗头的 Kelvin 老师。 Tony老师不能同时给两个人剪头,这个就叫
互斥
。Tony 在给别人在剪头的时候,你不能让他停下来帮你剪头,这个叫不能
强行剥夺
。如果 Tony 的客户对 Kelvin 说:你不帮我洗头我怎么剪头? Kelvin 的客户对 Tony 说:你不帮我剪头我怎么洗头?这个就叫
形成等待环路
。
实际上,发生死锁的情况非常多,但是都满足以上3个条件。
这个也是表锁是不会发生死锁的原因*,*因为表锁的资源都是一次性获取的。
如果锁一直没有释放,就有可能造成大量阻塞或者发生死锁,造成系统吞吐量下降, 这时候就要查看是哪些事务持有了锁。
首先,SHOW STATUS
命令中,包括了一些行锁的信息:
show status like 'innodb row lock';
结果如下:
SHOW
命令是一个概要信息。InnoDB 还提供了三张表来分析事务与锁的情况:
—--当前运行的所有事务,还有具体的语句
select * from information schema.INNODB TRX;
---当前出现的锁
select * from information schema.INNODB LOCKS;
---锁等待的对应关系
selectfrom information schema.INNODB LOCK WAITS;
更加详细的锁信息,开启标准监控和锁监控:
set GLOBAL innodb_status_output=ON;
set GLOBAL innodb_status_output_locks=ON;
通过分析锁日志,找出持有锁的事务之后。如果一个事务长时间持有锁不释放,可以kill
事务对应的线程 ID ,也就是INNODB TRX
表中的trx_mysqI_thread_id
,例如执行kill 4, kill 7, kill 8
当然,死锁的问题不能每次都靠kill
线程来解决,这是治标不治本的行为。我们应该尽量在应用端,也就是在编码过程中避免。
更加详细的锁信息,开启标准监控和锁监控:
set GLOBAL innodb_status_output=ON;
set GLOBAL innodb_status_output_locks=ON;
通过分析锁日志,找出持有锁的事务之后。如果一个事务长时间持有锁不释放,可以kill
事务对应的线程 ID ,也就是INNODB TRX
表中的trx_mysqI_thread_id
,例如执行kill 4, kill 7, kill 8
当然,死锁的问题不能每次都靠kill
线程来解决,这是治标不治本的行为。我们应该尽量在应用端,也就是在编码过程中避免。