阿里巴巴DBA,原去哪儿网DBA。专注于MySQL源码研究、DBA运维、CGroup虚拟化及Linux Kernel源码研究等。 github:https://github.com/HengWang/ Email:king_wangheng@163.com 微博 :@王恒-Henry QQ :506437736
分类: Mysql/postgreSQL
2013-01-12 13:59:14
目的
MySQL多线程锁数据结构THR_LOCK,是支撑MySQL层锁的基础结构,也是MySQL与存储引擎层在锁控制上的衔接结构。对于多线程的锁控制,直接关系到数据库的并发问题。本文将分析MySQL在多线程锁数据结构的设计和实现,以便于进一步深入了解MySQL层以及存储引擎层的锁。
数据结构
MySQL多线程锁的数据结构,在include/thr_lock.h和mysys/thr_lock.c源码文件中。THR_LOCK数据结构相关的结构还包括:thr_lock_type、THR_LOCK_INFO、THR_LOCK_DATA数据结构。具体定义及详细分析如下所示:
enum thr_lock_type { TL_IGNORE=-1, TL_UNLOCK, /* UNLOCK ANY LOCK */ /* Parser only! At open_tables() becomes TL_READ or TL_READ_NO_INSERT depending on the binary log format (SBR/RBR) and on the table category (log table). Used for tables that are read by statements which modify tables. */ TL_READ_DEFAULT, TL_READ, /* Read lock */ TL_READ_WITH_SHARED_LOCKS, /* High prior. than TL_WRITE. Allow concurrent insert */ TL_READ_HIGH_PRIORITY, /* READ, Don't allow concurrent insert */ TL_READ_NO_INSERT, /* Write lock, but allow other threads to read / write. Used by BDB tables in MySQL to mark that someone is reading/writing to the table. */ TL_WRITE_ALLOW_WRITE, /* WRITE lock used by concurrent insert. Will allow READ, if one could use concurrent insert on table. */ TL_WRITE_CONCURRENT_INSERT, /* Write used by INSERT DELAYED. Allows READ locks */ TL_WRITE_DELAYED, /* parser only! Late bound low_priority flag. At open_tables() becomes thd->update_lock_default. */ TL_WRITE_DEFAULT, /* WRITE lock that has lower priority than TL_READ */ TL_WRITE_LOW_PRIORITY, /* Normal WRITE lock */ TL_WRITE, /* Abort new lock request with an error */ TL_WRITE_ONLY}; typedef struct st_thr_lock_info { pthread_t thread; my_thread_id thread_id; } THR_LOCK_INFO; typedef struct st_thr_lock_data { THR_LOCK_INFO *owner; struct st_thr_lock_data *next,**prev; struct st_thr_lock *lock; mysql_cond_t *cond; enum thr_lock_type type; void *status_param; /* Param to status functions */ void *debug_print_param; struct PSI_table *m_psi; } THR_LOCK_DATA; struct st_lock_list { THR_LOCK_DATA *data,**last; }; typedef struct st_thr_lock { LIST list; mysql_mutex_t mutex; struct st_lock_list read_wait; struct st_lock_list read; struct st_lock_list write_wait; struct st_lock_list write; /* write_lock_count is incremented for write locks and reset on read locks */ ulong write_lock_count; uint read_no_write_count; void (*get_status)(void*, int); /* When one gets a lock */ void (*copy_status)(void*,void*); void (*update_status)(void*); /* Before release of write */ void (*restore_status)(void*); /* Before release of read */ my_bool (*check_status)(void *); } THR_LOCK; |
thr_lock_type枚举类型结构中,定义了多线程锁的类型。多线程锁的优先级为:WRITE_ALLOW_WRITE,WRITE_CONCURRENT_INSERT,WRITE_DELAYED,WRITE_LOW_PRIORITY,READ,WRITE,READ_HIGH_PRIORITY,WRITE_ONLY。特别的,锁类型TL_READ_NO_INSERT与TL_WRITE_CONCURRENT_INSERT不能共存;锁类型TL_WRITE_ALLOW_WRITE和TL_WRITE_ONLY锁互斥;锁类型TL_WRITE_LOW_PRIORITY的优先级低于TL_READ;锁类型TL_READ_HIGH_PRIORITY优先级高于TL_WRITE;获得TL_WRITE_ONLY时,其他任何锁请求都无法获取。
THR_LOCK_INFO数据结构定义了线程相关的信息,包括thread线程和thread_id线程id两个参数。
THR_LOCK_DATA数据结构定义了多线程锁的数据信息,包括线程信息owner,类型是THR_LOCK_INFO;next和prev指针为当前数据信息的下一个和上一个数据信息,是双向链表结构;lock是THR_LOCK数据类型的锁信息;多线程并发的条件变量cond;锁类型type,取值为thr_lock_type中的值;status_param是THR_LOCK数据结构中状态函数的参数;debug_print_param是DEBUG状态下输出的参数;m_psi是系统表的接口。通过以上分析,可以清晰了解到,THR_LOCK_DATA数据结构是一个双向链表结构。
此外,还定义了st_lock_list结构,是THR_LOCK_DATA双向链表的头信息,其中data和last指针分别指向THR_LOCK_DATA双向链表的头和尾信息。其结构图如下所示:
图1 st_lock_list结构图
THR_LOCK类型是多线程的锁数据结构,主要包括双向链表数据类型的锁对象list;互斥锁mutex;四个数据信息列表读等待列表read_wait、读列表read、写等待列表write_wait、写列表write;写锁的数目write_lock_count;只是读操作,但没有写操作的数目read_no_write_count;五个与状态信息操作相关的函数get_status、copy_status、update_status、restore_status和check_status。
以上仅对这些结构及其相关的参数进行了简要的说明,详细的操作将在源码分析中,给出详细的介绍。
源码实现
THR_LOCK相关的操作,主要对核心的处理方法进行详细的分析,对于简单处理逻辑,可以直接参考源码,不再赘述。
thr_lock函数
thr_lock()函数的具体逻辑如下图所示。如果lock_type小于等于TL_READ_NO_INSERT的值,说明当前请求的锁类型为读锁(READ lock),则执行请求读锁处理逻辑;否则请求的为写锁(WRITE lock),执行请求写锁处理逻辑。如果在锁处理逻辑过程中(包括读锁和写锁),未获得相应的锁,则调用wait_for_lock()函数,等待获得相应的锁。具体锁请求处理逻辑和wait_for_lock()函数的逻辑参考相应的逻辑分析过程。
图2 thr_lock()流程图
请求读锁(Request for READ lock)的处理逻辑如下图所示。具体的,如果当前写队列(lock->write)中有数据,并且写队列数据的已经获得了写锁,并且请求的锁类型(lock_type)不是TL_READ_NO_INSERT或者写队列数据的写锁类型不是TL_WRITE_CONCURRENT_INSERT(两种锁类型是互斥的),则将当前数据插入到写队列(lock_read)的末尾。如果写队列数据的写锁类型为TL_WRITE_ONLY(写独占式锁),则当前不能获得读锁,则直接返回。
否则,当前写队列为空,如果写等待队列也为空(说明没有任何写锁及写锁等待),或者写等待队列数据的锁类型不是重要的写锁(除了TL_WRITE和TL_WRITE_ONLY类型),或者读锁为TL_READ_HIGH_PRIORITY(该锁的优先级高于TL_WRITE),或者已经获得了读锁,那么同样将当前数据插入到写队列的末尾。如果此时请求的锁类型为TL_READ_NO_INSERT,那么变量read_no_write_count自增。
如果当前写队列中有活跃的写锁或者有重要的写锁等待,那么此时读锁需要等待写锁释放后,再获取读锁。因此,此时调用wait_for_lock()函数,等待获取读锁。
图3 Request for READ lock处理逻辑
请求写锁(Request for WRITE lock)的处理逻辑如下图所示。具体的详细处理逻辑包括三个过程:
第一个过程:如果请求的锁类型是延迟写锁(TL_WRITE_DELAYED),而写队列(lock->write)数据的锁是只写锁(TL_WRITE_ONLY),那么不能获得延迟写锁,直接返回。而如果请求延迟写锁,并且写队列或者读队列有数据信息,那么将数据添加到写等待队列(lock->write_wait)中。如果请求的锁类型是写并发插入的锁类型(TL_WRITE_CONCURRENT_INSERT),那么数据的锁类型和请求的锁类型将赋值为thr_upgraded_concurrent_insert_lock全局变量的值,该值默认是TL_WRITE锁类型,如果设置了系统全局变量low_priority_updates,那么该值为TL_WRITE_LOW_PRIORITY。
第二个过程:如果已经获得了写锁,即写队列中有数据信息,并且锁类型为TL_WRITE_ONLY,那么无法获得写锁,直接返回。如果请求锁的类型和写队列中数据的锁类型为TL_WRITE_ALLOW_WRITE(因为TL_WRITE_ALLOW_WRITE锁类型允许其他线程读写操作),并且无写等待队列(如果有写队列需要首先处理写等待队列中的请求)。或者当前数据信息已经获得了写锁,那么将当前数据信息添加到写队列的末尾。
第三个过程:如果写队列和写等待队列都没有数据信息,并且请求写锁类型为延迟写锁(TL_WRITE_DELAYED),以及变量lock->read_no_write_count的值为0(表明当前多线程中没有获得TL_READ_NO_INSERT锁类型的操作),那么数据直接添加到写队列中,而不是过程一中请求延迟写锁时,将数据添加到写等待队列中。原因是当前没有任何写锁,不需要延迟写。
图4 Request for WRITE lock处理逻辑
wait_for_lock函数
wait_for_lock()函数主要是在请求锁时,由于当前写队列中有活跃的写锁,或者写等待队列中有高优先级的写锁,导致无法获取锁时,将会等待锁释放后请求锁。具体逻辑是:如果当前线程没有被终止,或者获取锁的数据信息在等待列表中,那么调用mysql_cond_timedwait()函数条件等待,如果等待超时或者条件变量释放,那么锁等待结束。如果锁等待超时,那么将数据信息从等待队列中删除,调用wake_up_waiter()函数,唤醒数据请求的锁。否则,数据信息获取锁被终止。如果该函数的流程图如下所示:
图5 wait_for_lock()流程图
wake_up_waiters函数
wake_up_waiters()函数的流程图如下所示。具体的,如果当前写队列中有活跃的写锁,则说明不需要唤醒,直接退出程序。否则,从写等待队列中取出等待写锁的数据,如果没有活跃的读锁,则释放写等待队列中的写锁,并释放可能的读锁。
图6 wake_up_waiter()流程图
如果有活跃的读锁,并且如果当数据的锁类型为TL_WRITE_DELAYED、TL_WRITE_ALLOW_WRITE、TL_WRITE_CONCURRENT_INSERT或者允许读锁,判断条件如下所示。从判断条件可以看出,只有当data不为NULL,并且data->type是小于TL_WRITE_DELAYED时,因为data是写等待队列的数据,因此只有TL_WRITE_DELAYED、TL_WRITE_ALLOW_WRITE和TL_WRITE_CONCURRENT_INSERT三种类型的锁。如果锁类型是TL_WRITE_DELAYED时,或者read_no_write_count参数为0时,即没有TL_READ_NO_INSERT类型的读锁时。则唤醒写锁,并且与读锁共存。
if (data && (lock_type=data->type) <= TL_WRITE_DELAYED && ((lock_type != TL_WRITE_CONCURRENT_INSERT && lock_type != TL_WRITE_ALLOW_WRITE) || !lock->read_no_write_count)) |
否则,如果写等待队列中无等待写锁,并且读等待队列中有数据时,调用free_all_read_locks()函数释放所有读锁。
wake_up_waiter()函数处理逻辑中,包含两个重要的处理过程,分别是释放写锁(Release write-locks)处理过程和唤醒写锁(start WRITE locks with READ locks)处理过程。以下分别对这两个过程进一步分析:
1、释放写锁(Release write-locks)处理过程
Release write-locks处理过程主要逻辑如下所示:
图7 Release write-locks处理过程
从以上逻辑处理过程可知,如果数据data不是NULL,并且data的锁类型不是TL_WRITE_LOW_PRIORITY(该锁类型优先级低于TL_READ),或者读等待队列为空(如果锁类型为TL_WRITE_LOW_PRIORITY,那么读等待队列为空时,锁类型可以获得),或者读等待队列数据的类型小于TL_READ_HIGH_PRIORITY(如果请求的锁类型为TL_WRITE_LOW_PRIORITY,并且读等待队列不为空,那么读等待队列数据的锁类型优先级要低于写锁的优先级,锁类型TL_READ_HIGH_PRIORITY和TL_READ_NO_INSERT的优先级要高于某些写锁的优先级)。因为锁类型TL_WRITE_LOW_PRIORITY的优先级低于TL_READ,因此如果当前有读锁,那么data请求写锁TL_WRITE_LOW_PRIORITY的话,不需要等待,可以直接处理。
如果不满足判定条件,那么如果读等待队列不为空的话,调用free_all_read_locks()函数释放所有读锁。
如果满足判定条件,首先判断写锁计数是否大于最大写锁数(unsigned long类型)。如果大于该值,那么调用free_all_read_locks()函数释放读等待队列中的所有读锁。否则,将data从写等待队列中删除,添加到写队列中,并信号通知等待线程。如果data的锁类型或锁等待队列中的锁类型不是TL_WRITE_ALLOW_WRITE(该锁类型允许其他线程进行读写操作,如果为该锁类型时,可以继续释放写锁),或者写等待队列为空的情况下,退出循环。否则,继续从写等待队列中取数据,释放写锁。
2、唤醒写锁(start WRITE locks with READ locks)处理过程
唤醒写锁(start WRITE locks with READ locks)的详细处理逻辑如下所示:
图8 start WRITE locks with READ locks处理过程
由以上流程图可知,如果当前锁类型为TL_WRITE_CONCURRENT_INSERT,那么升级锁为TL_WRITE,并释放所有读等待队列中的读锁。这是之所以升级写锁,是由于TL_WRITE_CONCURRENT_INSERT锁类型允许READ锁,但是为了避免与读锁类型TL_READ_NO_INSERT发生冲突,所以需要升级写锁为TL_WRITE。
否则,data的锁类型只有TL_WRITE_DELAYED和TL_WRITE_ALLOW_WRITE两种类型,那么将data从写等待队列中删除,添加到写队列中,并信号通知等待线程。如果锁类型和数据的锁类型为TL_WRITE_ALLOW_WRITE,并且写等待队列不为空的情况下,继续从写等待队列中释放写锁。否则,释放读等待队列中的所有读锁。
thr_unlock函数
thr_unlock()函数是多线程释放数据data上锁的处理逻辑,详细流程如下图所示。首先将data从锁队列中删除,如果锁类型是TL_READ_NO_INSERT,那么锁的read_no_write_count计数减1。最终调用wake_up_waiters()函数,唤醒等待锁的请求。
图9 thr_unlock()流程图
除了以上核心函数处理过程之外,THR_LOCK还提供了thr_multi_lock()函数处理获取多个锁;thr_multi_unlock()释放获取的多个锁;thr_lock_merge_status()将同一个表的共享相同的状态信息,即同一个表的status_param参数的值相同,主要是针对MyISAM和Maria存储引擎;thr_abort_locks()是终止所有线程的锁请求,写队列中的锁升级为TL_WRITE_ONLY锁,从而阻止新的线程请求锁;thr_abort_locks_for_thread()用于终止给定线程的所有锁请求;thr_downgrade_write_lock()将高级别的写锁降级为低级别的写锁;thr_upgrade_write_delay_lock()升级TL_WRITE_DELAYED写锁为高级别的写锁类型;thr_reschedule_write_lock()将高级别的写锁降级到TL_WRITE_DELAYED锁。这些函数的处理逻辑较简单,可以参考源码的实现,不再赘述。
结论
以上是对THR_LOCK数据结构源码的详细分析,通过分析可知,MySQL细粒度的锁类型,使得多线程对不同锁类型的请求时,可以同时获得锁,但由于锁的优先级关系和锁之间存在互相排斥,也可以保证通过锁对资源的控制,实现多线程有条不紊的操作数据。并且细粒度控制锁,可以更大程度地提高多线程的并发性。
此外,推荐《MySQL数据库上层加锁逻辑》博文,该文系统的测试和说明MySQL数据库上层加锁的逻辑,其中包含THR_LOCK相关的操作。从逻辑和实际SQL测试,有利于了解MySQL的上层锁机制。结合本文对THR_LOCK的源码分析,可以更进一步的了解底层多线程锁控制的机制,对THR_LOCK上层锁控制源码的分析,将在之后进行详细的分析。
1、《MySQL数据库上层加锁逻辑》 :
gpfeng_cs2013-04-24 12:06:25
king_wangheng:THR_LOCK_DATA结构是双向链表结构应该没有错误,通过next可以向后遍历,通过prev可以向前遍历,符合双向链表的特征。而hlist应该主要指带头结点的双向链表,而这里的THR_LOCK_DATA本身是一个双向链表结构,并定义了st_lock_list结构,分别指向双向链表的头和尾。个人认为,这些都是对双向链表的一种扩展。
我的理解:prev保存的是前一个节点next指针的地址,是方便插入删除的,并不是用来向前遍历链表的(当然这个是可以做到的)
回复 | 举报king_wangheng2013-04-23 23:36:54
gpfeng_cs:“通过以上分析,可以清晰了解到,THR_LOCK_DATA数据结构是一个*双向链表*结构”
这个存在问题,准确的说它是一个hlist结构,因为节点是无法找到它的prev node
参考:http://blog.csdn.net/zhanglei4214/article/details/6767288
THR_LOCK_DATA结构是双向链表结构应该没有错误,通过next可以向后遍历,通过prev可以向前遍历,符合双向链表的特征。而hlist应该主要指带头结点的双向链表,而这里的THR_LOCK_DATA本身是一个双向链表结构,并定义了st_lock_list结构,分别指向双向链表的头和尾。个人认为,这些都是对双向链表的一种扩展。
回复 | 举报