SpringBoot + Redis + AOP 控制接口请求重复提交

2 篇文章 0 订阅
1 篇文章 0 订阅

前言

这个文章汇聚了很多。参考了很多文章和知识,最后实践汇总而成的。这里着重声明感谢 等待的萝卜,AOP这块就是借鉴他的文章去做的。但是由于Jedis的频频错误,我选择RedisTemplate配合lua脚本去做。这篇文章的目的,最终希望给大家一个正确的方式来实现。避免出现其他一些坑。

附上 等待的萝卜 文章地址:https://blog.csdn.net/a992795427/article/details/92834286

直接上代码:

Redis 分布式锁实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;

/**
 * Redis 分布式锁实现
 * lua 表达式为了保持数据的原子性。
 *
 * @see <a href="https://redis.io/commands/setnx">SETNX key value</a>
 * @see <a href="https://www.cnblogs.com/linjiqin/p/8003838.html">Redis分布式锁的正确实现方式</a>
 */
@Component
public class RedisLock {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * redis 锁成功标识常量
     */
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 加锁 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";

    /**
     * 加锁
     * 该加锁方法仅针对单实例 Redis 可实现分布式加锁
     * 对于 Redis 集群则无法使用
     * <p>
     * 支持重复,现成安全
     *
     * @param lockKey    加锁键
     * @param userId     加锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 String 类型)
     * @param expireTime 锁过期时间
     * @return 1 如果key被设置了  0 如果key没有被设置
     */
    public boolean tryLock(String lockKey, String userId, long expireTime) {
        try {
            RedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_TRY_LOCK_LUA, Long.class);
            Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), userId, expireTime);
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    /**
     * 解锁
     * 与 tryLock 相对应,用作释放锁
     * 解锁必须与加锁是同一人,其他人拿到锁也不可以解锁 (解铃还须系铃人)
     *
     * @param lockKey 加锁键
     * @param userId  解锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 String 类型)
     * @return
     */
    public boolean releaseLock(String lockKey, String userId) {
        RedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_RELEASE_LOCK_LUA, Long.class);
        Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), userId);
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }

        return false;
    }
}

自定义注解:

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 ControlRepeatSubmit {

    /**
     * 设置重复请求请求锁定时间 单位秒
     *
     * @return
     */
    int lockTime() default 3;
}

自定义注解切面实现:

import com.test.utils.RedisLock;
import com.test.utils.RequestUtils;
import com.test.utils.annotation.ControlRepeatSubmit;
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.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import java.util.Objects;

/**
 * 切面类
 */
@Aspect
@Component
public class RepeatSubmitAspect {

    @Autowired
    private RedisLock redisLock;

    @Pointcut("@annotation(controlRepeatSubmit)")
    public void pointCut(ControlRepeatSubmit controlRepeatSubmit) {

    }

    @Around("pointCut(controlRepeatSubmit)")
    public Object around(ProceedingJoinPoint point, ControlRepeatSubmit controlRepeatSubmit) throws Throwable {

        // 获得注解锁时间 单位秒
        int lockTime = controlRepeatSubmit.lockTime();

        // 从 HttpServletRequest 里获取登陆用户 id
        HttpServletRequest request = RequestUtils.getRequest();
        Assert.notNull(request, "request can not null");
        // 从消息头获取用户id
        String userIdStr = (String) Objects.requireNonNull(request).getAttribute("id");
        // 获取请求url路径
        String servletPath = request.getServletPath();
        String key = getKey(userIdStr, servletPath);

        boolean isSuccess = redisLock.tryLock(key, userIdStr, lockTime);
        if (isSuccess) {
            // 获取锁成功
            Object result;
            try {
                result = point.proceed();
            } finally {
                // 解锁
                redisLock.releaseLock(key, userIdStr);
            }

            return result;
        } else {
            // 获取锁失败,认为是重复提交的请求
            throw new RuntimeException("重复请求");
        }

    }

    private String getKey(String userId, String path) {
        return userId + path;
    }
}

HttpRequestUtils

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 获取 HttpServletRequest 工具类
 *
 */
public class RequestUtils {

    public static HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        return requestAttributes.getRequest();
    }
}

使用

/**
 * 在controller上,使用 @ControlRepeatSubmit 注解即可
 */
@GetMapping("/test")
@ControlRepeatSubmit(lockTime = 1)
public void test() {
    // 你的业务
}

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值