关于redis:redis分布式锁setnxlua脚本的java实现-京东物流技术团队

51次阅读

共计 4844 个字符,预计需要花费 13 分钟才能阅读完成。

1 前言

在当初工作中,为保障服务的高可用,应答单点故障、负载量过大等单机部署带来的问题,生产环境罕用多机部署。为解决多机房部署导致的数据不统一问题,咱们常会抉择用分布式锁。

目前其余比拟常见的实现计划我列举在上面:

  1. 基于缓存实现分布式锁(本文次要应用 redis 实现)
  2. 基于数据库实现分布式锁
  3. 基于 zookeeper 实现分布式锁

本文是基于 redis 缓存实现分布式锁,其中应用了 setnx 命令加锁,expire 命令设置过期工夫并 lua 脚本保障事务一致性。Java 实现局部基于 JIMDB 提供的接口。JIMDB 是京东自主研发的基于 Redis 的分布式缓存与高速键值存储服务。

2 SETNX

根本语法:SETNX KEY VALUE

SETNX 是示意 SET ifNot eXists,即命令在指定的 key 不存在时,为 key 设置指定的值。

KEY 是示意待设置的 key 名

VALUE是设置 key 的对应值

若设置胜利,则返回 1;若设置失败(key 存在),则返回 0。

由此,咱们会抉择用 SETNX 来进行分布式锁的实现,当 Key 存在时,会返回加锁失败的信息。

SET 与 SETNX 区别:

SET 如果 key 曾经存在,则会笼罩原值,且忽视类型

SETNX 如果 key 曾经存在,则会返回 0,示意设置 key 失败

Redis 2.6.12 版本前后比照:

2.6.12 版本前:分布式锁并不能只用 SETNX 实现,须要搭配 EXPIRE 命令设置过期工夫,否则,key 将永远无效。其中,为保障 SETNX 和 EXPIRE 在同一个事务里,咱们须要借助 LUA 脚本来实现事务实现。(因为在写这篇文章时,JIMDB 还未反对 **SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]** 语法,故本文仍然用 lua 事务)

2.6.12 版本后:SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL] 语法糖可用于分布式锁并反对原子操作,无需 EXPIRE 命令设置过期工夫。

3 LUA 脚本

什么是 LUA 脚本?

Lua 是一种轻量玲珑的脚本语言, 用规范 C 语言编写并以源代码模式凋谢, 其设计目标是为了嵌入应用程序种, 从而为程序提供灵便的扩大和定制性能。

为什么须要用到 LUA 脚本?

本文的锁实现是基于两个 Redis 命令 – SETNXEXPIRE。为保障命令的原子性,咱们将这两个命令写入 LUA 脚本,并上传至 Redis 服务器。Redis 服务器会单线程执行 LUA 脚本,以确保两个命令在执行期间不被其余申请打断。

LUA 脚本的劣势

  • 缩小网络开销。若干命令的屡次申请,可组合成一个脚本进行一次申请
  • 高复用性。脚本编辑一次后,雷同代码逻辑可多处应用,只需将不同的参数传入即可。
  • 原子性。若期望多个命令执行期间不被其余申请打断,或呈现竞争状态,能够用 LUA 脚本实现,同时保障了事务的一致性。

分布式锁 LUA 脚本的实现

假如在同一时刻只能创立一个订单,咱们能够将 orderId 作为 key 值,uuid作为 value 值。过期工夫设置为 3 秒。

LUA 脚本如下,通过 Redis 的 eval/evalsha 命令实现:

-- lua 加锁脚本
-- KEYS[1],ARGV[1],ARGV[2]别离对应了 orderId,uuid,3
-- 如果 setnx 胜利,则持续 expire 命令逻辑
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 
    then 
      -- 则给同一个 key 设置过期工夫
       redis.call('expire',KEYS[1],ARGV[2]) 
       return 1 
    else 
      -- 如果 setnx 失败,则返回 0
       return 0 
end


-- lua 解锁脚本
-- KEYS[1],ARGV[1]别离对应了 orderId,uuid
-- 若无奈获取 orderId 缓存,则认为曾经解锁
if redis.call('get',KEYS[1]) == false 
    then 
        return 1 
    -- 若获取到 orderId,并 value 值对应了 uuid,则执行删除命令
    elseif redis.call('get',KEYS[1]) == ARGV[1] 
    then 
        -- 删除缓存中的 key
        return redis.call('del',KEYS[1]) 
    else 
        -- 若获取到 orderId,且 value 值与存入时不统一,则返回非凡值,不便进行后续逻辑
        return 2 
end


【注】依据 Redis 的版本,在 LUA 脚本中,当应用 redis.call(‘get’,key)断定缓存 key 不存在时,须要留神对比值为布尔类型的 false,还是 null。

