Transaksi

Datastore mendukung transaksi. Transaksi adalah operasi atau serangkaian operasi yang bersifat atomik—baik semua operasi dalam transaksi terjadi maupun tidak ada yang terjadi. Aplikasi dapat menjalankan beberapa operasi dan penghitungan dalam satu transaksi.

Menggunakan transaksi

Transaksi adalah kumpulan operasi Datastore pada satu atau beberapa entity. Setiap transaksi dijamin bersifat atomik, yang berarti bahwa transaksi itu tidak pernah diterapkan sebagian. Semua operasi dalam transaksi akan diterapkan, atau tidak ada yang diterapkan. Transaksi memiliki durasi maksimum 60 detik dengan masa berlaku tidak ada aktivitas selama 10 detik setelah 30 detik.

Operasi mungkin gagal jika:

  • Terlalu banyak perubahan serentak yang dicoba di entity group yang sama.
  • Transaksi melebihi batas resource.
  • Datastore mengalami error internal.

Dalam semua kasus ini, Datastore API akan menampilkan error.

Transaksi adalah fitur opsional Datastore; Anda tidak perlu menggunakan transaksi untuk melakukan operasi Datastore.

Fungsi datastore.RunInTransaction menjalankan fungsi yang disediakan dalam transaksi.


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)
}

Jika fungsi menampilkan nil, RunInTransaction akan mencoba meng-commit transaksi, dan menampilkan nil jika berhasil. Jika fungsi menampilkan nilai error non-nil, perubahan Datastore tidak akan diterapkan dan RunInTransaction akan menampilkan error yang sama.

Jika tidak dapat meng-commit transaksi karena konflik, RunInTransaction akan mencoba lagi, dan akan menyerah setelah tiga kali percobaan. Ini berarti bahwa fungsi transaksi harus bersifat idempoten, yang berarti keduanya memiliki hasil yang sama ketika dijalankan beberapa kali. Perhatikan bahwa datastore.Get tidak idempoten saat membatalkan marshaling kolom slice.

Yang dapat dilakukan dalam transaksi

Datastore memberlakukan pembatasan pada apa yang dapat dilakukan di dalam satu transaksi.

Semua operasi Datastore dalam transaksi harus beroperasi di entity dalam entity group jika transaksi merupakan transaksi satu grup, atau di entitas dalam maksimum dua puluh lima entity group jika transaksi merupakan transaksi lintas grup. Ini termasuk membuat kueri untuk entity berdasarkan ancestor, mengambil entity berdasarkan kunci, mengupdate entity, dan menghapus entity. Perhatikan bahwa setiap root entity adalah milik entity group terpisah, sehingga satu transaksi tidak dapat membuat atau beroperasi di lebih dari satu root entity kecuali jika merupakan transaksi lintas grup.

Jika dua atau beberapa transaksi secara bersamaan mencoba mengubah entity dalam satu atau beberapa entity group umum, hanya transaksi pertama yang meng-commit perubahannya yang dapat berhasil; sementara yang lain akan gagal saat commit. Karena desain ini, penggunaan entity group akan membatasi jumlah operasi tulis serentak yang dapat Anda lakukan pada entity mana pun dalam grup. Saat transaksi dimulai, Datastore menggunakan kontrol konkurensi optimis dengan memeriksa waktu update terakhir untuk entity group yang digunakan dalam transaksi. Setelah melakukan transaksi untuk entity group, Datastore kembali memeriksa waktu pembaruan terakhir untuk entity group yang digunakan dalam transaksi. Jika terjadi perubahan sejak pemeriksaan awal, error akan ditampilkan.

Isolasi dan konsistensi

Di luar transaksi, tingkat isolasi Datastore itu paling dekat dengan batas baca yang di-commit. Di dalam transaksi, isolasi yang dapat di-serialisasi akan diterapkan. Artinya, transaksi lain tidak dapat mengubah secara serentak data yang dibaca atau diubah oleh transaksi ini.

Dalam transaksi, semua operasi baca mencerminkan status terbaru dan konsisten Datastore pada saat transaksi dimulai. Kueri dan aktivitas yang ada di dalam transaksi dijamin akan melihat satu snapshot Datastore yang konsisten sejak awal transaksi. Entity dan baris indeks dalam entity group transaksi diperbarui sepenuhnya sehingga kueri menampilkan kumpulan entity hasil yang lengkap dan benar, tanpa positif palsu (PP) atau negatif palsu (NP) yang dapat terjadi dalam kueri di luar transaksi.

