自在学

我们与你共同进步

  • 分类课程
  • 文章
  • 工作台
  • 订阅

  • 关于我们
  • 隐私政策
  • 使用条款

探索

  • 分类课程
  • 文章
  • 工作台
  • 订阅

网站信息

  • 关于我们
  • 隐私政策
  • 使用条款

加入社区

自在学学习社区微信二维码

微信扫码,交流学习

株洲市自在学教育科技有限公司© 2025 - 2026 版权所有

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

湘公网安备43020302000292号|湘ICP备2025148919号-1
分类课程工作台文章订阅
分类课程工作台文章价格

NoSQL

  1. 01认识NoSQL
  2. 02聚合数据模型
  3. 03数据模型的深入探索
  4. 04数据分布模型
  5. 05数据一致性
  6. 06版本戳
  7. 07Map-Reduce
  8. 08键值数据库
  9. 09文档数据库
  10. 10列族存储
  11. 11图数据库
  12. 12数据库模式演进
  13. 13多存储持久化
正在加载课程章节内容
课程编程NoSQL键值数据库

键值数据库:从一把钥匙到高并发状态系统

想象一下,你正在给一场限时抢购做购物车服务。活动开始后的几秒钟里,大量用户会同时打开购物车、增加商品、修改数量。页面不关心“所有北京用户的购物车平均有几件商品”,它只想回答一个非常具体的问题:编号为 user-128 的用户,现在的购物车是什么?

如果每次回答都要连接多张表、排序再聚合,数据库会把很多力气花在这个请求根本不需要的能力上。换一种思路,我们把 cart:user-128 当作一把唯一的钥匙,把该用户的整份购物车当作钥匙对应的储物格。服务拿着钥匙直接取值,读写路径就短了许多。

这就是键值数据库最核心的思路。它没有试图包办所有查询,而是把一个常见动作做到非常直接:已知键,定位值。 这一章我们不仅要理解这句话,还要顺着它继续追问:键怎么设计,值装多大合适,过期时间怎么设,并发更新会不会互相覆盖,数据分散到多台机器后又会发生什么。

键值模型就像用钥匙打开对应储物格

一、键值模型到底简单在哪里

一条键值记录由两部分组成:

  • 键(key)负责唯一标识数据,例如 session:8f2a、cart:user-128、product:3108。
  • 值(value)负责承载内容,可以是短字符串、数字、二进制数据,也可以是序列化后的对象。某些产品还会直接提供哈希、集合、列表、有序集合等结构。

最小的操作接口通常可以概括为读取、写入和删除。有些系统还提供“仅当键不存在时写入”“原子自增”“比较后更新”“设置生存时间”等能力。接口少,不代表内部实现简单;它代表调用者不必临时拼出复杂查询计划,系统也更容易针对按键访问做优化。

javascript
const sessionKey = 'session:8f2a'
 
await store.set(sessionKey, {
  userId: 'user-128',
  role: 'member',
  cartCount: 3,
  createdAt: '2026-08-11T09:30:00+08:00'
}, { ttlSeconds: 1800 })
 
const session = await store.get(sessionKey)
 
if (!session) {
  // 键不存在、已经过期,或者此前从未写入
  return requireLoginAgain()
}

这个例子里,应用在发起读取前已经知道完整的 sessionKey。数据库不需要理解 role 或 cartCount 的业务含义,也不需要扫描其他会话。它只负责找到这个键对应的值。

键值数据库的优势来自访问路径明确,而不是“任何操作都是常数时间”这样的绝对承诺。网络往返、值的大小、持久化方式、复制确认、热点键和底层数据结构都会影响真实延迟。工程上要看分位延迟和负载下的表现,不能只看一次本机测试。

“值是黑盒”也有例外

在最纯粹的键值模型里,数据库把值当成一段不透明的字节。应用想改购物车中的一个商品数量,可能需要把整份值取出、修改后再写回。另一些键值产品会理解值的内部结构,允许你直接修改哈希字段、向列表末尾追加元素或给计数器加一。

这两类系统都可以放在键值数据库的讨论范围里,但它们的能力边界不同。选型时不要只看到“支持键值”四个字,还要继续确认:能否局部更新、单条值有多大、哪些操作是原子的、是否支持过期、持久化和复制分别怎样工作。

二、一次定位是怎么发生的

