Spring 声明式事务应该怎么学?

news2025/5/11 10:43:12

1、引言

Spring 的声明式事务极大地方便了日常的事务相关代码编写,它的设计如此巧妙,以至于在使用中几乎感觉不到它的存在,只需要优雅地加一个 @Transactional 注解,一切就都顺理成章地完成了!

毫不夸张地讲,Spring 的声明式事务实在是太好用了,以至于大多数人都忘记了编程式事务应该怎么写。

不过,越是你认为理所应当的事情,如果出了问题,就越难排查。不知道你和身边的小伙伴有没有遇到过 @Transactional 失效的场景,这不但是日常开发中常踩的坑,也是面试中的高频问题。

其实这些失效场景不用死记硬背,如果搞明白了它的工作原理,再结合源码,需要用到的时候 Debug 一下就能自己分析出来。毕竟,源码才是最好的说明书。

还是那句话,授人以鱼不如授人以渔,课代表就算总结 100 种失效场景,也不一定能覆盖到你可能踩到的坑。所以本文中,课代表将结合几个常见失效情况,从源码层面解释其失效原因。认真读完本文,相信你会对声明式事务有更深刻的认识。

文中所有代码已上传至课代表的 github,为了方便快速部署并运行,示例代码采用了内存数据库H2,不需要额外部署数据库环境。

2、回顾手写事务

数据库层面的事务,有 ACID 四个特性,他们共同保证了数据库中数据的准确性。事务的原理并不是本文的重点,我们只需要知道样例中用的 H2 数据库完全实现了对事务的支持(read committed)。

编写 Java 代码时,我们使用 JDBC 接口与数据库交互,完成事务的相关指令,伪代码如下:

//获取用于和数据库交互的连接
Connection conn = DriverManager.getConnection();
try {
    // 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条SQL语句:
    insert(); 
    update(); 
    delete();
    // 提交事务:
    conn.commit();
} catch (SQLException e) {
    // 如果出现异常,回滚事务:
    conn.rollback();
} finally {
    //释放资源
    conn.close();
}

这是典型的编程式事务代码流程:开始前先关闭自动提交,因为默认情况下,自动提交是开启的,每条语句都会开启新事务,执行完毕后自动提交。

关闭事务的自动提交,是为了让多个 SQL 语句在同一个事务中。代码正常运行,就提交事务,出现异常,就整体回滚,以此保证多条 SQL 语句的整体性。

除了事务提交,数据库还支持保存点的概念,在一个物理事务中,可以设置多个保存点,方便回滚到指定保存点(其类似玩单机游戏时的存档,你可以在角色挂掉后随时回到上次的存档)设置和回滚到保存点的代码如下:

//设置保存点    
Savepoint savepoint = connection.setSavepoint();
//回滚到指定的保存点
connection.rollback(savepoint);
//回滚到保存点后按需提交/回滚前面的事务
conn.commit();//conn.rollback();

Spring 声明式事务所做的工作,就是围绕着 提交/回滚 事务,设置/回滚到保存点 这两对命令进行的。为了让我们尽可能地少写代码,Spring 定义了几种传播属性将事务做了进一步的抽象。注意哦,Spring 的事务传播(Propagation) 只是 Spring 定义的一层抽象而已,和数据库没啥关系,不要和数据库的事务隔离级别混淆。

3、Spring 的事务传播(Transaction Propagation)

观察传统事务代码:

 conn.setAutoCommit(false);
    // 执行多条SQL语句:
    insert(); 
    update(); 
    delete();
    // 提交事务:
    conn.commit();

这段代码表达的是三个 SQL 语句在同一个事务里。

他们可能是同一个类中的不同方法,也可能是不同类中的不同方法。如何来表达诸如事务方法加入别的事务、新建自己的事务、嵌套事务等等概念呢?这就要靠 Spring 的事务传播机制了。

事务传播(Transaction Propagation)就是字面意思:事务的传播/传递 方式。

在 Spring 源码的TransactionDefinition接口中,定义了 7 种传播属性,官网对其中的 3 个做了说明,我们只要搞懂了这 3 个,剩下的 4 个就是举一反三的事了。

1)PROPAGATION_REQUIRED

字面意思:传播-必须

PROPAGATION_REQUIRED是其默认传播属性,强制开启事务,如果之前的方法已经开启了事务,则加入前一个事务,二者在物理上属于同一个事务。

