这是我参加8月更文应战的第30天

本系列是 我TM人傻了 系列第四期[捂脸],往期精彩回想:

  • 升级到Spring 5.3.x之后,GC次数急剧添加,我TM人傻了
  • 这个大表走索引字段查询的 SQL 怎样就满足扫描了,我TM人傻了
  • 获取失常信息里再出失常就找不到日志了,我变量名TM人傻了

spring-data-redis 衔接走漏,我 TM 人傻了

本文依据 Spring Data Redis 2.4.9

最近线上又出事索引页儿了,新上线了一个微服务体系,上线之后就初步报各种发往变量之间的联系这个体系的央求超时,这是咋回事呢

spring-data-redis 衔接走漏,我 TM 人傻了

仍是经典的经过 JFR 去定位(能够参看我的其他系列文章,常索引失效常用到 JFR),关于前史某些央求呼应慢,我一般依照如下流程去看:

  1. 变量与函数否有 STW(Stop-the-world,参看我的另一篇文章:JVhttp 500M相关 – SafePoint 与 Stop The World 全解):
  2. 是否有 GC 导致的长期 STW
  3. 是否有其他原缓存数据能够铲除吗因导致进程全部线程进入 safepoint 导致 STW
  4. 是否 IO 花了太长期,例如调用其他微服务,拜访各种存储(httpwatch硬盘,数据库,缓存嵌套分类汇总等等)
  5. 是否在某些锁上面阻塞太长期?
  6. 是否 CPU 占用过高,哪些线程导致的?

经过 JFR 发现是许多 HTTP 线程在一个锁嵌套函数上面阻塞了,这个锁是从 Redis 联接池获取联接的锁索引失效的几种状况。咱们的项目运用的 spring-data-redis,底层客户端运用 l缓存视频兼并app下载ettuce。为何会阻塞在这儿呢?经过分析,我发现 spring-data-redis 存在联接走漏的问题

spring-data-redis 衔接走漏,我 TM 人傻了

咱们先来简略介绍下 Lettuce,简略变量是什么意思来说 Lettuce 便是运用 Pro变量ject Reactor + Netty 结束的 Redis 非阻塞呼应式客户端。嵌套函数零点问题spring-data-redis 是针对 Redis 操作的一致封装。咱们项目运用的是 spring-data-redis + Lettuce 的组合。

为了和咱们尽量说明白问题的原因,这儿先将 spring-data-redis + lettuce API 结构简略介绍下。

首要 lettuce 官方,是不举荐运用联接池的,可是官方没有说,这是什么状况下的决缓存视频变成本地视频。这儿先放上结论:

  • 假定你的项目中,运用的 spring-data-re变量英文dis + le缓存视频怎样转入本地视频ttuce,而且运用的都是 Redis 简略指令,没有运用 Redis 业务,Pipeline 等等,那么不运用联接池,是最好的(而且你没有封闭 Lettuce 联接同享,这个默许是翻开的)。
  • 假定你的项目中,许多运用了 Redis 业务,那么最好仍是运用联接池
  • 其实更精确地说,假定你运用了许多会触发 execute(SessionCallback) 的指索引页令,最好运用联接池,假定你运用的都是索引页是哪一页 exec嵌套ute(RedisCallback) 的指令,就不太有必要运用联接池了。假定许多运用 Pip索引失效eline,最好仍是运用联接池。

接下来介绍下 spring-dat变量是什么意思a-redis索引 的 API 原理。在咱们的项目中,首要运索引类型用 spring-data-redis 的两个核心 API,即同步的 RedisTemplate 和异步的 ReactiveRedisTemplate缓存数据能够铲除吗。咱们这儿首要以同步的 RedisTemplate 为比如,说明原理。Reactive嵌套循环RedisTemplate 其实便是做了异步封装,Lettuce缓存视频兼并app下载 自身便是异步客户端,所以 ReactiveRedisTemplate 其实结束更简略。

RedisTe嵌套if函数mplate 的全部嵌套查询和嵌套成果的差异 Redis 操作,毕竟都会被封装成两种操作目标,一是 RedisCallback<T>

public interface RedisCallback<T> {
@Nullab嵌套le
T doInRedis(RedisChttp 302onn变量类型有哪些ection connection) throws DataAccessException;
}

是一个 Functional Interface,入参是 RedisConne变量名ctionHTTP能够经过运用 Redi索引贴的用法sConnection 操作 Redis。能够是若干个 Re缓存di索引页s 操作的调集。大部分 Re索引符号disTemplate 的简略 Redis 操索引失效作都是经过这个结束的http://www.baidu.com。例如 Get 央求的源码结束便是:

//在 Redishttps和http的差异Callback 的根底上添加一致反序列化的操作
abstract class Va索引页是哪一页lueDeserializ索引贴的用法ingRedisCallback implements RedisCallback<V> {
private Object key;
public ValueDeserializingRedisCallback(Object key) {
this.key = key;
}
public final V do嵌套规划InRedis(httpclientRedisConnection connection) {
byte[] result = inRedis(rahttp 404wKey(key), connection);
ret缓存视频在手机哪里找urn deserializeValue(result);
}
@Nullable
protected abstract byte[] inRedis(byte[] rawKey, RedisConnection connection);
}
//Redis Get 指令的结束
public V get(Object key) {
reHTTPturn execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedi索引页是哪一页s(byte[] rawKey, RedisConnection connecti缓存视频怎样转入相册on) {
//运用 connection 实施 get 指令
return connection.get(httpwatchrawKey);
}
}, thttp 500rue);
}

另一种是SessionCallback<T>缓存视频怎样转入相册

public interface SessionCahttpclientllback<T> {缓存视频兼并app下载
@Nullable
<K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}

SessionCal变量泵lback也是一个 Functional Interface,办法体也是能够放若干HTTP个指令。望文生义,即在这个办法中变量与函数的全部指令,都是会同享同一个会话,即运用的变量英文 Redis缓存 联接是同一个而且不能被同享的。一般假定运用 Redis 业务则会运用这个结束嵌套是什么意思

RedisTemplate 的 API 首要是以索引贴的用法下这几个,全部的指令底层结束都是这几个 API:

  • ex嵌套查询ecute(RedisCallback<?> action)executePipelined(final SessionCallback<?> session缓存):实施一系列 Redis 指令,是全部办法的根底,里面运用的联接http 302资源会在实施后自动开释
  • executePipelined(https和http的差异RedisCallback<嵌套查询和嵌套成果的差异?> action)executePipelined(final SessionCal嵌套查询lback<?> session变量泵):运用 Pipe缓存视频怎样转入相册Line 实施一系列指令,联接资源会嵌套在实施后自动开释
  • executeWithStickyConnection(RedisCallback&lt变量名的命名规矩;T> callback):实施一系列 Redis 指令,联接资源不会自动开变量名的命名规矩,各种 Scan 指令便是经过这个办法结束的,因为 Scan 指令会回来一个 Cursor,这个 Cursor 需求坚嵌套函数持联接(会话),一同交给缓存用户决议什么时分封闭。

spring-data-redis 衔接走漏,我 TM 人傻了

经过源码咱们能够发现,RedisTemp索引页late 的三个 API 在实践运用的时分,常常会发生彼此嵌套递归的状况。

例如如下这种:

redisTemplate.executePipelined(new Rehttps和http的差异disCallback<Object>() {
@Override
p缓存视频怎样转入本地视频ubl嵌套ic O索引符号bject doInRedis(RedisConnection connecti嵌套分类汇总on) throws DataAccessException {
orders.forEach(order -> {
connection.hashCommands().hSet(orderKey.getBytes(), order.getId().getBytes()嵌套函数零点问题, JSON.toJSONBytes(order));
});
return null;
}
});

redisTemplate.executePipelined(new Redis嵌套规划Callback<Object>()http协议 {
@嵌套函数零点问题Override
public Object doInRedis(RedisConnec嵌套结构tion connection) throws DataAc变量的界说cess缓存的视频怎样保存到本地Exception {
orders.forEach(order -> {
redisTemplate.opsForH嵌套查询和嵌套成果的差异ash().put(or索引页derKey, order.getId(), JSON.toJSONString(order));
});
return null;
}
});

是等HTTP价的。redisTemplate.opsForHash().put()其实调用的是 execute(RedisCallba索引失效的几种状况ck) 办法,这种便是 execute变量的界说Pipelinedexecute(RedisCallback) 嵌套,由此咱们能够组合出各种杂乱的状况,可是里面运用的联接是怎样保护的呢?

其实这几个办法http 302获取联接的时分,运用的都是:RedisConnectionUtils.doGetConnection 办法,去获取联接并实施指令。关于 Lettuce 客户端,获取的是一个 or嵌套分类汇总g.springfram缓存视频兼并app下载ework.data.redi索引失效s.connection.lettuce.LettuceConnection. 这个联接封装包含两个实践 Lettuce Redis 联接,分别是:

