redis 判断存在性_springboot + redis + 注解 + 拦截器 实现接口幂等性校验

提醒:后面有些图片模糊,请点击原文查看清晰图片

一、概念

75c8a01c1806d5ca1e5e1abe368d1d12.png

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次

比如:

订单接口, 不能多次创建订单

支付接口, 重复支付同一笔订单只能扣一次钱

支付宝回调接口, 可能会多次回调, 必须处理重复回调

普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次

等等

二、常见解决方案

75c8a01c1806d5ca1e5e1abe368d1d12.png

唯一索引 -- 防止新增脏数据

token机制 -- 防止页面重复提交

悲观锁 -- 获取数据的时候加锁(锁表或锁行)

乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据

分布式锁 -- redis(jedis、redisson)或zookeeper实现

状态机 -- 状态变更, 更新数据时判断状态

三、本文实现

75c8a01c1806d5ca1e5e1abe368d1d12.png

本文采用第2种方式实现, 即通过redis + token机制实现接口幂等性校验

四、实现思路

75c8a01c1806d5ca1e5e1abe368d1d12.png

为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:

如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示

如果不存在, 说明参数不合法或者是重复请求, 返回提示即可

五、项目简介

75c8a01c1806d5ca1e5e1abe368d1d12.png

  • springboot

  • redis

  • @ApiIdempotent注解 + 拦截器对请求进行拦截

  • @ControllerAdvice全局异常处理

  • 压测工具: jmeter

说明:

本文重点介绍幂等性核心实现, 关于springboot如何集成redis、ServerResponse、ResponseCode等细枝末节不在本文讨论范围之内, 有兴趣的小伙伴可以查看我的Github项目: https://github.com/wangzaiplus/springboot/tree/wxw

六、代码实现

75c8a01c1806d5ca1e5e1abe368d1d12.png

pom

<dependency>    <groupId>redis.clientsgroupId>    <artifactId>jedisartifactId>    <version>2.9.0version>dependency><dependency>    <groupId>org.projectlombokgroupId>    <artifactId>lombokartifactId>    <version>1.16.10version>dependency>

JedisUtil

@Component@Slf4jpublic 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);        }    }    ..... }

自定义注解@ApiIdempotent

/** * 在需要保证 接口幂等性 的Controller的方法上使用此注解 */@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface ApiIdempotent {}

ApiIdempotentInterceptor拦截器

/** * 接口幂等性拦截器 */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 {    }}

TokenServiceImpl

@Servicepublic class TokenServiceImpl implements TokenService {    private static final String TOKEN_NAME = "token";    @Autowired    private JedisUtil jedisUtil;    @Override    public 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());    }    @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());        }    }}

TestApplication

