現(xiàn)開發(fā)的項(xiàng)目中决采,某接口需要頻獲取同一個(gè)實(shí)體,如果每次都查數(shù)據(jù)庫(kù)的話纵柿,性能消耗太大,于是決定使用redis做緩存启绰。
Redis默認(rèn)支持一般類型的數(shù)據(jù)昂儒,但需要對(duì)POJO進(jìn)行緩存的話,需要特殊處理委可,也就是需要對(duì)其進(jìn)行配置渊跋,使用jackson的ObjectMapper
對(duì)實(shí)體進(jìn)行特殊處理。以下是編寫的第一版配置的代碼
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() // 設(shè)置key為String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(template.stringSerializer)) // 設(shè)置value 為自動(dòng)轉(zhuǎn)Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(template.valueSerializer)) // 不緩存null
.disableCachingNullValues() // 緩存數(shù)據(jù)保存2小時(shí)
.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序列化對(duì)象
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
}
}
簡(jiǎn)單的團(tuán)隊(duì)POJO着倾,用于從數(shù)據(jù)庫(kù)中查出來拾酝,并放在緩存
data class Team(
var id: String? = null,
val name: String? = null,
var enabled: Boolean? = true,
var rf: Boolean? = false
)
團(tuán)隊(duì)Service,緩存分區(qū)配置屈呕,獲取團(tuán)隊(duì)與更新團(tuán)隊(duì)
@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
數(shù)據(jù)后微宝,會(huì)通過注解@Cacheable
把數(shù)據(jù)以Key-Value形式放入緩存,但通過日志發(fā)現(xiàn)Spring自動(dòng)生成的key貌似和預(yù)期不符虎眨,我們理想中的key格式應(yīng)該是teams::04422030192c*******8af8c684740bb
蟋软。所以使用@CacheEvict
注解怎么也無法刪除之前的緩存镶摘。
$3
SET
$153
teams::[04422030192c*******8af8c684740bb,Continuation at com.***.controllers.TeamController$findById$1.invokeSuspend(TeamController.kt:27)]
原因
Spring默認(rèn)使用的是SimpleKeyGenerator
去生成數(shù)據(jù)的key,從以下源碼可以看出岳守,它會(huì)把所有參數(shù)params
丟到SimpleKey
中進(jìn)行組合凄敢,生成上日志中的形式。
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
湿痢,只能自己寫一個(gè)涝缝。
- 在
RedisConfig
加入自定義的idKeyGenerator
。
@Bean
fun idKeyGenerator(): KeyGenerator {
return KeyGenerator { _, _, params ->
val sb = StringBuilder().append(params[0])
sb.toString()
}
}
- 只需修改
@Cacheable
內(nèi)參數(shù)譬重,使用自定義的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
坑二
問題
需對(duì)LocalDateTime
類型支持
解決
修改配置,加入以下代碼
val javaTimeModule = JavaTimeModule()
javaTimeModule.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))
objectMapper.registerModule(javaTimeModule)
坑三
問題
第一次獲取數(shù)據(jù)都是直接從數(shù)據(jù)庫(kù)查的臀规,所以都沒有問題滩援,但第二次查詢則是從緩存里面獲取。于是拋以下異常塔嬉。
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.XXX
找了好久的原因還有debug玩徊,發(fā)現(xiàn)是在之前的配置Jackson2JsonRedisSerializer
里的參數(shù)是Any::class.java
,因此把數(shù)據(jù)轉(zhuǎn)成LinkedHashMap
谨究,因此無論如何也無法轉(zhuǎn)成我們需要的實(shí)體恩袱。在網(wǎng)上也搜了好久,說給ObjectMapper
加一行配置objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)
即可胶哲,但enableDefaultTyping
該方法已經(jīng)過時(shí)畔塔,不再建議使用,正確的方法是下行代碼纪吮。
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
網(wǎng)上有介紹該行代碼的介紹俩檬,附上網(wǎng)址。但是加上后依然報(bào)錯(cuò)碾盟。
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
原因
加上那行代碼棚辽,只會(huì)對(duì)除了一些自然類型(String、Double冰肴、Integer屈藐、Double)類型外的非常量(non-final)類型加上值類型,但我們的實(shí)體類他并沒有加上類型熙尉,因此他仍然以Json形式繼續(xù)轉(zhuǎn)換联逻,但由于存到緩存里的數(shù)據(jù)已不是正常的Json了,因此導(dǎo)致轉(zhuǎn)換錯(cuò)誤检痰。
解決
只需把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
}