在划水摸鱼之际,突然听到有的用户反映增加了多条一样的数据,这用户立马就不干了,让我们要马上修复,不然就要投诉我们。

这下鱼也摸不了了,只能去看看发生了什么事情。据用户反映,当时网络有点卡,所以多点了几次提交,最后发现出现了十几条一样的数据。

只能说现在的人都太心急了,连这几秒的时间都等不了,惯的。心里吐槽归吐槽,这问题还是要解决的,不然老板可不惯我。


其实想想就知道为啥会这样,在网络延迟的时候,用户多次点击,最后这几次请求都发送到了服务器访问相关的接口,最后执行插入。

既然知道了原因,该如何解决。当时我的第一想法就是用注解 + 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的过期时间3sint 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 {// 获取requestRequestAttributes 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 < 0) {expire = 3;}// 获取锁boolean isSuccess = redisLock.tryLock(key, key + sessionId, expire);// 获取成功if (isSuccess) {// 执行请求result = pjp.proceed();int status = responese.getStatus();_log.debug("status = {}" + status);// 释放锁,3s后让锁自动释放,也可以手动释放// redisLock.releaseLock(key, key + sessionId);return result;} else {// 失败,认为是重复提交的请求return new UpmsResult(UpmsResultConstant.REPEAT_COMMIT, ValidationError.create(UpmsResultConstant.REPEAT_COMMIT.message));}}
}

拦截器定义的切点是NoRepeatCommit注解,所以被NoRepeatCommit注解标注的接口就会进入该拦截器。这里我使用了account + "_" + sessionId + "_" + url + "_" + pg作为唯一键,表示某个用户访问某个接口。

这样比较关键的一行是boolean isSuccess = redisLock.tryLock(key, key + sessionId, expire);。可以看看RedisLock这个类。

3、Redis工具类

上面讨论过了,获取锁和设置锁需要做成原子操作,不然并发环境下会出问题。这里可以使用Redis的SETNX命令。

/*** 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 = millisecondstime – 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实现防止接口重复提交功能相关推荐

  1. 接口重复提交解决方案

    接口重复提交解决方案 参考文章: (1)接口重复提交解决方案 (2)https://www.cnblogs.com/java-le/p/11056635.html (3)https://www.cod ...

  2. SpringBoot 自定义注解+AOP+Redis 防止接口重复提交表单数据

    SpringBoot结合Redis处理重复提交 数据重复提交导致多次请求服务.入库,产生脏数据.冗余数据等情况.禁止重复提交使我们保证数据准确性及安全性的必要操作. 实际上,造成这种情况的场景不少: ...

  3. SpringBoot+Redis防止接口重复提交

    前言 在项目的使用使用过程中,经常会出现某些操作在短时间内频繁提交.例如:用户鼠标点击过快而重复保存,从而创建了2笔一模一样的单据.针对类似情况,我们就可以全局地控制接口不允许重复提交. 实现思路 创 ...

  4. spring项目使用redis分布式锁解决重复提交问题

    场景演示 假设有一个录入学生信息的功能,为了便于演示,要求不能有重名的学生,并且数据库对应字段没有做唯一限制. @GetMapping("/student/{name}")publ ...

  5. 使用拦截器和redis+token实现防重复提交完整代码

    文章目录 redis配置: 自定义一个注解: 自定义类继承HandlerInterceptor mvc添加刚刚自定义的拦截器使之生效 tokenservice controller redis配置: ...

  6. php接口防止app重复提交,AOP防止接口重复提交

    实现原理 通过自定义注解标记哪些接口需要防范重复提交问题,并定义保持时间: 在Aspect中定义切点,织入所有被自定义注解标记的方法: 在Aspect中定义通知方法,通过PointCut获取类全名.被 ...

  7. redis防表单重复提交

    参考链接: 防表单重复提交的四种方法:https://www.cnblogs.com/huanghuizhou/p/9153837.html 补充几点个人想法: 1. 对于前后端传递token验证的方 ...

  8. redis防止表单重复提交

    1. 对于前后端传递token验证的方式,每次都需要页面加载才能在后端存放token,这样会导致用户在第一次提交表单失败后就无法提交成功,需要刷新页面.  2. 利用session去给前后端的toke ...

  9. 利用session防止表单重复提交

    1.是什么?一个表单不能多次提交: 2.为什么? 在网络不好或者并发请求时会导致多次重复提交数据的问题.防止重复提交,可以维护数据一致性: 3.怎么做? 把 session的编号和当前时间戳经过 MD ...

最新文章

  1. 数据集获取加速神器来了!
  2. 解释一下c语言 for(;;) printf(*);,printf()函数的一个问题
  3. 如何将RDS的数据同步到本地自建数据库
  4. Java8中的Mapreduce
  5. 已知两点的经度和纬度,计算两点间的距离(php,javascript)
  6. SSD 通俗易懂介绍
  7. zookeeper运维管理
  8. 加载checkpoint问题
  9. 转载:mongoDB java驱动学习笔记
  10. eclipse上的.properties文件中文编辑显示问题
  11. 误码率matlab怎么计算,Matlab 仿真(7,4)汉明码 传输误码率
  12. loop在python中什么意思_在python中使用loop打开多个文件
  13. Clojure 学习入门(8)- 连接mongodb
  14. vtiger6.0升级汇总
  15. 使用Blocs For Mac发布网站的方法
  16. 2020年COVID-19撤稿门系列:群魔乱舞,水军纷飞
  17. 本科生如何学习计算机科学与技术
  18. 文章:Mapping regulatory variants controlling gene expression in drought response and tolerance
  19. 物联卡中心:企业物联网卡,共享套餐和独立套餐哪一种实惠
  20. 关于tp-link 路由器

热门文章

  1. 腾讯万字Code Review规范
  2. 哈工大计算机网络-作业讲解
  3. 利用天翎知识文档+群晖NAS搭建企业知识库,享用智能检索
  4. FastDeRain解读
  5. unity-shader 水的效果WaterEffect
  6. C++环境下部署深度学习模型方案
  7. outlook如何同步服务器sent文件夹,.ost 文件的同步问题 - Exchange | Microsoft Docs
  8. Android面试准备之Java基础
  9. 魅族l681q详细开启Usb调试模式的步骤
  10. 【周志华机器学习】十四、概率图模型