开发中常遇到的接口幂等性问题及实现 太过爱你忘了你带给我的痛 2024-03-23 16:27 85阅读 0赞 ### 接口幂等性是什么? ### > **什么是接口幂等性?** 接口幂等性是指**用户对于同一个操作发起的一次或多次请求的结果是一致的**,即发出**多个请求**对服务器的预期效果应当与发出**单个请求**对服务器的预期效果相同。 > **为什么会有这样的需求呢?** **场景**:一个下单页面用户点击第一次的时候可能因为网络卡顿导致浏览器无法第一时间收到服务器的响应,用户可能会因为着急多次点击(相信这种操作许多人都做过),那么当用户点击多次会造成什么样的后果呢? **没有实现接口幂等性的后果**:可能会导致库存多次扣减、可能会产生多笔订单记录、订单支付时,多扣了几次钱等等。可见,如果没有实现接口幂等性,对于这一系列操作带来的后果是很严重的。 当然除了**多次点击**会带来重复提交外,像**用户页面回退再次提交**,**服务的重试机制**等都会带来接口幂等性的问题。 -------------------- ### 接口幂等性的常见实现方式 ### 根据上面的情况可以看到接口幂等性的实现是尤其重要的,接下来将介绍几个常见的实现方式。 > **数据库的乐观锁与悲观锁** 可通过使用数据库**悲观锁**方式在**获取数据时进行加锁**,当同时有多个重复请求时只会有一个请求能操作。当然使用悲观锁一般会伴随着事务一起,如果数据的锁定时间过长,可能会影响性能。 当然也可以使用数据库的**乐观锁**方式,在数据中**增加一个`version`字段**,当数据需要更新时,先获取数据的`version`与更新时数据的`version`做对比,因为**多次请求时所带的`version`版本号是相同的,而如果有一个请求已经完成更新,那么版本号会变为`version + 1`,那么此时其余的请求再去对比便会不同,提示更新失败**。 > **分布式锁** 这种情况是当分布式系统下,多台服务器同一时刻同时处理相同的数据,此时可以**通过分布式锁,锁定此数据,只允许同一时刻只有一个请求可以操作**,并且每次请求时都先**检查当前锁定的数据是否已被处理过**,这样就可以解决并发情况下的接口幂等性问题。 > **防重表(reids或数据库)** 防重表可以在数据库中实现也可以在Redis中实现。 数据库中可以**利用需要更改的数据的唯一索引特性创建一张防重表**,比如下单操作中可以拿订单号(本身就是唯一的,如果没有唯一值可以将数据的多个字段合并构成唯一值)**作为防重表这张表的唯一索引**,当多次相同请求访问接口时,最前面的请求操作成功后,会将数据中的唯一索引(订单号)加入这张防重表,**当其余请求操作时,因为无法加入相同索引进入防重表而导致请求失败,这样就可以避免幂等性的问题**。 Redis实现方式类似,可以将**需要更新的数据进行MD5加密等操作,将加密的数据存入Redis的set类型中**,每次处理请求时,先通过**Redis判断是否加密数据已存在**,存在则不进行请求处理。 > **token机制(常用)** **token机制核心实现原理**:调用方在发起请求时先向服务器获取一个**全局ID(token)**,并将其存储在Redis中,请求时携带token,先校验token是否存在Redis,存在则执行业务,不存在代表是重复提交,**在第一次校验后会删除token**。 **token机制流程图**: ![format_png][] **token机制主要步骤**: 1. 客户端向服务器发送获取token请求,服务器可以通过如UUID的方式生成一个token,并将其存入Redis中,而后响应给客户端。 2. 客户端发送业务请求时携带token一并发送,token可以放在请求头或者参数中。 3. 服务器校验token是否存在Redis中 * 存在,则先删除Redis中的token,而后执行业务。 * 不存在,则代表这次请求是重复请求,抛出异常,终止操作。 **token机制深入考虑**: 1. **为什么是先删除token,再执行业务呢?** ① **先执行业务再删除token**,会出现的情况是可能业务处理完成,准备删除token时出现故障或者超时等情况,导致没法删除token,此时会**造成重复请求也校验通过而多次执行业务**。 ② **先删除token再执行业务**,会出现情况是删除完token,服务器超时或宕机,此时业务将无法执行,但是**相当于多次相同的业务请求都无法执行,除了未执行成功之外并不会带来什么后果**。后续可以再次请求获取新token再次执行业务。(虽然这种情况也会带来影响,但是相对于第一种情况的多次执行造成的后果相比要好很多) 2. **token在Redis中的获取、比较和删除必须是原子性** 如果这三个操作不是原子性,**在高并发场景下,可能会导致多个token校验成功,重复执行相同业务**。 所以需要使用Redis的Lua脚本执行,LUA脚本代码可参考如下: // 通过传入的key获取到的value与参数avg进行对比,一致说明数据存在reids中,删除key的数据,不一致说明是重复提交,返回0后续业务处理。 if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end 复制代码 -------------------- ### token机制的详细实现方案 ### > 项目中**常用的token机制实现接口幂等性**,接下来放入部分核心代码用于参考 1. 环境的配置(需要引入Redis依赖) <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 复制代码 1. 可配置一下Redis的序列化方式 /** * Redis配置类 * @author 单程车票 */ @Configuration public class RedisConfig { /** * 配置Redis的序列化器 */ public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 使用jackson序列化方式序列化json Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } } 复制代码 1. 配置application.yml的Redis基本信息 # 端口号 server: port: 8888 # 配置Redis spring: redis: host: localhost port: 6379 database: 0 复制代码 1. 核心代码 **业务接口类** /** * 业务接口类 * @author 单程车票 */ public interface TokenService { // 创建token String getToken(); // 校验token并执行业务代码 boolean checkToken(HttpServletRequest request); } 复制代码 **接口实现类** /** * 主要业务 * @author 单程车票 */ @Service public class TokenServiceImpl implements TokenService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public String getToken() { // 通过UUID生成token String token = UUID.randomUUID().toString().replaceAll("-", ""); // 将生成的token存入redis中,这里为了方便观察,把key写死为testToken,因为只测试一个接口,实际开发中,key是需要动态的(可以找一个唯一值替代,比如订单号等) stringRedisTemplate.opsForValue().set("testToken", token); // 返回token return token; } @Override public boolean checkToken(HttpServletRequest request) { // 从请求头中获取token String token = request.getHeader("token"); // 判断是否为空 if (StringUtils.isEmpty(token)) { // token为空则抛出异常 throw new CustomException("请求头未携带token!"); } // LUA脚本保证获取token,校验token,删除token是原子性的 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; // 调用execute执行,第一个参数传入脚本,第二个参数传入key(这里是之前写死的testToken),第三个参数传入AVG(这里是请求头携带的token) // 该lua脚本会对比 传入的key获取到的value是否与请求头获取的token一致,一致说明存在,删除key的数据,不一致返回0 Long res = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), List.of("testToken"), token); // 不一致,说明不存在,抛出重复异常 if (res == 0) { throw new CustomException("请求重复提交!"); } // 执行业务代码 System.out.println("执行业务代码"); return true; } } 复制代码 **测试** /** * 测试 * @author 单程车票 */ @RestController public class OrderController { @Autowired private TokenService tokenService; @GetMapping("/getToken") public R<String> getToken() { String token = tokenService.getToken(); return R.success(token); } @PostMapping("/order") public R<String> order(HttpServletRequest request) { // 校验并执行业务代码 boolean res = tokenService.checkToken(request); if (res) return R.success("执行成功!"); else return R.fail("执行失败"); } } 复制代码 > **通过Postman验证该token机制是否可行** 1. 通过接口生成token:b6cbb045f0d84a5eb87cee324de44265 ![format_png 1][] 1. 查看对应的Redis是否存入token ![format_png 2][] 1. 携带上token进行第一次业务请求 ![format_png 3][] 1. 查看Redis是否被清除 ![format_png 4][] 1. 第二次重复提交相同业务请求 ![format_png 5][] [format_png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/23/b5a2ca1c1e1d4d9eb910fcf1c3bdf27f.png [format_png 1]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/23/6157a66e1a8243d38c35f459c15d5fe7.png [format_png 2]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/23/9951ce9e56a34dc087b624dde559d92f.png [format_png 3]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/23/680f53de83a04a58ae43f466bf907c0d.png [format_png 4]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/23/d74553a4fcf24d288bc2d8e0f6ea8fc8.png [format_png 5]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/23/ea4b590d4dbd4ce1af6c94c7b9f6a1a2.png
相关 开发中常遇到的接口幂等性问题及实现 接口幂等性是什么? > 什么是接口幂等性? 接口幂等性是指用户对于同一个操作发起的一次或多次请求的结果是一致的,即发出多个请求对服务器的预期效果应当与发出单个请求对服务 太过爱你忘了你带给我的痛/ 2024年03月23日 16:27/ 0 赞/ 86 阅读
相关 接口幂等性 文章目录 1、接口幂等性 2、幂等解决方案 2.1 token机制(令牌) 2.2 各种锁机制 2.3 各种唯一约束 偏执的太偏执、/ 2023年02月17日 03:12/ 0 赞/ 146 阅读
相关 接口幂等性 文章目录 一、什么是幂等性 二、哪些情况需要防止 三、什么情况下需要幂等 四、幂等性解决方案 4.1 token 机制 谁借莪1个温暖的怀抱¢/ 2023年01月15日 13:28/ 0 赞/ 249 阅读
相关 接口幂等性问题处理 文章目录 一、接口幂等性概念 1. 接口调用存在的问题 2. 什么是接口幂等性 3. 什么情况下需要保证接口的幂等性 骑猪看日落/ 2022年12月11日 09:19/ 0 赞/ 350 阅读
相关 接口幂等性 一、什么是幂等性 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是 旧城等待,/ 2022年10月20日 13:41/ 0 赞/ 318 阅读
相关 幂等性实现 -接口幂等性 接口幂等性 1.什么是幂等性 > 对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。 > 也就是方法调用一次和调用多次产生的额外效果是相同的,他就具有幂 矫情吗;*/ 2022年10月05日 12:51/ 0 赞/ 425 阅读
相关 接口的幂等性 什么是幂等性 幂等性是数学的一个概念,表示进行1次变换和进行N次变换产生的效果相同。 接口的幂等性:以相同的请求调用这个接口一次和调用这个接口多次,对系统产生的影响相 - 日理万妓/ 2022年10月05日 00:51/ 0 赞/ 411 阅读
相关 接口幂等性 1. 接口调用存在的问题 现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务 梦里梦外;/ 2022年10月01日 05:51/ 0 赞/ 381 阅读
相关 接口的幂等性 “Compare And Set”(CAS),是一种常见的降低读写锁冲突,保证数据一致性的方法。 幂等与你是不是分布式高并发还有JavaEE都没有关系。 关键是 桃扇骨/ 2022年07月21日 11:18/ 0 赞/ 365 阅读
相关 接口幂等性 什么是幂等 数学角度 f(n) = 1^n 。无论n等于多少,f(n)永远值等于1 编程角度 程序无论执行多少次,其产生的结果均与一次 古城微笑少年丶/ 2022年03月22日 05:07/ 0 赞/ 522 阅读
还没有评论,来说两句吧...