分布式事务回顾 分布式事务是指在分布式系统环境下跨多个服务节点或数据库的操作需要保证数据的一致性。其核心目标是实现传统单机事务的 ACID 属性原子性、一致性、隔离性、持久性但在分布式场景中由于网络延迟、节点故障等因素实现强一致性往往伴随着性能损耗因此需要在一致性与可用性之间进行权衡。一分布式事务一、核心理论基础分布式事务的设计离不开对以下两个基础理论的权衡CAP 定理‌指出在分布式系统中一致性Consistency、可用性Availability和分区容错性Partition tolerance三者不可兼得通常只能满足其中两项。一致性‌所有节点在同一时间看到的数据是一样的。可用性‌每次请求都能得到响应不保证是最新数据。分区容错性‌网络分区时系统仍能运行这是分布式系统的基本要求。权衡大多数互联网系统选择 AP高可用分区容错通过最终一致性来弥补 C 的不足。BASE 理论‌是对 CAP 中 AP 模式的补充主张基本可用、软状态和最终一致性适合对实时一致性要求不高的场景。基本可用 (Basically Available)‌系统出现故障时允许损失部分非核心可用性。软状态 (Soft state)‌允许系统中间存在不一致的状态。最终一致性 (Eventually consistent)‌经过一段时间后所有数据副本最终能达到一致状态。二、主流实现方案对比市面上没有绝对完美的方案需根据业务场景选择方案模式核心机制优点缺点适用场景‌2PC / XA‌两阶段提交预提交锁定资源提交阶段执行更新。由事务管理器协调。强一致性对业务代码侵入小。性能差同步阻塞协调者单点故障会导致资源长期锁定。金融核心交易等对一致性要求极高的场景。‌TCC‌Try-Confirm-Cancel业务层手动定义预留、确认、取消逻辑。性能高无长事务锁一致性可控。代码侵入性强需编写三套逻辑开发成本高。支付、转账等高并发且对一致性要求较高的核心业务。‌Saga‌长事务拆分将大事务拆分为多个本地短事务失败时执行逆向补偿。异步执行不阻塞容错性好。一致性难以保证可能读到中间状态需编写补偿逻辑。旅行订单机票酒店租车等多步骤长流程业务。‌本地消息表 / 事务消息‌最终一致性利用消息队列和本地事务表保证消息必达消费端幂等处理。高性能异步解耦实现最终一致性。不支持实时一致性需处理消息重试和幂等性。电商下单后发短信、加积分等异步解耦场景。‌Seata AT‌自动代理数据源解析 SQL 生成前后镜像失败时自动回滚。无侵入只需加注解开发简单。依赖全局锁高并发下可能有锁竞争性能瓶颈。大多数常规微服务业务场景。三、落地建议与避坑指南框架选择‌推荐使用 ‌Seata‌它整合了 AT、TCC、Saga、XA 四种模式能显著降低开发复杂度。AT 模式‌适合大多数无特殊要求的场景自动回滚无侵入。TCC 模式‌适合核心资金业务需手动控制补偿逻辑。场景化选型‌金融系统‌优先选择 TCC 或 2PC/XA确保资金绝对安全。电商系统‌推荐 Saga 模式配合状态机处理复杂的长流程订单。异步解耦‌使用事务消息如 RocketMQ 事务消息或本地消息表如下单后通知库存扣减或发送营销短信。关键避坑点‌能避免则避免‌最优解是通过业务设计如合并微服务、数据冗余、单库操作减少跨服务调用从源头规避分布式事务。幂等性处理‌无论采用哪种方案下游服务必须实现接口幂等性防止因网络重试导致的数据重复处理。超时与监控‌设置合理的超时时间并建立完善的分布式事务监控告警机制以便及时发现悬挂或未完成的事务。结合之前讲解的分布式事务特别是本地消息表、Saga模式及Seata框架的相关背景分布式接口幂等性设计是保障数据一致性的最后一道防线。在分布式环境下由于网络抖动、重试机制或用户重复点击同一个请求可能被多次提交幂等性设计的核心目标是‌确保同一操作无论执行多少次对系统状态的影响与执行一次完全相同‌。二分布式接口幂等性设计一、核心设计原则‌唯一标识‌每个业务请求必须携带唯一的业务ID如订单号、流水号、UUID作为幂等校验的基准。‌先查后改/原子更新‌在执行写操作前先判断该业务ID是否已处理或利用数据库的唯一索引约束保证物理层面的唯一性。‌状态机控制‌对于有明确状态流转的业务如订单从“待支付”到“已支付”通过限制状态变更方向来实现幂等。二、主流实现方案对比方案实现逻辑优点缺点适用场景‌数据库唯一索引‌建立包含业务ID的唯一索引插入重复数据时抛异常。实现简单可靠性最高强一致性。依赖数据库高并发下性能有瓶颈。核心资金交易、订单创建等关键业务。‌Token 机制‌服务端生成唯一Token存入Redis客户端提交时携带服务端校验并删除Token。防止表单重复提交用户体验好。需额外维护Token生命周期存在竞态条件风险。前端页面防抖、防止用户快速双击。‌Redis 原子操作‌使用 SETNX 或 INCR 命令以业务ID为Key设置过期时间。性能极高支持高并发。需处理Redis故障存在短暂不一致风险。高频访问的非核心业务、点赞、计数。‌状态机乐观锁‌更新SQL带上版本号或原状态条件UPDATE table SET status1 WHERE idxxx AND status0。无额外存储开销逻辑清晰。仅适用于有状态流转的场景。订单状态变更、审批流程。‌去重表/日志表‌专门建立一张去重表记录已处理的业务ID利用唯一索引拦截。解耦业务逻辑便于审计追踪。增加了一次数据库IO。异步消息消费、批量数据处理。三、落地实施建议‌分层防御‌‌前端层‌按钮点击后置灰、禁用减少无效请求。‌网关层‌基于IP或用户ID进行限流拦截异常高频请求。‌服务层‌采用 Token Redis 或 唯一索引 进行核心幂等校验。‌异常处理‌当检测到重复请求时不应直接报错而应返回“成功”或“处理中”的状态码避免触发上游系统的自动重试机制导致雪崩。‌与分布式事务配合‌在使用 Seata AT 或 TCC 模式时幂等性尤为重要。因为分布式事务的重试机制如 TCC 的 Confirm/Cancel 重试可能导致同一分支事务被多次调用必须在业务代码中通过去重表或状态判断来保证最终结果的正确性。四、避坑指南‌Token 竞态问题‌校验 Token 和删除 Token 必须是原子操作建议使用 Lua 脚本在 Redis 中一次性完成防止多线程同时校验通过。‌唯一索引冲突性能‌在高并发插入场景下大量唯一索引冲突会导致数据库性能下降可考虑先在 Redis 中预检再落库。‌业务ID生成规范‌确保业务ID的全局唯一性推荐使用雪花算法Snowflake或 UUID避免自增ID在多节点环境下重复。三基于 ‌Redis Lua 脚本‌ 实现的高性能幂等性校验工具类代码一、核心 Lua 脚本逻辑脚本逻辑判断 Key 是否存在 - 存在则删除并返回 1校验通过 - 不存在则返回 0重复请求。--key:业务唯一标识或Token--如果 key 存在删除它并返回1否则返回0ifredis.call(exists,KEYS)1thenreturnredis.call(del,KEYS)elsereturn0end二、Java 工具类实现基于 Spring Data Redis 封装支持泛型 Key确保高并发下的原子性校验。importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Component;importjavax.annotation.Resource;importjava.util.Collections;importjava.util.UUID;ComponentpublicclassIdempotentUtils{ResourceprivateRedisTemplateString,ObjectredisTemplate;// 定义 Lua 脚本privatestaticfinalStringLUA_SCRIPTif redis.call(exists, KEYS) 1 then return redis.call(del, KEYS) else return 0 end;/** * 执行幂等性校验 * param key 业务唯一标识如 Token 或 业务流水号 * return true-首次请求校验通过false-重复请求 */publicbooleancheckIdempotent(Stringkey){DefaultRedisScriptLongredisScriptnewDefaultRedisScript(LUA_SCRIPT,Long.class);// 执行脚本KEYS 为传入的 keyLongresultredisTemplate.execute(redisScript,Collections.singletonList(key));returnresult!nullresult1L;}/** * 生成并缓存 Token用于前端防重提交场景 * param expireTime 过期时间秒 * return 唯一 Token */publicStringgenerateToken(longexpireTime){StringtokenUUID.randomUUID().toString().replace(-,);redisTemplate.opsForValue().set(token,1,expireTime,java.util.concurrent.TimeUnit.SECONDS);returntoken;}}三、业务层调用示例在 Controller 或 Service 中先获取 Token 或业务 ID再调用工具类进行校验。RestControllerpublicclassOrderController{ResourceprivateIdempotentUtilsidempotentUtils;PostMapping(/createOrder)publicResultcreateOrder(RequestHeader(X-Token)Stringtoken){// 1. 执行幂等校验if(!idempotentUtils.checkIdempotent(token)){returnResult.fail(请勿重复提交);}// 2. 执行业务逻辑...returnResult.success(订单创建成功);}}四、方案优势与注意事项‌原子性保障‌Lua 脚本在 Redis 中单线程执行exists 和 del 操作不会被其他命令打断杜绝了并发漏洞。‌高性能‌减少了网络往返次数RTT只需一次交互即可完成校验与状态变更。‌Key 的设计‌‌防重提交‌Key 为前端生成的 Token校验通过后立即删除确保 Token 一次性有效。‌业务幂等‌Key 为业务流水号如订单号若需允许短时间内的重试可将 del 改为设置一个短暂的过期时间如 setex而非直接删除。

本月热点