MyBatis执行流程与插件机制详解

MyBatis执行流程与插件机制详解 1. 对应简历段落 这篇文章对应简历中“熟悉 MyBatis 执行流程,处理遗留 XML 兼容、自定义插件、分页改造和 SQL 性能治理”的项目经历。简历可以写成: 深入梳理核心系统 MyBatis 执行链路,完成 Mapper XML 加载校验、动态 SQL 兼容、分页插件顺序治理、自定义拦截器适配和慢 SQL 诊断,支撑 Spring Boot 迁移过程中数百个 Mapper 平稳运行。 面试官会从这段继续追问:Mapper 接口为什么不用实现类也能执行?SqlSession、Executor、StatementHandler、ParameterHandler、ResultSetHandler 分别做什么?插件到底拦截了哪个对象?为什么插件顺序会影响分页和数据权限?一级缓存和二级缓存在哪里生效?MyBatis 和 Spring 事务是怎么结合的? 这类问题不能只背源码类名。更好的回答方式是把一次查询从 Mapper 方法调用开始,讲到 SQL 解析、参数绑定、执行器调用、结果集映射、缓存处理和插件拦截。再结合项目说明为什么理解执行流程能解决真实问题,例如分页 SQL 改写、动态数据权限注入、SQL 日志脱敏、慢查询定位、迁移后 XML 未加载排查。 2. 业务背景 遗留保险系统中,MyBatis 往往承担最核心的数据访问职责。系统可能有几百个 Mapper XML,覆盖客户、保单、订单、产品、渠道、机构、佣金、回访、报表等模块。早期项目大量使用手写 SQL,这是 MyBatis 的优势:可以精细控制复杂查询、Oracle 方言、动态条件、批量操作和存储过程调用。但随着系统演进,问题也会累积。 第一,XML 数量多,命名空间、SQL id、ResultMap、TypeHandler、动态 SQL 依赖复杂。迁移到 Spring Boot 后,只要扫描路径、资源加载或 SqlSessionFactory 配置有差异,就可能出现运行时找不到 statement。第二,分页、数据权限、租户隔离、SQL 日志等横切能力通常靠插件实现,插件顺序一变,最终 SQL 就可能不同。第三,老系统常用 Oracle 特性,例如 ROWNUM、CONNECT BY、序列、存储过程、DECODE,迁移过程中需要保证 SQL 语义不变。第四,保险查询场景复杂,运营后台一个列表可能关联客户、保单、机构、产品、状态流水和标签表,慢 SQL 很常见。第五,事务问题经常被误判为 MyBatis 问题,例如同一事务内查询读到缓存、批量执行未 flush、异常被吞导致不回滚。 ...

March 3, 2025 · 3 min · 560 words · WY

Spring事务机制与保险业务一致性问题

