Cloud Datastore 交易

「交易」是指在最多 25 個實體群組中的一或多個實體上執行的一組 Cloud Datastore 操作。每筆交易都不可分割,這代表交易絕不會部分完成。換句話說,交易中的操作不是全部完成,就是全部不予進行。

使用交易

交易的最長持續時間為 60 秒,30 秒後會有 10 秒的閒置到期時間。

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

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

在這些情況下,Cloud Datastore API 都會傳回錯誤訊息。

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

應用程式可以在單一交易中執行一組陳述式和 Cloud Datastore 操作,這樣一來,假如任何陳述式或操作引發例外狀況,系統就不會套用組合中的任何 Cloud Datastore 操作。應用程式會定義要在交易中執行的動作。

下列程式碼片段示範如何使用 Cloud Datastore API 執行交易;在本範例中,系統會將金額從一個帳戶轉至另一個帳戶。

C#

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore C# API 參考說明文件

private void TransferFunds(Key fromKey, Key toKey, long amount)
{
    using (var transaction = _db.BeginTransaction())
    {
        var entities = transaction.Lookup(fromKey, toKey);
        entities[0]["balance"].IntegerValue -= amount;
        entities[1]["balance"].IntegerValue += amount;
        transaction.Update(entities);
        transaction.Commit();
    }
}

Go

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Go API 參考說明文件

type BankAccount struct {
	Balance int
}

const amount = 50
keys := []*datastore.Key{to, from}
tx, err := client.NewTransaction(ctx)
if err != nil {
	log.Fatalf("client.NewTransaction: %v", err)
}
accs := make([]BankAccount, 2)
if err := tx.GetMulti(keys, accs); err != nil {
	tx.Rollback()
	log.Fatalf("tx.GetMulti: %v", err)
}
accs[0].Balance += amount
accs[1].Balance -= amount
if _, err := tx.PutMulti(keys, accs); err != nil {
	tx.Rollback()
	log.Fatalf("tx.PutMulti: %v", err)
}
if _, err = tx.Commit(); err != nil {
	log.Fatalf("tx.Commit: %v", err)
}

Java

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Java API 參考說明文件

