资讯详情

资讯详情

没写 @Transactional,事务就不生效?从一次索引重建看懂事务边界

没写 Transactional事务就不生效从一次索引重建看懂事务边界方法里有提交和回滚不代表整个业务流程都能一起回滚。判断事务是否有效需要沿调用链找到真正的提交边界。读 PaiCLI 的代码索引模块时我注意到一个细节insertChunks()的注释写着“事务保护”方法上却没有Transactional。如果习惯了 Spring Boot很容易先去找事务注解。但这个项目通过 JDBC 直接操作 SQLite事务控制就写在方法内部。继续往调用处看更值得追问的事情出现了清理旧索引、插入新代码块、插入代码关系是不是一个整体如果最后一步失败前面的操作还能恢复吗一、先看项目怎么操作数据库PaiCLI 的VectorStore直接建立 JDBC 连接相关语句如下this.connectionDriverManager.getConnection(jdbc:sqlite:dbPath);它维护两张表code_chunks存代码块及其向量code_relations存代码之间的关系。在当前的CodeIndex.index()中持久化部分依次调用以下方法。下面节选调用顺序省略统计和日志try(VectorStorestorenewVectorStore(root.toString())){store.clearProject();store.insertChunks(entries);store.insertRelations(allRelations);// 后续统计和结果处理省略}从业务上看这是一次重建先清掉当前项目的旧数据再写入新数据。但同一个try块、同一个对象甚至同一个连接都不能单独证明这三步属于同一个事务。二、没有注解事务控制在哪里insertChunks()的关键结构如下。这是源码节选省略了 SQL 定义及部分参数绑定不是可独立运行的完整方法booleanautoCommitconnection.getAutoCommit();connection.setAutoCommit(false);try(PreparedStatementpsconnection.prepareStatement(sql)){for(CodeChunkEntryentry:entries){ps.setString(1,projectPath);// 中间参数绑定省略ps.setString(6,embeddingToJson(entry.embedding));ps.addBatch();}ps.executeBatch();connection.commit();}catch(SQLExceptione){connection.rollback();throwe;}finally{connection.setAutoCommit(autoCommit);}这里已经包含了 JDBC 手动事务的主要动作关闭自动提交、执行 SQL、成功时提交、捕获指定异常时回滚最后恢复连接状态。setAutoCommit(false)的含义是关闭自动提交数据库实际何时建立事务还与数据库和驱动有关不必把它机械等同于立即执行了一条BEGIN。commit()和rollback()作用于连接上的当前事务不是按 Java 方法名划分范围。Java 17 Connection 文档所以没写Transactional并不意味着没有事务。这个项目选择由代码直接控制数据库事务。还有一个容易混淆的点addBatch()用于积累批处理executeBatch()用于提交执行批处理本身不提供整批原子性的保证。驱动在部分命令失败后可能继续执行剩余命令也可能停止不能把一次批处理调用理解成一次网络发送或一个天然的事务。Java 17 Statement 文档三、每个方法有事务为什么重建仍可能只完成一半先看clearProject()。它顺序执行两条删除ps1.executeUpdate();// 删除当前项目的代码块ps2.executeUpdate();// 删除当前项目的代码关系这个方法没有自行关闭自动提交。沿当前调用链新连接保持默认自动提交模式因此两条删除分别提交连“清理旧索引”这一步都没有被统一包成一个事务。随后insertChunks()和insertRelations()分别关闭自动提交并各自执行commit()。正常路径下可以这样理解这些提交边界删除旧代码块 → 提交 删除旧代码关系 → 提交 插入新代码块 → 提交 插入新代码关系 → 提交假设旧数据已经删除并提交插入新代码块时遇到 SQL 异常即使该批插入成功回滚旧数据也不会随之恢复。再假设代码块已经插入并提交关系插入失败并回滚可能留下“新代码块已存在关系没有写入”的状态。这些都是按当前代码推导的失败场景不是一次已经复现的数据丢失事故。局部事务能回滚的是它当前尚未提交的工作它不能撤销前面已经提交的事务。图前三步已分别提交时后续回滚无法撤销它们统一事务方案在回滚成功时可撤销本次数据库修改。下半部分为改造示意非故障实测。四、为什么不能只在外面再套一层事务看到这里一个直觉方案是在外层关闭自动提交然后继续调用原来的三个方法最后统一提交。问题在于内部insertChunks()仍然会执行connection.commit();如果使用同一个连接它提交的是这个连接上当前事务中的全部工作可能连前面的删除一起提交。后续插入关系失败外层再回滚也无法撤销已经提交的部分。因此改造的关键是明确事务的拥有者。下面是职责划分示意其中的...SqlOnly方法需要新实现并非项目已有 APIrebuildIndex唯一负责事务边界 关闭自动提交 执行 clearProjectSqlOnly 执行 insertChunksSqlOnly 执行 insertRelationsSqlOnly 全部成功提交 提交前发生失败尝试回滚向上传递错误底层 SQL 方法必须使用同一个受控连接并且不再自行提交、回滚或切换自动提交模式。如果它们需要独立调用可以另外提供拥有事务边界的入口。落地时还要明确这个入口是独占一条新连接还是允许加入调用者已有的事务两种契约不能含混。对不支持事务嵌套的简单实现可以拒绝接收已经处于手动事务模式的连接而不是擅自提交调用者的工作。恢复连接状态之前也必须处理好事务结束和回滚失败。把自动提交从false改为true可能提交当前事务不能把finally中的状态恢复当成回滚的替代品。Connection.setAutoCommit 文档五、换成 Spring就只需要一个注解吗在 Spring 应用中声明式事务可以帮助管理边界。比如下面是一段独立的设计示意并非 PaiCLI 现有实现Transactional(rollbackForException.class)publicvoidrebuildIndex(){repository.clearProject();repository.insertChunks();repository.insertRelations();}这段示意成立需要事务管理已配置、调用经过事务拦截并且底层操作实际参与同一个事务。本文讨论的是 Spring 的org.springframework.transaction.annotation.Transactional以及默认的代理模式。这个模式下调用必须经过代理同一对象内的自调用不会触发被调用方法的新事务拦截。自调用也不意味着既有事务消失要看外层是否已经建立事务。Spring 事务注解文档底层连接同样重要。原项目直接通过DriverManager.getConnection()建立连接不能指望给外层加个注解就把这条独立连接自动纳入 Spring 本地事务。使用JdbcTemplate等事务感知的数据访问方式并匹配事务管理器及数据源才能让连接参与预期的事务。Spring 数据库连接管理文档还要看异常规则。未额外修改默认配置时Spring 默认对RuntimeException和Error回滚对受检异常并不默认回滚SQLException就是受检异常。rollbackFor Exception.class可以扩展规则但它也不能自动识别被业务代码吞掉的异常。Spring 回滚规则文档如果代码捕获异常后只返回失败标志事务拦截器可能看到的是一次正常返回。是否回滚还要看有没有其他机制将事务标记为仅回滚。六、源码里还有一个值得留意的异常分支回到最开始的insertChunks()它捕获的是SQLException。但循环中的embeddingToJson()在序列化失败时会抛出RuntimeException。这条异常路径不会进入catch (SQLException e)却仍然会进入finally。所以这个节选不适合不加说明地当成“覆盖所有失败的通用事务模板”。具体后果取决于异常发生时是否已有数据库修改等条件不能直接断言一定出现部分提交。实际实现至少需要考虑 SQL 异常、运行时异常以及回滚或清理再次失败时如何保留最初的错误。核心要求是提交前的失败应沿预期路径触发回滚连接状态恢复应建立在事务状态已妥善处理的基础上。这里也没有证据说明原作者故意选择了当前失败边界。单文件解析失败后继续收集数据与持久化阶段是否整体原子是两个独立设计问题不能用前者替后者解释动机。七、事务边界应该包住哪一段这个索引流程先读取文件、生成 Embedding、收集关系再进入数据库写入阶段。如果目标是避免持久化失败破坏旧数据可以优先考虑把“删除旧记录 写入两张表”放进一个短事务。通常不必为了这个目标把耗时的文件扫描和远程 Embedding 请求也放进去。数据库事务也不能撤销已经发生的远程调用。如果数据量更大、需要更长时间构建可以进一步研究版本化索引先准备新版本的数据成功后再切换当前版本标识。这是另一种设计方向不是本文已经完成的改造。回顾这次阅读我更愿意先问四个问题再判断“事务有没有生效”谁负责提交和回滚各个操作使用的连接是否属于同一个事务中途是否已经提交过异常能否抵达负责回滚的那一层事务注解是入口业务操作之间的提交边界才决定失败后能恢复到哪里。你遇到过哪些“方法明明有事务整个流程却没有一起回滚”的情况欢迎带上调用顺序和异常处理方式一起讨论。⛵ 我是小小放舟记录 Java 后端与 AI Agent 的学习、实践和踩坑过程。如果这篇文章对你有帮助欢迎收藏下次排查事务问题时对照检查。小小放舟的 CSDN
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →