在并发环境下如何优雅使用 Map 的完整案例(如缓存穿透、线程隔离)
                           
天天向上
发布: 2025-07-28 21:45:58

原创
660 人浏览过

本章节提供一个并发环境下优雅使用 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. 相关参考资料


更多详细内容请关注其他相关文章!

发表回复 0

Your email address will not be published. Required fields are marked *