系统设计实战 182:NoSQL数据库

5 阅读10分钟

🚀 系统设计实战 182:NoSQL数据库

摘要:本文深入剖析系统的核心架构关键算法工程实践,提供完整的设计方案和面试要点。

你是否想过,设计NoSQL数据库背后的技术挑战有多复杂?

1. 系统概述

1.1 业务背景

NoSQL数据库为现代应用提供高扩展性、高性能的数据存储解决方案。系统需要支持水平扩展、最终一致性、多种数据模型和分布式架构。

1.2 核心功能

  • 数据模型:文档、键值、列族、图数据库
  • 分片策略:一致性哈希、范围分片、目录分片
  • 复制机制:主从复制、多主复制、无主复制
  • 一致性模型:强一致性、最终一致性、因果一致性
  • 查询接口:RESTful API、原生查询语言、SQL兼容

1.3 技术挑战

  • CAP定理权衡:一致性、可用性、分区容错性的平衡
  • 数据分片:热点数据、数据倾斜、动态重分片
  • 一致性保证:分布式事务、冲突解决、版本控制
  • 性能优化:读写性能、缓存策略、索引设计
  • 运维管理:集群管理、故障检测、自动恢复

2. 架构设计

2.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│                    NoSQL数据库架构                           │
├─────────────────────────────────────────────────────────────┤
│  Client Layer                                               │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │ SDK客户端   │ │ REST API    │ │ 查询接口    │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
├─────────────────────────────────────────────────────────────┤
│  Coordination Layer                                         │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │ 路由服务    │ │ 元数据管理  │ │ 负载均衡    │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
├─────────────────────────────────────────────────────────────┤
│  Storage Node Layer                                         │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │ 存储节点1   │ │ 存储节点2   │ │ 存储节点N   │           │
│  │ - 数据分片  │ │ - 数据分片  │ │ - 数据分片  │           │
│  │ - 索引管理  │ │ - 索引管理  │ │ - 索引管理  │           │
│  │ - 复制管理  │ │ - 复制管理  │ │ - 复制管理  │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
└─────────────────────────────────────────────────────────────┘

3. 核心组件设计

3.1 分片管理器

// 时间复杂度:O(N),空间复杂度:O(1)

type ShardManager struct {
    hashRing        *ConsistentHashRing
    shardMap        map[ShardID]*ShardInfo
    rebalancer      *Rebalancer
    metadataStore   MetadataStore
    mutex           sync.RWMutex
}

type ShardInfo struct {
    ID          ShardID
    StartKey    []byte
    EndKey      []byte
    Replicas    []NodeID
    Primary     NodeID
    Status      ShardStatus
    Version     int64
}

type ConsistentHashRing struct {
    nodes       map[uint32]NodeID
    sortedKeys  []uint32
    virtualNodes int
    hasher      hash.Hash32
    mutex       sync.RWMutex
}

func (chr *ConsistentHashRing) AddNode(nodeID NodeID) {
    chr.mutex.Lock()
    defer chr.mutex.Unlock()
    
    for i := 0; i < chr.virtualNodes; i++ {
        virtualKey := fmt.Sprintf("%s:%d", nodeID, i)
        chr.hasher.Reset()
        chr.hasher.Write([]byte(virtualKey))
        hash := chr.hasher.Sum32()
        
        chr.nodes[hash] = nodeID
        chr.sortedKeys = append(chr.sortedKeys, hash)
    }
    
    sort.Slice(chr.sortedKeys, func(i, j int) bool {
        return chr.sortedKeys[i] < chr.sortedKeys[j]
    })
}

func (chr *ConsistentHashRing) GetNode(key []byte) NodeID {
    chr.mutex.RLock()
    defer chr.mutex.RUnlock()
    
    if len(chr.sortedKeys) == 0 {
        return ""
    }
    
    chr.hasher.Reset()
    chr.hasher.Write(key)
    hash := chr.hasher.Sum32()
    
    // 找到第一个大于等于hash的节点
    idx := sort.Search(len(chr.sortedKeys), func(i int) bool {
        return chr.sortedKeys[i] >= hash
    })
    
    if idx == len(chr.sortedKeys) {
        idx = 0 // 环形结构,回到第一个节点
    }
    
    return chr.nodes[chr.sortedKeys[idx]]
}

