世代编号和前提条件

对象世代编号使用户能够对数据资源进行唯一标识,并应用前提条件以保证多步骤事务的原子性。

世代

即使未启用对象版本控制,所有 Cloud Storage 对象仍会具有世代编号和元数据世代编号。每次覆盖对象时,世代编号都会发生更改;每次更新对象的元数据时,元数据世代编号也会发生更改。

对于每个新的对象世代,元数据世代编号会重置为 1,因此,仅当与世代编号结合到一起时,元数据世代编号才具有实际意义。

存储分区也会保留元数据世代编号,其目的是帮助用户对存储分区元数据状态进行唯一标识。存储分区没有负载数据,因此没有世代编号,其元数据世代编号本身具有实际意义。

示例:并行上传

并行上传中,您需要将对象分成多个片段,将各片段同时上传到一个临时位置,然后执行 compose 从这些临时片段合成原始对象。如果某一独立进程意外使用了与您上传的一个或多个临时片段相同的名称,那么,当您尝试对对象执行 compose 时,系统将使用不正确的组成部分来合成对象,从而导致对象发生损坏。

使用世代编号可以避免发生此类损坏情形。如果您在发出 compose 请求时将每个上传片段的世代编号包含在内,则会产生两种结果:要么 compose 操作使用正确的片段来合成对象;要么请求失败并返回 404 Not Found 响应。

前提条件

前提条件向 Cloud Storage 告知,仅当受影响的对象的世代编号或元数据世代编号满足您的前提条件时,Cloud Storage 才应执行请求。这些针对世代编号和元数据世代编号的检查可以确保对象处于预期状态,让您安全地对对象执行“读取-修改-写入”更新和有条件操作。

match 前提条件使用特定的世代编号或元数据世代编号时,请求所适用的 Cloud Storage 对象也必须具有相同的世代编号/元数据世代编号。如果满足上述条件,则请求成功。否则,请求失败并返回 412 Precondition Failed 响应。

如果 match 前提条件使用 0 值而非世代编号,那么,只有 Cloud Storage 存储分区中没有活跃对象具有请求中指定的名称时,请求才会成功。如果存在这样的对象,则请求将失败并返回 412 Precondition Failed 响应。

前提条件通常用在更改型请求(上传、删除、复制或元数据更新)中,其目的是防止出现竞争条件。当同一请求被反复发送,或独立进程相互干扰时,可能会出现竞争条件。例如,网络中断后多次重试请求,或者用户对同一对象执行“读取-修改-写入”操作;这些情况都可能会产生竞争条件。

除了那些使用世代编号和元数据世代编号的前提条件之外,我们还提供了使用 ETag 的前提条件。非复合对象的 XML API ETag 仅在内容更改时发生更改,而复合对象和 JSON API 资源的 ETag 会在内容或元数据更改时发生更改。如需详细了解 ETag,请参阅哈希和 ETag:最佳做法

前提条件的费用

使用前提条件时,会有性能影响和结算费用:执行每个更改操作时,您还会发出一个产生费用的 GET 元数据请求,以便确定对象的世代编号/元数据世代编号。在性能方面,前提条件会增加一次额外的往返,因此有可能导致总体操作延迟中网络延迟部分翻倍。那些对延迟时间敏感的操作需要考虑这一点。从价格方面考虑,允许您使用前提条件的 GET 元数据请求的结算费率为每 1 万次操作 0.004 美元。

根据您的应用情况,有一些方法可以避免使用前提条件的性能影响和结算费用,例如:

  • 在本地存储对象的世代编号和元数据世代编号,以便您提前知晓要在前提条件中使用的正确编号。
  • 使用适当的命名方案避免对同一对象名称进行多重更改,进而避免使用前提条件。
  • 让您的应用知晓哪些对象是新创建的,这样一来,您就可以提前知道应在何时使用 if-generation-match:0 前提条件。
  • 记住更改之前执行的 GET 调用的结果。

XML API 中的前提条件

在 XML API 中,世代编号和元数据世代编号会显示在 x-goog-generationx-goog-metageneration 响应标头中。这些标头会以对象 HEAD 请求的响应形式返回。

