参考
https://dev.mysql.com/doc/internals/en/innodb.html 《MySQL技术内幕:InnoDB存储引擎(第2版)》https://juejin.im/post/6844903974282362887 mysql锁机制及原理—锁的诠释
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问,并确保数据的完整性和一致性,实现事务的隔离性要求。
InnoDB实现了两种标准的行级锁:
共享锁(S Lock),允许事务读一行数据。语法为:select * from table lock in share mode。排他锁(X Lock),允许事务删除或更新一行数据。语法为:select * from table for update。排他锁和共享锁的兼容性:
InnoDB还支持多粒度(granular)锁定,允许事务同时存在行级锁和表级锁,这种种额外的锁方式,称为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次(如下图),意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁。
如果对最下层(最细粒度)的对象上锁,那么首先需要对粗粒度的对象上锁。 意向锁为表级锁,不会阻塞除全表扫描以外的任何请求。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。两种意向锁: 1)意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁; 2)意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁。 表级意向锁与行级锁的兼容性:
下面命令或表都可以查看当前锁的请求:
SHOW FULL PROCESSLIST; SHOW ENGINE INNODB STATUS; SELECT * FROM information_schema.INNODB_TRX; SELECT * FROM information_schema.INNODB_LOCKS; SELECT * FROM information_schema.INNODB_LOCK_WAITS;表INNODB_TRX的结构说明:
表INNODB_LOCKS的结构:
表INNODB_LOCK_WAITS的结构:
一致性的非锁定读(consistent nonlocking read)是指InnoDB通过多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时不会去等待行上锁的释放。而是去读取行的一个快照数据(之前版本的数据)。 一个行记录多个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。 非锁定的一致性读:
之所以称为非锁定读,因为不需要等待访问的行上X锁的释放。实现方式是通过undo段来完成。而undo用来在事务中回滚数据,快照数据本身没有额外的开销,也不需要上锁,因为没有事务会对历史数据进行修改操作。非锁定读机制极大地提高了数据库的并发性。 在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也不相同。 在事务隔离级别READ COMMITTED和REPEATABLE READ下,InnoDB使用非锁定的一致性读。但对快照数据的定义不相同。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。(这两种情况的快照数据定义没看出区别?) 注意,对于READ COMMITTED的事务隔离级别而言,从数据库理论的角度来看,其违反了事务ACID中的隔离性。如下表的例子。
InnoDB有如下3种行锁的算法:
Record Lock:单个行记录上的锁。总去锁住索引记录,如果表没有设置任何索引,会使用隐式的主键来进行锁定;Gap Lock:间隙锁,锁定一个范围,但不包含记录本身;Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。行的查询采用这种锁定算法。例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:
采用Next-Key Lock的锁定技术称为Next-Key Locking。其设计的目的是为了解决幻读问题(Phantom Problem)。Next-Key Lock是谓词锁(predict lock)的一种改进。还有previous-key locking技术。同样上述的索引10、11、13和20,若采用previous-key locking技术,那么锁定的区间为:
当查询的索引含有唯一属性时,会对Next-Key Lock进行优化。对聚集索引,将其降级为Record Lock。对辅助索引,将对下一个键值加上gap lock,即对下一个键值的范围为加锁。 Gap Lock的作用是为了阻止多个事务将记录插入到同一范围内,而这会产生导致幻读问题。 用户可以通过以下两种方式来显式地关闭Gap Lock:
将事务的隔离级别设置为READ COMMITTED将参数innodb_locks_unsafe_for_binlog设置为1上述设置破坏了事务的隔离性,并且对于replication,可能会导致主从数据的不一致。此外,从性能上来看,READ COMMITTED也不会优于默认的事务隔离级别READ REPEATABLE。
数据库在在高并发时,事物会出现三种异常问题。
脏读:在事物还没有提交前,修改的数据可以被其他事物所看到。不可重复读:在一个事物中使用相同的条件查询一条数据,前后两次查询所得到的数据不同,这是因为同时其他事物对这条数据进行了修改(已提交事物),第二次查询返回了其他事物修改的数据。幻读:在一个事物A中使用相同的条件查询了多条数据,同时其他事物添加或删除了符合事物A中查询条件的数据,这时候当事物A再次查询时候会发现数据多了或者少了,与前一次查询的结果不相同。注意:不可重复读与幻读很容易搞混,他们的区别在于:
不可重复读:是同一条记录(一条数据)的内容被其他事物修改了,关注的是update、delete操作一条数据的操作. 幻读:是查询某个范围(多条数据)的数据行变多或变少了,在于insert、delete的操作。
想要解决上述三种问题,只要利用所把并发操作变成串行操作就可以完美的解决上述三个问题了,但是这回牺牲掉高并发的性能优势,使数据库处理数据的速度变慢。
没有一种通用的方法可以完美的解决所有问题,所以SQL标准定义了四种隔离级别来分别解决上述的问题,在性能与问题直接找到一个适合的方案。
读未提交(READ UNCOMMITTED )读已提交(READ COMMITTED)可重复读(REPEATABLE READ)可串行化(SERIALIZABLE)不同隔离级别可以解决的的异常问题如下:
image.png
读未提交:在一个事物没有提交的情况下,其他事物可以看到该事物中对数据的修改。读已提交:在一个事物提交前,其他事物看不到该事物对数据的修改,有时也叫不可重复读,因为两次相同条件的查询,可能会得到不同的结果。可重复读:在同一个事物中按照相同的条件多次查询的结果都是相同。Innodb存储引擎通过多版本并发控制解决了幻读的问题。可串行化:将事物放到一个队列中按照顺序一个一个的执行,可以闭上三个异常问题,但是牺牲了并发的高性能。有两种方法可以改变当前会话的隔离级别
SET session TRANSACTION ISOLATION LEVEL Serializable; SET @@tx_isolation='read-committed';
参数可以为:
Read uncommittedRead committedRepeatable ReadSerializable查看当前会话的隔离级别
select @@tx_isolation;
image.png
这是Mysql的默认级别
设置当前会话的级别为最低的读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SET @@tx_isolation='read-committed';
使用上面的命令查看下是否已经修改了。
创建一个实例用的表
CREATE TABLE `heros_tmp` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `heros_tmp` (`id`, `name`) VALUES (1, '张飞');
开启两个Mysql的链接会话,命名为A和B。
A会话开启事物
begin; select * from hrefos_tmp; insert into heros_tmp values(NULL,'关羽');
image.png
B会话 查看heros_tmp表。
select * from hreos_tmp;
image.png
可以看到会话A添加了一条关羽的数据,并没有提交,但是在会话B中是可以看到这条关羽的数据。这就是脏读的问题。
升级隔离级别,解决脏读问题
在上面的表中可以看到,把隔离级别设置到读已提交以上的等级是可以解决脏读的问题。
接下里设置两个会话A、B为读已提交
SET @@tx_isolation='read-committed';
在会话A中开启事物,并插入一条数据
begin; select * from heros_tmp; insert into heros_tmp values(NULL,"刘备"); select * from heros_tmp;
image.png
可以看到会话A中的查询结果多了一条刘备的数据。
在会话B中查询heros_tmp表中的数据,看看是否可以看到刘备这条数据
select * from heros_tmp;
image.png
可以看到会话B,并没有读到会话A中事物未提交添加的数据。
在A会话中开启事物,查看id=1的数据
begin; select name from heros_tmp where id=1;
image.png
B会话开启事物对id=1的数据进行修改
begin; update heros_tmp set name='张翼德' where id=1; commit;
在A会话中再次查看id=1的数据,
image.png
升级隔离级别,解决不可重复读问题
修改两个会话的隔离级别为可重复读
SET session TRANSACTION ISOLATION LEVEL Repeatable Read;
会话A中,开启事物查询id=1的数据
begin; select name from heros_tmp where id=1;
image.png
会话B中,开启事物对id=1的数据进行修改后提交事物
begin; update heros_tmp set name='张飞' where id=1; commit; select name from heros_tmp where id=1;
image.png
已经修改成功了
最后会话A中,再次查询id=1的数据,看结果是不是变了。
image.png
会话A的查询结果和上一次查询一致,没有出现不可重复读的问题。
在会话A中查看heros_tmp表中的id>1的数据。
image.png
在会话B中插入一个数据
begin; insert into heros_tmp values(NULL,'吕布'); commit;
在会话A中再次查看表中的数据,会发现多了一个吕布的数据
image.png
前后多行数据的两次查询结果不同,出现了幻读问题
升级隔离级别,解决幻读问题
因为Innodb在可重复读级别使用了多版本并发解决了 幻读的问题,所以在Innodb引擎中不用把级别设置为可串行化就可以解决问题。
设置级别为可重复读
SET session TRANSACTION ISOLATION LEVEL Repeatable Read;
在会话A中开启事物,查询表中所有的数据。
begin; select name from heros_tmp where id > 1;
image.png
在会话B中开启事物,添加一条新数据并提交事物。
begin; insert into heros_tmp values(NULL,'孙权'); commit; select name from heros_tmp where id>1;
image.png
新数据写入成功
在会话A中查询,看是否可以读取到新加入的数据。
image.png
两次查询多行数据的结果一致,没有出现幻读的问题。
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。 解决死锁问题最简单的一种方法是超时,参数innodb_lock_wait_timeout用来设置超时的时间。 除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测,是一种更为主动的死锁检测方式。InnoDB也采用的这种方式。wait-for graph要求数据库保存以下两种信息:
锁的信息链表事务等待链表通过上述链表可以构造出一张图,图中若存在回路,就代表存在死锁,事务为图中的节点。在图中事务T1指向T2边的定义为:
事务T1等待事务T2所占用的资源;事务T1最终等待T2所占用的资源,也就是事务之间在等待相同的资源,而事务T1发生在事务T2的后面。示例:
对应的等待图(还有t4指向t1吧?):
可以发现存在回路(t1,t2),因此存在死锁。死锁检测采用深度优先的算法实现。若存在死锁,通常InnoDB选择回滚undo量最小的事务。 死锁应该非常少发生,若经常发生,则系统是不可用的。此外,死锁的次数应该还要少于等待,因为至少需要2次等待才会产生一次死锁。从纯数学的概率角度来分析,死锁发生的概率是非常小的。(分析过程,这里不展开讨论了) 死锁示例1:
死锁示例2: