我需要实现的逻辑是模拟一个滑动窗口,某个key根据业务需要30秒过期,如果有人获取了这个值,则从获取的时间开始进行延期30s,相当于滑动了一次过期窗口。
在Redis中,具体的实现是检查哈希表(hash)中的键(key)和字段(field)是否存在。如果存在,就重新设置该键的过期时间,并返回对应的值。
由于该滑动窗口的操作调用频率非常高,我不希望每一个步骤都与Redis进行一次交互。在网上查资料后,最先找到的是RedisTemplateexecutePipelined:

/**
 * Executes the given Redis session on a pipelined connection. Allows transactions to be pipelined. Note that the
 * callback <b>cannot</b> return a non-null value as it gets overwritten by the pipeline.
 *
 * @param session Session callback
 * @return list of objects returned by the pipeline
 */
List<Object> executePipelined(SessionCallback<?> session);

注释的意思是:在一个管道连接上执行给定的 Redis 会话。允许事务被管道化。注意,回调 不能 返回非空值,因为它会被管道覆盖。
当时,我错误地认为这个方法能够满足我的所有需求,因此基于这个方法编写了一个版本:

List<Object> results = stringRedisTemplate.executePipelined((RedisCallback<String>) connection -> {
    // 如果没有对应的key,则直接返回null
    StringRedisConnection stringRedisConn = (StringRedisConnection) connection;

    if (Boolean.FALSE.equals(stringRedisConn.exists(redisKey))) {
        return null;
    } else {
        // 如果有对应的没过期的key,则从hash里获取对应的数据
        final String result = stringRedisConn.hGet(redisKey, fieldKey);
        if (result == null || result.isEmpty()) {
            return null;
        } else {
            stringRedisConn.expire(redisKey, scanWindowTime);
            return result;
        }
    }
});

运行之后发现,results获取到的值很怪,而且非常不稳定,有时候是

[false, null]

有时候是

[true, null]

总之就是没有一个正确期望的值。
后来我感觉到自己应该是用错了executePipelined,去Github搜了下别人是如何使用这个函数的,果然这个函数的使用方式和我想的相差太远了。
首先,这个函数的正确运行方式应该是:return并不会返回你想要的东西,你需要的东西其实是connection中各种操作redis的方法自动添加到返回的results中的。比如:stringRedisConn.exists(redisKey)这句函数会自动向返回的results中添加是否存在的结果,也就是true或者false
第二,这个函数并不会执行Java逻辑,代码中所写的各种通过获取到的值来进行分支判断的逻辑都不会运行,因为这个函数的本意就是让使用者在一次连接中运行多条命令而不是进行业务逻辑。
因此,这个方案不可行。在网上查询了一下后决定改为使用Lua脚本的方式,RedisTemplate也提供了对应的方法:

/**  
 * Executes the given {@link RedisScript}  
 * @param script The script to execute  
 * @param keys Any keys that need to be passed to the script  
 * @param args Any args that need to be passed to the script  
 * @return The return value of the script or null if {@link RedisScript#getResultType()} is null, likely indicating a  
 *         throw-away status reply (i.e. "OK") */@Nullable  
<T> T execute(RedisScript<T> script, List<K> keys, Object... args);

通过传递一个script对象和对应的keys、args就能够执行lua脚本,我实现的lua脚本如下:

String luaScript = "local value = redis.call('HGET', KEYS[1], ARGV[1]) " +  
        "if value then " +  
        "  redis.call('EXPIRE', KEYS[1], ARGV[2]) " +  
        "  return value " +  
        "else " +  
        "  return nil " +  
        "end";

逻辑与之前的java版本基本相同,然后再创建对象并调用execute方法:
“`java
DefaultRedisScript redisScript = new DefaultRedisScript<>(luaScript, String.class);

String result = stringRedisTemplate.execute(redisScript, keys, field, String.valueOf(scanWindowTime));
“`
这样获取到的result就是对应的value(如果存在的话)并且会延长对应key的过期时间。

这一番折腾主要有两个原因,第一是executePipelined方法的注释上并未说明该方法的使用方式并且这个使用方式也非常的不Java;第二就是我并没有查看网上对应的使用用例而是凭着经验来武断的使用。还好解决了这个问题吧。
PS:executePipelined在LLM的幻觉中能在Java里处理逻辑,最后是我纠正了它。


0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用 * 标注