声明式事务与批量读写与导入

共 19 题
📑 题目列表 19 题
#
★★★

1. 声明式事务(@Transactional、Spring AOP)的工作原理,代理拦截?

声明式事务(@Transactional、Spring AOP)的工作原理是什么?代理拦截如何实现?

  • Spring AOP 事务代理
  • @Transactional 的拦截流程
  • 自调用失效原理

Spring 的 @Transactional 通过 AOP 代理实现:Spring 为标注了 @Transactional 的 Bean 创建代理对象(JDK 动态代理或 CGLIB 代理),代理在方法调用前后拦截,在方法前开启事务、方法后提交或回滚。拦截器(TransactionInterceptor)负责:获取事务配置(传播、隔离、超时、rollbackFor)、开启事务、执行目标方法、根据异常决定提交或回滚、清理资源。由于是代理拦截,只有通过代理对象调用方法才会触发事务,类内部自调用(this.method())不会经过代理,导致事务失效。

核心是"代理拦截 + 事务管理器"。Spring 通过代理把事务管理横切到业务方法上,使业务代码不关心事务细节。理解代理机制是理解自调用失效、事务边界等问题的前提。

#
★★★

2. 事务边界与异常的 rollbackFor、noRollbackFor 设置?

事务边界与异常的 rollbackFor、noRollbackFor 如何设置?

  • rollbackFor 指定回滚异常
  • noRollbackFor 指定不回滚异常
  • 默认回滚规则

@Transactional 默认只对 RuntimeException 与 Error 回滚,对受检异常(checked exception)不回滚。rollbackFor 用于显式指定某些异常(如受检异常)也应触发回滚,例如 @Transactional(rollbackFor = Exception.class);noRollbackFor 用于指定某些异常即使抛出也不回滚,例如 @Transactional(noRollbackFor = BusinessException.class) 表示业务异常不回滚。事务边界则由方法入口/出口决定:方法进入时开启(REQUIRED 传播下),正常返回提交,抛异常按规则回滚。

默认只回滚运行时异常是 Spring 的约定,因为受检异常通常表示"业务可预期"的失败,不一定需要回滚。显式 rollbackFor 控制哪些异常回滚,是事务正确性的关键。

#
★★★

3. Hibernate 批量操作,StatelessSession、JDBC BATCH?

Hibernate 批量操作如何实现?StatelessSession 与 JDBC BATCH 分别是什么?

  • Session 批量操作的问题(一级缓存膨胀)
  • StatelessSession 的特性
  • JDBC BATCH 批量提交

用普通 Session 做批量插入时,持久化上下文会积累大量实体,一级缓存膨胀、内存占用高,且默认不合并为批量 SQL。解决方式:一是 StatelessSession 无状态会话,不维护一级缓存、不做级联与脏检查,直接执行 SQL,适合大批量插入/更新,性能高但无缓存与级联;二是 JDBC BATCH(jdbc.batch_size),把多条 INSERT 合并成批量执行,减少网络往返。两者可结合:StatelessSession + JDBC batch 批量执行,并定期 flush/clear 控制内存。

批量操作的核心是"减少内存积累"(StatelessSession/clear)与"减少往返"(JDBC batch)。普通 Session 因缓存膨胀不适合大批量。

#
★★★

4. JDBC 批处理(addBatch、executeBatch)的实现与性能?

JDBC 批处理(addBatch、executeBatch)的实现与性能如何?

  • addBatch/executeBatch 的用法
  • 批处理减少网络往返
  • 驱动层优化(rewriteBatchedStatements)

JDBC 批处理通过 PreparedStatement.addBatch() 添加多条语句,再 executeBatch() 一次性发送给数据库执行,减少网络往返与解析开销。性能上,批处理将 N 次单独的发送/解析合并为 1 次批量发送,显著提升吞吐。MySQL 驱动还支持 rewriteBatchedStatements=true,将多条 INSERT 重写为一条多值 INSERT,进一步减少往返。但注意:批处理不等于事务,需要配合事务手动提交;批大小需合理,过大导致内存与锁压力。

批处理的核心收益是"减少往返"。rewriteBatchedStatements 是 MySQL 驱动的关键优化,把多次批量 INSERT 合并为多值语句。

