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

前面做了一个开始,沿着路慢慢走下去。

0. 起源

开始之前,这里可以说说这次准备开始研究源代码的一个很大诱因了。前一段时间在生产环境遇到了一个InnoDB报错,这个错误甚至会导致InnoDB Crash:

InnoDB: Warning: a long semaphore wait:
–Thread 1222654272 has waited at ./include/btr0btr.ic line 53 for 241.00 seconds the semaphore:
S-lock on RW-latch at 0x2aaab510b818 created in file buf/buf0buf.c line 680

沿着这里的线索 buf/buf0buf.c line 680 找到了:

678 mutex_create(&block->mutex, SYNC_BUF_BLOCK);
679
680 rw_lock_create(&block->lock, SYNC_LEVEL_VARYING);
681 ut_ad(rw_lock_validate(&(block->lock)));

继续,就开始看rw_lock_create的实现,然后感觉需要看更多基础的一点的内容,这样就有了前面一片文章,继续研究,就有了现在的这篇文章。

1. 读写锁

前文介绍了,InnoDB中如何使用mutex_struct结构来保护内存(变量)。我们可以看到,如果使用mutex_struct时,其实实现的是一个排他锁。如果每个变量可以被允许并发共享(只读)访问,写的时候才需要“排他”访问的话,再用mutex_struct的话就不合适了。所以,InnoDB在mutex_struct的基础上,又实现了一个读写锁

本文中”共享锁”即”读锁”,”排他锁”即”写锁”。关于读写锁的基本知识这里就不再介绍了。先说说InnoDB的实现原理,再来看细节。InnoDB使用了一个整数来标记当前锁的状态。

2. 读写锁 InnoDB实现机制

InnoDB用了一个很简单的读写锁实现机制,使用一个整型变量lock_word标志当前锁的状态:

struct rw_lock_struct {
volatile lint lock_word; /*!< Holds the state of the lock. */

初始化时,lock_word的值为1048576(0x00100000),当每一个线程获得一次读锁时,lock_word值减一。每次获取写锁时,lock_word的值则减少1048576。这样,每次根据lock_word当前值,就可以获取当前锁的状态了。下面是关于lock_word规则的详细说明:

1) lock_word初始值为X_LOCK_DECR(#define X_LOCK_DECR 0×00100000 //1,048,575)
2) 每一线程以读(共享)的方式获取该锁,则lock_word减1
3) 每次线程以写(排他)的方式获取锁,则lock_word减X_LOCK_DECR
4) 如果某线程以写(排他)方式获得锁,该线程仍可以以递归的方式获得该写锁
5) 如果线程在等待获取写锁,lock_word减X_LOCK_DECR,且这样可以阻止线程再获得读锁
6) 写锁虽然可以递归获取,但是读锁不能递归获取

根据上面的规律,我们就可以推导出,lock_word取不同区间的值时,当前锁的状态。例如当0<lock_word<X_LOCK_DECR时,当前锁以读的方式被获取,下面是lock_word不同区间值锁的状态表:

1) lock_word == X_LOCK_DECR 则锁处于初始状态,即没有任何线程获得该锁
2) 0 < lock_word < X_LOCK_DECR 则锁处于读锁状态,读锁持有者个数为X_LOCK_DECR-lock_word;且并没有任何写锁处于等待状态
3) lock_word == 0 则锁处于写锁状态
4) -X_LOCK_DECR < lock_word < 0 则锁处于读锁状态,且有一个线程在等待写锁。-lock_word就是读锁的个数
5) lock_word <= -X_LOCK_DECR 则当前锁处于“递归写锁”(同一个线程多次获得该写锁)状态,写锁的个数是((-lock_word) / X_LOCK_DECR) + 1

这里看到,当线程需要获取该锁(读、写)的时候,根据lock_word值即可以判断当前锁的状态,也就可以知道自己是否可以获得锁。

3. 关于递归写锁

lock_word基本上包含了锁的全部状态,但是如果细心的话,你会发现有一个例外:当一个线程A需要获得写锁的时候,lock_word的如果是写锁状态时,线程A还必须知道当前持有该锁的线程ID,如果持有锁的线程就是自己,则线程A可以获得写锁。所以,rw_lock_struct结构中还有另一个成员变量:

volatile os_thread_id_t writer_thread;

当锁以排他方式被持有的时候,该变量记录持有者的线程ID。

如果细心,你还可能发现,这里虽然提到了“递归写锁”,却没有提到“递归读锁”。在InnoDB的读写锁实现时,并不考虑读锁的递归,如果锁被排他方式获取,即使是同一个线程,仍然无法获取读锁。

4 .lock_word的一致性

在读写锁的实现中,lock_word的可以“举足轻重”。这就要求每次对lock_word的修改都要做到“一致”。否则,这把锁就坏了。

4.1 什么是一致性

如果是单线程,获取读锁我们可能写出下面的代码:

if(local_lock_word > 0){
local_lock_word = local_lock_word -1;
}

上面的代码,在多线多处理环境是不安全的,会导致lock_word的不一致。线程A需要获取读锁,则CPU在执行上面代码的时候,首先获取local_lock_word的值将其载入寄存器,然后运算,然后返回到内存。如果在“载入寄存器之后运算之前”,lock_word的值发生了改变(例如另一个线程B成功获取了读锁),而这时当前线程A却不知道,线程A仍然把自己的值回写到内存,则会覆盖线程B对lock_word的修改。lock_word的值就不一致了,等锁全部释放后,lock_word的值就可能不是X_LOCK_DECR。

