深入理解:脏读、不可重复读、幻读;事务隔离级别;Spring框架事务传播行为

发布时间:2024年01月14日

深入理解:脏读、不可重复读、幻读;事务隔离级别;Spring框架事务传播行为

一·什么是事务?

  1. 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。中途执行失败就会自动回滚到原本状态,事务由事务开始与事务结束之间执行的全部数据库操作组成。

二·数据库表中的数据记录也分提交版本的(行级、表级),类似git的版本控制

三·若没有事务隔离级别,多事务同时执行会出现什么问题?

  1. 因为多个事务同时操作相同数据库相同表时,可能出现一些数据不准确问题。而事务隔离级别则可以解决或者避免这些数据不准确问题。

  2. 举例说明:
    a事务正在对某部分数据进行操作期间,此时a事务还未结束,b事务就对相同部分数据进行了crud操作;此时a事务那边已经获取或者后续获取的数据,很可能就会跟实际数据库数据不一致,或者同个sql多次执行的结果不一致问题。

  3. 数据不准确问题,基本上都是读数据问题,增删改不存在这个问题。

四·事务隔离级别概述:

1.Read Uncommitted (读取未提交)

在这种最低级别的隔离下,一个事务可以读取到另一个事务尚未提交的数据变更。这意味着存在脏读、不可重复读、幻读问题:即事务读取到了其他事务还没有最终确认的数据。

2.Read Committed (读取已提交)

在这个级别,一个事务只能看到其他事务已经提交的数据。当事务开始时,它所能看到的数据是在那个时间点上所有已经提交的数据。这避免了脏读问题,但存在不可重复读、幻读问题,即在一个事务内,相同查询在不同时刻可能返回不同的结果,因为在此期间有其他事务提交了对数据的修改。

2-1 场景描述:若a事务正在更新user表前100条数据,a事务还未结束;b事务就开始查询user全表数据,b事务是Read Committed (读取已提交)事务隔离级别

1)a事务对这100条记录执行更新操作时,会对它们加排他锁(exclusive lock),阻止其他事务修改这些行。

(2)b事务开始查询user全表数据,由于是Read Committed隔离级别,它会看到那些已经提交的数据变更,对于a事务尚未提交的更改,则不会看到;因此,b事务能够读取到user表的所有数据,包括a事务正在更新但尚未提交的前100条数据的以前提交版本,该100条正在更新的版本数据是读取不到的

(3)在这个事务隔离级别下,b事务每次读取同一行数据时,都只能看到该行在读取时刻已经提交的最新版本,也就是说,如果在b事务进行查询的过程中,a事务提交了对某些行的更新,那么b事务在后续的查询中可能会看到不同的结果(即不可重复读问题)。但是,只要a事务未提交,b事务就能读取到这些行在a事务开始之前的状态。

注意:从Read Committed (读取已提交)开始,后续两个隔离级别也都是基于读取已提交

3.Repeatable Read (可重复读)

在这个级别,一旦事务开始,它在整个事务期间看到的数据视图是固定的,不会看到其他事务在这段时间内提交的任何更改,因此同一个事务内的多次读取操作会得到相同的结果,消除了脏读、不可重复读问题,但存在幻读问题。即同一事务在两次查询之间可能会看到新的行插入(或者满足条件的行数量发生变化),这些行是由其他事务在这两次查询之间提交的。

3-1 场景描述

1)假设有一个在线书店的库存表 BookStock,包含 ISBNQuantity 字段,表示每本书的库存数量。

(2)事务A开始:
用户小明开启了一个事务A,想要查看一本图书的库存量。
事务A执行第一条SELECT语句:SELECT Quantity FROM BookStock WHERE ISBN = '1234567890',此时查询结果显示这本书的库存为10本。

(3)事务B介入并修改数据:
在事务A未结束前,另一个事务B开始了,并且成功地将同本书减少了2本库存,然后事务B提交了这个更新操作。

(4)事务A再次查询:
在事务A中,小明决定再次确认这本书的库存,以确保购买时库存充足,于是他再次执行了与步骤1中相同的查询语句:SELECT Quantity FROM BookStock WHERE ISBN = '1234567890'。
在可重复读隔离级别下,即使事务B已经提交了减少库存的操作,但事务A仍然只会看到第一次查询时的库存量,也就是10本,而不是实际的8本。

