zoukankan      html  css  js  c++  java
  • Redis分布式锁

    概述

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

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

    代码实现

    首先我们要通过Maven引入Jedis开源组件:

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

    加锁代码

    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";
    
        /**
         * 尝试获取分布式锁
         */
        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()方法一共有五个参数:

    1. 第一个为key,我们使用key来当锁,因为key是唯一的;
    2. 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成;
    3. 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
    4. 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定;
    5. 第五个为time,与第四个参数相呼应,代表key的过期时间。

    总的来说,执行上面的set()方法就只会导致两种结果:

    • 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
    • 已有锁存在,不做任何操作。

    我们的加锁代码满足我们可靠性里描述的三个条件:

    首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。

    其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。

    最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。

    由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

    解锁代码

    先看代码:

    public class RedisTool {
    
        private static final Long RELEASE_SUCCESS = 1L;
        
        /**
         * 释放分布式锁
         */
        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语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

    错误示例1:最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

    public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
        jedis.del(lockKey);
    }

    错误示例2:这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

    public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时,这把锁突然不是这个客户端的,则会误解锁
            jedis.del(lockKey);
        }
    }

    其他问题

    上面说了许多,不过我们需要认识到一点:使用redis实现分布式锁(RedLock)并不是一个好的实践。主要问题在于RedLock的Client承担部分维护锁的责任,一旦客户端出现问题就容易出现锁冲突。

    如下图:

    01

    想了解更多可以参考这几篇文章:

  • 相关阅读:
    OutputCache 缓存key的创建 CreateOutputCachedItemKey
    Asp.net Web Api源码调试
    asp.net mvc源码分析DefaultModelBinder 自定义的普通数据类型的绑定和验证
    Asp.net web Api源码分析HttpParameterBinding
    Asp.net web Api源码分析HttpRequestMessage的创建
    asp.net mvc源码分析ActionResult篇 RazorView.RenderView
    Asp.Net MVC 项目预编译 View
    Asp.net Web.config文件读取路径你真的清楚吗?
    asp.net 动态创建TextBox控件 如何加载状态信息
    asp.net mvc源码分析BeginForm方法 和ClientValidationEnabled 属性
  • 原文地址:https://www.cnblogs.com/amunote/p/10415982.html
Copyright © 2011-2022 走看看