MySQL版本:8.0.29
测试表:
测试数据:
事务A先执行:
加锁分析:
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328944544:1066:140346432670032
ENGINE_TRANSACTION_ID: 3957
THREAD_ID: 75
EVENT_ID: 32
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140346432670032
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
1 row in set (0.00 sec)
事务B执行:
加锁分析:
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328944544:1066:140346432670032
ENGINE_TRANSACTION_ID: 3957
THREAD_ID: 75
EVENT_ID: 32
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140346432670032
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328944544:5:5:5:140346462116384
ENGINE_TRANSACTION_ID: 3957
THREAD_ID: 76
EVENT_ID: 36
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: age
OBJECT_INSTANCE_BEGIN: 140346462116384
LOCK_TYPE: RECORD
LOCK_MODE: X,REC_NOT_GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 96, 40
*************************** 3. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328945336:1066:140346432671984
ENGINE_TRANSACTION_ID: 421821305655992
THREAD_ID: 76
EVENT_ID: 36
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140346432671984
LOCK_TYPE: TABLE
LOCK_MODE: IS
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 4. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328945336:5:5:5:140346462120992
ENGINE_TRANSACTION_ID: 421821305655992
THREAD_ID: 76
EVENT_ID: 36
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: age
OBJECT_INSTANCE_BEGIN: 140346462120992
LOCK_TYPE: RECORD
LOCK_MODE: S
LOCK_STATUS: WAITING
LOCK_DATA: 96, 40
4 rows in set (0.00 sec)
事务A持有锁:
1)表user的意向排他锁
2)age索引:记录(96,40)的排他锁
事务B持有锁:
1)表user的意向共享锁
2)age索引:范围(89,96]的next-key lock
总结:
事务B想要获取age索引:范围(89,96]的next-key lock,
但是因为事务A已经拥有了age索引:记录(96,40)的排他锁,所以需要阻塞。
仔细思考,正常来说事务B想要当前读age=96的所有记录,
光是加这些锁不够,所以恢复运行后,加的锁又是不一样了,看下面。
事务A提交事务
事务B就可以查询到结果了
加锁分析:
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328945336:1066:140346432671984
ENGINE_TRANSACTION_ID: 3962
THREAD_ID: 76
EVENT_ID: 36
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140346432671984
LOCK_TYPE: TABLE
LOCK_MODE: IS
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328945336:5:5:5:140346462121336
ENGINE_TRANSACTION_ID: 3962
THREAD_ID: 76
EVENT_ID: 37
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: age
OBJECT_INSTANCE_BEGIN: 140346462121336
LOCK_TYPE: RECORD
LOCK_MODE: S
LOCK_STATUS: GRANTED
LOCK_DATA: 96, 40
*************************** 3. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328945336:5:4:5:140346462121680
ENGINE_TRANSACTION_ID: 3962
THREAD_ID: 76
EVENT_ID: 37
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140346462121680
LOCK_TYPE: RECORD
LOCK_MODE: S,REC_NOT_GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 40
*************************** 4. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140346328945336:5:5:2:140346462122024
ENGINE_TRANSACTION_ID: 3962
THREAD_ID: 76
EVENT_ID: 37
OBJECT_SCHEMA: db_study
OBJECT_NAME: user
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: age
OBJECT_INSTANCE_BEGIN: 140346462122024
LOCK_TYPE: RECORD
LOCK_MODE: S,GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 99, 10
4 rows in set (0.00 sec)
事务B持有的锁:
1)表user的意向共享锁
2)age索引:范围(89,96]的next-key lock
3)主键索引:id=40的行共享锁
4)age索引:范围(96,99)的gap lock
总结:
事务B刚开始获取的锁只是一个标记(阻塞在那里,
等着事务A释放行锁后恢复执行,因为行锁的获取单位的next-key lock,
记录锁和间隙锁是next-key lock的降级),恢复执行后,
需要重新上面这些锁,保证当前读
小结:insert如果没有并发,是不加锁,但底层会通过page latch保证并发修改同一page的线程安全。比如事务A要在某个间隙插入一条记录a,先会获取page x latch,然后插入数据(修改数据页),然后释放page x latch,还没有提交事务,此时事务B需要查询这个间隙中的数据,先获取page s latch,然后查询到记录a,通过记录a的隐藏字段发现这个记录是还没有提交的,所以给这个记录对应的事务加上行记录X锁,自己进入到这把锁的阻塞队列中,说白就是这个间隙有其他事务在操作,当前事务不能查询,需要先阻塞,等到前面的事务提交后才能继续查询,如果是select lock in share mode 进入S锁阻塞队列,如果是select for update 进入到X锁阻塞队列。
如果insert所在的事务获取page x latch后,发现这个间隙存在gap锁,会生成插入意向锁(插入意向锁是兼容的,本质上也是gap锁)然后阻塞,直到前面的gap锁释放
通过 LOCK_MODE 可以确认是 next-key 锁,还是间隙锁,还是记录锁:
如果 LOCK_MODE 为 X,说明是 next-key 锁;
如果 LOCK_MODE 为 X, REC_NOT_GAP,说明是记录锁;
如果 LOCK_MODE 为 X, GAP,说明是间隙锁;