MySQL/InnoDB源代码:线程并发访问控制

作为DBA关心的更多的可能是原理、机制,对于源码一般大家也不是特别关心,也不太用得上。而且对于对于源代码级别的细节也很难用文字表达自己的理解,最终理解必须还是需要自己去看看每一行代码到底是怎样的。也读过《Understanding.MySQL.Internals》的大部分章节,作者也是偏重于从代码的实现目的(原理、机制)来介绍的,国内的《MySQL核心内幕》(个人对于“核心内幕”这个词有莫名的反感)算是更多的从源码开始介绍MySQL了,可是这本书也许是授予篇幅的限制,介绍的东西也并不多。

开始写这篇文章,不能期待自己能写多少,也不能期待自己能研究多少,不过至少走出了自己探索的第一步。文章的宗旨不在于能够多么细致的分析MySQL的源代码,而是希望能给自己,能给他人打开走向源代码的第一道门。

我是BNU数学系毕业的,对C语言知道甚少,C语言的经历大概就是“计算方法”课程中实现的求解各种方程的撇脚程序了。所以虽然我会极力避免错误,但是相信还是会犯一堆错误,希望各位看官能够宽容点,有错误指出来,慢慢改正。

0. 开始

InnoDB的代码通过宏定义考虑很多平台的兼容问题,这里分析的主要是类Unix/Linux平台(POSIX标准)的代码段,所以下面的很多代码段也是删除相关兼容性代码的。本文InnoDB源码是Plugin1.0.6版本。

1. InnoDB多线程并发访问

MySQL是一个多线程的实现的DBMS,多线程编程需要解决的一个重要问题就是线程间并发访问的控制。一般某个线程更改需要更改某个内存块(变量)时,首先要先将数据从内存读取到寄存器,然后计算更改之,最后回写到内存块。如果有多个线程同时在修改内存块,而且读取同时(“同时”表示在两个线程修改该值之前读取)发生,那么两个线程将有一个会丢失修改。

所以,希望保持内存数据(变量)“一致”、“准确”时,线程在操作这些变量时,需要先获取对应的锁。利用各个变量对应的“锁机制”来保证数据访问的并发控制。

2. 基本原理:Posix thread

多线程并发访问控制中,InnoDB使用标准POSIX线程模型中的mutex(condition variable)实现基本的排他访问。在对mutex(condition variable)的封装的基础上,又实现了相应的读写锁。

2.1 os_fast_mutex_t VS pthread_mutex_t

pthread_mutex_t封装在os_fast_mutex_t中,condition variable则封装在了os_event_struct中。

typedef pthread_mutex_t os_fast_mutex_t;

对应的mutex操作分别封装在了os_fast_mutex_init / os_fast_mutex_lock / os_fast_mutex_unlock / os_fast_mutex_free中,这里我们看看os_fast_mutex_trylock的封装函数实现:

