开发多用户、数据库驱动的应用时,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁Clocking) 的机制,同时这也是数据库系统区别于文件系统的一个关键特性。InnoDB 存储引擎较之MySQL 数据库的其他存储引擎在这方面技高一筹,其实现方式非常类似于Oracle 数据库。
————《MySQL技术内幕INNODB存储引擎》
- 什么是锁
- lock 与latch
- lnnoDB 存储引擎中的锁
- 锁的类型
- 一致性非锁定读(造成不可重复度的本质原因)
- 通过一致性非锁定读解析事务的不可重复读问题
- 一致性锁定读
- 自增长与锁
- 外键和锁
- 💖感谢各位的暴击三连~💖
什么是锁
ssdss 锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问,InnoDB 存储引擎会在行级别上对表数据上锁,这固然不错。不过InnoDB 存储引擎也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。
aassa注 1:数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。对于锁,用户可能对某个特定的关系数据库系统的锁定模型有一定的经验,但这并不意味着知道其他数据库.也就是说,它们各自对于锁的实现完全不同。
aassa注 2:对于MyISAM 引擎,其锁是表锁设计。并发情况下的读没有问题,但是并发插入时的性能就要差一些了,若插入是在“底部", MyISAM 存储引擎还是可以有一定的并发写入操作。
aassa注 3:对于 ===Microsoft SQL Server == 数据库,在Microsoft SQL Server 2005 版本之前其是 页锁 的,相对表锁的MyISAM 引擎来说并发性能有所提高。页锁容易实现,然而对于热点数据页的并发问题依然无能为力。到2005 版本, Microsoft SQL Server 开始支持乐观并发和悲观并发,在乐观并发下开始支持行级锁,但是其实现方式与InnoDB 存储引擎的实现方式完全不同。用户会发现在Microsoft SQL Server 下,锁是一种稀有的资源,锁越多开销就越大,因此它会有锁升级。在这种情况下,行 锁会升级到表锁,这时并发的性能又回到了以前。
aassa注 4:InnoDB 存储引擎锁的实现和Oracle 数据库非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。
lock 与latch
ssdss 在数据库中, lock 与latch 都可以被称为"锁"。但是两者有着截然不同的含义:
ssdsddsss①、latch 一般称为 门锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB 存储引擎中, latch 又可以分为 mutex (互斥量)和rwlock (读写锁)。其目的是用来保证并发线程操作 临界资源 的正确性,并且通常没有死锁检测的机制。
ssdsddsss②、lock 的对象是 事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit 或rollback 后进行释放(不同事务隔离级别释放的时间可能不同)。此外, lock, 正如在大多数数据库中一样,是有 死锁 机制的。
ssdss对于InnoDB 存储引擎中的latch, 可以通过命令SHOW ENGINE INNODB MUTEX 来进行查看:
aassa分析:列Type 显示的总是InnoDB, 列Name 显示的是latch 的信息以及所在源码的位置(行数)。列Status 比较复杂,在Debug 模式下,除了显示os_waits,还会显示count、spin_waits 、spin_rounds 、os_yields 、os_ wait_ times 等信息。
aassa相对于latch 的查看, lock 信息就显得直观多了。用户可以通过命令 SHOW ENGINEINNODB STATUS 及information_schema 架构下的表INNODB_TRX 、INNODB_ LOCKS 、INNODB_LOCK_ WAITS 来观察锁的信息。
lnnoDB 存储引擎中的锁
锁的类型
ssdss InnoDB 存储引擎实现了如下两种标准的行级锁:
ssds dsss①、共享锁 (S Lock):允许事务读一行数据。
ssds dsss②、排他锁 (X Lock): 允许事务删除或更新一行数据。
aassa注 5:S 和X 锁都是行锁,兼容是指对同一记录 (row) 锁的兼容性情况。ssdss InnoDB 存储引擎支持多粒度(granular) 锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作, InnoDB 存储引擎支持一种额外的锁方式,称之为意向锁 (lntention Lock) 。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity) 上进行加锁。
ssdss如下图:若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。如果需要对页上的记录r 进行上X 锁,那么分别需要对数据库A 、表、页上意向锁 IX, 最后对记录 r 上X 锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。
ssdssInnoDB 存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。 其支持两种意向锁:
ssds dsss①、意向共享锁 (IS Lock) :事务想要获得一张表中某几行的共享锁
ssds dsss②、意向排他锁 (IX Lock) :事务想要获得一张表中某几行的排他锁
aassa注 6:由于lnnoDB 存储引擎支持的是行级别的锁,因此 意向锁其实不会阻塞除全表扫以外的任何请求。故 表级意向锁与行级锁的兼容性如下:(只要有IX,和别的S,X都不兼容)
aassa注 7:在InnoDB 1 . 0 版本之前,用户只能通过命令SHOW FULL PROCESSLIST, SHOW ENGINE INNODB STATUS 等来查看当前数据库中锁的请求,然后再判断事务锁的情况。从InnoDBl.O 开始,在 INFORMATION_SCHEMA 架构下添加了表INNODB_TRX 、INNODB_LOCKS 、INNODB_LOCK_WAITS 。通过这三张表,用户可以更简单地监控当前事务并分析可能存在的锁问题。aassa首先看表 INNODB_TRX:
aassdsa注 8:该表只是显示了当前运行的InnoDB 事务,并不能直接判断锁的一些情况。如果需要查看锁,则还需要访问表INNODB_LOCKS。aassa表 INNODB_LOCKS 的结构:
aassdsa注 9:lock_data 这个值并非是“可信”的值(其实这里我不太理解,锁住记录吗(比行锁粒度还小)?)。例如当用户运行一个范围查找时, lock_data 可能只返回第一行的主键值。 与此同时,如果当前资源被锁住了,若锁住的页因为InnoDB 存储引擎缓冲池的容量,导致该页从缓冲池中被刷出,则查看INNODB_LOCKS 表时,该值同样会显示为NULL, 即InnoDB 存储引擎不会从磁盘进行再一次的查找。
aassdsa注 10:在通过表INNODB_LOCKS 查看了每张表上锁的清况后,用户就可以来判断由此引发的等待情况了。当事务较小时,用户就可以人为地、直观地进行判断了。但是当事务量非常大,其中锁和等待也时常发生,这个时候就不这么容易判断。但是通过表 INNODB_LOCK_ WAITS, 可以很直观地反映当前事务的等待。
aassa表 INNODB_LOCK_WAITS 由4 个字段组成:
aassdsa注 11:通过上述的SQL 语句,用户可以清楚直观地看到哪个事务阻塞了另一个事务。当然,这里只给出了事务和锁的ID。如果需要,用户可以根据表INNODB_TRX 、INNODB_LOCKS 、INNODB_LOCK_WAITS 得到更为直观的详细信息。
一致性非锁定读(造成不可重复度的本质原因)
ssdss== 一致性的非锁定读 ( consistent nonlocking read) 是指 InnoDB 存储引擎通过行多版本控制(multi versioning) 的方式来读取当前执行时间数据库中行的数据。==
ssdss如果读取的行正在执行DELETE 或UPDATE 操作,这时读取操作不会因此去等待行上锁的释放。相反地, InnoDB 存储引擎会去读取行的一个快照数据。如下图:
aassdsa注 12:之所以称其为非锁定读,因为不需要等待访问的行上X 锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo 段来完成。而undo 用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
ssdss非锁定读机制极大地提高了数据库的并发性。在InnoDB 存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。 但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
aassdsa注 13:快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control, MVCC) 。
aassdsa注14:在事务隔离级别READ COMMITTED 和REPEATABLE READ (InnoDB 存储引擎的默认事务隔离级别)下, InnoDB 存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同:
aassdsdsa①、在 READ COMMITTED 事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据(因此没有解决不可重复读)。
aassdsdsa②、在 REPEATABLE READ 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本(解决了不可重复读问题)。
通过一致性非锁定读解析事务的不可重复读问题
ssdss首先在当前MySQL 数据库的连接会话A 中执行如下SQL 语句:
# Session A
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT* FROM parent WHERE id= l ;
+----+
| id |
+----+
| 1 |
+----+
1 r ow in set (0.00 sec)
ssdss会话A 中已通过显式地执行命令BEGIN 开启了一个事务,并读取了表parent 中id为1 的数据,但是事务并没有结束。与此同时,用户再开启另一个会话B, 这样可以模拟并发的情况,然后对会话B 做如下的操作:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE parent SET id=3 WHERE id=1 ;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed : 1 Warnings : 0
ssdss在会话B 中将事务表parent 中id 为1 的记录修改为id=3, 但是事务同样没有提交,这样id=1 的行其实加了一个X 锁。这时如果在会话A 中再次读取id 为1 的记录,根据InnoDB 存储引擎的特性,即在READ COMMITTED 和REPEATETABLE READ 的事务隔离级别下会使用非锁定的一致性读。回到之前的会话A, 接着上次未提交的事务,执行SQL 语句SELECT* FROM parent WHERE id=l 的操作,这时不管使用READ COMMITTED 还是REPEATABLE READ 的事务隔离级别,显示的数据应该都是:
mysql> SELECT* FROM parent WHERE id= 1;
+----+
| id |
+----+
| 1 |
+ ----+
1 row in set (0 . 00 sec)
ssdss由于当前id=1 的数据被修改了1 次,因此只有一个行版本的记录。接着,在会话B中提交上次的事务:
# Session B
mysql> commit;
Query OK , 0 rows affected (0.01 sec)
ssdss在会话B 提交事务后,这时在会话A 中再运行SELECT * FROM parent WHERE id=1的SQL 语句,在READ COMMITTED 和REPEATABLE 事务隔离级别下得到结果就不一样了。对于READ COMMITTED 的事务隔离级别,它总是读取行的最新版本,如果行被锁定了,则读取该行版本的最新一个快照(fresh snapshot) 。在上述例子中,因为会话B 已经提交了事务,所以READ COMMITTED 事务隔离级别下会得到如下结果:
mysql> SELECT @@tx_isolation\G;
*************************** 1. row***************************
@@tx_isolation: READ-COMMITTED
1 row in set (0.00 sec)
mysql> SELECT* FROM parent WHERE id= l;
Empty set (0.00 sec)
ssdss而对于REPEATABLE READ 的事务隔离级别,总是读取事务开始时的行数据。因此对于REPEATABLE READ 事务隔离级别,其得到的结果如下:
mysql> SELECT @@tx_isolation\G;
*************************** 1. row***************************
@@tx_isolation: REPEATABLE-READ
1 row in set(0.00 sec)
mysql> SELECT* FROM parent WHERE id= 1;
+----+
| id |
+----+
| 1 |
+----+
1 row in set (0.00 sec)
一致性锁定读
ssdss 在默认配置下,即事务的隔离级别为REPEATABLE READ 模式下, InnoDB 存储引擎的SELECT 操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支待加锁语句,即使是对于SELECT 的只读操作。InnoDB 存储引擎对于SELECT 语句支持两种一致性的锁定读(locking read) 操作:
ssds dsss①、SELECT…FOR UPDATE :对读取的行记录加一个X 锁,其他事务不能对已锁定的行加上任何锁。
ssds dsss②、SELECT…LOCK IN SHARE MODE:对读取的行记录加一个S 锁,其他事务可以向被锁定的行加S 锁,但是如果加X 锁,则会被阻塞。
aassdsa注 15:对于一致性非锁定读,即使读取的行已被执行了SELECT … FOR UPDATE, 也是可以进行读取的,这和之前讨论的情况一样。此外, SELECT … FOR UPDATE, SELECT… LOCK IN SHARE MODE 必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句SELECT 锁定语句时,务必加上BEGIN, START TRANSACTION 或者SET AUTOCOMMIT=0 。
自增长与锁
ssdss 自增长 在数据库中是非常常见的一种属性,也是很多DBA 或开发人员首选的主键方式。在InnoDB 存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器 (auto-increment counter) 。
ssdss 当对含有自增长的计数器的表进行插入操作时,这个计数器会被初始化,执行如下的语句来得到计数器的值:
SELECT MAX(auto inc col) FROM t FOR UPDATE ;
ssdss插入操作会依据这个自增长的计数器值加 1 赋予自增长列。这个实现方式称做AUTO-INC Locking 。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL 语句后立即释放。
aassdsa注 16:虽然AUTO-INC Locking 从一定程度上提高了并发插入的效率,但还是存在一些性能上的问题。首先,对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入的完成(虽然不用等待事务的完成) 。其次,对于INSERT … SELECT 的大数据量的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。
aassdsa改进:从MySQL 5.1.22 版本开始, InnoDB 存储引擎中提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。并且从该版本开始, InnoDB 存储引擎提供了一个参数innodb_ auto inc_ lock_ mode 来控制自增长的模式,该参数的默认值为1。在继续讨论新的自增长实现方式之前,需要对自增长的插入进行分类:
aassdsa注 17:InnoDB 存储引擎中自增长的实现和MyISAM 不同,MyISAM 存储引擎是表锁设计,自增长不用考虑并发插入的问题。因此在master 上用InnoDB 存储引擎,在slave 上用MyISAM 存储引擎的replication 架构下,用户必须考虑这种情况。
aassdsa注 18:在lnnoDB 存储引擎中,自增长值的列必须是索引,同时必须是索引的第一个列。如果不是第一个列,则MySQL 数据库会抛出异常,而MylSAM 存储引擎没有这个问题。
外键和锁
ssdss 外键主要用于引用完整性的约束检查。在InnoDB 存储引擎中,对于一个外键列,如果没有显式地对这个列加索引, InnoDB 存储引擎自动对其加一个索引,因为这样可以避免表锁——这比Oracle 数据库做得好, Oracle 数据库不会自动添加索引,用户必须自己手动添加,这也导致了Oracle 数据库中可能产生死锁。
ssdss对于外键值的插入或更新,首先需要查询父表中的记录,即SELECT 父表。但是对于父表的SELECT 操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此这时使用的是SELECT … LOCK IN SHARE MODE 方式,即主动对父表加一个S 锁。如果这时父表上已经这样加X 锁,子表上的操作会被阻塞。
💖感谢各位的暴击三连~💖