一图胜千言,下图表示它俩物理上是在同一个事务内:

图片

上图翻译成伪代码是这样的:

try {
    conn.setAutoCommit(false);
    transactionalMethod1(); 
    transactionalMethod2();
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
} finally {
    conn.close();
}

既然在同一个物理事务中,那如果transactionalMethod2()发生了异常,导致需要回滚,那么请问transactionalMethod1()是否也要回滚呢?

得益于上面的图解和伪代码,我们可以很容易地得出答案,transactionalMethod1()肯定回滚了。

这里抛一个问题:

事务方法里面的异常被 try catch 吃了,事务还能回滚吗?

先别着急出结论, 看下面两段代码示例。

示例一:不会回滚的情况(事务失效)

观察下面的代码,methodThrowsException()什么也没干,就抛了个异常,调用方将其抛出的异常try catch 住了,该场景下是不会触发回滚的

@Transactional(rollbackFor = Exception.class)
public void tryCatchRollBackFail(String name) {
    jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {
        methodThrowsException();
    } catch (RollBackException e) {
        //do nothing
    }
}

public void methodThrowsException() throws RollBackException {
    throw new RollBackException(ROLL_BACK_MESSAGE);
}


示例二:会回滚的情况(事务生效)

再看这个例子,同样是 try catch 了异常,结果却截然相反

@Transactional(rollbackFor = Throwable.class)
public void tryCatchRollBackSuccess(String name, String anotherName) {
    jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {
        // 带事务,抛异常回滚
        userService.insertWithTxThrowException(anotherName);
    } catch (RollBackException e) {
        // do nothing
    }
}

@Transactional(rollbackFor = Throwable.class)
public void insertWithTxThrowException(String name) throws RollBackException {
    jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    throw new RollBackException(ROLL_BACK_MESSAGE);
}

本例中,两个方法的事务都没有设置propagation属性,默认都是PROPAGATION_REQUIRED。即前者开启事务,后者加入前面开启的事务,二者同属于一个物理事务。insertWithTxThrowException()方法抛出异常,将事务标记为回滚。既然大家是在一条船上,那么后者打翻了船,前者肯定也不能幸免。

所以tryCatchRollBackSuccess()所执行的SQL也必将回滚,执行此用例可以查看结果

访问 http://localhost:8080/h2-console/ ,连接信息如下:

图片

点击Connect进入控制台即可查看表中数据:

图片

USER 表确实没有插入数据,证明了我们的结论,并且可以看到日志报错:

Transaction rolled back because it has been marked as rollback-only事务已经回滚,因为它被标记为必须回滚。

也就是后面方法触发的事务回滚,让前面方法的插入也回滚了。

看到这里,你应该能把默认的传播类型PROPAGATION_REQUIRED理解透彻了,本例中是因两个方法在同一个物理事务下,相互影响从而回滚。

你可能会问,那我如果想让前后两个开启了事务的方法互不影响该怎么办呢?

这就要用到下面要说的传播类型了。

2)、PROPAGATION_REQUIRES_NEW

字面意思:传播- 必须-新的

PROPAGATION_REQUIRES_NEWPROPAGATION_REQUIRED不同的是,其总是开启独立的事务,不会参与到已存在的事务中,这就保证了两个事务的状态相互独立,互不影响,不会因为一方的回滚而干扰到另一方。

一图胜千言,下图表示他俩物理上不在同一个事务内:

图片

上图翻译成伪代码是这样的:

//Transaction1
try {
    conn.setAutoCommit(false);
    transactionalMethod1(); 
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
} finally {
    conn.close();
}
//Transaction2
try {
    conn.setAutoCommit(false); 
    transactionalMethod2();
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
} finally {
    conn.close();
}

TransactionalMethod1 开启新事务,当他调用同样需要事务的TransactionalMethod2时,由于后者的传播属性设置了PROPAGATION_REQUIRES_NEW,所以挂起前面的事务(至于如何挂起,后面我们会从源码中窥见),并开启一个物理上独立于前者的新事务,这样二者的事务回滚就不会相互干扰了。

还是前面的例子,只需要把insertWithTxThrowException()方法的事务传播属性设置为Propagation.REQUIRES_NEW就可以互不影响了:

@Transactional(rollbackFor = Throwable.class)
public void tryCatchRollBackSuccess(String name, String anotherName) {
    jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {
        // 带事务,抛异常回滚
        userService.insertWithTxThrowException(anotherName);
    } catch (RollBackException e) {
        // do nothing
    }
}

