SpringBoot項目中創建大量redisMessageListenerContailner-X線程導致內存溢出問題分析及解決方案

具體問題描述:

項目採用的spring cloud微服務架構,使用spring session(redis存儲方式)達到各微服務之間session共享,即項目啓動內會添加

@EnableRedisHttpSession註解。

其中有個發郵件的公共服務mail-service,有個業務模塊每天執行定時任務會調用郵件服務,發送大量郵件,就會導致創建大量redisMessageListenerContailner-X線程,最終導致郵件服務內存溢出!

原因分析:

在Spring Session(redis)的配置類源碼中(RedisHttpSessionConfiguration):

@Autowired(
    required = false    //該處理監聽的線程池不是必須的,如果不自定義默認將使用SimpleAsyncTaskExecutor線程池
)
@Qualifier("springSessionRedisTaskExecutor")
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
    this.redisTaskExecutor = redisTaskExecutor;
}

springSessionRedisTaskExecutor不是必須的,如果不自定義則spring默認將使用SimpleAsyncTaskExecutor線程池。

題外話:

SimpleAsyncTaskExecutor:每次都將創建新的線程(說是“線程池”,其實並非真正的池化,但它可以設置最大併發線程數量。)
@EnableAsync開啓異步方法,背後默認使用的就是這個線程池。使用異步方法時如果業務場景存在頻繁的調用(該異步方法),請自定義線程池,以防止頻繁創建線程導致的性能消耗。如果該異步方法存在阻塞的情況,又調用量大,注意有可能導致OOM(線程還未結束,又增加了更多的線程,最後導致內存溢出)。@Async註解可以選擇使用自定義線程池。

它創建了SimpleAsyncTaskExecutor

說回RedisHttpSessionConfiguration,我們接着看:

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(this.redisConnectionFactory);
    if (this.redisTaskExecutor != null) {
        container.setTaskExecutor(this.redisTaskExecutor);
    }

    if (this.redisSubscriptionExecutor != null) {
        container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
    }

    container.addMessageListener(this.sessionRepository(), Arrays.asList(new PatternTopic("__keyevent@*:del"), new PatternTopic("__keyevent@*:expired")));
    container.addMessageListener(this.sessionRepository(), Collections.singletonList(new PatternTopic(this.sessionRepository().getSessionCreatedChannelPrefix() + "*")));
    return container;
}

RedisMessageListenerContainer正是處理監聽的類,RedisMessageListenerContainer設置了不爲空的redisTaskExecutor,因爲spring session默認沒有配置該Executor,那RedisMessageListenerContainer在處理監聽時怎麼使用線程呢?我們接着看RedisMessageListenerContainer的源碼:

public void afterPropertiesSet() {
    if (this.taskExecutor == null) {
        this.manageExecutor = true;
        this.taskExecutor = this.createDefaultTaskExecutor();
    }

    if (this.subscriptionExecutor == null) {
        this.subscriptionExecutor = this.taskExecutor;
    }

    this.initialized = true;
}

protected TaskExecutor createDefaultTaskExecutor() {
    String threadNamePrefix = this.beanName != null ? this.beanName + "-" : DEFAULT_THREAD_NAME_PREFIX;
    return new SimpleAsyncTaskExecutor(threadNamePrefix);
}

afterPropertiesSet()這個方法熟悉吧,這個方法將在所有的屬性被初始化後調用(InitializingBean接口細節這裏不再贅述)。
所以如果用戶沒有定義springSessionRedisTaskExecutor,Spring session將調用createDefaultTaskExecutor()方法創建SimpleAsyncTaskExecutor線程池。而這個“線程池”處理任務時每次都創建新的線程。所以你會發現很多個redisMessageListenerContailner-X線程。

解決方案:

爲spring session添加springSessionRedisTaskExecutor線程池。

/**
 * 用於spring session,防止每次創建一個線程
 * @return
 */
@Bean
public ThreadPoolTaskExecutor springSessionRedisTaskExecutor(){
    ThreadPoolTaskExecutor springSessionRedisTaskExecutor = new ThreadPoolTaskExecutor();
    springSessionRedisTaskExecutor.setCorePoolSize(8);
    springSessionRedisTaskExecutor.setMaxPoolSize(16);
    springSessionRedisTaskExecutor.setKeepAliveSeconds(10);
    springSessionRedisTaskExecutor.setQueueCapacity(1000);
    springSessionRedisTaskExecutor.setThreadNamePrefix("Spring session redis executor thread: ");
    return springSessionRedisTaskExecutor;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章