MySQL事务管理
- 3. 隔离性(Isolation)
- 查看和设置隔离级别
- 隔离级别作用域区别与解析
- 四种隔离级别解析
- 小结
- 4. 一致性(Consistency)
- 如何保持一致性
- 5.“保持原子性、隔离性、持久性就能保证一致性”的理解:
- 四、如何理解隔离性
- 1.数据库并发的场景有三种:
- 读-写
- MVCC(多版本并发控制)
- 理解undo log
- 模拟MVCC
- 快照
- 通过快照理解回滚(Rollback)
3. 隔离性(Isolation)
隔离性指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的隔离空间,一个事务的内部操作对其他事务都是不可见的。这样,事务之间就不可能产生干扰。MySQL中的事务隔离级别决定了事务之间的可见性和并发性能。
示例:考虑两个并发的转账事务,一个是账户A转账给账户B,另一个是账户C转账给账户D。由于事务的隔离性,这两个事务不会互相干扰。即使它们同时发生,一个事务也不会看到另一个事务的中间状态。例如,在账户A转账给账户B的过程中,即使账户B的余额已经增加(但事务尚未提交),账户C的事务也看不到这个更改。这样可以确保每个事务都在一个独立的环境中执行,从而避免了并发操作可能引起的数据不一致问题。
MySQL支持四种事务隔离级别: | |
---|---|
READ UNCOMMITTED(读取未提交) | |
READ COMMITTED(读取已提交) | |
REPEATABLE READ(可重复读) | |
SERIALIZABLE(可串行化) |
查看和设置隔离级别
在MySQL中,@@transaction_isolation
、@@session.transaction_isolation
和 @@global.transaction_isolation
(有时也被写作@@tx_isolation
、@@session.tx_isolation
和 @@global.tx_isolation
)是用于查看和设置事务隔离级别的系统变量。这些变量在MySQL中用于控制事务如何与其他事务进行隔离。
隔离级别作用域区别与解析
@@transaction_isolation
或@@tx_isolation
这个变量通常是@@session.transaction_isolation
的别名。当你不指定session
或global
时,它通常指的是当前会话(session)的事务隔离级别。也就是说,它会返回你当前会话的事务隔离级别设置。
例如,如果你在一个会话中执行SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
,然后执行SELECT @@transaction_isolation;
,你会得到与REPEATABLE READ
隔离级别相对应的值。
@@session.transaction_isolation
或@@session.tx_isolation
这个变量用于查看和设置当前会话的事务隔离级别。它只影响当前会话中的事务。如果你在一个会话中更改了这个值,它不会影响其他已经存在的会话或新创建的会话。
@@global.transaction_isolation
或@@global.tx_isolation
这个变量用于查看和设置全局(即服务器级别)的事务隔离级别。当你更改这个值时,它会影响所有新创建的会话。但是,已经存在的会话的事务隔离级别不会改变,除非它们明确地被设置为与全局设置不同。
但是设置了全局并未影响会话隔离级别
真的是这样吗?(原来是要重新启动服务后下一次生效)
这样就合理多了!
tips:理解全局隔离级别和会话隔离级别将其当成全局变量和局部变量一样理解会好记很多!
四种隔离级别解析
- READ UNCOMMITTED(读未提交)
- 定义:允许读取并发事务尚未提交的数据。
- 数据一致性:最低。由于可以读取到其他事务未提交的数据,所以可能会出现脏读(Dirty Read)、不可重复读(Non-repeatable Read)和幻读(Phantom Read)的问题。
- 并发性:最高。因为不需要对数据行进行加锁等待,所以可以提高并发性能。
- 应用场景:很少使用,因为它提供的数据一致性太低。
创建两个会话,设置其隔离级别为读未提交——如下图
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务就已经能够看到了。如下:
什么是脏读?怎么解决?
脏读(Dirty Read)
定义:一个事务读取了另一个尚未提交的事务的数据。如果另一个事务发生错误并执行了回滚操作,那么第一个事务读取到的数据就是脏数据。
示例:
-
事务A修改了一行数据。
-
事务B读取了事务A修改后的数据。
-
事务A由于某种原因发生错误并执行了回滚操作,撤销了之前的修改。
-
此时事务B读取到的数据就是脏数据,因为它读取的是从未真正提交过的数据。
-
READ COMMITTED(读已提交)
- 定义:对同一字段的多次读取结果都是一致的。但是,不同的事务之间可以读取到对方已经提交的数据。
- 数据一致性:中等。解决了脏读的问题,但可能会出现不可重复读和幻读。
- 并发性:较高。因为只需要等待其他事务提交,而不需要等待其完成。
- 应用场景:这是大多数数据库系统的默认隔离级别(但不是MySQL的默认级别)。它提供了较好的数据一致性和并发性之间的平衡。
创建两个会话,设置其隔离级别为读提交——如下图
两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下
与上述读未提交
相比明显可以看出,其他会话并不能查看到修改(提交后才可以)
一个事务在执行过程中,两个相同的select查询得到了不同的数据,这种现象叫做不可重复读。
- REPEATABLE READ(可重复读)
- 定义:对同一字段的多次读取结果都是一致的。在一个事务中,对同一字段的多次读取结果都是相同的。
- 数据一致性:较高。解决了脏读和不可重复读的问题,但可能会出现幻读(在InnoDB存储引擎中,通过多版本并发控制(MVCC)和Next-Key Locking策略,幻读也被解决了)。
- 并发性:中等。因为需要对数据行进行加锁等待,所以并发性能会有所降低。
- 应用场景:MySQL的InnoDB存储引擎的默认隔离级别。它提供了较高的数据一致性和相对较好的并发性能。
创建两个会话,设置其隔离级别为可重复读——如下图
同时启动事务后,左边终端进行数据更新,右端终端并不能查看到左端终端的修改(合理,这和读已提交一样)
左端终端结束事务,右端终端事务仍在继续,可以看到左端终端的提交的事务并不会让右端终端发生幻读(一个事务在执行过程中,相同的select查询得到了新的数据,如同出现了幻觉,这种现象叫做幻读。)
现象
只有当右端事务完成事务,才会更新修改数据
那两个终端同时启动事务,并对一条数据进行修改呢,因为是左端先执行,所以成功,而左端并没有结束提交事务,所以在右端同时对其进行修改的时候,并不能直接修改,而是被卡在修改数据这条指令上
长期的卡住会导致锁超时
我们提交后则可以修改
- SERIALIZABLE(可串行化)
- 定义:最高的隔离级别,也是完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就不可能产生干扰。
- 数据一致性:最高。解决了脏读、不可重复读和幻读的问题。
- 并发性:最低。因为事务需要排队执行,所以并发性能最差。
- 应用场景:当对数据一致性要求非常高,且可以接受较低并发性能的场景下使用。
创建两个会话,设置其隔离级别为可串行化——如下图
在两个终端各自启动一个事务,如果这两个事务都对表进行的是读操作,那么这两个事务可以并发执行,不会被阻塞。如下:
但如果这两个事务中有一个事务要对表进行写操作,那么这个事务就会立即被阻塞(这里就与可重复读有区别)。如下:
超时会被这条操作会被杀掉
两个事务同时修改,左边先一点,右边后一点,则会执行时间靠前的,时间靠后的error
事务提交后数据变得一致
tips:这里的锁有点类似于
悲观锁
,但是又有不同——
SERIALIZABLE(可串行化)中的锁和悲观锁在概念和应用上是有区别的。SERIALIZABLE是数据库事务的一个隔离级别,用于解决并发问题。当事务设置为SERIALIZABLE隔离级别时,所有的事务依次逐个执行,这样事务之间就不可能产生干扰。在这种隔离级别下,事务会对它所使用的资源(如行或表)进行加锁,以防止其他事务并发访问这些资源。但是,SERIALIZABLE隔离级别并不一定等同于悲观锁,因为具体的加锁策略可能因数据库管理系统的不同而有所差异。
悲观锁则是一种对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度的并发控制方式。它假设最坏的情况,即数据被并发修改的概率比较大,因此,在修改数据之前先加锁,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,如行锁、表锁等。
总结来说,SERIALIZABLE隔离级别和悲观锁都是为了解决并发问题而采用的策略,但它们在实现方式、应用场景和效果上有所不同。SERIALIZABLE隔离级别是一种全局性的策略,通过串行化事务的执行来避免并发问题;而悲观锁则是一种具体的加锁策略,它假设数据被并发修改的概率较大,因此在修改数据之前先加锁。
- 默认的隔离级别
在mysql中,InnoDB的默认隔离级别可以重启mysql服务查看——为可重复读
sudo systemctl restart mysqld
小结
tips:
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,在选择隔离级别时往往需要在两者之间找一个平衡点。
- 表中只写出了各种隔离级别下进行读操作时是否需要加锁,因为无论哪种隔离级别,只要需要进行写操作就一定需要加锁。
4. 一致性(Consistency)
事务(Transaction)是一个逻辑上独立的工作单位,它确保数据从一个一致性的状态转变到另一个一致性的状态。事务的一致性(Consistency)是事务ACID属性(Atomicity, Consistency, Isolation, Durability)中的一个关键部分。
一致性确保了数据库从一个一致性状态转变为另一个一致性状态。具体地说,它意味着:
- 数据的完整性:事务必须确保数据库中的数据和业务规则保持一致。例如,如果有一个规则规定一个账户的余额不能为负,那么任何导致余额为负的事务都必须被回滚(Rollback),以保持数据的一致性。
- 数据的完整性约束:数据库中的完整性约束(如主键约束、外键约束、唯一性约束等)必须在事务执行前后都得到满足。
- 数据的初始状态:如果事务由于某种原因(如系统崩溃)未能完成,那么数据库必须恢复到事务开始之前的状态,这确保了数据库的一致性。
如何保持一致性
- 预定义的业务规则:数据库和应用程序必须遵循预定义的业务规则来确保数据的一致性。这些规则可以是简单的(如确保余额不为负)或复杂的(如确保在多个表之间的数据关系保持一致)。
- 完整性约束:使用数据库的完整性约束(如主键、外键、唯一性约束等)可以帮助自动维护数据的一致性。
- 事务管理:DBMS使用事务管理来确保数据的一致性。如果事务中的某个操作失败,那么整个事务都会被回滚,数据库将恢复到事务开始之前的状态。
- 并发控制:并发事务可能导致数据不一致。DBMS使用各种并发控制机制(如锁、时间戳等)来防止这种情况的发生。
- 数据备份和恢复:定期备份数据库并在需要时恢复数据是确保数据一致性的重要手段。如果数据库因某种原因变得不一致,可以使用备份来恢复到一致的状态。
一致性指的是事务必须使数据库从一个一致性状态变换到另一个一致性状态。
也就是说,一个事务执行之前和执行之后都必须处于一致性状态。这种一致性状态是指数据库的完整性约束没有被破坏,数据的语义没有改变。
示例:考虑一个在线购物系统,用户在购买商品时需要扣除库存并更新用户的订单状态。这个操作涉及两个表:一个是商品库存表(products
),另一个是用户订单表(orders
)。在事务开始之前,商品库存表中的库存数量与用户订单表中的订单状态都是一致的。在事务执行过程中,我们需要先减少库存表中的库存数量,然后更新用户订单表中的订单状态为已购买。如果这两个操作都成功执行,那么事务结束后数据库的状态仍然是一致的。但是,如果其中任何一个操作失败(例如,由于网络问题或系统错误),那么我们需要回滚事务,以保持数据库的一致性。
5.“保持原子性、隔离性、持久性就能保证一致性”的理解:
- 这四个属性是事务的基本特性,它们共同确保了数据库系统的可靠性和稳定性。
- 原子性确保了事务要么全部完成,要么全部不完成,这为实现一致性提供了基础。
- 隔离性保证了事务之间不会互相干扰,从而确保了在并发环境下的一致性。
- 持久性确保了事务的结果在提交后是永久性的,这也有助于维护数据的一致性。
- 然而,仅仅保证原子性、隔离性和持久性并不足以完全确保一致性。还需要在数据库设计时定义正确的数据完整性约束和业务规则,并在事务中遵守这些规则和约束,以确保数据的一致性。
四、如何理解隔离性
经过上述实验,发现隔离性好像简而言之就是并发环境中,多个事务之间应该相互独立,一个事务的执行不应该影响其他事务。换句话说,当多个事务并发执行时,每个事务都应该感觉自己是在单独执行,不受其他事务的干扰。
但是数据库底层又是如何保证的呢?
1.数据库并发的场景有三种:
- 读-读 :不存在任何问题,也不需要并发控
- 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
读-写
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC
可以为数据库解决以下问题在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
理解 MVCC 需要知道三个前提知识:
- 3个记录隐藏字段
- undo 日志
- Read View
3个记录隐藏列字段 - DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在
undo log
中) - DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),
如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引
- 补充:
实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
示例
比如我们存在上述表,则上述表表达的意思为
我们目前并不知道创建该记录的事务ID,隐式主键,我们就默认设置成null,1。第一条记录也没有其他版本,我们设置回滚指针为null。
MVCC(多版本并发控制)
MVCC(Multi-Version Concurrency Control),即多版本并发控制,是一种并发控制的方法,主要用于数据库管理系统中实现对数据库的并发访问。
- 核心思想:MVCC允许在数据库系统中存在数据的多个版本,从而允许多个用户或事务同时访问数据库而不会产生冲突。通过为每个事务提供一个数据快照,每个事务都可以看到数据的一个一致性的视图,而不管其他事务正在进行的修改。
- 工作原理:
- 当一个事务启动时,系统会为其分配一个唯一的事务ID。
- 当事务要访问数据库中的某个数据时,系统会检查该数据的版本号和事务的启动时间。如果该数据的版本号早于该事务的启动时间,则该事务可以访问该数据;否则,该事务需要等待其他事务完成对该数据的访问。
- 当事务修改数据时,系统会为该数据创建一个新版本,并将修改后的数据存储在新的位置。同时,旧版本的数据仍然可用供其他事务访问。
- 当事务提交时,系统会将其所做的所有修改操作合并到数据库中,并删除旧版本的数据(或标记为可删除)。
- 优点:
- 提高并发访问数据库的效率:由于每个事务都访问数据的独立版本,因此多个事务可以并发执行而不会相互阻塞。
- 减少数据冲突和不一致性的发生:由于每个事务都看到数据的一个一致性的快照,因此不会出现脏读、不可重复读或幻读等问题。
- 实现方式:不同的数据库系统可能采用不同的方式来实现MVCC。例如,在MySQL的InnoDB存储引擎中,MVCC是通过为每个数据行添加额外的隐藏字段(如创建时间和删除时间戳)来实现的。这些字段用于跟踪行的版本和状态。
总的来说,MVCC是一种用于提高数据库并发性能和减少数据冲突的技术。它允许在并发环境中存在数据的多个版本,并通过为每个事务提供数据快照来确保事务之间的一致性和隔离性。
理解undo log
Undo log(撤销日志)是数据库管理系统中的一种重要机制,特别是在MySQL等关系型数据库中。以下是关于Undo log的详细解释:
- 定义与作用:
- Undo log,顾名思义,是一种用于撤销或回退的日志。其主要作用是记录事务回滚前的数据状态,以便在需要时能够进行数据恢复,保证数据的一致性和完整性。当事务进行修改操作时(如插入、更新或删除),数据库系统会先将修改前的数据状态记录到Undo log中。如果事务需要回滚或者系统发生故障需要恢复数据,就可以利用Undo log中的信息来撤销之前的操作或恢复到某个一致的状态。
- 组成部分:
- Undo log通常包含以下几个关键部分:事务ID(用于标识进行修改的事务)、操作类型(如插入、更新、删除等)、被修改的数据内容以及操作发生的时间戳。
- 存储位置:
- Undo log存在于一个特殊的段中,这个段被称为Undo log segment(撤销日志段),它通常位于数据库系统的表空间中。具体来说,在MySQL中,Undo log默认存放在共享表空间中,但也可以通过配置来指定其存放位置。每行数据通常都会有一个指向其对应Undo log的指针,以便在需要时能够快速定位并恢复数据。
总的来说,Undo log是数据库管理系统中确保数据一致性和完整性的重要机制之一。它通过记录事务修改前的数据状态,为数据恢复和事务回滚提供了可能。
简而言之:
undo log -> 回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性。
模拟MVCC
现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成name(李四)。
- 事务10,因为要修改,所以要先给该记录加行锁。
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)
- 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 ‘李四’。并且修改原始记录的隐藏字段
DB_TRX_ID
为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针DB_ROLL_PTR
列,里面写入undo log
中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。 - 事务10提交,释放锁。
tips: 此时,最新的记录是’李四‘那条记录。
现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)。
- 事务11,因为也要修改,所以要先给该记录加行锁。(该记录是那条?)
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们
采用头插方式,插入undo log。 - 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
- 事务11提交,释放锁。
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。
上面的一个一个版本,我们可以称之为一个一个的快照。
快照
在MVCC(Multi-Version Concurrency Control,多版本并发控制)中,快照是指数据库中数据的一个特定时间点的视图。这个快照版本是在事务开始时创建的,用于为事务提供所需的一致性视图。
具体来说,当一个事务开始时,数据库系统会根据该事务开始的时间点为该事务创建一个快照版本。通过使用这个快照版本,MVCC提供了每个事务的一致性视图,使得事务能够在并发执行时保持隔离性,避免了读取脏数据或互相覆盖的问题。每个事务都能够看到在其开始之前已经提交的数据版本,从而保证了数据的一致性和事务的隔离性。
在MVCC中,数据的多版本是通过隐藏列或元数据来跟踪的。这些隐藏列或元数据包含了数据的版本信息,例如创建时间戳、删除时间戳等。通过这些信息,数据库系统可以确定哪些数据版本对于当前事务是可见的。
需要注意的是,不同数据库系统的MVCC实现可能会有所不同,但其核心原理和目标都是类似的。通过创建快照版本和基于快照版本的并发控制,MVCC提供了一种高效且并发安全的方法来处理数据库事务的并发访问。
通过快照理解回滚(Rollback)
- 快照的定义:快照是关于指定数据集合的一个完全可用拷贝,这个拷贝包含了数据在某个时间点(拷贝开始的时间点)的映像。它可以是数据的副本或复制品。
- 快照的作用:快照的主要作用之一是进行在线数据备份与恢复。当存储设备发生应用故障或者文件损坏时,可以使用快照来进行快速的数据恢复,将数据恢复到快照创建时的状态。
- 回滚的定义:回滚是指程序或数据处理错误时,将程序或数据恢复到上一次正确状态的行为。在数据库系统中,回滚通常用于撤销事务中的更改,将数据库恢复到事务开始之前的状态。
- 快照与回滚的关系:通过快照,我们可以保存数据库在某个时间点的状态。如果后续发现数据出现了错误或者需要进行回滚操作,我们可以选择将数据库回滚到之前某个快照的状态,从而撤销在快照之后所做的更改。
具体来说,假设我们有一个数据库系统,并且定期为该数据库创建快照。在某个时间点,我们执行了一个事务,但由于某种原因,该事务导致了数据错误。此时,我们可以选择将数据库回滚到该事务执行之前的某个快照的状态,从而撤销该事务所做的更改,恢复数据的正确性。
因此,通过快照,我们可以为数据库提供多个时间点的备份状态。当需要进行回滚操作时,我们可以选择将数据库回滚到这些备份状态之一,从而恢复数据的正确性。