数据库系统原理

事务

事务指的是满足ACID特性的一组操作,可以通过commit提交一个事务,也可以使用Rollback进行回滚。
MySQL事务主要用于处理操作量大,复杂度高的数据。

原子性(Atomicity)

一个事务(transaction)中所有的操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。如果在执行过程中发生错误,
会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。

一致性(Consistency)

在事务开始之前和事务结束以后,数据库的完整性没有被破坏,也就是说,数据库在事务执行前后都保持一致性状态,在一致性状态下,所有事务对一个数据的读取结果都是相同的。

隔离性(Isolation)

一个事务所做的修改在最终提交以前,对其它事务是不可见的。

持久性(Durability)

一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务的执行结果也不能丢失。
使用重做日志来保证持久性。

在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGINSTART TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

并发一致性问题

单线程下事务不会出现问题,因为是单个线程在操作数据库,事务操作成功则记录到磁盘里,失败则进行回滚,但是在并发环境下,有多个事务在操作数据库,事务的隔离性很难保证,因此会出现很多并发一致性问题。

丢失修改

T1 和 T2 两个事务都对同一个数据进行修改,T1先修改,T2随后修改,T2的修改覆盖了T1的修改。

读脏数据

T1修改一个数据,T2随后读取这个数据。如果T1撤销了这次修改,那么T2读取的数据是脏数据。

不可重复读

指在一个事务中多次读取同一数据。在这个事务还没有结束时,另一个事务也访问该数据。T2读取一个数据,T1对该数据做了修改。如果T2再次读取这个数据,此时读取的结果和第一次读取的结果不同。

幻读

T1读取某个范围内的数据,T2在这个范围内插入新的数据,T1再次读取这个范围内的数据,此时读取的结果和第一次读取的结果不同。

产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,
相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。

封锁

封锁粒度

MySQL中提供了两种封锁粒度:行级锁以及表级锁。

应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。

但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。

在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。

封锁类型

  1. 读写锁
  • 排它锁(Exclusive),简称为 X 锁,又称写锁
  • 共享锁(Shared), 简称为 S 锁,又称读锁

一个事务对数据对象A加了X锁,就可以对A进行读取和更新。加锁期间其它事务不能对A加任何锁。
一个事务对数据对象A加了S锁,可以对A进行读取操作,但是不能进行更新操作。加锁期间其它事务能对A加S锁,但是不能加X锁。

锁的兼容关系:

- X S
X
S
  1. 意向锁

使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。

在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。

意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:

一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。

各种锁的兼容关系如下:

- X IX S IS
X
IX
S
IS
  • 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁;
  • S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。

封锁协议

在运用X锁和S锁对数据对象进行加锁时,还需要约定一些规则。例如何时申请X锁或S锁、持锁时间、何时释放等。称这些规则为封锁协议(Locking Protocol)

  1. 三级封锁协议

一级封锁协议(对应read uncommited)
事务T要修改数据A时必须加X锁,直到T结束释放锁。在一级封锁协议当中,如果仅仅是读取数据而不对其进行修改,是不会加S锁的
这个可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。

二级封锁协议(对应read commited)
在一级封锁协议的基础上,要求读取数据 A 时必须加S锁,读取完马上释放S锁。由于读取完数据立即就释放S锁(并没有等待事务结束才释放锁),所以不能保证可重复读。可以解决脏读数据问题,因为如果一个事务对数A进行修改,根据一级封锁协议,必须对数据A加上X锁,加了X锁之后就不能加S锁了,也就是不会读到事务未提交的数据。

三级封锁协议(reapetable read)
在二级的基础上,要求读取数据时必须加S锁,直到事务结束才能释放S锁。
可以解决不可重复读的问题,因为读A时,其它事务不能对A加X锁,从而避免了在读的期间数据发生改变。

  1. 两段锁协议

加锁和解锁阶分为两个阶段进行。
可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。

lock-x(A)…lock-s(B)…lock-s(C)…unlock(A)…unlock(C)…unlock(B)

但不是必要条件,例如以下操作不满足两段锁协议,但是它还是可串行化调度。

lock-x(A)…unlock(A)…lock-s(B)…unlock(B)…lock-s(C)…unlock(C)

MySQL隐式与显式锁定

MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。

InnoDB 也可以使用特定的语句进行显示锁定:

SELECT … LOCK In SHARE MODE;
SELECT … FOR UPDATE;

隔离级别

读未提交(read uncommitted)

事务对数据修改,即使没有提交,其它事务也能读取到。

读已提交(read committed)

事务对数据修改,在没有提交之前,其它事务是不能读取的。也就是说一个事务所做的修改在未提交之前对其它事务都是不可见的。

可重复读 (repeatable read)

保证在同一个事务中多次读取同样数据的结果是一样的。

可串行化 (serializable)

强制事务串行执行。

隔离级别 脏读 不可重复读 幻影读
未提交读
提交读 ×
可重复读 × ×
可串行化 × × ×

多版本并发控制(MVCC)

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。
可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

MySQL的大多数事务存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制。可以认为MVCC是行级锁的一个变种,但是它很多情况下避免了加锁,
因此开销更低。

MVCC的实现是通过保存数据在某个时间点的快照来实现的,不同存储引擎的MVCC实现是不同的,典型的有 乐观并发控制悲观并发控制

MVCC 是如何工作的

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)
当然这里的时间并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询
到的每行记录版本号进行比较。下面具体看一下在 REPEATABLE READ 隔离级别下,MVCC具体是如何操作的。

SELECT

InnoDB会根据以下两个条件检查每行记录:

  1. InnoDB只查找早于当前事务版本号的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以保证事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。

  2. 行的删除版本号要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

INSERT

InnoDB 为新插入的每一行保存当前系统版本号作为行版本号

DELETE

InnoDB 为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE

InnoDB 为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识

保存这两个额外的系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好。
但是不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

快照读和当前读

  1. 快照读
    使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。

select * from table …;

  1. 当前读

读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。

1
2
3
4
5
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;

Next-Key Locks

Next-Key Locks是MySQL的InnoDB存储引擎的一种锁实现。

MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。

Record Locks

锁定一个记录上的索引,而不是记录本身。

如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。

Gap Locks

锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:

1
2
3
4
5
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)

关系数据库设计理论

todo

参考资料

CS-Notes

『浅入浅出』MySQL 和 InnoDB

nnodb中的事务隔离级别和锁的关系

MySQL-InnoDB-MVCC多版本并发控制

显示评论