本文主要介紹 JSR107 ,Spring緩存抽象、整合redis
1. JSR107
javaee發佈了 JSR107緩存規範,其中定義了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設置。
2. Spring緩存抽象
由於後來整合要使用jsr107,整個系統難度較大等一系列原因,導致用的比較少,我們更多使用的是spring緩存抽象,其底層卻是和jsr107一樣。
Spring從3.1開始定義了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口來統一不同的緩存技術,並且支持使用JCache(JSR-107)註解簡化我們開發。
Cache |
緩存接口,定義緩存操作。實現有:RedisCache、EnCacheCache、ConcurrentMapCache等 |
CacheManager | 緩存管理器,管理各種緩存(Cache)組件 |
@Cacheable | 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存 |
@CacheEvict | 清空緩存 |
@CachePut | 緩存更新,保證方法被調用,又希望結果被緩存時使用該註解 |
@EnableCaching | 開啓基於註解的緩存 |
keyGenerator | 緩存數據時key生成策略 |
serialize | 緩存數據時value序列化策略 |
- Cache接口爲緩存的組件規範定義,包含緩存的各種操作集合。
- Cache接口下Spring提供了各種xxxCache的實現,如RedisCache、EhCacheCache、ConcurrentMapCache等。
- 每次調用需要緩存功能的方法時,Spring會檢查指定參數的指定的目標方法是否已經被調用過,如果有就直接從緩存中獲取方法調用後的結果,如果沒有就調用方法並緩存結果後返回給用戶。下次調用直接從緩存中獲取。
- 使用spring緩存抽象時,需要關注以下兩點
- 確定方法需要被緩存以及他們的緩存策略。
- 從緩存中讀取之前緩存存儲的數據。
搭建緩存測試基本環境:
數據庫表創建
/*
Navicat MySQL Data Transfer
Source Server : 本地
Source Server Version : 50528
Source Host : 127.0.0.1:3306
Source Database : springboot_cache
Target Server Type : MYSQL
Target Server Version : 50528
File Encoding : 65001
Date: 2018-10-29 10:54:04
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for department
-- ----------------------------
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`lastName` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`gender` int(2) DEFAULT NULL,
`d_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
創建一個新工程,引入以下依賴(idea中):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
創建javabean對象
public class Department {
private Integer id;
private String departmentName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
public Department() {
super();
}
public Department(Integer id, String departmentName) {
this();
this.id = id;
this.departmentName = departmentName;
}
@Override
public String toString() {
return "Department[" +
"id=" + id +
", departmentName='" + departmentName + '\'' +
']';
}
}
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; //性別 1男 0女
private Integer dId;
public Employee() {
}
public Employee(Integer id, String lastName, String email, Integer gender, Integer dId) {
this();
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
this.dId = dId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Integer getdId() {
return dId;
}
public void setdId(Integer dId) {
this.dId = dId;
}
@Override
public String toString() {
return "Employee[" +
"id=" + id +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", gender=" + gender +
", dId=" + dId +
']';
}
}
配置數據源:
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_cache
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 可以省略不寫,可根據連接自動判斷
mybatis:
configuration: # 開啓駝峯命名法
map-underscore-to-camel-case: true
使用註解版的mybatis
1) @MapperScan指定需要掃描的mapper接口所在的包
2)mapper文件編寫
@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},d_id=#{dId} WHERE id=#{id}")
public void update(Employee employee);
@Delete("DELETE FROM employee WHERE id=#{id}")
public void deleteEmpById(Integer id);
@Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#{lastName},#{email},#{gender},#{dId})")
public void insertEmp(Employee employee);
}
3)service層代碼編寫
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public Employee getEmp(Integer id) {
System.out.println("查詢 " + id + " 號員工");
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
}
4)controller層代碼編寫
@RestController
@RequestMapping("emp")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/{id}")
public Employee getEmp(@PathVariable("id") Integer id) {
return employeeService.getEmp(id);
}
}
啓動項目測試: localhost:8080/emp/1 ,如下所示
測試環境搭建完畢後,就可以體驗緩存的使用了。 其步驟如下:
1. 開啓基於註解的緩存 @EnableCaching
2. 標註緩存註解即可
@Cacheable @CacheEvict @CachePut
我們可以先查看沒有加緩存的情況:
在application.yml中,打開日誌輸出
logging:
level:
com.zhao.springboot.mapper: debug
瀏覽器訪問 http://localhost:8080/emp/1 ,每刷新一次頁面,就會查詢一次數據庫,如下所示:
這就說明現在是沒有緩存的。下面使用緩存:
在service層的該查詢方法上個加入 @Cacheable ,將方法的運行結果進行緩存,以後再要相同的數據,直接從緩存中獲取,不用調用方法。
@Cacheable(cacheNames={"emp"})
public Employee getEmp(Integer id) {
System.out.println("查詢 " + id + " 號員工");
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
重新啓動項目,訪問 http://localhost:8080/emp/1 ,經過幾次刷新,發現控制檯只有一次打印,訪問http://localhost:8080/emp/2發現有新的內容輸出。說明結果已被緩存。
那麼,是什麼樣的一個運行流程呢?
@Cacheable
1. 方法運行前,先去查詢Cache(緩存組件),按照cacheName指定的名字獲取(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,也可以自定義。
@CachePut 即調用方法,又更新緩存數據
運行時機:
1). 先調用目標方法
2). 將目標方法的結果緩存起來
編寫service層的修改員工方法
@CachePut(value = "emp")
public Employee update(Employee employee) {
System.out.println("更新" + employee.getId() + "號員工");
employeeMapper.update(employee);
return employee;
}
如上代碼所示,修改員工信息方法,上面加入註解 @CachePut ,爲了方便起見,在controller層,使用了@PutMapping註解,進行測試如下:
1)查詢1號員工:查詢的結果會放入到緩存中。
2) 再次查詢,查詢結果還是之前的結果lastName=張三,gender=1
3) 更新1號員工 [lastName=zhangsan,gender=0],更新成功後返回的值如下:
4) 查詢1號員工,查詢結果如下:
查詢結果卻是沒有更新前的,其原因如下:更新後,確實將返回的結果也放進緩存了,但是由於沒有指定key的名稱,所以默認key爲傳入的employee對象,值:返回的employee對象。於是再次查詢,並沒有去查數據庫,造成數據不一致。
修改如下: 指定key的名稱爲 id
@CachePut(value = "emp", key = "#result.id")
// @CachePut(value = "emp",key = "#employee.id")// 兩者都可以
public Employee update(Employee employee) {
System.out.println("更新" + employee.getId() + "號員工");
employeeMapper.update(employee);
return employee;
}
注意:使用result.id 或者employee.id都可以,但是result不能再@Cacheable下使用。執行時機不同。
此時重修更新員工信息後,再次查詢數據就一致了,另外,注意,更新後再去查詢,也並沒有查詢數據庫,而是查詢的緩存。
也就是說 @CachePut即更新了數據庫,也更新了緩存。
@CacheEvict :緩存清除
編寫service中的delete方法 ,並編寫controller中的delete方法 ,如下:
@CacheEvict(value = "emp",key = "#id",allEntries = false,beforeInvocation = false)
public void deleteEmp(Integer id){
System.out.println("delete "+id+"號員工");
employeeMapper.deleteEmpById(id);
}
@GetMapping("delete")
public String delete(Integer id) {
employeeService.deleteEmp(id);
return "success";
}
key:指定要清除的數據
allEntries = false, allEntries:指定清除這個緩存(value指定的)中所有的數據,默認爲false
當指定爲true時,就意味着當刪除了1號員工後,emp下的所有員工(1,2號)的緩存都會被清除。
beforeInvocation = false ,緩存的清除是否在方法執行前執行。
默認代表是緩存清除操作在方法執行後執行,如果出現異常緩存就不會被清除。 當指定爲true時,意味着,清除緩存操作是在方法執行前執行,無論方法是否出現異常,緩存都清除。
@Caching :定義複製的緩存註解
在mapper中新增一個方法,根據lastName查詢員工,在service新增方法 ,controller中同樣新增,如下:
@Select("SELECT * FROM employee WHERE lastName=#{lastName}")
public Employee getEmpByLastName(String lastName);
@Caching(
cacheable = {
@Cacheable(value = "emp", key = "#name")
},
put = {
@CachePut(value = "emp", key = "#result.id"),
@CachePut(value = "emp", key = "#result.email")
}
)
public Employee getEmpByLastName(String name) {
return employeeMapper.getEmpByLastName(name);
}
@GetMapping("/lastName/{lastName}")
public Employee getEmpByLastName(@PathVariable("lastName") String lastName) {
return employeeService.getEmpByLastName(lastName);
}
此時注意: 當調用 該方法後,會分別以key爲id,email,lastName將結果放入緩存中,此時如果在根據id查詢, 就直接查詢的數據庫,而不是查數據庫,但是當根據lastName查詢時,發現並沒有從緩存中,而是每次都查詢了數據庫,這是由於上面我們指定了 @CachePut ,該註解要求每次查詢數據庫,並更新緩存。
3. 整合redis
默認使用的是ConcurrentMapCacheManager==ConcurrentMapCache,將數據保存在ConcurrentMap<Object,Object>中,在開發中也可以使用緩存中間件:redis,memcached,ehcache
整合redis ,前提在linux上已經安裝了redis ,關於安裝redis的安裝可以參考這篇文章:redis安裝, 如果需要該工具,可以下載這個:
並通過工具進行連接 ,如下所示:
即連接成功。
引入redis的starter :在pom文件中添加以下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis
application.yml配置文件中新增redis配置: spring.redis.host: 192.168.xxx.xxx
可在測試類中簡單測試redis 。
@Autowired
StringRedisTemplate stringRedisTemplate; //操作字符串的
@Autowired
RedisTemplate redisTemplate; // k-v都是對象的
@Autowired
RedisTemplate<Object, Employee> empRedisTemplate;
/**
* Redis常見5大類型
* String(字符串),list(列表),Set(集合),Hash(散列),ZSet(有序)
* stringRedisTemplate.opsForValue();[String字符串]
* stringRedisTemplate.opsForList(); [list集合]
* stringRedisTemplate.opsForSet(); [Set集合]
* stringRedisTemplate.opsForHash(); [Hash散列]
* stringRedisTemplate.opsForZSet(); [ZSet(有序集合)]
*/
@Test
public void testRedis() {
//給redis 中保存數據
// stringRedisTemplate.opsForValue().append("msg","hello");
String val=(String )stringRedisTemplate.opsForValue().get("msg");
System.out.println(val);
// 測試 list
// stringRedisTemplate.opsForList().leftPush("myList","1");
// stringRedisTemplate.opsForList().leftPush("myList","2");
// stringRedisTemplate.opsForList().leftPush("myList","3");
}
//測試保存對象
@Test
public void testObj() {
Employee employee = employeeMapper.getEmpById(1);
// 默認如果保存對象,使用jdk序列化機智,序列化後的數據保存到redis中
// redisTemplate.opsForValue().set("emp-01",employee); //保存後,在查看,發現全部是 \xAc之類的,並不是json形式存入的
// 1. 將數據以json的方式保存
//2. redisTemplate默認的序列化規則:改變默認的序列化規則,在MyRedisConfig中配置的
empRedisTemplate.opsForValue().set("emp-01", employee);
}
注意:當在redis中放入對象時,該對象需要被序列化(實現Serializable接口 ),如果直接用restTempPlate放入(jdk的序列化器),那麼會產生以下的情況 ,並不是以json形式傳入的,
這是因爲默認使用的是jdk的序列化機制,而我們在日常生產中,更多使用的是json形式的數據,爲此,我們可以自定義一個配置,改變默認的序列化規則(使用json的序列化機制)。配置類如下:
@Configuration
public class MyRedisConfig {
// 同理,如果要轉換別的對象,可以重新定義新的template。
@Bean
public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Employee> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 創建一個json的序列化器
Jackson2JsonRedisSerializer<Employee> jjrs = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
template.setDefaultSerializer(jjrs);
return template;
}
}
這樣,使用empRedistemplate 再重新存入對象,就變成如下的形式:
測試緩存:
原理:CacheManager===Cache 緩存組件來實際給緩存中存取數據
1) 引入redis的starter ,容器中保存的是RedisCacheManager
2) RedisCacheManager幫我們自動創建RedisCache來作爲緩存組件,RedisCache通過操作redis來緩存數據。
3) 默認保存數據k-v都是object,利用序列化保存,如何保存爲json?
1. 引入redis的starter ,cacheManager變爲RedisCacheManager
2. 默認創建的RedisCacheManager操作redis的時候使用的是 jdk的序列化機制(RedisCacheConfiguration.class)
public static RedisCacheConfiguration defaultCacheConfig() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
registerDefaultConverters(conversionService);
return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(new StringRedisSerializer()), SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()), conversionService);
}
3. 自定義CacheManager。
@Bean
public RedisCacheManager empCacheManager(RedisConnectionFactory factory) {
Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
return redisCacheManager;
}
自定義CacheManager後,怎麼使得我們自己的定義的cacheManager生效呢?在RedisCacheConfiguration的註解中可以看到,
@ConditionalOnMissingBean ,也就是說,如果容器中沒有CacheManager,就使用該配置創建緩存管理器,但是容器中有,就會替換原有的,於是我們就可以直接啓動項目,測試。 如下:
另外 ,以上全部是通過註解式的將數據放入緩存,也可以通過編碼式的將數據放入緩存,可以在service中引入 自定義的manager,在需要操作緩存的地方,使用manager.getCache(),拿到緩存對象,然後操作數據。
@Service
@CacheConfig(cacheNames = "dep", cacheManager = "deptCacheManager") //因爲存在多個cacheManager ,通過該註解,指明使用某個cacheManager
public class DepartmentService {
@Qualifier("deptCacheManager") //精確的根據id名爲deptCacheManager拿到bean注入
@Autowired
RedisCacheManager deptCacheManager; //根據名稱獲取緩存
/**
* 編碼式 存入緩存
* @param id
* @return
*/
public Department getDepById(Integer id){
System.out.println("查詢部門 " + id);
Department dept= departmentMapper.getDeptById(id);
//獲取緩存。
Cache deptCache= deptCacheManager.getCache("dep");
//放入緩存
deptCache.put("dept:"+dept.getId(),dept);
return dept ;
}
}
注意:當容器中存在多個cacheManager時, 要有一個主cacheManager,即用 @Primary 標識的manager,另外不同的service操作時,要指定 其使用的cacheManager。