MemoryStoreService 是一种高吞吐量和低延迟的数据服务,提供快速的内存数据存储,可供所有服务器在实时会话中访问。内存存储 适合于频繁且短暂的数据,这些数据变化迅速且不需要持久化,因为它们访问更快,并且在达到最大存活时间后会消失。对于需要在会话之间持久保存的数据,请使用 数据存储。
数据结构
内存存储有三种在服务器之间共享的基本数据结构,供快速处理使用,而不是直接访问原始数据:排序映射、队列 和 哈希映射。每种数据结构非常适合特定的用例:
- 基于技能的匹配 - 在服务器之间的共享队列中保存用户信息,例如技能水平,并使用大厅服务器定期进行匹配。
- 跨服务器交易和拍卖 - 启用不同服务器之间的通用交易,用户可以对实时变化价格的项目进行投标,使用一个包含键值对的排序映射。
- 全球排行榜 - 在一个共享的排序映射中存储和更新用户排名。
- 共享库存 - 在一个共享的哈希映射中保存库存物品和统计数据,用户可以同时使用库存物品。
- 持久数据的缓存 - 将您在数据存储中的持久数据同步并复制到一个可以作为缓存的内存存储哈希映射中,以提高游戏性能。
一般来说,如果您需要基于特定键访问数据,请使用哈希映射。如果您需要数据有序,请使用排序映射。如果需要以特定顺序处理数据,请使用队列。
限制和配额
为了维护可扩展性和系统性能,内存存储对内存大小、API 请求和数据结构大小有数据使用配额。
内存存储有基于过期时间的驱逐政策,也称为生存时间(TTL)。项目在过期后被驱逐,内存配额为新条目释放。达到内存限制时,所有后续写请求失败,直到项目过期或您手动删除它们。
内存大小配额
内存配额限制了游戏可以消耗的总内存量。这个值不是固定的;而是根据游戏中用户数量而随时间变化,根据公式 64KB + 1.2KB * [用户数量]。配额适用于游戏层面而不是服务器层面。
当用户加入游戏时,额外的内存配额会立即可用。当用户离开游戏时,配额不会立即减少。配额在八天后会重新评估到更低的值。
当您的游戏达到内存大小配额时,任何增加内存大小的 API 请求总是失败。减少或不改变内存大小的请求仍然成功。
通过 可观察性 仪表板,您可以实时查看游戏的内存大小配额,使用 内存使用情况 图表。
API 请求限制
请求单位 配额适用于所有 MemoryStoreService API 调用。此配额是 1000 + 120 * [并发用户数量] 每分钟请求单位。
大多数 API 调用只消耗一个请求单位,少数例外:
MemoryStoreSortedMap:GetRangeAsync()
根据返回的项目数量消耗单位。例如,如果此方法返回 10 个项目,则调用计为 10 个请求单位。如果返回空响应,则计为一个请求单位。
根据返回的项目数量消耗单位,就像 MemoryStoreSortedMap:GetRangeAsync() 一样,但在读取时每两秒消耗一个额外的单位。使用 waitTimeout 参数指定最大读取时间。
MemoryStoreHashMap:UpdateAsync()
最少消耗两个单位。
MemoryStoreHashMap:ListItemsAsync()
消耗 [扫描的分区数量] + [返回的项目数量] 单位。
请求配额也适用于游戏层面,而不是服务器层面。这提供了灵活性,可以在多个服务器之间分配请求,只要总请求速率不超过配额。如果您超过配额,则在服务限制您的请求时会收到错误响应。
使用可用的 可观察性 功能,您可以实时查看游戏的请求单位配额。
数据结构大小限制
对于单个排序映射或队列,适用以下大小和项目数量限制:
- 最大项目数量:1,000,000
- 最大总大小(包括排序映射的键):100 MB
每分区限制
请参见 每分区限制。
最佳实践
为了保持您的内存使用模式最佳,避免触及 限制,请遵循以下最佳实践:
删除处理过的项目。 持续清理使用 MemoryStoreQueue:RemoveAsync() 方法的读取项目,对于队列和使用 MemoryStoreSortedMap:RemoveAsync() 的排序映射,可以释放内存,并保持数据结构最新。
在添加数据时将过期时间设置为可能的最小时间框架。 尽管 MemoryStoreQueue:AddAsync() 和 MemoryStoreSortedMap:SetAsync() 的默认过期时间为 45 天,但设置最短的时间可以自动清理旧数据,防止它们占用内存使用配额。
- 不要存储大量长过期的数据,因为这会导致超过内存配额,并可能导致严重问题,从而破坏整个游戏。
- 始终显式删除不需要的项目或设置短的项目过期。
- 通常,您应该使用显式删除来释放内存,项过期则作为一种安全机制,防止未使用的项目长期占用内存。
只在内存中保留必要的值。
例如,对于拍卖屋游戏,您只需要维护最高竞标。您可以在一个键上使用 MemoryStoreSortedMap:UpdateAsync() 保持最高竞标,而不是在数据结构中保持所有竞标。
使用 指数退避 来帮助保持低于 API 请求限制。
例如,如果您收到 DataUpdateConflict,可以在两秒后重试,然后是四秒,八秒等,而不是不断向 MemoryStoreService 发送请求以获取正确的响应。
通过 分片 将巨大数据结构拆分为多个较小的数据结构。
通常更容易在较小的结构中管理数据,而不是将所有内容都存储在一个大型数据结构中。此方法还可以帮助避免使用和速率限制。例如,如果您有一个使用前缀作为键的排序映射,请考虑将每个前缀分隔到自己的排序映射中。对于尤其受欢迎的游戏,您甚至可以根据用户 ID 的最后几位将用户分隔到多个映射中。
分片 哈希映射中频繁访问的键,使用多个键的副本来分散负载。
压缩存储的值。
例如,考虑使用 LZW 算法来减少存储值的大小。
注册扩展服务。
通过加入 扩展服务,您可以增加存储和请求限制配额。
可观察性
可观察性仪表板 提供了监控和故障排除内存存储使用的见解和分析。通过实时更新图表,展示内存使用情况和 API 请求的不同方面,您可以跟踪游戏的内存使用模式,查看当前的配额,监控 API 状态,并识别性能优化的潜在问题。
以下表格列出了可在可观察性仪表板的 按状态请求计数 和 按 API x 状态请求 图表中找到的所有响应状态码及其描述。有关如何解决这些错误的更多信息,请参见 故障排除。有关错误相关特定配额或限制的信息,请参见 限制和配额。
| 状态代码 | 描述 |
|---|---|
| 成功 | 成功。 |
| 数据结构内存超限 | 超出数据结构级别内存大小限制(100MB)。 |
| 数据更新冲突 | 由于并发更新而发生冲突。 |
| 访问被拒绝 | 无权访问游戏数据。此请求不消耗请求单位或使用配额。 |
| 内部错误 | 内部错误。 |
| 无效请求 | 请求没有所需的信息或信息格式错误。 |
| 数据结构项目超限 | 超出数据结构级别项目数量限制(1M)。 |
| 未找到项目 | MemoryStoreQueue:ReadAsync() 或 MemoryStoreSortedMap:UpdateAsync() 中未找到项目。ReadAsync() 每 2 秒轮询一次,并在找到队列中的项目之前返回此状态代码。 |
| 数据结构请求超限 | 超出数据结构级别请求单位限制(每分钟 100,000 请求单位)。 |
| 分区请求超限 | 超过分区请求单位限制。 |
| 总请求超限 | 超出宇宙级请求单位限制。 |
| 总内存超限 | 超出宇宙级内存配额。 |
| 项目值大小过大 | 值大小超出限制(32KB)。 |
以下表格列出来自客户端的状态代码,这些状态代码目前在可观察性仪表板上不可用。
| 状态代码 | 描述 |
|---|---|
| 内部错误 | 内部错误。 |
| 未发布地点 | 您必须发布此地点才能使用 MemoryStoreService。 |
| 无效客户端访问 | 必须从服务器调用 MemoryStoreService。 |
| 无效过期时间 | 字段 'expiration' 时间必须在 0 到 3,888,000 之间。 |
| 无效请求 | 无法将值转换为 json。 |
| 无效请求 | 无法将 sortKey 转换为有效的数字或字符串。 |
| 变换回调失败 | 调用变换回调函数失败。 |
| 请求被限制 | 最近的 MemoryStores 请求触及一个或多个限制。 |
| 更新冲突 | 超过最大重试次数。 |
故障排除
以下表格列出了每个响应状态代码的推荐解决方案:
| 错误 | 故障排除选项 |
|---|---|
| 数据结构请求超限 / 分区请求超限 |
|
| 总请求超限 | |
| 数据结构项目超限 |
|
| 数据结构内存超限 | |
| 总内存超限 | |
| 数据更新冲突 |
|
| 内部错误 |
|
| 无效请求 |
|
| 项目值大小过大 |
|
在工作室中测试和调试
MemoryStoreService 中的数据在工作室和生产之间是隔离的,因此在工作室中更改数据不会影响生产行为。这意味着您在工作室中的 API 调用不会访问生产数据,从而允许您安全地测试内存存储和新功能,然后再进入生产。
工作室测试与生产有相同的 限制和配额。对于基于用户数量计算的配额,最终的配额可能非常小,因为您是工作室测试中的唯一用户。在工作室测试时,您还可能会注意到延迟稍高和错误率上升,这与在生产中的使用不同,这是由于执行一些额外检查来验证访问和权限。
有关如何调试实时游戏或在工作室中测试内存存储的信息,请使用 开发者控制台。