交易

Cloud Datastore 支援「交易」。交易是一項或一組不可分割的操作,這表示交易中的操作必須全部完成,或是全部不進行。應用程式可以在一次交易中執行多項操作與計算。

使用交易

「交易」是指在一或多個實體上的一組 Cloud Datastore 操作。每次交易皆保證具有單元性,這代表交易絕不會部分完成。不是交易中的操作全部完成,就是全部不予進行。交易的最長持續時間為 60 秒,而在 30 秒後則有 10 秒的閒置到期時間。

如有以下情形,操作可能會失敗:

  • 同一個實體群組上嘗試進行太多並行修改。
  • 交易超過資源限制。
  • Cloud Datastore 遭遇內部錯誤。

在這些情況下,Cloud Datastore API 都會引發例外狀況。

交易是 Cloud Datastore 的選用功能;不必透過交易來執行 Cloud Datastore 作業。

以下示範如何在名為 JoeEmployee 種類實體中更新名為 vacationDays 的欄位:

Java 8

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();
  }
}

Java 7

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 將多個實體儲存在分散式網路中的相同部分。交易可為實體群組設定 Cloud Datastore 作業,且所有作業都會做為群組套用,或者如果交易失敗,則不會套用。

應用程式建立實體時,可將其他實體指派為新實體的「父項」。為新實體指派父項會將新實體放在與父系實體相同的實體群組中。

沒有父項的實體則是「根」實體。實體如果是另一個實體的父項,也可以擁有父項。從實體到根一連串的父系實體是實體的「路徑」,此外路徑的成員是實體的「祖系」。實體的父項是在實體建立時定義,而且之後無法進行變更。

以指定根實體做為祖系的每個實體位於相同的實體群組中。群組中的所有實體都儲存在相同的 Cloud Datastore 節點中。單一交易可以修改單一群組中的多個實體,也可以透過將新實體的父項設定為群組中的現有實體,來將新實體新增至群組。以下的程式碼片段示範各種不同實體類型的交易:

Java 8

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();

Java 7

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 實體的金鑰,接著使用該金鑰建立 Message 實體並將其設為持續性 (該實體與 MessageBoard 實體位在同一個實體群組中):

Java 8

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();

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

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

Transaction txn = datastore.beginTransaction();

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);
datastore.put(txn, message);

txn.commit();

使用跨群組交易

跨群組交易 (也稱為 XG 交易) 會跨越多個實體群組運作,其行為類似於單一群組交易 (如上所述),但如果程式碼嘗試從一個以上的實體群組更新實體,則跨群組交易不會失敗。

跨群組交易和單一群組交易的使用方法類似,不過當您展開交易時,需要使用 TransactionOptions 來指定您要讓交易成為跨群組交易:

Java 8

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();

Java 7

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();

在交易中可以完成什麼

Cloud Datastore 針對單一交易可執行的操作施加了限制。

假如交易是單一群組交易,則交易中的所有 Cloud Datastore 操作必須對同一個實體群組中的實體進行。假設交易為跨群組交易,則必須對最多 25 個實體群組中的實體進行操作。這些操作包括使用上階查詢實體、使用鍵擷取實體、更新實體以及刪除實體。請注意,每個根實體都屬於獨立的實體群組,因此除了跨群組交易,您無法在一個以上的根實體上建立或操作單一交易。

當兩個或多個交易同時嘗試修改一或多個共用實體群組中的實體時,只有提出修改的第一個交易才能成功;其他則會在提出時失敗。由於這項設計的緣故,使用實體群組會限制您可以對群組中任何實體執行的並行寫入次數。當交易開始時,Cloud Datastore 會透過檢查交易中使用實體群組的上次更新時間來使用最佳化並行控制。在為實體群組提出交易時,Cloud Datastore 會再次檢查交易中使用實體群組的上次更新時間。假如該值自初始檢查後已更改,則會擲回例外情況。如需實體群組的說明,請參閱 Cloud Datastore 總覽頁面。

