精华内容
下载资源
问答
  • Redis分布式锁

    万次阅读 2020-03-24 01:02:25
    Redis分布式锁 介绍 随着微服务的兴起,比如Spring Cloud、Dubbo的等分布式解决方案地兴起,java的jvm锁某些场景下已经不在适用。特定的场景下,往往依靠分布式锁,常见的分布式锁实现方式有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");
        }
    }
    
    
    展开全文
  • redis分布式锁

    万次阅读 2020-08-13 18:08:53
    2.redis分布式锁 3.zookeeper的分布式锁,zookeeper机制规定,同一个目录下只能有一个唯一的文件名, 借助zookeeper的临时节点实现 二、Redis分布式锁实现 1. 使用jedis的2.7.x及以上版本。 2. 获取锁: 命令:SET...

    一、分布式锁实现方式

    1.数据库单独建表维护,使用的是数据库乐观锁
    2.redis的分布式锁
    3.zookeeper的分布式锁,zookeeper机制规定,同一个目录下只能有一个唯一的文件名, 借助zookeeper的临时节点实现

    二、Redis的分布式锁实现

    1. 使用jedis的2.7.x及以上版本。
    2. 获取锁:

     命令:SET key value [NX|XX] [EX|PX] seconds
    
            NX – 只有键key不存在的时候才会设置key的值
    
            XX – 只有键key存在的时候才会设置key的值
    
            EX seconds – 设置键key的过期时间,单位时秒
    
            PX milliseconds – 设置键key的过期时间,单位时毫秒
    
    

    在这里插入图片描述
    获取锁方式:操作成功,rlt返回“OK”,否则返回null
    3.释放锁:Redis+Lua实现:
    在这里插入图片描述
    Redis Lua脚本来释放锁

    三、注意点

    1.首先,这个锁必须要设置一个过期时间,否则的话,当一个客户端获取锁成功之后,假如它奔溃了,或者由于发生了网络分割导致它再也无法和redis节点通信了,那么它就会一直持有这个锁,而其他客户端永远也无法获得这个锁了
    2.第二,第一步获取锁的操作,把它实现成了两个redis命令,SETNX+EXPIRE.虽然这两个命令和前面描述中的一个SET命令执行效果相同,但SETNX+EXPIRE却不是原子的.如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE,导致它一直持有这个锁,所以最好使用SET命令,因为SET命令是原子的
    3.第三,设置一个随机伺服穿randomVal是很有必要的,它保证了一个客户端释放的锁必须是自己持有的哪个锁(最好是唯一性UUID)
    4.第四,释放锁的操作必须使用Lua脚本来实现,释放锁其实包含三步操作:GET,判断和DEL,用Lua脚本来实现能保证这三步的原子性(获取锁也可通过Redis+Lua实现)

    四、思考

    还要一个问题是由failover(主备切换)引起的,基于单redis节点的分布式锁无法解决的,假如redis节点宕机了,那么所有客户端就都无法获得锁了,服务变的不可用,为了提高可用性,我们可以给这个redis节点挂一个slave,当master节点不可用的时候,系统自动切到slave上(failover),但是由于redis的主从复制(replication)是异步的,这可能导致在failover过程中丧失锁的安全性,针对这个问题,可以参看一下Redlock的算法https://redis.io/topics/distlock)

    展开全文
  • Redis 分布式锁

    2020-08-18 11:16:00
    Redis 分布式锁Redis 分布式锁1. 是什么?底层源码: Redis 分布式锁 1. 是什么? 为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而 这个分布式协调技术的核心...

    Redis 分布式锁

    1. 是什么?

    为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而
    这个分布式协调技术的核心就是来实现这个分布式锁。 redisson框架 @RedisLock 注解 加在业务接口上

    底层源码:

    在这里插入图片描述
    在这里插入图片描述

    KEYS[1]代表的是你加锁的那个key,比如说: RLock lock = redisson.getLock(“myLock”);
    这里你自己设置了加锁的那个锁key就是“myLock”。

    ARGV[1]代表的就是锁key的默认生存时间,默认30秒。 ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:
    8743c9c0-0795-4907-87fd-6c719a6b4586:1

    第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。
    如何加锁呢?很简单,用下面的命令: hset myLock
    8743c9c0-0795-4907-87fd-6c719a6b4586:1 1 通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
    上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。
    接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
    好了,到此为止,ok,加锁完成了。

    2)锁互斥机制

    那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢? 很简单,第一个if判断会执行“exists
    myLock”,发现myLock这个锁key已经存在了。
    接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
    所以,客户端2会获取到pttl
    myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。
    此时客户端2会进入一个while循环,不停的尝试加锁。

    (3)watch dog自动延期机制

    客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
    简单!只要客户端1一旦加锁成功,就会启动一个watch
    dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

    展开全文
  • redis 分布式锁

    2019-04-24 11:02:24
    虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。 可靠性 首先,为了确保分布式锁可用,我们至少要...

    分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。

     

    可靠性

    首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

    1. 互斥性。在任意时刻,只有一个客户端能持有锁。
    2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
    3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
    4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

    代码实现

    组件依赖

    首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>

    加锁代码

    正确姿势

    Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

    public class RedisTool {
        private static final String LOCK_SUCCESS = "OK";
        private static final String SET_IF_NOT_EXIST = "NX";
        private static final String SET_WITH_EXPIRE_TIME = "PX";
    
        /**
        * 尝试获取分布式锁
        * @param jedis Redis客户端
        * @param lockKey 锁
        * @param requestId 请求标识
        * @param expireTime 超期时间
        * @return 是否获取成功
        */
        public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    }

    可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

    • 第一个为key,我们使用key来当锁,因为key是唯一的。

    • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

    • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

    • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

    • 第五个为time,与第四个参数相呼应,代表key的过期时间。

    总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

    心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

    解锁代码

    正确姿势

    还是先展示代码,再带大家慢慢解释为什么这样实现:

    public class RedisTool {
        private static final Long RELEASE_SUCCESS = 1L;
    
        /**
        * 释放分布式锁
        * @param jedis Redis客户端
        * @param lockKey 锁
        * @param requestId 请求标识
        * @return 是否释放成功
        */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    }

    可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

    那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

    简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

    本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。

    如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件,具体请参考我的另一篇博客基于Spring boot 2.1 使用redisson实现分布式锁

    展开全文
  • redis分布式锁原理及实现

    万次阅读 多人点赞 2019-04-16 23:13:29
    一、写在前面 现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud...说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 14,177
精华内容 5,670
关键字:

redis分布式锁

redis 订阅