sharded jedis pipelined 执行后 数据并未存入redis

news2024/11/15 8:37:48

前言

因为历史原因,在某个同步菜单操作的方法中先清除缓存,然后在初始化缓存。本来很正常的逻辑,但是这个清除是db查询获取所有的菜单 然后循环一条条删除 然后在db查询有效的菜单操作 在循环一条条插进去 经统计这个菜单操作大概有个7千个 执行 耗时过久 大概50s -60s 不等

优化

因为一些体验问题 也自然而然 想到优化

第一种 使用并行 插入或者删除

使用到stream的parallelStream 来并行执行 由于redis本身的单线程执行限制 时间来到了 10-15秒左右 体验效果还不是很好

第二种 使用pipeline 来批量执行命令

由于并行执行 提升的效果有限,我们换个思路来解决问题,减少与redis的交互 将命令批量执行 这样就会大大减少执行耗时 时间来到了 1- 2秒这个优化效果还是比较理想的 但是也发现了新的问题

问题

虽然执行效果很快 但是在初始化缓存的时候 发现并没有成功初始化缓存

先看下 示例代码

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class Test{
	@Autowired
    private ShardedJedis shardedJedis;

    @Test
    public void test(){
        ShardedJedisPipeline pipelined1 = shardedJedis.pipelined();
        //模拟业务逻辑
        for (int i = 0; i < 50; i++) {
            String key = "key:"+i;
            pipelined1.set(key,String.valueOf(i));
            pipelined1.expire(key,-1);
        }
        pipelined1.sync();
    }
}

排查定位

这代码看着 好像也没啥问题 批量执行50个key的set 以及expire 操作
最后获取pipeline所有命令的执行结果

期间以为和使用pipelined的set方法 String 入参有关 于是更换为支持byte的方法 未果

后续还以为使用用法不对,经查询多方资料后 发现用法没问题

省略其他的尝试步骤。。。。。

最后将把expire 的设置注释掉 果然可以了

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class Test{
	@Autowired
    private ShardedJedis shardedJedis;

    @Test
    public void test(){
        ShardedJedisPipeline pipelined1 = shardedJedis.pipelined();
        //模拟业务逻辑
        for (int i = 0; i < 50; i++) {
            String key = "key:"+i;
            pipelined1.set(key,String.valueOf(i));
            //pipelined1.expire(key,-1);
        }
        pipelined1.sync();
    }
}

最后问题定位到 是因为 pipelined1.expire(key,-1) 命令执行导致数据无法存入redis

分析

pipelined1.expire(key,-1)  

这个命令看似很正常 想法是设置一个 -1 来表示这个缓存无过期时间 但实际上 好像并没有生效
查看源码后 并无没有什么特殊操作

@Deprecated
default Response<Long> expire(String key, int seconds) {
  return expire(key, (long) seconds);
}

Response<Long> expire(String key, long seconds);

由于未使用 long类型的时间 ,默认调用时间类为 int类型的方法 最后实际上调用的还是 long类型的时间方法
再往下就直接设置命令了

@Override
public Response<Long> expire(final String key, final long seconds) {
    getClient(key).expire(key, seconds);
    return getResponse(BuilderFactory.LONG);
}

# redis.clients.jedis.BinaryClient#expire(byte[], long)
public void expire(final byte[] key, final long seconds) {
   sendCommand(EXPIRE, key, toByteArray(seconds));
}

于是找到redis client 执行了命令 发现也很快失效
在这里插入图片描述
于是猜测 -1 这个过期时间会被设置 可能失效时间很短 有可能是 1毫秒 或者1 毫秒
带着问题 去找了下官方文档 看到这样一句描述
在这里插入图片描述
好像只写到了 会将过期时间戳存储为 绝对值 至于传入的时间 为负数 该如何处理并未说明

那就再来看下源码的逻辑
过期命令的实现类在 https://github.com/redis/redis/blob/unstable/src/expire.c

/* EXPIRE key seconds [ NX | XX | GT | LT] */
void expireCommand(client *c) {
    expireGenericCommand(c,commandTimeSnapshot(),UNIT_SECONDS);
}

