本篇文章小编给大家分享一下利用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 < 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 = 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,不然耗尽连接数就完蛋了