干货分享,感谢您的阅读!
在这个高度互联的数字时代,程序员们就像是在一场无尽的“抓娃娃机”游戏中,努力争取每一个想要的对象——无论是数据、资源还是服务。可是,和抓娃娃机一样,如果没有一些策略,最终的结果常常是空手而归。于是,分布式锁如同一位英俊的骑士,骑着“Redis”这匹快速的战马,挺身而出,帮助我们在并发的世界中安然无恙。
想象一下,如果没有分布式锁,多个程序就像一群不速之客挤在同一间派对上,每个人都想要那个“最终的披萨”,结果只会引发一场激烈的争夺战,而我们都知道,最终赢家往往是最爱抢食的那位——或者是派对的保安!而Redisson,则是我们掌控派对秩序的超级助手,确保每一位客人都能有序享用美食而不至于打乱氛围。
所以,准备好迎接这场关于分布式锁的奇妙旅程了吗?让我们一同探讨Redis和Redisson的世界,发现如何在程序的派对中优雅地舞动,确保没有人会错过任何美味的披萨!
一、回顾分布式锁
(一)理解分布式锁的定义
分布式锁是一种在分布式计算环境中用于控制多个节点(或多个进程)对共享资源的访问的机制。在分布式系统中,多个节点可能需要协调对共享资源的访问,以防止数据的不一致性或冲突。分布式锁允许多个节点在竞争访问共享资源时进行同步,以确保只有一个节点能够获得锁,从而避免冲突和数据损坏。以下是一些关键概念和理解:
锁:锁是一种同步机制,它可以被获取和释放。当一个节点获得锁时,它可以执行需要访问共享资源的操作,其他节点必须等待直到锁被释放才能获得锁。
分布式环境:在分布式系统中,多个节点分布在不同的物理位置或计算机上,它们通过网络相互通信。这增加了在多个节点之间协调共享资源访问的复杂性。
锁的种类:
- 互斥锁:在分布式环境中,互斥锁确保在任何给定时刻只有一个节点可以持有锁。其他节点必须等待锁被释放。
- 读写锁:允许多个节点同时读取共享资源,但只允许一个节点写入共享资源。这可以提高并发性能,但需要更复杂的管理。
锁的实现方式:分布式锁可以使用不同的实现方式,如基于数据库、基于缓存、基于分布式一致性算法(例如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] then
return redis.call("DEL", KEYS[1])
else
return 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; // 假设购买商品价格为100
if (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)。在代码中,首先获取了读锁,并读取文章内容,模拟了多个用户同时读取文章的场景。然后获取了写锁,并模拟了编辑和发布文章的操作。写锁在编辑和发布文章时保证了写入的原子性,并且会阻塞读取操作,直到写锁被释放。
四、总结
在今天的数字化世界中,分布式系统的广泛应用使得我们面临着前所未有的并发挑战。在这场技术的盛宴中,分布式锁成为我们维护数据一致性和防止资源争用的重要工具。通过对Redis和Redisson的深入探索,我们了解到分布式锁不仅可以有效解决并发访问的问题,还能极大提升系统的稳定性和可靠性。
从基本的锁机制到先进的锁管理策略,Redisson为我们提供了一种灵活且高效的解决方案,让开发者能够轻松应对复杂的分布式环境。通过合理配置和有效使用分布式锁,我们能够在保证系统性能的同时,避免因资源竞争而导致的数据混乱。
最后,尽管分布式锁不是“银弹”,但它无疑是我们在分布式系统中保障数据安全与一致性的一把利器。未来,随着技术的不断进步,分布式锁的应用和优化将愈发重要,值得每一位开发者深入理解和实践。让我们继续在这个领域探索,寻找更多提高系统性能和稳定性的创新方法,为我们的应用带来更大的价值!
具体原理源码可见
Redisson 实现分布式锁原理分析 - 知乎
Redis系列(二)Redisson分布式锁源码解析_redisson源码分析_白垩纪往事的博客-CSDN博客
版权归原作者 张彦峰ZYF 所有, 如有侵权,请联系我们删除。