Google Cloud Spanner
沼への誘い
Write
GCPUG Admin
Google Developers Expert
Mercari / Merpay Solution Team
@sinmetal
https://gcpug.jp
編集APIの種類
https://gcpug.jp
編集APIの種類
4
https://gcpug.jp
5
https://gcpug.jp
複数行の更新 (mutation API)
6
https://gcpug.jp
複数行の更新 (DML)
UPDATE文を都度実行するので、何度もSpannerにアクセスする
7
https://gcpug.jp
更新がTx内で反映されるか? (DML)
UPDATE文はtx.Update()呼び出し時点で実行されるので、その後ReadするとUpdateした内容が反映されている。
8
https://gcpug.jp
更新がTx内で反映されるか? (mutation)
mutation APIではInsert/Updateの反映は最後に行われるので、途中でReadしても、更新は反映されない
9
https://gcpug.jp
BatchUpdateを使うと複数のDMLを一度で実行
10
https://gcpug.jp
Abortしにくい設計
https://gcpug.jp
Aborted due to transient fault
12
https://gcpug.jp
Transactionの範囲が広い
13
Resultは1だが
Txの範囲は2322
https://gcpug.jp
存在しないRowを取得する
14
Not Foundを期待しているコードを書くとAbortされやすいので
INSERTでぶつけに行く方が良い
https://gcpug.jp
WriteSessionsの値が微妙
cli, err := spanner.NewClientWithConfig(ctx, database, spanner.ClientConfig{
SessionPoolConfig: spanner.SessionPoolConfig{
WriteSessions: 0.1, // Defaults to 0.2.
},
})
15
WriteSessionsはSession Poolの中でRWTxを発行しておく割合
Writeを恒常的に行う場合はPerformanceの向上が期待できるが、アクセスが少ない場合、Txが行われてからの時間が経ちすぎていて、TxがAbortされることがある
https://gcpug.jp
Abort頻度をより減らすために
16
https://gcpug.jp
冪等性
https://gcpug.jp
Tx func は自動的にRetryされるので、冪等に
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
var balance int64
row, err := txn.ReadRow(ctx, "Accounts", spanner.Key{"alice"}, []string{"balance"})
if err != nil { return err }
if err := row.Column(0, &balance); err != nil { return err }
if balance <= 10 {
return errors.New("insufficient funds in account")
}
balance -= 10
m := spanner.Update("Accounts", []string{"user", "balance"}, []interface{}{"alice", balance})
txn.BufferWrite([]*spanner.Mutation{m})
return nil
})
18
The transaction function will be called again if the error code of this error is Aborted.
The backend may automatically abort any read/write transaction if it detects a deadlock or other problems.
https://gcpug.jp
mutationの数を数える
https://gcpug.jp
Transaction Limit
20
https://gcpug.jp
mutationの数え方
21
https://gcpug.jp
Insert
22
ID | Col1 | Col2 |
A | Hello | |
INSERT INTO Measure (ID, Col1)
VALUES ('A', 'Hello')
ID |
NULL-A |
Data Table
Col2 Index Table
Col2がNULLの場合でも、NullのRowを作成する
Data Table (ID, Col1)
Col2 Index Table (ID)
mutationは 3
https://gcpug.jp
Update
23
ID | Col1 | Col2 |
A | Hello | hoge |
UPDATE Measure
SET Col2 = "hoge"
WHERE ID = "A"
ID |
NULL-A |
hoge-A |
Data Table
Col2 Index Table
Keyになっている部分を更新はできないので
既存の削除と新規追加をする
DELETE NULL-A
INSERT hoge-A
Data Table (ID, Col2)
Col2 Index Table (ID) * 2
mutationは 4
https://gcpug.jp
Delete
24
ID | Col1 | Col2 |
A | Hello | hoge |
DELETE Measure
WHERE ID = "A"
ID |
hoge-A |
Data Table
Col2 Index Table
Data Table (Row)
Col2 Index Table (Row)
mutationは 2
https://gcpug.jp
UPDATE, DELETE は慎重に
25
https://gcpug.jp
26
resp, err := client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
stmt := spanner.Statement{
SQL: `INSERT Singers (SingerId, FirstName, LastName)
VALUES (110, 'Virginia', 'Watson')`,
rowCount, err := txn.Update(ctx, stmt)
if err != nil {
return err
}
fmt.Fprintf(w, "%d record(s) inserted.\n", rowCount)
return nil
}, spanner.TransactionOptions{CommitOptions: spanner.CommitOptions{ReturnCommitStats: true}})
if err != nil {
return fmt.Errorf("commitStats.ReadWriteTransactionWithOptions: %v", err)
}
fmt.Fprintf(w, "%d mutations in transaction\n", resp.CommitStats.MutationCount)
https://gcpug.jp
Data Migration
https://gcpug.jp
28
https://gcpug.jp
Partitioned DML VS Batch App
29
Partitioned DML | Batch App |
DMLさえ書けば実行できる | コードを書き、それを動かすインフラも必要 |
スケーラビリティはSpanner任せ | スケーラビリティを自分でコントロールする |
JOINのような他のTableへのアクセスはできない | 複雑なロジックも組み込める |
クエリは冪等である必要がある | price * 1.3 のようなことも実行できる |
https://gcpug.jp
Resources
30
https://gcpug.jp