谈谈Redis分布式锁
目录
一、回顾分布式锁
(一)理解分布式锁的定义
(二)分布式锁的约束条件
(三)分布式锁常见实现方式
基于数据库的分布式锁
基于缓存的分布式锁
基于分布式一致性算法的分布式锁
基于文件系统的分布式锁
基于消息队列的分布式锁
基于第三方服务的分布式锁
二、分布式锁Redis原理
(一)Redis分布式锁的基本原理总揽
(二)核心指令:加锁
示例加锁参数解析
(三)核心指令:解锁
(四)错误案例分析:setNx
(五)常见解锁方案:通过Lua脚本解锁+使用Redis事务功能
通过Lua脚本执行解锁
使用Redis事务功能
(六)重点问题关注
三、Redisson分布式锁
(一)Redisson分布式锁-可重入锁
(二)Redisson分布式锁-公平锁(Fair Lock)
(三)Redisson分布式锁-联锁
(四)Redisson分布式锁-红锁(RedLock)
(五)Redisson分布式锁-读写锁(ReadWriteLock)
一、回顾分布式锁
(一)理解分布式锁的定义
分布式锁是一种在分布式计算环境中用于控制多个节点(或多个进程)对共享资源的访问的机制。在分布式系统中,多个节点可能需要协调对共享资源的访问,以防止数据的不一致性或冲突。分布式锁允许多个节点在竞争访问共享资源时进行同步,以确保只有一个节点能够获得锁,从而避免冲突和数据损坏。以下是一些关键概念和理解:
锁:锁是一种同步机制,它可以被获取和释放。当一个节点获得锁时,它可以执行需要访问共享资源的操作,其他节点必须等待直到锁被释放才能获得锁。
分布式环境:在分布式系统中,多个节点分布在不同的物理位置或计算机上,它们通过网络相互通信。这增加了在多个节点之间协调共享资源访问的复杂性。
锁的种类:
- 互斥锁:在分布式环境中,互斥锁确保在任何给定时刻只有一个节点可以持有锁。其他节点必须等待锁被释放。
- 读写锁:允许多个节点同时读取共享资源,但只允许一个节点写入共享资源。这可以提高并发性能,但需要更复杂的管理。
锁的实现方式:分布式锁可以使用不同的实现方式,如基于数据库、基于缓存、基于分布式一致性算法(例如ZooKeeper或etcd)等。
死锁和性能问题:在设计和使用分布式锁时,需要考虑到死锁(当多个节点相互等待锁释放而无法继续执行)和性能问题(锁争夺可能导致性能下降)。
分布式锁的主要目标是确保在分布式系统中对共享资源的访问是有序和安全的,从而避免数据不一致性和冲突。然而,分布式锁的设计和管理需要仔细考虑,以确保高可用性、性能和可伸缩性。在实际应用中,通常会根据具体的需求和环境选择适当的分布式锁实现方式。
(二)分布式锁的约束条件
在设计和实现分布式锁时,需要考虑一些约束条件,以确保锁的正确性和可用性。以下是一些常见的分布式锁的约束条件:
不同的分布式锁实现方式(如基于数据库、基于缓存、基于分布式一致性算法等)可能在满足这些约束条件时有不同的优缺点。在选择分布式锁实现方式时,需要根据具体的应用需求和性能要求来权衡这些约束条件。同时,为了确保分布式锁的正确性,需要进行严格的测试和验证。
(三)分布式锁常见实现方式
分布式锁可以使用多种不同的实现方式,每种方式都有其适用的场景和特点。以下是一些常见的分布式锁实现方式:(也可以见分布式锁实现方式分析-CSDN博客)
基于数据库的分布式锁
- 使用数据库的行级锁或乐观锁来实现分布式锁。
- 优点:可靠性高,容易理解和管理。
- 缺点:性能可能受到数据库访问的延迟影响,不适用于高并发场景。
基于缓存的分布式锁
- 使用分布式缓存(如Redis或Memcached)来存储锁状态。
- 优点:性能较高,适用于高并发场景。
- 缺点:可能存在缓存故障或数据不一致性问题。
基于分布式一致性算法的分布式锁
- 使用分布式一致性算法(如ZooKeeper或etcd)来实现锁。
- 优点:可靠性高,适用于复杂的分布式环境。
- 缺点:性能较低,不适用于高吞吐量的场景。
基于文件系统的分布式锁
- 使用共享文件系统(如NFS)或分布式文件系统(如HDFS)来创建锁文件。
- 优点:易于理解和维护。
- 缺点:性能可能受到文件系统的延迟影响,不适用于高并发场景。
基于消息队列的分布式锁
- 使用分布式消息队列(如Kafka或RabbitMQ)来协调锁状态。
- 优点:支持分布式异步操作,适用于特定场景。
- 缺点:需要谨慎处理消息队列中的消息重复和丢失问题。
基于第三方服务的分布式锁
- 使用专门的分布式锁服务(如Redlock、Curator等)来管理锁。
- 优点:可靠性高,提供了一些高级功能。
- 缺点:通常需要引入额外的依赖。
不同的实现方式适用于不同的应用场景和性能要求。选择合适的分布式锁实现方式时,需要考虑系统的可靠性、性能、复杂性和维护成本等因素。此外,在使用分布式锁时,也需要注意处理死锁、超时、自动释放等问题,以确保锁的正确性和可用性。
二、分布式锁Redis原理
Redis的分布式锁实现通常基于两个主要命令:SET
和EXPIRE
,结合一些原子性操作,如NX
(只在键不存在时设置键的值)。
(一)Redis分布式锁的基本原理总揽
获取锁
- 客户端使用
SET
命令尝试在Redis中设置一个特定的键,这个键通常被视为锁的名称。 - 为了确保锁是独占的,客户端通常会使用
NX
选项,只有在该键不存在时才能设置成功。 - 客户端可以在
SET
命令中设置一个带有超时时间的参数,这个时间决定了锁的有效期。
锁超时机制
- 为了避免锁被长时间持有,客户端在
SET
命令中设置了锁的超时时间。 - Redis允许使用
EXPIRE
命令来设置键的过期时间,这样即使客户端在释放锁时出现问题,也会在一段时间后自动释放锁。
释放锁
- 当客户端完成对共享资源的操作后,它可以使用
DEL
命令来删除锁键,从而释放锁。 - 由于
DEL
是一个原子操作,确保了释放锁的安全性。
处理竞争条件
- 如果多个客户端同时尝试获取锁,只有一个客户端能够成功设置锁,其余客户端会失败。
- 失败的客户端通常会通过轮询或其他方式等待锁的释放。
续约锁
- 为了防止因为客户端执行时间过长导致锁的过期,客户端可以定期续约锁。
- 客户端可以通过重置锁的超时时间(使用
EXPIRE
命令)来实现续约。
需要注意的是,Redis的分布式锁虽然简单,但也有一些潜在的问题需要处理,例如:
- 锁的过期时间需要谨慎设置,以免长时间锁定资源。
- 客户端在获取锁后发生崩溃或异常情况时,需要确保锁能够自动释放。
- 客户端需要小心处理续约机制,以防止死锁或其他问题。
总之,Redis分布式锁是一种轻量级的实现方式,适用于某些场景。但在高并发和复杂的分布式环境中,可能需要更复杂的分布式锁实现方式来满足更高的可靠性和性能要求。
(二)核心指令:加锁
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
对于使用Redis实现分布式锁,你可以使用SET
命令的以下选项来进行加锁操作:
KEY
:锁的名称,通常是一个字符串。VALUE
:锁的值,通常是一个唯一标识符或随机字符串,用于标识持有锁的客户端。EX seconds
:可选参数,设置锁的过期时间(以秒为单位)。锁在指定的秒数后会自动过期释放。PX milliseconds
:可选参数,设置锁的过期时间(以毫秒为单位)。锁在指定的毫秒数后会自动过期释放。NX
:可选参数,表示只有在键不存在时才能设置成功,用于确保锁是独占的。XX
:可选参数,表示只有在键已经存在时才能设置成功,用于更新锁的值或延续锁的过期时间。
示例加锁参数解析
SET lock_name my_random_value NX PX 30000
- lock_name,即分布式锁的名称,对于 Redis 而言,lock_name 就是 Key-Value 中的 Key且具有唯一性。
- my_random_value,由客户端生成的一个随机字符串,它要保证在足够长的一段时间内,且在所有客户端的所有获取锁的请求中都是唯一的,用于唯一标识锁的持有者。
- NX 表示只有当 lock_name(key) 不存在的时候才能 SET 成功,从而保证只有一个客户端能获得锁,而其它客户端在锁被释放之前都无法获得锁。
- PX 30000 表示这个锁节点有一个 30 秒的自动过期时间(目的是为了防止持有锁的客户端故障后,无法主动释放锁而导致死锁,因此要求锁的持有者必须在过期时间之内执行完相关操作并释放锁)。
(三)核心指令:解锁
del lock_name
- 在加锁时为锁设置过期时间,当过期时间到达,Redis 会自动删除对应的 Key-Value,从而避免死锁。
- 正常执行完毕,未到达锁过期时间,通过del lock_name主动释放锁。
- 注意,这个过期时间需要结合具体业务综合评估设置,以保证锁的持有者能够在过期时间之内执行完相关操作并释放锁。
(四)错误案例分析:setNx
Jedis jedis = jedisPool.getResource();// 如果锁不存在则进行加锁Long lockResult = jedis.setnx(lockName, myRandomValue);if (lockResult == 1) {// 设置锁过期时间,加锁和设置过期时间是两步完成的,非原子操作jedis.expire(lockName, expireTime);}
代码使用SETNX
和EXPIRE
命令来实现分布式锁的方式,虽然看似可行,但确实存在一定的问题,特别是在异常情况下,可能会导致死锁。让我重新梳理一下这个问题:
问题描述
- 使用
SETNX
命令尝试获取锁。 - 如果
SETNX
成功,表示锁被成功获取,接着使用EXPIRE
来设置锁的过期时间。 - 如果在设置过期时间时发生异常,锁就会一直存在,无法自动释放。
这个问题的核心在于SETNX
和EXPIRE
两个命令并没有原子性地组合在一起。如果在第2步和第3步之间发生了异常,就会导致锁没有过期时间,进而可能导致死锁。
解决方案
为了确保锁的安全性,需要将获取锁和设置过期时间这两个操作原子化。可以使用SET
命令的NX
和EX
选项来一次性完成这两个操作,以避免出现问题。下面是示例代码:
Jedis jedis = jedisPool.getResource();
String lockResult = jedis.set(lockName, myRandomValue, "NX", "EX", expireTime);
if ("OK".equals(lockResult)) {// 锁获取成功// 进行业务操作// ...// 业务完成后,释放锁jedis.del(lockName);
} else {// 锁获取失败// 可以进行重试或其他处理
}
在这个示例中,SET
命令的选项"NX"
表示只有在键不存在时才能设置成功,"EX"
表示设置键的过期时间。这样可以确保获取锁和设置过期时间是一个原子操作,从而避免了在异常情况下出现死锁问题。
总之,确保分布式锁的获取和释放是原子操作是非常重要的,以确保锁的正确性和可用性。使用SET
命令的组合选项可以简化代码并避免一些潜在的问题。
(五)常见解锁方案:通过Lua脚本解锁+使用Redis事务功能
通过Lua脚本执行解锁
要利用Lua脚本实现可靠的分布式锁解锁,可以编写一个Lua脚本,该脚本在执行时会检查锁的值是否与预期值匹配,并且只有在匹配时才会删除锁。这确保了只有持有锁的客户端才能成功解锁。
以下是一个示例Lua脚本,用于解锁:
if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])
elsereturn 0
end
在这个脚本中,KEYS[1]
表示锁的键,ARGV[1]
表示你持有的锁的值。脚本首先检查锁的值是否与预期值匹配(即检查锁是否仍然由当前客户端持有),如果匹配,则使用DEL
命令来删除锁,然后返回1表示解锁成功,否则返回0表示解锁失败。
在Java中,可以使用Jedis库执行Lua脚本:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;public class RedisLockUnlock {private static final String LOCK_KEY = "my_lock_key";private static final String LOCK_VALUE = "my_lock_value";public static void main(String[] args) {JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "localhost", 6379);try (Jedis jedis = jedisPool.getResource()) {String luaScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then " +"return redis.call('DEL', KEYS[1]) " +"else " +"return 0 " +"end";String result = (String) jedis.eval(luaScript, 1, LOCK_KEY, LOCK_VALUE);if ("1".equals(result)) {System.out.println("Lock released successfully.");} else {System.out.println("Failed to release lock. Lock may no longer be owned by this client.");}} catch (Exception e) {e.printStackTrace();} finally {jedisPool.close();}}
}
示例中使用eval
方法执行Lua脚本,传递锁的键(LOCK_KEY
)和持有的锁的值(LOCK_VALUE
)作为参数。脚本会尝试解锁,如果解锁成功,就会返回"1",否则返回"0"。根据返回值可以判断解锁是否成功。
请注意,使用Lua脚本来解锁可以确保解锁操作是原子的,只有持有锁的客户端才能成功解锁,这使得解锁更加可靠。
使用Redis事务功能
可以使用Redis的事务功能来实现可靠的分布式锁解锁。Redis事务使用MULTI
、EXEC
和WATCH
命令来执行一系列命令,这些命令在EXEC
中原子性地执行。通过使用WATCH
命令,你可以监视某个键是否被修改,如果被修改,事务将被取消,从而确保解锁是可靠的。
以下是一个Java示例,演示如何使用Redis事务来解锁:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;public class RedisLockUnlock {private static final String LOCK_KEY = "my_lock_key";private static final String LOCK_VALUE = "my_lock_value";public static void main(String[] args) {JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "localhost", 6379);try (Jedis jedis = jedisPool.getResource()) {// 监视锁的键jedis.watch(LOCK_KEY); String lockValue = jedis.get(LOCK_KEY);if (LOCK_VALUE.equals(lockValue)) {// 开启事务Transaction tx = jedis.multi(); // 删除锁tx.del(LOCK_KEY); if (tx.exec() != null) {System.out.println("Lock released successfully.");} else {System.out.println("Failed to release lock. Lock may no longer be owned by this client.");}} else {System.out.println("Lock is not owned by this client.");}} catch (JedisException e) {e.printStackTrace();} finally {jedisPool.close();}}
}
示例中使用WATCH
命令来监视锁的键(LOCK_KEY
),然后获取锁的值。如果锁的值与预期的值相同,表示锁仍然由当前客户端持有,我们就开启事务,使用DEL
命令删除锁,然后通过EXEC
来执行事务。如果事务执行成功,说明解锁成功;否则,说明锁可能已被其他客户端修改,解锁失败。
这种方法确保了解锁是原子的,并且只有持有锁的客户端才能成功解锁。如果锁不再属于当前客户端,事务将被取消,这使得解锁操作更加可靠。
(六)重点问题关注
上面的方案在主从架构的Redis集群中,主节点和从节点之间的异步复制存在一定的延迟,这可能导致在主节点宕机并切换到从节点时,之前在主节点上获取的锁在从节点上尚未完全同步,从而引发多个客户端获取同一把锁的问题。这是一个在分布式锁中需要考虑的常见问题。
为了解决这个问题,可以考虑使用RedLock算法或者使用Redis Sentinel来增强锁的可用性和可靠性。还有一种更加可靠健壮且易用性更好的Redis锁实现方式------Redisson分布式锁实现(关于Redisson的分布式锁可见分布式锁和同步器 )。
三、Redisson分布式锁
Redisson(Redis + Java + Jackson)是一个用于Java应用程序的开源分布式Java对象的框架。Redisson提供了一组用于在分布式环境下处理常见任务的API,其中包括分布式锁。Redisson的分布式锁实现是基于Redis的,它具有高性能、可靠性和可扩展性,可以用于解决分布式应用程序中的并发控制问题。
首先,你需要在你的Java项目中导入Redisson的依赖。你可以通过Maven或Gradle来添加依赖。以下是一个示例Maven依赖的配置:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><!-- 使用最新版本 --><version>3.16.1</version>
</dependency>
在使用Redisson之前,你需要创建一个Redisson实例,用于连接到Redis服务器。通常,你只需要创建一个全局的Redisson实例,并在整个应用程序中重复使用它。
Config config = new Config();
config.useSingleServer()// 设置Redis服务器地址.setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
(一)Redisson分布式锁-可重入锁
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
下面是一个具体的业务使用案例,演示如何使用Redisson的RLock
来管理并发访问。
业务场景:假设有一个电子商务网站,用户在购物时需要扣减商品的库存。由于多个用户可能同时购买相同的商品,需要确保库存的扣减是线程安全的,同时避免超卖的问题。
package org.zyf.javabasic.redisson;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;/*** @program: zyfboot-javabasic* @description: 假设有一个电子商务网站,用户在购物时需要扣减商品的库存。* 由于多个用户可能同时购买相同的商品,需要确保库存的扣减是线程安全的,同时避免超卖的问题。* @author: zhangyanfeng* @create: 2023-10-03 14:18**/
public class InventoryService {private static final String PRODUCT_STOCK_KEY = "product:12345:stock";public static void main(String[] args) {// 创建Redisson客户端Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379");RedissonClient redisson = Redisson.create(config);// 获取可重入锁RLock lock = redisson.getLock(PRODUCT_STOCK_KEY);try {// 尝试获取锁,最多等待10秒if (lock.tryLock(10, 10, java.util.concurrent.TimeUnit.SECONDS)) {// 获取锁成功,执行库存扣减操作int currentStock = getCurrentStock();if (currentStock > 0) {// 扣减库存currentStock--;updateStock(currentStock);System.out.println("库存扣减成功,当前库存:" + currentStock);} else {System.out.println("库存不足,无法扣减");}} else {System.out.println("获取锁超时,无法扣减库存");}} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放锁lock.unlock();}// 关闭Redisson客户端redisson.shutdown();}private static int getCurrentStock() {// 模拟从数据库或缓存中获取当前库存数量的操作return 10;}private static void updateStock(int newStock) {// 模拟更新数据库或缓存中库存数量的操作}
}
在上述示例中使用了Redisson的RLock
来保护库存扣减操作。主要步骤如下:
- 创建Redisson客户端并获取可重入锁。
- 尝试获取锁,最多等待10秒。如果获取锁成功,执行库存扣减操作。
- 扣减库存并更新库存数量。
- 最后释放锁。
这样,即使多个用户同时访问库存扣减操作,也能确保只有一个线程能够成功获取锁,从而保证库存操作的线程安全性。
这个示例展示了如何在分布式环境下使用Redisson的RLock
来处理并发控制问题。它能够轻松地解决类似的并发问题,确保数据的一致性和可靠性。同时,Redisson还提供了异步、反射式和RxJava2标准的接口,可以根据项目需求选择最适合的方式来使用RLock
。
(二)Redisson分布式锁-公平锁(Fair Lock)
Redisson的公平锁(Fair Lock)是一种分布式可重入锁,它基于Redis实现,提供了Java的java.util.concurrent.locks.Lock
接口,同时也支持异步、反射式和RxJava2标准的接口。公平锁确保当多个Redisson客户端线程同时请求加锁时,锁的获取顺序是公平的,即按照请求顺序分配锁。在公平锁中,所有请求线程会在一个队列中排队,等待获取锁。
业务场景:假设有一个共享资源,多个线程需要访问这个资源,但需要按照请求的先后顺序获取访问权,以保证公平性。
package org.zyf.javabasic.redisson;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;/*** @program: zyfboot-javabasic* @description: 假设有一个共享资源,多个线程需要访问这个资源,但需要按照请求的先后顺序获取访问权,以保证公平性。* @author: zhangyanfeng* @create: 2023-10-03 14:23**/
public class SharedResourceService {private static final String RESOURCE_KEY = "shared_resource";public static void main(String[] args) {// 创建Redisson客户端Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379");RedissonClient redisson = Redisson.create(config);// 获取公平锁RLock fairLock = redisson.getFairLock(RESOURCE_KEY);try {// 尝试获取锁fairLock.lock();// 执行需要访问共享资源的操作System.out.println("Thread " + Thread.currentThread().getId() + " is accessing the shared resource.");Thread.sleep(2000); // 模拟访问共享资源的耗时操作} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放锁fairLock.unlock();}// 关闭Redisson客户端redisson.shutdown();}
}
在上述示例中使用了Redisson的公平锁(RLock
)来实现多个线程访问共享资源的公平竞争。主要步骤如下:
- 创建Redisson客户端并获取公平锁。
- 尝试获取锁,如果有其他线程持有锁,当前线程将等待,直到获取到锁。
- 执行需要访问共享资源的操作。这里我们模拟了一个耗时的操作。
- 最后释放锁。
使用公平锁,多个线程会按照请求的顺序获取锁,确保了访问共享资源的公平性。这对于需要遵循先到先得原则的场景非常有用。
需要注意的是,公平锁可能会导致线程等待的时间较长,因为它会等待之前请求的线程释放锁。因此,在使用公平锁时,需要考虑性能和公平性之间的权衡。如果对性能要求较高,可以考虑使用非公平锁。不过,在某些场景下,公平锁是非常有价值的,例如需要遵循特定规则或优先级的应用程序。
(三)Redisson分布式锁-联锁
Redisson提供了RTransaction
两种方式来实现类似的分布式联锁行为。这两种方式都允许你在多个Redisson对象之间执行事务性操作,确保一组操作要么全部成功,要么全部失败。
业务场景:假设有一个在线购物系统,用户下单时需要满足以下条件:
- 商品库存充足。
- 用户账户余额充足。
- 支付渠道可用。
只有当以上三个条件都满足时,用户的订单才能成功下单。
RTransaction
是Redisson提供的事务管理方式,你可以将多个Redis命令包装在一个事务中,然后一起提交或回滚。这也可以用于模拟分布式联锁的行为。
package org.zyf.javabasic.redisson;import org.redisson.Redisson;
import org.redisson.api.*;
import org.redisson.config.Config;/*** @program: zyfboot-javabasic* @description: 假设有一个在线购物系统,用户下单时需要满足以下条件:* 商品库存充足。 用户账户余额充足。 支付渠道可用。* 只有当以上三个条件都满足时,用户的订单才能成功下单。* @author: zhangyanfeng* @create: 2023-10-03 14:41**/
public class OrdeRTransactionService {private static final String PRODUCT_STOCK_KEY = "product:12345:stock";private static final String USER_BALANCE_KEY = "user:1001:balance";private static final String PAYMENT_CHANNEL_KEY = "payment:channel:available";public static void main(String[] args) {// 创建Redisson客户端Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379");RedissonClient redisson = Redisson.create(config);// 获取各个锁RLock productStockLock = redisson.getLock(PRODUCT_STOCK_KEY);RLock userBalanceLock = redisson.getLock(USER_BALANCE_KEY);RLock paymentChannelLock = redisson.getLock(PAYMENT_CHANNEL_KEY);try {// 创建事务TransactionOptions options = TransactionOptions.defaults();RTransaction transaction = redisson.createTransaction(options);// 加锁并提交事务productStockLock.lock();userBalanceLock.lock();paymentChannelLock.lock();transaction.commit();// 所有锁都成功加锁,执行订单下单操作System.out.println("订单下单成功");} catch (Exception e) {e.printStackTrace();// 事务失败时回滚锁productStockLock.unlock();userBalanceLock.unlock();paymentChannelLock.unlock();} finally {// 关闭Redisson客户端redisson.shutdown();}}
}
在这个示例中创建了一个RTransaction
对象,将多个锁的加锁操作放入事务中,然后提交事务。如果事务中的任何操作失败会回滚锁。
(四)Redisson分布式锁-红锁(RedLock)
红锁(RedLock)是一种分布式锁算法,旨在提供高可用性和可靠性的分布式锁。Redisson库提供了RedissonRedLock
对象来实现这种算法,允许你将多个RLock
对象关联为一个红锁,其中每个RLock
可以来自不同的Redisson实例,以增强锁的可靠性和高可用性。
下面是一个关于如何使用RedissonRedLock的业务使用案例分析:
场景背景: 假设有一个在线购物平台,用户在购买商品时需要锁定商品的库存,并且需要扣减用户的余额。同时,需要在扣减余额时也锁定用户的余额,以防止并发问题。这个场景需要确保库存锁和余额锁同时成功,否则不能完成购买操作。
package org.zyf.javabasic.redisson;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RedissonRedLock;
import org.redisson.config.Config;/*** @program: zyfboot-javabasic* @description: 使用RedissonRedLock的业务场景* @author: zhangyanfeng* @create: 2023-10-03 15:24**/
public class PurchaseRedService {private static final String PRODUCT_STOCK_KEY = "product:12345:stock";private static final String USER_BALANCE_KEY = "user:1001:balance";private static final int LOCK_TIMEOUT = 10; // 锁超时时间,秒public static void main(String[] args) {// 创建Redisson客户端连接多个Redis节点Config config1 = new Config();config1.useSingleServer().setAddress("redis://host1:6379");Config config2 = new Config();config2.useSingleServer().setAddress("redis://host2:6379");Config config3 = new Config();config3.useSingleServer().setAddress("redis://host3:6379");RedissonClient redisson1 = Redisson.create(config1);RedissonClient redisson2 = Redisson.create(config2);RedissonClient redisson3 = Redisson.create(config3);// 获取商品库存锁、用户余额锁RLock productStockLock = redisson1.getLock(PRODUCT_STOCK_KEY);RLock userBalanceLock = redisson2.getLock(USER_BALANCE_KEY);// 创建红锁,关联多个锁RedissonRedLock redLock = new RedissonRedLock(productStockLock, userBalanceLock);try {// 尝试获取红锁,等待10秒,锁超时时间为10秒if (redLock.tryLock(LOCK_TIMEOUT, LOCK_TIMEOUT)) {// 获取红锁成功,执行购买操作// 检查库存是否足够int currentStock = getCurrentStock();if (currentStock > 0) {// 扣减库存currentStock--;updateStock(currentStock);// 扣减用户余额double currentBalance = getCurrentBalance();double purchaseAmount = 100.0; // 假设购买商品价格为100if (currentBalance >= purchaseAmount) {currentBalance -= purchaseAmount;updateBalance(currentBalance);System.out.println("购买成功,剩余库存:" + currentStock + ",剩余余额:" + currentBalance);} else {System.out.println("余额不足,购买失败");}} else {System.out.println("库存不足,购买失败");}} else {System.out.println("获取红锁失败,购买失败");}} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放红锁redLock.unlock();// 关闭Redisson客户端redisson1.shutdown();redisson2.shutdown();redisson3.shutdown();}}private static int getCurrentStock() {// 模拟从数据库或缓存中获取当前库存数量的操作return 10;}private static void updateStock(int newStock) {// 模拟更新数据库或缓存中库存数量的操作}private static double getCurrentBalance() {// 模拟从数据库或缓存中获取当前用户余额的操作return 500.0;}private static void updateBalance(double newBalance) {// 模拟更新数据库或缓存中用户余额的操作}
}
在这个示例中,首先创建了三个不同的Redisson客户端连接到不同的Redis节点。然后,我们获取了商品库存锁和用户余额锁,并使用RedissonRedLock
将它们关联为一个红锁。在购买过程中,我们使用红锁来确保在库存锁和余额锁都成功加锁时才能执行购买操作。如果任何一个锁获取失败,购买操作将被视为失败。
(五)Redisson分布式锁-读写锁(ReadWriteLock)
Redisson的分布式锁库也支持读写锁(ReadWriteLock),可以在分布式环境中更有效地管理读取和写入操作的并发性。读写锁允许多个线程同时读取数据,但只允许一个线程写入数据,并且写入数据时会阻塞读取操作。
业务使用案例:
假设我们有一个简单的文章发布系统,多个用户可以同时读取文章,但只能有一个用户同时进行编辑和发布文章的写入操作。
package org.zyf.javabasic.redisson;import org.redisson.Redisson;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;/*** @program: zyfboot-javabasic* @description: 使用Redisson的ReadWriteLock的业务场景* @author: zhangyanfeng* @create: 2023-10-03 15:28**/
public class ArticleService {private static final String ARTICLE_LOCK_KEY = "article:lock";private static final String ARTICLE_CONTENT_KEY = "article:content";public static void main(String[] args) {// 创建Redisson客户端Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379");RedissonClient redisson = Redisson.create(config);// 获取读写锁RReadWriteLock rwLock = redisson.getReadWriteLock(ARTICLE_LOCK_KEY);try {// 获取读锁rwLock.readLock().lock();// 读取文章内容String articleContent = getArticleContent();System.out.println("文章内容:" + articleContent);// 模拟读取操作耗时Thread.sleep(1000);// 释放读锁rwLock.readLock().unlock();// 获取写锁rwLock.writeLock().lock();// 编辑和发布文章editAndPublishArticle();System.out.println("文章编辑和发布成功");// 释放写锁rwLock.writeLock().unlock();} catch (InterruptedException e) {e.printStackTrace();} finally {// 关闭Redisson客户端redisson.shutdown();}}private static String getArticleContent() {// 模拟从数据库或缓存中获取文章内容的操作return "这是一篇文章的内容";}private static void editAndPublishArticle() {// 模拟编辑和发布文章的操作}
}
在这个示例中,首先创建了Redisson客户端,然后获取了一个读写锁(RReadWriteLock)。在代码中,首先获取了读锁,并读取文章内容,模拟了多个用户同时读取文章的场景。然后获取了写锁,并模拟了编辑和发布文章的操作。写锁在编辑和发布文章时保证了写入的原子性,并且会阻塞读取操作,直到写锁被释放。
具体原理源码可见
Redisson 实现分布式锁原理分析 - 知乎
Redis系列(二)Redisson分布式锁源码解析_redisson源码分析_白垩纪往事的博客-CSDN博客
相关文章:
谈谈Redis分布式锁
目录 一、回顾分布式锁 (一)理解分布式锁的定义 (二)分布式锁的约束条件 (三)分布式锁常见实现方式 基于数据库的分布式锁 基于缓存的分布式锁 基于分布式一致性算法的分布式锁 基于文件系统的分布…...
Redis的java客户端-RedisTemplate光速入门
一.创建springboot项目 二.引入2个依赖 <!-- redis依赖-->这个已经引入了,因为创建的时候勾选了<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><…...
格点数据可视化(美国站点的日降雨数据)
获取美国站点的日降雨量的格点数据,并且可视化 导入模块 from datetime import datetime, timedelta from urllib.request import urlopenimport cartopy.crs as ccrs import cartopy.feature as cfeature import matplotlib.colors as mcolors import matplotli…...
YoloV8改进策略:LSKNet加入到YoloV8中,打造更适合小目标的YoloV8
文章目录 摘要论文:LSKNet:大选择核网络在遥感目标检测中的应用1、简介2、相关工作2.1、遥感目标检测框架2.2、大核网络2.3、注意力/选择机制3、方法3.1、LSKNet架构3.2、大核卷积3.3、空间核选择4、实验4.1、数据集4.2、实现细节4.3、消融实验4.4、主要结果4.5、分析5、结论…...
力扣-303.区域和检索-数组不可变
Idea 需计算数组nums在下标right 和 left-1 的前缀和,然后计算两个前缀和的差即可。 需要注意的是,当left为0的时候,如果还是left-1则会发生数组访问越界错误。 AC Code class NumArray { public:vector<int> sum;NumArray(vector<…...
web:[极客大挑战 2019]LoveSQL
题目 打开页面显示如下 查看源代码,查到一个check.php,还是get传参 尝试账号密码输入 题目名为sql,用万能密码 1or 11# 或 admin or 11 给了一段乱码,也不是flag 查看字段数 /check.php?usernameadmin order by 3%23&pass…...
数据结构—快速排序(续)
引言:在上一篇中我们详细介绍了快速排序和改进,并给出了其中的一种实现方式-挖坑法 但其实快速排序有多种实现方式,这篇文章再来介绍其中的另外两种-左右指针法和前后指针法。有了上一篇挖坑法的启示,下面的两种实现会容易许多。 …...
Snapdragon Profiler分析Android GPU
Snapdragon Profiler(骁龙分析器)是一款性能分析软件,在Windows、 Mac、和 Linux平台上都可以运行,主要是用来分析使用了高通骁龙处理器的Android设备。 Snapdragon Profiler通过USB连接这些Android设备,开发者可以用…...
Cannot download sources:IDEA源码无法下载
问题 Swagger的相关包,无法看到注释; 在class文件的页面,点击下载源码,源码下载不了,IDEA报下面的错误。 报错 Cannot download sources Sources not found for: io.swagger.core.v3:swagger-annotations:2.2.9 解决…...
从零开始学习 Java:简单易懂的入门指南之IO字符流(三十一)
IO流之字符流 1. 字符流1.1 字符输入流【Reader】1.2 FileReader类构造方法读取字符数据 1.3 字符输出流【Writer】1.4 FileWriter类构造方法基本写出数据关闭和刷新写出其他数据 2. IO异常的处理JDK7前处理JDK7的处理JDK9的改进 3. 综合练习练习1:拷贝文件夹练习2&…...
监狱工具管理系统-监狱劳动工具管理系统
监狱劳动工具管理系统(智工具DW-S308)是依托互3D技术、云计算、大数据、RFID技术、数据库技术、AI、视频分析技术对工具进行统一管理、分析的信息化、智能化、规范化的系统。 当前各级监狱工器具管理更多的是借助于传统的人工管理方法和手段,数据的采集和录入一直以…...
蓄水池算法
题目: 假设有一组数据流元素有 N 个(事先不知道 N 具体值),我们希望选择 n 个样本(N > n),使用怎样的策略进行抽样可以使得数据流中每个元素被选择的概率恰为 n / N 结论: 创建大…...
作业 day4
完成父子进程通信...
erlang练习题(四)
题目一 传入列表 L1[K|]、L2[V|]、L3[{K,V}|_],L1和L2一一对应,L1为键列表,L2为值列表,L3为随机kv列表, 将L1和L2对应位合并成KV列表L4,再将L3和L4相加,相同key的value相加 如:L…...
YoloV5实时推理最短的代码
YoloV5实时推理最简单代码 import cv2 import torch# 加载YOLOv5模型 model torch.hub.load(ultralytics/yolov5, yolov5s)# 使用CPU或GPU进行推理 device cuda if torch.cuda.is_available() else cpu model.to(device)# 打开摄像头(默认摄像头) cap…...
Tensorflow、Pytorch和Ray(张量,计算图)
1.深度学习框架(Tensorflow、Pytorch) 1.1由来 可以追溯到2016年,当年最著名的事件是alphago战胜人类围棋巅峰柯洁,在那之后,学界普遍认为人工智能已经可以在一些领域超过人类,未来也必将可以在更多领域超过…...
TinyWebServer学习笔记-让程序跑起来
目标:通过这个HTTP项目熟悉网络编程 系统:Ubuntu20.04 首先,学习的第一步就是先让程序跑起来,使用git将项目下载到虚拟机内: git clone https://github.com/qinguoyi/TinyWebServer.git 提前把MySQL数据库安装好&am…...
_tkinter.TclError: no display name and no $DISPLAY environment variable 解决
启动kohya_ss时可能会发生错误: _tkinter.TclError: no display name and no $DISPLAY environment variable 解决办法: 1、apt-get install xvfb //安装xvfb // 启动虚拟显示器 2、Xvfb :99 -screen 0 1024x768x16 & export DISPLAY:99 ps aux…...
我出手了!
时光飞逝,程序员小灰这个微信公众号,已经运营整整7年时间了。 在这7年里,小灰输出过各种各样的文章和视频,有讲编程技术的,有讲职业规划的,有讲互联网行业新闻的,也有讲自己个人生活的。 不过&a…...
springboot的配置文件(properties和yml/yaml)
springboot的配置文件有两种格式分别是properties和yml/yaml 创建配置文件 在创建springboot项目时候,会默认生成application.properties这种格式 书写风格 端口 application.propertis server.port8080 application.yml server:port: 8080 连接数据库 applica…...
SLAM面试笔记(7) — Linux面试题
目录 问题1:Linux系统基本组件? 问题2:Linux和Unix有什么区别? 问题3:Linux下编译程序 问题4:gcc基本格式和常用指令 问题5:用什么命令查找内存和交换使用情况? 问题6…...
QUIC不是TCP的替代品
QUIC取代了TCP成为HTTP3的基础传输协议,不是因为QUIC能够取代TCP的所有应用场景,而是因为QUIC更适合HTTP的请求/响应业务模型。原文: QUIC Is Not a TCP Replacement TCP新规范(RFC 9293)的发布是网络界的一件大事,值得围绕这一主题发表第二篇…...
计算机竞赛 目标检测-行人车辆检测流量计数
文章目录 前言1\. 目标检测概况1.1 什么是目标检测?1.2 发展阶段 2\. 行人检测2.1 行人检测简介2.2 行人检测技术难点2.3 行人检测实现效果2.4 关键代码-训练过程 最后 前言 🔥 优质竞赛项目系列,今天要分享的是 行人车辆目标检测计数系统 …...
GPT系列模型解读:GPT-1
GPT系列 GPT(Generative Pre-trained Transformer)是一系列基于Transformer架构的预训练语言模型,由OpenAI开发。以下是GPT系列的主要模型: GPT:GPT-1是于2018年发布的第一个版本,它使用了12个Transformer…...
王杰国庆作业day3
父子进程对话 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <my_head.h> int main(int argc, const char *argv[]) {mkfifo("./fifo1",0664);mkfifo("./fifo2",0664);pid_t cpid fork();if(0 < cp…...
量子计算基础知识—Part1
1.什么是量子计算机? 量子计算机是基于量子力学原理构建的机器,采用了一种新的方法来处理信息,从而使其具有超强的功能。量子计算机使用Qubits处理信息。 2. 什么是量子系统? 一个量子系统指的是由量子力学规则描述和控制的物理…...
【PostgreSQL】【存储管理】表和元组的组织方式
外存管理负责处理数据库与外存介质(PostgreSQL8.4.1版本中只支持磁盘的管理操作)的交互过程。在PostgreSQL中,外存管理由SMGR(主要代码在smgr.c中)提供了对外存的统一接口。SMGR负责统管各种介质管理器,会根据上层的请求选择一个具体的介质管理器进行操作…...
VSCode安装图文详解教程
版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl 教程说明 本教程旨在详细介绍VSCode的安装过程及其注意事项。 下载VSCode 请在官方网站 https://code.visualstudio.com/ 下载https://code.visualstudio.com/至本地&…...
vscode 无法打开源文件
以下是c/c插件的intelligense设置情况: 解决办法: 重新安装vsode无用;重新下载mingw64,管用了!(我猜可能是之前换电脑移植文件的时候导致了部分文件丢失)...
1.8.C++项目:仿muduo库实现并发服务器之eventloop模块的设计
项目完整在: 文章目录 一、eventloop模块:进行事件监控,以及事件处理的模块二、提供的功能三、实现思想(一)功能(二)意义(三)功能设计 四、框架五、代码 一、eventloop模…...
政府网站建设需求方案/中国目前最好的搜索引擎
随便拿一个搜索引擎搜索一下“两列自适应布局”,都能得到很多种实现方法,这篇文章的重点是这些方法的底层逻辑是什么,他们是怎么得来的。 分析: 需要满足三个要求: ①两个盒子在同一行 ②右边的盒子需要占满剩下的空间…...
网站虚假备案/网址收录网站
标准容器 C容器分类:顺序容器,有序容器,无序容器。所有的容器都有一个基本的特性:容器保存元素采用的是值语义,也就是说,里面存储的是元素的副本、复件,而不是引用。 从这个基本的特性可以得出…...
aspx网站实例/seo整站优化费用
你的外部for循环条件给你带来了问题.这是你的循环: –for (int i 0; i < array[i].length; i)现在,当我达到值3时,您正在尝试访问数组[3] .length.这将抛出IndexOutOfBounds异常.由于每个内部数组的大小相同,您可以将循环更改为: –for (int i 0; i…...
做网站需要什么服务器配置/南宁百度seo推广
首先 继承是面向对象语言中最重要的一个概念 比方说 我现在有一个类 汽车类 现在我想要创在一个 法拉利类 首先法拉利他是车 它具备车型和制造商这样的属性 但是他可能具备一些新的属性 比如说 颜色 继承大白话的理解就是 你父亲生了你 ,你父亲(基…...
新校区建设网站管理规定/杭州疫情最新情况
如果版本开发测试过程中没有流程的约束,会出现什么样的情况?如果不管版本大小、不考虑版本特性强制使用标准流程约束,又会是什么样?我经常听到的抱怨是流程太厚重了,流程导致了版本开发周期变长、成本增加了。可是&…...