问题说明
线上报错@user_script:1: user_script:1: bad argument #2 to 'unpack' (data string too short)
org.redisson.client.RedisException: ERR Error running script (call to f_5ba8bfbad2a802bbb16bd42936dccf1ba5701d4a): @user_script:1: user_script:1: bad argument #2 to 'unpack' (data string too short). channel: [id: 0xc76f8970, L:/172.20.0.34:59666 - R:r-bp1p81fmrf8i661s51.redis.rds.aliyuncs.com/172.16.122.5:6379] command: (EVAL), promise: java.util.concurrent.CompletableFuture@8dbd602d[Not completed, 1 dependents], params: [local value = redis.call('hget', KEYS[1], ARGV[2]); if value == false then return nil; end; local t, val = struct.unpack('dLc0', value); local expireDate = 92233720368547758; local expireDateScore = redis.call('zscore', KEYS[2], ARGV[2]); if expireDateScore ~= false then expireDate = tonumber(expireDateScore) end; if t ~= 0 then local expireIdle = redis.call('zscore', KEYS[3], ARGV[2]); if expireIdle ~= false then if tonumber(expireIdle) > tonumber(ARGV[1]) then redis.call('zadd', KEYS[3], t + tonumber(ARGV[1]), ARGV[2]); end; expireDate = math.min(expireDate, tonumber(expireIdle)) end; end; if expireDate <= tonumber(ARGV[1]) then return nil; end; local maxSize = tonumber(redis.call('hget', KEYS[5], 'max-size')); if maxSize ~= nil and maxSize ~= 0 then local mode = redis.call('hget', KEYS[5], 'mode'); if mode == false or mode == 'LRU' then redis.call('zadd', KEYS[4], tonumber(ARGV[1]), ARGV[2]); else redis.call('zincrby', KEYS[4], 1, ARGV[2]); end; end; return val; , 5, digit:serve:source, redisson__timeout__set:{digit:serve:source}, redisson__idle__set:{digit:serve:source}, redisson__map_cache__last_access__set:{digit:serve:source}, {digit:serve:source}:redisson_options, 1720429243795, PooledUnsafeDirectByteBuf(ridx: 0, widx: 34, cap: 256)]
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:370)
at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:198)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:137)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:113)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:501)
at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:839)
对应的伪代码是
@Component
public class CacheableTest {
@Cacheable(cacheNames = "test")
public String getName(String key) {
return key;
}
}
这里使用了SpringCache,集成Redisson
问题解析
@user_script:1: user_script:1: bad argument #2 to 'unpack' (data string too short)是Redis命令行的报错,这里使用了Redisson作为缓存的中间件,所以从RedissonCache入手
先看下集成RedissonCache的代码
implementation("org.redisson:redisson-spring-boot-starter")
@EnableCaching
public class CacheAutoConfiguration {
@Resource
private RedissonClient redissonClient;
private static final String CONFIG_PATH = "classpath*:cache-config.yaml";
@Override
public CacheManager cacheManager() {
var patternResolver = new PathMatchingResourcePatternResolver();
try {
var resources = patternResolver.getResources(CONFIG_PATH);
var config = new HashMap<String, CacheConfig>();
// 先读取jar里面的配置,再读取项目配置,以便可以覆盖jar的配置
for (var i = resources.length - 1; i >= 0; i--) {
var resource = resources[i];
config.putAll(CacheConfig.fromYAML(resource.getInputStream()));
}
return new RedissonSpringCacheManager(redissonClient, config);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
具体可以参考官方文档:github.com/redisson/re…
关键点是RedissonSpringCacheManager#getCache
@Override
public Cache getCache(String name) {
Cache cache = instanceMap.get(name);
if (cache != null) {
return cache;
}
if (!dynamic) {
return cache;
}
CacheConfig config = configMap.get(name);
if (config == null) {
config = createDefaultConfig();
configMap.put(name, config);
}
if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
return createMap(name, config);
}
return createMapCache(name, config);
}
private Cache createMap(String name, CacheConfig config) {
RMap<Object, Object> map = getMap(name, config);
Cache cache = new RedissonCache(map, allowNullValues);
Cache oldCache = instanceMap.putIfAbsent(name, cache);
if (oldCache != null) {
cache = oldCache;
}
return cache;
}
protected RMap<Object, Object> getMap(String name, CacheConfig config) {
if (codec != null) {
return redisson.getMap(name, codec);
}
return redisson.getMap(name);
}
private Cache createMapCache(String name, CacheConfig config) {
RMapCache<Object, Object> map = getMapCache(name, config);
Cache cache = new RedissonCache(map, config, allowNullValues);
Cache oldCache = instanceMap.putIfAbsent(name, cache);
if (oldCache != null) {
cache = oldCache;
} else {
map.setMaxSize(config.getMaxSize());
}
return cache;
}
protected RMapCache<Object, Object> getMapCache(String name, CacheConfig config) {
if (codec != null) {
return redisson.getMapCache(name, codec);
}
return redisson.getMapCache(name);
}
关键代码解析:
- maxIdleTime、ttl和maxSize都为0时,创建无时限的缓存对象,使用的是RMap
- 其他情况创建对应时限的缓存对象,使用的是RMapCache
按照Redisson官方的说法,同一个key不可以使用不同的Redisson对象,所以最简化的报错复现如下
@Test
void testCache2() throws IOException {
var cache = createMap("test", new CacheConfig(0, 0)); // 无时限的缓存
cache.put("222333", new Person("张三", 12));
cache = createMapCache("test", new CacheConfig(1, 1)); // 有时限的缓存
cache.put("222333", new Person("张三", 12));
}
private Cache createMap(String name, CacheConfig config) {
RMap<Object, Object> map = getMap(name, config);
return new RedissonCache(map, false);
}
protected RMap<Object, Object> getMap(String name, CacheConfig config) {
return redissonClient.getMap(name);
}
private Cache createMapCache(String name, CacheConfig config) {
RMapCache<Object, Object> map = getMapCache(name, config);
Cache cache = new RedissonCache(map, config, false);
map.setMaxSize(config.getMaxSize());
return cache;
}
protected RMapCache<Object, Object> getMapCache(String name, CacheConfig config) {
return redissonClient.getMapCache(name);
}
先创建RMap,然后使用相同的key再创建RMapCache,就会报这个错误
问题解决
虽然知道了问题原因,但是解决方案也想了一下午才想出来
这个问题归根结底是第一次使用的时候没指定缓存失效时间,后来加上了缓存失效的时间,导致此问题。虽然我感觉是Redisson的问题,不过可能是由于Redisson只能用RMap来时限无时效限制的缓存,所以也没法从配置入手。
方案一
最简单的方案就是删除对应的key,然后就会正常,不过此方案比较不靠谱,会导致线上报错。
方案二
由于使用的是Cacheable注解,所以可以考虑实现一个错误处理器,来规避此问题
/**
* @author <a href="mailto:gcwm99@gmail.com">gcdd1993</a>
* Created by gcdd1993 on 2024/7/8
*/
@Slf4j
@RequiredArgsConstructor
public class RedissonCacheErrorHandler extends SimpleCacheErrorHandler {
private final RedissonClient redissonClient;
@Override
public void handleCacheGetError(RuntimeException ex, Cache cache, Object key) {
handleError(ex, cache, key);
}
@Override
public void handleCacheEvictError(RuntimeException ex, Cache cache, Object key) {
handleError(ex, cache, key);
}
@Override
public void handleCachePutError(RuntimeException ex, Cache cache, Object key, Object value) {
handleError(ex, cache, key);
}
private void handleError(RuntimeException ex, Cache cache, Object key) {
LOGGER.error("handle redisson cache error {} {}", cache.getName(), key, ex);
if (ex instanceof RedisException) {
if (ex.getMessage().contains("'unpack' (data string too short)")) {
try {
var map = redissonClient.getMap(cache.getName());
if (map != null) {
map.deleteAsync();
}
} catch (Exception ignore) {
try {
var mapCache = redissonClient.getMapCache(cache.getName()); // 删除错误的key
if (mapCache != null) {
mapCache.deleteAsync();
}
} catch (Exception ignore1) {
}
}
}
}
}
}
原理是出现上述异常的时候,主动删除对应的cache对象,比如一开始是RMap,出现错误时,就将RMap从Redis移除,此次请求正常返回结果(不使用缓存);下次再命中缓存时就正常了
/**
* @author <a href="mailto:gcwm99@gmail.com">gcdd1993</a>
* Created by gcdd1993 on 2024/7/8
*/
@EnableCaching
@ConditionalOnClass(KeyGenerator.class)
public class CacheAutoConfiguration extends CachingConfigurerSupport {
@Resource
private RedissonClient redissonClient;
@Bean
KeyGenerator hashKeyGenerator() {
return new HashKeyGenerator();
}
private static final String CONFIG_PATH = "classpath*:cache-config.yaml";
@Override
public CacheManager cacheManager() {
var patternResolver = new PathMatchingResourcePatternResolver();
try {
var resources = patternResolver.getResources(CONFIG_PATH);
var config = new HashMap<String, CacheConfig>();
// 先读取jar里面的配置,再读取项目配置,以便可以覆盖jar的配置
for (var i = resources.length - 1; i >= 0; i--) {
var resource = resources[i];
config.putAll(CacheConfig.fromYAML(resource.getInputStream()));
}
return new RedissonSpringCacheManager(redissonClient, config);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public CacheErrorHandler errorHandler() {
return new RedissonCacheErrorHandler(redissonClient);
}
}
不要忘记注册一下CacheErrorHandler