Spring事务底层原理与@Transactional失效场景排查指南
1. 先把“事务”这件事掰开揉碎1.1 事务的四个老朋友ACID聊Spring事务之前必须先说清楚“事务”本身是什么意思。大学教材上管它叫ACID四个字母分别是原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability。我用一个最经常被拿来举例的业务场景拆开讲假设用户在你的系统里发起一笔转账流程是从A账户扣款1000元往B账户加款1000元。这两步必须“要么都成功要么都别做”。如果扣款成功但加款失败这笔钱就凭空消失了用户第二天就会打电话找你。原子性保证的正是这一点事务内的所有操作看作一个不可分割的整体。隔离性稍微绕一点。两个并发事务同时操作同一张表时如果没有隔离机制就可能出现你读到一半的数据被别人改掉的尴尬场面。隔离级别就是用来约束这种并发访问的“交通规则”后文会展开讲。为什么事务必须在框架层面被管理因为数据库本身只认“开始事务”“提交”“回滚”这几条命令。拿MySQL来举例一条连接上的执行顺序是BEGIN或START TRANSACTION开启事务中间执行一系列SQL最后COMMIT提交或者ROLLBACK回滚。如果所有业务代码都直接跟这些底层命令打交道你的Service层会变成一片混乱到处都是重复的try-catch处理逻辑。Spring做的事简单来说就是把这套流程封装成了“声明式”的开关。你只需要在方法上标注一个注解框架就帮你把开启、提交、回滚全部处理掉不用每写一个业务方法都自己拼一遍事务命令。这种优雅背后实际上是AOP面向切面编程在默默干活。1.2 从JDBC手动事务到Spring自动管理说清楚Spring事务之前值得回头看两眼“原始时代”。假设你现在只用裸JDBCConnection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 关掉自动提交 // 执行扣款SQL statement.execute(update account set balance balance - 1000 where id A); // 执行加款SQL statement.execute(update account set balance balance 1000 where id B); conn.commit(); // 都执行成功才提交 } catch (SQLException e) { if (conn ! null) { conn.rollback(); // 出错了整体回滚 } } finally { if (conn ! null) { conn.close(); } }这段代码有几个非常让人头疼的地方。第一事务边界必须手动维护业务方法一多这些模板代码散落得到处都是第二每条业务逻辑都要自己写try-catch和rollback分支而且非常容易漏掉连接关闭第三如果业务里嵌入了多个DAO操作你得小心翼翼地保证它们用的是同一个连接否则事务会“断开”。Spring最初想解决的正是这三个问题。编程式事务由TransactionTemplate来处理模板代码声明式事务用AOP把事务逻辑彻底从业务逻辑中剥离出去。这两种方式各自有适用场景下面拆开讲。2. Spring事务的设计哲学把“复杂”留给框架2.1 编程式事务的救赎TransactionTemplate在Spring的早期体系中你不需要再手写裸JDBC的样板代码框架提供了PlatformTransactionManager接口来统一管理事务。最常见的实现是DataSourceTransactionManager它是围绕着DataSource设计的事务管理器专门用于JDBC和MyBatis这类走java.sql.Connection的数据访问方式。手动事务最标准的姿势是这样Service public class TransferService { private final TransactionTemplate transactionTemplate; public TransferService(PlatformTransactionManager transactionManager) { this.transactionTemplate new TransactionTemplate(transactionManager); } public void transfer(String fromAccountId, String toAccountId, BigDecimal amount) { transactionTemplate.execute(status - { try { accountDao.deduct(fromAccountId, amount); accountDao.add(toAccountId, amount); return null; } catch (RuntimeException e) { status.setRollbackOnly(); // 标记需要回滚 throw e; } }); } }TransactionTemplate把连接的获取、事务的开启、提交和回滚都包进去了你只需要往回调里塞业务逻辑。status.setRollbackOnly()这句就是“手动判死刑”告诉框架这个事务必须回滚别再想着提交了。这种写法比裸JDBC干净太多但仍然有一个致命问题它侵入到了业务代码里。如果你有几十个需要事务的方法每个方法里都要包一层transactionTemplate.execute这些样板代码依然存在只是从“数据库连接层”换到了“业务方法层”。2.2 编程式事务真的不够“香”吗肯定有朋友问既然编程式事务逻辑清晰、容易调试为什么主流项目都改用Transactional注解我的体会是编程式事务适合两类场景。第一类是事务边界不好用方法切分的时候你需要在同一个方法内部动态决定“这段要不要加进事务里”第二类是事务逻辑需要随运行时状态变化比如根据参数决定使用不同的事务管理器。但更多情况下你希望事务是“方法级别”的方法进入时开事务方法正常返回时提交抛异常时回滚。这种横切逻辑用编程式事务写在每个方法里本质上属于重复劳动而且特别容易在改代码时漏掉一层。声明式事务把这种横切关注点通过AOP统一收编一次配置处处生效。打个比方编程式事务就像你每天上班自己手动开关办公室的灯声明式事务就像给办公室装了感应灯你踏进门灯自动亮走出去灯自动灭。你根本不需要再操心“我今天进门有没有按开关”这件事。2.3 核心接口三件套理解Spring事务管理底层必须认识三个核心接口PlatformTransactionManager、TransactionDefinition、TransactionStatus。PlatformTransactionManager是所有事务管理器的总门户接口上定义了三个方法getTransaction(TransactionDefinition definition)负责获取事务状态commit(TransactionStatus status)负责提交事务rollback(TransactionStatus status)负责回滚事务。TransactionDefinition定义事务的“行为特征”包括传播行为Propagation、隔离级别IsolationLevel、超时时间Timeout、是否只读ReadOnly。它是一份配置档案告诉事务管理器这个事务该按什么规则来跑。TransactionStatus保存着当前事务的运行时状态。它封装了事务是否新建、是否存在保存点、是否已经被标记为rollback-only等信息setRollbackOnly()方法就是上面的代码里用到的那个。Spring的多个事务管理器实现比如DataSourceTransactionManager、JpaTransactionManager、HibernateTransactionManager分别对接不同数据访问技术。它们实现同一个接口于是上层业务代码不用关心底层用的是JDBC、JPA还是Hibernate迁移技术栈时事务逻辑不用大改。我实际做过一个老项目从Hibernate迁移到MyBatis的改造事务相关代码几乎没有动这就是接口抽象带来的直接好处。注意现代Spring Boot项目里如果你只引入了一个数据源框架会自动帮你配置好DataSourceTransactionManager你不需要手动注册Bean。只有在多数据源、强制指定某种事务管理器时才需要显式声明。3. 声明式事务的底层真相AOP在背后做了什么3.1 一句话说清楚AOP与声明式事务的关系声明式事务之所以能“一点注解就生效”核心是Spring AOP做了一个“代理对象”。当你把一个Bean交给Spring容器管理后容器在创建Bean的过程中发现目标类上有Transactional注解就会生成一个代理类。这个代理类看起来和原来的类一样但所有公开方法被调用时都会先经过一个拦截器链。在事务场景下这个拦截器就是TransactionInterceptor。业务方法本身完全不感知事务的存在就像演员不关心摄影机的机位一样代理负责了拍摄的所有协调工作。如果JDK动态代理和CGLIB代理这个概念听着陌生不需要太有压力。只需记住Spring容器中你拿到的UserServiceBean未必就是你写的那个类的直接实例在这条背后站着一个隐形的代理。方法上的Transactional能不能生效完全取决于这个代理有没有被正确创建和调用。3.2 事务拦截器的三个关键步骤TransactionInterceptor的invoke方法干了三件事第一步判断当前方法是否配置了事务属性也就是从Transactional里解析出来的传播行为、隔离级别、超时时间等。如果没有配置直接调用目标方法跟没加注解一样如果配置了走第二步。第二步调用TransactionAspectSupport的核心逻辑通过事务管理器获取事务状态。这里的getTransaction会根据传播行为决定是加入当前事务、新建事务还是挂起当前事务。拿到事务状态之后它会把事务信息绑定到当前线程上这里用到的就是TransactionSynchronizationManager。第三步执行业务方法。方法正常返回就提交事务方法抛出异常就根据rollbackFor的配置判断是否需要回滚。注意这里的“异常类型匹配”有讲究默认情况下只有RuntimeException和Error触发回滚受检异常checked exception不会触发回滚。这个默认行为和很多人直觉不一致后文单独讲怎么避坑。整个流程用代码描述大概是这样的伪代码public Object invoke(MethodInvocation invocation) throws Throwable { // 1. 解析事务属性 TransactionAttribute txAttr getTransactionAttribute(invocation); // 2. 获取事务管理器并开启事务 PlatformTransactionManager tm getTransactionManager(); TransactionInfo txInfo createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal null; try { // 3. 执行真正的业务逻辑 retVal invocation.proceed(); } catch (Throwable ex) { // 4. 异常时回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // 5. 正常返回时提交 commitTransactionAfterReturning(txInfo); return retVal; }这段流程透露了一个很重要的细节invocation.proceed()是在事务已经开启之后才调用的。所以你在业务方法里通过TransactionSynchronizationManager.isActualTransactionActive()判断当前是否存在事务返回的一定是true。3.3 一个从概念到代码的完整推演把理论落在代码上看一个常见的转账例子Service public class AccountService { Autowired private AccountDao accountDao; Transactional(rollbackFor Exception.class) public void transfer(String fromId, String toId, BigDecimal amount) { accountDao.deduct(fromId, amount); accountDao.add(toId, amount); } }当外部调用方调用accountService.transfer(...)时真实发生的过程是调用方持有的是AccountService的代理对象代理对象的方法入口触发TransactionInterceptor拦截器解析出Transactional的配置——传播行为默认REQUIRED、隔离级别默认使用数据库默认、需要回滚的异常是Exception及其子类。拦截器随后调DataSourceTransactionManager.getTransaction()从DataSource里拿一个连接执行conn.setAutoCommit(false)把连接绑定到当前线程的ThreadLocal里。下面进入业务方法accountDao.deduct()执行时MyBatis从当前线程拿到那个还没提交的连接执行扣款SQLaccountDao.add()同理。两步都执行完方法正常返回拦截器执行commit连接上的commit()被调用数据落库连接被释放。中间任何一步抛了Exception拦截器的completeTransactionAfterThrowing判断异常类型匹配后走rollback逻辑数据库撤销全部未提交的更改。业务代码里自始至终没有一行事务API调用这就是“声明式”三个字的精髓所在。4. Transactional的核心参数你手里握着哪些旋钮4.1 七种传播行为逐个过一遍传播行为是声明式事务中最容易绕晕的配置项。我的建议是先重点搞懂最常用的两个REQUIRED和REQUIRES_NEW其他几个偶尔见到知道什么意思即可。REQUIRED默认值表示“如果当前存在事务就加入当前事务如果当前没有事务就新建一个事务”。这符合大多数业务场景。外层方法配置了事务内层方法也标注了Transactional(propagation Propagation.REQUIRED)内层方法不会新开事务而是直接复用外层的事务。如果内层方法抛了异常整个外层方法的事务都会回滚。REQUIRES_NEW表示“无论如何都新建一个事务如果当前已经存在事务则把当前事务挂起”。这个配置适合“记录日志、审计”等场景——即使主业务失败回滚操作日志也需要写进数据库。注意这里挂起的含义外层事务会暂停等内层新事务结束后再恢复。两者之间互不影响内层事务的回滚不会拖累外层事务已执行的操作。其他几种传B播行为SUPPORTS表示当前有事务就加入没有就非事务执行MANDATORY要求当前必须已经存在事务否则直接抛异常NOT_SUPPORTED强制以非事务方式执行如果有事务先挂起NEVER强制以非事务方式执行如果有事务抛异常NESTED是“嵌套事务”有别于REQUIRES_NEW它利用数据库的保存点savepoint实现部分回滚只有在底层数据库支持保存点时才能用。我见过不少团队把REQUIRES_NEW当成万能药不管什么场景都加。实际上弄错传播行为很容易引发“大事务里套着小事务”的诡异问题比如外层事务已经锁了某行数据内层REQUIRES_NEW又去更新同一行直接死锁。传播行为的选择依据应该是“事务的独立性要求”而不是“感觉加一个更保险”。传播行为当前有事务时当前无事务时常见使用场景REQUIRED加入当前事务新建事务常规业务操作默认选择REQUIRES_NEW挂起当前新开事务新建事务日志记录、消息推送等必须独立的操作SUPPORTS加入当前事务非事务执行查询方法MANDATORY加入当前事务抛异常强制要求调用方提供事务NOT_SUPPORTED挂起当前非事务执行不需要事务的批量操作NEVER抛异常非事务执行严禁事务环境NESTED创建保存点嵌套执行新建事务需要部分回滚的场景4.2 隔离级别到底怎么选隔离级别解决的问题是四个字并发冲突。SQL标准定义了四种隔离级别从宽松到严格排列。READ_UNCOMMITTED最低允许一个事务读到另一个事务尚未提交的数据也就是脏读。实际项目里基本没人用除非你对数据一致性完全无所谓。READ_COMMITTED只能读到已提交的数据解决了脏读问题但从一个事务内部两次相同查询可能得到不同结果这就是不可重复读。REPEATABLE_READ在MySQL里是比较有意思的一档它通过MVCC机制让一个事务内的重复读结果保持一致解决了不可重复读问题但依然可能产生幻读指查询满足条件的记录集合发生了变化。SERIALIZABLE最高事务串行执行各种并发问题都没有了代价是并发性能极低。Spring的Transactional上配置隔离级别时默认Isolation.DEFAULT表示使用底层数据库的默认隔离级别。MySQL默认是REPEATABLE_READSQL Server和Oracle默认是READ_COMMITTED。我特意提一句很多人在Transactional里写isolation Isolation.SERIALIZABLE结果压测时发现吞吐量断崖式下跌。隔离级别不是越高越好要结合业务容忍度来权衡。读多写少的报表类查询可以放心用READ_COMMITTED资金类强一致业务才需要SERIALIZABLE或悲观锁兜底。4.3 readOnly、timeout与rollbackFor的配合技巧readOnly true是一个特别容易让人误解的属性。它并不是强制数据库“禁止写入”而是给底层框架一个“优化提示”。对于MySQL和MyBatis体系设置readOnly后Spring会把连接设置为只读模式有些数据库驱动会跳过某些锁或者走只读路由从而提升查询性能。但它不是万能护身符如果你在只读事务里执行了更新操作数据库会直接报错。timeout参数控制事务的最大执行时间单位是秒。一旦事务运行超过这个时间框架会强制回滚。这个配置对线上问题非常有价值避免一个慢SQL把数据库连接池拖垮。我处理过一个异常案例一个后台报表导出方法没有设置超时时间结果上游某个表被锁事务长时间悬着连接池被占满整条链路被打挂。后来全局统一加了合理超时时间问题立刻缓解。rollbackFor是我最希望所有开发者都认真看一眼的参数。前面提到默认情况下只有RuntimeException和Error会触发回滚。如果你在业务里封装了统一的BizException extends Exception受检异常然后在一个事务方法里抛出它事务是不会自动回滚的。要么把自定义异常改成继承RuntimeException要么在注解里写明rollbackFor Exception.class。这是线上事务“不回滚”的最常见原因之一。关键提示异常会被拦截器捕获但这有个前提——异常必须从被代理的方法边界抛出。如果你在方法内部用try-catch把异常吞掉了拦截器看到的是“方法正常返回”它会直接提交事务数据照样落库。5. 事务不生效十有八九踩了这几个坑5.1 经典中的经典自调用导致事务失效这是一个排查频次极高的问题。同一类内部的直接方法调用不经过代理对象于是Transactional形同虚设。Service public class OrderService { public void createOrder(Order order) { // 调用本类方法的内部事务方法 this.updateStock(order); } Transactional public void updateStock(Order order) { // 更新库存 stockDao.decrease(order.getProductId(), order.getQuantity()); } }createOrder()调this.updateStock()时this是原始目标对象而不是Spring生成的代理对象。注解配置的事务属性根本没机会被TransactionInterceptor读取到事务自然不生效。如果updateStock执行失败前面已经写过的订单数据不会回滚。解决方案有几种。第一种拆开类把事务方法放到另一个Spring管理的Bean里通过注入的Bean调用第二种从容器中获取代理对象调用比如在类中注入ApplicationContext用getBean(OrderService.class)获得代理第三种在当前类上Autowired自己这个Bean或者使用Lazy注入来避免循环依赖。用三种方案时我见过不少朋友踩循环依赖的坑。如果你在OrderService里注入了自己记得加Lazy否则Spring初始化时可能出现代理创建顺序上的问题。5.2 方法可见性与异常处理看似无关实则致命Transactional标注方法的可见性也会影响事务生效。Spring的默认代理方式中只有public方法上的事务注解会被识别。private、protected、package方法上的注解会被直接忽略Spring启动时还不会给任何警告提示这坑有多深只有踩过的人才知道。Java注解对private方法在反射层面确实可以读取到但Spring AOP要生成一个子类代理来拦截方法调用private方法是不能被子类重写的于是拦截器根本插不进去。如果你用的是CGLIB代理对非public方法的处理更复杂结论一样不生效。异常被吞的问题我放在这节再强调一次Transactional public void pay(Order order) { try { accountDao.deduct(order.getAmount()); // 假设这里抛了SQLException } catch (Exception e) { log.error(扣款失败, e); // 异常被吞没有重新抛出 } }方法看起来执行完毕了事务被提交扣款SQL如果被数据库回滚了也无所谓但如果数据库层面没有抛错而业务后续逻辑失败了就会出现资金账不平。正确的做法有两种要么先记录日志然后重新抛出异常要么在catch块里调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()强制标记回滚这种方式对调用方不透明我还是建议重新抛出异常。5.3 类未被Spring管理、多线程与异常类型匹配问题Transactional注解必须作用在Spring容器管理的Bean方法上。如果你直接用new关键字创建了一个Service对象再调用它的事务方法代理对象压根不存在事务完全不生效。这种情况常在工具类、定时任务里出现排查时看一眼调用链路就能发现。多线程是另一个冷门陷阱。Spring事务基于ThreadLocal绑定资源子线程默认拿不到父线程的事务绑定。比如你用一个ExecutorService异步执行某个事务方法这个子线程里的“事务”实际上是独立的甚至可能根本没有事务。要处理跨线程事务传播要么用TransactionTemplate在这个子线程里手动开启要么把需要在一个事务里完成的操作全部放在同一线程内执行。异常类型匹配的默认行为上面已经提过这里补充一个容易混淆的点rollbackFor Exception.class配上去之后是不是所有异常都会回滚答案是不一定异常如果发生在代理方法执行之前比如参数校验阶段抛出的异常此时事务还没有开启谈何回滚。另外如果异常被内层方法捕获并转换成了另一种异常类型抛出最终能否触发回滚要看最终抛出异常的类型是否匹配rollbackFor。6. 排查事务问题的现场实录与经验清单6.1 如何确定事务到底有没有生效我排查线上问题时第一步永远不是看代码而是确认“这个事务真的开起来了吗”。最实用的方法是开启Spring的日志输出。在application.yml里加上logging: level: org.springframework.transaction: DEBUG org.springframework.jdbc.datasource: DEBUG然后看控制台日志事务提交或回滚时会出现类似这样的一行Participating transaction failed - marking existing transaction as rollback-only出现rollback-only标记时说明有内层事务把外层事务标记成了只能回滚。这种情况下即使外层方法正常return提交时也会抛UnexpectedRollbackException。这个现象经常出现在内层方法自己catch了异常但没有重新抛出事务管理器却把它标记为rollback-only的场景。这是Spring事务体系里最令人困惑的一个行为没有之一。第二个排查思路是直接代码里打探boolean actualTransactionActive TransactionSynchronizationManager.isActualTransactionActive();在方法里打印这个值如果为false说明当前方法根本没有运行在事务中。这个方法在调试验证时非常好用能立刻定位“事务没开启”还是“事务开了但没按预期回滚”。6.2 回滚失效的经典现场我处理过一个非常典型的场景某商家对账系统settle()方法里调用了两个事务方法deductServiceCost()和calculateCommission()第一个成功第二个抛了一个受检异常CalcException。代码看起来很正常两个事务方法都是Transactional(propagation Propagation.REQUIRED)settle()自身也标注了Transactional。结果线上发现deductServiceCost()的扣款被提交了但calculateCommission()的佣金计算没有入账。排查下来有两个问题叠加。第一CalcException继承了Exception默认不回滚第二calculateCommission()内部自己没有catch异常由外层settle()方法吞掉并做了降级处理。修复其实很快统一让自定义业务异常继承RuntimeException同时全局配置Transactional(rollbackFor Exception.class)。最关键的是宣告一个团队规范事务方法内部禁止catch所有异常后不重新抛出要么记录日志后重新抛出要么让调用方处理。另外还有一个高频问题事务方法里顺序执行多次数据库操作中间有一步是远程调用或消息发送。远程调用耗时很长事务一直被拖着不提交后续锁竞争激烈。业内常规做法是把远程调用移到事务方法外部事务里只做纯粹的数据操作。以前遇到过一个接口P99延迟从200ms涨到2s排查发现就是有一个事务方法里同步调了短信服务接口对方响应慢导致数据库连接被长期占用。6.3 长期实践沉淀下来的经验清单先整理一份避坑速查表方便各位朋友贴在工位上问题现象可能原因解决方案事务方法没走代理自调用this调用方式绕过了代理拆分Bean、注入代理对象事务没回滚受检异常默认不触发回滚配置rollbackFor或继承RuntimeException事务没回滚异常被catch吞掉重新抛出或标记setRollbackOnly事务没开启类不是Spring管理的Bean检查是否使用了new创建对象子线程方法事务不生效ThreadLocal不跨线程传递子线程内手动开启事务抛UnexpectedRollbackException内层事务标记rollback-only内层异常统一向上抛出连接被长期占用事务方法里做远程调用把远程调用移出事务方法再补充几条更细节的经验第一事务方法尽量设计得短而快。事务粒度太大所有并发操作都在抢最长时间的锁后期优化无从下手。相反事务粒度太小一个业务逻辑拆成十几个事务方法中间一旦失败前面已完成的操作无法回滚业务一致性就崩了。事务边界的划分能力是区分资深开发和初级开发的重要标志之一。第二如果使用了Transactional尽量别再用编程式事务混搭。两种模式叠加时容易出现在同一线程内重复获取事务导致行为不符合预期的情况。除非确定要用的场景比如某些异常情况下需要“手动回滚”否则保持编程方式统一混用会让你在排障时心力交瘁。第三Transactional尽量放在实现类的方法上而不是接口方法上。Spring官方文档多年来一直在强调基于接口的代理存在“注解不被继承”的问题如果配置了JDK动态代理放在接口方法上的注解在某种边界条件下无法被正确解析。放在实现类上无论代理用的是JDK还是CGLIB都能稳定生效。第四事务超时时间需要结合实际SQL执行时间设置。我见过一个项目全局统一配置了超时10秒但管理后台有个统计接口正常就要跑15秒上线后频繁抛超时回滚。事务超时应该精细化到不同接口上而不是一刀切。第五对于大事务里嵌套调用REQUIRES_NEW的写法要格外小心。内层REQUIRES_NEW提交的事务不随外层一起回滚如果外层后续失败会出现“部分数据已提交、部分数据已回滚”的残局。设计阶段就该画清楚事务边界而不是靠后期补丁。最后再分享一个我常用的调试小技巧在测试环境开启事务相关日志写一个只有Transactional和一条更新语句的最小化测试接口跑一遍看日志。如果最小化场景能正常回滚问题一定出在调用链的异常处理或方法嵌套上如果不能正常回滚问题出在框架配置或Bean代理创建上。分而治之比“盯着代码猜”高效得多。7. 最终章事务设计的几个个人感悟做后端这些年事务相关的问题排过很多给我的总体感受是Spring的声明式事务把“开启、提交、回滚”这三个动作藏得很深使用门槛低但理解门槛不低。真正把一个系统的事务处理好需要你同时理解数据库锁、AOP代理机制、异常传播路径三条线。任何一个环节掉链子表现出来的都是“数据不对”。我个人的建议是团队里每个写Transactional的人都该花时间搞明白两件事第一访问代理对象的到底是什么第二自己抛出的异常最终会怎么被处理。这两点想清楚了事务失效这类问题基本能防住大半。如果你愿意再进一步看一遍TransactionInterceptor和TransactionAspectSupport的源码会比任何博客文章都来得深刻。有个习惯对我帮助很大每次评审代码时只要看到Transactional就条件反射般地问三个问题这个事务的边界是什么哪些异常会触发回滚有没有远程调用被包在事务里。这三问用于代码自审查也用于团队互审能拦截掉大量线上事务隐患。事务是业务系统数据一致性的最后一道水坝。这道坝能不能守得住靠的是每个工程师对底层机制的理解而不是靠框架替你兜底。如果你读到这里至少已经把Spring事务的底细摸清楚了七成剩下来的三成交给真实的业务故障去打磨。