應用程式只有在包含祖系篩選器的情況下可執行查詢。應用程式也可以在交易時,藉由鍵來取得資料儲存庫實體。您可以在交易之前先準備好「金鑰」,或是可以在交易中藉由金鑰名稱或 ID 來建構「金鑰」。

隔離與一致性

在交易之外,Cloud Datastore 的隔離等級最接近於已提出的讀取。交易內部則會強制執行可序列化隔離。這代表另一個交易無法並行修改由這個交易讀取或修改的資料。如要進一步瞭解隔離等級,請參閱維基百科的可序列化隔離條目和交易隔離的相關說明。

在交易中,所有讀取都會反映交易開始時 Cloud Datastore 的當前一致狀態。在交易開始時,交易內部的查詢和獲取一定可看到 Cloud Datastore 的單一且一致的快照。交易實體群組中的實體和索引列已完全更新,以便查詢傳回完整、正確的結果實體組合,而不會發生在交易隔離 (可能在交易之外的查詢中發生) 所述的誤判和漏判情形。

此一致性的快照檢視也擴充至交易內部的寫入後的讀取。與大部分資料庫不同,Cloud Datastore 交易內部的查詢和獲取「不會」看到先前在該交易中寫入的結果。尤其如果實體在交易內經過修改或刪除,查詢或取得 (get) 將傳回交易開始時的「原始」版實體,如果當時該實體尚不存在,則不傳回任何實體。

交易用途

這個範例將說明交易的一種用途:將實體更新為新的屬性值,而不是其目前的值。由於 Cloud Datastore API 不會重試交易,因此我們可以新增邏輯,讓交易在其他要求同時更新相同 MessageBoard 或其任何 Messages 時重試。

Java 8

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();
    }
  }
}

Java 7

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 失敗。應用程式可重複這個交易藉以使用新資料。

交易的另一個常見用途是擷取具有指定金鑰的實體,或是建立這個金鑰 (假如金鑰不存在):

Java 8

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();

Java 7

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 實體的情況。在沒有交易的情況下,假如實體不存在且兩個使用者嘗試建立該實體,則第二個實體會在不知情的情況下覆寫第一個實體。換句話說,如果交易存在,則第二個嘗試將自動失敗。但是當這個操作可行時,應用程式則可重試以擷取實體並加以更新。

當交易失敗時,您可以讓應用程式重試交易,直到成功為止,也可以將交易傳播至應用程式的使用者介面層級,讓使用者處理錯誤。您不必為每一個交易建立重試迴圈。

最後,您可以使用交易來讀取 Cloud Datastore 的一致快照。當需要多個讀取取得以轉譯網頁,或是匯出必須是一致的資料時,這項功能相當有用。由於這種交易不執行寫入,所以通常稱為「唯讀」交易。單一群組唯讀交易一律不會因並行修改而失敗,因此您不必在失敗時重試。不過,跨群組交易可能由於並行修改而失敗,因此必須進行重試。修訂和復原唯讀交易都是免人工管理的。

Java 8

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();

Java 7

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();

交易工作排入佇列

您可以將工作排入佇列做為 Cloud Datastore 交易的一部分,以便只在成功修訂交易時才將工作排入佇列,並確保工作已順利新增。如果該交易修訂完成,則可保證將該工作新增至佇列。工作新增至佇列後,並不能保證立即執行該工作,而且在工作內執行的所有作業會在原始交易中個別執行。工作會持續重試,直到成功為止。所有在交易中加入佇列的工作都適用此原則。

您可以透過交易工作,調用 Cloud Datastore 交易中的非 Cloud Datastore 動作 (例如傳送電子郵件以確認購買),因此非常實用。您也可以將 Cloud Datastore 動作與交易綁定,例如,只有在交易成功時,才會修訂對於交易之外的其他實體群組所做的變更。

應用程式無法在單一交易期間將超過五個的交易工作插入工作佇列中。交易工作不能有使用者指定的名稱。

Java 8

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

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

// ...

txn.commit();

Java 7

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

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

// ...

txn.commit();
本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁
Java 適用的 App Engine 標準環境