在实际开发中,一个接口往往面临很多次的请求,再此之前,我们先来了解一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,就是说对数据库的影响只能是一次性的,不能重复处理,如何保证幂等性,通常有一下手段:
- 数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据
- token机制,每次接口请求前先获取一个token,然后在下次请求的时候在header中加上这个token,后台进行验证,如果验证通过则删除token,下次请求再次判断token
- 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他的sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁表)
- 先查询后判断,首先通过查询判断数据库是否存在数据,如果存在,则证明已经请求过了,直接拒绝请求,如果没有,就证明是第一次进来,直接放行
文章图片
实现步骤 首先是搭建redis服务器
引入springboot中用到的redis的starter,或者spring中封装的redis也可以,后面主要用到的api就是他的set,exist方法,这是我们用的是springboot封装好的redisTemplate
/**
* redis工具类
*/
@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 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 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 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;
}}
编写自定义注解 自定义注解,定义次注解的主要目的就是要把他添加在需要实现幂等的方法上面,范式某个方法用了这个注解,都会实现自动幂等,后台利用反射,如果扫描到这个类,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时,@Inherited标记这个类是一个注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface AutoIdempotent {}
token的创建与校验 token服务接口:里面2个方法,创建token、校验token。创建token主要是产生一个字符串,校验token主要是传递request对象,主要是获取header里的token信息,然后进行校验,通过抛出的异常信息,来将具体的报错信息返回给前端。
public interface TokenService {/**
* 创建token
* @return
*/
publicString createToken();
/**
* 检验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;
}
token服务实现类:token引用了redis服务,创建token采用了随机算法工具类生成随机UUID,然后放到redis中(为了防止数据用于冗余,设置为10s)如果放入成功,则最后返回这个token,checkToken方法是从header中获取token值(如果拿不到就从paramter中获取)如果不存在,直接抛出异常,这个异常信息可以被拦截器捕捉到,然后返回给前端
@Service
public class TokenServiceImpl implements TokenService {@Autowired
private RedisService redisService;
/**
* 创建token
*
* @return
*/
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(),10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
}catch (Exception ex){
ex.printStackTrace();
}
return null;
}/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}if (!redisService.exists(token)) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}boolean remove = redisService.remove(token);
if (!remove) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
return true;
}
}
拦截器配置 web配置:实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}
拦截器处理:主要功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken的方法进行校验,如果捕捉到异常,就返回给前端
/**
* 拦截器
*/
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {@Autowired
private TokenService tokenService;
/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@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();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);
// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}catch (Exception ex){
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
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 {}/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;
charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}}
测试用例 模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:
@RestController
public class BusinessController {@Resource
private TokenService tokenService;
@Resource
private TestService testService;
@PostMapping("/get/token")
public StringgetToken(){
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo successResult = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(successResult);
}
return StrUtil.EMPTY;
}
}
使用postman请求,首先获取token
文章图片
利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:
文章图片
第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:
文章图片
然后:完毕
【java|SpringBoot+Redis,防止数据重复提交】
推荐阅读
- 架构|(原创)springboot+shiro+redis 限制同用户多次登录(不需要考虑sprinboot、redis版本)
- AOP|Springboot + redis 使用AOP实现防止重复提交问题
- mysql|MySQL高级——锁
- java软件设计模式|设计模式——解释器模式
- 和我一起学java|JavaSE——异常处理机制
- JAVA|java用位运算实现加减乘除
- 和我一起学java|JavaSE——字符串常量池(StringTable)
- Spring框架|Spring项目创建和Bean获取使用指南
- JAVA|Spring框架核心及设计思想