分布式服务幂等性问题如何解决

news/2025/3/17 20:22:00/文章来源:https://www.cnblogs.com/jimoliunian/p/18777501

在分布式系统中解决幂等性问题是保证系统健壮性的关键挑战之一。以下从原理到实践的完整解决方案,涵盖7种核心模式及落地实现细节:


一、幂等性问题本质

核心定义:相同请求多次执行对系统状态的影响 = 执行一次的结果
产生场景

  • 网络抖动导致客户端重复提交
  • 服务端超时后重试机制
  • MQ消费者重复消费消息
  • 分布式事务补偿机制
# 典型案例:支付重复扣款
curl -X POST https://pay/order/123
请求结果:因网络超时未响应 → 客户端重试 → 两次扣款❗

二、解决方案全景图

方案 适用场景 原理复杂度 实现成本
唯一请求ID 大部分写操作 ★☆☆
数据库唯一约束 数据强唯一性场景 ★★☆
Token机制 防表单重复提交 ★☆☆
状态机流转 订单/工单流程类系统 ★★★
分布式锁 高并发竞争资源 ★★☆
版本号控制(乐观锁) 存在版本属性的更新 ★★☆
去重表(Redis/HBase) 高频临时性幂等校验 ★★☆

三、核心解决方案详解

1. 唯一请求ID模式

实现流程

sequenceDiagramparticipant Clientparticipant Serverparticipant DBClient->>Server: 请求携带全局ID(X-Request-ID)Server->>DB: 查询是否存在相同IDalt 首次请求DB-->>Server: 不存在记录Server->>DB: 执行业务操作+记录IDServer-->>Client: 返回成功else 重复请求DB-->>Server: 已存在记录Server-->>Client: 返回先前结果end

技术要点

  • ID生成使用雪花算法(Snowflake)或Redis原子操作
  • 存储使用Redis(TTL自动过期)或MySQL(需定期清理)
  • 防暴力破解:ID长度≥20位(包含时间戳+随机数)

示例代码(Spring Boot+Redis):

