MySQL并发控制和事务管理
无论何时,只要有多个查询需要在同一时刻修改数据,都会产生并发控制的问题,MySQL在两个层面进行了并发控制:服务器层与存储引擎层。
MySQL服务器逻辑架构图
服务器层
MySQL中的锁
MySQL中按锁的类型划分有两种锁,读锁/共享锁和写锁/排他锁。
读锁是共享的,互不阻塞,多个客户可以同一时刻同时读取同一资源;
写锁是排他的,需要等待资源上的读锁全部释放后才能加写锁,一个写锁会阻塞其他的写锁和读锁;
事务对资源进行加锁时,若无法加锁则会将该事务放入锁队列进行等待;
持有锁的事务释放锁时,MySQL会从锁队列中取出第一个等待的事务,检查其锁请求是否可以被满足,可以满足则获取锁执行事务,不能则继续等待。
锁粒度
一种提高共享资源并发性的方法就是让锁定对象更有选择性,尽量只锁定需要修改的部分数据,而不是所有的资源。理想情况是只对将要修改的数据片进行精确锁定。
任何情况下,在给定的资源上,锁定的数据量越少,则系统的并发程度越高。
但加锁也需要消耗资源,锁的各种操作,包括获得锁、检查锁、释放锁等都会有开销,如果在管理锁上花费过多资源(时间),系统的性能可能受到影响。
锁策略就是在锁的开销和并发访问性之间寻求平衡。
每种MySQL存储引擎都可以实现自己的锁策略和锁粒度,将锁粒度固定在某个级别,可以为某些应用场景提供更好的性能,同时也会失去对某些应用场景的良好支持。
下面是两种最重要的锁策略。
表锁(table lock)
表锁是MySQL中最基本的锁策略,也是开销最小的策略。一个用户在对表进行写操作时,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。
在特定场景中,表锁可能会有良好的性能,写锁比读锁有更高的优先级,一个写锁请求可能会被插入到锁队列中读锁的前面。
行锁(row lock)
最大程度支持并发处理,同时也有最大的锁开销。
行级锁只在存储引擎层实现,MySQL服务器层没有实现,服务器层也完全不知道存储引擎中的锁实现。
事务
事务是一组原子性的SQL查询,一个独立的工作单元,事务内的语句要么全部执行,要么全部执行失败。
事务的ACID特性,原子性(atomicity),一致性(consistency),隔离性(isolation),持久性(durability)。
- 原子性
一个事务必须被视为一个不可分割的最小工作单元,要么全部成功提交,要么全部失败回滚,不可能只执行其中的一部分操作。
- 一致性
数据库总是从一个一致性的状态转换到另一个一致性的状态,事务执行的过程中是一种过渡状态,事务未提交成功之前,这些修改不会被保存在数据库中。
- 隔离性
通常来说,一个事务所做的修改在最终提交前,其对其他事务应当是不可见的。具体的可见策略由事务的隔离级别所决定。
- 持久性
一旦事务提交,其做的修改就会永久保存到数据库中,即使系统崩溃修改的数据也不会丢失。
事务日志
使用事务日志,存储引擎在修改表数据时只需要修改表在内存中的拷贝,再将该修改行为记录到硬盘上的事务日志中,而不用每次修改表数据都将数据刷入到磁盘。
事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序IO。事务日志持久化到磁盘以后,内存中被修改的内容在后台可以慢慢刷回到磁盘。这种被称为预写式日志,修改一次数据需要写两次磁盘。
如果事务日志被持久化到磁盘,数据本身还未被写回时,系统崩溃,存储引擎在重启时能够自动恢复这部分被修改的数据。
MySQL中,事务日志主要包括REDO LOG(重做日志)和UNDO LOG(回滚日志)。
REDO LOG
确保事务的持久性,即是前文中提到的事务日志。
包含具体的修改内容:
对于插入操作,可能包括插入的数据值;对于更新操作,可能包括更新前后的值;对于删除操作,可能包括被删除行的唯一标识等。
UNDO LOG
实现事务的回滚,当事务执行过程中出现错误或用户主动回滚事务时,MySQL利用UNDO LOG将数据恢复到事务开始之前的状态;
支持多版本并发控制(MVCC),通过保存数据的历史版本,使不同的事务能够看到不同版本的数据。
原理: 当事务对数据进行修改时,MYSQL先将修改前的数据副本保存在UNDO LOG中,如果事务需要回滚,就根据UNDO LOG中的记录将数据恢复到事务开始前的状态。
通常以链表的形式组织,每个事务对数据进行修改时,会生成一个或多个UNDO LOG日记录,按照事务执行的顺序链接在一起。
包含前像信息和事务信息:
包含被修改数据修改之前的状态;包含事务相关信息,事务ID、事务回滚指针(该事务的上一个UNDO LOG)等。
事务的隔离级别
SQL标准中定义了四种事务隔离级别,规定了一个事务中做的修改,哪些在事务内和事务间是可见,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统开销也更低,但可能出现的问题也会更多。
READ UNCOMMITED (未提交读)
事务中的修改,即使未提交,对其他事务也可见。
会出现脏读(事务读取未提交的数据),从性能上没有优势,但缺乏其他级别的很多好处。实际应用中一般很少使用。
READ COMMITED (提交读/不可重复读)
大多数数据库系统的默认隔离级别(MySQL不是)。
一个事务从开始直到提交前,所做的任何修改对其他事务都不可见,两次执行同样的查询,可能会得到不一样的结果(中间其他事务对数据库进行了修改),因此也被称为不可重复读。
REPEATABLE READ (可重复读)
MySQL的默认事务隔离级别,保证了在同一个事务中多次读取同样记录的结果是一致的。
不会出现脏读,但无法解决幻读(查询到的结果与实际的结果不同),因为可重复读保证同一事务中相同的读取结果是一致的,但途中若数据集发生了变化,将无法感知。
InnoDB和XtraDB存储引擎通过多版本并发控制MVCC解决了幻读的问题。
SERIALIZABLE (可串行化)
强制事务串行执行,可能导致大量的超时和锁争用问题。
死锁
多个事务在同一资源上相互占用,请求锁定对方占用的资源。
InnoDB处理方法,出现死锁后将持有最少行级排他锁的事务进行回滚。
多版本并发控制MVCC
MVCC没有一个统一的实现标准,可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了枷锁操作,开销更低。
MVCC的实现是通过保存数据的快照来实现的。无论事务需要执行时间多长,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的(因为快照不同)。
InnoDB的MVCC,是通过在每行记录后面添加两个隐藏的列来实现的,一个保存了该行的创建时的版本号,另一个保存了行的过期时的版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
在 REPEATABLE READ(可重复读) 隔离级别下,MVCC具体操作:
SELECT
InnoDB会根据以下两个条件检查每行记录:
- InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在事务开始前以及存在的,要么是事务自身插入或者修改过的。
- 行的删除版本要么为未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始前未被删除。
INSERT
- InnoDB为插入的每一行保存当前系统版本号作为行版本号。
DELETE
- InnoDB为删除的每一行保存当前系统版本号作为行删除版本号。
UPDATE
- InnoDB为插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除版本号(若无其他事务依赖旧版本数据)。
通过这两个额外的系统版本号列,大多数读操作都可以不用加锁。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和维护工作。
MySQL存储引擎
InnoDB存储引擎
MySQL的默认事务型引擎
数据存储在表空间中,是由InnoDB管理的一个黑盒子。
采用MVCC支持高并发,实现了四个标准的隔离级别,其默认级别是REPEATABLE READ(可重复读),并通过间隙锁策略防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,防止幻影行的插入。
InnoDB是基于聚簇索引建立的。