SpringBoot 緩存源碼分析
JSR107
在我們了解SpringBoot緩存深入的時候,我們首先需要了解JSR107。
JSR:
- 是Java Specification Requset 的縮寫,Java規(guī)范請求;
- 其是Java提供的一個接口規(guī)范,類似于jdbc規(guī)范,但是沒有具體的實現(xiàn),具體的實現(xiàn)就是redis等這些緩存。
JSR107核心接口:
- CachingProvider(緩存提供者):創(chuàng)建,配置,獲取,管理和控制多個CacheManager;
- CacheManager(緩存管理器):創(chuàng)建,配置,獲取,管理和控制多個唯一命名的Cache,Cache存在于CacheManager的上下文中,一個CacheManager僅對應一個CachingProvider;
- Cache(緩存):是由CacheManager管理的,CacheManager僅對應一個Cache的生命周期,Cache存在于CacheManager的上下文中。類似于map的數(shù)據(jù)結(jié)構,并臨時存儲以key為索引的值。一個Cache僅被一個CacheManager所擁有;
- Entry(緩存鍵值對):是一個存儲在Cache中的key-value對;
- Expiry(緩存時效):每一個存儲在Cache中的條目都有一個定義的有效期。一旦超過這個時間,條目就會自動過期,過期后,條目將不可用訪問,更新和刪除操作。緩存有效期可以通過ExpiryPolicy設置。

