本文共 4709 字,大约阅读时间需要 15 分钟。
目录
锁是数据库区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。例如:操作缓冲池中的LRU列表、删除、添加、移动LRU列表中的元素,为了保证一致性,必须有锁的介入。
InnoDB存储引擎锁的实现和Oracle数据库非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。
latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。 lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。有死锁检测机制。
1. 查看latch信息:show engine innodb mutex
mysql> show engine innodb mutex\G;*************************** 1. row *************************** Type: InnoDB Name: sum rwlock: buf0buf.cc:781Status: waits=11 row in set (0.00 sec)
2. 查看lock信息:通过命令SHOW ENGINE INNODB STATUS
及information_schema架构下的表INNODB_TRX(显示当前运行的InnoDB事务)
、INNODB_LOCKS(锁的情况)
、INNODB_LOCK_WAITS(事务的等待信息)
来观察锁的信息。
3. lock与latch的比较
实现了两种标准的行级锁:
共享锁(S Lock),允许事务读一行数据
排他锁(X Lock),允许事务删除或者更新一行数据
排他锁和共享锁的兼容性:
X锁与任何锁都不兼容
S锁仅和S锁兼容
例:如果事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,若有其他事务T3想要获取行r的排他锁,则必须等待T1,T2释放行r上的共享锁。
InnoDB支持多粒度锁定,允许事务在行级上和表级上的锁同时存在,称之为意向锁。意向锁将锁定的对象分为多个层次,意味着事务希望在更细粒度上锁。
若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。如:需要对页上的记录r行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。
由于InnoDB存储引擎是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求。InnoDB存储引擎中锁的兼容性。
一致性非锁定读指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中的行数据。如果读取的行正在执行DELETE或者UPDATE操作,这时读取操作就不会因此去等待行上的锁释放。相反地,会去读取行的一个快照数据。
快照数据是指行之前版本的数据,每行记录可能有多个版本,该实现是通过undo段(默认在表空间中)来完成的。undo用来在事务中回滚数据,因此快照数据本身没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
由上可知,一个行记录可能不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurency Control,MVCC)
在不同的事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。READ COMMITTED,总是读取被锁定行的最新一份快照数据。REPEATABLE READ,读取事务开始时的行数据版本。
案例:在不同的事务隔离级别下,执行结果会不同。
在某些情况下,需要显式地对数据库加锁以保证数据逻辑的一致性,即使是对于SELECT的只读操作。对于SELECT语句支持两种一致性锁定读的操作:
SELECT ... FOR UPDATE
,对读取的行数据加一个X锁,其他事务不能对已锁定的行加上任何锁
SELECT ... LOCK IN SHARE MODE
,对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但如果加X锁,则会被阻塞
注意:以上两个锁定语句,必须在事务中执行。
在InnoDB存储引擎的内存结构中,对每个自增长的计数器的表进行插入操作时,这个计数器会被初始化,执行如下语句来得到计数器的值:SELECT MAX(auto_inc_col) FROM t FOR UPDATE
。
插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。存在一定的性能问题,第一,对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入操作的完成;其次,对于insert ... select的大数据量的插入会影响插入的性能,因为另一个事务中的插入操作会被阻塞。
从MySQL 5.1.22开始,提供了一种轻量级互斥量的自增长实现机制,大大提高了自增长值插入的性能。
自增长值的列必须是索引的第一个列。
对于外键值的插入或更新操作,首先需要查询父表中的记录,即SELECT父表(使用的是一致性锁定读,即:SELECT ... LOCK IN SHARE MODE
,即主动对父表加S锁。如果这时父表上已经加了X锁,子表上的操作会被阻塞)。
有3种行锁算法,分别是:
Record Lock:单个行记录上的锁,没有主键,会使用隐式的主键进行锁定
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身,其设计目的是为了解决Phantom Problem(幻象问题)
用户可以通过以下两种方式来显式地关闭Gap Lock:
将事务的隔离级别设置为READ COMMITTED,读已提交会出现不可重复读和幻读
将参数inodb_ locks_ unsafe_ for_ binlog设置为1
在上述的配置下,除了外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。
当查询的索引含有唯一属性时,会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是一个范围。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,依然使用Next-Key Lock进行锁定。
案例讲解。
在默认的事务隔离级别下,即RETEATABLE READ下,采用Next-Key Locking机制来避免Phantom Problem问题。在READ COMMITTED下,仅使用Record Lock。
Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同结果,第二次SQL语句可能返回之前不存在的行,即:当前事务能够看到其他事务的结果。
如:SELECT * FROM t WHERE a > 2 FOR UPDATE
,在默认隔离级别下,是对(2, 正无穷)这个范围加X锁,因此任何对于这个范围的插入都是不被允许的,从而避免了Phantom Problem。
锁会带来的问题:
脏读
不可重复读
丢失更新
脏页跟脏数据是两个完全不同的概念。脏页指的是缓冲池中已经被修改的页,但是还没有刷新到磁盘中。脏数据是指事务对缓冲池中的行记录的修改,并且还没有提交。
脏读就是在不同的事务下,当前事务可以读到另外事务未提交的数据(脏数据)。
不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合, 并做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。
不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。
一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身不会带来很大的问题。
采用Next-Key Lock算法,避免不可重复读的现象。对于索引的扫描,不仅锁住扫描到的索引,而且还锁住这些索引覆盖的范围,因此在这个范围内的插入都是不允许的。
一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。例如:
事务T1将行记录r更新为v1,但是事务T1并未提交
与此同时,事务T2将行记录r更新为v2(阻塞),事务T2未提交
事务T1提交
事务T2提交
以上操作理论上不会导致丢失更新,但是如果是先查询,再在查询的基础上更新,就会产生逻辑意义的丢失更新问题,设想银行丢失更新的情况。。。
解决:让事务在这种情况下的操作变成串行化,而不是并行操作,即读取时也加锁,select ... for update
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占有的资源。
innodb_lock_wait_timeout
用来控制等待的时间(默认50秒);innodb_rollback_on_timeout
用来设定是否在等待超时时对进行中的事务进行回滚操作(默认off,不回滚),InnoDB不会回滚大部分的错误异常,但死锁除外。
死锁是指在两个或两个以上的事务在执行过程中,因争夺资源而造成的一种相互等待的现象。若无外力作用下,事务都将无法推进下去。
死锁检测:超时机制+wait-for graph(等待图)
wait-for graph要求数据库保存以下两种信息:
锁的信息链表
事务等待链表
通过上述链表可以构造出一张图,若图中存在回路,就代表存在死锁,资源间相互发生等待。
如图,存在t1和t2相互等待。
通常来说InnoDB存储引擎选择回滚undo量最小的事务。
死锁发生的机率是非常小的。
死锁示例,AB-BA死锁,1213错误,不需要对其进行回滚。
锁升级是将当前锁的粒度降低,即:行锁升级为页锁,或者将页所升级为行锁。若在数据库的设计中认为锁是一个稀缺资源,而且想避免锁的开销,那数据库中会频繁出现锁升级的现象。
InnoDB不存在锁升级问题
转载地址:http://gnqen.baihongyu.com/