Spring事务机制与保险业务一致性问题 1. 对应简历段落 这篇文章对应简历中“处理保险核心业务交易一致性,熟悉 Spring 声明式事务、传播行为、回滚规则和补偿机制”的项目经历。简历可以写成: 负责保险销售与订单流转链路的一致性治理,基于 Spring 声明式事务规范本地数据库操作,梳理事务传播、异常回滚、MyBatis 会话绑定和外部接口调用边界,并通过业务流水、幂等键、补偿任务和对账机制解决跨系统最终一致性问题。 面试官通常会追问:@Transactional 为什么有时不生效?默认回滚哪些异常?传播行为怎么选?同一个类内部方法调用为什么没有事务?MyBatis 和 Spring 事务怎么绑定?保险出单过程中数据库写成功、下游核保失败怎么办?能不能把外部 HTTP 调用放在事务里?分布式事务和补偿事务怎么取舍? 回答这类问题时,要把 Spring 事务机制和业务一致性分开讲。Spring 本地事务解决的是单数据源或单事务资源内的原子性;保险业务一致性经常跨数据库、第三方接口、消息、任务和人工处理,不可能只靠一个 @Transactional 解决。资深回答要能说明事务边界、失败分类、回滚策略、幂等设计、补偿和对账。 2. 业务背景 保险业务链路天然复杂。以投保为例,用户提交订单后,系统可能要保存投保单、客户信息、被保人信息、险种责任、保费试算结果、支付流水、营销活动信息;随后调用核保、风控、支付、短信、电子保单、佣金系统;还要支持撤单、退保、批改、状态回写和人工复核。每一步都可能失败,而且失败后业务状态不能混乱。 在老系统中,常见问题包括:Service 方法没有事务导致部分表写入成功;异常被 catch 后没有抛出导致事务不回滚;@Transactional 加在 private 方法或同类内部调用上不生效;事务范围过大,把外部接口调用也包进去导致数据库连接长时间占用;多数据源操作只回滚了一个库;定时任务补偿重复执行造成状态错乱;MyBatis 批量执行异常后定位困难。 保险场景对一致性的要求不是所有步骤都必须强一致,而是不同环节要采用不同策略。保单主状态、订单金额、支付流水属于强一致要求高的本地数据,必须在本地事务中保证。短信通知、邮件、日志、报表、佣金试算可以最终一致。调用外部核保、支付、保司接口时,需要业务流水和状态机支撑,因为外部系统不可由本地事务回滚。 3. 核心原理 Spring 声明式事务基于 AOP 代理。方法调用进入代理后,TransactionInterceptor 根据事务属性获取或创建事务,执行业务方法,成功则提交,异常则按规则回滚。底层事务由 PlatformTransactionManager 实现,常见的是 DataSourceTransactionManager、JpaTransactionManager 和 JTA 事务管理器。 事务传播行为决定当前方法如何参与已有事务。REQUIRED 最常用,有事务就加入,没有就新建;REQUIRES_NEW 挂起当前事务并新建事务,适合独立审计日志或失败记录;NESTED 使用保存点,依赖数据库和事务管理器支持;SUPPORTS 有事务就加入,没有就非事务;MANDATORY 要求必须已有事务;NOT_SUPPORTED 挂起事务;NEVER 禁止事务。 默认回滚规则是运行时异常和 Error 回滚,受检异常默认不回滚。保险业务中很多异常是业务异常,如果继承 Exception 而不是 RuntimeException,又没有配置 rollbackFor,就可能出现“抛异常但提交了”。推荐明确异常体系:参数校验、业务拒绝、下游失败、系统异常分层,并在事务注解中明确回滚规则。 事务失效常见原因有:同类内部自调用绕过代理;方法不是 public;对象不是 Spring Bean;异常被捕获没有继续抛出;数据库引擎不支持事务;使用了错误的事务管理器;多线程中事务上下文不传播;@Transactional 加在接口或类上但代理方式和调用方式不匹配。 MyBatis 与 Spring 事务通过 SqlSessionTemplate 和 TransactionSynchronizationManager 集成。同一事务内获取到的 SqlSession 会绑定到当前线程,Executor 使用同一个 JDBC Connection。事务提交时才真正提交连接,回滚时回滚连接。因此 MyBatis 本身不单独提交,除非开发者绕开 Spring 管理手动打开 SqlSession。 ...

March 2, 2025 · 2 min · 277 words · WY

Spring3到SpringBoot迁移的关键问题与改造路径

