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),但会增加测试耗时。
替代清理方案
用RedisTemplate管理连接执行flushAll:
RedisTemplate会自动处理连接的获取与释放,避免泄漏:@Autowired protected lateinit var redisTemplate: RedisTemplate<String, Any> @AfterEach fun clean() { redisTemplate.execute { connection -> connection.serverCommands().flushAll() null } }按前缀删除指定缓存:
若测试数据有统一键前缀,仅删除对应前缀的键,降低全量清理开销:@AfterEach fun clean() { val keys = redisTemplate.keys("test:*") if (!keys.isNullOrEmpty()) { redisTemplate.delete(keys) } }通过TestContainer执行容器内清理命令:
直接在Redis容器内执行flushall命令,绕过应用端连接问题:@AfterEach fun clean() { REDIS_CONTAINER.execInContainer("redis-cli", "flushall") }利用事务回滚:
在测试方法上添加@Transactional,Spring会自动回滚Redis操作(需Redis支持事务,且测试使用Spring事务管理)。
内容的提问来源于stack exchange,提问作者NeverSleeps
相关产品推荐
相关产品推荐

