内存化系统是怎么设计的?
只要系统一追求高性能、低延迟,几乎绕不开一个词:内存化系统。不管是撮合引擎、风控、实时账本、排行榜、在线游戏、实时计算,共同点都一样——慢一点就出事的系统,基本都在往内存里放演进。
之前的文章对比了传统架构和内存化架构的区别,以及对账系统的内存化案例。
这篇从工程实践角度聊清楚:内存化系统整体怎么设计,并发度怎么控制,内存数据怎么变更、怎么保证一致,重启之后数据怎么回来,单点故障怎么解,内存不够了怎么办,以及 Java 里常用哪些框架。
一、内存化系统不是缓存
这是很多系统一开始就设计偏的地方。
缓存系统里,DB 是权威数据源,内存只是加速,丢了可以随时重建。内存化系统不一样:内存就是权威数据源,DB 只是持久化副本,内存一乱,系统就乱。
| 传统系统 | 内存化系统 |
|---|---|
| 数据在数据库 | 数据在内存 |
| DB 是主角 | DB 是备胎 |
| 扛不住高并发 | 天生低延迟 |
二、核心内存模型怎么设计
内存里存什么
不要存 DTO,不要存 VO,不要直接映射表结构。内存里只存业务状态最小集合。
以账户系统为例:
class AccountState {long accountId;long balance; // 用最小单位,比如分long frozen;int version; // 内存版本号
}
两个要点:使用内存占用少的结构,不放无关字段。
用什么结构
内存结构取决于业务形态,除了业务数据本身,可能还需要索引结构。
HashMap / ConcurrentHashMap 查找 O(1),JVM 友好,90% 场景够用:
Map<Long, AccountState> accounts = new HashMap<>();
数据量特别大的场景可以用 Long2ObjectMap,避免 Long 装箱,减少 GC:
Long2ObjectOpenHashMap<AccountState> accounts;
三、并发度:不是线程越多越好
分片 + 单线程
请求↓┌──────────────────┐│ Router(按 key) │└──────────────────┘↓ ↓Shard-1 Shard-2Thread Thread
Router 按 key 分片:
int shard = (int) (accountId % shardCount);
executor[shard].execute(() -> handle(cmd));
每个 Shard 内部:一个线程、一个内存 Map、无锁。这是撮合、账本、风控系统里用得最多的模型。
为什么不用锁
锁隐藏了并发复杂度,状态一致性难验证,性能随并发退化。单线程分片模型保证了顺序确定性,这是内存化系统能跑稳的关键。
四、内存数据变更
禁止直接改状态
account.balance -= amount;
没来源,不可回放,不可审计。
Command → Event → State
外部输入 Command,内部转成 Event 落盘,内存中维护 State:
// Command
class DebitCommand {long accountId;long amount;
}// Event
class BalanceChangedEvent {long accountId;long delta;long before;long after;
}// Apply
void apply(BalanceChangedEvent e) {AccountState s = accounts.get(e.accountId);s.balance = e.after;s.version++;
}
所有变更都有记录,可以重放,可以异步落盘。
五、日志和快照
WAL 的基本要求
顺序写,先写日志再返回成功:
log.append(event);
apply(event);
日志内容至少包含:业务 key、变更前后值、全局递增序号。
快照怎么做
不要用 JVM heap dump 或 Java 序列化。正确方式是显式序列化业务字段:
for (AccountState s : accounts.values()) {write(s.accountId, s.balance, s.frozen, s.version);
}
明确字段,可跨版本,可校验。
重启恢复流程
加载最新快照↓
定位日志 offset↓
顺序回放 event↓
校验版本号
这个流程必须 100% 自动化完成,不能依赖人工干预。
六、单点故障
主从复制的是 Event
Master: 生成 Event↓
复制 Event↓
Slave: apply(Event)
不是序列化整个 Map,也不是网络同步内存对象。
切主的条件
日志 offset 对齐、内存版本一致。条件不满足,宁可不可用,也不乱切。
双活 / 多活
同一份数据,多实例共同维护。难点在顺序一致性和冲突解决。可以考虑 sofa-jraft 框架,支持主从同步和自动选主。
七、内存不够了怎么办
拆是第一优先级,按用户分片或业务分区。内存系统的扩展能力,90% 靠拆。
热冷分离:热数据留内存,冷数据落 SSD 或 DB。
结构压缩:用 long 替代 BigDecimal(在最小计量单位前提下),用 bit 表示状态,enum 转 int。
堆外内存(Chronicle Map、Unsafe / ByteBuffer)收益高,但复杂度陡增,谨慎使用。
八、Java 常用框架
| 场景 | 框架 | 用途 |
|---|---|---|
| 单机缓存 | Caffeine | 热数据 |
| 事件队列 | Disruptor | 单线程模型 |
| 同步数据 | sofa-jraft | 选主 + 日志同步 |
| 序列化 | protobuf | 数据写入与读取 |
核心状态尽量自己掌控生命周期,不要把它的管理权交给框架。
九、最后
内存化系统的本质是用架构复杂度换性能确定性。
如果已经在做高性能系统,希望这篇少帮你踩几个坑。如果还没到那一步——先把数据库用好,比什么都重要。
<h1>推荐一下我的微信小程序 - “两步动态验证”</h1>
- 有任何建议或需求可以直接联系我
- 免费云端加密备份 :换机不丢失,安全又便捷
- API快速集成 :提供开放API,实现自动化验证码获取
- 多端共享:基于微信小程度,可同时在手机,PC端共同使用,一键复制
- 扫描二维码试用,或微信小程序搜索“两步动态验证”

