赞
踩
因为历史原因,在某个同步菜单操作的方法中先清除缓存,然后在初始化缓存。本来很正常的逻辑,但是这个清除是db查询获取所有的菜单 然后循环一条条删除 然后在db查询有效的菜单操作 在循环一条条插进去 经统计这个菜单操作大概有个7千个 执行 耗时过久 大概50s -60s 不等
因为一些体验问题 也自然而然 想到优化
使用到stream的parallelStream 来并行执行 由于redis本身的单线程执行限制 时间来到了 10-15秒左右 体验效果还不是很好
由于并行执行 提升的效果有限,我们换个思路来解决问题,减少与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 等等
核心的逻辑
回到我们的执行操作中,我们执行expire 命令传入的时间参数为-1, 那过期时间就设置为当前时间戳 - 1000 。最后又因为设置的过期时间满足过期条件 (when 小于等于当前时间戳 非加载数据和非从实例),所以我们key 立刻会被删除 。这就导致了虽然我们方法执行完成,但是缓存却没有。
当需要设置一个没有过期时间的key的话 无需要调用expire方法 因为默认没有设置过期时间的话 就是永久不失效
参考官方文档: 官方文档地址: https://redis.io/docs/latest/commands/expire/
good day !!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。