在目前高并发的生产环境下,一个对外暴露的接口往往会面临很多次请求,此时我们需要对这些请求进行过滤,来保证幂等性的问题。
1.什么叫幂等性
任意多次执行所产生的影响均与一次执行的影响相同所以按照这个幂等性的定义,我们通俗来讲就是对数据库的影响只能是一次性的,不能重复处理。
2.如何保证幂等性
- 1.数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据
- 2.token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。
- 3.悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
对于悲观锁,乐观锁不了解的可以看看《浅谈悲观锁、乐观锁》,对于select … for update 行锁还是表锁不了解的,可以阅读一下《select…for update会锁表还是锁行》 - 4.先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
文章图片
3.搭建 Redis 服务 API
- 点击此处查看windows安装redis
- 点击此处查看SpringBoot集成redis
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 org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
@Service
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
/**
* 写入缓存 * @param key * @param value * @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations, 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, 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, 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;
}
}
4.自定义注解 AutoIdempotent 自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。
后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等
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 {
boolean required() default true;
}
- 使用元注解ElementType.METHOD表示它只能放在方法上
- etentionPolicy.RUNTIME表示它在运行时。
- 一个用来创建token
- 一个用来验证token
主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。5.1 TokenService
import javax.servlet.http.HttpServletRequest;
public interface TokenService {
/**
* 创建token * @return
*/
public String createToken();
/**
* 检验token * @param request * @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;
}
- token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。
- checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。
5.2 添加lombok 《idea集成lomBook》
5.3 创建自定义异常
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomException extends RuntimeException {
private Integer code;
private String msg;
}
但是我们返回给前端的信息不能是系统抛出的异常,我们需要拦截异常并且对用户进行友好提示 ,所以我们还需要拦截自定义异常
5.4 自定义异常拦截
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class ExceptionHandler {
//指定出现什么异常值执行这个方法
@org.springframework.web.bind.annotation.ExceptionHandler(CustomException.class)
@ResponseBody//为了返回数据
public Map, Object> customExceptionHandler(CustomException ex) {
Map, Object> map = new HashMap<>();
map.put("code", ex.getCode());
map.put("msg", ex.getMsg());
return map;
}
}
5.5 tokenService实现类
import com.example.repeat_submission.exception.CustomException;
import com.example.repeat_submission.service.TokenService;
import com.example.repeat_submission.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
RedisUtils redisUtils;
private final String TOKEN_PREFIX = "ninesun";
private final String TOKEN_NAME = "X-Token";
@Override
public String createToken() {
String str = UUID.randomUUID().toString();
StringBuilder token = new StringBuilder();
try {
token.append(TOKEN_PREFIX).append(str);
redisUtils.setEx(token.toString(), token.toString(), 10000L);
if (!StringUtils.isEmpty(token.toString())) {
return token.toString();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}@Override
public boolean checkToken(HttpServletRequest request) throws Exception {String token = request.getHeader(TOKEN_NAME);
if (StringUtils.isEmpty(token)) {// header中不存在token
token = request.getParameter(TOKEN_NAME);
if (StringUtils.isEmpty(token)) {// parameter中也不存在token
throw new CustomException(20001, "缺少参数token");
}
}if (!redisUtils.exists(token)) {
throw new CustomException(20001, "不能重复提交-------token不正确、空");
}boolean remove = redisUtils.remove(token);
if (!remove) {
throw new CustomException(20001, "Token刷新失败");
}
return true;
}
}
6.配置拦截器 为了方便处理json与对象的转换,我们添加以下依赖
com.alibaba
fastjson
1.2.47
commons-beanutils
commons-beanutils
1.9.3
commons-collections
commons-collections
3.2.1
commons-lang
commons-lang
2.6
commons-logging
commons-logging
1.1.1
net.sf.ezmorph
ezmorph
1.0.6
net.sf.json-lib
json-lib
2.2.3
jdk15
拦截器的配置如下:
- WebConfiguration
import com.example.repeat_submission.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import javax.annotation.Resource;
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
/** * 添加拦截器 * @param registry */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}
- AutoIdempotentInterceptor
import com.example.repeat_submission.annotation.AutoIdempotent;
import com.example.repeat_submission.service.TokenService;
import net.sf.json.JSONObject;
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.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 拦截器
*/
@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)) {//如果没有注解,直接返回true
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
if (method.isAnnotationPresent(AutoIdempotent.class)) {
AutoIdempotent autoIdempotentAnnotation = method.getAnnotation(AutoIdempotent.class);
//通过反射获取注解
if (autoIdempotentAnnotation.required()) {
return tokenService.checkToken(request);
// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示}}
//必须返回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();
}
}}
7.添加接口进行测试
import com.example.repeat_submission.annotation.AutoIdempotent;
import com.example.repeat_submission.service.TokenService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
public class TestController {@Resource
private TokenService tokenService;
@PostMapping("/get/token")
public Map, Object> getToken() {
Map, Object> map = new HashMap<>();
String token = tokenService.createToken();
if (!StringUtils.isEmpty(token)) {
map.put("code", 200);
map.put("data", token);
map.put("msg", "token创建成功");
} else {
map.put("code", 500);
map.put("msg", "token创建失败");
}
return map;
}@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
return "可以进行正常的业务";
}
}
我们先拿到token
文章图片
我们在header中添加token访问业务逻辑接口进行第一次测试
文章图片
我们同样的header,同样的请求再来一次
文章图片
可以看到请求已经被拦截了,原理呢就是需要我们每次请求结束之后,新的请求需要先获取一个令牌,有了这个令牌就可以进行正常的业务请求,一般适用于新增数据库操作或是高并发下获取大量数据的操作
推荐阅读
- 数据库|redis的常见问题总结
- java|SpringBoot从入门到入门学习笔记
- redis|秒杀项目前期之登录功能
- 面试题|分布式事务面试题 (史上最全、持续更新、吐血推荐)
- Java毕业设计项目实战篇|Java项目:在线点餐系统(java+Springboot+Maven+mybatis+Vue+mysql+Redis)
- redis学习之集群
- SpringBoot|【随记】Springboot集成Redis配置(单机,集群)
- mall学习教程|颜值爆表,Redis官方可视化工具来啦,功能很强大~
- redis|主库挂了,如何不间断服务((哨兵机制))