1.理論介紹
Java Caching定義了5個核心接口,分別是CachingProvider, CacheManager, Cache, Entry 和 Expiry。
CachingProvider定義了創建、配置、獲取、管理和控制多個CacheManager。一個應用可以在運行期訪問多個CachingProvider。
CacheManager定義了創建、配置、獲取、管理和控制多個唯一命名的Cache,這些Cache存在於CacheManager的上下文中。一個CacheManager僅被一個CachingProvider所擁有。
Cache是一個類似Map的數據結構並臨時存儲以Key為索引的值。一個Cache僅被一個CacheManager所擁有。
Entry是一個存儲在Cache中的key-value對。
Expiry 每一個存儲在Cache中的條目有一個定義的有效期。一旦超過這個時間,條目為過期的狀態。一旦過期,條目將不可訪問、更新和刪除。緩存有效期可以通過ExpiryPolicy設置。
Spring緩存抽象
Spring從3.1開始定義了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口來統一不同的緩存技術;
並支持使用JCache(JSR-107)注解簡化我們開發;
Cache接口為緩存的組件規范定義,包含緩存的各種操作集合;
Cache接口下Spring提供了各種xxxCache的實現;如RedisCache,EhCacheCache , ConcurrentMapCache等;
每次調用需要緩存功能的方法時,Spring會檢查檢查指定參數的指定的目標方法是否已經被調用過;如果有就直接從緩存中獲取方法調用后的結果,如果沒有就調用方法並緩存結果后返回給用戶。下次調用直接從緩存中獲取。
使用Spring緩存抽象時我們需要關注以下兩點;
1、確定方法需要被緩存以及他們的緩存策略
2、從緩存中讀取之前緩存存儲的數據
2.代碼結構及pom文件
<?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> <groupId>com.zy</groupId> <artifactId>spring-boot-cache</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-boot-cache</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.14.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <lombok.version>1.16.10</lombok.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <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>1.3.2</version> </dependency> <!-- 引入redis的starter,這里面有jedis客戶端 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <!--<scope>test</scope>--> </dependency> <!-- 引入lombok的依賴 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <!-- 引入caffeine緩存Jar包 --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.6.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3.啟動類
package com.zy; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; /** * 一、搭建基本環境 * 1、導入數據庫文件 創建出department和employee表 * 2、創建javaBean封裝數據 * 3、整合MyBatis操作數據庫 * 1.配置數據源信息 * 2.使用注解版的MyBatis; * 1)、@MapperScan指定需要掃描的mapper接口所在的包 * 二、快速體驗緩存 * 步驟: * 1、開啟基於注解的緩存 @EnableCaching * 2、標注緩存注解即可 * @Cacheable * @CacheEvict * @CachePut * 默認使用的是ConcurrentMapCacheManager==ConcurrentMapCache;將數據保存在 ConcurrentMap<Object, Object>中 * 開發中使用緩存中間件;redis、memcached、ehcache; * 三、整合redis作為緩存 * Redis 是一個開源(BSD許可)的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。 * 1、安裝redis:使用docker; * 2、引入redis的starter * 3、配置redis * 4、測試緩存 * 原理:CacheManager===Cache 緩存組件來實際給緩存中存取數據 * 1)、引入redis的starter,容器中保存的是 RedisCacheManager; * 2)、RedisCacheManager 幫我們創建 RedisCache 來作為緩存組件;RedisCache通過操作redis緩存數據的 * 3)、默認保存數據 k-v 都是Object;利用序列化保存;如何保存為json * 1、引入了redis的starter,cacheManager變為 RedisCacheManager; * 2、默認創建的 RedisCacheManager 操作redis的時候使用的是 RedisTemplate<Object, Object> * 3、RedisTemplate<Object, Object> 是 默認使用jdk的序列化機制 * 4)、自定義CacheManager; * */ @SpringBootApplication // 掃描mapper,支持mybatis @MapperScan("com.zy.mapper") // 開啟緩存注解,此外,還要在service層加上緩存注解@Cacheable才能生效 @EnableCaching public class SpringBootCacheApplication { public static void main(String[] args) { SpringApplication.run(SpringBootCacheApplication.class, args); } }
4.model層對象
package com.zy.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.*; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor @Builder @JsonIgnoreProperties(ignoreUnknown = true) public class Employee implements Serializable { private Integer id; private String lastName; private String email; //1 male, 0 female private Integer gender; private Integer dId; } package com.zy.model; /** * * 部門的實體類 */ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor @Builder @JsonIgnoreProperties(ignoreUnknown = true) public class Department implements Serializable { private Integer id; private String departmentName; }
5.mapper層
package com.zy.mapper; import com.zy.model.Employee; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; /** * * employee是xml配置文件版的mapper */ // 此處@Mapper也可以不寫,但是要在springboot啟動類出加上注解@MapperScan // @Mapper public interface EmployeeMapper { @Select("SELECT * FROM employee WHERE id = #{id}") public Employee getEmpById(Integer id); @Update("UPDATE employee SET lastName=#{lastName},email=#{email},gender=#{gender},dId=#{dId} WHERE id=#{id}") public void updateEmp(Employee employee); @Delete("DELETE FROM employee WHERE id=#{id}") public void deleteEmpById(Integer id); @Insert("INSERT INTO employee(lastName,email,gender,dId) VALUES(#{lastName},#{email},#{gender},#{dId})") public void insertEmployee(Employee employee); @Select("SELECT * FROM employee WHERE lastName = #{lastName}") Employee getEmpByLastName(String lastName); } package com.zy.mapper; /** * * department是注解版的mybatis */ import com.zy.model.Department; import org.apache.ibatis.annotations.*; // 此處@Mapper也可以不寫,但是要在springboot啟動類出加上注解@MapperScan // @Mapper public interface DepartmentMapper { @Options(useGeneratedKeys = true, keyProperty = "id") @Insert("insert into department(departmentName) values(#{departmentName})") public int addDept(Department department); @Delete("delete from department where id=#{id}") public void deleteDeptById(Integer id); @Update("update department set departmentName=#{departmentName} where id=#{id}") public void updateDeptById(Integer id); @Select("select * from department where id=#{id}") public Department getDeptById(Integer id); }
6.service層實現類
package com.zy.service; import com.zy.mapper.EmployeeMapper; import com.zy.model.Employee; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.*; import org.springframework.stereotype.Service; // 抽取公共配置-->到類上 @CacheConfig(cacheNames="emp") @Service("employeeService") public class EmployeeServiceImpl { @Autowired private EmployeeMapper employeeMapper; /** * 將方法的運行結果進行緩存;以后再要相同的數據,直接從緩存中獲取,不用調用方法; * CacheManager管理多個Cache組件的,對緩存的真正CRUD操作在Cache組件中,每一個緩存組件有自己唯一一個名字; * * * 原理: * 1、自動配置類;CacheAutoConfiguration * 2、緩存的配置類 * org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration * org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration * org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration * org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration * org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration * org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration * org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration * org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration * org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration * org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration【默認】 * org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration * 3、哪個配置類默認生效:SimpleCacheConfiguration; * * 4、給容器中注冊了一個CacheManager:ConcurrentMapCacheManager * 5、可以獲取和創建ConcurrentMapCache類型的緩存組件;他的作用將數據保存在ConcurrentMap中; * * 運行流程: * @Cacheable: * 1、方法運行之前,先去查詢Cache(緩存組件),按照cacheNames指定的名字獲取; * (CacheManager先獲取相應的緩存),第一次獲取緩存如果沒有Cache組件會自動創建。 * 2、去Cache中查找緩存的內容,使用一個key,默認就是方法的參數; * key是按照某種策略生成的;默認是使用keyGenerator生成的,默認使用SimpleKeyGenerator生成key; * SimpleKeyGenerator生成key的默認策略; * 如果沒有參數;key=new SimpleKey(); * 如果有一個參數:key=參數的值 * 如果有多個參數:key=new SimpleKey(params); * 3、沒有查到緩存就調用目標方法; * 4、將目標方法返回的結果,放進緩存中 * * @Cacheable標注的方法執行之前先來檢查緩存中有沒有這個數據,默認按照參數的值作為key去查詢緩存, * 如果沒有就運行方法並將結果放入緩存;以后再來調用就可以直接使用緩存中的數據; * * 核心: * 1)、使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】組件 * 2)、key使用keyGenerator生成的,默認是SimpleKeyGenerator * * * 幾個屬性: * cacheNames或者value:指定緩存組件的名字;將方法的返回結果放在哪個緩存中,是數組的方式,可以指定多個緩存; * * key:緩存數據使用的key;可以用它來指定。默認是使用方法參數的值 1-方法的返回值 * 編寫SpEL; #id;參數id的值 #a0 #p0 #root.args[0] * getEmp[2] * * keyGenerator:key的生成器;可以自己指定key的生成器的組件id * key和keyGenerator:二選一使用; * * * cacheManager:指定緩存管理器;或者cacheResolver指定獲取解析器;二選一使用; * * condition:指定符合條件的情況下才緩存; * ,condition = "#id>0" * condition = "#a0>1":第一個參數的值》1的時候才進行緩存 * * unless:否定緩存;當unless指定的條件為true,方法的返回值就不會被緩存;可以獲取到結果進行判斷 * unless = "#result == null" * unless = "#a0==2":如果第一個參數的值是2,結果不緩存; * sync:是否使用異步模式 * @param id * @return * */ // 此處與啟動類的@EnableCaching注解連用才能生效 @Cacheable(value = "{emp}") public Employee getEmpById(Integer id){ Employee employee = employeeMapper.getEmpById(id); return employee; } /** * @CachePut:既調用方法,又更新緩存數據;同步更新緩存 * 修改了數據庫的某個數據,同時更新緩存; * 運行時機: * 1、先調用目標方法 * 2、將目標方法的結果緩存起來 * * 測試步驟: * 1、查詢1號員工;查到的結果會放在緩存中; * key:1 value:lastName:張三 * 2、以后查詢還是之前的結果 * 3、更新1號員工;【lastName:zhangsan;gender:0】 * 將方法的返回值也放進緩存了; * key:傳入的employee對象 值:返回的employee對象; * 4、查詢1號員工? * 應該是更新后的員工; * key = "#employee.id":使用傳入的參數的員工id; * key = "#result.id":使用返回后的id * @Cacheable的key是不能用#result * 為什么是沒更新前的?【1號員工沒有在緩存中更新】 * */ @CachePut(/*value = "emp",*/key = "#result.id") public void updateEmp(Employee employee){ employeeMapper.updateEmp(employee); } /** * @CacheEvict:緩存清除 * key:指定要清除的數據 * allEntries = true:指定清除這個緩存中所有的數據 * beforeInvocation = false:緩存的清除是否在方法之前執行 * 默認代表緩存清除操作是在方法執行之后執行;如果出現異常緩存就不會清除 * * beforeInvocation = true: * 代表清除緩存操作是在方法運行之前執行,無論方法是否出現異常,緩存都清除 */ @CacheEvict public void deleteEmp(Integer id){ employeeMapper.deleteEmpById(id); } // @Caching 定義復雜的緩存規則 @Caching( cacheable = { @Cacheable(/*value="emp",*/key = "#lastName") }, put = { @CachePut(/*value="emp",*/key = "#result.id"), @CachePut(/*value="emp",*/key = "#result.email") } ) public Employee getEmpByLastName(String lastName){ return employeeMapper.getEmpByLastName(lastName); } }
7.controller層
package com.zy.controller; import com.zy.model.Employee; import com.zy.service.EmployeeServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/emps") public class EmployeeController { @Autowired private EmployeeServiceImpl employeeService; @GetMapping("/getEmpById/{id}") public Employee getEmpById(@PathVariable("id") Integer id){ Employee employee = employeeService.getEmpById(id); return employee; } @GetMapping("/updateEmp") public void updateEmp(Employee employee){ employeeService.updateEmp(employee); } @GetMapping("/delemp") public String deleteEmp(Integer id){ employeeService.deleteEmp(id); return "success"; } @GetMapping("/emp/lastname/{lastName}") public Employee getEmpByLastName(@PathVariable("lastName") String lastName){ return employeeService.getEmpByLastName(lastName); } }
8.application.yml文件
spring:
# 這里指定數據源即配置
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_boot
username: root
password: 123456
# 這里指定cache的類型即配置
cache:
type: caffeine
caffeine:
spec: initialCapacity=100,maximumSize=500,expireAfterWrite=30s
# Caffeine配置說明:
# initialCapacity=[integer]: 初始的緩存空間大小
# maximumSize=[long]: 緩存的最大條數
# maximumWeight=[long]: 緩存的最大權重
# expireAfterAccess=[duration]: 最后一次寫入或訪問后經過固定時間過期
# expireAfterWrite=[duration]: 最后一次寫入后經過固定時間過期
# refreshAfterWrite=[duration]: 創建緩存或者最近一次更新緩存后經過固定的時間間隔,刷新緩存
# weakKeys: 打開key的弱引用
# weakValues:打開value的弱引用
# softValues:打開value的軟引用
# recordStats:開發統計功能
# 連接redis緩存主機:這里是redis的配置
# redis:
# host: 192.168.0.100
logging:
level:
com.zy.mapper: debug
# 打印所有詳情
debug: true
9.當使用redis進行緩存時,若需要更改默認的序列化規則,則可以添加:
# docker中國鏡像加速http://www.docker-cn.com/registry-mirror
# docker安裝redis
docker pull registry.docker-cn.com/library/redis
# docker 運行redis
docker run --name redis01 -p 6379:6379 -d --rm registry.docker-cn.com/library/redis
報錯解決:
報錯1: WARNING: IPv4 forwarding is disabled. Networking will not work.
解決辦法: # vi /etc/sysctl.conf 或者 # vi /usr/lib/sysctl.d/00-system.conf 添加如下代碼: net.ipv4.ip_forward=1 重啟network服務 # systemctl restart network 查看是否修改成功 # sysctl net.ipv4.ip_forward 如果返回為“net.ipv4.ip_forward = 1”則表示成功了
報錯2: /usr/bin/docker-current: Error response from daemon: driver failed programming external connectivity on endpoint redis01 (19c4784b08511fa716aea4d0eb3da402e648f409afa3e532b178ff3c2d95a92d): exec: "docker-proxy": executable file not found in $PATH.
ln -s /usr/libexec/docker/docker-proxy-current /usr/bin/docker-proxy
報錯3: /usr/bin/docker-current: Error response from daemon: shim error: docker-runc not installed on system.
ln -s /usr/libexec/docker/docker-runc-current /usr/libexec/docker/docker-runc
9.1MyRedisConfig
package com.zy.config; import com.zy.model.Department; import com.zy.model.Employee; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import java.net.UnknownHostException; @Configuration public class MyRedisConfig { @Bean public RedisTemplate<Object, Employee> empRedisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(ser); return template; } @Bean public RedisTemplate<Object, Department> deptRedisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Department> template = new RedisTemplate<Object, Department>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Department> ser = new Jackson2JsonRedisSerializer<Department>(Department.class); template.setDefaultSerializer(ser); return template; } //CacheManagerCustomizers可以來定制緩存的一些規則 @Primary //將某個緩存管理器作為默認的 @Bean public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> empRedisTemplate){ RedisCacheManager cacheManager = new RedisCacheManager(empRedisTemplate); //key多了一個前綴 //使用前綴,默認會將CacheName作為key的前綴 cacheManager.setUsePrefix(true); return cacheManager; } @Bean public RedisCacheManager deptCacheManager(RedisTemplate<Object, Department> deptRedisTemplate){ RedisCacheManager cacheManager = new RedisCacheManager(deptRedisTemplate); //key多了一個前綴 //使用前綴,默認會將CacheName作為key的前綴 cacheManager.setUsePrefix(true); return cacheManager; } }
9.2RedisDemo
package com.zy;
import com.zy.mapper.EmployeeMapper;
import com.zy.model.Employee;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Set;
/**
* Redis常見的五大數據類型
* String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)
* stringRedisTemplate.opsForValue()[String(字符串)]
* stringRedisTemplate.opsForList()[List(列表)]
* stringRedisTemplate.opsForSet()[Set(集合)]
* stringRedisTemplate.opsForHash()[Hash(散列)]
* stringRedisTemplate.opsForZSet()[ZSet(有序集合)]
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootCacheApplicationTests {
@Autowired
private EmployeeMapper employeeMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 自定義的redisTemplate;
@Autowired
private RedisTemplate<Object, Employee> empRedisTemplate;
@Test
public void contextLoads() {
/*employeeMapper*/
System.out.println(employeeMapper.getEmpById(2));
}
@Test
public void fn01(){
// 1.向redis中放置數據,並取出
stringRedisTemplate.opsForValue().append("good", "morning");
String good = stringRedisTemplate.opsForValue().get("good");
System.out.println(good);
// 2.放置其他類型的數據
stringRedisTemplate.opsForList().leftPush("mylist", "zx");
RedisOperations<String, String> operations = stringRedisTemplate.opsForList().getOperations();
Set<String> mylist = operations.keys("mylist");
}
@Test
// 測試保存對象
public void fn02(){
Employee employee = employeeMapper.getEmpById(1);
/*
官方自帶的方式
redisTemplate.opsForValue().set("emp01", employee);
*/
//1、將數據以json的方式保存
//(1)自己將對象轉為json
//(2)redisTemplate默認的序列化規則;改變默認的序列化規則;
empRedisTemplate.opsForValue().set("emp01", employee);
}
}
9.3分別在對應的service實現類上加上對應的cacheManager