首页 > 数据库 >Redis分布式锁

Redis分布式锁

时间:2024-01-22 15:33:28浏览次数:25  
标签:过期 Redis 线程 key 分布式 id 客户端

Redis分布式锁

今天在做Lottery分布式抽奖项目中,接触到了分布式锁这个概念,普通单机系统中,我们可以使用mutex、cas等方式来确保不同线程之间的同步和互斥,但是显然在分布式系统下,如果想让所有机器在同一时刻只有一个线程可以访问到某个共享资源,那么传统的互斥方法不再可用。这时候就需要分布式锁来解决这个问题。(分布式系统导致出来的问题真的好多~开发难度极具提高)

分布式锁的特性

  • 「互斥性」: 任意时刻,只有一个客户端能持有锁。
  • 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
  • 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
  • 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
  • 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除

Redis分布式锁方案

所有的方案本质都是使用redis的setnx(Set not exist) 和expire(设置过期时间来实现)两个命令来实现

方案一:
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
    expire(key_resource_id,100); //设置过期时间
    try {
        do something  //业务请求
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

存在问题:setnx和expire命令分开,不是原子操作。如果setnx后,但是没有设置expire前,进程被关闭,别的线程将永远获取不到这个锁。

方案二

为了解决方案一中的问题,有个解决思路就是将过期时间设置在value值当中,避免使用expire来设置过期时间。

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);

// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
        return true;
} 
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);

// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
    // 类似cas的方法来考虑多线程并发问题,只有一个线程的设置值和当前值相同,它才可以加锁
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         return true;
    }
}
        
//其他情况,均返回加锁失败
return false;
}

但是该方法也存在问题:

  • 过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。
  • 如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖
方案三:

使用Lua脚本来保证原子性(现在还没有学习过Lua脚本),之后有空可以学习一下。

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;

加锁的代码如下:

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";   
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);

这种方案很简单,写一个Lua脚本即可。

方案四:

使用set的扩展命令,简单来说就是把原子性交给redis来实现。通过一条set命令即设置好过期时间,设置好NX

格式如下:

SET key value[EX seconds][PX milliseconds][NX|XX]

  • NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
  • EX seconds :设定key的过期时间,时间单位是秒。
  • PX milliseconds: 设定key的过期时间,单位为毫秒
  • XX: 仅当key存在时设置值

到此,上面的方案可能都会存在的问题:

锁设置的时间太短,导致业务还没有完成,其它线程去获取锁就拿到了分布式锁,导致临界区的代码不能互斥运行。比如:线程A设置过期时间100ms,业务由于某些原因运行了200ms,那么其它线程再100-200ms之间可以拿到分布式锁,且当线程A运行结束,会去释放锁,从而释放掉了其它线程拥有的锁。

方案五:

解决上述方案中存在的问题,方法是通过value值设置未一个标记当前线程唯一的随机数,做一个校验即可。

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}

上述代码存在的问题,判断相等和释放锁属于两个操作,依旧可能存在判断完成后,但没有执行del时,锁被其它线程占用,导致释放掉了其它线程的锁。解决方法也简单,就是使用lua脚本

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;
方案六

最后的问题就是如何解决锁过期释放,业务没有执行完的问题。显然,如果只是单纯设置超过业务执行时间的时间,是很容易出现问题的,因为我们不能确保业务在多久内可以执行结束,如果盲目设置过大的过期时间,会导致锁的性能过差。

在redisson框架中,使用了看门狗机制,每当线程加锁成功,会创建一个后台线程,每隔一段时间检查一下,如果锁还被当前线程持有,就去延长时间,直到业务完成释放锁。

标签:过期,Redis,线程,key,分布式,id,客户端
From: https://www.cnblogs.com/xyfhsy/p/17980163

相关文章

  • 记录使用Redis当分布式锁
    在网上看到一次使用redis当分布式锁的文章,我就自己写了个demo前置条件:建议新建一个springboot工程(添加web依赖),然后自行整合mybatisplus、redis,可以参照以下链接:mybatisPlus:https://blog.csdn.net/wang20000102/article/details/132615071redis:https://blog.csdn.net/lwj_07/art......
  • python redis示例
    Redis是一个基于内存的高性能键值对(key-value)存储系统,同时也支持丰富的数据结构,如字符串、哈希表、列表、集合、有序集合等。在Python中,我们通常使用redis-py这个第三方库来连接和操作Redis。以下是一个基本的使用步骤以及各种数据结构操作的示例:1.安装redis-py库pipinstall......
  • redis 安装教程
    一、初始化环境创建redis运行时的用户和组$groupaddredis$useradd-gredisredis-s/sbin/nologin-M初始化数据目录$rm-rf/data/redis$mkdir-pv/data/redis$chown-Rredis:redis/data/redis初始化日志目录$mkdir-pv/data/logs/redis$chown-Rredis:redis/......
  • 面试官:Redis持久化能关吗?怎么关?
    数据持久化是指将数据从内存中,保存到磁盘或其他持久存储介质的过程,这样做的目的是为了保证数据不丢失。而Redis的持久化功能默认是开启的,这样做的目的也是为了保证程序的稳定性(防止缓存雪崩、缓存击穿等问题)和数据不丢失。Redis持久化能关吗?怎么关?Redis持久化默认是开启的,......
  • Ray一个通用分布式计算框架基本使用
      Ray一个开源的通用分布式计算框架,支持传统的并行任务并支持AI模型的分布式训练,分布式任务包括有状态与无状态任务,Ray能够快速的构建分布式系统,支持按需申请CPU或GPU;Ray提供了统一的接口提供了基于任务的并行计算与基于行动器的计算,前者通常用于无状态的任务后者用于有状态的任......
  • django使用redis集群、连接池、MySQL连接池
    redis的相关设置CACHES={"default":{"BACKEND":"django_redis.cache.RedisCache","LOCATION":["redis://127.0.0.1:6379/1","redis://127.0.0.1:6380/1",#...],"OPTIONS":{"CLIENT_......
  • 阿里云 ACK 云原生 AI 套件中的分布式弹性训练实践
    作者:霍智鑫众所周知,随着时间的推移,算力成为了AI行业演进一个不可或缺的因素。在数据量日益庞大、模型体量不断增加的今天,企业对分布式算力和模型训练效率的需求成为了首要的任务。如何更好的、更高效率的以及更具性价比的利用算力,使用更低的成本来训练不断的迭代AI模型,变成了迫......
  • Redis常见面试题
    一、Redis做什么的,在哪些场景下使用Redis是一个开源的内存数据存储系统,它被广泛用于缓存、消息队列、实时统计分析、任务队列等场景。以下是一些常见的使用场景:缓存:Redis的主要用途之一是作为缓存层。它可以将经常访问的数据存储在内存中,以提高读取速度。常见的应用场景包括页面缓......
  • redis后台启动带脚本命令
    redis后台启动带脚本命令(windwos)方法11.新建.txt文件2.编辑文件内容@echooffstartcmd/c"cd/dD:\JAVA\Redis-x64-3.2.100&&redis-server.exeredis.windows-service.conf&&taskkill/f/t/imcmd.exe"改为.bat启动方法2服务启动如果原来已有redis服务,先将其卸载red......
  • 分布式环境下流控技术汇总
    本篇主要是对分布式环境流控技术及使用场景做个简要的汇总,包括:固定时间窗口算法,滑动时间窗口算法,漏桶算法,令牌桶算法,分布式消息中间件,流控与熔断利器Sentinel。1.前言在流量控制系列文章中的前六篇,分别介绍了固定时间窗口算法、滑动时间窗口算法、漏桶原理、令牌桶、消息中间件、S......