交易

附註:我們強烈建議建構新應用程式的開發人員使用 NDB 用戶端程式庫,因為 NDB 用戶端程式庫與本用戶端程式庫相較之下有幾個優點,例如能透過 Memcache API 自動將實體加入快取。如果您目前使用的是舊版的 DB 用戶端程式庫,請參閱從 DB 至 NDB 的遷移指南

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

使用交易

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

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

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

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

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

應用程式可以在單一交易中執行一組陳述式和資料儲存作業,這樣一來,假如任何陳述式或作業引發例外狀況,系統就不會套用組合中的任何 Datastore 作業。應用程式使用 Python 函式定義要在交易中執行的操作。應用程式會使用 run_in_transaction 方法之一啟動交易,這取決於交易是否會存取單一實體群組中的實體,或是交易是否為跨群組交易。

針對只在交易中使用的函式常見用途,請使用 @db.transactional 修飾符:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

如果在不含交易的情況下呼叫函式,請呼叫包含函式的 db.run_in_transaction() 做為引數 (而不是進行修飾):

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction() 會採用函式物件,以及位置和關鍵字引數,以便傳遞至函式。如果函式傳回值,db.run_in_transaction() 就會傳回該值。

如果函式傳回,系統就會認可交易,並套用 Datastore 作業的所有效果。如果函式發生例外狀況,交易會「回溯」,且不會套用效果。請參閱上述有關例外的說明。

當一個交易函式從另一個交易中呼叫時,@db.transactionaldb.run_in_transaction() 會有不同的預設行為。@db.transactional 會允許這項操作,內部交易會與外部交易成為相同的交易。呼叫 db.run_in_transaction() 會嘗試在現有交易上「以巢狀方式嵌入」另一個交易;不過目前尚未支援這個行為且會引發 db.BadRequestError。您可以指定其他行為,詳情請參閱交易選項的函式參考資料。

使用跨群組 (XG) 交易

跨多個實體群組操作的跨群組交易行為類似於單一群組交易,但如果程式碼嘗試從一個以上的實體群組更新實體,則不會失敗。如要叫用跨群組交易,請使用交易選項。

使用 @db.transactional

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

使用 db.run_in_transaction_options

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

在交易中可執行的操作

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

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

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

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

交易函式內部允許其他 Python 程式碼。您可以使用 db.is_in_transaction() 判定目前範圍是否嵌入交易函式中。除了資料儲存庫作業之外,交易函式不應有其他副作用。如果 Cloud Datastore 作業因其他使用者同時更新實體群組中的實體而失敗,交易函式可能會多次呼叫。在這種情況下,Datastore API 會重試交易的次數固定。如果全部失敗,db.run_in_transaction() 會引發 TransactionFailedError。您可以使用 db.run_in_transaction_custom_retries(),而不是 db.run_in_transaction(),調整交易的嘗試次數。

同樣地,除非呼叫交易函式的程式碼知道撤銷這些影響,否則交易函式不得包含會依賴交易成功的副作用。舉例來說,如果交易儲存新的 Datastore 實體,並將建立的實體 ID 儲存起來供日後使用,但交易失敗,則儲存的 ID 不會參照預期的實體,因為實體建立作業已回溯。在這種情況下,呼叫程式碼必須小心不要使用保留的 ID。

隔離與一致性

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

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

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

交易用途

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

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

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

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

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

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

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

取得或建立操作非常實用,因此已有內建的方法:Model.get_or_insert() 會取得一個金鑰名稱、選用父項以及引數,並在該名稱和路徑的實體不存在時將這些內容傳遞給模型建構函式。取得嘗試和建立都發生在一個交易中,因此 (假設交易成功) 方法將傳回代表真正實體的模型實例。

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

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

交易工作排入佇列

您可以將工作排入佇列做為 Datastore 交易的一部分,以便只在成功修訂交易時才將工作排入佇列。如果未提出交易,則工作不會排入佇列。假如已確實提出交易,則工作就會排入佇列。排入佇列後,工作不會立即執行,因此工作與交易並非不可分割。不過,一旦排入佇列,工作就會重試,直到成功為止。這適用於在 run_in_transaction() 函式中排入佇列的任何工作。

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

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

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)