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

Redis分布式锁

时间:2023-12-25 19:22:26浏览次数:33  
标签:加锁 过期 Redis id 时间 key 节点 分布式

1.分布式锁的方案

分类 方案 原理 优点 缺点
基于数据库 mysql数据库表的唯一索引 1.表创建唯一索引
2.加锁:执行insert语句,成功则加锁成功,失败则加锁失败
3.解锁:执行delete语句
完全利用DB实现,实现简单 1.锁无超时自动失效机制,有死锁风险
2.不支持锁冲入,不支持阻塞等待
3.操作数据库开销比较大,性能不高
MongoDB的findAndModify原子操作 1加锁:执行findAndModify原子命令查找document,不存在则新增
2.解锁:删除document
实现比较容易,比mysql的方式性能要高 锁无超时自动失效机制
基于分布式协调系统 基于Zookeeper 1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;不是则看/lock目录下需要比自身小的前一个节点
2.解锁:删除节点
1.由zk保障系统高可用
2.Curator框架已原生支持系列分布式锁命令,使用简单
需要单独维护一套zk集群,维护成本高
基于缓存 基于redis 1.加锁:执行setnx与expire命令加锁设置过期时间。
解锁:执行delete命令
2.通过执行Lua脚本
3.开源框架:Redisson
4.RedLock
相比上面几种,这种性能最好 1.第一种setnx与expire非原子操作,可能会出现死锁,delete命令存在误删的可能
2.第一,二种不支持阻塞等待,不可重入。

2.靠谱的分布式锁有的特征

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

3.redis分布式锁的方案

3.1 setnx+expire

这种方式是先通过setnx操作进行加锁,加锁成功后通过expire操作进行设置过期时间。但是由于两个命令是分开的,不是原子操作。因此可能会出现在加锁成功后,还没设置过期时间的时候进程重启了,那么这个锁就会一直存在了。具体的伪代码如下:

if (jedis.setnx(id, value) == 1) { //加锁成功
	expire(id, 100); //设置锁过期时间
	try {
		//业务操作
	} catch() {
		//业务处理异常
	} finally {
		jedis.del(id); //释放锁
	}
}

3.2 setnx+value值是(系统时间+过期时间)

这种就是一个setnx命令,在设值的时候把value值设为系统时间加过期时间,如果加锁成功,则过期时间就为value的值,加锁失败,则需要校验一下value的值是否已过期,重新设置过期时间,返回加锁成功。伪代码如下:

{
	String expiresStr = String.valueOf(System.currentTimeMillis() + 60000); //当前时间+过期时间
	//当前锁不存在,则返回加锁成功
	if (jedis.setnx(id, expiresStr) == 1) {
		return true;
	}
	//如果锁已存在,获取锁的过期时间
	String expireTime = jedis.get(id);
	//判断锁是否已过期
	if (StringUtils.isNotBlank(expireTime) && Long.parseLong(expireTime) < System.currentTimeMillis()) {
		//锁已过期,获取旧的过期时间设置新的过期时间
		String oldExpireTime = jedis.getSet(id, expiresStr);
		//防止多线程并发,只有一个线程的设置值与当前值相同,才可以加锁
		if (StringUtils.isNotBlank(oldExpireTime) && oldExpireTime.equals(expireTime)) {
			return true;
		}
	}
	return false;
}

这种方式可以解决setnx与expire操作不是原子操作的问题。但该方案同样有缺点:

  • 过期时间是客户端生成的,因此每个客户端的时间必须同步
  • 如果有多个线索执行jedis.getSet(),最终只有一个加锁成功,但是可能会导致其它线程覆盖加锁线程的锁过期时间。
  • 锁可能会被别的线程解锁。

3.3 使用Lua脚本

使用Lua脚本能够保证setnx+expire原子性

String luaLock = "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(luaLock, Collections.singletonList(id), Colections.singletonList(valuse));
return result.equals(1L);

这种方式也是有缺点的,学习成本比较高,使用lua脚本应该要掌握一定的lua语法。无法进行代码复用。

3.4 set的扩展命令

另外还可以通过set的扩展参数*(SET key value [EX seconds] [PX milliseconds] [NX|XX])来实现,其也是原子性的。

  • NX:表示key不存在的时候,才能set成功。
  • EX seconds:设定key的过期时间,时间单位是秒
  • PX milliseconds:设定key的过期时间,单位为毫秒
  • XX:仅当key存在时设置值
    伪代码如下:
