冷饭新炒:理解Redisson中分布式锁的实现

在很早很早之前,写过一篇文章介绍过Redis中的red lock的实现,但是在生产环境中,笔者所负责的项目使用的分布式锁组件一直是Redisson。Redisson是具备多种内存数据网格特性的基于Java编写的Redis客户端框架(Redis Java Client with features of In-Memory Data Grid),基于Redis的基本数据类型扩展出很多种实现的高级数据结构,具体见其官方的简介图:

冷饭新炒:理解Redisson中分布式锁的实现

本文要分析的R(ed)Lock实现,只是其中一个很小的模块,其他高级特性可以按需选用。下面会从基本原理、源码分析和基于Jedis仿实现等内容进行展开。本文分析的Redisson源码是2020-01左右Redisson项目的main分支源码,对应版本是3.14.1。

基本原理

red lock的基本原理其实就"光明正大地"展示在Redis官网的首页文档中(具体链接是https://redis.io/topics/distlock):

冷饭新炒:理解Redisson中分布式锁的实现

摘录一下简介进行翻译:在许多环境中不同进程必须以互斥方式使用共享资源进行操作时,分布式锁是一个非常有用的原语。此试图提供一种更规范的算法来实现Redis的分布式锁。我们提出了一种称为Redlock的算法,它实现了DLM(猜测是Distributed Lock Manager的缩写,分布式锁管理器),我们认为它比普通的单实例方法更安全。

算法的三个核心特征(三大最低保证):

Safety property(安全性):互斥。确保在任何给定时刻下,只有一个客户端可以持有锁

Liveness property A(活性A):无死锁。即使存在曾经锁定资源的客户端崩溃或者出现网络分区异常,确保锁总是能够成功获取

Liveness property B(活性B):容错性。只要大多数Redis节点处于正常运行状态,客户端就可以获取和释放锁

文档中还指出了目前算法对于故障转移的实现还存在明显的竞态条件问题(描述的应该是Redis主从架构下的问题):

客户端A获取Redis主节点中的锁(假设锁定的资源为X)

在Redis主节点把KEY同步到Redis从节点之前,Redis主节点崩溃

Redis从节点因为故障晋升为主节点

此时,客户端B获取资源X的锁成功,问题是资源X的锁在前面已经被客户端A获取过,这样就出现了并发问题

算法的实现很简单,单个Redis实例下加锁命令如下:

SET $resource_name $random_value NX PX $ttl

这里的Nx和PX是SET命令的增强参数,自从Redis的2.6.12版本起,SET命令已经提供了可选的复合操作符:

EX:设置超时时间,单位是秒

PX:设置超时时间,单位是毫秒

NX:IF NOT EXIST的缩写,只有KEY不存在的前提下才会设置K-V,设置成功返回1,否则返回0

XX:IF EXIST的缩写,只有在KEY存在的前提下才会设置K-V,设置成功返回1,否则返回0

单个Redis实例下解锁命令如下:

# KEYS[1] = $resource_name # ARGV[1] = $random_value if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end 使用Redisson中的RLock

使用RLock要先实例化Redisson,Redisson已经适配了Redis的哨兵、集群、普通主从和单机模式,因为笔者本地只安装了单机Redis,所以这里使用单机模式配置进行演示。实例化RedissonClient:

static RedissonClient REDISSON; @BeforeClass public static void beforeClass() throws Exception { Config config = new Config(); // 单机 config.useSingleServer() .setTimeout(10000) .setAddress("redis://127.0.0.1:6379"); REDISSON = Redisson.create(config); // // 主从 // config.useMasterSlaveServers() // .setMasterAddress("主节点连接地址") // .setSlaveAddresses(Sets.newHashSet("从节点连接地址")); // REDISSON = Redisson.create(config); // // 哨兵 // config.useSentinelServers() // .setMasterName("Master名称") // .addSentinelAddress(new String[]{"哨兵连接地址"}); // REDISSON = Redisson.create(config); // // 集群 // config.useClusterServers() // .addNodeAddress(new String[]{"集群节点连接地址"}); // REDISSON = Redisson.create(config); }

加锁和解锁:

@Test public void testLockAndUnLock() throws Exception { String resourceName = "resource:x"; RLock lock = REDISSON.getLock(resourceName); Thread threadA = new Thread(() -> { try { lock.lock(); process(resourceName); } finally { lock.unlock(); System.out.println(String.format("线程%s释放资源%s的锁", Thread.currentThread().getName(), resourceName)); } }, "threadA"); Thread threadB = new Thread(() -> { try { lock.lock(); process(resourceName); } finally { lock.unlock(); System.out.println(String.format("线程%s释放资源%s的锁", Thread.currentThread().getName(), resourceName)); } }, "threadB"); threadA.start(); threadB.start(); Thread.sleep(Long.MAX_VALUE); } private void process(String resourceName) { String threadName = Thread.currentThread().getName(); System.out.println(String.format("线程%s获取到资源%s的锁", threadName, resourceName)); try { Thread.sleep(1000); } catch (InterruptedException ignore) { } } // 某次执行的输出结果 线程threadB获取到资源resource:x的锁 线程threadB释放资源resource:x的锁 线程threadA获取到资源resource:x的锁 线程threadA释放资源resource:x的锁

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpzsws.html