版本

  • MySQL 8.0.25

背景

除了 Mutex 的使用, 在 InnoDB 内核中, 为了增加 InnoDB 的并发读写, 引入了读写锁. Mutex 严格的限制只有一个 thread 可以进入临界区, 但是实际的存储引擎中, 例如针对数据 Page 可以区分读和写, 多个读操作同时访问一个数据 Page 是合法的. InnoDB 实现了一套读写锁的逻辑, 除了读锁 S, 写锁 X, 还引入了 SX 锁, SX 锁和 SX 和 X 锁之间互斥, 但是兼容 S 锁. 实际的使用场景例如数据 Page 在刷脏的时候加的是 SX 锁, 但是允许读 S 锁, 这也符合数据库的使用特征.

rw_lock_t 数据结构

struct rw_lock_t 是 InnoDB 读写锁的数据结构, 关键的成员变量有 lock_word, waiters, recursive, writer_thread, 在我们 GDB 调试 InnoDB 代码时, print rw_lock_t 时这几个成员变量可以帮助我们理解该读写锁的持有情况.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

/* 锁的兼容矩阵:
* 1. X 与 X 和 SX 和 S 全部互斥.
* 2. SX 和 SX 和 X 互斥, 但是兼容 S 锁. */

LOCK COMPATIBILITY MATRIX
S SX X
S + + -
SX + - -
X - - -

struct rw_lock_t
#ifdef UNIV_DEBUG
: public latch_t
#endif /* UNIV_DEBUG */
{
/** ... */

/** 锁状态. */
std::atomic<int32_t> lock_word;

/** 是否有线程等待在这个读写锁. */
std::atomic<bool> waiters;

/** 这个锁是否处于递归加锁的状态, 一个线程允许对一个 rw_lock 加了 SX 锁再次申请 X 锁. */
std::atomic<bool> recursive;

/** 持有锁的线程 id. */
std::atomic<std::thread::id> writer_thread;

/** ... */
};

lock_word

lock_word 代表当前读写锁的锁状态, 所以我们可以从 lock_word 来判断这个锁的持有状态, 比如可以得知该锁目前是 S 锁还是 X 锁, 还是 SX 锁. 如果是 S 锁, 有多少 S 锁, 或者当前这个锁是否已经被一个线程预定了 X 锁等等.

lock_word 初始值为X_LOCK_DECR.

1
2
#define X_LOCK_DECR 0x20000000 (10 进制 536870912)
#define X_LOCK_HALF_DECR 0x10000000 (10 进制 268435456)

申请 X 锁

通过函数rw_lock_x_lock_low()来申请获取 X 锁:

  1. 判断当前的lock_word是否大于 X_LOCK_HALF_DECR.

  2. 如果条件 1 满足就使用 CAS 减去 X_LOCK_DECR, 然后等待lock_word的值为 0.

  3. 如果条件 1 不满足, 则判断是否可以递归加锁, 即在申请 X 锁之前是否已经持有了 SX 或者 X 锁.

  4. 如果该线程之前已经持有了 SX 锁, 则将lock_word的值减去 X_LOCK_DECR, 然后等待lock_word的值为 -X_LOCK_HALF_DECR, 因为 SX 锁和 S 锁兼容, 所以由 SX 锁升级为 X 锁后, 需要等待其他线程持有的 S 锁释放.

  5. 如果该线程之前已经持有了 X 锁, 则修改lock_word后即可.

  6. 上述条件都不满足, 即其他线程可能持有了 SX 锁或者 X 锁, 则进入 spin 的等待状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
