【微服务】接口的幂等性怎么设计?

一、什么是幂等?

幂等性:短时间内,对于相同输入的请求,无论进行多少次重复操作,都应该和单次调用的结果一致。

二、幂等问题产生的原因是什么?(或者说为什么需要实现幂等性?)

1、前端重复提交

在用户注册,用户创建商品的时候,用户填写完成注册表单或者创建好了商品点击提交,很多时候会因为网络波动没有及时对用户做出提交成功响应,致使用户认为自己没有成功提交,然后一直点击提交按钮,这时就会发生重复提交表单请求,在数据库中重复创建多条记录。

2、接口超时重试

很多时候HTTP客户端工具都默认开启超时重试的机制,比如Feign。为了防止网络波动超时等造成请求失败,都会添加重试机制,导致一个请求可能提交多次。

3、消息重复消费

当使用MQ消息中间件的时候,如果消费者处理完生产者消息,但是还没有提交offset,然后自己挂掉了。等到自己重启以后就会重复消费生产者消息。

三、幂等问题的解决方案

1、防重token令牌

防重token令牌

具体流程步骤:

  1. 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端。
  2. 客户端第一次调用业务请求的时候必须携带这个 token。
  3. 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token。
  4. 客户端第二次调用业务请求的时候必须携带这个 token。
  5. 服务端会校验这个 token,如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端。

注意:

  1. 对 redis 中是否存在 token 以及删除 token 的代码逻辑建议用 Lua 脚本实现,保证原子性。Redis 结合 Lua 脚本可以解决多线程并发安全问题。
  2. 全局唯一 ID 可以用UUID (分布式 ID )。

2、基于 mysql 唯一索引实现

基于 mysql 唯一索引实现

具体流程步骤:

  1. 客户端会先发送一个请求去获取到分布式 ID。
  2. 客户端第一次调用业务请求的时候会携带分布式 ID,服务端使用这个分布式ID作为唯一索引来进行插入,一旦出现重复提交的情况,插入自然不会成功。

3、基于 redis 分布式锁实现

基于 redis 分布式锁实现

具体流程步骤:

  1. 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段。
  2. 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间。
  3. 如果设置成功,表示这是第一次请求,则执行后续的业务逻辑。
  4. 如果设置失败,表示已经执行过当前请求,直接返回。

四、SpingBoot集成Redis实现防重token令牌机制

1.Token生成和验证的工具类TokenUtils

@Slf4j
@Component
public class TokenUtils {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";

    /**
     * 创建 Token 存入 Redis,并返回该 Token
     */
    public static String generateToken(String value) {
        // 创建Token
        String token = UUID.randomUUID().toString();
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        // 存储 Token 到 Redis,且设置过期时间为 5分钟
        redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
        // 返回 Token
        return token;
    }

    /**
     * 验证 Token
     */
    public static boolean validToken(String token, String value) {
        // 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value
        String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        // 根据 Key 前缀拼接 Key
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        // 执行 Lua 脚本
        Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
        // 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,如果结果不为空和0,则验证通过。
        if (result != null && result != 0L) {
            log.info("验证 token={},key={},value={} 成功", token, key, value);
            return true;
        }
        log.info("验证 token={},key={},value={} 失败", token, key, value);
        return false;
    }
}

2.防重接口的自定义注解ApiIdempotent

package com.changlu.annontions;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}

3.防重注解拦截器ApiIdempotentInterceptor,会拦截所有标注自定义防重注解的controller方法

@Component
public class ApiIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenUtils tokenUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //校验是否有执行方法
        if (!(handler instanceof HandlerMethod)) {
            return true;//若没有对应的方法执行器,就直接放行
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        ApiIdempotent annotation = method.getAnnotation(ApiIdempotent.class);
        //若是没有防重注解直接放行
        if (annotation != null) {
            //解析对应的请求头
            String token = request.getHeader("token");
            if (ObjectUtils.isEmpty(token)) {
                ServletUtils.renderString(response, "请携带token令牌");
                return false;
            }
            //若是校验失败直接进行响应
            if (!tokenUtils.validToken(token, "changlu")) {
                ServletUtils.renderString(response, "重复提交");
                return false;
            }
        }
        return true;
    }
}

4.注册拦截器

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {

    @Autowired
    private ApiIdempotentInterceptor apiIdempotentInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInterceptor);
    }
}

5.控制器接口

@RestController
public class OrderController {

    @GetMapping("/order/token")
    public String getToken() {
        String userInfo = "changlu";
        // 生成 Token 字符串,并返回
        return TokenUtils.generateToken(userInfo);
    }

    //防重注解
    @ApiIdempotent
    @PostMapping("/order/create")
    public Object createOrder() {
        return "创建订单成功!";
    }
}

五、最后总结

对于下单等存在唯一主键的业务,可以使用基于mysql唯一索引的方式实现。订单号是唯一索引。
对于更新订单状态等相关的更新场景操作,可以使用基于mysql乐观锁的方式实现。version是订单状态
对于一人一单的业务,可以使用基于redis分布式锁的方式实现。set的key是商品+用户,别忘记超时时间。
对于其他场景,可以通过防重token令牌方案的方式实现。Redis+Lua脚本解决多线程并发安全问题。

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
保证微服务接口幂等性是非常重要的,这样可以避免在重复请求或者并发操作时产生不一致的结果。以下是几种常见的方法来保证接口幂等性: 1. 使用唯一标识符:在每个请求中使用唯一的标识符来标记请求,服务端可以根据这个标识符来判断是否已经处理过该请求。例如,在HTTP请求中可以使用UUID作为请求的唯一标识符。 2. 使用乐观锁:在数据库操作中,可以使用乐观锁机制来实现幂等性。乐观锁基于版本号或者时间戳,在更新数据时比较版本号或者时间戳,如果发现不一致则说明数据已被其他请求修改,此时可以返回错误提示或者重试。 3. 幂等性检查:在服务端处理请求之前,先检查请求的内容是否已经处理过。可以通过查询数据库、缓存或者其他持久化存储来判断是否已经存在相同的请求。如果已经存在,则直接返回之前的处理结果而不是再次处理。 4. 原子操作:将多个操作组合成一个原子操作,确保整个操作过程是原子性的。例如,在数据库中使用事务来保证多个数据库操作的原子性,如果操作失败则进行回滚。 5. 幂等性设计:在设计接口时,尽量避免引入非幂等性操作。例如,不要设计会对同一资源进行多次增加或者删除的接口。 综上所述,通过使用唯一标识符、乐观锁、幂等性检查、原子操作和幂等性设计等方法,可以有效地保证微服务接口幂等性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寂寞烟火~

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值