栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

一种解决海量重复提交问题的方案(以Java语言为例,使用SpringBoot + Redis实现)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

一种解决海量重复提交问题的方案(以Java语言为例,使用SpringBoot + Redis实现)

一、前言及原理分析

在实际的项目开发中,对于支持对外访问的接口,很多时候会出现被多次请求的情况,而这些多余的请求可能会对数据库中的数据产生多次影响,导致产异常数据,我们是不希望发生的,因此提出了幂等的概念,所谓幂等,即任意多次执行所产生的影响均与一次执行产生的影响相同。换言之,多次的请求对数据库的影响只能是一次性的,不能重复处理。关于如何保证接口幂等性,通常情况有如下几种方式:

  • 数据库建立唯一索引,可以保证最终插入的数据只有一条
  • 悲观锁或者乐观锁的方式,悲观锁可以保证每次for update时,其他sql无法update(若数据库引擎时innodb的时候,select的条件必须是唯一索引,防止锁全表)
  • 先查询,后判断,受限通过查询数据库中是否存在相应数据,如果存在,则说明已经请求过了,直接拒绝请求即可,如果不存在,直接放行
  • 通过token机制,每次请求接口前先获取一个token,然后下次请求时,在请求头中携带token,后台进行认证,如果认证通过了,放行并删除token,下次请求重复上述操作

暂以token机制的方式描述接口幂等性,如图,通过Redis实现幂等的简单原理图:

演示环境不再做详细说明:以Java语言为例,采用SpringBoot和Redis

二、搭建环境并封装Redis工具类
  • Maven依赖

    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
        org.springframework.boot
        spring-boot-starter-web
    

    
        org.projectlombok
        lombok
        true
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    

  • 环境配置(application.properties)
server.port=12001
spring.redis.host=192.168.56.10
  • Redis工具类封装,可有可无
package com.ideax.idempotence.utils;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;


@Component
public class RedisUtils {
    
