交易

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

使用交易

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

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

  • 同一個實體群組上嘗試進行太多並行修改。
  • 交易超過資源限制。
  • 資料儲存庫發生內部錯誤。

在這些情況下,Datastore API 都會擲回例外狀況。

交易是 Datastore 的選用功能,您不一定要透過交易才能執行 Datastore 作業。

以下範例說明如何更新 Joe 類型 Employee 實體中的 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 操作都必須在同一個實體群組中的實體上運作;如果交易是跨群組交易,則必須在最多二十五個實體群組中的實體上運作。這些操作包括使用祖系查詢實體、使用鍵擷取實體、更新實體以及刪除實體。請注意,每個根實體都屬於不同的實體群組,因此單一交易無法建立或操作多個根實體,除非是跨群組交易。

當兩個或多個交易同時嘗試修改一或多個共用實體群組中的實體時,只有提出修改的第一個交易才能成功;其他則會在提出時失敗。由於這項設計的緣故,使用實體群組會限制您可以對群組中任何實體執行的並行寫入次數。當交易開始時,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();