MySQL InnoDB下事务与锁及MVCC

以下讨论,部分内容仅基于MySQL场景下讨论

事务

事务的四大特性(ACID)

原子性:同一事务的所有操作,要么都成功,要么都不成功
一致性:事务前和事务后都不破坏数据完整性
隔离性:事务之间并发执行不相互影响,隔离性分为四个等级:读未提交、读已提交、可重复读、串行化
持久性:事务完成后,数据修改永久生效,不会因程序或服务器故障而丢失

锁及MVCC

MySQL使用锁和MVCC(Multi-Version Concurrency Control,多版本并发控制)机制,以实现事务各等级的隔离性。

事务并发的问题及事务的隔离性级别

读未提交读已提交可重复读序列化
脏读XYYY
不可重复读XXYY
幻读XX通常为 X (MySQL 下通过间隙锁解决 因此为 Y)Y

锁分类

按粒度

  1. 表级锁:意向锁共享锁、排他共享锁、自增锁
  2. 行级锁:记录锁、间隙锁、临键锁、插入意向锁
  3. 元数据锁:元数据共享锁、元数据排他锁

按性质

  1. 排他锁(Exclusive Lock,X锁):排他锁与其他任意锁互斥
  2. 共享锁(Shared Lock,S锁):共享锁可以与共享锁兼容

按类型

  1. 记录锁:对单条记录加锁,可解决事务的可重复读问题,配合MVCC机制,可提高事务的并行度,注意:该锁基于索引实现,若查询未使用索引,将转为表锁(注意区分表级锁和表锁)
  2. 间隙锁:对一个开区间内的所有记录加锁,MySQL中引入用于解决可重复读事务等级下的幻读问题
  3. 临键锁:相当于间隙锁+记录锁,左开右闭
  4. 插入意向锁:一种特殊的X间隙锁,普通X间隙锁之间互斥,在插入的场景下,若添加间隙锁,将极大的降低吞吐量,因此引入插入意向锁,多个插入意向锁之间,不是插入相同数据时可兼容,可提高并发插入效率,插入意向锁与间隙锁不兼容
  5. 自增锁:一种特殊的锁,仅针对自增列使用,用于保证自增列数值不重复,具备传统模式(表级别,并发弱)、连续模式(确定数量时为页锁,不定数量时为表锁)、交叉模式(乐观锁,处理新增时获取ID,并发最高,同一语句生成的ID可能不连续,主从SBR格式binlog可能导致主从数据不一致)
  6. 意向共享锁:用于与部分表级锁互斥时,无需扫描获取全表是否存在哪些行具有共享锁,获取行级共享锁前,必须先获取相应意向共享锁,与其他意向SX锁兼容,与其他表级排他锁互斥(如不含筛选条件的全表更新等)
  7. 意向排他锁:用于与表级锁互斥时,无需扫描获取全表是否存在哪些行具有排他锁,获取行级排他锁前,必须先索取相应意向排他锁,与其他意向SX锁兼容,与其他表级锁互斥(如不含筛选条件的全表查询等)
  8. 元数据共享锁:对表下数据进行操作时,都会获取元数据共享锁,避免数据修改或查询过程中,表结构被修改,与元数据共享锁兼容
  9. 元数据排他锁:对表结构进行操作(执行DDL语句)时,都会获取元数据排他锁,避免对表结构进行操作时,对表数据的查询或操作,与其他元数据锁互斥

加锁方式

select ... where id = 5 for update:id字段为主键或唯一索引索引,记录存在时,添加记录锁,记录不存在时,添加间隙锁
select ... where id between 5 and 7 for update:id字段为主键或唯一索引索引,添加间隙锁

RR事务等级下间隙锁在以下情况下产生:

  1. 使用普通索引锁定
  2. 使用多列唯一索引
  3. 使用唯一索引锁定多行记录

唯一索引只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁,指定给某条存在的记录加锁的时候,只会加记录锁,不会产生间隙锁
普通索引的间隙,优先以普通索引排序,然后再根据主键索引排序;

查询MySQL当前存在的锁信息

SHOW ENGINE INNODB STATUS;
SELECT * FROM information_schema.innodb_trx;

-- 查看InnoDB引擎状态
SHOW ENGINE INNODB STATUS;
-- 查看事务信息
SELECT * FROM `information_schema`.innodb_trx;
-- 查看锁
SELECT * FROM `performance_schema`.data_locks;
-- 查看锁等待时间
SELECT * FROM `performance_schema`.data_lock_waits;
SELECT * FROM `sys`.innodb_lock_waits;
-- 查看元数据锁信息
SELECT
	l.object_schema `数据库名`,
	l.object_type `对象类型`,
	l.object_name `对象名称`,
	l.lock_type `锁类型`,
	l.lock_duration `持续类型`,
	l.lock_status `锁状态`,
	l.owner_thread_id `线程ID`,
	t.processlist_id `会话ID`,
	s.sql_text 
FROM
	`performance_schema`.`metadata_locks` l
	JOIN `performance_schema`.threads t ON t.thread_id = l.owner_thread_id
	JOIN `performance_schema`.events_statements_current s ON s.thread_id = l.owner_thread_id;

MVCC

实现原理

  1. MVCC在表字段列的基础上,额外引入 最近更新该行事务ID(DATA_TRX_ID 6字节)、事务回滚指针(DB_ROLL_PTR 7字节)列以实现
  2. 在对行数据进行某些需要生成快照版本的交互时,将该行数据复制到UndoLog中,同时更新更新数据行的事务ID、回滚指正指向新复制的UndoLog,通过此方式,会形成提交以事务ID作为版本戳的数据版本单向链表
  3. 基于该带版本戳的单向链表,结合版本生成时机,即可解决RC及以上隔离级别下的不可重复读问题
  4. 事务需要回滚时,在某些情况下将表数据回滚到指向的UndoLog版本并删除UndoLog即可实现,在部分修改的场景,为了可重复读,UndoLog需要在所有事务都结束后,才能删除

MVCC版本生成时机

读未提交:不生成
读已提交:每次查询该数据都将生成版本
可重复读:第一次查询该数据时生成版本
串行化:第一次查询该数据时生成版本

参考文档