GeXiangDong

精通Java、SQL、Spring的拼写,擅长Linux、Windows的开关机

0%

Redis中存储POJO,POJO增删属性后重新部署,未清除Redis缓存导致的反序列化失败

现象

使用spring boot的时候,缓存是常用的服务之一,放在缓存里的数据经常是个pojo,Java类放入缓存默认是通过序列化实现存储的。有时候升级改代码会增删一些属性,如果部署前忘记把相应的缓存先清除一下,就会遇到反序列化失败的异常了,异常信息一般如下:

org.springframework.data.redis.serializer.SerializationException: Cannot deserialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is java.io.InvalidClassException: com.package-of-pojo.Xxxx; local class incompatible: stream classdesc serialVersionUID = -2364286648166609117, local class serialVersionUID = -8974455668551700477
    at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:84)
    at org.springframework.data.redis.serializer.DefaultRedisElementReader.read(DefaultRedisElementReader.java:48)
    at org.springframework.data.redis.serializer.RedisSerializationContext$SerializationPair.read(RedisSerializationContext.java:272)
    at org.springframework.data.redis.cache.RedisCache.deserializeCacheValue(RedisCache.java:260)
    at org.springframework.data.redis.cache.RedisCache.lookup(RedisCache.java:94)
    at org.springframework.cache.support.AbstractValueAdaptingCache.get(AbstractValueAdaptingCache.java:58)
    at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:73)
    at org.springframework.cache.interceptor.CacheAspectSupport.findInCaches(CacheAspectSupport.java:554)
    at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:519)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:401)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:345)
    at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)

解决方案

遇到这种异常,一般很容易解决,使用 redis-cli keys 'xxx*' | xargs -n 1 redis-cli del 这个命令去把所有的这个缓存查出来并且删了就好了。

但是这是一个很容易出现的问题,每次都手工去避免比较麻烦。是不是能够通过程序实现,如果遇到这种异常,自动清除redis内的对应内容并自动执行对应的方法,不从redis取了呢(例如 Cacheable 注解的方法,执行方法返回结果并将返回内容重新放入缓存服务器),这个思路是可行的,通过配置一个自定义的 CacheErrorHandler来实现。

自定义的CacheErrorHandler

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.core.serializer.support.SerializationFailedException;

/** 此类处理过的异常,spring 不会再次抛出了,除非这里的代码里再次抛出 */
public class CustomCacheErrorHandler implements CacheErrorHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomCacheErrorHandler.class);

@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
logger.error("获取缓存数据时发生异常 cache-name: {}, cache-key:{}", cache.getName(), key, e);
if (e instanceof SerializationFailedException) {
logger.warn("序列化失败导致,清除该cache");
cache.clear();
}
}

@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object o, Object key) {
logger.error("handleCachePutError cache-name: {}, cache-key:{}", cache.getName(), key, e);
}

@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
logger.error("handleCacheEvictError cache-name: {}, cache-key:{}", cache.getName(), key, e);
}

@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
logger.error("handleCacheClearError cache-name: {}, cache-key:{}", cache.getName(), e);
}
}


spring在redis里存储的key都是 User:1111 User:2222 这种类型,其中 User 是cache的name, 1111 / 2222 则是key,当pojo反序列化失败时,所有cache-name相同的条目都失效了,所以17行有 cache.clear() 把整个cache都清除了;如果不写这行,则每个key执行一次,从结果上看也没问题。

注册这个自定义个的CacheErrorHandler

1
2
3
4
5
6
7
8
9
10
11

@Configuration
public class CacheErrorHandlerConfig extends CachingConfigurerSupport {
private static final Logger logger = LoggerFactory.getLogger(CacheErrorHandlerConfig.class);

@Override
public CacheErrorHandler errorHandler() {
return new CustomCacheErrorHandler();
}
}