你可以把读取过程粗略理解成三步:应用给出业务键,系统对键做哈希计算,再利用计算结果找到负责保存这条记录的位置。单机系统可能定位到内存中的槽位;分布式系统则可能先定位到某个分区,再去该分区的节点读取数据。

业务键经过哈希计算后均匀落到多个分区

哈希的价值在于把形式各异的键映射到有限的存储空间。只要键空间足够分散,请求就有机会均匀落到不同分区,让多台机器一起承担负载。不过,“机器很多”并不自动等于“压力均匀”。如果几百万次请求都集中在同一个直播间键上,再大的集群也可能被那个键所在的路径卡住。

下面这个实验台用一个简化哈希函数展示分区过程。先点“均匀用户键”,观察不同用户编号如何散开;再点“单一热点键”,看看所有请求挤向同一分区时会发生什么。这个演示不复刻任何具体数据库的算法,但能帮助你抓住分区键设计的核心影响。

分区系统通常只保证“给出完整键后能快速定位”。如果你想按值里的城市、价格或标签筛选,系统可能只能扫描全部记录,或者依赖你事先维护的二级索引。键值建模的第一步不是先存数据,而是先列出必须高效支持的访问问题。

三、键设计其实是在设计访问路径

假设产品经理说:“我们要存购物车。”这句话还不足以直接决定键。你要先追问:按用户读取整车,还是按店铺读取一段?需要一次拿到所有商品,还是经常只更新一个商品?一个用户可能有多少条目?是否需要把游客购物车合并到登录账号?

答案不同,键和值的边界也会不同。按用户整车读取时,可以把键设计成 cart:{tenantId}:{userId};如果单个购物车可能非常大,又经常独立修改商品,则可以拆成 cart-item:{tenantId}:{userId}:{skuId},再额外维护商品编号集合。前者读取简单、网络往返少,后者局部更新轻,却多了索引维护和多键一致性问题。

合理键名、热点键与压力拆分的对比

一条好键通常包含什么

我们常用冒号把键分成几段,这不是语法强制要求,而是一种便于人和工具理解的约定。例如:

text
业务域:实体类型:租户编号:实体编号:用途:版本
shop:cart:tenant-7:user-128:active:v2

设计时可以逐项检查:

  • 命名空间清楚:cart:、session:、rate-limit: 能减少不同团队之间的键名碰撞。
  • 唯一性稳定:用不会随昵称、手机号或展示文案变化的内部编号做身份部分。
  • 租户边界明确:多租户系统把租户编号纳入键,既方便隔离,也避免同号实体相撞。
  • 基数足够大:分区键如果只有“今天”“全国”这几个值,很容易制造热点。
  • 长度适度:键会随每条记录一起占空间,过长的重复前缀会增加内存和网络成本。
  • 避免敏感信息:不要把口令、访问令牌或明文身份证号放进键名。键经常出现在日志、监控和排障工具里。
  • 为演进留出口:当序列化格式或业务语义会变化时,版本段能帮助新旧数据并存迁移。

用访问问题反推键

我们来为“用户查看当前购物车”走一遍完整过程。

先把访问问题写成一句完整的话:“给出租户编号和用户编号,读取该用户当前购物车。”这里没有按价格筛选,也没有跨用户聚合,因此最自然的入口就是租户与用户组成的键。

再估算单个值的上界。普通购物车只有几十个条目,整车读取合理;如果业务允许收藏上万件商品,就应该拆分或设置明确上限,避免一个巨大值拖慢每次网络传输。

接着检查更新方式。若所有增删都能使用服务端的原子结构操作,整车方案仍可行;若只能“先读整车、在应用里修改、再整车写回”,就必须增加版本号或比较后更新,防止并发覆盖。

最后考虑分布和生命周期。用户编号通常能提供较大的键空间;游客购物车需要过期,已下单的购物车则应转入订单系统,而不是仅靠缓存长期保存。

javascript
function cartKey({ tenantId, userId }) {
  if (!tenantId || !userId) throw new Error('缺少购物车身份信息')
  return `shop:cart:${tenantId}:${userId}:active:v2`
}
 
const cart = {
  version: 17,
  items: [
    { skuId: 'sku-3108', quantity: 2, priceSnapshot: 12900 }
  ],
  updatedAt: '2026-08-11T10:05:00+08:00'
}

这里的价格用整数分表示,避免浮点计算误差;version 用来识别并发修改;updatedAt 便于排障,但不能替代数据库自己的并发控制。值的结构虽然灵活,团队仍然应该用校验规则约束必要字段,否则“什么都能放”很快就会变成“没人知道里面有什么”。

四、值的粒度:整份取出,还是拆成多条

键值建模常见的误区,是把所有相关数据塞进一个键,觉得“一次 GET 最快”。一次取回确实减少往返,但值越大,更新放大、序列化耗时和网络传输也越明显。更麻烦的是,只想修改一个字段时,整份读写会扩大并发冲突范围。

反过来,把每个字段都拆成独立键也不理想。读取一份用户资料可能要请求几十次,多键更新还需要额外协调。真正实用的原则是:一起读取、一起过期、一起保持一致的数据,优先放在一起;更新频率和生命周期明显不同的数据,考虑拆开。

例如用户主页的昵称、头像和简介经常一起显示,可以聚合为一个值;登录会话与长期偏好虽然都属于用户,却有完全不同的过期时间,应该使用不同的键。订单基本信息与不断增长的操作日志也不适合无限堆在同一个值里。

一些产品提供服务端数据结构,让我们不必在“整份对象”和“每字段一键”之间二选一:

  • 哈希适合一条实体中的多个字段,可以只修改其中一项。
  • 集合适合去重后的成员关系,例如某活动已领取优惠券的用户集合。
  • 有序集合适合按分数维护顺序,例如实时榜单,但分数相同、并列规则和榜单截断仍需提前设计。
  • 列表适合按插入顺序追加数据,不过它不天然等于可靠消息队列,消费确认、重试和积压治理需要另行确认。
  • 原子计数器适合访问次数、库存预占计数或限流窗口,但业务不变量不能只靠一个孤立数字表达。

“Redis 能做某种结构操作”与“所有键值数据库都能做”是两回事。这一章讨论的是数据模型与工程取舍,具体命令、大小限制、原子范围和集群约束必须以你实际使用的产品为准。

五、TTL:让临时状态知道何时退场

登录会话、验证码、幂等标记、临时锁和缓存都有一个共同点:过了某个时间,它们就不应该继续存在。键值系统常用生存时间(TTL)表达这条生命周期规则。写入时附上 TTL,时间到后,这个键在逻辑上失效,系统再通过自己的过期机制回收空间。

带有生存时间的会话从写入到自动失效

TTL 不是随手填一个“差不多”的数字。太短会让仍然有效的会话频繁丢失,太长会放大安全窗口并占用更多空间。我们应该从业务语义出发:验证码可能只需几分钟,空闲会话可以在每次有效访问后续期,商品缓存则要结合更新频率和允许陈旧的时间来决定。

javascript
// 把“写值”和“设置过期”作为一个原子命令提交
await store.set('verify:login:req-72', '493821', {
  onlyIfAbsent: true,
  ttlSeconds: 300
})
 
// 不推荐先写入,再单独设置 TTL:进程若在两步之间崩溃,键可能永不过期

过期与淘汰不是同一件事

过期回答的是“这条数据在业务上何时无效”;淘汰回答的是“内存不够时先移走谁”。一个还没到期的缓存键,可能因为内存压力和淘汰策略提前消失。因此读取方必须把未命中当成正常分支,而不是系统异常。

反过来,一条刚到期的数据也不一定在物理空间里立刻消失。只要对外读取已经把它当作不存在,后台稍后清理通常不会影响业务正确性。监控内存时,我们要理解逻辑失效与物理回收之间可能有时间差。

下面拖动时间轴,观察会话、数据库和读取结果的变化;再切换“内存紧张”,可以看到尚未到期的缓存也可能提前被淘汰。

为了避免大量缓存同一秒到期,工程上常在基础 TTL 上加入小范围随机抖动。这样不会改变“几分钟左右失效”的业务语义,却能把重建压力分散开。滑动续期也要谨慎:只有经过验证的活跃请求才应刷新会话,不能让攻击流量把失效时间无限推后。

不要把 TTL 当成删除关键业务数据的唯一保证。资金流水、订单和审计记录需要明确的持久化与归档流程;缓存过期只适合表达临时副本或短期状态的生命周期。

六、把键值库用作缓存:快只是第一步