if (jedis.set(id, value, "NX", "EX", 100s) == 1) {
	try {
		//业务逻辑
	} catch() {
		//异常时的操作
	} finally {
		jedis.del(id);
	}
}

这种方式也会存在一些问题:
线程A持有锁过期释放掉了,但是业务还没有处理完成,此时有另一个线程B获取到锁,执行业务逻辑,但是此时线索A执行完业务逻辑之后会释放掉线程B持有的锁。

3.5 set的扩展命令+校验唯一的随机值,再删除

为了保证只会删除自己的锁而不会误删其它线程的锁,我们可以为value设置一个唯一的值,在删除之前进行校验一下是不是当前线程的锁就行了。这里为了保证校验是原子性的,可以使用lua脚本的方式。伪代码如下:

if (jedis.set(id, value, "NX", "EX", 100s) == 1) {
	try {
		//业务逻辑
	} catch() {
		//异常时的操作
	} finally {
		deleteLock(id, value);
	}
}

public boolean deleteLock(String id, String value) {
	String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
	return jedis.eval(lua, Collections.singletonList(id), Collections.singletonList(value)).equals(1L);
}

3.6 Redisson框架

上面的方式也会存在当设置的锁的已经过期释放了,但是业务代码还没执行完的问题。那么此时就可以通过Redisson框架实现,其会启动一个watchDog用来给锁进行延时。需要注意的是使用此方法通过tryLock方法进行加锁时不能设置过期时间,设置的话就不会启动watchDog。伪代码如下:

public void testLock(String key) {
    RLock lock = null;
    try {
        // 读取配置文件
        Config config = Config.fromYAML(new File("redis-file.yaml"));
        RedissonClient redissonClient = Redisson.create(config);
        lock = redissonClient.getLock(key);
        // 不设置锁的过期时间,启动watchDog机制
        boolean b = lock.tryLock(10, TimeUnit.SECONDS);
        if (b) {
            // 加锁成功后的业务逻辑
        }
    } catch (Exception e) {
        log.error("加锁失败");
    } finally {
        if (Objects.nonNull(lock)) {
            lock.unlock();
        }
    }
}

redisson框架的底层是通过lua脚本进行加锁与解锁的,保证了其加锁时的原子性。

3.7 RedLock

对于集群的情况,如果线索成A在主节点拿到锁,但是加锁的key还没有同步到从节点,此时主节点发生了宕机,一个从节点升为主节点。线程B可以再次获取同个key的锁。这样就会导致锁不安全了。
而为了解决这个问题则可以使用redisson的redlock算法。其核心思想是部署多个Redis主节点,这些节点完全互相独立,不存在主从复制或者其它集群协调机制;依次尝试从N个Master实例使用相同的key和随机值获取锁,当至少有N/2 + 1个redis实例加锁成功后,才是加锁成功。
其实现步骤如下:

  • 获取当前时间,以毫秒为单位
  • 按顺序向所有的mater节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间)。如果超时,跳过该master节点,尽快取尝试下一个master节点。
  • 客户端使用当前时间减去开始获取锁时间,得到获取锁使用的时间。当且仅当超过一半的Redis master节点都获得锁,并且使用的时间(所有节点获取锁所用时间与超时时间和)小于锁失效时间时,锁才算获取成功。
  • 如果获取到了锁,key的真正有效时间就变了,需要减去获取锁所使用的时间。
  • 如果获取锁失败,需要在所有的master节点上解锁。

伪代码如下:

public void testLock(String key) {
    RedissonRedLock redLock = null;
    try {
        // 读取配置文件
        Config config1 = Config.fromYAML(new File("redis-file1.yaml"));
        RedissonClient redissonClient1 = Redisson.create(config1);
        RLock lock1 = redissonClient1.getLock(key);
        Config config2 = Config.fromYAML(new File("redis-file2.yaml"));
        RedissonClient redissonClient2 = Redisson.create(config2);
        RLock lock2 = redissonClient2.getLock(key);
        Config config3 = Config.fromYAML(new File("redis-file3.yaml"));
        RedissonClient redissonClient3 = Redisson.create(config3);
        RLock lock3 = redissonClient3.getLock(key);
        redLock = new RedissonRedLock(lock1, lock2, lock3);
        // 不设置锁的过期时间,启动watchDog机制
        boolean b = redLock.tryLock(10, TimeUnit.SECONDS);
        if (b) {
            // 加锁成功后的业务逻辑
        }
    } catch (Exception e) {
        log.error("加锁失败");
    } finally {
        if (Objects.nonNull(redLock)) {
            redLock.unlock();
        }
    }
}

