【分布式锁】Redis+AOP 实现防止重复提交注解

您所在的位置:网站首页 redis锁实现防刷机制采用的类型 【分布式锁】Redis+AOP 实现防止重复提交注解

【分布式锁】Redis+AOP 实现防止重复提交注解

2024-07-13 20:20| 来源: 网络整理| 查看: 265

目录工作原理需求实现1)自定义防重复提交注解2)定义防重复提交AOP切面3)RedisLock 工具类4)过滤器 + 请求工具类5)测试Controller6)测试结果

工作原理

分布式环境下,可能会遇到用户对某个接口被重复点击的场景,为了防止接口重复提交造成的问题,可用 Redis 实现一个简单的分布式锁来解决问题。

在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

需求实现 自定义一个防止重复提交的注解,注解中可以携带到期时间和一个参数的key 为需要防止重复提交的接口添加注解 注解AOP会拦截加了此注解的请求,进行加解锁处理并且添加注解上设置的key超时时间 Redis 中的 key = token + "-" + path + "-" + param_value; (例如:17800000001 + /api/subscribe/ + zhangsan) 如果重复调用某个加了注解的接口且key还未到期,就会返回重复提交的Result。 1)自定义防重复提交注解

自定义防止重复提交注解,注解中可设置 超时时间 + 要扫描的参数(请求中的某个参数,最终拼接后成为Redis中的key)

package com.lihw.lihwtestboot.noRepeatSubmit; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 防重复提交注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatSubmit { /** * 锁过期的时间 */ int seconds() default 5; /** * 要扫描的参数 */ String scanParam() default ""; } 2)定义防重复提交AOP切面

@Pointcut("@annotation(noRepeatSubmit)") 表示切点表达式,它使用了注解匹配的方式来选择被注解 @NoRepeatSubmit 标记的方法。