相关新闻

DAY70:前端Leader转型AI Agent工程师的认知跃迁

DAY70:前端Leader转型AI Agent工程师的认知跃迁

1. 为什么“DAY70”这个数字比“AI Agent”更值得深挖看到标题里那个醒目的“DAY70”,我第一反应不是去查AI Agent的最新论文,而是下意识翻开了自己三年前的项目日志——那会儿我正带一个五人前端团队,同时在啃LangChain源码、调试RAG pipeli…

2026/10/12 4:03:26 阅读更多 →
Kubernetes离线部署CoreDNS v1.8.0镜像导入与DNS解析实战

Kubernetes离线部署CoreDNS v1.8.0镜像导入与DNS解析实战

简介:coredns_v1.8.0.tar.gz 面向 Kubernetes 集群运维与部署人员,提供 v1.8.0 版本的 CoreDNS 镜像离线包,适用于 k8s v1.21.2 环境,可解决内网或受限网络下无法拉取官方镜像、集群 DNS 组件部署受阻的问题。压缩包共 8 个文件&a…

2026/10/12 4:03:26 阅读更多 →
iOS原生侧滑菜单实现:手势、布局与生命周期协同

iOS原生侧滑菜单实现:手势、布局与生命周期协同

简介:本资源是一份面向iOS初中级开发者的侧滑菜单栏实现方案,聚焦于点击按钮触发View位移动画的轻量级交互设计,适用于需要快速集成导航菜单或功能入口的App项目。压缩包共25个文件,包含7个Objective-C实现文件(.m/.h&…

2026/10/12 4:03:26 阅读更多 →

最新新闻

沟通管理万金油:信息系统项目管理师案例分析实战答题框架

沟通管理万金油:信息系统项目管理师案例分析实战答题框架

1. 为什么“沟通管理”是案例分析里的万金油先聊点实在的:信息系统项目管理师下午案例分析,很多人最怕的就是“不知道怎么答题”。我也经历过那个阶段——背了十大管理的过程,刷了几十道真题,一上考场看到那些描述项目现场混乱的案…

2026/10/12 4:48:50 阅读更多 →
叮当小宝CS管理篇:客服日报怎么写?五个字段让主管一眼看懂

叮当小宝CS管理篇:客服日报怎么写?五个字段让主管一眼看懂

写得越长的日报,越没人认真看。主管要的不是过程记录,而是「今天哪里需要我出手、明天哪里可能出问题」。一份有用的客服日报,五个字段就够:接待量、首响、未回、异常单、待跟进。这篇把五个字段的写法和反面案例一起说清。先记住…

2026/10/12 4:48:50 阅读更多 →
STM32开发之UART串口编程从入门到实战:协议与标准外设库实现详解

STM32开发之UART串口编程从入门到实战:协议与标准外设库实现详解

1. 什么是串口(UART)串口是嵌入式开发中最常用的通信接口之一。UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)是一种异步串行通信协议,它通过一根数据线逐位(bit&…

2026/10/12 4:48:50 阅读更多 →
STM32开发之IIC通信详解:从协议基础到FM24CL04实战

STM32开发之IIC通信详解:从协议基础到FM24CL04实战

1. IIC概述IIC(Inter-Integrated Circuit,集成电路间总线)是一种由飞利浦公司(现恩智浦半导体)开发的两线式串行通信总线,广泛应用于微控制器与各类外设(如 EEPROM、传感器、RTC 时钟芯片等&…

2026/10/12 4:48:50 阅读更多 →
一篇文章教会你:用“经纬度思维”看懂比特币链上所有数据

一篇文章教会你:用“经纬度思维”看懂比特币链上所有数据

一、为什么你需要“经纬度思维”?很多人看比特币链上数据,第一反应是“太复杂了”。地址、区块、交易、手续费、活跃地址、算力……几十个指标堆在一起,越看越乱。其实,链上数据就像一张地图。地图上有经度和纬度,帮你…

2026/10/12 4:48:50 阅读更多 →
agent-skills实践:打造可复用、可校验的Agent技能系统

agent-skills实践:打造可复用、可校验的Agent技能系统

开头这几年做大模型应用开发,大家应该都感觉到了:单纯的 Prompt 工程已经撑不起复杂业务,真正难的是让 Agent 在真实环境里稳定地干活。“agent-skills”这个项目就是冲着这个痛点来的——它把 Agent 的能力拆解成一个个可复用、可管理、可验…

2026/10/12 4:47:50 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →