新闻详情

新闻详情

首页 / 资讯中心 / 详情

微服务分布式事务实战:Seata AT模式原理与SpringCloud Alibaba落地

发布时间:2026/9/9 7:00:18来源:尧图网络
微服务分布式事务实战:Seata AT模式原理与SpringCloud Alibaba落地
微服务拆完之后分布式事务就成了躲不掉的硬仗。尤其像“下单扣库存”这种跨服务、跨库的典型场景订单服务成功了、库存服务却失败数据就彻底对不上了。SpringCloud生态里处理这块最常用也最需要系统搞明白的方案就是阿里巴巴开源的Seata。这篇我会从分布式事务的底层问题讲起把Seata的TC/TM/RM核心架构、AT模式二阶段原理、TCC和Saga的选择逻辑再到SpringCloud Alibaba实际落地的配置和代码一条线全部捋清楚最后整理一份面试高频问答和排查实录帮你在项目里真正会用而不是只会背个概念。这篇文章适合两种人一是微服务项目里已经遇到数据一致性问题的开发者二是准备面试、想系统梳理Seata相关知识点的人。我会按“原理 → 选型 → 实操 → 排坑”的顺序来写尽量把每个关键点背后的为什么也讲透。1. 为什么微服务里“本地事务”不管用了1.1 一个典型的订单库存事故现场很多朋友刚拆微服务的时候都会有这种错觉原来单体应用用Transactional标注一下方法里的所有数据库操作要么全成功、要么全失败切到微服务之后把这个注解继续放在Service方法上不就行了实际跑一次“创建订单 扣减库存”就能发现问题。订单服务和库存服务各自独立部署各自连独立的数据库订单服务里的Transactional只能保证本服务内这一组SQL的原子性。它调用库存服务是通过Feign或者Dubbo发起的远程请求这个远程请求到底成不成功、对方事务提交了没有本地事务是完全感知不到的。于是就会出现这样的事故订单表里insert了一条订单返回给前端“下单成功”但下游库存服务的扣减接口因为数据库连接池满了抛了一个超时异常订单服务没有做补偿库存就是没扣掉。结果就是用户在页面上看到已经买到了商品实际库存却还在等发货的时候才发现没货可发。这就是典型的“本地事务管不到远程调用”也是微服务架构下分布式事务问题最直观的解释。1.2 分布式事务到底在解决什么问题要理解分布式事务得先明确它解决的是什么不是“两个数据库的事务合并成一个事务”而是要从CAP理论说起。分布式系统在出现网络分区的情况下只能从一致性和可用性里二选一。微服务之间跨库的数据操作不可能像单机数据库那样靠锁和redo/undo日志在一个事务控制范围里完成它必须通过某种协调机制让多个服务各自的事务最终保持全局一致。这里面又分两个层次强一致性和最终一致性。强一致性指的是全局事务处理的任何时刻所有参与方都能看到一致的数据最终一致性则允许中间出现短暂的不一致只要最终能对上就行。大部分业务系统其实不需要强一致比如订单、积分、日志这类场景最终一致就够了但像账户扣款、库存扣减这种涉及资金和实物数量的操作往往需要更高的一致性保障。Seata的核心设计思路就是把“分布式”的问题重新拉回“本地事务 补偿”的框架里让业务方感知到的还是一个完整的大事务这就是它存在的价值。1.3 常见方案对比为什么最终选了Seata分布式事务解决方案不是只有Seata业界还有不少路子。我在项目里选型时会把它们拉出来做一个对比这样心里才踏实。方案核心思路侵入性性能适用场景XA / 两阶段提交数据库层面预提交资源锁定低低锁时间太长并发量低的传统系统Seata AT业务SQL直接提交日志补偿很低中绝大多数CRUD业务TCC业务方实现Try/Confirm/Cancel高高高并发且强一致的核心链路本地消息表业务和消息事务绑定异步通知中中可靠性要求高的异步场景MQ事务消息半消息机制异步最终一致中较高可以容忍异步一致性的场景XA方案最大的问题在于“资源锁定时间太长”两个阶段之间数据库连接和行锁一直被抱着微服务调用链路一长并发稍微上来一点就把数据库拖垮。TCC对性能友好但Try、Confirm、Cancel三个方法都得自己写对业务侵入实在太大。MQ事务消息和本地消息表本质上都在解决“异步化后的最终一致”但是不适合实时性要求高的强一致场景。Seata的AT模式所以受欢迎核心就是它可以在业务SQL几乎无感知的情况下把全局事务的框架搭起来。你只需要在入口方法加一个GlobalTransactional注解剩下的事务协调、分支注册、回滚补偿都有框架代劳。在SpringCloud Alibaba的生态里Seata和Nacos配合也最顺滑这才是我选择它的真正原因。2. Seata核心架构与原理拆解2.1 三个角色TC、TM、RMSeata把一次全局事务里的参与者分成了三个角色分别是TC、TM和RM。这三个缩写面试必问但很多同学只是死记硬背并没有真正理解它们的关系。TCTransaction Coordinator是事务协调者它是一个独立部署的服务端进程也就是我们常说的Seata Server。它的职责是维护全局事务和所有分支事务的状态决定到底应该提交还是回滚。TMTransaction Manager是事务管理器它在业务的发起方也就是那个加了GlobalTransactional注解的服务里。TM负责向TC申请开启一个全局事务并且在业务执行完之后向TC发起全局提交或回滚指令。RMResource Manager是资源管理器它管理每个参与分支事务的服务本地资源。RM负责向TC注册分支事务并执行TC下发的分支提交或回滚指令。用生活化的方式理解TC是整个流程的导演它掌握全局TM是监制它喊“Action”开始拍戏拍完喊“Cut”决定这条要不要RM是执行导演只管自己手底下这一摊导演让删就删、让留就留。这个类比能帮你把角色职责瞬间记住。2.2 一次完整全局事务的流程要真正理解Seata光记住三个角色还不够得走一遍完整时序面试和排查问题时你才知道每一步卡在了哪里。一次全局事务是这样的TM向TC发起“开启全局事务”请求TC生成一个全局事务ID也就是XID然后返回给TM。XID会跟着业务调用链一直往下传。RM接收到请求后发现里面带着XID就明白自己参与到了一场全局事务中于是向TC注册一个分支事务。当主业务方法执行结束TM向TC发起“全局提交”或者“全局回滚”的请求。如果是回滚TC会通知所有已经注册的RM执行回滚逻辑如果是提交TC就通知所有RM释放本地锁、清理事务日志完成资源释放。这个流程里最关键的一点是TM只是把“开始”和“结束”的信号发给TC真正的分支事务明细全部由RM向TC汇报。所以TC其实不需要知道业务代码是怎么写的它只维护状态机做到“只协调不参与”。2.3 XID如何跨服务传递XID是贯穿整个全局事务的“通行证”它必须在调用链中传递否则下游服务就不知道自己属于哪个全局事务。SpringCloud环境下使用Feign作为远程调用时Seata提供了自己的Feign拦截器会自动从当前线程上下文取出XID放进Feign的请求头里下游接收到之后再把XID设置到自己的线程上下文里这个过程对业务代码完全透明。我第一次实操时以为只要依赖引入对了就万事大吉后来发现一个隐蔽问题如果Feign请求里面没有传递XID通常是因为你自定义了Feign的RequestInterceptor把Seata的拦截器覆盖了。解决方案很简单在自己的拦截器里手动把XID附加进去或者把Seata的拦截器排在前面。还有一个坑是异步线程比如用Async或者线程池异步处理业务逻辑子线程拿不到主线程里的XID这时候要用Seata提供的RootContext.bind()手动传递或者干脆避免在全局事务里开异步子任务。2.4 三种模式怎么选AT、TCC、SagaSeata不只提供AT模式它还支持TCC和Saga面试也经常问如何选型。AT模式是在业务SQL执行的同时把数据快照记录到日志表二阶段靠反向SQL做补偿业务代码几乎无侵入。TCC模式把生命周期拆成Try、Confirm、Cancel三个阶段所有操作都要业务自己实现好处是性能好、锁粒度可控适合高并发强一致场景代价是每增加一个参与方都要写大量模板代码。Saga模式则更像一个业务流程编排器它把一个长事务拆成一系列本地子事务每个子事务有对应的补偿操作任何一步失败就按相反顺序执行补偿。Saga适合业务流程非常长且不适合长时间锁资源的场景比如一个跨天的审批流程。做技术选型时我给团队定的原则很简单绝大多数业务用AT高并发且愿意付出开发成本的核心链路用TCC长流程的业务编排用Saga。千万别一上来就上TCC开发成本会让你怀疑人生。3. AT模式原理看完不用再看源码3.1 一阶段本地提交 记录镜像AT模式之所以对业务侵入小是因为它把“两阶段提交”变成了“一次本地提交 一份日志”。一阶段时Seata不是把SQL拦截下来延迟执行而是让业务SQL正常执行、正常提交但是在执行之前和之后分别对涉及的数据行做了一次快照也就是before image和after image然后把这份镜像写进额外的undo_log表同时向TC注册分支事务。如果你第一次接触AT一定会困惑本地事务都提交了锁都释放了后面全局回滚还怎么回滚答案就在这里——回滚靠的不是数据库的undo日志而是Seata自己记的这份undo_log。它相当于给每一次修改都拍了一张“修改前长这样”的照片回滚时对着照片把数据再改回去。这个过程可以用一句话概括先干活留后手。一阶段完成时本地事务已经提交资源锁释放得很快所以AT模式的并发性能比XA好很多瓶颈主要在多出的快照读和日志写入上。3.2 二阶段提交就是清理回滚就是补偿二阶段分为全局提交和全局回滚两种情况。全局提交时TC通知所有RM提交分支事务但是因为本地事务在一阶段就已经提交了这里所谓的“提交”实际上只是异步删除undo_log清理掉之前留下的镜像数据整个动作非常轻快。全局回滚时RM会根据undo_log里的before image展开补偿。回滚过程并不是简单地“把数据改回before image”就完事它要先检查当前数据和after image是否一致如果不一致说明这行数据在全局事务执行后被其他事务修改过了这时就需要人工处理避免覆盖掉别人的修改。这一点是AT模式和其他方案最大的不同也是它保证不丢数据的关键设计。用一个比喻你在白板上写了一份方案同时拍了三张照片。第一张是动笔之前第二张是写完的时候第三张是别人又改了之后。任务没通过要撤销你不能盲目擦掉得先看看现在白板上的内容和“你刚写完”那张照片是否一致如果一致才敢擦如果不一致说明有别人继续写了这时候就不能乱动。3.3 全局锁如何防住脏写AT模式要解决一个并发问题两个全局事务同时操作同一行数据怎么办如果事务A中途回滚而事务B已经修改了A操作过的数据A的补偿操作就可能把B的修改覆盖。Seata的解决办法是全局锁。TC维护了一个lock_table当一阶段执行update语句时RM会向TC申请对涉及的主键ID加全局锁锁由TC统一管理。其他事务如果也想操作这行数据需要同样申请全局锁发现锁被占用就会等待。通过这种方式不同全局事务之间对同一行数据的操作被串行化了脏写就被拦住了。但是要注意全局锁只能防住“全局事务之间”的冲突如果普通本地事务绕过Seata直接修改数据库那全局锁管不住依然可能造成数据不一致。我见过不少项目只让部分接口走Seata其他接口直接写数据库结果跑一段时间数据就乱了。这也是AT模式下需要提前约定规范凡是Seata管理的业务表所有写操作最好都纳入事务链路里。3.4 AT模式使用前提与性能边界AT模式不是万能的它有一堆使用前提。首先是数据库类型目前主要在MySQL这类关系型数据库上用得最成熟而且表必须有主键。其次是SQL必须能被Seata解析它内置了一套SQL解析器比较复杂的分页更新、跨表join更新等场景可能解析不了这类SQL就不要硬塞到AT模式里了。性能边界也要心里有数。AT模式虽然一阶段锁释放很快但多了快照的查询与写入、全局锁的申请与释放、以及二阶段日志的清理整体开销还是比纯本地事务高不少。如果单个全局事务涉及的分支特别多或者每笔业务都长时间占用某一行数据的全局锁吞吐量照样上不去。我实际测过正常情况下AT模式相比本地事务大概有一两成的性能损耗高并发扣库存这种场景如果全部走AT热点行的锁竞争会非常明显。4. 实操落地SpringCloud Alibaba Seata实现订单与库存4.1 版本选型与部署准备版本问题永远是SpringCloud生态绕不开的坑。我目前项目里用的一套组合是Spring Boot 2.7.x、Spring Cloud 2021.0.x、Spring Cloud Alibaba 2021.0.5.0、Seata Server 1.8.0、Nacos Server 2.2.x。这套组合在社区里验证得比较多属于稳妥型。如果你用的Spring Boot 3.x那Spring Cloud和Spring Cloud Alibaba的版本都得跟着大版本走Seata客户端依赖也需要选择对应的新版本具体对照关系一定要以官方版本说明为准。我见过太多人栽在版本上Stan错误地引进了新版本的Seata客户端结果跟Spring Cloud Alibaba自动配置冲突项目启动时一直报ClassNotFoundException或者Nacos配了一堆但事务就是注册不上。所以版本选型的第一原则是保守能用稳定版本就不要追新先跑通再考虑升级。部署Seata Server比较简单下载对应release包修改registry.conf指定注册中心和配置中心类型为Nacos再启动就完事。4.2 建表Seata需要的几张关键表Seata Server如果使用DB模式存储事务状态需要先在它的数据库里创建三张表global_table保存全局事务、branch_table保存分支事务、lock_table保存全局锁。这三张表官方SQL脚本里都有安装目录的script目录下可以直接拿。业务参与方也不轻松每个使用AT模式的业务库都需要创建undo_log表这张表是AT模式回滚的基础。建表语句我直接放出来CREATE TABLE IF NOT EXISTS undo_log ( id BIGINT NOT NULL AUTO_INCREMENT, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT NOT NULL, log_created DATETIME NOT NULL, log_modified DATETIME NOT NULL, ext VARCHAR(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4;xid和branch_id唯一索引很重要它能防止回滚日志重复执行幂等性就靠这个约束兜底。实际项目中我习惯把undo_log表写进数据初始化脚本保证每个环境执行一遍就自动带上避免服务上线后忘了建表而报错。4.3 服务端与客户端配置Seata Server端的registry.conf核心配置如下把Nacos作为注册中心和配置中心这样Seata Server启动后会自动注册到Nacos客户端可以通过Nacos找到它registry { type nacos nacos { application seata-server serverAddr 127.0.0.1:8848 group SEATA_GROUP namespace cluster default } } config { type nacos nacos { serverAddr 127.0.0.1:8848 namespace group SEATA_GROUP dataId seataServer.properties } }config里指向的seataServer.properties需要提前在Nacos配置中心里创建里面至少要配置存储模式。我用的DB模式配置类似store.modedb store.db.datasourcedruid store.db.dbTypemysql store.db.urljdbc:mysql://127.0.0.1:3306/seata?useUnicodetruecharacterEncodingutf8 store.db.userroot store.db.password123456客户端这边每个微服务要在application.yml里加上Seata配置段。最关键的是tx-service-group这个事务分组名必须和服务端配置中的分组映射一致否则找不到TCseata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP cluster: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP >Service RequiredArgsConstructor public class OrderService { private final OrderMapper orderMapper; private final StockFeignClient stockFeignClient; GlobalTransactional(name order-create, rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto) { Order order new Order(); order.setUserId(dto.getUserId()); order.setSkuId(dto.getSkuId()); order.setCount(dto.getCount()); order.setStatus(0); orderMapper.insert(order); stockFeignClient.deduct(dto.getSkuId(), dto.getCount()); return order; } }库存服务的Feign接口很简单FeignClient(name stock-service) public interface StockFeignClient { PostMapping(/stock/deduct) void deduct(RequestParam(skuId) Long skuId, RequestParam(count) Integer count); }这里我遇到过一个大坑必须保证数据源被Seata代理。Seata的AT模式要拦截SQL、生成undolog就必须在数据源外层包一个DataSourceProxy。Spring Cloud Alibaba的自动配置通常能处理但如果你在项目里自定义了数据源或者用了Druid的starter需要手动把DataSource包装一下否则运行时会发现分支事务没有走Seata的逻辑。Configuration public class SeataDataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource) public DataSource dataSource() { return new DruidDataSource(); } Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSourceProxy); return factoryBean.getObject(); } }这是我自己在实际项目里用过的配置虽然代码稍多但能确保MyBatis走的是Seata代理后的数据源避免“undo_log没写”这类诡异问题。4.5 验证模拟扣库存失败观察自动回滚配置做完得真正验证全局事务是否生效。我在库存服务的deduct方法里故意加一个异常模拟库存服务业务失败PostMapping(/stock/deduct) public String deduct(RequestParam(skuId) Long skuId, RequestParam(count) Integer count) { Stock stock stockMapper.selectBySkuId(skuId); if (stock.getStock() count) { throw new RuntimeException(库存不足); } stock.setStock(stock.getStock() - count); stockMapper.updateById(stock); return ok; }调用下单接口库存一定不足这时候stock-service会抛出异常。观察order-service的日志先是看到了XID的生成然后是分支事务的注册最终输出全局事务回滚再查一下订单表果然没有新增数据。整个过程中我在订单服务里并没有写任何try/catch去调库存接口的补偿逻辑回滚完全是Seata自动完成的。我当时第一次跑通这个过程还挺有成就感的因为从业务代码上看确实像写了一个单体事务一样潇洒。但日志里“全局事务回滚”这几个字一出现就意味着你在微服务架构里拿到了“跨服务原子性”。5. 常见问题与排查实录5.1 全局事务没生效XID丢失最典型的排查场景是代码里加了GlobalTransactional日志也打了但分布式事务就是没有任何反应回滚不生效。这时候第一件事是看调用链日志里有没有XID。正常情况第一次请求Seata会打印类似xid 192.168.1.100:8091:1234567的信息如果日志里压根没有XID说明全局事务根本没开启。原因一般有三个。一是注解放在被同类内部调用的方法上了Spring代理失效全局事务不会生效只能通过注入自身代理或者把方法放到另一个SpringBean里解决。二是事务分组配置不对TM向TC申请全局事务时找不到TC。三是Feign的拦截器没生效XID没有从订单服务传到库存服务库存服务那边自然也不知道自己参与了全局事务。排查这种问题最快的方式就是在两个服务里分别打印RootContext.getXID()看看调用库存接口时上下文里有没有值。5.2 undo_log 相关报错业务跑着跑着控制台突然报io.seata.rm.datasource.undo.UndoLogManager相关的异常最常见的就是Table xxx.undo_log doesnt exist。有些环境建表脚本没同步过去或者建表顺序不对服务已经启动了但表还没建。还有一种情况是undo_log的字段和客户端版本不匹配。Seata版本升级之后日志表结构可能发生变化如果旧表缺字段就会报“Column not found”。解决方式很简单检查版本把表结构和当前Seata版本对齐。建议在开发环境启动服务后先跑一个真实的全局事务链路确认日志表正常再往下做这样能在最早阶段发现问题。5.3 全局锁冲突与性能问题全局锁冲突的典型报错是“Global lock wait timeout”也就是某一行数据的全局锁迟迟拿不到。这个在高并发扣库存、扣余额的场景特别常见。热点行被多个全局事务同时操作后到的一直等待等到超时就抛异常。优化手段我整理过几条一是给where条件涉及的字段加合适的索引让锁尽量只落在需要修改的行上避免锁范围扩大二是尽量缩短全局事务的时间把远程调用、IO等耗时操作移出事务范围或者用异步化三是如果热点行冲突实在太严重可以考虑把这类操作从AT模式换成TCC或者改造成本地消息表让锁只在真正的资源扣减瞬间出现。5.4 数据源代理失效的坑这个坑很隐蔽排查的时候容易忽略Seata明明配好了undolog表也有但AT模式就是不生效本地SQL直接执行了。最后发现数据源没有被DataSourceProxy包装Seata的SQL解析拦截器根本没有机会介入。尤其是项目里同时用了Druid和MyBatis时数据源创建顺序或者自动配置的优先级经常出问题。我的处理习惯是统一使用上一节那种手动定义DataSourceProxy的配置不再依赖框架的自动装配这样最可控。判断数据源是否被代理可以写一个启动日志观察一下DataSource实例的类型看到DataSourceProxy就说明没问题看到DruidDataSource就要小心了。6. Seata面试高频题与避坑经验6.1 面试官最爱问的几个问题这部分有准备面试的朋友可以直接做速查用我也把回答的关键方向列出来。面试题关键回答方向Seata中TC/TM/RM各自职责TC维护全局和分支事务状态TM负责开启和提交/回滚全局事务RM注册分支并执行二阶段AT模式如何做到回滚一阶段提交本地事务并写undolog镜像二阶段回滚时比对after image按before image反向补偿AT和XA有什么区别XA是数据库层面真正的两阶段锁定AT是业务SQL直接提交加日志补偿AT锁时间更短、侵入更小TCC是什么如何防止空回滚Try预留资源Confirm确认Cancel补偿空回滚问题要通过业务状态或控制字段处理Cancel发现主记录不存在时也要正确处理什么场景不能用AT模式复杂SQL解析不了、表没主键、非关系型数据库等场景不适合Seata全局事务性能瓶颈在哪快照读写、全局锁申请、分支事务取消时的额外网络请求还有一个高频考点是TCC的三个经典问题空回滚、幂等、悬挂。空回滚说的是Try没执行成功Cancel却提前到了要能识别并安全退出幂等是说一阶段或者二阶段可能被重复执行需要业务幂等字段控制悬挂是Try在Cancel之后才到达导致数据被Cancel处理过了但Try又执行成功造成状态不一致。面试时能把这三个问题讲清楚比你背十遍概念有用得多。6.2 我强烈建议你先判定的场景技术说到底是用来解决问题的不是用来炫技的。我见过太多项目用Seata之前没想清楚结果全局事务套了一层又一层性能掉了还说不清楚为什么。所以我每次给别人建议时都会先说一段话不是所有业务都需要全局事务。如果你的场景可以接受异步最终一致比如下单后发短信通知、生成日志报表那用MQ或者本地消息表就够了完全没必要上Seata。如果你的场景确实是强一致再判断数据量级和并发量。低并发、链路简单的AT模式是首选因为开发成本最低。高并发、热点数据极其明显的比如秒杀场景扣减库存可以优先考虑TCC甚至直接用Redis预扣减。整个调用链特别长、中间步骤需要人工介入的再去考虑Saga编排。我自己的习惯是先画一张业务链路图标出每个节点是强一致还是最终一致把强一致节点全部收拢到一个尽量小的范围内然后用Seata只保护这个范围。范围越大性能越差问题越难排查。这个习惯真的能让你少做很多无用功。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2026年SSD选购指南:PCIe 4.0还是5.0?原厂颗粒与避坑全解析 2026/9/9 7:48:22

2026年SSD选购指南:PCIe 4.0还是5.0?原厂颗粒与避坑全解析

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

阅读更多 →
ruflo低功耗无线采集终端实战:从硬件选型到LoRaWAN上云的完整记录 2026/9/9 7:48:22

ruflo低功耗无线采集终端实战:从硬件选型到LoRaWAN上云的完整记录

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

阅读更多 →
SSH客户端三强对比:MobaXterm、Termius、Xterminal选型指南 2026/9/9 7:48:22

SSH客户端三强对比:MobaXterm、Termius、Xterminal选型指南

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

阅读更多 →
OrCAD Capture报Illegal character?网表非法字符定位与修复指南 2026/9/9 7:48:22

OrCAD Capture报Illegal character?网表非法字符定位与修复指南

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

阅读更多 →
AI搜索工具深度横评:大模型如何学会实时检索与引用溯源 2026/9/9 7:48:22

AI搜索工具深度横评:大模型如何学会实时检索与引用溯源

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

阅读更多 →
GEO核心战场:非图文内容与信息块覆盖如何决定AI引用率 2026/9/9 7:45:22

GEO核心战场:非图文内容与信息块覆盖如何决定AI引用率

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