詳解SpringBoot的三種緩存技術(shù)(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
引言
前兩天在寫一個實(shí)時數(shù)據(jù)處理的項(xiàng)目,項(xiàng)目要求是 1s 要處理掉 1k 的數(shù)據(jù),這時候顯然光靠查數(shù)據(jù)庫是不行的,技術(shù)選型的時候老大跟我提了一下使用 Layering-Cache 這個開源項(xiàng)目來做緩存框架。
之間問了一下身邊的小伙伴,似乎對這塊了解不多。一般也就用用 Redis 來緩存,應(yīng)該是很少用多級緩存框架來專門性的管理緩存吧。
趁著這個機(jī)會,我多了解了一些關(guān)于 SpringBoot 中緩存的相關(guān)技術(shù),于是有了這篇文章!
在項(xiàng)目性能需求比較高時,就不能單單依賴數(shù)據(jù)庫訪問來獲取數(shù)據(jù)了,必須引入緩存技術(shù)。
常用的有本地緩存、Redis 緩存。
- 本地緩存:也就是內(nèi)存,速度快,缺點(diǎn)是不能持久化,一旦項(xiàng)目關(guān)閉,數(shù)據(jù)就會丟失。而且不能滿足分布式系統(tǒng)的應(yīng)用場景(比如數(shù)據(jù)不一致的問題)。
- Redis 緩存:也就是利用數(shù)據(jù)庫等,最常見的就是 Redis。Redis 的訪問速度同樣很快,可以設(shè)置過期時間、設(shè)置持久化方法。缺點(diǎn)是會受到網(wǎng)絡(luò)和并發(fā)訪問的影響。
本節(jié)介紹三種緩存技術(shù):Spring Cache、Layering Cache 框架、Alibaba JetCache 框架。示例使用的 SpringBoot 版本是 2.1.3.RELEASE。非 SpringBoot 項(xiàng)目請參考文章中給出的文檔地址。
項(xiàng)目源碼地址:https://github.com/laolunsi/spring-boot-examples
一、Spring Cache
Spring Cache 是 Spring 自帶的緩存方案,使用簡單,既可以使用本地緩存,也可以使用 Redis
CacheType 包括:
GENERIC, JCACHE, EHCACHE, HAZELCAST, INFINISPAN, COUCHBASE, REDIS, CAFFEINE, SIMPLE, NONE
Spring Cache 的使用很簡單,引入 即可,我這里使用創(chuàng)建的是一個 web 項(xiàng)目,引入的 `spring-boot-starter-web` 包含了 。
這里利用 Redis 做緩存,再引入 spring-boot-starter-data-redis 依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--Redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
在配置類 or Application 類上添加 @EnableCaching 注解以啟動緩存功能。
配置文件很簡潔(功能也比較少):
server: port: 8081 servlet: context-path: /api spring: cache: type: redis redis: host: 127.0.0.1 port: 6379 database: 1
下面我們編寫一個對 User 進(jìn)行增刪改查的 Controller,實(shí)現(xiàn)對 User 的 save/delete/findAll 三個操作。為演示方便,DAO 層不接入數(shù)據(jù)庫,而是使用 HashMap 來直接模擬數(shù)據(jù)庫操作。
我們直接看 service 層的接口實(shí)現(xiàn):
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
@Cacheable(value = "user", key = "#userId")
public User findById(Integer userId) {
return userDAO.findById(userId);
}
@Override
@CachePut(value = "user", key = "#user.id", condition = "#user.id != null")
public User save(User user) {
user.setUpdateTime(new Date());
userDAO.save(user);
return userDAO.findById(user.getId());
}
@Override
@CacheEvict(value = "user", key = "#userId")
public boolean deleteById(Integer userId) {
return userDAO.deleteById(userId);
}
@Override
public List<User> findAll() {
return userDAO.findAll();
}
}
我們可以看到使用了 @Cacheable、@CachePut、@CacheEvict 注解。
- Cacheable:啟用緩存,首先從緩存中查找數(shù)據(jù),如果存在,則從緩存讀取數(shù)據(jù);如果不存在,則執(zhí)行方法,并將方法返回值添加到緩存
- @CachePut:更新緩存,如果 condition 計(jì)算結(jié)果為 true,則將方法返回值添加到緩存中
- @CacheEvict:刪除緩存,根據(jù) value 與 key 字段計(jì)算緩存地址,將緩存數(shù)據(jù)刪除
測試發(fā)現(xiàn)默認(rèn)的對象存到 Redis 后是 binary 類型,我們可以通過修改 RedisCacheConfiguration 中的序列化規(guī)則去調(diào)整。比如:
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(){
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofDays(30));
return configuration;
}
}
Spring Cache 的功能比較單一,例如不能實(shí)現(xiàn)緩存刷新、二級緩存等功能。下面介紹一個開源項(xiàng)目:Layering-Cache,該項(xiàng)目實(shí)現(xiàn)了緩存刷新、二級緩存(一級內(nèi)存、二級 Redis)。同時較容易擴(kuò)展實(shí)現(xiàn)為自己的緩存框架。
二、Layering Cache 框架
文檔:https://github.com/xiaolyuh/layering-cache/wiki/文檔
引入依賴:
<dependency> <groupId>com.github.xiaolyuh</groupId> <artifactId>layering-cache-starter</artifactId> <version>2.0.7</version> </dependency>
配置文件不需要做什么修改。啟動類依然加上 @EnableCaching 注解。
然后需要配置一下 RedisTemplate:
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
return createRedisTemplate(redisConnectionFactory);
}
public RedisTemplate createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替換默認(rèn)序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 設(shè)置value的序列化規(guī)則和 key的序列化規(guī)則
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//Map
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
下面我們使用 layering 包中的 @Cacheable @CachePut @CatchEvict 三個注解來替換 Spring Cache 的默認(rèn)注解。
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
//@Cacheable(value = "user", key = "#userId")
@Cacheable(value = "user", key = "#userId",
firstCache = @FirstCache(expireTime = 5, timeUnit = TimeUnit.MINUTES),
secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, isAllowNullValue = true, timeUnit = TimeUnit.MINUTES))
public User findById(Integer userId) {
return userDAO.findById(userId);
}
@Override
//@CachePut(value = "user", key = "#user.id", condition = "#user.id != null")
@CachePut(value = "user", key = "#user.id",
firstCache = @FirstCache(expireTime = 5, timeUnit = TimeUnit.MINUTES),
secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, isAllowNullValue = true, timeUnit = TimeUnit.MINUTES))
public User save(User user) {
user.setUpdateTime(new Date());
userDAO.save(user);
return userDAO.findById(user.getId());
}
@Override
//@CacheEvict(value = "user", key = "#userId")
@CacheEvict(value = "user", key = "#userId")
public boolean deleteById(Integer userId) {
return userDAO.deleteById(userId);
}
@Override
public List<User> findAll() {
return userDAO.findAll();
}
}
三、Alibaba JetCache 框架
文檔:https://github.com/alibaba/jetcache/wiki/Home_CN
JetCache是一個基于Java的緩存系統(tǒng)封裝,提供統(tǒng)一的API和注解來簡化緩存的使用。 JetCache提供了比SpringCache更加強(qiáng)大的注解,可以原生的支持TTL、兩級緩存、分布式自動刷新,還提供了Cache接口用于手工緩存操作。 當(dāng)前有四個實(shí)現(xiàn),RedisCache、TairCache(此部分未在github開源)、CaffeineCache(in memory)和一個簡易的LinkedHashMapCache(in memory),要添加新的實(shí)現(xiàn)也是非常簡單的。
全部特性:
- 通過統(tǒng)一的API訪問Cache系統(tǒng)
- 通過注解實(shí)現(xiàn)聲明式的方法緩存,支持TTL和兩級緩存
- 通過注解創(chuàng)建并配置Cache實(shí)例
- 針對所有Cache實(shí)例和方法緩存的自動統(tǒng)計(jì)
- Key的生成策略和Value的序列化策略是可以配置的
- 分布式緩存自動刷新,分布式鎖 (2.2+)
- 異步Cache API (2.2+,使用Redis的lettuce客戶端時)
- Spring Boot支持
SpringBoot 項(xiàng)目中,引入如下依賴:
<dependency> <groupId>com.alicp.jetcache</groupId> <artifactId>jetcache-starter-redis</artifactId> <version>2.5.14</version> </dependency>
配置:
server:
port: 8083
servlet:
context-path: /api
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
local:
default:
type: caffeine
keyConvertor: fastjson
remote:
default:
expireAfterWriteInMillis: 86400000 # 全局,默認(rèn)超時時間,單位毫秒,這里設(shè)置了 24 小時
type: redis
keyConvertor: fastjson
valueEncoder: java #jsonValueEncoder #java
valueDecoder: java #jsonValueDecoder
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
database: 1
redis:
host: 127.0.0.1
port: 6379
Application.class
@EnableMethodCache(basePackages = "com.example.springcachealibaba")
@EnableCreateCacheAnnotation
@SpringBootApplication
public class SpringCacheAlibabaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCacheAlibabaApplication.class, args);
}
}
字如其意,@EnableMethodCache 用于注解開啟方法上的緩存功能,@EnableCreateCacheAnnotation 用于注解開啟 @CreateCache 來引入 Cache Bean 的功能。兩套可以同時啟用。
這里以上面對 User 的增刪改查功能為例:
3.1 通過 @CreateCache 創(chuàng)建 Cache 實(shí)例
@Service
public class UserServiceImpl implements UserService {
// 下面的示例為使用 @CreateCache 注解創(chuàng)建 Cache 對象來緩存數(shù)據(jù)的示例
@CreateCache(name = "user:", expire = 5, timeUnit = TimeUnit.MINUTES)
private Cache<Integer, User> userCache;
@Autowired
private UserDAO userDAO;
@Override
public User findById(Integer userId) {
User user = userCache.get(userId);
if (user == null || user.getId() == null) {
user = userDAO.findById(userId);
}
return user;
}
@Override
public User save(User user) {
user.setUpdateTime(new Date());
userDAO.save(user);
user = userDAO.findById(user.getId());
// cache
userCache.put(user.getId(), user);
return user;
}
@Override
public boolean deleteById(Integer userId) {
userCache.remove(userId);
return userDAO.deleteById(userId);
}
@Override
public List<User> findAll() {
return userDAO.findAll();
}
}
3.2 通過注解實(shí)現(xiàn)方法緩存
@Service
public class UserServiceImpl implements UserService {
// 下面為使用 AOP 來緩存數(shù)據(jù)的示例
@Autowired
private UserDAO userDAO;
@Autowired
private UserService userService;
@Override
@Cached(name = "user:", key = "#userId", expire = 1000)
//@Cached( name = "user:", key = "#userId", serialPolicy = "bean:jsonPolicy")
public User findById(Integer userId) {
System.out.println("userId: " + userId);
return userDAO.findById(userId);
}
@Override
@CacheUpdate(name = "user:", key = "#user.id", value = "#user")
public User save(User user) {
user.setUpdateTime(new Date());
boolean res = userDAO.save(user);
if (res) {
return userService.findById(user.getId());
}
return null;
}
@Override
@CacheInvalidate(name = "user:", key = "#userId")
public boolean deleteById(Integer userId) {
return userDAO.deleteById(userId);
}
@Override
public List<User> findAll() {
return userDAO.findAll();
}
}
這里用到了三個注解:@Cached/@CacheUpdate/@CacheInvalidate,分別對應(yīng)著 Spring Cache 中的 @Cacheable/@CachePut/@CacheEvict
具體含義可以參考:https://github.com/alibaba/jetcache/wiki/MethodCache_CN
3.3 自定義序列化器
默認(rèn)的 value 存儲格式是 binary 的,JetCache 提供的 Redis key 和 value 的序列化器僅有 java 和 kryo 兩種??梢酝ㄟ^自定義序列化器來實(shí)現(xiàn)自己想要的序列化方式,比如 json。
JetCache 開發(fā)者提出:
jetcache老版本中是有三個序列化器的:java、kryo、fastjson。 但是fastjson做序列化兼容性不是特別好,并且某次升級以后單元測試就無法通過了,怕大家用了以后覺得有坑,就把它廢棄了。 現(xiàn)在默認(rèn)的序列化器是性能最差,但是兼容性最好,大家也最熟悉的java序列化器。
參考原倉庫中 FAQ 中的建議,可以通過兩種方式來定義自己的序列化器。
3.3.1 實(shí)現(xiàn) SerialPolicy 接口
第一種方式是定義一個 SerialPolicy 的實(shí)現(xiàn)類,然后將其注冊成一個 bean,然后在 @Cached 中的 serialPolicy 屬性中指明 bean:name
比如:
import com.alibaba.fastjson.JSONObject;
import com.alicp.jetcache.CacheValueHolder;
import com.alicp.jetcache.anno.SerialPolicy;
import java.util.function.Function;
public class JsonSerialPolicy implements SerialPolicy {
@Override
public Function<Object, byte[]> encoder() {
return o -> {
if (o != null) {
CacheValueHolder cacheValueHolder = (CacheValueHolder) o;
Object realObj = cacheValueHolder.getValue();
String objClassName = realObj.getClass().getName();
// 為防止出現(xiàn) Value 無法強(qiáng)轉(zhuǎn)成指定類型對象的異常,這里生成一個 JsonCacheObject 對象,保存目標(biāo)對象的類型(比如 User)
JsonCacheObject jsonCacheObject = new JsonCacheObject(objClassName, realObj);
cacheValueHolder.setValue(jsonCacheObject);
return JSONObject.toJSONString(cacheValueHolder).getBytes();
}
return new byte[0];
};
}
@Override
public Function<byte[], Object> decoder() {
return bytes -> {
if (bytes != null) {
String str = new String(bytes);
CacheValueHolder cacheValueHolder = JSONObject.parseObject(str, CacheValueHolder.class);
JSONObject jsonObject = JSONObject.parseObject(str);
// 首先要解析出 JsonCacheObject,然后獲取到其中的 realObj 及其類型
JSONObject jsonOfMy = jsonObject.getJSONObject("value");
if (jsonOfMy != null) {
JSONObject realObjOfJson = jsonOfMy.getJSONObject("realObj");
String className = jsonOfMy.getString("className");
try {
Object realObj = realObjOfJson.toJavaObject(Class.forName(className));
cacheValueHolder.setValue(realObj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return cacheValueHolder;
}
return null;
};
}
}
注意,在 JetCache 的源碼中,我們看到實(shí)際被緩存的對象的 CacheValueHolder,這個對象包括了一個泛型字段 V,這個 V 就是實(shí)際被緩存的數(shù)據(jù)。為了將 JSON 字符串和 CacheValueHolder(包括了泛型字段 V )進(jìn)行互相轉(zhuǎn)換,我在轉(zhuǎn)換過程中使用 CacheValueHolder 和一個自定義的 JsonCacheObject 類,其代碼如下:
public class JsonCacheObject<V> {
private String className;
private V realObj;
public JsonCacheObject() {
}
public JsonCacheObject(String className, V realObj) {
this.className = className;
this.realObj = realObj;
}
// ignore get and set methods
}
然后定義一個配置類:
@Configuration
public class JetCacheConfig {
@Bean(name = "jsonPolicy")
public JsonSerializerPolicy jsonSerializerPolicy() {
return new JsonSerializerPolicy();
}
}
使用很簡單,比如:
@Cached( name = "user:", key = "#userId", serialPolicy = "bean:jsonPolicy")
這種序列化方法是局部的,只能對單個緩存生效。
下面介紹如何全局序列化方法。
3.3.2 全局配置 SpringConfigProvider
JetCache 默認(rèn)提供了兩種序列化規(guī)則:KRYO 和 JAVA (不區(qū)分大小寫)。
這里在上面的 JSONSerialPolicy 的基礎(chǔ)上,定義一個新的 SpringConfigProvider:
@Configuration
public class JetCacheConfig {
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider() {
@Override
public Function<byte[], Object> parseValueDecoder(String valueDecoder) {
if (valueDecoder.equalsIgnoreCase("myJson")) {
return new JsonSerialPolicy().decoder();
}
return super.parseValueDecoder(valueDecoder);
}
@Override
public Function<Object, byte[]> parseValueEncoder(String valueEncoder) {
if (valueEncoder.equalsIgnoreCase("myJson")) {
return new JsonSerialPolicy().encoder();
}
return super.parseValueEncoder(valueEncoder);
}
};
}
}
這里使用了類型 myJson 作為新序列化類型的名稱,這樣我們就可以在配置文件的 jetcache.xxx.valueEncoder 和 jetcache.xxx.valueDecoder 這兩個配置項(xiàng)上設(shè)置值 myJson/java/kryo 三者之一了。
關(guān)于 Java 中緩存框架的知識就介紹到這里了,還有一些更加深入的知識,比如:如何保證分布式環(huán)境中緩存數(shù)據(jù)的一致性、緩存數(shù)據(jù)的刷新、多級緩存時定制化緩存策略等等。這些都留待以后再學(xué)習(xí)和介紹吧!
參考資料:
Spring Cache: https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
Caffeine 緩存: https://www.jianshu.com/p/9a80c662dac4
Layering-Cache:https://github.com/xiaolyuh/layering-cache
Alibaba JetCache: https://github.com/alibaba/jetcache
JetCache FAQ: https://github.com/alibaba/jetcache/wiki/FAQ_CN
以上就是詳解SpringBoot的三種緩存技術(shù)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot 緩存技術(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot自動掃描添加的BeanDefinition源碼實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于springboot自動掃描添加的BeanDefinition的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-02-02
IntelliJ IDEA 關(guān)閉多余項(xiàng)目的操作方法
這篇文章主要介紹了IntelliJ IDEA 關(guān)閉多余項(xiàng)目的操作方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
Spring Boot 如何使用Liquibase 進(jìn)行數(shù)據(jù)庫遷移(操作方法)
在Spring Boot應(yīng)用程序中使用Liquibase進(jìn)行數(shù)據(jù)庫遷移是一種強(qiáng)大的方式來管理數(shù)據(jù)庫模式的變化,本文重點(diǎn)講解如何在Spring Boot應(yīng)用程序中使用Liquibase進(jìn)行數(shù)據(jù)庫遷移,從而更好地管理數(shù)據(jù)庫模式的變化,感興趣的朋友跟隨小編一起看看吧2023-09-09
解決SpringCloud gateway網(wǎng)關(guān)配置MVC攔截器報(bào)錯問題
這篇文章主要介紹了解決SpringCloud gateway網(wǎng)關(guān)配置MVC攔截器報(bào)錯問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-06-06
解決tomcat啟動時報(bào)Junit相關(guān)錯誤java.lang.ClassNotFoundException:
這篇文章主要介紹了解決tomcat啟動時報(bào)Junit相關(guān)錯誤java.lang.ClassNotFoundException: org.junit.Test問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-05-05

