【SpringBoot应用篇】SpringBoot+Redis实现接口幂等性校验

  • 幂等性
  • 解决方法
    • Pom
    • token令牌
      • yml
      • @ApiIdempotentAnn
      • ApiIdempotentInterceptor
      • MVC配置类
      • ApiController
    • 分布式锁 Redisson
      • pom
      • @RedissonLockAnnotation
      • DistributeLocker
      • RedissonDistributeLocker
      • RedissonLockUtils
      • RedissonConfig
      • RedissonLockAop
      • ResultVO
      • BusiController

幂等性

幂等性的定义是:一次和屡次请求某一个资源对于资源自己应该具备一样的结果(网络超时等问题除外)。也就是说,其任意屡次执行对资源自己所产生的影响均与一次执行的影响相同。

WEB系统中: 就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生不同的结果。

什么状况下须要保证幂等性
以SQL为例,有下面三种场景,只有第三种场景须要开发人员使用其余策略保证幂等性:
SELECT col1 FROM tab1 WHER col2=2,不管执行多少次都不会改变状态,是自然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,不管执行成功多少次状态都是一致的,所以也是幂等操做。
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,这种不是幂等的。

解决方法

这里主要使用token令牌分布式锁解决

Pom

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.2.RELEASE</version><relativePath/>
</parent>
<dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.4</version><scope>provided</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- springboot 对aop的支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- springboot mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency>
</dependencies>

token令牌

这种方式分红两个阶段:
1、客户端向系统发起一次申请token的请求,服务器系统生成token令牌,将token保存到Redis缓存中,并返回前端(令牌生成方式可以使用JWT)
2、客户端拿着申请到的token发起请求(放到请求头中),后台系统会在拦截器中检查handler是否开启幂等性校验。取请求头中的token,判断Redis中是否存在该token,若是存在,表示第一次发起支付请求,删除缓存中token后开始业务逻辑处理;若是缓存中不存在,表示非法请求。

yml

spring:redis:host: 127.0.0.1timeout: 5000msport: 6379database: 0datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/study_db?serverTimezone=GMT%2B8&allowMultiQueries=trueusername: rootpassword: root
redisson:timeout: 10000

@ApiIdempotentAnn

@ApiIdempotentAnn幂等性注解。说明: 添加了该注解的接口要实现幂等性验证

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotentAnn {boolean value() default true;
}

ApiIdempotentInterceptor

这里可以使用拦截器或者使用AOP的方式实现。

幂等性拦截器的方式实现

@Component
public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {@Autowiredprivate StringRedisTemplate redisTemplate;/*** 前置拦截器*在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行,* 也就是说我们想调用的方法 不会被执行,但是你可以修改response为你想要的响应。*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//如果hanler不是和HandlerMethod类型,则返回trueif (!(handler instanceof HandlerMethod)) {return true;}//转化类型final HandlerMethod handlerMethod = (HandlerMethod) handler;//获取方法类final Method method = handlerMethod.getMethod();// 判断当前method中是否有这个注解boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class);//如果有幂等性注解if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) {// 需要实现接口幂等性//检查token//1.获取请求的接口方法boolean result = checkToken(request);//如果token有值,说明是第一次调用if (result) {//则放行return super.preHandle(request, response, handler);} else {//如果token没有值,则表示不是第一次调用,是重复调用response.setContentType("application/json; charset=utf-8");PrintWriter writer = response.getWriter();writer.print("重复调用");writer.close();response.flushBuffer();return false;}}//否则没有该自定义幂等性注解,则放行return super.preHandle(request, response, handler);}//检查tokenprivate boolean checkToken(HttpServletRequest request) {//从请求头对象中获取tokenString token = request.getHeader("token");//如果不存在,则返回false,说明是重复调用if(StringUtils.isBlank(token)){return false;}//否则就是存在,存在则把redis里删除tokenreturn redisTemplate.delete(token);}
}

MVC配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate ApiIdempotentInterceptor apiIdempotentInceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(apiIdempotentInceptor).addPathPatterns("/**");}
}

ApiController

@RestController
public class ApiController {@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 前端获取token,然后把该token放入请求的header中* @return*/@GetMapping("/getToken")public String getToken() {String token = UUID.randomUUID().toString().substring(1, 9);stringRedisTemplate.opsForValue().set(token, "1");return token;}//定义int类型的原子类的类AtomicInteger num = new AtomicInteger(100);/*** 主业务逻辑,num--,并且加了自定义接口* @return*/@GetMapping("/submit")@ApiIdempotentAnnpublic String submit() {// num--num.decrementAndGet();return "success";}/*** 查看num的值* @return*/@GetMapping("/getNum")public String getNum() {return String.valueOf(num.get());}
}

