重点:
1.事务是什么
2.acid特性
3.隔离级别
4.实现:mvcc锁,undolog,redolog
5.并发异常,读异常 。 死锁(怎么加锁)
一、事务
目的
事务将数据库从一种一致性状态转换为另一种一致性状态
组成
事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成
mysql的innodb中,单条SQL语句是隐含事务的。
多条语句需要手动开启事务
特征
在数据库提交事务时,可以确保要么所有修改都已经保存,要么所有修改都不保存;
事务是访问并更新数据库各种数据项的一个程序执行单元。
在 MySQL innodb 下,每一条语句都是事务;可以通过 set autocommit = 0; 设置当前会话手
动提交;
通过外键关联子表和父表,进行相应的事务处理,保证数据一致性
事务空间语句
-- 显示开启事务 START TRANSACTION | BEGIN -- 提交事务,并使得已对数据库做的所有修改持久化 COMMIT -- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改 ROLLBACK -- 创建一个保存点,一个事务可以有多个保存点 SAVEPOINT identifier -- 删除一个保存点 RELEASE SAVEPOINT identifier -- 事务回滚到保存点 ROLLBACK TO [SAVEPOINT] identifier
注意:上面只是列举出所有事务语句,并不是顺序执行的
事务未COMMIT可以回滚,COMMIT之后不可以回滚 ,也就是说要么回滚要么提交。
使用rollback就会回到开启事务之前
还可以创建一个保存点savepoint,通过rollback回滚事务到保存点
一般常用的是前三个语句
开启事务,提交事务,回滚事务
二、ACID特性
原子性(A)
事务操作要么都做(提交),要么都不做(回滚);事务是访问并更新数据库各种数据项的一个程
序执行单元,是不可分割的工作单位;通过 undolog 来实现回滚操作。undolog 记录的是事务每
步具体操作,当回滚时,回放事务具体操作的逆运算;
undolog会记录回滚,比如在事务中有一条语句插入,如果使用rollback回滚,会自动执行删除一行的语句。(执行相反的操作)
undolog存储在共享表空间中
隔离性(I)
事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,并发事务之间不会相互
影响,设定了不同程度的隔离级别,通过适度破环一致性,得以提高性能;通过 MVCC 和 锁来实
现;MVCC 时多版本并发控制,主要解决一致性非锁定读,通过记录和获取行版本,而不是使用
锁来限制读操作,从而实现高效并发读性能。锁用来处理并发 DML 操作;数据库中提供粒度锁的
策略,针对表(聚集索引B+树)、页(聚集索引B+树叶子节点)、行(叶子节点当中某一段记录
行)三种粒度加锁;
程序是并发运行的,每条连接都有个线程,那么必然会出现同时执行的语句,让它们互不影响。
为了实现并发性能,使得并发性能最高,因而设定了不同程度的隔离级别,隔离级别越高并发性能越低,隔离级别越低并发性能越高。
比如,对一颗红黑树整棵树加锁,它的隔离级别高,同时一个线程在操作的时候,其他线程没法操作,因此并发性能低。但如果只对某个节点加锁,它的隔离级别低,其他线程也能操作其他节点,因此并发性能高。
mvcc为了提高性能,不采用加锁,而是使用记录多版本
或者使用 不同粒度加锁,提升并发性能
持久性(d)
事务提交后,事务DML操作将会持久化(写入 redolog 磁盘文件 哪一个页 页偏移值 具体数
据);即使发生宕机等故障,数据库也能将数据恢复。redolog 记录的是物理日志;
一致性(c)
一致性指事务将数据库从一种一致性状态转变为下一种一致性的状态,在事务执行前后,数据库完
整性约束没有被破坏;一个事务单元需要提交之后才会被其他事务可见。例如:一个表的姓名是唯
一键,如果一个事务对姓名进行修改,但是在事务提交或事务回滚后,表中的姓名变得不唯一了,
这样就破坏了一致性;一致性由原子性、隔离性以及持久性共同来维护的。
三、隔离级别
ISO 和 ANIS SQL 标准制定了四种事务隔离级别的标准,各数据库厂商在正确性和性能之间做了妥
协,并没有严格遵循这些标准;MySQL innodb默认支持的隔离级别是 REPEATABLE READ
写是都要上锁的
只有最高级别隔离,读才需要加锁
当mysql出现读异常,死锁问题,第一步就要考虑到隔离级别问题
READ UNCOMMITTED
读未提交;该级别下读不加锁,写加排他锁,写锁在事务提交或回滚后释放锁;
(orcal和sql server的默认隔离级别)
最低的隔离级别:
但是会出现很多读异常
READ COMMITTED
读已提交(RC);从该级别后支持 MVCC (多版本并发控制),也就是提供一致性非锁定读;此时
读取操作读取历史快照数据;该隔离级别下读取历史版本的最新数据,所以读取的是已提交的数
据;
REPEATABLE READ
可重复读(RR);该级别下也支持 MVCC,此时读取操作读取事务开始时的版本数据;
(mysql中的默认隔离级别)
1)RC隔离级别时,事务中的每一条select语句会读取到他自己执行时已经提交了的记录,也就是每一条select都有自己的一致性读ReadView;
2)而RR隔离级别时,事务中的一致性读的ReadView是以第一条select语句的运行时,作为本事务的一致性读snapshot的建立时间点的。只能读取该时间点之前已经提交的数据。
SERIALIZABLE
可串行化;该级别下给读加了共享锁;所以事务都是串行化的执行;此时隔离级别最严苛
命令
-- 设置隔离级别 SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 或者采用下面的方式设置隔离级别 SET @@tx_isolation = 'REPEATABLE READ'; SET @@global.tx_isolation = 'REPEATABLE READ'; -- 查看全局隔离级别 SELECT @@global.tx_isolation; -- 查看当前会话隔离级别 SELECT @@session.tx_isolation; SELECT @@tx_isolation; -- 手动给读加 S 锁 SELECT ... LOCK IN SHARE MODE; -- 手动给读加 X 锁 SELECT ... FOR UPDATE; -- 查看当前锁信息 SELECT * FROM information_schema.innodb_locks;
命令
GLOBAl代表设置全局的隔离级别,SESSION代表设置当前连接的隔离方式
-- 设置隔离级别 SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 或者采用下面的方式设置隔离级别 SET @@tx_isolation = 'REPEATABLE READ'; SET @@global.tx_isolation = 'REPEATABLE READ'; -- 查看全局隔离级别 SELECT @@global.tx_isolation; -- 查看当前会话隔离级别 SELECT @@session.tx_isolation; SELECT @@tx_isolation; -- 手动给读加 S 锁 SELECT ... LOCK IN SHARE MODE; -- 手动给读加 X 锁 SELECT ... FOR UPDATE; -- 查看当前锁信息 SELECT * FROM information_schema.innodb_locks;
四、锁
锁机制用于管理对共享资源的并发访问;用来实现事务的隔离级别 ;
锁类型
共享锁和排他锁都是行级锁;MySQL当中事务采用的是粒度锁;针对表(B+树)、页(B+树叶子
节点)、行(B+树叶子节点当中某一段记录行)三种粒度加锁;
意向共享锁和意向排他锁都是表级别的锁;
myisam中使用的是表锁,不会引起并发读异常和并发死锁,但是锁的粒度太大
使用行锁,对一行行数据加锁
共享锁(S锁)又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S 锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
排他锁(X锁)又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
共享锁(S)
事务读操作加的锁;对某一行加锁;
在 SERIALIZABLE 隔离级别下,默认帮读操作加共享锁;
在 REPEATABLE READ 隔离级别下,需手动加共享锁,可解决幻读问题;
在 READ COMMITTED 隔离级别下,没必要加共享锁,采用的是 MVCC;
在 READ UNCOMMITTED 隔离级别下,既没有加锁也没有使用 MVCC;
通常所说的读锁
在rc和rr两个隔离级别下默认是不加锁的,可以手动加S锁。
排他锁(X)
事务删除或更新加的锁;对某一行加锁;
在4种隔离级别下,都添加了排他锁,事务提交或事务回滚后释放锁;
通常所说的写锁
在删除、更新都是直接加上X锁,但是插入还会加入其他的锁
意向共享锁(IS)
对一张表中某几行加的共享锁
如果一张表中几行数据加了共享锁,那么整张表都会加意向共享锁,这样可以将整张表写 这种操作给排除在外。
意向排他锁(IX)
对一张表中某几行加的排他锁;
目的:为了告诉其他事务,此时这条表被一个事务在访问;作用:排除表级别读写锁 (全面扫描
加锁);
如果一张表中几行数据加了排他锁(X),那么整张表都会加意向排他锁,告诉其他事务,当前表有事务正在访问。
这样做的目的,可以将 整张表读或者整张表写 这种操作给排除在外。因此其他事务是先访问表结构的,可以知道意向排他锁,如果没有意向排他锁,就要一行一行遍历去判断,才知道有没有加锁,这样会导致效率低下。
比如delete from where number='isddddd'
其中number不是索引,因此要整张表加锁,遍历整张表。但是有意向排他锁,这条语句就会被阻塞等待。
锁的兼容性
注意下图中的S,X代表 表的全局扫描的读和写,会冲突。
锁 | S | X | IS | IX | AI |
S | 兼容 | 冲突 | 兼容 | 冲突 | 冲突 |
X | 冲突 | 冲突 | 冲突 | 冲突 | 冲突 |
IS | 兼容 | 冲突 | 兼容 | 兼容 | 兼容 |
IX | 冲突 | 冲突 | 兼容 | 兼容 | 兼容 |
AI | 冲突 | 冲突 | 兼容 | 兼容 | 冲突 |
由于innodb支持的是行级别的锁,意向锁并不会阻塞除了全表扫描以外的任何请求; 意向锁之间是互相兼容的; IS 只对排他锁不兼容;
当想为某一行添加 S 锁,先自动为所在的页和表添加意向锁 IS,再为该行添加 S 锁; 当想为某一行添加 X
锁,先自动为所在的页和表添加意向锁 IX,再为该行添加 X 锁;
当事务试图读或写某一条记录时,会先在表上加上意向锁,然后才在要操作的记录上加上读锁或写
锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。意向锁之间是 不会产生冲突的,也不和 AUTO_INC
表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁 也不会和行锁冲突,行锁只会和行锁冲突。
五、锁算法
Record Lock
记录锁,单个行记录上的锁
Gap Lock
间隙锁,锁定一个范围,但不包含记录本身;全开区间;REPEATABLE READ级别及以上支持间隙
锁;
如果 REPEATABLE READ 修改 innodb_locks_unsafe_for_binlog = 0 ,那么隔离级别相当于
退化为 READ COMMITTED;
-- 查看是否支持间隙锁,默认支持,也就是 innodb_locks_unsafe_for_binlog = 0; SELECT @@innodb_locks_unsafe_for_binlog;
只有在可重复读隔离级别下,才有gap lock。
gap锁是造成死锁的一个重要原因
rc隔离级别下没有gap锁
Next-Key Lock
记录锁+间隙锁,锁定一个范围,并且锁住记录本身;左开右闭区间;
只有在可重复读隔离界别下,才有next-key lock
Next-Key 锁是造成死锁的一个重要原因
Insert Intention Lock
插入意向锁,insert操作的时候产生;在多事务同时写入不同数据至同一索引间隙的时候,并不需
要等待其他事务完成,不会发生锁等待。
假设有一个记录索引包含键值4和7,两个不同的事务分别插入5和6,每个事务都会产生一个加在
4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
如果要在1和7,之间插入3和4。如果没有插入意向锁,那么插入3的时候会通过gap lock将1和7之间锁住,那么4会等待3插入完成,才会插入。如果有了插入意向锁,就可以让3和4并发插入。
目的:提升间隙插入的性能。
Insert Intention Lock和gap lock冲突,容易产生死锁。
AUTO-INC Lock(AI锁)
自增锁,是一种特殊的表级锁,发生在 AUTO_INCREMENT 约束下的插入操作;采用的一种特殊
的表锁机制(较低概率造成B+树分裂);完成对自增长值插入的SQL语句后立即释放;在大数据
量的插入会影响插入性能,因为另一个事务中的插入会被阻塞;从MySQL 5.1.22开始提供一种轻
量级互斥量的自增长实现机制,该机制提高了自增长值插入的性能;
往B+树末尾插入,有较低概率造成B+树分裂。因此加入表锁,性能很好(??为什么要加表锁)。
锁兼容
锁 | GAP(持有) | Insert Intention(持有) | Record(持有) | Next-key(持有) |
GAP(请求) | 兼容 | 兼容 | 兼容 | 兼容 |
Insert Intention(请求) | 冲突 | 兼容 | 兼容 | 冲突 |
Record(请求) | 兼容 | 兼容 | 冲突 | 冲突 |
Next-key(请求) | 兼容 | 兼容 | 冲突 | 冲突 |
(重要) 横向:表示已经持有的锁;纵向:表示正在请求的锁;
一个事务已经获取了插入意向锁,对其他事务是没有任何影响的;
一个事务想要获取插入意向锁,如果有其他事务已经加了 gap lock 或 Next-key lock 则会阻塞;
这个是重点,死锁之源;
这是大部分死锁的原因
mysql并发insert死锁问题——gap、插入意向锁冲突
六、锁对象
行级锁是针对表的索引加锁;索引包括聚集索引和辅助索引; 表级锁是针对页或表进行加锁; 重点考虑 InnoDB 在 read
committed 和 repeatable read 级别下锁的情况; 如下图 students 表作为实例,其中 id
为主键,no(学号)为辅助唯一索引,name(姓名)和 age(年龄)为二级非唯一索引,score(学分)无索引
没有索引,就是表级别的读写锁
查询未命中情况下,rc隔离级别,不加锁。rr隔离级别下加gap锁
首先会在辅助索引b+树这一行加X锁(因为是更新操作),同时也会在聚集索引b+树的这一行加上X锁。
这里rc和rr是一样的。
命中了就加行锁,没有命中就加gap锁
辅助索引未命中
在rr隔离级别下,要加gap 锁 (为了避免幻读)
rc隔离级别下, 辅助索引b+树对应行加x锁,相应的聚集索引对应行也加x锁
rr隔离级别下, 辅助索引b+树对应行加x锁,相应的聚集索引对应行也加x锁,在对应的行两侧和中间都要加上gap锁
rr下,按照字母排序的位置,插入gap锁,(主键索引是没有加锁的)
rr下无索引情况下,整张表都要遍历,都要加上行锁和间隙锁。rc下都加上行锁
rc下,对范围内的列加锁
rr下,除了对列加锁,还要加入gap锁。
有时候(有时加有时不加)还会加入(20,30]这边加入gap锁。
rc下对 指定范围加行锁,辅助索引和聚集索引都加
rr下,在加行锁的基础上,还要加上gap锁
总结:
主要看where里面的字段有没有走索引
如果用的辅助索引,rr下通常,只给辅助索引加gap锁,不会给主键索引加gap锁
修改name会走change buffer,对于非唯一索引,会走change buffer,然后异步刷入辅助索引b+树
这个是怎么加锁,先找到主键id=15再去找辅助索引吗,
七、MVCC
比如一个事务正在写该行数据,该行数据加了锁。另一个事务打算读该行数据,但是加锁了,因此就读历史版本的数据。
这两个mvcc只是读的历史版本不一样
在rc模式下,当前事务读取是最新的快照版本,因此如果另一个事务进行修改,后续,当前事务再去读取的时候,就会出现两次数据不一致,也就是不可重复度。
rc模式下,读取表的开始版本的行数据,比如读取两行连续的数据,因此如果另一个事务在两行之间进行插入,可能会造成,幻读。因此要使用gap锁
注意都是行版本,不是表版本
快照读就是读snapshot
当前读情况下,下面第一行SQL是加读锁(S),第二行是加写锁(X)
增删改都是默认当前读
八、redo
redo 日志用来实现事务的持久性;内存中包含 redo log buffer,磁盘中包含 redo log file;
当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操 作完成才完成了事务的提交; redo log
顺序写,记录的是对每个页的修改(页、页偏移量、以及修改的内容);在数据库运行 时不需要对 redo log
的文件进行读取操作;只有发生宕机的时候,才会拿redo log进行恢复;
九、undo
undo 日志用来帮助事务回滚以及 MVCC 的功能;存储在共享表空间中;undo 是逻辑日志,回滚
时将数据库逻辑地恢复到原来的样子,根据 undo log 的记录,做之前的逆运算;比如事务中有
insert 操作,那么执行 delete 操作;对于 update 操作执行相反的 update 操作
十、并发读异常
并发异常:读异常+死锁
异常的原因:
1.行锁
2.并发
脏读
事务(A)可以读到另外一个事务(B)中未提交的数据;也就是事务A读到脏数据;在读写分离的
场景下,可以将slave节点设置为 READ UNCOMMITTED;此时脏读不影响,在slave上查询并不
需要特别精准的返回值。
读到的是未提交的数据
解决:升级隔离级别
不可重复读
务(A) 可以读到另外一个事务(B)中提交的数据;通常发生在一个事务中两次读到的数据是不
一样的情况;不可重复读在隔离级别 READ COMMITTED 存在。一般而言,不可重复读的问题是
可以接受的,因为读到已经提交的数据,一般不会带来很大的问题,所以很多厂商(如Oracle、
SQL Server)默认隔离级别就是READ COMMITTED;
两次读取的数据不一样。这会影响到逻辑问题
因为rc隔离级别下,读取的是最新的快照。
解决方法:提升隔离级别至rr
一个事务内部,不需要隔离,修改走的是当前读,后面的select走的也是当前读
幻读
两次读取同一个范围内的记录得到的结果集不一样;例如:以 name 为唯一键的表,一个事务中
查询 select * from t where name = ‘mark’; 不存在,接下来 insert into t(name)
values (‘mark’); 出现错误,此时另外一个事务也执行了 insert 操作;幻读在隔离级别
REPEATABLE READ 及以下存在;但是可以在 REPEATABLE READ 级别下通过读加锁(使用nextkey locking)解决;
执行select的时候是快照读,其余是当前读,快照读的时候不会有幻读问题,但是当前读会有
如果两次都是读快照,那么不会出现幻读。
两次读取同一个范围内的记录得到的结果集不一样
加了in share mode就变成当前读了,和之前读取的数据不一样了,那么就是出现幻读了。
解决幻读:加gap锁
下面就是加了share mode,除了对执行的行加了读锁,还加了间隙锁,线程A中的插入就插不进去了
因为左边是要加写锁(X),右边已经加了读锁(S),因此左边的写锁不兼容读锁,要阻塞等待。
其他幻读的情况:比如这边变成了DML操作,当前读,也会产生幻读。
锁的持续范围:
update delete insert 在当前事务中都属于当前读 因为 mysql 是采用 B+ 树存储数据 修改都是属于就地更新 更新前需要先读出来,锁在事务结束时才会释放
丢失更新
脏读、不可重复读、幻读都是一个事务写,一个事务读,由于一个事务的写导致另一个事务读到了
不该读的数据;丢失更新是两个事务都是写;丢失更新分为提交覆盖和回滚覆盖;回滚覆盖数据库
拒绝不可能产生,重点关注提交覆盖
下面是提交覆盖,也就是说,线程b对money的提交被线程A后来提交的money给覆盖了,出现了提交覆盖
解决办法:
加入读锁,线程B就没法去使用 写锁(update部分),从而使得B阻塞
区别
脏读和不可重复读的区别在于,脏读是读取了另一个事务未提交的数据,而不可重复读是读取了另
一个事务提交之后的修改;本质上都是其他事务的修改影响了本事务的读取
可重复读和幻读比较类似;不可重复读是两次读取同一条记录,得到不一样的结果;而幻读是两
次读取同一个范围内的记录得到的结果集不一样(可能不同个数,也可能相同个数内容不一样,比
如删除一行后又添加新行);不可重复读是因为其他事务进行了 update 操作,幻读是因为其他
事务进行了 insert 或者 delete 操作。
隔离级别下并发读异常
重点:面试中
1.各种读异常的细微差异
2.怎么解决异常? 回答:隔离级别机制+解决方案
十一、并发死锁
死锁:两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象;
MySQL 中采用 wait-for graph (等待图-采用非递归深度优先的图算法实现)的方式来进行死锁检
测;
异常报错:deadlock found when trying to get lock;
比如下面这种情况,就会发生死锁,
相反加锁顺序死锁
不同表的加锁顺序相反或者相同表不同行加锁顺序相反造成死锁;其中相同表不同行加锁顺序相反
造成死锁有很多变种,其中容易忽略的是给辅助索引行加锁的时候,同时会给聚集索引行加锁;同
时还可能出现在外键索引时,给父表加锁,同时隐含给子表加锁;触发器同样如此,这些都需要视
情况分析;
调整加锁顺序
锁冲突死锁
innodb 在 RR 隔离级别下,最常见的是插入意向锁与 gap 锁冲突造成死锁;主要原理为:一个事
务想要获取插入意向锁,如果有其他事务已经加了 gap lock 或 Next-key lock 则会阻塞
查看死锁
系统表
-- 开启标准监控 CREATE TABLE innodb_monitor (a INT) ENGINE=INNODB; -- 关闭标准监控 DROP TABLE innodb_monitor; -- 开启锁监控 CREATE TABLE innodb_lock_monitor (a INT) ENGINE=INNODB; -- 关闭锁监控 DROP TABLE innodb_lock_monitor
系统参数
-- 开启标准监控 set GLOBAL innodb_status_output=ON; -- 关闭标准监控 set GLOBAL innodb_status_output=OFF; -- 开启锁监控 set GLOBAL innodb_status_output_locks=ON; -- 关闭锁监控 set GLOBAL innodb_status_output_locks=OFF; -- 将死锁信息记录在错误日志中 set GLOBAL innodb_print_all_deadlocks=ON;
命令
-- 查看事务 select * from information_schema.INNODB_TRX; -- 查看锁 select * from information_schema.INNODB_LOCKS; -- 查看锁等待 select * from information_schema.INNODB_LOCK_WAITS;
死锁解决
对于顺序相反型,调整执行顺序;
对于锁冲突型,更换语句或者降低隔离级别;
如何避免死锁
- 尽可能以相同顺序来访问索引记录和表;
- 如果能确定幻读和不可重复读对应用影响不大,考虑将隔离级别降低为RC;
- 添加合理的索引,不走索引将会为每一行记录加锁,死锁概率非常大;
- 尽量在一个事务中锁定所需要的所有资源,减小死锁概率;
- 避免大事务,将大事务分拆成多个小事务;大事务占用资源多,耗时长,冲突概率变高;
- 避免同一时间点运行多个对同一表进行读写的概率;