@SpringBootApplication@MapperScan("com.wangzaiplus.test.mapper")public class TestApplication  extends WebMvcConfigurerAdapter {    public static void main(String[] args) {        SpringApplication.run(TestApplication.class, args);    }    /**     * 跨域     * @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);    }    @Override    public void addInterceptors(InterceptorRegistry registry) {        // 接口幂等性拦截器        registry.addInterceptor(apiIdempotentInterceptor());        super.addInterceptors(registry);    }    @Bean    public ApiIdempotentInterceptor apiIdempotentInterceptor() {        return new ApiIdempotentInterceptor();    }}

OK, 目前为止, 校验代码准备就绪, 接下来测试验证

七、测试验证

获取token的控制器TokenController

@RestController@RequestMapping("/token")public class TokenController {    @Autowired    private TokenService tokenService;    @GetMapping    public ServerResponse token() {        return tokenService.createToken();    }}

TestController, 注意@ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响

@RestController@RequestMapping("/test")@Slf4jpublic class TestController {    @Autowired    private TestService testService;    @ApiIdempotent    @PostMapping("testIdempotence")    public ServerResponse testIdempotence() {        return testService.testIdempotence();    }}
  1. 获取token

11b49fbfba17e91eed24396e0c245597.png

查看redis

30485dacbab82ee67c05c2c2e61d28f0.png

  1. 测试接口安全性: 利用jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数

ba505a7b2bdb21aa6505253e82874c20.png

  1. header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"abcd"

    ad5a4bf731055c2791ce5df4a41b0dda.png

八、注意点(非常重要)

99adc7c86e7695c88b2e17d08ae234c4.png

上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第46行, 此时token还未被删除, 所以继续往下执行, 如果不校验jedisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作, 下面重现一下

稍微修改一下代码:

7d506f2f8a78b6f638cd2b1777c36305.png

再次请求

ec6fa1f9a65c460eaf2d01b0af791294.png

再看看控制台

f62083f4a839fd083cbb035a74d3b4e3.png

虽然只有一个真正删除掉token, 但由于没有对删除结果进行校验, 所以还是有并发问题, 因此, 必须校验

九、总结

其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用spring aop实现, 无所谓

如果小伙伴有什么疑问或者建议欢迎提出

Github
https://github.com/wangzaiplus/springboot/tree/wxw

作者:wangzaiplus

链接:https://www.jianshu.com/p/6189275403ed

【推荐阅读】

数据库之架构:主备+分库?主从+读写分离?

(完)

c7342c3c6590082d02ea4e30211efa96.png

(java思维导图)

长按关注,每天java一下,成就架构师

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/496732.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++学习之路 | PTA乙级—— 1089 狼人杀-简单版(精简)

1089 狼人杀-简单版 (20 分)以下文字摘自《灵机一动好玩的数学》&#xff1a;“狼人杀”游戏分为狼人、好人两大阵营。在一局“狼人杀”游戏中&#xff0c;1 号玩家说&#xff1a;“2 号是狼人”&#xff0c;2 号玩家说&#xff1a;“3 号是好人”&#xff0c;3 号玩家说&#…

Java并发编程实战~Condition

利用两个条件变量快速实现阻塞队列呢&#xff1f; public class BlockedQueue<T>{final Lock lock new ReentrantLock();// 条件变量&#xff1a;队列不满 final Condition notFull lock.newCondition();// 条件变量&#xff1a;队列不空 final Condition notEmpty …

汽车行业最大创新仍未到来,四大力量将重塑未来汽车新纪元

来源&#xff1a; 资本实验室 作者&#xff1a;王进自第一辆福特“T”型车问世以来&#xff0c;汽车行业已经诞生了众多层出不穷、持续进化的创新成果。例如&#xff0c;制造商不断创造了新的车体风格&#xff0c;拓展了新的市场区隔&#xff0c;改进了自动换档和动力转向系…

C++学习之路 | PTA乙级——1090 危险品装箱 (25 分)(精简)

1090 危险品装箱 (25 分) 集装箱运输货物时&#xff0c;我们必须特别小心&#xff0c;不能把不相容的货物装在一只箱子里。比如氧化剂绝对不能跟易燃液体同箱&#xff0c;否则很容易造成爆炸。 本题给定一张不相容物品的清单&#xff0c;需要你检查每一张集装箱货品清单&#x…

echarts柱形图x轴y轴互换_数控机床在加工零件时,突然出现X、Y、Z轴失控?如何处理...

数控机床现在广泛应用于单品种大批量的零件加工中&#xff0c;由于稳定性强、精度高、效率高&#xff0c;取代了原来的普通机床。同时数控铣床、数控车床被大规模的配置到各产品自动化生产线上&#xff0c;实现了自动化无入管理。但在生产中由于数控机床的伺服系统出现故障&…

PHP新手上路(十)

9. 简易banner动态更替   不知大家有没有发现各大站点上的标头广告banner&#xff0c;我们每次访问这些站点时&#xff0c;都会看到不同的广告图标&#xff0c;或者如果你每次刷新页面时&#xff0c;这些广告banner就会不断地随机更替变换。要实现这种效果虽然用javascript…

python beautiful soup库的用法

Python 爬虫利器二 之 Beautiful Soup 的用法&#xff1a;http://cuiqingcai.com/1319.html Beautiful Soup 4.2.0 文档&#xff1a;https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html Python3 --- BeautifulSoup --- 节点选择器&#xff1a;https://www.…

Java并发编程实战~原子类

对于简单的原子性问题&#xff0c;还有一种无锁方案&#xff0c;先看看如何利用原子类解决累加器问题。 public class Test {AtomicLong count new AtomicLong(0);public void add10K() {int idx 0;while(idx < 10000) {count.getAndIncrement();}}} 无锁方案相对互斥锁…

著名物理学家斯蒂芬•霍金去世,他曾告诫人类要学会避免人工智能可能的风险

据多家媒体报道&#xff0c;著名的英国物理学家斯蒂芬霍金于3 月 14 日去世&#xff0c;享年 76 岁。霍金教授的孩子露西&#xff0c;罗伯特和蒂姆发表了声明确认了这一消息。斯蒂芬威廉霍金(Stephen William Hawking)&#xff0c;1942年1月8日出生于英国牛津&#xff0c;英国剑…

对《生产流水线模式》讨论的总结性回复

我的上一篇文章《生产流水线模式》发布以后&#xff0c;引起了很多朋友的关注&#xff0c;大家发表了很多意见&#xff0c;现在我针对留言中大家提得比较多的问题&#xff0c;做一个总结性的回复。 问题一&#xff1a;我的敏捷开发架构是不是只实现了简单的增删改查功能&#x…

C++学习之路 | PTA乙级—— 1091 N-自守数 (15 分)(精简)

1091 N-自守数 (15 分) 如果某个数 K 的平方乘以 N 以后&#xff0c;结果的末尾几位数等于 K&#xff0c;那么就称这个数为“N-自守数”。例如 392​2​​25392&#xff0c;而 25392 的末尾两位正好是 92&#xff0c;所以 92 是一个 3-自守数。 本题就请你编写程序判断一个给定…

电路板上的插头怎么拔下来_空调插头一直不拔费电吗?实测一周竟然发现了真相!...

随着气温的不断走低&#xff0c;全国绝大部分城市已经进入了真正意义上的秋天。那么&#xff0c;经过了酷热一夏&#xff0c;你家的空调电源插头拔掉了吗? 说道空调&#xff0c;想必大家都知道这“家伙”可是耗电大户。尤其在七八月份&#xff0c;几乎都是24小时不停机在运行。…

C++ stringstream

参考&#xff1a;http://www.usidcbbs.com/read-htm-tid-1898.html 常见格式串 %% 印出百分比符号&#xff0c;不转换。 %c 整数转成对应的 ASCII 字元。 %d 整数转成十进位。 %f 倍精确度数字转成浮点数。 %o 整数转成八进位。 %s 整数转成字符串。 %x 整数转成小写十六进位…

Java并发编程实战~思维导图

#原图 System.out.println("https://www.processon.com/view/link/61a235be1efad425fd6ff5f6")

中国“脑计划”研究正在悄然布局

来源&#xff1a;中国科学报 作者&#xff1a;黄辛 作为“十三五”上海科研发展的重点&#xff0c;一张神奇的脑计划研究“地图”正在悄然加紧布局。日前&#xff0c;在教育部科技司、国家自然科学基金委员会政策局、华东师范大学主办的“交叉融合的教育科学基础研究”研讨会上…

C++学习之路 | PTA乙级——1092 最好吃的月饼 (20 分)(精简)

1092 最好吃的月饼 (20 分)、 月饼是久负盛名的中国传统糕点之一&#xff0c;自唐朝以来&#xff0c;已经发展出几百品种。 若想评比出一种“最好吃”的月饼&#xff0c;那势必在吃货界引发一场腥风血雨…… 在这里我们用数字说话&#xff0c;给出全国各地各种月饼的销量&…

JDBC链接SQLServer2005 Express

SQLServer2005的数据库链接的jar包可以到微软网站上去下载&#xff0c;驱动和链接字符串如下&#xff1a; 1: String driver "com.microsoft.sqlserver.jdbc.SQLServerDriver"; 2: String url "jdbc:sqlserver://localhost:1433;databaseNamepubs"; SQLS…

dnf时装预览怎么打开_DNF:史上最好看时装上线,大佬已经玩疯,全服都是真香的气息...

大家好&#xff01;我是风柜君&#xff0c;这次又是我来给大家带来DNF游戏内外的趣事&#xff0c;希望各位喜欢。DNF国服将在6月18日迎来第11周年庆&#xff0c;而在此之前&#xff0c;国服在6月6日的时候已经更新了一波周年庆预热活动。这次的更新除了普雷妮普通副本以外&…

Java并发编程实战~Immutability模式

解决并发问题&#xff0c;其实最简单的办法就是让共享变量只有读操作&#xff0c;而没有写操作。这个办法如此重要&#xff0c;以至于被上升到了一种解决并发问题的设计模式&#xff1a;不变性&#xff08;Immutability&#xff09;模式。所谓不变性&#xff0c;简单来讲&#…

深度解析:AWS、谷歌云、IBM Cloud和微软 Azure四巨头2018将会有哪些布局?

来源&#xff1a; IDC圈近来&#xff0c;公司规模已经不再是企业选择云服务商的重要因素&#xff0c;市场对云服务商优劣的判断有了多种标准。企业对全球一些大型云计算服务商&#xff08;例如亚马逊AWS&#xff0c;谷歌云平台&#xff0c;IBM Cloud和微软 Azure等&#xff09;…