三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

地方网站 o2o网页打不开被拦截了怎么办

地方网站 o2o网页打不开被拦截了怎么办 地方网站 o2o,网页打不开被拦截了怎么办,建设工程消防备案凭证网站,佛山网站建设公司-印象互动上篇文章解决了同一个类里方法调方法不走代理的问题——你在方法 A 上加 @Transactional,调本类方法 B,B 的事务配置被无视,退化为 outer 事务的一部分。 现在你知道了,把方法 B 抽到另一个 Service,… 上篇文章解决了"同一个类里方法调方法不走代理"的问题——你在方法 A 上加 @Transactional,调本类方法 B,B 的事务配置被无视,退化为 outer 事务的一部分。 现在你知道了,把方法 B 抽到另一个 Service,给它加上 @Transactional(propagation = Propagation.REQUIRES_NEW),于是 B 应该有一个"全新独立"的事务。 这个直觉只对了一半。REQUIRES_NEW 确实是新事务,但它不是"免费"的。每嵌套调用一次,你的连接池可能被吃掉两份连接。 更隐蔽的是,如果 B 和 A 共享同一个 EntityManagerFactory,B 的回滚可能顺手把 A 的 PersistenceContext 也污染了——而你还在开心的 catch 异常,以为万事大吉。 REQUIRES_NEW 最贵的代价不是创建新事务,是外层连接挂起期间——它不吃不喝也不还。 【现场】一个 for 循环,HikariPool 爆了 事故全景 去年有一个线上工单:批量导入接口在数据量超过 50 条时随机超时。 代码结构是这样: // OrderImportService.java @Service public class OrderImportService {private final OrderValidator validator;@Transactionalpublic void batchImport(ListOrderImportRequest requests) {for (OrderImportRequest req : requests) {try {validator.validateAndMark(req); // REQUIRES_NEW} catch (InvalidOrderException e) {log.warn("跳过无效订单: {}", req.getOrderNo());}}} }// OrderValidator.java @Service public class OrderValidator {@Transactional(propagation = Propagation.REQUIRES_NEW)public void validateAndMark(OrderImportRequest req) {// 读取配置表 + 校验规则 + 写入标记orderConfigRepo.findByType(req.getType());validationRepo.save(mark(req));} }团队在 validateAndMark 上用了 REQUIRES_NEW,期望的是每条订单独立提交:校验失败的订单回滚自己的记录,不影响其他订单。 但线上跑 50 条订单时,到第 30 条左右接口就 hang 住了。 异常日志maxPoolSize=10,active=10,idle=0——池子吃满了。 开发者第一反应:"连接池不够,调大。" 但调大到 20 后,能处理的条数从 30 条涨到了 60 条,依然有上限。 问题在哪?问题不在池子小,在 REQUIRES_NEW 每个循环吃 2 个连接:时机 OrderImportService 事务(outer) OrderValidator 事务(inner)第 1 条订单 持连接 C1,挂起 取连接 C2,执行,释放 C2第 2 条订单 恢复 C1,再挂起 取连接 C3,执行,释放 C3... 循环 每次一条新连接第 k 条订单 C1 一直没还 取连接 C_C1 被外层事务 挂起但没有归还连接池。每个并发的导入请求都占着 C1 不放,再吃一条临时连接给内层。10 个并发请求同时导入 → 每个请求占 C1 + C_临时 → 20 条连接被申请。maxPoolSize=10 时,10 个请求只有 5 个能拿到内层连接,其余 5 个的 inner 方法阻塞等待。 【拆解】REQUIRES_NEW 的三个隐形成本 陷阱一:挂起 = 持连接不还这是最容易被忽略的代价。DataSourceTransactionManager 处理 REQUIRES_NEW 的核心逻辑:关键行在 suspend → begin 之间:C1 只是从线程解绑,物理连接还在,没有归还连接池。这意味着 N 层嵌套 = N 个连接同时被占用。 这也是为什么调大连接池能缓解但治标不治本——连接池再大,也架不住每个请求按嵌套层数吃多份。 陷阱二:JPA PersistenceContext 污染——更隐蔽的连锁反应 假设你的代码涉及 JPA/Hibernate: @Transactional public void outerMethod(Long orderId) {Order order = orderRepo.findById(orderId).orElseThrow();order.setStatus("PROCESSING");// Hibernate 此时已跟踪 order 为 "脏" 状态auditService.auditChange(order); // REQUIRES_NEW }这里有个微妙的问题:Spring JpaTransactionManager 处理 REQUIRES_NEW 时,外层 EntityManager 被挂起,内层创建一个新的 EntityManager。内层执行完提交或回滚后,内层 EM 关闭,外层 EM 恢复。 但 Hibernate 的 PersistenceContext 是有状态的。如果内层方法操作了和外层相同的实体对象:外层加载了 Order(managed 状态) 内层也加载了同一个 Order,做了修改 内层回滚 → 内层 EM 的 PersistenceContext 清空 外层恢复后 flush → Hibernate 尝试把外层的 order 变更写回数据库 但此时一级缓存中的 order 可能因为内层的操作而处于不一致状态最典型的现象是 StaleObjectStateException 或 ConstraintViolationException,但错误堆栈指向的是外层方法最后一行,让人完全联想不到是内层 REQUIRES_NEW 的回滚导致的。 陷阱三:REQUIRES_NEW 在同类方法上退化为 REQUIRED(上一篇文章的延续) 这个问题和上篇文章本质相同——REQUIRES_NEW 也必须走代理才能生效。 @Service public class OrderService {@Transactionalpublic void process(Order order) {saveOrder(order);// 这里直接调本类方法deductStock(order); // REQUIRES_NEW? 不,退化为 REQUIRED}@Transactional(propagation = Propagation.REQUIRES_NEW)public void deductStock(Order order) {stockRepo.deduct(order.getProductId(), order.getQuantity());} }Spring AOP 代理的工作机制决定了:process() 内部直接调 deductStock(),走的是 this.deductStock(),不是代理对象 → 事务拦截器不触发 → propagation 配置被无视 → 退化为外层事务(REQUIRED)。 你说:"我把 deductStock 抽到别的 Service 了,REQUIRES_NEW 生效了。那第一个陷阱还适用吗?" 适用。REQUIRES_NEW 生效 + 连接翻倍,是两个独立问题。生效解决的是"事务隔离",翻倍解决的是"资源消耗"。 【处方】选型决策树 + 正确配置 REQUIRES_NEW 不是银弹你的需求 用哪个 原因外层失败时内层也回滚 REQUIRED(默认) 一个事务,全部回滚,无额外连接开销内层失败时不影响外层 NESTED(JDBC savepoint) 只回滚 savepoint 部分,外层继续,不额外吃连接内层需要完全独立提交/回滚(不受外层影响) REQUIRES_NEW 独立事务,但注意连接翻倍批量处理,每条记录独立提交 TransactionTemplate + 手动控制 比 REQUIRES_NEW 更省连接,控制更精确关键区别: NESTED 用 savepoint 实现,不申请新连接;REQUIRES_NEW 挂起外层 + 取新连接,连接成本翻倍。 当必须用 REQUIRES_NEW 时 如果业务真的需要完全独立的事务(比如审计日志必须单独提交,不随主事务回滚): 方案 A:增大连接池余量 spring.datasource.hikari.maximum-pool-size = (预期并发数 × 嵌套层数) + 其他连接消耗例:20 并发 × 2 层嵌套 + 5 条管理查询 = 45。但这不是线性增长的——还要算上每个请求的执行时间。 更靠谱的方案 B:内外层用不同的连接池 配置两个 DataSource,内层专用一个较小的池,避免干扰主流程的连接。 spring:datasource:primary:hikari.maximum-pool-size: 20audit:hikari.maximum-pool-size: 5@Transactional(value = "auditTransactionManager",propagation = Propagation.REQUIRES_NEW ) public void auditLog(...) { ... }最推荐的方案 C:能用 TransactionTemplate 就别用 REQUIRES_NEW @Service public class BatchProcessor {private final TransactionTemplate txTemplate;public void batchProcess(ListItem items) {for (Item item : items) {try {txTemplate.execute(status - {processOneItem(item);return null;});} catch (TransactionException e) {log.error("处理失败: {}", item.getId());}}} }TransactionTemplate 本质是每次新建事务,和 REQUIRES_NEW 效果一样,但:没有"外层挂起"的开销——因为没有外层事务 连接用完即还,不会长时间持有 捕获异常后不会出现 UnexpectedRollbackException快速自查:REQUIRES_NEW 用对了没有? □ 目标方法是否在另一个 Service 中?(同类调用会退化为 REQUIRED) □ 调用方是否真的需要内层完全独立?(NESTED 可能更省连接) □ 连接池是否已按嵌套翻倍估算?(n 层嵌套 = n 倍单层连接消耗) □ 如果有 JPA,内层是否操作了和外层相同的实体?(可能导致 PersistenceContext 污染)【雷标】🔴 搜索你的项目 搜索关键词 propagation = Propagation.REQUIRES_NEW @Transactional(propagation = Propagation.REQUIRES_NEW) REQUIRES_NEW搜索后检查REQUIRES_NEW 方法是否被另一个 @Transactional 方法调用? → 逐层数嵌套深度,评估连接翻倍风险 REQUIRES_NEW 方法是否和调用方在同一个类中? → 退化为 REQUIRED,事务配置等于白写 REQUIRES_NEW 方法和调用方是否操作了相同的实体? → 有 PersistenceContext 污染风险,考虑提取独立 DataSource 批量场景是否一定要用 REQUIRES_NEW? → 试试 TransactionTemplate 或 NESTED,连接消耗降低 50%+
← 返回列表