
大促场景下基于CaffeineRedisson的电商订单防重复提交与扣款实践问题背景电商大促如618、双11期间订单创建接口的QPS往往可达数万甚至数十万用户端网络波动、按钮连击、客户端自动重试以及服务端熔断、限流后的重试机制都可能导致同一个订单的创建请求被重复发送。若没有完善的幂等机制极易出现重复扣库存、重复扣款的问题给平台和用户带来资损风险。传统方案往往直接基于Redis分布式锁做全量幂等校验高并发下Redis的网络开销和锁竞争压力会导致接口延迟飙升且出现重复扣款问题时缺乏全链路可观测能力难以快速定位故障根因。本文结合Caffeine本地缓存、Redisson分布式锁、OpenTelemetry可观测框架设计一套分层幂等方案在保证一致性的前提下提升接口性能同时实现问题的快速排查。方案设计三者在本场景中各有明确分工不存在强行拼接Caffeine作为前置流量拦截层承担90%以上的重复请求过滤降低后端压力Redisson作为全局一致性保障层解决跨实例的幂等和扣款原子性问题OpenTelemetry作为可观测层提供全链路埋点能力辅助问题排查和性能优化。整体流程如下 1. 请求进入订单创建接口后首先基于userId:orderId或前端传入的requestId查询Caffeine本地缓存若缓存中存在已处理完成的记录直接返回上次的处理结果拦截重复请求。 2. 若本地缓存未命中则尝试获取基于订单ID的Redisson分布式锁获取失败的请求直接返回“处理中”避免重复执行业务逻辑。 3. 获取锁成功的请求执行业务校验、库存扣减、支付扣款逻辑处理完成后将结果写入Caffeine本地缓存设置合理的过期时间最后释放分布式锁。 4. OpenTelemetry在缓存查询、锁获取、业务执行、扣款操作四个核心节点埋点采集请求ID、订单ID、缓存命中状态、锁等待耗时、业务执行结果等属性实现全链路可观测。关键原理1. Caffeine本地缓存的前置拦截原理Caffeine是Java生态下性能最高的本地缓存实现基于LRU最近最少使用和LFU最不经常使用的混合淘汰策略命中率远高于Guava Cache且读操作无锁纳秒级响应。本场景中利用其高性能特性做第一层重复请求拦截同一个用户短时间内重复提交的请求大概率会命中本地缓存无需访问Redis既降低了分布式锁的竞争压力又将接口响应耗时从毫秒级降到微秒级。缓存Key设计为userId:orderId或前端传入的唯一requestId避免不同用户提交同一订单时的误拦截过期时间设置为30秒略大于一般网络重试、用户连击的最大间隔既保证重复请求能被拦截又不会长期缓存过期订单导致状态不一致。2. Redisson分布式锁的全局一致性保障原理Caffeine作为本地缓存仅能实现单实例内的幂等若同一个订单的请求被负载均衡到不同服务实例本地缓存无法生效。Redisson的分布式锁基于Redis实现支持可重入、看门狗自动续期、RedLock高可用模式可保证跨实例下同一个订单仅有一个线程能执行业务逻辑从全局层面避免重复扣款。本场景中锁的粒度为订单ID而非全局锁保证不同订单的请求并行处理不影响整体吞吐量锁的默认租期设置为30秒开启看门狗机制后若业务执行时间超过租期Redisson会自动续期避免锁提前释放导致重复请求进入。3. OpenTelemetry的可观测价值OpenTelemetry是CNCF下的可观测标准框架本场景中不参与核心业务逻辑但承担故障排查和性能优化的核心支撑作用通过埋点可以清晰看到每个请求的缓存命中情况、锁等待耗时、业务执行错误信息若出现重复扣款可快速定位是缓存未命中、锁获取失败还是业务逻辑异常导致同时可基于埋点数据监控缓存命中率、锁竞争激烈程度为容量规划和参数调优提供依据。完整示例本示例基于JDK 17、Spring Boot 3.2.x、Redisson 3.21.x、Caffeine 3.1.x、OpenTelemetry 1.32.x开发可直接运行验证。依赖配置pom.xml片段dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version3.2.5/version /dependency !-- Caffeine本地缓存 -- dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId version3.1.8/version /dependency !-- Redisson分布式锁 -- dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.21.0/version /dependency !-- OpenTelemetry可观测 -- dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-api/artifactId version1.32.0/version /dependency dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-sdk/artifactId version1.32.0/version /dependency !-- 测试依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId version3.2.5/version scopetest/scope /dependency /dependencies核心配置类import com.github.benmanes.caffeine.cache.Caffeine; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Tracer; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; Configuration public class OrderConfig { // 配置Caffeine本地缓存最大1万条过期时间30秒写后过期 Bean public CaffeineCacheManager caffeineCacheManager() { CaffeineCacheManager cacheManager new CaffeineCacheManager(orderProcessCache); cacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(30, TimeUnit.SECONDS) .recordStats()); return cacheManager; } // 配置Redisson客户端连接本地Redis Bean public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer().setAddress(redis://127.0.0.1:6379); return Redisson.create(config); } // 配置OpenTelemetry Tracer用于链路埋点 Bean public Tracer tracer() { return GlobalOpenTelemetry.getTracer(order-service); } }核心业务代码import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; RestController public class OrderController { Autowired private CacheManager cacheManager; Autowired private RedissonClient redissonClient; Autowired private Tracer tracer; // 模拟支付服务记录扣款次数 private static int payCount 0; PostMapping(/createOrder) public MapString, Object createOrder(RequestBody MapString, String request) { String userId request.get(userId); String orderId request.get(orderId); String requestId request.getOrDefault(requestId, UUID.randomUUID().toString()); MapString, Object result new HashMap(); // OpenTelemetry 埋点开始处理请求 Span span tracer.spanBuilder(createOrder).startSpan(); span.setAttribute(requestId, requestId); span.setAttribute(orderId, orderId); span.setAttribute(userId, userId); try { // 1. 查询Caffeine本地缓存判断是否重复请求 Cache cache cacheManager.getCache(orderProcessCache); Cache.ValueWrapper cachedValue cache.get(userId : orderId); if (cachedValue ! null) { span.setAttribute(cacheHit, true); span.setAttribute(result, duplicate); result.put(code, 200); result.put(msg, 订单处理中或已完成); result.put(data, cachedValue.get()); return result; } span.setAttribute(cacheHit, false); // 2. 获取Redisson分布式锁按订单ID加锁 RLock lock redissonClient.getLock(order:lock: orderId); boolean lockSuccess lock.tryLock(30, 30, TimeUnit.SECONDS); span.setAttribute(lockAcquired, lockSuccess); if (!lockSuccess) { span.setAttribute(result, lock_failed); result.put(code, 429); result.put(msg, 订单处理中请稍后重试); return result; } try { // 3. 执行业务逻辑校验订单、扣库存、扣款 boolean paySuccess mockPay(orderId); MapString, Object orderResult new HashMap(); orderResult.put(orderId, orderId); orderResult.put(payStatus, paySuccess ? SUCCESS : FAILED); // 4. 处理结果写入本地缓存 cache.put(userId : orderId, orderResult); span.setAttribute(result, success); result.put(code, 200); result.put(msg, 订单创建成功); result.put(data, orderResult); return result; } finally { // 释放锁避免死锁 lock.unlock(); } } catch (Exception e) { span.setAttribute(result, error); span.recordException(e); result.put(code, 500); result.put(msg, 系统异常); return result; } finally { span.end(); } } // 模拟支付扣款保证同一个订单仅扣款一次 private synchronized boolean mockPay(String orderId) { // 实际业务中这里调用支付接口 payCount; System.out.println(订单 orderId 扣款成功累计扣款次数 payCount); return true; } }并发测试验证import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.ResponseEntity; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; SpringBootTest public class OrderDuplicateTest { Autowired private OrderController orderController; Test public void testDuplicateSubmit() throws InterruptedException { int threadCount 10; ExecutorService executor Executors.newFixedThreadPool(threadCount); CountDownLatch latch new CountDownLatch(threadCount); String orderId ORDER_ System.currentTimeMillis(); for (int i 0; i threadCount; i) { executor.execute(() - { try { MapString, String request new HashMap(); request.put(userId, USER_001); request.put(orderId, orderId); request.put(requestId, REQ_ orderId); ResponseEntityMap response orderController.createOrder(request); System.out.println(Thread.currentThread().getName() 响应 response.getBody()); } finally { latch.countDown(); } }); } latch.await(); executor.shutdown(); // 预期扣款次数为1验证防重复扣款生效 assert OrderController.payCount 1 : 重复扣款预期1次实际 OrderController.payCount; } }常见问题与踩坑点缓存Key设计错误导致误拦截若仅用订单ID作为缓存Key不同用户提交同一商品订单时会被错误拦截必须加入用户ID或使用前端传入的唯一requestId作为Key。锁粒度不合理导致性能下降禁止使用全局锁必须按订单ID加锁否则所有订单请求串行执行接口吞吐量会下降90%以上。缓存过期时间设置不当过期时间短于业务重试间隔会导致重复请求穿透缓存长于订单有效周期会导致新订单被旧缓存拦截建议设置为30秒~2分钟根据业务重试策略调整。OpenTelemetry同步上报影响性能高并发场景下必须使用异步上报器将Span数据异步发送到Jaeger、Prometheus等采集端避免同步上报阻塞业务线程。跨服务调用的幂等遗漏若支付逻辑独立部署需将幂等键如订单ID传递给支付服务本方案的分布式锁仅能保证订单服务内部不重复调用支付接口支付侧需额外实现幂等校验。适用边界与方案取舍适用场景本方案适合同机房部署的订单创建、扣款类高并发场景对一致性要求高QPS在1万~10万级业务执行耗时在秒级以内。若业务为跨地域部署的微服务Caffeine本地缓存的拦截率会下降需适当延长缓存过期时间或配合全局Redis缓存使用。方案取舍用Caffeine前置拦截换取更高的性能和更低的Redis压力牺牲少量本地内存且实例重启时缓存会丢失因此Redisson分布式锁作为全局一致性保障不可省略。OpenTelemetry埋点会带来约3%~5%的性能损耗换取全链路可观测能力适合故障率要求高的大促场景若为低并发内部系统可酌情省略。核心局限本方案仅能保证单个订单的幂等性无法解决分布式事务场景下的跨服务一致性问题若订单创建、库存扣减、支付扣款分布在多个独立服务需额外引入Seata等分布式事务框架配合。总结本方案通过Caffeine本地缓存做前置流量拦截Redisson分布式锁做全局一致性兜底OpenTelemetry做可观测支撑三者分层协作既解决了大促场景下的重复提交、重复扣款问题又保证了接口的高性能同时实现了问题的快速排查。实际落地时可根据业务特点调整缓存过期时间、锁粒度等参数也可基于OpenTelemetry的埋点数据构建缓存命中率、锁竞争等监控指标进一步优化方案效果。