(5)总结:这样,在整个事务A的生命周期内,针对同一本书的库存查询始终返回的是初始查询结果,这就实现了“可重复读”的特性。MySQLInnoDB存储引擎默认采用的就是可重复读隔离级别,并通过多版本并发控制(MVCC)来实现这一效果,尽管如此,需要注意的是在可重复读隔离级别下仍可能出现幻读现象(Phantom Reads),即新增满足查询条件的数据行。

4.Serializable (序列化)

这是最高的隔离级别,提供严格的事务隔离,相当于事务串行执行,从而避免了脏读、不可重复读以及幻读问题。为了达到这个级别,通常需要使用更为严格的锁策略,如范围锁或表级锁等,但这可能导致更高的锁竞争和更长的等待时间,从而影响并发性能

注意:实际应用中,不同的数据库系统可能会有不同的实现方式来达到上述的隔离级别,而且并非所有的数据库系统都完全支持所有级别的特性。例如,MySQL的InnoDB存储引擎通过多版本并发控制(MVCC)机制实现了Repeatable Read级别,并且可以通过Next-Key Locks来部分解决幻读问题;而Oracle数据库默认的事务隔离级别就是Read Committed。

五·多事务操作,读数据不准确问题,大致可以分为三类,:脏读、不可重复读、幻读

1.脏读:问题解析

脏读(Dirty Read)是数据库事务并发控制中的一种现象,指的是在一个事务内读取到了另一个未提交事务修改的数据,如果这个未提交事务最终选择回滚,那么之前读到的数据就是无效的、不一致的或者说是“脏”的。

1-1 注意重点:

1)一个事务内读取到另外多个未提交事务的数据
(2)最终引起第一个事务获取的数据跟数据库实际数据不一致

1-2 场景描述:

1)假设有一个银行转账表 BankTransaction,包含 FromAccount(转出账户)、ToAccount(转入账户)和 Amount(转账金额)字段。

(2)事务A开始并修改数据:
用户张三开启了一个事务A,准备从自己的账户向李四转账 $1000。
事务A执行UPDATE语句:UPDATE BankTransaction SET Amount = Amount + 1000 WHERE ToAccount = 'LiSi',但是此时事务A还没有提交。

(3)事务B介入并读取数据:
在事务A未提交的情况下,用户王五开启了事务B,并且查询了李四的账户余额,执行查询语句:SELECT SUM(Amount) FROM BankTransaction WHERE ToAccount = 'LiSi'。
由于隔离级别设置较低,事务B读取到了事务A尚未提交的转账操作,因此结果显示李四的账户多了 $1000。

(4)事务A回滚:
然后,事务A因为某种原因(比如网络中断或用户取消操作),选择了回滚,即撤销了转账操作。

(5)脏读结果:
这时,虽然事务A已经回滚,但事务B在之前看到的结果却是错误的——李四的账户实际上并没有增加 $1000,这就是一个典型的脏读场景。

1-3 解决办法:

在SQL标准中,通过将事务隔离级别设置为Read Committed及以上级别,可以避免脏读的现象发生。在这些较高的隔离级别下,一个事务只能读取到其他事务已经提交的数据。而在最低级别的Read Uncommitted(读取未提交)隔离级别下,就可能出现上述脏读问题。

2.不可重复读:问题解析

不可重复读(Non-repeatable Read)是事务并发控制中的一种现象,指的是在一个事务内,同一个查询语句在不同时刻执行两次或多次,却得到了不同的结果集。这是因为在该事务执行过程中,其他事务对数据进行了修改并提交

2-1 注意重点:

1)一个事务内读取到另外多个已提交事务的数据
(2)最终引起第一个事务内,同一个sql获取的字段值,前后不一致

2-2 场景描述:

1)事务A开始:
用户张三开启一个事务A,想要查看他的银行账户余额。
事务A执行第一条SELECT语句:SELECT Balance FROM BankAccount WHERE AccountId = 'ZhangSan',此时查询结果显示张三的账户余额为 $1000。

(2)事务B介入并修改数据:
在事务A未结束前,另一个事务B开始了,并且成功地从张三的账户转账 $500 到李四的账户,然后事务B提交了这个转账操作。

