深入理解TiKV分布式事务,加锁的 KEY 是什么

Tiit 580 2024-03-08

前言

深入理解TiKV分布式事务,加锁的 KEY 是什么

一般为了表述的简洁,我们在介绍 TiDB 分布式事务的时候,经常说 TIDB 会对 KEY 加锁,或者对 KEY 进行锁的检测,从而保障事务的逻辑性和隔离性。

但是这个 KEY 到底是什么,对想要了解分布式事务的同学来说,可能不够直观。

我们今天将会使用具体的 SQL,结合不同类型的主键与索引,来观察 TIKV 分布式事务中的锁到底是什么。

开始之前,需要参考文章:《说计算》来了解关系型数据库KV 键值之间的关联

TiDB 表中的一条数据,最后会对应多个 Key-Value 去存储。我们来一起回顾一下重点:这些 Key-Value 分为两类:

  • RowID => Value : 主键信息,该类数据存储着当前对应行的所有信息。

    • { Key: tablePrefix{tableID}_recordPrefixSep{rowID}, Value: [col1, col2, col3, col4] }

  • Index Key => RowID: 索引信息,该类数据为索引到 RowID 的映射。表有多少的索引,每行数据就有多少个这样的 Key-Value。根据是否是唯一索引,分为如下两种类型

    • { Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue,            Value: rowID }

    • { Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID, Value: null }

理解了上面,其实分布式事务就是对这些 KEY 进行加锁: 一个是对 主键 进行加锁,一个是对 唯一索引 进行加锁。

对于普通的索引来说: (tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID)

由于分布式事务会对 rowID 主键进行加锁,因此普通索引的 KEY 实际上受到了 rowID 这个主键上面锁的保护,不需要加锁。

测试过程中,建表语句如下:

CREATE TABLE `MANAGERS_UNIQUE` (  `MANAGER_ID` int(11) NOT NULL,  `FIRST_NAME` varchar(45) NOT NULL,  `LAST_NAME` varchar(45) NOT NULL,  `LEVER` int(11) DEFAULT NULL,
  PRIMARY KEY (`MANAGER_ID`) 
  UNIQUE KEY `FIRST` (`FIRST_NAME`),
  KEY `LEVEL` (`LEVEL`)
)

插入数据:

mysql> INSERT INTO MANAGERS_UNIQUE(MANAGER_ID,FIRST_NAME, LAST_NAME) VALUES (14273,'Brad7','Craven7',7);
mysql> INSERT INTO MANAGERS_UNIQUE(MANAGER_ID,FIRST_NAME, LAST_NAME) VALUES (14274,'Brad8','Craven8',8);

INSERT 语句

