在实际的开发项目中,一个对外暴露的接口往往会面临,瞬间大量的重复的请求提交,如果想过滤掉重复请求造成对业务的伤害,那就需要实现幂等!
幂等的概念:
幂等性,就是一个接口, 多次发起同一个请求,,必须保证操作只能执行一次,最终的含义就是对数据库的影响只能是一次性的,不能重复处理。
比如:
常见解决方案:
通过Redis+Token机制实现接口幂等性校验。
原理图:
为需要保证幂等性,每一次请求创建一个唯一标识token,先获取token,并将此token存入Redis,请求接口时,将此token放到header或者作为请求参数请求接口,后端接口判断Redis中是否存在此token:
@AutoIdempotent 注解 + 拦截器对请求进行拦截。
@ControllerAdvice 全局异常处理
封装一个操作Redis的API工具类,使用RedisTemplate进行封装,需引入Redis的stater:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
@Component
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存设置时效时间
*
* @param key
* @param value
* @return
*/
public boolean setEx(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 判断缓存中是否有对应的value
*
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
*
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 删除对应的value
*
* @param key
*/
public boolean remove(final String key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
return false;
}
}
自定义注解AutoIdempotent:
自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。
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 AutoIdempotent {
}
token服务接口,token创建和检验:
createToken创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,返回这个token值。
checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。
import javax.servlet.http.HttpServletRequest;
public interface TokenService {
// 创建token
String createToken();
// 检验token
void checkToken(HttpServletRequest request);
}
import com.demo.exception.ServiceException;
import com.demo.service.RedisService;
import com.demo.service.TokenService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
@Component
public class TokenServiceImpl implements TokenService {
private static final String TOKEN_NAME = "token";
@Autowired
private RedisService redisService;
@Override
public String createToken() {
String token = UUID.randomUUID().toString();
try {
redisService.setEx(token, token, 10000L);
return token;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
@Override
public void checkToken(HttpServletRequest request) {
String token = request.getHeader(TOKEN_NAME);
if (StringUtils.isBlank(token)) {
// header中不存在token
token = request.getParameter(TOKEN_NAME);
if (StringUtils.isBlank(token)) {
// parameter中也不存在token
throw new ServiceException("参数不合法,必须带token参数");
}
}
if (!redisService.exists(token)) {
throw new ServiceException("请勿重复操作");
}
boolean remove = redisService.remove(token);
// 必须再次判断是否移除成功,因为可能多个请求同时执行上面移除的代码,但是最终只有一个返回移除成功的,如果不判断是否移除成功,就会失去幂等性的
if (!remove) {
throw new ServiceException("请勿重复操作");
}
}
}
拦截器处理幂等:
主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。
import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
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.lang.reflect.Method;
/**
* 接口幂等性拦截器
*/
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
/**
* 预处理
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
tokenService.checkToken(request);
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
配置拦截器:
继承WebMvcConfigurationSupport,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。
import com.demo.interceptor.AutoIdempotentInterceptor;
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 AutoIdempotentInterceptor autoIdempotentInterceptor;
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}
业务请求类:
通过/get/token
路径通过getToken()方法去获取具体的token。
调用test/idempotence
方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者。
import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class BusinessController {
@Resource
private TokenService tokenService;
@GetMapping("/get/token")
public String getToken() {
String token = tokenService.createToken();
return token;
}
@AutoIdempotent
@GetMapping("/test/idempotence")
public String testIdempotence() {
return "ok";
}
}
获取Token:
第一次请求:
第二次请求:
参考:
瞬间几千次的重复提交,我用 SpringBoot+Redis 扛住了
Sprinig Boot + Redis 实现接口幂等性,写得太好了!