    @Resource
    public RedisTemplate redisTemplate;

    
    private final StringRedisTemplate stringRedisTemplate;
    public RedisUtils(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    
    public Object get(final String key) {
        return redisTemplate.opsForValue().get(key);
    }

    
    public boolean setExpire(final String key, Object value, Long timeout) {
        boolean result = false;
        try {
            redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            redisTemplate.opsForValue().set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    
    public boolean getStringValue(final String key, String value) {
        boolean result = false;
        try {
            stringRedisTemplate.opsForValue().get(key);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    
    public boolean setStringValue(final String key, String value) {
        boolean result = false;
        try {
            stringRedisTemplate.opsForValue().set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    
    public boolean setStringValueExpire(final String key, String value, Long timeout) {
        boolean result = false;
        try {
            stringRedisTemplate.opsForValue().set(key, value, timeout);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    
    public boolean exists(final String key) {
        // 不能直接通过redisTemplate.hasKey(key)获取结果去判断,拆箱时有可能空指针异常
        return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
    }

    
    public boolean remove(final String key) {
        if (exists(key)) {
            // 不能直接通过redisTemplate.delete(key)获取结果去判断,拆箱时有可能空指针异常
            return Boolean.TRUE.equals(stringRedisTemplate.delete(key));
        }
        return false;
    }
}
三、自定义注解@Idempotent

自定义一个注解,该注解将会在有幂等性要求的接口上标注,为啥叫@Idempotent这个名字,是由于可读性考虑,采用了幂等俩字的英文单词,您随意。后续将通过反射扫描到这个注解,处理对应请求,实现幂等效果,注解定义如下:

package com.ideax.idempotence.annotation;

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 Idempotent {

}
四、创建和校验token

模拟一个token服务,包含两个接口及其实现类,一个用来创建token,一个用来验证token是否正确。创建token暂且简单用一个字符串作为token,并设置一个过期时间。验证token时,我们需要通过请求对象获取请求头信息,进而获取token信息,代码如下:

  • 接口:
package com.ideax.idempotence.service;

import javax.servlet.http.HttpServletRequest;


public interface TokenService {
    
    String createToken();

    
    boolean checkToken(HttpServletRequest request);
}
  • 实现类
package com.ideax.idempotence.service.impl;

import com.ideax.idempotence.service.TokenService;
import com.ideax.idempotence.utils.RedisUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;


@Service
public class TokenServiceImpl implements TokenService {
    private final RedisUtils redisUtils;

    public TokenServiceImpl(RedisUtils redisUtils) {
        this.redisUtils = redisUtils;
    }

    @Override
    public String createToken() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        redisUtils.setStringValueExpire("idempotent:cache:token:" + token, token, 20000L);
        return token;
    }

    @Override
    public boolean checkToken(HttpServletRequest request) {
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter("token");
            if (StringUtils.isEmpty(token)) {
                throw new RuntimeException("不正确的token参数!");
            }
        }
        
        final String key = "idempotent:cache:token:" + token;

        if (!redisUtils.exists(key)) {
            throw new RuntimeException("重复性操作!");
        }

        boolean tag = redisUtils.remove(key);
        if (!tag) {
            throw new RuntimeException("重复性操作!");
        }
        return true;
    }
}
五、配置拦截器
  • 定义拦截器,拦截请求后,通过反射机制,扫描到@Idempotent注解标注的接口,通过token服务中的checkToken()方法校验token是否正确,若出现异常,则捕获并渲染JSON数据返回给前端,代码如下:
package com.ideax.idempotence.interceptor;

import com.ideax.idempotence.annotation.Idempotent;
import com.ideax.idempotence.service.TokenService;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;


@Component
public class IdempotentInterceptor implements HandlerInterceptor {
    private final 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)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        // 扫描被@Idempotent标记的方法
        Idempotent annotation = method.getAnnotation(Idempotent.class);
        if (annotation != null) {
            try {
                return tokenService.checkToken(request);
            } catch (Exception e) {
                printResult(response, e.getMessage());
                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 {
    }

    private void printResult(HttpServletResponse response, String message) {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 注册定义好的拦截器,我们需要定义一个Web配置类,并将拦截器注册,在启动时可以将其加载到context中,代码如下:
package com.ideax.idempotence.config;

import com.ideax.idempotence.interceptor.IdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import javax.annotation.Resource;


@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {
    @Resource
    private IdempotentInterceptor idempotentInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(idempotentInterceptor);
        super.addInterceptors(registry);
    }
}
六、测试

这次不用postman了,用一个更牛逼的apipost测试,先模拟一个业务场景,比如商品添加操作,我们并不希望同一个商品添加进来多个请求,因此需要对商品添加接口保证幂等性,代码如下:

package com.ideax.idempotence.controller;

import com.ideax.idempotence.annotation.Idempotent;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;


@RestController
@RequestMapping("/product")
public class ProductController {

    @GetMapping("/{id}")
    public ResponseEntity> get(@PathVariable("id") int id) {
        Map map = new HashMap<>(10);
        map.put("id", id);
        map.put("serial", UUID.randomUUID().toString().replaceAll("-", ""));
        map.put("name", id + "手机");
        return ResponseEntity.ok(map);
    }

    @Idempotent
    @PostMapping
    public ResponseEntity> save(@RequestBody Map map) {
        return ResponseEntity.ok(map);
    }
}

上面说到了,在请求时,需要在请求头中携带token,所以在此之前,先获取一个token,代码如下:

package com.ideax.idempotence.controller;

import com.ideax.idempotence.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("token")
public class TokenController {
    @Autowired
    private TokenService tokenService;

    @GetMapping
    public ResponseEntity getToken() {
        return ResponseEntity.ok(tokenService.createToken());
    }
}
  • 获取token
  • 携带token进行第一次商品添加请求,返回请求成功的信息

  • 携带token进行第二次商品添加请求,请求失败,返回重复操作提示
七、总结

接口幂等性的保证在实际开发中是非常重要的环节,一个接口被无数客户端访问时,保证幂等性,将保证其操作不影响后台业务处理,数据只影响一次,防止产生脏乱数据,同时在一定程度上还可以减少并发量。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/684125.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号