Redis分布式锁

Redis分布式锁

介绍

随着微服务的兴起,比如Spring Cloud、Dubbo的等分布式解决方案地兴起,java的jvm锁某些场景下已经不在适用。特定的场景下,往往依靠分布式锁,常见的分布式锁实现方式有Redis分布式锁以及Zookeeper分布式锁。Redis分布式锁在项目编码中更为常见,比较成熟的框架有Redisson,就实现了分布式锁。虽然项目中我们可以使用比较成熟的Redisson,但是这篇文章侧重于自己实现一个分布式锁。

原理

Redis可以作为分布式锁的关键因素有2个:1.单线程模型;2.基于内存高性能。单线程模型意味着,在任意时刻,只有一个线程可以持有并操作一个key。如果我们实现以个分布式锁,当A线程持有锁时,其他任何一个线程不能再获取同样的锁。因此,加锁的操作必须是一个原子操作。

SETNX命令

SETNX是redis的一个命令,也是一个原子操作命令,当redis中key不存在时,使用SETNX命令给key设置一个value,设值成功时返回1,若key存在,则设值失败,返回0。完美契合了我们加锁的条件,原子操作、加锁成功返回true、加锁失败返回false。命令执行如下:
image.png

问题

java jvm锁中提供tryLock(long** **time, TimeUnit unit)方法,当程序试图加锁时,可以设置持有锁的时间,超过持锁时间后,即使没有手动释放锁,锁也可以被释放掉,可以有效防止死锁。redis分布式锁如何实现这个功能呢?我们想到了EXPIRE,当SETNX获取锁成功后,在使用EXPIRE设置锁的过期时间。但是,SETNX、EXPIRE两个命令顺序执行就不是原子操作了。redis提供了lua脚本的方式,可以在脚本中执行redis命令,执行lua脚本是原子操作,因此我们可以在lua脚本中SETNX,再EXPIRE。

代码

# SETNX_EXPIRE.lua
if (redis.call('setnx', KEYS[1], ARGV[1]) == 1)
then
    redis.call('expire', KEYS[1], ARGV[2])
    return true
else
    return false
end
# COMPARE_DELETE.lua
if (redis.call('get', KEYS[1]) == ARGV[1])
then
    return redis.call('del', KEYS[1]) == 1
end
return false
package com.github.rxyor.distributed.lock;

import com.github.rxyor.common.util.FileUtil;
import com.github.rxyor.common.util.RandomUtil;
import com.github.rxyor.redis.util.LettuceConnectionUtil;
import io.lettuce.core.RedisClient;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import java.util.Objects;
import java.util.Optional;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;

/**
 *<p>
 *Redis 分布式锁
 *</p>
 *
 * @author liuyang
 * @date 2019-05-14 Tue 18:51:00
 * @since 1.0.0
 */
public class RedisDistributedLock implements DistributedLock {

    /**
     * taskId本地线程缓存
     */
    private static final ThreadLocal<String> LOCAL_TASK_IDS = new ThreadLocal<>();
    /**
     * redis key本地线程缓存
     */
    private static final ThreadLocal<String> LOCAL_KEYS = new ThreadLocal<>();
    /**
     * 默认锁失效时间
     */
    private static final long DEFAULT_TIMEOUT = 5L;
    /**
     * 加锁LUA脚本
     */
    private static final String LUA_LOCK_SCRIPT;
    /**
     * 释放锁LUA脚本
     */
    private static final String LUA_UNLOCK_SCRIPT;

    static {
        LUA_LOCK_SCRIPT = readLuaLockScript();
        LUA_UNLOCK_SCRIPT = readLuaUnlockScript();
    }

    /**
     * redis 客户端
     */
    private RedisClient redisClient;

    /**
     * redis key前缀
     */
    @Getter
    @Setter
    private String keyPrefix;

    public RedisDistributedLock(RedisClient redisClient) {
        Objects.requireNonNull(redisClient, "redisClient can't be null");
        this.redisClient = redisClient;
    }

    /**
     * 获取锁
     *
     * @param redisKey
     * @param timeout 超时时间(秒)
     * @return 获取锁结果
     */
    @Override
    public boolean getLock(String redisKey, Long timeout) {
        long expire = (timeout == null || timeout <= 0L ? DEFAULT_TIMEOUT : timeout);
        String redisKeyWithPrefix = this.gainRedisKey(redisKey);
        LOCAL_KEYS.set(redisKeyWithPrefix);
        String taskId = this.generateTaskId();
        LOCAL_TASK_IDS.set(taskId);

        return evalRedisScript(LUA_LOCK_SCRIPT, new String[]{redisKeyWithPrefix},
            new String[]{taskId, String.valueOf(expire)});
    }

    /**
     * 释放锁
     *
     * @param redisKey
     * @return 释放结果
     */
    @Override
    public boolean releaseLock(String redisKey) {
        String redisKeyWithPrefix = this.gainRedisKey(redisKey);
        boolean success = evalRedisScript(LUA_UNLOCK_SCRIPT, new String[]{redisKeyWithPrefix},
            LOCAL_TASK_IDS.get());
        if (success) {
            this.clean();
        }
        return success;
    }


    @Override
    public boolean releaseLock() {
        String redisKeyWithPrefix = this.gainRedisKey(null);
        boolean success = evalRedisScript(LUA_UNLOCK_SCRIPT, new String[]{redisKeyWithPrefix},
            LOCAL_TASK_IDS.get());
        if (success) {
            this.clean();
        }
        return success;
    }

    /**
     * 执行lua脚本
     *
     * @param script 脚本
     * @param keys redis key
     * @param values redis参数
     * @return 执行结果
     */
    private boolean evalRedisScript(String script, String[] keys, String... values) {
        StatefulRedisConnection<String, String> conn = null;
        try {
            conn = LettuceConnectionUtil.getConnection(redisClient);
            RedisCommands<String, String> commands = conn.sync();
            return commands.<Boolean>eval(script, ScriptOutputType.BOOLEAN, keys, values);
        } finally {
            LettuceConnectionUtil.releaseConnection(conn);
        }
    }

    /**
     * 拼装或从缓存中取redis key
     *
     * @param key 原始redis key
     * @return String
     */
    private String gainRedisKey(String key) {
        if (StringUtils.isNotEmpty(key)) {
            String redisKeyPrefix = Optional.ofNullable(keyPrefix).orElse("");
            return redisKeyPrefix + key;
        }
        return Optional.ofNullable(LOCAL_KEYS.get()).filter(s -> StringUtils.isNotEmpty(s))
            .orElseThrow(() -> new IllegalArgumentException("redis key can't be empty"));
    }

    /**
     * 生成一个唯一的TaskId(UUID)
     * @return UUID
     */
    private String generateTaskId() {
        return RandomUtil.createUuid();
    }

    /**
     * 清空ThreadLocal缓存
     */
    private void clean() {
        LOCAL_KEYS.remove();
        LOCAL_TASK_IDS.remove();
    }

    /**
     * 读取加锁LUA脚本
     * @return String
     */
    private static String readLuaLockScript() {
        return FileUtil.readTextFromResource(RedisDistributedLock.class, "/lua/SETNX_EXPIRE.lua");
    }

    /**
     * 读取释放锁LUA脚本
     * @return String
     */
    private static String readLuaUnlockScript() {
        return FileUtil.readTextFromResource(RedisDistributedLock.class, "/lua/COMPARE_DELETE.lua");
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值