利用Redis实现防止接口重复提交功能
在数据库实战开发的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天golang学习网就整理分享《利用Redis实现防止接口重复提交功能》,聊聊重复、redis接口,希望可以帮助到正在努力赚钱的你。
这下鱼也摸不了了,只能去看看发生了什么事情。据用户反映,当时网络有点卡,所以多点了几次提交,最后发现出现了十几条一样的数据。
只能说现在的人都太心急了,连这几秒的时间都等不了,惯的。心里吐槽归吐槽,这问题还是要解决的,不然老板可不惯我。
其实想想就知道为啥会这样,在网络延迟的时候,用户多次点击,最后这几次请求都发送到了服务器访问相关的接口,最后执行插入。
既然知道了原因,该如何解决。当时我的第一想法就是用注解 + AOP。通过在自定义注解里定义一些相关的字段,比如过期时间即该时间内同一用户不能重复提交请求。然后把注解按需加在接口上,最后在拦截器里判断接口上是否有该接口,如果存在则拦截。
解决了这个问题那还需要解决另一个问题,就是怎么判断当前用户限定时间内访问了当前接口。其实这个也简单,可以使用Redis来做,用户名 + 接口 + 参数啥的作为唯一键,然后这个键的过期时间设置为注解里过期字段的值。设置一个过期时间可以让键过期自动释放,不然如果线程突然歇逼,该接口就一直不能访问。
这样还需要注意的一个问题是,如果你先去Redis获取这个键,然后判断这个键不存在则设置键;存在则说明还没到访问时间,返回提示。这个思路是没错的,但这样如果获取和设置分成两个操作,就不满足原子性了,那么在多线程下是会出错的。所以这样需要把俩操作变成一个原子操作。
分析好了,就开干。
1、自定义注解
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 防止同时提交注解 */ @Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatCommit { // key的过期时间3s int expire() default 3; }
这里为了简单一点,只定义了一个字段expire
,默认值为3,即3s内同一用户不允许重复访问同一接口。使用的时候也可以传入自定义的值。
我们只需要在对应的接口上添加该注解即可
@NoRepeatCommit 或者 @NoRepeatCommit(expire = 10)
2、自定义拦截器
自定义好了注解,那就该写拦截器了。
@Aspect public class NoRepeatSubmitAspect { private static Logger _log = LoggerFactory.getLogger(NoRepeatSubmitAspect.class); RedisLock redisLock = new RedisLock(); @Pointcut("@annotation(com.zheng.common.annotation.NoRepeatCommit)") public void point() {} @Around("point()") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { // 获取request RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = servletRequestAttributes.getRequest(); HttpServletResponse responese = servletRequestAttributes.getResponse(); Object result = null; String account = (String) request.getSession().getAttribute(UpmsConstant.ACCOUNT); User user = (User) request.getSession().getAttribute(UpmsConstant.USER); if (StringUtils.isEmpty(account)) { return pjp.proceed(); } MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); NoRepeatCommit form = method.getAnnotation(NoRepeatCommit.class); String sessionId = request.getSession().getId() + "|" + user.getUsername(); String url = ObjectUtils.toString(request.getRequestURL()); String pg = request.getMethod(); String key = account + "_" + sessionId + "_" + url + "_" + pg; int expire = form.expire(); if (expire <p>拦截器定义的切点是<code>NoRepeatCommit</code>注解,所以被<code>NoRepeatCommit</code>注解标注的接口就会进入该拦截器。这里我使用了<code>account + "_" + sessionId + "_" + url + "_" + pg</code>作为唯一键,表示某个用户访问某个接口。</p> <p>这样比较关键的一行是<code>boolean isSuccess = redisLock.tryLock(key, key + sessionId, expire);</code>。可以看看<code>RedisLock</code>这个类。</p> <h2>3、Redis工具类</h2> <p>上面讨论过了,获取锁和设置锁需要做成原子操作,不然并发环境下会出问题。这里可以使用Redis的<code>SETNX</code>命令。</p> <pre class="brush:java;"> /** * redis分布式锁实现 * Lua表达式为了保持数据的原子性 */ public class RedisLock { /** * redis 锁成功标识常量 */ private static final Long RELEASE_SUCCESS = 1L; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "EX"; private static final String LOCK_SUCCESS= "OK"; /** * 加锁 Lua 表达式。 */ private static final String RELEASE_TRY_LOCK_LUA = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end"; /** * 解锁 Lua 表达式. */ private static final String RELEASE_RELEASE_LOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; /** * 加锁 * 支持重复,线程安全 * 既然持有锁的线程崩溃,也不会发生死锁,因为锁到期会自动释放 * @param lockKey 加锁键 * @param userId 加锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 String 类型) * @param expireTime 锁过期时间 * @return OK 如果key被设置了 */ public boolean tryLock(String lockKey, String userId, long expireTime) { Jedis jedis = JedisUtils.getInstance().getJedis(); try { jedis.select(JedisUtils.index); String result = jedis.set(lockKey, userId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) jedis.close(); } return false; } /** * 解锁 * 与 tryLock 相对应,用作释放锁 * 解锁必须与加锁是同一人,其他人拿到锁也不可以解锁 * * @param lockKey 加锁键 * @param userId 解锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 String 类型) * @return */ public boolean releaseLock(String lockKey, String userId) { Jedis jedis = JedisUtils.getInstance().getJedis(); try { jedis.select(JedisUtils.index); Object result = jedis.eval(RELEASE_RELEASE_LOCK_LUA, Collections.singletonList(lockKey), Collections.singletonList(userId)); if (RELEASE_SUCCESS.equals(result)) { return true; } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) jedis.close(); } return false; } }
在加锁的时候,我使用了String result = jedis.set(lockKey, userId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
。set方法如下
/* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1 GB). Params: key – value – nxxx – NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. expx – EX|PX, expire time units: EX = seconds; PX = milliseconds time – expire time in the units of expx Returns: Status code reply */ public String set(final String key, final String value, final String nxxx, final String expx, final long time) { checkIsInMultiOrPipeline(); client.set(key, value, nxxx, expx, time); return client.getStatusCodeReply(); }
在key不存在的情况下,才会设置key,设置成功则返回OK。这样就做到了查询和设置原子性。
需要注意这里在使用完jedis,需要进行close,不然耗尽连接数就完蛋了,我不会告诉你我把服务器搞挂了。
4、其他想说的
其实做完这三步差不多了,基本够用。再考虑一些其他情况的话,比如在expire设置的时间内,我这个接口还没执行完逻辑咋办呢?
其实我们不用自己在这整破轮子,直接用健壮的轮子不好吗?比如Redisson
,来实现分布式锁,那么上面的问题就不用考虑了。有看门狗来帮你做,在键过期的时候,如果检查到键还被线程持有,那么就会重新设置键的过期时间。
本篇关于《利用Redis实现防止接口重复提交功能》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于数据库的相关知识,请关注golang学习网公众号!

- 上一篇
- 基于Redis实现阻塞队列的方式

- 下一篇
- 使用Redis实现令牌桶算法原理解析
-
- 爱听歌的大神
- 太给力了,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢博主分享文章!
- 2023-03-02 05:55:57
-
- 激昂的老师
- 这篇技术贴太及时了,太细致了,写的不错,码住,关注up主了!希望up主能多写数据库相关的文章。
- 2023-02-13 22:03:04
-
- 数据库 · Redis | 3小时前 |
- Redis数据安全防护全攻略
- 413浏览 收藏
-
- 数据库 · Redis | 8小时前 |
- 多线程Redis优化技巧全解析
- 493浏览 收藏
-
- 数据库 · Redis | 22小时前 |
- Redis漏洞扫描与修复方法大全
- 267浏览 收藏
-
- 数据库 · Redis | 1天前 |
- Redis启动后无法访问怎么办
- 499浏览 收藏
-
- 数据库 · Redis | 1天前 |
- 多线程Redis优化技巧全解析
- 168浏览 收藏
-
- 数据库 · Redis | 1天前 |
- Redis集群监控工具及关键指标解析
- 483浏览 收藏
-
- 数据库 · Redis | 1天前 |
- Prometheus监控Redis配置详解
- 375浏览 收藏
-
- 数据库 · Redis | 2天前 |
- 多线程Redis优化技巧与实战
- 262浏览 收藏
-
- 数据库 · Redis | 2天前 |
- Redis主从复制配置与搭建教程
- 145浏览 收藏
-
- 数据库 · Redis | 2天前 |
- Redis集群节点规划与部署全攻略
- 499浏览 收藏
-
- 数据库 · Redis | 2天前 |
- Redis与Elasticsearch整合应用全解析
- 243浏览 收藏
-
- 数据库 · Redis | 2天前 |
- Redis内存过高怎么优化?
- 149浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 边界AI平台
- 探索AI边界平台,领先的智能AI对话、写作与画图生成工具。高效便捷,满足多样化需求。立即体验!
- 422次使用
-
- 免费AI认证证书
- 科大讯飞AI大学堂推出免费大模型工程师认证,助力您掌握AI技能,提升职场竞争力。体系化学习,实战项目,权威认证,助您成为企业级大模型应用人才。
- 426次使用
-
- 茅茅虫AIGC检测
- 茅茅虫AIGC检测,湖南茅茅虫科技有限公司倾力打造,运用NLP技术精准识别AI生成文本,提供论文、专著等学术文本的AIGC检测服务。支持多种格式,生成可视化报告,保障您的学术诚信和内容质量。
- 561次使用
-
- 赛林匹克平台(Challympics)
- 探索赛林匹克平台Challympics,一个聚焦人工智能、算力算法、量子计算等前沿技术的赛事聚合平台。连接产学研用,助力科技创新与产业升级。
- 665次使用
-
- 笔格AIPPT
- SEO 笔格AIPPT是135编辑器推出的AI智能PPT制作平台,依托DeepSeek大模型,实现智能大纲生成、一键PPT生成、AI文字优化、图像生成等功能。免费试用,提升PPT制作效率,适用于商务演示、教育培训等多种场景。
- 574次使用
-
- golang移除数组中重复的元素操作
- 2022-12-24 115浏览