Redis的复合SET命令和简易的分布式锁优化

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 最近在跟进一个比较老的系统的时候,发现了所有调度任务使用了spring-context里面的@Scheduled注解和自行基于Redis封装的简易分布式锁控制任务不并发执行。为了不引入其他框架的情况下做一些简单优化,笔者花点时间去研读了一下Redis的SET命令的相关文档。

前提



最近在跟进一个比较老的系统的时候,发现了所有调度任务使用了spring-context里面的@Scheduled注解和自行基于Redis封装的简易分布式锁控制任务不并发执行。为了不引入其他框架的情况下做一些简单优化,笔者花点时间去研读了一下RedisSET命令的相关文档。


场景还原



使用@Scheduled注解实现定时任务,使用spring-data-redis提供的API实现简易的Redis分布式锁的伪代码如下:


// 每30分钟跑一次
@Scheduled(cron = "* */30 * * * ? ")
public void scheduledMethod(){
    // 判断KEY存在性并且设置KEY,带超时时间5分钟
    if (StringRedisTemplate#opsForValue()#hasKey("定时任务唯一字符串标识")){
        StringRedisTemplate#opsForValue()#set("定时任务唯一字符串标识", "1[这里暂时可以使用任何值]", 5 , TimeUnit.MINUTES);
    }
    // 这里做调度正常业务逻辑
    doBusiness();
    // 删除KEY
    StringRedisTemplate#opsForValue()#delete("定时任务唯一字符串标识");
}
复制代码


上面的代码存在如下显然的缺陷:

  1. 如果应用部署多个节点,由于判断KEY的存在性和SET操作是两个操作(非原子操作),该定时任务有可能在同一个时刻并发执行多次。
  2. 如果业务逻辑执行方法doBusiness()抛出了异常,会导致删除KEY的操作无法执行,KEY会到达超时时间后被删除,这个时候相当于加锁时间长达5分钟,显然是无法接受的。


但是实际上,以上两个问题在生产环境中并没有出现过,分析一下具体原因是:

  • 对于第1点,该应用在生产环境只部署了2个节点,节点的重启时间并不相同,所以从天然上避免了重复执行的问题,如果CRON表达式设计为0 */30 * * * ?(0秒开始每30分钟执行一次)就有可能出现并发问题。
  • 对于第2点,开发者在处理业务方法里面全局捕获异常并且没有外抛,所以调度方法总是会执行到删除KEY的逻辑。


以上仅仅是巧合的情况下规避了问题出现的因子,但是从编码规范的角度来看显然是存在问题。基于Redis实现的分布式锁的方案在Redis官方文档中有一篇文章做了详细的分析-Distributed locks with Redis,对于Java语言来说,有现成的类库Redisson提供对应的实现。但是在解决这个问题的时候,为了简易起见并没有引入Redisson,而是想办法通过原来的SETDEL两个操作的相关思路进行优化。


SET复合命令



自从Redis2.6.12版本起,SET命令已经提供了可选的复合操作符:


SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
复制代码


  • 时间复杂度:O(1)


可选参数:

  • EX:设置超时时间,单位是秒。
  • PX:设置超时时间,单位是毫秒。
  • NXIF NOT EXIST的缩写,只有KEY不存在的前提下才会设置值。
  • XXIF EXIST的缩写,只有在KEY存在的前提下才会设置值。


列举一些等价的命令:


原始命令 等价命令
SETEX KEY_1 1 SET KEY_1 EX 1
SETNX KEY_1 SET KEY_1 NX
SETNX KEY_1 && EXPIRE KEY_1 1 SET KEY_1 EX 1 NX
SETNX KEY_1 && PEXPIRE KEY_1 1000 SET KEY_1 PX 1000 NX


对比一下,发现SET复合命令十分简便,可以把两个命令合并成一个原子命令。不过注意一下,spring-data-redis里面的封装做得不太好,ValueOperations并没有提供相关的方法,因此最好还是使用Redis的Java客户端Jedis


简易的分布式锁实现



其实官方文档里面已经有很详细的Redis分布式锁方案(尽管这个方案在某些论文里面被热烈讨论它存在的问题,但是生产中它已经被广泛使用),获取锁的伪代码如下:


SET RESOURCE_NAME RANDOM_VALUE NX PX 30000
复制代码


释放锁的伪代码(Lua脚本)如下:


if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
复制代码


改造前文中提及到的例子:


// 每30分钟跑一次
@Scheduled(cron = "* */30 * * * ? ")
public void scheduledMethod(){
    try (Jedis jedis = getJedis()){
        SetParams params = new SetParams().ex(300).nx();
        String code = jedis.set("定时任务唯一字符串标识", "1", params);
        // 加锁成功
        if ("OK".equals(code)){ 
           // 这里做调度正常业务逻辑
           doBusiness();
        }
    }finally{
        jedis.del("定时任务唯一字符串标识");
    }
}
复制代码


这里直接在finally代码块中进行KEY的删除,实际上,我们不需要关注这个删除动作是否成功(假如在最后阶段删除KEY出现Redis服务故障,无论使用Lua还是直接删除导致的结果都是一样的)。为了避免多余的DEL操作,可以简单优化为:


// 每30分钟跑一次
@Scheduled(cron = "* */30 * * * ? ")
public void scheduledMethod(){
    boolean lock = false;
    try (Jedis jedis = getJedis()){
        SetParams params = new SetParams().ex(300).nx();
        String code = jedis.set("定时任务唯一字符串标识", "1", params);
        // 加锁成功
        if ("OK".equals(code)){ 
            lock = true;
           // 这里做调度正常业务逻辑
           doBusiness();
        }
    }finally{
        if (lock){
           jedis.del("定时任务唯一字符串标识");
        }
    }
}
复制代码


通过SET RESOURCE_NAME RANDOM_VALUE NX PX 30000Redis单线程处理的特性,就能避免定时任务重复执行。其实这里还存在一些隐患:


  • 如果一个线程加锁时候指定的超时时间很长,并且在跑到finally代码块之前由于不可抗因素(例如很多人喜欢提到的断电)中断导致锁没有释放,那么这个锁就相当于一个僵尸锁。
  • 锁的持有和锁的释放应该由同一个操作者进行,否则操作者A进行了加锁,如果有恶意操作者B进行解锁,那么会导致锁并不安全。


上面这些隐患在Redisson中都有对应的解决方案,迟点分析一下Redisson的源码实现。


小结



本文在改造一个老系统的时候尝试使用改动最小的方式进行简易的基于Redis实现的分布式锁优化,实际生产环境中应该尽量使用主流的可靠的类库,如Redisson(编写本文的时候Github的星星数已经超过10200,提交和Issue都比较活跃,遇到坑了比较容易找到解决方案,值得信赖)。如果需要造轮子,那么就需要熟练使用中间件提供的API,同时注意一下编码规范,尽可能避免因为规范和使用方式不当带来的问题。


附件




相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
NoSQL Redis
基于Redis的高可用分布式锁——RedLock
这篇文章介绍了基于Redis的高可用分布式锁RedLock的概念、工作流程、获取和释放锁的方法,以及RedLock相比单机锁在高可用性上的优势,同时指出了其在某些特殊场景下的不足,并提到了ZooKeeper作为另一种实现分布式锁的方案。
73 2
基于Redis的高可用分布式锁——RedLock
|
5天前
|
存储 NoSQL Redis
6)深度解密 Redis 的集合(Set)
6)深度解密 Redis 的集合(Set)
14 1
|
8天前
|
存储 JSON NoSQL
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
这篇文章是关于Redis基本数据结构的学习笔记,包括了String、Hash、Set、List和SortedSet的介绍和常用命令。文章解释了每种数据结构的特点和使用场景,并通过命令示例演示了如何在Redis中操作这些数据结构。此外,还提供了一些练习示例,帮助读者更好地理解和应用这些数据结构。
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
|
15天前
|
监控 NoSQL Redis
redis-server --service-install redis.windows.conf --loglevel verbose 命令的作用是什么?
redis-server --service-install redis.windows.conf --loglevel verbose 命令的作用是什么?
27 3
|
5天前
|
NoSQL 安全 关系型数据库
20)用 Redis 实现分布式锁
20)用 Redis 实现分布式锁
15 0
|
6天前
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
13 0
|
2月前
|
存储 缓存 负载均衡
【PolarDB-X 技术揭秘】Lizard B+tree:揭秘分布式数据库索引优化的终极奥秘!
【8月更文挑战第25天】PolarDB-X是阿里云的一款分布式数据库产品,其核心组件Lizard B+tree针对分布式环境优化,解决了传统B+tree面临的数据分片与跨节点查询等问题。Lizard B+tree通过一致性哈希实现数据分片,确保分布式一致性;智能分区实现了负载均衡;高效的搜索算法与缓存机制降低了查询延迟;副本机制确保了系统的高可用性。此外,PolarDB-X通过自适应分支因子、缓存优化、异步写入、数据压缩和智能分片等策略进一步提升了Lizard B+tree的性能,使其能够在分布式环境下提供高性能的索引服务。这些优化不仅提高了查询速度,还确保了系统的稳定性和可靠性。
62 5
|
2月前
|
C# UED 定位技术
WPF控件大全:初学者必读,掌握控件使用技巧,让你的应用程序更上一层楼!
【8月更文挑战第31天】在WPF应用程序开发中,控件是实现用户界面交互的关键元素。WPF提供了丰富的控件库,包括基础控件(如`Button`、`TextBox`)、布局控件(如`StackPanel`、`Grid`)、数据绑定控件(如`ListBox`、`DataGrid`)等。本文将介绍这些控件的基本分类及使用技巧,并通过示例代码展示如何在项目中应用。合理选择控件并利用布局控件和数据绑定功能,可以提升用户体验和程序性能。
46 0
|
2月前
|
NoSQL Go Redis
用 Go + Redis 实现分布式锁
用 Go + Redis 实现分布式锁
|
2月前
|
自然语言处理 Java
自研分布式训练框架EPL问题之实现显存的极致优化如何解决
自研分布式训练框架EPL问题之实现显存的极致优化如何解决
下一篇
无影云桌面