事务
概念与特性
事务(Transaction)指的是一组数据库操作,这些操作要么全部成功执行,要么全部不执行,保证了数据库的一致性和完整性,它使得数据库操作可以按照逻辑上的单元进行组织和执行,提高了数据的可靠性和可维护性。事务通常具有以下四个特性,通常被称为ACID属性:
-
原子性(Atomicity):事务中的所有操作要么全部执行成功,要么全部不执行,即事务是一个不可分割的最小执行单元。如果事务中的任何一部分操作失败,整个事务都将被回滚(Rollback),数据库状态将会恢复到事务开始前的状态,保持数据的一致性。
-
一致性(Consistency):事务执行的结果必须使数据库从一个一致性状态转变到另一个一致性状态。这意味着事务必须满足数据库的约束和规则,如唯一性约束、外键约束等。
-
隔离性(Isolation):多个事务同时并发执行时,每个事务都应该被隔离开来,互相不受影响。即使有多个事务同时修改同一数据,它们之间也不能相互干扰,每个事务应该感知不到其他事务的存在。
-
持久性(Durability):一旦事务提交(Commit),其所做的修改将会永久保存在数据库中,即使系统发生故障,数据也不会丢失。数据库系统通常通过将事务日志写入持久存储介质(如磁盘)来实现持久性。
并发事务
并发事务可能引发的问题:脏读、不可重复读、幻读
通过隔离级别解决上述问题:读未提交、读已提交、可重复读、串行化
问题 | 描述 |
---|---|
脏读 | 一个事务读到另外一个事务还没有提交的数据。 |
不可重复读 | 在一个事务中,同一查询多次执行却得到了不同的结果。 |
幻读 | 在一个事务中,同一查询多次执行却得到了不同数量的结果。 |
脏读
假设有两个事务,事务 A 读取了事务 B 修改但尚未提交的数据,然后事务 B 回滚了修改,那么事务 A 所读取的数据就是不正确的,因为它读取到了未提交的、“脏”的数据。脏读可能导致读取到不一致的数据,降低了数据的可靠性。
不可重复读
假设事务 A 在执行查询时读取了某一行数据,然后事务 B 修改了这行数据并提交了事务,之后事务 A 再次执行相同的查询,此时得到的数据就可能与之前不同,导致了不一致的读取结果。这种情况下,事务 A 的读操作就发生了不可重复读。不可重复读可能破坏了事务的隔离性。
幻读
假设事务 A 在执行一个范围查询时,得到了一些行,然后事务 B 在执行 INSERT 或 DELETE 操作,导致事务 A 再次执行相同的查询时得到了不同的行数,就发生了幻读。幻读可能会导致在同一事务中看到不一致的数据,破坏了事务的隔离性。
还有一种情况——在解决了不可重复读问题的背景下,事务A想要在数据库中添加id为1的数据,首先它会查询是否存在id为1的数据,此时的查询结果是没有;在事务A进行插入操作之前,事务B向数据库中插入了id为1的数据并提交;当事务A再进行插入操作时,就会报错;事务A再次查询数据,但查询结果和第一次查询一致(表示没有该数据),此时就出现了幻读:
假设有一个银行账户表,其中有两个账户:账户A和账户B。
-
脏读:假设账户A的余额为1000元,事务A读取了账户A的余额,此时账户A的余额为1000元。然后事务B修改了账户A的余额为2000元,但尚未提交。此时,事务A再次读取账户A的余额,就会读到未提交的“脏”数据2000元,而实际上账户A的余额还是1000元。
-
不可重复读:假设事务A读取了账户A的余额为1000元,然后事务B将账户A的余额修改为2000元并提交。接着,事务A再次读取账户A的余额,此时读取到的余额为2000元,与之前的结果不同。
-
幻读:假设事务A执行了一个范围查询,查询所有余额大于1000元的账户,此时得到了账户A和账户B两个结果。然后事务B插入了一笔新的账户记录,使得账户C的余额也大于1000元。接着,事务A再次执行相同的查询,却得到了账户A、账户B和账户C三个结果,出现了幻读现象。
事务隔离
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted 未提交读 | √ | √ | √ |
Read committed 读已提交 | × | √ | √ |
Repeatable Read(MySQL默认) 可重复读 | × | × | √ |
Serializable 串行化 | × | × | × |
注意:事务隔离级别越高(上表中,隔离级别有上到下依次增高),数据越安全,但是性能越低。MySQL默认隔离级别是可重复读
Redo Log与Undo Log
缓冲池(buffer pool):是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。
数据页(page):是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据。
图1:
当我们操作数据的时候(如上图中的update,delete语句),并不是直接操作磁盘,而是首先去操作内存(缓冲池)。当一个操作被提交后,会先查询缓冲池中是否存在需要操作的数据。若没有,则从磁盘中将数据加载到内存(将数据所在的某页数据存储在缓冲池)。当缓冲池中有数据,则在操作完数据之后,按一定的频率将缓冲池的数据同步到磁盘中。以此减少磁盘的I/O,加快处理速度。
以上方法虽然提高了效率,但也存在一些问题。例如,当操作完数据后,缓冲区里面的已有新的数据页,但还并未同步到磁盘中,此时缓冲区里面的数据页称为脏页。若数据还未完成同步,而服务器发生宕机,那么内存中的数据就会丢失,已经操作完成的数据(在缓存中)也会丢失,因此违背了事务的持久化的特性。
Redo Log(重做日志):记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。
图2:
当进行数据操作时,缓冲池中的数据发生了变化,同时Redo Log buffer 中会记录这些数据页的变化,一旦Redo Log buffer 中发生变化,就会同步把这些数据记录到磁盘文件中。若对脏页中的数据进行同步失败时,就会从Redo Log中同步数据。
ps:在磁盘中,Redo log file实际有两份,他们是循环写的。有两份Redo log file的原因:
-
容错和可靠性:有两份Redo Log文件可以提高数据库的容错能力。如果一份Redo Log文件发生损坏或丢失,数据库可以继续正常运行并使用另一份Redo Log文件来进行恢复。这样可以避免因为单点故障导致的数据丢失或数据库无法启动等问题。
-
性能和并发:两份Redo Log文件可以提高并发性能。MySQL的Redo Log是循环写入的,当一个Redo Log文件写满时,数据库会切换到另一份Redo Log文件,这样可以避免写操作的阻塞,提高了并发处理能力。
-
恢复速度:当数据库需要进行恢复操作时,拥有两份Redo Log文件可以加快恢复的速度。数据库可以同时使用两份Redo Log文件进行恢复操作,从而加快了恢复的速度,减少了系统 downtime。
关于通过日志同步数据(图2),而不是采用直接同步数据(图1)的问题?
因为操作数据的时候,可能有多条记录(包含大量的操作)。而将数据同步到磁盘的时候,都是随机磁盘I/O,因此同步数据的性能是非常低的。如果使用Redo Log,在进行日志文件同步的时候是顺序磁盘I/O(因为日志文件是以追加的形式添加的数据),因此同步日志文件的性能会提升很多。所以,采用Redo Log方式同步。这种方式也称为WAL。
这部分可与Redis中的双写一致性联系https://blog.csdn.net/Z__XY_/article/details/136820674?spm=1001.2014.3001.5501。
补充:
-
随机磁盘I/O:
- 随机磁盘I/O指的是对磁盘上的数据进行随机的读取或写入操作,即访问不连续的数据块。
- 随机磁盘I/O的特点是磁盘臂需要频繁地移动,寻找数据所在的位置,因此会导致磁盘的寻道时间增加,I/O性能相对较低。
-
顺序磁盘I/O:
- 顺序磁盘I/O指的是对磁盘上的数据进行顺序的读取或写入操作,即按照数据在磁盘上的物理顺序进行访问。
- 顺序磁盘I/O的特点是磁盘臂不需要频繁移动,可以顺序地读取连续的数据块,因此具有较高的I/O性能,能够充分利用磁盘的吞吐能力。
Undo Log(回滚日志): 用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚 和 MVCC(多版本并发控制) 。undo log和redo log记录物理日志不一样,它是逻辑日志,它保证了事务的原子性和一致性。
- 可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,
- 当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
Undo Log与Redo Log的区别总结如下:
- Undo日志记录了事务对数据库的修改操作的逆操作(记录的是逻辑日志),用于回滚操作和MVCC,保证事务的原子性和一致性。
- Redo日志记录了事务对数据库的修改操作的正向操作(记录的是数据页的物理变化),用于恢复数据库的持久性,保证事务的持久性。
- Undo日志记录旧值,Redo日志记录新值。
- Undo日志的主要目的是回滚,而Redo日志的主要目的是恢复。
- Undo日志通常与事务数据绑定,Redo日志通常是独立的。
MVCC
MVCC(Multi-Version Concurrency Control,多版本并发控制),指维护一个数据的多个版本,使得读写操作没有冲突。当一个事务执行写操作时,数据库系统会为其创建一个新版本,并将该版本标记为最新版本。其他事务仍然可以读取旧版本的数据,直到该版本被清除或过期。
MVCC的具体实现,主要依赖于数据库记录中的3个隐式字段、undo log、readView。
隐藏字段
如图:
隐藏字段 | 含义 |
---|---|
DB_TRX_ID | 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。 |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。 |
DB_ROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。 |
undo log
- 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
- 当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
- 而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。
undo log版本链:
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
如图1——事务2对数据进行修改:
然后,事务3又对同一个数据进行修改:
随后,事务4对同一数据进行修改:
此时得到一个undo log版本链。
Readview
ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
-
当前读
- 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。
- 如下图,事务A分别使用操作1和操作3查询id为1的数据,事务B在事务A执行操作1、3之间进行了数据修改操作,那么事务A在操作3拿到的是修改后的最新数据:
-
快照读
- 简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- Read Committed:每次select,都生成一个快照读,即,每次读到的都是最细数据。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方,解决了可重复读的问题。
ReadView维护的事务包含了一下四个核心字段:
下图中展示了4个事务在不同时刻的操作:
如果在事务5的sql语句——“查询id为30的记录”时刻,此时readview中记录核心字段的组成——m_ids:事务3、4和5(事务2在此刻之前已经提交了事务);min_trx_id:事务3;max_trx_id:事务6(当前最大事务是5);creator_trx_id:事务5(因为在该事务中进行的查询操作,说明在该事务中创建的readview)。
readview为了能够在快照读中读取最准确的数据,定义了以下规则(还是用上面的事务5作为创建事务id为例):
- 针对①,说明事务5 可以访问 自己更改的数据;
- 针对②,当前最小的活跃事务id是事务3,比事务3小的事务id则是事务2,说明该事务已经提交了,则事务5也能访问事务2修改的数据;
- 针对③,事务5的id小于事务6,则说明 不能访问 事务6更改后的数据;
- 针对④,判断事务id 大于等于最小活跃事务id,且小于等于预分配事务id,且不在m_ids中,这表示事务5能已提交的事务修改后的数据。(举例:若事务4在事务5的第一条查询语句之前完成事务提交,则说明事务5可以访问事务4修改后的数据。)
不同的隔离级别,生成ReadView的时机不同:
- READ COMMITTED :在事务中每一次执行快照读时生成ReadView。
- REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
RC隔离级别下,在事务中每一次执行快照读时生成ReadView。 如图:
事务5在第一次查询时,可以访问事务2更改后的数据(从上到下依次将事务id带入版本链数据访问规则进行判断,如——针对事务4:①事务4不等于事务5;②事务4大于最小活跃事务(事务id为3);③事务4<小于预分配事务id(id为6);④事务4虽在最小活跃事务id和预分配事务id范围内,但它同时数据m_ids,因此不能访问事务4。其他事务按相同的方式判断):
事务5在第二次查询,可以访问事务3更改后的数据:
RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。 如图:
MVCC机制的优点包括:
- 提高了并发性:多个事务可以同时读取和修改数据库,而不会相互阻塞,提高了数据库的并发处理能力。
- 避免了读写冲突:通过使用快照读和版本控制,MVCC可以避免读写冲突,提高了系统的稳定性和可靠性。
- 支持一致性读取:MVCC保证了读取操作可以读取到一致性的数据快照,即使在并发写入的情况下也能保持一致性。