依据 官网文档:Lua Boolean -> RESP3 Boolean reply (note that this is a change compared to the RESP2, in which returning a Boolean Lua true returned the number 1 to the Redis client, and returning a false used to return a null .

在 RESP3 中,redis cli 返回的是空值时,lua 会用布尔类型 false 来代替。

RESP3 简介

RESP3 是 Redis6 的新个性,是 RESP v2 的新版本。该协定用于客户端和服务器之间的申请响应通信。因为该协定能够不对称的应用,即客户端发送一个简略的申请,服务器能够将更简单的并裁减后的相干信息返回到客户端。降级后的协定,引入了 13 种数据类型,使之更实用于数据库的交互场景。

4 基于 JIMDB 的 Java 分布式锁实现

调用类实现代码

SoRedisLock soJimLock = null;
try{soJimLock = new SoRedisLock("orderId", jimClient);
    if (!soJimLock.lock(3)) {log.error("订单创立加锁失败");
        throw new BPLException("订单创立加锁失败");
    }
} catch(Exception e) {throw e;} finally {if (null != soJimLock) {soJimLock.unlock();
    }
}


分布式锁实现类代码

public class SoRedisLock{

    /** 加锁标记 */
    public static final String LOCKED = "TRUE";
    /** 锁的关键词 */
    private String key;
    private Cluster jimClient;
    
    /**
     * lock 的构造函数
     * 
     * @param key
     *            key+"_lock" (key 应用惟一的业务单号)
     * @param
     *
     */
    public SoRedisLock(String key, Cluster jimClient)
    {
        this.key = key + "_LOCK";
        this.jimClient = jimClient;
    }
    
    /**
     * 加锁
     *
     * @param expire
     *            锁的持续时间(秒),过期删除
     * @return 胜利或失败标记
     */
    public boolean lock(int expire)
    {
        try
        {log.info("分布式事务加锁,key:{}", this.key);   
            String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
                    "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
            String sha = jimClient.scriptLoad(lua_scripts);
            List<String> keys = new ArrayList<>();
            List<String> values = new ArrayList<>();
            keys.add(this.key);
            values.add(LOCKED);
            values.add(String.valueOf(expire));
            this.locked = jimClient.evalsha(sha, keys, values, false).equals(1L);
            return this.locked;
        } catch (Exception e){throw new RuntimeException("Locking error", e);
        }
    }

    /**
     * 解锁 无论是否加锁胜利,都须要调用 unlock 倡议放在 finally 办法块中
     */
    public void unlock()
    {if (this.jimClient == null || !this.locked) {return ;}
        try {String luaScript = "if redis.call('get',KEYS[1]) == false then return 1" +
                "elseif redis.call('get',KEYS[1]) == ARGV[1] then" +
                "return redis.call('del',KEYS[1]) else return 2 end";
        String sha = jimClient.scriptLoad(luaScript);
        if(!jimClient.evalsha(sha, Collections.singletonList(this.key), Collections.singletonList(LOCKED), false).equals(1L)){throw new RuntimeException("解锁失败,key:"+this.key);
        }
        } catch (Exception e) {log.error("unLocking error, key:{}", this.key, e);
            throw new RuntimeException("unLocking error, key:"+this.key);
        }
    }
}


因为咱们只是应用 key-value 做一个加锁动作,value 并无意义。故,本文 key 对应的 value 给定固定值。Jimdb 提供了上传脚本的 API,咱们通过 scriptLoad()办法将 lua 脚本上传至 redis 服务器中。并利用 evalsha()办法来进行脚本的执行。evalsha()返回值即为脚本中的设置的 return 的返回值。

咱们通过 list 将参数传入脚本中,并对应脚本中的标记位。例如上方的代码中:

orderId_LOCK”对应了脚本中的KEYS[1]

TRUE”对应了脚本中的ARGV[1]

3”对应了脚本中的ARGV[2]

【注】若在一个脚本中存在多个 key,须要确保 redis 中的 hashtag 被启用,以防分片导致的 key 不处于同一分片,进而呈现“Only support single key or use same hashTag”异样。当然,hashtag 启用须要审慎,否则分片不均导致流量的集中,造成服务器压力过大。

理论应用中的日志截图

5 总结

通过上述介绍咱们理解到如何保障 Redis 多个命令的原子性。当然,Redis 事务一致性,也能够抉择 Redis 的事务(Transaction)操作来实现。Jimdb 也有 API 反对事务的 multi,discard,exec,watch 和 unwatch 命令。本文之所以抉择应用 LUA 脚本来进行实现,次要是思考到目前 Jimdb 在执行事务时,流量只会打到主实例,多实例的负载平衡会生效。更多的可行计划期待大家的摸索,咱们下个文档见。

6 参考资料

Redis 分布式锁:https://www.cnblogs.com/niceyoo/p/13711149.html

Redis 中应用 Lua 脚本:https://zhuanlan.zhihu.com/p/77484377

Redis Eval 命令:https://www.redis.net.cn/order/3643.html

LUA API:https://redis.io/docs/interact/programmability/lua-api/

作者:京东物流 牟佳义

起源:京东云开发者社区 自猿其说 Tech 转载请注明起源

正文完
 0