func (sm *ShardManager) RouteRequest(key []byte) (*ShardInfo, error) {
    sm.mutex.RLock()
    defer sm.mutex.RUnlock()
    
    // 使用一致性哈希找到负责的分片
    nodeID := sm.hashRing.GetNode(key)
    
    // 查找该节点上的分片信息
    for _, shard := range sm.shardMap {
        if shard.Primary == nodeID && sm.keyInRange(key, shard) {
            return shard, nil
        }
    }
    
    return nil, ErrShardNotFound
}

func (sm *ShardManager) keyInRange(key []byte, shard *ShardInfo) bool {
    return bytes.Compare(key, shard.StartKey) >= 0 && 
           bytes.Compare(key, shard.EndKey) < 0
}

3.2 存储引擎

type StorageEngine struct {
    memTable        *MemTable
    immutableTables []*MemTable
    sstables        []*SSTable
    wal             *WriteAheadLog
    compactor       *Compactor
    bloomFilters    map[string]*BloomFilter
    mutex           sync.RWMutex
}

type MemTable struct {
    data        *skiplist.SkipList
    size        int64
    maxSize     int64
    mutex       sync.RWMutex
}

type SSTable struct {
    filePath    string
    index       *SparseIndex
    bloomFilter *BloomFilter
    minKey      []byte
    maxKey      []byte
    level       int
}

func (se *StorageEngine) Put(key, value []byte, timestamp int64) error {
    // 1. 写WAL日志
    logEntry := &WALEntry{
        Key:       key,
        Value:     value,
        Timestamp: timestamp,
        Type:      EntryTypePut,
    }
    
    if err := se.wal.Append(logEntry); err != nil {
        return err
    }
    
    // 2. 写入MemTable
    se.mutex.Lock()
    defer se.mutex.Unlock()
    
    entry := &Entry{
        Key:       key,
        Value:     value,
        Timestamp: timestamp,
        Deleted:   false,
    }
    
    se.memTable.Put(entry)
    
    // 3. 检查是否需要刷新MemTable
    if se.memTable.Size() >= se.memTable.maxSize {
        if err := se.flushMemTable(); err != nil {
            return err
        }
    }
    
    return nil
}

func (se *StorageEngine) Get(key []byte) (*Entry, error) {
    se.mutex.RLock()
    defer se.mutex.RUnlock()
    
    // 1. 查找MemTable
    if entry := se.memTable.Get(key); entry != nil {
        if entry.Deleted {
            return nil, ErrKeyNotFound
        }
        return entry, nil
    }
    
    // 2. 查找不可变MemTable
    for i := len(se.immutableTables) - 1; i >= 0; i-- {
        if entry := se.immutableTables[i].Get(key); entry != nil {
            if entry.Deleted {
                return nil, ErrKeyNotFound
            }
            return entry, nil
        }
    }
    
    // 3. 查找SSTable(从新到旧)
    for i := len(se.sstables) - 1; i >= 0; i-- {
        sstable := se.sstables[i]
        
        // 使用布隆过滤器快速排除
        if !sstable.bloomFilter.MightContain(key) {
            continue
        }
        
        // 检查键范围
        if bytes.Compare(key, sstable.minKey) < 0 || 
           bytes.Compare(key, sstable.maxKey) > 0 {
            continue
        }
        
        if entry := sstable.Get(key); entry != nil {
            if entry.Deleted {
                return nil, ErrKeyNotFound
            }
            return entry, nil
        }
    }
    
    return nil, ErrKeyNotFound
}

func (se *StorageEngine) flushMemTable() error {
    // 将当前MemTable标记为不可变
    se.immutableTables = append(se.immutableTables, se.memTable)
    
    // 创建新的MemTable
    se.memTable = NewMemTable(se.memTable.maxSize)
    
    // 异步刷新到磁盘
    go func() {
        immutable := se.immutableTables[len(se.immutableTables)-1]
        sstable, err := se.writeSSTable(immutable)
        if err != nil {
            log.Printf("Failed to flush memtable: %v", err)
            return
        }
        
        se.mutex.Lock()
        se.sstables = append(se.sstables, sstable)
        // 移除已刷新的不可变MemTable
        se.immutableTables = se.immutableTables[:len(se.immutableTables)-1]
        se.mutex.Unlock()
        
        // 触发压缩
        se.compactor.TriggerCompaction()
    }()
    
    return nil
}

