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

本章主要讲解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 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 1

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!