(3)事务A再次查询:
事务A还在进行中,同一用户张三决定再次确认一下自己的账户余额,于是执行了与步骤1中相同的查询语句:SELECT Balance FROM BankAccount WHERE AccountId = 'ZhangSan'。这次查询结果显示张三的账户余额为 500,而不是第一次查询时看到的1000。

(4)总结:这就是典型的“不可重复读”情况,即在同一事务内部,同样的查询被执行了两次,但由于其他事务的影响,导致前后两次查询的结果不同。不可重复读问题在数据库隔离级别为Read Committed或更低级别的情况下可能发生。在SQL标准中,通过将事务隔离级别设置为Repeatable Read(可重复读)或者Serializable(序列化),可以避免不可重复读的现象发生。对于MySQL InnoDB存储引擎,默认事务隔离级别就是Repeatable Read,在此级别下使用MVCC机制来防止不可重复读的发生。

2-3 解决办法:

在SQL标准中,通过将事务隔离级别设置为Repeatable Read(可重复读)或者Serializable(序列化),可以避免脏读、不可重复读的现象发生。对于MySQL InnoDB存储引擎,默认事务隔离级别就是Repeatable Read,在此级别下使用MVCC机制来防止不可重复读的发生。

3.幻读:问题解析

幻读(Phantom Read)是数据库事务并发控制中的一种现象,它与脏读不同,发生在同一事务内多次执行相同的查询语句时,尽管每次查询条件相同,但是由于其他事务提交了新的数据插入或删除操作,导致前后两次查询结果的记录数量不一致,即出现了“幻影”行。

3-1 注意重点:

1)一个事务内读取到另外多个已提交事务的数据
(2)最终引起第一个事务内,同一个sql查询结果的记录数量,前后不一致。
(3)注意不是字段值不一致,是结果集数量不一致,这是幻读跟不可重复读的区别点

3-2 场景描述:

1)假设有一个图书借阅系统中的图书表 Books,包含 BookIDTitle 字段。

(2)事务A开始并执行查询:
用户小明开启了一个事务A,并执行了一条查询语句来查看某类图书的数量:SELECT COUNT(*) FROM Books WHERE Category = 'Fiction',得到的结果为50本小说。

(3)事务B介入并插入数据:
在事务A未结束前,另一个事务B开始了,事务B成功地将一本新的小说添加到了Books表中,类别也为Fiction,然后事务B提交了这个插入操作。

(4)事务A再次执行相同的查询:
事务A仍在进行中,小明决定再次确认一下此类别图书的数量,执行了与步骤1中相同的查询:SELECT COUNT(*) FROM Books WHERE Category = 'Fiction'。这次查询结果显示的小说数量为51本,而不是第一次查询时看到的50本。

(5)这就是典型的幻读场景,即使在同一事务内使用相同的查询条件,由于其他事务提交的新插入操作,使得结果集在事务A看来似乎出现了“幻影”的行。

3-3 解决办法:

在SQL标准中,通过将事务隔离级别设置为Serializable(序列化),可以避免脏读、不可重复读、幻读的现象发生。

六·MySQL设置事务隔离级别方法:

sql语法格式:

SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL level;

其中 level 可以是以下值之一:

READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ (MySQL InnoDB引擎的默认隔离级别)
SERIALIZABLE

1.会话级别设置:

通过SQL命令设置当前会话(Session)的事务隔离级别。这种方式只对当前连接有效。

示例:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

2.全局级别设置:

要改变所有新建立的会话的默认隔离级别,可以使用 GLOBAL 关键字,但这通常需要具有管理员权限,并且重启服务器后该设置会失效。

示例:

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

3.配置文件设置:

若要永久性地设置整个MySQL服务的默认事务隔离级别,可以在MySQL的配置文件(如my.cnf或my.ini)中添加以下行:

[mysqld]
transaction-isolation = READ-COMMITTED

然后需要重启MySQL服务以使更改生效。

七·Spring框架事务传播行为概念:

(1) 数据库并没有传播行为这个概念,在使用Spring框架进行事务管理时,确实存在一个与之类似的术语——事务传播行为(Transaction Propagation)

(2) 事务传播行为是指在一个方法调用链中,当一个事务方法被另一个事务方法调用时,如何处理这两个方法之间的事务边界和事务一致性的问题

