1. 為什么使用ConcurrentHashMap?
在Java中,ConcurrentHashMap
是一個線程安全且高效的哈希表實現,廣泛用于高并發場景。將其用作一級緩存的原因主要包括以下幾點:
1.1. 線程安全性
ConcurrentHashMap
是線程安全的,支持多個線程同時進行讀寫操作而不會出現數據不一致或競態條件問題。這使其非常適合用作多線程環境下的緩存,因為緩存通常會被多個線程并發訪問。- 傳統的
Hashtable
也是線程安全的,但它使用全局鎖,性能較低。而ConcurrentHashMap
使用分段鎖(Segment)機制,將鎖粒度降低,從而在高并發場景下性能更高。
1.2. 高效的并發訪問
ConcurrentHashMap
在高并發場景下表現出色,因為它通過分段鎖(Segment)和無鎖操作(如讀操作)最大限度地減少了鎖競爭。- 它支持高吞吐量和低延遲,非常適合緩存這種需要快速讀寫的場景。
1.3. 內存使用效率
ConcurrentHashMap
在內存使用上非常高效,適合存儲大量緩存數據。- 它通過動態調整容量和負載因子,確保內存的高效利用。
1.4. 擴展性
ConcurrentHashMap
支持動態擴容,能夠根據實際需求自動調整內部數組的大小,從而適應數據量的動態變化。- 這種特性使得它非常適合用作緩存,因為緩存的大小可能會隨著業務需求的變化而動態調整。
1.5. 與緩存策略結合
- 一級緩存通常用于快速訪問最近或頻繁訪問的數據,而
ConcurrentHashMap
的高效性和線程安全性使其成為實現這一目標的理想選擇。 - 它可以與其他緩存策略(如基于時間的過期、基于容量的淘汰等)結合使用,進一步提升緩存的性能和靈活性。
1.6. 集成方便
ConcurrentHashMap
是 Java 標準庫的一部分,使用簡單且集成方便,無需引入額外的依賴。- 它可以與各種緩存框架(如 Ehcache、Caffeine)或自定義緩存實現無縫結合。
1.7. 補充介紹,多線程環境下使用哈希表
HashMap
:線程不安全,不建議多線程環境使用ConcurrentHashMap
:線程安全,但是使用的是分段鎖(Segment)機制Hashtable
:線程安全,但使用的是全局鎖,對所有的操作都加鎖,對性能有很大的影響,會導致嚴重的效率問題HashMap
實現原理:
- put一個對象的時候,先根據對象的hashcode和數組的長度進行求余,通過余數來確定對象放在數組中的哪一個下表
- 每個hash桶中存放的是具體對象的鏈表
- 初始化的數組長度為16,中間還可能發生擴容,擴容的時候會對當前的表中的元素hash到新的哈希表中
- 鏈表的長度大于6的時候,同時數組的長度大于64時,鏈表會轉化為紅黑樹
Hashtable
實現原理:
- 對數組進行全局加鎖,但是實際操作的時候只會針對一個哈希桶,因此會對系統性能有很大影響,多線程環境不建議使用
ConcurrentHashMap
實現原理:
- 對于所操作的特定哈希桶實施加鎖機制,而其余哈希桶則保持解鎖狀態,這意味著其他未鎖定的哈希桶中的數據可以并行地執行讀寫操作。
- 理論上講,系統支持的并發讀寫線程數量等同于哈希桶的數量,即每個哈希桶都可以獨立地被一個線程訪問而不影響其它桶的操作。
- 擴容優化策略包括:
a. 當檢測到存儲空間不足時,將底層數組容量翻倍。但值得注意的是,在此過程中,并非一次性遷移所有元素至新映射結構中,而是僅遷移當前正在訪問的那個索引位置上的元素。
b. 此種方式導致在一段時間內存在兩個版本的數據結構共存。
c. 在執行查詢操作時,需同時對這兩個版本的數據結構進行搜索以確保結果準確性。
d. 同樣地,當需要刪除條目時,也需要在這兩份數據結構上分別實施刪除動作。
e. 新增數據項時,則僅向最新擴展后的映射結構中添加。
f. 該設計采用了一種典型的空間換時間策略,通過犧牲一定的內存開銷來換取更高的并發性能,這正是ConcurrentHashMap
能夠在高并發場景下表現優異的原因之一。更具其底層源碼可以發現,在執行put
操作的時候會進行加鎖,使用CAS(Compare-And-Swap)
操作和synchronized
鎖來保證線程安全,但是get
操作不會加鎖,它通過volatile
語義來保證可見性,能夠讀取到最新的數據,它不會阻塞其他線程的并發訪問,所以ConcurrentHashMap
的設計是在線程安全和性能之間找到平衡點,get 操作的無鎖化設計是其高性能的關鍵之一
g. 每次調用get
或put
方法時,都會觸發一個過程:將舊映射中對應索引下的元素逐步遷移到新的映射中;只有當遷移完成后,才會從舊映射中移除這些元素。每次調用get、put方法的時候把舊的map中對應的下標中的元素搬運到新的map中,搬運完之后才會刪除
1.8. 總結
ConcurrentHashMap
作為一級緩存的主要原因是其線程安全性、高效的并發訪問能力、內存使用效率以及擴展性。這些特性使其非常適合在高并發場景下快速讀寫數據,從而提高應用性能。
2. 為什么選擇Redis作為二級緩存?
2.1. 高可用性和持久化
- Redis 提供了多種持久化機制(如 RDB 和 AOF),能夠在服務器重啟后恢復數據,避免緩存數據丟失。
ConcurrentHashMap
是內存中的數據結構,數據僅存在于 JVM 內存中,不具備持久化能力。
2.2. 豐富的數據結構
- Redis 提供了多種數據結構(如字符串、列表、哈希、集合、有序集合等),能夠更靈活地支持復雜的緩存需求。
ConcurrentHashMap
僅支持鍵值對的簡單存儲,功能相對單一。
2.3. 分布式支持
- Redis 是一個分布式數據庫,支持多節點集群,能夠滿足高并發、大規模數據場景下的緩存需求。
ConcurrentHashMap
是單機內存數據結構,無法直接支持分布式場景。
2.4. 4. 高性能
- Redis 的單線程模型通過事件驅動和非阻塞 IO 實現了高性能的讀寫操作,特別適合高并發場景。
ConcurrentHashMap
是基于 CAS 和分段鎖實現的,雖然性能很高,但在高并發場景下可能會因鎖競爭導致性能下降。
2.5. 緩存穿透、擊穿、失效問題的解決方案
- Redis 提供了多種機制來解決緩存穿透(如布隆過濾器)、緩存擊穿(如互斥鎖)和緩存失效(如預熱)等問題。
ConcurrentHashMap
難以直接解決這些問題,需要額外的邏輯實現。
2.6. 數據共享和一致性
- Redis 可以作為分布式緩存,支持多個服務實例共享緩存數據,保證數據一致性。
ConcurrentHashMap
是單機的,無法實現跨服務實例的數據共享。
2.7. 緩存分層
- Redis 通常作為二級緩存,而
ConcurrentHashMap
作為一級緩存(本地內存緩存)。這種分層設計能夠優化性能,同時降低內存占用。 - 本地緩存(一級緩存)負責快速訪問,Redis(二級緩存)負責數據持久化和跨服務共享。
2.8. 支持復雜業務邏輯
- Redis 提供了豐富的命令和事務支持,能夠直接在緩存層處理一些復雜的業務邏輯。
ConcurrentHashMap
僅支持簡單的鍵值操作,無法處理復雜邏輯。
2.9. 總結
Redis 作為二級緩存的優勢在于其高性能、分布式能力、持久化支持和豐富的功能,能夠彌補 ConcurrentHashMap
的不足。通過將 ConcurrentHashMap
作為一級緩存(本地內存緩存),Redis 作為二級緩存(分布式緩存),可以構建一個高效、可靠、可擴展的緩存系統。
3. 使用ConcurrentHashMap
和redis實現二級緩存的優點和缺點
3.1. 優點
- 性能分層優化
- ConcurrentHashMap(本地緩存):內存級訪問速度(納秒級),減少高頻熱點數據的重復遠程請求。
- Redis(遠程緩存):提供跨進程/節點的數據共享,支持高并發讀取,避免直接穿透到數據庫。
- 降低數據庫壓力
- 兩級緩存組合可攔截大多數查詢請求,尤其在突發流量下,本地緩存直接響應請求,減少對Redis和數據庫的負載。
- 適應分布式與單機場景
- 本地緩存:適用于單機高頻熱點數據(如配置信息)。
- Redis:解決分布式環境下多節點數據一致性問題。
- 資源利用優化
- 本地緩存節省網絡開銷,Redis支持豐富的數據結構(如Hash、SortedSet)和持久化能力。
3.2. 缺點
- 數據一致性挑戰
- 同步延遲:本地緩存更新可能滯后于Redis,尤其是在分布式場景下(如某節點未及時收到失效通知)。
- 更新策略復雜性:需實現雙重失效機制(如Redis Pub/Sub通知本地緩存失效),增加代碼復雜度。
- 資源占用風險
- 本地內存壓力:ConcurrentHashMap緩存過多數據可能導致JVM內存溢出或頻繁GC。
- Redis運維成本:需監控內存、持久化策略,集群部署增加運維復雜度。
- 緩存異常場景放大
- 緩存穿透:本地和Redis均未命中時,請求可能直接擊穿到數據庫。
- 雪崩風險:兩級緩存同時失效可能導致數據庫瞬時壓力激增。
- 設計復雜度高
- 需實現緩存逐級加載(如本地→Redis→DB)、鎖競爭控制(如本地緩存未命中時,避免多個線程重復加載數據)。
3.3. 適用場景建議
- 推薦使用:讀多寫少的高頻數據(如商品詳情、配置信息),且對一致性要求不苛刻(允許短暫過期)。
- 避免使用:寫多讀少或強一致性場景(如庫存扣減),本地緩存頻繁失效會抵消性能優勢。
3.4. 優化思路
- 一致性保障
- 通過Redis的Pub/Sub或定時輪詢,主動失效本地緩存。
- 對本地緩存設置較短TTL,結合寫后更新策略(Write-Through)。
- 異常防護
- 本地緩存使用軟引用(SoftReference)防止內存溢出。
- Redis層增加分布式鎖或熔斷機制,避免緩存擊穿。
- 監控與治理
- 監控本地緩存命中率、Redis內存使用率。
- 使用Guava Cache或Caffeine替代ConcurrentHashMap,支持容量限制、權重過期等策略。
通過合理設計,ConcurrentHashMap+Redis二級緩存可顯著提升系統性能,但需在一致性、復雜度、資源消耗之間謹慎權衡。
4. Java Spring項目中的使用
4.1. ConcurrentHashMap
一級緩存
package com.project.demo.Admin.cache;import com.project.demo.Admin.model.constant.CacheConstant;
import com.project.demo.Admin.model.response.GetTrendResponse;
import lombok.extern.slf4j.Slf4j;import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;/*** @className: LocalCache* @author: 顧漂亮* @date: 2025/7/22 11:33*/
@Slf4j
//本地緩存 -- 一級緩存
public class LocalCache {// 提供線程安全的操作,適合多線程環境下的緩存訪問,支持高效的并發讀寫操作private static final ConcurrentHashMap<String, CacheItem> cache = new ConcurrentHashMap<>();// 使用ScheduledExecutorService實現定時清理過期數據,創建了一個單線程的調度執行器 (newScheduledThreadPool(1))private static final ScheduledExecutorService cleaner = Executors.newScheduledThreadPool(1);// 定時任務,每一分鐘清理一次static {cleaner.scheduleAtFixedRate(LocalCache::cleanExpiredCache,0, //等待幾分鐘后開始,此處0代表立即開始CacheConstant.CLEANUP_INTERVAL_MINUTES, //每隔指定分鐘進行一次TimeUnit.MINUTES); // 時間單位,此處以分鐘為單位log.info("清除ConcurrentHashMap中的緩存");}// 緩存項包裝類,記錄存儲時間private static class CacheItem{final GetTrendResponse value; //存儲的數據類型final LocalDateTime storedTime; //開始存儲的時間//初始化數據CacheItem(GetTrendResponse value) {this.value = value;this.storedTime = LocalDateTime.now(); //獲取當前系統的時間戳}}/*** 獲取緩存* @param key 鍵* @return 值*/public static GetTrendResponse get(String key) {CacheItem item = cache.get(key);if (item != null && !isExpired(item)){return item.value;}return null;}/*** 添加緩存* @param key 鍵* @param value 值*/public static void put(String key, GetTrendResponse value) {cache.put(key, new CacheItem(value));}/*** 清理過期緩存*/private static void cleanExpiredCache() {log.info("開始清理ConcurrentHashMap中過期緩存");int initialSize = cache.size(); // 初始緩存大小cache.entrySet().removeIf(entry -> isExpired(entry.getValue()));int finalSize = cache.size(); // 清理之后緩存大小log.info("清理了{}個緩存項,剩余{}個緩存項", initialSize - finalSize, finalSize);}/*** 判斷緩存是否過期* @param item 鍵* @return true:過期,false:未過期*/private static boolean isExpired(CacheItem item) {//1. 計算從存儲時間到現在的時間差Duration duration = Duration.between(item.storedTime, LocalDateTime.now());//2. 檢查是否超過了緩存有效期return duration.toMinutes() > CacheConstant.CACHE_EXPIRY_MINUTES;}/*** 關閉定時任務,調用 cleaner.shutdown() 來關閉 ScheduledExecutorService,避免內存泄露和資源浪費*/public static void shutdown() {cleaner.shutdown();}
}
4.2. 使用Redis結合Spring Cache進行二級緩存
package com.project.demo.common.config.cache;import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;/** @className: RedisConfig* @author: 顧漂亮* @date: 2025/7/29 16:49*//*** Redis緩存配置類* 用于配置Spring Cache與Redis的整合,定義緩存管理器及序列化方式*/
@EnableCaching // 啟動緩存功能
@Configuration
public class RedisConfig {/*** 創建緩存管理器* @param factory Redis連接工廠* @return 緩存管理器*/@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory factory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() //獲取Redis緩存的默認配置作為基礎配置.serializeKeysWith(RedisSerializationContext //配置緩存鍵(key)的序列化方式,Key: "users::1".SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext //配置緩存值(value)的序列化方式,Value: {"id":1,"name":"張三"}.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));return RedisCacheManager.builder(factory) //構建Redis緩存管理器.cacheDefaults(config).build();}
}