void transferFunds(Key fromKey, Key toKey, long amount) {
  Transaction txn = datastore.newTransaction();
  try {
    List<Entity> entities = txn.fetch(fromKey, toKey);
    Entity from = entities.get(0);
    Entity updatedFrom =
        Entity.newBuilder(from).set("balance", from.getLong("balance") - amount).build();
    Entity to = entities.get(1);
    Entity updatedTo = Entity.newBuilder(to).set("balance", to.getLong("balance") + amount)
        .build();
    txn.put(updatedFrom, updatedTo);
    txn.commit();
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

Node.js

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Node.js API 參考說明文件

async function transferFunds(fromKey, toKey, amount) {
  const transaction = datastore.transaction();
  await transaction.run();
  const results = await Promise.all([
    transaction.get(fromKey),
    transaction.get(toKey),
  ]);
  const accounts = results.map(result => result[0]);

  accounts[0].balance -= amount;
  accounts[1].balance += amount;

  transaction.save([
    {
      key: fromKey,
      data: accounts[0],
    },
    {
      key: toKey,
      data: accounts[1],
    },
  ]);

  return transaction.commit();
}

PHP

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore PHP API 參考說明文件

/**
 * Update two entities in a transaction.
 *
 * @param DatastoreClient $datastore
 * @param Key $fromKey
 * @param Key $toKey
 * @param $amount
 */
function transfer_funds(
    DatastoreClient $datastore,
    Key $fromKey,
    Key $toKey,
    $amount
) {
    $transaction = $datastore->transaction();
    // The option 'sort' is important here, otherwise the order of the result
    // might be different from the order of the keys.
    $result = $transaction->lookupBatch([$fromKey, $toKey], ['sort' => true]);
    if (count($result['found']) != 2) {
        $transaction->rollback();
    }
    $fromAccount = $result['found'][0];
    $toAccount = $result['found'][1];
    $fromAccount['balance'] -= $amount;
    $toAccount['balance'] += $amount;
    $transaction->updateBatch([$fromAccount, $toAccount]);
    $transaction->commit();
}

Python

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Python API 參考說明文件

def transfer_funds(client, from_key, to_key, amount):
    with client.transaction():
        from_account = client.get(from_key)
        to_account = client.get(to_key)

        from_account['balance'] -= amount
        to_account['balance'] += amount

        client.put_multi([from_account, to_account])

Ruby

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Ruby API 參考說明文件

def transfer_funds from_key, to_key, amount
  datastore.transaction do |tx|
    from = tx.find from_key
    from["balance"] -= amount
    to = tx.find to_key
    to["balance"] += amount
    tx.save from, to
  end
end

請注意,為了讓範例保持精簡,我們有時會省略交易失敗時會進行的 rollback。在實際執行的程式碼中,請務必確認每項交易是否已明確修訂或復原。

在交易中可執行的操作

交易中的所有 Cloud Datastore 操作都可在最多 25 個實體群組上運作。這些操作包括使用祖系查詢實體、使用鍵擷取實體、更新實體以及刪除實體。

當兩個或多項交易同時嘗試修改一或多個共用實體群組中的實體時,只有第一個提出變更的交易會成功,其他則會在提出時失敗。由於這項設計的緣故,使用實體群組會限制您可以對群組中任何實體執行的並行寫入次數。當交易開始時,Cloud Datastore 會檢查交易中所使用的實體群組上次更新的時間,以便使用開放式並行控制;為實體群組認可交易時,Cloud Datastore 會再次檢查交易中所使用的實體群組上次更新的時間,假如該值和首次檢查時不同,則會傳回錯誤訊息。如需實體群組的說明,請參閱祖系路徑

隔離與一致性

在交易之外,Cloud Datastore 的隔離等級最接近已修訂的讀取作業;在交易內部,系統則會強制執行可序列化隔離,這代表另一項交易無法並行修改由這項交易讀取或修改的資料。如要進一步瞭解隔離等級,請參閱維基百科的可序列化隔離說明和交易隔離一文。

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

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

交易用途

交易的其中一種用途是將實體目前的屬性值變更為較新的值。在上方的 transferFunds 範例中,從一個帳戶領取金額然後轉移到另一個帳戶,會更新兩個實體的屬性值。Cloud Datastore API 不會自動重試交易,但您可以自行新增邏輯以便進行重試;假如其他要求同時對同一實體進行更新而導致衝突,您便可藉此予以解決。

C#

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore C# API 參考說明文件

/// <summary>
/// Retry the action when a Grpc.Core.RpcException is thrown.
/// </summary>
private T RetryRpc<T>(Func<T> action)
{
    List<Grpc.Core.RpcException> exceptions = null;
    var delayMs = _retryDelayMs;
    for (int tryCount = 0; tryCount < _retryCount; ++tryCount)
    {
        try
        {
            return action();
        }
        catch (Grpc.Core.RpcException e)
        {
            if (exceptions == null)
                exceptions = new List<Grpc.Core.RpcException>();
            exceptions.Add(e);
        }
        System.Threading.Thread.Sleep(delayMs);
        delayMs *= 2;  // Exponential back-off.
    }
    throw new AggregateException(exceptions);
}

private void RetryRpc(Action action)
{
    RetryRpc(() => { action(); return 0; });
}

[Fact]
public void TestTransactionalRetry()
{
    int tryCount = 0;
    var keys = UpsertBalances();
    RetryRpc(() =>
    {
        using (var transaction = _db.BeginTransaction())
        {
            TransferFunds(keys[0], keys[1], 10, transaction);
            // Insert a conflicting transaction on the first try.
            if (tryCount++ == 0)
                TransferFunds(keys[1], keys[0], 5);
            transaction.Commit();
        }
    });
    Assert.Equal(2, tryCount);
}

Go

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Go API 參考說明文件

type BankAccount struct {
	Balance int
}

const amount = 50
_, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
	keys := []*datastore.Key{to, from}
	accs := make([]BankAccount, 2)
	if err := tx.GetMulti(keys, accs); err != nil {
		return err
	}
	accs[0].Balance += amount
	accs[1].Balance -= amount
	_, err := tx.PutMulti(keys, accs)
	return err
})

Java

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Java API 參考說明文件

int retries = 5;
while (true) {
  try {
    transferFunds(fromKey, toKey, 10);
    break;
  } catch (DatastoreException e) {
    if (retries == 0) {
      throw e;
    }
    --retries;
  }
}
// Retry handling can also be configured and automatically applied using google-cloud-java.

Node.js

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Node.js API 參考說明文件