3.3 复制管理器

type ReplicationManager struct {
    nodeID          NodeID
    replicas        map[ShardID]*ReplicaGroup
    consensusEngine ConsensusEngine
    logReplicator   *LogReplicator
    conflictResolver *ConflictResolver
}

type ReplicaGroup struct {
    ShardID     ShardID
    Primary     NodeID
    Secondaries []NodeID
    Status      ReplicaStatus
    Version     int64
}

func (rm *ReplicationManager) ReplicateWrite(shardID ShardID, operation *WriteOperation) error {
    replica, exists := rm.replicas[shardID]
    if !exists {
        return ErrReplicaNotFound
    }
    
    if replica.Primary != rm.nodeID {
        return ErrNotPrimary
    }
    
    // 1. 本地写入
    if err := rm.applyOperation(operation); err != nil {
        return err
    }
    
    // 2. 复制到从节点
    replicationResults := make(chan error, len(replica.Secondaries))
    
    for _, secondary := range replica.Secondaries {
        go func(nodeID NodeID) {
            err := rm.replicateToNode(nodeID, operation)
            replicationResults <- err
        }(secondary)
    }
    
    // 3. 等待足够的确认(可配置一致性级别)
    successCount := 1 // 主节点已成功
    requiredAcks := (len(replica.Secondaries) + 1) / 2 + 1 // 大多数
    
    for i := 0; i < len(replica.Secondaries); i++ {
        err := <-replicationResults
        if err == nil {
            successCount++
            if successCount >= requiredAcks {
                break
            }
        }
    }
    
    if successCount < requiredAcks {
        return ErrInsufficientReplicas
    }
    
    return nil
}

func (rm *ReplicationManager) HandleSecondaryWrite(operation *WriteOperation) error {
    // 从节点处理复制的写操作
    return rm.applyOperation(operation)
}

// 多主复制的冲突解决
func (rm *ReplicationManager) ResolveConflicts(conflicts []*Conflict) error {
    for _, conflict := range conflicts {
        resolution, err := rm.conflictResolver.Resolve(conflict)
        if err != nil {
            return err
        }
        
        // 应用解决方案
        if err := rm.applyResolution(resolution); err != nil {
            return err
        }
    }
    
    return nil
}

type VectorClockResolver struct{}

func (vcr *VectorClockResolver) Resolve(conflict *Conflict) (*Resolution, error) {
    // 使用向量时钟确定因果关系
    if vcr.happensBefore(conflict.Version1.VectorClock, conflict.Version2.VectorClock) {
        return &Resolution{
            WinningVersion: conflict.Version2,
            Action:         ActionKeepWinner,
        }, nil
    }
    
    if vcr.happensBefore(conflict.Version2.VectorClock, conflict.Version1.VectorClock) {
        return &Resolution{
            WinningVersion: conflict.Version1,
            Action:         ActionKeepWinner,
        }, nil
    }
    
    // 并发冲突,需要合并或选择策略
    return vcr.resolveConcurrentConflict(conflict)
}

func (vcr *VectorClockResolver) happensBefore(vc1, vc2 VectorClock) bool {
    allLessOrEqual := true
    atLeastOneLess := false
    
    for nodeID, timestamp1 := range vc1 {
        timestamp2, exists := vc2[nodeID]
        if !exists || timestamp1 > timestamp2 {
            allLessOrEqual = false
            break
        }
        if timestamp1 < timestamp2 {
            atLeastOneLess = true
        }
    }
    
    return allLessOrEqual && atLeastOneLess
}

4. 查询处理

4.1 查询引擎

type QueryEngine struct {
    parser      QueryParser
    planner     QueryPlanner
    executor    QueryExecutor
    indexManager *IndexManager
}

type Query struct {
    Collection  string
    Filter      map[string]interface{}
    Projection  []string
    Sort        []SortField
    Limit       int
    Skip        int
}