PreparedStatement ps = conn.prepareStatement("INSERT INTO t(id,v) VALUES(?,?)");
for (int i = 0; i < 1000; i++) {
    ps.setInt(1, i);
    ps.setString(2, "v" + i);
    ps.addBatch();
}
ps.executeBatch();
#
★★★

5. MyBatis 批量操作,foreach、ExecutorType.BATCH?

MyBatis 批量操作如何实现?foreach 与 ExecutorType.BATCH 分别是什么?

  • foreach 批量拼接
  • ExecutorType.BATCH 批量执行器
  • 两者的区别与适用场景

MyBatis 批量操作两种方式:一是 foreach 标签在 SQL 中拼接多值(如 INSERT INTO t VALUES (...),(...),...),生成一条大 SQL,减少往返,但受 SQL 长度与参数个数限制;二是 ExecutorType.BATCH 批处理执行器,在一次会话中复用同一 PreparedStatement 连续 addBatch,最后统一执行,不拼接 SQL,适合动态参数多、无法用 foreach 的场景。foreach 适合固定结构、参数可控的批量;ExecutorType.BATCH 更通用、对 SQL 长度无压力,但需手动 flush。

foreach 是把多条合并成一条 SQL,ExecutorType.BATCH 是复用语句分批执行。两者都是减少往返,选择取决于 SQL 复杂度与参数规模。

