Redis实现分布式锁(利用分布式锁,实现分布式定时任务)

2020-12-10  本文已影响0人  Knight_9

简述

利用Redis的Setnx命令,来实现一个分布式的加锁方案。利用注解,在拥有该注解的方法上,进行切面处理,在方法执行前,进行加锁,执行结束后,根据是否自动释放锁,进行解锁。
将该注解用在定时任务的方法上,即可实现分布式定时任务,即获取到锁的方法,才会执行。

1 redis命令

1.1 setnx命令

# 事务正常使用
127.0.0.1:6379> multi
127.0.0.1:6379> set name jack
127.0.0.1:6379> exec

# 取消事务
127.0.0.1:6379> multi
127.0.0.1:6379> set name jack
127.0.0.1:6379> discard

# watch使用
# number初始为10
127.0.0.1:6379> watch number
127.0.0.1:6379> multi
127.0.0.1:6379> set number 11
127.0.0.1:6379> exec
# 如果在执行exec时,number没有被其他客户端修改,还是10,则事务执行成功;
# 如果被其他客户端修改了,number不是10了,则事务执行失败,这时候就需求程序自行处理,进行再次提交或者其他操作

// org.springframework.data.redis.core 中实现的方法
@Override
public Boolean setIfAbsent(K key, V value) {

    byte[] rawKey = rawKey(key);
    byte[] rawValue = rawValue(value);
    return execute(connection -> connection.setNX(rawKey, rawValue), true);
}

@Override
public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {

    byte[] rawKey = rawKey(key);
    byte[] rawValue = rawValue(value);

    Expiration expiration = Expiration.from(timeout, unit);
    return execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.ifAbsent()), true);
    }

1.2 DEL命令、lua脚本

在加锁之后,解锁时,需要判断锁,是否是当前线程所拥有的,如果是当前线程拥有的,则删除该key,删除key,用del命令。

我们会先取出key对应的值,然后判断是否和当前线程的定义的值一致。如果一致,则说明是该线程拥有的key。如果我们在代码中取出key的值,然后判断通过后,调用redis del 删除key,这就不是一个原子操作了。如果在我们取出key的值后,然后在删除前,其他线程获取了锁,当前线程删除的动作,就会导致删除其他线程拥有的锁。所以释放锁,需要利用lua脚本进行,将判断和删除,这两个动作,合为一个原子性的操作。
所以我们会利用代码去执行下面的lua脚本,保证判断和删除的原子性。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

一般教程中,利用RedisTemplate来执行lua脚本时,会将lua脚本放到静态资源目录中。而在下面的代码中,利用ByteArrayResource直接从String字符串中读取了lua脚本内容:

    /*
     * 保存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));
    }

2 分布式锁实现

下面是实现的核心类:

2.1 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);
        }
    }

}

2.2 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;
}

2.3 LockRangeEnum, 分布式锁的范围枚举

package com.emdata.lowvis.common.redislock;

/**
 * 分布式锁的范围枚举
 *
 * @author pupengfei
 * @version 1.0
 * @date 2020/12/10 13:46
 */
public enum LockRangeEnum {

    /**
     * 应用级别,锁的级别在整个应用容器内
     */
    APPLICATION,

    /**
     * 线程级别,锁的级别在每个线程
     */
    THREAD

}

2.4 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;
    }

}

3 使用示例

3.1 使用RedisLock

      @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);
        }
    }

3.2 使用EmLock

@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.......");
    }
}

4 使用注意

上一篇 下一篇

猜你喜欢

热点阅读