Spring3到SpringBoot迁移的关键问题与改造路径 1. 对应简历段落 这篇文章对应简历中“遗留系统架构升级、保险业务核心系统现代化改造、从传统单体应用迁移到 Spring Boot 微服务体系”的项目经历。可以在简历中表述为: 参与公司核心业务系统现代化改造,负责将 Ant 构建的 Spring3 + MyBatis + WebLogic 遗留应用逐步迁移为 Maven + Spring Boot + Undertow 的服务化架构;改造过程中完成 MyBatis 兼容适配、配置中心接入、服务注册发现、统一网关、ELK 日志链路、定时任务平台化等能力建设,支撑保险销售、订单流转、客户服务、运营管理等业务模块平稳迁移。 这个段落真正想表达的不是“我会 Spring Boot”,而是“我能把一个多年运行、技术栈陈旧、依赖复杂、业务风险高的系统,拆解为可控的迁移工程”。面试官通常不会只问 Spring Boot 自动配置原理,还会追问:为什么要迁移?迁移过程中哪些地方最危险?老系统和新系统如何并行?WebLogic 和 Undertow 行为差异怎么处理?MyBatis 老 XML 怎么兼容?配置、日志、任务和网关如何治理?上线失败如何回滚? 因此,这类项目要讲成一条完整的改造链路:从构建体系开始,解决可编译、可依赖、可打包;再解决框架升级,解决 Bean、事务、MVC、配置加载差异;然后解决运行容器差异,处理 JNDI、ClassLoader、Session、编码、线程池;最后补上分布式运行能力,包括 Nacos、Gateway、ELK、XXL-Job、灰度发布和回滚机制。 2. 业务背景 遗留系统通常不是因为“代码丑”才迁移,而是因为它已经影响业务迭代效率、稳定性和运维成本。以保险销售和客户运营系统为例,老系统最初可能是一个部署在 WebLogic 上的传统 WAR 包,业务覆盖代理人管理、客户资料、产品配置、保单试算、订单提交、回访工单、运营报表等模块。早期系统采用 Spring3、Spring MVC、MyBatis、Oracle、Quartz、Log4j,构建方式使用 Ant 脚本,依赖 jar 包放在 lib 目录,环境配置通过 properties、XML 或 WebLogic 控制台维护。 这种架构在业务规模较小时能够稳定运行,但随着线上渠道增加、营销活动频繁、移动端和内部运营平台不断接入,问题会逐渐暴露。第一,交付效率低。Ant 构建脚本缺少标准依赖管理,jar 包版本靠人工维护,不同开发环境和测试环境经常出现“我本地能跑、测试环境缺类”的问题。第二,部署复杂。WebLogic 启停慢,单个 WAR 包变大后发布窗口长,一次小改动也可能需要完整重启。第三,配置分散。不同环境的数据库、缓存、外部接口、任务开关散落在服务器目录和控制台里,缺少集中审计。第四,日志不可观测。出现客户投保失败、订单状态不一致、接口超时等问题时,只能登录机器翻日志,缺少 traceId、调用链和统一检索。第五,任务不可控。Quartz 任务随应用部署,集群环境下需要处理锁和重复执行,任务失败重试、手动触发、执行历史都不友好。 业务侧的诉求也很明确:核心交易不能中断,已有接口不能大面积改动,数据一致性不能破坏;同时,新业务希望更快上线,服务能够水平扩容,日志能够快速定位,任务能够统一调度,配置能够动态发布。也就是说,这不是一次“技术升级秀”,而是一次在业务连续性约束下完成的工程治理。 ...

March 1, 2025 · 4 min · 671 words · WY

生产环境稳定性治理:指标、告警、日志与应急流程

生产环境稳定性治理:指标、告警、日志与应急流程 对应简历段落 简历中关于稳定性治理可以写成: 参与生产环境稳定性治理,围绕 JVM、接口、线程池、数据库、缓存、消息队列和业务关键链路建立指标、告警、日志与应急流程,推动故障从被动救火转向可观测、可预警、可复盘的闭环管理。 这类表述看起来很宏观,面试官会追问落地细节:你监控哪些指标?告警阈值怎么设?什么样的日志才算有用?Full GC 告警来了如何处置?接口超时和错误率升高怎么分级?应急时谁来决策回滚、扩容、降级?故障复盘怎么避免流于形式? 这篇文章把稳定性治理拆成可讲、可做、可复盘的部分。它不是单纯 JVM 调优文章,但和 JVM 线上诊断强相关,因为真正成熟的 JVM 治理一定嵌在生产稳定性体系里。 业务背景 保险核心系统对稳定性要求很高。承保链路影响新单出单,理赔链路影响客户赔付,保全链路影响合同变更,缴费链路影响续期和扣款,运营后台影响人工审核和批量处理。一个 JVM Full GC、线程池耗尽或导出 OOM,表面看是技术故障,业务上可能造成出单失败、理赔延迟、财务对账中断、客服投诉。 保险系统还有明显的业务峰谷。工作日上午运营人员集中查询和导出,月底月初财务对账压力大,凌晨批处理任务集中,促销或渠道活动期间承保请求上升。稳定性治理不能只看平均值,而要关注峰值、长尾、批处理窗口和关键业务链路。 早期团队常见问题是:监控很多但不知道看什么,告警很多但没人响应,日志很多但故障时搜不到关键字段,应急靠个人经验,复盘只写“加强监控”。这种状态下,即使个别工程师会用 GC 日志和 Arthas,也很难形成团队级稳定性能力。 核心原理 稳定性治理的核心是四个闭环:可观测、可告警、可处置、可复盘。 可观测是指系统运行状态能够被看见。它包括指标、日志、链路追踪和事件记录。指标适合看趋势和阈值,比如堆使用率、Full GC 次数、接口 P99、错误率;日志适合还原具体请求和业务上下文;链路追踪适合定位跨服务耗时;事件记录适合关联发布、配置变更、定时任务、降级开关。 可告警是指异常能在影响扩大前被发现。告警不是越多越好,而是要有分级、降噪和责任人。一个服务堆使用率 80% 不一定要半夜叫醒人,但连续 Full GC、核心接口错误率升高、线程池队列打满、容器重启就必须及时处理。 可处置是指故障发生后有预案。比如 Full GC 频繁时先摘除异常实例、停止大导出任务、降低批处理并发、必要时扩容或回滚;数据库慢时切换降级策略;消息堆积时扩消费者或限流入口。没有预案的告警,只会制造焦虑。 可复盘是指故障结束后沉淀改进。复盘不是找人背锅,而是明确触发条件、影响范围、发现时间、响应时间、根因、止血动作、永久修复和预防措施。稳定性能力就是靠一次次复盘长出来的。 项目落地 指标体系可以分成五层。 第一层是基础资源指标:CPU、Load、内存、磁盘、网络、容器重启、进程 RSS。它们回答“机器和容器是否健康”。 第二层是 JVM 指标:堆使用率、老年代使用率、年轻代使用率、Full GC 次数和耗时、Young GC 次数和耗时、线程数、类加载数、直接内存、元空间。它们回答“JVM 是否稳定”。 第三层是应用指标:接口 QPS、P95/P99 RT、错误率、超时率、线程池活跃数、队列长度、拒绝次数、连接池使用率、本地缓存大小、导出任务数量。它们回答“应用是否还能正常服务”。 第四层是依赖指标:数据库慢 SQL、连接池等待、Redis 延迟、MQ 堆积、文件服务耗时、外部接口成功率。它们回答“问题是不是来自下游或依赖”。 第五层是业务指标:出单成功率、理赔提交成功率、保全处理量、支付成功率、导出任务失败率、批处理完成时间。它们回答“技术异常对业务造成了什么影响”。 告警设计要围绕症状和影响,而不是只围绕资源。比如“老年代使用率超过 85%”可以作为预警,但真正需要高优先级的是“10 分钟内 Full GC 超过 3 次且接口 P99 超过阈值”或“核心承保接口错误率超过 1%”。告警要带上服务名、实例、环境、时间窗口、当前值、阈值、最近发布、相关日志链接和应急手册链接。 ...

