03 | 事务隔离:为什么你改了我还看不见?

2023-11-07 01:32
文章标签 03 事务 隔离 看不见

本文主要是介绍03 | 事务隔离:为什么你改了我还看不见?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 03 | 事务隔离:为什么你改了我还看不见?
    • 隔离性与隔离级别
    • 事务隔离的实现
    • 事务的启动方式
    • 小结

03 | 事务隔离:为什么你改了我还看不见?

提到事务,你肯定不陌生,和数据库打交道的时候,我们总是会用到事务。最经典的例子就是转账,你要给朋友小王转 100 块钱,而此时

你的银行卡只有 100 块钱。

转账过程具体到程序里会有一系列的操作,比如查询余额、做加减法、更新余额等,这些操作必须保证是一体的,不然等程序查完之后,

还没做减法之前,你这 100 块钱,完全可以借着这个时间差再查一次,然后再给另外一个朋友转账,如果银行这么整,不就乱了么?这时

就要用到 “ 事务 ” 这个概念了。

简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务支持是在引擎层实现的。你现在知道,

MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM

被 InnoDB 取代的重要原因之一。

今天的文章里,我将会以 InnoDB 为例,剖析 MySQL 在事务支持方面的特定实现,并基于原理给出相应的实践建议,希望这些案例能加

深你对 MySQL 事务原理的理解。

隔离性与隔离级别

提到事务,你肯定会想到 ACID ( Atomicity 、 Consistency 、 Isolation 、 Durability ,即原子性、一致性、隔离性、持久性),今天

我们就来说说其中 I ,也就是 “ 隔离性 ” 。

当数据库上有多个事务同时执行的时候,就可能出现脏读( dirty read )、不可重复读( non-repeatable read )、幻读( phantom

read )的问题,为了解决这些问题,就有了 “ 隔离级别 ” 的概念。

在谈隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们都要

在二者之间寻找一个平衡点。 SQL 标准的事务隔离级别包括:读未提交( read uncommitted )、
读提交( read committed )、可重复读( repeatable read )和串行化( serializable )。下面我逐一为你解释:
读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变
更对其他事务也是不可见的。
串行化,顾名思义是对于同一行记录, “ 写 ” 会加 “ 写锁 ” , “ 读 ” 会加 “ 读锁 ” 。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

其中 “ 读提交 ” 和 “ 可重复读 ” 比较难理解,所以我用一个例子说明这几种隔离级别。假设数据表 T 中只有一列,其中一行的值为 1 ,下

面是按照时间顺序执行两个事务的行为。

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);

我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1 、 V2 、 V3的返回值分别是什么。

若隔离级别是 “ 读未提交 ” , 则 V1 的值就是 2 。这时候事务 B 虽然还没有提交,但是结果已经被A 看到了。因此, V2 、 V3 也都是2 。
若隔离级别是 “ 读提交 ” ,则 V1 是 1 , V2 的值是 2 。事务 B 的更新在提交后才能被 A 看到。所以,V3 的值也是 2 。
若隔离级别是 “ 可重复读 ” ,则 V1 、 V2 是 1 , V3 是 2 。之所以 V2 还是 1 ,遵循的就是这个要求:
事务在执行期间看到的数据前后必须是一致的。
若隔离级别是 “ 串行化 ” ,则在事务 B 执行 “ 将 1 改成 2” 的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1 、 V2 值是 1 , V3 的值是 2 。

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在 “ 可重复读 ” 隔离级别下,这个视图是在事务启动时创建
的,整个事务存在期间都用这个视图。在 “ 读提交 ” 隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的
是, “ 读未提交 ” 隔离级别下直接返回记录上的最新值,没有视图概念;而 “ 串行化 ” 隔离级别下直接用加锁的方式来避免并行访问。

我们可以看到在不同的隔离级别下,数据库行为是有所不同的。 Oracle 数据库的默认隔离级别其实就是 “ 读提交 ” ,因此对于一些从

Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为 “ 读提交 ” 。

配置的方式是,将启动参数 transaction-isolation 的值设置成 READ-COMMITTED 。你可以用show variables 来查看当前的值。

mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+

总结来说,存在即合理,哪个隔离级别都有它自己的使用场景,你要根据自己的业务情况来定。

我想 你可能会问那什么时候需要 “ “ 可重复读 ” ” 的场景呢?我们来看一个数据校对逻辑的案例。

假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的

余额和当前余额的差额,是否与本月的账单明细一致。

你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。

这时候使用 “ 可重复读 ” 隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。

事务隔离的实现

理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的。这里我们展开说明 “ 可重复读 ” 。

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的

值。

假设一个值从 1 被按顺序改成了 2 、 3 、 4 ,在回滚日志里面就会有类似下面的记录。

当前值是 4 ,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view 。如图中看到的,在视图 A 、 B 、 C 里面,这一

个记录的值分别是 1 、 2 、 4 ,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制( MVCC )。对于 read-view A

,要得到 1 ,就必须将当前值依次执行图中所有的回滚操作得到。

同时你会发现,即使现在有另外一个事务正在将 4 改成 5 ,这个事务跟 read-view A 、 B 、 C 对应的事务是不会冲突的。

你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。

也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。

什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。

基于上面的说明,我们来讨论一下为什么建议你尽量不要使用长事务。

长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面

它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB ,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。

除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个我们会在后面讲锁的时候展开。

事务的启动方式

