Spring声明式数据库事务的使用-隔离级别

上面我们只是简单的使用事务,这里将讨论Spring事务机制中最重要的两个配置项,即隔离级别与传播行为。毫无疑问本节内容是本章的核心内容,也是互联网企业最关注的内容之一,因此他十分重要,值得花费大的篇幅去讨论。我们从这两个配置项的大概含义谈起
首先是隔离级别,因为互联网时刻存在着高并发的环境,如商品库存,时刻都是多个线程共享的数据,这样就会在多线程的环境中扣减商品库存。对于数据库而言,时刻都是多个事务同时访问同一记录的情况,这样引起数据出现不一致的情况,便是数据库的丢失更新问题(Lose update)问题。应该说,隔离级别是数据库的概念,有些难度,所以在使用它之前应该先了解数据库的相关知识

1. 数据库事务的知识

数据库事务具有一下四个特征:
- Atomic(原子性):事务中包含的操作被看作一个整体的单元,这个业务单元中的操作要么完全成功,要么完全失败。不会出现部分成功,部分失败的场景
- Consistency(一致性):事务在完成时,必须使所有的数据都保持一致的状态,在数据库中所有的修改都基于事务,保证了数据的完整性。
- Isolation(隔离性):一个事务中的操作语句所作的修改必须与其他事务所作的修改相互隔离。在进行事务查看书就时数据所处的状态,要么是被另一个并发事务修改之前的状态,要么是被另一个事务修改之后的状态,即当前事务不会查询由另一个并发事务正在修改的数据。这种特性通过锁机制实现。这是我们讨论的核心内容。正如上述,可能多个应用程序线程同时访问同一个数据,这样数据库同样的数据就会在各个不同的事务中被访问,这样就会产生丢失更新。为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过他的选择,可以在不同程度上压制丢失更新的产生。因为互联网的应用常常面临高并发的场景,所以隔离性是需要掌握的重点内容
- Durability(持久性):事务结束后所有的数据都会固化到一个地方,如保存在磁盘中,即断电重启之后也可以提供给应用程序使用。
这四个特性除了隔离性都还是比较好理解的,所以这里才会更加深入的讨论隔离性。再多个事务同时操作数据的情况下,会引发丢失更新的场景。例如,电商有一种商品,在疯狂抢购中,会出现多个事务同时访问商品库存的场景,这样就会产生丢失更新。易班而言,存在两种类型的丢失更新。让我们了解他们。下面假设一种商品库存数量还有100,每次抢购都只能抢购1件商品,那么在抢购中就可能出现如下这种场景。
在这里插入图片描述
可以看到,T5时刻事务1回滚,导致原本库存99的变成了100,显然事务2的结果就丢失了,这就是一个错误的值。对于这样一个事务回滚另一个事务提交而引发的数据不一致的情况,我们称之为第一类丢失更新。
撤销一个事务的时候,会把其他事务已经提交的更新数据覆盖掉。这是完全没有事务隔离级别造成的。如果事务2被提交,事务1被撤销,那么连同事务2的更新也会被撤销
然而他却已经没有讨论价值了,因为目前大部分的数据库已经克服了第一类丢失更新的问题,也就是现在数据库系统不会出现上表的情况了,所以对于这样的场景不再深入讨论,而是讨论第二类丢失更新,也就是多个事务都提交的场景。
如图,如果多个事务并发提交会出现怎样的场景呢?
在这里插入图片描述

注意T5时刻提交的事务。因为在事务1中,无法感知事务2的操作,这样他就不知道事务2已经修改过数据,因为他依旧人为只是发生了一笔业务,所以库存变成了99,而这个结果又是一个错误的结果。这是T5时刻事务1提交的事务,就会引发事务2提交结果的丢失,我们把这样的多个事务都提交引发的丢失gentian称之为第二类丢失更新。这就是我们互联网系统重点关注的内容。为了克服这些问题,数据库提出了事务之间的隔离级别的概念,这就是本章的重点内容之一。

2.详解隔离级别

