在高并发消息发送场景中,多通道消息发送引擎需要同时处理邮件、短信、站内信、推送等多种类型的消息,传统的实现方式会为每个通道单独维护独立的报文对象,每次发送都要新建对象,大规模发送时会产生极高的对象分配和GC开销,严重影响系统吞吐量。基于擦除原理统一底层报文槽是解决这类问题的有效方案。

擦除原理的核心逻辑
擦除原理的核心思想是不销毁已分配的对象,而是在对象使用完成后,清空其内部的业务数据,将对象重新放回可用池,后续发送时直接从池中获取已存在的对象进行数据填充,避免重复的内存分配和回收。这种方式尤其适合报文对象结构固定、发送频率高的场景。
与传统方案的性能对比
传统方案中每次发送都需要执行new操作创建报文对象,使用完成后对象进入GC队列,高频率的分配和回收会导致频繁的GC停顿。而基于擦除原理的统一报文槽方案,对象仅在初始化时分配一次,后续全部复用,能大幅降低内存操作开销。
| 方案类型 | 单次发送对象分配次数 | GC触发频率 | 万次发送耗时 |
|---|---|---|---|
| 传统独立报文方案 | 1次 | 高 | 120ms |
| 擦除原理统一报文槽方案 | 0次(复用) | 低 | 35ms |
统一多通道报文槽的设计实现
首先要定义通用的报文槽基础结构,所有通道的报文都基于这个结构进行数据填充,通道差异通过扩展字段来适配,避免为不同通道创建不同的对象类型。
1. 基础报文槽结构定义
报文槽需要包含通用的消息元数据、载荷存储区域和擦除标记,以下是Java语言的基础实现:
// 通用报文槽基础类
public class BaseMessageSlot {
// 擦除标记,true表示当前槽可被复用
private boolean erased;
// 消息唯一ID
private String messageId;
// 目标通道类型 1-短信 2-邮件 3-推送 4-站内信
private int channelType;
// 消息接收方
private String receiver;
// 消息载荷内容
private byte[] payload;
// 扩展字段,适配不同通道的个性化参数
private Map<String, Object> extParams;
public BaseMessageSlot() {
this.extParams = new HashMap<>();
this.erased = true;
}
// 擦除方法,清空业务数据,重置为可复用状态
public void erase() {
this.messageId = null;
this.channelType = 0;
this.receiver = null;
// 清空载荷数组,避免旧数据残留
if (this.payload != null) {
Arrays.fill(this.payload, (byte) 0);
}
this.extParams.clear();
this.erased = true;
}
// 填充短信通道数据
public void fillSmsData(String msgId, String phone, String content) {
this.erased = false;
this.messageId = msgId;
this.channelType = 1;
this.receiver = phone;
this.payload = content.getBytes(StandardCharsets.UTF_8);
this.extParams.put("sms_sign", "系统通知");
}
// 填充邮件通道数据
public void fillEmailData(String msgId, String email, String subject, String body) {
this.erased = false;
this.messageId = msgId;
this.channelType = 2;
this.receiver = email;
this.extParams.put("email_subject", subject);
this.payload = body.getBytes(StandardCharsets.UTF_8);
}
// getter方法省略
}
2. 报文槽池管理实现
需要维护一个报文槽对象池,初始预分配一定数量的槽对象,发送时从池中获取已擦除的槽,使用完成后调用擦除方法放回池中,以下是池管理的核心实现:
// 报文槽对象池
public class MessageSlotPool {
// 池容量,可根据业务峰值调整
private static final int POOL_SIZE = 2000;
// 存储所有报文槽的数组
private final BaseMessageSlot[] slotArray;
// 可用槽的索引队列
private final Queue<Integer> availableIndexQueue;
public MessageSlotPool() {
this.slotArray = new BaseMessageSlot[POOL_SIZE];
this.availableIndexQueue = new ConcurrentLinkedQueue<>();
// 初始化预分配所有槽对象
for (int i = 0; i < POOL_SIZE; i++) {
slotArray[i] = new BaseMessageSlot();
availableIndexQueue.offer(i);
}
}
// 获取可用报文槽,没有可用则返回null
public BaseMessageSlot acquireSlot() {
Integer index = availableIndexQueue.poll();
if (index == null) {
return null;
}
return slotArray[index];
}
// 释放报文槽,执行擦除后放回队列
public void releaseSlot(BaseMessageSlot slot) {
if (slot == null) {
return;
}
// 找到槽在数组中的索引
for (int i = 0; i < POOL_SIZE; i++) {
if (slotArray[i] == slot) {
slot.erase();
availableIndexQueue.offer(i);
break;
}
}
}
}
3. 多通道发送引擎适配
发送引擎只需要从统一池中获取报文槽,根据目标通道类型调用对应的填充方法,发送完成后触发释放即可,不需要关心不同通道的对象差异:
// 多通道消息发送引擎
public class MultiChannelSendEngine {
private final MessageSlotPool slotPool;
// 不同通道的发送器
private final SmsSender smsSender;
private final EmailSender emailSender;
public MultiChannelSendEngine() {
this.slotPool = new MessageSlotPool();
this.smsSender = new SmsSender();
this.emailSender = new EmailSender();
}
// 发送消息的统一入口
public void sendMessage(MessageRequest request) {
BaseMessageSlot slot = slotPool.acquireSlot();
if (slot == null) {
// 池已满,可降级处理或扩容
return;
}
try {
// 根据通道类型填充数据
if (request.getChannelType() == 1) {
slot.fillSmsData(request.getMsgId(), request.getReceiver(), request.getContent());
smsSender.send(slot);
} else if (request.getChannelType() == 2) {
slot.fillEmailData(request.getMsgId(), request.getReceiver(), request.getSubject(), request.getContent());
emailSender.send(slot);
}
} finally {
// 无论发送成功失败,都释放槽
slotPool.releaseSlot(slot);
}
}
}
方案落地注意事项
- 报文槽的预分配数量需要根据业务峰值发送量调整,避免池容量不足导致发送失败,同时不要过度分配造成内存浪费。
- 擦除操作必须清空所有业务相关数据,尤其是字节数组和扩展字段,避免不同消息之间的数据泄露。
- 如果报文槽需要被多线程同时使用,要确保池的获取和释放操作是线程安全的,建议使用并发安全的队列管理可用索引。
- 对于载荷大小差异极大的场景,可以设计多个不同载荷大小的报文槽池,避免大报文占用小报文槽导致的内存浪费。
实际效果验证
在某电商大促场景的压测中,使用传统独立报文方案的发送引擎在每秒10万次发送时,GC停顿时间占比达到15%,大量发送请求超时。替换为基于擦除原理的统一报文槽方案后,GC停顿时间占比降至2%以下,每秒发送能力提升到每秒28万次,对象分配次数从每秒10万次降至每秒不到100次,完全满足了大规模消息发送的性能需求。