侧边栏壁纸
  • 累计撰写 247 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

并发事务存在的问题和 MySQL 事务隔离级别

kaixindeken
2021-07-26 / 0 评论 / 0 点赞 / 95 阅读 / 3,986 字

并发事务的由来

我们知道,MySQL 数据库是一个典型的 C/S 架构(即 Client/Server,作为对比,网站应用属于 B/S 架构,即 Browser/Server),在服务器上启动 MySQL 服务端软件后,可以通过多种不同的 MySQL 客户端软件建立与服务端的连接,这些客户端软件包括命令行、各种图形化工具(MySQL Workbench、Navicat for MySQL、TablePlus 等等)、以及不同编程语言嵌入的 MySQL 客户端 SDK。

所以,一个 MySQL 服务端可能同时需要处理多个客户端的连接和查询请求(为了简化模型,我们只考虑单台 MySQL 服务器的情况),对于使用 InnoDB 引擎的数据库/数据表而言,这些请求可能同时也是事务的一部分,因此,MySQL 服务端软件可能需要同时并发处理多个事务。

并发事务可能遇到的问题

MySQL 的数据库事务具备隔离性,这就意味着这些并发处理的事务操作彼此之间不能相互影响,这种相互响应主要包括以下这些情况:

脏写

注:我们以同时执行的事务 A 和事务 B 为例进行介绍。

事务 B 修改了事务 A 修改过但尚未提交的数据,就意味着发生了脏写(反之亦然)。

以前面介绍事务的支付宝转账为例,如果小明使用支付宝购物消费了 100 元(对应事务 A),同时小强给小明转账 500 元(对应事务 B),为了方便理解,我们把相应的 SQL 语句执行流程列举如下:

SQL 语句执行序列事务A事务B
1BEGIN;
2UDPATE wallets SET balance = balance - 100 WHERE id = 1BEGIN;
3...UDPATE wallets SET balance = balance + 500 WHERE id = 1
4...COMMIT;
5ROLLBACK;

这两个并发执行的事务都对小明账户记录进行了修改,假设事务 B 提交的时候,事务 A 还没有提交,还做了一些其它的事情,但是遇到问题最后回滚了,这样一来,将导致 id = 1 的这条记录恢复到事务 A 执行之前的状态,事务 B 所提交的更改生生被抹掉了,仿佛从来没有发生一样,小强明明给小明转账 500 元,转账成功了,小强的钱扣了,小明却没有收到,小强找谁要回这 500 块钱!

在这个场景下,事务 B 修改了事务 A 未提交的更改,这就是脏写。

脏写会导致一个事务的回滚清空另一个事务提交的更改这种非常严重的事故,这是任何数据库系统设计时都必须要避免的,否则就没人敢用了!

脏读

事务 B 读取到了事务 A 修改过但未提交的数据,就意味着发生了脏读(反之亦然)。

还是以上面的数据表为例进行介绍,事务 A 对应小强给小明转账了 100 块钱,事务 B 对应的是小明去查看支付宝余额,对应的 SQL 语句执行序列:

SQL 语句执行序列事务A事务B
1BEGIN;
2UDPATE wallets SET balance = balance - 100 WHERE id = 1BEGIN;
3...SELECT balance FROM wallets WHERE id = 1
4...COMMIT;
5ROLLBACK;

事务 A 执行更新后没有直接提交而是做了一些其它的事情,事务 B 读取到了事务 A 更新后但未提交的记录,并返回结果,但是事务 A 后续遇到问题回滚了,意味着这条记录又恢复到事务 A 开启前的状态,小明的余额栏里已经显示的是转账后的值,但是小强这边转账失败回滚了,小明满怀期待拿着这钱去消费,却被提示扣款失败,余额不足(再次查询返回的就是回滚后的余额值了),刷新余额一看,懵逼了,不是刚刚还有钱的吗,去哪了,长翅膀飞走了?!

在这个场景下,事务 B 读取了事务 A 修改过但未提交的数据,这就是脏读。

