某票务平台在一场顶流歌手演唱会开票的瞬间,5 万个并发请求同时涌入。Web 层有 40 个实例,应用层有 20 个实例,但数据库只有一个主节点。连接池在 3 秒内打满,锁等待队列排到上千,磁盘 I/O 利用率飙到 100%。结果是:70% 的用户看到”系统繁忙”,开票 10 分钟后仍有大量请求超时。
事后复盘,团队发现问题不在 Web 层,也不在应用层——这两层都可以水平扩展。问题在最底下那一层:关系型数据库。你可以把前面两层扩到 100 个实例、1000 个实例,但只要所有实例最终都要读写同一个数据库,数据库就是那个无法绕开的瓶颈。
读写分离能缓解一部分读压力,但写操作仍然集中在主节点。分库分表能分散数据,但引入了分布式事务和跨分片查询的复杂性。缓存能挡住大部分读请求,但缓存一致性本身又是一个难题。这些方案都是在数据库中心架构的框架内打补丁,没有动根本结构。
空间架构(Space-Based Architecture,SBA)的思路完全不同:与其围绕数据库打补丁,不如把数据库从请求的关键路径上移除。数据放在内存里,和处理逻辑放在一起,处理单元可以独立复制和扩展。数据库退化为异步持久化的后备存储,不再参与实时读写。
这篇文章从空间架构的理论源头讲起,拆解它的核心组件和数据策略,深入分析 Hazelcast 和 Apache Ignite 两个主流实现框架,最后用一个票务系统的完整案例说明这套架构在极端并发场景下的设计思路。
导航:上一篇:管道-过滤器架构 | 下一篇:扩展原则
一、为什么数据库会成为瓶颈
传统三层架构的扩展天花板
传统的 Web 应用普遍采用三层架构:表现层(Presentation)、业务逻辑层(Business Logic)、数据持久层(Data Persistence)。前两层是无状态的,扩展方式简单——加机器、加实例、挂负载均衡器。但数据持久层是有状态的,而且通常是集中式的。
这个结构在中低并发场景下工作得很好。当每秒请求数(QPS)从几百涨到几千时,你可以通过加缓存、读写分离来应对。但当 QPS 从几千涨到几万甚至几十万时,三层架构的扩展模型就撞上了天花板。
数据库瓶颈的三个维度
连接池耗尽。 关系型数据库的连接数是有限的。MySQL 默认最大连接数是 151,PostgreSQL 默认是 100。即使调高到几千,每个连接也要消耗内存和 CPU 资源。当应用层有 50 个实例、每个实例的连接池配置 20 个连接时,数据库需要承受 1000 个并发连接。连接池打满后,新请求只能排队等待,延迟急剧上升。
锁竞争。 关系型数据库用锁来保证事务隔离。在高并发写入场景下,多个事务同时修改同一行数据会产生行锁竞争。以库存扣减为例,1000 个并发请求同时扣减同一个商品的库存,每个事务都需要先获取行锁、修改数据、提交事务、释放锁。这个过程是串行的,并发再高也只能一个一个来。
磁盘 I/O。 关系型数据库的持久化依赖磁盘。即使使用 SSD,随机写的 IOPS(Input/Output Operations Per Second)也有上限。当事务日志(WAL/Redo Log)的写入速度跟不上事务提交速度时,整个数据库的吞吐量就被磁盘 I/O 锁死。
读写分离和分库分表的局限
读写分离(Read/Write Splitting)把读请求分发到从库,减轻主库的读压力。但写操作仍然集中在主库,写密集场景下没有本质改善。而且主从复制存在延迟,读从库可能读到过期数据,应用层需要处理数据不一致的问题。
分库分表(Sharding)把数据按某个维度(如用户 ID)分散到多个数据库实例。这确实能分散读写压力,但代价是:
- 跨分片查询变得复杂,性能下降。
- 分布式事务需要两阶段提交(2PC)或 Saga 模式,实现难度大、性能差。
- 数据迁移和再均衡(Rebalancing)是运维噩梦。
- 全局唯一 ID 生成、全局排序等操作不再简单。
本质问题
这些方案都有一个共同的前提:数据库仍然在请求的关键路径上。每一次读写操作,最终都要经过数据库。无论你在前面加了多少层缓存、队列、代理,数据库始终是那个最终的串行化点(Serialization Point)。
空间架构的核心洞察就是:如果数据库是瓶颈,那就把数据库从关键路径上移走。
二、空间架构的理论根基
元组空间:David Gelernter 的 Linda 语言
空间架构的理论源头可以追溯到 1985 年,耶鲁大学的 David Gelernter 发表了论文”Generative Communication in Linda”。Linda 不是一个独立的编程语言,而是一组可以嵌入到其他语言中的协调原语(Coordination Primitives)。
Linda 的核心概念是元组空间(Tuple Space)——一个共享的、关联式的内存空间。进程不直接通信,而是通过向元组空间写入和读取元组(Tuple)来协调工作。三个基本操作:
- out(t):把元组 t 放入空间。
- in(s):从空间中取出匹配模板 s 的元组(阻塞式,取出后元组消失)。
- rd(s):从空间中读取匹配模板 s 的元组(阻塞式,元组不消失)。
这个模型的关键特征是时间解耦和空间解耦。生产者和消费者不需要同时在线(时间解耦),也不需要知道对方的地址(空间解耦)。这两个特征使得系统的组件可以独立扩展和替换。
用一个具体的例子来说明。假设有一个分布式计算任务,需要把一批数据分发给多个 Worker 处理:
// 生产者:把任务放入元组空间
for (int i = 0; i < 1000; i++) {
space.out("task", i, data[i]);
}
// 消费者(多个 Worker 并行运行):从空间中取任务
while (true) {
Tuple t = space.in("task", Integer.class, Object.class);
int taskId = (int) t.get(1);
Object taskData = t.get(2);
Object result = process(taskData);
space.out("result", taskId, result);
}生产者和消费者完全解耦。你可以启动 10 个 Worker,也可以启动 100 个,不需要修改任何代码。这就是元组空间模型的扩展性优势。
JavaSpaces 和 Jini 技术
Sun Microsystems 在 1990 年代末将元组空间的概念引入 Java 生态,推出了 Jini 技术和 JavaSpaces 规范(JSR 393 的前身)。JavaSpaces 提供了一个分布式的、基于 Java 对象的元组空间实现,支持事务和事件通知。
JavaSpaces 的 API 和 Linda 的原语直接对应:
| Linda 操作 | JavaSpaces 方法 | 说明 |
|---|---|---|
| out(t) | write(entry, txn, lease) | 写入一个 Entry 对象 |
| in(s) | take(template, txn, timeout) | 取出匹配的 Entry(阻塞式) |
| rd(s) | read(template, txn, timeout) | 读取匹配的 Entry(阻塞式) |
JavaSpaces 的核心抽象是 Entry——一个实现了 net.jini.core.entry.Entry 接口的 Java 对象。匹配基于字段值的精确匹配,null 字段表示通配。
// 定义一个 Entry
public class OrderEntry implements Entry {
public String orderId;
public String userId;
public Integer status; // 0=待处理, 1=处理中, 2=已完成
public OrderEntry() {}
public OrderEntry(String orderId, String userId, Integer status) {
this.orderId = orderId;
this.userId = userId;
this.status = status;
}
}
// 写入订单
space.write(new OrderEntry("ORD-001", "U-123", 0), null, Lease.FOREVER);
// 读取所有待处理订单(status = 0)
OrderEntry template = new OrderEntry();
template.status = 0;
OrderEntry pending = space.read(template, null, Long.MAX_VALUE);Jini/JavaSpaces 在商业上没有大规模成功,但它证明了一个重要思路:把数据和计算放在同一个共享空间里,系统的扩展性可以通过增加空间的参与者来实现,而不需要修改中心化的数据存储。
空间架构的正式提出
空间架构(Space-Based Architecture)作为一种明确的架构模式,在 Mark Richards 的《Software Architecture Patterns》(O’Reilly,2015)中得到了系统的描述。Richards 将它定义为一种专门解决高并发、高弹性需求的架构模式,核心思想可以归纳为一句话:
消除中心化的数据存储瓶颈,把数据放在内存中与处理逻辑绑定,通过复制处理单元实现水平扩展。
这个思路并不是凭空冒出来的。GigaSpaces(现在叫 GigaSpaces InsightEdge)在 2000 年代初就开始推广基于内存数据网格(In-Memory Data Grid,IMDG)的架构方案。GigaSpaces 的创始人 Nati Shalom 在多次技术会议上阐述了这种架构的设计理念,他的核心论点是:传统架构的扩展性问题不是靠优化数据库能解决的,而是要改变数据在系统中的位置。
三、空间架构的核心组件
空间架构包含两个主要部分:处理单元(Processing Unit)和虚拟化中间件(Virtualized Middleware)。下面用 Mermaid 图展示它们的关系和内部结构:
graph TB
subgraph 客户端
C1[用户请求]
C2[用户请求]
C3[用户请求]
end
subgraph 虚拟化中间件
MG[消息网格<br/>Messaging Grid]
DG[数据网格<br/>Data Grid]
PG[处理网格<br/>Processing Grid]
DM[部署管理器<br/>Deployment Manager]
end
subgraph 处理单元A
BL_A[业务逻辑]
IMDG_A[内存数据网格]
DW_A[数据写入器<br/>Data Writer]
end
subgraph 处理单元B
BL_B[业务逻辑]
IMDG_B[内存数据网格]
DW_B[数据写入器<br/>Data Writer]
end
subgraph 处理单元C
BL_C[业务逻辑]
IMDG_C[内存数据网格]
DW_C[数据写入器<br/>Data Writer]
end
C1 --> MG
C2 --> MG
C3 --> MG
MG --> BL_A
MG --> BL_B
MG --> BL_C
DG --- IMDG_A
DG --- IMDG_B
DG --- IMDG_C
DM --> 处理单元A
DM --> 处理单元B
DM --> 处理单元C
DW_A --> DB[(数据库<br/>异步持久化)]
DW_B --> DB
DW_C --> DB
PG --- BL_A
PG --- BL_B
PG --- BL_C
处理单元(Processing Unit)
处理单元是空间架构的基本部署单位。每个处理单元包含三个核心组件:
业务逻辑模块。 处理具体的业务请求——下单、查询库存、计算价格。这和传统架构中的应用层逻辑没有本质区别,只是它直接读写本地内存中的数据,而不是调用远程数据库。
内存数据网格(In-Memory Data Grid,IMDG)。 每个处理单元内部维护一份数据的内存副本。多个处理单元之间的内存数据通过数据网格保持同步。这是空间架构的核心:数据不在远程数据库里,而在本地内存里。读写延迟从毫秒级(网络 + 磁盘)降到微秒级(本地内存访问)。
数据写入器(Data Writer)。 负责把内存中的数据变更异步写回到持久化存储(通常是关系型数据库或 NoSQL 存储)。这个过程是异步的,不在请求的关键路径上。持久化的目的不是为了日常读写,而是为了系统重启后的数据恢复。
处理单元的关键设计原则是自包含(Self-Contained)。一个处理单元拥有处理请求所需的全部代码和数据,不需要依赖外部数据库来完成业务操作。这意味着你可以启动 5 个处理单元,也可以启动 50 个,每个都能独立处理请求。
虚拟化中间件(Virtualized Middleware)
虚拟化中间件负责管理多个处理单元的协调工作,它包含四个子组件:
消息网格(Messaging Grid)。 管理用户请求的路由和会话。当一个请求进入系统时,消息网格决定把它转发给哪个处理单元。路由策略可以是轮询(Round Robin)、基于内容的路由(Content-Based Routing)、或基于会话亲和性(Session Affinity)的路由。
消息网格的一个重要职责是会话管理。在传统架构中,会话通常存储在 Web 服务器的本地内存或集中式的会话存储(如 Redis)中。在空间架构中,会话数据随处理单元一起存储在内存数据网格中,会话亲和性确保同一用户的请求被路由到同一个处理单元,避免了跨节点的会话同步问题。
数据网格(Data Grid)。 这是空间架构中最关键的中间件组件。数据网格负责管理数据在多个处理单元之间的复制和同步。当处理单元 A 修改了一条数据,数据网格负责把这个变更传播到处理单元 B 和 C 的内存数据网格中。
数据网格的核心挑战是一致性和性能的权衡。同步复制(Synchronous Replication)保证强一致性,但增加了请求延迟;异步复制(Asynchronous Replication)性能好,但在复制完成前可能读到旧数据。不同的业务场景需要不同的一致性级别——库存扣减需要强一致性,用户浏览记录只需要最终一致性。
处理网格(Processing Grid)。 当一个请求的处理需要跨多个处理单元协调时——比如一个分布式聚合查询——处理网格负责把任务分发到相关的处理单元,收集结果,合并返回。这类似于 MapReduce 的思路:把计算推送到数据所在的节点,而不是把数据拉到计算节点。
部署管理器(Deployment Manager)。 监控处理单元的健康状态和系统负载,负责处理单元的动态启停和弹性伸缩。当负载上升时,部署管理器启动新的处理单元;当负载下降时,关闭多余的处理单元。这个过程需要和数据网格配合——新启动的处理单元需要从数据网格同步数据,关闭的处理单元需要先把内存数据持久化。
四、数据复制与分区策略
数据管理是空间架构中最复杂的部分。核心问题是:多个处理单元都在各自的内存中维护数据,怎么保证这些数据的一致性?
全量复制 vs 分区复制
全量复制(Full Replication)。 每个处理单元持有全部数据的完整副本。任何一个处理单元的数据变更都要同步到所有其他处理单元。
优点:任何处理单元都能处理任何请求,路由策略可以是简单的轮询。读操作完全本地化,零网络开销。
缺点:数据量受单节点内存限制。写操作的同步开销随处理单元数量线性增长——如果有 20 个处理单元,一次写操作需要同步到 19 个节点。数据量大时不可行。
分区复制(Partitioned Replication)。 数据按某个维度(如用户 ID 的哈希值)分成多个分区(Partition),每个处理单元只持有部分分区的数据。每个分区可以有一个主副本(Primary)和若干备份副本(Backup)。
优点:数据量不再受单节点内存限制,可以通过增加处理单元来扩展数据容量。写操作的同步范围限定在分区的备份节点内,不需要广播到所有节点。
缺点:请求路由变得复杂——消息网格需要知道请求涉及的数据在哪个分区,路由到对应的处理单元。跨分区操作(如跨用户的聚合查询)需要处理网格来协调。
分区策略
哈希分区(Hash Partitioning)。 对分区键(Partition Key)计算哈希值,再对分区数量取模,决定数据归属哪个分区。Hazelcast 默认使用 271 个分区,对键的 hashCode 取模来分配。
partition = hash(key) % partitionCount
哈希分区的优势是数据分布均匀,不需要维护分区边界的元数据。劣势是范围查询需要扫描所有分区。
范围分区(Range Partitioning)。 按照键的值域范围划分。例如,用户 ID 0-99999 在分区 1,100000-199999 在分区 2。范围查询效率高,但容易出现数据倾斜(Hot Partition)——如果新注册用户比老用户活跃,最后一个分区的负载会远高于前面的分区。
自定义分区(Custom Partitioning)。 根据业务规则决定数据归属。比如在票务系统中,按演出场次分区——同一场演出的所有座位数据在同一个分区中,这样座位锁定和库存扣减不需要跨分区操作。
一致性模型
强一致性(Strong Consistency)。 写操作在所有副本都确认后才返回成功。保证任何时刻从任何副本读到的都是最新数据。代价是写延迟等于最慢副本的响应时间。
在空间架构中,强一致性通常通过同步复制(Synchronous Replication)实现。Hazelcast 的 IMap 默认使用同步备份——一次 put 操作会等待备份节点确认后才返回。
最终一致性(Eventual Consistency)。 写操作在主副本确认后即返回成功,变更异步传播到其他副本。在短暂的时间窗口内,不同副本可能返回不同的数据。但最终所有副本会收敛到一致状态。
最终一致性的时间窗口长度取决于网络延迟和复制策略。在同机房的局域网环境下,异步复制的延迟通常在毫秒级。
冲突解决机制
当使用最终一致性模型时,可能出现两个处理单元同时修改同一条数据的情况。常见的冲突解决策略包括:
- 最后写入胜出(Last-Writer-Wins,LWW)。 以时间戳最晚的写操作为准。简单但可能丢失数据。要求各节点时钟同步(通常使用 NTP 或逻辑时钟)。
- 合并函数(Merge Function)。 由业务逻辑定义合并规则。例如,计数器冲突时取两个增量的和,而不是选一个值覆盖另一个。
- 向量时钟(Vector Clock)。 用版本向量记录每个节点的修改历史,检测并发冲突后交由应用层处理。Amazon DynamoDB 的早期版本使用了这种方案。
数据持久化策略
空间架构中的持久化不是为了日常读写,而是为了灾难恢复(Disaster Recovery)和冷启动时的数据加载。两种主要模式:
异步写回(Write-Behind)。 数据变更先写入内存,累积到一定量或间隔一定时间后批量写入数据库。优点是写操作的延迟不受数据库影响,批量写入效率高。缺点是在批量写入完成前如果节点故障,可能丢失这批未持久化的数据。
// Hazelcast Write-Behind 配置示例
MapStoreConfig mapStoreConfig = new MapStoreConfig();
mapStoreConfig.setClassName("com.example.OrderMapStore");
mapStoreConfig.setWriteDelaySeconds(5); // 延迟 5 秒批量写入
mapStoreConfig.setWriteBatchSize(100); // 每批 100 条
mapStoreConfig.setWriteCoalescing(true); // 合并同 key 的多次写入
MapConfig mapConfig = new MapConfig("orders");
mapConfig.setMapStoreConfig(mapStoreConfig);写穿(Write-Through)。 每次数据变更都同步写入数据库。保证持久化存储和内存数据的一致性,但写延迟增加了数据库的往返时间。适用于数据丢失容忍度极低的场景。
在实际生产环境中,Write-Behind 是更常见的选择。通过配合数据网格的备份副本,即使一个节点故障,数据也不会丢失(因为备份节点持有相同的数据)。只有当多个副本同时故障时,未持久化的数据才会丢失——而这通常通过跨机架或跨机房部署来避免。
五、Hazelcast 架构剖析
Hazelcast 是目前最流行的内存数据网格(IMDG)框架之一,也是构建空间架构最常用的基础设施。它是一个开源的分布式计算平台,用 Java 编写,支持嵌入式和客户端-服务器两种部署模式。
核心架构
嵌入式模式(Embedded Mode)。 Hazelcast 实例作为应用进程的一部分运行。每个应用实例内嵌一个 Hazelcast 节点,多个节点自动发现彼此并组成集群。数据存储在应用进程的 JVM 堆内存中,访问延迟最低(本地方法调用级别)。
这种模式天然适合空间架构——每个处理单元就是一个内嵌了 Hazelcast 的应用实例,业务逻辑直接访问本地的 Hazelcast 数据结构,不需要网络往返。
// 嵌入式模式:Hazelcast 实例和应用在同一个 JVM 中
Config config = new Config();
config.setClusterName("ticket-system");
// 配置网络发现
config.getNetworkConfig()
.getJoin()
.getMulticastConfig()
.setEnabled(false);
config.getNetworkConfig()
.getJoin()
.getTcpIpConfig()
.setEnabled(true)
.addMember("10.0.1.1")
.addMember("10.0.1.2")
.addMember("10.0.1.3");
HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
// 直接使用分布式数据结构
IMap<String, SeatInventory> seatMap = hz.getMap("seats");
seatMap.put("SHOW-001-A01", new SeatInventory("A01", true));客户端-服务器模式(Client-Server Mode)。 Hazelcast 节点作为独立的服务集群运行,应用通过轻量级客户端连接到集群。数据存储和管理在服务端集群中,客户端负责请求转发。
这种模式的优势是数据层和应用层可以独立扩展和部署。但引入了一次网络往返,延迟高于嵌入式模式。在空间架构的语境下,客户端-服务器模式更接近传统的”应用层 + 缓存层”架构,而非纯粹的空间架构。
分布式数据结构
Hazelcast 提供了一系列分布式数据结构,API 设计尽量贴近 Java 标准集合接口:
IMap(分布式 Map)。 实现了 java.util.concurrent.ConcurrentMap 接口。数据按键的哈希值分布到 271 个分区中,每个分区有一个主副本和可配置数量的备份副本。支持 EntryProcessor 进行原子的读-改-写操作。
IMap<String, Order> orderMap = hz.getMap("orders");
// 原子操作:使用 EntryProcessor 避免竞态条件
orderMap.executeOnKey("ORD-001", (EntryProcessor<String, Order, Object>) entry -> {
Order order = entry.getValue();
if (order.getStatus() == OrderStatus.PENDING) {
order.setStatus(OrderStatus.CONFIRMED);
entry.setValue(order);
return true;
}
return false;
});IQueue(分布式队列)。 实现了 java.util.concurrent.BlockingQueue 接口。与 IMap 不同,IQueue 的数据不做分区——整个队列存储在集群的一个节点上(有备份)。适用于生产者-消费者模式,但不适合高吞吐场景(单节点瓶颈)。
ITopic(分布式发布-订阅)。 提供发布-订阅消息模式。消息会被广播到所有订阅者节点。适合事件通知场景,如”库存变更通知”、“价格更新通知”。
Near Cache 机制
Near Cache 是 Hazelcast 在客户端-服务器模式下提供的本地缓存机制。当客户端频繁读取某些数据时,可以在客户端本地维护一份缓存副本,避免每次都通过网络访问服务端。
// Near Cache 配置
NearCacheConfig nearCacheConfig = new NearCacheConfig();
nearCacheConfig.setName("seats");
nearCacheConfig.setInMemoryFormat(InMemoryFormat.OBJECT);
nearCacheConfig.setInvalidateOnChange(true); // 数据变更时失效本地缓存
nearCacheConfig.setTimeToLiveSeconds(300);
nearCacheConfig.setMaxIdleSeconds(60);
nearCacheConfig.getEvictionConfig()
.setEvictionPolicy(EvictionPolicy.LRU)
.setMaxSizePolicy(MaxSizePolicy.ENTRY_COUNT)
.setSize(10000);
ClientConfig clientConfig = new ClientConfig();
clientConfig.addNearCacheConfig(nearCacheConfig);Near Cache 本质上是一个 L1 缓存(客户端本地)与 L2 缓存(服务端集群)的分层结构。invalidateOnChange 选项确保当服务端数据变更时,所有客户端的 Near Cache 中对应的条目会被失效。但失效是异步的,所以 Near Cache 只能提供最终一致性。
CP Subsystem:基于 Raft 的强一致性
Hazelcast 4.0 引入了 CP Subsystem(CP 子系统),基于 Raft 共识算法(Consensus Algorithm)实现强一致性的数据结构。CP 代表 CAP 定理中的 C(Consistency)和 P(Partition tolerance)——在网络分区时优先保证一致性,拒绝可能返回不一致数据的请求。
CP Subsystem 提供的数据结构包括 CPMap、AtomicLong、AtomicReference、FencedLock、CountDownLatch 和 Semaphore。这些结构适用于需要严格一致性保证的场景,如分布式锁、全局计数器、Leader 选举。
// 使用 CP Subsystem 的分布式锁
FencedLock lock = hz.getCPSubsystem().getLock("seat-lock-A01");
long fence = lock.lockAndGetFence();
try {
// 在持有锁的情况下执行座位预订逻辑
SeatInventory seat = seatMap.get("SHOW-001-A01");
if (seat.isAvailable()) {
seat.setAvailable(false);
seat.setHolderId(userId);
seatMap.put("SHOW-001-A01", seat);
}
} finally {
lock.unlock();
}CP Subsystem 的 Raft 实现要求至少 3 个 CP 成员才能工作(需要多数派同意)。它和 Hazelcast 主集群的 AP 数据结构(IMap、IQueue 等)是独立的——CP 数据结构走 Raft 协议,AP 数据结构走 Hazelcast 自己的分区复制协议。
六、Apache Ignite 架构剖析
Apache Ignite 是另一个主流的内存计算平台,最初由 GridGain Systems 开发,2014 年进入 Apache 孵化器,2015 年成为顶级项目。和 Hazelcast 相比,Ignite 的功能覆盖面更广,除了内存数据网格之外,还提供了分布式 SQL 引擎、计算网格、机器学习库等。
内存优先(Memory-First)架构
Ignite 的设计哲学是”内存优先”——数据默认存储在内存中,磁盘持久化是可选的。和 Hazelcast 类似,Ignite 把数据分布到集群的多个节点上,支持分区和复制两种数据分布模式。
Ignite 的内存管理有一个独特的设计:它不依赖 JVM 的堆内存(On-Heap),而是使用堆外内存(Off-Heap Memory)。这避免了 Java GC(Garbage Collection)在大数据量下的长时间停顿问题。数据以二进制格式存储在堆外内存区域中,只有在应用代码访问时才反序列化为 Java 对象。
// Ignite 配置:内存区域设置
DataStorageConfiguration storageCfg = new DataStorageConfiguration();
// 配置默认内存区域:2GB 初始大小,8GB 最大大小
DataRegionConfiguration defaultRegion = new DataRegionConfiguration();
defaultRegion.setName("default");
defaultRegion.setInitialSize(2L * 1024 * 1024 * 1024); // 2 GB
defaultRegion.setMaxSize(8L * 1024 * 1024 * 1024); // 8 GB
defaultRegion.setPersistenceEnabled(false); // 纯内存模式
storageCfg.setDefaultDataRegionConfiguration(defaultRegion);
IgniteConfiguration cfg = new IgniteConfiguration();
cfg.setDataStorageConfiguration(storageCfg);
Ignite ignite = Ignition.start(cfg);分布式 SQL 引擎
Ignite 最显著的差异化特性之一是它内置的分布式 SQL 引擎。你可以用标准的 SQL 语句查询分布在多个节点上的内存数据,就像查询普通的关系型数据库一样。引擎基于 Apache Calcite 构建(Ignite 3.x),支持大部分 ANSI SQL 语法。
// 定义缓存(表)的 Schema
CacheConfiguration<Long, Order> orderCacheCfg = new CacheConfiguration<>("orders");
orderCacheCfg.setIndexedTypes(Long.class, Order.class);
orderCacheCfg.setCacheMode(CacheMode.PARTITIONED);
orderCacheCfg.setBackups(1);
IgniteCache<Long, Order> orderCache = ignite.getOrCreateCache(orderCacheCfg);
// 使用 SQL 查询
SqlFieldsQuery query = new SqlFieldsQuery(
"SELECT orderId, userId, totalAmount " +
"FROM Order " +
"WHERE status = ? AND totalAmount > ? " +
"ORDER BY totalAmount DESC"
);
query.setArgs("CONFIRMED", 1000.0);
List<List<?>> results = orderCache.query(query).getAll();
for (List<?> row : results) {
System.out.println("Order: " + row.get(0) + ", User: " + row.get(1) +
", Amount: " + row.get(2));
}SQL 查询在分布式环境下的执行逻辑:Ignite 将查询拆分为 Map 阶段和 Reduce 阶段。Map 阶段在每个持有相关数据分区的节点上执行,返回局部结果;Reduce 阶段在查询发起节点上合并局部结果。对于带有 JOIN 的查询,如果 JOIN 的两个表按相同的键分区(Collocated Join),JOIN 可以在本地完成,效率很高。如果分区键不同(Non-Collocated Join),需要跨节点传输数据,性能可能显著下降。
Ignite Persistence(原生持久化)
Ignite 2.1 开始提供原生的磁盘持久化功能(Ignite Persistence)。启用后,数据同时存储在内存和磁盘上。内存作为热数据的高速访问层,磁盘作为完整数据集的持久化存储。当数据量超过内存容量时,冷数据可以从内存中驱逐,但仍然可以从磁盘读取。
// 启用 Ignite Persistence
DataRegionConfiguration persistentRegion = new DataRegionConfiguration();
persistentRegion.setName("persistent-region");
persistentRegion.setInitialSize(1L * 1024 * 1024 * 1024);
persistentRegion.setMaxSize(4L * 1024 * 1024 * 1024);
persistentRegion.setPersistenceEnabled(true); // 启用持久化
storageCfg.setDataRegionConfigurations(persistentRegion);启用持久化后,Ignite 使用预写日志(Write-Ahead Log,WAL)来保证数据的持久性。每次数据变更先写入 WAL,再更新内存页面。WAL 定期进行检查点(Checkpoint),把脏页面(修改过但未写入数据文件的页面)刷到磁盘上的数据文件中。
这个设计和传统数据库的持久化机制非常相似(MySQL 的 InnoDB 引擎也使用 WAL + Checkpoint)。区别在于 Ignite 的主要读写路径仍然是内存,磁盘持久化是后台异步进行的。
计算网格(Compute Grid)
Ignite 的计算网格允许你把计算任务推送到数据所在的节点上执行,而不是把数据拉到计算节点。这在处理大量数据时可以显著减少网络传输。
// 将计算推送到数据所在节点
IgniteCompute compute = ignite.compute();
// 在所有节点上执行统计任务
Collection<Integer> results = compute.broadcast(() -> {
IgniteCache<Long, Order> cache = Ignition.localIgnite().cache("orders");
// 只处理本地分区的数据
int localCount = 0;
try (QueryCursor<Cache.Entry<Long, Order>> cursor =
cache.query(new ScanQuery<Long, Order>().setLocal(true))) {
for (Cache.Entry<Long, Order> entry : cursor) {
if (entry.getValue().getStatus().equals("CONFIRMED")) {
localCount++;
}
}
}
return localCount;
});
int totalConfirmed = results.stream().mapToInt(Integer::intValue).sum();
System.out.println("Total confirmed orders: " + totalConfirmed);更精细的控制可以使用 affinityRun 或 affinityCall,将计算任务发送到持有特定数据分区的节点:
// 把计算发送到持有 key=orderId 的数据所在的节点
ignite.compute().affinityRun("orders", orderId, () -> {
IgniteCache<Long, Order> cache = Ignition.localIgnite().cache("orders");
Order order = cache.localPeek(orderId, CachePeekMode.ALL);
// 在本地处理这个订单——零网络开销
processOrder(order);
});Hazelcast vs Ignite 对比
下面从几个关键维度对比两个框架:
| 维度 | Hazelcast | Apache Ignite |
|---|---|---|
| 核心定位 | 内存数据网格 + 流处理 | 内存计算平台(数据网格 + SQL + 计算) |
| 部署模式 | 嵌入式 / 客户端-服务器 | 嵌入式 / 客户端-服务器 / 独立集群 |
| 内存管理 | 堆内存为主,支持 HD Memory(堆外) | 堆外内存(Off-Heap)为主 |
| SQL 支持 | Jet SQL(流式查询为主) | 完整的分布式 SQL 引擎(基于 Calcite) |
| 原生持久化 | 无(依赖 MapStore 写回外部存储) | 有(Ignite Persistence,WAL + Checkpoint) |
| 强一致性 | CP Subsystem(基于 Raft) | 事务模式(PESSIMISTIC + SERIALIZABLE) |
| 计算能力 | Jet 流处理引擎 | 计算网格(affinityRun 等) |
| 社区与生态 | 社区活跃,文档清晰 | Apache 项目,功能全但学习曲线陡 |
| 适用场景 | 缓存、会话管理、简单的空间架构 | 需要 SQL 查询、大数据量、计算密集型场景 |
| 许可证 | Apache 2.0(社区版) | Apache 2.0 |
选择建议:
- 如果你的需求以缓存和简单的键值操作为主,Hazelcast 的 API 更简洁,上手更快。
- 如果你需要对内存数据执行复杂的 SQL 查询,或者数据量超过单节点内存需要磁盘持久化支撑,Ignite 的功能覆盖更全。
- 两者都可以作为空间架构的基础设施。在纯粹的空间架构模式下(嵌入式、内存数据 + 异步持久化),Hazelcast 的嵌入式模式通常是更轻量的选择。
七、案例分析:票务系统的空间架构
回到文章开头的场景:大型演出票务系统在开票瞬间面临的极端并发问题。下面用一个完整的案例来说明空间架构如何解决这个问题。
业务场景
一场大型演出,座位数 5 万个,分为 5 个区域(A-E),每个区域 1 万个座位。开票时间固定,所有用户在同一时刻涌入。根据历史数据,开票前 10 秒的并发请求量预估为 8 万 QPS,其中:
- 座位查询请求占 60%(4.8 万 QPS)
- 选座锁定请求占 30%(2.4 万 QPS)
- 支付确认请求占 10%(0.8 万 QPS)
传统架构的瓶颈
用传统的三层架构 + MySQL 来处理这个场景:
- Web 层 50 个实例,每个实例处理 1600 QPS——没有问题。
- App 层 30 个实例,每个实例处理 2700 QPS——没有问题。
- MySQL 单主节点,连接池上限 2000。30 个 App 实例 * 每实例
50 个连接 = 1500 个数据库连接。连接数勉强够,但:
- 选座锁定需要行锁(UPDATE seats SET status=‘locked’ WHERE seat_id = ? AND status=‘available’),2.4 万 QPS 的写操作在热点行上产生严重的锁竞争。
- 即使做了分库分表(按区域分 5 个库),每个库仍要承受约 4800 QPS 的写操作,锁竞争依然存在。
- 同一区域热门座位(如前排中央)的锁竞争尤为严重。
空间架构方案设计
分区策略。 按区域分区,5 个区域对应 5 组处理单元。每组处理单元负责一个区域的全部座位数据。每组部署 3 个处理单元实例,形成 1 主 2 备的分区副本。
# 处理单元部署配置
processing-units:
- name: "pu-zone-a"
replicas: 3
data-scope: "zone-A"
seats-range: "A-00001 ~ A-10000"
memory: "4GB"
- name: "pu-zone-b"
replicas: 3
data-scope: "zone-B"
seats-range: "B-00001 ~ B-10000"
memory: "4GB"
- name: "pu-zone-c"
replicas: 3
data-scope: "zone-C"
seats-range: "C-00001 ~ C-10000"
memory: "4GB"
- name: "pu-zone-d"
replicas: 3
data-scope: "zone-D"
seats-range: "D-00001 ~ D-10000"
memory: "4GB"
- name: "pu-zone-e"
replicas: 3
data-scope: "zone-E"
seats-range: "E-00001 ~ E-10000"
memory: "4GB"处理单元内部结构。 每个处理单元是一个嵌入了 Hazelcast 的 Spring Boot 应用,内存中维护该区域所有座位的状态数据。
@Component
public class SeatProcessingUnit {
private final HazelcastInstance hazelcast;
private final IMap<String, SeatState> seatMap;
private final FencedLock zoneLock;
public SeatProcessingUnit(HazelcastInstance hazelcast) {
this.hazelcast = hazelcast;
this.seatMap = hazelcast.getMap("seats");
this.zoneLock = hazelcast.getCPSubsystem().getLock("zone-lock");
}
/**
* 查询座位状态——直接读本地内存,无网络和磁盘 I/O
*/
public SeatState querySeat(String seatId) {
return seatMap.get(seatId);
}
/**
* 查询区域内所有可用座位
*/
public List<SeatState> queryAvailableSeats(String zone) {
Predicate<String, SeatState> predicate = Predicates.and(
Predicates.equal("zone", zone),
Predicates.equal("status", SeatStatus.AVAILABLE)
);
return new ArrayList<>(seatMap.values(predicate));
}
/**
* 锁定座位——使用 EntryProcessor 保证原子性
*/
public LockResult lockSeat(String seatId, String userId) {
return seatMap.executeOnKey(seatId,
(EntryProcessor<String, SeatState, LockResult>) entry -> {
SeatState seat = entry.getValue();
if (seat == null) {
return LockResult.SEAT_NOT_FOUND;
}
if (seat.getStatus() != SeatStatus.AVAILABLE) {
return LockResult.ALREADY_LOCKED;
}
seat.setStatus(SeatStatus.LOCKED);
seat.setHolderId(userId);
seat.setLockTime(System.currentTimeMillis());
entry.setValue(seat);
return LockResult.SUCCESS;
}
);
}
/**
* 确认购买——将状态从 LOCKED 改为 SOLD
*/
public boolean confirmPurchase(String seatId, String userId) {
return (boolean) seatMap.executeOnKey(seatId,
(EntryProcessor<String, SeatState, Boolean>) entry -> {
SeatState seat = entry.getValue();
if (seat == null) return false;
if (seat.getStatus() != SeatStatus.LOCKED) return false;
if (!userId.equals(seat.getHolderId())) return false;
seat.setStatus(SeatStatus.SOLD);
seat.setSoldTime(System.currentTimeMillis());
entry.setValue(seat);
return true;
}
);
}
}消息网格路由。 用户请求经过负载均衡器后,由消息网格根据请求中的区域信息路由到对应的处理单元组。路由逻辑如下:
@Component
public class SeatRequestRouter {
private final Map<String, List<String>> zoneToEndpoints;
/**
* 根据座位 ID 提取区域信息,路由到对应的处理单元
*/
public String route(String seatId) {
String zone = seatId.substring(0, 1); // "A-00001" -> "A"
List<String> endpoints = zoneToEndpoints.get(zone);
if (endpoints == null || endpoints.isEmpty()) {
throw new NoAvailableProcessingUnitException(zone);
}
// 在同组的处理单元之间轮询
int index = Math.abs(seatId.hashCode()) % endpoints.size();
return endpoints.get(index);
}
}避免超卖的一致性保证
超卖(Overselling)是票务系统最严重的业务错误。空间架构通过以下机制避免超卖:
1. EntryProcessor 的原子性保证。 Hazelcast 的 EntryProcessor 在数据所在的分区主节点上以原子方式执行。对同一个 key 的多个 EntryProcessor 请求会被串行化执行。这意味着对同一个座位的并发锁定请求不会产生竞态条件——第一个请求成功锁定后,后续请求会看到状态已经是 LOCKED,返回失败。
2. 同步备份。 IMap 配置为同步备份(sync-backup-count = 1),写操作在主副本和备份副本都确认后才返回。即使主节点在 EntryProcessor 执行后立刻故障,备份节点上已经有最新的数据,不会丢失锁定状态。
// 同步备份配置
MapConfig seatMapConfig = new MapConfig("seats");
seatMapConfig.setBackupCount(1); // 1 个同步备份
seatMapConfig.setAsyncBackupCount(0); // 不使用异步备份
seatMapConfig.setReadBackupData(true); // 允许从备份读取(提高读吞吐)3. 锁定超时机制。 用户锁定座位后有 15 分钟完成支付。处理单元中运行一个定时任务,每 30 秒扫描一次所有 LOCKED 状态的座位,超时未支付的自动释放:
@Scheduled(fixedRate = 30000)
public void releaseExpiredLocks() {
long now = System.currentTimeMillis();
long timeout = 15 * 60 * 1000; // 15 分钟
Predicate<String, SeatState> expired = Predicates.and(
Predicates.equal("status", SeatStatus.LOCKED),
Predicates.lessThan("lockTime", now - timeout)
);
Set<String> expiredKeys = seatMap.keySet(expired);
for (String seatId : expiredKeys) {
seatMap.executeOnKey(seatId,
(EntryProcessor<String, SeatState, Void>) entry -> {
SeatState seat = entry.getValue();
if (seat != null && seat.getStatus() == SeatStatus.LOCKED
&& seat.getLockTime() < now - timeout) {
seat.setStatus(SeatStatus.AVAILABLE);
seat.setHolderId(null);
seat.setLockTime(0);
entry.setValue(seat);
}
return null;
}
);
}
}弹性伸缩
票务系统的流量模式非常特殊:开票前 5 分钟到开票后 30 分钟是峰值期,其他时间流量很低。空间架构配合弹性伸缩策略,可以在峰值期自动扩容,峰值后自动缩容:
- 开票前 10 分钟:部署管理器将每组处理单元从 3 实例扩展到 8 实例。新实例启动后自动加入 Hazelcast 集群,数据网格自动重新分配分区,新实例获得部分数据的副本。
- 开票后 30 分钟:流量回落到正常水平。部署管理器将每组处理单元从 8 实例缩减到 3 实例。缩减前先触发数据重分配,确保被关闭的实例上的数据已经迁移到存活的实例上。
- 扩容预热:新实例启动后不立刻接受请求,而是先从现有实例同步数据。数据同步完成(Hazelcast 的 migration 事件结束)后,消息网格才将请求路由到新实例。
性能数据与效果分析
下面的数据基于类似票务场景的压力测试结果,给出数量级的参考(具体数值与硬件配置、网络条件相关):
| 指标 | 传统架构(MySQL + Redis) | 空间架构(Hazelcast IMDG) |
|---|---|---|
| 座位查询 P99 延迟 | 15-50 ms(缓存命中时 2-5 ms) | 0.1-0.5 ms(本地内存读取) |
| 座位锁定 P99 延迟 | 30-200 ms(含锁等待) | 1-5 ms(EntryProcessor 原子操作) |
| 最大持续写入 QPS | ~8000(单主节点瓶颈) | ~50000(5 组 * 每组处理主分区约 10000) |
| 扩容时间 | 分钟级(需要连接池调整、缓存预热) | 30-60 秒(Hazelcast 分区迁移) |
| 超卖风险 | 需要额外的分布式锁机制 | EntryProcessor 原子保证 |
需要说明的是,上述数据是量级参考,不是绝对值。实际效果受硬件规格(CPU 核数、内存大小、网络带宽)、JVM 配置(堆大小、GC 策略)、数据量(座位总数、并发用户数)等因素影响。空间架构的核心优势不在于某个具体指标提升了多少倍,而在于它把数据库从请求关键路径上移除后,系统的扩展能力不再受数据库连接数和锁竞争的限制。
八、空间架构 vs 传统架构
三种架构方案的 Trade-off 对比
| 维度 | 传统数据库中心架构 | 微服务 + 分布式缓存 | 空间架构(SBA) |
|---|---|---|---|
| 扩展能力 | 垂直扩展为主,水平扩展受限于数据库 | 应用层水平扩展,缓存层水平扩展,数据库仍是瓶颈 | 近线性水平扩展,数据库不在关键路径 |
| 数据一致性 | 强一致性(ACID 事务) | 最终一致性(缓存与数据库之间) | 可配置(强一致性到最终一致性) |
| 运维复杂度 | 低(成熟的运维体系) | 中(缓存失效、缓存穿透、缓存雪崩) | 高(数据网格运维、网络分区处理、脑裂) |
| 基础设施成本 | 低到中(数据库 + 应用服务器) | 中(应用 + 缓存集群 + 数据库) | 高(大量内存、多处理单元实例) |
| 开发复杂度 | 低(SQL + ORM,开发者熟悉) | 中(需要处理缓存一致性逻辑) | 高(需要理解数据网格、分区、复制) |
| 故障恢复 | 依赖数据库备份和主从切换 | 缓存故障时回退到数据库(可能导致雪崩) | 备份副本自动接管,冷启动从持久化存储恢复 |
| 适用场景 | CRUD 应用、中低并发业务系统 | 读多写少、可容忍缓存不一致的场景 | 极端并发、弹性伸缩、延迟敏感的场景 |
| 读延迟 | 毫秒级(磁盘/缓存命中依赖) | 微秒级(缓存命中)/ 毫秒级(未命中) | 微秒级(本地内存) |
| 写延迟 | 毫秒级(磁盘 I/O) | 毫秒级(同步写数据库 + 更新缓存) | 微秒到毫秒级(内存写入 + 同步备份) |
| 数据容量 | 受磁盘容量限制(TB 级) | 受磁盘容量限制(TB 级) | 受集群总内存限制(通常 GB 到低 TB 级) |
什么时候空间架构是正确选择
空间架构适合以下场景:
极端并发的突发流量。 如票务开售、电商秒杀、在线考试开考。这些场景的特征是:流量在短时间内暴增数十倍甚至数百倍,传统数据库完全无法承受。空间架构可以在峰值前快速扩容,峰值后缩容,内存中处理请求的延迟远低于数据库。
延迟敏感的实时系统。 如金融交易系统(低延迟要求)、在线游戏的状态同步、实时竞价(RTB,Real-Time Bidding)系统。这些场景要求单次请求的处理延迟在个位数毫秒甚至微秒级,数据库的磁盘 I/O 延迟无法满足。
可预测的数据集大小。 空间架构的数据存储受限于集群的总内存。如果数据集的大小是可预测的(如一场演出的座位数、一批商品的库存),空间架构可以精确规划内存容量。反之,如果数据量不可预测且持续增长(如社交媒体的帖子数据),空间架构不合适。
什么时候空间架构是过度设计
中低并发的 CRUD 系统。 如果你的系统日常 QPS 在几百到几千,一个做了读写分离和适当缓存的传统架构就够了。空间架构带来的额外复杂度(数据网格管理、分区策略设计、冲突解决)远超收益。
写操作不频繁的读密集系统。 如果系统的主要负载是读操作,一层 Redis 缓存就能解决大部分问题。空间架构在读密集场景下和”应用 + 缓存”架构没有本质区别,但运维复杂度高得多。
数据量远超内存容量的系统。 如果你的数据集是 TB 级甚至 PB 级(如日志系统、数据仓库),空间架构不适合作为主架构。虽然 Ignite 支持磁盘持久化来扩展容量,但那本质上退化为一个带内存缓存的分布式数据库,失去了空间架构”内存优先”的核心优势。
需要复杂事务的系统。 如果你的业务逻辑依赖跨多个数据实体的 ACID 事务(如银行转账的两个账户必须同时成功或同时失败),空间架构的分区数据模型会让分布式事务变得非常复杂。虽然 Hazelcast 和 Ignite 都支持某种形式的分布式事务,但性能和可靠性远不如单机数据库事务。
九、空间架构的局限与挑战
内存成本
内存的单位成本远高于磁盘。截至目前,服务器级 DDR5 内存的价格大约是企业级 NVMe SSD 的 8-15 倍(按每 GB 计算)。如果你的数据集是 500 GB,使用空间架构(考虑备份副本,总内存需要 1 TB 以上)的存储成本可能是传统数据库方案的 10 倍以上。
这不是说空间架构一定更贵——它可能在计算资源(更少的应用服务器实例、不需要数据库服务器)和运维成本(更少的数据库运维工作)上有所节省。但内存成本是一个显著的考量因素,尤其在数据量较大的场景下。
数据容量上限
虽然可以通过增加节点来扩展集群的总内存,但内存容量终归有物理上限。单台服务器的最大内存通常在 512 GB 到 2 TB(取决于主板和 CPU 的支持)。一个 20 节点的集群最多提供 10-40 TB 的原始内存容量,考虑备份副本后实际可用容量减半。
对于数据量持续增长的业务,空间架构需要配合数据归档策略——将历史数据定期迁移到外部存储(如对象存储或数据仓库),只在内存中保留活跃数据。
网络分区与脑裂
在分布式系统中,网络分区(Network Partition)是必须面对的问题。当网络分区把集群分成两个子集群时,可能出现脑裂(Split-Brain)——两个子集群都认为自己是”正确”的,各自独立处理请求,导致数据分叉。
Hazelcast 和 Ignite 都提供了脑裂检测和恢复机制:
- Hazelcast 使用成员数量检查(Quorum/Split-Brain Protection)——如果一个子集群的成员数量低于阈值,该子集群会拒绝写操作。恢复后,较小的子集群合并到较大的子集群中,冲突数据按配置的策略解决。
- Ignite 使用基线拓扑(Baseline Topology)来管理集群成员变更,网络分区后通过通信恢复自动合并集群。
但脑裂恢复的数据合并策略本质上是”选择哪边的数据”或”丢弃哪边的修改”——在业务层面,这可能意味着数据丢失或不一致。运维团队需要理解这些机制,制定应急预案。
事务支持的局限
空间架构中的数据分布在多个分区和节点上,跨分区的事务需要分布式事务协议(如两阶段提交)来保证。分布式事务的问题在于:
- 性能低:需要多轮网络通信。
- 可用性差:任何参与者节点故障都可能导致事务阻塞。
- 锁持有时间长:事务期间锁定的资源在所有参与者都确认后才释放。
因此,空间架构中的最佳实践是尽量把相关数据放在同一个分区中(Collocated Data),让大部分操作在单分区内完成,避免跨分区事务。这要求在系统设计初期就仔细规划分区策略。
团队学习曲线
空间架构引入了一系列传统 Web 开发中不常见的概念:数据分区、复制一致性、EntryProcessor、Near Cache、脑裂处理、集群成员管理。团队需要花时间学习这些概念和相关框架的 API。
调试也比传统架构更困难。一个请求可能在消息网格路由时出错、在 EntryProcessor 执行时抛异常、在数据复制时丢失——问题的定位需要理解整个数据流转路径。
与现有系统的集成
如果你的系统已经有一个运行多年的关系型数据库,迁移到空间架构意味着:
- 数据需要从数据库加载到内存数据网格。
- 原来直接连数据库的组件需要改为通过数据网格访问数据。
- 原来依赖数据库触发器、存储过程的逻辑需要重写。
- 报表系统和 BI 工具需要适配新的数据访问方式。
这不是一个”加一层缓存”那么简单的事情,而是对系统架构的根本性改造。渐进式迁移是可行的——先把最热的数据(如库存、会话)迁到空间架构,其他数据保留在数据库中——但两套系统并存期间的数据一致性需要仔细处理。
十、总结
空间架构的核心思想可以用一句话概括:把数据从集中式的数据库搬到分布式的内存中,和处理逻辑放在一起,消除数据库作为扩展瓶颈。
这个思路从 1985 年 Gelernter 的元组空间开始,经过 JavaSpaces 的工程探索,到 GigaSpaces、Hazelcast、Apache Ignite 等框架的成熟实现,已经在票务、金融交易、在线游戏、实时竞价等领域得到了验证。
但空间架构不是通用解法。它解决的是一个特定的问题——集中式持久化存储在极端并发下的扩展性瓶颈。如果你的系统没有这个瓶颈,空间架构带来的复杂性(内存成本、数据容量限制、运维难度、分布式事务挑战)大概率超过它的收益。
架构选择的判断标准不是”先进不先进”,而是”问题和方案是否匹配”。在上一篇中我们讨论了管道-过滤器架构适合数据流处理场景,在下一篇中我们将从更高的层面讨论系统扩展的通用原则——包括什么时候需要空间架构这样的激进方案,什么时候简单的优化就够了。
参考资料
David Gelernter. “Generative Communication in Linda.” ACM Transactions on Programming Languages and Systems, Vol. 7, No. 1, January 1985, pp. 80-112.
Mark Richards. Software Architecture Patterns. O’Reilly Media, 2015. Chapter 5: Space-Based Architecture.
Mark Richards, Neal Ford. Fundamentals of Software Architecture. O’Reilly Media, 2020. Chapter 15: Space-Based Architecture Style.
Eric Freeman, Susanne Hupfer, Ken Arnold. JavaSpaces Principles, Patterns, and Practice. Addison-Wesley, 1999.
Hazelcast Reference Manual (Platform). https://docs.hazelcast.com/hazelcast/latest/
Apache Ignite Documentation. https://ignite.apache.org/docs/latest/
Martin Abbott, Michael Fisher. The Art of Scalability: Scalable Web Architecture, Processes, and Organizations for the Modern Enterprise. 2nd Edition, Addison-Wesley, 2015.
Nati Shalom. “Space-Based Architecture and the End of Tier-Based Computing.” GigaSpaces Technologies, 2006.
Pat Helland. “Life beyond Distributed Transactions: an Apostate’s Opinion.” CIDR, 2007. (讨论了为什么分布式事务在可扩展系统中不可行,这是空间架构避免跨分区事务的理论基础之一。)
Hazelcast CP Subsystem Reference. https://docs.hazelcast.com/hazelcast/latest/cp-subsystem/cp-subsystem
Apache Ignite Persistence Documentation. https://ignite.apache.org/docs/latest/persistence/native-persistence
Leslie Lamport. “The Part-Time Parliament.” ACM Transactions on Computer Systems, Vol. 16, No. 2, May 1998, pp. 133-169. (Paxos 协议的原始论文,与 Raft 同属共识算法家族,是理解 CP Subsystem 一致性保证的理论基础。)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】架构质量属性:不只是"高可用高性能"
需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。
【系统架构设计百科】告警策略:如何避免"狼来了"
大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略
【系统架构设计百科】微服务架构深度审视:优势、代价与适用边界
微服务不是免费的午餐。本文从分布式系统八大谬误出发,拆解微服务真正解决的问题与引入的代价,梳理服务边界划分的工程方法论,还原 Amazon 和 Netflix 从单体到微服务的真实演进时间线,给出微服务适用与不适用的判断框架。