RedisTemplate 接口误用造成的空指针异常记录(深扒multiGet接口)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: RedisTemplate 接口误用造成的空指针异常记录(深扒multiGet接口)

RedisTemplate 接口误用造成的空指针异常记录

redis读写在现阶段,除了原生的调用接口,例如jedis、lettuce等,许多都使用了redisTemplate,当然,更多的使用了@Cacheable、@CaachePut之类的注解。

redisTemplate的封装避免了底层api的不同。而注解@Cacheable等则更多的符合了旁路设计,避免了更多人为try、catch,代码更加优雅、不容易出错。

BUG级别

低级

BUG描述

典型的空指针异常。据产生BUG的童鞋在一定的排查后描述:向redis进行查询数据,redis返回给他一个数组,但是数组里面的那个对象为null,导致了他空指针、redis难道查不到不应该直接返回给他一个null对象吗?

BUG相关代码

List<String> values = null;
        values = redisTemplate.opsForHash().multiGet(HASH, keys);
// 以下是multiGet源码定义
List<HV> multiGet(H var1, Collection<HK> var2);

分析

这个接口的作用是,传入一个HASH类型的KEY,以及需要去这个HASH对象中查询VALUE的KEY列表。

那么当查多个KEY的时候,如何知道自己对应的结果是什么呢?自然是和KEY下标对应的结果列表下标位置的值了。那么自然,其中存在可能查询不到结果的情况,那下标肯定是不能乱的,那就在对应位置放一个null,不是合情合理吗?

那使用这个借口的同学就是因为并不了解这个接口,没有查阅过文档,随意使用。所以没有进行非空判断,最终导致了空指针。(PS:不过该童鞋作为该团队的核心开发人员,犯这样的低级错误实在不应该)。

接口文档

  1. RedisTemplate

https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/core/RedisTemplate.html

  1. HashOperations

https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/core/HashOperations.html

multiGet

List<HV> multiGet(H key,
                  Collection<HK> hashKeys)

Get values for given hashKeys from hash at key.

  • Parameters:
    key - must not be null.
    hashKeys - must not be null.
  • Returns:
    null when used in pipeline / transaction.

根据接口文档描述,该接口传入的key, hashKeys不可为null。 以及在管道中与事务中使用时会返回为null。

倒是没有具体描述返回的内容内部可能为null。

源码

既然文档不能给出对应位置查询不到时会放入null的解答,就来查看一下源码。

  1. 首先看看opsForHash的返回结果:
// org.springframework.data.redis.core.HashOperations
    public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
        return new DefaultHashOperations(this);
    }
//  org.springframework.data.redis.core.DefaultHashOperations#DefaultHashOperations
  DefaultHashOperations(RedisTemplate<K, ?> template) {
        super(template);
    }
// org.springframework.data.redis.core.AbstractOperations#AbstractOperations
    final RedisTemplate<K, V> template;
    AbstractOperations(RedisTemplate<K, V> template) {
        this.template = template;
    }

这个方法就是new了一个DefaultHashOperations对象,将redisTemplate对象传递了进去赋值。

  1. 再看看multiGet方法
// org.springframework.data.redis.core.DefaultHashOperations#multiGet
  public List<HV> multiGet(K key, Collection<HK> fields) {
        if (fields.isEmpty()) {
            return Collections.emptyList();
        } else {
            byte[] rawKey = this.rawKey(key);
            byte[][] rawHashKeys = new byte[fields.size()][];
            int counter = 0;
            Object hashKey;
            for(Iterator var6 = fields.iterator(); var6.hasNext(); rawHashKeys[counter++] = this.rawHashKey(hashKey)) {
                hashKey = var6.next();
            }
            List<byte[]> rawValues = (List)this.execute((connection) -> {
                return connection.hMGet(rawKey, rawHashKeys);
            }, true);
            return this.deserializeHashValues(rawValues);
        }
    }

这个方法会将key序列化为二进制数组byte[],同样的对hashKeys,也就是此处的入参fields进行序列化,成为rawHashKeys。然后,它声明了rawValues对象,用于存放查询redis的结果,并且通过传入回调的方式,将rawKey,rawHashKeys往下传递。

再来看看核心去查询的方法,execute.

// org.springframework.data.redis.core.RedisTemplate#execute(org.springframework.data.redis.core.RedisCallback<T>, boolean) 
 @Nullable
    public <T> T execute(RedisCallback<T> action, boolean exposeConnection) {
        return this.execute(action, exposeConnection, false);
    }
//  org.springframework.data.redis.core.RedisTemplate#execute(org.springframework.data.redis.core.RedisCallback<T>, boolean)
  @Nullable
  public <T> T execute(RedisCallback<T> action, boolean exposeConnection) {
    return execute(action, exposeConnection, false);
  }
