关于数据库事务的整体认知

数据库事务

最近看了某位大神关于事务的理解,决定记录一下。地址

ACID

关于ACID,就是我们常见的即原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability)。

这里重点讲一下一致性和隔离性,首先是一致性,这里我理解为数据库状态一致,即在如果一个事务在执行前数据库的状态是合法的,那么在事务执行后它的状态也必将合法。

然后隔离性就是意味着,多个并发事务对同一数据操作,他们之间的行为不会相互影响。在数据库的各种隔离级别下,所有事务的执行结果跟它们按照某种顺序排列后顺序执行的效果一致。然而在实践中,出于性能考量,可序列化的隔离性很少被使用。

关于可能产生的数据错误

在这里插入图片描述

各种隔离级别以及其能够解决的问题

Read Committed

read committed 可以解决 脏读 和 脏写问题。

脏读的理解:当一个数据被事务A写入时,另一个事务B读到了这个修改后的数据就是脏读。但是笔者在理解脏读的时候,产生了一个疑问,反正都是要读的为什么不能读修改后的呢,脏读也没什么大碍啊。但是其实不是,万一读到这个数据后面被回退了,就会产生问题,就像我看到账户上显示有500,但是事实上银行只存了400。还有就是即使正常的存入了这一笔钱,其他事务也不应该读到还未commit的数据,因为事务与事务之间存在隔离性,这是数据库的基本性质。所以在某个事务能够读到另一个事务还未commit的数据就是脏读。

脏写的理解:当事务A正在写入数据是,但是它还未commit,B事务同样也在针对那条数据进行写入,且B比A先写完,然后A再写改变了B的写入。这就叫脏读。

针对脏读和脏写,设计了read commit (RC)

针对脏读,原理是只需要让其他事务在A事务commit之后才能读就行,但是现实中不是这样实现的,因为当A事务太长了,将引起数据库阻塞甚至雪崩。所以比较理想的情况就是,在A事务进行时,数据库自动保存一份旧的数据提供给其他事务读取,当A事务commit后,后续的请求才会在新的数据上。

针对脏写,原理是只需要让其他事务在A事务commit后才能写就行,现实实现就是,通过加行级别锁,要修改该行先获取锁,其他事务要操作要等锁释放就行。

Snapshot Isolation

实现了RC隔离级别,还是会有其他的冲突,例如不可以重复读(nonrepeatable read)同时也叫读倾斜(read skew)。 下面将描述一下什么不可重复度。

不可重复读的理解:举例银行转账如下图,由于有RC隔离级别,A事务数据读取count1数据是500,同时由于A事务正在执行,所以来更新账户数据的B事务只能读到A事务开始之前的旧数据,所以count1也是500,同时由于A事务没有写操作,所以B事务能够获取到对count1,count2的行级别写锁,那么B事务将count1+100,对count2-100(count2原来为500)。这时A事务继续对count2查询,查出B事务刚刚修改的数据,即400。那么A事务完成时就会返回count1=500,count2=400。这种现象就成为不可重复读。个人理解就是事务执行中没有意识到,事务发生的先后时间顺序,导致了这种现象。
在这里插入图片描述

针对不可重复读的现象,在RC隔离级别上实现了Snapshot Isolation(SI)

同时由于不同的事务在同一时刻可能需要看到数据的不同版本,因此数据库需要同时保持同一条数据的多个版本,这种技术被称为多版本并发控制(multi-version concurrency control, MVCC)。下面我用大神的原文进行讲述。

在这里插入图片描述

每个事务在启动后都被赋予一个单调递增的事务 id,即 txid。当该事务向数据库中写数据时,数据被标记上 txid,数据表中的每一行都有一个内置的 created_by,和一个 deleted_by 字段。该行被插入时,created_by 记录相应事务的 txid;该行被删除时,deleted_by 记录相应事务的 txid。当数据库可以确定没有事务会访问被删除的数据时,后台垃圾收集进程会将标记删除的数据清除,释放磁盘空间。当更新操作发生时,在数据库内部会被转化为一次删除和一次插入。

Snapshot Isolation 的命名问题

尽管 Snapshot Isolation 很受欢迎,但不同的数据库对它的称呼不同:

  • Oracle:serializable
  • PostgreSQL/MySQL:repeatable read

出现这种命名混乱的现象,主要原因在于 SQL 标准并未对 Snapshot Isolation 作出定义。

Preventing Lost Updates

上面我们主要讨论的是 一个只读事务,在其它并发写事务存在的情况下,读取到数据的保证;但对于一个写事务,在其它并发写事务存在的情况下,写入数据的保证,我们只讨论了 dirty writes 的情况。

在真实场景下有个很经典的现象,更新丢失问题。

在这里插入图片描述

要知道,上面这种情况不是脏写,因为B事务在A事务后进行的写,这也侧面反映了RC隔离级别的局限性。

为了避免更新丢失的问题,有很多方法。

原子写操作,实现方式是

  • 每条数据(object/document)加锁
  • 单线程执行

显式加锁,实现方式是,在更新数据的时候,对数据加锁,在事务结束之前不能被其他事务读写。

