RedisLock, reids分布式锁工具类

package com.emdata.lowvis.common.redislock;
​
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
​
import java.util.concurrent.TimeUnit;
​
/**
 * reids分布式锁工具类
 *
 * @version 1.0
 * @date 2020/12/8 14:37
 */
@Slf4j
@Component
public class RedisLock {
​
    private static final String SPLIT = "_";
​
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
​
    /**
     * 加锁解锁工具类
     * @param lockKey 加锁的key
     * @param uuid 线程的标志
     * @param timeout 超时时间
     * @param timeUnit 超时时间粒度
     * @return true:获取成功
     */
    public boolean lock(String lockKey, String uuid, long timeout, TimeUnit timeUnit) {
        // 根据key获取值
        String currentLock = stringRedisTemplate.opsForValue().get(lockKey);
​
        // 值为:uuid_时间
        String value = uuid + SPLIT + (timeUnit.toMillis(timeout) + System.currentTimeMillis());
​
        // 如果为空,则设置值
        if (StringUtils.isEmpty(currentLock)) {
            if (stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, timeout, timeUnit)) {
                // 对应setnx命令,可以成功设置,也就是key不存在,获得锁成功
                return true;
            } else {
                return false;
            }
        } else {
            // 可重入锁,如果是这个uuid持有的锁,则更新时间
            if (currentLock.startsWith(uuid)) {
                stringRedisTemplate.opsForValue().set(lockKey, value, timeout, timeUnit);
                return true;
            } else {
                return false;
            }
        }
    }
    
    /*
     * 保存lua脚本
     */
    private DefaultRedisScript<List> getRedisScript;
​
    @PostConstruct
    public void init(){
        // 定义lua脚本资源
        // 也可以放到文件中,加载进来: new ResourceScriptSource(new ClassPathResource("redis/demo.lua"))
        String luaStr = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";
        ByteArrayResource resource = new ByteArrayResource(luaStr.getBytes());
​
        getRedisScript = new DefaultRedisScript<>();
        getRedisScript.setResultType(List.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(resource));
    }
​
    /**
     * 释放锁
     *
     * @param lockKey 加锁的key
     * @param uuid 线程的标志
     */
    public void release(String lockKey, String uuid) {
        try {
            List<Integer> execute = stringRedisTemplate.execute(getRedisScript, Collections.singletonList(lockKey), uuid);
            log.debug("解锁结果: {}", execute.get(0) == 0);
        } catch (Exception e) {
            log.error("解锁异常, key: {}, uuid: {}", lockKey, uuid);
            log.error("", e);
        }
    }
​
}

使用

  @Autowired
private RedisLock redisLock;
​
public void useLock() {
    // 定义锁的key
    String lockKey = "camera_update_key";
    String uuid = UUIDUtils.get();
​
    // 定义超时时间
    long timeout = 5;
    TimeUnit timeUnit = TimeUnit.SECONDS;
​
    // 加锁
    boolean lock = redisLock.lock(lockKey, uuid, timeout, timeUnit);
    try {
        if (lock) {
            log.info("执行...");
        } else {
            throw new IllegalStateException("未获取到锁,放弃执行");
        }
    } finally {
        // 在finally里面进行解锁
        redisLock.release(lockKey, uuid);
    }
}

EmLock,分布式锁注解

package com.emdata.lowvis.common.redislock;
​
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
​
/**
 * 分布式锁注解
 *
 * @version 1.0
 * @date 2020/12/8 17:59
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface EmLock {
​
    /**
     * 锁的范围,默认应用级别
     * @return 锁的范围
     */
    LockRangeEnum lockRange() default LockRangeEnum.APPLICATION;
​
    /**
     * 锁对应的key
     * @return key
     */
    String key();
​
    /**
     * 锁超时时间
     * @return 时间
     */
    int timeout() default 5;
​
    /**
     * 锁超时时间粒度
     * @return 粒度
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
​
    /**
     * 是否自动释放锁
     * @return true: 方法完成后,自动释放
     */
    boolean autoRelease() default true;
}

使用

