文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

再深一点:面试工作两不误,源码级理解Spring事务

2024-12-24 18:05

关注

 

xjjdog在一些事务的基础上,再探讨几个容易淡忘的概念,从源码层面找原因,加深我们的理解,问题大概包括:

  1. Spring的事务和数据库的事务隔离是一个概念么?
  2. Spring是如何实现事务的?
  3. 事务隔离机制都有哪些?
  4. 事务传播机制都有哪些?
  5. 查询语句需要开事务么?
  6. private方法加事务注解有用么?

1、Spring的事务和数据库的事务隔离是一个概念么?

先来第一个问题,Spring的事务隔离级别和数据的事务隔离级别,是一回事么?

其实,数据库一般只有4种隔离机制,Spring抽象出一种default,根据数据设置来变动。

 

这是因为,Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现(如MySQL)。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。

在DataSourceUtils文件中,代码详细的输出了这个过程。

  1. // Apply specific isolation level, if any
  2. Integer previousIsolationLevel = null
  3. if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { 
  4.     if (logger.isDebugEnabled()) { 
  5.         logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + 
  6.                 definition.getIsolationLevel()); 
  7.     } 
  8.     int currentIsolation = con.getTransactionIsolation(); 
  9.     if (currentIsolation != definition.getIsolationLevel()) { 
  10.         previousIsolationLevel = currentIsolation; 
  11.         con.setTransactionIsolation(definition.getIsolationLevel()); 
  12.     } 

结论:三种情况,如果Spring没有指定事务隔离级别,则会采用数据库默认的事务隔离级别;当Spring指定了事务隔离级别,则会在代码里将事务隔离级别修改为指定值;当数据库不支持这种隔离级别,效果则以数据库的为准(比如采用了MyISAM引擎)。

我们会使用如下的方式进行声明。如果不是有性能和需求问题,就不要瞎改。事务处理弄不好是会锁表的,而锁表在大并发的情况下是会死人的。

  1. @Transactional(isolation = Isolation.READ_UNCOMMITTED) 

2、Spring事务的7种传播机制

只要写代码,代码总会存在嵌套,或者循环,造成了事务的嵌套或者循环。那么事务在这些情况下,根据配置会有不同的反应。

 

一般用得比较多的是REQUIRED , REQUIRES_NEW,用到其他的,你就要小心了,搞懂再用。

最怕如果这俩字了,它会将事情搞的很复杂,尤其是代码量大的时候,你永远不知道你写的service会被谁用到。这就很尴尬了。

我们会采用下面的方式进行声明。鉴于Spring的事务传播非常的绕,如果功能满足需求,那么就用默认的就好,否则会引起不必要的麻烦。

  1. @Transactional(propagation=Propagation.REQUIRED) 

3、事务传播机制是怎么实现的?

事务传播机制看似神奇,实际上是使用简单的ThreadLocal的机制实现的。所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。这不同于我们以前讲到的透传,Spring并没有做这样的处理。

所以事务传播机制,只有翻一遍源代码,才能印象深刻。仅靠文字去传播,很多东西会变得不可描述。

 

如图,PlatformTransactionManager只有简单的三个抽象接口,定义了包含JDBC在内的Spring所有的事务操作。

 

我们平常说的JDBC,只是占到其中一部分。

实现的方式,依然是使用AOP来实现的,具体的实现类是由TransactionAspectSupport来实现的。可以看到,代码定义了一个叫做transactionInfoHolder的ThreadLocal变量,当用到它的时候,就能够确保在同一个线程下,获取的变量是一致的。

  1.  
  2. private static final ThreadLocal transactionInfoHolder = 
  3.         new NamedThreadLocal<>("Current aspect-driven transaction"); 