商品详情是一个典型场景。主数据库保存权威商品记录,键值库保存一份短期副本。用户请求先查缓存,命中就直接返回;未命中再查主数据库,并把结果回填。这种模式常被称为旁路缓存。

旁路缓存的读取、更新与下次重建过程

读取路径

应用先用 product:3108 查询缓存。命中时返回缓存值,同时记录命中率和读取延迟;不要因为“缓存应该很快”就省略监控。

未命中时查询主数据库。这里的未命中可能是首次访问、TTL 到期、内存淘汰,也可能是更新流程主动删除了旧缓存。

查到结果后设置一个有限 TTL 并回填缓存。若主数据库也没有这条商品,可以缓存一个很短时间的“空结果”,避免不存在的编号被反复查询。

更新路径

写入时更稳妥的基础顺序通常是:先提交主数据库,再删除缓存。下一次读取会从主数据库取得新值并重建缓存。直接“同时更新数据库和缓存”看似省一次未命中,却容易让两个并发写操作以不同顺序完成,最后留下旧缓存。

javascript
async function getProduct(productId) {
  const key = `product:${productId}:v3`
  const cached = await cache.get(key)
  if (cached) return JSON.parse(cached)
 
  const product = await productRepository.findById(productId)
  if (!product) {
    await cache.set(key, JSON.stringify({ missing: true }), { ttlSeconds: 20 })
    return null
  }
 
  const ttlWithJitter = 300 + Math.floor(Math.random() * 60)
  await cache.set(key, JSON.stringify(product), { ttlSeconds: ttlWithJitter })
  return product
}
 
async function updateProduct(productId, patch) {
  await productRepository.update(productId, patch)
  await cache.delete(`product:${productId}:v3`)
}

这段代码只是基础骨架。真实系统还要处理一个重要窗口:数据库已经更新,但删除缓存暂时失败。常见补强方法包括重试删除、把失效消息写入可靠事件表再异步投递,以及给缓存保留合理 TTL 作为最后兜底。选择哪一种,要看业务能容忍多长时间的旧值。

下面的模拟器把缓存和主数据库并排摆出来。你可以按顺序执行读取和更新,也可以故意让删除缓存失败,观察为什么 TTL 仍然重要。

缓存的三个高频故障模式

缓存穿透发生在请求的数据本来就不存在,每次都越过缓存访问主数据库。短暂缓存空结果、参数校验和概率型存在性判断可以缓解它,但空结果 TTL 不宜太长,否则新创建的数据可能迟迟不可见。

缓存击穿发生在一个极热门键失效的瞬间,大量请求同时回源。可以让一个请求负责重建,其余请求短暂等待、使用可接受的旧副本或走限流降级。锁本身也要有过期时间和所有者标识,不能制造永久死锁。

缓存雪崩指大量键在相近时间失效,回源压力突然超过主数据库能力。TTL 抖动、分批预热、限流和降级能把尖峰摊开。真正可靠的方案还要验证主数据库在“缓存大面积不可用”时是否能保护自己。

七、并发更新:不要把“先读后写”误当成原子操作

假设当前点赞数是 10。请求甲读取 10,请求乙也读取 10;两边各自加一,再先后写回 11。系统实际收到两次点赞,结果却只增加了一次。这不是键值库算错了,而是应用把三个独立动作误当成了一个不可分割的动作。

先读后写丢失更新与原子自增的对比

正确方向是优先使用数据库提供的原子自增,让读取、计算、写回在服务端成为一个操作。如果更新逻辑更复杂,可以使用比较后更新:读取值和版本号,提交时要求版本仍然相同;若版本已经变化,当前请求就重新读取并重试。

javascript
// 计数器:让数据库执行原子自增
const newLikes = await store.increment('post:890:likes', 1)
 
// 对象:用版本号防止静默覆盖
async function changeCart(key, change) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const current = await store.getWithVersion(key)
    const next = applyCartChange(current.value, change)
 
    const saved = await store.compareAndSet(key, current.version, next)
    if (saved) return next
  }
  throw new Error('购物车刚刚被其他请求修改,请重试')
}

有些系统提供事务批处理,但“命令连续执行”不一定等于关系数据库里的完整事务语义。你仍然要确认:执行中某条命令失败时会不会回滚,断线后如何判断结果,事务能否跨分区,脚本或批处理最长会阻塞多久。

