在并发环境下如何优雅使用 Map 的完整案例(如缓存穿透、线程隔离)
本章节提供一个并发环境下优雅使用 Map 的完整实战案例,涵盖以下内容:
- 线程安全的缓存设计(防止缓存穿透)
- 线程隔离(ThreadLocal + Map)
- 结合
ConcurrentHashMap实现高效读写 - 典型代码示例与设计思路详解
1. 背景
- 在高并发场景,使用普通
HashMap会引发线程安全问题,导致数据混乱或程序崩溃。 ConcurrentHashMap提供了高效的线程安全读写能力,是并发环境首选 Map 实现。- 但是,缓存穿透问题和线程隔离问题需要额外设计。
2. 缓存穿透 & 解决方案
缓存穿透
- 说明:查询一个数据库不存在的 key,缓存没命中就去数据库查,数据库也没有,导致缓存和数据库都没有数据。
- 在高并发时,如果没做防护,恶意请求或异常请求会持续打击数据库。
解决方案
- 缓存空结果:对于不存在的 key,也缓存一个空对象(或特殊标记),避免数据库重复访问。
- 使用带过期时间的缓存,定期清理空缓存。
3. 线程隔离
- 说明:某些场景需要每个线程拥有独立的 Map,比如用户上下文、请求信息存储。
- 方案:
ThreadLocal<Map<Key, Value>>,保证线程安全且互不干扰。
4. 代码示例
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCache<K, V> {
// 真实缓存,线程安全
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
// 用于存储空值标记,避免缓存穿透
private final V NULL_VALUE;
public ConcurrentCache(V nullValue) {
this.NULL_VALUE = nullValue;
}
/**
* 通过缓存获取数据,缓存未命中则从数据源加载
* @param key 查询的 key
* @param loader 数据加载器函数
* @return value 或 nullValue(标记空)
*/
public V get(K key, DataLoader<K, V> loader) {
V value = cache.get(key);
if (value != null) {
// 命中缓存,直接返回
if (value == NULL_VALUE) {
return null; // 代表空
}
return value;
}
// 缓存未命中,从数据源加载
V loadedValue = loader.load(key);
if (loadedValue == null) {
// 缓存空值,防穿透
cache.put(key, NULL_VALUE);
return null;
} else {
cache.put(key, loadedValue);
return loadedValue;
}
}
/**
* 清理缓存
*/
public void clear() {
cache.clear();
}
// 数据加载器接口
public interface DataLoader<K, V> {
V load(K key);
}
}
// 线程隔离示例
class ThreadLocalContext {
private static final ThreadLocal<Map<String, Object>> threadLocalMap = ThreadLocal.withInitial(ConcurrentHashMap::new);
public static void put(String key, Object value) {
threadLocalMap.get().put(key, value);
}
public static Object get(String key) {
return threadLocalMap.get().get(key);
}
public static void clear() {
threadLocalMap.get().clear();
}
}
5. 使用示例
public class Demo {
public static void main(String[] args) {
// 创建缓存,空值标记使用特殊对象
ConcurrentCache<String, String> cache = new ConcurrentCache<>((String) "<NULL>");
// 模拟数据加载器
ConcurrentCache.DataLoader<String, String> loader = key -> {
System.out.println("从数据库加载 key=" + key);
if ("exist".equals(key)) {
return "value_for_exist";
}
return null; // 不存在
};
System.out.println(cache.get("exist", loader)); // 输出 value_for_exist
System.out.println(cache.get("not_exist", loader)); // 输出 null,缓存空值
// 第二次访问,缓存生效,无数据库加载输出
System.out.println(cache.get("exist", loader)); // 缓存命中
System.out.println(cache.get("not_exist", loader)); // 缓存命中空值
// 线程隔离示例
ThreadLocalContext.put("userId", 12345L);
System.out.println(ThreadLocalContext.get("userId")); // 输出 12345
ThreadLocalContext.clear(); // 清理
}
}
6. 设计总结与注意点
| 设计点 | 说明 |
|---|---|
| 缓存空值标记 | 防止缓存穿透,避免重复数据库请求 |
| 并发安全 | 使用 ConcurrentHashMap 保证高并发安全性 |
| 线程隔离 | 使用 ThreadLocal 实现线程级别的数据隔离,防止共享冲突 |
| 缓存淘汰 | 实际应用中建议配合过期时间(如 Guava Cache 或 Caffeine)实现自动淘汰 |
| 负载均衡与降级 | 配合限流、降级策略防止系统崩溃 |
7. 推荐工具库
| 工具库 | 说明 |
|---|---|
| Guava Cache | 支持自动加载、过期淘汰、线程安全缓存 |
| Caffeine | 高性能缓存库,支持异步刷新、权重淘汰 |
| Redisson | 基于 Redis 的分布式缓存和锁 |
8. 相关参考资料
- Java ConcurrentHashMap 官方文档
- Guava Cache 使用指南
- ThreadLocal 官方文档
- 《Java 并发编程实战》 — Brian Goetz
更多详细内容请关注其他相关文章!