UNIV_INLINE
bool rw_lock_x_lock_low(
rw_lock_t *lock, /*!< in: pointer to rw-lock */
ulint pass, /*!< in: pass value; != 0, if the lock will
be passed to another thread to unlock */
const char *file_name, /*!< in: file name where lock requested */
ulint line) /*!< in: line where requested */
{
if (rw_lock_lock_word_decr(lock, X_LOCK_DECR, X_LOCK_HALF_DECR)) {
/* 1. 如果当前不存在 S 锁或者 SX 锁或者其他 X 锁, 步骤 1 是满足的,
* 步骤 2 也无需等待直接返回加锁成功. */
rw_lock_set_writer_id_and_recursion_flag(lock, !pass);

/* 2. 如果这个锁已经被其他线程加了数量小于 X_LOCK_HALF_DECR 个 S 锁,
* 步骤 1 也是满足的, 但是步骤 2 需要进入等待逻辑直到所有的 S 锁释放,
* 即使当前读写锁上已经有其他的 S 锁, 但是为了防止 X 锁饥饿状态,
* 所以将 X 预先分配给申请线程. */
rw_lock_x_lock_wait(lock, pass, 0, file_name, line);

} else {
if (!pass && lock->recursive.load(std::memory_order_acquire) &&
lock->writer_thread.load(std::memory_order_relaxed) ==
std::this_thread::get_id()) {

/* The existing X or SX lock is from this thread */
if (rw_lock_lock_word_decr(lock, X_LOCK_DECR, 0)) {
/* 之前已经持有了 SX 锁, 由 SX 锁升级为 X 锁. */
rw_lock_x_lock_wait(lock, pass, -X_LOCK_HALF_DECR, file_name, line);

} else {
/* 之前已经持有了 X 锁. */
if (lock->lock_word == 0 || lock->lock_word == -X_LOCK_HALF_DECR) {
/* 1. 上一次直接申请了 X 锁, 所以 lock_word 为 0.
* 2. 上一次是 SX 锁升级为 X 锁, 所以 lock_word 为-X_LOCK_HALF_DECR.
* 3. 所以 X 锁的 lock_word 减去 X_LOCK_DECR. */
lock->lock_word -= X_LOCK_DECR;
} else {
ut_ad(lock->lock_word <= -X_LOCK_DECR);
/* 之前已经持有了两个 X 锁, 所以当前的 lock_word 递减 1 即可. */
--lock->lock_word;
}
}
} else {
/* 申请失败, 返回进入等待. */
return false;
}
}

ut_d(rw_lock_add_debug_info(lock, pass, RW_LOCK_X, file_name, line));

lock->last_x_file_name = file_name;
ut_ad(line <= std::numeric_limits<decltype(lock->last_x_line)>::max());
lock->last_x_line = line;

/* 申请 X 锁成功. */
return true;
}

申请 SX 锁

通过函数rw_lock_sx_lock_low()来申请获取 SX 锁:

  1. 判断当前的lock_word是否大于 X_LOCK_HALF_DECR.

  2. 如果条件 1 满足就使用 CAS 减去 X_LOCK_DECR, 因为 SX 锁和 S 锁兼容, 所以无需等待 lock_word 的值为 0.

  3. 如果条件 1 不满足, 则判断是否可以递归加锁, 即在申请 X 锁之前是否已经持有了 SX 或者 X 锁.

  4. 如果该线程之前已经成功申请了一个 X 锁, 所以 lock_word -= X_LOCK_HALF_DECR.

  5. 上述条件都不满足, 即 SX 或者 X 锁已经被其他线程持有, 则进入 spin 的等待状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