February 6, 2025 · 1 min · 161 words · WY

大Excel导出OOM的原因、排查与优化

大Excel导出OOM的原因、排查与优化 对应简历段落 简历中关于大 Excel 导出的经历可以写成: 针对运营后台大数据量 Excel 导出导致 OOM、Full GC 频繁和接口超时的问题,完成导出链路重构:同步导出改异步任务,数据库分页读取,流式写出文件,限制导出行数和并发数,显著降低堆内存峰值。 这是非常适合 Java 面试展开的项目,因为它同时涉及 JVM、数据库、文件 IO、线程池、业务体验和稳定性治理。面试官可能追问:为什么 Excel 导出会 OOM?POI 的 XSSFWorkbook 和 SXSSFWorkbook 有什么区别?分页查询是不是一定安全?为什么 response.getOutputStream() 直接写也可能内存高?怎么设计异步导出?怎么防止多个大导出同时把服务打垮? 这篇文章围绕保险运营后台最常见的大报表导出场景,讲清楚原因、证据、改造和面试表达。 业务背景 保险公司运营后台有大量导出诉求。承保部门要导出保单清单,理赔部门要导出赔案明细,财务部门要导出保费和佣金对账,客服部门要导出客户回访名单,渠道部门要导出代理人业绩。数据量经常是几万、几十万甚至上百万行,字段也很宽,包含保单号、客户信息、产品信息、渠道信息、缴费计划、状态、时间、金额等。 早期系统为了开发方便,常见写法是:接口接收查询条件,一次性从数据库查出所有数据;把 Entity 转成 DTO;再把 DTO 转成 Excel VO;使用 Apache POI 的 XSSFWorkbook 创建 Workbook、Sheet、Row、Cell;最后写到 HTTP 响应。这个流程在几千行时没问题,一旦数据量变大,就会出现堆内存飙升、Full GC 频繁、接口超时甚至 OOM。 大 Excel 导出还有一个麻烦点:它往往由业务人员手动触发,时间不可控。如果多个运营人员同时导出,或者定时任务也在跑,服务实例会瞬间承受很高的内存和数据库压力。由于导出接口通常部署在和核心后台同一个应用里,它还可能影响正常查询、审核、核保等功能。 核心原理 Excel 导出 OOM 的根因通常不是“Excel 文件本身太大”这么简单,而是导出过程中同时存在多份数据和大量中间对象。 第一份是数据库查询结果。如果一次性 selectList 60 万行,每行几十个字段,Java 堆里会有大量 Entity 对象、字符串、BigDecimal、Date、集合节点。 第二份是转换结果。很多系统会把 Entity 转成 DTO,再转成 VO,再转成用于 Excel 的数组或 Map。每一层转换都会创建新对象,字段里的字符串可能还会拼接、格式化、字典翻译。 ...

