第六章 锁(学习笔记)

  开发多用户、数据库驱动的应用时,最大的一个难点是:一方面要最大程度地利用数据库地并发访问,另外一方面还要确保每个用户能以一致地方式读取和修改数据。

  1. 什么是锁

  锁机制用于管理对共享资源地并发访问。InnoDB存储引擎会在行级别上对表数据上锁。

  2. lock与latch

  latch是一种轻量级地锁,分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源地正确性,并且通常没有死锁检测机制。

  lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。lock的对象仅在事务commit或rollback后进行释放。另外,lock有死锁机制。

  3. InnoDB存储引擎中的锁

  3.1 锁的类型

  • 共享锁(S lock)允许事务读一行数据
  • 排他锁(X lock)允许事务删除或更新一行数据

  InnoDB存储引擎还支持一种额外的锁方式,即意向锁(Intension lock)。意向锁将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。  

  若将上锁的对象看成一个树,那么对对下层的对象上锁(最细粒度的对象上锁),需要首先对粗粒度的对象上锁。如下图所示,如果需要对页上的记录r上X锁,需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。

   

  锁的兼容性如下图所示。

        

  3.2 一致性非锁定读

  一致性非锁定读(consistent nonlocking read)指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中的行数据。如果读取的行正在执行DELETE或UPDATE操作,这时,读取操作不会等待行上锁的释放,InnoDB存储引擎会去读取行的一个快照数据。非锁定读机制可以极大的提高数据库的并发性。 

  

  读取的快照数据来自undo段,undo用来在事务中回滚数据,因此快照数据本身没有额外的开销。由图中可以看到,一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(multi version concurrency control MVCC)。

  在READ COMMITED 事务隔离级别下,对于快照数据,一致性非锁定读总是读取非锁定行的最新一份快照数据。在REPEATABLE READ 事务隔离级别下,对于快照数据,一致性非锁定读总是读取事务开始时的行数据版本。

 

  如上表所示执行,在时间点5,两种隔离模式,得到的结果一样,即id = 1; 在时间点7,两种隔离模式,会得到不同的结果,READ COMMITED 得到 Empty Set (读取最新的行数据快照), REPEATABLE READ仍是id =1(事务开始时的行数据)。 

 

  3.3 一致性锁定读

  在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。InnoDB存储引擎对于SELECT语句支持两种一致性地锁定读(locking read)操作:

  • SELECT.....FOR UPDATE (对读取的行加X锁,其他事务不能对已锁定的行加上任何锁)
  • SELECT.....LOCK IN SHARE MODE (对读取的行加S锁,其他事务可以对被锁定地行加S锁,但是加X锁,则会被阻塞)

  此外,这两种操作必须在一个事务中,当事务提交了,锁也就释放了。因此,在使用上述两句SELECT锁定语句时,务必加上BEGIN, START TRANSACTION 或者SET AUTOCOMMIT=0。

  3.4 外键和锁

  对于外键地插入或更新,首先需要查询父表中的记录,即SELECT父表。但是对于父表的SELECT操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一致的问题。这时,使用的是SELECT...LOCK IN SHARE MODE方式,即主动对父表加一个S锁。如果这时父表上已经有了X锁,子表上的操作会被阻塞,如下表所示。

        

  4 锁的算法

  4.1 行锁的三种算法

  InnoDB存储引擎有三种行锁的算法:

  • Record Lock:单个行记录上的锁 (总是会锁住索引记录,如果存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定)
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key-Lock:Gap Lock + Record Lock 

  InnoDB对行的查询都是采用Next-Key-Lock算法,该算法可以解决Phantom Problem,假如一个索引有10,11,13,20这四个值,那么被索引的区间为:(-∞, 10], (10, 11], (11, 13], (13, 20], (20, +∞)

  当查询的列是唯一索引时,会降级为Record Lock,若是辅助索引,情况会不太一样,先创建如下测试表z:

  

      

  现在会话A中执行上面的SQL语句,由于b列是辅助索引,Next-Key-Lock算法会锁定(1,3] ,另外,特别需要注意的是,InnoDB存储引擎还会对辅助索引下个键值(即6)加上gap lock,所以锁定的辅助索引为1 2 3 4 5,所以运行下面的SQL语句都会被阻塞。

    

   而下面的SQL语句则不会被阻塞:

  

  4.2 解决Phantom Problem

  在默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key-Locking机制来避免Phantom Problem(幻像问题)。

  Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能返回之前不存在的行。

   假设表由1、2、5三个值组成。若执行如下的SQL语句:

   

        

    会话A在时间3 和 7 执行的SQL语句会得到不同的结果。为了避免Phantom Problem,对于上述SQL语句,其锁住的不是5这个值,而是对(2,∞)这个范围加了X锁。因此,对于这个范围的插入都是不被允许的,从而避免了Phantom Problem。

  5. 锁问题

  5.1 脏读

  脏数据是指事务对缓冲池中行记录进行了修改,但是还没有提交的数据。如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性(脏读)。下表是一个脏读的例子。

 

  READ UNCOMMITTED可以应用在一些比较特殊的情况。例如,replication环境中的slave节点,并且在该slave上的查询并不需要特别精确的返回值

  5.2 不可重复读

  不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问同一数据集合,并做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的,即一个事务内两次读到的数据是不一样的,即不可重复读。不可重复读的示例如下表所示。

   

   InnoDB存储引擎的默认事务隔离级别是READ REPEATABLE,采用Next-Key-Lock算法,避免了不可重复读的现象。

  5.3 丢失更新

   一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。出现下面的情况时,就会发生丢失更新:

  1) 事务T1查询一行数据,放入本地内存,并显示给一个终端用户User1

  2) 事务T2也查询该行数据,并将取得的数据显示给终端用户User2

  3) User1修改该行记录,更新数据库并提交

  4) User2修改该行记录,更新并提交数据

  要避免丢失更新发生,需要事务在这种情况下的操作变成串行化,而不是并行的操作。如下表所示:

        

  6. 阻塞

  因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。

  在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来控制等待的时间(默认50s,动态参数,可以在运行时调整),innodb_on_timeout(静态参数,不可在启动后,修改)用来设定是否在等待超时时,对进行中的事务进行回滚操作(默认是OFF,代表不回滚)。当做默认设置时,可能存在如下问题:

  

  

 

     

    

 

   由以上代码可知,事务B由于等待事务A释放a<4的锁资源发生了超时,虽然没有进行COMMIT操作,但是数值5还是插入到了数据库中。这是十分危险的状态,用户必须判断是否需要COMMIT还是ROLLBACK,然后再进行下一步操作。 

  7. 死锁

   当两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。

  解决死锁问题最简单的方法就是超时回滚(当一个等待时间超过阈值,进行回滚)。

  数据库一般采用wait-for graph(等待图)的方式来进行死锁检测,需要保存以下两种信息:

  • 锁的信息链表
  • 事务等待链表

  在Transaction list 中可以看到共有四个事务t1, t2, t3, t4.

  • 事务t1需要等待t2中row1的资源(t1指向t2)
  • 事务t2需要等待t1 t4所占用的row2资源
  • 事务t3需要等待t1, t2, t4占用的资源

  

    

 

  存在t1 t2的回路,故而存在死锁。InnoDB存储引擎一般选择回滚undo量最小的事务。

  锁升级是指将当前锁的粒度降低。即把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁。

  InnoDB不存在锁升级问题。其根据每个事务访问的每个页对锁进行管理,采用位图的方式。因此,不管一个事务锁住页中一个记录还是多个记录,开销通常是一致的。

  假设一张表有3 000 000个数据页,每个页大约有100条记录,总共有300 000 000条记录。若一个事务更新全表更新语句,需要对所有记录加X锁。若根据每行记录产生锁对象,假设每个锁10字节,则锁管理需要3GB内存。

  而InnoDB存储引擎根据页进行加锁,每个页的锁信息占30个字节,则锁对象仅需90MB内存。

 

 

  

 

 

  

posted @ 2021-08-08 22:34  慕仙白  阅读(118)  评论(0)    收藏  举报