使用自定义注解实现Redis分布式锁

@ 作者: 一恍过去
@ 主页: https://blog.csdn.net/zhuocailing3390
@ 社区: Java技术栈交流
@ 主题: 使用自定义注解实现Redis分布式锁
⏱️ @ 创作时间: 2022年06月29日

目录

  • 1、RedisLockServer
  • 2、RedisLock
  • 3、RedisLockAspect
  • 4、开启自动代理
  • 5、使用
  • 6、测试

1、RedisLockServer

定义Redis上锁、解锁实现类

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;


@Component
@Slf4j
public class RedisLockServer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    public Boolean setLock(String lockKey, String value, long time) {
        if(time<=0){
            // 不设置过期时间
            return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
        }
        return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, time, TimeUnit.MILLISECONDS);
    }

    public void deleteLock(String lockKey, String value) {
        List<String> lockKeys = Collections.singletonList(lockKey);
        String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) return 1 else return 0 end";
        RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
        // 删除锁
        stringRedisTemplate.execute(luaScript, lockKeys, value);
    }
}

2、RedisLock

定义“自定义注解”,用于切面实现分布式锁

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
    /**
     * 上锁的方法中,key所在参数的位置,索引从0开始
     * @return
     */
    int keyNum();

    /**
     * 上锁时长,默认设置时间
     * @return
     */
    long lockTime() default 0;

    /**
     * 尝试时间,设置时间内通过自旋一致尝试获取锁,默认0ms
     * @return
     */
    long tryTime() default 0;
}

字段说明:

  • keyNum:
    keyNum表示在被@RedisLock 修饰的方法上,第几个参数表示上锁的key,默认第一个参数,也可以指定key的位置,并且参数的名称必须要为keyName
// key为第一个参数值
void test(String keyName){}

// key为第三个参数值
@RedisLock(keyNum=2)
void test(int a,int b,String keyName){}
  • lockTime:
    lockTime表示上锁过期时间,默认为0,表示不进行上锁处理;
    public Boolean setLock(String lockKey, String value, long time) {
        if(time<=0){
            // 不设置过期时间
            return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
        }
        return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, time, TimeUnit.MILLISECONDS);
    }
  • tryTime:
    tryTime表示尝试获取锁的时间,当设置的时间内,通过自旋的方式一直获取锁

3、RedisLockAspect

切面类定义,代码如下:

import lhz.lx.config.RedisLockServer;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.UUID;

@Aspect
@Component
@Slf4j
public class RedisLockAspect {

    @Resource
    private RedisLockServer redisLockServer;

    private static final ThreadLocal<String> VALUE_THREAD = new ThreadLocal<>();

    private static final ThreadLocal<String> KEY_THREAD = new ThreadLocal<>();

    private static final ThreadLocal<Boolean> LOCK_THREAD = new ThreadLocal<>();

    @Pointcut("@annotation(lhz.lx.aspect.RedisLock)")
    public void lockPoint() {

    }

    /**
     * 环绕通知,调用目标方法
     */
    @Around("lockPoint()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 记录方法执行开始时间
        long startTime = System.currentTimeMillis();

        Object[] args = proceedingJoinPoint.getArgs();
        if(args.length<=0){
            throw new RuntimeException("keyName不存在!");
        }
        String[] argNames = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();

        Signature signature = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        RedisLock lock = method.getAnnotation(RedisLock.class);

        int keyNum = lock.keyNum();
        if(!"keyName".equals(argNames[keyNum])){
            throw new RuntimeException("keyName不存在!");
        }
        String key = args[keyNum].toString();
        long lockTime = lock.lockTime();
        long tryTime = lock.tryTime();
        String value = UUID.randomUUID().toString();

        VALUE_THREAD.set(value);
        KEY_THREAD.set(key);

        log.info("分布式锁上锁,key:{},value:{},lockTime:{}",key,value,lockTime);

        Boolean setLock = redisLockServer.setLock(key, value, lockTime);
        while (!setLock){
            // 重试
            setLock = redisLockServer.setLock(key, value, lockTime);
            if(System.currentTimeMillis()-startTime>tryTime) {
                LOCK_THREAD.set(false);
                log.error("上锁失败");
                throw new RuntimeException("上锁失败!");
            }
        }
        log.info("分布式锁上锁成功,key:{},value:{},lockTime:{}",key,value,lockTime);
        LOCK_THREAD.set(true);
        // 调用目标方法
        return proceedingJoinPoint.proceed();
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(value = "lockPoint()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleData();
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "lockPoint()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleData();
    }

    private void handleData() {
        try {
            String value = VALUE_THREAD.get();
            String key = KEY_THREAD.get();
            if(LOCK_THREAD.get()) {
                log.info("分布式锁解锁,key:{},value:{}", key, value);
                redisLockServer.deleteLock(key, value);
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        } finally {
            VALUE_THREAD.remove();
            KEY_THREAD.remove();
            LOCK_THREAD.remove();
        }
    }
}

4、开启自动代理

因为说明AOP无法拦截类内部的方法之间的调用,需要对启动类加上@EnableAspectJAutoProxy配置,代码如下:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class RedisDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisDemoApplication.class, args);
    }
}

