一、并发控制基本知识
数据库是共享资源,通常有许多个事务同时在运行,当多个事务并发地存取同一个数据库时就会产生冲突,若对并发操作不加控制就可能会存取和存储不正确的数据,破坏数据库的一致性。所以数据库管理系统必须提供并发控制机制。
当多个事务同时对数据库进行操作时,会出现3种冲突情形:
- 读-读:不存在任何问题。
- 读-写:有隔离性问题,可能遇到脏读(会读到未提交的数据),幻影读等。
- 写-写:可能丢失更新数据。
1.1 读写锁
为了最大化数据库事务的并发能力,数据库在处理并发读或者写的时候,可以通过一个由两种类型的锁组成的锁系统来解决问题,这两种类型的锁分别是共享锁和互斥锁,也叫做读锁和写锁。
读锁是共享的,多个客户在同一时刻读取同一个资源的时候,互不干扰。写锁是排他的,一个写锁会阻塞其他读锁以及其他写锁,保证在给定的时间段中,只能有一个用户能执行写入操作。
1.2 锁粒度与锁策略
锁粒度顾名思义就是加锁时需要锁住的范围有多大(让锁对象更有选择性),这个概念的提出是为了提高共享资源的并发性,只对需要修改的资源进行精确的锁定。
锁策略就是在锁的开销和数据的安全性之间寻求平衡。因为在加锁的时候也需要消耗资源,锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。如果系统花费大量的时间来管理锁,而不是存取数据,那么系统的性能可能会因此受到影响。
1.3 表锁与行级锁
表锁与行级锁是两种比较重要锁策略(MySQL中还有一种为页级锁)。
表锁是MySQL中最基本的锁策略,锁定是MySQL各存储引擎中最大颗粒度的锁定机制(每次锁定一张表)。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。
行级锁可以最大程度的支持并发处理,同时锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要的开销更大了。此外,行级锁定也最容易发生死锁。使用行级锁定的主要是InnoDB存储引擎。
二、MVCC
MVCC,多版本的并发控制,英文全称:Multi Version Concurrency Control。是数据库中常用的解决读-写冲突的操作。前面提到,行级锁是主要用于处理事务的锁,但是在MySQL中大多事务型存储引擎实现的都不是简单的行级锁,而是多版本并发控制(MVCC),可以简单的认为MVCC是行级锁的一个变种。除了MySQL,其他数据库也实现了MVCC,但是各自的实现机制不同,没有一个统一的标准。
在MVCC中,很多情况下都避免了加锁操作,因此开销更低,大多数标准的MVCC都实现令非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照实现的。就是当我们在修改数据的时候,可以为这条数据创建一个快照,后面就可以直接读取这个快照。
2.1InnoDB MVCC实现原理
在InnoDB的MVCC,是通过每行记录后面保存的两个隐藏列实现的。
每一行记录都有两个隐藏列: DATA_TRX_ID(保存了行的建立时间)、 DATA_ROLL_PTR(保持了行的过期时间或删除时间)。保存的时间值指的是系统版本号而不是真正的时间,同时每开始一个新的事务时,系统的版本号就会递增。
在进行事务的操作前,MVCC设置了以下规则:
SELECT
InnoDB会根据以下两个条件检查每行纪录:
InnoDB只查找版本早于当前事务版本的数据行,即,行的系统版本号小于或等于事务的系统版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
-
行的删除版本,要么未定义,要么大于当前事务版本号。这样可以确保事务读取到的行,在事务开始之前未被删除。
只有符合上述两个条件的纪录,才能作为查询结果返回。
INSERT
InnoDB为插入的每一行保存当前系统版本号作为行版本号。
DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE
InnoDB为插入一行新纪录,保存当前系统版本号作为行版本号,同时,保存当前系统版本号到原来的行作为行删除标识。
2.2 InnoDB MVCC实现实例
首先需要了解的一个概念是ReadView,ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是开始了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。
以可重复读隔离级别为例,假设当前列表里的事务id为[80,100]。在可重复读隔离级别,这时候我的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。当我需要执行一次select语句时
1.如果我需要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。
2.如果我需要访问的记录版本的事务id为70,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。
3.如果我要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。
2.3 MVCC特点
在MVCC中,不管执行多少时间,每个事务看到的数据都是一致的;而根据事务开始的时间不同,每个事务对同一张表,看到的同一时间看到的数据也不同。
MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read),因为读未提交会读取最新的数据行,而可串行化则会将读取的行都加锁。
三、补充
在并发控制中,要解决冲突,总共由三种方式:
1.(悲观)锁,即基于锁的并发控制,比如2PL,这种方式开销比较高,而且无法避免死锁。
2.多版本并发控制(MVCC),是一种用来解决读-写冲突的无锁并发控制。
3.乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,认为事务间争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,如果没有就提交,如果有就放弃并重试。乐观并发控制类似自选锁。乐观并发控制适用于低数据争用,写冲突比较少的环境。
在使用MVCC控制的时候,可以以结合基于锁的并发控制来解决写-写冲突,即MVCC+2PL;也可以结合乐观并发控制来解决写-写冲突。