设计模式与库存系统:享元模式共享库存数据、观察者更新库存、策略模式选择库存策略

库存系统,说白了就是电商后台最核心的那块肉。我这些年做过好几个库存相关的项目,从单机到分布式,从几百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);

三种模式如何协同工作

这三种模式不是孤立的。在实际项目中,它们经常一起出现。我画了一张图来说明它们的关系:

库存系统三种设计模式协同架构 享元工厂 StockFlyweightFactory 共享内部状态 减少内存占用 库存管理器 StockManager 核心库存操作 事件通知 策略上下文 StockStrategyContext 动态选择策略 普通/预售/秒杀 获取共享数据 调用扣库存 观察者列表(注册/通知) 日志记录 | 缓存更新 | 消息推送 | 数据同步 notifyObservers() 普通策略 NormalStockStrategy 库存不足直接拒绝 预售策略 PreSaleStockStrategy 允许超卖一定数量 秒杀策略 FlashSaleStockStrategy 先扣库存再校验

从图上可以看得很清楚:

  1. 享元工厂提供共享的库存基础数据,减少内存开销
  2. 库存管理器是核心,负责实际的库存增减操作
  3. 策略上下文根据商品类型选择不同的扣库存策略
  4. 观察者列表监听库存变化,做日志、缓存、推送等后续处理

实际项目中的避坑指南

我曾经在一个大促项目里同时用了这三种模式,效果不错,但也踩了几个坑:

  • 享元对象的线程安全:内部状态虽然是共享的,但外部状态是每个线程独立的。记得用ThreadLocal或者不可变对象。
  • 观察者通知顺序:如果观察者之间有依赖关系,要小心。我习惯用优先级或者异步队列来解耦。
  • 策略的动态切换:商品类型可能会变,比如从普通商品改成预售商品。策略模式支持运行时切换,但要注意切换时的库存一致性。

总结一下:

  • 享元模式解决内存问题,共享不变数据
  • 观察者模式解决一致性问题,事件驱动更新
  • 策略模式解决灵活性问题,动态选择算法

这三种模式组合起来,库存系统既省内存、又实时、又灵活。你想想看,是不是比一堆if-else优雅多了?

公众号:蓝海资料掘金营,微信deep3321