分布式锁 Redisson

Redisson是redis官网推荐实现分布式锁的一个第三方类库,通过开启另一个服务,后台进程定时检查持有锁的线程是否继续持有锁了,是将锁的生命周期重置到指定时间,即防止线程释放锁之前过期,所以将锁声明周期通过重置延长)

Redission执行流程如下:(只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下(锁续命周期就是设置的超时时间的三分之一),如果线程还持有锁,就会不断的延长锁key的生存时间。因此,Redis就是使用Redisson解决了锁过期释放,业务没执行完问题。当业务执行完,释放锁后,再关闭守护线程,

pom

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.13.6</version>
</dependency>

@RedissonLockAnnotation

分布式锁注解

@Target(ElementType.METHOD) //注解在方法
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLockAnnotation {/*** 指定组成分布式锁的key,以逗号分隔。* 如:keyParts="name,age",则分布式锁的key为这两个字段value的拼接* key=params.getString("name")+params.getString("age")*/String keyParts();
}

DistributeLocker

分布式锁接口

public interface  DistributeLocker {/*** 加锁* @param lockKey key*/void lock(String lockKey);/*** 释放锁** @param lockKey key*/void unlock(String lockKey);/*** 加锁,设置有效期** @param lockKey key* @param timeout 有效时间,默认时间单位在实现类传入*/void lock(String lockKey, int timeout);/*** 加锁,设置有效期并指定时间单位* @param lockKey key* @param timeout 有效时间* @param unit    时间单位*/void lock(String lockKey, int timeout, TimeUnit unit);/*** 尝试获取锁,获取到则持有该锁返回true,未获取到立即返回false* @param lockKey* @return true-获取锁成功 false-获取锁失败*/boolean tryLock(String lockKey);/*** 尝试获取锁,获取到则持有该锁leaseTime时间.* 若未获取到,在waitTime时间内一直尝试获取,超过watiTime还未获取到则返回false* @param lockKey   key* @param waitTime  尝试获取时间* @param leaseTime 锁持有时间* @param unit      时间单位* @return true-获取锁成功 false-获取锁失败*/boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)throws InterruptedException;/*** 锁是否被任意一个线程锁持有* @param lockKey* @return true-被锁 false-未被锁*/boolean isLocked(String lockKey);
}

RedissonDistributeLocker

redisson实现分布式锁接口

public class RedissonDistributeLocker implements DistributeLocker {private RedissonClient redissonClient;public RedissonDistributeLocker(RedissonClient redissonClient) {this.redissonClient = redissonClient;}@Overridepublic void lock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);lock.lock();}@Overridepublic void unlock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);lock.unlock();}@Overridepublic void lock(String lockKey, int leaseTime) {RLock lock = redissonClient.getLock(lockKey);lock.lock(leaseTime, TimeUnit.MILLISECONDS);}@Overridepublic void lock(String lockKey, int timeout, TimeUnit unit) {RLock lock = redissonClient.getLock(lockKey);lock.lock(timeout, unit);}@Overridepublic boolean tryLock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);return lock.tryLock();}@Overridepublic boolean tryLock(String lockKey, long waitTime, long leaseTime,TimeUnit unit) throws InterruptedException {RLock lock = redissonClient.getLock(lockKey);return lock.tryLock(waitTime, leaseTime, unit);}@Overridepublic boolean isLocked(String lockKey) {RLock lock = redissonClient.getLock(lockKey);return lock.isLocked();}
}

RedissonLockUtils

redisson锁工具类

public class RedissonLockUtils {private static DistributeLocker locker;public static void setLocker(DistributeLocker locker) {RedissonLockUtils.locker = locker;}public static void lock(String lockKey) {locker.lock(lockKey);}public static void unlock(String lockKey) {locker.unlock(lockKey);}public static void lock(String lockKey, int timeout) {locker.lock(lockKey, timeout);}public static void lock(String lockKey, int timeout, TimeUnit unit) {locker.lock(lockKey, timeout, unit);}public static boolean tryLock(String lockKey) {return locker.tryLock(lockKey);}public static boolean tryLock(String lockKey, long waitTime, long leaseTime,TimeUnit unit) throws InterruptedException {return locker.tryLock(lockKey, waitTime, leaseTime, unit);}public static boolean isLocked(String lockKey) {return locker.isLocked(lockKey);}
}