@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRES_NEW)
public void insertWithTxThrowException(String name) throws RollBackException {
    jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    throw new RollBackException(ROLL_BACK_MESSAGE);
}

PROPAGATION_REQUIREDPropagation.REQUIRES_NEW已经足以应对大部分应用场景了,这也是开发中常用的事务传播类型。前者要求基于同一个物理事务,要回滚一起回滚,后者是大家使用独立事务互不干涉。还有一个场景就是:外部方法和内部方法共享一个事务,但是内部事务的回滚不影响外部事务,外部事务的回滚可以影响内部事务。这就是嵌套这种传播类型的使用场景。

3)、PROPAGATION_NESTED

字面意思:传播-嵌套

PROPAGATION_NESTED可以在一个已存在的物理事务上设置多个供回滚使用的保存点。这种部分回滚可以让内部事务在其自己的作用域内回滚,与此同时,外部事务可以在某些操作回滚后继续执行。其底层实现就是数据库的savepoint

这种传播机制比前面两种都要灵活,看下面的代码:

@Transactional(rollbackFor = Throwable.class)
public void invokeNestedTx(String name,String otherName) {
    jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    try {
        userService.insertWithTxNested(otherName);
    } catch (RollBackException e) {
        // do nothing
    }
    // 如果这里抛出异常,将导致两个方法都回滚
    // throw new RollBackException(ROLL_BACK_MESSAGE);
}

@Transactional(rollbackFor = Throwable.class,propagation = Propagation.NESTED)
public void insertWithTxNested(String name) throws RollBackException {
    jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
    throw new RollBackException(ROLL_BACK_MESSAGE);
}

外部事务方法invokeNestedTx()开启事务,内部事务方法insertWithTxNested标记为嵌套事务,内部事务的回滚通过保存点完成,不会影响外部事务。而外部方法的回滚,则会连带内部方法一块回滚。

小结:本小节介绍了 3 种常见的Spring 声明式事务传播属性,结合样例代码,相信你也对其有所了解了,接下来我们从源码层面看一看,Spring 是如何帮我们简化事务样板代码,解放生产力的。

4、源码窥探

在阅读源码前,先分析一个问题:我要给一个方法添加事务,需要做哪些工作呢?

就算我们自己手写,也至少得需要这么四步:

  • 开启事务

  • 执行方法

  • 遇到异常就回滚事务

  • 正常执行后提交事务

这不就是典型的AOP嘛~

没错,Spring 就是通过 AOP,将我们的事务方法增强,从而完成了事务的相关操作。下面给出几个关键类及其关键方法的源码走读。

既然是 AOP 那肯定要给事务写一个切面来做这个事,这个类就是 TransactionAspectSupport,从命名可以看出,这就是“事务切面支持类”,他的主要工作就是实现事务的执行流程,其主要实现方法为invokeWithinTransaction

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {
    
 // 省略代码...
    // Standard transaction demarcation with getTransaction and commit/rollback calls.
 // 1、开启事务
    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
    try {
  // This is an around advice: Invoke the next interceptor in the chain.
  // This will normally result in a target object being invoked.
  //2、执行方法
  retVal = invocation.proceedWithInvocation();
 }
 catch (Throwable ex) {
  // target invocation exception
  // 3、捕获异常时的处理
  completeTransactionAfterThrowing(txInfo, ex);
  throw ex;
 }
 finally {
  cleanupTransactionInfo(txInfo);
 }

 if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
  // Set rollback-only in case of Vavr failure matching our rollback rules...
  TransactionStatus status = txInfo.getTransactionStatus();
  if (status != null && txAttr != null) {
   retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
  }
 }
 //4、执行成功,提交事务
 commitTransactionAfterReturning(txInfo);
 return retVal;
 // 省略代码...

结合课代表增加的这四步注释,相信你很容易就能看明白。

搞懂了事务的主要流程,它的传播机制又是怎么实现的呢?这就要看AbstractPlatformTransactionManager这个类了,从命名就能看出, 它负责事务管理,其中的handleExistingTransaction方法实现了事务传播逻辑,这里挑PROPAGATION_REQUIRES_NEW的实现跟一下代码:

private TransactionStatus handleExistingTransaction(
   TransactionDefinition definition, Object transaction, boolean debugEnabled)
   throws TransactionException {
  // 省略代码...
  if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
   if (debugEnabled) {
    logger.debug("Suspending current transaction, creating new transaction with name [" +
      definition.getName() + "]");
   }
   // 事务挂起
   SuspendedResourcesHolder suspendedResources = suspend(transaction);
   try {
    return startTransaction(definition, transaction, debugEnabled, suspendedResources);
   }
   catch (RuntimeException | Error beginEx) {
    resumeAfterBeginException(transaction, suspendedResources, beginEx);
    throw beginEx;
   }
  }
     // 省略代码...
 }

前文我们知道PROPAGATION_REQUIRES_NEW会将前一个事务挂起,并开启独立的新事务,而数据库是不支持事务的挂起的,Spring 是如何实现这一特性的呢?

通过源码可以看到,这里调用了返回值为SuspendedResourcesHoldersuspend(transaction)方法,它的实际逻辑由其内部的doSuspend(transaction)抽象方法实现。这里我们使用的是JDBC连接数据库,自然要选择DataSourceTransactionManager这个子类去查看其实现,代码如下:

protected Object doSuspend(Object transaction) {
  DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
  txObject.setConnectionHolder(null);
  return TransactionSynchronizationManager.unbindResource(obtainDataSource());
 }

这里是把已有事务的connection解除,并返回被挂起的资源。在接下来开启事务时,会将该挂起资源一并传入,这样当内层事务执行完成后,可以继续执行外层被挂起的事务。

那么,什么时候来继续执行被挂起的事务呢?

事务的流程,虽然是由TransactionAspectSupport实现的,但是真正的提交,回滚,是由AbstractPlatformTransactionManager来完成,在其processCommit(DefaultTransactionStatus status)方法最后的finally块中,执行了cleanupAfterCompletion(status):

private void cleanupAfterCompletion(DefaultTransactionStatus status) {
  status.setCompleted();
  if (status.isNewSynchronization()) {
   TransactionSynchronizationManager.clear();
  }
  if (status.isNewTransaction()) {
   doCleanupAfterCompletion(status.getTransaction());
  }
   // 有挂起事务则获取挂起的资源,继续执行
  if (status.getSuspendedResources() != null) {
   if (status.isDebug()) {
    logger.debug("Resuming suspended transaction after completion of inner transaction");
   }
   Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
           
   resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
  }
 }

这里判断有挂起的资源将会恢复执行,至此完成挂起和恢复事务的逻辑。

对于其他事务传播属性的实现,感兴趣的同学使用课代表的样例工程,打断点自己去跟一下源码。限于篇幅,这里只给出了大概处理流程,源码里有大量细节,需要同学们自己去体验,有了上文介绍的主逻辑框架基础,跟踪源码查看其他实现应该不怎么费劲了。

5、常见失效场景

很多人(包括课代表本人)一开始使用声明式事务,都会觉得这玩意儿真坑,使用起来那么多条条框框,一不小心就不生效了。为什么会有这种感觉呢?

爬了多次坑之后,课代表总结了两条经验:

  1. 没看官方文档

  2. 不会读源码

下面简单列举几个失效场景:

1)非 public 方法不生效

官网有说明:

Method visibility and @Transactional

When you use transactional proxies with Spring’s standard configuration, you should apply the @Transactional annotation only to methods with public visibility.

2)Spring 不支持 redis 集群中的事务

redis事务开启命令是multi,但是 Spring Data Redis 不支持 redis 集群中的 multi 命令,如果使用了声明式事务,将会报错:MULTI is currently not supported in cluster mode.

3)多数据源情况下需要为每个数据源配置TransactionManager,并指定transactionManager参数

第四部分源码窥探中已经看到实际执行事务操作的是AbstractPlatformTransactionManager,其为TransactionManager的实现类,每个事务的connection连接都受其管理,如果没有配置,无法完成事务操作。单数据源的情况下正常运行,是因为 SpringBoot 的DataSourceTransactionManagerAutoConfiguration为我们自动配置了。

4)rollbackFor 设置错误

默认情况下只回滚非受检异常(也就是,java.lang.RuntimeException的子类)和java.lang.Error,如果明确知道抛异常就要回滚,建议设置为@Transactional(rollbackFor = Throwable.class)

5)AOP不生效问题

其他诸如 MyISAM 不支持,es 不支持等等就不一一列举了。

