今天遇到了一个非常神奇的的问题,这是我的代码
核心逻辑是通过redisson加两个redis分布式锁,处理完业务以后再解锁
public void executeTaskWithLocks(String key1, String key2) {
CompletableFuture.runAsync(() -> {
RLock lock1 = redissonClient.getLock(redisKeyPrefix + key1);
RLock lock2 = redissonClient.getLock(redisKeyPrefix + key2);
try {
// 尝试获取第一个锁
if (!lock1.tryLock(5, 10, TimeUnit.SECONDS)) {
System.out.println("Lock1 is already in use for key: " + key1);
return;
}
System.out.println("Lock1 acquired for key: " + key1);
// 尝试获取第二个锁
if (!lock2.tryLock(5, 10, TimeUnit.SECONDS)) {
System.out.println("Lock2 is already in use for key: " + key2);
return;
}
System.out.println("Lock2 acquired for key: " + key2);
// 模拟 Redis 操作
String redisKey = "example:data:" + key1;
redisTemplate.opsForValue().set(redisKey, "Processing", 10, TimeUnit.MINUTES);
System.out.println("Data set in Redis for key: " + redisKey);
// 模拟任务处理
Thread.sleep(2000); // 模拟耗时操作
// 更新 Redis 数据
redisTemplate.opsForValue().set(redisKey, "Completed", 10, TimeUnit.MINUTES);
System.out.println("Data updated in Redis for key: " + redisKey);
} catch (Exception e) {
System.err.println("Exception occurred while executing task: " + e.getMessage());
e.printStackTrace();
} finally {
// 释放锁
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
System.out.println("Lock1 released for key: " + key1);
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
System.out.println("Lock2 released for key: " + key2);
}
}
})
}
但是在环境上执行的时候,每次打印完Lock2 aquired,就不再打印 Data set in Redis
而是直接打印了finally里面的Lock1 released和Lock2 released
且没有打印任何异常信息
处理流程
1.首先就是加各种异常日志,结果是干干净净,无论我怎么加日志,就是走不到Data set in Redis这一行
2.然后就在本地加了断点,神奇的事情发生了,断点过了Lock2 Aquired这一行,头也不回的进了finally
这对于我这种小菜鸡来说已经属于灵异事件了
3.百思不得其解,只能去请GPT,GPT首先是让我完善下线程池配置
增加了核心线程数,改用了linkedBlockedList,重试,依然如此
看来也不是线程被异常关闭的问题啊
4.那就关注一下为什么没有任何异常就退出的问题
然后就在末尾加上了一行线程异常报错打印
public void executeTaskWithLocks(String key1, String key2) {
CompletableFuture.runAsync(() -> {
RLock lock1 = redissonClient.getLock(redisKeyPrefix + key1);
RLock lock2 = redissonClient.getLock(redisKeyPrefix + key2);
try {
// 尝试获取第一个锁
if (!lock1.tryLock(5, 10, TimeUnit.SECONDS)) {
System.out.println("Lock1 is already in use for key: " + key1);
return;
}
System.out.println("Lock1 acquired for key: " + key1);
// 尝试获取第二个锁
if (!lock2.tryLock(5, 10, TimeUnit.SECONDS)) {
System.out.println("Lock2 is already in use for key: " + key2);
return;
}
System.out.println("Lock2 acquired for key: " + key2);
// 模拟 Redis 操作
String redisKey = "example:data:" + key1;
redisTemplate.opsForValue().set(redisKey, "Processing", 10, TimeUnit.MINUTES);
System.out.println("Data set in Redis for key: " + redisKey);
// 模拟任务处理
Thread.sleep(2000); // 模拟耗时操作
// 更新 Redis 数据
redisTemplate.opsForValue().set(redisKey, "Completed", 10, TimeUnit.MINUTES);
System.out.println("Data updated in Redis for key: " + redisKey);
} catch (Exception e) {
System.err.println("Exception occurred while executing task: " + e.getMessage());
e.printStackTrace();
} finally {
// 释放锁
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
System.out.println("Lock1 released for key: " + key1);
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
System.out.println("Lock2 released for key: " + key2);
}
}
}).exceptionally(e -> {
System.err.println("Exception in async task: " + e.getMessage());
e.printStackTrace();
return null;
});
}
结果还真的打印出了异常:
2025-02-27 17:46:28 [fullTextTranslationExecutorService-pool-1] ERROR o.p.x.s.b.a.FullTextTranslationStreamService [translateArticle] Exception in async task
java.util.concurrent.CompletionException: java.lang.NoClassDefFoundError: org/springframework/data/redis/connection/RedisStreamCommandsXAddOptions at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273) at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280) at java.util.concurrent.CompletableFutureXAddOptionsatjava.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)atjava.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)atjava.util.concurrent.CompletableFutureAsyncRun.runcapture(CompletableFuture.java:1643) at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: java.lang.NoClassDefFoundError: org/springframework/data/redis/connection/RedisStreamCommands$XAddOptions at org.redisson.spring.data.connection.RedissonStreamCommands.xAdd(RedissonStreamCommands.java:65) at org.springframework.data.redis.connection.DefaultedRedisConnection.xAdd(DefaultedRedisConnection.java:451) at org.springframework.data.redis.core.DefaultStreamOperations.lambda$add$1(DefaultStreamOperations.java:135) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:228) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:188) at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96) at org.springframework.data.redis.core.DefaultStreamOperations.add(DefaultStreamOperations.java:135) at org.springframework.data.redis.core.StreamOperations.add(StreamOperations.java:110) at org.springframework.data.redis.core.StreamOperations.add(StreamOperations.java:97) at org.pjlab.xscholar.service.business.article.FullTextTranslationStreamService.lambda$startTranslationTask$3(FullTextTranslationStreamService.java:104) at java.util.concurrent.CompletableFuture$AsyncRun.runcapture(CompletableFuture.java:1640)
... 4 common frames omitted
在这个异常里面,可以看到,是因为找不到Xadd这个类
事情到这里终于水落石出了,是因为执行redis操作的时候因为版本的原因,不支持写stream
但这里又出现了一个问题,其实我在调试分布式锁之前,stream读写一直是没问题的
问题是在redisson引入以后导致的
但是我写redis stream用的是redisTemplate啊,跟redisson有什么关系呢?
5. 进一步研究发现,引入redisson后,它默认会覆盖掉原本的redis连接池
使用redisson版本的连接池,但是我使用的spring boot 2.2.X版本的starter
只支持3.16.X版本的redisson
3.16.X版本的redisson使用的spring-data-redis里,是不包含stream操作相关方法的
因为我们现在只需要用redisson做分布式锁
所以使用的方案是,强制覆盖了redis的连接池,使用原生的lettuce连接池
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(redisProperty.getHost());
configuration.setPort(redisProperty.getPort());
configuration.setPassword(redisProperty.getPassword());
configuration.setDatabase(redisProperty.getDatabase());
return new LettuceConnectionFactory(configuration);
}
覆盖后重启,报错消失了,业务流程也正常了
----------------------------分割线------------------------
到这里,终于解决了这个灵异事件
对线程池的理解,的确是一个程序员需要重点修炼的部分
到目前我仍然没能找到为什么这里完全没有经过我的catch Exception,而是直接进了Finally
难道是因为抛出的不是Exception?
我们看下之前那段异常
的确,报错类型为java.lang.NoClassDefFoundError, 属于Error,而不是Exception
也是导致异常没有被正确捕获的根本原因
也是给我上了一课,所以并不是捕获了Exception就高枕无忧的
那为什么在线程池里就被异常捕获了呢?
public CompletableFuture<T> exceptionally( Function<Throwable, ? extends T> fn) { return uniExceptionallyStage(fn); }
看下源码就可以知道,还是得捕获Throwable啊
所以如果下次遇到这种灵异事件,可以试试捕获Throwable,看看有什么猫腻
------------------------------分割线----------------------------
这里还涉及到一个问题,我之前使用的是RedisLockRegistry
后来换成Redisson,是因为RedisLockRegistry其实并不完善
RedisLockRegistry本身是通过setNx+一个自身的reentrantlock实现的
但是有个潜在的问题就是,如果说redis里的key超时了,但这之前没有主动释放锁
就会导致unlock失败,且服务内存中的reentrantlock也无法被释放
导致就算redis里没有这个key,其他线程也始终无法加上这把锁
所以如果任务结束时间无法预估,只能用一个绝对长的时间来保证解锁的正常进行
而redisson解决了这个问题,主要是通过watchdog机制,在任务执行完之前
不断地刷新redis里面的key来实现
因此我选择切换到了redis