数据库事务基础

一、为什么需要事务?

举个例子:
A 和 B 账户各有 50 元余额,现在 A 要给 B 转账 10 元,刨除掉具体的业务逻辑和工作流程,仅考虑数据库层面的操作:A 账户-10,B 账户+10 。
如果没有事务,就只能让两个 update 操作串行执行:

// 代码片段1
run("update account set balance=balance-10 where account_id=A");
run("update account set balance=balance+10 where account_id=B");
图1

如图1,对于这个转账操作,我们的期望是:

  • 代码片段1执行前,数据库数据:A 余额 50,B 余额 50。
  • 代码片段1执行后,数据库数据:A 余额 40,B 余额 60。

但是,如果执行完 A-10 后,系统崩溃了,B+10 没能得到执行。则:

  • 代码片段1执行前,数据库数据:A 余额 50,B 余额50。
  • 代码片段1执行后,数据库数据:A 余额 40,B 余额50。
  • 在此场景下,对 A 来说,这个转账操作是成功了的,毕竟钱都扣掉了,但 B 却没有收到对应的款项,也就是说从 A 账户扣掉的 10 块钱凭空消失了。这在实际业务中是不可接受的。

要解决这个问题,就必须有一套机制,来保证 A-10 和 B+10 这两个操作同时成功,或同时失败,以确保两个 update 操作执行前后的数据的一致性、正确性和完整性。

这套机制,就是“事务”。

二、事务定义

数据库事务 是可以作为一个完整的逻辑工作单元来执行的不可分割的一组操作,这些操作要么全部执行,要么全部不执行。

  • 在关系型数据库中,事务可以是一条sql语句,也可以是多条sql语句。
  • 在此基础之上,为了保证这一组操作执行结果的正确性和有效性,事务又附加了一些条件,就是ACID了。

ACID 的关注点和对事务的要求如下:


ACID

一致状态,是指数据处于一种语义上的有意义且正确的状态。
隔离性还有其他的称呼,如:并发控制、可串行化、锁等。

三、事务并发问题和隔离模式

并发访问场景下,若没有采取必要的隔离措施,会存在一些读写问题,包括:

  • 3 类数据读问题:脏读、不可重复读和幻读。
  • 2 类数据更新问题:第一类丢失更新、第二类丢失更新。
并发读写问题

数据库提供不同级别的事务隔离模式,解决部分或全部上述的读写问题,SQL 规范定义了四种级别的隔离模式(级别由低到高):

  1. Read Uncommitted(读未提交):最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。
  2. Read Committed(读已提交):只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。
  3. Repeated Read(可重复读):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。
  4. Serialization(串行化):事务串行化执行,最高隔离级别,牺牲系统的并发性,将所有事务串行执行。可以解决并发事务的所有问题。

事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。

四、ACID的实现技术

ACID 的实现技术包括:并发控制、日志管理、备份恢复、锁管理、MVCC等内容。
其中,并发控制和日志技术是核心。

4.1 并发控制技术

并发控制技术是实现原子性、一致性和隔离性的重要技术之一。并发控制的本质就是要对并发的事务实现正确又高效的调度。

从实现思想的角度看,并发控制技术分两类:

  • 乐观并发控制,Optimistic Concurrency Control,OOC,事后检查。
  • 悲观并发控制,Pessimistic Concurrency Control,PCC,提前预防。

从实现技术角度,并发控制机制有如下几类:

  1. 基于锁的并发控制机制

基于锁的并发控制机制是最常见的一种并发控制机制,事务中可能涉及到的一些锁的概念如下图:


  1. 基于时间戳/数据版本的并发控制

基于数据版本的并发访问控制,是通过给数据表加一个版本号或时间戳字段实现。

  • 当读取数据时,将 version字段的值一同读出,数据每更新一次,对此 version 值加一。
  • 当提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据,拒绝更新。

基于时间戳的并发控制类似,把版本号换成时间戳就行了。

  1. 基于MVCC的并发控制

MVCC(Multi-Version Concurrent Control),即多版本并发控制协议,是个行级锁的变种,它在普通读情况下避免了加锁操作,因此开销更低,同时在保证数据一致性的前提下,提供一种高并发的访问性能。

虽然不同数据库或数据库引擎对 MVCC 的实现不同,但通常都是实现非阻塞读,对于写操作只锁定必要的行:

  • 第一种实现方式:将数据记录的多个版本保存在数据库中,当这些不同版本数据不再需要时,垃圾回收器回收这些记录。——PostgreSQL 和 Firebird/Interbase 采用。
  • 第二种实现方式:只在数据库保存最新版本的数据,但是会在使用undo时动态重构旧版本数据。——Oracle 和 MySQL/InnoDB 采用。

4.2 日志技术

数据库的日志可以大体分为3类:binlog、redo log、undo log。

其中,binlog是Server层记录的日志, redo log 和 undo log 是数据库存储引擎层的日志。

大部分关系型数据库系统是通过 redo log 和 undo log 来实现事务的原子性、一致性和持久性,同时也用于支持数据备份和恢复:

  • redo log,记录数据被修改后的值,可以用来恢复未写入 data file 的已成功事务更新的数据。redo log 又包括:redo log buffer 和 redo log file,一个写内存,一个写硬盘。
  • undo log,记录数据被修改前的值,可以用来在事务失败时进行 rollback。

举个例子,假设 A=1且 B=2,某事务 T 要做 A=3 和 B=4,则

  1. 事务简化过程:
1.start
2.A=1——>undo log
3.set A=3
4.A=3——>redo log buffer
5.B=2——>undo log
6.set B=4
7.B=4——>redo log buffer
8.redo log buffer——>redo log file
9.commit
  1. undo 和 redo 日志:
// undo日志:
<T,A,1>
<T,B,2>
// redo日志:
<T,A,3>
<T,B,4>
  1. 数据恢复(重做、撤销)
    若执行 9 时出现系统异常,则下次启动时可以通过 redo log 重做该事务。
    若执行 6 时出现异常,则可以通过 undo log 撤销已经做过的修改。
  2. undo/redo日志
    也有把 undo 和 redo 结合起来的做法,叫做 Undo/Redo 日志,在前面中的例子
    Undo/Redo 日志为:
<T, A, 1, 3>
<T, B, 2, 4>

五、参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 导语:计算机硬件在飞速发展,数据规模在急速膨胀,但是数据库仍然使用是十年以前的架构体系,WiredTiger 尝试...
    isgiker阅读 3,609评论 0 7
  • 很多人喜欢这篇文章,特此同步过来 由浅入深谈论spring事务 前言 这篇其实也要归纳到《常识》系列中,但这重点又...
    码农戏码阅读 4,982评论 2 59
  • 数据库范式 https://www.zhihu.com/question/24696366 索引 索引可以加快数据...
    EakonZhao阅读 3,419评论 3 23
  • 引言 事务,体现的是部分与整体的思想,多个部分组成一个整体,要么全部生效,要么全部失效。事务就是实现这种效果的一个...
    程序猿胖子阅读 506评论 0 2
  • 人到中年万事休,每天虚度光阴收。远山金矿力不从,近水楼台看月变。一天一天方步行,一杯一怀早茶饮。日出东方西山落,也...
    甘朝武阅读 166评论 0 0

友情链接更多精彩内容