开发多用户 数据库驱动应用的难点:一方面要最大程度利用数据库的并发访问,另一方面还要保证每个访问的操作或者获取数据是成功&正确的。为此就有了锁的机制,这也是数据库区别于文件系统的一个关键特性。文件系统的锁是互斥锁,同一时刻只会有一个进程在访问文件,同时通过下文我们可以了解到人们认为行级锁会增加开销,但实际上,只有当实现本身增加增加开销时,行级锁才会增加开销。
什么是锁
数据库的锁是为了支持对共享资源的并发访问,提供数据的完整性和一致性。innodb引擎中使用锁的地方很多,例如最基本的数据行锁,操作缓冲池中的LRU列表,删除,添加,移动LRU列表中的元素。
目前市面上的数据库种类比较多,无论是关系的或者非关系的,每种数据库的锁的实现都不尽相同, 在sql层面,因为有sql标准的存在, 要熟悉使用多个数据库问题不大,对于数据库锁的实现,其实只需要对某个特定的锁实现有经验就行。
例如:mysql的myisam,NDB Cluster存储引擎也是大家常用的存储引擎,甚至其他的数据库例如oracle,microsoft sql server等。但是他们对锁的实现是完全不同的,对于MyISAM引擎,其锁是表锁。在并发读的情况下是没有问题的,但是并发写入的时候性能就要差很多了。对于microsoft sql server2005版本之前是其实都是页锁,相对于myisam的表锁,并发性能有所提高。页锁容易实现,但是面对热点页数据的并发问题依然束手无策。2005版本之后,microsoft sql server开始支持乐观并发和悲观并发,在乐观并发下开始支持行级锁。但是在microsoft sql server看来, 锁是一种珍贵的资源,锁越多他的开销就越大,因此他存在锁升级的情况,也就是会从行级锁升级到表锁,这时的并发性能又回去了。
lock和latch
latch也被称为闩(shuan)锁,轻量级的锁,这里的轻量级是锁的时间要求必须非常短。在innodb引擎中latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是保证并发线程操作共享资源的正确性,并且通常没有死锁检测的机制
lock的对象是事务,用来锁定事务流程中需要更新的数据,如果整个事务中包括select和update,只有在update时才会将需要更新的数据锁住,select是不会锁的,只是做了隔离,保证数据的一致性, 并且一般lock的对象仅在事务commit或rollback之后进行释放(不同的事务隔离级别释放的时间可能不同)。此外lock也是有死锁机制的(这里的死锁机制是指死锁检测机制,下篇文章单独讲)
| lock | latch | |
|---|---|---|
| 对象 | 事务 | 线程 |
| 保护 | 数据库内容 | 内存数据结构 |
| 持续时间 | 这个事务过程 | 临界资源 |
| 模式 | 行锁,表锁,意向锁 | 读写锁互斥量 |
| 死锁 | 通过waits-for graph ,time out等机制进行死锁检测与处理 | 无死锁检测与处理机制,仅通过应用程序加锁的顺序保证无死锁情况的发生 |
| 存在于 | lock Manager的哈希表中 | 每个数据结构的对象中 |
查看innodb存储引擎中的latch,可以通过命令show engine innodb mutex来进行查看
如上图,列type总是显示innodb,列name显示的是latch的信息以及在源码中的位置(行数),列Status比较复杂,在Debug模式下,除了显示os_waits,还会显示count、spin_waits、spin_rounds、os_yields、os_wait_times等信息
| 名称 | 说明 |
|---|---|
| count | mutex被请求的次数 |
| spin_waits | spin lock(自旋锁)的次数,innodb存储引擎latch在不能获得锁时首先进行自旋,若自旋后还不能获得锁,则进入等待状态 |
| spin_rounds | 自旋内部循环得总次数,每次自旋得内部循环是一个随机数,spin_round/spain_waits表示平均每次自旋得内部循环次数 |
| os_waits | 表示操作系统等待得次数,当spin lock通过自旋还不能获得latch时,则进入操作系统等待状态,等待被唤醒 |
| os_yields | 进行os_thread_yield唤醒操作的次数 |
| os_wait_times | 操作系统等待得时间,单位时ms |
以上信息都是比较底层的,一般是供数据库开发人员参考的。应用开发人员就当是拓展下知识面吧。
innodb的锁
innodb存储引擎实现了如下两种标准的行级锁:
- 共享锁(Slock):允许事务读一行数据
- 排他锁(Xlock):允许事务删除或更新一行数据 两个事务针对同一行数据的共享锁是可以同时持有的,因为读取并没有改变行数据,这种情况称为锁兼容。但若有其他的事务想要获得这一行数据的排它锁,则需要等待前两个事务释放行r上的共享锁,这种情况称为锁不兼容。兼容性如下图:
| X | S | |
|---|---|---|
| X | 不兼容 | 不兼容 |
| S | 不兼容 | 兼容 |
innodb存储引擎支持同一个事务中多粒度锁定,意味着同一个事务可以同时持有行级锁和表锁。innodb的意向锁(Intention Lock)支持在不同粒度上进行加锁操作,意向锁是将锁定的对象分为多个层次,意味着事务希望在更细粒度上进行加锁
如上图, 我们可以就看出如果A事务想对表1的r记录加排它锁,那么就需要对r记录所在的页,表,库加意向锁。这是一个由上而下的过程。如果B事务在对r记录加锁之前,C事务已经对表1加上了共享锁,这时B事务相对表1增加意向锁,由于锁不兼容,所以该事物需要等待C事务锁操作的完成。
innodb存储引擎的意向锁实质为表级别的锁。设计目的只要是为了在一个事务中揭示下一行将请求的锁类型。其支持两种意向锁:
- 意向共享锁(IS lock):事务想要获得一张表中某几行的共享锁(表级锁)
- 意向排他锁(IX Lock):事务想要获得一张表中某几行的排它锁(表级锁) 由于innodb存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级别意向锁与行级锁的兼容性如图:
| IS | IX | S | X | |
|---|---|---|---|---|
| IS | 兼容 | 兼容 | 兼容 | 不兼容 |
| IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
| S | 兼容 | 不兼容 | 兼容 | 不兼容 |
| X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
从上图可以看出意向锁之间都是互相兼容的,但是与表级读写锁之间大部分都是不兼容的,意向锁不会与行级的读写锁互斥
读到这里可能就有问题了,为什么说意向锁不会与行级的读写锁互斥?
- 我们可以反方向思考,三个事务T1,T2,T3分别想对某张表中的记录行r1,r2,r3进行修改,在实际的应用场景中这是很常见的并发场景。这三个事务目前同时对该表加意向写锁,因为意向锁之间是兼容的,所以这一步没问题,接下来增加行级排它锁,三个事务顺利执行完成。
小结
本文旨在带大家,同时也带我了解下innodb存储引擎的锁基本知识,通过本文我们了解到了innodb的锁包含了共享锁和排他锁,在表级别上有意向锁。同时也了解了这几种锁的兼容情况, 这点在我们推理锁的执行流程上是有帮助的。