目录
0 引言
针对mysql事务相关知识的总结,分为事务,MVCC和锁三个部分
1 事务与其ACID特性
事务是并发控制的基本单位,是一系列对于数据库的操作,这些操作要么全部成功,要么全部失败,事务由4大特性,分别是原子性,一致性,隔离性和持久性
- 原子性:一个事务中的操作要么全部成功,要么全部失败
- 一致性:事务执行前后,必须保证从一个一致性状态到另一个一致性状态
- 隔离性:一个事务中执行的操作对于和他并发执行的事务来说是不可见的
- 持久性:一个事务结束后,其对数据库的修改就是永久的,即使发生系统崩溃,其数据也是能够恢复的
2 并发事务可能带来的问题
事务并发执行时,可能带来脏写、脏读、不可重复读和幻读的问题,这几类问题的具体介绍如下:
- 脏写:一个事务修改了另一个事务未提交的修改数据
- 脏读:一个事务读取到了另一个事务未提交的修改数据
- 不可重复读:一个事务按一定的搜索条件搜索到一定的数据后,其他事务对这些事务做了修改,导致前一个事务再次以相同的条件搜索数据时得到的结果不一样
- 幻读:一个事务读取到了另外一个事务插入但还没有提交的数据
3 事务的隔离级别
事务的隔离级别表示了事务对于上述四个问题的容忍程度,隔离级别越高,对上述四个问题的容忍程度越低,在mysql数据库中有以下四个隔离级别:
- 读未提交:脏读,不可重复读,幻读均可能发生
- 读已提交:脏读不会发生,不可重复读和幻读均可能发生
- 可重复读:脏读,不可重复读不可能发生,幻读可能发生
- 可串行化:脏读,不可重复读和幻读都不可能发生
对于以上四种隔离级别,脏写都不可能发生。
事务隔离级别的设置语句如下:
事务隔离级别的设置:
-
设置全局隔离级别
set global transaction isolation level (隔离级别)
-
设置会话隔离级别
set session transaction isolation level (隔离级别)
4 多版本并发控制MVCC
可重复读是通过MVCC机制实现的,MVCC机制又是依靠版本链和ReadView(一致性视图)实现的,在可重复读的隔离级别,在事务开启时就会生成一个ReadView。(针对的是普通的SELECT语句)
-
版本链
各个事务对于每条记录的修改操作会通过记录的roll_pointer指针和undo日志构成一个版本链,这个版本链中就记录了各个事务对该记录的操作信息。
-
ReadView
生成readview实际上就是给当前事务创建了一个当前正在活跃的读写事务的列表。列表中有三个重要信息,一个是当前事务的id,当前正在活跃的最小事务id(min),和生成readview时,系统应该分配给下一个事务的id(max)。所以事务列表是一个[min, max)区间。有了readview后,当前事务访问某条记录时,就需要根据以下步骤来判断记录的某个版本是否可见:
-
如果被访问记录的事务id值和当前事务id相同,也就是说当前事务正在访问它自己修改过的记录,所以该版本对当前事务可见
-
若访问记录的事务id值小于min,则表示该事务已经提交了,所以该版本的记录时可见的
-
若访问记录的事务id大于等于max,则表示生成该版本的事务在生成readview后才开启,故对当前事务是不可见的
-
若访问的版本的事务id在[min, max)区间内,则需要判断当前版本的事务id是都在事务列表中,若在则表示在生成readview时生成该版本的事务是活跃的,即该版本不可以被访问,若不在则表示生成该版本的事务已经提交,该版本是可见的
若某个版本对某个事务不可见,则按照版本链找到下一个版本的记录后再进行如上步骤直到版本链末尾,若最后还不可见则表示这条记录对当前事务是不可见的。
-
-
二级索引与MVCC
对于二级索引,就没有版本链了,但是每个二级索引页面中记录了一修改该页面的最大事务ID,若访问二级索引的事务的readview的min大于该页面的最大事务id,则该页面的所有记录对于当前事务可见,否则需要执行回表后取查看当前版本是否可见。
注意点
-
事务ID分配的时机,并不是一开启事务就分配事务ID,而是在当前事务执行修改操作的时候才会为当前事务分配事务id,若还未执行修改操作,则默认给的事务id是0。
-
可重复读隔离级别下是在事务执行第一次select操作的时候就生成一个readview,后面就一直使用它;读已提交在每次执行select时都会生产一个readview
-
mvcc是针对普通的select语句,也就是一致性读语句来说的;对于锁定读或者说当前读读取的都是最新修改的数据
版本可见性判断
-
一个事务自己的修改版本都是可见的
-
版本未提交,不可见
-
版本已提交,但是在创建视图之后提交的,不可见
-
版本已提交,且是在创建视图之前提交的,可见
5 锁
锁的种类
我们的Innodb引擎支持被表级锁和行级锁,而其他的存储引擎只支持表级锁。下面针对Innodb引起说一下表级锁和行级锁:
-
表级锁
-
S/X锁
S共享锁,X独占锁
只有S锁和S锁之间兼容,其他组合均不兼容
-
IS/IX锁
-
IS意向共享锁,当某个事物想读取表中的某条记录的时候会给表加上一个IS锁
-
IX意向独占锁,当某个事务想修改表中的某条记录的时候会给表加上一个IX锁
引入IS/IX锁的目的:当我们要给表加X锁时我们需要直到是否有一条记录被加上了S锁或者X锁,所以这是我们需要扫描所有的记录来查看是否满足条件,当引入意向锁后,当我们在对记录加S锁或者X锁的时候会给表加上一个IS或IX锁,这样后面想要加表级锁的事务就不需要区遍历所有记录看是否有记录被加锁了
-
-
AUTO_INC锁
当我们给字段添加上AUTO_INCREMNET属性时,在插入数据时会给该字段进行自增。要实现这个递增过程不会产生相同的id就会使用到锁,共有以下两种实现:
-
当我们知道具体要求插入多少数据时,会采用一个轻量级锁,我们在获取该列的id之前获取这个锁,得到id后就释放这个锁
-
当我们并不清楚需要插入的数据有多少时,就会采用一个AUTO_INC锁,这是一个表级锁,在执行插入语句的时候就会给整个表加上该锁,然后其他插入语句都要被阻塞,待该插入语句执行完后释放该表级锁
-
-
-
行级锁
-
record lock 记录锁,锁的是一个记录
-
gap lock 间隙锁,锁住的是一个开区间,区间内不能执行插入操作
-
next-key lock,锁住的是一个左开右闭的区间(a, b](这也是我们加锁的基本单位)
-
insert intention lock,插入意向锁,当某个插入语句在遇到gap锁阻塞后会会改等待中的事务生成一个插入意向锁,表示该事务向在某个间隙插入新纪录。该锁并不会阻止别的事务继续获取该记录上的任何类型的锁
-
隐式锁
隐式锁并不是一个锁结构,而是利用事务ID来延迟锁结构生成的。来看一个场景:事务A给表中插入了一条记录(此时并没有上锁);事务B立刻锁定读该记录,若不做处理,拿这条记录就会被事务B读取到,就可能出现脏读。所以我们是需要在事务A新插入的记录上加锁的,但是并不是插入记录的时候就添加,而是需要做一个判断,若新插入的记录上的trx_id不是一个活跃事务的id,则可以直接读取,否则先给事务A在该记录上创建一个X锁,is_waiting属性是false,然后再给自己创建一个X锁,is_waiting属性是true,之后事务B就进入等待状态
-
锁的内存结构
锁是内存中的一种结构,主要由以下部分组成
-
锁所在的事务信息
-
索引信息
-
表/行锁信息
-
type_mode锁的类型
-
其他信息
并不是每条记录都要有对应的锁结构,有些锁结构是可以合并的,也就是一个锁结构中记录了多条记录的锁信息,能够合并的锁结构需要满足以下条件
-
在同一个事务中
-
在同一个页面中
-
属于同一种类型的锁
-
等待状态是一样的
加锁的机制
innodb加锁的过程可以概括如下几条规则
-
只有扫描到的记录才会加锁
-
加锁的基本单位是next-key锁,也就是一个左开右闭的区间
-
对于唯一索引,在做等值查询时next-key会退化为record lock
-
对于普通索引,在扫描到第一个不符合条件的记录时会退化为gap lock
-
对于唯一索引,在进行范围查询时,在扫描到第一个不符合条件的记录时会退化为gap lock,这也可以称为一个bug
加锁分析是有一个注意点,我们页面内是有两个默认的记录的,即最大记录和最小记录