交易

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

使用交易

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

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

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

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

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

datastore.RunInTransaction 函式會在交易中執行提供的函式。


package counter

import (
	"context"
	"fmt"
	"net/http"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/taskqueue"
)

func init() {
	http.HandleFunc("/", handler)
}

type Counter struct {
	Count int
}

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	key := datastore.NewKey(ctx, "Counter", "mycounter", 0, nil)
	count := new(Counter)
	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		// Note: this function's argument ctx shadows the variable ctx
		//       from the surrounding function.
		err := datastore.Get(ctx, key, count)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		count.Count++
		_, err = datastore.Put(ctx, key, count)
		return err
	}, nil)
	if err != nil {
		log.Errorf(ctx, "Transaction failed: %v", err)
		http.Error(w, "Internal Server Error", 500)
		return
	}

	fmt.Fprintf(w, "Current count: %d", count.Count)
}

如果函式傳回 nilRunInTransaction 會嘗試認可交易,成功時就會傳回 nil。如果函式傳回非 nil 的錯誤值,則不會套用任何 Datastore 變更,RunInTransaction 也會傳回相同的錯誤。

如果 RunInTransaction 因衝突而無法認可交易,就會重試,並在三次嘗試後放棄。這表示交易函式應該是等冪的,也就是說,執行多次的情況下,其結果是相同的。請注意,datastore.Get 在對切割欄位進行解除封送處理時,並不是冪等的。

在交易中可執行的操作

Datastore 會限制單一交易中可執行的操作。

如果交易是單一群組交易,交易中的所有 Datastore 操作都必須在同一個實體群組中的實體上運作;如果交易是跨群組交易,則必須在最多二十五個實體群組中的實體上運作。這些操作包括使用祖系查詢實體、使用鍵擷取實體、更新實體以及刪除實體。請注意,每個根實體都屬於不同的實體群組,因此單一交易無法建立或操作多個根實體,除非是跨群組交易。

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

隔離與一致性

在交易之外,Datastore 的隔離等級最接近已修訂的讀取作業;在交易內部,系統則會強制執行可序列化隔離,這代表另一項交易無法並行修改由這項交易讀取或修改的資料。

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

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

交易用途

這個範例將說明交易的一種用途:將實體更新為新的屬性值,而不是其目前的值。

func increment(ctx context.Context, key *datastore.Key) error {
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		count := new(Counter)
		if err := datastore.Get(ctx, key, count); err != nil {
			return err
		}
		count.Count++
		_, err := datastore.Put(ctx, key, count)
		return err
	}, nil)
}

這需要使用交易,因為在此程式碼擷取物件之後,其他使用者可能會在系統尚未儲存已修改物件的情況下更新值。如果沒有交易,則使用者的要求將採用其他使用者更新之前的 count 值,而儲存也將覆寫新的值。但是藉由交易,應用程式將得知其他使用者的更新。如果實體在交易期間更新,則系統將重試交易,直到所有步驟不受中斷地完成。

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

type Account struct {
	Address string
	Phone   string
}

func GetOrUpdate(ctx context.Context, id, addr, phone string) error {
	key := datastore.NewKey(ctx, "Account", id, 0, nil)
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		acct := new(Account)
		err := datastore.Get(ctx, key, acct)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		acct.Address = addr
		acct.Phone = phone
		_, err = datastore.Put(ctx, key, acct)
		return err
	}, nil)
}

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

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

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

交易工作排入佇列

您可以將工作排入佇列做為 Datastore 交易的一部分,以便只在成功修訂交易時才將工作排入佇列,並確保工作已順利新增。工作新增至佇列後,不一定會立即執行,因此該工作並不是交易的一部分。不過,將工作排入佇列後,該工作將不斷重試直到成功為止。這項限制適用於在 RunInTransaction 函式中排入佇列的任何工作。

交易工作非常實用,因為您可以將非 Datastore 動作與取決於交易成功的交易結合 (例如傳送電子郵件來確認購買交易)。您也可以將 Datastore 動作繫結至交易,例如在交易成功時,將變更提交至交易以外的實體群組。

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

datastore.RunInTransaction(ctx, func(ctx context.Context) error {
	t := &taskqueue.Task{Path: "/path/to/worker"}
	if _, err := taskqueue.Add(ctx, t, ""); err != nil {
		return err
	}
	// ...
	return nil
}, nil)