ulint os_fast_mutex_trylock( os_fast_mutex_t* fast_mutex) { #ifdef __WIN__ EnterCriticalSection(fast_mutex); return(0); #else return((ulint) pthread_mutex_trylock(fast_mutex)); #endif }
2.2 os_event_struct VS condition variable

Condition variable较之pthread_mutex_t要复杂一点,对应的封装也更复杂。os_event_struct封装了condition variable全部操作,主要包括初始化,陷入等待,广播唤醒:os_event_create(初始化)/os_event_wait_low(等待)/os_event_set(广播唤醒)。下面是基本数据结构struct os_event_struct:

struct os_event_struct { os_fast_mutex_t os_mutex; ibool is_set; ib_longlong signal_count; pthread_cond_t cond_var; /* condition variable is used in waiting for the event */ ...... };

这里抽取一个广播唤醒开函数os_event_set看看:

void os_event_set( os_event_t event) /*!< in: event to set */ { os_fast_mutex_lock(&(event->os_mutex)); if (event->is_set) { /* Do nothing */ } else { event->is_set = TRUE; event->signal_count += 1; ut_a(0 == pthread_cond_broadcast(&(event->cond_var))); } os_fast_mutex_unlock(&(event->os_mutex)); }

可以看到上面函数,没有什么神秘的地方,就是判断了一下是否需要广播,然后调用了pthread_cond_broadcast函数广播。

数据结构os_fast_mutex_t和os_event_t看到,InnoDB并不是那么复杂,但是需要对Linux(或者说POSIX)有一定的理解。

另外,算是最底层的数据结构,事实上,这个封装还没结束,InnoDB其他模块中用的较多的是mutex_struct和rw_lock结构,mutex_t是对os_event_t封装,rw_lock是对mutex_t的封装。

3. 前奏:mutex_struct
3.1 mutex_struct的一般说明
struct mutex_struct { os_event_t event; /* Used by sync0arr.c for the wait queue */ ulint lock_word; /* This ulint is the target of the atomic test-and-set instruction in Win32 */ ulint waiters; //如果有线程在等待,设置为1 UT_LIST_NODE_T(mutex_t) list; const char* cfile_name;/* File name where mutex created */ ulint cline; /* Line where created */ #ifndef UNIV_HOTBACKUP ulong count_os_wait; /* count of os_wait */ #endif /* !UNIV_HOTBACKUP */ };

mutex_struct相关的函数有很多,一般需要使用的有:mutex_create、mutex_enter、mutex_exit;还有一些其他的函数:mutex_spin_wait、mutex_free、mutex_signal_object。

3.2 mutex_struct的典型用法

InnoDB中一个典型的mutex_struct使用如下:

mutex_create(&rw_lock_list_mutex); mutex_enter(&rw_lock_list_mutex); ... //这里,对rw_lock_list_mutex所保护的对象,这里可以进行一致的、排他操作 ... mutex_exit(&rw_lock_list_mutex);

mutex_create函数相对简单,做了一些初始化的工作。我们继续沿着线索来看看mutex_enter的实现。这个函数实现:

typedef struct mutex_struct mutex_t; void mutex_enter_func( mutex_t* mutex, const char* file_name, ulint line) { ut_d(mutex->count_using++); if (!mutex_test_and_set(mutex)) { ut_d(mutex->thread_id = os_thread_get_curr_id()); return; /* Succeeded! */ } mutex_spin_wait(mutex, file_name, line); }

代码说明:首先,尝试使用mutex_test_and_set(这是对os_fast_mutex_trylock的封装)获得锁,成功则返回。失败,则调用mutex_spin_wait陷入“spin lock的方式”获取锁。spin lock的方式是指,自己不释放cpu资源,而是自己空循环一段时间后,在重新尝试获取锁。

3.3 继续深入:mutex_spin_wait

线索到了mutex_spin_wait了,该函数是InnoDB里面实现了spin lock的主要部分。这里简单介绍一下原理:首先线程检查lock_word,如果被设置0(表示当前并没有变量并没有被锁住),则直接调用mutex_test_and_set(其实是os_fast_mutex_trylock)来尝试获取锁。如果lock_word=1那么,则表示当前的锁已经被某个线程获取,则线程进入空循环,延迟一段时间(spin lock),等待lock_word被设置为0后,立刻调用mutex_test_and_set尝试获取锁。如果lock_word一直1(锁一直在被占用),那么线程将在延迟SYNC_SPIN_ROUNDS个单位时间后,线程将尝试调用mutex_test_and_set一次(碰运气),如果仍失败,这时线程将陷入等待(这时Condition variables开始登上舞台),如果其他线程释放该锁,则会将lock_word被设置为0,并进行一次广播,那么前面陷入等待的线程将会被唤醒,并重新调用mutex_test_and_set尝试获取锁。

下面是mutex_spin_wait的一个简单流程图:

mutex_spin_wait_v3

3.4 小结mutex_struct

再回头看看mutex_struct的一般使用方法,先是mutex_create,然后是mutex_enter,最后是mutex_exit,该函数会调用os_event_set进行一次广播(广播函数本身是会释放pthread_mutex_t的)。

4. 休息一会儿

至此,我们看到,InnoDB一般通过数据结构mutex_struct来实现对内存块(变量)的完全排他访问。如果只是排他访问,可能会导致效率较低,因为很多时候,不需要排他,只需要共享方式访问就可以了。InnoDB在mutex_struct基础上,再做了一次封装,对应的数据结构为rw_lock_struct。

好了,这篇文章已经够长了….欲知后事如何,且听下回分解。

广告时间:工作机会–MySQL Hacker

14 responses to “MySQL/InnoDB源代码:线程并发访问控制”

  1. Jordan Fung

    把mutex和cond封装成event是为了考虑方便移植到win平台

  2. 是的。看得出来,大量的封装和预定义都是为了跨平台考虑的

  3. 板桥

    好文顶啊!

  4. 近距离膜拜高手!

  5. 咋看到你最近在搞C了呢?现在明白了!

  6. […] Wish list 上一篇:MySQL/InnoDB源代码:线程并发访问控制 […]

  7. nana0730

    楼主是淘宝的DBA哦,敬仰敬仰。。

  8. nana0730

    可否留个qq或者阿里旺旺,交流下。。

  9. 我预谋1年。一直无从下手。。膜拜大神。。期待更多作品

  10. […] 在innobase_xa_prepare函数中需要需要获取排他锁prepare_commit_mutex,直到上面的第一阶段结束才释放该排他锁。这意味着,上面的“第一阶段”就无法再并发一次性提交fsync了,即Group Commit也就无法完成了。(注:第一阶段包括了binlog的fsync操作,直到整个第一阶段结束prepare_commit_mutex才释放,所以Binlog就无法Group commit了,如果prepare_commit_mutex提前释放了,则可能导致InnoDB内部的事务顺序和Binlog内部事务的顺序不一致,Replication就没有意义了) 2.2 Facebook的解决方案 […]

  11. […] [4]http://www.orczhou.com/index.php/2010/06/mysql-InnoDB-source-code-sync-1/ Mysql/InnoDB源代码:线程并发访问控制 […]

  12. billy

    好文,有一些疑问
    “首先线程检查lock_word,如果被设置0(表示当前并没有变量并没有被锁住)”
    这个似乎不准确
    我大概翻了下代码,如下示
    #define X_LOCK_DECR 0x00100000 /* 0x00100000 allows 1,048,575 concurrent readers and 2047 recursive writers */

    267 lock->lock_word = X_LOCK_DECR;
    268 lock->waiters = 0;
    请指教?

  13. 不理解为何要使用这一套mutex机制,而不是简单调用pthread_mutex_lock?

  14. […] 自从有了微薄后博客就写得少了,上一篇博客已经是6月份写的了… 从写第一篇关于MySQL源码的文章之后也已经过了很久,继续上路。 […]

Leave a Reply

Your email address will not be published. Required fields are marked *