async function transferFundsWithRetry() {
  const maxTries = 5;

  async function tryRequest(currentAttempt, delay) {
    try {
      await transferFunds(fromKey, toKey, 10);
    } catch (err) {
      if (currentAttempt <= maxTries) {
        // Use exponential backoff
        setTimeout(async () => {
          await tryRequest(currentAttempt + 1, delay * 2);
        }, delay);
      }
      throw err;
    }
  }

  await tryRequest(1, 100);
}

PHP

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore PHP API 參考說明文件

$retries = 5;
for ($i = 0; $i < $retries; $i++) {
    try {
        transfer_funds($datastore, $fromKey, $toKey, 10);
    } catch (Google\Cloud\Exception\ConflictException $e) {
        // if $i >= $retries, the failure is final
        continue;
    }
    // Succeeded!
    break;
}

Python

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Python API 參考說明文件

for _ in range(5):
    try:
        transfer_funds(client, account1.key, account2.key, 50)
        break
    except google.cloud.exceptions.Conflict:
        continue
else:
    print('Transaction failed.')

Ruby

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Ruby API 參考說明文件

(1..5).each do |i|
  begin
    return transfer_funds from_key, to_key, amount
  rescue Google::Cloud::Error => e
    raise e if i == 5
  end
end

這需要使用交易,因為在此程式碼擷取物件之後,其他使用者可能會在系統尚未儲存已修改物件的情況下更新實體中的 balance 值。如果沒有使用交易,使用者的要求將在其他使用者更新之前運用 balance 值,且儲存的內容會覆寫新值,但是藉由交易,應用程式便可得知其他使用者的更新。

交易的另一個常見用途是擷取具有指定鍵的實體,或是在該鍵不存在的狀態下予以建立。本範例是以建立實體一節的 TaskList 範例做為基礎:

C#

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore C# API 參考說明文件

Entity task;
using (var transaction = _db.BeginTransaction())
{
    task = transaction.Lookup(_sampleTask.Key);
    if (task == null)
    {
        transaction.Insert(_sampleTask);
        transaction.Commit();
    }
}

Go

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Go API 參考說明文件

_, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
	var task Task
	if err := tx.Get(key, &task); err != datastore.ErrNoSuchEntity {
		return err
	}
	_, err := tx.Put(key, &Task{
		Category:    "Personal",
		Done:        false,
		Priority:    4,
		Description: "Learn Cloud Datastore",
	})
	return err
})

Java

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Java API 參考說明文件