//核心调用方法
void expireGenericCommand(client *c, long long basetime, int unit) {
    robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */
    long long current_expire = -1;
    int flag = 0;

    /* checking optional flags */
    if (parseExtendedExpireArgumentsOrReply(c, &flag) != C_OK) {
        return;
    }
    //解析我们传入的时间参数 并赋值给when 这里我们传入的是-1
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK)
        return;

    /* EXPIRE allows negative numbers, but we can at least detect an
     * overflow by either unit conversion or basetime addition. */
    if (unit == UNIT_SECONDS) {
        if (when > LLONG_MAX / 1000 || when < LLONG_MIN / 1000) {
            addReplyErrorExpireTime(c);
            return;
        }
        when *= 1000;
    }

    if (when > LLONG_MAX - basetime) {
        addReplyErrorExpireTime(c);
        return;
    }
    // 时间戳计算 这里相当于是 当前时间戳 -1 
    when += basetime;

    /* No key, return zero. */
    if (lookupKeyWrite(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }

    if (flag) {
        current_expire = getExpire(c->db, key);

        /* NX option is set, check current expiry */
        if (flag & EXPIRE_NX) {
            if (current_expire != -1) {
                addReply(c,shared.czero);
                return;
            }
        }

        /* XX option is set, check current expiry */
        if (flag & EXPIRE_XX) {
            if (current_expire == -1) {
                /* reply 0 when the key has no expiry */
                addReply(c,shared.czero);
                return;
            }
        }

        /* GT option is set, check current expiry */
        if (flag & EXPIRE_GT) {
            /* When current_expire is -1, we consider it as infinite TTL,
             * so expire command with gt always fail the GT. */
            if (when <= current_expire || current_expire == -1) {
                /* reply 0 when the new expiry is not greater than current */
                addReply(c,shared.czero);
                return;
            }
        }

        /* LT option is set, check current expiry */
        if (flag & EXPIRE_LT) {
            /* When current_expire -1, we consider it as infinite TTL,
             * but 'when' can still be negative at this point, so if there is
             * an expiry on the key and it's not less than current, we fail the LT. */
            if (current_expire != -1 && when >= current_expire) {
                /* reply 0 when the new expiry is not less than current */
                addReply(c,shared.czero);
                return;
            }
        }
    }

    //检测设置的过期时间 是否已经过期 
    if (checkAlreadyExpired(when)) {
        // 过期执行删除逻辑
        robj *aux;

        int deleted = dbGenericDelete(c->db,key,server.lazyfree_lazy_expire,DB_FLAG_KEY_EXPIRED);
        serverAssertWithInfo(c,key,deleted);
        server.dirty++;

        /* Replicate/AOF this as an explicit DEL or UNLINK. */
        aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
        rewriteClientCommandVector(c,2,aux,key);
        signalModifiedKey(c,c->db,key);
        notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
        //删除后 回复了一个 1 和我们之前测试的情况相符
        addReply(c, shared.cone);
        return;
    } else {
        setExpire(c,c->db,key,when);
        addReply(c,shared.cone);
        /* Propagate as PEXPIREAT millisecond-timestamp
         * Only rewrite the command arg if not already PEXPIREAT */
        if (c->cmd->proc != pexpireatCommand) {
            rewriteClientCommandArgument(c,0,shared.pexpireat);
        }

        /* Avoid creating a string object when it's the same as argv[2] parameter  */
        if (basetime != 0 || unit == UNIT_SECONDS) {
            robj *when_obj = createStringObjectFromLongLong(when);
            rewriteClientCommandArgument(c,2,when_obj);
            decrRefCount(when_obj);
        }

        signalModifiedKey(c,c->db,key);
        notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
        server.dirty++;
        return;
    }
}

//只有在非加载数据和非从实例的情况下,当 when 小于等于当前时间戳时,checkAlreadyExpired 函数才会返回 true,表示该过期时间已经过期,可以立即删除该键。
int checkAlreadyExpired(long long when) {
    /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
     * should never be executed as a DEL when load the AOF or in the context
     * of a slave instance.
     *
     * Instead we add the already expired key to the database with expire time
     * (possibly in the past) and wait for an explicit DEL from the master. */
    return (when <= commandTimeSnapshot() && !server.loading && !server.masterhost);
}

看了代码后 思路也清晰了, 这个设置时间过期的逻辑 我们简单梳理下
这个当执行过期时间命令时,我们会传入 key 以及 过期时间(单位秒 或者 毫秒值) 以及 flag 参数 例如 nx xx 等等
核心的逻辑

  • 判断时间参数
  • 计算过期时间 = 当前时间戳 + 传入的过期时间参数 (单位秒/毫秒)
  • 执行 flag参数 逻辑
  • 执行 checkAlreadyExpired 判断时间是否已经过期 只有在 只有在非加载数据和非从实例的情况下,当 when 小于等于当前时间戳时,checkAlreadyExpired 函数才会返回 true 就会走到删除key的逻辑 并返回
  • 没过期则进行设置新的过期时间 并返回

回到我们的执行操作中,我们执行expire 命令传入的时间参数为-1, 那过期时间就设置为当前时间戳 - 1000 。最后又因为设置的过期时间满足过期条件 (when 小于等于当前时间戳 非加载数据和非从实例),所以我们key 立刻会被删除 。这就导致了虽然我们方法执行完成,但是缓存却没有。

解决

当需要设置一个没有过期时间的key的话 无需要调用expire方法 因为默认没有设置过期时间的话 就是永久不失效
在这里插入图片描述
参考官方文档: 官方文档地址: https://redis.io/docs/latest/commands/expire/


good day !!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1685308.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实战Java虚拟机-高级篇

一、GraalVM 什么是GraalVM GraalVM是Oracle官方推出的一款高性能JDK&#xff0c;使用它享受比OpenJDK或者OracleJDK更好的性能。GraalVM的官方网址&#xff1a;https://www.graalvm.org/官方标语&#xff1a;Build faster, smaller, leaner applications。 更低的CPU、内存…

化简资源分配图判断是否发生死锁

目录 1.资源分配图的概念 2.判断是否发生死锁 1.资源分配图的概念 资源分配图表示进程和资源之间的请求关系&#xff0c;例如下图&#xff1a; P代表进程&#xff0c;R代表资源&#xff0c;R方框中 有几个圆球就表示有几个这种资源&#xff0c;在图中&#xff0c;R1指向P1&a…

【加密与解密(第四版)】第十五章笔记

第十五章 专用加密软件 15.1 认识壳 15.2 压缩壳 UPX、ASPack、PECompact 15.3 加密壳 ASProtect(压缩、加密、反跟踪代码、CRC校验、花指令)、Armadillo(穿山甲)、EXECryptor、Themida 15.4 虚拟机保护软件 虚拟机引擎&#xff08;编译器解释器虚拟CPU环境指令系统&#xff…

后端数据库开发JDBC编程Mybatis之用基于XML文件的方式映射SQL语句实操

之前的SQL语句是基于注解 以后开发中一般是一个接口对应一个映射文件 书写映射文件 基本结构 框架 <?xml version"1.0" encoding"UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.or…

嵩山是颍水的嵩山

颍水对于嵩山具有特别重要的意义&#xff0c;嵩山的水流大多数汇入了颍河&#xff0c;颍河流域约占登封市总面积88%&#xff0c;从这个角度讲&#xff0c;嵩山就是颍水的嵩山。 再看环嵩山地区&#xff0c;即“嵩山文化圈”&#xff0c;学者们按黄、淮、济分为三个水系区。黄河…

解决IE11通过主机名访问和IP地址访问,CSS渲染效果不一致问题

软件环境 spingboot:版本2.6.13 浏览器&#xff1a;IE11 问题描述 html用css渲染&#xff0c;浏览器输入IP地址访问&#xff0c;和输入主机名访问&#xff0c;效果不一样&#xff0c;如下图&#xff1a; IP地址访问才是我想要的效果&#xff0c;主机访问菜单半透明向下箭头…

如何让大模型更聪明?

如何让大模型更聪明&#xff1f; *随着人工智能技术的飞速发展&#xff0c;大模型在多个领域展现出了前所未有的能力&#xff0c;但它们仍然面临着理解力、泛化能力和适应性等方面的挑战。那么&#xff0c;如何让大模型变得更聪明呢&#xff1f; 方向一&#xff1a;算法创新 …