上面我们讨论了第二类丢失更新。为了压制丢失更新,数据库标准提出了4类隔离级别,在不同程度上压制丢失更新,这四类隔离级别是未提交读,读写提交,可重复读和串行化,他们会在不同程度上压制丢失更新的情景。

  1. 未提交读
    未提交读(read uncommited)是最低隔离级别,其含义是允许一个事务读取另一个事务没有提交的数据。未提交读,是一种危险的隔离级别,所以一般在我们实际的开发中应用不广,但是他的优点在于并发能力高,适合那些对数据一致性没有要求而是追求高并发的场景,他最大的坏处是出现脏读。让我们看看出现脏读的场景
    在这里插入图片描述
    T3时刻,因为采用未提交读,所以事务2可以读取事务1未提交的库存数据为1,这里当他扣减库存后则数据为0,然后他提交了数据,库存就变成了0,而事务1在T5时刻回滚事务,因为第一类丢失已经克服,所以他不会将库存回滚到2,那么最后的结果就变成了0,这样就出现了错误。
    脏读一般是比较危险的隔离级别,在我们实际应用中采用的不多。为了克服脏读事件,数据库隔离级别还提供了读写提交的级别,下面我们将讨论他。

  2. 读写提交
    读写提交(read commited)是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。例如,上图中的场景限制为读写提交之后,就变成下图中的场景。
    在这里插入图片描述
    在T3时刻,由于采用了读写提交的隔离级别,因此事务2不能读取事务1中未提交的库存1,所以扣减库存的结果依旧为1,然后他提交事务,则库存在T4时刻就变成了1。T5时刻,事务1回滚,因为第一类丢失更新已经克服,所以最后结果库存为1,这是正确的结果。但是读写提交也会产生下面的问题。如图场景
    在这里插入图片描述
    在T3时刻事务2读取库存的时候,因为事务1为提交事务,所以读出的库存为1,于是事务2人为当前可间扣库存,在T4时刻,事务1已经提交了事务,所以在T5时刻,他见扣库存的时候就会发现库存为0,于是就无法间扣库存了。这样的问题在于事务2之前人为可以间扣,而到了间扣的那一步却发现已经不可以间扣,于是库存对与事务2而言是一个可变化的值,这个现象我们称为不可重复读,这就是读写提交的一个不足。为了克服这个不足,数据库的隔离级别还提出了可重读读的隔离级别,他将能够消除不可重复读的问题。

  3. 可重读读
    可重复读的目标是克服读写提交中出现的不可重复读的现象,因此在读写提交的时候,可能出现一些值得变化,影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出了可重复读的隔离级别。这样就可以克服不可重复读的现象。
    在这里插入图片描述
    可以看到,事务2在T3时刻尝试读取库存,但是此时这个库存已经被事务1事先读取了,所以这个时候数据库就阻塞他的读取了,直到事务1提交,事务2才能读取库存的值。此时已经是T5时刻了,这时候已经无法间扣了,显然在读写提交中出现不可重复读的场景被擦除了,但是这样也引发了新的问题出现,就是幻读。假设商品交易正在进行中,而后台有人也在查询分析和打印的业务,让我们看看会发生下图所示的场景
    在这里插入图片描述
    这便是幻读的现象,可重复读和幻读,是读者比较难以理解的内容。这里稍微论述一下。首先这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值,这一点是要注意的。也就是幻读不是针对一条数据库记录而言,而是多条记录,例如,这51笔交易数就是多条数据库记录统计出来的。而可重复读是针对数据库的单一条记录,例如,商品的库存是以数据库里面的一条记录存储的,它可以产生可重复读,而不会产生幻读。

  4. 串行化
    串行化(serializable)是数据库最高的隔离级别,他会要求所有的SQL都按照顺序执行,这样就可以克服上述隔离级别产生的各种问题,所以它可以保证一致性。

  5. 使用合理的隔离级别
    通过上面的描述,读者应该对隔离级别有了更多的认识,使用他能够在不同程度上压制丢失更新,于是总结成表
    在这里插入图片描述
    作为互联网开发人员,在高并发业务中需要时刻记住隔离级别可能发生的各种概念与相关现象,这是数据库事务的核心内容之一,也是互联网企业关注的核心内容之一。追求更高的隔离级别,它能够更好的保证数据的一致性,但是也需要付出锁的代价,有了锁,就意味着性能的丢失,而且隔离级别越高性能也就越是直线下降。所以我们在选择隔离级别的时候,要考虑的不单单是数据一致性的问题,还要考虑系统性能的问题。
    所以现实中一般而言,选择隔离级别会以读写提交为主,它能够预防脏读但是不能预防不可重复读和幻读。为了克服数据不一致的问题,程序开发者还设计了乐观锁,甚至不再使用数据库而是使用其他手段。例如使用Redis作为载体,这个我们会在后续的章节中提到。对于隔离级别,不同的数据库支持也是不一样的,MySQL数据库四种都支持,对于MySQL默认的隔离级别是可重复读,这些需要根据具体的数据库来决定。
    只要掌握了隔离级别的含义,使用隔离级别就简单了,只需要在@Transactional中配置对应即可,代码如下所示

//使用隔离级别

    @Override
    @Transactional(isolation = Isolation.SERIALIZABLE, timeout = 1)
    public int save(SysRole record) {
        return sysRoleMapper.insert(record);`在这里插入代码片`
    }

上述代码我们使用序列化的隔离级别来保证数据的一致性,这使得他将阻塞其他的事务进行并发,所以他只能运行在那些低并发而又需要保证数据一致性的场景下。对于高并下又要保证数据一致性的场景,则需要另外处理了。
当然有时候一个个的指定隔离级别会很不方便,因此Spring Boot可以通过配置文件指定默认的隔离级别。例如,当我们需要把隔离级别设置为读写提交时,可以在application.yml文件中加入默认的配置,如下代码

#隔离级别数字配置的定义
#-1数据库默认隔离级别(MySQL重复读)
#1  未提交读
#2  读写提交
#4  可重复读
#8  串行化
#druid数据库连接池默认隔离级别
spring:
  datasource: #数据源
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
    #druid数据源默认隔离级别
      default-transaction-isolation: 2
    tomcat:
    #tomcat数据源默认隔离级别
      default-transaction-isolation: 2

代码中配置了tomcat数据源的默认隔离级别,以及druid数据源的隔离级别,注释中已经说明了数字代表的隔离级别,相信读者也会有一个比较清晰的认识,这里配置2说明数据库的隔离级别默认为读写提交

 Changing isolation level of JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@19c466cb] to 2
发布了180 篇原创文章 · 获赞 114 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43404791/article/details/105514462
今日推荐