утечка соединения spring-data-redis, я тупой

Java Redis задняя часть
утечка соединения spring-data-redis, я тупой

Это мой 30-й день августовского испытания обновлений.

Эта серияя тупойЧетвертый выпуск серии [закрой лицо], замечательный обзор предыдущих выпусков:

image

Эта статья основана на Spring Data Redis 2.4.9.

Недавно в сети снова что-то произошло, и была запущена новая микросервисная система.После выхода в сеть он начал сообщать о том, что время ожидания отправленных в эту систему запросов истекло.Что происходит??

image

все еще классическийЧтобы найти через JFR (вы можете обратиться к моей другой серии статей, часто используйте JFR),заМедленный ответ на некоторые запросы в истории, я обычно смотрю на это в соответствии со следующим процессом:

  1. Есть ли STW (Stop-the-world, см. мою другую статью:Связано с JVM - комплексное решение SafePoint и Stop The World):
  2. Есть ли длительный STW, вызванный GC
  3. Есть ли какая-либо другая причина, из-за которой все потоки процесса входят в точку сохранения, вызывая STW?
  4. Занимает ли слишком много времени ввод-вывод, например вызов других микросервисов, доступ к различным хранилищам (жесткий диск, база данных, кеш и т. д.)?
  5. Вы слишком долго блокируете некоторые замки?
  6. Слишком высокая загрузка ЦП, какие потоки вызывают это?

С помощью JFR обнаружено, что многие потоки HTTP блокируются блокировкой,Эта блокировка является блокировкой для получения соединения из пула соединений Redis.. В нашем проекте используется spring-data-redis, а базовый клиент использует салат. Почему здесь заблокировано? После анализа я обнаружил, что spring-data-redis существуетпроблема с утечкой соединения.

image

Давайте кратко представим Lettuce.Коротко, Lettuce — это неблокирующий отзывчивый клиент Redis, реализованный с использованием Project Reactor + Netty. spring-data-redis — это унифицированный пакет для операций Redis. В нашем проекте используется комбинация spring-data-redis + Lettuce.

Чтобы максимально объяснить причину проблемы, вот краткое введение в структуру API spring-data-redis + lettuce.

Первый официальный салат,Не рекомендуется использовать пул соединений, но чиновник не сказал, при каких обстоятельствах это решение. Вот первый вывод:

  • Если вы используете в своем проекте spring-data-redis + lettuce и используете все простые команды Redis, и не используете транзакции Redis, Pipeline и т. д., то лучше не использовать пул соединений(И вы не отключили общий доступ к подключению Lettuce, который включен по умолчанию).
  • Если в вашем проекте используется много транзакций Redis, лучше всего использовать пул соединений.
  • На самом деле правильнее сказать, который сработает, если вы используете многоexecute(SessionCallback)команда, лучше использовать пул соединений, если вы используетеexecute(RedisCallback)команда, нет необходимости использовать пул соединений. Если вы часто используете Pipeline, лучше использовать пул соединений.

Далее мы представим принцип API spring-data-redis. В нашем проекте мы в основном используем два основных API spring-data-redis, а именно синхронныйRedisTemplateи асинхронныйReactiveRedisTemplate. Здесь мы в основном используем синхронныйRedisTemplateВ качестве примера объясните принцип.ReactiveRedisTemplateФактически это асинхронная инкапсуляция, сам Lettuce является асинхронным клиентом, поэтомуReactiveRedisTemplateНа самом деле это проще реализовать.

RedisTemplateВсе операции Redis в конечном итоге будут инкапсулированы в два объекта операций:ОдинRedisCallback<T>:

public interface RedisCallback<T> {
	@Nullable
	T doInRedis(RedisConnection connection) throws DataAccessException;
}

является функциональным интерфейсом, а входные параметрыRedisConnection, что может быть достигнуто с помощьюRedisConnectionУправляйте Redis. Может быть набором нескольких операций Redis. самыйRedisTemplateБлагодаря этому реализованы все простые операции Redis. Например, реализация исходного кода запроса Get:

//在 RedisCallback 的基础上增加统一反序列化的操作
abstract class ValueDeserializingRedisCallback implements RedisCallback<V> {
	private Object key;

	public ValueDeserializingRedisCallback(Object key) {
		this.key = key;
	}

	public final V doInRedis(RedisConnection connection) {
		byte[] result = inRedis(rawKey(key), connection);
		return deserializeValue(result);
	}

	@Nullable
	protected abstract byte[] inRedis(byte[] rawKey, RedisConnection connection);
}

//Redis Get 命令的实现

public V get(Object key) {

	return execute(new ValueDeserializingRedisCallback(key) {

		@Override
		protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
		    //使用 connection 执行 get 命令
			return connection.get(rawKey);
		}
	}, true);
}

ДругойSessionCallback<T>:

public interface SessionCallback<T> {

	@Nullable
	<K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}

SessionCallbackЭто также функциональный интерфейс, и тело метода также может содержать несколько команд. Как следует из названия, все команды этого метода,Все будут использовать один и тот же сеанс, то есть используемое соединение Redis будет одинаковым и не может использоваться совместно.. Обычно эта реализация используется, если используются транзакции Redis.

RedisTemplateAPI-интерфейсы в основном следующие, а базовые реализации всех команд — это эти API-интерфейсы:

  • execute(RedisCallback<?> action)иexecutePipelined(final SessionCallback<?> session): выполнить серию команд Redis, лежащих в основе всех методов, используемых вРесурсы подключения автоматически освобождаются после выполнения.
  • executePipelined(RedisCallback<?> action)иexecutePipelined(final SessionCallback<?> session): используйте PipeLine для выполнения ряда команд,Ресурсы подключения автоматически освобождаются после выполнения.
  • executeWithStickyConnection(RedisCallback<T> callback): выполняет серию команд Redis,Ресурсы подключения не освобождаются автоматически, этим методом реализованы различные команды сканирования, поскольку команда сканирования возвращает курсор, этот курсор должен поддерживать соединение (сеанс) и в то же время давать пользователю возможность решить, когда закрыть.

image

Через исходный код мы можем найти, что,RedisTemplateКогда на самом деле применяются три API, часто возникает вложенная рекурсия друг в друга.

Например следующее:

redisTemplate.executePipelined(new RedisCallback<Object>() {
    @Override
    public Object doInRedis(RedisConnection connection) throws DataAccessException {
        orders.forEach(order -> {
            connection.hashCommands().hSet(orderKey.getBytes(), order.getId().getBytes(), JSON.toJSONBytes(order));
        });
        return null;
    }
});

и

redisTemplate.executePipelined(new RedisCallback<Object>() {
    @Override
    public Object doInRedis(RedisConnection connection) throws DataAccessException {
        orders.forEach(order -> {
            redisTemplate.opsForHash().put(orderKey, order.getId(), JSON.toJSONString(order));
        });
        return null;
    }
});

эквивалентны.redisTemplate.opsForHash().put()На самом деле звонитexecute(RedisCallback)метод, этоexecutePipelinedиexecute(RedisCallback)Вложенность, из которой мы можем комбинировать различные сложные ситуации, но как поддерживаются используемые в ней связи?

На самом деле, когда эти методы используются для получения соединений, используются все они:RedisConnectionUtils.doGetConnectionметод, чтобы получить соединение и выполнить команду. Для клиентов Lettuce получитеorg.springframework.data.redis.connection.lettuce.LettuceConnection, Эта оболочка соединения содержит два фактических соединения Lettuce Redis, а именно:

private final @Nullable StatefulConnection<byte[], byte[]> asyncSharedConn;

