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类型的最大值