事务

Datastore 支持事务。事务是一项或一组原子操作,其中的所有操作要么都发生,要么都不发生。应用可以在单个事务中执行多个操作和计算。

使用事务

事务是对一个或多个实体执行的一组 Datastore 操作。每个事务一定是原子性的,因此事务决不会只应用一部分。事务中的所有操作要么都应用,要么都不应用。事务的最长持续时间为 60 秒,在 30 秒后有 10 秒的空闲到期时间。

出现以下情况时,操作可能会执行失败:

  • 尝试对同一实体组进行太多并发修改。
  • 事务超出资源限制。
  • Datastore 遇到内部错误。

在上述所有情况下,Datastore API 都会返回错误。

事务是 Datastore 的可选功能;执行 Datastore 操作并非必须使用事务。

datastore.RunInTransaction 函数在事务中运行提供的函数。

package counter

import (
	"fmt"
	"net/http"

	"golang.org/x/net/context"

	"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 操作都必须针对同一实体组中的实体;如果事务属于跨组事务,则这些操作最多可以针对 25 个实体组中的实体。这包括通过祖先实体查询实体、通过键检索实体、更新实体以及删除实体。 请注意,每个根实体都属于单独的实体组,因此,除非是跨组事务,否则单个事务无法创建或操作多个根实体。

当两个或更多事务同时尝试修改一个或多个公共实体组中的实体时,只有第一个提交其更改的事务可以应用成功;所有其他事务均会提交失败。由于这种设计,使用实体组会限制您可以对组中任意实体执行的并发写入次数。事务开始时,Datastore 会通过检查事务中使用的实体组的上次更新时间来使用乐观并发控制。在为实体组提交事务后,Datastore 会再次检查事务中使用的实体组的上次更新时间。如果该时间自初次检查后发生了变化,将返回错误。如需了解实体组的说明,请参阅 Datastore 概览页面。

隔离和一致性

在事务之外,Datastore 的隔离级别最接近提交的读取操作。在事务之内,系统将采用可序列化隔离。 这意味着另一个事务不能并发修改由此事务读取或修改的数据。 如需详细了解隔离级别,请参阅可序列化隔离 Wiki 和事务隔离一文。

在事务中,所有读取都反映事务开始时 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)