结合redisson实现自己的分布式锁

97 阅读1分钟
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;


/**
 * 较为基本的redis-lock
 * 不支持重入
 * 暂时不支持锁续命,锁续命可能导致死锁
 * uuid -> 视为客户端版本,只有正确的客户端才能解锁
 * 增加了lock,会持续获取,直到成功
 */
@Slf4j
@Service
public class RedisLock {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    private static final ScheduledExecutorService scheduledSinglePool =
            Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors() * 2 + 1);

    private static final String FIXED_TIME = "15";

    // 无法获取到锁时,尝试重试
    public void lock(String key, String uuid) {

        Long pttl = tryAcquire(key, uuid);
        // 如果能快速获得,则不用进入循环
        if (pttl == null) { 
            return;
        }

        while (true) {

            if (pttl == null || pttl <= 0) {
                startDaemon(key, uuid);
                break;
            }
            try {
                Thread.sleep(pttl);
            } catch (InterruptedException e) {
            }
        }
    }

    /**
     * @return 注意返回的是 毫秒数
     */
    public Long tryAcquire(String key, String uuid) {

        // 当前lua脚本中,加锁时间偏短, 15s
        String tryLockInner =
                "if ((redis.call('exists', KEYS[1]) == 0)) then " +
                        "redis.call('set', KEYS[1], ARGV[1]); " +
                        "redis.call('expire', KEYS[1], ARGV[2]); " +
                        "return nil; " +
                        "end; " +
                        "return redis.call('pttl', KEYS[1]);";

        return redisTemplate.execute((RedisCallback<Long>) connection ->
                connection.eval(tryLockInner.getBytes(), ReturnType.INTEGER, 1,
                        key.getBytes(StandardCharsets.UTF_8),
                        uuid.getBytes(StandardCharsets.UTF_8),
                        FIXED_TIME.getBytes(StandardCharsets.UTF_8)));
    }

    // 未获取到redis锁时,应当放弃任务
    public boolean tryLock(String key, String uuid) {
        Long pttl = tryAcquire(key, uuid);
        return pttl == null || pttl <= 0;
    }

    private void startDaemon(String key, String uuid) {
        Thread daemon = new Thread(new DaemonTask(key, uuid));
        daemon.setDaemon(true);
        daemon.start();
    }

    public void unlock(String key, String uuid) {
        try {
            // 释放锁使用 Lua 脚本,验证传入的 key--uuid 跟 Redis 中是否一样
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            redisTemplate.execute((RedisCallback<Boolean>) connection ->
                    connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
                            key.getBytes(StandardCharsets.UTF_8), uuid.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            log.error("解锁失败 key = {}", key);
        }
    }

    @AllArgsConstructor
    class DaemonTask implements Runnable {
        private String key;
        private String uuid;

        // 开启守护线程,可以续命锁
        // 但是如果时序不正确,会导致死锁
        @Override
        public void run() {

            // lua 脚本
            // 当前锁不存在,则是由当前客户端删除的,不应当继续续命
            // 锁的声明周期已经完全结束
            // 当前锁存在,不论ttl<0还是>0,都应当续命
            Long pttl = redisTemplate.getExpire(key);
        	// 删除锁时,应当直接结束任务
            if (pttl == null) {
                return;
            }
        	
            Long pttl = tryAcquire(key, uuid);
        	
            // 使用延时任务,延续时间
            if (pttl >= 0) {
                scheduledSinglePool.schedule(this, pttl, TimeUnit.MICROSECONDS);    
            }
        }
    }

}

代码是完成了,应当做哪些测试来确保安全哪?

修改版本

todo 修改续命锁逻辑

// 追加 lua 脚本

// 当前锁不存在,则是由当前客户端删除的,不应当继续续命

// 锁的声明周期已经完全结束

// 当前锁存在,则应当续命