标签:加锁,过期,Redis,id,时间,key,节点,分布式
From: https://www.cnblogs.com/mcj123/p/17914372.html

相关文章

  • 在arm架构的银河麒麟系统部署Redis
    以下是在arm架构的银河麒麟系统上部署Redis的详细步骤:1.创建文件夹首先,在合适的位置创建必要的文件夹。在本例中,我们将创建/opt/redis和/usr/src/redis两个文件夹。mkdir/opt/redismkdir/usr/src/redis2.准备Redis及其依赖库移动redis-6.2.12.tar.gz到/usr/src/redis......
  • AI分布式训练:DDP (数据并行)技术详解与实战
    分布式训练:DDP(数据并行)技术详解与实战一、背景介绍什么是AI分布式训练分布式训练作为一种高效的训练方法,在机器学习领域中得到了广泛的应用和关注。而其中的一种技术,就是数据并行(DDP)。在本节中,我们将详细介绍什么是AI分布式训练,并重点讨论了数据并行技术的原理和实施方式。我们将......
  • 分布式系统 9种实现接口幂等性方案
    在开发订单系统时,我们常遇见支付问题,既用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生......
  • Redis分布式锁实现方案
    2023.12.25 今天和宝宝复盘吵架,宝宝明明错了还不承认,希望她以后能有长进。 Redis的几种数据类型String最多存储512M数据List setSortedSet有序集合Hashs哈希 Redis的几种内存淘汰策略noeviction内存满了就报错,并执行扩充可用内存命令LRU 回收最少使用的键......
  • 【国际会议| IEEE出版】首届并行计算与分布式系统国际会议 (PCDS2024)
    首届并行计算与分布式系统国际会议(PCDS2024)将于2024年9月21-22日在新加坡及线上同步举办。PCDS旨在为研究人员、学者和行业专业人士提供交流平台,让他们聚在一起讨论并行计算与分布式系统领域的最新进展。 PCDS2024诚邀国内外高校、科研机构专家、学者,企业界人士及其他相关人员......
  • 看看 Asp.net core Webapi 项目如何优雅地使用分布式缓存
    前言缓存是提升程序性能必不可少的方法,Asp.netcore支持多级缓存配置,主要有客户端缓存、服务器端缓存,内存缓存和分布式缓存等。其中客户端缓和服务器端缓存在使用上都有比较大的限制,而内存缓和分布式缓存则比较灵活。分布式缓存是一种用于存储和管理数据的技术,它将数据存储在......
  • Docker部署Redis7.X版本Cluster模式三主三从集群
    前言:最近给客户部署项目提供三台机器,需要用到redis就想着部署RedisCluster模式集群,但是找遍了csdn都没找到我想要的。花了好久参考了很多博主的帖子终于让我搞出来了,个人比较菜各位看官老爷见笑。话不多说开搞!!!!!!安装前准备:1.在三台机器上分别创建对应配置文件夹,一台机器两个节点(一......
  • 架构与思维:如何应对Redis热Key?
    ★Redis系列文章Redis系列1:深刻理解高性能Redis的本质Redis系列2:数据持久化提高可用性Redis系列3:高可用之主从架构Redis系列4:高可用之Sentinel(哨兵模式)Redis系列5:深入分析Cluster集群模式追求性能极致:Redis6.0的多线程模型追求性能极致:客户端缓存带来的革命Redis系列8......
  • 从Redis读取.NET Core配置
    在本文中,我们将创建一个自定义的.NETCore应用配置源和提供程序,用于从Redis中读取配置。在此之前,您需要稍微了解一些.NETCore配置提供程序的工作原理,相关的内容可以在Microsoft开发者官网搜索到。另外您可能还需要了解一些Redis的基础知识,比如Redis的基础数据类型,持久化等等。一......
  • Windows电脑上的多开器与分布式存储系统的关系
    当今,随着信息技术的不断发展,人们对于计算机性能和存储需求的要求也越来越高。在Windows电脑上,多开器与分布式存储系统之间存在着密切的关系,二者共同构建了一个高效、可靠的计算环境。首先,让我们来了解一下多开器的概念。多开器是一种软件工具,可以让用户在单台计算机上同时启动多......