Springboot如何优雅高效的清除Redis中的业务key

 更新时间:2025年05月07日 08:54:50   作者:血小溅  
这篇文章主要为大家详细介绍了Springboot如何优雅高效的清除Redis中的业务key,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

1、问题背景

云服务运维工程师联系我说老系统有个服务连接redis集群实例使用keys命令导致实例夯住了并给我截了个图。然后刚开始我是挺懵逼的,同事跟我说在某个服务中,我去找了找压根没有,后来我仔细想了想,并看了运维老师提供的图,我想到了方法找对应的应用进程。以下是排查及解决过程。

2、如何找到对应的应用进程

根据下面的图,我们可以看到,redis集群的服务端口为9000,客户端连接分配的客户端本地通信端口【本地端口只是一个临时标识,用于客户端与 Redis 之间的通信,通常是由操作系统在每次创建新连接时自动分配的,并不会影响连接的实际功能。】为39720,那么我们就可以通过netstat命令来查找对应的应用进程了。

2.1、使用netstat查找进程

进入应用部署的服务器,使用如下netstat命令查找进程,如下图,从下图我们可以看出,进程是个java进程,进程号为15817

netstat -anlp |grep 9000 |grep EST |grep 39720

2.2、使用jps命令查看应用名称

使用jps命令查看java进程对应的应用名称,通过命令我们可以看出

jps -l |grep 15817

3、问题代码及原因分析

3.1、查找问题代码

根据步骤2我们找到了对应的应用,下面我们就可以通过redis中的key关键词YZ_MULTI_DIAG搜索代码了,然后找到了如下图的代码,确实使用了keys命令。

private void cleanCache(String toUserId) {
    Set<String> keys = stringRedisTemplate.keys("YZ_MULTI_DIAG:" + toUserId + "*");
    stringRedisTemplate.delete(keys);
}

3.2、原因分析

keys 命令在 Redis 中遍历所有的键,是一个阻塞操作,尤其是当 Redis 数据量大时,可能会导致 Redis 实例卡住或响应变慢。在 Redis 中,keys 命令用于查找与给定模式匹配的所有键,它会扫描整个数据库,并返回符合条件的所有键。这个命令在某些情况下会导致 Redis 实例“夯住”或变得非常缓慢,原因如下:

3.2.1、 阻塞和性能影响

  • keys 命令需要遍历 Redis 实例中所有的键,无论数据库中有多少个键。对于存储大量键的 Redis 实例来说,keys 命令会消耗大量的 CPU 和内存资源,因为它必须检查每个键,并将结果返回给客户端。
  • 如果有大量的键,keys 命令可能会导致 Redis 被阻塞,直到命令完成执行。在此期间,Redis 无法处理其他客户端请求,这可能会导致延迟或服务中断。

3.2.2、 不适合生产环境

  • 在生产环境中,通常不建议使用 keys 命令,特别是在有大量键值对的情况下。keys 命令的性能是 O(N),其中 N 是数据库中键的数量。这意味着数据库中键越多,执行时间就越长,负载越重。
  • 更适合使用 scan 命令,它是增量式的,并不会一次性返回所有匹配的键,而是通过多次迭代逐步获取。这使得 Redis 在扫描键时不会被完全阻塞。

3.2.3、 其他客户端请求的影响

  • 由于 keys 命令会导致 Redis 扫描整个键空间,它会占用 Redis 实例的 CPU 和内存资源,这可能导致其他客户端请求的响应时间延迟,甚至阻塞其他操作,导致整个 Redis 实例性能下降。
  • 在 Redis 集群环境中,keys 命令会对集群的每个节点进行全局扫描,可能会对整个集群的性能产生影响。

4、优化方案

  • 使用 scan 命令替代 keys 命令。scan 命令是增量的,可以分批次扫描键,避免一次性操作导致的阻塞。
  • 如果需要列出键,尽量使用特定的键模式(例如,前缀)来限制扫描的范围,避免扫描整个数据库。
  • 在生产环境中,应该避免在高负载期间使用 keys 命令。

优化后的代码如下,使用类似分页概念进行批量删除。

private void cleanCache(String toUserId) {
    String pattern = "YZ_MULTI_DIAG:" + toUserId + "*";
    ScanOptions scanOptions = ScanOptions.scanOptions().match(pattern).count(100).build();
    stringRedisTemplate.execute((RedisCallback<Void>) connection -> {
        String cursor = "0"; // 初始游标
        try {
            do {
                // 使用SCAN命令分页获取匹配的键
                Cursor<byte[]> scanCursor = connection.scan(scanOptions);
                List<byte[]> keysToDelete = new ArrayList<>();
                while (scanCursor.hasNext()) {
                    keysToDelete.add(scanCursor.next());
                    // 分批删除,避免内存占用过高
                    if (keysToDelete.size() >= 100) {
                        connection.del(keysToDelete.toArray(new byte[0][]));
                        keysToDelete.clear();
                    }
                }
                // 删除剩余的键
                if (!keysToDelete.isEmpty()) {
                    connection.del(keysToDelete.toArray(new byte[0][]));
                }
                cursor = scanCursor.getCursorId() + ""; // 更新游标
            } while (!"0".equals(cursor)); // 如果游标为0,表示扫描结束
        } catch (Exception e) {
            log.error("Error while scanning and deleting Redis keys with pattern: {}", pattern, e);
        }
        return null;
    });
}