func (qe *QueryEngine) ExecuteQuery(query *Query) (*ResultSet, error) {
    // 1. 解析查询
    parsedQuery, err := qe.parser.Parse(query)
    if err != nil {
        return nil, err
    }
    
    // 2. 生成执行计划
    plan, err := qe.planner.CreatePlan(parsedQuery)
    if err != nil {
        return nil, err
    }
    
    // 3. 执行查询
    return qe.executor.Execute(plan)
}

type QueryPlanner struct {
    statisticsManager *StatisticsManager
    indexManager      *IndexManager
}

func (qp *QueryPlanner) CreatePlan(query *ParsedQuery) (*ExecutionPlan, error) {
    plan := &ExecutionPlan{}
    
    // 选择最优的访问路径
    accessPath := qp.selectAccessPath(query)
    plan.AccessPath = accessPath
    
    // 添加过滤操作
    if query.Filter != nil {
        plan.Operations = append(plan.Operations, &FilterOperation{
            Predicate: query.Filter,
        })
    }
    
    // 添加排序操作
    if len(query.Sort) > 0 {
        plan.Operations = append(plan.Operations, &SortOperation{
            SortFields: query.Sort,
        })
    }
    
    // 添加投影操作
    if len(query.Projection) > 0 {
        plan.Operations = append(plan.Operations, &ProjectionOperation{
            Fields: query.Projection,
        })
    }
    
    // 添加限制操作
    if query.Limit > 0 {
        plan.Operations = append(plan.Operations, &LimitOperation{
            Limit: query.Limit,
            Skip:  query.Skip,
        })
    }
    
    return plan, nil
}

func (qp *QueryPlanner) selectAccessPath(query *ParsedQuery) AccessPath {
    // 检查是否可以使用索引
    for field, value := range query.Filter {
        if index := qp.indexManager.GetIndex(query.Collection, field); index != nil {
            return &IndexAccessPath{
                IndexName: index.Name,
                Field:     field,
                Value:     value,
            }
        }
    }
    
    // 回退到全集合扫描
    return &CollectionScanPath{
        Collection: query.Collection,
    }
}

4.2 索引管理

type IndexManager struct {
    indexes     map[string]*Index
    builder     *IndexBuilder
    maintenance *IndexMaintenance
}

type Index struct {
    Name        string
    Collection  string
    Fields      []IndexField
    Type        IndexType
    Options     IndexOptions
    btree       *BTree
    statistics  *IndexStatistics
}

type IndexField struct {
    Name      string
    Direction SortDirection
}

func (im *IndexManager) CreateIndex(spec *IndexSpec) error {
    index := &Index{
        Name:       spec.Name,
        Collection: spec.Collection,
        Fields:     spec.Fields,
        Type:       spec.Type,
        Options:    spec.Options,
        btree:      NewBTree(spec.Options.Order),
    }
    
    // 构建索引
    if err := im.builder.BuildIndex(index); err != nil {
        return err
    }
    
    im.indexes[spec.Name] = index
    return nil
}

func (im *IndexManager) UpdateIndex(indexName string, document *Document, operation IndexOperation) error {
    index, exists := im.indexes[indexName]
    if !exists {
        return ErrIndexNotFound
    }
    
    // 提取索引键值
    keyValues := im.extractKeyValues(document, index.Fields)
    
    switch operation {
    case IndexOperationInsert:
        return index.btree.Insert(keyValues, document.ID)
    case IndexOperationUpdate:
        // 先删除旧值,再插入新值
        oldKeyValues := im.extractKeyValues(document.OldVersion, index.Fields)
        index.btree.Delete(oldKeyValues, document.ID)
        return index.btree.Insert(keyValues, document.ID)
    case IndexOperationDelete:
        return index.btree.Delete(keyValues, document.ID)
    }
    
    return nil
}

func (im *IndexManager) QueryIndex(indexName string, condition *IndexCondition) ([]DocumentID, error) {
    index, exists := im.indexes[indexName]
    if !exists {
        return nil, ErrIndexNotFound
    }
    
    switch condition.Type {
    case ConditionTypeEqual:
        return index.btree.Find(condition.Value)
    case ConditionTypeRange:
        return index.btree.FindRange(condition.StartValue, condition.EndValue)
    case ConditionTypePrefix:
        return index.btree.FindPrefix(condition.Prefix)
    }
    
    return nil, ErrUnsupportedCondition
}