package com.lihw.lihwtestboot.noRepeatSubmit; import com.alibaba.fastjson.JSONObject; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.util.UUID; /** * 重复提交aop */ @Aspect @Component public class RepeatSubmitAspect { private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class); @Autowired private RedisLock redisLock; @Pointcut("@annotation(noRepeatSubmit)") public void pointCut(NoRepeatSubmit noRepeatSubmit) { } @Around("pointCut(noRepeatSubmit)") public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable { //获取基本信息 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Assert.notNull(request, "request can not null"); int lockSeconds = noRepeatSubmit.seconds();//过期时间 String threadName = Thread.currentThread().getName();// 获取当前线程名称 String param = noRepeatSubmit.scanParam();//请求参数 String path = request.getServletPath(); String type = request.getMethod(); String param_value = ""; if (type.equals("POST")){ param_value = JSONObject.parseObject(new BodyReaderHttpServletRequestWrapper(request).getBodyString()).getString(param); }else if (type.equals("GET")){ param_value = request.getParameter(param); } String token = request.getHeader("uid"); LOGGER.info("线程:{}, 接口:{},重复提交验证",threadName,path); String key; if (!"".equals(param) && param != null){ key = token + "-" + path + "-" + param_value;//生成key }else { key = token + "-" + path;//生成key } String clientId = getClientId();// 调接口时生成临时value(UUID) // 用于添加锁,如果添加成功返回true,失败返回false boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds); ApiResult result = new ApiResult(); if (isSuccess) { LOGGER.info("加锁成功:接口 = {}, key = {}", path, key); // 获取锁成功 Object obj; try { // 执行进程 obj = pjp.proceed();// aop代理链执行的方法 } finally { // 据key从redis中获取value if (clientId.equals(redisLock.get(key))) { // 解锁 redisLock.releaseLock(key, clientId); LOGGER.info("解锁成功:接口={}, key = {},",path, key); } } return obj; } else { // 添加锁失败,认为是重复提交的请求 LOGGER.info("重复请求:接口 = {}, key = {}",path, key); result.setData("重复提交"); return result; } } private String getClientId() { return UUID.randomUUID().toString(); } public static String getRequestBodyData(HttpServletRequest request) throws IOException{ BufferedReader bufferReader = new BufferedReader(request.getReader()); StringBuilder sb = new StringBuilder(); String line = null; while ((line = bufferReader.readLine()) != null) { sb.append(line); } return sb.toString(); } } 3)RedisLock 工具类 package com.lihw.lihwtestboot.noRepeatSubmit; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class RedisLock { private static final Logger logger = LoggerFactory.getLogger(RedisLock.class); /** 不设置过期时长 */ public final static long NOT_EXPIRE = -1; @Autowired private StringRedisTemplate redisTemplate; /** * @param lockKey 加锁键 * @param clientId 加锁客户端唯一标识(采用UUID) * @param seconds 锁过期时间 * @return */ public boolean tryLock(String lockKey, String clientId, long seconds) { if (redisTemplate.opsForValue().setIfAbsent(lockKey, clientId,seconds, TimeUnit.SECONDS)) { return true;//得到锁 }else{ return false; } } /** * 与 tryLock 相对应,用作释放锁 * * @param lockKey * @param clientId * @return */ public boolean releaseLock(String lockKey, String clientId) { String currentValue = redisTemplate.opsForValue().get(lockKey); try { if (!StringUtils.isEmpty(currentValue) && currentValue.equals(clientId)) { redisTemplate.opsForValue().getOperations().delete(lockKey); return true; }else { return false; } } catch (Exception e) { logger.error("解锁异常,,{}" , e); return false; } } /** * 获取 * @param key * @return */ public String get(String key) { return get(key, NOT_EXPIRE); } public String get(String key, long expire) { String value = redisTemplate.opsForValue().get(key); if(expire != NOT_EXPIRE){ redisTemplate.expire(key, expire, TimeUnit.SECONDS); } return value; } /** * 删除 * @param key */ public void delete(String key) { redisTemplate.delete(key); } } 4)过滤器 + 请求工具类

Filter类

package com.lihw.lihwtestboot.noRepeatSubmit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.web.servlet.ServletComponentScan; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @ServletComponentScan @WebFilter(urlPatterns = "/*",filterName = "channelFilter") public class ChannelFilter implements Filter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { logger.info("-----------------------Execute filter start---------------------"); // 防止流读取一次后就没有了, 所以需要将流继续写出去 HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest); filterChain.doFilter(requestWrapper, servletResponse); } }

BodyReaderHttpServletRequestWrapper

对GET和POST请求的获取参数方法进行了封装

package com.lihw.lihwtestboot.noRepeatSubmit; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{ /** * Request请求参数获取处理类 */ private final byte[] body; public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); String sessionStream = getBodyString(request); body = sessionStream.getBytes(StandardCharsets.UTF_8); } /** * 获取请求Body * * @param request * @return */ private String getBodyString(final ServletRequest request) { StringBuilder sb = new StringBuilder(); InputStream inputStream = null; BufferedReader reader = null; try { inputStream = cloneInputStream(request.getInputStream()); reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); String line = ""; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return sb.toString(); } public String getBodyString() { return new String(body, StandardCharsets.UTF_8); } /** * Description: 复制输入流 * * @param inputStream * @return */ public InputStream cloneInputStream(ServletInputStream inputStream) { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; try { while ((len = inputStream.read(buffer)) > -1) { byteArrayOutputStream.write(buffer, 0, len); } byteArrayOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); return byteArrayInputStream; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return bais.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } } 5)测试Controller package com.lihw.lihwtestboot.noRepeatSubmit; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.NotEmpty; @RestController @RequestMapping("/api") @Validated public class noRepeatSubmitController { @GetMapping("/subscribe/{channel}") @NoRepeatSubmit(seconds = 10,scanParam = "username") public ApiResult subscribe(@RequestHeader(name = "uid") String phone,@RequestHeader(name = "username") String username,@PathVariable("channel") @NotEmpty(message = "channel不能为空") String channel) { System.out.println("phone=" + phone); System.out.println("username=" + username); System.out.println("channel=" + channel); try { Thread.sleep(5000);//模拟耗时 } catch (InterruptedException e) { e.printStackTrace(); } return new ApiResult("success","data"); } } 6)测试结果

重复点击



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3