设计模式与库存系统:享元模式共享库存数据、观察者更新库存、策略模式选择库存策略
库存系统,说白了就是电商后台最核心的那块肉。我这些年做过好几个库存相关的项目,从单机到分布式,从几百SKU到几百万SKU,踩过的坑真不少。今天咱们就聊聊怎么用三种设计模式,把库存系统做得既省内存又灵活。
库存系统的三大痛点
先说说库存系统常见的几个问题:
- 内存爆炸:几百万个商品,每个都存一份完整数据,内存扛不住
- 数据不一致:多个服务同时扣库存,一不小心就超卖
- 策略僵化:不同商品用不同库存策略,代码里全是if-else
嗯,这三个问题,正好对应三种设计模式。咱们一个一个来拆。
享元模式:共享库存数据
先看享元模式。我记得有一次做电商项目,商品详情页要展示库存信息。每个商品有几十个属性:SKU编号、名称、规格、价格、库存数量、仓库位置……如果每个页面请求都new一个对象,几万并发上来,内存直接爆了。
享元模式的核心思想就是:把对象拆成内部状态和外部状态。内部状态是共享的,外部状态是变化的。
拿库存来说:
- 内部状态:商品ID、名称、规格等不变信息
- 外部状态:当前库存数量、所在仓库、更新时间等变化信息
代码实现大概是这样的:
// 享元接口
public interface StockFlyweight {
void displayStock(StockExternalState state);
}
// 具体享元类
public class ConcreteStock implements StockFlyweight {
private String skuId; // 内部状态
private String skuName; // 内部状态
private String spec; // 内部状态
public ConcreteStock(String skuId, String skuName, String spec) {
this.skuId = skuId;
this.skuName = skuName;
this.spec = spec;
}
@Override
public void displayStock(StockExternalState state) {
System.out.println("商品:" + skuName + ",规格:" + spec
+ ",当前库存:" + state.getQuantity()
+ ",仓库:" + state.getWarehouse());
}
}
// 外部状态
public class StockExternalState {
private int quantity;
private String warehouse;
private Date updateTime;
// getter/setter 省略
}
// 享元工厂
public class StockFlyweightFactory {
private static Map<String, StockFlyweight> pool = new ConcurrentHashMap<>();
public static StockFlyweight getStock(String skuId, String skuName, String spec) {
String key = skuId + ":" + spec;
StockFlyweight stock = pool.get(key);
if (stock == null) {
stock = new ConcreteStock(skuId, skuName, spec);
pool.put(key, stock);
}
return stock;
}
}
你看,几百万个商品,共享的只有几十万个内部状态对象。外部状态每次请求单独创建,用完就丢。内存占用直接降了一个数量级。
小提示:享元模式在Java里其实很常见。String常量池、Integer缓存,都是享元模式的应用。你想想看,Integer.valueOf(127)和Integer.valueOf(127)是同一个对象,但valueOf(128)就不是了。这就是享元。
观察者模式:实时更新库存
库存数据不是一成不变的。用户下单要扣库存,退货要加库存,后台调拨也要改库存。这些操作分散在不同的服务里,怎么保证数据一致?
观察者模式就派上用场了。我习惯把库存变化当作一个事件源,所有关心库存变化的模块都注册成观察者。
// 主题接口
public interface StockSubject {
void registerObserver(StockObserver observer);
void removeObserver(StockObserver observer);
void notifyObservers(StockChangeEvent event);
}
// 具体主题
public class StockManager implements StockSubject {
private List<StockObserver> observers = new CopyOnWriteArrayList<>();
private Map<String, Integer> stockMap = new ConcurrentHashMap<>();
@Override
public void registerObserver(StockObserver observer) {
observers.add(observer);
}
@Override
public void removeObserver(StockObserver observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(StockChangeEvent event) {
for (StockObserver observer : observers) {
observer.update(event);
}
}
public void deductStock(String skuId, int quantity) {
int current = stockMap.getOrDefault(skuId, 0);
if (current < quantity) {
throw new RuntimeException("库存不足");
}
stockMap.put(skuId, current - quantity);
notifyObservers(new StockChangeEvent(skuId, -quantity, current - quantity));
}
}
// 观察者接口
public interface StockObserver {
void update(StockChangeEvent event);
}
// 具体观察者:日志记录
public class StockLogger implements StockObserver {
@Override
public void update(StockChangeEvent event) {
System.out.println("【日志】SKU:" + event.getSkuId()
+ " 变化:" + event.getDelta()
+ " 当前:" + event.getCurrent());
}
}
// 具体观察者:缓存更新
public class StockCacheUpdater implements StockObserver {
@Override
public void update(StockChangeEvent event) {
// 更新Redis缓存
System.out.println("【缓存】更新SKU:" + event.getSkuId() + " 库存为:" + event.getCurrent());
}
}
这样做的好处很明显:
- 扣库存的核心逻辑只写一次
- 新增观察者不用改核心代码
- 观察者之间互不干扰
注意:观察者模式在分布式环境下要小心。我曾经遇到过一个坑:观察者回调里又去调了库存接口,结果死循环了。建议观察者里只做异步操作,或者用消息队列解耦。
策略模式:灵活选择库存策略
不同商品的库存策略是不一样的。举个例子:
- 普通商品:库存不足时直接拒绝下单
- 预售商品:允许超卖,按付款顺序发货
- 秒杀商品:先扣库存再校验,保证速度
如果每个策略都写if-else,代码会越来越臃肿。策略模式就是干这个的。
// 策略接口
public interface StockStrategy {
boolean deductStock(String skuId, int quantity, StockManager manager);
}
// 普通策略
public class NormalStockStrategy implements StockStrategy {
@Override
public boolean deductStock(String skuId, int quantity, StockManager manager) {
int current = manager.getStock(skuId);
if (current < quantity) {
return false; // 直接拒绝
}
manager.setStock(skuId, current - quantity);
return true;
}
}
// 预售策略
public class PreSaleStockStrategy implements StockStrategy {
private int overSellLimit = 100; // 允许超卖数量
@Override
public boolean deductStock(String skuId, int quantity, StockManager manager) {
int current = manager.getStock(skuId);
if (current + overSellLimit < quantity) {
return false; // 超卖太多也拒绝
}
manager.setStock(skuId, current - quantity);
return true;
}
}
// 秒杀策略
public class FlashSaleStockStrategy implements StockStrategy {
@Override
public boolean deductStock(String skuId, int quantity, StockManager manager) {
// 先扣库存,再校验
int current = manager.getStock(skuId);
manager.setStock(skuId, current - quantity);
if (current < quantity) {
// 库存不足,回滚
manager.setStock(skuId, current);
return false;
}
return true;
}
}
// 策略上下文
public class StockStrategyContext {
private StockStrategy strategy;
public void setStrategy(StockStrategy strategy) {
this.strategy = strategy;
}
public boolean executeDeduct(String skuId, int quantity, StockManager manager) {
return strategy.deductStock(skuId, quantity, manager);
}
}
使用的时候,根据商品类型选择策略:
StockStrategyContext context = new StockStrategyContext();
if ("normal".equals(product.getType())) {
context.setStrategy(new NormalStockStrategy());
} else if ("presale".equals(product.getType())) {
context.setStrategy(new PreSaleStockStrategy());
} else if ("flash".equals(product.getType())) {
context.setStrategy(new FlashSaleStockStrategy());
}
context.executeDeduct(skuId, quantity, stockManager);
三种模式如何协同工作
这三种模式不是孤立的。在实际项目中,它们经常一起出现。我画了一张图来说明它们的关系:
从图上可以看得很清楚:
- 享元工厂提供共享的库存基础数据,减少内存开销
- 库存管理器是核心,负责实际的库存增减操作
- 策略上下文根据商品类型选择不同的扣库存策略
- 观察者列表监听库存变化,做日志、缓存、推送等后续处理
实际项目中的避坑指南
我曾经在一个大促项目里同时用了这三种模式,效果不错,但也踩了几个坑:
- 享元对象的线程安全:内部状态虽然是共享的,但外部状态是每个线程独立的。记得用ThreadLocal或者不可变对象。
- 观察者通知顺序:如果观察者之间有依赖关系,要小心。我习惯用优先级或者异步队列来解耦。
- 策略的动态切换:商品类型可能会变,比如从普通商品改成预售商品。策略模式支持运行时切换,但要注意切换时的库存一致性。
总结一下:
- 享元模式解决内存问题,共享不变数据
- 观察者模式解决一致性问题,事件驱动更新
- 策略模式解决灵活性问题,动态选择算法
这三种模式组合起来,库存系统既省内存、又实时、又灵活。你想想看,是不是比一堆if-else优雅多了?