多键业务不变量怎么办

从账户甲扣款、给账户乙加款、写入流水,这三个变化必须共同满足资金守恒。把它们分别塞进三个键,再依次写入,会产生部分成功的风险。如果某种业务不变量天然跨越多个实体,而且失败代价很高,支持成熟事务的关系数据库通常更合适。

确实需要在分布式键值系统上协调多步流程时,可以使用幂等请求编号、状态机、事务日志和补偿动作,但这是一套业务协议,不是给三个 SET 外面套一层重试那么简单。重试前必须能辨认“上次没执行”和“上次已执行但响应丢了”,否则重试本身会造成重复扣款或重复发货。

原子性的范围一定要说完整:是单个键、同一分区中的多个键,还是任意分区?只写“支持事务”不足以判断系统能否守住你的业务不变量。

八、复制、故障切换与一致性边界

为了避免一台机器故障就让数据不可用,分布式键值系统通常会保存多个副本。常见路径是先把写入交给主节点,再复制到其他节点。异步复制能缩短前台等待时间,却会产生一个短暂窗口:主节点已经回答“写入成功”,某个副本还没收到最新值。

主节点向副本异步复制以及故障切换的边界

如果应用从落后的副本读取,就可能暂时看到旧值;如果主节点在最新写入尚未复制时突然故障,切换后的副本甚至可能缺少这次写入。等待更多副本确认可以降低这个风险,但写入延迟会升高,遇到网络故障时也更容易暂时拒绝服务。

这不是一个“强一致一定好”或“最终一致一定快”的口号题。我们要按数据语义决定:

  • 商品介绍晚几秒更新通常可以接受,读副本能分担压力。
  • 用户刚修改密码后的安全状态,不能轻易从可能落后的副本读取。
  • 库存预占要明确是否允许超卖、失败后怎样归还,以及扣减在哪个原子范围内完成。
  • 会话丢失可能只需要重新登录,但如果会话里存着唯一一份未提交订单,就说明数据边界设计错了。

复制也不等于持久化。副本会把主节点的错误删除一起复制,内存中的多个副本也可能在机房级故障中同时消失。反过来,持久化到磁盘也不自动提供高可用,因为单节点恢复仍需时间。你要分别回答四个问题:服务是否继续可用、已确认写入会不会丢、能读到多新的数据、误删后能否恢复。

热点键为何不能只靠扩容解决

如果一个键只能由一个分区处理,那么给集群增加机器并不会拆散它的单键请求。可以考虑把可合并的计数拆成多个分片,例如把活动浏览量写入 view:campaign-7:shard-0 到 shard-31,读取总量时再聚合。不过,拆分只适用于允许合并的场景;余额、唯一库存这样的严格状态不能随意分片后相加。

热点治理还包括本地缓存、请求合并、只读副本、限流以及重新划分业务入口。无论采用哪种方法,都要同时观察单键请求频率、值大小、分区吞吐和尾延迟。平均值看起来健康时,最热的那个分区可能已经持续排队。

九、适合键值数据库的场景

会话与临时令牌

会话编号天然是完整键,读取频繁、值相对小,还有清晰的过期需求。设计时要让值只保存恢复会话所需的状态,不要把唯一一份长期业务数据藏在会话里。退出登录应主动删除或吊销,而不是只等待 TTL。

购物车与流程草稿

如果系统总是按用户读取整份购物车或草稿,键值模型能让路径非常直接。你需要额外处理并发版本、值大小上限、游客数据合并和提交后的归档。购物车是可变状态,订单则是需要审计的事实,两者不应混为同一份长期记录。

缓存

缓存是权威数据的可重建副本。键值库很适合按主键缓存商品、配置和计算结果,但必须提前定义未命中、旧值、回源失败、删除失败与大面积失效时的行为。命中率高不代表缓存正确,返回旧价格造成的业务影响同样要监控。

计数、限流与幂等标记

原子自增加过期时间,可以表达固定时间窗口内的请求次数;“仅当不存在时写入”可以帮助登记幂等请求。这里要特别注意时间窗口边界、键的 TTL 是否在同一原子操作中设置,以及故障切换后是否允许少量计数回退。

排行榜与集合关系

