事务

Datastore 支持事务。事务是一个或一组原子操作:事务中的所有操作要么都发生,要么都不发生。应用可以在单个事务中执行多个操作和计算。

使用事务

事务是对一个或多个实体执行的一组 Datastore 操作。每个事务都必须具有原子性,因此事务决不会只应用一部分。事务中的所有操作要么都应用,要么都不应用。事务的最长持续时间为 60 秒,在 30 秒后有 10 秒的空闲到期时间。

出现以下情况时,操作可能会执行失败:

  • 尝试对同一实体组进行太多并发修改。
  • 事务超出资源限制。
  • Datastore 遇到内部错误。

在以上所有情况下,Datastore API 都会引发异常。

事务是 Datastore 的可选功能;执行 Datastore 操作并非必须使用事务。

下面的示例更新类型为 Employee、名称为 Joe 的实体中名称为 vacationDays 的字段:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

请注意,为了使示例更加简洁,我们有时会省略 finally 块,如果事务仍然处于活动状态,该块将执行回滚。在生产代码中,请务必确保显式提交或回滚每个事务。

实体组

每个实体都属于一个实体组,后者是可以在一个事务中控制的一组实体(一个或多个实体)。实体组关系会让 App Engine 在分布式网络的相同部分中存储若干实体。事务针对实体组设置 Datastore 操作,且所有操作都会作为一个组进行应用。如果事务失败,则不应用任何操作。

当应用创建实体时,它可以将另一个实体指定为新实体的父实体。为新实体分配父实体时,会将新实体放在父实体所在的实体组中。

没有父实体的实体是根实体。如果某实体是另一个实体的父实体,则该实体也可以有父实体。从某实体到根实体的父实体链是该实体的路径,路径的成员是该实体的祖先实体。实体的父实体是在创建该实体时定义的,且以后不能再更改。

每个采用指定根实体作为祖先实体的实体都位于同一个实体组中。同一组中的所有实体都存储在同一 Datastore 节点中。一个事务可以修改一个组中的多个实体,或向组添加新实体(方法是以组中的现有实体作为新实体的父实体)。下面的代码段演示了针对各种实体的事务:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

在特定实体组中创建实体

在应用构造新实体时,您可以通过提供另一个实体的键来将新实体分配到一个实体组。以下示例构造了 MessageBoard 实体的键,然后使用该键创建并保留与 MessageBoard 属于同一实体组的 Message 实体:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);

Transaction txn = datastore.beginTransaction();
datastore.put(txn, message);

txn.commit();

使用跨组事务

跨组事务(也称为 XG 事务)跨多个实体组运行,其行为类似于上述单组事务,不同之处在于,如果代码尝试更新多个实体组中的实体,跨组事务不会失败。

使用跨组事务的方式与使用单组事务类似,只是在开始运行事务时,您需要使用 TransactionOptions 将事务指定为跨组事务:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

可以在事务中执行的操作

Datastore 对可以在单个事务中执行的操作设定了限制。

如果事务属于单组事务,则事务中的所有 Datastore 操作都必须针对同一实体组中的实体;如果事务属于跨组事务,则这些操作最多可以针对 25 个实体组中的实体。这包括通过祖先实体查询实体、通过键检索实体、更新实体以及删除实体。请注意,每个根实体都属于单独的实体组,因此,除非是跨组事务,否则单个事务无法创建或操作多个根实体。

当两个或多个事务同时尝试修改一个或多个公共实体组中的实体时,只有第一个提交其更改的事务可以应用成功;所有其他事务均会提交失败。由于上述机制,使用实体组会限制您可以对组中任意实体执行的并发写入数量。事务开始时,Datastore 会通过检查事务中使用的实体组的上次更新时间来使用乐观并发控制。在为实体组提交事务后,Datastore 会再次检查事务中使用的实体组的上次更新时间。如果该时间自初次检查后发生了变化,则会引发异常。

应用可以在事务期间执行查询,但前提是查询包含祖先实体过滤条件。应用还可以在事务期间通过键获取 Datastore 实体。您可以在事务之前准备键,也可以在事务内通过键名称或 ID 来构建键。

隔离和一致性

在事务之外,Datastore 的隔离级别最接近提交的读取操作。在事务之内,系统将采用可序列化隔离。这意味着另一个事务不能并发修改由此事务读取或修改的数据。

在事务中,所有读取都反映事务开始时 Datastore 的当前一致状态。事务内部的查询和获取操作可以保证在事务开始时看到单个一致的 Datastore 快照。事务的实体组中的实体和索引行将完全更新,以便查询操作返回一组完整且正确的结果实体,而不会出现可能在事务外部的查询操作中发生的假正例或假负例。

在事务内,此一致的快照视图还延伸到写入后的读取。与大多数数据库不同,Datastore 事务内部的查询和获取操作不会看到该事务中先前写入的结果。具体而言,如果某个实体在事务中被修改或删除,则查询或获取操作会返回该实体在事务开始时的原始版本;如果当时并不存在该实体,则系统不会返回任何内容

事务的用途

此示例说明了事务的一个用途:使用相对于属性当前值的新属性值更新实体。由于 Datastore API 不会重试事务,因此在另一个请求同时更新同一个 MessageBoard 或其任何 Messages 的情况下,我们可以为要重试的事务添加逻辑。

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

这需要使用事务,因为在此代码提取对象之后到保存修改后的对象之前的这段时间内,其他用户可能会更新该值。如果不使用事务,则用户的请求将使用另一用户进行更新前的 count 值,且保存操作会覆盖新值。如果使用了事务,应用会被告知其他用户在进行更新。如果在事务期间更新实体,则事务将失败并出现 ConcurrentModificationException。应用可以重复事务以使用新数据。

事务的另一个常见用途是提取具有指定键的实体;如果实体尚不存在,则创建该实体:

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

与之前一样,如果另一用户试图创建或更新具有同一字符串 ID 的实体,则需要使用事务来处理这种情况。不使用事务时,如果实体不存在且两位用户都试图创建它,则第二位用户会覆盖第一位用户的操作,而不知道实体已被创建。使用事务时,第二个尝试会出现原子性失败。如果需要,应用可以重新尝试提取实体并对其进行更新。

当事务失败时,您可以让应用重试事务直到成功,或者可以将错误传递到应用的用户界面层,让用户处理该错误。您无需对每个事务都创建重试循环。

最后,您可以使用事务来读取一致的 Datastore 快照。在需要进行多次读取来呈现页面或导出完全一致的数据时,这非常有用。此类事务通常称为“只读”事务,因为它们不执行写入操作。只读单组事务始终不会由于并发修改而执行失败,所以您无需执行失败重试。但是,跨组事务可能由于并发修改而执行失败,因此这些事务应当执行重试。提交和回滚只读事务都是空操作。

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

将事务性任务加入队列

您可以将任务作为 Datastore 事务的一部分加入队列,以便只有在成功提交事务时才会将任务加入队列,并且在这种情况下保证任务入队。如果事务确实得以提交,则保证任务入队。在入队后,不能保证任务会立即执行,并且在任务中执行的任何操作都独立于原始事务执行。该任务会一直重试,直到取得成功。在事务的上下文中入队的任何任务都是如此。

事务性任务非常有用,因为它们允许您在 Datastore 事务中包含非 Datastore 操作(例如发送电子邮件以确认购买)。您还可以将 Datastore 操作绑定到事务,例如,当且仅当事务成功时才提交在事务外对其他实体组的更改。

在单个事务期间,应用插入任务队列事务性任务不能超过五个。事务性任务不能具有用户指定的名称。

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();