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>() {
    @Override
    public 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.Long

at 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>() {
            @Override
            public 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>() {
    @Override
    public 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类型的最大值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值