当产品提供有序集合或集合结构时,实时榜单、唯一成员集合和交集判断会很方便。不过这已经不再是单纯的黑盒值读取,复杂度、内存占用和分片限制取决于具体结构。大榜单要限制保留范围,不能让一个键无限增长。

十、哪些需求不该硬塞进键值库

如果需求经常变化成“按任意字段筛选、连接多个实体、临时增加排序条件”,而调用方事先并不知道完整键,关系模型或文档检索系统会更自然。为了模拟查询而维护几十套手工索引,会把每次写入变成脆弱的多键同步。

如果核心流程依赖跨实体强事务,例如总账、清结算或需要严格外键约束的业务,选择成熟事务数据库往往更稳妥。键值库可以作为读缓存、幂等层或辅助状态库,但不必承担它不擅长的唯一事实来源角色。

大范围统计分析也不是按键读取的强项。“统计过去一年各城市、各品类的销量趋势”需要扫描和聚合大量记录,更适合数据仓库或分析引擎。在线键值系统应服务于明确、低延迟的访问路径,而不是被离线报表拖住。

看到性能问题就把所有表搬进键值库,通常只是把数据库中的复杂度搬到了应用层。先确认慢在哪里、访问模式是否稳定、正确性边界是什么,再决定是否需要键值模型。

十一、落地前的一张检查表

真正开始建库前,我们可以用下面这组问题做设计评审:

  1. 每个高频请求是否都能在调用前得到完整键?
  2. 键是否稳定、唯一、不含敏感信息,并能形成足够分散的键空间?
  3. 单个值的典型大小和最坏大小是多少,是否会无限增长?
  4. 哪些数据一起读取、一起更新、一起过期,哪些应该拆开?
  5. 并发写是覆盖、自增、集合追加还是比较后更新,原子范围多大?
  6. TTL 表达什么业务语义,提前淘汰或删除失败时怎样处理?
  7. 缓存未命中、热点重建、主数据库故障时是否有限流和降级?
  8. 分区键是否会制造热点,是否能从监控中看到单键和单分区压力?
  9. 复制是同步还是异步,读取可能多旧,故障切换可能丢多少已确认写入?
  10. 持久化、备份、恢复演练和误删除恢复是否分别有方案?
  11. 未来出现新的查询维度时,是新增受控索引,还是交给更合适的数据库?

这些问题没有一套通用答案。它们的作用,是让团队在写下第一条数据之前就把隐含假设说出来。键值系统越简洁,越需要应用把访问路径和正确性边界设计清楚。

练习:把模型边界说清楚

1
某接口要按用户编号读取当前购物车,且购物车最多几十项。下列哪种设计最符合键值访问路径?
2
下面哪些做法能降低缓存集中失效带来的回源压力?
3
数据拥有三个副本,就等于同时具备强一致、持久化和误删除恢复能力。

下面再做一道不依赖具体产品命令的设计题:一个热门直播间每秒收到大量点赞,请说明你会如何设计键、更新动作、过期策略与热点治理。

可以先用直播间编号构造计数键,并优先使用服务端原子自增,避免“先读后写”丢失更新。如果单键吞吐已经成为瓶颈,而且业务允许点赞数短暂近似,可以把计数拆成固定数量的分片,读取时求和;若展示只需近实时,还可以周期性汇总。直播结束后是否过期取决于计数是不是唯一事实:临时展示缓存可以设置 TTL,最终统计则应落到持久化系统。整个方案还要配合单键频率、分区负载、尾延迟监控与入口限流,不能只靠增加机器数量。

小结

键值数据库并不是“更简单的关系数据库”,而是一种围绕已知键优化的数据模型。它把复杂查询能力让出去,换来短而清晰的读写路径,也让分区扩展、TTL、原子操作和缓存成为很自然的能力组合。

真正决定系统质量的,往往不是 GET 或 SET 怎么写,而是键和值的边界是否贴合访问问题:键要稳定且分散,值要控制大小,并发更新要落在明确的原子范围内,临时数据要有生命周期,缓存要能承受未命中,复制与持久化也要分开讨论。

当你能把这些取舍讲清楚时,键值数据库会成为高并发系统里非常趁手的一块积木;当需求主要是复杂关联、任意筛选、跨实体事务或大规模分析时,主动选择别的数据库,同样是成熟的设计。

上一章Map-Reduce下一章文档数据库