MySQL InnoDB 事务,锁和MVCC

    事务特性(ACID):原子性、一致性、隔离性、持久性。
    原子性(atomicity)
    一个事务必须被视为一个不可分割的最小单位,整个事务的所有操作么全部成功,要么全部失败会滚。
    一致性(consistency)
    数据库总是从一个一致性的状态转换到另一个一致性的状态。即事务没有提交,之前的修改也不会保存到数据库中。
    隔离性(isolation)
    通常来说,一个事务所做的修改在最终提交之前,对其它事务是不可见的。
    持久性(durability)
    一旦事务提交,则所做的修改就会永久的保存到数据库中。即使此刻数据库系统崩溃,修改的数据也不会丢失。

    在SQL标准中定义了四种隔离级别。

    隔离级别 脏读 不可重复读 幻读
    未提交读Read Uncommitted
    已提交读Read Committed
    可重复读Repeatable Read
    可串行化Serializable

    读未提交:一个事务可以读取另一个未提交事务的数据,这也被称为脏读。
    读提交:一个事务开始时,只能“看见”已提交事务所做的修改。这个级别有时候也叫不可重复读。因为两次同样的查询操作,可能会得到不一样的结果。(另一个事务修改了第一次查询的记录。)
    重复读:同一个事务中,多次读取同样记录的结果是一致的。(禁止另一个事务对你第一次查询的记录进行修改操作)重复读可以解决不可重复读问题,但还是无法解决另一个幻影读问题。一个事务在读取某个范围记录时,另一个事务又在该范围内插入一条新的记录,导致你两次读取到数据记录不一致,产生幻影行。
    Serializable:最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。


    上面是对数据库理论的回顾,主要讲解了事务的特性,事务隔离级别。
    每个数据库产品对理论的实现不尽相同。
    下面针对Mysql数据库 InnoDB数据库引擎来谈谈这些问题。

    InnoDB中锁和MVVC
    **
    InnoDB存储引擎中的锁(最大限度地利用数据库的并发访问,确保每个用户能以一致的方式读取和修改数据)
    Lock的对象是事务,用来锁定数据库中的对象,如表、页、行。一般lock的对象仅在事务commit或rollback后进行释放。

    1)行锁的类型
    InnoDB有两种标准的行级锁:共享锁、排他锁。
    共享锁(S Lock),允许事务读取一行数据。
    排他锁(X Lock),允许事务删除或更新一行数据。


    X S
    X 不兼容 不兼容
    S 不兼容 兼容

    为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁:意向共享锁和意向排他锁,这两种意向锁都是表锁。一个事务在给数据行加锁之前必须先取得对应表对应的意向锁。
    意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显式给记录集加共享锁或排他锁。

    1. //设置非自动提交事务
    2. Set autocommit=0;
    3. //共享锁
    4. SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
    5. //排他锁
    6. SELECT * FROM table_name WHERE ... FOR UPDATE
    7. //释放锁
    8. unlock tables;

    2)行锁的实现方式
    InnoDB行锁是通过给索引上的索引项加锁来实现的,InnoDB这种行锁实现特点意味着:
    (1)只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
    (2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
    (3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

    3)锁的模式(记录锁、间隙锁、临键锁都是排它锁)
    记录锁(Record Lock):封锁一个索引记录,其它事务只能读被锁的记录,不能insert、update、delete操作。
    对于行的查询,默认采用Next-key Lock。但是,对于唯一键值的锁定,Next-key Lock降级为Record Lock仅存在于查询所有的唯一索引。

    间隙锁(Gap Lock):封锁索引记录中的间隔,或者查询到的第一条索引记录之前的范围,又或者查询到的最后一条索引记录之后的范围。
    假设一个索引包含以下几个值:10,11,13,20。那么这个索引的next-key锁将会覆盖以下区间:
    (negative infinity, 10]
    (10, 11]
    (11, 13]
    (13, 20]
    (20, positive infinity)
    多个事务可以获得同一个gap的间隙锁,间隙锁的唯一目的就是防止在间隙中insert数据。共享间隙锁与排他间隙锁没有区别。

    临键锁(Next-key Lock):是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
    临键锁主要是为了避免幻读。如果把事务的隔离级别降级为RC,临键锁则会失效。

    MVCC(Multi-Version Concurrency Control)即多版本并发控制。
    MySQL的大多数事务型(如InnoDB,Falcon等)存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了MVCC。
    MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。
    MVCC的实现原理

    为了便于理解MVCC的实现原理,这里简单介绍一下undo log的工作过程
    在不考虑redo log的情况下利用undo log工作的简化过程为:

    序号 动作
    1 开始事务
    2 记录数据行数据快照到undo log
    3 更新数据
    4 将undo log写到磁盘
    5 将数据写到磁盘
    6 提交事务

    1)为了保证数据的持久性数据要在事务提交之前持久化
    2)undo log的持久化必须在在数据持久化之前,这样才能保证系统崩溃时,可以用undo log来回滚事务

    Innodb中的隐藏列
    Innodb通过undo log保存了已更改行的旧版本的信息的快照。
    InnoDB的内部实现中为每一行数据增加了三个隐藏列用于实现MVCC。

    列名 长度(字节) 作用
    DB_TRX_ID 6 插入或更新行的最后一个事务的事务标识符。(删除视为更新,将其标记为已删除)
    DB_ROLL_PTR 7 写入回滚段的撤消日志记录(若行已更新,则撤消日志记录包含在更新行之前重建行内容所需的信息)
    DB_ROW_ID 6 行标识(隐藏单调自增id)

    结构

    数据列 .. DB_ROW_ID DB_TRX_ID DB_ROLL_PTR

    MVCC工作过程
    MVCC只在READ COMMITED 和 REPEATABLE READ 两个隔离级别下工作。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE 则会对所有读取的行都加锁。
    SELECT
    InnoDB 会根据两个条件来检查每行记录:
    InnoDB只查找版本(DB_TRX_ID)早于当前事务版本的数据行(行的系统版本号<=事务的系统版本号,这样可以确保数据行要么是在开始之前已经存在了,要么是事务自身插入或修改过的)
    行的删除版本号(DB_ROLL_PTR)要么未定义(未更新过),要么大于当前事务版本号(在当前事务开始之后更新的)。这样可以确保事务读取到的行,在事务开始之前未被删除。
    INSERT
    InnoDB为新插入的每一行保存当前系统版本号作为行版本号
    DELETE
    InnoDB为删除的每一行保存当前的系统版本号作为行删除标识
    UPDATE
    InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识
    1)默认的数据库隔离级别:重复读。

    1. mysql> select @@transaction_isolation; //数据库版本8
    2. +-------------------------+
    3. | @@transaction_isolation |
    4. +-------------------------+
    5. | REPEATABLE-READ |
    6. +-------------------------+
    7. 1 row in set (0.00 sec)

    2)一致性非锁定读
    一致性的非锁定读是指InnoDB存储引擎通过多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行delete或update操作,这时读取操作不会因此取等待行上的锁释放。它会去读行的一个快照数据。
    image.png

    ①每行记录可能有多个版本
    ②在事务隔离级别READ COMMITTED (简写RC)和 REPEATABLE READ(简写RR)下,InnoDB存储引擎使用一致性非锁定读。但是对快照的定义却不相同。在RC下,一致性非锁定读总是读取被锁定行的最新一份快照数据。而在RR级别下,总是读取事务开始时的数据版本。

    如何验证上面的结论呢?
    事务隔离级别=重复读

    1— 事务一:开启事务并更新一条记录

    1. mysql> begin;
    2. Query OK, 0 rows affected (0.00 sec)
    3. mysql> update teacher set tname='maps' where tid=1;
    4. Query OK, 1 row affected (0.00 sec)
    5. Rows matched: 1 Changed: 1 Warnings: 0

    2— 事务二:开启事务先查询一条记录

    1. mysql> begin;
    2. Query OK, 0 rows affected (0.00 sec)
    3. mysql> select * from teacher where tid=1;
    4. +------+-------+------+
    5. | tid | tname | tcid |
    6. +------+-------+------+
    7. | 1 | tom | 1 |
    8. +------+-------+------+
    9. 1 row in set (0.00 sec)

    3— 事务一:提交事务

    1. mysql> commit;
    2. Query OK, 0 rows affected (0.00 sec)

    4— 事务二:再一次查询上一条记录、并提交

    1. mysql> select * from teacher where tid=1;
    2. +------+-------+------+
    3. | tid | tname | tcid |
    4. +------+-------+------+
    5. | 1 | tom | 1 |
    6. +------+-------+------+
    7. 1 row in set (0.00 sec)
    8. mysql> commit;

    可以发现1⃣️事务二并没有因为事务一在更改操作而阻塞;2⃣️事务二两次读取到的数据一致,并没有因为事务一更改数据而不一致。

    如果事务隔离级别=读提交.

    第4步查询到结果 tname=maps
    可以发现,事务二读取到了事务一提交更新的数据。

    结论:在RC下,一致性非锁定读总是读取被锁定行的最新一份快照数据。而在RR级别下,总是读取事务开始时的数据版本。

    总结
    1)首先,回顾数据库事务的特性以及隔离级别,介绍了每种隔离级别针对哪种问题。
    2)InnoDB的锁。行级锁的类型、行级锁的实现方式、行级锁的模式。
    3)InnoDB的多版本控制。MVVC实现原理、工作过程。
    4)Mysql的默认隔离级别,一致性非锁定读。

    推荐阅读MVVC:https://segmentfault.com/a/1190000012650596