文章目錄
1. 前言
Ehcache 作爲本地緩存,接入簡單。
想了解更多知識,可 Google 下 Ehcache,Redis, memcache 等的區別。
我們今天主要將實操,SpringBoot 集成 Ehcache 實現本地緩存。
2. 搭建SpringBoot項目
Intellij IDEA 一路 next 或者 spring.io
2.1 引入依賴
springBoot 相關依賴
<!-- starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 測試 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 緩存支持,超級重要 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
ehcache 依賴,重頭戲
<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.8.1</version>
</dependency>
cache-api 提供基於JSR-107的緩存規範
<!-- https://mvnrepository.com/artifact/javax.cache/cache-api -->
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
lombok 用於簡化代碼
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
工具類,提高效率
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
2.2. 編寫業務類
實體類。採用 Lombok 簡化代碼
@Data
@NoArgsConstructor
@AllArgsConstructor
/**
* 建造者模式 鏈式調用
*/
@Accessors(chain = true)
public class User implements Serializable {
private static final long serialVersionUID = -8451701378160929387L;
/**
* 用戶ID
*/
private Integer id;
/**
* 用戶name
*/
private String name;
}
倉庫類。爲簡化代碼(懶得搭建數據庫及連接池),基於 ConcurrentMap 實現的內存數據庫。採用 Slf4j 做日誌支持,採用 PostConstruct 做初始化調用。
@Slf4j
@Component
public class UserRepository {
/**
* 內存數據庫
*/
private Map<Integer, User> userMap = Maps.newConcurrentMap();
/**
* 初始化
*/
@PostConstruct
private void init() {
userMap.put(1, new User(1, "Alice"));
userMap.put(2, new User(2, "Bob"));
}
public User getById(Integer id) {
log.info("UserRepository#getById: id={}", id);
return userMap.get(id);
}
}
用戶服務類。簡單調用,進行日誌記錄
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User getById(Integer id) {
log.info("UserService#getById: id={}", id);
return userRepository.getById(id);
}
}
web 類。提供路由,採用 Validator 做參數校驗,接口採用 RESTful 風格。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 根據ID獲取用戶信息
* @param id
* @return
*/
@GetMapping("/{id}")
public User getById(@PathVariable(value = "id")
@NonNull @Min(value = 1, message = "id 爲非負數") Integer id) {
return userService.getById(id);
}
}
3. 配置 Ehcache
基於前文已經進行了依賴的引入,配置 Ehcache 較爲簡單。
application.yml 配置 Ehcache 配置文件路徑,方便 SpringBoot 掃描到配置文件。其中, ehcache.xml 爲 Ehcache 配置文件名稱。
spring:
cache:
jcache:
config: classpath:ehcache.xml
配置 Listener,目前進行日誌記錄。
@Slf4j
public class CacheEventLogger implements CacheEventListener<Object, Object> {
@Override
public void onEvent(CacheEvent<?, ?> cacheEvent) {
log.info("cache event logger: type={}, key={}, oldValue={}, newValue={}",
cacheEvent.getType(),
cacheEvent.getKey(),
cacheEvent.getOldValue(),
cacheEvent.getNewValue());
}
}
配置 Ehcache
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<service>
<jsr107:defaults enable-statistics="true"/>
</service>
<!-- user 爲該緩存名稱 對應@Cacheable的屬性cacheNames-->
<cache alias="user">
<!-- 指定緩存 key 類型,對應@Cacheable的屬性key -->
<key-type>java.lang.Integer</key-type>
<!-- 配置value類型 -->
<value-type>club.chenlinghong.demo.ehcache.entity.User</value-type>
<expiry>
<!-- 緩存 ttl -->
<ttl unit="minutes">1</ttl>
</expiry>
<listeners>
<listener>
<!-- 配置Listener -->
<class>club.chenlinghong.demo.ehcache.listener.CacheEventLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>UPDATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>REMOVED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<!-- 分配資源大小 -->
<heap unit="entries">2000</heap>
<offheap unit="MB">100</offheap>
</resources>
</cache>
</config>
@EnableCaching 開啓緩存支持
@SpringBootApplication
@EnableCaching
public class EhcacheApplication {
public static void main(String[] args) {
SpringApplication.run(EhcacheApplication.class, args);
}
}
4. 使用 Ehcache
使用較爲簡單,只需要在需要使用緩存的地方添加註解 @Cacheable ,以及配置相應屬性項即可
一般我們會分析業務,對於一些耗時較長,並且數據不易改變的接口請求做緩存處理。此處我們對 UserRepository 做緩存,用戶信息一般不易改變,滿足緩存的條件;另一方面,用戶信息一般存儲在數據庫、磁盤中,訪問耗時較長,有做緩存的必要。
@Slf4j
@Component
public class UserRepository {
/**
* 內存數據庫
*/
private Map<Integer, User> userMap = Maps.newConcurrentMap();
/**
* 初始化
*/
@PostConstruct
private void init() {
userMap.put(1, new User(1, "Alice"));
userMap.put(2, new User(2, "Bob"));
}
/**
* 此處的 cacheNames 需要和 ehcache 配置文件的配置項一致
*/
@Cacheable(cacheNames = "user", key = "#id")
public User getById(Integer id) {
log.info("UserRepository#getById: id={}", id);
return userMap.get(id);
}
}
完整代碼:https://github.com/chenlinghong/demo
5. 測試
此時我們運行項目,直接通過瀏覽器、Postman 進行測試,並查看其日誌即可驗證。
5.1. 第一次請求接口
接口正常返回數據,日誌如下:
2019-09-20 01:21:23.511 INFO 13548 --- [nio-8080-exec-1] c.c.d.e.service.impl.UserServiceImpl : UserService#getById: id=1
2019-09-20 01:21:23.544 INFO 13548 --- [nio-8080-exec-1] c.c.d.ehcache.repository.UserRepository : UserRepository#getById: id=1
2019-09-20 01:21:23.559 INFO 13548 --- [e [_default_]-0] c.c.d.ehcache.listener.CacheEventLogger : cache event logger: type=CREATED, key=1, oldValue=null, newValue=User(id=1, name=Alice)
第一行日誌:接口執行到了 UserService 的 getById 方法。
第二行日誌:接口執行到了 UserRepository 的 getById 方法。從而知道此次執行並未走緩存,因爲我們是第一次請求,還沒把數據寫入緩存。
第三行日誌:配置的 Listener 記錄的日誌,Ehcache 添加(CREATED)了緩存,key 爲 1, oldValue 爲 null, newValue 爲 User(id=1, name=Alice)
5.2. 第二次請求接口(與上一次間隔 TTL 以內)
接口正常返回數據,日誌如下:
2019-09-20 01:24:46.441 INFO 13548 --- [io-8080-exec-10] c.c.d.e.service.impl.UserServiceImpl : UserService#getById: id=1
只有一行日誌,表示執行到 UserService 的 getById 方法,並未執行到 UserRepository,說明緩存命中成功。
5.3. 第三次請求(與上一次間隔超過 TTL)
接口正常返回數據,日誌如下:
2019-09-20 01:28:16.156 INFO 13548 --- [nio-8080-exec-6] c.c.d.e.service.impl.UserServiceImpl : UserService#getById: id=1
2019-09-20 01:28:16.161 INFO 13548 --- [nio-8080-exec-6] c.c.d.ehcache.repository.UserRepository : UserRepository#getById: id=1
2019-09-20 01:28:16.167 INFO 13548 --- [e [_default_]-2] c.c.d.ehcache.listener.CacheEventLogger : cache event logger: type=EXPIRED, key=1, oldValue=User(id=1, name=Alice), newValue=null
2019-09-20 01:28:16.169 INFO 13548 --- [e [_default_]-2] c.c.d.ehcache.listener.CacheEventLogger : cache event logger: type=CREATED, key=1, oldValue=null, newValue=User(id=1, name=Alice)
第一行日誌:接口執行到了 UserService 的 getById 方法。
第二行日誌:接口執行到了 UserRepository 的 getById 方法。從而知道此次執行並未走緩存。
第三行日誌:緩存日誌,檢查到原來的緩存過期了,把緩存值更新爲 null。
第四行日誌:新添加了緩存值
值得注意的是,這裏是分了兩步進行,先進行校驗 TTL ,沒過期則直接進行返回緩存數據,過期了則直接置爲 null。然後又進行寫入緩存數據。
所以我猜想,按照正常執行流程,這個日誌應該是有一定的順序錯誤,可能是因爲異步任務造成。第二行和第三行應該進行替換。
正確流程應該爲:
1、先進行校驗緩存值是否存在
2、如果緩存不存在,則直接進行查詢。如果存在,則根據 TTL 校驗緩存是否有效
3、如果有效,則直接返回。如果失效,則將緩存值置爲 null,並進行查詢,重新寫入緩存
知識有限,如有錯誤,歡迎留言指正。