Redis分布式锁及其常见问题解决方案 男娘i 2023-10-16 00:24 41阅读 0赞 > Redis 是一种内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。由于其高性能和灵活的数据结构,Redis 被广泛应用在各种场景中,包括实现分布式锁。 > > 分布式锁是一种在分布式系统中实现互斥访问的技术。在许多实际应用场景中,我们需要确保某些操作在同一时间只能被一个节点执行,例如更新共享资源、处理任务队列等。这时,我们就需要使用到分布式锁。 > > Redis 提供了一种简单有效的分布式锁实现方式。其基本思想是使用 Redis 的 SETNX 命令,这个命令可以在键不存在时设置值,如果键已存在则不做任何操作。通过这个原子操作,我们可以实现在多个节点之间的互斥访问。 > > 然而,虽然 Redis 分布式锁的实现相对简单,但在实际使用中还需要考虑很多问题,例如锁的超时和续期问题、锁的公平性问题、网络分区的问题等。在接下来的文章中,我们将详细介绍这些问题以及解决方案。 -------------------- #### 文章目录 #### * * * * 1、Redis分布式锁简介 * * 1.1、关于分布式锁 * 1.2、Redis分布式锁概述 * 2、Redis分布式锁的问题及解决方案 * * 2.1、锁超时机制 * 2.2、锁续期机制 * 2.3、误删锁问题 * 2.4、脑裂问题与Redlock * 2.5、公平性问题 * 3、Java下Redis分布式锁实现 * * 3.1、Jedis实现 * 3.2、SpringBoot实现 -------------------- ##### 1、Redis分布式锁简介 ##### ###### 1.1、关于分布式锁 ###### 在一个分布式系统中,当一个线程去读取数据并修改的时候,因为读取和更新保存不是一个原子操作,在并发时就很容易遇到并发问题,进而导致数据的不正确。这种场景很常见,比如电商秒杀活动,库存数量的更新就会遇到。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。 一般来说,实现分布式锁的方式有以下几种: 1. 使用 MySQL:这种方式是通过在数据库中创建一个唯一索引的表,然后通过插入一条数据来获取锁,如果插入成功则获取锁成功,否则获取锁失败。释放锁的操作就是删除这条数据。这种方式的优点是实现简单,缺点是性能较低,因为涉及到数据库的操作。 2. 使用 ZooKeeper:ZooKeeper 提供了一个原生的分布式锁实现。其基本思想是创建一个临时有序节点,然后判断自己是否是所有子节点中序号最小的,如果是则获取锁成功,否则监听比自己序号小的节点,当该节点删除时再次尝试获取锁。这种方式的优点是能够保证公平性,缺点是实现较为复杂。 3. 使用 Redis:这种方式是通过 Redis 的 SETNX 命令来实现的,这个命令可以在键不存在时设置值,如果键已存在则不做任何操作。通过这个原子操作,我们可以实现在多个节点之间的互斥访问。这种方式的优点是性能高,实现简单,缺点是需要处理锁的超时和续期问题。 ###### 1.2、Redis分布式锁概述 ###### 在 Redis 中,我们可以使用 `SETNX` 命令来实现分布式锁。以下是具体的步骤: 1. 加锁:客户端使用 `SETNX key value` 命令尝试设置一个键,其中 `key` 是锁的名称,`value` 是一个唯一标识符(例如 UUID),用于标识加锁的客户端。如果键不存在,`SETNX` 命令会设置键的值并返回 1,表示加锁成功;如果键已存在,`SETNX` 命令不会改变键的值并返回 0,表示加锁失败。 ![image-20230916113717663][] 1. 执行业务操作:客户端在成功获取锁后,可以执行需要保护的业务操作。 2. 解锁:客户端在完成业务操作后,需要释放锁以让其他客户端可以获取锁。为了确保只有加锁的客户端可以解锁,客户端需要先获取锁的值(即唯一标识符),然后比较锁的值和自己的唯一标识符是否相同,如果相同则使用 `DEL key` 命令删除键以释放锁。 -------------------- ##### 2、Redis分布式锁的问题及解决方案 ##### ###### 2.1、锁超时机制 ###### 以下是一个基本的 Redis 分布式锁的使用流程: 1. 客户端 A 发送一个 SETNX lock.key 命令,如果返回 1,那么客户端 A 获得锁。 2. 客户端 A 执行完毕后,通过 DEL lock.key 命令释放锁。 然而,这种最基本的锁存在一个问题,那就是如果客户端 A 在执行完毕后,因为某些原因(比如崩溃或网络问题)无法发送 DEL 命令来释放锁,那么其他客户端将永远无法获得锁。为了解决这个问题,我们需要引入锁的超时机制。 ![image-20230916130718524][] 下是一个带有超时机制的 Redis 分布式锁的使用流程: 1. 客户端 A 发送一个 SETNX lock.key 命令,如果返回 1,那么客户端 A 获得锁。 2. 客户端 A 通过 EXPIRE lock.key timeout 命令设置锁的超时时间。 3. 客户端 A 执行完毕后,通过 DEL lock.key 命令释放锁。 这样,即使客户端 A 在执行完毕后无法释放锁,其他客户端也可以在锁超时后获得锁。 ###### 2.2、锁续期机制 ###### 然而,这种带有超时机制的锁还存在一个问题,那就是如果客户端 A 在锁即将超时时仍在执行,那么锁可能会被其他客户端获得,从而导致多个客户端同时持有锁。为了解决这个问题,我们需要引入锁的续期机制。 ![image-20230916130800910][] 以下是一个带有续期机制的 Redis 分布式锁的使用流程: 1. 客户端 A 发送一个 SETNX lock.key 命令,如果返回 1,那么客户端 A 获得锁。 2. 客户端 A 通过 EXPIRE lock.key timeout 命令设置锁的超时时间。 3. 客户端 A 在执行过程中,定期通过 EXPIRE lock.key timeout 命令续期锁。 4. 客户端 A 执行完毕后,通过 DEL lock.key 命令释放锁。 这样,即使客户端 A 的执行时间超过了最初的超时时间,也可以通过续期机制保证锁的互斥性。 ###### 2.3、误删锁问题 ###### 引入锁的续期机制可以解决锁提前过期的问题,但是并不能解决解锁时可能删除其他线程锁的问题。这是因为,即使有了续期机制,仍然存在这样一种情况:线程 A 在锁即将过期时仍在执行业务逻辑,此时锁过期,线程 B 获取到了锁,然后线程 A 执行完业务逻辑,尝试去删除锁,结果删除的是线程 B 的锁。 为了解决这个问题,我们可以使用 Redis 的 Lua 脚本功能,将这三个操作封装在一个 Lua 脚本中,然后使用 `EVAL` 命令执行这个 Lua 脚本。由于 Redis 会单线程顺序执行所有命令,因此 `EVAL` 命令可以保证 Lua 脚本中的操作是原子的。 以下是一个使用 Lua 脚本实现解锁的例子: if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end 在这个 Lua 脚本中,我们首先使用 `get` 命令获取锁的值,然后比较锁的值和客户端的唯一标识符,如果相同则使用 `del` 命令删除锁。 客户端可以使用以下命令执行这个 Lua 脚本: EVAL script 1 key value 其中,`script` 是 Lua 脚本的内容,`key` 是锁的名称,`value` 是客户端的唯一标识符 ###### 2.4、脑裂问题与Redlock ###### 在 Redis 集群中,如果主节点在同步锁到从节点之前挂掉,那么从节点在升级为主节点后可能会误认为锁不存在,从而允许其他客户端获取锁,这就导致了同一把锁被多个客户端同时持有的问题。 为了解决这个问题,我们可以使用 RedLock 算法。RedLock 是 Redis 官方推荐的一种分布式锁实现算法,其基本思想是在多个独立的 Redis 节点上同时尝试获取锁,只有当大多数的 Redis 节点都成功获取到锁时,才认为整个操作成功。 以下是 RedLock 算法的基本步骤: 1. 获取当前时间,以毫秒为单位。 2. 依次尝试在所有 Redis 节点上获取锁,每个尝试都有一个固定的超时时间。如果获取锁失败,立即返回,不再尝试其他节点。 3. 如果成功获取到了大多数的 Redis 节点的锁,并且获取锁的总时间小于锁的有效期,那么整个操作成功。 4. 如果获取锁的总时间大于锁的有效期,或者没有成功获取到大多数的 Redis 节点的锁,那么在所有 Redis 节点上释放锁。 5. 如果整个操作成功,那么锁的有效期就是原来的有效期减去获取锁的总时间。 以上就是 RedLock 算法的基本步骤。通过在多个独立的 Redis 节点上同时尝试获取锁,RedLock 算法可以在一定程度上解决主节点挂掉导致的锁丢失问题。然而,需要注意的是,RedLock 算法并不能完全保证锁的安全性,因为在网络分区或者节点时间不同步的情况下,仍然可能出现同一把锁被多个客户端同时持有的问题。因此,在使用 RedLock 算法时,需要根据实际情况进行详细的设计和测试。 ###### 2.5、公平性问题 ###### 此外,在 Redis 分布式锁的实现中,锁的公平性可能会成为一个问题。所谓公平性,是指当多个客户端同时请求锁时,锁应该被按照请求的顺序分配。然而,由于网络延迟和 Redis 的单线程模型,Redis 分布式锁无法保证公平性。具体来说,当多个客户端同时请求锁时,由于网络延迟,这些请求可能会在不同的时间到达 Redis,而 Redis 会按照请求到达的顺序分配锁,这可能与客户端的请求顺序不同。此外,即使多个请求同时到达 Redis,由于 Redis 的单线程模型,Redis 也只能依次处理这些请求,而处理的顺序可能与客户端的请求顺序不同。 因此,如果你的应用需要公平的分布式锁,你可能需要使用其他的分布式锁实现,例如基于 ZooKeeper 的分布式锁。ZooKeeper 的分布式锁通过在锁的节点下创建顺序临时节点,并通过比较自己的节点是否为最小节点来判断是否获取到锁,从而保证了锁的公平性。 -------------------- ##### 3、Java下Redis分布式锁实现 ##### ###### 3.1、Jedis实现 ###### 在 Java 中,我们可以使用 Jedis 或 Lettuce 这样的 Redis 客户端库来实现 Redis 分布式锁。以下是一个基本的实现示例: import redis.clients.jedis.Jedis; public class RedisLock { private Jedis jedis; private String lockKey; private String lockValue; private int expireTime; private boolean locked = false; public RedisLock(Jedis jedis, String lockKey, int expireTime) { this.jedis = jedis; this.lockKey = lockKey; this.expireTime = expireTime; this.lockValue = Thread.currentThread().getId() + "-" + System.nanoTime(); } public boolean lock() { long startTime = System.currentTimeMillis(); while (true) { String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime); if ("OK".equals(result)) { locked = true; return true; } // 如果没有获取到锁,需要稍微等待一下再尝试 try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } // 如果尝试获取锁超过了expireTime,那么返回失败 if (System.currentTimeMillis() - startTime > expireTime) { return false; } } } public void unlock() { if (!locked) { return; } String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(script, 1, lockKey, lockValue); } } 在这个示例中,我们使用 `set` 命令的 `NX` 和 `PX` 选项来实现锁的获取和超时设置,使用 Lua 脚本来实现安全的解锁操作。我们还使用了一个 while 循环来不断尝试获取锁,直到成功获取锁或者超过了尝试的时间。 然而,这个示例并没有实现锁的续期机制。为了实现续期机制,我们需要在另一个线程中定期检查锁的剩余时间,如果剩余时间不足,那么就需要使用 `expire` 命令来重新设置锁的超时时间。这需要更复杂的代码来实现,例如使用 Java 的 ScheduledExecutorService 来定期执行续期操作。 ###### 3.2、SpringBoot实现 ###### 在 Spring Boot 中,我们可以使用 Redisson 这个 Redis 客户端库来实现 Redis 分布式锁。Redisson 提供了一套丰富的分布式服务,包括分布式锁、分布式集合、分布式队列等,而且 Redisson 已经内置了锁的超时、续期机制,并解决了误删锁问题。 以下是一个基本的使用 Redisson 实现 Redis 分布式锁的示例: import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.concurrent.TimeUnit; @Component public class RedissonDistributedLocker { private RedissonClient redissonClient; @PostConstruct public void init() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); redissonClient = Redisson.create(config); } public void lock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); // Wait for 100 seconds and automatically unlock it after 10 seconds lock.lock(10, TimeUnit.SECONDS); } public void unlock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); lock.unlock(); } } 在这个示例中,我们首先在 `init` 方法中创建了一个 RedissonClient 实例,然后在 `lock` 方法中获取了一个 RLock 对象,并调用其 `lock` 方法来获取锁。在 `unlock` 方法中,我们同样获取了一个 RLock 对象,并调用其 `unlock` 方法来释放锁。 需要注意的是,Redisson 的 `lock` 方法会自动续期,只要持有锁的线程还在运行,锁就会一直被续期,直到线程结束或者显式调用 `unlock` 方法。因此,我们不需要手动实现续期机制。此外,Redisson 的 `unlock` 方法会检查当前线程是否持有锁,只有持有锁的线程才能释放锁,这解决了误删锁问题。 [image-20230916113717663]: https://img-blog.csdnimg.cn/img_convert/43d32f5f19ffb76b2536eed6e16284af.png [image-20230916130718524]: https://img-blog.csdnimg.cn/img_convert/5ef3fd6106808717a0e74a8c7207f9d0.png [image-20230916130800910]: https://img-blog.csdnimg.cn/img_convert/62d7dabad476b49a15609e70359846bb.png
相关 Java并发编程中常见的死锁问题及其解决方案 Java并发编程中,死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种僵局,导致这些线程无法继续执行下去。死锁的产生通常需要满足以下四个条件: 1. **互斥条件** 怼烎@/ 2024年11月06日 20:51/ 0 赞/ 1 阅读
相关 Redis分布式锁及其常见问题解决方案 > Redis 是一种内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。由于其高性能和灵活的数据结构,Redis 被广泛应用在各种场景中,包括实现分布式锁。 > > 男娘i/ 2023年10月16日 00:24/ 0 赞/ 42 阅读
相关 Redis缓存实现及其常见问题解决方案 > 随着互联网技术的发展,数据处理的速度和效率成为了衡量一个系统性能的重要指标。在众多的数据处理技术中,缓存技术以其出色的性能优化效果,成为了不可或缺的一环。而在众多的缓存技术 男娘i/ 2023年10月16日 00:23/ 0 赞/ 9 阅读
相关 Redis教程--redis分布式锁+企业解决方案+redis实战 Redis,目前全国甚至是全球最常用的缓存中间件之一,在现在公司的开发中,可以说是离不开Redis。 在企业越来越注重用户体验的今天,Redis因具有高性能、高响应的特性,大 桃扇骨/ 2023年10月07日 18:18/ 0 赞/ 45 阅读
相关 分布式锁-Redis红锁解决方案 文章目录 1:分布式锁的概念 1:概念 2:锁/分布式锁/事务区别 2:本文使用的案例场景 1:需求 2: 古城微笑少年丶/ 2023年09月29日 15:28/ 0 赞/ 1 阅读
相关 深入理解分布式技术 - Redis 分布式锁解决方案 文章目录 Pre 分布式锁特征 使用 setnx 实现分布式锁 使用 setnx 和 expire 实现 使用 set 扩展命令实现 绝地灬酷狼/ 2022年10月25日 11:25/ 0 赞/ 226 阅读
相关 Redis - 分布式锁实现以及相关问题解决方案 文章目录 Redis - 分布式锁实现以及相关问题解决方案 1.分布式锁是什么? 1.1 分布式锁设计目的 1 青旅半醒/ 2021年12月19日 03:53/ 0 赞/ 260 阅读
还没有评论,来说两句吧...