利用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实现阻塞队列的方式
- 下一篇
- 使用Redis实现令牌桶算法原理解析
-
- 数据库 · Redis | 1天前 |
- 监控Redis集群健康状态的工具与指标
- 112浏览 收藏
-
- 数据库 · Redis | 1星期前 |
- Redis数据安全防护全攻略
- 252浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- Redis主从复制故障排查与修复技巧
- 302浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- Redis与HBase存储方案详解
- 325浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- Redis数据安全防护全攻略
- 157浏览 收藏
-
- 数据库 · Redis | 2星期前 |
- 高并发Redis优化技巧分享
- 257浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- Redis数据安全防护全攻略
- 398浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- Redis配置加密方法与安全设置
- 232浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- RedisHyperLogLog高效统计技巧
- 283浏览 收藏
-
- 数据库 · Redis | 3星期前 |
- Redis与MySQL缓存同步方法详解
- 141浏览 收藏
-
- 数据库 · Redis | 4星期前 |
- Redis布隆过滤器防穿透原理解析
- 312浏览 收藏
-
- 数据库 · Redis | 1个月前 |
- Redis容器化部署实战技巧分享
- 195浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3179次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3390次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3419次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4525次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3798次使用
-
- golang移除数组中重复的元素操作
- 2022-12-24 115浏览