要使用JSR107需要導入相關的maven依賴
<dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> </dependency>
Spring的緩存抽象
Spring Cache:
- 只負責維護抽象層,具體的實戰(zhàn)由自己的技術選型來決定;
- 將緩存處理和緩存技術解除耦合;
- 每次調(diào)用緩存功能方法時,Spring會檢查指定參數(shù)的目標方法是否已經(jīng)被調(diào)用;
- 如果有就直接從緩存中獲取方法調(diào)用后的結(jié)果,如果沒有就調(diào)用方法并緩存結(jié)果返回給用戶,下次直接從緩存中獲取即可。
當我們使用Spring Cache緩存抽象的時候,我們需要關注兩點:
- 確定那些方法需要被緩存;
- 緩存策略。
Spring 緩存使用
重要概念和緩存注解
在正式開始進入SpringCache實戰(zhàn)之間,我們需要先了解一下Spring Cache 的緩存注解和幾個重要概念。
概率/注解 | 作用 |
Cache | 緩存接口,定義緩存操作。實現(xiàn)有RedisCache等 |
CacheManaer | 緩存管理器,管理各種緩存(Cache)組件 |
@Cacheable | 主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結(jié)果進行緩存 |
@CacheEvict | 清空緩存 |
@CachePut | 保證方法被調(diào)用,又希望結(jié)果被緩存 |
@EnableCaching | 開啟緩存注解 |
keyGenerator | 緩存數(shù)據(jù)時key生成 |
serialize | 緩存數(shù)據(jù)時value序列化策略 |
說明:
@Cacheable標注在方法上,表示該方法的結(jié)果需要被緩存起來;- 緩存的鍵由
keyGenerator的策略決定,緩存的值的形式是由serialize決定(序列化還是json格式); - 標注上該注解之后,在緩存時效內(nèi)再次調(diào)用該方法將不會調(diào)用方法本身而是直接從緩存中獲取結(jié)果;
@CachePut也是標注在方法上,和@Cacheable相似也會將方法的返回值存儲起來,不同的是標注@CachePut的方法每次都會被調(diào)用,而且每次都會將結(jié)果緩存起來,適用對象的更新。
環(huán)境搭建
首先我們要創(chuàng)建數(shù)據(jù)庫表結(jié)構
SET FOREIGN_KEY_CHECKS=0; DROP TABLE IF EXISTS `department`; CREATE TABLE `department` ( `id` int(11) NOT NULL AUTO_INCREMENT, `department_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `employee`; CREATE TABLE `employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `gender` int(11) DEFAULT NULL, `d_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后添加maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.9.RELEASE</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<groupId>com.guslegend</groupId>
<artifactId>SpringCacheDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>接下來生成實體類,controller層,service層,mapper層。
還有我們需要開啟mybatis的駝峰映射,和配置日志級別,方便我們查看sql語句,看緩存是否生效。

緩存初體驗
首先我們需要再啟動類上添加@EnableCaching,允許使用緩存。
Cacheable

屬性名 | 描述 |
cacheNames/value | 指定緩存的名字,用于區(qū)分不同緩存組件;可通過該屬性指定緩存鍵值,將一個緩存鍵值分到多個緩存中。 |
key | 緩存數(shù)據(jù)時的 key 值,默認使用方法參數(shù)值,支持 SpEL 表達式生成 key。 |
keyGenerator | 緩存的生成策略,與 key 功能一致,支持自定義生成規(guī)則。 |
cacheManager | 定義緩存管理器(如 ConcurrentHashMap、Redis 等)。 |
cacheResolver | 與 cacheManager 功能一致,二者選其一。 |
condition | 指定緩存的條件,滿足條件時才緩存(如 表示入?yún)⒋笥?0 時緩存),支持 SpEL 表達式。 |
unless | 緩存后判斷條件,滿足時不緩存(如 表示結(jié)果為 null 時不緩存),支持 SpEL 表達式。 |
sync | 是否使用異步模式進行緩存。 |
注意:
- 即滿足condition又滿足unless條件的也不進行緩存;
- 使用異步模式進行緩存時(sync=true):unless條件將不被支持。
名字 | 位置 | 描述 | 示例 |
methodName | root object | 當前被調(diào)用的方法名 | #root.methodName |
method | root object | 當前被調(diào)用的方法 | #root.method.name |
target | root object | 當前被調(diào)用的目標對象 | #root.target |
targetClass | root object | 當前被調(diào)用的目標對象類 | root.targetClass |
args | root object | 當前被調(diào)用的方法的參數(shù)列表 | #root.args[0] |
caches | root object | 當前方法調(diào)用使用的緩存列表(如 cacheNames={"cache1","cache2"} 則有兩個 cache) | #root.caches[0].name |
argument name | evaluation context | 方法參數(shù)的名字,可直接用 #參數(shù)名或 #p0/#a0(0 代表參數(shù)索引) | #iban, #a0, #p0 |
result | evaluation context | 方法執(zhí)行后的返回值(僅當方法執(zhí)行之后判斷有效,如 cacheable 的 unless、cachePut 的表達式、cacheEvict 的beforeInvocation=false) | #result |
實戰(zhàn),進行兩次查詢方法,只出現(xiàn)一次sql語句
@GetMapping("/{id}")
@Cacheable(cacheNames = "emp",key = "#id",condition = "#id>0",unless = "#result == null ")
public Employee getEmpById(@PathVariable("id") Integer id) {
return employeeService.getEmpById(id);
}@Cacheable源碼分析
- 在運行方法之間會先去查詢Cache(緩存組件),按照cacheNames指定的名字獲取(CacheManager)先獲取相應的緩存,第一次獲取緩存如果沒有Cache組件會自動創(chuàng)建;
- 去Cache里面查找緩存的內(nèi)容,使用的key默認就是方法的參數(shù);key默認是使用keyGenerator生成的,默認使用SimpleKeyGenerator;
- 沒有查詢到緩存就調(diào)用目標方法;
- 將目標方法返回的結(jié)果放到緩存里面。
@CachePut @CacheEnvict @CacheConfig
@CachePut
調(diào)用方法,有更新緩存數(shù)據(jù),一般用于更新操作,在更新緩存時一定要和想要更新的緩存有相同的緩存名稱和相同的key(可類比同一張表的同一條數(shù)據(jù))。
@PutMapping("/update")
@CachePut(cacheNames = "emp",key = "#employee.id")
public void updateEmp(@RequestBody Employee employee) {
employeeService.updateEmployee(employee);
}@CacheEnvict
緩存清除,清除緩存時要指定緩存的名字和key,相當于告訴數(shù)據(jù)庫要刪除哪個表中的哪個數(shù)據(jù),key默認為參數(shù)值。
屬性:
- value/cacheNames:緩存的名字;
- key:緩存的鍵;
- allEnries:是否清除指定緩存中的所有鍵值對,默認為false,設置為true時會清除緩存中的所有鍵值對,與key屬性二選一使用;
- beforeInvocation:在@CacheEnvict注解的方法調(diào)用之間清除指定緩存,默認為false,即在方法調(diào)用之后清除緩存,設置為true時則會在方法調(diào)用之間清除緩存(子啊方法調(diào)用之前還是之后清除緩存的區(qū)別在于方法調(diào)用時是否會出現(xiàn)異常,若不出現(xiàn)異常,這兩種設置沒有區(qū)別,若出現(xiàn)異常,設置為在方法調(diào)用之后清除緩存則不起作用,因為方法調(diào)用失敗了)。
@Delete("/{id}")
@CacheEvict(cacheNames = "emp",key = "#id",beforeInvocation = true)
public void deleteEmp(@PathVariable("id") Integer id) {
employeeService.deleteEmployee(id);
}@CacheConfig
作用:標注在類上,抽取緩存相關的公共配置,可抽取的公共配置有緩存的名字,主鍵生成器等
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}實列:通過@CacheConfig的cacheNames屬性指定緩存的名字之后,該類中的其他緩存注解就不必再寫value或者cacheName了,會使用該名字作為value或cacheName的值,也會遵循就近原則。
@Service
@CacheConfig(cacheNames = "emp")
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Override
@Cacheable(key = "#empId")
public Employee getEmpById(int empId) {
return employeeMapper.getEmpById(empId);
}
}自定義RedisCacheManager
通過前面運用緩存,我們發(fā)現(xiàn)緩存亂碼了

這時我們就需要自定義RedisCacheManager將其加入到SpringIOC容器中解決這個問題
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory
redisConnectionFactory) {
// 分別創(chuàng)建String和JSON格式序列化對象,對緩存數(shù)據(jù)key和value進行轉(zhuǎn)換
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial =
new Jackson2JsonRedisSerializer(Object.class);
// 解決查詢緩存轉(zhuǎn)換異常的問題
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 定制緩存數(shù)據(jù)序列化方式及時效
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(strSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jacksonSeial))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager
.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
}
到此這篇關于SpringBoot 緩存源碼分析的文章就介紹到這了,更多相關SpringBoot 緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java Yml格式轉(zhuǎn)換為Properties問題
本文介紹了作者編寫一個Java工具類來解決在線YAML到Properties轉(zhuǎn)換時屬性內(nèi)容遺漏的問題,通過遍歷YAML文件的樹結(jié)構,作者成功實現(xiàn)了屬性的完整轉(zhuǎn)換,總結(jié)指出,該工具類適用于多種數(shù)據(jù)類型,并且代碼簡潔易懂2024-12-12

