一、Spring Framework 核心 (IoC & AOP)

1. 简单说说Spring是什么,它的核心功能IoC和AOP分别解决了什么痛点?

优秀详细回答(含扩展):

Spring是一个轻量级的、一站式的Java应用开发框架。其核心是提供一个容器(Container),用于管理应用组件的生命周期和配置,其两大核心功能是IoC和AOP。

  • IoC (控制反转) / DI (依赖注入):

    • 解决的痛点: 传统编程中,对象A依赖对象B,通常会在A内部直接new B()。这导致代码高度耦合(A和B紧密绑定)、难以测试(无法在测试A时轻松替换B为Mock对象)、资源管理复杂(如单例、池化)。

    • 解决方案: IoC将对象的创建和依赖组装的控制权“反转”给容器。容器负责new B(),并在创建A时,通过构造器Setter字段将B“注入”给A。这实现了依赖关系的解耦,使得组件就像“零件”一样可以被灵活组装和替换。

    • 联想扩展: IoC是工厂模式的升华。它不仅负责创建对象,更负责管理对象间错综复杂的依赖关系图。现代Spring推荐使用构造器注入,因为它能明确表示强依赖、保证依赖不可变(final字段)、避免循环依赖问题,并且更利于测试。

  • AOP (面向切面编程):

    • 解决的痛点: 诸如日志、事务、安全、性能监控等代码,往往会横向散布在众多业务方法中,造成大量重复代码(“横切关注点”),使得核心业务逻辑不清晰,修改和维护困难。

    • 解决方案: AOP允许将这些散布的代码抽取到一个独立的模块中,称为切面(Aspect)。然后通过切入点(Pointcut) 定义在哪些方法的哪些位置(连接点 - Join Point)应用这个切面,并通过通知(Advice) 定义具体的增强逻辑(如@Before@After@Around)。

    • 联想扩展: Spring AOP默认使用动态代理实现。如果目标对象实现了接口,则采用JDK动态代理;如果没有,则使用CGLIB字节码增强。这意味着它主要适用于方法级别的拦截。@Transactional的事务管理就是AOP的经典应用,它通过在方法前后代理性地开启和提交/回滚事务来实现。


二、Spring MVC & Spring Boot

2. 描述一下Spring MVC处理一个HTTP请求的完整工作流程。

优秀详细回答(含扩展):

  1. Http Request: 用户发起请求,被Web容器(如Tomcat)的DispatcherServlet(前端控制器,所有请求的统一入口)捕获。

  2. HandlerMapping: DispatcherServlet 查询一个或多个 HandlerMapping Bean,根据请求的URL路径,找到能够处理该请求的处理器(Handler) 和拦截器(Interceptor)链

  3. HandlerAdapter: DispatcherServlet 通过 HandlerAdapter 来实际执行找到的处理器(通常是@Controller中的方法)。HandlerAdapter 屏蔽了处理器的不同类型(如基于注解的Controller、旧的Controller接口实现等),是一种适配器模式的应用。

  4. Controller Execution: HandlerAdapter 调用目标Controller方法。在此过程中,会进行参数绑定(将HTTP参数、路径变量、Body内容等通过@RequestParam@PathVariable@RequestBody等注解解析成方法参数)和校验

  5. Return ModelAndView: Controller方法执行完毕,返回一个String(视图名)、ModelAndView或响应体(@ResponseBody)对象。

  6. ViewResolver: 如果返回的是视图名,DispatcherServlet 会调用 ViewResolver 来将逻辑视图名解析为具体的View对象(如JSP, Thymeleaf, FreeMarker模板)。

  7. View Rendering: View 对象结合Model中的数据,进行渲染,生成最终的响应内容(如HTML)。

  8. Http Response: DispatcherServlet 将渲染结果返回给客户端。

    • 扩展点: 拦截器(Interceptor) 在整个流程的preHandle(Controller方法前)、postHandle(方法执行后,视图渲染前)、afterCompletion(请求完成后)三个时间点提供了横切逻辑的注入能力,常用于权限检查、日志记录等。