February 5, 2025 · 2 min · 281 words · WY

堆Dump分析实战:如何定位内存泄漏

堆Dump分析实战:如何定位内存泄漏 对应简历段落 简历中关于堆 Dump 和内存泄漏的表述,通常可以写成: 参与核心保险业务系统线上内存泄漏排查,通过堆 Dump、MAT、Arthas heapdump、GC 日志和业务链路分析定位静态缓存、ThreadLocal、导出任务上下文等对象无法释放的问题,完成缓存治理和对象生命周期优化。 面试官看到这段,很容易追问:你怎么抓 Dump?线上抓 Dump 会不会影响服务?MAT 里 Dominator Tree、Histogram、Retained Size、Shallow Size 分别怎么看?怎么从 GC Roots 引用链证明是泄漏?怎么区分内存泄漏和内存峰值?如果 Dump 很大打不开怎么办? 这篇文章的重点是把“会用 MAT”升级为“会用 Dump 证明问题”。资深候选人不能只说“某对象很多”,还要能解释这些对象为什么活着、由谁引用、业务上为什么不该活、怎么改造,以及改造后如何验证。 业务背景 保险业务系统里,内存泄漏很少是“传统 C 语言意义上的忘记释放内存”,更多是对象已经没有业务价值,但仍然被 Java 引用链持有,GC 无法回收。比如运营后台做导出任务时,把每次导出的查询条件、用户信息、结果摘要放进一个静态 Map,只新增不删除;批量核保任务为了复用上下文,把保单明细放进 ThreadLocal,线程池复用后没有 remove;权限系统把用户机构树缓存在本地 ConcurrentHashMap,离职用户、历史渠道、临时权限长期不清理。 这类问题的线上表现通常不是立刻 OOM,而是慢慢变坏。第一天 Full GC 后老年代还能降到 40%,第二天只能降到 55%,第三天降到 70%,到了高峰期再来一个大导出就直接 OOM。应用重启后恢复,运行一段时间又复现。这个特征非常适合用 GC 日志发现趋势,用堆 Dump 找证据。 真实项目里,泄漏排查往往发生在压力很大的场景:服务已经抖动,业务方催促恢复,Dump 文件很大,机器磁盘紧张,线上又不能随便停服务。因此排查前要有清晰流程,先止血,再留证,再分析,再改造。 核心原理 Java 的内存泄漏本质是“无用对象仍然可达”。GC 判断对象是否可回收,不关心业务是否还需要它,只关心从 GC Roots 出发能不能到达它。GC Roots 包括线程栈中的局部变量、静态字段、JNI 引用、正在运行的线程、类加载器等。 堆 Dump 是某一时刻堆内对象的快照。它能告诉我们:有哪些对象、各有多少、占用多少内存、对象之间如何引用、哪些对象因为某条引用链而无法回收。但 Dump 不是时间序列,它只是一张照片,所以最好结合 GC 日志、监控和多个时间点 Dump 一起判断。 ...

February 4, 2025 · 2 min · 375 words · WY

Arthas在线诊断实战:trace、watch、jad、thread、dashboard

