目录
一、概念
幂等性, 通俗的说就是⼀个接口 , 多次发起同⼀个请求, 必须保证操作只能执⾏⼀次,⽐如:
- 订单接⼝ , 不能多次创建订单
- ⽀付接⼝ , 重复⽀付同⼀笔订单只能扣⼀次钱
- ⽀付宝回调接⼝ , 可能会多次回调, 必须处理重复回调
- 普通表单提交接⼝ , 因为网络超时等原因多次点击提交, 只能成功⼀次等
⼆ 、常见解决方案
- 唯⼀索引 -- 防止新增脏数据
- token机制 -- 防止页面重复提交
- 悲观锁 -- 获取数据的时候加锁(锁表或锁⾏)
- 乐观锁 -- 基于版本号version实现, 在更新数据那⼀刻校验数据分布式锁 -- redis(jedis、redisson)或zookeeper实现
- 状态机 -- 状态变更, 更新数据时判断状态
三、实现
下面采取第2种方式实现,通过redis + token机制实现接口幂等性校验
四、实现思路
为需要保证幂等性的每⼀次请求创建⼀个唯⼀标识token, 先获取token, 并 将此token存⼊redis, 请求接⼝时, 将此token放到header或者作为请求参数请求接⼝ , 后端接⼝判断redis中是否存在token:
- 如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提⽰
- 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
RedisUtils工具类
@Component
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间 *
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time,TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间 *
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效 */
public long getExpire(String key) {
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 判断key是否存在 *
* @param key 键
* @return true 存在 false不存在 */
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存 *
* @param key 可以传一个值 或多个 */
public boolean del(String... key) {
Boolean flag = true;
if (key != null && key.length > 0) {
if (key.length == 1) {
flag = redisTemplate.delete(key[0]); } else {
Long aLong =redisTemplate.delete(CollectionUtils.arrayToList(key)); if (aLong <= 0){
flag = false;
}
}
}
return flag;
}
/**
* 普通缓存获取 *
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null :
redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入 *
* @param key 键
* @param value 值
* @return true成功 false失败 */
public boolean set(String key, Object value) { try {
redisTemplate.opsForValue().set(key, value); return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间 *
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
自定义注解 ApIdempotent
/**
* 自定义幂等性接口,在需要使用的方法上标注
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApIdempotent {
}
ApiIdempotentInterceptor拦截器实现对幂等性请求拦截
/**
* 自定义幂等性拦截器,用于校验标注幂等性注解的方法
*/
public class ApiIdempotentInterceptor implements
HandlerInterceptor {
@Autowired
private TokenService tokenService;
/**
* 重写前置拦截
* @param request
* @param response
* @param handler 选择要执行的处理程序,用于类型和或实例评估
* @return true:程序继续执行,false:程序中断执行
* @throws Exception */
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果handler不是处理方法的处理其,返回true,
if ( !(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取拦截的方法
Method method = handlerMethod.getMethod();
//如果拦截的方法使用了自定义幂等性接口,校验token
ApIdempotent apIdempotent =method.getAnnotation(ApIdempotent.class);
if (apIdempotent != null){
//如果方法上标注有幂等性注解,则进行幂等性验证
check(request);
}
return true;
}
//验证token
private void check(HttpServletRequest request){ tokenService.checkToken(request);
}
}
TokenServiceImpl :实现token保存以及验证逻辑
@Service
public class TokenServiceImpl implements TokenService {
private static final String TOKEN_NAME = "token";
@Autowired
private RedisUtils redisUtils;
//生成token并将其保存到ServerResponse (http响应对象)
@Override
public ServerResponse createToken() {
//随机生成字符串
String s = RandomUtil.UUID32();
//拼接生成token值
StringBuilder token = new StringBuilder(); token.append("token:").append(s);
//保存token到redis中 此处失效时间根据需求定
redisUtils.set(token.toString(), token.toString(),60);
//将token保存并返回
return ServerResponse.success(token.toString()); }
/**
* 验证token,从请求头中获取token并进行验证
* @param request */
@Override
public void checkToken(HttpServletRequest request) {
//从请求头中获取名为token的数据
String token = request.getHeader(TOKEN_NAME);
//如果请求头中token为空
if (StringUtils.isEmpty(token)) {
//尝试从请求参数中获取token
token = request.getParameter(TOKEN_NAME); if (StringUtils.isEmpty(token)) {
//如果请求参数中也不包含token,抛出非法操作异常
throw new ServiceException("非法操作异常 ......");
}
}
//判断redis中是否包含token
if ( !redisUtils.hasKey(token)) {
//如果redis中不包含token,抛出请勿重复操作异常
throw new ServiceException("请勿重复操作"); }
//删除token
boolean del = redisUtils.del(token);
//如果删除失败,抛出异常;一定要进行删除的判断,不然还会出现 重复提交
if (!del) {
throw new ServiceException("请勿重复提交"); }
}
}
WebConfig: 注解拦截器到web容器中
@Configuration
public class WebConfig implements WebMvcConfigurer {
//设置跨域请求
@Bean
public CorsFilter corsFilter(){
final UrlBasedCorsConfigurationSource
urlBasedCorsConfigurationSource = new
UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials( true);
corsConfiguration.addAllowedOrigin("*"); corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration ("/**", corsConfiguration);
return new
CorsFilter(urlBasedCorsConfigurationSource);
}
//添加注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInterceptor());
WebMvcConfigurer.super.addInterceptors(registry);
}
@Bean
public ApiIdempotentInterceptor
apiIdempotentInterceptor(){
return new ApiIdempotentInterceptor();
}
}
测试
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;
//获取token
@GetMapping
public ServerResponse token(){
return tokenService.createToken();
}
//假设此接口访问具有幂等性,添加幂等注解,拦截器会自动拦截进行 token验证
@PostMapping("/idemt")
@ApIdempotent
public ServerResponse testIdept(){
return ServerResponse.success("success"); }
}
发起token请求创建并保存token 到redis中
发起/token/idemt请求测试:
携带正确token请求头时:
若不携带token或者携带token数据错误,观察控制台异常打印数据!!!!!
注:服务响应实体类
ServerResponse:
/**
* 自定义封装服务返回状态对象
*/
public class ServerResponse implements Serializable {
private static final long serialVersionUID = 7498483649536881777L;
//响应状态
private Integer status;
//响应信息
private String msg;
//相应数据
private Object data;
public ServerResponse() {
}
public ServerResponse(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
@JsonIgnore
public boolean isSuccess() {
return this.status ==
ResponseCode.SUCCESS.getCode();
}
public static ServerResponse success() {
return new
ServerResponse(ResponseCode.SUCCESS.getCode(), null, null);
}
public static ServerResponse success(String msg) {
return new
ServerResponse(ResponseCode.SUCCESS.getCode(), msg, null); }
public static ServerResponse success(Object data) {
return new
ServerResponse(ResponseCode.SUCCESS.getCode(), null, data);
}
public static ServerResponse success(String msg, Object data) {
return new
ServerResponse(ResponseCode.SUCCESS.getCode(), msg, data); }
public static ServerResponse error(String msg) { return new
ServerResponse(ResponseCode.ERROR.getCode(), msg, null); }
public static ServerResponse error(Object data) { return new
ServerResponse(ResponseCode.ERROR.getCode(), null, data); }
public static ServerResponse error(String msg, Object data) {
return new
ServerResponse(ResponseCode.ERROR.getCode(), msg, data); }
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}