如前面所述,长事务有这些潜在风险,我当然是建议你尽量避免。其实很多时候业务开发同学并不是有意使用长事务,通常是由于误用所致。 MySQL 的事务启动方式有以下几种:

  1. 显式启动事务语句, begin 或 start transaction 。配套的提交语句是 commit ,回滚语句是rollback 。

  2. set autocommit=0 ,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行commit 或 rollback 语句,或者断开连接。

有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。
因此,我会建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。

但是有的开发同学会纠结 “ 多一次交互 ” 的问题。对于一个需要频繁使用事务的业务,第二种方式每个事务在开始时都不需要主动执行一

次 “begin” ,减少了语句的交互次数。如果你也有这个顾虑,我建议你使用 commit work and chain 语法。

在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行commit work and chain ,则是提

交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否

处于事务中。

你可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

小结

这篇文章里面,我介绍了 MySQL 的事务隔离级别的现象和实现,根据实现原理分析了长事务存在的风险,以及如何用正确的方式避免长事务。希望我举的例子能够帮助你理解事务,并更好地使用 MySQL 的事务特性。
我给你留一个问题吧。你现在知道了系统里面应该避免长事务,如果你是业务开发负责人同时也是数据库负责人,你会有什么方案来避免出现或者处理这种情况呢?
你可以把你的思考和观点写在留言区里,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
上期问题时间
在上期文章的最后,我给你留下的问题是一天一备跟一周一备的对比。
好处是 “ 最长恢复时间 ” 更短。
在一天一备的模式里,最坏情况下需要应用一天的 binlog 。比如,你每天 0 点做一次全量备份,而要恢复出一个到昨天晚上 23 点的备份。
一周一备最坏情况就要应用一周的 binlog 了。
系统的对应指标就是 @ 尼古拉斯 · 赵四 @ 慕塔 提到的 RTO (恢复目标时间)。
当然这个是有成本的,因为更频繁全量备份需要消耗更多存储空间,所以这个 RTO 是成本换来的,就需要你根据业务重要性来评估了。

这篇关于03 | 事务隔离:为什么你改了我还看不见?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/360350

相关文章

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

SpringBoot嵌套事务详解及失效解决方案

《SpringBoot嵌套事务详解及失效解决方案》在复杂的业务场景中,嵌套事务可以帮助我们更加精细地控制数据的一致性,然而,在SpringBoot中,如果嵌套事务的配置不当,可能会导致事务不生效的问题... 目录什么是嵌套事务?嵌套事务失效的原因核心问题:嵌套事务的解决方案方案一:将嵌套事务方法提取到独立类

cross-plateform 跨平台应用程序-03-如果只选择一个框架,应该选择哪一个?

跨平台系列 cross-plateform 跨平台应用程序-01-概览 cross-plateform 跨平台应用程序-02-有哪些主流技术栈? cross-plateform 跨平台应用程序-03-如果只选择一个框架,应该选择哪一个? cross-plateform 跨平台应用程序-04-React Native 介绍 cross-plateform 跨平台应用程序-05-Flutte

MySql 事务练习

事务(transaction) -- 事务 transaction-- 事务是一组操作的集合,是一个不可分割的工作单位,事务会将所有的操作作为一个整体一起向系统提交或撤销请求-- 事务的操作要么同时成功,要么同时失败-- MySql的事务默认是自动提交的,当执行一个DML语句,MySql会立即自动隐式提交事务-- 常见案例:银行转账-- 逻辑:A给B转账1000:1.查询

FreeRTOS内部机制学习03(事件组内部机制)

文章目录 事件组使用的场景事件组的核心以及Set事件API做的事情事件组的特殊之处事件组为什么不关闭中断xEventGroupSetBitsFromISR内部是怎么做的? 事件组使用的场景 学校组织秋游,组长在等待: 张三:我到了 李四:我到了 王五:我到了 组长说:好,大家都到齐了,出发! 秋游回来第二天就要提交一篇心得报告,组长在焦急等待:张三、李四、王五谁先写好就交谁的

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因

Vue day-03

目录 Vue常用特性 一.响应更新 1. 1 v-for更新监测 1.2 v-for就地更新 1.3 什么是虚拟DOM 1.4 diff算法更新虚拟DOM 总结:key值的作用和注意点: 二.过滤器 2.1 vue过滤器-定义使用 2.2 vue过滤器-传参和多过滤器 三. 计算属性(computed) 3.1 计算属性-定义使用 3.2 计算属性-缓存 3.3 计算属

spring事务属性的xml格式配置

实际是使用代理做的事务优化 <!--配置事务的属性--><tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--匹配所有以add开头的方法--><tx:method name="add*" propagation="REQUIRED" /> <tx:metho

Spring Cloud整合Seata实现分布式事务

文章目录 1.Seata1.1 官网1.2 下载1.3 通过安装包运行seata1.3.1 解压seata-server-1.3.0.zip1.3.2 修改 conf/file.conf 配置文件1.3.3 修改conf/registry.conf配置文件1.3.4 添加seata配置信息到nacos1.3.5 配置seata服务端数据库表结构1.3.6 启动seata 2.Spring

分布式 事务的几种实现方案

背景 四月初,去面试了本市的一家之前在做办公室无人货架的公司,虽然他们现在在面临着转型,但是对于我这种想从传统企业往互联网行业走的孩子来说,还是比较有吸引力的。 在面试过程中就提到了分布式事务问题。我又一次在没有好好整理的问题上吃了亏,记录一下,还是长记性 !!! 先看面试过程 面试官先是在纸上先画了这样一张图: 让我看这张图按照上面的流程走,有没有什么问题?面试官并没有直接说出来这里面