3. Spring Boot的“约定大于配置”和“自动配置”是如何实现的?能深入讲讲它的启动过程吗?

优秀详细回答(含扩展):

  • 约定大于配置: Spring Boot通过建立大量默认约定,极大减少了决策和配置点。例如:默认内嵌Tomcat、默认的application.properties配置文件名、默认的静态资源路径(/static/public)、Starter依赖约定(spring-boot-starter-*提供了某方面开发所需的一切依赖)。

  • 自动配置(Auto-Configuration)的核心魔法:

    • 它基于条件化配置,通过大量的@Conditional注解(如@ConditionalOnClass@ConditionalOnProperty@ConditionalOnMissingBean)来实现。

    • 原理: Spring Boot启动时会加载spring-boot-autoconfigurejar包下的META-INF/spring.factories文件,该文件中定义了大量的自动配置类

    • 这些配置类不会全部生效。每个配置类上都有一系列@Conditional条件。例如DataSourceAutoConfiguration只有在classpath下存在DataSource.class且用户没有自己配置DataSource Bean时才会生效。它根据application.properties中的spring.datasource.*属性自动配置一个数据源。

  • Spring Boot启动过程深度解析:

    1. SpringApplication.run() 这是入口。

    2. 准备环境(Environment): 加载application.properties、系统属性、命令行参数等。

    3. 创建应用上下文(ApplicationContext): 通常是AnnotationConfigServletWebServerApplicationContext

    4. 刷新上下文(核心中的核心 - refresh()方法): 这是Spring容器启动的标准流程,Spring Boot在此基础上进行了扩展:

      • BeanFactory初始化 -> BeanFactoryPostProcessor执行(这里会解析配置属性)。

      • BeanPostProcessor注册

      • onRefresh(): 这是Spring Boot的扩展关键点!在这个方法里,ServletWebServerApplicationContext会调用ServletWebServerFactory 创建内嵌的Web服务器(如Tomcat),并初始化DispatcherServlet

      • 注册DispatcherServlet,并启动Web服务器。

      • ** finishRefresh():** 完成上下文刷新,发布相应事件。

    5. 执行CommandLineRunner/ApplicationRunner 容器完全启动后,会执行这些Runner接口的实现,用于做一些后置初始化工作。


三、MyBatis

4. MyBatis中 #{} 和 ${} 的本质区别是什么?为什么 #{} 能防SQL注入?

优秀详细回答(含扩展):

  • 本质区别: 这不仅是用法区别,更是安全理念的区别。

    • #{} 是参数化查询(PreparedStatement) 的占位符。MyBatis会将其转换为?,然后通过PreparedStatement.setXXX()方法来安全地设置参数值。数据库引擎会将传入的值纯粹地当作数据来处理,而不是SQL代码的一部分。

    • ${} 是字符串拼接。MyBatis会将其直接替换为变量的字面值,然后拼接成完整的SQL语句。如果这个值来自用户输入,且包含SQL片段,就会被数据库引擎解析并执行。

  • 防注入原理: #{} 利用了JDBC的PreparedStatement机制。SQL语句的结构(模板) 在预编译阶段就已经确定下来了。后续传入的参数,无论内容是什么,都无法改变这个结构。例如,即使用户输入' OR '1'='1,在数据库看来,它也只是一个完整的字符串值,用于和username字段进行比较,而绝不会变成OR条件逻辑。这就从根本上杜绝了SQL注入。

  • ${} 的正确使用场景: 用于动态指定表名字段名非值数据。例如:ORDER BY ${columnName}。但即使如此,也绝对不能直接使用用户输入,必须在代码层面对columnName进行白名单校验,防止用户传入恶意参数(如; DROP TABLE users; --)。

5. 谈谈MyBatis的缓存机制,以及你在项目中是如何管理/使用它的?

优秀详细回答(含扩展):