具体的业务逻辑,是在invokeWithinTransaction中实现的。如果你继续向下跟踪的话,会找到AbstractPlatformTransactionManager类中的getTransaction方法。

  1. @Override 
  2. public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) 
  3.         throws TransactionException { 
  4.  
  5.  
  6.     // Use defaults if no transaction definition given. 
  7.     TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); 
  8.  
  9.  
  10.     Object transaction = doGetTransaction(); 
  11.     boolean debugEnabled = logger.isDebugEnabled(); 
  12.  
  13.  
  14.     if (isExistingTransaction(transaction)) { 
  15.         // Existing transaction found -> check propagation behavior to find out how to behave. 
  16.         return handleExistingTransaction(def, transaction, debugEnabled); 
  17.     } 
  18.  
  19.  
  20.     // Check definition settings for new transaction
  21.     if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { 
  22.         throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout()); 
  23.     } 
  24.  
  25.  
  26.     // No existing transaction found -> check propagation behavior to find out how to proceed. 
  27.     if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { 
  28.         throw new IllegalTransactionStateException( 
  29.                 "No existing transaction found for transaction marked with propagation 'mandatory'"); 
  30.     } 
  31.     else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || 
  32.             def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || 
  33.             def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { 
  34.         SuspendedResourcesHolder suspendedResources = suspend(null); 
  35.         if (debugEnabled) { 
  36.             logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def); 
  37.         } 
  38.         try { 
  39.             return startTransaction(def, transaction, debugEnabled, suspendedResources); 
  40.         } 
  41.         catch (RuntimeException | Error ex) { 
  42.             resume(null, suspendedResources); 
  43.             throw ex; 
  44.         } 
  45.     } 
  46.     else { 
  47.         // Create "empty" transactionno actual transaction, but potentially synchronization. 
  48.         if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { 
  49.             logger.warn("Custom isolation level specified but no actual transaction initiated; " + 
  50.                     "isolation level will effectively be ignored: " + def); 
  51.         } 
  52.         boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); 
  53.         return prepareTransactionStatus(def, nulltrue, newSynchronization, debugEnabled, null); 
  54.     } 

不用我做过多解释了吧,一切明显的逻辑,都在代码里。事务就是在这里创建的。

4、查询方法可以不开启事务么?

事务有个readonly,控制了事务的只读属性,和事务是否开启没半毛钱关系。

在以前的一篇文章中,谈到通过设置readonly属性来控制语句的路由:”MySQL官方驱动“主从分离的神秘面纱(扫盲篇),这其中就用到了事务的其中一个属性readonly,它最终是体现在数据库连接层面的。

  1. connection.setReadOnly(true); 

在Spring中的使用方式如下:

  1. @Transactional(readOnly=true

值得注意的是,这个属性设置之后,并不是每个底层的数据库都支持。中间层的ORM或者驱动,也可能会拿这个属性做一些文章,所以与其说这个readonly是功能性的,不如说是一种暗示。

拿MySQL来说,有两种提交模式:

这都是实打实的SQL语句,所以如果开启了事务,AUTOCOMMIT要为false。我们可以看到Spring做了以下几个操作。

  1. con.setAutoCommit(false); 

如果是只读事务,还不忘手动设置一下。

  1. if (isEnforceReadOnly() && definition.isReadOnly()) {    try (Statement stmt = con.createStatement()) { 
  2.         stmt.executeUpdate("SET TRANSACTION READ ONLY"); 
  3.     } 

这种操作是很昂贵的,如果不加Transaction注解,默认是不开启事务的。单条的查询语句也是没有必要开启事务的,数据库默认的配置就能满足需求。

但如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,就会造成数据的前后不一。

也仅有在这种情况下,要开启读事务。

5、private方法加事务注解有用么?

@Transaction注解加在private上,并没有什么卵用。

这倒不是事务处理的代码去写的特性。由于事务的这些功能,是通过AOP方式强加进去的,所以它收到动态代理的控制。

private和final修饰的方法,不会被代理。

但是,你却可以把private方法放在带有事务功能的public方法里。这样,它看起来也有了事务的一些功能特性,但它并没有。

End

互联网中,用到的事务并不多,很多都是非常小、速度非常快的接口,对于开发人员来说,事务是个累赘。

但在一些带有金融属性的业务中,或者一些企业级开发应用中,事务确实一个绕不过的坎。一旦深入其中,就会发现这个知识点,露着血盆大口,等君入瓮。

xjjdog从源码层次,聊到了几个面试常问的问题。不要觉得奇怪,有的人确实一直在拿着脏读、幻读这样的名词来面试。

而这些东西,都属于当时看了恍然大悟,第二天就继续懵逼的内容。

什么时候,才能务实一点呢?

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流

来源:小姐姐味道内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