5. 一致性保证

5.1 最终一致性

type EventualConsistencyManager struct {
    antiEntropy     *AntiEntropyService
    merkleTree      *MerkleTree
    gossipProtocol  *GossipProtocol
    repairService   *RepairService
}

func (ecm *EventualConsistencyManager) StartAntiEntropy() {
    ticker := time.NewTicker(time.Minute * 10)
    go func() {
        for range ticker.C {
            ecm.performAntiEntropy()
        }
    }()
}

func (ecm *EventualConsistencyManager) performAntiEntropy() {
    // 1. 构建本地Merkle树
    localTree := ecm.merkleTree.Build()
    
    // 2. 与其他节点交换Merkle树
    peers := ecm.gossipProtocol.GetPeers()
    for _, peer := range peers {
        remoteTree, err := ecm.exchangeMerkleTree(peer, localTree)
        if err != nil {
            continue
        }
        
        // 3. 比较Merkle树找出差异
        differences := ecm.merkleTree.Compare(localTree, remoteTree)
        
        // 4. 修复差异
        for _, diff := range differences {
            ecm.repairService.RepairRange(peer, diff.StartKey, diff.EndKey)
        }
    }
}

type MerkleTree struct {
    root   *MerkleNode
    hasher hash.Hash
}

type MerkleNode struct {
    Hash     []byte
    StartKey []byte
    EndKey   []byte
    Children []*MerkleNode
    IsLeaf   bool
}

func (mt *MerkleTree) Build() *MerkleNode {
    // 递归构建Merkle树
    return mt.buildNode(nil, nil, 0)
}

func (mt *MerkleTree) buildNode(startKey, endKey []byte, depth int) *MerkleNode {
    if depth >= mt.maxDepth {
        // 叶子节点:计算范围内数据的哈希
        hash := mt.calculateRangeHash(startKey, endKey)
        return &MerkleNode{
            Hash:     hash,
            StartKey: startKey,
            EndKey:   endKey,
            IsLeaf:   true,
        }
    }
    
    // 内部节点:分割范围并递归构建子节点
    midKey := mt.calculateMidKey(startKey, endKey)
    leftChild := mt.buildNode(startKey, midKey, depth+1)
    rightChild := mt.buildNode(midKey, endKey, depth+1)
    
    // 计算内部节点哈希
    mt.hasher.Reset()
    mt.hasher.Write(leftChild.Hash)
    mt.hasher.Write(rightChild.Hash)
    hash := mt.hasher.Sum(nil)
    
    return &MerkleNode{
        Hash:     hash,
        StartKey: startKey,
        EndKey:   endKey,
        Children: []*MerkleNode{leftChild, rightChild},
        IsLeaf:   false,
    }
}

5.2 因果一致性

type CausalConsistencyManager struct {
    vectorClock    *VectorClock
    dependencyLog  *DependencyLog
    sessionManager *SessionManager
}

type VectorClock map[NodeID]int64

func (vc VectorClock) Increment(nodeID NodeID) {
    vc[nodeID]++
}

func (vc VectorClock) Update(other VectorClock) {
    for nodeID, timestamp := range other {
        if vc[nodeID] < timestamp {
            vc[nodeID] = timestamp
        }
    }
}

func (vc VectorClock) Copy() VectorClock {
    copy := make(VectorClock)
    for nodeID, timestamp := range vc {
        copy[nodeID] = timestamp
    }
    return copy
}

type CausalOperation struct {
    ID            OperationID
    Type          OperationType
    Key           []byte
    Value         []byte
    VectorClock   VectorClock
    Dependencies  []OperationID
    SessionID     SessionID
}

func (ccm *CausalConsistencyManager) ExecuteOperation(op *CausalOperation) error {
    // 1. 检查因果依赖是否满足
    if !ccm.dependenciesSatisfied(op.Dependencies) {
        return ccm.deferOperation(op)
    }
    
    // 2. 更新向量时钟
    ccm.vectorClock.Update(op.VectorClock)
    ccm.vectorClock.Increment(ccm.nodeID)
    
    // 3. 执行操作
    if err := ccm.applyOperation(op); err != nil {
        return err
    }
    
    // 4. 记录依赖关系
    ccm.dependencyLog.Record(op)
    
    // 5. 检查是否有等待的操作可以执行
    ccm.checkDeferredOperations()
    
    return nil
}