如需查看前提条件请求标头的完整列表,请参阅 HTTP 标头参考;您可以使用这些标头,根据所请求对象的状态执行条件请求。例如,您可以执行以下操作:

  • 使用 x-goog-if-generation-match 前提条件,仅当其中的世代编号与所请求对象的世代编号匹配时才执行请求。如果使用 0(而不是世代编号),那么,只有在您的存储分区中没有活跃对象与请求中指定的对象匹配时,请求才会成功。

  • 使用 x-goog-if-metageneration-match 前提条件,仅当标头中的元数据世代编号与所请求对象的元数据世代编号匹配时才执行请求。

  • 搭配 GETHEAD 请求使用 If-Modified-Since 前提条件。仅当对象的最新世代的创建时间(即对象的最后修改时间)晚于前提条件中指定的时间时,才会执行这些请求。

  • 搭配 GETHEAD 请求使用 ETag 和 If-MatchIf-None-Match 前提条件。根据条件,仅当请求的对象与前提条件中指定的 ETag 匹配或不匹配时,才会执行这些请求。

  • 在同一请求中使用多个前提条件。例如,如果您使用的是 x-goog-if-generation-match,则还可以使用 x-goog-if-metageneration-match

JSON API 中的前提条件

在 JSON API 中,您可以通过包含对象存储分区资源的响应的 generationmetageneration 属性来获取世代编号和元数据世代编号。对象或存储分区资源会在对象存储分区GET 请求响应正文中返回。

如需在请求中使用前提条件,请将前提条件作为查询参数添加到网址的末尾。对于每个前提条件,请添加一个与前提条件同名的查询参数。 例如,以下请求使用 ifGenerationMatch: https://www.googleapis.com/storage/v1/b/testgrid-triage-testing/o/test?ifGenerationMatch=1122334455

在此示例中,只有在对象的世代是 1122334455 时,API 才会执行此请求。

JSON API 还支持为所有资源(包括存储分区、对象和 ACL)使用 HTTP 1.1 ETag 以及相应的 HTTP If-MatchIf-None-Match 标头。每当返回资源时,ETag 都会作为响应标头的一部分返回,并包含在资源中。

以下是一些前提条件的示例,您可以使用这些前提条件,根据所请求对象的状态执行条件请求:

  • 使用 ifGenerationMatchifGenerationNotMatch 前提条件,仅当属性中的世代编号与所请求对象的世代编号匹配/不匹配时才执行请求。如果在 ifGenerationMatch 中使用 0 而不是世代编号,那么,只有在存储分区中没有活跃对象与请求中指定的对象匹配时,请求才会成功。

  • 使用 ifMetagenerationMatchifMetagenerationNotMatch 前提条件对存储分区或对象执行请求。只有在属性中的元数据世代编号与所请求的存储分区或对象的元数据世代编号匹配/不匹配时,请求才会成功。

  • 在请求中使用 ETag 以及 If-MatchIf-None-Match 前提条件作为标头。仅当请求的对象与前提条件中指定的 ETag 匹配或不匹配时,才会执行这些请求。

  • 在同一请求中使用多个前提条件。例如,如果您在对象请求中使用 ifGenerationMatch,也可以使用 ifMetagenerationMatch

请注意,ACL 操作不接受世代前提条件和元数据世代前提条件;请改用访问控制条目资源 ETag。您可以在每个访问控制条目资源中找到 Etag(也可以从包含该资源的对象或存储分区资源进行访问)。

竞争条件的示例

同时读取-修改-写入

在更新存储分区或对象元数据时,常见的模式包括以下几个步骤:读取当前状态、在本地应用修改、以及将修改后的元数据发回 Cloud Storage 进行写入。如果两个或更多个独立进程同时尝试执行此序列,此模式可能并不稳定。

请考虑以下情况:您希望为协作者添加一个 ACL 条目,以允许他们访问您的存储分区。同时,您的一位同事想要移除另一位不再需要访问存储分区的协作者。

为实现目的,您和您的同事都会读取存储分区元数据的同一初始状态,并且你们都对存储分区的 ACL 条目做出所需的修改。当您将修改写回到 Cloud Storage 时,元数据会正确更新。不幸的是,一旦您的同事上传他的修改,您所做的更改就会丢失,因为他无法考虑到您也做出了更新。因此,您的协作者的 ACL 条目将丢失,她将无法访问您的存储分区,并且没有人知道发生了什么情况(除非您再次去检查 ACL 条目)。