bool rw_lock_sx_lock_low(
rw_lock_t *lock, /*!< in: pointer to rw-lock */
ulint pass, /*!< in: pass value; != 0, if the lock will
be passed to another thread to unlock */
const char *file_name, /*!< in: file name where lock requested */
ulint line) /*!< in: line where requested */
{
if (rw_lock_lock_word_decr(lock, X_LOCK_HALF_DECR, X_LOCK_HALF_DECR)) {
/* 1. 判断当前的 lock_word 是否大于 X_LOCK_HALF_DECR.
* 2. 如果条件 1 满足就使用 CAS 减去 X_LOCK_DECR, 因为 SX 锁和 S 锁兼容,
* 所以无需等待 lock_word 的值为 0. */

/* lock->recursive == true implies that the lock->writer_thread is the
current writer. As we are going to write our own thread id in that field it
must be the case that the current writer_thread value is not the current
writer anymore, thus recursive flag must be false. */
ut_a(!lock->recursive.load(std::memory_order_relaxed));

/* Decrement occurred: we are the SX lock owner. */
rw_lock_set_writer_id_and_recursion_flag(lock, !pass);

/* 设置 sx_recursive 的标记为 1. */
lock->sx_recursive = 1;

} else {
/* ... */
if (!pass && lock->recursive.load(std::memory_order_acquire) &&
lock->writer_thread.load(std::memory_order_relaxed) ==
std::this_thread::get_id()) {
/* This thread owns an X or SX lock */
if (lock->sx_recursive++ == 0) {
/* ... */

ut_ad((lock->lock_word == 0) ||
((lock->lock_word <= -X_LOCK_DECR) &&
(lock->lock_word > -(X_LOCK_DECR + X_LOCK_HALF_DECR))));
/* 之前已经成功申请了一个 X 锁, 所以 lock_word -= X_LOCK_HALF_DECR. */
lock->lock_word -= X_LOCK_HALF_DECR;
}
} else {
/* Another thread locked before us */
return false;
}
}

ut_d(rw_lock_add_debug_info(lock, pass, RW_LOCK_SX, file_name, line));

lock->last_x_file_name = file_name;

ut_ad(line <= std::numeric_limits<decltype(lock->last_x_line)>::max());
lock->last_x_line = line;

return true;
}

申请 S 锁

通过函数rw_lock_s_lock_low()来申请获取 S 锁, 判断的逻辑是当前的lock_word是否大于 0, 如果大于 0 则使用 CAS 操作减去 1.

如何通过 lock_word 判断锁信息

  1. lock_word = X_LOCK_DECR: 没有任何锁持有.

  2. X_LOCK_HALF_DECR < lock_word < X_LOCK_DECR: 存在 S 锁, S 锁的数量是(X_LOCK_DECR - lock_word), 并且没有任何 SX/X 锁的申请等待.

  3. lock_word == X_LOCK_HALF_DECR: 存在 SX 锁, 并且没有任何 SX/X 锁的申请在等待.

  4. 0 < lock_word < X_LOCK_HALF_DECR: 存在 SX 锁和 S 锁, 并且没有任何 SX/X 锁的申请在等待.

  5. lock_word == 0: 存在 X 锁, 并且没有任何 SX/X 锁的申请在等待.

  6. -X_LOCK_HALF_DECR < lock_word < 0: 存在 S 锁, 并且有一个 X 锁申请在等待.

  7. lock_word == -X_LOCK_HALF_DECR: 存在一个 SX 锁和 X 锁, 是递归持有, 并且没有 SX/X 锁的申请在等待.

  8. -X_LOCK_DECR < lock_word < -X_LOCK_HALF_DECR: 存在 S 锁, 并且有一个持有 SX 锁的线程在等待升级为 X 锁.

  9. lock_word == -X_LOCK_DECR: 存在两个递归持有的 X 锁.

  10. -(X_LOCK_DECR + X_LOCK_HALF_DECR) < lock_word < -X_LOCK_DECR: 存在 X 锁, 数量为 2 - (lock_word + X_LOCK_DECR).

  11. lock_word == -(X_LOCK_DECR + X_LOCK_HALF_DECR): 存在递归持有的 1 个 SX 锁和两个 X 锁.

  12. lock_word < -(X_LOCK_DECR + X_LOCK_HALF_DECR): 存在递归持有的 X 锁和 SX 锁, X 锁的数量是 2 - (lock_word + X_LOCK_DECR + X_LOCK_HALF_DECR).

总结

InnoDB 自行实现了一套读写锁, 并且允许 X 锁的预定从而避免饿死, 不过存在一个可能的情况是如果同时有多个 X 锁来申请, 仍然需要争抢, 并不保证申请顺序.

参考

WL#6363: InnoDB: implement SX-lock for rw_lock