Cloud SpannerのトランザクションをPHPで試してみた

[グレンジ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/grenge) 5日目の記事を担当の jinjin1 です。
グレンジでサーバサイドエンジニアをしています。

今回は、「[Cloud Spanner](https://cloud.google.com/spanner/)」のトランザクションをPHPを使って簡単に確認していきたいと思います。

公式ドキュメントの「[トランザクション](https://cloud.google.com/spanner/docs/transactions)」、「[PHP で Cloud Spanner を使ってみる](https://cloud.google.com/spanner/docs/getting-started/php/)」を参考にしています。

## インストール

composerでSpannerのPHPクライアントライブラリを入れます。

```
composer require google/cloud-spanner
```

gRPC拡張も必要です。

```
pecl install grpc
pecl install protobuf
```

## トランザクション

Database::runTransactionを使って、読み書きトランザクションを実行します。
トランザクションがアボートされた場合、成功するまで関数の実行を繰り返すようになっています。

```
$spanner = new SpannerClient();
$database = $spanner->connect($instanceId, $databaseId);

$database->runTransaction(function (Transaction $transaction) {

    // トランザクション処理

    $transaction->commit();
});
```
### 同時に、同一レコードへの更新を行う

以下のコードを同時に実行してみます。

```a-1.php
$database->runTransaction(function (Transaction $transaction) {
    echo "start\n";
    
    sleep(1);
    
    echo "select\n";
    $test = $transaction->execute('SELECT count FROM test WHERE id = 1')->rows()->current();
    
    sleep(2);
    
    echo "update\n";
    $transaction->executeUpdate('UPDATE test SET count = 1 WHERE id = 1');
    
    echo "commit\n";
    $transaction->commit();
});
```

```a-2.php
$database->runTransaction(function (Transaction $transaction) {
    echo "start\n";
    
    sleep(2);
    
    echo "select\n";
    $test = $transaction->execute('SELECT count FROM test WHERE id = 1')->rows()->current();
    
    echo "update\n";
    $transaction->executeUpdate('UPDATE test SET count = 1 WHERE id = 1');
    
    echo "commit\n";
    $transaction->commit();
});
```

sleepを入れて、1つ目のトランザクションが最初にSELECTをした後、
2つ目のトランザクションが先にSELECT、UPDATEを行うようにしました。

```
$ php a-1.php
start
select
update
commit
```

```
$ php a-2.php
start
select
update
commit
start
select
update
commit
```

2つ目のトランザクションがcommit後アボートされ、処理が最初から再実行されています。

### 同時に、同一レコードの挿入を行う

```b-1.php
$database->runTransaction(function (Transaction $transaction) {
    echo "start\n";
    
    sleep(1);
    
    echo "select\n";
    $test = $transaction->execute('SELECT count FROM test WHERE id = 2')->rows()->current();
    
    sleep(2);
    
    if (is_null($test)) {
        echo "insert\n";
        $transaction->executeUpdate('INSERT INTO test (id, count) VALUES (2, 1)');
    }
    
    echo "commit\n";
    $transaction->commit();
});
```

```b-2.php
$database->runTransaction(function (Transaction $transaction) {
    echo "start\n";
    
    sleep(2);
    
    echo "select\n";
    $test = $transaction->execute('SELECT count FROM test WHERE id = 2')->rows()->current();
    
    if (is_null($test)) {
        echo "insert\n";
        $transaction->executeUpdate('INSERT INTO test (id, count) VALUES (2, 1)');
    }
    
    echo "commit\n";
    $transaction->commit();
});
```

sleepを入れて、1つ目のトランザクションが最初にSELECTをした後、
2つ目のトランザクションが先にSELECT、INSERTを行うようにしました。

```
$ php b-1.php
start
select
insert
commit
```

```
$ php b-2.php
start
select
insert
commit
start
select
commit
```

こちらも、2つ目のトランザクションがcommit後アボートされ、処理が最初から再実行されています。

### 同時に、同一レコードの更新（加算）を行う

```c-1.php
$database->runTransaction(function (Transaction $transaction) {
    echo "start\n";
    
    sleep(1);
    
    echo "update\n";
    $transaction->executeUpdate('UPDATE test SET count = count + 1 WHERE id = 1');
    
    sleep(2);
    
    echo "commit\n";
    $transaction->commit();
});
```

```c-2.php
$database->runTransaction(function (Transaction $transaction) {
    echo "start\n";
    
    sleep(2);
    
    echo "update\n";
    $transaction->executeUpdate('UPDATE test SET count = count + 1 WHERE id = 1');
    
    echo "commit\n";
    $transaction->commit();
});
```

こちらはSELECTを行わずに、1つ目のトランザクションが最初にUPDATE（加算の式）を行った後に、
2つ目のトランザクションがUPDATEを行うようにしました。

```
$ php c-1.php
start
update
commit
```

```
$ php c-2.php
start
update
commit
start
update
commit
```

こちらも、2つ目のトランザクションがcommit後アボートされ、処理が最初から再実行されています。

## まとめ

Cloud Spannerでは、MySQLのような「FOR UPDATE」はなく、
Database::runTransactionに渡す関数内で読み取り、書き込みのトランザクション処理を書きます。
トランザクションの同時実行で、一方のトランザクションがアボートされ、トランザクション処理が再実行される挙動を確認することができました。