MySQL事务原理分析
一 事务简介
事务是访问和更新数据库的程序执行单元,一个事务中可能包含一个或者多个SQL语句,这些语句要么都执行,要么都不执行。在MySQL的innodb下,如果没有显示指明事务,那么默认一条语句就是一个事务。
事务是为了将数据库从一种一致性状态转换为另一种一致性状态。在数据库提交事务的时候,可以确保要么所有的修改都已经保存,要么所有的修改都不保存。
事务控制的语句如下所示:
-- 显示开启事务
START TRANSACTION | BEGIN
-- 提交事务,并使得已对数据库做的所有修改持久化
COMMIT
-- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改
ROLLBACK
-- 创建一个保存点,一个事务可以有多个保存点
SAVEPOINT identifier
-- 删除一个保存点
RELEASE SAVEPOINT identifier
-- 事务回滚到保存点
ROLLBACK TO [SAVEPOINT] identifier
二 事务的ACID特征
事务具有原子性(A),一致性©,隔离性(I),持久性(D):
- 原子性:
事务是访问并更新数据库各种数据项的一个程序执行单元,是不可分割的工作单位,其操作要么都做(提交),要么都不做(回滚)。
事务需要通过undolog来实现回滚操作。undolog记录的是事务每步具体操作,当回滚时,回放事务具体操作的逆运算。 - 一致性:
一致性指事务将数据库从一种一致性状态转变为下一种一致性的状态,在事务执行前后,数据库完整性约束没有被破坏。
一个事务单元需要提交之后才会被其他事务可见。 - 隔离性:
事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,并发事务之间不会相互影响。 - 持久性:
事务提交后,事务DML操作将会持久化(写入 redolog 磁盘文件 哪一个页 页偏移值 具体数据);即使发生宕机等故障,数据库也能将数据恢复。redolog 记录的是物理日志;??
三 事务的隔离级别
ISO和ANIS SQL标准制定了四种事务隔离级别的标准,各数据库厂商在正确性和性能之间做了拖鞋,并没有严格遵循这些标准。MySQL的innodb默认支持的隔离级别是REPEATABLE READ。
这四个隔离级别分别是:
- READ UNCOMMITTED
读未提交。该级别下读不加锁,写加排他锁,写锁在事务提交或者回滚后释放锁。 - READ COMMITTED
读已提交(RC)。从该级别后支持MVCC(多版本并发控制),也就是提供一致性非锁定读,此时读取操作读取历史快照数据;该隔离级别下读取历史版本的最新数据,所以读取的是已提交的数据。 - REPEATABLE READ
可重复读,该级别下也支持MVCC,此时读取操作读取事务开始时的版本数据。 - SERIALIZABLE
可串行化。该级别下给读加了共享锁。所有事务都是串行化执行的。此时隔离级别最严苛。
各个级别下的加锁,是指调用相应的SQL语句就会自动加锁吗?不需要手动加锁?读取历史最新数据和读取操作读取事务开始时的版本数据有什么区别吗?
四 数据库锁
锁机制用于管理对共享资源的并发访问,用来实现事务的隔离级别。
4.1 锁的类型
MySQL中事务采用的锁是粒度锁,分别可以针对表(B+树)、页(B+树叶子节点)、行(B+树叶子节点当中某一段记录行)三种粒度加锁。什么时候给页加锁???
innodb中的锁分为四种,分别是共享锁(S)、排他锁(X)、意向共享锁(IS)、意向排他锁(IX)和自增锁。共享锁和排他锁都是行级锁,意向共享锁、意向排他锁和自增锁都是表级别的锁。
这四种锁的具体细节如下:
- 共享锁
事务读操作加的锁,对某一行加锁。 - 排他锁
事务删除或更新加的锁,对某一行加锁。在4种隔离级别下,都添加了排他锁,事务提交或事务回滚后释放锁。 - 意向共享锁
对一张表中的某几行加的共享锁。 - 意向排他锁
对一张表中某几行加的排他锁。 - AUTO-INC Lock
自增锁,是一种特殊的表级锁,发生AUTO_INCREMENT约束下的插入操作。采用的一种特殊的表锁机制。完成对自增长值插入的SQL语句后立即释放。在大数据量的插入会影响插入性能,因为另一个事务中的插入会被阻塞。从MySQL5.1.22开始提供一种轻量级互斥量的自增长实现机制,该机制提高了自增长值插入的性能。
轻量级互斥量的实现原理是啥??不锁表了??
排他锁和共享锁的区别是什么?
4.2 锁的兼容性
锁 | S | X | IS | IX | AI |
---|---|---|---|---|---|
S | 兼容 | 冲突 | 兼容 | 冲突 | 冲突 |
X | 冲突 | 冲突 | 冲突 | 冲突 | 冲突 |
IS | 兼容 | 冲突 | 兼容 | 兼容 | 兼容 |
IX | 冲突 | 冲突 | 兼容 | 兼容 | 兼容 |
AI | 冲突 | 冲突 | 兼容 | 兼容 | 冲突 |
由于innodb支持的是行级别的锁,意向锁并不会阻塞除了全表扫描以外的任何请求;
意向锁之间是互相兼容的;IS 只对排他锁不兼容;
当事务试图读或写某一条记录时,会先在表上加上意向锁,然后才在要操作的记录上加上读锁或写锁。
这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。
当想为某一行添加 S 锁,先自动为所在的页和表添加意向锁 IS,再为该行添加 S 锁;
当想为某一行添加 X 锁,先自动为所在的页和表添加意向锁 IX,再为该行添加 X 锁;
什么时候解锁? AUTO_INC 表锁是什么?和其他锁是什么关系?
4.3 锁算法
- record lock
记录锁,单个行记录上的锁 - gap lock
间隙锁,锁定一个区间,但不包含记录本身,是开区间。repeatable read级别及以上支持间隙锁。
如果repeatable read修改innodb_locks_unsafe_for_binlog = 0,那么隔离级别相当于退化未read committed。 - next-key lock
记录锁+间隙锁,锁定一个范围,并且锁住记录本身,是左开右闭区间。 - insert intention lock
插入意向锁,insert操作的时候产生;在多事务同时写入不同数据至同一索引间隙的时候,并不需
要等待其他事务完成,不会发生锁等待。
假设有一个记录索引包含键值4和7,两个不同的事务分别插入5和6,每个事务都会产生一个加在
4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
4.4 锁兼容性
锁 | GAP(持有) | Insert Intention(持有) | Record(持有) | Next-key(持有) |
---|---|---|---|---|
GAP(请求) | 兼容 | 兼容 | 兼容 | 兼容 |
Insert Intention(请求) | 冲突 | 兼容 | 兼容 | 冲突 |
Record(请求) | 兼容 | 兼容 | 冲突 | 冲突 |
Next-key(请求) | 兼容 | 兼容 | 冲突 | 冲突 |
横向:表示已经持有的锁。纵向:表示正在请求的锁。
一个事务已经获取了插入意向锁,对其他事务是没有任何影响的;
一个事务想要获取插入意向锁,如果有其他事务已经加了 gap lock 或 Next-key lock 则会阻塞。这个是重点,死锁之源;
4.5 锁的对象
行级锁是针对表的索引加锁,索引包括聚集索引和辅助索引。
表级锁是针对页或表进行加锁。
重点考虑 InnoDB 在 read committed 和 repeatable read 级别下锁的情况;
五 MySQL辅助工具
5.1 MVCC
MVCC 是一种多版本并发控制的方法,实现对数据库的并发访问。MVCC会保存某个时间点上的数据快照。这意味着不同的事务在同一个时间点看到的同一个表的数据可能是不同的,这可以用来实现一致性的非锁定读。非锁定读是指不需要等待访问的行上X锁的释放。
在 read committed 和 repeatable read下,innodb使用MVCC;然后对于快照数据的定义不同;
在 read committed 隔离级别下,对于快照数据总是读取被锁定行的最新一份快照数据;而在
repeatable read 隔离级别下,对于快照数据总是读取事务开始时的行数据版本;
这两种快照有什么区别??
5.2 redo
redo 日志用来实现事务的持久性;内存中包含 redo log buffer,磁盘中包含 redo log file。
当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操作完成才完成了事务的提交。
redo log 顺序写,记录的是对每个页的修改(页、页偏移量、以及修改的内容)。
在数据库运行时不需要对 redo log 的文件进行读取操作;只有发生宕机的时候,才会拿redo log进行恢复;
5.3 undo
undo 日志用来帮助事务回滚以及 MVCC 的功能。它存储在共享表空间中。
undo 是逻辑日志,回滚时将数据库逻辑地恢复到原来的样子,根据 undo log 的记录,做之前的逆运算;比如事务中有insert 操作,那么执行 delete 操作;对于 update 操作执行相反的 update 操作。
同时 undo 日志记录行的版本信息,用于处理 MVCC 功能;
六 MySQL异常状况
6.1 并发读异常
- 脏读
事务(A)可以读到另外一个事务(B)中未提交的数据,及事务A读到脏数据。
在读写分离的场景下,可以将slave节点设置为 READ UNCOMMITTED;此时脏读不影响,在slave上查询并不需要特别精准的返回值。
这是什么意思?为什么说读写分离的场景下不需要特别精准的返回值??slave是什么??为何这样可以比避免脏数据??? - 不可重复读
事务(A) 可以读到另外一个事务(B)中提交的数据;通常发生在一个事务中两次读到的数据是不一样的情况;不可重复读在隔离级别 READ COMMITTED 存在。一般而言,不可重复读的问题是可以接受的,因为读到已经提交的数据,一般不会带来很大的问题,所以很多厂商(如Oracle、SQL Server)默认隔离级别就是READ COMMITTED;
不可重复读在隔离级别 READ COMMITTED 存在,为什么在这个隔离级别下存在?? - 幻读
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。例如:以 name 为唯一键的表,一个事务中查询 select * from t where name =‘mark’; 不存在,接下来 insert into t(name) values (‘mark’); 出现错误,此时另外一个事务也执行了insert 操作;幻读在隔离级别 REPEATABLE READ 及以下存在;但是可以在REPEATABLE READ 级别下通过读加锁(使用next-key locking)解决; - 丢失更新
脏读、不可重复读、幻读都是一个事务写,一个事务读,由于一个事务的写导致另一个事务读到了不该读的数据;丢失更新是两个事务都是写;丢失更新分为提交覆盖和回滚覆盖;回滚覆盖数据库拒绝不可能产生,重点关注提交覆盖。
数据库不是有回滚覆盖的功能吗??为何是拒绝不可能产生??
前三种异常状况的区别:
脏读和不可重复读的区别在于,脏读是读取了另一个事务未提交的数据,而不可重复读是读取了另一个事务提交之后的修改;本质上都是其他事务的修改影响了本事务的读取;
不可重复读和幻读比较类似;不可重复读是两次读取同一条记录,得到不一样的结果;而幻读是两次读取同一个范围内的记录得到的结果集不一样(可能不同个数,也可能相同个数内容不一样,比如删除一行后又添加新行);不可重复读是因为其他事务进行了 update 操作,幻读是因为其他事务进行了 insert 或者 delete 操作。
隔离级别下并发读异常
隔离级别 | 回滚覆盖 | 脏读 | 不可重复读 | 幻读 | 提交覆盖 |
---|---|---|---|---|---|
READ UNCOMMITTED | 不存在 | 存在 | 存在 | 存在 | 存在 |
READ COMMITTED | 不存在 | 不存在 | 存在 | 存在 | 存在 |
REPEATABLE READ | 不存在 | 不存在 | 不存在 | 存在(手动加锁) | 存在(手动加锁) |
SERIALIZABLE | 不存在 | 不存在 | 不存在 | 不存在 | 不存在 |
6.2 并发死锁
死锁:两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象;MySQL 中采用 wait-for graph (等待图-采用非递归深度优先的图算法实现)的方式来进行死锁检测;
死锁的异常报错:deadlock found when trying to get lock;
- 相反加锁顺序死锁
不同表的加锁顺序相反或者相同表不同行加锁顺序相反造成死锁;其中相同表不同行加锁顺序相反造成死锁有很多变种,其中容易忽略的是给辅助索引行加锁的时候,同时会给聚集索引行加锁;同时还可能出现在外键索引时,给父表加锁,同时隐含给子表加锁;触发器同样如此,这些都需要视情况分析; - 锁冲突死锁
innodb 在 RR 隔离级别下,最常见的是插入意向锁与 gap 锁冲突造成死锁;主要原理为:一个事务想要获取插入意向锁,如果有其他事务已经加了 gap lock 或 Next-key lock 则会阻塞; - 死锁解决
对于顺序相反型,调整执行顺序;
对于锁冲突型,更换语句或者降低隔离级别; - 如何避免死锁
- 尽可能以相同顺序来访问索引记录和表;
- 如果能确定幻读和不可重复读对应用影响不大,考虑将隔离级别降低为RC;
- 添加合理的索引,不走索引将会为每一行记录加锁,死锁概率非常大;
- 尽量在一个事务中锁定所需要的所有资源,减小死锁概率;
- 避免大事务,将大事务分拆成多个小事务;大事务占用资源多,耗时长,冲突概率变高;
- 避免同一时间点运行多个对同一表进行读写的概率;