RedissonConfig

Redisson配置类

@Configuration
public class RedissonConfig {@Autowiredprivate Environment env;/*** Redisson客户端注册* 单机模式*/@Bean(destroyMethod = "shutdown")public RedissonClient createRedissonClient() {Config config = new Config();SingleServerConfig singleServerConfig = config.useSingleServer();singleServerConfig.setAddress("redis://" + env.getProperty("spring.redis.host") + ":" + env.getProperty("spring.redis.port"));singleServerConfig.setTimeout(Integer.valueOf(env.getProperty("redisson.timeout")));return Redisson.create(config);}/*** 分布式锁实例化并交给工具类* @param redissonClient*/@Beanpublic RedissonDistributeLocker redissonLocker(RedissonClient redissonClient) {RedissonDistributeLocker locker = new RedissonDistributeLocker(redissonClient);RedissonLockUtils.setLocker(locker);return locker;}
}

RedissonLockAop

这里可以使用拦截器或者使用AOP的方式实现。

分布式锁AOP切面拦截方式实现

@Aspect
@Component
@Slf4j
public class RedissonLockAop {/*** 切点,拦截被 @RedissonLockAnnotation 修饰的方法*/@Pointcut("@annotation(cn.zysheep.biz.redis.RedissonLockAnnotation)")public void redissonLockPoint() {}@Around("redissonLockPoint()")@ResponseBodypublic ResultVO checkLock(ProceedingJoinPoint pjp) throws Throwable {//当前线程名String threadName = Thread.currentThread().getName();log.info("线程{}------进入分布式锁aop------", threadName);//获取参数列表Object[] objs = pjp.getArgs();//因为只有一个JSON参数,直接取第一个JSONObject param = (JSONObject) objs[0];//获取该注解的实例对象RedissonLockAnnotation annotation = ((MethodSignature) pjp.getSignature()).getMethod().getAnnotation(RedissonLockAnnotation.class);//生成分布式锁key的键名,以逗号分隔String keyParts = annotation.keyParts();StringBuffer keyBuffer = new StringBuffer();if (StringUtils.isEmpty(keyParts)) {log.info("线程{} keyParts设置为空,不加锁", threadName);return (ResultVO) pjp.proceed();} else {//生成分布式锁keyString[] keyPartArray = keyParts.split(",");for (String keyPart : keyPartArray) {keyBuffer.append(param.getString(keyPart));}String key = keyBuffer.toString();log.info("线程{} 要加锁的key={}", threadName, key);//获取锁if (RedissonLockUtils.tryLock(key, 3000, 5000, TimeUnit.MILLISECONDS)) {try {log.info("线程{} 获取锁成功", threadName);// Thread.sleep(5000);return (ResultVO) pjp.proceed();} finally {RedissonLockUtils.unlock(key);log.info("线程{} 释放锁", threadName);}} else {log.info("线程{} 获取锁失败", threadName);return ResultVO.fail();}}}
}

ResultVO

统一响应实体

@Data
public class ResultVO<T> {private static final ResultCode SUCCESS = ResultCode.SUCCESS;private static final ResultCode FAIL = ResultCode.FAILED;private Integer code;private String message;private T  data;public static <T> ResultVO<T> ok() {return result(SUCCESS,null);}public static <T> ResultVO<T> ok(T data) {return result(SUCCESS,data);}public static <T> ResultVO<T> ok(ResultCode resultCode) {return result(resultCode,null);}public static <T> ResultVO<T> ok(ResultCode resultCode, T data) {return result(resultCode,data);}public static <T> ResultVO<T> fail() {return result(FAIL,null);}public static <T> ResultVO<T> fail(ResultCode resultCode) {return result(FAIL,null);}public static <T> ResultVO<T> fail(T data) {return result(FAIL,data);}public static <T> ResultVO<T> fail(ResultCode resultCode, T data) {return result(resultCode,data);}private static <T>  ResultVO<T> result(ResultCode resultCode, T data) {ResultVO<T> resultVO = new ResultVO<>();resultVO.setCode(resultCode.getCode());resultVO.setMessage(resultCode.getMessage());resultVO.setData(data);return resultVO;}
}

BusiController

@RestController
public class ApiController {@PostMapping(value = "testLock")@RedissonLockAnnotation(keyParts = "name,age")public ResultVO testLock(@RequestBody JSONObject params) {/*** 分布式锁key=params.getString("name")+params.getString("age");* 此时name和age均相同的请求不会出现并发问题*///TODO 业务处理dwadreturn ResultVO.ok();}
}

【SpringBoot应用篇】SpringBoot+Redis实现接口幂等性校验相关推荐