// org.springframework.data.redis.core.RedisTemplate#execute(org.springframework.data.redis.core.RedisCallback<T>, boolean, boolean)
@Nullable
  public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
    Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
    Assert.notNull(action, "Callback object must not be null");
    RedisConnectionFactory factory = getRequiredConnectionFactory();
    RedisConnection conn = RedisConnectionUtils.getConnection(factory, enableTransactionSupport);
    try {
      boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
      RedisConnection connToUse = preProcessConnection(conn, existingConnection);
      boolean pipelineStatus = connToUse.isPipelined();
      if (pipeline && !pipelineStatus) {
        connToUse.openPipeline();
      }
      RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
      T result = action.doInRedis(connToExpose);
      // close pipeline
      if (pipeline && !pipelineStatus) {
        connToUse.closePipeline();
      }
      return postProcessResult(result, connToUse, existingConnection);
    } finally {
      RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
    }
  }

可以看到,最终的T result是回调action执行 doInRedis进行获取的。通过debug进去可以看到, 调用了一开始的hGet:

// org.springframework.data.redis.connection.DefaultedRedisConnection#hMGet
  @Override
  @Deprecated
  default List<byte[]> hMGet(byte[] key, byte[]... fields) {
    return hashCommands().hMGet(key, fields);
  }
//org.springframework.data.redis.connection.lettuce.LettuceHashCommands#hMGet
  @Override
  public List<byte[]> hMGet(byte[] key, byte[]... fields) {
    Assert.notNull(key, "Key must not be null!");
    Assert.notNull(fields, "Fields must not be null!");
    return connection.invoke().fromMany(RedisHashAsyncCommands::hmget, key, fields)
        .toList(source -> source.getValueOrElse(null));
  }

这里我们需要关注到这样的代码:.toList(source -> source.getValueOrElse(null));

这行代码正是将每条查询的结果合并成List返回的结果,入参是一个Converter<S, T> converter接口(及回调)。语意就是将结果进行转换,如果getValue失败,则Else为null。这就是最终返回的List中含有null对象的原因了。

当然,我们可以通过对toList方法进行debug,看到更详细的内容:

其中查询的结果是一个KeyValue对象,其中还有一个empty对象,其值为null。

总结

  1. 这次空指针事件,原因为不清楚RedisTemplate提供的接口,仅看语意相近则使用,没有考虑边界事件,以及为空的事件,开发人员自测不足都占一定的成分。
  2. 追查列表中null对象如何产生,光看接口文档仍不足,需要通过阅读源码才能看到原因。 Spring Data Redis 工程的源码抽象程度高,使用回调、函数式编程较多,还需仔细阅读。
相关实践学习
基于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
目录
相关文章
|
4月前
|
资源调度 监控 关系型数据库
实时计算 Flink版操作报错合集之处理大量Join时报错空指针异常,是什么原因
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
实时计算 Flink版操作报错合集之处理大量Join时报错空指针异常,是什么原因
|
7月前
|
Kubernetes 关系型数据库 MySQL
seata启动问题之指针异常如何解决
Seata是一款开源的分布式事务解决方案,旨在提供高效且无缝的分布式事务服务;在集成和使用Seata过程中,开发者可能会遇到不同的异常问题,本合集针对Seata常见异常进行系统整理,为开发者提供详细的问题分析和解决方案,助力高效解决分布式事务中的难题。
332 11
|
5月前
|
运维
系统日志使用问题之如何防止在打印参数时遇到NPE(空指针异常)
系统日志使用问题之如何防止在打印参数时遇到NPE(空指针异常)
java.lang.NullPointerExceptionMybatisPlus出现,测试,java.lang.NullPointe,空指针异常,public方法少写了一个字段,没加注解
java.lang.NullPointerExceptionMybatisPlus出现,测试,java.lang.NullPointe,空指针异常,public方法少写了一个字段,没加注解
|
7月前
|
存储 Java 开发者
探索Java开发中触发空指针异常的场景
作为一名后端开发者在Java编程的世界中,想必大家对空指针并不陌生,空指针异常是一种常见而又令人头疼的问题,它可能会在我们最不经意的时候突然出现,给我们的代码带来困扰,甚至导致系统的不稳定性,而且最可怕的是有时候不能及时定位到它的具体位置。针对这个问题,我们需要深入了解触发空指针异常的代码场景,并寻找有效的方法来识别和处理这些异常情况,而且我觉得空指针异常是每个Java开发者都可能面临的挑战,但只要我们深入了解它的触发场景,并采取适当的预防和处理措施,我们就能够更好地应对这个问题。那么本文就来分享一下实际开发中一些常见的触发空指针异常的代码场景,并分享如何有效地识别和处理这些异常情况。
108 1
探索Java开发中触发空指针异常的场景
|
7月前
|
Java 容器
自定义数据类型中的空指针异常
自定义数据类型中的空指针异常
53 2
|
7月前
|
Oracle 安全 Java
Seata常见问题之启动seata一直报空指针异常如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
项目中常见NPE空指针异常
项目中常见NPE空指针异常
|
7月前
|
安全 IDE Java
终结空指针异常:Java开发者的生存指南
终结空指针异常:Java开发者的生存指南
187 1
|
7月前
|
安全 IDE Java
【2024java面试题无需C币下载】终结空指针异常:Java开发者的生存指南
【2024java面试题无需C币下载】终结空指针异常:Java开发者的生存指南
92 1