<insert id="batchInsert">
  INSERT INTO t(id, v) VALUES
  <foreach collection="list" item="it" separator=",">
    (#{it.id}, #{it.v})
  </foreach>
</insert>
#
★★★

6. 大文件导入(CSV 百万行)的工程实践,分批提交、临时表、去重与失败重试

大文件导入(CSV 百万行)的工程实践有哪些?如何分批提交、用临时表、去重与失败重试?

  • 分批提交策略
  • 临时表与 LOAD DATA
  • 去重(唯一约束/幂等)

百万行 CSV 导入的工程实践:先用 LOAD DATA(MySQL)或 COPY(PostgreSQL)将文件高速载入临时表,避免逐行 INSERT;再对临时表做清洗、去重(用唯一约束或 GROUP BY 去重)、校验,最后批量插入目标表;分批提交(每批 500~1000 条)控制事务大小与锁、内存;失败重试采用断点续传(记录已处理的行号/检查点)与事务粒度控制(整批失败回滚该批),并对账校验行数与校验和。同时考虑唯一索引做幂等去重,避免重复导入。

百万行导入的关键是"批量/高速加载 + 分批 + 幂等 + 可恢复"。临时表把"加载"与"转换"分离,LOAD DATA 比逐行快几个数量级,配合分批与对账保证正确性。

LOAD DATA LOCAL INFILE '/path/data.csv' INTO TABLE tmp_import
  FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n';
INSERT INTO target_t(id, v) SELECT id, v FROM tmp_import
  WHERE NOT EXISTS (SELECT 1 FROM target_t WHERE target_t.id = tmp_import.id);
#
★★

7. rewriteBatchedStatements 参数(MySQL JDBC)对 INSERT 性能的优化?

MySQL JDBC 的 rewriteBatchedStatements 参数如何优化 INSERT 性能?

  • rewriteBatchedStatements 的作用
  • 多值 INSERT 的合并
  • 优化效果

rewriteBatchedStatements=true 时,MySQL 驱动会把 executeBatch 的多次 INSERT 重写为一条多值 INSERT(INSERT INTO t VALUES (...),(...),...),从而把多次网络往返合并为一次,显著提升批量插入性能。默认此参数为 false,驱动会逐条发送 INSERT。启用后,批量 INSERT 的吞吐可提升数倍到数十倍。注意:该参数对非 INSERT 语句(如 UPDATE)也可能适用,且受 max_allowed_packet 限制,批过大时需分批。

该参数的本质是"协议层合并多语句为一条多值语句",减少往返与解析。是 MySQL 批量插入性能优化的关键开关。

#
★★

8. @Transactional 自调用失效问题(AOP 代理不拦截内部调用)?

@Transactional 自调用失效问题是什么?为什么 AOP 代理不拦截内部调用?

  • 自调用的概念
  • 代理拦截原理
  • 解决方案

自调用失效指:在同一个类内部,一个方法调用同类的另一个 @Transactional 方法(this.method()),由于该调用发生在代理对象内部,不经过代理,事务拦截器不会执行,导致事务不生效。原因是 Spring 事务依赖 AOP 代理,只有通过代理对象调用外部方法才会被拦截开启事务;内部 this 调用是直接调用目标对象自身的方法,绕过了代理。解决方案:注入自身代理(self-injection)、使用 AopContext.currentProxy()、或将事务方法拆分到独立 Bean 中,让跨 Bean 调用经过代理。

这是 AOP 代理机制的直接推论。理解"代理才拦截"是理解自调用失效的关键,解决方案本质是"让调用经过代理"。

#
★★

9. PROPAGATION_REQUIRED 的语义?

事务传播 PROPAGATION_REQUIRED 的语义是什么?

  • REQUIRED 的定义
  • 已存在事务与新建事务的行为
  • 在传播体系中的位置

PROPAGATION_REQUIRED 是 Spring 默认的传播级别:如果当前已存在事务,则加入该事务(同一事务、共享连接与提交/回滚);如果当前没有事务,则新建一个事务。这是最常用的传播级别,保证多个方法在同一事务中执行,要么全部成功要么全部回滚。它是事务传播的"默认值",理解 REQUIRED 就理解了事务边界如何向外扩展。

REQUIRED 的关键是"有则加入、无则新建",使内部方法成为外部事务的一部分,适合普通的业务操作(如服务方法调用多个 DAO 操作)。

#
★★

10. StatelessSession 的应用?

StatelessSession 的应用场景是什么?它有什么特性和限制?

  • StatelessSession 的特性
  • 适用场景(批量操作)
  • 限制(无缓存、无级联)

StatelessSession 是 Hibernate 的无状态会话,不维护一级缓存、不做脏检查、不级联、不强制执行带缓存的查询,直接与数据库交互。它适用于大批量插入/更新/删除等场景,因为不积累缓存,内存占用低、性能高。限制:没有持久化上下文,返回的实体是游离(detached)状态,无法利用缓存与会话级一致性;不支持一级缓存、级联保存、延迟加载等特性。因此只适合纯粹的批量数据操作,不适合复杂对象图管理。

StatelessSession 牺牲了缓存与级联等 ORM 特性换取批量性能,是"批处理专用"的会话。

#
★★

11. Spring 事务传播 NESTED 的应用场景,基于保存点实现部分回滚的机制,与 REQUIRED/REQUIRES_NEW 的差异及对底层保存点能力的依赖?

Spring 事务传播 NESTED 的应用场景是什么?基于保存点实现部分回滚的机制是什么?与 REQUIRED/REQUIRES_NEW 有何差异?

  • NESTED 的保存点机制
  • 部分回滚
  • 与 REQUIRED/REQUIRES_NEW 的差异

NESTED 传播使用嵌套事务(savepoint 保存点)机制:当外部事务存在时,内层方法在外部事务内建立保存点,内层异常只回滚到保存点,不影响外部事务已做的工作,实现"部分回滚"。与 REQUIRED 不同——REQUIRED 内层异常会导致整个外部事务回滚;与 REQUIRES_NEW 不同——REQUIRES_NEW 是完全独立的新事务,与外层提交/回滚无关,而 NESTED 仍属于外层事务的一部分,只是回滚范围被保存点限定。NESTED 依赖底层数据库对保存点(SAVEPOINT)的支持,若数据库不支持保存点则退化为 REQUIRED 行为。

NESTED 的价值是"内层失败不回滚外层已提交的工作",实现部分回滚。它依赖保存点能力,是 JDBC 3.0 SAVEPOINT 的映射。

#
★★

12. Spring REQUIRES_NEW 的应用场景,审计日志与异步补偿等需独立提交/回滚的子事务,其新增连接与延长锁持有时间的代价如何避免滥用?

Spring REQUIRES_NEW 的应用场景是什么?它如何用于审计日志与异步补偿等需独立提交/回滚的子事务?其代价如何避免滥用?

  • REQUIRES_NEW 的语义
  • 独立提交/回滚场景
  • 新增连接与锁持有时间的代价

REQUIRES_NEW 总是开启一个新事务,与外层事务完全独立,内层提交/回滚不影响外层。典型场景:审计日志——即使外层业务失败回滚,审计日志也要成功写入;异步补偿——子任务需要独立提交/回滚。代价:REQUIRES_NEW 会挂起外层事务并新建连接(需要额外从连接池取连接),导致连接占用增加;内层事务独立提交,可能延长数据锁的持有时间(外层未提交时内层已提交,锁释放时机不同)。避免滥用:仅当确实需要独立提交/回滚的安全边界时才使用,能合并为同一事务的优先用 REQUIRED,避免嵌套 REQUIRES_NEW 造成连接与锁开销。

REQUIRES_NEW 是"子事务独立于父事务"的机制,代价是额外连接与锁时长。滥用会造成连接池压力与锁竞争,应只在必须独立提交时使用。

#
★★

13. @Transactional rollbackFor 的应用,为何默认仅回滚 RuntimeException 与 Error?对受检异常显式声明 rollbackFor 的实践,以及异常被吞掉导致事务不生效的陷阱?

@Transactional rollbackFor 如何应用?为何默认仅回滚 RuntimeException 与 Error?异常被吞掉为什么导致事务不生效?

  • 默认回滚规则
  • 受检异常显式 rollbackFor
  • 异常被吞掉导致事务不生效

Spring 默认只对 RuntimeException 与 Error 回滚,因为受检异常通常表示"业务可预期的失败"(如校验失败),默认不应当回滚整个事务。若希望受检异常也回滚,需显式 @Transactional(rollbackFor = Exception.class),或指定具体异常类型。陷阱:若在事务方法内用 try-catch 吞掉异常(不抛出),事务拦截器无法感知异常,方法正常返回则提交事务,导致"该回滚却提交",数据写入错误。因此一定要让异常传播到事务边界(抛出),或显式调用回滚。

默认回滚规则是"约定优于配置",rollbackFor 是显式扩展。异常被吞掉是事务失效的常见根因——事务回滚依赖异常抛出,吞掉异常就是吞掉回滚信号。

#
★★

14. MyBatis 流式查询(Cursor)/JDBC fetchSize 防止大结果集 OOM

MyBatis 流式查询(Cursor)与 JDBC fetchSize 如何防止大结果集 OOM?

  • Cursor 流式查询
  • fetchSize 与游标
  • 防止 OOM 的原理

大结果集一次性加载到内存会 OOM。MyBatis 流式查询(Cursor)通过游标逐条/分批读取,不一次性加载全部结果;JDBC 的 setFetchSize(n) 控制每次从数据库取回的行数,配合数据库端游标(useCursorFetch)实现分批读取。原理:游标模式下,数据库保留结果集,驱动按 fetchSize 分批拉取,应用逐条处理,内存只保留当前批次。需注意:游标占用数据库游标资源,处理完要关闭;流式查询期间连接被占用,不能并发执行其他操作。

防止 OOM 的核心是"不一次性加载全部",用游标+fetchSize 分批拉取。代价是游标资源占用与处理期间连接不可复用。

// JDBC 游标分批读取
stmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(1000);
conn.setAutoCommit(false);
try (ResultSet rs = stmt.executeQuery()) {
    while (rs.next()) { process(rs); }
}
#
★★

15. 批量导入的断点续传与对账,大批量导入中断后如何从检查点恢复,导入前后如何用行数/校验和对账?

批量导入的断点续传与对账如何实现?中断后如何从检查点恢复?导入前后如何用行数/校验和对账?

  • 检查点(断点)机制
  • 从检查点恢复
  • 行数与校验和对账

断点续传需要记录处理进度(检查点),如已处理的行数、批次号或唯一键游标。中断后从检查点继续,跳过已处理部分,避免重复导入。实现方式:每批提交后更新检查点(存文件/数据库/Redis),导入时先读取检查点。对账:导入前记录源文件行数与校验和(如 MD5),导入后统计目标表行数、求和/校验和,与源对比,验证数据完整性;对账不通过则定位差异并补偿。结合唯一索引保证幂等,使重试可控。

断点续传解决"中断恢复",对账解决"数据完整性验证"。两者结合保证大批量导入在故障下可恢复、可验证。

#
★★

16. 批量大小(batch size)的调优,太大导致锁竞争与内存压力、太小吞吐不足,如何结合数据库负载实测确定?

批量大小(batch size)如何调优?太大与太小的代价分别是什么?如何结合数据库负载实测确定?

  • 批太大的代价(锁竞争、内存压力)
  • 批太小的代价(吞吐不足)
  • 实测调优方法

批大小(batch size)过大:单批事务大、锁竞争激烈、内存(SQL 拼接/参数)压力大、长期不提交导致锁持有久;批太小:提交次数多、网络往返多、吞吐不足。合理的批大小需结合数据库负载实测:在目标负载下测试不同批大小(如 100、500、1000、2000)的吞吐与延迟,观察锁等待、redo、内存占用,选择吞吐高且稳定的值。通用经验值 500~1000 条,但应以压测为准,并考虑 max_allowed_packet 等限制。

批大小是"吞吐 vs 资源消耗"的平衡点。理论上越大越好,但受锁、内存、事务长度约束,必须实测。

#
★★

17. 事务边界对批量操作的影响,分批提交的间隔与一致性窗口如何权衡,出错时回滚范围如何控制(整批 vs 单条)?

事务边界对批量操作有什么影响?分批提交的间隔与一致性窗口如何权衡?出错时回滚范围如何控制(整批 vs 单条)?

  • 分批提交与一致性窗口
  • 事务边界对锁与可见性的影响
  • 回滚范围控制(整批 vs 单条)

批量操作的事务边界决定一致性窗口:每批一个事务,提交后该批数据对外可见,未提交的批不可见,形成"按批一致的窗口"。批间隔越大(一次提交越多),一致性窗口内数据越多、锁持有越久,但提交次数少、性能好;批越小,窗口小、锁短,但提交频繁。回滚范围控制:出错时整批回滚(一个事务内所有数据回滚)还是单条回滚(如逐条提交或跳过错误行),取决于业务——需要原子性的用整批事务,允许部分成功(如数据清洗)则逐条或跳过错误并记录。权衡点:一致性要求高则整批原子,性能与容错优先则小批/逐条。

事务边界决定"多少数据作为一个原子单元"。分批提交把一致性窗口与性能折中,回滚范围决定容错粒度。

#
★★

18. 批量写入与锁/日志的代价,批量 INSERT 对行锁、redo/binlog 与从库复制延迟的影响,如何通过分批与错峰控制?

批量写入对行锁、redo/binlog 与从库复制延迟有什么影响?如何通过分批与错峰控制?

  • 批量 INSERT 对行锁的影响
  • 对 redo/binlog 的影响
  • 从库复制延迟

批量 INSERT 会:一是持有大量行锁,批越大锁越多、锁持久越长,可能阻塞其他事务;二是产生大量 redo log 与 binlog,写放大,磁盘 IO 压力增大;三是主库写入量大时,从库(基于 binlog 异步复制)复制跟不上,产生复制延迟。控制手段:分批提交(控制每次事务的行数与锁)、错峰执行(在业务低峰期导入,避开高峰)、控制并发(限制同时导入的进程数)、必要时调大 innodb_buffer_pool 与优化从库复制参数。批量写入应"小批多次 + 错峰",平衡性能与数据库稳定性。

批量写入是"性能 vs 资源代价"的权衡。写放大(redo/binlog)与锁是主要代价,分批与错峰是其核心控制手段。

#

19. 批量操作与 ORM 一级缓存的交互(批量插入时 Session 缓存膨胀)

批量操作与 ORM 一级缓存的交互是什么?批量插入时 Session 缓存为什么会膨胀?

  • 一级缓存对批量插入的影响
  • Session 缓存膨胀的原理
  • 解决方式(flush/clear)

用 ORM Session 做批量插入时,每个持久化实体都会进入一级缓存(持久化上下文),批量插入大量实体时缓存不断膨胀,占用大量内存,且脏检查在整个缓存上进行,性能下降。解决方式:定期调用 session.flush() 把数据写入数据库并 session.clear() 清空缓存,控制缓存规模;或使用 StatelessSession 不维护一级缓存。批量插入时应避免缓存无限增长,否则内存 OOM。

一级缓存是"批量插入的内存杀手",因为实体默认被持久化到上下文。flush+clear 定期清空是标准解法。