脏读会导致一个事务的回滚清空另一个事务已经读取到的记录,相比脏写,虽然没有那么严重了,但是如果依赖这个读取的值进行了一些不可逆的或者回滚成本很高的其他操作,依然是很严重的事故,一般数据库系统的设计也要避免这个问题,否则后患无穷。

不可重复读

事务 B 能够读取事务 A 提交过的修改数据,就意味着发生了不可重复读(反之亦然)。

还是以上面的示例为例,只是这次,我们不回滚事务 A 了,而是提交它:

SQL 语句执行序列事务A事务B
1BEGIN;
2BEGIN;SELECT balance FROM wallets WHERE id = 1
3UDPATE wallets SET balance = balance - 100 WHERE id = 1
4...SELECT balance FROM wallets WHERE id = 1
5COMMIT;
6SELECT balance FROM wallets WHERE id = 1

事务 B 进行了三次读取,第一次事务 A 尚未开启,第二次事务 A 修改了数据但尚未提交,因此事务 B 读取到的记录和第一次一样,第三次事务 A 已经提交,可以读取到修改后的值。

以后每次其他事务针对该记录的提交更改后,事务 B 都可以读取到最新值,所以这种场景称之为不可重复读。

不可重复读似乎不像脏读和脏写那样会造成什么重大的事故,但是会导致同一个事务中前后读取到的同一条记录值不一致,造成业务逻辑困扰。

幻读

事务 B 先通过指定条件查询出一些记录,然后事务 A 又向该表插入了符合同样条件的记录;事务 B 再次按照同样条件查询时,能够把事务 A 插入的记录也读取出来,就意味着发生了幻读(反之亦然)。

可以看到,前面的脏写、脏读、不可重复读都是针对单行记录,而幻读侧重点是读取到了之前没有的新记录(一行或者多行):在同一个事务中进行两次(也可能是多次)条件完全相同的查询,返回的结果第二次竟然比第一次多,就好像出现了「幻觉」一样,因此叫做幻读。

注:注意幻读侧重点是多出新的记录来,如果只是原纪录的更新或者后面的查询记录数比前面少,都不算做幻读,而是不可重复读。

可以看到,幻读针对的是 INSERT 操作产生的新记录,不是原有记录的更新,因此影响比不可重复读还要小。基于上面的介绍,我们也容易得出上面四种并发事务产生问题的严重性孰大孰小:

脏写 > 脏读 > 不可重复读 > 幻读

MySQL 是如何解决这些问题的

事务隔离级别

为了解决并发事务产生的问题,SQL 标准设定了不同事务隔离级别:

隔离级别脏读不可重复读幻读
未提交读(Read uncommitted)可能可能可能
已提交读(Read committed)不可能可能可能
可重复读(Repeatable read)不可能不可能可能
可串行化(Serializable )不可能不可能不可能

注:列表里为啥没有脏写呢?因为脏写是任何数据库系统都不允许出现的,出现脏写这个事故锅太大,没人背的动。

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据;
  • 提交读(Read Committed):只能读取到已经提交的数据,即允许不可重复读(Oracle 等多数数据库默认都是该级别);
  • 可重复读(Repeated Read):在同一个事务中相同条件的查询结果都是事务开始时保持一致的,这是 MySQL InnoDB 引擎的默认级别,在这种级别下 InnoDB 通过 Next-key Lock 机制消除了幻读;
  • 可串行化(Serializable):完全串行化读写数据,每次读都需要获得表级共享锁,读写相互都会阻塞,虽然事务隔离性最好,但是性能太差,故而一般不会设置这个隔离级别。

如何设置事务隔离级别

在 MySQL 中,我们可以通过如下命令查询系统默认的事务隔离级别:

SHOW VARIABLES LIKE 'transaction_isolation';

1.jpeg

要修改系统默认的事务隔离级别,可以使用如下命令:

SET TRANSACTION ISOLATION LEVEL {level};

你可以将 替换成如下隔离级别对应的值:

level: {
     REPEATABLE READ
   | READ COMMITTED
   | READ UNCOMMITTED
   | SERIALIZABLE
}

除非本地测试,否则不建议线上修改默认隔离级别,因为它已经解决了并发事务可能遇到的所有问题。

0

评论区