MyBatis提供了一级缓存和二级缓存。

  • 一级缓存:

    • 范围: SqlSession级别,默认开启。

    • 生命周期: 与一次数据库会话相同。在同一个SqlSession中执行相同的查询,第二次会直接返回缓存的结果。

    • 失效时机: 执行任何增、删、改操作(insert|update|delete)、手动清空缓存sqlSession.clearCache())、提交/回滚事务关闭SqlSession

    • 注意: 在Spring中,通常将SqlSession的生命周期与事务绑定。一个事务通常对应一个SqlSession。因此,一级缓存在一个事务内有效。

  • 二级缓存:

    • 范围: Mapper (Namespace)级别,需要手动在Mapper XML文件中添加<cache/>标签来开启。

    • 生命周期: 应用级别。多个SqlSession共享同一个Mapper的二级缓存。

    • 工作流程: 事务提交后,查询结果才会被放入二级缓存。读取顺序是:二级缓存 -> 一级缓存 -> 数据库

    • 严重问题与应对策略:

      • 分布式环境脏读: 在单机多实例或分布式部署时,多个应用实例各有自己的二级缓存,一个实例修改了数据,无法通知其他实例清空缓存,导致脏读

      • 策略: 在生产环境中,通常禁用MyBatis自带的二级缓存。对于需要缓存的热点数据,使用集中式缓存中间件,如RedisMemcached。它们提供分布式一致性,可以完美解决多个应用实例间的缓存同步问题。MyBatis提供了Cache接口,可以方便地整合这些第三方缓存。


四、Spring Cloud Alibaba 微服务生态

6. 服务注册中心Eureka和Nacos的核心区别是什么?为什么你们选择Nacos?

优秀详细回答(含扩展):

特性NacosEureka深度解析
功能定位服务发现 + 动态配置管理仅服务发现Nacos “一个组件,两大功能”,简化技术栈,降低运维成本。
一致性协议AP + CP 模式可切换 (默认为AP)AP这是最核心的区别。Nacos支持Raft协议实现CP模式,在要求强一致性的场景(如服务元信息、配置管理)下至关重要。Eureka只为服务发现设计,遵循AP原则,保证高可用。
健康检查TCP/HTTP/MYSQL/Client BeatClient BeatNacos的检查机制更丰富、更灵活,对非HTTP服务(如数据库)的支持更好。
雪崩保护两者都能在服务器端保护自己,防止因大量实例摘除导致流量洪峰。
性能与容量官方称能支撑百万级服务实例万级实例Nacos在数据模型和通信协议上做了优化,承载能力更强。
生态与社区Spring Cloud Alibaba,中文社区活跃,阿里持续维护Netflix,已进入维护模式,新特性少技术选型必须考虑长期维护性和社区活力。

选择Nacos的理由: 功能强大(服务+配置)、一致性模型更灵活、性能更高、社区活跃、符合国内技术发展趋势。

7. Sentinel的熔断降级规则有哪几种?分别适用于什么场景?

优秀详细回答(含扩展):

Sentinel提供三种熔断策略:

  1. 慢调用比例 (SLOW_REQUEST_RATIO):

    • 规则: 统计单位时间内,响应时间超过设定阈值(maxRt)的请求的比例。若比例超过设定值,且请求数达到最小请求数,则触发熔断。

    • 适用场景: 下游服务响应变慢。例如,依赖的某个接口平时耗时50ms,突然变成2s,大量请求被拖慢。此时用此规则可以快速失败,避免线程池被占满导致整个服务雪崩。

  2. 异常比例 (ERROR_RATIO):

    • 规则: 统计单位时间内,异常请求数量的比例。若比例超过设定值,且请求数达到最小请求数,则触发熔断。

    • 适用场景: 下游服务出现大量错误(如5xx、超时、网络异常)。例如,数据库连接池耗尽,导致大量请求抛出异常。此规则可以快速熔断,给下游服务恢复的时间。

  3. 异常数 (ERROR_COUNT):

    • 规则: 统计单位时间内,异常请求的数量。若数量超过设定值,则触发熔断。

    • 适用场景: 与异常比例类似,但它不关心总量,只关心绝对数量。适用于对绝对故障量敏感的场景。