func (ccm *CausalConsistencyManager) dependenciesSatisfied(dependencies []OperationID) bool {
    for _, depID := range dependencies {
        if !ccm.dependencyLog.Contains(depID) {
            return false
        }
    }
    return true
}

NoSQL数据库通过灵活的数据模型、水平扩展能力和最终一致性保证,为现代分布式应用提供了高性能、高可用的数据存储解决方案。


🎯 场景引入

你打开App,

你打开手机准备使用设计NoSQL数据库服务。看似简单的操作背后,系统面临三大核心挑战:

  • 挑战一:高并发——如何在百万级 QPS 下保持低延迟?
  • 挑战二:高可用——如何在节点故障时保证服务不中断?
  • 挑战三:数据一致性——如何在分布式环境下保证数据正确?

📈 容量估算

假设 DAU 1000 万,人均日请求 50 次

指标数值
数据总量10 TB+
日写入量~100 GB
写入 TPS~5 万/秒
读取 QPS~20 万/秒
P99 读延迟< 10ms
节点数10-50
副本因子3

❓ 高频面试问题

Q1:NoSQL数据库的核心设计原则是什么?

参考正文中的架构设计部分,核心原则包括:高可用(故障自动恢复)、高性能(低延迟高吞吐)、可扩展(水平扩展能力)、一致性(数据正确性保证)。面试时需结合具体场景展开。

Q2:NoSQL数据库在大规模场景下的主要挑战是什么?

  1. 性能瓶颈:随着数据量和请求量增长,单节点无法承载;2) 一致性:分布式环境下的数据一致性保证;3) 故障恢复:节点故障时的自动切换和数据恢复;4) 运维复杂度:集群管理、监控、升级。

Q3:如何保证NoSQL数据库的高可用?

  1. 多副本冗余(至少 3 副本);2) 自动故障检测和切换(心跳 + 选主);3) 数据持久化和备份;4) 限流降级(防止雪崩);5) 多机房/多活部署。

Q4:NoSQL数据库的性能优化有哪些关键手段?

  1. 缓存(减少重复计算和 IO);2) 异步处理(非关键路径异步化);3) 批量操作(减少网络往返);4) 数据分片(并行处理);5) 连接池复用。

Q5:NoSQL数据库与同类方案相比有什么优劣势?

参考方案对比表格。选型时需考虑:团队技术栈、数据规模、延迟要求、一致性需求、运维成本。没有银弹,需根据业务场景权衡取舍。



| 方案一 | 简单实现 | 低 | 适合小规模 | | 方案二 | 中等复杂度 | 中 | 适合中等规模 | | 方案三 | 高复杂度 ⭐推荐 | 高 | 适合大规模生产环境 |

🚀 架构演进路径

阶段一:单机版 MVP(用户量 < 10 万)

  • 单体应用 + 单机数据库,功能验证优先
  • 适用场景:产品早期验证,快速迭代

阶段二:基础版分布式(用户量 10 万 - 100 万)

  • 应用层水平扩展 + 数据库主从分离
  • 引入 Redis 缓存热点数据,降低数据库压力
  • 适用场景:业务增长期

阶段三:生产级高可用(用户量 > 100 万)

  • 微服务拆分,独立部署和扩缩容
  • 数据库分库分表 + 消息队列解耦
  • 多机房部署,异地容灾
  • 全链路监控 + 自动化运维

✅ 架构设计检查清单

检查项状态
缓存策略
分布式架构
数据一致性
高可用设计
性能优化
水平扩展

⚖️ 关键 Trade-off 分析

🔴 Trade-off 1:一致性 vs 可用性

  • 强一致(CP):适用于金融交易等不能出错的场景
  • 高可用(AP):适用于社交动态等允许短暂不一致的场景
  • 本系统选择:核心路径强一致,非核心路径最终一致

🔴 Trade-off 2:同步 vs 异步

  • 同步处理:延迟低但吞吐受限,适用于核心交互路径
  • 异步处理:吞吐高但增加延迟,适用于后台计算
  • 本系统选择:核心路径同步,非核心路径异步