如何实现幂等性,通常实现手段有以下几点:
- 数据库建立唯一索引,可以保证插入数据库的只有一条数据;
- Token机制,每次接口请求前先获取一个token,然后再下次请求的时候在header体中加上这个token,然后进行后天验证,如果验证通过删除token,下次请求再次判断token;
- 悲观锁和乐观锁,悲观锁可以保证每次for update的时候,其他sql无法update数据(在数据库引擎是innodb的是时候,select的条件必须为唯一索引,方式锁全表);
- 先查询后判断,首选通过查询数据库是否存在数据,如果存在,说明已经请求过了,直接拒绝请求,如果没存在,就说明是第一次,直接放行。
一、搭建redis的服务API
1、首先是搭建redis服务器
2、引入springboot中到redis的starter中,或者Spring封装的jedis也可以。后面主要用到的api就是它的set方法和exists方法,这里我们使用Springboot封装好的redisTempate。
创建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<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存,带超时失效
*
* @param key
* @param value
* @param expireTime
* @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, expireTime);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 检查key是否存在
*
* @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;
try {
ValueOperations operations = redisTemplate.opsForValue();
result = operations.get(key);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 删除key
*
* @return
*/
public boolean remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
return true;
}
return false;
}
/**
* redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
添加注解类:
/**
* @author Administrator
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckRedo {
/**
* 默认值
* @return
*/
String value() default "";
}
自定义拦截器,对自定义注解的扫描处理加入拦截器:
@Component
public class AutoIdInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@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();
CheckRedo checkRedo = method.getAnnotation(CheckRedo.class);
if (checkRedo!=null){
//幂等性校验
return tokenService.checkToken(request);
}
return true;
}
}
将拦截器加入webconfiguration:
@Component
public class WebConfiguration implements WebMvcConfigurer{
@Resource
private AutoIdInterceptor autoIdInterceptor;
/**
* 添加拦截器
*
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdInterceptor);
}
}
token判断接口定义
public interface TokenService {
/**
* 创建token
* @return
*/
public String creatToken();
/**
*检查token请求
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;
}
具体接口实现类,处理判断逻辑:
@Component
public class TokenServiceImpl implements TokenService {
@Autowired
RedisService redisService;
@Override
public String creatToken() {
String tokenStr = null;
try {
String uuid = UUID.randomUUID().toString();
StringBuilder sb = new StringBuilder();
tokenStr = sb.append(Constant.redis_prefix).append(uuid).toString();
if (redisService.set(tokenStr, tokenStr)) {
return tokenStr;
}
} catch (Exception e) {
e.printStackTrace();
}
return tokenStr;
}
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader(Constant.redis_prefix);
if (StringUtils.isEmpty(token)) {
token = request.getParameter(Constant.redis_prefix);
if (StringUtils.isEmpty(token)) {
throw new Exception("token 不能为空或者为null");
}
}
if (!redisService.exists(token)) {
throw new Exception("redis 中不存在此 token:" + token);
}
boolean remove = redisService.remove(token);
if (!remove) {
throw new Exception("redis 中删除key失败");
}
return true;
}
}
、Controller 控制层处理
/**
* @author zhengzheng046
*/
@RestController
public class BusinessController {
@Autowired
TokenService tokenService;
@GetMapping("/getToken")
public String getToken() {
String token = tokenService.creatToken();
if (!StringUtils.isEmpty(token)) {
System.out.println("token:"+token);
return token;
}
return null;
}
@CheckRedo
@PostMapping("/checkToken")
public String checkToken() {
System.out.println("进入方法,测试完成");
return "success";
}
}
总结:
本篇文章通过Springboot+redis实现token获取和判断是否存在,从而实现接口的幂等性判断。