防止出现竞争条件

为了防止出现此竞争条件,您和您的同事可以为各自的写入操作添加 if-metageneration-match 前提条件。在此前提条件中,您们两位都使用存储分区的元数据世代编号(此编号包含在您在初始读取操作中收到的元数据中)。

如果您的修改操作会添加 ACL 条目,存储分区的元数据世代编号会发生变化。使用前提条件后,当您的同事尝试写入他的 ACL 条目版本时,存储分区的元数据世代编号会与前提条件中的编号不匹配,因此系统会向他告知更新失败并显示响应代码 412 Precondition Failed。收到此响应代码后,您的同事可以做出相应的反应,例如使用更新后的元数据执行新的“读取-修改-写入”循环。

多次重试请求

Cloud Storage 是一个分布式系统。由于请求可能会因网络或服务状况而失败,因此,Google 建议您使用指数回退算法重试失败的请求。但是,由于分布式系统的性质,这些重试操作有时会导致令人惊讶的行为。

请考虑以下情况:您想要删除存储在 Cloud Storage 中的文件 file.txt。之后,您希望将具有相同名称的新文件添加到 Cloud Storage 中。

为了完成此操作,您发出删除请求以删除该对象。但是,网络条件(例如,中间路由器暂时失去连接)会阻止请求到达 Cloud Storage,因此您不会收到响应。

由于没有收到第一个请求的响应,您对该对象发出第二个删除请求。这一次请求成功了,并且您收到了确认删除对象的响应。一分钟后,您决定上传新的 file.txt,并且上传成功。

如果连接中断的路由器随后重新建立连接,并将原本好像丢失的删除请求继续发送到 Cloud Storage,则会出现竞争条件。当请求到达 Cloud Storage 时,请求会成功,因为存在新的 file.txt。Cloud Storage 会发送一则响应,而您不会收到此响应,因为您的客户端已停止侦听响应。 不仅新文件会被删除(与您的意图相反),而且您还不知道发生了第二次删除操作。

下图显示了上述过程:

防止出现竞争条件

为防止出现上述情况,您应首先获取 file.txt 的元数据,以确定它的当前世代。然后发送删除请求并在其中加入使用该世代编号的 if-generation-match 前提条件。通过使用该前提条件,您可以确保只有具有该特定世代编号的对象才会被删除(无论删除请求何时到达 Cloud Storage,或发送了多少次具有该前提条件的删除请求)。使用 if-generation-match 前提条件时,任何意外尝试更改 file.txt 的其他世代的操作都将失败,并返回响应代码 412 Precondition Failed

由于类似的网络中断可能会导致上传请求(在您的删除请求之后发出)出现竞争条件,因此,通过向上传请求应用 if-generation-match:0,您可以避免很多此类情况。使用此前提条件可以确保重试的上传作业不会意外地将对象写入两次,因为此前提条件规定,仅当对象当前不存在任何世代时,请求才能成功。

借助这些前提条件,您可以在执行删除和上传请求时防止数据意外丢失。下图显示了此过程:

if-generation-match:0 的限制

如果第一个对象被删除,if-generation-match:0 无法防止对象被创建两次,因为缺失的对象不具有唯一标识性。考虑以下的情况,虽然没有发生数据丢失,但却生成了一个您意料之外的文件:

  1. 您首先对 file.txt 的元数据发出 GET 请求,以找出其世代编号。在响应中,您发现 file.txt 不存在。

  2. 知道这一点后,您使用 if-generation-match:0 前提条件发出上传 file.txt 的请求,但因中间路由器的连接暂时中断,请求发生超时。

  3. 第一次失败后,您再次使用 if-generation-match:0 前提条件重试上传请求。这一次请求成功了。

  4. 不久后,您发送一条删除 file.txt 的请求,并且该请求取得成功。

  5. 如果连接中断的路由器现在重新建立连接,并将您的第一个上传请求发送到 Cloud Storage,则与此请求一起发出的前提条件仍然存在匹配,因此,系统将重新创建 file.txt。无论是否存在前提条件,file.txt 都会再次被意外上传。

此页内容是否有用?请给出您的反馈和评价:

发送以下问题的反馈:

此网页
Cloud Storage
需要帮助?请访问我们的支持页面