  1. springboot + redis + 注解 + 拦截器 实现接口幂等性校验

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 来源:https://www.jianshu.com/p/6189275403ed 一.概念 ...

  2. Springboot + redis + 注解 + 拦截器来实现接口幂等性校验

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:wangzaiplus www.jianshu.com/p/ ...

  3. redis 判断存在性_实战 | springboot+redis+拦截器 实现接口幂等性校验

    来源:https://www.jianshu.com/p/6189275403ed 一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能 ...

  4. springboot redis token_Spring Boot + Redis + 注解 + 拦截器来实现接口幂等性校验

    优质文章,及时送达 作者 | wangzaiplus 链接 | www.jianshu.com/p/6189275403ed 一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证 ...

  5. springboot幂等性_Spring Boot + Redis + 注解 + 拦截器来实现接口幂等性校验

    一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如:订单接口, 不能多次创建订单 支付接口, 重复支付同一笔订单只能扣一次钱 支付宝回调接口, 可能会多次 ...

  6. 太好了 | 这篇写的太好了!Spring Boot + Redis 实现接口幂等性

    Hi ! 我是小小,今天是本周的第四篇,第四篇主要内容是 Spring Boot + Redis 实现接口幂等性 介绍 幂等性的概念是,任意多次执行所产生的影响都与一次执行产生的影响相同,按照这个含义 ...

  7. Spring Boot + Redis 实现接口幂等性 | 分布式开发必知!

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 来源:http://tinyurl.com/y5k2sx5t >>阿里云8月最新 ...

  8. Sprinig Boot + Redis 实现接口幂等性,写得太好了!

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:收藏了!7 个开源的 Spring Boot 前后端分离优质项目个人原创+1博客:点击前往,查看更多 作者:wa ...

  9. @slf4j注解_SpringBoot + Redis + 注解 + 拦截器 实现接口幂等性校验

    一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能多次创建订单 支付接口, 重复支付同一笔订单只能扣一次钱 支付宝回调接口, 可能会多 ...

最新文章

  1. 012 pandas与matplotlib结合制图
  2. 状态码301 302
  3. 使用jOOQ的MockDataProvider破解简单的JDBC ResultSet缓存
  4. Django 的简单ajax
  5. java去掉mongodb日志_MongoDB日志文件过大的解决方法 清理
  6. 通信网络安全还应从基础设施保护做起
  7. JMeter分布式负载测试(吞吐量控制器)
  8. 第四季-专题18-FLASH驱动程序设计
  9. Direct3D学习笔记
  10. Python time和datetime模块
  11. webx框架升级springboot遇到的问题及解决方案
  12. allegro转AD教程
  13. 艾孜尔江_国二MS Office考试Excel函数常考知识点
  14. 百度搜索关键词自动提交
  15. 使用家庭或宿舍宽带将个人电脑变为服务器
  16. 第五届“飞思卡尔”智能车竞赛分赛区赛后总结
  17. 【opencv】背景消除
  18. 在maven中创建jsp依赖
  19. http cookie设置失效
  20. 计算机专业的烧脑问题,这几类专业很“烧脑”,数学不好的同学慎报,不然就是噩梦的开始...

热门文章

  1. 支付宝公众账号商户网关的搭建, RSA密钥对生成
  2. 七麦数据js爬虫(附代码)
  3. 在Python中一步一步实现Principal Component Analysis(PCA)
  4. SVN本地电脑存储配置
  5. gloo pytorch_使用Solo Gloo等微服务/ API网关公开在AWS EKS中运行的微服务
  6. B/S聊天室(websocket)
  7. Eslint +Vue配置
  8. LeetCode 937. 重新排列日志文件 / 1823. 找出游戏的获胜者(约瑟夫环问题) / 713. 乘积小于 K 的子数组
  9. 递推数列【清华大学】
  10. 【Kafka】第三篇-Kafka的集群及Canal介绍