幂等性的含义:
任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是对数据库的影响只能是一次性的,不能重复处理。
在我们编程中中一些常见的操作:
1、select查询天然幂等
2、delete删除也是幂等,删除同一个多次效果一样
3、update直接更新某个值的,幂等
4、update更新累加操作的,非幂等
5、insert非幂等操作,每次新增一条
造成多次请求的原因:
1、点击提交按钮两次;
2、点击刷新按钮;
3、使用浏览器后退按钮重复之前的操作,导致重复提交表单;
4、使用浏览器历史记录重复提交表单;
5、浏览器重复的HTTP请;
经常使用解决方案:
1、前端js处理,没有获取到返回值时候,防止客户重复提交。
2、对于部分要求唯一的字段做唯一约束。
3、对于部分数据先查询再进行插入更新。
4、在提交之前生成唯一id作为标识,请求一次获取id删除缓存,防止反复提交,这里通过redis和拦截器,做个例子展示。
一、进行对是否加入等幂性校验的接口,写个注解标识是否加入等幂性校验
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface VerifyIdempotent {
boolean isVerify() default true;
}
二、配置注册拦截器的类
@Configuration
public class IdempotentConfig implements WebMvcConfigurer {
@Autowired
private TokenService tokenService;
@Bean
public HandlerInterceptor idempotentInterceptor(){
return new IdempotentInterceptor(tokenService);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(idempotentInterceptor()).addPathPatterns("/**");
}
}
三、配置拦截器对方法上标识了VerifyIdempotent 进行等幂性校验
public class IdempotentInterceptor implements HandlerInterceptor {
private TokenService tokenService;
public IdempotentInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
VerifyIdempotent annotation = method.getAnnotation(VerifyIdempotent.class);
if(Objects.isNull(annotation) || !annotation.isVerify()){
return true;
}
//校验等幂性
JsonVO jsonVO = tokenService.checkToken(request);
if("200".equals(jsonVO.getStatusCode())){
return true;
}
response.getWriter().write(JSON.toJSONString(jsonVO));
return false;
}
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 {
}
}
五、写进行真正对等幂性校验的类
@Service
public class TokenServiceImpl implements TokenService {
private Logger logger = LoggerFactory.getLogger(TokenServiceImpl.class);
private static final String REDIS_VALUE = "true";
private static final String TOKEN_NAME= "token";
@Autowired
private JedisService jedisService;
@Override
public JsonVO createToken() {
String uuid = UUID.randomUUID().toString();
try {
boolean isSet = jedisService.setEx(uuid, 600, REDIS_VALUE);
if (isSet) {
return JsonVO.success(uuid);
}
} catch (Exception e) {
logger.error("create token fail", e);
}
return JsonVO.failure("create token fail");
}
@Override
public JsonVO checkToken(HttpServletRequest request) {
String token = request.getHeader(TOKEN_NAME);
if (StringUtils.isEmpty(token)) {
return JsonVO.failure("token is empty");
}
//通过lua表达式里面 如果存在就返回 false,完成对存在和删除双重校验
boolean remove = jedisService.remove(token, REDIS_VALUE);
if (remove) {
return JsonVO.success();
}
return JsonVO.failure("token is expire or is exist");
}
}
六、写controller类进行测试
@RestController
@RequestMapping("/idempotent")
public class IdempotentController {
private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentController.class);
@Resource
private TokenService tokenService;
@GetMapping("/getToken")
public JsonVO getToken(){
return tokenService.createToken();
}
@VerifyIdempotent
@PostMapping("/test/Idempotence")
public JsonVO testIdempotence() {
LOGGER.error("testIdempotence receive request");
return JsonVO.success();
}
}
1、先获取token用于作为提交凭证
2、当重复提交时候出现:
由于中间使用到redis这里将redis的附录展示出来,让需要的参考
一、引入redis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
二、加入redis.properties 配置文件
#客户端超时时间单位是毫秒 默认是2000
redis.timeout=10000
#最大空闲数
redis.maxIdle=300
#连接池的最大数据库连接数。设为0表示无限制,如果是jedis 2.4以后用redis.maxTotal
#redis.maxActive=600
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
redis.maxTotal=2000
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
redis.maxWaitMillis=1000
redis.nodes=192.168.25.128:7000,192.168.25.128:7001,192.168.25.128:7002,192.168.25.128:7003,192.168.25.128:7004,192.168.25.128:7005,192.168.25.128:7006,192.168.25.128:700
三、对redis进行配置
@Configuration
@PropertySource("classpath:config/redis.properties")
public class RedisConfig {
@Value("${redis.maxIdle}")
private Integer maxIdle;
@Value("${redis.timeout}")
private Integer timeout;
@Value("${redis.maxTotal}")
private Integer maxTotal;
@Value("${redis.maxWaitMillis}")
private Integer maxWaitMillis;
@Value("${redis.nodes}")
private String clusterNodes;
@Bean
public JedisCluster getJedisCluster(){
String[] cNodes = clusterNodes.split(",");
HashSet<HostAndPort> nodes = new HashSet<>();
//分割集群节点
for (String node : cNodes) {
String[] hp = node.split(":");
nodes.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));
}
JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxTotal);
//创建集群对象
JedisCluster jedisCluster = new JedisCluster(nodes, timeout, jedisPoolConfig);
return jedisCluster;
}
}
四、redis方法书写
public interface JedisService {
Long delete(String key);
Long incr(String key);
Long decr(String key);
boolean lock(String key, String requestId, int tt1);
boolean remove(String key, String requestId);
boolean setEx(String redisKey,Integer ttl,String redisValue);
boolean hexists(String redisKey,String redisValue);
}
@Service
public class JedisServiceImpl implements JedisService {
@Autowired
public JedisCluster jedisCluster;
private static final String prefix = "jedis_lock";
private static final String KEY_PREFIX = "idempotent";
//锁状态
private static final String LOCK_SUCCESS = "OK";
//释放状态
private static final Long RELEASE_SUCCESS=1L;
//NX标志
private static final String SET_IF_NOT_EXIST = "NX";
//超时标志
private static final String SET_WITH_EXPIRE_TIME = "PX";
@Override
public Long delete(String key) {
return jedisCluster.del(prefix + key);
}
@Override
public Long incr(String key) {
return jedisCluster.incr(prefix + key);
}
@Override
public Long decr(String key) {
return jedisCluster.decr(prefix + key);
}
@Override
public boolean lock(String key, String requestId, int tt1) {
String result = jedisCluster.set(prefix + key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, tt1);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
@Override
public boolean remove(String key, String requestId) {
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Object result = jedisCluster.eval(script, Collections.singletonList(KEY_PREFIX + key), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
@Override
public boolean setEx(String redisKey,Integer ttl,String redisValue) {
String setex = jedisCluster.setex(KEY_PREFIX+redisKey, ttl, redisValue);
if (LOCK_SUCCESS.equals(setex)) {
return true;
}
return false;
}
@Override
public boolean hexists(String redisKey,String redisValue){
return jedisCluster.hexists(KEY_PREFIX + redisKey, redisValue);
}
}