Tampilan snapshot yang konsisten ini juga meluas ke operasi baca setelah operasi tulis di dalam transaksi. Tidak seperti kebanyakan database, kueri dan get di dalam transaksi Datastore tidak melihat hasil penulisan sebelumnya di dalam transaksi tersebut. Khususnya, jika suatu entity diubah atau dihapus dalam transaksi, kueri atau get akan menampilkan versi asli entity tersebut sejak awal transaksi, atau tidak sama sekali jika entity tersebut belum ada pada saat itu.

Penggunaan transaksi

Contoh ini menunjukkan salah satu penggunaan transaksi: memperbarui entity dengan nilai properti baru yang relatif terhadap nilainya saat ini.

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)
}

Tindakan ini memerlukan transaksi karena nilai mungkin diperbarui oleh pengguna lain setelah kode ini mengambil objek, tetapi sebelum menyimpan objek yang diubah. Tanpa transaksi, permintaan pengguna akan menggunakan nilai count sebelum diupdate pengguna lain, dan penyimpanan akan menimpa nilai baru. Dengan transaksi, aplikasi diberi tahu tentang update pengguna dari lain. Jika entitas diperbarui selama transaksi, transaksi tersebut akan dicoba lagi hingga semua langkah selesai tanpa gangguan.

Penggunaan umum lainnya untuk transaksi adalah mengambil entity dengan kunci bernama, atau membuatnya jika belum ada:

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)
}

Seperti sebelumnya, transaksi diperlukan untuk menangani kasus saat pengguna lain mencoba membuat atau memperbarui entity dengan ID string yang sama. Tanpa transaksi, jika entity tidak ada dan dua pengguna mencoba untuk membuatnya, yang kedua akan menimpa entity pertama tanpa menyadarinya.

Jika transaksi gagal, Anda dapat meminta aplikasi mencoba kembali transaksi tersebut hingga berhasil, atau membiarkan pengguna menangani error dengan menyebarkannya ke level antarmuka pengguna aplikasi. Anda tidak perlu membuat loop percobaan ulang di setiap transaksi.

Terakhir, Anda dapat menggunakan transaksi untuk membaca snapshot yang konsisten dari Datastore. Hal ini dapat berguna saat beberapa operasi baca diperlukan untuk merender halaman atau mengekspor data yang harus konsisten. Jenis transaksi ini sering disebut sebagai transaksi hanya baca, karena tidak melakukan penulisan. Transaksi grup tunggal hanya baca tidak pernah gagal karena perubahan serentak, sehingga Anda tidak perlu menerapkan percobaan ulang jika gagal. Namun, transaksi lintas grup dapat gagal karena perubahan serentak, sehingga transaksi tersebut harus dilakukan percobaan ulang. Melakukan commit dan roll back transaksi hanya baca bersifat tanpa pengoperasian.

Antrean tugas transaksional

Anda dapat mengantrekan tugas sebagai bagian dari transaksi Datastore, sehingga tugas tersebut hanya diantrekan—dan dijamin diantrekan—jika transaksi berhasil di-commit. Setelah diantrekan, tugas tidak dijamin akan segera dieksekusi, sehingga tugas tersebut tidak bersifat atomik dengan transaksi. Namun, setelah diantrekan, tugas akan dicoba lagi hingga berhasil. Hal ini berlaku untuk setiap tugas yang diantrekan selama fungsi RunInTransaction.

Tugas transaksional berguna karena memungkinkan Anda menggabungkan tindakan non-Datastore ke transaksi yang bergantung pada keberhasilan transaksi (seperti mengirim email untuk mengonfirmasi pembelian). Anda juga dapat mengaitkan tindakan Datastore ke transaksi, misalnya untuk melakukan perubahan pada entity group di luar transaksi jika dan hanya jika transaksi berhasil.

Aplikasi tidak dapat memasukkan lebih dari lima tugas transaksional ke dalam task queue selama satu transaksi. Tugas transaksi tidak boleh memiliki nama yang ditentukan pengguna.

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)