比如方法直接的调用,A方法调用B方法,使用方式如下:

public class RedisServiceImpl implements RedisService{
	public void A() {
		// 方法类调用
		RedisServiceImpl service = (RedisServiceImpl) AopContext.currentProxy();
		 service.B();
	}
	
	@RedisLock
	public void B() {
	}
}

5、使用

注意: 在调用使用分布式锁的方法时,需要进try…catch…,并且在catch中处理上锁失败的情况;

TestController:

@RestController
@RequestMapping
@Slf4j
public class TestController {

    @Resource
    private RedisService redisService;

    @GetMapping(value = "/test")
    public String test() {
        redisService.test();
        return "success";
    }

    @GetMapping(value = "/test2")
    public String test2() {
        String key = UUID.randomUUID().toString();
        redisService.test2(key);
        return "success";
    }
}

RedisService :

public interface RedisService {
    /**
     *  方法内部调用使用锁
     */
    void test();

    /**
     * 方法直接调用使用锁
     * @param keyName
     */
    void test2(String keyName);
}

RedisServiceImpl:

@Service
@Slf4j
public class RedisServiceImpl implements RedisService{

    /**
     *  方法内部调用使用锁
     */
    @Override
    public void test() {
        log.info("方法内部调用使用锁");

        // 调用内部方法
        RedisServiceImpl service = (RedisServiceImpl) AopContext.currentProxy();
        // 在调用使用分布式锁的方法时,需要进try...catch...,并且在catch中处理上锁失败的情况
        try {
            service.testLock("test11");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 方法直接调用使用锁
     */
    @Override
    @RedisLock(keyNum = 0)
    public void test2(String keyName) {
        log.info("方法直接调用使用锁");
    }
    /**
     * 被上锁的方法中,一定要存在一个叫"keyName"的参数
     * @param keyName
     */
    @RedisLock
    public void testLock(String keyName) throws InterruptedException {
        log.info(keyName);
    }
}

6、测试

测试一:
正常上锁情况
在这里插入图片描述

测试二:
设置tryTime,并且tryTime小于lockTime;当这样配置时,在第一个线程没有结束时,第二个线程,超过tryTime就会出现上锁失败;

修改代码如下:

    @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 2000)
    public void testLock(String keyName) throws InterruptedException {
        log.info(keyName);
         TimeUnit.SECONDS.sleep(5);
    }

快速请求两次接口,截图如下:
在这里插入图片描述
通过截图可以看到,在第一个线程上锁后,过了2000ms出现了上锁失败的提示;

测试三:
设置tryTime,并且tryTime大于lockTime;当这样配置时,不会出现上锁失败,并且第二个线程会一直等到第一个线程结束;
修改代码如下:

    @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 4000)
    public void testLock(String keyName) throws InterruptedException {
        log.info(keyName);
         TimeUnit.SECONDS.sleep(5);
    }

快速请求两次接口,截图如下:
在这里插入图片描述

通过截图可以看到,在第一个线程上锁后,超过了3000ms后,第二个线程开始上锁成功

你可能感兴趣的:(Redis,分布式,Java,redis,分布式,java)