上面这段话可能有点绕,简单的说:多线程并发时,上面简单的赋值操作是不可靠的。回想一下多线程,就不难理解上面这段话了。

4.2 InnoDB如何保证lock_wrod的一致性

介绍完了前面那么多原理,这里总算可以看看源代码的一些东西了。InnoDB封装了函数rw_lock_lock_word_decr来实现对lock_wrod的一致操作。InnoDB会根据不同的编译环境,决定使用不同的策略。简单地,InnoDB使用了前面介绍的mutex_t(排他锁)来实现lock_word的一致操作,即在操作之前获取排他锁;另外,InnoDB根据不同的CPU平台还可以使用Compare-And-Swap来实现,下面来看看源码:

这里是核心代码,详细参考sync0rw.ic文件

ibool rw_lock_lock_word_decr(rw_lock_t* lock, ulint amount) { #ifdef INNODB_RW_LOCKS_USE_ATOMICS while (local_lock_word > 0) { if (os_compare_and_swap_lint(&lock->lock_word,local_lock_word,local_lock_word - amount)) { return(TRUE); } local_lock_word = lock->lock_word; } return(FALSE); #else /* INNODB_RW_LOCKS_USE_ATOMICS */ ibool success = FALSE; mutex_enter(&(lock->mutex)); if (lock->lock_word > 0) { lock->lock_word -= amount; success = TRUE; } mutex_exit(&(lock->mutex)); return(success); #endif /* INNODB_RW_LOCKS_USE_ATOMICS */ }

有了Compare-And-Swap或者排他锁(前文的mutex_t)的保证,InnoDB就可以一致的更改锁的状态(lock_word)了。

5. 再休息一下

这篇文章算是介绍了一下InnoDB读写锁的实现机制,本来想把具体实现也写了,发现作为一篇博文是有点长了,而且已经非常的晦涩难懂了。所以暂时休息一下,欲知后事如何,且听下回分解。

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

9 responses to “MySQL/InnoDB源代码:线程并发访问控制(续)”

  1. hoterran

    innodb 居然自己实现读写锁,我还以为和mysql一样用pthread_rwlock_t原语,compare and swap 也是与时俱进啊~~~

    文章不错,赞~~

  2. plantegg

    InnoDB: Warning: a long semaphore wait:
    –Thread 1222654272 has waited at ./include/btr0btr.ic line 53 for 241.00 seconds the semaphore:
    S-lock on RW-latch at 0×2aaab510b818 created in file buf/buf0buf.c line 680
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~不知道这个问题怎么样解决才能让MySQL不当机呢?

    我也多次碰到过这种问题,一旦这样的错误一多、等待时间一长,基本上MySQL要挂掉了。

    我的经验是大概等待13分钟,MySQL自动重启,这个过程中,MySQL不再有任何响应,系统压力基本为0了

  3. @plantegg 说说你的OS版本 MySQL/InnoDB版本

  4. plantegg

    CentOS 5.0 MySQL 5.0.41 innodb的话官网打包自带的,估计是1.0.6?

  5. @plantegg 深层的原因并没有找到。我们的症状,发现是磁盘IO繁忙的时候会有,适当避免IO繁忙可以绕过。有很多其他相关的Bug报告在MySQL Bug上,也可以看一下。

  6. plantegg

    @orczhou 磁盘IO繁忙
    ~~~~~~~~~~这个也是我发现的症状,当大量并发冲进来(比如定时任务),内存有不够的时候,需要使用Swap,问题就出现了。

    我也是估计,但是没证实,应该是这个时候全局锁被OS Swap到硬盘上了,而MySQL没有意识到这点。

    我翻了一下源代码,好像这个锁是一个全局锁。

    现在只是从实践上验证了,尽量把定时任务分散、避免Swap IO,这个问题就不再重现了。

  7. […] MySQL/InnoDB源代码:线程并发访问控制(再续) 2010-07-26  |  19:19分类:MySQL,代码细节  |  5 views 这是该系列的第三篇文章(1,2)了。之所以选择并发线程控制着手研究InnoDB的代码有两个原因:第一,这段代码相对独立,不要了解太多的相关代码就可以理解;第二,稍微多看一些代码你会发现,到处都是线程并发控制相关的代码出现,所以这也是一个基础。 […]

  8. 了沧桑

    不是说写锁都是独占锁吗?怎么这部分:”5) lock_word <= -X_LOCK_DECR 则当前锁处于“递归写锁”(同一个线程多次获得该写锁)状态,写锁的个数是((-lock_word) / X_LOCK_DECR) + 1; “ 还计算写锁的个数吗?这里的个数不是说占有该锁的线程个数?没明白!

  9. 了沧桑

    在网上还看一个观点: 读写锁 在读的状态, 若有写请求的线程在等待,又有读请求的线程进来,会先与这个写请求的线程完成。 这与你说的“5) 如果线程在等待获取写锁,lock_word减X_LOCK_DECR,且这样可以阻止线程再获得读锁” 也是矛盾的。

Leave a Reply

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