@RestController
public class PaymentController {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@PostMapping("/pay")public ResponseEntity<?> pay(@RequestHeader("X-Request-ID") String requestId, @RequestBody PaymentRequest request) {// Redis原子操作判断是否存在Boolean isAbsent = redisTemplate.opsForValue().setIfAbsent("payment:req:"+requestId, "processing", 5, TimeUnit.MINUTES);if (!isAbsent) {return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("code": 409, "message": "Duplicate request"));}try {// 真实支付逻辑...return ResponseEntity.ok(Map.of("status", "success"));} finally {// 完成处理时更新状态(异步操作)redisTemplate.opsForValue().set("payment:req:"+requestId, "completed");}}
}

2. 数据库唯一约束

设计模式

-- 订单表增加唯一索引
ALTER TABLE orders ADD UNIQUE INDEX uniq_request (user_id, request_hash);-- 请求哈希生成算法(Java示例)
String requestHash = DigestUtils.md5Hex(userId + orderNo + productId + amount
);

异常处理

@Transactional
public void createOrder(Order order) {try {orderDao.insert(order); // 触发唯一约束} catch (DataIntegrityViolationException ex) {// 抓取重复提交异常Order existing = orderDao.selectByHash(order.getRequestHash());throw new DuplicateOrderException(existing.getOrderNo());}
}

性能优化

  • 分库分表时需确保唯一键在分片键内
  • 高频写场景配合消息队列削峰填谷

3. Token令牌机制

交互流程

  1. 客户端先请求获取Token(携带用户ID)
  2. 服务端生成Token存储(Redis: user:123 -> token:xyz)
  3. 客户端提交请求时携带Token
  4. 服务端验证Token后立即删除(原子操作)

关键代码

public class TokenService {public String generateToken(String userId) {String token = UUID.randomUUID().toString();String key = "token:" + userId;redisTemplate.opsForValue().set(key, token, 5, TimeUnit.MINUTES);return token;}public boolean validateToken(String userId, String token) {String key = "token:" + userId;// Lua脚本保证原子性校验+删除String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) else return 0 end";Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),Collections.singletonList(key), token);return result != null && result == 1L;}
}

4. 分布式锁 + 幂等框架整合

架构设计
分布式锁幂等架构

集成框架(推荐)

  • Spring自带@Idempotent注解+AOP
  • Redisson分布式锁(支持多种锁类型)
  • 阿里开源的resilience4j幂等模块

AOP实现示例

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {String key() default "";         // SpEL表达式int ttl() default 300;          // 默认5分钟Class<? extends Payload> type(); // 根据payload类型处理
}@Aspect
@Component
public class IdempotentAspect {@Around("@annotation(idempotent)")public Object checkIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) {// 解析SpEL表达式生成唯一KeyString lockKey = generateKey(joinPoint, idempotent);// Redisson尝试获取锁RLock lock = redissonClient.getLock(lockKey);try {if (lock.tryLock(0, idempotent.ttl(), TimeUnit.SECONDS)) {return joinPoint.proceed();} else {throw new ConcurrentRequestException("请求正在处理中");}} finally {lock.unlock();}}
}

四、特殊场景解决方案

1. MQ消息幂等

RocketMQ方案

// 消费者实现MessageListener
public class OrderListener implements MessageListenerOrderly {@Overridepublic ConsumeOrderlyStatus consumeMessage(List<MessageExt> messages, ConsumeOrderlyContext context) {for (MessageExt msg : messages) {String msgId = msg.getMsgId();if (processed(msgId)) {continue; // 跳过已处理消息}processOrder(msg);markProcessed(msgId);}return ConsumeOrderlyStatus.SUCCESS;}private boolean processed(String msgId) {// Redis记录已处理消息IDreturn !redisTemplate.opsForValue().setIfAbsent("mq:processed:"+msgId, "1", 7, TimeUnit.DAYS);}
}

2. 第三方接口调用

public class ThirdPartyService {@Retryable(value = IOException.class, maxAttempts = 3)@Idempotent(type = Payment.class, key = "#payment.tradeNo")public void callPaymentAPI(Payment payment) {// 使用feign调用对方接口Response resp = paymentClient.create(payment);if (resp.code() == 504) {// 明确要求对方做幂等处理的场景throw new RetryableException("需要重试");}}
}

五、最佳实践建议

  1. 分层防御策略

    • 前端:按钮防重+Token机制(第一道防线)
    • 网关:全局请求ID生成与校验(第二道防线)
    • 服务层:分布式锁/唯一索引(最后屏障)
  2. 监控指标

    # Grafana监控面板关键指标
    idempotent_requests_total{status="duplicate"}  // 重复请求计数
    redis_lock_wait_duration_seconds_bucket        // 锁竞争延迟分布
    db_unique_violations_total                     // 唯一约束冲突数
    
  3. 压力测试要点

    # 使用wrk测试高并发幂等性
    wrk -t12 -c400 -d60s --脚本=post_duplicate.lua http://api/order
    

    测试Lua脚本:

    -- 模拟重复提交同一个请求ID
    request_id = math.random(1000000)
    wrk.headers["X-Request-ID"] = tostring(request_id)
    for i=1,5 do  -- 每个线程重复提交5次wrk.body = '{"amount":100}'wrk.method = "POST"wrk.path = "/order"wrk.request()
    end
    
  4. 容灾方案

    • 白名单机制:在Redis故障时允许特定商户跳过校验
    • 异步对账:定时任务扫描疑似重复数据
    • 熔断降级:检测到存储层异常时暂时关闭复杂幂等校验

通过以上多层级、多维度的幂等解决方案,可满足从简单到复杂的分布式场景需求,结合具体业务特性选择组合策略,可有效保障系统在高并发、网络不稳定环境下的数据一致性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/900549.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一图看懂|2025年经济社会发展10大主要任务

一图看懂|2025年经济社会发展10大主要任务

新朋友:线段树

目录认识线段树和他的梦想 实现梦想之有个小身板 实现梦想之一点一点查 实现梦想之一点一点变 实现梦想之一坨一坨查 实现梦想之一坨一坨变 实现梦想之…不,没能力帮你了1. 认识线段树和他的梦想 差分想必大家都认识,它是可以进行区间加与区间减,但是要查询某个区间的和。他…

攻防世界 get_shell WriteUp

WriteUp 题目信息 来源:攻防世界 名称:get_shell 分类:Pwn 描述:运行就能拿到shell呢,真的题目链接: https://adworld.xctf.org.cn/challenges/list解题思路 首先使用DIE对文件进行查壳,发现这是一个64位ELF文件,所以选择使用64位IDA对文件进行反汇编。发现可以直接运行…

分享一个我遇到过的“量子力学”级别的BUG。

你好呀,我是歪歪。 前几天在网上冲浪的时候,看到知乎上的这个话题:一瞬间,一次历史悠久但是记忆深刻的代码调试经历,“刷”的一下,就在我的脑海中蹦出来了。 虽然最终定位到的原因令人无语,对于日常编码也没啥帮助,但是真的是:情景再现 我记得当时我是学习 Concurren…

day:23 python模块——时间,random,string

一、模块的介绍 (1)python模块,是一个python文件,以一个.py文件,包含了python对象定义和pyhton语句(2)python对象定义和python语句 (3)模块让你能够有逻辑地组织你的python代码段。 (4)把相关的代码分配到一个模块里能让你的代码更好用,更易懂 (5)模块能定义函数…

实验一:Tableau数据可视化入门

实验目的:1.熟悉TableauDesktop使用方法。2.通过Tableau软件来实现Excel中数据的基本可视化。 实验原理: Tableau是新一代商业智能工具软件,它将数据连接、运算、分析与图表结合在 一起,通过拖放方式创建各种图表。 Tableau产品包括TableauDesktop、Tableauserver、Tableau…

实验二:D3数据可视化基础

实验目的:熟悉 D3 数据可视化的使用方法。 实验原理:D3 的全称是(Data-Driven Documents),是一个被数据驱动的文档,其实就是 一个 JavaScript 的函数库,使用它主要是用来做数据可视化的。本次实践主要介绍D3一些最基本的使用方法,以及生成一些比较简单的图表。D3 是一个…

PCB的通孔、盲孔、埋孔|元器件的符号和封装

他们的本质都是用来切换的层的通孔:从顶层到底层,可以看到头 盲孔:看不到头的,如图从第一层切换到了第二层 埋孔:顾名思义就是埋进去了,无论从正面还是反面都是看不到的,它是处于内层的原理图就是一个表示符号,封装是元器件具体实物大小,具体形状

在IDEA编辑器中,如何在.gitignore 的文件中,把 .ides 的文件忽略,提交git的时候不提交 .idea文件夹

方法 1:直接编辑 .gitignore 文件创建或编辑 .gitignore 文件在项目根目录(与 .git 文件夹同级)右键点击 → New → File,输入文件名 .gitignore。如果已存在 .gitignore,直接双击打开。添加忽略规则在 .gitignore 文件中添加以下内容:# 忽略所有 .idea 目录及其内容 .id…

揭秘EtherCAT转profinet玻璃制造厂的复杂生产环境与智能设备运用

玻璃制造厂的生产环境都比较复杂,需要严格的操作规程,及安全规范。玻璃制造厂的生产环境通常具有以下特点:高温环境:玻璃的熔化过程需要在高温下进行,熔炉的温度通常达到1400℃以上。因此,厂房内的设备和材料必须能够耐高温,并具备良好的隔热性能。 粉尘和化学物质:在玻…

20242943 2024-2025-2 《网络攻防实践》实验三

一.实验内容(1)动手实践了tcpdump等嗅探工具。通过嗅探工具,可以分析进入某一网站时,浏览器访问了多少个web服务器以及它们的IP地址都是什么。(2)动手实践Wireshark等抓包工具。通过使用Wireshark开源软件对在本机上以TELNET方式登录BBS进行嗅探与协议分析,得出了所登录…

20241904 2024-2025-2 《网络攻防实践》实验三

一、.实验内容动手实践tcpdump 使用tcpdump开源软件对在本机上访问www.tianya.cn网站过程进行嗅探,回答问题:在访问www.tianya.cn网站首页时,浏览器将访问多少个Web服务器? 他们的IP地址都是什么?动手实践Wireshark 使用Wireshark开源软件对在本机上以TELNET方式登录BBS进…