相信在項(xiàng)目中凯楔,你一定是經(jīng)常使用 Redis ,那么锦募,你是怎么使用的呢摆屯?在使用時(shí),有沒(méi)有遇到同我一樣糠亩,對(duì)象緩存序列化問(wèn)題的呢虐骑?那么,你又是如何解決的呢赎线?
Redis 使用示例
添加依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在應(yīng)用啟動(dòng)如何添加啟用緩存注解(@EnableCaching
)廷没。
假如我們有一個(gè)用戶對(duì)象(UserVo
):
@Data
public class UserVo implements Serializable {
@Serial
private static final long serialVersionUID = 2215423070276994378L;
private Long id;
private String name;
private LocalDateTime createDateTime;
}
這里,我們實(shí)現(xiàn)了 Serializable
接口垂寥。
在我們需要緩存的方法上颠黎,使用 @Cacheable
注解,就表示如果返回的對(duì)象不是 null 時(shí)滞项,就會(huì)對(duì)其進(jìn)行緩存狭归,下次查詢,首先會(huì)去緩存中查詢文判,查到了过椎,就直接返回,不會(huì)再去數(shù)據(jù)庫(kù)查詢戏仓,查不到疚宇,再去數(shù)據(jù)庫(kù)查詢。
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
@Override
@Cacheable(
value = "sample-redis",
key = "'user-'+#id",
unless = "#result == null"
)
public UserVo getUserById(Long id) {
log.info("userVo from db query");
UserVo userVo = new UserVo();
userVo.setId(1L);
userVo.setName("Zhang San");
userVo.setCreateDateTime(LocalDateTime.now());
return userVo;
}
}
核心代碼:
@Cacheable(
value = "sample-redis",
key = "'user-'+#id",
unless = "#result == null"
)
模擬測(cè)試赏殃,再寫(xiě)一個(gè)測(cè)試接口:
@RestController
@RequestMapping("/sample")
@RequiredArgsConstructor
@Slf4j
public class SampleController {
private final IUserService userService;
@GetMapping("/user/{id}")
public UserVo getUserById(@PathVariable Long id) {
UserVo vo = userService.getUserById(id);
log.info("vo: {}", JacksonUtils.json(vo));
return vo;
}
}
我們?cè)偌由线B接 redis 的配置:
spring:
data:
redis:
host: localhost
port: 6379
測(cè)試:
### getUserById
GET http://localhost:8080/sample/user/1
輸出結(jié)果跟我們想的一樣敷待,第一次從數(shù)據(jù)庫(kù)查,后面都從緩存直接返回嗓奢。
總結(jié)一下:
添加
spring-boot-starter-data-redis
依賴。使用啟用緩存注解(
@EnableCaching
)浑厚。需要緩存的對(duì)象實(shí)現(xiàn)
Serializable
接口股耽。使用
@Cacheable
注解緩存查詢的結(jié)果。
遇到問(wèn)題
在上面我們通過(guò) spring boot 提供的 redis 實(shí)現(xiàn)了查詢對(duì)象緩存這樣一個(gè)功能钳幅,有下面幾個(gè)問(wèn)題:
- 緩存的對(duì)象物蝙,必須序列化,不然會(huì)報(bào)錯(cuò)敢艰。
- redis 存儲(chǔ)的數(shù)據(jù)诬乞,看不懂,可以轉(zhuǎn)成 json 格式嗎?
- 使用 Jackson 時(shí)震嫉,遇到特殊類型的字段會(huì)報(bào)錯(cuò)森瘪,比如 LocalDateTime。
第1個(gè)問(wèn)題票堵,如果對(duì)象沒(méi)有實(shí)現(xiàn) Serializable
接口扼睬,會(huì)報(bào)錯(cuò):
關(guān)鍵信息:
java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [xxx.xxx.UserVo]
我詳細(xì)描述一下第3個(gè)問(wèn)題,默認(rèn)是使用 Jdk序列化 JdkSerializationRedisSerializer
悴势,redis 里面存的數(shù)據(jù)如下:
問(wèn)題很明顯窗宇,對(duì)象必須要實(shí)現(xiàn)序列化接口,存的數(shù)據(jù)不易查看特纤,所以军俊,改用 GenericJackson2JsonRedisSerializer
,這就有了第3個(gè)問(wèn)題捧存。
我們加上下面的配置粪躬,就能解決第2個(gè)問(wèn)題。
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration
.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(RedisSerializer.json())
);
}
下面看第三個(gè)問(wèn)題的錯(cuò)誤:
如何解決矗蕊?
既然有了明確的錯(cuò)誤提示短蜕,那也是好解決的,我們可以這樣:
@JsonDeserialize(using = LocalDateTimeDeserializer.class) // 反序列化
@JsonSerialize(using = LocalDateTimeSerializer.class) // 序列化
private LocalDateTime createDateTime;
這樣就可以了傻咖,我們看下redis里面存的數(shù)據(jù):
{"@class":"com.fengwenyi.erwin.component.sample.redis.vo.UserVo","id":1,"name":"Zhang San","createDateTime":[2023,12,29,23,44,3,479011000]}
其實(shí)到這里朋魔,已經(jīng)解決了問(wèn)題,那有沒(méi)有更省心的辦法呢卿操?
解決辦法
其實(shí)我們知道警检,使用的就是 Jackson 進(jìn)行 json 轉(zhuǎn)換,而 json 轉(zhuǎn)換害淤,遇到 LocalDateTime 問(wèn)題時(shí)扇雕,我們配置一下 module 就可以了,因?yàn)槟J(rèn)用的 SimpleModule
窥摄,我們改用 JavaTimeModule
就可以了镶奉。
這時(shí)候問(wèn)題又來(lái)啦,錯(cuò)誤如下:
這時(shí)候存的數(shù)據(jù)如下:
{"id":1,"name":"Zhang San","createDateTime":"2023-12-29T23:31:52.548517"}
這就涉及到 Jackson 序列化漏洞的問(wèn)題了崭放,采用了白名單機(jī)制哨苛,我們就粗暴一點(diǎn):
jsonMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
redis 存的數(shù)據(jù)如下:
["com.fengwenyi.erwin.component.sample.redis.vo.UserVo",{"id":1,"name":"Zhang San","createDateTime":"2023-12-29T23:56:18.197203"}]
最后,來(lái)一段完整的 RedisCacheConfiguration
配置代碼:
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration
.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext
.SerializationPair
// .fromSerializer(RedisSerializer.json())
// .fromSerializer(
// new GenericJackson2JsonRedisSerializer()
// )
.fromSerializer(redisSerializer())
);
}
private RedisSerializer<Object> redisSerializer() {
JsonMapper jsonMapper = new JsonMapper();
JacksonUtils.configure(jsonMapper);
jsonMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
return new GenericJackson2JsonRedisSerializer(jsonMapper);
}
希望今天的分享對(duì)你有一定的幫助币砂。