Arthas在线诊断实战:trace、watch、jad、thread、dashboard 对应简历段落 简历中和 Arthas 相关的经历可以写成: 熟练使用 Arthas 对生产 Java 服务进行在线诊断,结合 dashboard、thread、trace、watch、jad、heapdump 等命令定位接口耗时、线程阻塞、参数异常、代码版本不一致和内存问题,支撑核心保险业务系统线上故障快速恢复。 面试官看到这段会非常容易追问细节:trace 和 watch 有什么区别?线上执行 watch 会不会有风险?jad 反编译解决过什么问题?thread -n 5 看到 CPU 高线程后怎么继续分析?dashboard 能看出哪些 JVM 风险?Arthas 是怎么 attach 到进程的?你有没有因为表达式写得太重影响线上? 这篇文章不把 Arthas 当命令手册,而是放到保险业务系统的线上诊断场景里讲。重点是:什么时候用哪个命令、看到结果后如何判断、如何控制风险、如何把观察结果转成修复方案。 业务背景 保险系统的线上问题经常发生在“不能重启、不能加日志、不能马上发版”的时候。比如承保接口突然变慢,但日志只打印了入口和异常,没有打印中间耗时;理赔查询返回数据为空,但测试环境无法复现;大 Excel 导出偶发 OOM,但不知道一次到底查了多少数据;某个定时任务凌晨跑满 CPU,但代码路径很深;新版本发布后怀疑某个实例没有更新到正确代码。 传统方式通常是补日志、发版、等复现。但生产问题等不起,尤其是核心交易链路,一次发版也有流程成本和风险。Arthas 的价值就在于在线观察 JVM 运行状态、方法调用、参数返回、线程栈和实际加载的字节码,不重启服务也能补齐很多证据。 一个典型场景是保单导出接口变慢。业务方说“导出卡住”,数据库同事说慢 SQL 不明显,应用日志只看到接口总耗时 60 秒。此时可以用 trace 看耗时到底在查询、对象转换、字典补全、Excel 写出还是文件上传;用 watch 看入参和返回值大小;用 thread 看是否有线程阻塞;用 dashboard 看 GC 和线程总体状态。如果怀疑线上代码与仓库不一致,可以用 jad 反编译实际运行的类。 核心原理 Arthas 通过 Java Attach 机制连接到目标 JVM,并利用字节码增强技术在运行时观察方法调用。它不是简单读日志,而是在目标方法执行前后插入观测逻辑,所以功能强大,也必须控制使用范围。 dashboard 是全局体检命令,可以快速看到线程、内存、GC、运行时间、系统负载等信息。它适合故障初期判断应用是否处于 GC 压力、线程暴涨、CPU 高或内存接近上限。 ...

February 3, 2025 · 2 min · 341 words · WY

GC日志如何看:从FullGC频繁到问题定位

GC日志如何看:从FullGC频繁到问题定位 对应简历段落 简历里关于 JVM 调优的描述,经常会写成: 负责核心保险业务系统 JVM 性能调优,结合 GC 日志、监控指标和业务链路定位 Full GC 频繁、接口抖动、批量任务高峰内存飙升等问题,优化堆参数、导出链路和批处理并发策略,降低停顿时间并提升系统稳定性。 这句话在面试里一定会被追问。面试官不会满足于“我看过 GC 日志”,而会继续问:你怎么看?看哪些字段?Full GC 频繁一定是内存泄漏吗?G1 的 Young GC、Mixed GC、Full GC 分别代表什么?你怎么从日志推断是大对象、晋升失败、老年代泄漏还是批量任务峰值?参数是怎么调的,调完怎么验证? 这篇文章的目标,是把 GC 日志从“看起来很吓人的一串文本”拆成可复盘、可讲述、可落地的诊断链路。面向资深 Java 面试,不只背概念,而是能把一次线上 Full GC 频繁问题讲成完整项目经验。 业务背景 保险业务系统的 JVM 压力往往不是来自持续高 QPS,而是来自低频但重型的业务操作。比如承保系统在月末集中处理批量核保,理赔系统批量生成赔付清单,运营后台导出几十万行保单数据,财务系统凌晨跑对账和佣金计算。这些任务会在短时间内创建大量 DTO、List、Map、字符串、Excel 单元格对象和 JSON 中间对象。 线上表现通常是这样的:接口响应时间突然从几百毫秒升到数秒;网关开始出现 502、504;业务日志中大量请求超时;机器 CPU 看起来很高,但业务线程并没有明显计算热点;监控里 Full GC 次数突然变多,单次停顿从几百毫秒到十几秒不等。此时如果只看数据库慢 SQL,可能会误判为数据库问题;如果只把堆调大,可能只是推迟下一次故障。 一个典型案例是运营人员在工作日上午发起“保单全量导出”。接口一次性查出 60 万条保单,每条保单又关联客户、险种、渠道、缴费计划等信息。应用堆设置为 -Xms4g -Xmx4g,使用 G1。导出开始后年轻代快速被填满,大量对象还没来得及死亡就晋升到老年代,老年代空间被导出对象、缓存对象和日志字符串挤占。几分钟内出现连续 Full GC,每次 Full GC 后老年代只回落一点点,接口进入长时间停顿。 GC 日志在这个场景里的作用,是把“系统卡了”转化为可验证的问题:什么时候开始 GC 频繁,什么类型的 GC,停顿多久,GC 前后堆变化如何,老年代是否回落,是否有 Humongous 对象,是否出现 To-space exhausted 或 evacuation failure。 ...

