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相关推荐

  1. redis 存储数据不设置过期时间 会自动过期吗_Redis-数据淘汰策略持久化方式(RDB/AOF)Redis与Memcached区别...

    Redis与Memcached区别: 两者都是非关系型数据库.主要有以下不同: 数据类型: Memcached仅支持字符串类型. redis支持:String,List,set,zset,hash 可 ...

  2. 利用redis保存验证码并设置过期时间

    package com.atguigu.jedis;import redis.clients.jedis.Jedis;import java.util.Random;public class Phon ...

  3. redis hash结构如何设置过期时间

    Redis中有个设置时间过期的功能,即通过setex或者expire实现,目前redis没有提供hsetex()这样的方法,redis中过期时间只针对顶级key类型,对于hash类型是不支持的,这个时 ...

  4. 玩转Redis-干掉钉子户-没有设置过期时间的key

      <玩转Redis>系列文章 by zxiaofan主要讲述Redis的基础及中高级应用.本文是<玩转Redis>系列第[15]篇,最新系列文章请前往 公众号"zx ...

  5. stringredistemplate设置过期时间_Redis的过期删除策略和内存淘汰机制

    Redis的key可以设置过期时间,那是否意味着时间一到就会马上被删除呢? Redis的数据存储大小是有限的,假如内存不足Redis有什么应对策略呢? 本篇文章将介绍一下Redis的过期策略和内存淘汰 ...

  6. spring缓存注解@Cacheable和@CacheEvict,设置过期时间和批量模糊删除

    spring缓存注解@Cacheable和@CacheEvict,设置过期时间和批量模糊删除 配置 CacheManager 类 key前缀配置 RedisCache配置 RedisCache 模糊匹 ...

  7. Redis 笔记(03)— string类型(设置key、获取key、设置过期时间、批量设置获取key、对key进行加减、对key值进行追加、获取value子串)

    字符串 string 是 Redis 最简单的数据结构.Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据.不同类型的数据结构的 ...

  8. Spring中RedisTemplate方法中,redis相关操作笔记。[redis生成指定长度自增批次号,删除、设置过期时间等]

    Spring中RedisTemplate方法中,redis相关操作笔记. redis获取自增批次号 // opsForValue()生成long UUID = redisTemplate.opsFor ...

  9. Spring Data Redis 让 NoSQL 快如闪电(2)

    2019独角兽企业重金招聘Python工程师标准>>> 把 Redis 当作数据库的用例 现在我们来看看在服务器端 Java 企业版系统中把 Redis 当作数据库的各种用法吧.无论 ...

最新文章

  1. R包vegan的Mantel tests探索群落物种组成是否与环境相关
  2. EOS开发工具Visual-studio-code和CLion设置
  3. 如何判断当前主机是物理机还是虚拟机
  4. html5表格图片按比例缩放,JS图片等比例缩放方法完整示例
  5. getprocaddress得到为0_拼接图像得到全景图
  6. Application,Session和Cookies的区别
  7. paip.动画透明淡入淡出窗口之重绘性能
  8. 日记 [2008年01月05日]NTP 服务器
  9. wps里的茶色字体怎么设置_VRay茶色玻璃材质参数是什么,要怎么设置?
  10. Linux/Unix如何将日志发送到日志服务器
  11. Windows 下使用PDH 获取CPU 使用率
  12. 华南理工大学计算机学院创新班,高校纷推创新班揽生源 逐步淘汰冷门专业
  13. 2022-07-02 Android 进入app 后 距离传感器控制手机屏幕熄灭的方法-接近传感器Proximity Sensor的信号
  14. Pythonic写法
  15. deepin20.3 的问题
  16. Bus Video System CodeForces - 978E(思维)
  17. 十年磨一剑-企业核心竞争力的重塑
  18. Android EventBus使用
  19. 塔罗牌微信小程序源码
  20. 【springboot】redisTemplate Redis key出现\xac\xed\x00\x05t\x00

热门文章

  1. python————简易的编程题目
  2. cb32a_c++_STL_算法_查找算法_(5)adjacent_find
  3. micropython stm32f429 tft_基于STM32F429的TFT0.96屏幕驱动
  4. linux 心脏滴血漏洞,心脏出血漏洞(heartbleeder 自动检测 OpenSSL 心脏出血漏洞 (附修复指南))...
  5. 菊花台-中国朝代歌[摘]
  6. C语言函数一章教学,c语言中函数教学的探讨
  7. MathType开学季,半价倾情奉献
  8. 动态规划——最长非降子序列
  9. ffmpeg截取视频片段(傻瓜教程)
  10. LORA手持机便携终端PDA的应用场景