设计模式与缓存系统:享元模式共享缓存、代理模式缓存结果、策略模式选择缓存策略
缓存这东西,说起来简单,做起来全是坑。我这些年重构过的系统里,十有八九都跟缓存有关。今天咱们就聊聊怎么用设计模式把缓存这块理清楚。
说白了,缓存的核心就三件事:存什么、怎么存、存多久。对应到设计模式上,正好是享元、代理和策略三个模式。我习惯把它们放在一起讲,因为实际项目中它们经常配合使用。
享元模式:共享缓存对象
先说说享元模式。你想想看,如果每个请求都去数据库查一遍用户信息,那数据库肯定扛不住。但如果你把查到的数据缓存起来,又有个问题——重复的对象会占用大量内存。
我在项目中遇到过这样一个场景:一个电商系统,商品详情页要展示几百个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);
});
}
}
策略模式:灵活切换缓存策略
缓存策略这东西,没有银弹。有的场景适合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);
// 存储逻辑...
}
}
三种模式如何协同工作
实际项目中,这三种模式经常一起出现。我画了一张图,帮你理清它们的关系:
实际项目中的组合使用
我参与过一个高并发秒杀系统,缓存设计就是这三种模式的组合:
| 设计模式 | 在缓存系统中的角色 | 实际应用场景 |
|---|---|---|
| 享元模式 | 共享缓存对象 | 商品基本信息、用户基础资料等不可变数据 |
| 代理模式 | 缓存查询结果 | 数据库查询结果、远程API调用结果 |
| 策略模式 | 选择缓存策略 | 根据业务场景切换LRU/LFU/TTL策略 |
最后说一句,设计模式不是银弹。我见过有人为了用模式而用模式,把简单问题复杂化。缓存系统设计,核心还是理解业务场景。享元、代理、策略这三个模式,说白了就是帮你把缓存这块的代码组织得更清晰、更灵活。用好了,系统性能提升明显;用不好,反而增加复杂度。
嗯,今天就聊到这儿。如果你在实际项目中遇到缓存相关的问题,不妨想想这三个模式能不能帮上忙。