一、幂等性介绍
什么是幂等
幂等性简单来说就是用户对同一个操作发起多次请求以后,对于数据的影响的结果是不变的,一次请求跟N次请求的结果是一样的
幂等场景
支付场景:如果用户对同一个订单发起两次支付操作,那么这个时候服务端不去做相应的处理的话,它就会完成两笔扣款记录,那么这个地方就会造成用户的资金损失,这种情况是绝对不允许在互联网产品上出现的。
幂等的实现方法
1)唯一索引 – 防止新增脏数据
2)token机制 – 防止页面重复提交
3)悲观锁 – 获取数据的时候加锁(锁表或锁行)
4)乐观锁 – 基于版本号version实现, 在更新数据那一刻校验数据
5)分布式锁 – redis(jedis、redisson)或zookeeper实现
6)状态机 – 状态变更, 更新数据时判断状态
今天要讲的是第二种–token机制,token机制的实现方法也有很多种,我们今天要说的是token机制的实现方法是使用springboot搭配redis利用自定义注解和拦截器实现的,流程如下
二、代码实现
1.公共类
package com.hl.springbootidempotence.common;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
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;
}
}
package com.hl.springbootidempotence.common;
public enum ResponseCode {
// 系统模块
SUCCESS(0, "操作成功"),
ERROR(1, "操作失败"),
SERVER_ERROR(500, "服务器异常"),
// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
REPETITIVE_OPERATION(10001, "请勿重复操作");
ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
2.工具类
@Configuration
public class JedisConfig {
@Bean(name = "jedisPoolConfig")
public JedisPoolConfig jedisPoolConfig(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(500);
jedisPoolConfig.setMaxIdle(200);
jedisPoolConfig.setNumTestsPerEvictionRun(1024);
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
jedisPoolConfig.setMinEvictableIdleTimeMillis(-1);
jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(10000);
jedisPoolConfig.setMaxWaitMillis(1500);
jedisPoolConfig.setTestOnBorrow(true);
jedisPoolConfig.setTestWhileIdle(true);
jedisPoolConfig.setTestOnReturn(false);
jedisPoolConfig.setJmxEnabled(true);
jedisPoolConfig.setBlockWhenExhausted(false);
return jedisPoolConfig;
}
@Bean
public JedisPool redisPool() {
String host = "127.0.0.1";
int port = 6379;
return new JedisPool(jedisPoolConfig(), host, port);
}
}
package com.hl.springbootidempotence.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@Component
@Slf4j
public class JedisUtil {
@Autowired
private JedisPool jedisPool;
private Jedis getJedis() {
return jedisPool.getResource();
}
/**
* 设值
*
* @param key
* @param value
* @return
*/
public String set(String key, String value) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.set(key, value);
} catch (Exception e) {
log.error("set key:{} value:{} error", key, value, e);
return null;
} finally {
close(jedis);
}
}
/**
* 设值
*
* @param key
* @param value
* @param expireTime 过期时间, 单位: s
* @return
*/
public String set(String key, String value, int expireTime) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.setex(key, expireTime, value);
} catch (Exception e) {
log.error("set key:{} value:{} expireTime:{} error", key, value, expireTime, e);
return null;
} finally {
close(jedis);
}
}
/**
* 取值
*
* @param key
* @return
*/
public String get(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.get(key);
} catch (Exception e) {
log.error("get key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 删除key
*
* @param key
* @return
*/
public Long del(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.del(key.getBytes());
} catch (Exception e) {
log.error("del key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 判断key是否存在
*
* @param key
* @return
*/
public Boolean exists(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.exists(key.getBytes());
} catch (Exception e) {
log.error("exists key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 设值key过期时间
*
* @param key
* @param expireTime 过期时间, 单位: s
* @return
*/
public Long expire(String key, int expireTime) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.expire(key.getBytes(), expireTime);
} catch (Exception e) {
log.error("expire key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 获取剩余时间
*
* @param key
* @return
*/
public Long ttl(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.ttl(key);
} catch (Exception e) {
log.error("ttl key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
private void close(Jedis jedis) {
if (null != jedis) {
jedis.close();
}
}
}
3.实现自定义注解
/**
* ElementType.METHOD:该注解作用于方法上
* RetentionPolicy.RUNTIME:表示它在运行时
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
4.定义生成token的service
public interface TokenService {
ServerResponse createToken();
void checkToken(HttpServletRequest request);
ServerResponse testIdempotence();
}
@Service
public class TokenServiceImpl implements TokenService {
private static final String TOKEN_NAME = "token";
private static final String TOKEN_PREFIX = "token:";
// 过期时间, 60s, 一分钟
private static final Integer EXPIRE_TIME_MINUTE = 60 * 5;
@Autowired
private JedisUtil jedisUtil;
@Override
public ServerResponse createToken() {
String str = UUID.randomUUID().toString().replaceAll("-", "");
StringBuilder token = new StringBuilder();
token.append(TOKEN_PREFIX).append(str);
jedisUtil.set(token.toString(), token.toString(), EXPIRE_TIME_MINUTE);
return ServerResponse.success(token.toString());
}
@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(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
}
if (!jedisUtil.exists(token)) {
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
}
Long del = jedisUtil.del(token);
if (del <= 0) {
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
}
}
@Override
public ServerResponse testIdempotence() {
return ServerResponse.success("testIdempotence: success");
}
}
5.自定义异常类
/**
* 业务逻辑异常
*/
public class ServiceException extends RuntimeException{
private String code;
private String msg;
public ServiceException() {
}
public ServiceException(String msg) {
this.msg = msg;
}
public ServiceException(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
6.自定义拦截器
package com.hl.springbootidempotence.interceptor;
import com.hl.springbootidempotence.annotation.ApiIdempotent;
import com.hl.springbootidempotence.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 ApiIdempotentInterceptor 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();
ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null) {
check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}
return true;
}
private void check(HttpServletRequest request) {
tokenService.checkToken(request);
}
@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 {
}
}
7.注册拦截器
package com.hl.springbootidempotence.config;
import com.hl.springbootidempotence.interceptor.ApiIdempotentInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置文件
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 跨域
* @return
*/
@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);
}
//关键,将拦截器作为bean写入配置中
@Bean
public ApiIdempotentInterceptor apiIdempotentInterceptor(){
return new ApiIdempotentInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInterceptor()).addPathPatterns("/**");
}
}
8.controller层
哪个接口需要实现幂等性就加入自定义注解@ApiIdempotent
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;
@GetMapping("/create")
public ServerResponse token() {
return tokenService.createToken();
}
@ApiIdempotent
@PostMapping("/testIdempotence")
public ServerResponse testIdempotence() {
return tokenService.testIdempotence();
}
}
三、测试
首先生成token
然后拿生成的token去另外一个接口去测试幂等性
这里要注意:请求头的名字是token,他的值是token:c8c5ef3256294bafbcdecd592259cd6f
,这里要和redis中存储的值要一样。
--end--