接口幂等性的解决方案(用springboot配置拦截器redis校验token 是否重复)

幂等性的含义:
任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是对数据库的影响只能是一次性的,不能重复处理。

在我们编程中中一些常见的操作:
1、select查询天然幂等
2、delete删除也是幂等,删除同一个多次效果一样
3、update直接更新某个值的,幂等
4、update更新累加操作的,非幂等
5、insert非幂等操作,每次新增一条

造成多次请求的原因:
1、点击提交按钮两次;
2、点击刷新按钮;
3、使用浏览器后退按钮重复之前的操作,导致重复提交表单;
4、使用浏览器历史记录重复提交表单;
5、浏览器重复的HTTP请;

经常使用解决方案:
1、前端js处理,没有获取到返回值时候,防止客户重复提交。

2、对于部分要求唯一的字段做唯一约束。

3、对于部分数据先查询再进行插入更新。

4、在提交之前生成唯一id作为标识,请求一次获取id删除缓存,防止反复提交,这里通过redis和拦截器,做个例子展示。

一、进行对是否加入等幂性校验的接口,写个注解标识是否加入等幂性校验

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface VerifyIdempotent {
    boolean isVerify() default true;
}

二、配置注册拦截器的类

@Configuration
public class IdempotentConfig implements WebMvcConfigurer {
    @Autowired
    private TokenService tokenService;
    @Bean
    public HandlerInterceptor idempotentInterceptor(){
        return new IdempotentInterceptor(tokenService);
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(idempotentInterceptor()).addPathPatterns("/**");
    }
}

三、配置拦截器对方法上标识了VerifyIdempotent 进行等幂性校验

public class IdempotentInterceptor implements HandlerInterceptor {

    private  TokenService tokenService;

    public IdempotentInterceptor(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            VerifyIdempotent annotation = method.getAnnotation(VerifyIdempotent.class);
            if(Objects.isNull(annotation) || !annotation.isVerify()){
                return true;
            }
            //校验等幂性
            JsonVO jsonVO = tokenService.checkToken(request);
            if("200".equals(jsonVO.getStatusCode())){
                return true;
            }
            response.getWriter().write(JSON.toJSONString(jsonVO));
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

五、写进行真正对等幂性校验的类

@Service
public class TokenServiceImpl implements TokenService {
    private Logger logger = LoggerFactory.getLogger(TokenServiceImpl.class);

    private static final String REDIS_VALUE = "true";
    private static final String TOKEN_NAME= "token";



    @Autowired
    private JedisService jedisService;
    @Override
    public JsonVO createToken() {
        String uuid = UUID.randomUUID().toString();
        try {
            boolean isSet = jedisService.setEx(uuid, 600, REDIS_VALUE);
            if (isSet) {
                return JsonVO.success(uuid);
            }
        } catch (Exception e) {
            logger.error("create token fail", e);
        }
        return JsonVO.failure("create token fail");
    }

    @Override
    public JsonVO checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isEmpty(token)) {
            return JsonVO.failure("token is empty");
        }
        //通过lua表达式里面 如果存在就返回 false,完成对存在和删除双重校验
        boolean remove = jedisService.remove(token, REDIS_VALUE);
        if (remove) {
            return JsonVO.success();
        }
        return JsonVO.failure("token is expire or is exist");

    }
}

六、写controller类进行测试

@RestController
@RequestMapping("/idempotent")
public class IdempotentController {

    private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentController.class);
    @Resource
    private TokenService tokenService;


    @GetMapping("/getToken")
    public JsonVO getToken(){
        return tokenService.createToken();
    }

    @VerifyIdempotent
    @PostMapping("/test/Idempotence")
    public JsonVO testIdempotence() {
        LOGGER.error("testIdempotence receive request");
        return JsonVO.success();

    }

}

1、先获取token用于作为提交凭证
在这里插入图片描述

2、当重复提交时候出现:
在这里插入图片描述
由于中间使用到redis这里将redis的附录展示出来,让需要的参考
一、引入redis依赖

 <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.0.5.RELEASE</version>
        </dependency>

二、加入redis.properties 配置文件

#客户端超时时间单位是毫秒 默认是2000
redis.timeout=10000
#最大空闲数
redis.maxIdle=300
#连接池的最大数据库连接数。设为0表示无限制,如果是jedis 2.4以后用redis.maxTotal
#redis.maxActive=600
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
redis.maxTotal=2000
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
redis.maxWaitMillis=1000

redis.nodes=192.168.25.128:7000,192.168.25.128:7001,192.168.25.128:7002,192.168.25.128:7003,192.168.25.128:7004,192.168.25.128:7005,192.168.25.128:7006,192.168.25.128:700

三、对redis进行配置

@Configuration
@PropertySource("classpath:config/redis.properties")
public class RedisConfig {

 
    @Value("${redis.maxIdle}")
    private Integer maxIdle;

    @Value("${redis.timeout}")
    private Integer timeout;
 
    @Value("${redis.maxTotal}")
    private Integer maxTotal;
 
    @Value("${redis.maxWaitMillis}")
    private Integer maxWaitMillis;


    @Value("${redis.nodes}")
    private String clusterNodes;


    @Bean
    public JedisCluster getJedisCluster(){
        String[] cNodes = clusterNodes.split(",");
        HashSet<HostAndPort> nodes = new HashSet<>();
        //分割集群节点
        for (String node : cNodes) {
            String[] hp = node.split(":");
            nodes.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));
        }
            JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
            jedisPoolConfig.setMaxIdle(maxIdle);
            jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
            jedisPoolConfig.setMaxTotal(maxTotal);

        //创建集群对象
        JedisCluster jedisCluster = new JedisCluster(nodes, timeout, jedisPoolConfig);
        return jedisCluster;
    }

}

四、redis方法书写

public interface JedisService {

    Long delete(String key);

    Long incr(String key);

    Long decr(String key);

    boolean lock(String key, String requestId, int tt1);


    boolean remove(String key, String requestId);

    boolean setEx(String redisKey,Integer ttl,String redisValue);

    boolean hexists(String redisKey,String redisValue);
}

@Service
public class JedisServiceImpl implements JedisService {

    @Autowired
    public JedisCluster jedisCluster;

    private static final String prefix = "jedis_lock";

    private static final String KEY_PREFIX = "idempotent";
    //锁状态
    private static final String LOCK_SUCCESS = "OK";

    //释放状态
    private static final Long RELEASE_SUCCESS=1L;

    //NX标志
    private static final String SET_IF_NOT_EXIST = "NX";

    //超时标志
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    @Override
    public Long delete(String key) {
        return jedisCluster.del(prefix + key);
    }

    @Override
    public Long incr(String key) {
        return jedisCluster.incr(prefix + key);
    }

    @Override
    public Long decr(String key) {
        return jedisCluster.decr(prefix + key);
    }

    @Override
    public boolean lock(String key, String requestId, int tt1) {
        String result = jedisCluster.set(prefix + key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, tt1);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    @Override
    public boolean remove(String key, String requestId) {
        String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        Object result = jedisCluster.eval(script, Collections.singletonList(KEY_PREFIX + key), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    @Override
    public boolean setEx(String redisKey,Integer ttl,String redisValue) {
        String setex = jedisCluster.setex(KEY_PREFIX+redisKey, ttl, redisValue);
        if (LOCK_SUCCESS.equals(setex)) {
            return true;
        }
        return false;
    }

    @Override
    public boolean hexists(String redisKey,String redisValue){
        return jedisCluster.hexists(KEY_PREFIX + redisKey, redisValue);
    }
}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值