并发控制
-
读写锁
- 共享锁(读锁):读锁是共享的,相互不阻塞,但会阻塞写锁;多个客户在同一时刻可以读取同一个资源,互不干扰。
- 排他锁(写锁):写锁是排他的,一个写锁会阻塞其他的写锁和读锁;这是出于安全策略,确保只有一个客户能执行写入,并防止其他用户读取正在写入的资源。
锁粒度
一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据(最理想的方式),而不是所有的资源。锁定范围越小,并发性越高。大多数数据库都是在表上施加行级锁,并以各种复杂的方式来实现。
- 表锁(table lock):MySQL中最基本的锁策略,并且是开销最小的策略(不涉及过多逻辑),会锁定整张表;eg:服务器会为诸如alter table之类的语句使用表锁,而忽略存储引擎的锁机制。【这么看来服务器层就实现了表锁】
-行级锁(row lock):行级锁可以最大程度地支持并发处理(同时有带来了最大的锁开销)。行级锁只在储存引擎层实现,而MySQL服务器层没有实现;储存引擎Innodb就实现了行级锁。
事务
事务就是一组原子性的SQL执行,或者说一个独立的工作单元。事务内的语句,要么全部执行成功,要么全部执行失败。
严格意义上来说,一个运行良好的事务应该具备4个标准特性:ACID
- 原子性(atomicity):最小工作单元,要么都成功,要么都失败回滚。
- 一致性(consistency):数据库总是从一个一致性状态转换到另外一个一致性的状态,失败事务中的改动不会保存到数据库中
- 隔离性(isolation):,一个事务的修改在最终提交前,对其他事务是不可见的。这里是考虑到下文会提及的隔离级别的,所以说通常来说,并不绝对。
- 持久性(durability):一旦事务成功提交,其修改就会永久保存到数据库中。
隔离级别
- read uncommitted(未提交读)
- 事务中的修改尚未提交,对其他事务是可见的;一般不用这个隔离级别
- 事务可以读取其他事务未提交的数据,这就叫。
- read committed(提交读/不可重复读)
- 提交读就开始满足之前的隔离性定义了:一个事务从开始到提交之前,所做的修改对其他事务都是不可见的。
- 也叫不可重复读,因为一个事务前后两次执行同样的SQL查询,可能会得到不一样的结果(两次查询间隙可能会有其他事务提交了改动,而第二次查询读取了其他事务的提交修改)
- 大多数数据库系统的默认隔离级别
- repeatable read(可重复读)
- 在提交读的基础上,做到了可重复读:前后两次同样的SQL查询得到的结果一定相同。
- 幻读(Phantom Read):指的是当某个事务在读取某个范围内的记录时,另外一个事务有在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。
- 理论上,可重复读无法解决幻读的问题;但是Innodb存储引擎通过MVCC解决了幻读的问题;
- MySQL的默认事务隔离级别
- serializable(可串行化)
- 最高的隔离级别,通过强制事务串行执行,避免了前面的幻读问题。
- 会在的每一行数据上都加上锁,所以可能导致大量的超时和锁争用的问题。
- 实际应用很少用这个级别,只有在非常需要确保数据一致性并接受没有并发的情况下,才考虑使用
死锁
死锁是指两个或多个事务在同一资源上互相占用,并请求对方占用的资源。
Innodb目前处理死锁的方式是,将持有最少行级排他锁的事务进行回滚(这是相对简单的死锁回滚方法)。
多版本并发控制-MVCC(Multi-Version Concurrency Control)
- MySQL的大多数事务型存储引擎实现的都不是简单的行级锁,基于提升并发性能的考虑,他们一般都实现了各自的MVCC。包括Oracle,PostgreSQL等其他数据库也都实现了MVCC,只是机制不尽相同,MVCC没有一个统一的实现标准。
- 可以认为MVCC是行级锁的一个变种,很多情况下它可以避免加锁操作,因此开销更低。
- MVCC的实现,是通过保存数据在某个时间点的快照来实现的。
:下面仅仅是大意的说明,实际上行记录隐藏列是创建事务id、undo log指针,还有一个delete bit标记。
不同存储引擎的MVCC的实现是不同的,典型的有乐观并发控制和悲观并发控制。从下文可以看出innodb的MVCC实现属于乐观并发控制:
- InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。一个列保存了行的创建时间,另一个列保存行的过期时间(删除时间)。当然存储的并不是实际的时间值,而是系统版本号。事务开始时刻的系统版本号会作为事务的版本号,每开始一个新的事务,系统版本号都会自动递增。(类似自增主键概念,系统版本号始终比最大的事务版本号大1)下面是repeatable read的隔离级别下,MVCC的具体操作:
- select
- 只查找创建版本早于当前事务版本的数据行(create_system_version_number <= now_tx_id),从而确保读取的行是事务开始前就存在的,或者是事务本身插入(或修改)的数据行
- 查找的数据行的删除版本号要么未定义(数据没被删除),要么大于当前事务版本号(del_system_version_number > now_tx_id),从而确保事务读取的行在事务开始之前未被删除,也就是说事务之后其他事务删除的话该事务不可见。
- 说白了就是读取数据的时候只认now_tx_id之前的数据来处理,其他的忽略。
- insert:
- 事务给自己新插入的行的创建版本号标记为now_system_id(now_system_id即当前系统版本号)
- delete:
- 事务给自己删除的行的删除版本号标记为now_system_id
- update:
- 事务的修改实际上是删除老的行,新增一行。
- 删除、新增的隐藏列处理逻辑与上面保持一致
- select
保存这两个额外的系统版本号,使大多数的读操作都可以不用加锁。可以不加锁去做到安全的并发,这可不就是乐观并发控制嘛!不足之处是需要额外的存储空间,还有一些额外行检查工作、维护工作。
MVCC只是在可重复读和提交读这两个隔离级别下工作。其他两个隔离级别不兼容,因为未提交读总是读最新数据而不管隐藏列的事务版本,可串行化则对所有读取的行都会加锁。
PS:脏读 VS 幻读的理解
其实这两个概念没有什么联系,只是名称都带个读而已。就我的理解而言:
- 脏读强调的是其他事务未提交的改动就会被读取,这里的改动包括update、insert、delete
- 幻读从定义来看仅仅是强调其他事务提交的改动会被读取,但是这里的改动仅仅包含了insert、delete,没有涉及update。只要没有update,就可以认为两次查询的结果一致,这里说的一致应该是指特定数据行的内容一致,但是数据行的范围发生了变化,多出来的行就是幻行(我认为少了的行也可以认为一种变相的幻行,这里书籍没有明确指明)。
- 总而言之,就是脏读是读取了未提交的update,幻读是读取了已提交的insert/delete。讨论两者的比较没啥意义,本身就是特定模式下的概念,都没啥相似之处