前言
学习/导流:
小林coding - 事务篇
学习意义
- 理解MySQL如何去处理并发问题,借鉴其思想
- 存储作为应用的关键能力,而事务作为关系型数据库的关键概念,掌握很必要,也为分布式事务学习做奠基
相关说明
该篇博文是个人阅读的重要梳理,仅做简单参考,详细请阅读小林coding的原文!
一、基本场景
银行转账示例:
对于用户来说,转账是一件事情,即用户A把钱转到用户B。现实中,用户A直接把钱(x)递给用户B,表现出来则为用户A减少x,用户B增加x。
假设用户A 的余额acount=y,用户acount=z;.那么 简单地进行计算,则需要 y-x. z+x.才能算该次转账成功。
对于计算机的指令计算,y-x,z+x则会进进行多次计算操作(包括i++,实际上也不是跟不上的原子性,通过汇编也可发现会经过取值、计算多步操作)。
对于应用来说,还需要引入数据库第三方的存储软件来存储 用户的各自余额,那么这中间的计算步骤更长,出错。
为了保证不出现中间状态,要么转账成功、要么失败。数据库则引入了事务的概念。
注:这也是抽象的魅力吧,屏蔽底层细节,只为用户保证简洁、正确的处理结果。
基本手段:若所有操作成功则进行提交事务,失败则进行回滚。
二、事务特性
事务是由 MySQL 的引擎来实现的,我们常见的 InnoDB 引擎它是支持事务的。
不过并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。
- A(Atomic)原子性:基本手段,进行操作的捆绑、封装,也有点组合思想,将不同的操作组合为一个事务,对用户视角又是一个新的操作。一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态。
- C(consistency)一致性:目标,事务的最终目标是保证正确性、给用户看到正确的计算结果,不能产生相互矛盾,则需要数据一致性,而其他三个特性都是为了实现该特性。指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
- I(isolation)隔离性:基本手段,由于数据库设计为多个应用的存储系统,因此需要考虑并发控制。而单靠原子性只能保证该事务事实上的正确性,却无法保证每个应用的视角正确性,可能存在相互影响的情况。而隔离性则是保证 各个 读写操作互不影响。
- D(durability)持久性:基本手段,对于处理过的数据需要进行永久存储、持久化。就像时间流逝之后所发生的的事情就永远无法改变,尘封于世间。但数据库其实也可以存在物理删除、逻辑删除,不管怎么样,都需要去记录。
对于以上特性,MySQL的InnoDB采用以下方式去实现。
**原子性:**通过 undo log(回滚日志) 来保证的;
**持久性:**通过 redo log (重做日志)来保证的;
**隔离性:**通过 锁机制、MVCC 来保证的;
一致性则是通过持久性+原子性+隔离性来保证;
对于原子性和持久性问题,InnoDB已经采用日志去内部保证了。暂时去了解、学习相应日志设计原理及思想即可。先主要来分析一下隔离问题,也是程序员应对复杂性问题(并发性处理)的关键能力。
三、并发问题
就像CPU调度进程一样,存在一定的临界资源,竞争。那么则会产生并发问题。对于数据库来说,采用C\S架构,并通过TCP进行提供客户端给用户应用程序连接,可能会存在操作相同的数据(数据库的单位一般为 表、记录,借此可梳理 进程并发问题的临界资源是什么? 代码块,本质是内存? 计算来源于对存储的数据操作 ,落脚需要在 数据对象上,分析其数据的粒度、单位)。那么则会存在并发问题。关于数据库的并发问题主要为以下:
- 丢失或覆盖更新
- 未确认的相关性(脏读)
- 不一致的分析(不可重复读)
- 幻读
丢失
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。
例如 用户A对acount+ x,用户B对acount+y。他们同时进行更新,则会都从最初的值,即acount操作。若用户A的更新在后面则会覆盖B的操作,从acount+y覆盖为acount+x。若保证用户A和B的先后则不会产生此种情况。
脏读
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。理解脏读的取名,脏读,读取到脏数据(中间状态数据、因为引入了事务的概念,对于未提交的数据都不能算最终处理的数据,因为它随时会因为错误等原因回滚)
当第二个事务选择其它事务正在更新的行时,会发生未确认的相关性问题。第二个事务正在读取的数据还没有确认并且可能由更新此行的事务所更改。
因为事务 A 是还没提交事务的,也就是它随时可能发生回滚操作,如果在上面这种情况事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读、
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象**。**
当第二个事务多次访问同一行而且每次读取不同的数据时,会发生不一致的分析问题。
不一致的分析与未确认的相关性类似,因为其它事务也是正在更改第二个事务正在读取的数据。然而,在不一致的分析中,第二个事务读取的数据是由已进行了更改的事务提交的。而且,不一致的分析涉及多次(两次或更多)读取同一行,而且每次信息都由其它事务更改;因而该行被非重复读取。
幻读
在一个事务内多次查询某个符合查询条件的**「记录数量」**,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。【虽然某行数据未被修改,但中间可能存在进行删除或插件新的行了】
并发问题的来源:事务的组合情况(读、写及次数),提交情况。根据这些情况去讨论。以上组合有:
- 事务A:写;事务B:写。
- 事务A:读;事务B:写(但未提交)。
- 事务A:读、读;事务B:写(修改所读的数据);
- 事务A:读、读;事务B:写(影响的是行数);
四、隔离级别
MySQL为解决上述的并发问题,进行了以下四种隔离级别措施保护。性能和正确性往往是对立的,在追求性能的同时,那么正确性则会向下降。随着隔离级别的增大,性能也会随之增大。
- 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
- 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;【可解决脏读问题】
- 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;【可解决不可重复读问题】
- 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;【可解决幻读问题】
注:在解决不可重复读的情况下,绝大数也是不会产生幻读的。【对于串行化会性能大大下降,因此一般不提倡将隔离级别上升至串行化】
解决幻读的主要办法:
-
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
-
针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。