熔断后的行为: 进入熔断状态后,所有对该资源的访问都会立即抛出DegradeException。经过一段熔断时长(timeWindow 后,会进入半开状态,释放一个请求尝试通信。如果成功,则关闭熔断;如果失败,则继续保持熔断。

8. Seata的AT模式如何实现全局一致性和回滚?它的“全局锁”机制是怎样的?

优秀详细回答(含扩展):

  • 一阶段(执行并注册分支事务):

    1. 解析SQL: RM(Resource Manager,即每个微服务)拦截业务SQL,解析其语义,生成查询数据的前置镜像(修改前的数据)和后置镜像(修改后的数据)。

    2. 执行业务SQL并提交。

    3. 注册分支事务: RM向TC(Transaction Coordinator)注册分支事务,并将前后镜像数据行锁信息报告给TC。

    4. 本地事务提交: 关键点: 一阶段就提交了本地事务,释放了本地锁。这保证了高并发场景下数据的性能。

  • 二阶段-提交:

    1. TC收到所有分支事务的成功报告后,发起全局提交。

    2. RM收到TC的提交指令,异步地、快速地删除undo_log日志记录。

    3. 因为一阶段已经提交,所以二阶段提交非常快。

  • 二阶段-回滚:

    1. TC收到任一分支事务的失败报告后,发起全局回滚。

    2. RM收到回滚指令,根据一阶段保存的undo_log中的前置镜像,生成反向的回滚SQL并执行。

    3. 回滚完成后,删除undo_log记录。

  • 全局锁机制(防脏写):

    • 问题: 在一阶段提交后、二阶段完成前,如果其他分布式事务要修改同一数据,可能造成脏写

    • 解决方案: Seata通过全局锁来避免。在一阶段,RM在向TC注册分支事务时,会尝试获取相关数据的全局锁

    • 如果获取成功,才进行后续操作。

    • 如果获取失败(说明其他分布式事务正在修改该数据),则会进行重试。

    • 这个全局锁由TC统一管理,保证了在分布式事务上下文中,对数据的写操作是串行化的,避免了脏写。


五、综合设计与深度

9. 设计一个高并发“秒杀系统”,你会从哪些层面考虑?核心思路是什么?

优秀详细回答(含扩展):

核心思路:分层过滤、逐级削峰、异步化、极致缓存、故障隔离

  1. 前端/客户端层:

    • 静态化: 将活动页、商品详情页等提前静态化,推送到CDN,减少后端压力。

    • 按钮控制: 点击后置灰,防止用户重复提交。

    • 验证码/答题: 在秒杀开始前出现,拉长请求间隔,削峰填谷。

  2. 网关层:

    • 限流: 使用Sentinel进行最严厉的限流,将90%以上的无效/超额请求直接拒绝在最外层。

    • 恶意请求过滤: 识别并拦截刷单脚本、机器人请求。

  3. 服务层:

    • 缓存热点数据: 将秒杀商品库存、活动信息等预热到Redis中。所有读写操作都在Redis中进行。

    • Redis原子操作扣减库存: 使用DECRLUA脚本保证库存扣减的原子性,防止超卖。

    • 异步化下单: 核心思想:同步校验,异步执行

      • 在Redis中完成资格(库存、用户频次)校验后,立即返回“抢购排队中”。

      • 将生成订单的请求发送到消息队列(如RocketMQ/Kafka) 中。

      • 后端服务异步消费消息,执行创建订单、扣减DB库存等耗时久的落盘操作

    • 服务隔离: 将秒杀系统与主站业务隔离部署,使用独立的数据库、Redis集群,避免秒杀流量拖垮整个网站。

  4. 数据层:

    • 数据库优化: 最终订单数据入库时,可能涉及批量插入等优化手段。

10. 在分布式系统中,如何保证接口的幂等性?请举例说明。

优秀详细回答(含扩展):

幂等性是指一次请求和多次请求对系统资源造成的改变是一致的。

  • 方案一:数据库唯一键约束(最简单有效)

    • 实现: 利用数据库主键或唯一索引的排他性。例如,订单表的订单ID、支付流水表的流水号。

    • 场景: 创建业务唯一的场景,如创建订单。插入时如果发生唯一键冲突,数据库会抛出异常,插入失败,保证了不会产生重复数据。

  • 方案二:Token机制(适用于新增和更新)

    • 流程:

      1. 客户端先向服务端申请一个全局唯一的Token(通常与业务关联,如订单ID),服务端将Token存入Redis并设置有效期。

      2. 客户端执行业务请求时携带此Token。

      3. 服务端接口逻辑:

        • 检查Redis中是否存在该Token。

        • 如果不存在,说明是重复请求,返回错误。

        • 如果存在,则原子性地(getdel或Lua脚本)删除Token,然后执行业务逻辑。

    • 关键: 检查Token和删除Token必须是原子操作,否则在高并发下可能失效。

  • 方案三:乐观锁(适用于更新操作)

    • 实现: 在数据表中增加一个version版本号字段。

    • SQL: UPDATE table SET stock = stock - 1, version = version + 1 WHERE id = #id# AND version = #oldVersion#

    • 原理: 执行更新时,带上旧的版本号作为条件。如果版本号已被其他请求修改,则本次更新影响的行数为0,即可判断为重复请求或数据已过期。

  • 方案四:状态机(适用于有状态流转的业务)

    • 实现: 业务数据本身有明确的状态流转(如订单状态:0未支付->1已支付->2已发货)。在设计接口时,只有在上一个状态符合预期时,才允许执行状态变更。

    • SQL: UPDATE order SET status = 1 WHERE order_id = #orderId# AND status = 0

    • 如果更新条数为0,说明订单已不是未支付状态,可能是重复支付请求。

六、分布式事务

问题 1: 什么是分布式事务?为什么在微服务架构下它会成为一个难题?除了Seata,你还了解哪些常见的分布式事务解决方案?

优秀详细回答(含扩展):

  • 定义: 分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统节点上。一个分布式事务通常由多个独立的本地事务组成,这些本地事务要么全部成功,要么全部失败,需要满足ACID特性中的原子性(Atomicity)

  • 成为难题的原因:

    1. 网络问题: 微服务间通过网络调用,网络延迟、丢包、中断都会导致事务参与者状态不一致。

    2. 服务可用性: 任何一个参与事务的微服务宕机,都会导致整个事务无法完成,需要设计完善的恢复和补偿机制。

    3. 数据一致性: 每个服务有自己的私有数据库(Database per Service),无法再像单体应用一样依靠数据库本身的事务机制(如MySQL的InnoDB事务)来保证一致性。这被称为 “跨库事务” 或 “跨数据库事务”

    4. 性能开销: 维持强一致性需要复杂的协调和同步机制(如两阶段提交),这会带来巨大的性能损耗,与微服务追求的高可用和高性能目标相悖。

  • 常见解决方案(知识广度):

    • 两阶段提交 (2PC - Two-Phase Commit): 如JTA。一个协调者协调所有参与者进行“准备”和“提交”两个阶段。优点是强一致性。缺点是同步阻塞、性能差、协调者单点故障。

    • 三阶段提交 (3PC): 在2PC基础上增加了超时机制和预提交阶段,缓解了阻塞和单点问题,但实现更复杂,且仍无法完美解决数据不一致问题。

    • TCC (Try-Confirm-Cancel): 一个业务层面的分布式事务解决方案。它将事务分为三个阶段:

      • Try: 尝试执行,完成所有业务检查,并预留必需的业务资源(如冻结库存、预扣优惠券)。

      • Confirm: 确认执行,真正执行业务操作,使用Try阶段预留的资源。Confirm需要保证幂等性

      • Cancel: 取消执行,释放Try阶段预留的资源。Cancel需要保证幂等性

      • 优点:性能较好,避免了长事务对资源的锁定。缺点:对业务侵入性强,需要为每个操作实现三个接口。

    • 可靠消息最终一致性(异步确保): 依靠消息队列的可靠性来保证数据最终一致。核心流程是:1. 业务执行完成 -> 2. 发送消息 -> 3. 其他服务消费消息。为了保证前两步的原子性,常用事务消息表RocketMQ的事务消息机制。这是互联网公司最常用的模式之一。

    • 最大努力通知: 更弱的一种最终一致性方案。服务A完成业务后,最大努力地向服务B发送通知,直到B返回成功。适用于对一致性要求不极高的场景,如支付结果通知。

总结: Seata的AT模式可以看作是对2PC的优化,它通过undo_log实现了“无侵入”的补偿机制,但其全局锁在超高并发下可能成为瓶颈。技术选型时,需要在一致性强度性能业务侵入性三者之间做出权衡。


问题 2: Seata的AT模式、TCC模式和XA模式有什么区别?分别在什么场景下选用?

优秀详细回答(含扩展):

这是一个考察对Seata不同工作模式理解深度的问题。

特性模式AT模式 (默认)TCC模式XA模式
原理两阶段提交的优化。一阶段提交本地事务并生成回滚日志;二阶段异步删除或回滚。业务层面的两阶段提交。需要开发者实现Try/Confirm/Cancel三个接口。基于数据库原生XA协议的标准两阶段提交。
侵入性无代码侵入(像使用普通本地事务一样加@GlobalTransactional即可)。侵入性极强,需要根据业务逻辑实现三个阶段的方法。无代码侵入,但需要数据库支持XA协议。
隔离性默认读未提交,通过全局锁实现写隔离,可防止脏写。通过业务代码在Try阶段预留资源隔离性最好完全满足ACID的隔离性。
性能较好(一阶段已提交,二阶段异步)。非常好(一阶段只是预留资源,不锁数据库记录)。很差(整个事务期间全程锁资源,同步阻塞)。
适用场景绝大多数非旧系统改造的场景。希望用最小代价解决分布式事务问题。高性能要求需要自定义隔离级别的场景。如金融核心系统、积分兑换、资金处理。旧系统改造且数据库支持XA。传统银行系统应用较多。
不适用场景不支持非关系型数据库;高性能场景下全局锁可能成瓶颈。业务逻辑复杂的场景,实现TCC成本非常高。高并发长事务场景,性能无法接受。

选型建议:

  • 首选AT模式: 对于90%的业务场景,AT模式因其无侵入够用的性能是最佳选择。

  • 考虑TCC模式: 当你的业务是资金、交易核心等高敏感度、高性能要求的场景,并且愿意投入开发成本实现复杂的资源预留逻辑时,选用TCC。

  • 慎用XA模式: 除非是遗留系统且技术栈受限,否则在微服务架构下一般不选用XA模式,因为其性能瓶颈明显。


问题 3: 在使用Seata AT模式时,如果某个服务在第二阶段(回滚阶段)失败,比如undo_log表被误删或网络问题导致回滚SQL执行失败,Seata会如何处理?

优秀详细回答(含扩展):

这是一个考察对Seata运维和异常处理机制理解的问题。回滚失败是一个极端但必须考虑的场景。

  1. 自动重试机制: Seata的TC(事务协调器)在向RM(资源管理器)发送回滚指令后,如果没有得到成功的响应(例如网络超时、RM宕机),TC会自动进行重试。重试策略通常包括次数和间隔。

  2. 人工介入处理(最终保障): 如果重试多次后依然失败,Seata会将这个全局事务标记为回滚失败(Rollback Retry Failed) 状态。此时,自动恢复已经无法解决这个问题,必须引入人工干预

    • Seata Dashboard: 运维人员可以在Dashboard上查看到所有状态异常的事务。

    • 处理方式: 人工排查回滚失败的原因(例如:数据库宕机、undo_log表损坏、数据被人为修改等)。修复底层问题后,可以在Dashboard上手动触发重试回滚,或者根据日志手动执行补偿操作。

  3. 启示与最佳实践:

    • 监控报警: 必须对Seata TC中的异常事务状态设置强有力的监控和报警,确保能第一时间发现回滚失败的问题。

    • 日志完善: 确保Seata和应用的日志记录完整,包含全局事务ID(XID)、分支事务ID等信息,方便人工排查时进行链路追踪。

    • 防止误操作: undo_log表是Seata的生命线,要避免对其进行不必要的操作。

这个问题的回答体现了你不仅会用工具,还对其可靠性边界运维实践有深刻理解,这是高级工程师的必备素质。


问题 4: TCC模式要求实现Try、Confirm、Cancel三个接口。在实际编码中,要实现一个可靠的TCC接口,需要注意哪些关键点?

优秀详细回答(含扩展):

实现TCC接口绝非简单实现三个方法那么简单,必须考虑各种异常情况。

  1. 空回滚(Empty Rollback):

    • 问题: 由于网络超时等原因,TC在未收到Try请求的情况下,发起了Cancel请求。

    • 解决: 在Cancel接口中,需要检查Try是否执行过。通常需要一张分布式事务控制表,在Try阶段成功插入一条记录,标记该分支事务已尝试。Cancel时先查此表,如果不存在记录,说明是空回滚,直接返回成功即可。

  2. 幂等性(Idempotence):

    • 问题: 由于网络抖动,TC可能重复调用Confirm或Cancel接口。

    • 解决: Confirm和Cancel接口必须是幂等的。同样可以借助分布式事务控制表。在Confirm/Cancel执行成功后,在该表中记录状态(如 status = 'confirmed')。下次再被调用时,先检查状态,如果已经是完成状态,则直接返回成功。

  3. 防悬挂(Hang Prevention):

    • 问题: Try因网络阻塞超时,导致TC发起Cancel(空回滚)。之后阻塞的Try请求又到达了服务并执行成功。此时,Try操作预留的资源就再也无法被Cancel了,被“悬挂”了起来。

    • 解决: 在Try接口中,需要检查当前全局事务是否已回滚。可以查询控制表,如果已有该XID的Cancel记录,则拒绝执行Try操作,直接抛出异常。

  4. 资源预留(Resource Preparation):

    • Try操作不能只是做检查,必须完成所有资源的预留。例如,扣减库存不是直接扣DB库存,而是冻结库存frozen_stock字段);扣金额不是直接扣账户余额,而是冻结资金

