数据库并发如何让数据操作串行化

本章主要讲解 MySQL 是如何在并发请求保证数据的正确性。
引入了一些概念,指引学习方向,文末有我们学习参考文章

概念 (针对于 InnoDB)#

学习一个新鲜事物的时,我们总会碰到一些新的概念或设计,如果你细心领悟或许可以举一反三,通过之前学过的东西与之关联。但是如果你并无感悟,倒也无妨。因为大多数你只需要整理后自己能记住就可以。毕竟我们不是开创者而是使用者。接下来我将要介绍几个概念或是设计是针对于今天所探讨的主题。

MVCC (多版本并发控制)#

对于未接触过这个概念的人肯定是一脸雾水,那我只简介一下它的作用。如果想学习可以自行查找资料。

它会在每个数据行后面追加隐藏两个字段,分别代表创建(创建的事务版本号),删除(删除的事务版本号)。

事务版本号:每启动一个事务都会产生版本号

id name create_version delete_version
1 dog 1 2
1 pig 2 null
2 cat 2 3
3 cats 3 null

上表为 MVCC 的一个简图,我来描述一下它的经历。

  1. 事务 1 :插入一条 id=1,name=dog 的数据。
  2. 事务 2:更新了 id=1,name=pig。 并且插入一条数据 id=2,name=cat
  3. 事务 3:插入了一条 id=3,name=cats,并且删除了 id=2 的数据

有了这个设计我们就能弄出不同版本的查询结果(隔离级别中的快照读就与之相识)

  1. 获取最新版本(版本 3)的数据,查询条件为:create_version>=3 and delete_version=null。
  2. 获取指定版本(版本 2)的数据,查询条件为:create_version<=2 and (delete_version=null or delete_version>2)。

锁 (防止数据并发操作)#

锁一个特别简单的名词,但是初学者未必能理的清楚概念。像我这样文字理解能不好的人就卡了半天。

最后我的理解是:阻碍你获取数据的就是锁。锁是你与数据交流间的一道屏障。

行锁#

共享锁(S 锁)又称为读锁(Share lock, 简记为 S 锁),若事务 T 对数据对象 A 加上 S 锁,则其它事务只能再对 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。#
排它锁(X 锁)又称为写锁((eXclusive lock, 简记为 X 锁)),若事务 T 对数据对象 A 加上 X 锁,则只允许 T 读取和修改 A,其它任何事务都不能再对 A 加任何类型的锁,直到 T 释放 A 上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。#

这里我进行了一个比喻,假设数据是一个透明的箱子。如果箱子里东西能看到就是可查询,能触碰到就是可修改。

共享锁:添加条件(能看到箱子里东西的事务都能添加,否者需要等待),相当于给箱子加了一个锁,这个锁只有自己能打开。

排它锁:添加条件(能触碰到箱子里东西的事务都能添加,否者需要等待),相当于打开箱子给里面东西套上一个盒子,再对盒子加锁,这个锁只有自己能打开

  • 事务 1 为数据添加共享锁,事务 2 能再添加共享锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?
  • 事务 1 为数据添加共享锁,事务 2 能添加排它锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?
  • 事务 1 为数据添加排它锁,事务 2 能添加共享锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?
  • 事务 1 为数据添加排它锁,事务 2 能添加排它锁吗?如果能,事务 2 能修改数据吗,事务 1 能修改数据吗?

间隙锁 (gap 锁)#

会在查询数据的两侧插入间隙锁,导致无法插入。 这与 InnoDB 索引顺序排序有关。#

四种事务隔离#

在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。

锁和多版本数据(MVCC)是 InnoDB 实现一致性读和隔离级别的手段。

因此,在不同的隔离级别下,InnoDB 处理 SQL 时采用的一致性读策略和需要的锁是不同的。

Read Uncommitted (RU)#

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

Read Committed (RC)#

这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。

Repeatable Read (RR)#

这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的 “幻影” 行。InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

Serializable(可串行化)#

这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

隔离级别与 MVCC 的关系#