@Component
@Slf4j
public class ScheduleTask {
​
    /**
     * 用在定时任务方法上,锁的key为test_lock,指定了超时时间为2秒钟
     * 锁的级别为默认的应用级别(LockRangeEnum.APPLICATION),在这个如果应用启动了多个容器运行,在只会有一个容器获取到锁,
     * 自动释放锁为false,即方法执行完成后,也不会自动释放锁,只有到超时时间了,锁才会释放
     */
    @Scheduled(cron = "0 0/1 * * * ? ")
    @EmLock(key = "test_lock", timeout = 2, timeUnit = TimeUnit.SECONDS, autoRelease = false)
    public void recordUpdateTask() {
        log.info("执行任务.......");
    }
   
   /**
     * 用在普通的方法上,锁的key为method_Lock,指定了超时时间为1分钟,
     * 锁的级别为默认的线程级别,在该应用内多个线程执行该方法,则只会有一个线程获取到锁
     * 如果启动了多个应用容器,同样多个容器内的所有线程,也只会有一个线程获取到锁
     */
    @EmLock(key = "method_Lock", timeout = 1, timeUnit = TimeUnit.MINUTES, lockRange = LockRangeEnum.THREAD)
    public void recordUpdate() {
        log.info("执行任务2.......");
    }
}

LockRangeEnum, 分布式锁的范围枚举

package com.emdata.lowvis.common.redislock;
​
/**
 * 分布式锁的范围枚举
 *
 * @author pupengfei
 * @version 1.0
 * @date 2020/12/10 13:46
 */
public enum LockRangeEnum {
​
    /**
     * 应用级别,锁的级别在整个应用容器内
     */
    APPLICATION,
​
    /**
     * 线程级别,锁的级别在每个线程
     */
    THREAD
​
}

EmLockAspect,分布式锁切面

package com.emdata.lowvis.common.redislock;
​
import com.emdata.lowvis.common.utils.UUIDUtils;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
​
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
​
/**
 * 分布式锁切面
 *
 * @version 1.0
 * @date 2020/12/8 17:59
 */
@Slf4j
@Component
@Aspect
@Configuration
public class EmLockAspect {
​
    @Autowired
    private RedisLock redisLock;
​
    /**
     * 应用级别的容器的id
     */
    private final String appUUID = UUIDUtils.get();
​
    /**
     * 线程级别的线程的id
     */
    private final ThreadLocal<String> threadUUID = ThreadLocal.withInitial(UUIDUtils::get);
​
    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.emdata.lowvis.common.redislock.EmLock)")
    public void lockAop() {
​
    }
​
    @Around("lockAop()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        // 获取方法
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
​
        // 看有没有日志注解
        EmLock emLock = method.getAnnotation(EmLock.class);
        if (emLock == null) {
            return point.proceed();
        }
​
        // 获取锁的级别
        LockRangeEnum lockRangeEnum = emLock.lockRange();
        String uuid = lockRangeEnum == LockRangeEnum.APPLICATION ? appUUID : threadUUID.get();
​
        // 获取锁的key和超时时间
        String key = emLock.key();
        int timeout = emLock.timeout();
        TimeUnit timeUnit = emLock.timeUnit();
​
        // 加锁
        boolean lock = redisLock.lock(key, uuid, timeout, timeUnit);
​
        Object proceed = null;
​
        try {
            if (lock) {
                log.info("获取到锁,继续执行...");
                // 继续执行
                proceed = point.proceed();
            }
        } finally {
            // 自动释放,则释放锁
            if (emLock.autoRelease()) {
                redisLock.release(key, uuid);
            }
        }
​
        return proceed;
    }
​
}

注意

使用Redis作为分布式锁的实现,依赖于Redis服务,如果Redis服务无法正常访问,则会导致整个方法无法执行。 如果EmLock注解用在定时任务上时,如果应用运行在不同的服务器上,或者不同的docker容器里面时,必须保证运行环境的时间一致。 如果设置了定时任务上面的锁,不是自动释放的,则运行环境的时间,相差不大于锁超时时间的时候,也可以保证定时任务,唯一执行。因为在超时时间范围内,某个应用容器持有该锁,其他应用来获取锁时,同样获取不到,方法不会执行。