总结: 一个生产可用的TCC接口,几乎必然需要一张额外的控制表来记录事务状态,以实现空回滚、幂等和防悬挂的控制。这增加了开发的复杂性,但换来了极高的事务可靠性。


问题 5: 可靠消息最终一致性方案是如何保证消息生产者和消费者两端的数据一致的?请详细描述RocketMQ事务消息的机制。

优秀详细回答(含扩展):

这是除了Seata之外,最主流的一种分布式事务解决方案,其核心是保证消息一定能可靠地投递和消费

以RocketMQ为例,其事务消息机制流程如下:

  1. 发送半消息(Half Message): 生产者向RocketMQ发送一条“半消息”,这条消息对消费者是不可见的,即暂时不能被消费。

  2. 执行本地事务: 生产者执行本地数据库事务(例如:订单状态更新为“已支付”)。

  3. 提交/回滚消息(Local Transaction Execution):

    • 如果本地事务成功,生产者向RocketMQ发送一个Commit指令。此时,半消息才变为正常消息,对消费者可见。

    • 如果本地事务失败,生产者发送一个Rollback指令,RocketMQ会丢弃这条半消息。

  4. 事务状态回查(Transaction Check): 这是解决生产者端一致性的关键! 如果生产者执行完本地事务后,由于宕机、网络异常等原因,一直没有给RocketMQ发送Commit或Rollback指令,这条消息就会处于“未知”状态。RocketMQ会定期回调生产者提供的一个接口,询问这个事务的最终状态。生产者需要根据本地事务记录(例如查订单表状态)来返回Commit或Rollback。

通过以上4步,保证了消息生产者端的事务与消息发送的最终一致性:只要本地事务成功,消息最终一定会被发出;只要失败,消息最终一定会被丢弃。

对于消费者端的一致性:

  1. 幂等消费: 消息队列通常提供 “至少一次(At Least Once)” 的投递语义,意味着同一条消息可能会被消费多次。因此,消费者必须实现幂等性。常见方法有:利用数据库唯一键、状态机、或分布式锁(Redis setNX)来判断消息是否已被处理过。

  2. 人工介入: 如果消费者消费一直失败(业务逻辑错误),消息会进入死信队列(Dead-Letter Queue)。此时需要监控死信队列,并人工介入处理

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