Web3空投流程分享:如何操作及注意事项

空投&#xff08;Airdrop&#xff09;指通过向特定用户群体免费分发代币&#xff0c;项目方希望能够吸引更多的用户和关注。对于许多刚刚接触加密货币和区块链的新手来说&#xff0c;都会疑惑空投的流程究竟是什么样的呢&#xff1f;又该如何参与其中&#xff1f;本文将为您详细…

AI智能体|使用扣子Coze基于IDE创建自定义插件

大家好&#xff0c;我是无界生长。 在使用Coze的过程中&#xff0c;有些个性化场景无法通过插件商店已有的插件满足&#xff0c;这个时候就需要通过自定义插件的方式来实现业务需求。下面将通过一个实际案例来简单介绍下如何使用Coze基于IDE创建自定义插件&#xff0c;完成在Co…

SpringCloud微服务02-微服务拆分-注册中心-OpenFeign-网关路由-网关登录校验-配置管理

一、认识微服务 1.单体架构 当高并发功能和重要功能都在同一个Tomcat服务器&#xff0c;当高并发的时候&#xff0c;重要的功能接口就会受到影响。 2.微服务 跨服务就需要一些技术栈 3.SpringCloud 二、微服务拆分 1.熟悉黑马商场 2.服务拆分原则 什么时候用微服务&#xf…

HTML+CSS+JS 扩散登录表单动画

效果演示 Code <!DOCTYPE html> <html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,us…

Grafana HTML Panel展示post获取后数据

<!DOCTYPE html> <html> <head><title>API 数据表格展示</title><script src"https://code.jquery.com/jquery-3.6.0.min.js"></script> </head> <body><table id"data-table" border"1&qu…

每日一题《leetcode--143.重排链表》

https://leetcode.cn/problems/reorder-list/ 这道题我们可以用两种方法 第一种&#xff1a;用数组存储该链表&#xff0c;然后通过访问数组下标的方式来改变该链表的排列方式。 void reorderList(struct ListNode* head) {第一种方法&#xff1a;用线性表存储该链表if(head …

【ARMv8/v9 异常模型入门及渐进 10 -- WFI 与 WFE 使用详细介绍 1】

请阅读【ARMv8/v9 ARM64 System Exception】 文章目录 WFI 与 WFE等待事件&#xff08;WFE&#xff09;发送事件&#xff08;SEV&#xff09;本地发送事件&#xff08;SEVL&#xff09;WFE 唤醒事件 WFE 使用场景举例与代码实现wfe睡眠函数sev 事件唤醒函数全局监视器和自旋锁 …

项目从 Mysql切换 PostgreSQL 改造及踩坑记录

项目从 Mysql切换 PostgreSQL 改造及踩坑记录 切换流程 项目引入postgresql驱动包 由于我们要连接新的数据库&#xff0c;理所当然的要引入该数据库的驱动包&#xff0c;这与mysql驱动包类似 <dependency><groupId>org.postgresql</groupId><artifactI…

美容美发行业收银系统源码、美业SaaS系统源码

美业SaaS系统 连锁多门店美业收银系统源码 多门店管理 / 会员管理 / 预约管理 / 排班管理 / 商品管理 / 促销活动 PC管理后台、手机APP、iPad APP、微信小程序

leetcode-设计LRU缓存结构-112

题目要求 思路 双链表哈希表 代码实现 struct Node{int key, val;Node* next;Node* pre;Node(int _key, int _val): key(_key), val(_val), next(nullptr), pre(nullptr){} };class Solution { public: unordered_map<int, Node*> hash; Node* head; Node* tail; int …

【520】《架构演进之路》选题通过

选题通过 碰巧是520这一天&#xff0c;清华社编辑今天下午回复书籍&#xff08;架构演进之路&#xff09;选题通过&#xff0c;一个有纪念意义的日子。 架演共创者联盟成立 目前参与者有8位成员&#xff0c;涉及前后端、大数据、app、数字孪生&#xff0c;可比一个真实项目团队…