Entity task;
Transaction txn = datastore.newTransaction();
try {
  task = txn.get(taskKey);
  if (task == null) {
    task = Entity.newBuilder(taskKey).build();
    txn.put(task);
    txn.commit();
  }
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Node.js

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Node.js API 參考說明文件

async function getOrCreate(taskKey, taskData) {
  const taskEntity = {
    key: taskKey,
    data: taskData,
  };
  const transaction = datastore.transaction();

  try {
    await transaction.run();
    const [task] = await transaction.get(taskKey);
    if (task) {
      // The task entity already exists.
      transaction.rollback();
    } else {
      // Create the task entity.
      transaction.save(taskEntity);
      transaction.commit();
    }
    return taskEntity;
  } catch (err) {
    transaction.rollback();
  }
}

PHP

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore PHP API 參考說明文件

$transaction = $datastore->transaction();
$existed = $transaction->lookup($task->key());
if ($existed === null) {
    $transaction->insert($task);
    $transaction->commit();
}

Python

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Python API 參考說明文件

with client.transaction():
    key = client.key('Task', datetime.datetime.utcnow().isoformat())

    task = client.get(key)

    if not task:
        task = datastore.Entity(key)
        task.update({
            'description': 'Example task'
        })
        client.put(task)

    return task

Ruby

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Ruby API 參考說明文件

task = nil
datastore.transaction do |tx|
  task = tx.find task_key
  if task.nil?
    task = datastore.entity task_key do |t|
      t["category"] = "Personal"
      t["done"] = false
      t["priority"] = 4
      t["description"] = "Learn Cloud Datastore"
    end
    tx.save task
  end
end

和先前一樣,如要處理另一個使用者嘗試建立或更新具有相同字串 ID 的實體的情況,就必須使用交易。在沒有交易的情況下,假如實體不存在且有兩位使用者嘗試建立該實體,則第二個實體會在不知情的狀態下覆寫第一個實體。

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

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

C#

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore C# API 參考說明文件

Entity taskList;
IReadOnlyList<Entity> tasks;
using (var transaction = _db.BeginTransaction(TransactionOptions.CreateReadOnly()))
{
    taskList = transaction.Lookup(taskListKey);
    var query = new Query("Task")
    {
        Filter = Filter.HasAncestor(taskListKey)
    };
    tasks = transaction.RunQuery(query).Entities;
    transaction.Commit();
}

Go

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Go API 參考說明文件

tx, err := client.NewTransaction(ctx, datastore.ReadOnly)
if err != nil {
	log.Fatalf("client.NewTransaction: %v", err)
}
defer tx.Rollback() // Transaction only used for read.

ancestor := datastore.NameKey("TaskList", "default", nil)
query := datastore.NewQuery("Task").Ancestor(ancestor).Transaction(tx)
var tasks []Task
_, err = client.GetAll(ctx, query, &tasks)

Java

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Java API 參考說明文件

Entity taskList;
QueryResults<Entity> tasks;
Transaction txn = datastore.newTransaction(
    TransactionOptions.newBuilder()
        .setReadOnly(ReadOnly.newBuilder().build())
        .build()
);
try {
  taskList = txn.get(taskListKey);
  Query<Entity> query = Query.newEntityQueryBuilder()
      .setKind("Task")
      .setFilter(PropertyFilter.hasAncestor(taskListKey))
      .build();
  tasks = txn.run(query);
  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Node.js

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Node.js API 參考說明文件

async function getTaskListEntities() {
  const transaction = datastore.transaction({readOnly: true});
  try {
    const taskListKey = datastore.key(['TaskList', 'default']);

    await transaction.run();
    const [taskList] = await datastore.get(taskListKey);
    const query = datastore.createQuery('Task').hasAncestor(taskListKey);
    const [taskListEntities] = await datastore.runQuery(query);
    await transaction.commit();
    return [taskList, taskListEntities];
  } catch (err) {
    transaction.rollback();
  }
}

PHP

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore PHP API 參考說明文件

$transaction = $datastore->readOnlyTransaction();
$taskListKey = $datastore->key('TaskList', 'default');
$query = $datastore->query()
    ->kind('Task')
    ->hasAncestor($taskListKey);
$result = $transaction->runQuery($query);
$taskListEntities = [];
/* @var Entity $task */
foreach ($result as $task) {
    $taskListEntities[] = $task;
}

Python

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Python API 參考說明文件

with client.transaction(read_only=True):
    task_list_key = client.key('TaskList', 'default')

    task_list = client.get(task_list_key)

    query = client.query(kind='Task', ancestor=task_list_key)
    tasks_in_list = list(query.fetch())

    return task_list, tasks_in_list

Ruby

要瞭解如何安裝及使用 Cloud Datastore 的用戶端程式庫,請參閱 Cloud Datastore 用戶端程式庫相關說明。詳情請參閱 Cloud Datastore Ruby API 參考說明文件

task_list_key = datastore.key "TaskList", "default"
datastore.read_only_transaction do |tx|
  task_list = tx.find task_list_key
  query = datastore.query("Task").ancestor(task_list)
  tasks_in_list = tx.run query
end

交易和實體群組

實體群組是透過祖系連接至共同根元素的一組實體。將資料歸納為實體群組,即可限制執行的交易類型:

  • 一項交易所存取的所有資料,最多只能存在於 25 個實體群組中。
  • 如果您想在交易中使用查詢,必須將資料歸納成實體群組,才可指定能夠比對出正確資料的祖系篩選條件。
  • 單一實體群組的寫入總處理量大約是每秒一次交易。之所以會有這項限制,原因在於 Datastore 會針對橫跨大範圍地理區域的每個實體群組執行免主機的同步複製作業,以發揮高度可靠性和容錯能力。

許多程式在廣泛取得不相關資料時,可以接受使用最終一致性 (亦即跨多個實體群組的非祖系查詢,這類查詢有時會傳回稍微過時的資料),待查看或編輯一組高度相關資料時再使用同步一致性 (祖系查詢,或者是單一實體的 lookup)。在此類應用程式中,針對各組高度相關的資料使用獨立實體群組,通常會有不錯的效果。詳情請參閱資料一致性

後續步驟

本頁內容對您是否有任何幫助?請提供意見:

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

這個網頁
Cloud Datastore 說明文件