线程莫名终止

今天遇到了一个非常神奇的的问题,这是我的代码

核心逻辑是通过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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值