前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >看完此文,再也不怕面试官考你数据库事务方面的问题了!

看完此文,再也不怕面试官考你数据库事务方面的问题了!

作者头像
用户3587585
发布2022-09-21 07:06:02
3660
发布2022-09-21 07:06:02
举报
文章被收录于专栏:阿福谈Web编程阿福谈Web编程

0 引言

最近参加各种面试,5次面试里至少有3次被面试官问到有关数据库事务方面的问题,尤其以事务的隔离级别和传播行为的问题问得比较多。因为笔者之前对这有关数据库事务方面涉及事务的隔离级别和事务的传播行为理解的不是很深入,因此回答面试官时停留在一知半解的地步。痛定思痛后,我决定写下一篇系统总结数据库事务方面的博客,不仅对自己未来的面试会有很大帮助,希望对从事Java技术栈开发且有想法在金三银四跳槽的小伙伴们会有帮助!下面开启正文模式

1 事务的概念及其四大特征

1.1 事务的概念

我们把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称之为一个事务。

举个粟子:如银行转账事务,需要同时保证转账人账户扣款和被转账人收款要不一起成功,要么一起失败,绝不会出现扣款成功而收款失败或者扣款失败而收款成功的情况。

1.2 事务的四大特征

数据库事务具有以下四个特征,简称ACID

  • Atomic(原子性):事务中包含的操作被看成是一个整体的业务单元,这个业务单元中的操作要么全部成功,要么全部失败,不会出现部分成功、部分失败的场景。
  • Consistency(一致性):事务在完成时必须使所有的数据都保持一致状态,在数据库中所有的修改都是基于事务,保证了数据的完整性。
  • Isolation(隔离性):在互联网实际应用场景中可能存在多个应用线程同时访问同一数据,这样数据库中同样的数据就会在各个不同的事务中被访问,也就会产生丢失更新。为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过选择不同的事务隔离级别可以在不同程度上压制丢失更新的发生(因为在互联网应用中常常需要面对高并发的场景,所以隔离性是程序员需要重点掌握的内容)。
  • Durability(持久性):事务结束后,所有数据都会固化到一个地方,如保存到磁盘当中,及时断电重启也可以提供给应用程序访问。

2 事务的隔离级别

为了压制丢失更新,数据库标准提出了4种隔离级别,分别在不同程度上压制丢失更新。它们分别是未提交读读已提交可重复度串行化。事务的隔离级别可以说是面试官高频问到求职者的问题了,笔者应聘高级Java开发的面试几乎5次面试中就有3次被面试官问到。

2.1 未提交读

未提交读(read uncommited)是最低级的隔离级别,它允许一个事务读取另外一个事务还没有提交的数据。未提交读是一个危险的隔离级别,所以我们一般在实际应的开发中使用不多。但是它的优点在于并发能力高,适合那些对数据一致性没有严格要求 的场景。其最大的缺点是会出现脏读,脏读就是一个事务读取到了另一个未提交事务修改过的数据。

让我们看看可能发生脏读的场景

表 1 未提交读产生脏读场景

脏读一般是比较危险的隔离级别 在我们实际应用中采用得不多 为了克服脏读的问题,数据 隔离级别还提供了读写提交(read commited )的级别

2.2 读已提交

读已提交read committed)是指一个事务只能读取另外 个事务已经提交的数据, 不能读取未提交的数据。表1中的场景在限制读写提交后就变成了表2中的场景了

表 2 读已提交克服脏读场景

在T3时刻,由于采取了读已提交的隔离级别,因此事务2不能读取到事务1未提交的库存1,所以扣减库存后的结果仍然为1,然后它提交事务,库存在T4时刻就变成了1。T5时刻,事务1回滚,最后结果库存为1,这是一个正确的结果。但是读已提交也会产生不可重复读的场景

表 3 不可重复读场景

在T3时刻数据库读取库存的时候,因为事务1未提交事务,所以读出的库存为1,于是事务2认为当前可减库存;在T4时刻,事务1已提交事务,所以在T5时刻,它扣减库存的时候就发现库存为0,于是无法扣减库存。这种前后不一致的读取结果现象称为不可重复读。这就是读已提交的一个不足。为了克服这个不足,数据库的隔离级别提出了可重复读的隔离级别,它能够消除不可重复读的问题。

2.3 可重复读

可重读读REPEATABLE READ)是指一个事务第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据,这就是可重复读。表4为克服不可重复读场景

表 4 克服不可重复读场景

可以看到事务2在T3时刻尝试读取库存,但此时这个库存已经被事务1事先读取,所以这个时候数据库就阻塞它的读取,直至事务1提交,事务2才能读取到库存的值。此时已经是T5时刻,而读到的值为0,这时就已经无法扣减了,显然在读写提交中出现的不可重读读的场景被消除了。但这样也会引发新的问题出现,那就是幻读。假设商品交易正在进行,而后台有人也在进行查询分析和打印业务,我们看看表5可能发生的场景