如果感兴趣,以上这些在源码中都能找到解答。

6、结束语

关于 Spring 的声明式事务,如果想用好,还真得多 Debug 几遍源码,由于 Spring 的源码细节过于丰富,实在不适合全部贴到文章里,建议自己去跟一下源码。熟悉之后就不怕再遇到失效情况了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2319674.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从 0 到 1 掌握鸿蒙 AudioRenderer 音频渲染:我的自学笔记与踩坑实录(API 14)

最近我在研究 HarmonyOS 音频开发。在音视频领域&#xff0c;鸿蒙的 AudioKit 框架提供了 AVPlayer 和 AudioRenderer 两种方案。AVPlayer 适合快速实现播放功能&#xff0c;而 AudioRenderer 允许更底层的音频处理&#xff0c;适合定制化需求。本文将以一个开发者的自学视角&a…

支持多系统多协议且可提速的下载工具

在网络下载需求日益多样的当下&#xff0c;一款好用的下载器能极大提升效率。今天就给大家介绍 AB Download Manager&#xff0c;它免费又开源&#xff0c;能适配 Windows 和 Linux 系统&#xff0c;带来超便捷的下载体验。 AB Download Manager 采用先进的多线程技术&#xf…

如何在 HTML 中创建一个有序列表和无序列表,它们的语义有何不同?

大白话如何在 HTML 中创建一个有序列表和无序列表&#xff0c;它们的语义有何不同&#xff1f; 1. HTML 中有序列表和无序列表的基本概念 在 HTML 里&#xff0c;列表是一种用来组织信息的方式。有序列表就是带有编号的列表&#xff0c;它可以让内容按照一定的顺序呈现&#…

【武汉·4月11日】Parasoft联合光庭信息研讨会|邀您共探AI赋能新机遇

Parasoft联合光庭信息Workshop邀您共探AI赋能新机遇 AI浪潮已至&#xff0c;你准备好了吗&#xff1f; 在智能网联汽车飞速发展的今天&#xff0c;AI技术正以前所未有的速度重塑行业生态。如何把握AI机遇&#xff0c;赋能企业创新&#xff1f; 4月11日&#xff0c;自动化软件…

闻所闻尽:穿透声音的寂静,照见生命的本真

在《楞严经》的梵音缭绕中&#xff0c;"闻所闻尽"四个字如晨钟暮鼓&#xff0c;叩击着每个修行者的心门。这个源自观世音菩萨耳根圆通法门的核心概念&#xff0c;既是佛门修行的次第指引&#xff0c;更蕴含着东方哲学对生命本质的终极叩问。当我们穿越时空的帷幕&…

VLAN综合实验报告

一、实验拓扑 网络拓扑结构包括三台交换机&#xff08;LSW1、LSW2、LSW3&#xff09;、一台路由器&#xff08;AR1&#xff09;以及六台PC&#xff08;PC1-PC6&#xff09;。交换机之间通过Trunk链路相连&#xff0c;交换机与PC、路由器通过Access或Hybrid链路连接。 二、实验…

Midjourney使用教程—2.作品修改

当您已生成第一张Midjourney图像的时候&#xff0c;接下来该做什么&#xff1f;了解我们用于修改图像的工具&#xff01;使用 Midjourney 制作图像后&#xff0c;您的创意之旅就不会止步于此。您可以使用各种工具来修改和增强图像。 一、放大操作 Midjourney每次会根据提示词…

3.5 平滑滤波

请注意:笔记内容片面粗浅&#xff0c;请读者批判着阅读&#xff01; 一、引言 平滑空间滤波是数字图像处理中用于降低噪声和模糊细节的核心技术&#xff0c;常用于图像预处理或特定场景下的视觉效果优化。其核心思想是通过邻域像素的加权平均或统计操作&#xff0c;抑制高频噪…

Sympy入门之微积分基本运算

Sympy是一个专注于符号数学计算的数学工具&#xff0c;使得用户可以轻松地进行复杂的符号运算&#xff0c;如求解方程、求导数、积分、级数展开、矩阵运算等。本文&#xff0c;我们将详细讲解Sympy在微积分运算中的应用。 获取方式 pip install -i https://mirrors.tuna.tsin…

Qemu-STM32(十):STM32F103开篇

