Redis 实现接口幂等性的三种高效策略
1. 接口幂等性基础认知第一次听说幂等性这个词时我正盯着生产环境里两条完全相同的订单记录发愁。用户只是抱怨页面卡顿多点了两次提交按钮结果系统就产生了重复数据。这种场景就像你去ATM机取钱输入密码后机器没反应于是你又按了一次——如果银行系统没有幂等性控制你的账户可能就被扣了两次款。幂等性的数学本质其实很简单f(x) f(f(x))。翻译成程序员能理解的话就是无论调用多少次结果都和调用一次相同。在HTTP协议中GET、PUT、DELETE都是幂等的而POST是非幂等的。这就像你刷新网页(GET)不会改变内容但重复提交表单(POST)就可能产生多条数据。在实际业务中这些场景最容易出现幂等问题支付系统网络超时导致重复扣款订单创建用户快速点击生成多个订单库存扣减并发请求导致超卖消息队列消费者重试造成重复处理我曾遇到过最棘手的案例是医院挂号系统某个专家号被重复预约了三次。排查发现是移动端在网络抖动时自动重试导致的。这就是为什么我们需要Redis这样的利器——它不仅能解决幂等问题还能保持系统的高性能。2. Token机制先领票再办事去年给电商系统做秒杀功能时Token机制帮了大忙。它的原理就像银行排队先取号(Token)等叫到号才能办理业务。具体实现分为五个步骤客户端调用/getToken接口获取令牌服务端生成UUID作为Token存入Redis并设置过期时间客户端提交业务请求时携带该Token服务端校验Redis中是否存在该Token存在则删除Token并处理业务不存在则拒绝请求这里有个关键细节检查Token和删除Token必须是原子操作。我早期版本用两步操作就踩了坑// 错误示范非原子操作可能重复处理 if(redisTemplate.hasKey(token)) { redisTemplate.delete(token); processRequest(); }后来改用Lua脚本保证原子性-- KEYS[1]是token键名 if redis.call(GET, KEYS[1]) then redis.call(DEL, KEYS[1]) return true end return false在SpringBoot中的完整实现RestController public class TokenController { Autowired private StringRedisTemplate redisTemplate; GetMapping(/token) public String getToken() { String token UUID.randomUUID().toString(); redisTemplate.opsForValue().set( token, 1, 5, TimeUnit.MINUTES); // 5分钟过期 return token; } PostMapping(/submit) public ResponseEntityString submit( RequestHeader(X-Token) String token) { String script if redis.call(GET, KEYS[1]) then redis.call(DEL, KEYS[1]) return true end return false; Boolean result redisTemplate.execute( new DefaultRedisScript(script, Boolean.class), Collections.singletonList(token)); if(Boolean.TRUE.equals(result)) { // 处理业务逻辑 return ResponseEntity.ok(成功); } return ResponseEntity.badRequest().body(请勿重复提交); } }实际踩坑经验Token过期时间要根据业务合理设置支付类建议5分钟前端需要实现Token自动获取和重试机制在高并发场景下Redis可能需要分片部署3. 唯一序列号给每个请求办身份证物流系统的运单号给了我灵感——为什么不用唯一ID来标识每个请求呢这种方案特别适合第三方系统对接调用方生成唯一请求ID如订单号调用接口时携带该ID服务端将ID作为Redis Key存入重复请求会被Redis拦截public boolean checkRequestId(String requestId) { // SETNX EXPIRE 原子操作 Boolean result redisTemplate.opsForValue().setIfAbsent( req: requestId, 1, 10, TimeUnit.MINUTES); return Boolean.TRUE.equals(result); }性能优化技巧对RequestId做MD5压缩减少存储空间使用Redis集群提高吞吐量针对热点请求可以采用本地缓存Redis二级校验在物流轨迹更新系统中我们结合了唯一序列号和版本号UPDATE package_tracking SET status DELIVERED, version version 1 WHERE package_id ? AND version ?这样即使重复请求数据库层面的乐观锁也能兜底。4. 分布式锁业务处理的VIP通道大促时库存系统的惨痛教训让我意识到有些业务需要强一致性保障。Redis分布式锁就是解决方案public String deductStock(String productId) { String lockKey lock:stock: productId; String clientId UUID.randomUUID().toString(); try { // 尝试获取锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); if(Boolean.TRUE.equals(locked)) { // 处理核心业务 return 扣减成功; } return 系统繁忙请重试; } finally { // 确保只释放自己的锁 String script if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end; redisTemplate.execute( new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), clientId); } }关键注意事项必须设置锁的过期时间防止死锁每个客户端使用唯一标识避免误删他人锁考虑锁续期问题复杂场景可用Redisson锁粒度要尽可能细如按商品ID加锁在秒杀系统中我们最终采用了分段锁方案将100个商品库存分成10组用10个锁来减少竞争。配合Redis集群QPS从最初的200提升到了5000。5. 方案选型指南面对具体业务场景时我的选择策略是这样的方案适用场景性能影响实现复杂度可靠性Token机制用户交互类操作表单提交中低高唯一序列号第三方系统对接低中中分布式锁资金/库存等强一致性场景高高高最近在物联网项目中我们还创新性地组合使用了这些方案先用Token防表单重复提交再用分布式锁保证设备状态变更的原子性。Redis的多种数据结构为幂等控制提供了灵活的选择比如String存储Token/序列号Set快速查找是否存在Hash存储复杂校验信息记得在方案落地时一定要加上详细的日志和监控。我们曾经用PrometheusGranfa搭建了幂等拦截看板能实时发现异常重复请求。这些数据对后续的性能调优和容量规划也很有帮助。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2504766.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!