执行的 SQL 为:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO MANAGERS_UNIQUE(MANAGER_ID,FIRST_NAME, LAST_NAME, LEVEL) VALUES ('14275','Brad9','Craven9',9);
Query OK, 1 row affected (0.01 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad9\000\000\000\374\201\200\240\337\201\206\353\233\006\357\246\002f\0067\2540\033\350\000\001c\0067\2540\033\350\000\002l\000\000\000\000\000\000\000\000\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad9\000\000\000\374\201\200\240\337\201\206\353\233\006\365\246\002f\0067\2540\033\350\000\001c\0067\2540\033\350\000\002l\000\000\000\000\000\000\000\000\001"

Total scanned keys: 2

分别对两个 KEY 进行解析:

zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374
├─## decode rocksdb data key
│ └─"t\\200\\000\\000\\000\\000\\000\\000\\377j_i\\200\\000\\000\\000\\000\\377\\000\\000\\002\\001Brad\\3779\\000\\000\\000\\374\\000\\000\\000\\374"
│   ├─## table prefix
│   │ └─table: -2579946652266647504
│   └─## decode go literal key
│     └─"t\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374"
│       ├─## decode mvcc key
│       │ └─"t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad9\000\000\000\374"
│       │   ├─## table prefix
│       │   │ └─table: 106
│       │   └─## table index key
│       │     ├─table: 106
│       │     ├─index: 2
│       │     └─"\001Brad9\000\000\000\374"
│       │       └─## decode index values
│       │         └─kind: Bytes, value: Brad9
│       └─## table prefix
│         └─table: 255


"zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374"
├─## decode rocksdb data key
│ └─"t\\200\\000\\000\\000\\000\\000\\000\\377j_r\\0011427\\3775\\000\\000\\000\\374\\000\\000\\000\\374"
│   ├─## table prefix
│   │ └─table: -2579946652266647504
│   └─## decode go literal key
│     └─"t\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374"
│       ├─## decode mvcc key
│       │ └─"t\200\000\000\000\000\000\000j_r\00114275\000\000\000\374"
│       │   ├─## table prefix
│       │   │ └─table: 106
│       │   └─## table row key
│       │     ├─table: 106
│       │     └─"\00114275\000\000\000\374"
│       │       └─## decode index values
│       │         └─kind: Bytes, value: 14275
│       └─## table prefix
│         └─table: 255

INSERT 比较简单,分别对 RowID 和 唯一索引 进行加锁。

对唯一索引加锁是必要的,防止有其他数据并发的也在更新其唯一索引到相同的值。

DELETE

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM MANAGERS_UNIQUE WHERE MANAGER_ID=14274;
Query OK, 1 row affected (0.02 sec)

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3778\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad8\000\000\000\374\201\200\200\363\203\210\353\233\006\227\230\004f\0067\254@>`\000\001c\0067\254@>`\000\002l\0067\253\223\264<\000\002\001"key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad8\000\000\000\374\201\200\200\363\203\210\353\233\006\233\230\004f\0067\254@>`\000\001c\0067\254@>`\000\002l\0067\253\223\264<\000\002\001"

Total scanned keys: 2

DELETE 也比较简单,分别对 RowID 和 唯一索引 进行加锁。

DELETE 对唯一索引加锁好像是非必要的?目前本人想不到需要对 DELETE 的唯一索引加锁的场景。

SELECT FOR UPDATE

主键搜索场景

首先开启悲观事务,同时对主键进行 SELECT FOR UPDATE:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14273 FOR UPDATE;
+------------+------------+-----------+
| MANAGER_ID | FIRST_NAME | LAST_NAME |
+------------+------------+-----------+
| 14273      | Brad7      | Craven7   |
+------------+------------+-----------+
1 row in set (0.01 sec)

此时扫描 TIKV 所有的 LOCK 记录:

➜ ./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", 
value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\246\322\243\352\233\006\262\307\025f\0067\251\035$\314\000\001c\0067\251\035$\314\000\002l\0067\244\235?\264\000\002\001"

Total scanned keys: 1

对 KEY 进行验证解析:

"74800000000000006A5F72013134323733000000FC"
└─## decode hex key
  └─"t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374"
    ├─## table prefix
    │ └─table: 106
    └─## table row key
      ├─table: 106
      └─"\00114273\000\000\000\374"
        └─## decode index values
          └─kind: Bytes, value: 14273

可以得出结论,名字主键的场景下 SELECT FOR UPDATE 语句只会对 ROWID 进行加锁。

唯一键搜索场景

首先开启悲观事务,同时对唯一键进行 SELECT FOR UPDATE:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where FIRST_NAME='Brad7' FOR UPDATE;
+------------+------------+-----------+
| MANAGER_ID | FIRST_NAME | LAST_NAME |
+------------+------------+-----------+
| 14273      | Brad7      | Craven7   |
+------------+------------+-----------+
1 row in set (0.00 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374", 
value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\203\200\260\311\312\251\352\233\006\227\323\003f\0067\251L\251,\000\003c\0067\251L\251,\000\004l\0067\244\235?\264\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", 
value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\203\200\260\311\312\251\352\233\006\343\204\003f\0067\251L\251,\000\003c\0067\251L\251,\000\004l\0067\244\235?\264\000\002\001"

Total scanned keys: 2

发现有两个 KEY,我们分别解析一下:

"t\\200\\000\\000\\000\\000\\000\\000\\377j_i\\200\\000\\000\\000\\000\\377\\000\\000\\002\\001Brad\\3777\\000\\000\\000\\374\\000\\000\\000\\374"
├─## table prefix
│ └─table: -2579946652266647504
└─## decode go literal key
  └─"t\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374"
    ├─## decode mvcc key
    │ └─"t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374"
    │   ├─## table prefix
    │   │ └─table: 106
    │   └─## table index key
    │     ├─table: 106
    │     ├─index: 2
    │     └─"\001Brad7\000\000\000\374"
    │       └─## decode index values
    │         └─kind: Bytes, value: Brad7
    └─## table prefix
      └─table: 255


"zt\\200\\000\\000\\000\\000\\000\\000\\377j_r\\0011427\\3773\\000\\000\\000\\374\\000\\000\\000\\374"
├─## decode rocksdb data key
│ └─"t\\200\\000\\000\\000\\000\\000\\000\\377j_r\\0011427\\3773\\000\\000\\000\\374\\000\\000\\000\\374"
│   ├─## table prefix
│   │ └─table: -2579946652266647504
│   └─## decode go literal key
│     └─"t\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374"
│       ├─## decode mvcc key
│       │ └─"t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374"
│       │   ├─## table prefix
│       │   │ └─table: 106
│       │   └─## table row key
│       │     ├─table: 106
│       │     └─"\00114273\000\000\000\374"
│       │       └─## decode index values
│       │         └─kind: Bytes, value: 14273
│       └─## table prefix
│         └─table: 255

可以得出结论,以唯一索引为搜索条件的话,那么会加两个 KEY 的锁。

一个是 RowID,一个是 UniqueIndex

普通索引搜索场景

执行的 SQL 为:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where level=7 for update;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
+------------+------------+-----------+-------+
1 row in set (0.02 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock[2024/02/27 20:03:06.977 +08:00] [WARN] [config.rs:315] ["Create jemalloc nodump allocator for block cache failed: Not implemented: Not compiled with JEMALLOC, continue with default allocator"] [thread_id=0x4]
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\344\357\270\352\233\006\252\256\003f\0067\251\311 \370\000\001c\0067\251\311 \370\000\002l\0067\251\306\253L\000\002\001"

Total scanned keys: 1

普通索引命中的情况下,也只对 RowID 进行加锁。

无索引搜索场景

执行的 SQL 为:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where LAST_NAME='Craven7' FOR UPDATE;
+------------+------------+-----------+
| MANAGER_ID | FIRST_NAME | LAST_NAME |
+------------+------------+-----------+
| 14273      | Brad7      | Craven7   |
+------------+------------+-----------+
1 row in set (0.01 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\207\200\220\330\230\261\352\233\006\260\347\002f\0067\251\212\204<\000\001c\0067\251\212\204<\000\002l\0067\244\235?\264\000\002\001"

Total scanned keys: 1

无索引命中的情况下,也只对 RowID 进行加锁。

SELECT 未命中数据场景

RR 隔离级别

命中点查

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14275 FOR UPDATE;
Empty set (0.00 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114275\000\000\000\374\201\200\360\306\231\335\362\233\006\333\267\003f\0067\312\351\230\334\000\001c\0067\312\351\230\334\000\002l\0067\301ja\344\000\002\001"

Total scanned keys: 1

RR 隔离级别下,命中点查,即使没有数据,也会对点查的唯一索引或者 RowID 进行加锁。

可以看做弱化的间隙锁。

未命中点查

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where LEVEL=9;
Empty set (0.01 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

Total scanned keys: 0

未命中点查情况下,没有数据情况下,TIKV 是不加锁的。

RC 隔离级别

命中点查

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14275 FOR UPDATE;
Empty set (0.00 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

Total scanned keys: 0

RR 隔离级别下,命中点查,即使没有数据,也会对点查的唯一索引或者 RowID 进行加锁。

可以看做弱化的间隙锁。

未命中点查

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where LEVEL=9;
Empty set (0.01 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

Total scanned keys: 0

RR 级别下,无论是否命中点查,没有数据,TIKV 是不加锁的。

SELECT 命中多条数据场景

执行的 SQL 为:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from MANAGERS_UNIQUE where MANAGER_ID > 0 for update;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
| 14274      | Brad8      | Craven8   |     8 |
+------------+------------+-----------+-------+
2 rows in set (0.02 sec)

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", 
value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\200\355\321\363\352\233\006\264\256\001f\0067\253\235BX\000\001c\0067\253\235BX\000\002l\0067\251\306\253L\000\002\001"

key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374", 
value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\200\355\321\363\352\233\006\264\256\001f\0067\253\235BX\000\001c\0067\253\235BX\000\002l\0067\253\223\264<\000\002\001"

Total scanned keys: 2

命中多条数据的情况下,每条数据的 RowID 将会被加锁。

特别的,如果使用 SQL:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from MANAGERS_UNIQUE where FIRST_NAME in ("Brad7","Brad8")  for update;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
| 14274      | Brad8      | Craven8   |     8 |
+------------+------------+-----------+-------+
2 rows in set (0.01 sec)

也就是使用唯一索引的 WHERE IN 来搜索的话,此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\310\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\244\235?\264\000\002\002"
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3778\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\310\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\253\223\264<\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\313\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\251\306\253L\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\313\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\253\223\264<\000\002\001"

Total scanned keys: 4

虽然命中了 2 行数据,但是却对 4 个 KEY 进行了加锁。分别是每行数据的 RowID 和 唯一索引。

总结

上述场景总结如下:

Result 行数LOCK 数量RowID锁唯一键锁SQL
主键搜索1110select * from MANAGERS_UNIQUE where MANAGER_ID=14274 for update;

唯一索引搜索1211select * from MANAGERS_UNIQUE where FIRST_NAME='Brad7'   for update;

普通索引搜索1110select * from MANAGERS_UNIQUE where level=7 for update;

无索引搜索1110select * from MANAGERS_UNIQUE where LAST_NAME='Craven7' for update;









命中多条数据2220select * from MANAGERS_UNIQUE where MANAGER_ID IN (14273, 14274) for update;

命中多条数据+唯一键搜索2422select * from MANAGERS_UNIQUE where FIRST_NAME IN ('Brad7' , 'Brad8' ) for update;

如果 SELECT SQL 未能命中数据的话,情况就比较复杂了:

Result 行数LOCK 数量RowID锁唯一键锁SQL
RR + 点查0 01 11 00 1select * from MANAGERS_UNIQUE where MANAGER_ID=999999 for update; select * from MANAGERS_UNIQUE where FIRST_NAME='Brad9999'   for update;
RR + 普通查0000select * from MANAGERS_UNIQUE where LAST_NAME='Craven9' for update;
RC + 点查0000select * from MANAGERS_UNIQUE where MANAGER_ID=999999 for update;
RC + 普通查0000select * from MANAGERS_UNIQUE where LAST_NAME='Craven9' for update;

 

疑问

我们发现一个比较奇怪的现象,如果 SQL 语句的 WHERE 语句命中了唯一索引的点查,那么 TIDB 将会对唯一索引也加锁。其他情况下,不会对唯一索引进行加锁。(RC 级别下,未命中数据的话,还比较特殊, 唯一索引 / RowID 不加锁)

那么 SELECT FOR UPDATE 语句是否有必要对唯一索引加锁呢?

个人认为好像并不需要,因为 RowID 已经被加锁,所以被锁定的行数据不会被更新。

同时其他行数据是否可以更新唯一键呢?也不会。例如:

+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
| 14274      | Brad8      | Craven8   |     8 |
+------------+------------+-----------+-------+

事务一:
BEGIN PESSIMISTIC;
mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14273;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
+------------+------------+-----------+-------+

事务二:
BEGIN PESSIMISTIC;
update MANAGERS_UNIQUE SET FIRST_NAME ="Brad7" WHERE MANAGER_ID = 14274;

这种情况下,事务二会首先检查唯一索引 FIRST_NAME 的唯一性,发现已经有一个 "Brad7" 的唯一索引后,会直接返回错误。因此好像并不需要对唯一键进行加锁就可以实现这个功能。(实际上,悲观锁的接口参数 should_not_exist 就是这个作用)

目前本人未能想出 SELECT FOR UPDATE 语句需要对唯一索引加锁的必要场景。

UPDATE 语句

更新普通 Value

执行的 SQL 为:

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE MANAGERS_UNIQUE SET LAST_NAME='Craven9' where LAST_NAME='Craven8';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3778\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114274\000\000\000\374\201\200\360\331\241\217\353\233\006\320\254\004f\0067\254};<\000\001c\0067\254};<\000\002l\0067\254K-\230\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114274\000\000\000\374\201\200\360\331\241\217\353\233\006\314\254\004f\0067\254};<\000\001c\0067\254};<\000\002l\0067\254K-\230\000\002\001"

Total scanned keys: 2

因此普通的 UPDATE 语句也会对 RowID 和 唯一索引 加锁。

同样的疑问:这个唯一索引 Value (Brad7) 的加锁,是否是 必要 的呢?

更新主键 Value

执行的 SQL 为:

+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
| 14274      | Brad8      | Craven8   |     8 |
+------------+------------+-----------+-------+

mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE MANAGERS_UNIQUE SET MANAGER_ID=14275 where MANAGER_ID=14273;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\200\360\223\353\233\006\307\202\002f\0067\254\237\314\274\000\001c\0067\254\237\314\274\000\002l\0067\244\235?\264\000\002\002"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\200\360\223\353\233\006\304\202\002f\0067\254\237\314\274\000\001c\0067\254\237\314\274\000\002l\0067\251\306\253L\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\200\360\223\353\233\006\307\202\002f\0067\254\237\314\274\000\001c\0067\254\237\314\274\000\002l\000\000\000\000\000\000\000\000\001"

Total scanned keys: 3

如果更新的是主键,那么新主键 Value 和旧主键的 Value 都需要被加锁。

同时还要对唯一索引进行加锁。

更新唯一索引 Value

执行的 SQL 为:

BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE MANAGERS_UNIQUE SET FIRST_NAME='Brad9' where FIRST_NAME='Brad7';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings:

此时扫描 TIKV 所有的 LOCK 记录:

./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock

key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\201\200\360\254\332\224\353\233\006\257\306\003f\0067\254\245\245\234\000\001c\0067\254\245\245\234\000\002l\0067\244\235?\264\000\002\002"
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\201\200\360\254\332\224\353\233\006\270\306\003f\0067\254\245\245\234\000\001c\0067\254\245\245\234\000\002l\000\000\000\000\000\000\000\000\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\201\200\360\254\332\224\353\233\006\264\306\003f\0067\254\245\245\234\000\001c\0067\254\245\245\234\000\002l\0067\251\306\253L\000\002\001"

Total scanned keys: 3

如果更新的是唯一索引,那么新唯一索引 Value 和旧唯一索引的 Value 都需要被加锁。

同时还需要对 RowID 进行加锁。

对新的唯一索引 Value 加锁这是必要的,例如如下场景:

+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
| 14274      | Brad8      | Craven8   |     8 |
+------------+------------+-----------+-------+

事务一:
mysql> BEGIN PESSIMISTIC;
mysql> UPDATE MANAGERS_UNIQUE SET FIRST_NAME='Brad9' where FIRST_NAME='Brad7';
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273      | Brad7      | Craven7   |     7 |
+------------+------------+-----------+-------+

事务二:
mysql> BEGIN PESSIMISTIC;
mysql> UPDATE MANAGERS_UNIQUE SET FIRST_NAME='Brad9' where FIRST_NAME='Brad8';

总结

RowID 主键进行加锁基本是每个 DML 语句必须操作。

如果数据表存在唯一索引的话,还需要根据各种场景对唯一索引进行加锁。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:分布式数据库是什么专业
下一篇:全面掌握TiUP工具(一)
相关文章