面試官:如何實現(xiàn)多級緩存?
對于高并發(fā)系統(tǒng)來說,有三個重要的機制來保障其高效運行,它們分別是:緩存、限流和熔斷。而緩存是排在最前面也是高并發(fā)系統(tǒng)之所以高效運行的關(guān)鍵手段,那么問題來了:緩存只使用 Redis 就夠了嗎?
1.冗余設(shè)計理念
當然不是,不要把所有雞蛋放到一個籃子里,成熟的系統(tǒng)在關(guān)鍵功能實現(xiàn)時一定會考慮冗余設(shè)計,注意這里的冗余設(shè)計不是貶義詞。
冗余設(shè)計是在系統(tǒng)或設(shè)備完成任務(wù)起關(guān)鍵作用的地方,增加一套以上完成相同功能的功能通道(or 系統(tǒng))、工作元件或部件,以保證當該部分出現(xiàn)故障時,系統(tǒng)或設(shè)備仍能正常工作,以減少系統(tǒng)或者設(shè)備的故障概率,提高系統(tǒng)可靠性。
例如,飛機的設(shè)計,飛機正常運行只需要兩個發(fā)動機,但在每臺飛機的設(shè)計中可能至少會設(shè)計四個發(fā)動機,這就有冗余設(shè)計的典型使用場景,這樣設(shè)計的目的是為了保證極端情況下,如果有一個或兩個發(fā)動機出現(xiàn)故障,不會因為某個發(fā)動機的故障而引起重大的安全事故。
2.多級緩存概述
緩存功能的設(shè)計也是一樣,我們在高并發(fā)系統(tǒng)中通常會使用多級緩存來保證其高效運行,其中的多級緩存就包含以下這些:
- 瀏覽器緩存:它的實現(xiàn)主要依靠 HTTP 協(xié)議中的緩存機制,當瀏覽器第一次請求一個資源時,服務(wù)器會將該資源的相關(guān)緩存規(guī)則(如 Cache-Control、Expires 等)一同返回給客戶端,瀏覽器會根據(jù)這些規(guī)則來判斷是否需要緩存該資源以及該資源的有效期。
- Nginx 緩存:在 Nginx 中配置中開啟緩存功能。
- 分布式緩存:所有系統(tǒng)調(diào)用的中間件都是分布式緩存,如 Redis、MemCached 等。
- 本地緩存:JVM 層面,單系統(tǒng)運行期間在內(nèi)存中產(chǎn)生的緩存,例如 Caffeine、Google Guava 等。
以下是它們的具體使用。
2.1 開啟瀏覽器緩存
在 Java Web應(yīng)用中,實現(xiàn)瀏覽器緩存可以使用 HttpServletResponse 對象來設(shè)置與緩存相關(guān)的響應(yīng)頭,以開啟瀏覽器的緩存功能,它的具體實現(xiàn)分為以下幾步。
① 配置 Cache-Control
Cache-Control 是 HTTP/1.1 中用于控制緩存策略的主要方式。它可以設(shè)置多個指令,如 max-age(定義資源的最大存活時間,單位秒)、no-cache(要求重新驗證)、public(指示可以被任何緩存區(qū)緩存)、private(只能被單個用戶私有緩存存儲)等,設(shè)置如下:
response.setHeader("Cache-Control", "max-age=3600, public"); // 緩存一小時
② 配置 Expires
設(shè)置一個絕對的過期時間,超過這個時間點后瀏覽器將不再使用緩存的內(nèi)容而向服務(wù)器請求新的資源,設(shè)置如下:
response.setDateHeader("Expires", System.currentTimeMillis() + 3600 * 1000); // 緩存一小時
③ 配置 ETag
ETag(實體標簽)一種驗證機制,它為每個版本的資源生成一個唯一標識符。當客戶端發(fā)起請求時,會攜帶上先前接收到的 ETag,服務(wù)器根據(jù) ETag 判斷資源是否已更新,若未更新則返回 304 Not Modified 狀態(tài)碼,通知瀏覽器繼續(xù)使用本地緩存,設(shè)置如下:
String etag = generateETagForContent(); // 根據(jù)內(nèi)容生成ETag
response.setHeader("ETag", etag);
④ 配置 Last-Modified
指定資源最后修改的時間戳,瀏覽器下次請求時會帶上 If-Modified-Since 頭,服務(wù)器對比時間戳決定是否返回新內(nèi)容或發(fā)送 304 狀態(tài)碼,設(shè)置如下:
long lastModifiedDate = getLastModifiedDate();
response.setDateHeader("Last-Modified", lastModifiedDate);
整體配置
在 Spring Web 框架中,可以通過 HttpServletResponse 對象來設(shè)置這些頭信息。例如,在過濾器中設(shè)置響應(yīng)頭以啟用緩存:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 設(shè)置緩存策略
httpResponse.setHeader("Cache-Control", "max-age=3600");
// 其他響應(yīng)頭設(shè)置...
chain.doFilter(request, response);
}
以上就是在 Java Web 應(yīng)用程序中利用 HTTP 協(xié)議特性控制瀏覽器緩存的基本方法。
2.2 開啟 Nginx 緩存
Nginx 中開啟緩存的配置總共有以下 5 步。
① 定義緩存配置
在 Nginx 配置中定義一個緩存路徑和配置,通過 proxy_cache_path 指令完成,例如,以下配置:
proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
其中:
- /path/to/cache:這是緩存文件的存放路徑。
- levels=1:2:定義緩存目錄的層級結(jié)構(gòu)。
- keys_zone=my_cache:10m:定義一個名為 my_cache 的共享內(nèi)存區(qū)域,大小為 10MB。
- max_size=10g:設(shè)置緩存的最大大小為 10GB。
- inactive=60m:如果在 60 分鐘內(nèi)沒有被訪問,緩存將被清理。
- use_temp_path=off:避免在文件系統(tǒng)中進行不必要的數(shù)據(jù)拷貝。
② 啟用緩存
在 server 或 location 塊中,使用 proxy_cache 指令來啟用緩存,并指定要使用的 keys zone,例如,以下配置:
server {
...
location / {
proxy_cache my_cache;
...
}
}
③ 設(shè)置緩存有效期
使用 proxy_cache_valid 指令來設(shè)置哪些響應(yīng)碼的緩存時間,例如,以下配置:
location / {
proxy_cache my_cache;
proxy_cache_valid 200 304 12h;
proxy_cache_valid any 1m;
...
}
④ 配置反向代理
確保你已經(jīng)配置了反向代理,以便 Nginx 可以將請求轉(zhuǎn)發(fā)到后端服務(wù)器。例如,以下配置:
location / {
proxy_pass http://backend_server;
...
}
⑤ 重新加載配置
保存并關(guān)閉 Nginx 配置文件后,使用 nginx -s reload 命令重新加載配置,使更改生效。
2.3 使用分布式緩存
在 Spring Boot 項目中使用注解的方式來操作分布式緩存 Redis 的實現(xiàn)步驟如下。
① 添加依賴
在你的 pom.xml 文件中添加 Spring Boot 的 Redis 依賴,如下所示:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
② 配置 Redis 連接信息
在 application.properties 或 application.yml 文件中配置 Redis 的相關(guān)信息,如下所示。
# application.properties
spring.redis.host=localhost
spring.redis.port=6379
③ 啟動緩存
在 Spring Boot 主類或者配置類上添加 @EnableCaching 注解來啟用緩存。
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
④ 使用緩存
在服務(wù)類或方法上使用 @Cacheable,@CacheEvict,@CachePut 等注解來定義緩存行為。
例如,使用 @Cacheable 注解來緩存方法的返回值:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable("users")
public User findUserById(Long id) {
// 模擬從數(shù)據(jù)庫中查詢用戶
return new User(id, "Alice");
}
}
也可以使用 @CacheEvict 注解來刪除緩存:
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
// 模擬從數(shù)據(jù)庫中刪除用戶
}
}
在這個例子中,deleteUser 方法會刪除 "users" 緩存中 key 為 id 的緩存項。
可以使用 @CachePut 注解來更新緩存:
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// 模擬更新數(shù)據(jù)庫中的用戶信息
return user;
}
}
在這個例子中,updateUser 方法會更新 "users" 緩存中 key 為 user.id 的緩存項,緩存的值是方法的返回值。
2.4 使用本地緩存
以 Caffeine 本地緩存的使用為例,它在 Spring Boot 項目中的使用如下。
① 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
② 配置 Caffeine 緩存
在 application.properties 或 application.yml 文件中配置 Caffeine 緩存的相關(guān)參數(shù)。例如:
# application.properties
spring.cache.type=caffeine
spring.cache.caffeine.spec=initialCapacity=100,maximumSize=1000,expireAfterWrite=10s
這里 spring.cache.caffeine.spec 是一個 Caffeine 規(guī)范字符串,用于設(shè)置初始容量、最大容量和寫入后過期時間等緩存策略,其中:
- initialCapacity:初始容器容量。
- maximumSize:最大容量。
- expireAfterWrite:寫入緩存后 N 長時間后過期。
③ 自定義 Caffeine 配置類(可選步驟)
如果需要更復(fù)雜的配置,可以創(chuàng)建一個 Caffeine CacheManager 的配置類:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CaffeineCacheConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager() {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.SECONDS) // 10 秒后過期
.recordStats(); // 記錄緩存統(tǒng)計信息
return new CaffeineCacheManager("default", caffeine::build);
}
@Override
public CacheResolver cacheResolver() {
// 自定義緩存解析器(如果需要)
// ...
return super.cacheResolver();
}
}
④ 開啟緩存
若要利用 Spring Cache 抽象層,以便通過注解的方式更方便地管理緩存,需要在啟動類上添加 @EnableCaching 注解,如下所示:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
⑤ 使用注解進行緩存操作
在業(yè)務(wù)邏輯類中使用 @Cacheable、@CacheEvict 等注解實現(xiàn)數(shù)據(jù)的緩存讀取和更新,和上面分布式緩存的使用相同,具體示例如下:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(value = "users", key = "#id") // 假設(shè)我們有一個名為"users"的緩存區(qū)域
public User getUserById(Long id) {
// 這里是真實的數(shù)據(jù)庫查詢或其他耗時操作
return userRepository.findById(id).orElse(null);
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
}
課后思考
除了以上的緩存之外,還有哪些緩存可以加速程序的執(zhí)行效率呢?