February 2, 2025 · 3 min · 444 words · WY

JVM内存模型与保险业务系统常见内存问题

JVM内存模型与保险业务系统常见内存问题 对应简历段落 在简历中,和本文对应的经历通常会写成类似下面的表述: 负责核心保险业务系统 JVM 性能调优与线上问题排查,结合 GC 日志、堆 Dump、Arthas、监控告警定位内存泄漏、Full GC 频繁、大 Excel 导出 OOM 等问题;通过参数优化、代码改造、批处理拆分、对象生命周期治理等手段,降低 Full GC 频率,提升系统稳定性。 这段话看起来只有几行,但面试官往往会继续追问:你调过哪些参数?怎么看 GC 日志?Dump 怎么分析?Arthas 具体用过哪些命令?大 Excel 导出为什么会 OOM?你是怎么证明不是数据库慢,而是 JVM 内存问题?Full GC 降频之后有什么指标变化?这些问题不是背几个 JVM 名词就能回答的,需要把业务场景、内存结构、定位链路和项目落地串起来。 保险系统的特点是业务对象复杂、批量数据多、历史数据时间跨度长、报表导出需求重、定时任务集中、峰值访问明显。比如承保系统要处理投保单、保单、险种、责任、客户、缴费计划;理赔系统要处理报案、立案、查勘、定损、赔付、影像材料;保全系统要处理批改、退保、受益人变更;运营后台还会有各种 Excel 导出、对账文件生成、续期提醒、批量短信、批量核保任务。对象层级深、一次查询返回数据多、缓存和集合使用频繁,这些都会放大 JVM 内存问题。 所以这篇文章的目标不是孤立讲 JVM 内存模型,而是回答一个更贴近项目的问题:在保险业务系统里,常见内存问题是怎么产生的,如何定位,如何改造,如何在面试中讲得可信。 业务背景 保险业务系统通常是典型的 Java 后端系统,技术栈可能包括 Spring Boot、Spring Cloud、MyBatis、Redis、消息队列、定时任务调度、分布式文件服务、报表服务等。线上部署一般是多实例集群,单个 JVM 设置固定堆大小,例如 -Xms4g -Xmx4g 或 -Xms8g -Xmx8g,根据服务类型配置 G1、CMS 或其他收集器。 系统日常流量并不一定很高,但内存风险经常来自“低频重操作”。例如: 运营人员导出几十万行保单清单,接口把所有数据一次性查到内存,再用 Apache POI 的 XSSFWorkbook 生成 Excel,瞬间占满堆。 理赔影像或附件元数据查询时,把大字段、Base64 内容、历史轨迹一次性组装到 DTO,导致单次请求对象非常大。 批量续保、批量对账、批量佣金计算任务在凌晨集中执行,多个任务同时拉取大批数据,年轻代快速晋升到老年代,引发频繁 Full GC。 为了提升查询性能,开发人员使用本地 Map 缓存产品、机构、渠道或用户权限数据,但没有过期策略,随着业务运行不断膨胀。 线程池、异步任务、ThreadLocal、静态集合、监听器等释放不当,导致对象被长期引用,GC 无法回收。 保险业务还有一个特殊点:数据结构天然“宽而深”。一个保单对象可能关联投保人、被保人、受益人、险种、责任、批单、缴费计划、核保结论、影像材料、销售人员、渠道信息等。如果在代码中无节制地使用全量对象转换、深拷贝、JSON 序列化、日志打印,就会让短时间内创建的对象数量非常惊人。很多问题表面是“接口慢”或“机器 CPU 高”,本质可能是内存分配过快和 GC 压力过大。 ...

February 1, 2025 · 5 min · 911 words · WY

月底结算与营销活动高峰的稳定性设计复盘