private @Nullable StatefulConnection<byte[], byte[]> asyncDedicatedConn;
  • asyncSharedConn: может быть пустым, если включено совместное использование соединения, оно не пусто и включено по умолчанию; все соединения Redis, используемые LettuceConnection, на самом деле являются одним и тем же соединением для каждого LettuceConnection; используется для выполнения простых команд, поскольку клиент Netty, совместно использующий одно и то же соединение также быстрое благодаря характеру Redis с однопроцессорным потоком. Если совместное использование соединения не включено, это поле остается пустым и для выполнения команды используется asyncDedicatedConn.
  • asyncDedicatedConn: частное соединение, если вам нужно поддерживать сеансы, выполнять транзакции и команды Pipeline, фиксированные соединения, вы должны использовать этот asyncDedicatedConn для выполнения команд Redis.

Давайте посмотрим на поток выполнения на простом примере, начиная с простой команды:redisTemplate.opsForValue().get("test"), согласно предыдущему анализу исходного кода, мы знаем, что нижний слой на самом делеexecute(RedisCallback), процесс такой:

image

Видно, что если использоватьRedisCallback, тогда нет необходимости привязывать соединение, и транзакция не задействована. Соединение Redis будет возвращено внутри обратного вызова. Следует отметить, что если он называетсяexecutePipelined(RedisCallback),Вам нужно использовать соединение обратного вызова, чтобы сделать вызов Redis, который нельзя использовать напрямую.redisTemplatecall, иначе пайплайн не вступит в силу:

Трубопровод вступает в силу:

List<Object> objects = redisTemplate.executePipelined(new RedisCallback<Object>() {
    @Override
    public Object doInRedis(RedisConnection connection) throws DataAccessException {
        connection.get("test".getBytes());
        connection.get("test2".getBytes());
        return null;
    }
});

Трубопровод не вступает в силу:

List<Object> objects = redisTemplate.executePipelined(new RedisCallback<Object>() {
    @Override
    public Object doInRedis(RedisConnection connection) throws DataAccessException {
        redisTemplate.opsForValue().get("test");
        redisTemplate.opsForValue().get("test2");
        return null;
    }
});

Затем мы пытаемся добавить его в транзакцию, так как наша цель не в том, чтобы действительно протестировать транзакцию, а просто в том, чтобы продемонстрировать проблему, поэтому просто используйтеSessionCallbackОберните команду GET:

redisTemplate.execute(new SessionCallback<Object>() {
    @Override
    public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
        return operations.opsForValue().get("test");
    }
});

Самая большая разница здесь заключается в том, что когда внешний уровень получает соединение, на этот раз bind = true свяжет соединение с текущим потоком для поддержания сеансового соединения. Внешний процесс выглядит следующим образом:

image

внутриSessionCallbackНа самом деле этоredisTemplate.opsForValue().get("test"),Используйте общее соединение вместо монопольного, потому что здесь мы не начали транзакцию.(то есть выполнить команду multi), если транзакция открыта, используется эксклюзивное соединение, и процесс выглядит следующим образом:image

так какSessionCallbackСоединение нужно поддерживать, поэтому процесс сильно изменился.Во-первых, нужно привязать соединение, то есть собственно получить соединение и поместить его в ThreadLocal. В то же время LettuceConnection инкапсулируется, и мы в основном фокусируемся на этой инкапсуляции с помощью переменной со счетчиком ссылок. за гнездоexecuteЭтот счет будет +1, а после выполнения этот счет будет -1, и каждый разexecuteЭтот счетчик ссылок проверяется в конце, еслиКогда счетчик ссылок обнуляется, вызовLettuceConnection.close().

Давайте посмотрим на это дальше, если этоexecutePipelined(SessionCallback)Что случается:

List<Object> objects = redisTemplate.executePipelined(new SessionCallback<Object>() {
    @Override
    public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
        operations.opsForValue().get("test");
        return null;
    }
});

На самом деле основное отличие от второго примера в процессе в том, что,Используемое соединение не является общим соединением, а является эксклюзивным соединением напрямую..

image

Наконец, давайте посмотрим на пример, если он находится вexecute(RedisCallback)исполнение на основеexecuteWithStickyConnection(RedisCallback<T> callback)То, что произойдет с командами различных SCAN, основано наexecuteWithStickyConnection(RedisCallback<T> callback), Например:

