接口重复提交解决方案

1
2
3
4
5
6
7
8
9
10
/**
 * 防止重复提交的注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AvoidRepeatSubmit {
 
    long lockTime() default 1000;
 
}

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import com.mushi.anno.AvoidRepeatSubmit;
import com.mushi.config.ResultGenerator;
import com.mushi.redis.service.RedisDistributedLock;
import lombok.extern.slf4j.Slf4j;
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.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.util.UUID;
 
 
/**
 * 防止重复提交的切面
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
 
 
    @Autowired
    private RedisDistributedLock redisDistributedLock;
 
 
    /**
     * 切点
     *
     * @param avoidRepeatSubmit
     */
    @Pointcut("@annotation(avoidRepeatSubmit)")
    public void pointCut(AvoidRepeatSubmit avoidRepeatSubmit) {
    }
 
    /**
     * 利用环绕通知进行处理重复提交问题
     *
     * @param pjp
     * @param avoidRepeatSubmit
     * @return
     * @throws Throwable
     */
    @Around("pointCut(avoidRepeatSubmit)")
    public Object around(ProceedingJoinPoint pjp, AvoidRepeatSubmit avoidRepeatSubmit) throws Throwable {
 
        /**
         * 获取锁的时间
         */
        long lockSeconds = avoidRepeatSubmit.lockTime();
 
        //获得request对象
        HttpServletRequest request = httpServletRequest();
 
        Assert.notNull(request, "request can not null");
 
        // 此处可以用token或者JSessionId
        String token = request.getHeader("token");
        String path = request.getServletPath();
        String key = getKey(token, path);
        String clientId = getClientId();
 
        //锁定多少秒
        boolean isSuccess = redisDistributedLock.setLock(key, clientId, lockSeconds);
 
        if (isSuccess) {
 
            log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
 
            // 获取锁成功, 执行进程
            Object result;
            try {
                result = pjp.proceed();
 
            } finally {
 
                //解锁
                redisDistributedLock.releaseLock(key, clientId);
                log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
 
            }
 
            return result;
 
        } else {
 
            // 获取锁失败,认为是重复提交的请求
            log.info("tryLock fail, key = [{}]", key);
            return ResultGenerator.genRepeatSubmitResult("重复请求,请稍后再试");
 
        }
 
    }
 
    /**
     * 获得request对象
     *
     * @return
     */
    private HttpServletRequest httpServletRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes.getRequest();
    }
 
 
    /**
     * 获得请求key
     *
     * @param token
     * @param path
     * @return
     */
    private String getKey(String token, String path) {
        return token + path;
    }
 
    /**
     * 获得uuid
     *
     * @return
     */
    private String getClientId() {
        return UUID.randomUUID().toString();
    }
 
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;
 
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
 
 
@Component
public class RedisDistributedLock {
 
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
 
    public static final String UNLOCK_LUA;
 
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }
 
    private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
 
    public boolean setLock(String key, String clientId, long expire) {
        try {
            RedisCallback<String> callback = (connection) -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                return commands.set(key, clientId, "NX", "PX", expire);
            };
            String result = redisTemplate.execute(callback);
 
            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
        }
        return false;
    }
 
    public String get(String key) {
        try {
            RedisCallback<String> callback = (connection) -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                return commands.get(key);
            };
            String result = redisTemplate.execute(callback);
            return result;
        } catch (Exception e) {
            logger.error("get redis occured an exception", e);
        }
        return "";
    }
 
    public boolean releaseLock(String key, String requestId) {
        // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
        try {
            List<String> keys = new ArrayList<>();
            keys.add(key);
            List<String> args = new ArrayList<>();
            args.add(requestId);
 
            // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
            RedisCallback<Long> callback = (connection) -> {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                // 集群模式
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
                }
 
                // 单机模式
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
                }
                return 0L;
            };
            Long result = redisTemplate.execute(callback);
 
            return result != null && result > 0;
        } catch (Exception e) {
            logger.error("release lock occured an exception", e);
        } finally {
            // 清除掉ThreadLocal中的数据,避免内存溢出
            //lockFlag.remove();
        }
        return false;
    }
 
}

  

  

 

然后再需要接口防重的接口上加上AvoidRepeatSubmit注解 

 

posted @   浪涛飞  阅读(4882)  评论(1编辑  收藏  举报
编辑推荐:
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
阅读排行:
· 2025成都.NET开发者Connect圆满结束
· 后端思维之高并发处理方案
· 千万级大表的优化技巧
· 在 VS Code 中,一键安装 MCP Server!
· 10年+ .NET Coder 心语 ── 继承的思维:从思维模式到架构设计的深度解析
点击右上角即可分享
微信分享提示