锁机制用于管理对共享文件的并发访问,并提供数据的完整性和一致性。对于MyISAM引擎,其锁是表锁结构,在并发情况下读没有问题,但是并发插入时性能较差。而对于Microsoft SQL Server,在乐观并发下支持行级锁,但是锁越多开销越大,因此会有锁升级,行锁会升级到表锁,导致并发能力回退。InnoDB引擎支持一致性的非锁定读,行级锁支持,且行级锁没有额外的开销。
lock锁是一种轻量级的锁,其要求锁定的时间非常短,若时间较长性能会变差。在InnoDB存储引擎中,latch又分为互斥量mutex和rwlock读写锁,其目的是为了保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
lock的对象是事务,锁定的是数据库中的对象,如表、页、行,并且lock对象在事务commit或rollbck后进行释放,此外lock具有死锁机制。用户可以使用Show Engine InnoDB Status来观察锁的信息
InnoDB存储引擎实现了两种标准的行级锁:共享锁(S Lock)-允许事务读一行数据,排他锁(X Lock)-允许事务删除或跟更新一行数据。若事务T1获得了行A的共享锁,事务T2同样可以立即获得行A的共享锁,这种情况称为锁兼容。若事务T3想获取行A的排他锁,则必须等待事务T1、T2释放行A上的共享锁。因此只有S锁与S锁兼容,其他几种情况下都是不兼容的
InnoDB存储引擎支持多粒度的锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在,这种不同粒度的锁称为意向锁(Intention Lock)。例如需要对页上的记录M上X锁,那么需要分别对数据库、表、页上意向锁,最后对记录上锁,若任何一个部分导致等待,那么该操作需要等待粗粒度的锁的完成。
InnoDB存储引擎中的意向锁为表级别的锁,其目的是为了在一个事务中揭示下一行将被请求的锁类型,其分为两种意向锁:意向共享锁(IS Lock)-事务想要获得一张表中某几行的共享锁,意向排他锁(IX Lock)-事务想要获得一张表中某几行的排他锁。由于InnoDB存储引擎支持的是行级别的锁,所以意向锁不会阻塞除全表扫描以外的任何请求。兼容性如下:
- | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
一致性非锁定读是指InnoDB存储引擎通过多版本控制的方式来读取执行时间数据库中行的数据,如果读取行的数据在执行一个Update或者Delete操作,读取行不会等待,而是去读取一个快照数据。其实现是通过undo段来实现的,undo是用来在事务中进行回滚的,因此快照数据本身是没有额外开销的。非锁定读是InnoDB存储引擎在默认设置下的读取方式,但在不同的隔离级别下,其表现形式是不同的。
在Read Committed和Repeatable Read(默认隔离级别)下,InnoDB存储引擎使用非锁定的一致性读,但对于快照的定义不同。在Read Committed事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据;而在Repeatable Read事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行版本数据(如两个事务对同一行进行操作,事务A在读取时事务B在进行更新操作,事务A读取到的是更新前的数据)。
在某些情况下,用户需要显式地对数据库读写操作进行加锁以保证数据逻辑的一致性,这就要求数据库支持加锁语句,即使是Select只读操作。InnoDB存储引擎对于Select语句支持两种一致性锁定读操作:
需要注意的是上述两种操作都必须在一个事务中,当事务提交后,锁会被释放
在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器,当对含有自增长计数器的表进行插入操作时,计数器会被初始化。插入数据会依据自增长的计数器的值加1,这个实现方式称为Auto-Inc Locking。这种锁采用了一种表锁机制,它会在自增长值插入SQL语句后立即释放。
Auto-Inc Locking从一定程度上提高了并发插入的效率,但是对于自增长列的并发插入性能较差,需要等待一个事务完成之后再进行下一个插入。MySQL5.1.22版本开始,InnoDB存储引擎提供了一种轻量级互斥量的自增长实现机制,大大提高了自增长值插入的性能,其通过innodb_autoinc_lock_mode来控制自增长的模式
外键主要用于完整性的约束检查,在InnoDB存储引擎中,对于一个外键列,如果存储引擎没有显式地对这个列加索引,InnoDB存储引擎会自动地对其加一个索引,避免表锁。对于外键的插入或更新,首先要查询父表中的记录,此时使用的是一致性读写中的Select...Lock In Share Mode操作,子表上的操作会被阻塞,以避免父表与子表数据不一致的情况
InnoDB存储引擎有3中行锁的算法,其分别是:
Record Lock总是会锁住索引记录,如果InnoDB存储引擎在建立时没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法,但是当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock。若唯一索引由多个列组成,而查询的是多个唯一索引列中的一个,那么查询是Range类型的查询而非Point类型,InnoDB存储引擎依旧会使用Next-Key Lock。
Gap Lock的作用是为了阻止多个事务将记录插入到同一个范围内,但这会导致Phantom Problem(幻像问题)的产生。用户可以将隔离级别设置为Read Committed或者将参数innodb_locks_unsafe_for_binlog设置为1来显式地关闭Gap Lock,但这样会破坏事务的隔离性,对于主从架构会导致数据的不一致。
Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能会导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。InnoDB存储引擎默认使用Repeatable Read隔离级别,其使用Next-Key Locking机制来避免幻象问题。而在Read Committed隔离级别下,其仅采用Record Lock
脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另一个事务中未提交的数据,则违反了数据库的隔离性。脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,即读到了脏数据。脏读只有在事务隔离级别为Read UnCommitted时会发生,目前主流的数据库如MSSQL和Oracle的默认隔离级别都是Read Committed,Mysql的隔离级别是Read Repeatable,因此在实际生产环境中很少发生
不可重复读是指在同一事务内多次读取同一数据集合,由于事务未结束而另一事务的DML操作对该数据集合进行了变化,导致同一事务内读取到的数据不一致。其与脏读的区别是,脏读是读到了未提交的数据,不可重复读是读到了已提交的数据,不可重复读违反了数据一致性的原则。默认情况下的Read Committed是允许不可重复读的现象的。Mysql将不可重复读问题定义为幻象问题(注意不是幻读-即下面的丢失更新),InnoDB存储引擎的隔离级别是Read Repeatable,使用Next-Key Lock算法来避免不可重复读
丢失更新简单来说就是一个事务的更新操作会被另一个事务的更新操作覆盖,导致数据的不一致性。在数据库理论层面是不会出现该现象的,这个因为DML操作需要对行或其他粗粒度级别的对象加锁,后一个事务不能对某数据进行更新直到前一个事务提交,但是在实际应用场景是存在的,比如将数据展现给多个用户后数据发生了变化。避免丢失更新发生需要让事务在这种情况下的操作串行化,而不是并行。
由于不同锁之间的兼容性,有时一个事务的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。在InnoDB存储引擎中,参数innodb_lock_timeout用来控制等待的时间(默认为50s),参数innodb_rollback_on_timeout用来设定是否在等待超时时对进行中的事务进行回滚操作(默认为Off)
默认情况下,InnoDB存储引擎不会回滚超时引发的错误异常,用户必须判断是否需要Commit还是RollBack以避免在等待过程中的变更的数据带来的影响。
死锁指的是两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。解决死锁最简单的方式是不需要等待,将任何的等待都转化为回滚,并且事务重新开始,但这会导致并发性能的下降,甚至任何一个事务都不能进行。另一种方式是设定超时时间,当一个等待超过设置的阈值时,当前事务回滚,另一个事务可以继续执行,这样的缺点是仅按照时间权重来决定,可能会导致事务更新操作较多,即占用了较多的undo log的事务回滚,浪费系统资源
目前数据库普遍采用wait-for graph(等待图)的方式来进行死锁的检测,它是一种主动的死锁检测方式,InnoDB存储引擎采用的也是这种方式。wait-for graph要求数据库保存锁的信息链表以及事务等待链表,通过这两种信息构造一张图,若图出现回路即代表存在死锁。图中节点代表事务,事务T1指向事务T2边的定义为:①事务T1等待事务T2所占用的资源;②事务T1最终等待T2所占用的资源,即事务之间存在等待的资源且T1发生在T2之后。每个事务请求锁并发生等待时都会判断是否存在回路,若存在死锁,InnoDB存储引擎通常会选择回滚undo量最小的事务。wait-for graph通常采用深度优先算法
锁升级是指将当前锁的粒度降低,即将行锁升级为页锁,或者将页锁升级为表锁。MSSQL中将锁认定为一种稀有的资源,在合适的时候自动地将行、键或者分页锁升级为更粗粒度的表级锁,这种升级保护了系统资源,防止系统使用过多的内存来维护锁,在一定程度上提高了效率,到那时会降低系统的并发性能。
MSSQL在以下情况发生时可能发生锁的升级:
InnoDB存储引擎不存在锁升级的问题,它是根据每个事务访问每个页对锁进行管理的,采用的是位图的方式,因此不管事务是锁住页中的一个记录还是多个记录,其开销通常是一致的