表 5 幻读场景

这便是幻读。首先这里的交易笔数不是数据库存储的值,而是一个统计值,商品库存才是数据库存储的值,这一点需要注意。幻读不是针对一条数据库记录而言,而是针对多条记录,例如这51笔交易记录就是多条数据库记录统计出来的;而可重复读时针对数据库中单一一条记录,例如商品的库存是以数据库里的一条记录存储的,它可以产生可重复读,但不能产生幻读。

2.4 串行化

串行化SERIALIZABLE)是数据库最高的隔离级别,它会要求数据库所有的SQL都按顺序执行,对同数据库中一条记录的操作都是串行的。所以它能保证数据的一致性,不会出现脏读幻读等现象。

2.5 四种隔离级别的总结

  • 未提交读READ UNCOMMITTED)隔离级别下可能发生脏读、不可重复读和幻读等问题
  • 读已提交READ COMMITTED)隔离级别下可能发生不可重复读幻读等问题,但不会发生脏读问题
  • 可重复读REPEATABLE READ)隔离级别下可能发生幻读问题,但不会发生脏读不可重复读问题
  • 串行化SERIALIZABLE)隔离级别下各种问题都不会发生,它能保证数据的一致性。

2.6 使用合理的隔离级别

作为一名互联网开发人员在开发高并发业务时需要时刻记住隔离级别可能发生的各种场景和现象。数据库的隔离级别是数据库事务的核心内容之一,也是互联网企业重点关注的内容之一。追求更高的隔离级别它能更好的保证数据的一致性,但是也要付出锁的代价。有了锁也就意味着性能的丢失,而且隔离级别越高性能越是直线下降。

所以我们在考虑隔离级别时不单单是考虑数据的一致性问题,还要考虑系统的性能问题。例如一个高并发抢票场景,如果采用串行化隔离级别,能够有效避免数据的不一致性,但这样也会使得并发的各个线程挂起,因为只有一个线程可以操作数据,这样就会导致大量的线程挂起和恢复,导致系统缓慢。而后续的用户要等到系统的响应就要等待很长时间,最终因为响应缓慢而影响了用户的忠诚度。

所以在大部分的实际应用中选择的隔离级别会以读已提交为主,它能防止脏读,但不可避免不可重复读幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用关系型数据库而使用其他手段,例如使用非关系型数据库Redis作为数据载体。

对于隔离级别,不同的数据库对其的支持也是不一样的。例如Oracle数据库只支持读已提交串行化两种隔离级别,而Mysql数据库则对4种事务的隔离级别都支持。对于Oralce默认的隔离级别是读已提交READ COMMITED),而Mysql则是可重复读REPEATABLE READ),这些需要根据具体的数据库来决定。

3 事务的传播行为

传播行为是方法之间调用事务采取的策略问题,绝大多数情况下我们会认为数据库事务要么全部成功,要么全部失败。但现实中也会有特殊的情况。例如执行一个批量程序,它会处理很多的交易,绝大部分交易可以顺利完成的,但是也有极少数的交易因为发生异常而不能完成。这时不能因为极少数的交易不能完成而回滚批量任务调用的其他交易,而使得那些本能完成的交易也 不能完成。此时我们真实的需求是在一个批量任务执行的过程中调用多个交易时,如果有一些交易发生异常,只需回滚那些出现异常的交易,而不是整个批量任务。这样就能使得那些没有问题的交易顺利完成。

图 1 事务的传播行为

Spring中,当一个方法调用另外一个方法时可以事务采用不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。这样讲还是有点抽象,我们回到图1中,图中批量任务我们称之为当前方法,那么批量事务就成为当前事务,当它调用单个交易时称单个交易为子方法。当前方法调用子方法时,让每个子方法不在当前事务中执行,而是创建一个新的事务去执行子方法。我们就说当前方法调用子方法的传播行为为新建事务。此外还可以让子方法在无事务、独立事务中执行,这些完全取决于业务需求。

传播行为的定义

Spring事务中对数据库存在7中传播行为,它是通过枚举类Propagation来定义的,下面来研究它的源码:

代码语言:javascript
复制
package org.springframework.transaction.annotation;
/**** imports **/
public enum Propagation {
    /**
    *需要事务,它是默认的传播行为
    *如果当前事务存在就沿用当前事务;否则新建一个事务运行子方法
    */
    REQUIRED(0),
    /**
    *支持事务;如果当前事务存在就继续沿用当前事务
    *如果不存在事务,则继续沿用无事务的方式运行子方法
    */
    SUPPORTS(1),
    /**
    *必须使用事务,如果当前没有事务则会抛出异常
    *如果存在当前事务,则沿用当前事务
    */
    MANDATORY(2),
    /**
    *无论当前事务是否存在,都会创建新事物运行子方法
    *这样新事务就可以拥有新的锁和隔离级别,与当前事务互相独立
    */
    REQUIRES_NEW(3),
    /**
    *不支持事务,当前存在事务时将挂起事务,运行方法
    */
    NOT_SUPPORTED(4),
    /**
    *不支持事务,如果当前方法存在事务则抛出异常;
    *否则继续沿用无事务机制运行子方法
    */
    NEVER(5),
    /**
    *当前方法调用子方法时,如果子方法发生异常
    *只回滚子方法调用过的SQL,不回滚当前方法的事务
    */
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

以上传播行为一共有7种,但是常用的只有REQUIRED,REQUIRES_NEW 和 NESTED三种。NESTED 播行为和 QUIRES_NEW 还是有区别的, NESTED 播行为会沿用当前事务的隔 离级别和锁等特性,而 REQUIRES_NEW 则可以拥有自己独立的隔离级别和锁等特性。

4 Spring 声明式事务的使用

4.1 Spring 声明式数据库事务约定

为了擦除令人厌烦的try...catch...finally语句,减少那些数据库连接关闭和事务回滚提交的代码,Spring 利用AOP为我们提供了一个数据库事务的约定流程。通过这个约定流程可以减少大量的冗余代码和没必要的try...catch...finally语句,让开发者能够更加集中于业务的开发,而不是数据库连接资源和数据库事务。这样开发的代码可读性更高,也更好维护。

对于事务,需要使用标注告诉Spring在什么地方启用数据库事务功能。对于声明式事务是使用@Transactional注解进行标注的。这个注解可以标注在类或者方法上,当它标注在类上时,代表这个类中的所有公共(public)非静态的方法都将启用事务功能。

@Transactional中还可以配置许多属性,如事务的隔离级别和传播行为;又如异常类型,从而确定方法发生什么异常时回滚事务,发生什么异常时无需回滚事务。这些配置内容是在Spring IOC在加载时就会将这些配置信息解析出来,然后将这些信息存到事务定义器(TransactionDefination接口定义的实现类)类,并记录哪些类或者方法需要启动事务功能,采用什么策略去执行事务。在这个过程中我们需要做的只是给需要事务的类或者方法标注@Transactional注解和配置其属性而已,并不是很复杂。

有了@Transactional注解,Spring就会知道在哪里启用事务机制,其约定流程如下图所示:

图 2 Spring 数据库事务约定

4.2 Transactional 的 配置项

数据库事务属性都可以通过@Transactional注解来配置,下面来探讨下它的源码:

代码语言:javascript
复制
package org.springframework.transaction.annotation;
/****imports****/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
   //制定事务管理器
   @AliasFor("transactionManager")
   String value() default "";
   //同value属性
   @AliasFor("value")
   String transactionManager() default "";
   //指定传播行为,默认为REQUIRED,需要事务
   Propagation propagation() default Propagation.REQUIRED;
   //指定隔离级别,若使用Oracle数据库默认为读已提交;若Mysql数据库默认为可重复读
   Isolation isolation() default Isolation.DEFAULT;
   //超时时间,单位秒;-1为不限制
   int timeout() default -1;
   //是否为只读事务
   boolean readOnly() default false;
   //方法在发生指定异常时回滚,默认是所有异常都回滚
   Class<? extends Throwable>[] rollbackFor() default {};
   //方法在发生指定异常名称时回滚,默认是所有异常都回滚
   String[] rollbackForClassName() default {};
   //方法在发生指定异常时不回滚,默认是所有异常都回滚
   Class<? extends Throwable>[] noRollbackFor() default {};
   //方法在发生指定异常名称时不回滚,默认是所有异常都回滚
   String[] noRollbackForClassName() default {};
}

关于注解@Transactional 值得注 的是它可以放在接口 ,也可以放在实现类上。Spring团队推荐放在实现类上,因为放在接口上将使得你的类基于接口的代理时它才生效。我们知道在 Spring可以使用 JDK 动态代理,也可以使用 CGLIG 动态代理。如果使用接口, 那么你将不能切换为 CGLIB动态代理,而 只能 许你使用JDK动态代理,并且使用对应的接口去代理你的类 ,这样才能驱动这个注解这将大大地限制你的使用,因此在实现类上使用@Transactiona 注解才是最佳的方式。

5 参考资料

【1】杨开振著《深入浅出SpringBoot2.x》第6章 "聊聊数据库事务处理"

【2】图灵学院语雀课堂笔记之Mysql事务及锁原理(https://www.yuque.com/books/share/9f4576fb-9aa9-4965-abf3-b3a36433faa6/ywaa1q)

推荐阅读

[1] 深入分析Synchronized原理(阿里面试题)

[2] BAT大厂面试官必问的HashMap相关面试题及部分源码分析

[3] 能让你Hold住面试官的Mysql 数据页结构及索引底层原理总结(文末附新春红包福利)

声明:码字不易,虽然不是100%原创,但是也是花了心思整理和编辑的,喜欢的朋好请点个再看并转发给需要的朋友,谢谢!

--END--

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 阿福谈Web编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 4.2 Transactional 的 配置项
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档