首页/新闻资讯/正文详情

Spring事务范围优化:从连接池告警到@Transactional边界拆解

发布时间:2026/9/25 8:33:22 来源:云帆数科 栏目:资讯中心
Spring事务范围优化:从连接池告警到@Transactional边界拆解
前阵子我们订单服务半夜突然告警数据库连接池被打满接口大面积超时。一开始以为是数据库慢查询结果翻慢日志什么都没抓到。后来查了information_schema.innodb_trx才发现罪魁祸首是一个挂了接近四十秒的事务——它把一个库存远程调用、一条短信发送、几行审计日志写入全部塞进了同一个Transactional方法里。那天之后的很长一段时间我都在跟“事务范围”这件事较劲。这篇博文就是想把这段时间的排查过程、背后原理和优化手段一次性讲清楚适合那些天天用Transactional但很少认真琢磨事务边界的开发者尤其是刚接触 Spring 事务、或者正在做接口性能优化的朋友。我先把结论放前面事务范围优化的核心不是“少写几个注解”而是把事务边界、方法边界和业务边界三者拆开看。很多线上事故不是因为代码写错了而是因为一个事务里塞了太多不该塞的东西。1. 从一次连接池告警说起大事务是怎么拖垮系统的1.1 事故现场与初步定位那次事故的代码逻辑其实很常见创建一个订单接口里面做五件事写订单主表、写订单明细、远程扣减库存、发送通知短信、记录审计日志。看起来每一步都是“必要操作”所以很自然地写在了同一个方法里方法上顶着一个Transactional。上线半年都没出问题直到某次大促流量上来库存服务的响应时间从 20ms 涨到 2 秒短信通道也开始抖动然后整个订单接口就崩塌了。这里有一个很多人忽略的资源绑定关系一个 Spring 事务从开启到提交会一直占有一个数据库连接。连接不是用完就还而是要等整个事务commit或rollback才会释放。当远程调用在事务中间耗时 2 秒那这条连接就白白挂 2 秒如果远程调用超时设置是 10 秒连接就要挂 10 秒。连接池的常用配置maximum-pool-size也就 20 到 50一个接口并发一上来连接瞬间被耗光后续所有需要数据库连接的业务全部排队。我后来喜欢用一个比喻来向组里新同事解释这件事事务就像你去银行柜台的整个办理过程你在等一个远程审批电话的时候柜员不能去服务下一个人座位也只有那么多。真正高效的做法是凡是能事后确认的事情都别占用柜台时间。1.2 事务的本质方法边界就是事务边界Spring 声明式事务之所以好用是因为它把“事务开关”藏进了 AOP 代理里。你写一个方法加上Transactional代理会在方法执行前开启事务方法正常返回后提交抛异常就回滚。问题恰恰出在这事务的边界被绑定到了方法边界上。方法越长方法内做的事情越多事务范围就越大。一个事务从 begin 到 commit 之间到底持有哪些东西首先是那一条物理数据库连接其次是所有被修改行的行锁、间隙锁再次是 undo log 中记录的前镜像。行锁和间隙锁会直接影响其他事务的读写事务的持续时间越长锁被持有的时间就越长别人等待锁的概率就越高。还有一个容易被忽略的点在REPEATABLE READ隔离级别下事务内的第一条SELECT会创建快照事务持续越久快照读取的数据越接近“过去”业务上可能出现读到旧数据的情况。所以排查这类问题有一个基本思路列出事务方法内每一个操作的耗时把所有耗时不稳定的操作远程调用、外部 IO、大循环排除到事务之外。事务内只留下“必须跟主记录一起成功或一起失败”的数据库写操作。2. 事务边界的幕后规则传播机制与代理陷阱2.1 传播行为决定了事务粒度要优化事务范围先得理解 Spring 是怎么处理方法嵌套的。默认的传播行为是REQUIRED它的语义是如果当前线程已经存在一个事务就加入这个事务如果没有才新开一个。这意味着你调用了一个带Transactional的 B 方法而 B 方法本身也有Transactional两个方法会合并成同一个事务。这就出现了一个很隐蔽的问题。假设 A 方法是纯查询逻辑但是因为业务需要调用了 BB 里面做了一次更新并提交由于传播行为是REQUIREDB 的提交并不会真的提交而是要等 A 方法整个返回后一起提交。在调用链很深的时候事务范围会被层层扩大比单独看任何一个方法时看到的都要大。传播行为行为说明适用场景REQUIRED有事务则加入没有则新建默认最常用REQUIRES_NEW挂起当前事务新开一个独立事务长流程分批处理、独立子任务NESTED利用 savepoint 实现嵌套事务部分失败回滚到保存点SUPPORTS有事务就加入没有就以无事务方式执行只读查询方法NOT_SUPPORTED以无事务方式执行挂起当前事务事务内不想占连接的操作优化时最常见的一个误区是盲目加REQUIRES_NEW。它的代价是外层事务被挂起如果内层事务提交了但外层事务后续失败回滚内层事务的数据并不会跟着回滚会造成数据不一致。而且它需要额外占用一个数据库连接在高并发场景下反而会加剧连接池的压力。我在下文会给出一个相对安全的用法。2.2 自调用与代理失效事务没生效的常见原因事务范围优化的前提是事务本身是有效的。很多人踩过一个坑同一个类里一个普通方法调用另一个Transactional方法事务完全不生效。原因不复杂Spring 事务通过代理对象开事务而this.method()直接绕过代理等于裸调。Service public class OrderService { public void createWithoutTx() { // 这里看起来调用了带事务的方法实际没走代理 this.createWithTx(); } Transactional public void createWithTx() { orderMapper.insert(order); } }解决办法通常是三种把被调用方法拆分到另一个Service里注入那个 bean 来调用或者注入自身代理也就是Autowired自己这个类型的 bean或者用AopContext.currentProxy()。我不太推荐第三种因为需要额外配置exposeProxy而且代码里读起来很绕。最稳妥的做法还是拆类把事务方法独立出去顺便也能让职责更清晰这本身就是一种事务范围优化。2.3 嵌套调用会悄悄扩大事务边界在实际业务代码里事务边界往往不是写代码的人主观决定的而是被调用链“推”着走的。我见过一个典型的订单查询接口Controller 层调 Service 的查询方法查询方法里为了写操作日志调了一个logOperation()方法而这个日志方法上恰好有Transactional。结果查询接口的无意中被包了一个事务虽然日志方法很快返回但如果调用链中任何一个检查、远程调用、复杂计算被放进这个链路事务就会被无限拉长。这里想强调一个观点**事务边界应该是显式设计出来的而不是被方法调用关系自然带出来的。**我在做代码评审时会重点关注两个信号一是带Transactional的方法内部出现了明显的耗时操作二是无状态查询入口下面带着写操作。这两个信号基本能定位到 80% 的大事务隐患。3. 事务范围优化的五条实战策略3.1 策略一事务内只保留数据库写操作这是最基础也最容易被接受的一条原则。还是拿开头的下单场景举例优化前的代码大概是这样的Transactional public void createOrder(OrderCreateRequest request) { // 1. 写订单主记录 orderMapper.insert(buildOrder(request)); // 2. 写订单明细 orderItemMapper.batchInsert(buildItems(request)); // 3. 远程扣减库存网络耗时不可控 stockClient.deduct(request.getSkuId(), request.getCount()); // 4. 发送通知短信外部接口耗时不可控 smsClient.sendMessage(request.getMobile(), 您的订单已创建); // 5. 记录审计日志 auditLogMapper.insert(buildAuditLog(request)); }这个方法的数据库操作只有三步但实际事务占用时间 3 4 的耗时。库存接口稳定的时候还好一抖动整个事务就悬了。优化后第一版我把远程调用挪出事务Transactional public void createOrder(OrderCreateRequest request) { orderMapper.insert(buildOrder(request)); orderItemMapper.batchInsert(buildItems(request)); auditLogMapper.insert(buildAuditLog(request)); } public void createOrderWithRemote(OrderCreateRequest request) { createOrder(request); // 事务提交后才执行外部调用 stockClient.deduct(request.getSkuId(), request.getCount()); smsClient.sendMessage(request.getMobile(), 您的订单已创建); }这里有一个关键细节createOrderWithRemote不能加Transactional。,createOrder自己带事务方法一返回事务就提交了连接也随之释放。如果把createOrderWithRemote 也加上事务注解那远程调用又回到事务内了等于白拆。这是我对很多同事强调过的一个点不是把代码物理挪个位置就完了要保证外部调用所在的方法不带事务传播进来。3.2 策略二用事务同步把外部调用挪到提交后单纯把远程调用放到事务方法外部会引入一个新的问题事务提交成功后如果远程调用失败了怎么办订单数据已经入库短信没发出去用户状态不对。更稳妥的方案是用 Spring 的事务同步机制注册一个afterCommit回调。Transactional public void createOrder(OrderCreateRequest request) { orderMapper.insert(buildOrder(request)); orderItemMapper.batchInsert(buildItems(request)); auditLogMapper.insert(buildAuditLog(request)); TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCommit() { // 事务提交成功后才执行连接已经释放 stockClient.deduct(request.getSkuId(), request.getCount()); smsClient.sendMessage(request.getMobile(), 您的订单已创建); } }); }注意几个坑。afterCommit里如果抛出异常不会触发主事务回滚因为事务已经提交了但异常会传回主方法可能让接口报错。所以在回调里必须自己捕获异常记录日志必要时走补偿逻辑。另一个点是注册回调后如果事务回滚了afterCommit不会执行这恰恰是我们想要的数据没落库就不该发通知。我在生产环境使用这个方案时通常会把afterCommit里要做的远程操作继续封装成一种可重试的任务用本地消息表加定时任务兜底保证“订单已创建”的通知最终一定到达。事务同步解决的是时序问题不解决可靠性问题。3.3 策略三批量长流程用 REQUIRED_NEW 分批提交有些业务天然是长流程比如定时任务里批量处理一万条对账单每条对账要更新余额、记录流水、可能还要锁账户行。如果整个方法一个事务跑完这一万条的所有行锁、更新操作全都挤在一个事务里一旦某一条失败全部回滚前功尽弃。这种场景的正确做法是外层无事务内层按批次提交。我常用的一个结构是public void batchProcess(ListLoanAccount accounts) { for (ListLoanAccount batch : partition(accounts, 500)) { processBatch(batch); } } Transactional(propagation Propagation.REQUIRES_NEW) public void processBatch(ListLoanAccount batch) { for (LoanAccount account : batch) { accountMapper.updateBalance(account); flowMapper.insert(buildFlow(account)); } }这样每个 500 条的批次是一个独立事务成功就提交失败只影响当前批次。加REQUIRES_NEW是因为外层方法没有事务如果不加第一个批次开启的事务会一直传到后续批次又变回一个大事务。我踩过的坑是忘记在外层无事务的情况下内层方法互相调用导致事务被共享所以建议把processBatch放在独立的 bean 里。REQUIRES_NEW的性能代价也需要评估。每个批次提交时的fsync消耗是之前我说过的;连接占用REQUIRES_NEW挂起外层事务后会从连接池再拿一个连接如果同时有几十个批次在跑连接池需要足够大。我的经验是批次数和连接池大小要放到一个池子里综合考虑不要通过无限加大连接池来解决问题。3.4 策略四TransactionTemplate 实现编程式事务声明式事务注解适合边界固定的场景但现实业务里经常出现“事务边界不完全是方法边界”的情况。比如一个方法里前面的数据库操作不要求事务中间有一段必须原子更新后面的远程调用又不希望被事务拖住。这种时候用注解反而别扭——切来切去拆方法虽然能做但代码结构被事务需求扭曲了。TransactionTemplate是一个很顺手的工具它把事务边界控制显式地放在代码里Service public class OrderService { private final TransactionTemplate transactionTemplate; public OrderService(TransactionTemplate transactionTemplate) { this.transactionTemplate transactionTemplate; } public void createOrder(OrderCreateRequest request) { // 事务外校验、组装 Order order buildOrder(request); // 事务内只做数据库写 transactionTemplate.executeWithoutResult(status - { orderMapper.insert(order); orderItemMapper.batchInsert(buildItems(request)); }); // 事务外远程操作 stockClient.deduct(request.getSkuId(), request.getCount()); } }executeWithoutResult会在回调方法正常返回后自动提交回调里定义的TransactionTemplate的默认传播行为是REQUIRED如果你发现它已经在一个事务里就会加入。如果想强制新事务可以用构造TransactionTemplate时传入TransactionDefinition.PROPAGATION_REQUIRES_NEW。我通常会用编程式事务细化某些热点方法的范围作为声明式事务的补充而不是替代。3.5 策略五事务外的非核心动作异步化很多时候我们把所有操作都塞进事务方法里是因为不敢丢逻辑而不是因为逻辑必须和主数据同生共死。通知类型的操作短信、站内信、邮件、分析埋点、操作日志都属于“可以晚一点执行但不能丢”的事情。这类任务用异步化最合适。实现上可以用 Spring 的Async搭配独立线程池或者直接丢进消息队列。public void createOrder(OrderCreateRequest request) { transactionTemplate.executeWithoutResult(status - { orderMapper.insert(buildOrder(request)); }); // 异步发送通知不阻塞主流程 notifyService.sendOrderCreatedMessage(request); }给notifyService.sendOrderCreatedMessage加上Async注解调用方不需要等待执行完成。这里要提醒的是不要直接从事务内部调用Async方法因为在事务提交前异步线程就已经开始了如果它去读刚写入的数据可能读不到因为数据还没提交。正确的方式是事务提交后再触发异步调用或者把异步调用排在afterCommit回调里。异步化的问题在于可观测性变差线程池满了会丢任务消息队列会引入新的运维成本。所以我个人的排序是能同步简单调用的尽量同步只有像群发通知、报表生成、日志清洗这类明显可以延后的动作才建议异步化。4. 如何量化与定位线上大事务4.1 数据库侧观测长事务优化不是拍脑袋得先量化。数据库层面对长事务最直接的观测手段是查innodb_trx表SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started ASC;查出来的结果里trx_age_seconds比较大的就是长事务拿到对应的trx_mysql_thread_id再去怼到具体的线程和 SQLSELECT * FROM performance_schema.processlist WHERE id 这里填查到的ID;MySQL 8.0 的performance_schema里还有events_statements_current能看到这个连接当前正在执行的 SQL。如果事务卡住是因为锁等待还能通过sys.innodb_lock_waits看锁等待关系。线上实践的时候我会做三件事一是定时任务每隔几分钟扫一次innodb_trx超过 10 秒的事务就告警二是把告警信息直接推到排查群三是要求所有慢事务都必须登记原因。这个机制上线后大促期间的长事务数量下降得很明显——因为大家都知道了线上真的会有人盯着你的事务。4.2 应用侧统计事务耗时与锁等待数据库侧能看到“事务存在”但定位“事务是哪个方法开启的”需要应用侧配合。我常用的一个办法是加一个 AOP 切面拦截所有带Transactional注解的方法统计方法总耗时。更精细一点可以注册事务同步来统计“事务实际活跃时间”和“事务外时间”的差值public class TransactionMetricsAspect { Around(annotation(org.springframework.transaction.annotation.Transactional)) public Object measure(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCompletion(int status) { long cost System.currentTimeMillis() - start; // 记录事务方法总耗时超过阈值告警 } }); return pjp.proceed(); } }事务里真正跟数据库相关的耗时可以通过注册事务同步时再包一层 DataSource 代理来统计也可以直接观察连接池的活跃连接数指标。Spring Boot 的 Actuator 配合 Micrometer 一般会暴露hikaricp_connections_active之类的指标如果活跃连接数持续高位大事务的嫌疑非常大。还有一种更省事的办法把方法耗时、连接获取耗时、已占用连接数打点进日志用 APM 工具的慢调用链去看。生产环境上我好几次就是靠着链路追踪里“数据库连接持有时间”这个指标反向找到了还在事务内跑远程调用的方法。4.3 代码审查中的大事务坏味道即使没有监控系统代码审查也能发现大部分大事务隐患。我总结了一套高频坏味道清单看到以下情况就应该警惕带Transactional的方法里出现 HTTP 调用、RPC 调用、短信/邮件发送。不分原因一律先质疑。方法内部出现循环写数据库。循环内每条记录单独 update 或 insert意味着事务持有了大量行锁。一个事务方法里的代码行数超过某个阈值比如 50 行以上。查询方法被莫名其妙加上Transactional很可能只是为了某条查询语句完全没有必要。同类内自调用不是事务失效的问题就是事务边界被内部逻辑绕过的隐患。这里有一个比较有争议的地方查询方法能不能加Transactional(readOnly true)我的看法是它能帮助数据库优化只读事务的路径也能避免查询过程中意外写入但在大多数业务系统里readOnly true并不能显著降低连接持有时间它只是给了数据库一个“只读”的提示。如果查询链路里没有写操作加不加都行加了反而产生不必要的代理成本。把长查询拆到事务外收益更直接。5. 踩坑实录与个人经验总结5.1 常见问题速查表我在推进事务范围优化的过程中被问过很多次“为什么我改完了还是有问题”这里把几个高频问题整理成了速查表遇到类似症状可以直接对照。现象可能原因解决办法事务方法里调了远程接口连接池还是满远程调用所在方法整体被外层事务包住拆类确保远程调用方法不参与事务方法加了Transactional但数据没回滚同类自调用代理未生效拆类注入或改用编程式事务小事务提交特别慢事务内持有行锁等待其他事务释放排查锁等待缩短持锁时间批量任务跑一半失败全部回滚单线程单事务处理大量数据按批次REQUIRES_NEW分批提交事务提交后发通知通知发出去了但数据没落库通知放在事务内异步执行读到了未提交数据把异步调用移到事务提交后afterCommitREQUIRES_NEW加了之后连接池耗尽新事务额外占用连接连接池配置不足评估并发连接需求适当调大连接池或减少并发这张表也是我在团队内部分享时的核心内容。对照排查上表基本能解决 90% 的事务范围相关疑难杂症。5.2 几个值得记住的实操教训最后分享几个在我自己的项目里踩出来的深刻教训。第一个是事务方法命名要立规矩。我们团队后来约定不带事务的方法命名里不写 create、update、delete 这类暗示写库的词带事务的方法名一律加InTx后缀比如createOrderInTx。命名一改代码评审时一眼就能看出哪些方法会开事务很多误加事务的情况自然就消失了。第二个是注册事务同步的代码最好封装不要在每个事务方法里写一大段匿名内部类。建议封装成工具方法比如TransactionUtils.afterCommit(Runnable),内部自动捕获异常、记录失败日志。我们后来在回调里统一弹一个事务消息到本地消息表由独立的消费线程负责真正的远程通知实现了解耦和重试这个改造带来的稳定性提升非常明显。第三个是不要为了优化而把事务拆得支离破碎。事务的存在本身是为了保证一致性如果一段逻辑确实需要原子性强行拆开反而造出数据不一致的坑。优化的目标是把“不需要原子性”的部分挪出事务而不是把“必须原子性”的逻辑拆散。判断的标准只有一个如果这段操作在事务提交前失败主数据是不是会出现不可接受的状态。如果是就留在事务内如果不是就挪出去。我在实际项目中体会到的最重要的一点事务范围优化不是一次性的重构而是一种持续的设计习惯。每当你往一个带事务的方法里多塞一个操作都应该问自己一句——这个操作真的需要跟主数据同生共死吗多问几次很多坑在写代码的时候就能避开而不是等线上事故来教你。

相关推荐

微信公众号网页 JSAPI 支付接入实战:基于 Senparc.Weixin SDK 微信支付 V3 全流程解析
微信公众号网页 JSAPI 支付接入实战:基于 Senparc.Weixin SDK 微信支付 V3 全流程解析

后端即时通讯金融科技 【免费下载链接】WeiXinMPSDK 微信全平台 .NET SDK, Senparc.Weixin for C#,支持 .NET Framework 及 .NET Core、.NET 10.0。已支持微信公众号、小程序、小游戏、微信支付、企业微信/企业号、开放平台、JSSDK、微信周边等全平台。 … · 2026/9/25 8:32:57

可信数据空间:安全共享与隐私计算实践指南
可信数据空间:安全共享与隐私计算实践指南

1. 可信数据空间概述在数字化转型浪潮中,数据已成为核心生产要素。可信数据空间作为一种新型数据共享与协作模式,正在重塑企业间的数据流通方式。简单来说,它就像是一个经过严格安检的"数据会议室",不同参与方可以带着自… · 2026/9/25 8:32:57

Arch Linux手动安装豆包客户端:deb解包与依赖适配实战
Arch Linux手动安装豆包客户端:deb解包与依赖适配实战

1. 项目概述:为什么要在Arch Linux上“手动安装”豆包客户端?Arch Linux用户圈里流传着一句老话:“你不是在装系统,就是在为装系统做准备。”这话听着调侃,实则精准点出了Arch的哲学内核——控制权必须握在自己手里。而… · 2026/9/25 8:32:51

Atlas 300V部署YOLOv5全流程:从硬件到推理优化的踩坑指南
Atlas 300V部署YOLOv5全流程:从硬件到推理优化的踩坑指南

搞了快一周的Atlas 300V,总算把YOLOv5在Atlas 300V 24G上跑通了。如果你也是第一次拿到这张卡,第一反应估计和我一样:Atlas 300V 24G是运算加速卡吗?它到底能不能像GPU那样,装几个包就直接跑YOLO?先说结论&… · 2026/9/25 9:21:32

treg CLI Agent 实战:OpenRouter 与 MCP 集成指南
treg CLI Agent 实战:OpenRouter 与 MCP 集成指南

1. 从“treg”这个标题说起:一个被低估的CLI Agent入口第一次看到“treg”这个词,很多人会以为是某个拼写错误,或者某个小众库的缩写。但如果你最近在折腾 AI Agent、OpenRouter、MCP 这一整套东西,就会意识到它大概率是一个围绕C… · 2026/9/25 9:21:32

为什么Windows 11 24H2 LTSC没有Microsoft Store?LTSC-Add-MicrosoftStore完整背景指南
为什么Windows 11 24H2 LTSC没有Microsoft Store?LTSC-Add-MicrosoftStore完整背景指南

为什么Windows 11 24H2 LTSC没有Microsoft Store?LTSC-Add-MicrosoftStore完整背景指南 【免费下载链接】LTSC-Add-MicrosoftStore Add Windows Store to Windows 11 24H2 LTSC 项目地址: https://gitcode.com/gh_mirrors/ltscad/LTSC-Add-MicrosoftStore Wi… · 2026/9/25 9:21:07

科研加速器再升级!云克隆多因子检测试剂盒重磅扩容,新增80+ Panel,覆盖470+核心指标
科研加速器再升级!云克隆多因子检测试剂盒重磅扩容,新增80+ Panel,覆盖470+核心指标

引言:在多靶点时代,如何突破科研效率的瓶颈?在现代生命科学研究的宏大版图中,免疫学、肿瘤学、神经生物学以及干细胞研究正以前所未有的速度交织融合。当我们深入探索复杂的疾病机制、解密细胞间的通讯网络时,单一的细… · 2026/9/25 9:20:49

B_S仓库管理系统源码从解压到二次开发:环境搭建、库存逻辑与避坑指南
B_S仓库管理系统源码从解压到二次开发:环境搭建、库存逻辑与避坑指南

简介:这份B/S仓库管理系统源码面向Web开发初学者与需要企业级项目练手的开发者,基于浏览器-服务器架构,覆盖库存查询、出入库、盘点、报表统计与权限管理等完整业务场景,可作为理解前后端分离与数据库设计的实战教材。压缩包共625… · 2026/9/25 9:20:23

802.11n协议深度解析:MIMO、信道绑定与MAC增强实战指南
802.11n协议深度解析:MIMO、信道绑定与MAC增强实战指南

简介:本资源为IEEE官方发布的《IEEE Std 802.11™-2007》标准原文PDF,是WiFi 802.11n协议的权威技术规范,面向无线通信工程师、网络协议研究者、高校通信/计算机专业师生及嵌入式无线开发人员,用于深入理解MIMO多天线架构、双频段… · 2026/9/25 9:20:23

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

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

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

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

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

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

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码