现开发的项目中,某接口需要频获取同一个实体,如果每次都查数据库的话,性能消耗太大,于是决定使用redis做缓存。
Redis默认支持一般类型的数据,但需要对POJO进行缓存的话,需要特殊处理,也就是需要对其进行配置,使用jackson的ObjectMapper
对实体进行特殊处理。以下是编写的第一版配置的代码
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.data.redis.RedisProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cache.annotation.CachingConfigurerSupport
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer
/**
* @author Anson
* @date 2020/11/7
*/
@EnableCaching
@Configuration
@EnableConfigurationProperties(
RedisProperties::class
)
class RedisConfig @Autowired constructor(
private val properties: RedisProperties
): CachingConfigurerSupport() {
@Bean
fun redisConnectionFactory(): LettuceConnectionFactory {
val config = RedisStandaloneConfiguration()
config.hostName = properties.host
config.port = properties.port
return LettuceConnectionFactory(config)
}
@Bean
fun cacheManager(template: RedisTemplate<String, Any>): CacheManager {
val defaultCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig() // 设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(template.stringSerializer)) // 设置value 为自动转Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(template.valueSerializer)) // 不缓存null
.disableCachingNullValues() // 缓存数据保存2小时
.entryTtl(Duration.ofHours(2))
return RedisCacheManager.RedisCacheManagerBuilder // Redis 连接工厂
.fromConnectionFactory(template.connectionFactory!!) // 缓存配置
.cacheDefaults(defaultCacheConfiguration) // 配置同步修改或删除 put/evict
.transactionAware()
.build()
}
private fun jackson2JsonRedisSerializer(): Jackson2JsonRedisSerializer<Any> {
val jackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(Any::class.java)
val objectMapper = ObjectMapper()
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
jackson2JsonRedisSerializer.setObjectMapper(objectMapper)
return jackson2JsonRedisSerializer
}
@Bean
fun redisTemplate(factory: RedisConnectionFactory): RedisTemplate<String, Any> {
val template = RedisTemplate<String, Any>()
// 配置连接工厂
template.setConnectionFactory(factory)
// 定义Jackson2JsonRedisSerializer序列化对象
val jacksonSeial = jackson2JsonRedisSerializer()
val stringSerial = StringRedisSerializer()
// redis key 序列化方式使用stringSerial
template.keySerializer = stringSerial
// redis value 序列化方式使用jackson
template.valueSerializer = jacksonSeial
// redis hash key 序列化方式使用stringSerial
template.hashKeySerializer = stringSerial
// // redis hash value 序列化方式使用jackson
template.hashValueSerializer = jacksonSeial
template.afterPropertiesSet()
return template
}
}
简单的团队POJO,用于从数据库中查出来,并放在缓存
data class Team(
var id: String? = null,
val name: String? = null,
var enabled: Boolean? = true,
var rf: Boolean? = false
)
团队Service,缓存分区配置,获取团队与更新团队
@Service
@Transactional
@CacheConfig(cacheNames = ["teams"])
class TeamServiceImpl(private val mapper: TeamMapper) {
@Cacheable(key = "#id", unless = "#result==null")
suspend fun getById(id: String): Team? {
return mapper.getById(id)
}
@CacheEvict(key = "#id")
suspend fun update(id: String, t: Team): Team {
return mapper.update(t).let { t }
}
}
坑一
问题
通过getById
获取team
数据后,会通过注解@Cacheable
把数据以Key-Value形式放入缓存,但通过日志发现Spring自动生成的key貌似和预期不符,我们理想中的key格式应该是teams::04422030192c*******8af8c684740bb
。所以使用@CacheEvict
注解怎么也无法删除之前的缓存。
$3
SET
$153
teams::[04422030192c*******8af8c684740bb,Continuation at com.***.controllers.TeamController$findById$1.invokeSuspend(TeamController.kt:27)]
原因
Spring默认使用的是SimpleKeyGenerator
去生成数据的key,从以下源码可以看出,它会把所有参数params
丢到SimpleKey
中进行组合,生成上日志中的形式。
public class SimpleKeyGenerator implements KeyGenerator {
public SimpleKeyGenerator() {
}
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
} else {
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
}
}
解决
因此不能使用Spring自带的SimpleKeyGenerator
,只能自己写一个。
- 在
RedisConfig
加入自定义的idKeyGenerator
。
@Bean
fun idKeyGenerator(): KeyGenerator {
return KeyGenerator { _, _, params ->
val sb = StringBuilder().append(params[0])
sb.toString()
}
}
- 只需修改
@Cacheable
内参数,使用自定义的idKeyGenerator
@Cacheable(keyGenerator = "idKeyGenerator", unless = "#result==null")
suspend fun getById(id: String): Team? {
return mapper.getById(id)
}
成功插入、查询与删除缓存
$3
SET
$42
teams::04422030192c*******8af8c684740bb
// 省略
$3
GET
$42
teams::04422030192c*******8af8c684740bb
// 省略
$3
DEL
$42
teams::04422030192c*******8af8c684740bb
坑二
问题
需对LocalDateTime
类型支持
解决
修改配置,加入以下代码
val javaTimeModule = JavaTimeModule()
javaTimeModule.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))
objectMapper.registerModule(javaTimeModule)
坑三
问题
第一次获取数据都是直接从数据库查的,所以都没有问题,但第二次查询则是从缓存里面获取。于是抛以下异常。
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.XXX
找了好久的原因还有debug,发现是在之前的配置Jackson2JsonRedisSerializer
里的参数是Any::class.java
,因此把数据转成LinkedHashMap
,因此无论如何也无法转成我们需要的实体。在网上也搜了好久,说给ObjectMapper
加一行配置objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)
即可,但enableDefaultTyping
该方法已经过时,不再建议使用,正确的方法是下行代码。
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
网上有介绍该行代码的介绍,附上网址。但是加上后依然报错。
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.lang.Object
at [Source: (byte[])"{"id":"0510c38023********c7c7a8747f6e40","[truncated 32 bytes]; line: 1, column: 1]; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.lang.Object
原因
加上那行代码,只会对除了一些自然类型(String、Double、Integer、Double)类型外的非常量(non-final)类型加上值类型,但我们的实体类他并没有加上类型,因此他仍然以Json形式继续转换,但由于存到缓存里的数据已不是正常的Json了,因此导致转换错误。
解决
只需把ObjectMapper.DefaultTyping.NON_FINAL)
改成ObjectMapper.DefaultTyping.EVERYTHING)
就解决了我们的问题。
最后的jackson代码块
private fun jackson2JsonRedisSerializer(): Jackson2JsonRedisSerializer<Any> {
val jackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(Any::class.java)
val objectMapper = ObjectMapper()
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.EVERYTHING)
val javaTimeModule = JavaTimeModule()
javaTimeModule.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))
objectMapper.registerModule(javaTimeModule)
jackson2JsonRedisSerializer.setObjectMapper(objectMapper)
return jackson2JsonRedisSerializer
}