redisTemplate.execute(new SessionCallback<Object>() {
    @Override
    public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
        Cursor<Map.Entry<Object, Object>> scan = operations.opsForHash().scan((K) "key".getBytes(), ScanOptions.scanOptions().match("*").count(1000).build());
        //scan 最后一定要关闭,这里采用 try-with-resource
        try (scan) {
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
});

Процесс обратного вызова сеанса здесь, как показано на следующем рисунке, поскольку он находится в сеансе обратного вызова, поэтомуexecuteWithStickyConnectionВы обнаружите, что соединение в настоящее время привязано, поэтому отметьте + 1, но не отметите - 1, потому чтоexecuteWithStickyConnectionРесурсы могут быть открыты снаружи, например, курсор здесь, который необходимо вручную закрыть извне.image

image

В этом примере произойдет утечка соединения, сначала выполните:

redisTemplate.execute(new SessionCallback<Object>() {
    @Override
    public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
        Cursor<Map.Entry<Object, Object>> scan = operations.opsForHash().scan((K) "key".getBytes(), ScanOptions.scanOptions().match("*").count(1000).build());
        //scan 最后一定要关闭,这里采用 try-with-resource
        try (scan) {
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
});

как насчет этого,LettuceConnection будет привязан к текущему потоку, и в конце,Счетчик ссылок не ноль, а 1. И когда курсор будет закрыт, будет вызвано закрытие LettuceConnection. Но реализация LettuceConnection на самом деле близкаПросто отметьте состояние и поставьте эксклюзивное соединениеasyncDedicatedConnзакрытие, так как в настоящее время не используется монопольное соединение, оно пусто и его не нужно закрывать, как показано в следующем исходном коде:

LettuceConnection:

@Override
public void close() throws DataAccessException {
	super.close();

	if (isClosed) {
		return;
	}

	isClosed = true;

	if (asyncDedicatedConn != null) {
		try {
			if (customizedDatabaseIndex()) {
				potentiallySelectDatabase(defaultDbIndex);
			}
			connectionProvider.release(asyncDedicatedConn);
		} catch (RuntimeException ex) {
			throw convertLettuceAccessException(ex);
		}
	}

	if (subscription != null) {
		if (subscription.isAlive()) {
			subscription.doClose();
		}
		subscription = null;
	}

	this.dbIndex = defaultDbIndex;
}

После этого приступаем к выполнению команды Pipeline:

List<Object> objects = redisTemplate.executePipelined(new RedisCallback<Object>() {
    @Override
    public Object doInRedis(RedisConnection connection) throws DataAccessException {
        connection.get("test".getBytes());
        redisTemplate.opsForValue().get("test");
        return null;
    }
});

На данный момент, поскольку соединение было привязано к текущему потоку, и как было проанализировано в предыдущем разделе, мы знаем, что первым шагом является освобождение привязки, но вызовLettuceConnectionблизко. Выполнение этого кода создает эксклюзивное соединение, и, поскольку счетчик не может быть обнулен, соединение всегда привязано к текущему потоку, поэтомуЭто эксклюзивное соединение никогда не будет закрыто (если есть пул соединений, оно никогда не вернется в пул соединений)

Даже если мы позже закроем эту ссылку вручную, согласно исходному коду, поскольку состояние isClosed уже истинно, монопольную ссылку закрыть нельзя. Это приведет кутечка соединения.

Для этой ошибки я открыл проблему для spring-data-redis:Lettuce Connection Leak while using execute(SessionCallback) and executeWithStickyConnection in same thread by random turn

image

  • старайтесь избегать использованияSessionCallback, попробуйте использовать его только тогда, когда вам нужно использовать транзакции RedisSessionCallback.
  • использоватьSessionCallbackФункции инкапсулируются отдельно, команды, связанные с транзакциями, объединяются отдельно, а внешний уровень старается избегать продолжения установки.RedisTemplateизexecuteсопутствующие функции.

Ищите «My Programming Meow» в WeChat, подписывайтесь на официальный аккаунт, чистите каждый день, легко улучшайте свои технологии и получайте различные предложения.