在继MDL锁系统改为使用LOCK-FREE方式实现后,Server层又再5.7.5迎来重大改动:THR_LOCK被彻底移除,而是完全使用MDL锁来实现。
对应的change log entry:
Scalability for InnoDB
tables was improved by avoiding THR_LOCK
locks. As a result of this change, DML statements for InnoDB
tables that previously waited for a THR_LOCK
lock will wait for a metadata lock:
- Explicitly or implicitly started transactions that update any table (transactional or nontransactional) will block and be blocked by
LOCK TABLES ... READ
for that table. This is similar to howLOCK TABLES ... WRITE
works. - Tables that are implicitly locked by
LOCK TABLES
now will be locked using metadata locks rather thanTHR_LOCK
locks (forInnoDB
tables), and locked using metadata locks in addition toTHR_LOCK
locks (for all other storage engines). Implicit locks occur for underlying tables of a locked view, tables used by triggers for a locked table, or tables used by stored programs called from such views and triggers.Multiple-table updates now will block and be blocked by concurrentLOCK TABLES ... READ
statements on any table in the update, even if the table is used only for reading. HANDLER ... READ
for any storage engine will block and be blocked by a concurrentLOCK TABLES ... WRITE
, but now using a metadata lock rather than aTHR_LOCK
lock.
这两大改动,完全消除了Server层的MDL锁开销和THR LOCK锁开销,在PK-SELECT查询中,瓶颈居然压到了Lock_grant上…
这篇博客是我看代码时的记录,对细节不感兴趣的直接跳到最后,看看官方博客和对应worklog的描述,比我清楚多啦 :)
在此之前,先来了解下什么叫做thr lock. thr lock实际上是早期版本控制Server层表级并发的锁。
0. 主要数据结构
THR_LOCK_DATA
THR_LOCK_INFO
MYSQL_LOCK
锁对象: THR LOCK锁对象具有比较特殊的结构
在“Understanding MySQL Internal”一书中也有一节专门讲Table Lock Manager:
table lock管理模块为每个表维护了四个队列:
Current read-lock queue (lock->read)
Pending read-lock queue(lock->read_wait)
Current write-lock queue(lock->write)
Pending write-lock queue(lock->write_wait)
当前正在等待的线程都会被加入到write_wait 和 read_wait队列中。而已经拥有了的则加入到read 和write队列。
1.加锁
Backtrace:
lock_tables->mysql_lock_tables->thr_multi_lock->thr_lock
根据SQL类型的不同,加锁的类型也不同,例如对于SELECT操作,类型为lock_type=TL_READ,而对于普通的DML操作,类型为TL_WRITE_ALLOW_WRITE
显然对于正常的DML/SELECT操作,这两种锁类型是不冲突的。
LOCK TABLE t1 READ
lock_type=TL_READ_NO_INSERT
LOCK TABLE t1 WRITE
lock_type=TL_WRITE
TRUNCATE TABLE t1
lock_type=TL_WRITE
更多的例子,不一一列举了,具体的描述见枚举类型thr_lock_type。
如果线程持有更高级别的锁在表上,就不会去加低级别的锁,例如我们先执行LOCK TABLE t1 WRITE 会执行加锁,随后执行TRUNCATE TABLE,就无需再次加锁了。
锁冲突检测,举个简单的例子:
Session 1: BEGIN; LOCK TABLE t1 READ;
Session 2: BEGIN; INSERT INTO t1 VALUES(1,2,3);
Session 2将被堵塞住,Backtrace为:
1 pthread_cond_timedwait,safe_cond_timedwait(thr_mutex.c:278),inline_mysql_cond_timedwait(mysql_thread.h:1188),wait_for_lock(thr_lock.c:462),thr_lock(thr_lock.c:787),thr_multi_lock(thr_lock.c:1060),mysql_lock_tables(lock.cc:321),lock_tables(sql_base.cc:5920),mysql_insert(sql_insert.cc:908)
如果使用LOCK TABLE t1 WRITE, 那么会发现Session 2堵在MDL这了。
key function:
check_locks : 检查锁是否冲突
“以下摘录自Understanding MySQL Internals一书”
关于读锁:
如果没有写锁或者在等待队列中的写锁,则可以执行,否则加入到read_wait队列中.
在等待队列中锁的优先级规则为:
#TL_WRITE的优先级总是比读锁的优先级要高,但TL_READ_HIGH_PRIORITY除外.
#TL_READ_HIGH_PRIORITY 优先级在任何的Pending 写锁之上
#所有在write_wait队列中的写锁,如果不是TL_WRITE,则其优先级低于读锁
当前高优先的写锁会导致其他锁请求suspend并进入condtion wait.但以下场景除外:
#通过THR_LOCK中的回调指针check_status,存储引擎层可能允许除TL_READ_NO_INSERT的读锁和一个TL_WRITE_CONCURRENT_INSERT 锁
#TL_WRITE_ALLOW_WRITE 允许除TL_WRITE_ONLY外的所有读锁和写锁
#TL_WRITE_ALLOW_READ 允许除TL_READ_NO_INSERT外的所有读锁
#TL_WRITE_DELAYED 允许除TL_READ_NO_INSERT外的所有读锁
#TL_WRITE_CONCURRENT_INSERT允许除TL_READ_NO_INSERT外的所有读锁
关于写锁
如果存在读锁,则读锁会阻塞写锁,但以下情况除外:
#请求的锁类型为TL_WRITE_DELAYED
#请求的锁类型为TL_WRITE_CONCURRENT_INSERT或者TL_WRITE_ALLOW_WRITE,并且在读锁队列中没有没有TL_READ_NO_INSERT。
2.解锁
SELECT
Backtrace:
mysql_execute_command->close_thread_tables->mysql_unlock_tables->thr_multi_unlock->thr_unlock
3.问题
实际上在Innodb场景下,大部分表级锁检测都已经被Metadata Lock覆盖了,INNODB 唯一严重依赖THR_LOCK的地方是LOCK TABLE READ, 以此来保证在LOCK TABLE READ 能够完全阻塞住. 对于DDL和DML的冲突,已有的逻辑已经能保证这一点了。
因此在新的逻辑中,只需要增加一种新的MDL锁类型来代替THR_LOCK在LOCK TABLE READ场景下的角色即可。THR LOCK的调用可以完全被避免掉。
不过这也引入了一点不兼容性:LOCK TABLE READ 将会堵塞住MULTI-UPDATE涉及到的表,即时这个表只是用来进行读操作。
0.如何避免使用thr_lock
需要注意,这个是引擎相关的,某些存储引擎可能依旧依赖于thr lock,因此Innodb对此有特殊处理。
一个新的存储引擎FLAG引入,以对不同的引擎做区分:
HA_NO_READ_LOCAL_LOCK: 存储引擎不支持 LOCK TABLE … READ LOCAL,并且不想在 handler::store_lock接口中将其升级为LOCK TABLE…READ
引入新的MDL锁类型:
MDL_SHARED_READ_ONLY (简称SRO):允许读,同时阻塞写入,用于代替LOCK TABLE READ锁使用的THR LOCK (TL_READ_NO_INSERT)
MDL_SHARED_WRITE_LOW_PRIO: 当DML存在LOW_PRIORITY子句时,使用该类型的MDL,其权限比SRO要低
针对LOCK TABLES操作,使用SNRW锁来替换TL_WRITE锁
lock_count用于返回引擎层对单个实例表需要的thr lock个数,类似Merge引擎或者partition表可能需要多个thr lock。而对于innodb,之前版本直接引用handler的虚函数,返回1。在5.7.5版本中,为innodb定义了对应接口:
uint
ha_innobase::lock_count(void) const
/*===============================*/
{
return 0;
}
返回0到上层函数get_lock_data,那么对于Innodb表,将不会再为其创建thr lock锁。如此简单的避免了为Innodb表创建THR LOCK.
ha_innobase::store_lock :不再存储thr lock 类型
2346 TL_IGNORE */
2347 @@ -13474,8 +13495,6 @@
2348 lock.type = lock_type;
2349 }
2350
2351 – *to++= &lock;
ha_innobase::store_lock() doesn’t try to store type of thr_lock.c lock
in MYSQL_LOCK::locks[] array it gets as a parameter
关于如何协调新加MDL类型与已有类型之间的冲突检测和优先级关系,代码做了大量的修改,不细看了,感兴趣的直接翻worklog
worklog:
http://dev.mysql.com/worklog/task/?id=6671
前置补丁:
http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8100
MDL_context::is_lock_owner 更名为MDL_context::owns_equal_or_stronger_lock
http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8110 //单元测试修改
http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8119
一些函数的返回值从bool修改为void,因为从未使用到.
http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8232
相关:http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8633
官方博客: