文章目录
执行Lua脚本后一直查询不到Redis中的数据(附带详细问题排查过程,一波三折)
这个问题坑惨我了,估计耗费了我两个小时😫,中间走了不少弯路,好在我灵光一闪+GPT给我的灵感否则就栽在这上面了
问题背景
-
问题背景
在使用 Redis 实现接口调用次数扣减操作时,发现响应结果一直返回的是 -1,也就是查询不到数据
问题1:Lua脚本无法切库
-
问题排查过程1:
经过一段排查,加上GPT的提示信息,我快速定位到了是可能是参数的问题,但是通过 Debug 断点调试,我可以百分百肯定这个参数肯定没有问题;然后我又到 redis-cli 执行原生的 Redis 指令,发现也查不到数据!(○´・д・)ノ,懵逼了,捣鼓了半天突然想起来有没有可能是这个数据库中压根没有数据,于是我使用
keys *
指令查看数据库中是否有数据,结果发现有数据,但是压根就没有我预热的数据!!!这是什么原因呢?一看 REP 就恍然大悟了,我操作的数据库1,但是由于我配置文件中使用的是数据库 0,导致我预热的数据 都在 数据库1中,而 Lua脚本默认操作的数据库0,那么问题就简单了,直接使用redis('select',1)
这条指令切换一下数据库不久OK了吗(我真聪明🤭)然后我先在redis-cli上实验,发现的确是这个的原因,嘿嘿问题成功解决了?(你不会以为这就完了吧🤣) -
问题原因:Lua脚本默认使用的数据库0,我的数据在数据库1中
-
问题解决
在执行Lua脚本前,先切换一下数据库
luaredis('SELECT', 1)
==结果发现在 Lua 脚本中添加了
redis('SELECT', 1)
之后压根就没有用!==┭┮﹏┭┮只能继续排查问题,
-
问题排查过程2
在lua脚本中查询数据前,加上
redis('select',1)
,结果发现嘿嘿还是返回-1!这下彻底懵逼了,结果经过询问ChartGPT,发现:Lua脚本不能切换数据库,还是太天真了既然Lua脚本无法完成切库,那么就需要使用 RedisTemplate 进行切库
-
问题解决:
javaRedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.select(1);
你不会以为这么快就解决了这个问题吧!
问题2:RedisTemlate切库报错
-
问题排查过程2:
redisTemplate
切库引发的新问题:在添加了上面代码后,结果又报错Selecting a new database not supported due to shared connection. Use separate ConnectionFactorys to work with multiple databases
.在此询问GPT,发现是由于 Redis 的共享连接限制,无法直接在同一个连接上切换到不同的数据库。如果你需要在执行 Lua 脚本之前切换 Redis 数据库,可以尝试使用不同的连接工厂来处理多个数据库
-
问题解决
-
解决方案一:直接关闭RedisTemplate的共享连接(不推荐)
详情请参考这篇文章:Springboot整合redis切库问题
在使用 Spring Data Redis 时,默认情况下是共享连接的,因为这可以提高性能和效率。然而,如果你确实需要关闭共享连接,并为每个数据库创建一个独立的连接,也是可以的。但是需要注意一些潜在的问题:
- 性能影响:共享连接可以减少连接的开销,而独立连接则需要更多的资源来维护和管理。因此,关闭共享连接可能会对系统的整体性能产生一定的影响。
- 连接池限制:Redis 连接池有着最大连接数的限制,每个连接都占用一部分连接资源。如果为每个数据库都创建独立的连接,那么连接池中可用的连接数量将被均分,这可能导致每个数据库可用连接数的减少。
- 连接管理复杂性:对于每个独立的连接,你需要额外管理和维护连接的生命周期,包括创建、销毁、异常处理等。这可能增加代码的复杂性和维护成本。
总之,关闭共享连接并为每个数据库创建独立的连接可能会带来性能和连接管理方面的一些负面影响。因此,在进行决策时,请权衡利弊,并根据具体需求和系统规模做出选择。
-
解决方案二:新建一个RedisTemplate
为了提高代码的复用性,我就打算利用 单例模式+原型模式 对IOC容器中的RedisTemplate进行一个拷贝,然后将这个新的RedisTemplate来切库,相对于方案一要更加方便
下面是代码:
javapackage com.ghp.admin.redis; import com.ghp.common.utils.BeanConvertorUtils; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisTemplate; /** * @author ghp * @title * @description */ public class RedisUtils { private RedisTemplate redisTemplate; private static RedisUtils instance; static { instance = new RedisUtils(); } public RedisUtils getInstance(RedisTemplate redisTemplate){ this.redisTemplate = redisTemplate; return instance; } /** * 创建一个新的RedisTemplate,并且切换数据库 * @param databaseIndex * @return */ public RedisTemplate<String, String> createRedisTemplate(int databaseIndex) { // 进行拷贝(这里是是使用自己封装的工具类,你可以选择使用Spring框架自带的拷贝工具类) RedisTemplate newRedisTemplate = BeanConvertorUtils.copyBean(redisTemplate, RedisTemplate.class); RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.select(databaseIndex); return newRedisTemplate; } }
看上去好像已经成功解决了,但是还没完,经过测试发现执行 Lua 脚本后仍然无法从Redis中查询到数据库w(゚Д゚)w
这些我又陷入了懵逼中
-
问题3:序列化导致数据不一致
-
问题排查过程3:
我开始陷入怀疑当中,因为我在 redis-cli 中经过切库后,发现执行指令是可以查询到数据的,难道是切库失败了?经过检验发现切库是成功的,因为我在 Lua 脚本中执行硬编码
local leftNum = redis.call('HGET', 'cache:interface:pathMethod:', '/api/name/user-POST')
是的的确确查询到了数据的!我开始怀疑是不是我参数传递错了,但是经过
return leftNum
发现是有数据的,而且数据是真确的,这是什么原因呢?虽然返回结果是一样的,可能不可能Lua脚本中的值不一样呢?我继续抱着试一试的态度,执行下面的代码
lualocal key1 = KEYS[1] if key1 == '/api/name/user' then return 1 end return 2
结果意料之外的居然返回了 2,也就是说我 传入了 '/api/name/user',然后从 Lua 中 返回的是 '/api/name/user',但是 Lua 中两个值竟然不相等!
我开始怀疑难道是编码的问题吗?因为之前在看微信公众号上的一篇文章,有个大佬就是因为编码(全角和半角)的问题导致查询不到数据,于是我抱着试一试的心态,测试了一下编码,有看了一下编译器使用的编码格式,发现是全局
UTF-8
所以可以排除编码问题了,那会是什么原因呢,百思不得其解,突然灵光一闪会不会是序列化的问题,我全局为RedisTemplate配置了序列化转换器,但是我还是不敢相信,因为我在Lua脚本中返回的
retrun ARGV[1]
和我查询使用的 key也就是/api/name/user
是一摸一样的发现仍然照样没有查询到数据,在 Lua 脚本中返回 pathJson,发现是
\"/api/name/user\"
,这一下又给了灵感,会不会是传入的JSON多了有个\"
,于是我做出如下实验lualocal key1 = KEYS[1] if key1 == '\"/api/name/user\"' then return 1 end return 2
惊奇的发现这回终于返回 1,也就是说传入的 path 是 竟然是
\"/api/name/user\"
(结果很震惊,因为之前我在Lua脚本中返回path时,结果就是/api/name/user
,返回的结果根本没有"
),我猜测应该是RedisTemplate在处理Lua的返回结果时会多做一层序列化,登录参数的传递过程中需要经过一层序列化!至此问题终于解决了(你不会以为这就成功解决了吧) -
解决方案
在接收参数后进行预处理,也就是去掉传入参数中多余的
"
lualocal key1 = string.gsub(KEYS[1], "\"", "") -- cache:interface:pathMethod: local key2 = string.gsub(KEYS[2], "\"", "") -- cache:interface:leftNum: -- 获取hashKey local path = string.gsub(ARGV[1], "\"", "") local method = string.gsub(ARGV[2], "\"", "") local userId = string.gsub(ARGV[3], "\"", "")
问题4:Lua脚本中单引号无法拼接字符串
-
问题排查4
后面我改造了代码,发现仍然不成功!!!😫 ┭┮﹏┭┮
经过测试,我发现
lualocal key1 = path .. '-' .. method return key1
发现只会返回
-
前面的字符串 也就是 path,如果调换path和method的未知,那么只会返回method,我又陷入了沉思。后面我突然想起来了,是Lua中双引号和单引号的区别,使用
..
运算符可以用来拼接字符串。这个运算符可以连接两个字符串(或者将其他数据类型转换为字符串后连接)。然而,..
运算符只能用于连接双引号字符串。单引号字符串在 Lua 中表示字符,而不是字符串。如果你试图使用..
运算符连接两个单引号字符串,会得到一个语法错误。 -
解决方法
lualocal key1 = tostring(path) .. "-" .. tostring(method) return key1
成功解决了。如果你对Lua感兴趣的可以参考这篇文章:Lua快速入门笔记_知识汲取者的博客-CSDN博客
总结
终于解决了这个问题,感觉好舒爽😄
总的来说,我遇到的问题是 Java 代码使用 RedisTemplate.execute
执行 Lua脚本的时候,由于我配置了全局序列化,所以导致 传入Lua脚本中的参数 会先被序列化,而Lua脚本的参数被传出来时同样会被反序列化,这个序列化和反序列化对我而言是透明的,所以导致我没有想到居然序列化了一遍,从而导致我走了好多弯路
这里提一嘴:前面那个Lua脚本默认使用数据库 0不完全正确,如果我们在配置文件中配置了使用哪一个数据库,那么在Lua脚本中就会使用哪一个数据库,所以我们可以不用切库,也能保障 Lua脚本能够操作数据库1,所以前面的 问题1 和 问题2 不需要,根本原因在于序列化和反序列化问题
参考资料
- ChartGPT3.5
- Springboot整合redis切库问题