(3) 在Spring框架中,通过@Transactional注解的propagation属性可以指定被标注的事务方法,在被其他方法调用时,该如何开启事务操作

1.以下是Spring中常见的几种事务传播行为:

示例代码:

@Transactional(propagation = Propagation.REQUIRED)
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    System.out.println(1/0);
}

(1)REQUIRED (默认值)

如果当前没有事务,则新建一个事务。
如果当前存在事务,则加入到该事务中执行。
这种情况下的所有数据库操作都在同一事务中完成。

(2)REQUIRES_NEW

总是开启一个新的事务,如果当前存在事务,则将当前事务挂起(暂停)。
新事务与当前事务完全独立,即使外部事务回滚,新事务也会提交。
通常用于需要确保业务逻辑隔离性或者数据一致性的场景。

(3)SUPPORTS

如果当前存在事务,则在该事务内运行;
若当前不存在事务,则以非事务方式运行。
不主动创建事务,仅支持已有的事务上下文。

(4)MANDATORY

必须在一个已存在的事务中运行,否则抛出异常。
如果当前存在事务,则加入到该事务中执行;
若不存在事务,则抛出异常。

(5)NOT_SUPPORTED

完全不支持事务,如果当前存在事务,则把当前事务挂起。
执行时不使用任何事务上下文,即以非事务方式运行。

(6)NEVER

必须在非事务环境下执行,如果当前存在事务,则抛出异常。
禁止在事务上下文中运行。

(7)NESTED

如果当前存在事务,则在嵌套事务内执行。
嵌套事务可以独立于外层事务进行提交或回滚,而不会影响外层事务的状态,但最终还是受外层事务的约束,也就是说,如果外层事务回滚,那么嵌套事务所做的任何更改也将被回滚。

注意:每种传播行为都适用于特定的业务场景和并发控制需求,开发者应根据实际情况选择合适的事务传播行为,以保证数据的一致性和系统的正确运作。

2.两个携带事务注解方法互相调用,Spring事务执行流程:

示例:Spring框架中,一个@Transactional方法a调用另一个@Transactional方法b,假如a、b事务传播行为都是REQUIRES_NEW,a、b方法是如何根据传播行为,进行协调事务执行

1)方法a开始执行:
当调用方法a时,由于其传播行为设置为REQUIRES_NEWSpring会首先检查当前是否有事务。即使在方法a之前存在一个事务(假设为Transaction T0),Spring也会暂时挂起这个外部事务,并开启一个新的、与T0独立的事务(Transaction T1)。

(2)方法a调用方法b:
在方法a内部调用带有同样传播行为REQUIRES_NEW的方法b。
即使方法a本身正处于Transaction T1中,Spring仍然会创建一个新的、与T1完全独立的事务(Transaction T2)来运行方法b。

(3)方法b执行其事务:
方法b在其新创建的事务T2内执行所有数据库操作。如果方法b内部出现异常并且未被捕获,则仅T2会被回滚,而不会影响到方法a的事务T1或更外层的任何事务。

(4)方法b结束并提交或回滚事务:
如果方法b正常完成(没有未捕获异常),则T2会被提交,它的更改对其他事务立即可见。无论方法b的结果如何,方法a所在的事务T1仍保持活跃状态。

(5)方法a继续执行及事务处理:
不管方法b的结果如何,方法a在事务T1下继续执行其剩余逻辑。如果方法a后续也正常完成,则T1将被提交;如果发生未捕获的非检查型异常,T1将会回滚,而不影响已经提交了的T2。

(6)恢复外层事务(如果有):
方法a的事务T1提交或回滚后,若存在先前挂起的外层事务T0,那么T0将被恢复并根据自身情况决定是否继续执行或者回滚。

总结:在这种情况下,每个方法都将在自己的全新事务中执行,即使它们互相嵌套调用。这种方式确保了各个方法的事务隔离性,且各自的操作结果互不影响,除非通过共享数据等方式显式关联。

3.数据库本身是不支持事务嵌套执行的,Spring框架也是先后开启多个独立的事务,分别依照顺序执行提交或者回滚操作,给人感觉就像事务嵌套一样。

4.Spring多层事务回滚机制,底层其实是利用数据库的保存点机制实现

文章来源:https://blog.csdn.net/weixin_48033662/article/details/135583836
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。