面试官:Redis分布式锁如何实现,为什么推荐使用 SET命令而不是 SETNX命令?使用过程中还存在哪些问题?
Redis分布式锁的实现原理主要依赖于Redis的原子指令来确保在同一时刻只有一个进程(或线程)能够获取锁。
1. Redis分布式锁的实现原理
Redis分布式锁的实现通常涉及以下几个关键步骤:
- 尝试获取锁:客户端使用Redis的某个命令尝试设置一个特定的键值对,这个键通常代表锁,而值可以是客户端的唯一标识符或随机生成的字符串。如果设置成功,则表示客户端获取到了锁;如果设置失败(因为键已存在),则表示锁已被其他客户端占用。
- 处理业务逻辑:在获取到锁之后,客户端可以安全地处理需要同步的业务逻辑。
- 释放锁:处理完业务逻辑后,客户端需要释放锁,这通常是通过删除之前设置的键值对来实现的。
在实现过程中,为了确保锁的安全性和可靠性,需要考虑以下几点:
- 锁的过期时间:为了防止死锁,必须为锁设置一个合理的过期时间。这样,即使持有锁的客户端出现异常或崩溃,锁也不会被永久占用。
- 锁的原子性释放:释放锁时,必须确保操作的原子性,以防止在检查锁持有者和删除锁之间发生竞态条件。这通常通过使用Lua脚本来实现。
伪代码如下:
try{
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
} finally {
unlock(lockKey);
}
2. 使用SET命令而非SETNX实现分布式锁的原因
虽然SETNX命令也可以用于实现分布式锁(当键不存在时设置键值对),但推荐使用SET命令的原因如下:
SET命令支持NX(Not Exist,仅当键不存在时设置)和PX(expire time in milliseconds,设置键的过期时间,单位为毫秒)选项,这使得客户端可以在一个原子操作中完成获取锁和设置过期时间的两个步骤。而SETNX命令则需要与EXPIRE命令配合使用,这增加了操作的复杂性和出错的可能性。
if (jedis.setnx(lockKey, val) == 1) {
jedis.expire(lockKey, timeout);
}
上述代码使用SETNX命令时,如果客户端在获取锁后崩溃或网络中断,而未能及时执行EXPIRE命令设置过期时间,那么锁可能会被永久占用,导致死锁问题。
假如在高并发场景中,有大量的lockKey加锁成功了,但不会失效,有可能直接导致redis内存空间不足。而使用SET命令的NX和PX选项则可以在获取锁的同时设置过期时间,从而避免了这一问题。
使用redis分布式锁的过程中还可能存在如下问题:
(1) A线程释放了B线程的锁
假如线程A加锁成功了,锁的名字叫做lockKey,但是由于业务功能耗时时间很长,超过了设置的超时时间,这时候redis会自动释放锁。此时,并发的线程B就会给lockKey加锁成功了,执行它的业务操作。恰好这个时候,线程A执行完了业务功能,并且A在finally方法中释放了锁lockKey,导致线程B的锁,被线程A释放了。
这个时候如果线程C再获取锁就会获取成功,而实际上B还没有结束它在临界区的任务。
解决方法:
① 使用唯一标识
在获取锁时,每个进程或线程都应该使用一个唯一标识(如UUID)来标记锁。这个唯一标识将存储在Redis中作为锁的value值。在释放锁时,进程或线程需要先检查Redis中锁的值是否与自己的唯一标识匹配,如果匹配则释放锁。
if (jedis.get(lockKey).equals(requestId)) {
jedis.del(lockKey);
return true;
}
return false;
② 使用Lua脚本
Lua脚本是Redis提供的一种在服务器端执行多个命令的方式。通过将“检查锁值”和“删除锁”这两个操作封装在一个Lua脚本中,可以确保这两个操作是原子的,即它们要么同时成功,要么同时失败。这样可以避免在并发情况下出现释放了别人的锁的问题。
③ 合理设置锁的过期时间
锁的过期时间应该根据任务的预期执行时间来合理设置。如果任务执行时间过长,而锁的过期时间设置得过短,那么锁可能会在任务完成之前自动失效。
(2) 并发时出现大量失败请求
问题描述:
在并发环境下,多个进程或线程可能会同时尝试获取同一个锁来访问共享资源。如果有1万的请求同时去竞争那把锁,可能只有一个请求是成功的,其余的9999个请求都会失败。
解决方法:
引入重试机制:在并发环境下,为了处理这些失败请求,可以引入重试机制,即进程或线程当获取锁失败时,可以等待一段时间后再尝试获取锁。通过合理设置重试次数和重试间隔,可以在一定程度上提高并发请求的成功率,而且不同线程或进程之间的等待时间应该使用随机时间,以免线程同时被唤醒并竞争锁。
try {
Long start = System.currentTimeMillis();
while(true) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
// do something
return true;
}
long time = System.currentTimeMillis() - start;
if (time>=timeout) {
return false;
}
try {
int randomSleepTime = 10 + random.nextInt(90);
Thread.sleep(randomSleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally{
unlock(lockKey,requestId);
}
return false;
面试官:使用过 Redisson 么?它是如何实现分布式锁的?解释一下Redisson的watch dog机制?你还知道Redisson的哪些锁?
Redisson是一个在Redis基础上实现的Java驻内存数据网格(In-Memory Data Grid,IMDG),它不仅提供了对分布式和可伸缩数据结构的支持,还提供了多种分布式服务,包括但不限于分布式锁、集合、映射、计数器、发布/订阅消息等。
Redisson实现分布式锁主要依赖于Redis的相关命令和Lua脚本的原子性操作。以下是Redisson分布式锁实现的关键点:
- Redis的SETNX命令:Redisson的分布式锁实现基于Redis的SETNX(SET if Not eXists)命令。当一个节点试图获取锁时,它会使用SETNX命令在Redis中设置一个特定的键值对。如果键不存在(表示锁未被占用),则设置成功,该节点获取锁;如果键已经存在(表示锁已被其他节点占用),则设置失败,该节点无法获取锁。
- 加锁机制:通过Lua脚本实现原子性加锁操作,确保在多个客户端同时请求锁时,只有一个客户端能够成功获取锁。Lua脚本的原子性保证了在执行过程中不会被其他命令打断,从而避免了竞态条件。
- 锁互斥机制:利用Redis的数据结构(如哈希表)和唯一性标识(如UUID+threadId)来确保锁的互斥性。每个获取锁的客户端都会生成一个唯一的标识,并将其与锁关联起来。在释放锁时,只有持有该唯一标识的客户端才能成功释放锁。
- 锁续期机制:通过看门狗(Watchdog)定时任务自动续锁,防止锁因超时而被其他客户端获取。看门狗会定期检查锁的状态,如果锁仍然被持有,则将其有效期重置为一个较长的值。这样,即使业务操作需要较长时间,锁也不会因为超时而被释放。需要注意的是,当业务操作完成后,需要手动释放锁以避免资源泄露。
- 可重入加锁机制:允许同一个客户端在同一个线程中多次获取同一个锁,而不会导致死锁。Redisson使用Hash结构来存储线程信息和重入次数,从而实现了可重入锁的功能。
- 锁释放机制:在释放锁时,需要验证锁的持有者身份,确保只有锁的持有者才能释放锁。这是通过比较释放锁的客户端的唯一标识与锁关联的唯一标识来实现的。如果匹配成功,则释放锁;否则,释放操作将失败。
1. Watch Dog机制
只要线程一加锁成功,就会启动一个 watch dog 看门狗,它是一个后台线程,会每隔 10 秒检查一下,如果线程 1 还持有锁,那么就会不断的延长锁 key 的生存时间。因此,Redisson 就是使用 watch dog机制解决了锁过期释放,业务没执行完问题。
2. Redisson可重入锁
Redisson分布式锁默认是可重入锁,它允许同一个线程在获得锁之后,如果再次尝试获得该锁,能够成功获取而不会进入死锁状态。这种特性使得同一个线程可以安全地在多个方法或代码块中使用锁。
以下是对Redisson可重入锁的详细介绍以及代码示例:
可重入性:
- 当一个线程已经持有锁时,它可以多次重入该锁,而不会被阻塞。Redisson通过递增锁的持有计数来实现这一功能。
- 第一次加锁时,Redisson会在Redis中设置一个键(例如锁的标识),并将该线程的唯一标识符(如UUID)作为锁的值。
- 如果同一个线程再次请求同一个锁,Redisson会检查Redis中该锁的值。如果值与当前线程的UUID相同,即当前线程已经持有该锁,那么Redisson会递增持有计数。
释放锁:
- 当线程释放锁时,Redisson会检查当前线程的持有计数。
- 如果持有计数大于1,则将计数递减而不是删除锁。
- 当持有计数减为0时,锁会被删除,表示该线程完全释放了锁。
自动过期解锁:
- Redisson的可重入锁还支持自动过期解锁功能,即可以在加锁时指定一个过期时间。如果超过这个时间锁还没有被释放,那么锁会自动被删除,从而避免死锁的发生。
以下是一个使用Redisson可重入锁的Java代码示例:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonReentrantLockExample {
public static void main(String[] args) {
// 配置Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
// 获取可重入锁对象
RLock lock = redissonClient.getLock("myLock");
try {
// 第一次加锁
lock.lock();
System.out.println("第1次加锁成功");
// 执行需要同步的代码块
// ...
// 第二次加锁(同一线程)
lock.lock();
System.out.println("第2次加锁成功");
// 执行需要同步的代码块
// ...
// 第一次释放锁
lock.unlock();
System.out.println("第1次释放锁");
// 执行其他代码
// ...
// 第二次释放锁
lock.unlock();
System.out.println("第2次释放锁,锁被完全释放");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 确保在程序结束时关闭Redisson客户端
redissonClient.shutdown();
}
}
}
3. Redisson公平锁
Redisson的公平锁是一种基于Redis实现的分布式公平锁,它确保所有线程按照请求锁的顺序来获得锁,从而避免了某些线程长时间占用资源而导致其他线程一直无法获得锁的问题。与Redisson的可重入锁相比,公平锁增加了一个公平的机制,使得锁的分配更加公平和有序。
- 请求队列:Redisson的公平锁在Redis中维护了一个请求队列,所有请求锁的线程都会在这个队列中排队。
- 锁分配:当锁被释放时,Redisson会检查请求队列中的线程,并按照请求的顺序将锁分配给队列中的第一个线程。
以下是一个使用Redisson公平锁的Java代码示例:
import org.redisson.Redisson;
import org.redisson.api.RFairLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonFairLockExample {
public static void main(String[] args) {
// 配置Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
// 获取公平锁对象
RFairLock fairLock = redissonClient.getFairLock("myFairLock");
// 模拟多个线程竞争锁
Runnable task = () -> {
try {
// 请求锁
fairLock.lock();
try {
// 获得锁后执行的代码
System.out.println(Thread.currentThread().getName() + " 获得了锁");
// 模拟任务执行时间
Thread.sleep(2000);
} finally {
// 释放锁
fairLock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了锁");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
};
// 创建并启动多个线程
for (int i = 0; i < 5; i++) {
new Thread(task, "Thread-" + i).start();
}
// 确保在程序结束时关闭Redisson客户端
Runtime.getRuntime().addShutdownHook(new Thread(() -> redissonClient.shutdown()));
}
}
4. Redisson读写锁
Redisson的读写锁(ReadWriteLock)是一种允许多个读线程同时访问共享资源,但写线程独占访问的锁机制。这种锁在读取操作远多于写入操作的场景下特别有用,因为它可以提高并发性能,同时确保数据的一致性。
Redisson的读写锁实现了java.util.concurrent.locks.ReadWriteLock接口,因此它提供了与Java标准库中的读写锁相似的API。Redisson的读写锁是基于Redis的分布式实现,这意味着它可以在分布式系统中工作,确保多个服务实例之间的读写同步。
以下是一个使用Redisson读写锁的示例代码:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RWLock;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonReadWriteLockExample {
public static void main(String[] args) {
// 配置Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 创建Redisson客户端
RedissonClient redissonClient = Redisson.create(config);
// 获取读写锁
RWLock rwLock = redissonClient.getReadWriteLock("myReadWriteLock");
// 读取操作示例
new Thread(() -> {
RLock readLock = rwLock.readLock();
try {
// 获取读锁
readLock.lock();
// 执行读取操作
System.out.println("Read operation started");
// 模拟读取操作耗时
TimeUnit.SECONDS.sleep(2);
System.out.println("Read operation completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放读锁
readLock.unlock();
}
}).start();
// 写入操作示例
new Thread(() -> {
RLock writeLock = rwLock.writeLock();
try {
// 获取写锁
writeLock.lock();
// 执行写入操作
System.out.println("Write operation started");
// 模拟写入操作耗时
TimeUnit.SECONDS.sleep(2);
System.out.println("Write operation completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放写锁
writeLock.unlock();
}
}).start();
}
}
5. Redisson闭锁
Redisson的闭锁(CountDownLatch)是一种同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程等待。这个机制特别适用于等待直到一组并行操作全部完成时才继续执行的场景。Redisson的闭锁实现了java.util.concurrent.CountDownLatch接口,但它是在分布式环境中工作的,这意味着它可以在多个服务实例之间协调等待/通知操作。
以下是一个使用Redisson闭锁的示例代码:
import org.redisson.Redisson;
import org.redisson.api.RCountDownLatch;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonCountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
// 配置Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 创建Redisson客户端
RedissonClient redissonClient = Redisson.create(config);
// 创建一个计数为3的闭锁
RCountDownLatch latch = redissonClient.getCountDownLatch("myCountDownLatch");
latch.trySetCount(3);
// 启动三个线程,每个线程在完成工作时减少闭锁的计数
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 模拟工作耗时
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " - Work done, count down.");
// 减少闭锁的计数
latch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
// 等待直到闭锁的计数达到0
System.out.println("Main thread - Waiting for all threads to complete.");
latch.await();
System.out.println("Main thread - All threads have completed.");
// 关闭Redisson客户端
redissonClient.shutdown();
}
}
面试官:什么是Redlock算法,说说它的核心思想?
Redis 一般都是集群部署的,假设数据在主从同步过程,主节点挂了,Redis分布式锁就可能会失效。
如果线程一在 Redis 的 master 节点上拿到了锁,但是加锁的 key 还没同步到slave 节点。恰好这时,master 节点发生故障,一个 slave 节点就会升级为master 节点。线程二就可以获取同个 key 的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis 作者 antirez 提出一种高级的分布式锁算法:Redlock。
Redlock 核心思想是这样的:搞多个 Redis master 部署,以保证它们不会同时宕掉。并且这些 master 节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master 实例上,是与在 Redis 单实例使用相同方法来获取和释放锁。
我们假设当前有 5 个 Redis master 节点,在 5 台服务器上面运行这些 Redis实例。
RedLock 的实现步骤如下:
- 获取当前时间,以毫秒为单位。
- 按顺序向 5 个 master 节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为 10 秒,则超时时间一般在 5-50 毫秒之间,我们就假设超时时间是 50ms 吧)。如果超时,跳过该 master 节点,尽快去尝试下一个 master 节点。
- 客户端使用当前时间减去开始获取锁时间(即步骤 1 记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是 5/2+1=3 个节点)的Redis master 节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
如果取到了锁,key 的真正有效时间就变啦,需要减去获取锁所使用的时间。如果获取锁失败(没有在至少 N/2+1 个 master 实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的 master 节点上解锁(即便有些 master 节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。
Redlock算法的优势包括:
- 安全性:通过在多个Redis节点上尝试获取锁来提高安全性。
- 高可用性:即使在部分Redis实例不可用或网络分割的情况下,也能保证锁的正确性和系统的可用性。
- 防止单点故障:大大降低了单点故障对分布式系统的影响。
Redlock算法的劣势:
- 部署成本:需要维护多个Redis实例,增加了部署成本。
- 网络延迟:加锁和释放锁的操作需要同时与多个Redis实例通信,可能导致较高的网络延迟。
- 时钟漂移:Redlock算法假设Redis节点的时钟一致。如果节点时钟漂移较大,可能导致锁的有效期计算错误。
面试官:Redis事务的原理是怎样的,如何使用?Redis事务是否支持回滚?Redis事务的隔离性、原子性、持久性和一致性是如何实现的?
Redis事务主要包含以下几个命令和阶段:
- MULTI:该命令用于开启一个事务,它标志着执行该命令的客户端从非事务状态切换至事务状态。在MULTI命令之后,客户端发送的命令不会被立即执行,而是被放入一个事务队列中等待执行。
- 命令入队:在MULTI命令开启事务后,客户端发送的命令会被放入事务队列中,而不是立即执行。这些命令在队列中保持其发送的顺序。
- EXEC:该命令用于执行事务队列中的所有命令。当客户端发送EXEC命令时,Redis会按照事务队列中命令的顺序依次执行它们,并返回每个命令的执行结果。
- DISCARD:该命令用于取消事务,即清空事务队列并放弃执行事务中的所有命令。
1. 事务的原子性
Redis事务在一定程度上保证了原子性,但有一定的条件限制:
当命令入队时报错(如语法错误),Redis会放弃事务执行,从而保证原子性。
redis> MULTI
OK
redis> SET msg "other msg"
QUEUED
redis> wrongcommand ### 故意写错误的命令
(error) ERR unknown command 'wrongcommand'
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
redis> GET msg
"hello world"
当命令入队时正常,但执行EXEC命令后报错(如运行时错误,如操作了错误类型的键),Redis不会回滚已经执行的命令,而是继续执行事务队列中的剩余命令。这意味着在这种情况下,Redis事务的原子性无法得到保证。
redis> MULTI
OK
redis> SET msg "other msg"
QUEUED
redis> SET mystring "I am a string"
QUEUED
redis> HMSET mystring name "test"
QUEUED
redis> SET msg "after"
QUEUED
redis> EXEC
1) OK
2) OK
3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
4) OK
redis> GET msg
"after"
2. 事务的隔离性
Redis并没有传统数据库中的事务隔离级别概念。然而,在并发场景下,Redis事务之间可以做到一定程度的互不干扰:
- 在EXEC命令执行前,Redis key仍然可以被其他客户端修改。此时,可以使用WATCH机制来实现乐观锁的效果。WATCH命令可以监控一个或多个键,在事务执行前如果这些键被修改了,那么事务将被中断。
- 由于Redis是单线程执行操作命令的,所以在EXEC命令执行后,Redis会保证命令队列中的所有命令按顺序执行完。这样可以保证在当前事务执行期间,不会有其他客户端的命令插入到事务命令序列中。
3. 事务的持久性与一致性
- 持久性:Redis事务的持久性取决于Redis的持久化配置模式。如果没有配置RDB或AOF,事务的持久性无法保证。即使使用了RDB或AOF模式,也可能存在数据丢失的情况。因此,Redis事务的持久性并不能得到完全保证。
- 一致性:一致性是指事务只能将数据库从一个有效状态带到另一个有效状态,同时保持数据库的完整性约束。在Redis事务中,如果某个命令执行失败,它不会影响其他命令的执行(除非使用了WATCH机制)。因此,这可能导致数据库状态的不一致。因此,开发者需要在事务执行出错后自行处理,以恢复数据库到一致状态。
面试官:Redis中的WATCH命令有什么作用?它是如何实现乐观锁的?Redis事务与Lua脚本的关系是什么?它们之间有哪些异同点?
Redis中的WATCH命令具有关键作用,主要用于实现乐观锁机制。以下是关于WATCH命令的作用及其如何实现乐观锁的详细解释:
1. WATCH命令的作用
WATCH命令的主要作用是对Redis中的某个或某些键进行监视。当这些被监视的键在事务执行之前被其他客户端修改时,事务将会被中断,从而避免脏数据的问题。这种机制确保了事务的一致性。
2. WATCH命令实现乐观锁的原理
监视键的变化:
- 客户端使用WATCH命令指定需要监视的一个或多个键。
- 被WATCH的键会被记录在内存中,与当前客户端关联起来,直到事务执行完成或者取消。
事务的执行与检查:
- 如果没有变化,Redis会按顺序执行事务队列中的所有命令。
- 如果发生了变化,Redis会中断事务的执行,不执行事务队列中的任何命令,并返回一个空结果给客户端。
- 在事务开始(通过MULTI命令)之后,客户端可以执行一系列Redis命令,但这些命令会被放入事务队列中,并不会立即执行。
- 当客户端发送EXEC命令尝试执行事务时,Redis会首先检查被WATCH的键自WATCH命令执行后是否发生了变化。
乐观锁的实现:
- 乐观锁基于“假设最好的情况会发生”的思想,即假设在事务执行期间,被监视的键不会被其他客户端修改。
- 当使用WATCH命令监视键时,就相当于设置了一个检查点。如果在事务提交(EXEC命令)之前,这些键被其他客户端修改了,那么事务就会因为检查失败而被中断,从而实现了乐观锁的效果。
事务的取消与重新监视:
- 如果客户端在事务提交之前决定取消事务,可以使用DISCARD命令。此时,WATCH的效果也会失效。
- 如果事务因为被监视的键发生变化而被中断,客户端可以选择重新监视这些键并尝试重新执行事务。
3. WATCH命令的注意事项
- WATCH命令的执行是立即的,但具体的监视行为是延迟到EXEC命令被执行时才会起作用。
- 可以使用UNWATCH命令手动取消对所有被WATCH键的监视。
- 在Redis的Cluster模式中,WATCH命令可能无效,因为Cluster模式涉及多个节点之间的数据同步和分片。
下面是watch的使用示例。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.WatchMonitor;
public class RedisWatchExample {
public static void main(String[] args) {
// 连接到Redis服务器
Jedis jedis = new Jedis("localhost", 6379);
// 要监视的键
String watchedKey = "myKey";
// 初始值设置(仅用于示例初始化)
jedis.set(watchedKey, "initialValue");
while (true) {
try (WatchMonitor monitor = jedis.watch(watchedKey)) {
// 开始事务
Transaction tx = jedis.multi();
// 获取当前值(注意:这个值在EXEC之前可能是过时的,如果其他客户端修改了它)
String currentValue = jedis.get(watchedKey);
System.out.println("Current value before transaction: " + currentValue);
// 假设我们要在值后面追加一些内容
String newValue = currentValue + "_appended";
// 在事务中设置新值
tx.set(watchedKey, newValue);
// 尝试执行事务
tx.exec();
System.out.println("Transaction succeeded, new value set to: " + newValue);
break; // 如果事务成功,跳出循环
} catch (Exception e) {
// 如果捕获到异常(通常是jedis.exceptions.JedisDataException: WATCHED KEY MODIFIED),表示事务期间键被修改
System.out.println("Transaction failed due to watched key being modified. Retrying...");
// 这里可以选择重试或者其他错误处理逻辑
}
// 注意:在try-with-resources语句中,monitor会自动unwatch,但在catch块中退出循环时不会执行到那里。
// 在实际应用中,你可能需要在finally块中显式调用jedis.unwatch()以确保资源释放,但在这个简单示例中不是必需的。
// 因为jedis连接在整个main方法期间都保持打开状态,并且我们在每次循环迭代开始时都会重新调用watch。
}
// 关闭Jedis连接
jedis.close();
}
}
4. Redis事务 和 Lua脚本异同点
实现方式:
- Redis事务是基于乐观锁的,它使用WATCH命令来监视一个或多个键,如果在事务执行之前这些键被其他客户端修改了,那么事务将被中断。事务的执行是通过MULTI、EXEC等命令来实现的。
- Lua脚本则是将一系列Redis命令嵌入到Lua语言中,然后在Redis服务器上一次性执行。Lua脚本的执行是通过EVAL或EVALSHA命令来实现的。
灵活性:
- Redis事务相对简单,它只能按照固定的顺序执行一系列预定义的命令,不能包含复杂的逻辑判断或循环结构。
- Lua脚本则更加灵活,它可以使用Lua语言的全部特性,包括条件判断、循环结构、函数定义等。这使得Lua脚本能够执行更加复杂的操作,并能够处理各种异常情况。
性能:
- 在性能方面,Lua脚本通常比Redis事务更具优势。因为Lua脚本是在Redis服务器上执行的,避免了客户端与服务器之间的多次往返通信开销。此外,Lua脚本可以利用Redis的单线程特性,确保一系列命令以极高的效率执行。
- 然而,需要注意的是,如果Lua脚本中包含大量复杂的逻辑或数据操作,可能会导致执行时间过长,从而影响Redis服务器的性能。因此,在使用Lua脚本时,需要权衡其灵活性和性能之间的关系。