spring-data-redis 使用pipeline批量设置过期时间的bug
redis没有批量设置过期时间的命令,所以当我们需要为多个key设置过期时间时,只能循环调用expire或pExpire命令为每个key设置过期时间,为了提高性能,我打算使用pipeline来批量操作,我使用的是spring-data-redis的stringRedisTemplate,版本为1.6.4-release,代码如下:
final String[] keys = {"key1", "key2", "key3", key4"};
final Long timeout = 30L;
final TimeUnit unit = TimeUnit.DAYS;
final long rawTimeout = TimeoutUtils.toMillis(timeout, unit);
stringRedisTemplate.executePipelined(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {for (String key : keys) {byte[] rawKey = RedisTemplateSerializerUtil.serializeKey(stringRedisTemplate, key);connection.pExpire(rawKey, rawTimeout);}return null;}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
但这段代码执行下来却会抛出一个异常:
java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.lang.Longat redis.clients.jedis.BuilderFactory$4.build(BuilderFactory.java:46)
at redis.clients.jedis.BuilderFactory$4.build(BuilderFactory.java:44)
at redis.clients.jedis.Response.build(Response.java:51)
at redis.clients.jedis.Response.get(Response.java:36)
at org.springframework.data.redis.connection.jedis.JedisConnection$JedisResult.get(JedisConnection.java:153)
at org.springframework.data.redis.connection.jedis.JedisConnection.convertPipelineResults(JedisConnection.java:353)
at org.springframework.data.redis.connection.jedis.JedisConnection.closePipeline(JedisConnection.java:338)
at org.springframework.data.redis.connection.DefaultStringRedisConnection.closePipeline(DefaultStringRedisConnection.java:2266)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.springframework.data.redis.core.CloseSuppressingInvocationHandler.invoke(CloseSuppressingInvocationHandler.java:57)
at com.sun.proxy.$Proxy112.closePipeline(Unknown Source)
at org.springframework.data.redis.core.RedisTemplate$2.doInRedis(RedisTemplate.java:279)
at org.springframework.data.redis.core.RedisTemplate$2.doInRedis(RedisTemplate.java:264)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:191)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:153)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:141)
at org.springframework.data.redis.core.RedisTemplate.executePipelined(RedisTemplate.java:264)
at org.springframework.data.redis.core.RedisTemplate.executePipelined(RedisTemplate.java:260)
at com.netdragon.course.center.course.service.CourseServiceTest.testRedisPipelinedExpire(CourseServiceTest.java:37)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:233)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:87)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:176)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
跟踪代码执行过程发现,报错的原因在于
图中红线框的部分,这段代码意图在于:如果当传入的时间大于Integer的最大值时,变为去调用pExpireAt方法。
这貌似看起来有点奇怪,但注意到下面的代码里,当调用pipeline.pexpire时,发现居然将传入的过期时间强转为int时,才明白这估计是jedis版本的一个bug,过期时间只支持int类型,如果我们传入的时间大于int类型的最大值时,就不得不去调用pExpireAt方法。上面的那个注释也说明了这个问题(to avoid overflow in jedis)
那么为什么调用pExpireAt就会报错呢,再看上面的代码,调用pExpireAt时,调用了一个time()方法,如图:
我们看看这个time方法是如何执行的,代码如下:
实际上就是通过jedis去获取服务器的当前时间,然后算出实际的过期时间,但是这个命令的调用并不是通过pipeline去调用的,而是直接通过jedis实例去调用,而此时我们还有其他命令通过pipeline去调用,所以导致抛出这个异常。
具体原因如下:
我们都知道pipeline是一直发送命令,然后等到命令都发送完毕后再批量去获取结果,回过头来看之前的那段代码
stringRedisTemplate.executePipelined(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {for (String key : keys) {byte[] rawKey = RedisTemplateSerializerUtil.serializeKey(stringRedisTemplate, key);connection.pExpire(rawKey, rawTimeout);}return null;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
由于jedis版本的问题,导致pExpire在传入的timeout参数大于int类型最大值时,会转为调用pExpireAt方法,这个方法会去获取redis服务器的时间,会调用一次time命令。
那么以上这段代码,每一次循环执行的redis命令为
time (normal) // 普通调用
pexpireat (pipeline) // pipeline调用
- 1
- 2
第一次执行的时候,不会有问题,但第二次执行的时候,由于上一次执行了一次pexpireat命令,因为是pipeline执行,所以结果没有立即返回,实际上在redis-server端已经缓存了这次pexpireat这条命令执行的结果,当循环到第二次时,这时候调用了time命令,且是普通调用,由于使用的是同一个jedis对象(可认为是同一个会话),普通调用会立即向服务端获取执行结果,这样就把上次执行的pexpireat命令的结果拿回来了,由于time命令期望的返回结果类型的是list,而pexpireat执行的结果为long,所以在执行time命令的时候,拿到的是long类型的结果,但强转为list,所以产生了异常,这个异常应该是java.lang.Long cannot be cast to java.util.List。
至于上面的异常为什么是 java.util.ArrayList cannot be cast to java.lang.Long,跟踪stringRedisTemplate的代码,在executePipelined方法内看到
由于上面的time命令发生了异常,这个方法被迫中止,向外抛出异常,但有一个finally代码块,这个代码块里会去关闭pipeline,实际上就是去拿之前的命令执行的结果,基于上诉代码的分析,我们可以知道服务端还缓存着一次time命令执行结果,所以closePipeline将获得这个time命令执行的结果,类型是list,由于expire方法返回的都是long类型,但time命令返回的是list类型,stringRedisTemplate会认为所有的结果都是long类型,所以接收到time命令的list类型返回值时,也会强转会long,所以才会出现java.util.ArrayList cannot be cast to java.lang.Long的异常,将上诉代码改为下面的代码,可以看到两个异常。
final String[] keys = {"key1", "key2", "key3", key4"};
final Long timeout = 30L;
final TimeUnit unit = TimeUnit.DAYS;
final long rawTimeout = TimeoutUtils.toMillis(timeout, unit);
stringRedisTemplate.executePipelined(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {try{for (String key : keys) {byte[] rawKey = RedisTemplateSerializerUtil.serializeKey(stringRedisTemplate, key);connection.pExpire(rawKey, rawTimeout);}} catch (Exception) {e.printStackTrace();}return null;}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
解决方案:升级spring-data-redis版本已经jedis的版本,或使用expire方法,该方法接受的过期时间单位为秒,只要不是太长,都不会超高int类型的最大值
spring-data-redis 使用pipeline批量设置过期时间的bug相关推荐
- redis 存储数据不设置过期时间 会自动过期吗_Redis-数据淘汰策略持久化方式(RDB/AOF)Redis与Memcached区别...
Redis与Memcached区别: 两者都是非关系型数据库.主要有以下不同: 数据类型: Memcached仅支持字符串类型. redis支持:String,List,set,zset,hash 可 ...
- 利用redis保存验证码并设置过期时间
package com.atguigu.jedis;import redis.clients.jedis.Jedis;import java.util.Random;public class Phon ...
- redis hash结构如何设置过期时间
Redis中有个设置时间过期的功能,即通过setex或者expire实现,目前redis没有提供hsetex()这样的方法,redis中过期时间只针对顶级key类型,对于hash类型是不支持的,这个时 ...
- 玩转Redis-干掉钉子户-没有设置过期时间的key
<玩转Redis>系列文章 by zxiaofan主要讲述Redis的基础及中高级应用.本文是<玩转Redis>系列第[15]篇,最新系列文章请前往 公众号"zx ...
- stringredistemplate设置过期时间_Redis的过期删除策略和内存淘汰机制
Redis的key可以设置过期时间,那是否意味着时间一到就会马上被删除呢? Redis的数据存储大小是有限的,假如内存不足Redis有什么应对策略呢? 本篇文章将介绍一下Redis的过期策略和内存淘汰 ...
- spring缓存注解@Cacheable和@CacheEvict,设置过期时间和批量模糊删除
spring缓存注解@Cacheable和@CacheEvict,设置过期时间和批量模糊删除 配置 CacheManager 类 key前缀配置 RedisCache配置 RedisCache 模糊匹 ...
- Redis 笔记(03)— string类型(设置key、获取key、设置过期时间、批量设置获取key、对key进行加减、对key值进行追加、获取value子串)
字符串 string 是 Redis 最简单的数据结构.Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据.不同类型的数据结构的 ...
- Spring中RedisTemplate方法中,redis相关操作笔记。[redis生成指定长度自增批次号,删除、设置过期时间等]
Spring中RedisTemplate方法中,redis相关操作笔记. redis获取自增批次号 // opsForValue()生成long UUID = redisTemplate.opsFor ...
- Spring Data Redis 让 NoSQL 快如闪电(2)
2019独角兽企业重金招聘Python工程师标准>>> 把 Redis 当作数据库的用例 现在我们来看看在服务器端 Java 企业版系统中把 Redis 当作数据库的各种用法吧.无论 ...
最新文章
- R包vegan的Mantel tests探索群落物种组成是否与环境相关
- EOS开发工具Visual-studio-code和CLion设置
- 如何判断当前主机是物理机还是虚拟机
- html5表格图片按比例缩放,JS图片等比例缩放方法完整示例
- getprocaddress得到为0_拼接图像得到全景图
- Application,Session和Cookies的区别
- paip.动画透明淡入淡出窗口之重绘性能
- 日记 [2008年01月05日]NTP 服务器
- wps里的茶色字体怎么设置_VRay茶色玻璃材质参数是什么,要怎么设置?
- Linux/Unix如何将日志发送到日志服务器
- Windows 下使用PDH 获取CPU 使用率
- 华南理工大学计算机学院创新班,高校纷推创新班揽生源 逐步淘汰冷门专业
- 2022-07-02 Android 进入app 后 距离传感器控制手机屏幕熄灭的方法-接近传感器Proximity Sensor的信号
- Pythonic写法
- deepin20.3 的问题
- Bus Video System CodeForces - 978E(思维)
- 十年磨一剑-企业核心竞争力的重塑
- Android EventBus使用
- 塔罗牌微信小程序源码
- 【springboot】redisTemplate Redis key出现\xac\xed\x00\x05t\x00
热门文章
- python————简易的编程题目
- cb32a_c++_STL_算法_查找算法_(5)adjacent_find
- micropython stm32f429 tft_基于STM32F429的TFT0.96屏幕驱动
- linux 心脏滴血漏洞,心脏出血漏洞(heartbleeder 自动检测 OpenSSL 心脏出血漏洞 (附修复指南))...
- 菊花台-中国朝代歌[摘]
- C语言函数一章教学,c语言中函数教学的探讨
- MathType开学季,半价倾情奉献
- 动态规划——最长非降子序列
- ffmpeg截取视频片段(傻瓜教程)
- LORA手持机便携终端PDA的应用场景