事务中 sql 语句有加锁与不加锁两种,我们把不加锁的定义为快照读,加锁的定义为当前读,以下是分类

  • 快照读:就是 select
    • select * from table ….;
  • 当前读:特殊的读操作,插入 / 更新 / 删除操作,属于当前读,处理的都是当前的数据,需要加锁。
    • select * from table where ? lock in share mode; S 锁,Gap 锁
    • select * from table where ? for update; X 锁,Gap 锁
    • insert; X 锁,Gap 锁
    • update ; X 锁,Gap 锁
    • delete; X 锁,Gap 锁

MVCC 定义了隔离级别不同的快照读版本,只对 RC 与 RR 进行分析。

  1. RC 隔离级别:每次快照读都是获取最新版本。
  2. RR 隔离级别:每次快照读都是获取指定版本。

隔离级别与锁的关系#

不同的隔离级别当前读加锁情况不同,只对 RC 与 RR 进行分析。

  1. 共同点:在添加 X 锁与 S 锁方面是一样的。
  2. 不同点:RR 隔离级别有时还会添加 Gap 锁。

加锁分析#

数据库加锁解锁时间点#

数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

事务 加锁 / 解锁处理
begin
insert into test ….. 加 insert 对应的锁
update test set… 加 update 对应的锁
delete from test …. 加 delete 对应的锁
delete from test …. 事务提交时,同时释放 insert、update、delete 对应的锁

但在实际使用过程当中,MySQL 做了一些改进,在 MySQL Server 过滤条件,发现不满足后,会调用 unlock_row 方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是 MySQL,为了效率也是会违反规范的。(参见《高性能 MySQL》中文第三版 p181)

数据库如何加锁#

数据库加锁是比较复杂,有比较多的情况。需要你了解 InnoDB 的索引机制。这里简单通过 sql 执行计划(explain)分析,步骤如下:

  • type=range
    • key=PRIMARY:主键的范围扫描,会在主键 B + 树添加行锁。RR 隔离级别下主键 B + 树会添加间隙锁。
    • key = 唯一索引:唯一索引的范围扫描,会在主键 B + 树添加行锁,唯一索引 B + 树上添加行锁。RR 隔离级别下唯一索引 B + 树会添加间隙锁。
    • key = 普通索引:普通索引的范围扫描,会在主键 B + 树添加行锁,普通索引 B + 树上添加行锁。RR 隔离级别下普通索引 B + 树会添加间隙锁。
  • type=ref
    • key = 普通索引:普通索引扫描,返回匹配某个单独值的所有行,会在主键 B + 树添加行锁,普通索引 B + 树上添加行锁。RR 隔离级别下普通索引 B + 树会添加间隙锁。
  • type=eq_ref
    • key = 唯一索引:唯一索引扫描,返回匹配某个单独值行,会在主键 B + 树添加行锁,唯一索引 B + 树上添加行锁。
  • type=const
    • key = 主键索引:主键索引扫描,返回匹配某个单独值行,会在主键 B + 树添加行锁。

事务隔离中关于不可重复读,可重复读,幻读的理解#

以下是作者的理解,符合所学知识,大家可以参考。

  • 是否可重复读是针对于快照读,幻读是针对于当前读。
  • RC 的不可重复读,是因为在 RC 隔离中快照读是获取最新快照版本,所有每次相同 sql 快照读的数据都可能不一样。
  • RR 可重复读,是因为在 RR 隔离中快照读是获取指定快照版本,所有每次相同 sql 快照读数据的是一样的。
  • RC 隔离中存在幻读可能性。举个例子有一个获取某表全部信息 sql 当前读(select from table for update)(sql 的语义是锁住全表并获取最新信息),但是这时如果另一个事务执行(insert into table) 语句仍然可以插入,因为 RC 隔离中没有间隙锁。这会导致前面 select from table for update 的数据不是最新有效数据。这就是幻读。

    幻读 —— 一个当前读的查询 sql 但是保证不了数据的正确性,这个查询就可能存在幻读。

  • RR 隔离中有间隙锁,所以不会幻读。资料上会出现幻读的其实已经被 InnoDB 引擎优化了。

MySQL 架构图 (题外)#

数据库并发如何让数据操作串行化

参考文档#

真正理解 Mysql 的四种隔离级别
MySQL 锁总结
MYSQL MVCC 实现原理
理解事务 - MySQL 事务处理机制
Innodb 中的事务隔离级别和锁的关系
MySQL 加锁处理分析

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。