private final @Nullable StatefulConnection<byte[], byte[]> asyncSharedConn;
private @Nullable Sta缓存视频在手机哪里找tefulConnection<byte[], byte[]> asyncDedic索引页atedCo索引贴nn;
  • asyncSharedConn:可以为空,假定翻开了联索引页接同享,则不为空,默许是翻开的;全部 LettuceConnection 同享的 Redis 联接,关于每个 LettuceConnection 实践上都是同一个联接;用于实施简略指令,因为 Netthttpwatchy 客户端与 Redis 的单处理线程特性,同享同一个联接也是很快的。假定没翻开联接同享,则这个字段为空,运用 asy缓存ncDedicatedConn 实施指令。
  • asyncDedicatedConn:私有联http署理接,假定需求坚持会话,实施业务,以及 Pipeline 指令,固定联接,则有必要运用这个 asyncDedicatedConn 实施 Redis 指令。

咱们经过一个简略比如来看一下实施流程,首要是一个简略指令:redisTemplate.opsForValue().get("test"),依据之前的源码分析,咱们知道,底层其实便是 execute(RedisCallback),流程是:

spring-data-redis 衔接走漏,我 TM 人傻了

能够看出,假定运用的是 RedisCallback,那么其实不需求绑定联接,不涉及业务。Redis 联接会在回调内回来。需求注意的缓存是,假定是调用 executePipelined(RedisCallback)需求运用回调的联接进缓存视频兼并app下载行 Redis 调用,不能直接运用 redisTe索引mplate 调用,不然 pipel嵌套ine 不收效

Pipeline 收效

List<Object> objects = redisTemplate.exec索引失效的几种状况uthttp://192.168.1.1登录ePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
connection.get("test".getBytes());缓存
connection.get("test2".getByt索引类型es(嵌套规划));
return null;
}
})缓存视频兼并;

Pipeline 不收效

List<Object> objects = redisTemplate.executePipelined(nehttpclientw RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
redisTemplate.opsForValue()变量名的命名规矩.get("thttp署理est");
redisTemplat索引类型e.opsForValue().get("test2");
return null;
}变量
});

然后,咱们检验将其参加业务中,因为咱们的目的不是真的检验业务,只是为了演示问题索引超出了数组界限什么意思,所以,只是是用 SessionCallback 将 GET 指令包装起来:

redisTemplate.execute(new SessionCallbahttp://www.baidu.comck<Objec索引失效的几种状况t&ghttp 302t;() {
@Overhttp协议ride
publi索引失效c <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
return operations.opsForValue().get("test");
}
});

这儿最大的变量提高差异便是,外层获取联接的时分,这次是 bind = true 即变量提高将联接与当时线程绑定,用于坚持会话联接。外层流程如下:

spring-data-redis 衔接走漏,我 TM 人傻了

里面的 SessionCallback 其实便是 redisTemplate.opsForValue().get("test")运用的是同享的联接,而不是独占的联接,因为咱们这儿还没翻开业务(即实施 multi 指令),假定打索引超出了数组界限什么意思开了业务运用的便是独占的联接,流程如下:
spring-data-redis 衔接走漏,我 TM 人傻了

因为 SessionCallback 需求坚持联接,所以流程有很大改变,首要需求绑定联接,其实便是获取联接放入 ThreadLocal 中。一同,针对 LettuceConnection 进索引贴行了封装,咱们首要注重这个缓存视频在手机哪里找封装有一个引证计数的变量。每嵌套一次 execute嵌套循环会将这个计数 + 1,实施完之后,就会将这个计数 -1, 一同每次 execu缓存视频怎样转入相册te 结束的时嵌套分都会检查这个引证计数,假定引证计数归零,就嵌套分类汇总索引调用 LettuceConnection.close()缓存数据能够铲除吗

接下来再缓存是什么意思来看,假定是 executePipelined(SessionCallback) 会怎样样:

List<Objec索引t> objects = redisTemplate.executePipelined(new SessionCallback<O缓存视频兼并bject>() {
@Override
public &lthttp 500;K, V> Ohttp 302bject execute(R索引贴edisOperations<K,缓存视频怎样转入本地视频 V> operations) throws DataAccessException {
operations.opsFohttp://www.baidu.comrValue().get("test");
return null;
}
});

其实与第二个嵌套规划比如在流程上的首要差异在缓存的视频怎样保存到本地于,运用的联接不是同享联接,而是直接是独索引超出了数组界限什么意思占的联接

spring-data-redis 衔接走漏,我 TM 人傻了

终究咱们再来看一个比如,假定是在 execute(RedisCallback) 中实施依据 executeWithStickyConnection(RedisCallback<T> callback) 的指令会怎样样,各种 SCAN 便是依据 executeWithStic变量kyConnection(RedisCallback&l缓存视频怎样转入相册t;T&gt嵌套结构; call嵌套if函数back) 的,例如:

redisTemp变量late.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(), Sca嵌套分类汇总nOpt索引贴ions.scanOptions().match("*").count(1000).build());
//scan 终究一定要封闭,这儿选用 try-wit变量提高h-resohttp 500urce
try (scan) {
} catch (IOhttps和http的差异Exception e) {
e.printStackTra变量类型有哪些ce();
}
return null;
}
});变量的界说

这儿 Session callb嵌套函数ack 的流程,如下图所示,因为处于 S变量泵essionCallback,所以 execut变量英文eWithStickyConnection 会发现当时http署理绑定了联接,所以符号 + 1,可是并不会符号 – 1,因为 executeWithStickyConnection 能够将资源暴露到外部,例如这儿的 Cursor,需求外部手动封闭。
spring-data-redis 衔接走漏,我 TM 人傻了

spring-data-redis 衔接走漏,我 TM 人傻了

在这个比如中,会发生联接走漏,首要实施:

redis嵌套是什么意思Template.execute(new S变量essionCallback<Object>() {
@Override
public <K, V> Object execute(Rehttp 500disOperations<K, V> operations) throws DataAccessException {
Cursor<Map.Entry<Object, Object>>https和http的差异 scan = operations.opsForHashttps和http的差异h().scan((K) "key".get索引贴Bytes(), ScanOptions.scanOptio缓存视频在手机哪里找ns().match("*").count(1000).build())变量提高;
//scan 终究一定要封闭,这儿选用 try-with-resource
try (scan) {
} catch (IOException e) {
e.printStackTrace();变量与函数
}
re索引页t索引是什么意思urn null;
}
});

这样呢嵌套规划LettuceConnection 会和当时线程绑定,而且在结束时,引证计数不为零,而是 1。而且 cursor 封闭时,会调用 LettuceCo缓存数据能够铲除吗nnection 的 close。可是 LettuceCohttp协议nnection 的 close 的结束,其实嵌套结构只是索引失效符号状况,而且把独占的联接 asyncDedicatedCo变量的界说nn 封闭,因为索引页是哪一页当时没有运用到独占的联接,所以为空,不需求封闭;索引类型如下面源码所示:

Lettuce缓存Connection:

@Override
pub变量与函数lic void close() throws D索引符号ataAccessException {
super.close();
if (isClohttps和http的差异sed) {
return;
}
isClosed = true缓存视频兼并;
if (asyncDedicatedConn != null) {
try {
if (嵌套结构customizedDatabaseIndex()) {
pot嵌套if函数entiallySelectDatabase(defaultDbInde嵌套规划x);
}
connectionProv嵌套查询和嵌套成果的差异ider.release(asyncDedicatedConn);
} catch (RuntimeException ex) {
throw convertLettuceAccessException(ex);
}
}
if (subscription != null) {
if (subscript缓存视频在手机哪里找ion.isAlive()) {
subscri变量名ption.doCl缓存视频怎样下载到手机相册ose();
}
subscription = null;
}
this.dbIndex = defaultDbIndex;
}

之后咱们持续实施一个 Pipeline 指令缓存视频变成本地视频

List<Object> o嵌套if函数bjects = redisTemplate.executePipelined(缓存new RedisCallback<Objec变量之间的联系t>() {
@Overrid变量名的命名规矩e
public Object doInRedis(RedisConnection connection) throws DataAc索引失效的几种状况cessException {
connection.get("test".HTTPgetBytes());
redisTemplate.opsForValue().g缓存视频兼并et("test");
return null;
}
});

这时分因为联接现已绑定到当时线程,一同同上上一节分析咱们知道第一步解开开释这个绑定,可是调用了 LettuceConnection 的 close。实施这个代码,会创建一个独占联接,而且,因为计数不能归零,导致联接一贯与当时线程绑定,这样,这个独占联索引失效的几种状况接一贯不会封闭(假定有联接池的话,便是一贯不回变量来联接池)

即便后边咱们手动封闭这个链接,可是依据源码,因为状况 isClosed 现已是 true,仍是不能将独占链接封闭。这样,就会构成联接走漏http 404

针对这个 Bug,我现已向 spring-data-redis 一个 Issue:Lettuce Connection Leak while using execute(SessionCall嵌套if函数back) and executeWi变量英文thStickyConnectio缓存视频变成本地视频n i变量的界说n same thread by random turn

spring-data-redis 衔接走漏,我 TM 人傻了

  • 尽量避免运用索引是什么意思 SessionCallback,尽量仅在需求运用 Redis 业务的时分,运用 SessionCallback
  • 运用 SessionCallbackhttp署理 的函数单独封装,将业务相关的指令单独放在一同,而且外层尽量避免再持续套 Redhttp 500isTemplateexecute 相关https和http的差异函数。

微信查找“我的编程喵”注重公众号,每日一刷,轻松提高技术,斩获各种offer