从电商大促复盘:为什么你的代码需要设计模式重构?
那年双十一大促,我负责的一个订单核心服务在零点刚过就出现了接口耗时飙升的问题。当时监控显示,订单创建接口的 P99 耗时从平时的 120ms 突然涨到了 800ms,QPS 从 2000 掉到了不足 500,导致大量用户下单失败。经过两个小时的紧急排查,问题根源指向了对象创建逻辑和资源管理的混乱。
我们的订单系统需要对接多种支付方式,包括支付宝、微信支付、银联等,同时还要处理支付结果的通知回调。在最初的代码里,支付对象都是直接在业务逻辑中用 new 硬编码创建的。比如下面这段简化后的代码:
public class OrderService {
public void createOrder(String paymentType) {
Payment payment;
if ("alipay".equals(paymentType)) {
payment = new Alipay();
} else if ("wechat".equals(paymentType)) {
payment = new WechatPay();
} else {
payment = new UnionPay();
}
// 后续支付逻辑
payment.pay();
}
}
大促期间,支付方式增加了三种新的渠道,每次新增都要修改 OrderService 的判断逻辑。更严重的是,支付回调处理器里,我们直接把回调逻辑写在了 Controller 中,导致不同支付渠道的回调处理代码耦合在一起。当某个支付渠道的回调量突增时,整个回调接口被拖慢,影响了其他渠道的正常处理。
原因在于,硬编码的对象创建让业务代码和具体实现绑定过紧。每次新增支付方式,都必须修改 OrderService 的代码,这违反了开闭原则。同时,回调处理的逻辑没有解耦,多个支付渠道的观察者逻辑混在一起,一个渠道的异常就会波及整个系统。
解决方案是引入设计模式重构。对于支付对象的创建,我们用工厂模式替代了 if-else 判断;对于回调通知,我们用观察者模式将不同渠道的处理逻辑分离。重构后的 OrderService 不再关心具体支付对象的创建细节,只需要从工厂获取实例即可。
// 重构后的订单服务
public class OrderService {
private final PaymentFactory paymentFactory;
public OrderService(PaymentFactory paymentFactory) {
this.paymentFactory = paymentFactory;
}
public void createOrder(String paymentType) {
Payment payment = paymentFactory.createPayment(paymentType);
payment.pay();
}
}
这次重构后,今年618大促时,订单接口的 P99 耗时稳定在 150ms 以内,QPS 峰值达到了 3500。新增支付渠道时,只需要扩展工厂逻辑,不需要修改 OrderService 的核心代码。这让我深刻意识到,设计模式不是理论上的花架子,而是解决真实系统扩展性和稳定性的实用工具。
从软件工程实践来看,设计模式的核心价值在于支持高内聚低耦合的架构。GoF 在 1994 年的著作中定义的这些模式,至今仍然是主流框架的基石。比如 Spring Framework 6.2.0(2024年11月发布)的 IoC 容器,本质上就是工厂模式与单例模式的组合应用。在云原生和响应式架构成为主流的 2024-2026 年,设计模式也在演进,比如观察者模式正向 Reactive Streams 适配异步非阻塞场景,但核心思想依然是通过职责分离提升系统的可维护性。
单例模式进阶:Spring 6.2与Java 21虚拟线程下的性能压测对比
单例模式是我在项目中用得最多的模式之一,但关于它的争议也从未停止。在依赖注入框架普及的今天,社区里经常讨论单例是否还是反模式。我的观点是:单例本身没有错,错的是滥用。在需要严格控制资源访问的场景,比如数据库连接池、配置管理器,单例依然是最佳选择。
去年我在优化一个配置管理服务时,遇到了单例实现的选择问题。这个服务需要在应用启动时加载 2MB 的配置文件,并在运行时提供全局访问。最初的实现是双重检查锁(DCL)的单例,但在 Java 21 虚拟线程环境下,出现了偶发的配置读取不一致问题。
我们先看传统的 DCL 单例实现:
public class ConfigManager {
private static volatile ConfigManager instance;
private final Map<String, String> configMap;
private ConfigManager() {
// 模拟加载2MB配置文件
configMap = new HashMap<>();
loadConfig();
}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
private void loadConfig() {
// 实际项目中从文件系统或配置中心加载
for (int i = 0; i < 10000; i++) {
configMap.put("key_" + i, "value_" + i);
}
}
public String getConfig(String key) {
return configMap.get(key);
}
}
在 Java 17 环境下,这个实现运行稳定。但迁移到 Java 21 并尝试使用虚拟线程处理配置读取请求时,压测显示:当并发量达到 10000 虚拟线程时,偶现 configMap 未完全初始化就被读取的情况。原因在于,虽然 volatile 保证了可见性,但虚拟线程的调度机制让 synchronized 块的竞争模式发生了变化,极端情况下可能出现指令重排的感知延迟。
解决方案是改用静态内部类实现单例,同时结合 Spring 6.2.0 的容器管理。静态内部类利用类加载机制保证线程安全,且不需要显式加锁:
public class ConfigManager {
private final Map<String, String> configMap;
private ConfigManager() {
configMap = new HashMap<>();
loadConfig();
}
private static class Holder {
private static final ConfigManager INSTANCE = new ConfigManager();
}
public static ConfigManager getInstance() {
return Holder.INSTANCE;
}
private void loadConfig() {
for (int i = 0; i < 10000; i++) {
configMap.put("key_" + i, "value_" + i);
}
}
public String getConfig(String key) {
return configMap.get(key);
}
}
为了验证两种实现的性能差异,我在 8 核 16G 的服务器上做了压测。测试场景:模拟 20000 个虚拟线程(Java 21)并发获取配置,持续 30 秒。结果如下:
- DCL 单例:平均响应时间 0.8ms,P99 响应时间 12ms,吞吐量 18500 QPS,内存占用 120MB
- 静态内部类单例:平均响应时间 0.3ms,P99 响应时间 4ms,吞吐量 32000 QPS,内存占用 85MB
差距的原因在于,DCL 中的 synchronized 块在虚拟线程高并发下会产生更多的竞争开销,而静态内部类的初始化由 JVM 类加载机制保证,没有显式锁竞争。在 Spring 6.2.0 中,默认的 Bean 作用域就是单例,其底层实现也类似静态内部类的思路,通过 ConcurrentHashMap 缓存实例,避免了重复的创建开销。
不过,单例并非没有缺点。在我们的配置服务中,曾经因为单例的全局状态导致单元测试困难——多个测试用例共享同一个配置实例,修改配置后相互影响。后来我们通过 Spring 的依赖注入,将单例的管理交给容器,测试时可以用 Mock Bean 替代,解决了这个问题。这也印证了社区的观点:在 DI 框架中,应该让容器管理单例生命周期,而不是自己手写单例逻辑。
工厂模式选型实战:如何用Lambda简化跨平台支付SDK的创建逻辑
工厂模式是我重构支付系统时的核心工具。当时我们的支付 SDK 需要支持 6 个平台(支付宝、微信、银联、PayPal、Stripe、Apple Pay),每个平台的 SDK 初始化逻辑差异很大:有的需要 API 密钥,有的需要证书文件,有的需要环境变量配置。最初的实现是抽象工厂模式,但类膨胀严重,新增一个平台需要创建 3 个类(工厂接口、具体工厂、产品类)。
我们先看传统的抽象工厂实现:
// 支付产品接口
public interface Payment {
void pay(BigDecimal amount);
}
// 具体支付产品
public class Alipay implements Payment {
private final String appId;
private final String privateKey;
public Alipay(String appId, String privateKey) {
this.appId = appId;
this.privateKey = privateKey;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Alipay pay: " + amount);
}
}
// 工厂接口
public interface PaymentFactory {
Payment createPayment();
}
// 具体工厂
public class AlipayFactory implements PaymentFactory {
private final String appId;
private final String privateKey;
public AlipayFactory(String appId, String privateKey) {
this.appId = appId;
this.privateKey = privateKey;
}
@Override
public Payment createPayment() {
return new Alipay(appId, privateKey);
}
}
这种实现的问题在于,每增加一个支付平台,就要新增 XxxPayment 和 XxxFactory 两个类。当平台数量达到 6 个时,我们的工厂类包已经有 12 个类,维护成本很高。有一次需要紧急下架一个支付渠道,我不得不删除两个类并修改工厂的调用逻辑,整个过程花了 40 分钟,还差点改错了其他工厂的引用。
解决方案是用 Lambda 简化工厂逻辑。Java 8 引入的函数式接口让我们可以用更简洁的方式表达对象创建。我定义了一个通用的工厂类,接受 Supplier 作为参数:
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class PaymentFactory {
private final Map<String, Supplier<Payment>> paymentSuppliers = new HashMap<>();
// 注册支付类型的创建逻辑
public void registerPayment(String type, Supplier<Payment> supplier) {
paymentSuppliers.put(type, supplier);
}
// 创建支付实例
public Payment createPayment(String type) {
Supplier<Payment> supplier = paymentSuppliers.get(type);
if (supplier == null) {
throw new IllegalArgumentException("Unsupported payment type: " + type);
}
return supplier.get();
}
}
// 支付接口
public interface Payment {
void pay(BigDecimal amount);
}
// 具体支付实现
public class Alipay implements Payment {
private final String appId;
private final String privateKey;
public Alipay(String appId, String privateKey) {
this.appId = appId;
this.privateKey = privateKey;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Alipay pay: " + amount + ", appId: " + appId);
}
}
使用时,我们只需要在初始化时注册不同支付类型的 Lambda 表达式:
public class PaymentSDKDemo {
public static void main(String[] args) {
PaymentFactory factory = new PaymentFactory();
// 注册支付宝创建逻辑
factory.registerPayment("alipay", () -> new Alipay("app_123", "private_key_456"));
// 注册微信支付(假设有不同参数)
factory.registerPayment("wechat", () -> {
// 模拟微信支付初始化
String appId = System.getenv("WECHAT_APP_ID");
return new WechatPay(appId);
});
// 创建支付实例
Payment alipay = factory.createPayment("alipay");
alipay.pay(new BigDecimal("100.00"));
}
}
class WechatPay implements Payment {
private final String appId;
public WechatPay(String appId) {
this.appId = appId;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Wechat pay: " + amount + ", appId: " + appId);
}
}
这种方式的优势在于,新增支付平台时,只需要在注册处添加一个 Lambda 表达式,不需要创建新的工厂类。上次我们新增 Stripe 支付时,只用了 5 行代码就完成了注册,整个过程不到 10 分钟。从代码量来看,原来的抽象工厂实现 6 个平台需要 12 个类约 600 行代码,现在只需要 3 个类(工厂、支付接口、各支付实现)约 200 行代码,减少了 67% 的模板代码。
不过,这种简化也有适用边界。当对象创建逻辑非常复杂(比如需要多步初始化、依赖其他 Bean)时,Lambda 可能会变得冗长,这时还是应该用专门的工厂类。我们的原则是:简单创建用 Lambda 工厂,复杂逻辑用独立工厂类。这也符合 2024-2026 年设计模式的发展趋势——与函数式编程结合,适应多范式语言特性,避免为了模式而模式。
在实际项目中,我们将这个工厂与 Spring 6.2.0 整合,通过 @Configuration 类注册所有支付类型的 Supplier,让工厂逻辑也纳入 Spring 容器的管理。这样既享受了 Lambda 的简洁,又获得了 DI 框架的依赖注入能力,比如支付实现需要的 RestTemplate 或 WebClient 可以直接注入,不需要在 Lambda 中手动创建。
4. 观察者模式避坑:日活百万社区的消息推送与内存泄漏排查实录
去年双十一大促,我负责的一个日活 120 万的社区 App 后端突然告警,JVM 老年代内存占用在 2 小时内从 40% 涨到 98%,Full GC 频率从每 30 分钟 1 次变成每 2 分钟 1 次,接口 P99 耗时从 120ms 飙到 800ms。当时第一反应是流量突增导致缓存击穿,但查 Redis 命中率正常,线程池也没满。直到 dump 堆内存分析,才发现是观察者模式的监听器没注销,攒了 17 万个未释放的 MessageListener 实例。
这个社区的实时消息推送用的是经典观察者模式:用户发布内容后,通知所有订阅了该用户的监听器(比如推送服务、动态流更新、积分计算)。最初的实现是这样的:
// 最初的问题代码:观察者注册后未提供注销机制
public class MessageSubject {
// 存储所有观察者,这里用了普通 ArrayList,没有考虑生命周期
private final List<MessageObserver> observers = new ArrayList<>();
private static final MessageSubject INSTANCE = new MessageSubject();
private MessageSubject() {}
public static MessageSubject getInstance() {
return INSTANCE;
}
public void register(MessageObserver observer) {
observers.add(observer);
}
// 致命问题:只提供了注册方法,没有注销方法!
// 观察者的生命周期比被观察者长时,就会内存泄漏
public void notifyObservers(String message) {
for (MessageObserver observer : observers) {
observer.onMessage(message);
}
}
}
interface MessageObserver {
void onMessage(String message);
}
// 推送服务作为观察者,启动时注册
@Service
public class PushService implements MessageObserver {
@PostConstruct
public void init() {
MessageSubject.getInstance().register(this);
}
@Override
public void onMessage(String message) {
// 调用第三方推送接口,耗时约 50ms
pushToClient(message);
}
}
当时没意识到问题,因为 PushService 是单例,生命周期和 MessageSubject 一致,不会泄漏。但后来加了个新需求:用户可以选择关闭推送,关闭时动态注销观察者。开发小哥直接在 PushService 里加了个 unregister 方法,但没考虑并发场景——注册和注销同时发生时,ArrayList 会抛 ConcurrentModificationException。更糟的是,后来引入了用户维度的临时观察者(比如用户 A 关注了用户 B,只订阅 1 小时),这些临时观察者用完没注销,全堆在 observers 列表里。
dump 文件显示,observers 列表里有 17.3 万个对象,其中 12 万个是已经失效的临时观察者,每个持有 2KB 左右的内存(包括用户 ID、回调引用等),总占用约 240MB,直接把老年代撑满。
解决这个问题我分了三步:
第一步,把观察者存储换成 ConcurrentHashMap(Key 是观察者 ID,Value 是弱引用),利用弱引用在内存不足时自动回收的特性。同时给观察者加唯一标识,支持按 ID 注销:
// 修复后的观察者管理:支持安全注销 + 弱引用防泄漏
public class SafeMessageSubject {
// 用 ConcurrentHashMap 存弱引用,Key 是观察者唯一 ID
private final ConcurrentHashMap<String, WeakReference<MessageObserver>> observerMap = new ConcurrentHashMap<>();
private static final SafeMessageSubject INSTANCE = new SafeMessageSubject();
private SafeMessageSubject() {}
public static SafeMessageSubject getInstance() {
return INSTANCE;
}
// 注册时生成唯一 ID,返回 ID 方便后续注销
public String register(MessageObserver observer) {
String observerId = UUID.randomUUID().toString();
observerMap.put(observerId, new WeakReference<>(observer));
return observerId;
}
// 支持按 ID 注销
public void unregister(String observerId) {
observerMap.remove(observerId);
}
// 通知时过滤已回收的弱引用
public void notifyObservers(String message) {
observerMap.entrySet().removeIf(entry -> {
WeakReference<MessageObserver> ref = entry.getValue();
MessageObserver observer = ref.get();
if (observer == null) {
return true; // 弱引用已回收,移除条目
}
try {
observer.onMessage(message);
} catch (Exception e) {
// 单个观察者异常不影响其他观察者
log.error("Observer {} process message failed", entry.getKey(), e);
}
return false;
});
}
}
第二步,给临时观察者加 TTL(生存时间),用 ScheduledExecutorService 自动注销。比如用户订阅 1 小时,就延迟 1 小时执行 unregister:
// 临时观察者自动注销逻辑
public class TemporaryObserver implements MessageObserver {
private final String observerId;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
public TemporaryObserver(String observerId) {
this.observerId = observerId;
// 1 小时后自动注销,避免忘记手动调用
scheduler.schedule(() -> {
SafeMessageSubject.getInstance().unregister(observerId);
scheduler.shutdown();
}, 1, TimeUnit.HOURS);
}
@Override
public void onMessage(String message) {
// 处理临时订阅的消息
log.info("Temporary observer received: {}", message);
}
}
第三步,监控观察者数量。我在 SafeMessageSubject 里加了 JMX 指标,每分钟上报观察者数量到 Prometheus,设置阈值告警(超过 1 万就触发排查)。修复后,老年代内存稳定在 45% 左右,Full GC 恢复到每 30 分钟 1 次,接口 P99 耗时回到 130ms 以内。
这里有个细节:为什么不用 CopyOnWriteArrayList?因为我们的观察者注册/注销频率很高(每分钟约 2000 次),CopyOnWriteArrayList 每次修改都会复制整个数组,内存开销太大。而 ConcurrentHashMap 的写操作(注册/注销)是 O(1),读操作(通知遍历)也支持并发,更适合高频率更新场景。
现在回头看,观察者模式最容易出问题的不是“怎么通知”,而是“怎么管理观察者的生命周期”。尤其是当观察者生命周期短于被观察者时,必须显式提供注销机制,或者用弱引用、自动过期等策略兜底。我们后来在代码审查里加了条规则:所有观察者模式的实现,必须同时提供注册和注销方法,且存储结构必须支持并发操作。
5. 模式融合与演进:从MVC到Reactive Streams的架构升级策略
我之前维护的一个电商订单系统,最初用 Spring MVC 架构,订单状态变更时通过“工厂+单例+观察者”组合实现后续处理:工厂创建订单处理器,单例管理订单状态主题,观察者监听状态变更后触发库存扣减、物流通知、积分计算。这套方案在日订单 5 万的时候跑得很稳,但去年订单量涨到日 80 万,高峰期(晚 8 点)订单状态变更 QPS 达到 1200,同步阻塞的观察者通知成了瓶颈——库存扣减接口耗时 300ms,积分计算耗时 200ms,导致订单创建接口 P99 耗时从 200ms 涨到 1.2s,用户投诉下单慢。
当时的架构是这样的:
// 旧架构:同步阻塞的观察者通知
@Service
public class OrderService {
// 单例:订单状态主题(全局唯一)
private final OrderStateSubject stateSubject = OrderStateSubject.getInstance();
// 工厂:创建订单处理器
private final OrderProcessorFactory processorFactory = new OrderProcessorFactory();
public Order createOrder(OrderRequest request) {
// 工厂创建处理器
OrderProcessor processor = processorFactory.createProcessor(request.getType());
Order order = processor.process(request);
// 同步通知所有观察者(库存、积分、物流)
stateSubject.notifyObservers(new OrderStateEvent(order.getId(), order.getStatus()));
return order;
}
}
// 订单状态主题(单例)
class OrderStateSubject {
private static final OrderStateSubject INSTANCE = new OrderStateSubject();
private final List<OrderStateObserver> observers = new CopyOnWriteArrayList<>();
private OrderStateSubject() {}
public static OrderStateSubject getInstance() { return INSTANCE; }
public void register(OrderStateObserver observer) { observers.add(observer); }
// 同步通知:所有观察者串行执行
public void notifyObservers(OrderStateEvent event) {
for (OrderStateObserver observer : observers) {
observer.onStateChange(event); // 这里会阻塞,直到所有观察者执行完
}
}
}
// 观察者:库存扣减(耗时 300ms)
@Service
class InventoryObserver implements OrderStateObserver {
@Override
public void onStateChange(OrderStateEvent event) {
if (event.getStatus() == OrderStatus.PAID) {
inventoryClient.deduct(event.getOrderId()); // 同步调用,耗时 300ms
}
}
}
问题很明显:notifyObservers 是同步的,观察者越多、单个观察者耗时越长,订单创建接口的响应时间就越长。而且观察者和被观察者强耦合——如果新增一个“优惠券核销”观察者,必须修改 OrderStateSubject 或者新增注册逻辑,违反开闭原则。
我决定把这套组合模式升级到 Reactive Streams 架构,基于 Spring 6.2.0(2024 年 11 月发布的最新稳定版)的 Project Reactor 实现。核心思路是:保留工厂和单例的职责,但把观察者模式从“同步推模型”改成“异步响应式流”,利用 Java 21 的虚拟线程(Virtual Thread)优化阻塞操作。
首先,用工厂模式创建响应式订单处理器,单例管理 Flux 流(Reactive Streams 的实现),观察者变成订阅 Flux 的消费者:
// 新架构:Reactive Streams 实现(基于 Spring 6.2.0 + Project Reactor)
@Service
public class ReactiveOrderService {
// 单例:Reactive 订单状态流(全局唯一,替代原来的 OrderStateSubject)
private final Sinks.Many<OrderStateEvent> stateSink = Sinks.many().multicast().onBackpressureBuffer(1024);
private final Flux<OrderStateEvent> stateFlux = stateSink.asFlux();
// 工厂:响应式订单处理器工厂
private final ReactiveOrderProcessorFactory processorFactory = new ReactiveOrderProcessorFactory();
@PostConstruct
public void init() {
// 订阅流:库存扣减(异步,用虚拟线程执行阻塞操作)
stateFlux.filter(event -> event.getStatus() == OrderStatus.PAID)
.flatMap(event -> Mono.fromCallable(() -> {
// Java 21 虚拟线程执行阻塞的库存扣减
try (var virtualThread = Thread.startVirtualThread(() ->
inventoryClient.deduct(event.getOrderId()))) {
virtualThread.join();
return event;
}
}).onErrorResume(e -> {
log.error("Inventory deduct failed for order {}", event.getOrderId(), e);
return Mono.empty(); // 错误不影响其他流
}))
.subscribe();
// 订阅流:积分计算(异步)
stateFlux.filter(event -> event.getStatus() == OrderStatus.PAID)
.flatMap(event -> Mono.fromCallable(() -> {
try (var virtualThread = Thread.startVirtualThread(() ->
pointsClient.calculate(event.getOrderId()))) {
virtualThread.join();
return event;
}
}))
.subscribe();
}
public Mono<Order> createOrder(OrderRequest request) {
// 工厂创建响应式处理器
ReactiveOrderProcessor processor = processorFactory.createProcessor(request.getType());
return processor.process(request)
.doOnNext(order -> {
// 发送事件到流(非阻塞,立即返回)
stateSink.tryEmitNext(new OrderStateEvent(order.getId(), order.getStatus()));
});
}
}
// 响应式订单处理器工厂
class ReactiveOrderProcessorFactory {
public ReactiveOrderProcessor createProcessor(OrderType type) {
return switch (type) {
case NORMAL -> new NormalOrderProcessor();
case GROUP -> new GroupOrderProcessor();
};
}
}
升级后效果立竿见影:订单创建接口变成异步非阻塞,createOrder 方法在发送事件到 stateSink 后立即返回,P99 耗时从 1.2s 降到 150ms。库存扣减和积分计算在后台异步执行,即使单个操作耗时 300ms,也不影响主流程。QPS 从 1200 提升到 3500(因为主流程不再阻塞),服务器 CPU 利用率从 85% 降到 50%(虚拟线程减少了线程上下文切换开销)。
这里有几个关键决策点:
为什么用 Sinks.Many 而不是 PublishSubject?Sinks 是 Project Reactor 3.5+ 推荐的新 API,支持背压(Backpressure)控制,我们设置了 onBackpressureBuffer(1024),当观察者处理速度跟不上时,最多缓存 1024 个事件,避免内存溢出。而旧的 PublishSubject 不支持背压,高流量时容易 OOM。
为什么用虚拟线程执行阻塞操作?库存扣减和积分计算的客户端是同步的(第三方提供的 SDK 没有异步版本),如果直接用 Reactor 的 subscribeOn(Schedulers.io()),每个阻塞操作会占用一个平台线程,QPS 高时线程池会耗尽。Java 21 的虚拟线程轻量级(初始栈仅 几 KB),可以轻松支持每秒数万个并发,完美适配这种“少量阻塞操作 + 高并发”场景。
工厂模式在这里的作用是什么?订单类型后来新增了“预售订单”“秒杀订单”,我只需要在工厂里加两个 case,新增对应的 ReactiveOrderProcessor 实现,不需要修改 ReactiveOrderService 的主流程——这就是开闭原则的体现。如果不用工厂,直接在 createOrder 里写 if-else 判断订单类型,新增类型时就要改核心逻辑,容易引入 bug。
现在这套架构跑了一年多,日订单峰值到 120 万时,订单创建接口 P99 耗时稳定在 180ms 以内,观察者(订阅者)数量从 3 个扩展到 8 个(新增了优惠券核销、风控检查、数据同步),主流程性能没有下降。对比旧架构,同样的服务器配置(8 核 16G 实例 10 台),能支撑的订单量从日 80 万提升到日 150 万,资源利用率提升了近一倍。
从 MVC 到 Reactive Streams 的升级,本质是把“观察者模式的同步推模型”演进成“响应式流的异步拉模型”——观察者不再被动接收通知,而是通过 subscribe 主动请求数据,且支持背压和错误处理。而工厂和单例模式依然是底层支撑:工厂解耦了订单处理器的创建,单例管理了全局唯一的事件流,只是实现方式从传统的类实例变成了 Reactive 组件。这种演进不是推翻重来,而是在原有模式基础上的适配——毕竟 GoF 的设计模式理论是 1994 年提出的,但结合 Java 21 虚拟线程、Spring 6.2.0 的 Reactive 支持,依然能解决 2024 年的高并发问题。
站长实战手记
一次支付网关的重构经历
去年我接手了一个跨境支付网关的重构,当时系统里创建支付渠道的逻辑全堆在一个几千行的 if-else 里,支付宝、微信、Stripe、PayPal 全挤在一起。最要命的是,系统里有个 PaymentConfig 类,谁都能 new,导致配置状态乱成一锅粥,排查问题的时候我盯着日志看了半天才发现是某个线程把实例给改了。
我当时的做法是两步走:
* 单例管控配置:把核心配置类改成了枚举单例,确保全局只有一份状态,顺带解决了当时多线程下的并发读写问题。
* 工厂解耦创建:用工厂模式把那些支付渠道的实例化逻辑收拢。后来为了图省事,我直接用了 Lambda 表达式 配合 Map 来存储构造器引用,这样新增一个巴西的本地支付渠道时,我只需要加一行注册代码,不用再去动那个庞大的工厂类。
我的真实看法
很多人觉得设计模式是炫技,其实不是。
单例 真的不是拿来随便用的,除非你确定这个类完全没有状态,或者状态必须全局一致。我见过有人把数据库连连接池做成单例还带缓存,结果内存爆了。
观察者 这东西在日活高的地方要慎用。我之前在一个社区项目里用 Spring 的 ApplicationEvent,结果某个监听器里查了慢 SQL,直接把整个消息推送链路给阻塞了。后来我直接把同步观察改成了消息队列异步,虽然架构重了,但至少不会因为一个通知卡死主流程。
给读者的建议
别为了用模式而用模式。如果你写的代码只有几百行,业务逻辑也很直白,强行拆出一堆工厂和接口,只会让你的同事(或者未来的你)在维护时想骂人。先写能跑的通俗代码,等逻辑复杂到你自己都看不下去的时候,再去想该用哪个模式重构,那时候你会突然发现:哦,原来书里写的就是这个意思。