You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Redis单元测试中flushAll()执行无限阻塞问题求助

问题描述

我开发了一个基于Kotlin Spring Data的Redis缓存应用,在集成测试中使用redisConnectionFactory.connection.serverCommands().flushAll()清理Redis数据库。单独运行单个单元测试时一切正常,但运行全部约200个测试或任意类的多个测试时,在通过8个测试后,程序会卡在redisConnectionFactory.connection.serverCommands().flushAll()命令上(已通过日志确认),只能手动终止测试。该现象可在不同测试类中复现,与测试内容无关。

相关代码如下:

抽象集成测试类

@SpringBootTest
@ExtendWith(SpringExtension::class)
@Import(TestChannelBinderConfiguration::class)
@ContextConfiguration(initializers = [RedisInitializer::class])
@RecordApplicationEvents
abstract class AbstractIntegrationTest {
    @Autowired
    protected lateinit var redisConnectionFactory: RedisConnectionFactory


    @AfterEach
    fun clean() {
        redisConnectionFactory.connection.serverCommands().flushAll()
    }
}

Redis初始化器

class RedisInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {

    override fun initialize(context: ConfigurableApplicationContext) {
        TestPropertyValues.of(
            "spring.redis.host=" + REDIS_CONTAINER.host,
            "spring.redis.port=" + REDIS_CONTAINER.getMappedPort(REDIS_PORT)
        ).applyTo(context.environment)
    }

    companion object {
        private const val REDIS_PORT = 6379
        private val IMAGE_NAME = DockerImageName.parse("redis:6.2-alpine")
        var REDIS_CONTAINER: GenericContainer<*> = GenericContainer<Nothing>(IMAGE_NAME)
            .withExposedPorts(REDIS_PORT)

        init {
            REDIS_CONTAINER.start()
        }
    }
}

Redis配置类

@Configuration
@EnableCaching
class RedisConfig(
    val redisProperties: RedisProperties
) {
    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> =
        RedisTemplate<String, Any>().apply {
            setConnectionFactory(connectionFactory)
            setDefaultSerializer(StringRedisSerializer())
            setValueSerializer(redisSerializer())
        }

    @Bean
    fun redisSerializer(): RedisSerializer<Any> {
        val objectMapper = ObjectMapper().apply {
            setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
        }
        return Jackson2JsonRedisSerializer(Any::class.java)
            .apply {
                setObjectMapper(objectMapper)
            }
    }

    @Bean
    fun jedisConnectionFactory(): RedisConnectionFactory {
        val jedisConnectionFactory = if (redisProperties.sentinel != null) {
            JedisConnectionFactory(getSentinelConfiguration())
        } else {
            JedisConnectionFactory(getStandaloneConfiguration())
        }
        jedisConnectionFactory.afterPropertiesSet()
        return jedisConnectionFactory
    }

    private fun getSentinelConfiguration(): RedisSentinelConfiguration {
        val sentinelConfiguration = RedisSentinelConfiguration()
        sentinelConfiguration.setMaster(redisProperties.sentinel.master)
        sentinelConfiguration.password = RedisPassword.of(redisProperties.password)
        sentinelConfiguration.sentinelPassword = RedisPassword.of(redisProperties.sentinel.password)
        sentinelConfiguration.setSentinels(
            redisProperties.sentinel.nodes.stream().map { node: String ->
                val nodeConf = node.split(":").toTypedArray()
                RedisNode(nodeConf[0], nodeConf[1].toInt())
            }.collect(Collectors.toList())
        )
        return sentinelConfiguration
    }

    /**
     * Standalone is used only in integration tests 
     * when raising the application context and connecting 
     * to the Redis docker image from the test container.
     */
    private fun getStandaloneConfiguration(): RedisStandaloneConfiguration {
        val configuration = RedisStandaloneConfiguration()
        configuration.hostName = redisProperties.host
        configuration.port = redisProperties.port
        configuration.password = RedisPassword.of(redisProperties.password)
        return configuration
    }
}
排查方向
  • 连接泄漏检查:每次调用redisConnectionFactory.connection()会获取新连接,当前代码未手动关闭连接,会耗尽Jedis连接池的最大连接数,导致后续获取连接的请求阻塞。需在flushAll()后调用connection.close()释放连接。
  • Redis容器资源与持久化阻塞:TestContainer启动的Redis容器可能因内存不足或默认持久化(RDB/AOF)触发后台任务,导致flushAll命令等待任务完成。可禁用持久化:
    var REDIS_CONTAINER: GenericContainer<*> = GenericContainer<Nothing>(IMAGE_NAME)
        .withExposedPorts(REDIS_PORT)
        .withCommand("redis-server --save '' --appendonly no")
    
  • Jedis连接池配置优化:默认连接池参数可能无法支撑大量测试并发,需自定义JedisPoolConfig:
    private fun getStandaloneConfiguration(): RedisStandaloneConfiguration {
        // 原有配置...
        val poolConfig = JedisPoolConfig()
        poolConfig.maxTotal = 50
        poolConfig.maxIdle = 20
        poolConfig.minIdle = 5
        (jedisConnectionFactory as JedisConnectionFactory).poolConfig = poolConfig
    }
    
  • flushAll阻塞场景:Redis若在执行大key删除、持久化等后台任务,flushAll会等待任务完成。可查看Redis日志排查慢查询或阻塞操作,或尝试flushAllAsync()异步清理(注意可能存在测试数据污染风险)。
  • 测试上下文复用问题:Spring Boot Test默认复用上下文,共享连接工厂可能导致连接状态不一致。可尝试添加@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD),但会增加测试耗时。
替代清理方案
  1. 用RedisTemplate管理连接执行flushAll:
    RedisTemplate会自动处理连接的获取与释放,避免泄漏:

    @Autowired
    protected lateinit var redisTemplate: RedisTemplate<String, Any>
    
    @AfterEach
    fun clean() {
        redisTemplate.execute { connection ->
            connection.serverCommands().flushAll()
            null
        }
    }
    
  2. 按前缀删除指定缓存:
    若测试数据有统一键前缀,仅删除对应前缀的键,降低全量清理开销:

    @AfterEach
    fun clean() {
        val keys = redisTemplate.keys("test:*")
        if (!keys.isNullOrEmpty()) {
            redisTemplate.delete(keys)
        }
    }
    
  3. 通过TestContainer执行容器内清理命令:
    直接在Redis容器内执行flushall命令,绕过应用端连接问题:

    @AfterEach
    fun clean() {
        REDIS_CONTAINER.execInContainer("redis-cli", "flushall")
    }
    
  4. 利用事务回滚:
    在测试方法上添加@Transactional,Spring会自动回滚Redis操作(需Redis支持事务,且测试使用Spring事务管理)。

内容的提问来源于stack exchange,提问作者NeverSleeps

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 10:37:01