Это мой 30-й день августовского испытания обновлений.
Эта серияя тупойЧетвертый выпуск серии [закрой лицо], замечательный обзор предыдущих выпусков:
Эта статья основана на Spring Data Redis 2.4.9.
Недавно в сети снова что-то произошло, и была запущена новая микросервисная система.После выхода в сеть он начал сообщать о том, что время ожидания отправленных в эту систему запросов истекло.Что происходит??
все еще классическийЧтобы найти через JFR (вы можете обратиться к моей другой серии статей, часто используйте JFR),заМедленный ответ на некоторые запросы в истории, я обычно смотрю на это в соответствии со следующим процессом:
- Есть ли STW (Stop-the-world, см. мою другую статью:Связано с JVM - комплексное решение SafePoint и Stop The World):
- Есть ли длительный STW, вызванный GC
- Есть ли какая-либо другая причина, из-за которой все потоки процесса входят в точку сохранения, вызывая STW?
- Занимает ли слишком много времени ввод-вывод, например вызов других микросервисов, доступ к различным хранилищам (жесткий диск, база данных, кеш и т. д.)?
- Вы слишком долго блокируете некоторые замки?
- Слишком высокая загрузка ЦП, какие потоки вызывают это?
С помощью JFR обнаружено, что многие потоки HTTP блокируются блокировкой,Эта блокировка является блокировкой для получения соединения из пула соединений Redis.. В нашем проекте используется spring-data-redis, а базовый клиент использует салат. Почему здесь заблокировано? После анализа я обнаружил, что spring-data-redis существуетпроблема с утечкой соединения.
Давайте кратко представим 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.
RedisTemplate
API-интерфейсы в основном следующие, а базовые реализации всех команд — это эти API-интерфейсы:
-
execute(RedisCallback<?> action)
иexecutePipelined(final SessionCallback<?> session)
: выполнить серию команд Redis, лежащих в основе всех методов, используемых вРесурсы подключения автоматически освобождаются после выполнения. -
executePipelined(RedisCallback<?> action)
иexecutePipelined(final SessionCallback<?> session)
: используйте PipeLine для выполнения ряда команд,Ресурсы подключения автоматически освобождаются после выполнения. -
executeWithStickyConnection(RedisCallback<T> callback)
: выполняет серию команд Redis,Ресурсы подключения не освобождаются автоматически, этим методом реализованы различные команды сканирования, поскольку команда сканирования возвращает курсор, этот курсор должен поддерживать соединение (сеанс) и в то же время давать пользователю возможность решить, когда закрыть.
Через исходный код мы можем найти, что,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)
, процесс такой:
Видно, что если использоватьRedisCallback
, тогда нет необходимости привязывать соединение, и транзакция не задействована. Соединение Redis будет возвращено внутри обратного вызова. Следует отметить, что если он называетсяexecutePipelined(RedisCallback)
,Вам нужно использовать соединение обратного вызова, чтобы сделать вызов Redis, который нельзя использовать напрямую.redisTemplate
call, иначе пайплайн не вступит в силу:
Трубопровод вступает в силу:
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 свяжет соединение с текущим потоком для поддержания сеансового соединения. Внешний процесс выглядит следующим образом:
внутриSessionCallback
На самом деле этоredisTemplate.opsForValue().get("test")
,Используйте общее соединение вместо монопольного, потому что здесь мы не начали транзакцию.(то есть выполнить команду multi), если транзакция открыта, используется эксклюзивное соединение, и процесс выглядит следующим образом:
так как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;
}
});
На самом деле основное отличие от второго примера в процессе в том, что,Используемое соединение не является общим соединением, а является эксклюзивным соединением напрямую..
Наконец, давайте посмотрим на пример, если он находится в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
Ресурсы могут быть открыты снаружи, например, курсор здесь, который необходимо вручную закрыть извне.
В этом примере произойдет утечка соединения, сначала выполните:
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
закрытие, так как в настоящее время не используется монопольное соединение, оно пусто и его не нужно закрывать, как показано в следующем исходном коде:
@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
- старайтесь избегать использования
SessionCallback
, попробуйте использовать его только тогда, когда вам нужно использовать транзакции RedisSessionCallback
. - использовать
SessionCallback
Функции инкапсулируются отдельно, команды, связанные с транзакциями, объединяются отдельно, а внешний уровень старается избегать продолжения установки.RedisTemplate
изexecute
сопутствующие функции.
Ищите «My Programming Meow» в WeChat, подписывайтесь на официальный аккаунт, чистите каждый день, легко улучшайте свои технологии и получайте различные предложения.