SpringBoot集成Ehcache實現本地緩存

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,並進行查詢,重新寫入緩存

知識有限,如有錯誤,歡迎留言指正。

參考

http://www.ehcache.org
ehcache github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章