背景
项目使用springboot整合redis做缓存,代码中使用spring的缓存注解配置缓存策略。在jarvis上部署时接入了公司分布式redis平台代替本地的redis。结果测试的时候,新增一条记录时报了错,提示 ERR unknown command 'keys' 。
经排查发现问题原因:新增记录的函数上有@CacheEvit,用于废弃redis中的缓存。推测是由于底层使用了redis的 keys命令进行缓存key的规则匹配。而生成环境禁用了Keys命令,导致报错。
配置类
1、CustomRedisCacheWriter.java ------ 根据 DefaultRedisCacheWriter.java 修改的
import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; /** * {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone} * and {@literal cluster} environments. Works upon a given {@link RedisConnectionFactory} to obtain the actual * {@link RedisConnection}. <br /> * {@link CustomRedisCacheWriter} can be used in * {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or * {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While * {@literal non-locking} aims for maximum performance it may result in overlapping, non atomic, command execution for * operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents * command overlap by setting an explicit lock key and checking against presence of this key which leads to additional * requests and potential command wait times. * * @author Christoph Strobl * @author Mark Paluch * @since 2.0 */ class CustomRedisCacheWriter implements RedisCacheWriter { private final RedisConnectionFactory connectionFactory; private final Duration sleepTime; /** * @param connectionFactory must not be {@literal null}. */ CustomRedisCacheWriter(RedisConnectionFactory connectionFactory) { this(connectionFactory, Duration.ZERO); } /** * @param connectionFactory must not be {@literal null}. * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO} * to disable locking. */ CustomRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); Assert.notNull(sleepTime, "SleepTime must not be null!"); this.connectionFactory = connectionFactory; this.sleepTime = sleepTime; } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCacheWriter#put(java.lang.String, byte[], byte[], java.time.Duration) */ @Override public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { Assert.notNull(name, "Name must not be null!"); Assert.notNull(key, "Key must not be null!"); Assert.notNull(value, "Value must not be null!"); execute(name, connection -> { if (shouldExpireWithin(ttl)) { connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); } else { connection.set(key, value); } return "OK"; }); } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCacheWriter#get(java.lang.String, byte[]) */ @Override public byte[] get(String name, byte[] key) { Assert.notNull(name, "Name must not be null!"); Assert.notNull(key, "Key must not be null!"); return execute(name, connection -> connection.get(key)); } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCacheWriter#putIfAbsent(java.lang.String, byte[], byte[], java.time.Duration) */ @Override public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { Assert.notNull(name, "Name must not be null!"); Assert.notNull(key, "Key must not be null!"); Assert.notNull(value, "Value must not be null!"); return execute(name, connection -> { if (isLockingCacheWriter()) { doLock(name, connection); } try { if (connection.setNX(key, value)) { if (shouldExpireWithin(ttl)) { connection.pExpire(key, ttl.toMillis()); } return null; } return connection.get(key); } finally { if (isLockingCacheWriter()) { doUnlock(name, connection); } } }); } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCacheWriter#remove(java.lang.String, byte[]) */ @Override public void remove(String name, byte[] key) { Assert.notNull(name, "Name must not be null!"); Assert.notNull(key, "Key must not be null!"); execute(name, connection -> connection.del(key)); } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCacheWriter#clean(java.lang.String, byte[]) */ @Override public void clean(String name, byte[] pattern) { Assert.notNull(name, "Name must not be null!"); Assert.notNull(pattern, "Pattern must not be null!"); execute(name, connection -> { boolean wasLocked = false; try { if (isLockingCacheWriter()) { doLock(name, connection); wasLocked = true; } // byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet()) // .toArray(new byte[0][]); // // 使用scan命令代替原本的keys命令搜索key Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(new String(pattern)) .count(1000).build()); Set<byte[]> byteSet = new HashSet<>(); while (cursor.hasNext()) { byteSet.add(cursor.next()); } byte[][] keys = byteSet.toArray(new byte[0][]); if (keys.length > 0) { connection.del(keys); } } finally { if (wasLocked && isLockingCacheWriter()) { doUnlock(name, connection); } } return "OK"; }); } /** * Explicitly set a write lock on a cache. * * @param name the name of the cache to lock. */ void lock(String name) { execute(name, connection -> doLock(name, connection)); } /** * Explicitly remove a write lock from a cache. * * @param name the name of the cache to unlock. */ void unlock(String name) { executeLockFree(connection -> doUnlock(name, connection)); } private Boolean doLock(String name, RedisConnection connection) { return connection.setNX(createCacheLockKey(name), new byte[0]); } private Long doUnlock(String name, RedisConnection connection) { return connection.del(createCacheLockKey(name)); } boolean doCheckLock(String name, RedisConnection connection) { return connection.exists(createCacheLockKey(name)); } /** * @return {@literal true} if {@link RedisCacheWriter} uses locks. */ private boolean isLockingCacheWriter() { return !sleepTime.isZero() && !sleepTime.isNegative(); } private <T> T execute(String name, Function<RedisConnection, T> callback) { RedisConnection connection = connectionFactory.getConnection(); try { checkAndPotentiallyWaitUntilUnlocked(name, connection); return callback.apply(connection); } finally { connection.close(); } } private void executeLockFree(Consumer<RedisConnection> callback) { RedisConnection connection = connectionFactory.getConnection(); try { callback.accept(connection); } finally { connection.close(); } } private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) { if (!isLockingCacheWriter()) { return; } try { while (doCheckLock(name, connection)) { Thread.sleep(sleepTime.toMillis()); } } catch (InterruptedException ex) { // Re-interrupt current thread, to allow other participants to react. Thread.currentThread().interrupt(); throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name), ex); } } private static boolean shouldExpireWithin(@Nullable Duration ttl) { return ttl != null && !ttl.isZero() && !ttl.isNegative(); } private static byte[] createCacheLockKey(String name) { return (name + "~lock").getBytes(StandardCharsets.UTF_8); } }
2、BaseRedisConfig.java
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; /** * Redis基础配置 */ @EnableCaching @Configuration public class BaseRedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisSerializer<Object> serializer = redisSerializer(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(serializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(serializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public RedisSerializer<Object> redisSerializer() { //创建JSON序列化器 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 全局配置 忽略未知属性 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 序列化JSON串时,在值上打印出对象类型,反序列化时,不需要自己转类型 objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // 处理反序列化LocalDateTime的问题 objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.registerModule(new JavaTimeModule()); serializer.setObjectMapper(objectMapper); return serializer; } @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { //创建JSON序列化器 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 全局配置 忽略未知属性 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 序列化JSON串时,在值上打印出对象类型 objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // 处理反序列化LocalDateTime的问题 objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.registerModule(new JavaTimeModule()); serializer.setObjectMapper(objectMapper); // RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); RedisCacheWriter redisCacheWriter = new CustomRedisCacheWriter(redisConnectionFactory); //设置Redis缓存有效期为10分钟 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) .entryTtl(Duration.ofMinutes(10)); // 注入cacheManager return RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(redisConnectionFactory) .fromCacheWriter(redisCacheWriter) .cacheDefaults(redisCacheConfiguration) .build(); } }
参考: