1. 程式人生 > >【開源專案系列】如何基於 Spring Cache 實現多級快取(同時整合本地快取 Ehcache 和分散式快取 Redis)

【開源專案系列】如何基於 Spring Cache 實現多級快取(同時整合本地快取 Ehcache 和分散式快取 Redis)

## 一、快取 當系統的併發量上來了,如果我們頻繁地去訪問資料庫,那麼會使資料庫的壓力不斷增大,在高峰時甚至可以出現數據庫崩潰的現象。所以一般我們會使用快取來解決這個資料庫併發訪問問題,使用者訪問進來,會先從快取裡查詢,如果存在則返回,如果不存在再從資料庫裡查詢,最後新增到快取裡,然後返回給使用者,當然了,接下來又能使用快取來提供查詢功能。 而快取,一般我們可以分為本地快取和分散式快取。 常用的本地快取有 ehcache、guava cache,而我們一般都是使用 ehcache,畢竟他是純 Java 的,出現問題我們還可以根據原始碼解決,並且還能自己進行二次開發來擴充套件功能。 常用的分散式快取當然就是 Redis 了,Redis 是基於記憶體和單執行緒的,執行效率非常的高。 ## 二、Spring Cache 相信如果要整合快取到專案中,大家都會使用到 Spring Cache,它不但整合了多種快取框架(ehcache、jcache等等),還可以基於註解來使用,是相當的方便。 快取框架的整合在 spring-context-support 中: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200406213112118.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hvd2luZnVu,size_16,color_FFFFFF,t_70) 快取註解在 spring-context 中: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200406213121312.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hvd2luZnVu,size_16,color_FFFFFF,t_70) 當然了,在 Spring 的 context 中沒有整合 Redis,但是我們可以在 spring-data-redis 中找到。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200406213157932.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hvd2luZnVu,size_16,color_FFFFFF,t_70) 但是我們都知道,不管是在 Spring 專案 還是 Spring Boot 中,我們都只能整合一種快取,不能同時整合多種快取。 在 Spring Boot 中,我們一般是利用 `spring.cache.type` 來指定使用哪種快取,然後填寫相關配置資訊來完成自動配置。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/202004062132171.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hvd2luZnVu,size_16,color_FFFFFF,t_70) CacheType 的原始碼:我們可以看到,Spring 是支援非常多種快取框架的。 ```java package org.springframework.boot.autoconfigure.cache; public enum CacheType { GENERIC, JCACHE, EHCACHE, HAZELCAST, INFINISPAN, COUCHBASE, REDIS, CAFFEINE, SIMPLE, NONE; private CacheType() { } } ``` 那麼如果我們就是有這麼一種需求,要整合兩種快取框架:例如一個本地快取 Ehcache,一個分散式快取 Redis, 那能整麼? 能是能,但是 Spring 可不提供這種多級快取,而是需要你自己動手來整了。 ## 三、h2cache-spring-boot-starter ### 1、什麼是 h2cache-spring-boot-starter? 在微服務中,每個服務都是無狀態的,服務之間需要經過 HTTP 或者 RPC 來進行通訊。而每個服務都擁有自己對應的資料庫,所以說如果服務A 需要獲取服務B 的某個表的資料,那麼就需要一次 HTTP 或 RPC 通訊,那如果高峰期每秒需要呼叫100次,那豈不是需要100次 HTTP 或 RPC 通訊,這是相當耗費介面效能的。 那怎麼解決呢? 本地快取那是肯定不是的,因為一般不同服務都是部署在不同的機器上面的,所以此時我們需要的是分散式快取,例如 Redis;但是,訪問量高的的服務當然還是需要本地快取了。所以最後,我們不但需要本地快取,還需要分散式快取,但是 Spring Boot 卻不能提供這種多級快取的功能,所以需要我們自己來整合。 不用怕,我已經自己整了一個 Spring Boot Starter了,就是` h2cache-spring-boot-starter`,我們只需要在配置檔案配置上對應的資訊,就可以啟用這個多級快取的功能了。 ### 2、開始使用 #### 新增依賴: 大家正常引入下面依賴即可,因為我已經將此專案釋出到 Maven 中央倉庫了~ ```xml com.github.howinfun h2cache-spring-boot-starter 0.0.1 ``` #### 在 Spring Boot properties 啟用服務,並且加上對應的配置: 開啟多級快取服務: ```properties # Enable L2 cache or not h2cache.enabled=true ``` 配置 Ehcache: ```properties # Ehcache Config ## the path of ehcache.xml (We can put it directly under Resources) h2cache.ehcache.filePath=ehcache.xml #Set whether the EhCache CacheManager should be shared (as a singleton at the ClassLoader level) or independent (typically local within the application).Default is "false", creating an independent local instance. h2cache.ehcache.shared=true ``` 配置 Redis:主要包括預設的快取配置和自定義快取配置 要注意一點的是:h2cache-spring-boot-starter 同時引入了 `Lettuce` 和 `Jedis` 客戶端,而 Spring Boot 預設使用 Lettuce 客戶端,所以如果我們需要使用 Jedis 客戶端,需要將 Lettuce 依賴去除掉。 ```properties # Redis Config ## default Config (expire) h2cache.redis.default-config.ttl=200 ### Disable caching {@literal null} values.Default is "false" h2cache.redis.default-config.disable-null-values=true ### Disable using cache key prefixes.Default is "true" h2cache.redis.default-config.use-prefix=true ## Custom Config list ### cacheName ->
@CacheConfig#cacheNames @Cacheable#cacheNames and other comments, etc h2cache.redis.config-list[0].cache-name=userCache h2cache.redis.config-list[0].ttl=60 h2cache.redis.config-list[0].use-prefix=true h2cache.redis.config-list[0].disable-null-values=true h2cache.redis.config-list[1].cache-name=bookCache h2cache.redis.config-list[1].ttl=60 h2cache.redis.config-list[1].use-prefix=true #Redis spring.redis.host=10.111.0.111 spring.redis.password= spring.redis.port=6379 spring.redis.database=15 # 連線池最大連線數(使用負值表示沒有限制) spring.redis.jedis.pool.max-active=8 # 連線池中的最小空閒連線 spring.redis.jedis.pool.min-idle=0 # 連線池中的最大空閒連線 spring.redis.jedis.pool.max-idle=8 # 連線池最大阻塞等待時間(使用負值表示沒有限制) spring.redis.jedis.pool.max-wait=30 ``` #### 如何使用快取註解 我們只要像之前一樣使用 `Spring Cache` 的註解即可。 **for example:** 程式碼裡的持久層,我使用的是: [mybatis-plus](https://github.com/baomidou/mybatis-plus#links). ``` package com.hyf.testDemo.redis; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Repository; /** * @author Howinfun * @desc * @date 2020/3/25 */ @Repository // Global cache config,We usually set the cacheName @CacheConfig(cacheNames = {"userCache"}) public interface UserMapper extends BaseMapper { /** * put the data to cache(Ehcache & Redis) * @param id * @return */ @Cacheable(key = "#id",unless = "#result == null") User selectById(Long id); /** * put the data to cache After method execution * @param user * @return */ @CachePut(key = "#user.id", condition = "#user.name != null and #user.name != ''") default User insert0(User user) { this.insert(user); return user; } /** * evict the data from cache * @param id * @return */ @CacheEvict(key = "#id") int deleteById(Long id); /** * Using cache annotations in combination * @param user * @return */ @Caching( evict = {@CacheEvict(key = "#user.id", beforeInvocation = true)}, put = {@CachePut(key = "#user.id")} ) default User updateUser0(User user){ this.updateById(user); return user; } } ``` ### 測試一下: 查詢:我們可以看到,在資料庫查詢到結果後,會將資料新增到 `Ehcache` 和 `Redis` 快取中;接著之後的查詢都將會先從 `Ehcache` 或者 `Redis` 裡查詢。 ``` 2020-04-03 09:55:09.691 INFO 5920 --- [nio-8080-exec-7] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2020-04-03 09:55:10.044 INFO 5920 --- [nio-8080-exec-7] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2020-04-03 09:55:10.051 DEBUG 5920 --- [nio-8080-exec-7] c.h.t.redis.BookMapper2.selectById : ==> Preparing: SELECT id,create_time,update_time,read_frequency,version,book_name FROM book WHERE id=? 2020-04-03 09:55:10.068 DEBUG 5920 --- [nio-8080-exec-7] c.h.t.redis.BookMapper2.selectById : ==> Parameters: 51(Long) 2020-04-03 09:55:10.107 DEBUG 5920 --- [nio-8080-exec-7] c.h.t.redis.BookMapper2.selectById : <== Total: 1 2020-04-03 09:55:10.113 INFO 5920 --- [nio-8080-exec-7] c.hyf.cache.cachetemplate.H2CacheCache : insert into ehcache,key:51,value:Book2(id=51, bookName=微服務架構, readFrequency=1, createTime=2020-03-20T16:10:13, updateTime=2020-03-27T09:14:44, version=1) 2020-04-03 09:55:10.118 INFO 5920 --- [nio-8080-exec-7] c.hyf.cache.cachetemplate.H2CacheCache : insert into redis,key:51,value:Book2(id=51, bookName=微服務架構, readFrequency=1, createTime=2020-03-20T16:10:13, updateTime=2020-03-27T09:14:44, version=1) 2020-04-03 09:55:31.864 INFO 5920 --- [nio-8080-exec-2] c.hyf.cache.cachetemplate.H2CacheCache : select from ehcache,key:51 ``` 刪除:刪除資料庫中的資料後,也會刪除 `Ehcache` 和 `Redis` 中對應的快取資料。 ``` 2020-04-03 10:05:18.704 DEBUG 5920 --- [nio-8080-exec-3] c.h.t.redis.BookMapper2.deleteById : ==> Preparing: DELETE FROM book WHERE id=? 2020-04-03 10:05:18.704 DEBUG 5920 --- [nio-8080-exec-3] c.h.t.redis.BookMapper2.deleteById : ==> Parameters: 51(Long) 2020-04-03 10:05:18.731 DEBUG 5920 --- [nio-8080-exec-3] c.h.t.redis.BookMapper2.deleteById : <== Updates: 1 2020-04-03 10:05:18.732 INFO 5920 --- [nio-8080-exec-3] c.hyf.cache.cachetemplate.H2CacheCache : delete from ehcache,key:51 2020-04-03 10:05:18.844 INFO 5920 --- [nio-8080-exec-3] c.hyf.cache.cachetemplate.H2CacheCache : delete from redis,key:51 ``` 其他的就不用演示了... ## 四、最後 當然啦,這個 starter 還是比較簡單的,如果大家感興趣,可以去看看原始碼是如何基於 Spring Cache 實現多級快取的~