5、测试验证

5.1、编写测试类

新增测试类,代码如下,新增100个key,然后按照每个批次10个进行删除测试,代码如下

package com.jianjang.zhgl.person.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.ActiveProfiles;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @program: zhgl_server
 * @description: 缓存清理测试类
 * @author: Jian Jang
 * @create: 2025-05-06 11:25:51
 * @blame ZHSF Team
 */

@Slf4j
@ActiveProfiles("local")
@SpringBootTest
public class RedisCleanCacheTest {
    /**
     * 测试key
     */
    private final static String TEST_KEY = "TEST_KEY:";
    private final static String BIZ_KEY = "userId";
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void addCache() {
        for (int i = 0; i < 100; i++) {
            stringRedisTemplate.opsForValue().set(TEST_KEY+BIZ_KEY+i, "value" + i);
        }
    }
    @Test
    public void cleanCache() {
        cleanCache(BIZ_KEY, 10);
    }


    /**
     * 清除缓存内容
     *
     * @param redisKey
     * @param batchSize
     */
    private void cleanCache(String redisKey, int batchSize) {
        String pattern = TEST_KEY + redisKey + "*";
        ScanOptions scanOptions = ScanOptions.scanOptions().match(pattern).count(batchSize).build();
        stringRedisTemplate.execute((RedisCallback<Void>) connection -> {
            String cursor = "0"; // 初始游标
            try {
                do {
                    // 使用SCAN命令分页获取匹配的键
                    Cursor<byte[]> scanCursor = connection.scan(scanOptions);
                    List<byte[]> keysToDelete = new ArrayList<>();
                    while (scanCursor.hasNext()) {
                        keysToDelete.add(scanCursor.next());
                        // 分批删除,避免内存占用过高
                        if (keysToDelete.size() >= batchSize) {
                            connection.del(keysToDelete.toArray(new byte[0][]));
                            keysToDelete.clear();
                        }
                    }
                    // 删除剩余的键
                    if (!keysToDelete.isEmpty()) {
                        connection.del(keysToDelete.toArray(new byte[0][]));
                    }
                    cursor = scanCursor.getCursorId() + ""; // 更新游标
                } while (!"0".equals(cursor)); // 如果游标为0,表示扫描结束
            } catch (Exception e) {
                log.error("Error while scanning and deleting Redis keys with pattern: {}", pattern, e);
            }
            return null;
        });
    }
}

5.2、测试新增

执行新增测试方法后,新增成功,如下图,

5.3、测试批量删除

执行批量删除方法后,删除成功,如下图,100个TEST_KEY已被清除。

到此这篇关于Springboot如何优雅高效的清除Redis中的业务key的文章就介绍到这了,更多相关Springboot清除Redis业务key内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring MVC数据绑定方式

    Spring MVC数据绑定方式

    这篇文章主要介绍了Spring MVC数据绑定方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • Java ThreadPoolExecutor线程池有关介绍

    Java ThreadPoolExecutor线程池有关介绍

    这篇文章主要介绍了Java ThreadPoolExecutor线程池有关介绍,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • Eclipse中导入Maven Web项目并配置其在Tomcat中运行图文详解

    Eclipse中导入Maven Web项目并配置其在Tomcat中运行图文详解

    这篇文章主要介绍了Eclipse中导入Maven Web项目并配置其在Tomcat中运行图文详解,需要的朋友可以参考下
    2017-12-12
  • Java网络编程之简单的服务端客户端应用实例

    Java网络编程之简单的服务端客户端应用实例

    这篇文章主要介绍了Java网络编程之简单的服务端客户端应用,以实例形式较为详细的分析了java网络编程的原理与服务器端客户端的实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-04-04
  • 十五道tomcat面试题,为数不多的机会!

    十五道tomcat面试题,为数不多的机会!

    这篇文章主要介绍了十五道tomcat面试题,Tomcat的本质是一个Servlet容器。一个Servlet能做的事情是:处理请求资源,并为客户端填充response对象,需要的朋友可以参考下
    2021-08-08
  • Java数据结构之对象的比较

    Java数据结构之对象的比较

    比较对象是面向对象编程语言的一个基本特征,下面这篇文章主要给大家介绍了关于Java数据结构之对象的比较,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-02-02
  • springboot如何使用logback-spring配置日志格式,并分环境配置

    springboot如何使用logback-spring配置日志格式,并分环境配置

    这篇文章主要介绍了springboot如何使用logback-spring配置日志格式,并分环境配置的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • Spring Cloud入门教程之Zuul实现API网关与请求过滤

    Spring Cloud入门教程之Zuul实现API网关与请求过滤

    这篇文章主要给大家介绍了关于Spring Cloud入门教程之Zuul实现API网关与请求过滤的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-05-05
  • Java中如何将 int[] 数组转换为 ArrayList(list)

    Java中如何将 int[] 数组转换为 ArrayList(list)

    这篇文章主要介绍了Java中将 int[] 数组 转换为 List(ArrayList),本文通过示例代码给大家讲解的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-12-12
  • Java中的引用类型和使用场景详细

    Java中的引用类型和使用场景详细

    这篇文章介绍的是Java中的引用类型和使用场景,主要内容展开Java中的引用类型,有强引用、软引用 、弱引用、虚引用,需要的朋友可以参考一下
    2021-10-10

最新评论