自动检测更新丢失,这个实现靠数据库系统进行实现自动检测

Write Skew & Phantoms

写倾斜,想象这样一个场景:假设你在写一个医院的值班管理系统,要求每次必须有至少一名医生值班,只要有一名医生值班,其它医生可以选择请假。假设 Alice 和 Bob 同时按下请假按钮:Alice 和 Bob 同时查询当前值班的医生人数,确认人数大于 2,于是两人都认为自己可以请假,最终没有人值班。这个现象就是 write skew。

在这里插入图片描述

写倾斜就是在并发写场景下,两个事务先读取相同的数据,再更新某些数据, 如果这里更新的是相同的数据,就是更新丢失现象;如果更新的是不同的数据,就是写倾斜现象。

解决写倾斜的办法就是序列化隔离(Serializability),或者一种简单粗暴的方法,就是记录下这个事务读过哪些数据,等提交时,检查这些数据没有在这个事务期间被人改写过。如果有就中断,如果没有就成功。这个方法也能解决幻读的问题。

幻读,当事物A查询某一范围的数据时,另一个事务B又在该范围内插入了新行并作了提交,此时事物A看不到新行,却在新行做了更新操作,此时事物A再查询会看到新行,就想产生了幻觉一样。

幻读事实上想要真正避免幻读只能采取serializable串行化隔离级别,因为都要加表级共享锁或排他锁,所以性能会很差,一般不会采用。

可序列化隔离 (Serializability)

serializable isolation。该隔离级别通常被认为是最强的隔离级别,它保证:即使所有事务可能并行执行,但执行的结果一定和它们按某个顺序执行的结果完全一致。换句话说,数据库保证所有竞争条件都能被避免。但在一个地方得到的,必将在另一个地方失去,serilizable isolation 虽然能解决并发问题,但它在其它方面做出了牺牲与让步。

其实现为:

  • 顺序执行事务 (Actual Serial Execution)
  • 两段锁 (Two-phase Locking)
  • 乐观并发控制技术 (Optimistic concurrency control techniques)
顺序执行事务

顺序执行事务就是很简单的只用一个线程处理事务,这样数据库会的吞吐量受限于单台机器上的cpu处理能力。为了应用多节点,可以采用数据分片,每个分片中依然遵循但线程事务处理。

两段锁协议

假设事务 A 已经进入写数据的过程,事务 B 取锁失败,必须等到事务 A commit 或者 abort 才能继续事务。2PL 的做法与这种方式类似,但更加苛刻:

  • 如果事务 A 已经读取一个数据对象,而事务 B 想要修改该对象,B 必须等待 A commit 或 abort 之后才能继续。这保证 B 无法背着 A 偷偷修改 A 读取的数据。
  • 如果事务 A 已经修改一个数据对象,而事务 B 想要读取那个数据对象,B 必须等待 A commit 或 abort 之后才能继续。与此同时,在 2PL 中,让 B 在 A 尚未结束时读取旧版本的数据对象也被禁止。

两端锁实现:

  • 如果事务 A 想要读取一个数据对象,它必须获取读锁。读锁允许多个事务同时获取锁,但如果事务 B 事先以及获取了写锁,那么包括 A 在内的其它想要读取该数据的事务必须等待。
  • 如果事务 A 想要修改一个数据对象,它必须获取写锁。其它任何事务都不能在这时候获取读锁或写锁,它们必须等待 A 释放锁后才能继续。
  • 如果事务 A 先读或写某一数据对象,它可以将自己已经拥有的读锁升级为写锁,升级过程类似于直接获取写锁。
  • 一旦事务 A 获取了锁,它必须持有该锁直到最后结束。这也是 2PL 名字中的 two-phase 的来历:
    • Phase 1:获取锁,这个阶段未结束前不会释放锁
    • Phase 2:释放锁,这个阶段开始后不会再获取锁

由于系统中存在许许多多的锁,不同的事务很容易因为互相等待锁而陷入死锁。数据库会自动检测事务之间的死锁,然后 abort 其中一个,保证另一个可以继续进行,aborted 事务需要应用程序自行重试。

乐观并发控制技术:

Serializable Snapshot Isolation (SSI) 就是乐观并发控制机制,

即先让事务进行,在事务将要 commit 时,数据库需要检查是否违背了隔离的要求,如果违背了,就将事务 abort。乐观并发控制并不是一个新观点,它的优点和缺点也一直被大家拿出来讨论。系统负载较高时,事务竞争的可能越大,abort 的比例越大,如果系统已经接近它的最大吞吐量,不断重试还会继续使得情况进一步恶化,这时乐观的并发控制机制不是一种好的选择;当系统负载适中时,乐观并发控制通常要表现地比悲观并发控制好。

SSI 在 snapshot isolation 之上加了一层检测事务读写冲突的检测逻辑,从而判断哪些事务应该 abort,保证隔离性达到 serialization 的要求。

至此我们已经完成了整个数据库事务的大致认知,再次感谢大神的笔记。

https://zhenghe.gitbook.io/open-courses/ddia-bi-ji/transactions

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值