spring-data-redis 连接泄漏(上)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: spring-data-redis 连接泄漏(上)
本系列是 我TM人傻了 系列第四期[捂脸],往期精彩回顾:


image.png


本文基于 Spring Data Redis 2.4.9

最近线上又出事儿了,新上线了一个微服务系统,上线之后就开始报各种发往这个系统的请求超时,这是咋回事呢


image.png


还是经典的通过 JFR 去定位(可以参考我的其他系列文章,经常用到 JFR),对于历史某些请求响应慢,我一般按照如下流程去看:

  1. 是否有 STW(Stop-the-world,参考我的另一篇文章:JVM相关 - SafePoint 与 Stop The World 全解):
  2. 是否有 GC 导致的长时间 STW
  3. 是否有其他原因导致进程所有线程进入 safepoint 导致 STW
  4. 是否 IO 花了太长时间,例如调用其他微服务,访问各种存储(硬盘,数据库,缓存等等)
  5. 是否在某些锁上面阻塞太长时间?
  6. 是否 CPU 占用过高,哪些线程导致的?

通过 JFR 发现是很多 HTTP 线程在一个锁上面阻塞了,这个锁是从 Redis 连接池获取连接的锁。我们的项目使用的 spring-data-redis,底层客户端使用 lettuce。为何会阻塞在这里呢?经过分析,我发现 spring-data-redis 存在连接泄漏的问题


image.png


我们先来简单介绍下 Lettuce,简单来说 Lettuce 就是使用 Project Reactor + Netty 实现的 Redis 非阻塞响应式客户端。spring-data-redis 是针对 Redis 操作的统一封装。我们项目使用的是 spring-data-redis + Lettuce 的组合。

为了和大家尽量说明白问题的原因,这里先将 spring-data-redis + lettuce API 结构简单介绍下。

首先 lettuce 官方,是不推荐使用连接池的,但是官方没有说,这是什么情况下的决定。这里先放上结论:

  • 如果你的项目中,使用的 spring-data-redis + lettuce,并且使用的都是 Redis 简单命令,没有使用 Redis 事务,Pipeline 等等,那么不使用连接池,是最好的(并且你没有关闭 Lettuce 连接共享,这个默认是开启的)。
  • 如果你的项目中,大量使用了 Redis 事务,那么最好还是使用连接池
  • 其实更准确地说,如果你使用了大量会触发 execute(SessionCallback) 的命令,最好使用连接池,如果你使用的都是 execute(RedisCallback) 的命令,就不太有必要使用连接池了。如果大量使用 Pipeline,最好还是使用连接池。

接下来介绍下 spring-data-redis 的 API 原理。在我们的项目中,主要使用 spring-data-redis 的两个核心 API,即同步的 RedisTemplate和异步的 ReactiveRedisTemplate。我们这里主要以同步的 RedisTemplate 为例子,说明原理。ReactiveRedisTemplate 其实就是做了异步封装,Lettuce 本身就是异步客户端,所以 ReactiveRedisTemplate 其实实现更简单。

RedisTemplate 的一切 Redis 操作,最终都会被封装成两种操作对象,一是RedisCallback

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

是一个 Functional Interface,入参是 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

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

SessionCallback也是一个 Functional Interface,方法体也是可以放若干个命令。顾名思义,即在这个方法中的所有命令,都是会共享同一个会话,即使用的 Redis 连接是同一个并且不能被共享的。一般如果使用 Redis 事务则会使用这个实现。

RedisTemplate 的 API 主要是以下这几个,所有的命令底层实现都是这几个 API:

  • execute(RedisCallback action)executePipelined(final SessionCallback session):执行一系列 Redis 命令,是所有方法的基础,里面使用的连接资源会在执行后自动释放
  • executePipelined(RedisCallback action)executePipelined(final SessionCallback session):使用 PipeLine 执行一系列命令,连接资源会在执行后自动释放
  • executeWithStickyConnection(RedisCallback callback):执行一系列 Redis 命令,连接资源不会自动释放,各种 Scan 命令就是通过这个方法实现的,因为 Scan 命令会返回一个 Cursor,这个 Cursor 需要保持连接(会话),同时交给用户决定什么时候关闭。



image.png


通过源码我们可以发现,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) 方法,这种就是 executePipelinedexecute(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:可以为空,如果开启了连接共享,则不为空,默认是开启的;所有 LettuceConnection 共享的 Redis 连接,对于每个 LettuceConnection 实际上都是同一个连接;用于执行简单命令,因为 Netty 客户端与 Redis 的单处理线程特性,共享同一个连接也是很快的。如果没开启连接共享,则这个字段为空,使用 asyncDedicatedConn 执行命令。
  • asyncDedicatedConn:私有连接,如果需要保持会话,执行事务,以及 Pipeline 命令,固定连接,则必须使用这个 asyncDedicatedConn 执行 Redis 命令。

我们通过一个简单例子来看一下执行流程,首先是一个简单命令:redisTemplate.opsForValue().get("test"),根据之前的源码分析,我们知道,底层其实就是 execute(RedisCallback),流程是:



image.png


可以看出,如果使用的是 RedisCallback,那么其实不需要绑定连接,不涉及事务。Redis 连接会在回调内返回。需要注意的是,如果是调用 executePipelined(RedisCallback)需要使用回调的连接进行 Redis 调用,不能直接使用redisTemplate调用,否则 pipeline 不生效

Pipeline 生效

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;
    }
});
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
NoSQL 网络安全 Redis
用Windows连接Liunx中的redis
用Windows连接Liunx中的redis
26 0
|
2月前
|
监控 NoSQL Linux
Redis突现拒绝连接问题处理总结
Redis突现拒绝连接问题处理总结
|
3月前
|
消息中间件 移动开发 NoSQL
Redis 协议 事务 发布订阅 异步连接
Redis 协议 事务 发布订阅 异步连接
|
26天前
|
缓存 运维 NoSQL
【Redis故障排查】「连接失败问题排查和解决」带你总体分析和整理Redis的问题故障实战开发指南及方案
【Redis故障排查】「连接失败问题排查和解决」带你总体分析和整理Redis的问题故障实战开发指南及方案
127 0
|
20天前
|
NoSQL 安全 网络安全
Redis连接:加速数据访问与保障安全传输的关键
Redis连接:加速数据访问与保障安全传输的关键
|
1月前
|
Cloud Native NoSQL 数据管理
Serverless 应用引擎常见问题之首次启动获取不到redis连接如何解决
Serverless 应用引擎(Serverless Application Engine, SAE)是一种完全托管的应用平台,它允许开发者无需管理服务器即可构建和部署应用。以下是Serverless 应用引擎使用过程中的一些常见问题及其答案的汇总:
28 3
Serverless 应用引擎常见问题之首次启动获取不到redis连接如何解决
|
2月前
|
XML NoSQL Java
java 连接redis 执行查询
要使用Java连接Redis并执行查询,你需要使用Jedis这个Java开源库。首先,你需要在你的Java项目中添加Jedis的依赖。你可以通过在你的项目的pom.xml文件中添加以下代码来实现: ```xml <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.7.0</version> </dependency> ``` 然后,你需要在代码中导入Jedis库: ```java import redis.clients.jedis.Jedis
|
3月前
|
NoSQL Java Redis
SpringBoot连接redis
SpringBoot连接redis
26 0
|
3月前
|
存储 NoSQL 关系型数据库
Redis协议与异步方式(redis网络层、pipeline、事务、lua脚本、ACID特性、发布订阅、hiredis实现同步连接与异步连接)
Redis协议与异步方式(redis网络层、pipeline、事务、lua脚本、ACID特性、发布订阅、hiredis实现同步连接与异步连接)
68 0
|
4月前
|
设计模式 JSON 前端开发
Spring MVC 详解(连接、获取参数、返回数据)
Spring MVC 详解(连接、获取参数、返回数据)

热门文章

最新文章