简介 本系列博客主要描述了STM32F103的qemu模拟器实现&#xff0c;进行该项目的原因有两点: 作者在高铁上&#xff0c;想在STM32F103上验证一个软件框架时&#xff0c;如果此时掏出开发板&#xff0c;然后接一堆的线&#xff0c;旁边的人估计会投来异样的目光&#xff0c;特别…

在 ABAP 开发工具 (ADT-ABAP Development Tools) 中创建ABAP 项目

第一步&#xff1a;安装 SAP NetWeaver 的 ABAP 开发工具 (ADT) 开发工具下载地址&#xff1a;https://tools.hana.ondemand.com/#abap 也可以在SAP Development Tools下载工具页面直接跳转到对应公开课教程页面&#xff0c;按课程步骤下载eclipse解压安装即可&#xff0c;过程…

【架构】单体架构 vs 微服务架构:如何选择最适合你的技术方案?

文章目录 ⭐前言⭐一、架构设计的本质差异&#x1f31f;1、代码与数据结构的对比&#x1f31f;2、技术栈的灵活性 ⭐二、开发与维护的成本博弈&#x1f31f;1、开发效率的阶段性差异&#x1f31f;2、维护成本的隐形陷阱 ⭐三、部署与扩展的实战策略&#x1f31f;1、部署模式的本…

【鸿蒙开发】Hi3861学习笔记- WIFI应用AP建立网络

00. 目录 文章目录 00. 目录01. LwIP简介02. AP模式简介03. API描述3.1 RegisterWifiEvent3.2 UnRegisterWifiEvent3.3 GetStationList3.4 GetSignalLevel3.5 EnableHotspot3.6 DisableHotspot3.7 SetHotspotConfig3.8 GetHotspotConfig3.9 IsHotspotActive 04. 硬件设计05. 模…

大模型的微调技术(高效微调原理篇)

背景 公司有需求做农业方向的大模型应用以及Agent助手&#xff0c;那么适配农业数据就非常重要。但众所周知&#xff0c;大模型的全量微调对算力资源要求巨大&#xff0c;在现实的限制条件下基本“玩不起”&#xff0c;那么高效微调技术就非常必要。为了更好地对微调技术选型和…

区间震荡指标

区间震荡指标的逻辑如下&#xff1a; 一、函数注解 1. Summation函数 功能&#xff1a; 计算给定价格序列Price的前Length个数据点的和&#xff0c;或在数据点数量超过Length时&#xff0c;计算滚动窗口内的价格和。 参数&#xff1a; Price(1)&#xff1a;价格序列&#…

HCIE-SLAAC

文章目录 SLAAC &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Datacom专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2025年03月21日10点58分 SLAAC 帮助设备发现本地直连链路相连的设备&#xff0c;并获取与地址自动配置的相关前缀和其他…

JavaScript | 爬虫逆向 | 掌握基础 | 01

一、摘要 实践是最好的导师 二、环境配置 在开始之前&#xff0c;需要确保你的计算机上已经安装了 Node.js。Node.js 是一个开源的、跨平台的 JavaScript 运行时环境&#xff0c;它允许你在服务器端运行 JavaScript 代码。 1. 下载 安装地址&#xff1a;https://nodejs.org…

力扣算法Hot100——128. 最长连续序列

题目要求时间复杂度为O(n)&#xff0c;因此不能使用两次循环匹配。 首先使用 HashSet 去重&#xff0c;并且 HashSet 查找一个数的复杂度为O(1)外循环还是遍历set集合&#xff0c;里面一重循环需要添加判断&#xff0c;这样才不会达到O( n 2 n^2 n2)判断是否进入最长序列查找循…

深入解析 Java Stream API:从 List 到 Map 的优雅转换!!!

&#x1f680; 深入解析 Java Stream API&#xff1a;从 List 到 Map 的优雅转换 &#x1f527; 大家好&#xff01;&#x1f44b; 今天我们来聊聊 Java 8 中一个非常常见的操作&#xff1a;使用 Stream API 将 List 转换为 Map。&#x1f389; 具体来说&#xff0c;我们将深入…

当全球化成为商业常态,Shopify 如何为品牌生意铺平出海之路?

从独立站搭建到支付履约&#xff0c;从数据分析到生态整合&#xff0c;Shopify 为不同规模的企业提供可扩展的解决方案。 在数字化浪潮的推动下&#xff0c;跨境电商与品牌出海的黄金时代已然到来。然而&#xff0c;看似广阔的市场蓝海背后&#xff0c;是无数企业正在经历的“成…