spring-data-redis 使用pipeline批量设置过期时间的bug

来源:互联网 发布:百度炒股软件 编辑:程序博客网 时间:2024/06/05 15:46

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;    }});

但这段代码执行下来却会抛出一个异常:

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)

跟踪代码执行过程发现,报错的原因在于
这里写图片描述
图中红线框的部分,这段代码意图在于:如果当传入的时间大于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;            }

由于jedis版本的问题,导致pExpire在传入的timeout参数大于int类型最大值时,会转为调用pExpireAt方法,这个方法会去获取redis服务器的时间,会调用一次time命令。
那么以上这段代码,每一次循环执行的redis命令为

time  (normal) // 普通调用pexpireat  (pipeline) // pipeline调用

第一次执行的时候,不会有问题,但第二次执行的时候,由于上一次执行了一次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;    }});

解决方案:升级spring-data-redis版本已经jedis的版本,或使用expire方法,该方法接受的过期时间单位为秒,只要不是太长,都不会超高int类型的最大值

0 0
原创粉丝点击