设计模式与缓存系统:享元模式共享缓存、代理模式缓存结果、策略模式选择缓存策略

缓存这东西,说起来简单,做起来全是坑。我这些年重构过的系统里,十有八九都跟缓存有关。今天咱们就聊聊怎么用设计模式把缓存这块理清楚。

说白了,缓存的核心就三件事:存什么、怎么存、存多久。对应到设计模式上,正好是享元、代理和策略三个模式。我习惯把它们放在一起讲,因为实际项目中它们经常配合使用。

享元模式:共享缓存对象

先说说享元模式。你想想看,如果每个请求都去数据库查一遍用户信息,那数据库肯定扛不住。但如果你把查到的数据缓存起来,又有个问题——重复的对象会占用大量内存。

我在项目中遇到过这样一个场景:一个电商系统,商品详情页要展示几百个SKU。每个SKU都包含图片URL、价格、库存等信息。如果每个SKU都独立创建对象,内存消耗巨大。后来我用享元模式,把不变的部分(比如图片URL、商品名称)共享,只让每个SKU持有自己变化的部分(比如价格、库存)。

核心思想:享元模式通过共享细粒度对象,减少内存占用。适合缓存那些被大量重复使用的、不可变的数据。
// Java 示例:享元模式实现缓存对象共享
public class ProductFlyweight {
    private final String productId;      // 内部状态,可共享
    private final String productName;    // 内部状态,可共享
    private final String imageUrl;       // 内部状态,可共享
    
    // 外部状态由客户端维护
    // 比如价格、库存等
    
    public ProductFlyweight(String productId, String productName, String imageUrl) {
        this.productId = productId;
        this.productName = productName;
        this.imageUrl = imageUrl;
    }
    
    // getters...
}

public class ProductFlyweightFactory {
    private static final Map<String, ProductFlyweight> cache = new ConcurrentHashMap<>();
    
    public static ProductFlyweight getProduct(String productId) {
        return cache.computeIfAbsent(productId, id -> {
            // 从数据库加载基础信息
            return loadFromDB(id);
        });
    }
    
    private static ProductFlyweight loadFromDB(String productId) {
        // 实际数据库查询逻辑
        return new ProductFlyweight(productId, "商品名称", "http://example.com/img.jpg");
    }
}
我的经验:享元模式里的内部状态一定要是不可变的。我曾经犯过错误,让内部状态可以被修改,结果一个线程改了共享数据,所有引用都跟着变了,排查了好久。

代理模式:缓存查询结果

代理模式在缓存系统里扮演的角色很直接——拦截请求,返回缓存结果。说白了就是给真实对象加一层代理,代理负责检查缓存有没有数据,有就直接返回,没有才去查真实数据。

我记得有一次做接口优化,一个查询接口平均响应时间800ms,用户投诉不断。我加了个代理缓存,第一次请求还是800ms,但后续请求直接降到5ms以内。效果立竿见影。

// Java 示例:代理模式实现结果缓存
public interface DataService {
    String queryData(String key);
}

public class RealDataService implements DataService {
    @Override
    public String queryData(String key) {
        // 模拟耗时操作
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        return "Data for " + key;
    }
}

public class CachedDataServiceProxy implements DataService {
    private final DataService realService;
    private final Map<String, String> cache = new ConcurrentHashMap<>();
    
    public CachedDataServiceProxy(DataService realService) {
        this.realService = realService;
    }
    
    @Override
    public String queryData(String key) {
        return cache.computeIfAbsent(key, k -> {
            System.out.println("缓存未命中,查询真实数据");
            return realService.queryData(k);
        });
    }
}
注意:代理模式缓存结果时,一定要考虑缓存失效的问题。我见过一个系统,缓存永不过期,结果数据更新后用户看到的还是旧数据。建议配合TTL(生存时间)使用。

策略模式:灵活切换缓存策略

缓存策略这东西,没有银弹。有的场景适合LRU(最近最少使用),有的适合LFU(最不经常使用),还有的适合FIFO(先进先出)。你不可能写死一种策略,得让系统能灵活切换。

策略模式正好解决这个问题。把每种缓存策略封装成一个独立的算法族,让它们可以互相替换。我习惯在项目里定义一个策略接口,然后实现LRU、LFU、TTL等具体策略。

// Java 示例:策略模式实现缓存策略切换
public interface EvictionStrategy {
    void recordAccess(String key);
    String evictKey();
}

public class LRUEvictionStrategy implements EvictionStrategy {
    private final LinkedHashMap<String, Long> accessMap = new LinkedHashMap<>() {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Long> eldest) {
            return size() > MAX_SIZE;
        }
    };
    
    @Override
    public void recordAccess(String key) {
        accessMap.put(key, System.currentTimeMillis());
    }
    
    @Override
    public String evictKey() {
        // 返回最久未访问的key
        return accessMap.keySet().iterator().next();
    }
}

public class LFUEvictionStrategy implements EvictionStrategy {
    private final Map<String, Integer> frequencyMap = new HashMap<>();
    
    @Override
    public void recordAccess(String key) {
        frequencyMap.merge(key, 1, Integer::sum);
    }
    
    @Override
    public String evictKey() {
        return frequencyMap.entrySet().stream()
                .min(Map.Entry.comparingByValue())
                .map(Map.Entry::getKey)
                .orElse(null);
    }
}

// 使用策略模式
public class CacheManager {
    private EvictionStrategy strategy;
    
    public CacheManager(EvictionStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void setStrategy(EvictionStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void put(String key, String value) {
        strategy.recordAccess(key);
        // 存储逻辑...
    }
}

三种模式如何协同工作

实际项目中,这三种模式经常一起出现。我画了一张图,帮你理清它们的关系:

缓存系统设计模式协同架构 客户端请求 代理模式:缓存结果 拦截请求,返回缓存数据 策略模式:选择策略 LRU / LFU / TTL 灵活切换 享元模式:共享对象 复用不可变数据,减少内存 工作流程 1. 客户端发起请求 2. 代理检查缓存 3. 命中则直接返回 4. 未命中则查询数据 5. 策略决定淘汰算法 6. 享元共享不可变对象 7. 结果存入缓存 8. 返回给客户端

实际项目中的组合使用

我参与过一个高并发秒杀系统,缓存设计就是这三种模式的组合:

设计模式 在缓存系统中的角色 实际应用场景
享元模式 共享缓存对象 商品基本信息、用户基础资料等不可变数据
代理模式 缓存查询结果 数据库查询结果、远程API调用结果
策略模式 选择缓存策略 根据业务场景切换LRU/LFU/TTL策略
避坑指南:我曾经在一个项目里把享元和代理混为一谈,结果代码耦合严重。记住:享元管的是对象共享,代理管的是访问控制,策略管的是算法切换。各司其职,别混在一起。

最后说一句,设计模式不是银弹。我见过有人为了用模式而用模式,把简单问题复杂化。缓存系统设计,核心还是理解业务场景。享元、代理、策略这三个模式,说白了就是帮你把缓存这块的代码组织得更清晰、更灵活。用好了,系统性能提升明显;用不好,反而增加复杂度。

嗯,今天就聊到这儿。如果你在实际项目中遇到缓存相关的问题,不妨想想这三个模式能不能帮上忙。