如果我的文章对您有帮助,麻烦回复一个赞,给我坚持下去的动力

内存化系统是怎么设计的?

内存化系统是怎么设计的?

只要系统一追求高性能、低延迟,几乎绕不开一个词:内存化系统。不管是撮合引擎、风控、实时账本、排行榜、在线游戏、实时计算,共同点都一样——慢一点就出事的系统,基本都在往内存里放演进。

之前的文章对比了传统架构和内存化架构的区别,以及对账系统的内存化案例。

这篇从工程实践角度聊清楚:内存化系统整体怎么设计,并发度怎么控制,内存数据怎么变更、怎么保证一致,重启之后数据怎么回来,单点故障怎么解,内存不够了怎么办,以及 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-2
        Thread    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 数据写入与读取

核心状态尽量自己掌控生命周期,不要把它的管理权交给框架。


九、最后

内存化系统的本质是用架构复杂度换性能确定性。

如果已经在做高性能系统,希望这篇少帮你踩几个坑。如果还没到那一步——先把数据库用好,比什么都重要。

posted @ 2026-04-28 18:26  无所事事O_o  阅读(7)  评论(0)    收藏  举报