如何建网站老鱼网,wordpress $wp_query,网站建设先修课程,网站欢迎页怎么做一、概念
幂等是一个数学与计算机学概念#xff0c;在数学中某一元运算为幂等时#xff0c;其作用在任一元素两次后会和其作用一次的结果相同。在计算机中编程中#xff0c;一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
幂等函数或幂等方法是…一、概念
幂等是一个数学与计算机学概念在数学中某一元运算为幂等时其作用在任一元素两次后会和其作用一次的结果相同。在计算机中编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
幂等函数或幂等方法是指可以使用相同参数重复执行并能获得相同结果的函数。这些函数不会影响系统状态也不用担心重复执行会对系统造成改变。
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如:
前端重复提交表单 在填写一些表格时候用户填写完成提交很多时候会因网络波动没有及时对用户做出提交成功响应致使用户认为没有成功提交然后一直点提交按钮这时就会发生重复提交表单请求。用户恶意进行刷单 例如在实现用户投票这种功能时如果用户针对一个用户进行重复提交投票这样会导致接口接收到用户重复提交的投票信息这样会使投票结果与事实严重不符。接口超时重复提交 很多时候 HTTP 客户端工具都默认开启超时重试的机制尤其是第三方调用接口时候为了防止网络波动超时等造成的请求失败都会添加重试机制导致一个请求提交多次。消息进行重复消费 当使用 MQ 消息中间件时候如果发生消息中间件出现错误未及时提交消费信息导致发生重复消费。
二、常见解决方案
唯一索引 -- 防止新增脏数据token机制 -- 防止页面重复提交悲观锁 -- 获取数据的时候加锁(锁表或锁行)乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据分布式锁 -- redis(jedis、redisson)或zookeeper实现状态机 -- 状态变更, 更新数据时判断状态
三、本文实现
本文采用第2种方式实现, 即通过redis token机制实现接口幂等性校验
四、实现思路
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
五、项目简介
springbootredisApiIdempotent注解 拦截器对请求进行拦截ControllerAdvice全局异常处理压测工具: jmeter
六、代码实现
1、pom依赖加载 !-- Redis-Jedis --dependencygroupIdredis.clients/groupIdartifactIdjedis/artifactIdversion2.9.0/version/dependency!--lombok 本文用到Slf4j注解, 也可不引用, 自定义log即可--dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.16.10/version/dependency也可以用SpringData
dependency
groupIdorg.springframework.boot/groupId
artifactIdspring-boot-starter-data-redis/artifactId
/dependency spring:redis:ssl: falsehost: 127.0.0.1port: 6379database: 0timeout: 1000password:lettuce:pool:max-active: 100max-wait: -1min-idle: 0max-idle: 20
2、JedisUtil
package com.wangzaiplus.test.util;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 {Autowiredprivate 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();}}} SpringData代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** description*/
Component
public class RedisUtil {private static final Logger logger LoggerFactory.getLogger(RedisUtil.class);Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 设值* param key* param value* return*/public void set(String key, String value) {logger.info(set key:{} value:{}, key, value);stringRedisTemplate.opsForValue().set(key,value);}/*** 设值* param key* param value* param expireTime 过期时间, 单位: s* return*/public void set(String key, String value, int expireTime) {logger.info(set key:{} value:{} expireTime:{}, key, value, expireTime);stringRedisTemplate.opsForValue().set(key,value, expireTime,TimeUnit.SECONDS);}/*** 取值* param key* return*/public String get(String key) {logger.info(get key:{}, key);return stringRedisTemplate.opsForValue().get(key);}/*** 删除key* param key* return*/public Boolean del(String key) {if (exists(key)) {return stringRedisTemplate.delete(key);} else {logger.error(del key:{}, key 不存在);return false;}}/*** 判断key是否存在* param key* return*/public Boolean exists(String key) {Boolean exists stringRedisTemplate.hasKey(key);logger.info(exists key:{} hasKey:{}, key, exists);return exists;}
} 3、自定义注解ApiIdempotent
package com.wangzaiplus.test.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 在需要保证 接口幂等性 的Controller的方法上使用此注解*/
Target({ElementType.METHOD})
Retention(RetentionPolicy.RUNTIME)
public interface ApiIdempotent {
}
4、ApiIdempotentInterceptor拦截器
package com.wangzaiplus.test.interceptor;import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
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;/*** 接口幂等性拦截器*/
public class ApiIdempotentInterceptor implements HandlerInterceptor {Autowiredprivate TokenService tokenService;Overridepublic 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);}Overridepublic void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {}Overridepublic void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {}
}
5、TokenServiceImpl
package com.wangzaiplus.test.service.impl;import com.wangzaiplus.test.common.Constant;
import com.wangzaiplus.test.common.ResponseCode;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.exception.ServiceException;
import com.wangzaiplus.test.service.TokenService;
import com.wangzaiplus.test.util.JedisUtil;
import com.wangzaiplus.test.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;Service
public class TokenServiceImpl implements TokenService {private static final String TOKEN_NAME token;Autowiredprivate JedisUtil jedisUtil;Overridepublic ServerResponse createToken() {String str RandomUtil.UUID32();StrBuilder token new StrBuilder();token.append(Constant.Redis.TOKEN_PREFIX).append(str);jedisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_MINUTE);return ServerResponse.success(token.toString());}Overridepublic void checkToken(HttpServletRequest request) {String token request.getHeader(TOKEN_NAME);if (StringUtils.isBlank(token)) {// header中不存在tokentoken request.getParameter(TOKEN_NAME);if (StringUtils.isBlank(token)) {// parameter中也不存在tokenthrow 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());}}}
6、TestApplication
package com.wangzaiplus.test;import com.wangzaiplus.test.interceptor.ApiIdempotentInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
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.WebMvcConfigurerAdapter;SpringBootApplication
MapperScan(com.wangzaiplus.test.mapper)
public class TestApplication extends WebMvcConfigurerAdapter {public static void main(String[] args) {SpringApplication.run(TestApplication.class, args);}/*** 跨域* return*/Beanpublic 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);}Overridepublic void addInterceptors(InterceptorRegistry registry) {// 接口幂等性拦截器registry.addInterceptor(apiIdempotentInterceptor());super.addInterceptors(registry);}Beanpublic ApiIdempotentInterceptor apiIdempotentInterceptor() {return new ApiIdempotentInterceptor();}}
OK, 目前为止, 校验代码准备就绪, 接下来测试验证
七、测试验证
1、获取token的控制器TokenController
package com.wangzaiplus.test.controller;import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/token)
public class TokenController {Autowiredprivate TokenService tokenService;GetMappingpublic ServerResponse token() {return tokenService.createToken();}}
2. TestController, 注意ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响
package com.wangzaiplus.test.controller;import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/test)
Slf4j
public class TestController {Autowiredprivate TestService testService;ApiIdempotentPostMapping(testIdempotence)public ServerResponse testIdempotence() {return testService.testIdempotence();}}
3. 获取token 查看redis 4. 测试接口安全性: 利用jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数 5. header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为abcd 八、注意点(非常重要) 上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第46行, 此时token还未被删除, 所以继续往下执行, 如果不校验jedisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作, 下面重现一下
稍微修改一下代码: 再次请求 再看看控制台 虽然只有一个真正删除掉token, 但由于没有对删除结果进行校验, 所以还是有并发问题, 因此, 必须校验 参考https://www.jianshu.com/p/6189275403ed