引言
在當今網際網路領域,尤其在大型電商平臺如淘寶這樣的複雜分散式系統中,資料的高效管理和快速訪問至關重要。面對數以千萬計的商品、交易記錄以及其他各類業務資料,如何在MySQL等傳統關係型資料庫之外,藉助記憶體資料庫Redis的力量,對部分高頻訪問資料進行高效的快取處理,是提升整個系統效能的關鍵一環。
比如淘寶,京東,拼多多等電商系統每日處理的訂單量級龐大,其資料庫中儲存的商品、使用者資訊及相關交易資料可達數千萬條。為了降低資料庫查詢的壓力,加速資料讀取,Redis常被用於搭建二級快取系統,以容納部分最為活躍的“熱點資料”。然而,在資源有限的情況下,如何確保僅有的20萬條快取資料精準匹配到系統中的熱點資料,避免頻繁的冷資料替換熱資料導致的快取失效,這就涉及到了一套精密的資料管理策略和快取淘汰機制的設計。
本文將圍繞這一實戰場景展開討論:在MySQL擁有2000萬條資料的前提下,如何確保Redis僅快取的20萬條資料全都是系統中的熱點資料,從而最大程度上發揮快取的優勢,提高系統的響應速度和併發能力,進而提升使用者的購物體驗和服務質量。透過對Redis內部機制的深入理解以及對業務場景的精細分析,我們將揭示一套綜合運用各種技術手段來確保Redis中熱點資料準確有效的管理方案。
技術背景
在探討如何確保Redis中儲存的20萬資料均為熱點資料之前,首先需要明確MySQL與Redis在實際業務環境中的互補關係以及Redis自身的記憶體管理和資料淘汰機制。
MySQL與Redis的關係及應用場景
MySQL作為一種成熟的關係型資料庫管理系統,適用於儲存大量持久化且具有複雜關係的資料,其強大的事務處理能力和安全性保障了資料的一致性和完整性。但在大規模併發環境下,尤其是對那些讀多寫少、訪問頻次極高的熱點資料,直接從MySQL中讀取可能會成為系統效能瓶頸。
Redis則是一種高效能的記憶體鍵值資料庫,以其極快的速度和靈活的資料結構著稱。在淘寶這類大型電商平臺中,Redis主要用於快取頻繁訪問的資料,例如熱門商品資訊、使用者購物車、會話狀態等,以此減輕主資料庫的壓力,提高響應速度,增強系統的可擴充套件性和容錯性。
對於Redis高效能原理,請參考:京東二面:Redis為什麼快?我說Redis是純記憶體操作的,然後他對我笑了笑。
對於Redis的使用的業務場景,請參考:美團一面:專案中使用過Redis嗎?我說用Redis做快取。他對我哦了一聲
Redis記憶體管理和資料淘汰機制簡介
Redis的所有資料都儲存在記憶體中,這意味著它的容量相較於磁碟儲存更為有限。為了解決記憶體容量不足的問題,Redis提供了多種資料淘汰策略。其中,與保證熱點資料密切相關的是LFU(Least Frequently Used)策略,它能夠根據資料物件的訪問頻次,將訪問次數最少(即最不常用)的資料淘汰出記憶體,以便為新的資料騰出空間。
對於Redis高效能的一方面原因就是Redis高效的管理記憶體,具體請參考:京東二面:Redis為什麼快?我說Redis是純記憶體操作的,然後他對我笑了笑。
此外,Redis允許使用者根據自身需求選擇不同的淘汰策略,例如“volatile-lfu”只針對設定了過期時間的key採用LFU演算法,“allkeys-lfu”則對所有key都執行LFU淘汰規則。
熱點資料定義及其識別方法
熱點資料是指在一定時間內訪問頻率極高、對系統效能影響重大的資料集。在電商平臺中,這可能表現為熱銷商品詳情、活動頁面資訊、使用者高頻查詢的搜尋關鍵詞等。識別熱點資料主要依賴於對業務日誌、請求統計和系統效能監控工具的分析,透過收集和分析使用者行為資料,發現並量化哪些資料是系統訪問的熱點,以便有針對性地將它們快取至Redis中。
實現方案
在實際應用中,確保Redis中儲存的資料為熱點資料,我們可以從以下幾個方案考慮實現。
LFU淘汰策略
Redis中的LFU(Least Frequently Used)淘汰策略是一種基於訪問頻率的記憶體管理機制。當Redis例項的記憶體使用量達到預先設定的最大記憶體限制(由maxmemory
配置項指定)時,LFU策略會根據資料物件的訪問頻次,將訪問次數最少(即最不常用)的資料淘汰出記憶體,以便為新的資料騰出空間。
LFU演算法的核心思想是透過跟蹤每個鍵的訪問頻率來決定哪些鍵應當優先被淘汰。具體實現上,Redis並非實時精確地計算每個鍵的訪問頻率,而是採用了近似的LFU方法,它為每個鍵維護了一個訪問計數器(counter)。每當某個鍵被訪問時,它的計數器就會遞增。隨著時間推移,Redis會依據這些計數器的值來決定淘汰哪些鍵。
在Redis 4.0及其後續版本中,LFU策略可以透過設定maxmemory-policy
配置項為allkeys-lfu
或volatile-lfu
來啟用。其中:
allkeys-lfu
:適用於所有鍵,無論它們是否有過期時間,都會基於訪問頻率淘汰鍵。volatile-lfu
:僅針對設定了過期時間(TTL)的鍵,按照訪問頻率淘汰鍵。
Redis實現了自己的LFU演算法變體,它使用了一個基於訪問計數和老化時間的組合策略來更好地適應實際情況。這意味著不僅考慮訪問次數,還會考慮到鍵的訪問頻率隨時間的變化,防止長期未訪問但曾經很熱門的鍵佔據大量記憶體空間而不被淘汰。在實現上,Redis使用了一種稱為“頻率跳錶(frequency sketch)”的資料結構來儲存鍵的訪問頻率,允許快速查詢和更新計數器。為了避免長期未訪問但計數器較高的鍵永久保留,Redis會在一段時間後降低鍵的訪問計數,模擬訪問頻率隨時間衰減的效果。
在Redis中使用LFU淘汰策略,在配置檔案redis.conf
中找到maxmemory-policy
選項,將其設定為LFU相關策略之一:
maxmemory-policy allkeys-lfu # 對所有鍵啟用LFU淘汰策略
# 或者
maxmemory-policy volatile-lfu # 對有過期時間的鍵啟用LFU淘汰策略
確保你也設定了Redis的最大記憶體使用量(maxmemory
),只有當記憶體到達這個上限時,才會觸發淘汰策略:
maxmemory <size_in_bytes> # 指定Redis可以使用的最大記憶體大小
LFU策略旨在儘可能讓那些近期最不活躍的資料優先被淘汰,以此保持快取中的資料相對活躍度更高,提高快取命中率,從而提升系統的整體效能。(這也是我們面試中需要回答出來的答案)
LRU淘汰策略
Redis中的LRU(Least Recently Used)淘汰策略是一種用於在記憶體不足時自動刪除最近最少使用的資料以回收記憶體空間的方法。儘管Redis沒有完全精確地實現LRU演算法(因為這在O(1)時間內實現成本較高),但Redis確實提供了一種近似LRU的行為。
當我們配置了最大記憶體限制,如果記憶體超出這個限制時,Redis會選擇性地刪除一些鍵值對來騰出空間。Redis提供了幾種不同的淘汰策略,其中之一就是volatile-lru
和allkeys-lru
,這兩種都試圖模擬LRU行為。
- volatile-lru:僅針對設定了過期時間(TTL)的鍵,按照最近最少使用的原則來刪除鍵。
- allkeys-lru:不論鍵是否設定過期時間,都會根據最近最少使用的原則來刪除鍵。
Redis實現LRU的方式並不是真正意義上的雙向連結串列加引用計數這樣的完整LRU結構,因為每個鍵值對的插入、刪除和訪問都需要維持這樣的資料結構會帶來額外的開銷。所以Redis實現LRU會採取以下方式進行:
- Redis內部為每個鍵值對維護了一個“空轉時間”(idle time)的欄位,它是在Redis例項啟動後最後一次被訪問或修改的時間戳。
- 當記憶體達到閾值並觸發淘汰時,Redis不會遍歷整個鍵空間找出絕對意義上的最近最少使用的鍵,而是隨機抽取一批鍵檢查它們的空轉時間,然後刪除這批鍵中最久未被訪問的那個。
Redis在大多數情況下能較好地模擬LRU效果,有助於保持活躍資料在記憶體中,減少因頻繁換入換出帶來的效能損失。
記憶體淘汰策略通常是在Redis伺服器端的配置檔案(如redis.conf
)中設定,而不是在應用中配置。你需要在Redis伺服器端的配置中設定maxmemory-policy
引數為allkeys-lru
。(同LFU策略)
使用Redis的LRU淘汰策略實現熱點資料的方式,簡單易行,能較好地應對大部分情況下的熱點資料問題。但是若訪問模式複雜或資料訪問分佈不均勻,單純的LRU策略可能不夠精準,不能確保絕對的熱點資料留存。
結合訪問頻率設定過期時間
在實際應用中,除了依賴Redis的淘汰策略外,還可以結合業務邏輯,根據資料的訪問頻率動態設定Key的過期時間。例如,當某個Key被頻繁訪問時,延長其在Redis中的有效期,反之則縮短。
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void updateKeyTTL(String key, int ttlInSeconds) {
redisTemplate.expire(key, ttlInSeconds, TimeUnit.SECONDS);
}
// 示例呼叫,當檢測到某個資料訪問增多時,增加其快取過期時間
public void markAsHotSpot(String key) {
updateKeyTTL(key, 3600); // 將熱點資料快取時間延長至1小時
}
這種方式靈活性強,可根據實際訪問情況動態調整快取策略。但是需要在應用程式中進行較多定製開發,以捕捉並響應資料訪問的變化。
基於時間視窗的快取淘汰策略
在給定的時間視窗(如過去1小時、一天等)內,對每個資料項的訪問情況進行實時跟蹤和記錄,可以使用計數器或其他資料結構統計每條資料的訪問次數。到達時間視窗邊界時,計算每個資料項在該視窗內的訪問頻率,這可以是絕對訪問次數、相對訪問速率或者其他反映訪問熱度的指標。根據預先設定的閾值,將訪問次數超過閾值的資料項加入Redis快取,或者將其快取時間延長以確保其能在快取中停留更久。而對於訪問次數低於閾值的資料項,要麼從快取中移除,要麼縮短其快取有效期,使其更容易被後續淘汰策略處理。
@Service
public class TimeWindowCacheEvictionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final Map<String, AtomicInteger> accessCounts = new ConcurrentHashMap<>();
// 時間視窗長度(例如,1小時)
private static final long TIME_WINDOW_MILLIS = TimeUnit.HOURS.toMillis(1);
@Scheduled(fixedRate = TIME_WINDOW_MILLIS)
public void evictBasedOnFrequency() {
accessCounts.entrySet().forEach(entry -> {
int accessCount = entry.getValue().get();
if (accessCount > THRESHOLD) { // 假設THRESHOLD是訪問次數閾值
// 將資料存入或更新到Redis快取,並設定較長的過期時間
redisTemplate.opsForValue().set(entry.getKey(), getDataFromDB(entry.getKey()), CACHE_EXPIRATION_TIME, TimeUnit.MINUTES);
} else if (redisTemplate.hasKey(entry.getKey())) {
// 訪問次數低,從快取中移除或縮短過期時間
redisTemplate.delete(entry.getKey());
}
});
// 清零訪問計數器,準備下一個時間視窗
accessCounts.clear();
}
public void trackDataAccess(String dataId) {
accessCounts.computeIfAbsent(dataId, k -> new AtomicInteger()).incrementAndGet();
}
}
關於@Scheduled是Springboot中實現定時任務的一種方式,對於其他幾種方式,請參考:玩轉SpringBoot:SpringBoot的幾種定時任務實現方式
透過這種方法,系統能夠基於實際訪問情況動態調整快取內容,確保Redis快取中存放的總是具有一定熱度的資料。當然,這種方法需要與實際業務場景緊密結合,並結合其他快取策略共同作用,以實現最優效果。同時,需要注意此種策略可能帶來的額外計算和儲存成本。
手動快取控制
針對已識別的熱點資料,可以透過監聽資料庫變更或業務邏輯觸發器主動將資料更新到Redis中。例如,當商品銷量劇增變為熱點商品時,立即更新Redis快取。
這種方式可以確保熱點資料及時更新,提高了快取命中率。
利用資料結構最佳化
使用Sorted Set等資料結構可以進一步精細化熱點資料管理。例如,記錄每個商品最近的訪問的活躍時間,並據此決定快取哪些商品資料。
// 商品訪問活躍時更新其在Redis中的排序
String goodsActivityKey = "goods_activity";
redisTemplate.opsForZSet().add(goodsActivityKey, sku, System.currentTimeMillis());
// 定時清除較早的非熱點商品資料
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3點清理前一天的資料
public void cleanInactiveUsers() {
long yesterdayTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
redisTemplate.opsForZSet().removeRangeByScore(goodsActivityKey, 0, yesterdayTimestamp);
}
這種方式能夠充分利用Redis內建的資料結構優勢,實現複雜的資料淘汰邏輯。
實際業務中實踐方案
在例如淘寶這樣龐大的電商生態系統中,面對MySQL中海量的業務資料和Redis有限的記憶體空間,我們採用了多元化的策略以確保快取的20萬資料是真正的熱點資料。
LFU策略的運用
自Redis 4.0起,我們可以透過配置Redis淘汰策略為近似的LFU(volatile-lfu
或 allkeys-lfu
),使得Redis能夠自動根據資料訪問頻率進行淘汰決策。LFU策略基於資料的訪問次數,使得訪問越頻繁的資料越不容易被淘汰,從而更好地保持了熱點資料在快取中的存在。
訪問頻率動態調整
除了依賴Redis內建的LFU淘汰策略,我們還可以實現應用層面的訪問頻率追蹤和響應式快取管理。例如,每當商品被使用者訪問時,系統會更新該商品在Redis中的訪問次數,同時根據訪問頻率動態調整快取過期時間,確保訪問頻率高的商品在快取中的生存期得到延長。
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
public void updateProductViewCount(String productId) {
// 更新產品訪問次數
redisTemplate.opsForValue().increment("product:view_count:" + productId);
// 根據訪問次數調整快取過期時間
Long viewCount = redisTemplate.opsForValue().get("product:view_count:" + productId);
if (viewCount > THRESHOLD_VIEW_COUNT) {
redisTemplate.expire("product:info:" + productId, LONGER_CACHE_EXPIRATION, TimeUnit.MINUTES);
}
}
}
資料結構最佳化
我們還可以利用Redis豐富的資料結構,如有序集合(Sorted Sets)和雜湊(Hashes),來實現商品熱度排行、使用者行為分析等功能。例如,透過Sorted Set儲存商品的瀏覽量,自動按照瀏覽量高低進行排序,並淘汰訪問量低的商品快取。
// 更新商品瀏覽量並同步到Redis有序集合
public void updateProductRanking(String productId, long newViewCount) {
redisTemplate.opsForZSet().add("product_ranking", productId, newViewCount);
// 自動淘汰瀏覽量低的商品快取
redisTemplate.opsForZSet().removeRange("product_ranking", 0, -TOP_RANKED_PRODUCT_COUNT - 1);
}
總結
本文詳細闡述了在電商平臺例如淘寶及其他類似場景下,如何結合LFU策略與訪問頻率調整,最佳化Redis中20萬熱點資料的管理。透過配置Redis近似的LFU淘汰策略,結合應用層面對訪問頻率的實時追蹤與響應式調整,以及利用多樣化的Redis資料結構如有序集合和雜湊表,成功實現了熱點資料的精確快取與淘汰。
透過電商平臺的一些實際業務實踐證明,這種綜合策略可以有效提升快取命中率,降低資料庫訪問壓力,確保快取資源始終服務於訪問最頻繁的資料。未來隨著資料探勘與分析技術的進步,以及Redis或其他記憶體資料庫功能的擴充,預計將進一步細化和完善熱點資料的識別與管理機制。例如,探索更具前瞻性的預測性快取策略,或是結合機器學習模型對使用者行為進行深度分析,以更精準地預判和儲存未來的熱點資料。
本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等