關於Spring整合Redis我們之前已經有過介紹,這裏對相關注解的使用我們就不再介紹太多,可以查看Spring整合Redis註解實現瞭解
JSR107緩存規範
Java Caching定義了5個核心接口
-
CachingProvider
定義了創建、配置、獲取、管理和控制多個CacheManager。一個應用可以在運行期間訪問多個CachingProvider
-
CacheManager
定義了創建、配置、獲取、管理和控制多個唯一命名的Cache,這些Cache存在於CacheManage的上下文中,一個CacheManage只被一個CachingProvider擁有
-
Cache
類似於Map的數據結構並臨時儲存以key爲索引的值,一個Cache僅僅被一個CacheManage所擁有
-
Entry
存儲在Cache中的key-value對
-
Expiry
存儲在Cache的條目有一個定義的有效期,一旦超過這個時間,就會設置過期的狀態,過期無法被訪問,更新,刪除。緩存的有效期可以通過ExpiryPolicy設置。
Spring的緩存抽象
功能 | |
---|---|
Cache | 緩存接口,定義緩存操作,實現有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 緩存管理器,管理各種緩存(Cache)組件 |
@Cacheable | 針對方法配置,根據方法的請求參數對其結果進行緩存 |
@CacheEvict | 清空緩存 |
@CachePut | 保證方法被調用,又希望結果被緩存 update,調用,將信息更新緩存 |
@EnableCaching | 開啓基於註解的緩存 |
KeyGenerator | 緩存數據時key生成的策略 |
serialize | 緩存數據時value序列化策略 |
SpringBoot整合緩存
1. 關於數據庫相關配置以及Mapper、數據源的配置與編寫我這裏就不在贅述,注意這裏的model我們要實現Serializable接口
import java.io.Serializable;
public class Department implements Serializable{
private Integer id;
private String deptName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
@Override
public String toString() {
return "Department{" +
"id=" + id +
", deptName='" + deptName + '\'' +
'}';
}
}
2. EnableCaching
在我們的項目啓動類上加上@EnableCaching 註解,開啓緩存
@MapperScan("com.example.cache.dao")
@SpringBootApplication
@EnableCaching
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
3. DepartmentServiceImpl
將方法的運行結果進行緩存,以後要是再有相同的數據,直接從緩存中獲取,不用調用方法
CacheManager中管理多個Cache組件,對緩存的真正CRUD操作在Cache組件中,每個緩存組件都有自己的唯一名字;
屬性:
- CacheName/value:指定存儲緩存組件的名字
- key:緩存數據使用的key,可以使用它來指定。默認是使用方法參數的值,1-方法的返回值
- 編寫Spel表達式:#id 參數id的值, #a0/#p0 #root.args[0]
- keyGenerator:key的生成器,自己可以指定key的生成器的組件id
- key/keyGendertor二選一使用
- cacheManager指定Cache管理器,或者cacheReslover指定獲取解析器
- condition:指定符合條件的情況下,才緩存;
- unless:否定緩存,unless指定的條件爲true,方法的返回值就不會被緩存,可以獲取到結果進行判斷
- sync:是否使用異步模式,unless不支持
@Service
public class DepartmentServiceImpl implements IDepartmentService{
@Autowired
private IDepartmentDao departmentDao;
@Cacheable(key="'depts'+#root.methodName") //cacheable的key是不能使用result的參數的
@Override
public List<Department> getAll() throws Exception{
System.out.println("進來了.............");
return departmentDao.getAll();
}
}
@Cacheable
針對方法配置,根據方法的請求參數對其結果進行緩存,將方法的運行結果進行緩存,以後要是再有相同的數據,直接從緩存中獲取,不用調用方法,因此cacheable的key是不能使用result的參數的(如果直接取緩存,此時是沒有result的)
我們第一次運行getAll 方法,發現方法是被執行的
我們第二次運行getAll 方法,發現方法沒有執行的,說明從緩存中取數據
那麼問題來了,我們並沒有使用redis,mogodb這種緩存中間件,springBoot是如何做緩存的呢?存在哪裏呢?
原理
SpringBoot爲我們自動配置了緩存相關CacheAutoConfiguration,並且引入了很多的緩存組件
在我們沒有配置任何緩存中間件的時候也就是沒有導入任何相關緩存啓動器,默認SimpleCacheConfiguration生效
給容器註冊一個CacheManager:ConcurrentMapCacheManager
可以獲取和創建ConcurrentMapCache,作用是將數據保存在ConcurrentMap中
這也就是我們沒有配置任何緩存中間件就可以使用緩存的原因了!
運行流程
1、方法運行之前,先查Cache(緩存組件),按照cacheName的指定名字獲取;
(CacheManager先獲取相應的緩存),第一次獲取緩存如果沒有cache組件會自己創建
2、去Cache中查找緩存的內容,使用一個key,默認就是方法的參數;
key是按照某種策略生成的,默認是使用keyGenerator生成的,默認使用SimpleKeyGenerator生成key
沒有參數 key=new SimpleKey()
如果有一個參數 key=參數值
如果多個參數 key=new SimpleKey(params);
3、沒有查到緩存就調用目標方法
4、將目標方法返回的結果,放回緩存中
方法執行之前,@Cacheable先來檢查緩存中是否有數據,按照參數的值作爲key去查詢緩存,如果沒有,就運行方法,存入緩存,如果有數據,就取出map的值。
SpringBoot整合Redis
關於Redis這裏就不做過多的介紹了,我們直接開始整合
- 加入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置文件配置主機地址
spring.redis.host=127.0.0.1
SpringBoot會自動幫我們配置RedisTemplate 和 StringRedisTemplate
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testRedis() throws Exception{
stringRedisTemplate.opsForList().leftPush("testString","hello");
stringRedisTemplate.opsForList().leftPush("testString","world");
System.out.println(stringRedisTemplate.opsForList().leftPop("testString"));
System.out.println(stringRedisTemplate.opsForList().leftPop("testString"));
Department department = departmentDao.getOne(19);
redisTemplate.opsForList().leftPush("departments",department);
}
可以看到字符串已經正常出隊,而部門數據也緩存在Redis
接下來我們使用Redis完成我們部門的增刪改差功能(一個List的緩存以及每個部門的緩存)
/**
* CacheConfig抽取緩存的公共配置
* 然後下面的value=department就不用寫了
*/
@CacheConfig(cacheNames="department")
@Service
public class DepartmentServiceImpl implements IDepartmentService{
@Autowired
private IDepartmentDao departmentDao;
/**
* List查詢
* @return
* @throws Exception
*/
@Cacheable(key="'depts'+#root.methodName") //cacheable的key是不能使用result的參數的
@Override
public List<Department> getAll() throws Exception{
System.out.println("進來了.............");
return departmentDao.getAll();
}
/**
* 在添加的時候我們要清除list緩存並更新改記錄緩存
* 注意因爲我們key使用id作爲其中一部分,所以我們添加之後要把主鍵映射到dept對象上
* @param dept
* @return
* @throws Exception
*/
@Caching(put={
@CachePut(key = "'yangzhao-'+#dept.id")
},evict = {
@CacheEvict(key="'deptsgetAll'")//可以指定先刪除緩存還是先刪除數據庫
})
@Override
public Department insert(Department dept) throws Exception {
return 1 == departmentDao.insert(dept)?dept:null;
}
/**
* 在修改的時候我們要清除list緩存並更新改記錄緩存
* @param dept
* @return
* @throws Exception
*/
@Caching(put={
@CachePut(key = "'yangzhao-'+#dept.id")//先執行方法 再緩存
},evict = {
@CacheEvict(key="'deptsgetAll'")
})
@Override
public Department update(Department dept) throws Exception {
departmentDao.update(dept);
return dept;
}
/**
* 在刪除的時候我們要清空list緩存以及該對象緩存,對此項目可以使用allEntries
* 可以使用自定義keyGenerator
* @param id
* @throws Exception
*/
@Caching(evict={
@CacheEvict(keyGenerator = "myKeyGenerator",beforeInvocation = true),//方法執行前清空緩存 allEntries = true清空所有
@CacheEvict(key="'depts'+'getAll'")
})
// @CacheEvict(allEntries = true)
@Override
public void delete(Integer id) throws Exception {
departmentDao.delete(id);
}
/**
* Id查詢(如果Id大於16才緩存)
* @param id
* @return
* @throws Exception
*/
@Cacheable(keyGenerator = "myKeyGenerator",condition = "#id > 16") //滿足條件才緩存 與 unless 相反 有緩存走緩存 沒有走方法
@Override
public Department get(Integer id) throws Exception {
System.out.println("get............");
return departmentDao.getOne(id);
}
}
MyKeyGenerator
@Component
public class MyKeyGenerator implements KeyGenerator
{
@Override
public Object generate(Object target, Method method, Object... params) {
System.out.println("參數="+params[0]);
System.out.println("參數="+ Arrays.asList(params));
return "yangzhao-"+params[0];
}
}
IDepartmentDao
@Insert({"insert into department (dept_name) values(#{deptName})"})
@SelectKey(keyProperty="id",before=false,statement="SELECT LAST_INSERT_ID()",resultType=int.class,keyColumn="id")
public int insert(Department department) throws Exception;
測試添加
@Test
public void add() throws Exception{
Department dept = new Department();
dept.setDeptName("測試添加");
departmentService.insert(dept);
}
可以看到 Redis中已經多了一個緩存,並且List緩存已經被清空
@Test
public void getOne() throws Exception{
System.out.println(departmentService.get(32));
}
運行get方法並沒有打印get…說明從緩存讀取數據
但是確是十六進制不方便查看
以json方式傳輸對象
新建一個Redis的配置類MyRedisConfig
@Configuration
public class MyRedisConfig {
@Bean
RedisTemplate<Object,Department> deptRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Department> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Department.class);
redisTemplate.setDefaultSerializer(serializer);
return redisTemplate;
}
@Primary // 有多個cacheManager需要制定一個默認的一般將jdk的cacheManager作爲默認
@Bean
RedisCacheManager deptRedisCacheManager(RedisTemplate<Object,Department> deptRedisTemplate){
RedisCacheManager manager = new RedisCacheManager(deptRedisTemplate);
manager.setUsePrefix(true);// 使用前綴,默認將CacheName作爲前綴
return manager;
}
@Bean
RedisTemplate<Object,Employee> employeeRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Employee> employeeRedisTemplate = new RedisTemplate<>();
employeeRedisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Employee.class);
employeeRedisTemplate.setDefaultSerializer(serializer);
return employeeRedisTemplate;
}
@Bean
RedisCacheManager empRedisCacheManager(RedisTemplate<Object,Employee> employeeRedisTemplate){
RedisCacheManager empRedisCacheManager = new RedisCacheManager(employeeRedisTemplate);
empRedisCacheManager.setUsePrefix(true);
return empRedisCacheManager;
}
}
然後在我們的DepartmentServiceImpl指定
@CacheConfig(cacheNames="department",cacheManager = "deptRedisCacheManager")
@Service
public class DepartmentServiceImpl implements IDepartmentService{
再次運行添加方法,可以看已經是已json方式保存了
測試getAll
可以看到第一次訪問數據庫且Redis中也將查詢結果做了緩存
測試刪除
@Test
public void del() throws Exception{
departmentService.delete(32);
}
刪除前緩存情況
刪除後緩存情況
測試修改
@Test
public void update() throws Exception{
Department dept = new Department();
dept.setDeptName("測試修改");
dept.setId(31);
departmentService.update(dept);
}
修改前
修改後
總結
- 使用緩存時,默認使用的是ConcurrentMapCache,將數據保存在ConcurrentMap中,開發中使用的是緩存中間件,redis、memcached、ehcache等
- starter啓動時,有順序,redis優先級比ConcurrentMapCache更高,CacheManager變爲RedisCacheManager,所以使用的是redis緩存
- 傳入的是RedisTemplate<Object, Object> 默認使用的是jdk的序列化保存, 我們可以自定義CacheManager,編寫一個配置類
- XXXRedisTemplate只能用來反序列化XXX,而不能反序列化其他對象,因此要創建不同的XXXRedisTemplate 並且分別創建XXXManager