月底结算与营销活动高峰的稳定性设计复盘 对应简历段落 在保险销售系统中,参与月底佣金结算、营销活动高峰、商机流转和保单状态回写等核心链路建设。针对月底批处理任务与在线活动流量叠加导致的线程池打满、数据库连接耗尽、MQ 积压、接口超时等问题,设计线程池隔离、MQ 削峰、限流降级、批量任务分片、幂等补偿和监控告警方案,提升系统整体稳定性。 这段简历适合做一次完整复盘。资深面试官会希望听到的不只是“我用了哪些技术”,而是问题发生前的架构状态、故障表现、根因分析、改造方案、验证方式和最终收益。复盘型回答最能体现工程判断力。 业务背景 月底结算和营销活动高峰在保险业务里经常撞在一起。月底要计算代理人佣金、活动奖励、渠道费用、机构业绩,还要生成对账明细和报表。营销活动则会带来投保提交、客户咨询、商机分配、代理人跟进、短信通知等在线流量。一个偏批处理,一个偏在线交易,但它们可能共享同一套应用、数据库、线程池和 MQ 集群。 早期系统为了快速交付,很多异步任务共用一个默认线程池,数据库连接池也没有按业务隔离。结算任务在月底一次性扫描大量订单,按批次提交任务;活动高峰时,投保提交也会触发商机流转和通知。两个高峰叠加后,低优先级任务可能占住高优先级资源。 典型现象是:活动提交接口 RT 从几百毫秒涨到数秒;商机分配延迟增加;MQ 消费积压;数据库连接池等待明显;结算任务本身也变慢;运维看到 CPU 不一定满,但线程大量阻塞在数据库连接、外部 RPC 或队列等待上。这类问题的难点在于它不是单个 bug,而是容量、隔离和任务模型共同失衡。 核心原理 稳定性复盘要先区分在线链路和离线链路。在线链路关注低延迟和用户体验,典型请求包括活动页查询、投保提交、商机查询。离线链路关注吞吐和最终完成,典型任务包括佣金结算、对账报表、批量同步。两类链路对资源的使用方式不同,不能完全共用资源。 月底结算的风险来自批量放大。单条结算明细计算可能很快,但几十万条订单一起处理,会放大数据库扫描、锁冲突、批量写入、缓存访问和 MQ 投递。结算任务如果没有分片、限速和状态管理,就会把压力集中打到数据库。 营销活动的风险来自脉冲流量。活动开始后的几分钟内,入口请求可能远高于平时。在线链路如果同步执行后置动作,会把短信、CRM、保司等弱依赖的抖动传递给用户接口。 稳定性设计的核心是四个词:分层、隔离、削峰、补偿。分层是把核心请求和后置任务拆开;隔离是不同优先级任务不用同一份线程和队列;削峰是用 MQ 和限流把瞬时压力变平;补偿是允许部分动作延迟或失败,但最终可恢复。 项目落地 第一,入口层做限流和核心链路瘦身。活动提交接口只做资格校验、幂等校验、投保意向落库和事件写入,不在请求内同步做短信、CRM、报表刷新。超过承载能力时,入口快速返回繁忙提示,避免请求堆在线程里。 第二,异步层按业务拆 topic、消费组和线程池。商机流转、通知、CRM 同步、结算明细生成、运营埋点分别使用独立消费组和线程池。商机流转优先级最高,通知和埋点可以延迟,结算任务限制并发并尽量放在低峰窗口。 第三,结算任务分片和限速。按月份、机构、产品、代理人或订单 ID 范围拆分结算批次。每个批次都有独立状态:待处理、处理中、成功、失败、超时。调度器不一次性提交全部任务,而是按窗口和并发阈值派发。写入结算明细时使用批量提交,但批次大小受数据库能力约束。 第四,数据库访问优化。结算扫描使用覆盖索引和游标分页,避免深分页和大事务。结算结果写入采用幂等键,例如 settleMonth + policyNo + agentId + rewardType。在线活动表和结算明细表避免在同一事务中互相等待。必要时读写分离或结算库分离。 第五,外部依赖治理。保司、CRM、短信通道都设置独立超时、重试和熔断。在线链路中外部依赖失败时进入异步补偿,不让用户接口长时间等待。结算中外部依赖尽量提前拉取快照,减少结算过程中实时调用。 第六,监控告警从技术指标扩展到业务指标。除了 CPU、内存、线程池、连接池、MQ 积压,还要监控投保提交成功率、商机分配延迟、结算批次完成率、失败批次数、状态超时数量、补偿成功率。稳定性不是机器没挂,而是核心业务按预期完成。 关键伪代码或流程 结算批次创建: public void createSettlementTask(String month) { List<ShardKey> shards = shardService.splitByOrgAndAgent(month); for (ShardKey shard : shards) { settlementTaskRepository.insertIgnore(SettlementTask.builder() .taskKey(month + ":" + shard.key()) .month(month) .shard(shard) .status("INIT") .build()); } } 任务抢占: ...

January 7, 2025 · 2 min · 214 words · WY