読者です 読者をやめる 読者になる 読者になる

BizStationブログ

ビズステーション株式会社の公式ブログです。

TransactdでInnoDBロックを自在に操る

前回の MySQL/MariaDBとTransactdのInnoDBロック制御詳細 その1 - BizStationブログ では、InnoDBのロックの詳細について説明しました。
今回は、TransactdのトランザクションにおいてInnoDBのロックをどう扱うかを説明します。Transactdは、InnoDBロックを自在に操って同時実行性を高めることができます。

ロック制御は、マルチスレッドプログラミングとmutex lockなどの制御とよく似ています。これらを扱ったことがあるようでしたら、Transactdのロック制御はとてもやり易く感じると思います。

書き込みにおけるパフォーマンスの確保には、ロックを最小限にし同時実行性を高めることが不可欠です。ミッションクリティカルなアプリケーションもパフォーマンスの良いものにしましょう。

Transactdのロック制御

TransactdのAPIは行単位のナビゲーションアクセスが基本です。そのため、InnoDBの行ロックの制御とはとても相性が良く、行単位でロックを解放したり、種類を変えたりといったことができます。ミッションクリティカルなアプリケーションも最小限のロックで安全に処理することが可能です。

トランザクションの種類

Transactdのトランザクションには以下の3つの種類があります。

それぞれ順に説明します。

自動トランザクション

自動トランザクションとは、Transactd内部で自動的に開始されるトランザクションです。その他の2つが明示的に開始を指定するのに対して、自動トランザクションは暗黙のうちに開始されます。
自動トランザクションは、ユーザートランザクションまたはスナップショットが開始されていないときにテーブルに対するアクセスを行うと自動で開始され、1つのオペレーションが終わると終了します。すなわち、テーブルアクセスの通信1回ごとに開始され終了します。

読み取りオペレーション

自動トランザクションの読み取りオペレーションは、常に nonlocking readsでありロックを取得しません。オペレーションごとにトランザクションが開始されるため、その都度最新のスナップショットが使用されます。

更新と行削除

更新nstable::update()と行削除nstable::del()は、直前のオペレーションで読み取られたカレントレコードをサーバー側で再度読み直し、排他ロック(X)を取得してから、更新または削除を行います。
直前のオペレーションで読み取られた値と最新の値が異なる場合は、それを検出してnstable::stat()にSTATUS_CHANGE_CONFLICTエラーが返ります。そのため、このシナリオではロストアップデートは起こりません。また、事前にロックを取得してから更新する方法もあります。それらについては、以降で詳しく説明します。

ユーザートランザクション

ユーザートランザクションは読み書き可能なトランザクションです。 nsdatabase::beginTrn(bias=SINGLELOCK_READ_COMMITED+NOWAIT_WRITE)メソッドで開始し、 nsdatabase::endTrn() または nsdatabase::abortTrn()で終了します。
ユーザートランザクションは開始メソッドのbias値によって複数のロックタイプとロック粒度を選択できます。ロック粒度には、シングルレコードロックとマルチレコードロックの2種類があります。

  • シングルレコードロックは、テーブルごとに最後にアクセスしたレコードのみロックを保持します。
  • マルチレコードロックは、トランザクション中にアクセスしたすべてのレコードのロックを保持します。

以下はbias値に対するロック粒度とロックタイプの一覧表です。

beginTranのbias値 ロック粒度 InnoDB分離レベル 行ロックタイプ
SINGLELOCK_NOGAP
(デフォルト)
シングルレコードロック READ_COMMITED row lock(X)
MULTILOCK_NOGAP マルチレコードロック READ_COMMITED row lock(X)
MULTILOCK_GAP マルチレコードロック REPEATABLE_READ next key lock(X)

SINGLELOCK_NOGAPはbiasのデフォルト値です。SINGLELOCK_NOGAPは、最後に読み取ったレコードの排他ロック(X)のみ保持します。更新や削除を行う際にはその行を確定するために読み取りを必要とします。読み取った値に基づいて更新を行うことでロストアップデートのない処理を行うことができます。また、ロックの範囲が非常に狭いため最も同時実行性の高い処理が行えます
MULTILOCK_NOGAPは、アクセスしたすべてのレコードを排他ロック(X)を取得します。ただし、row lockなので読み取り範囲内への挿入はブロックできません。ロックの範囲は広くなりますので、同時実行性は悪くなります
MULTILOCK_GAPは、アクセスしたすべてのレコードを排他ロック(X)+GAPロックします。これによりアクセスした範囲への挿入もブロックします。InnoDB分離レベルは、REPEATABLE_READ を使用していますが、機能的な分離レベルはSERIALIZABLEになります。同時実行性は最も悪くなりますが 完全な読み取り一貫性を確保した処理が行えます。(以降に説明するunlock()を使用しなかった場合です。)

SINGLELOCK_NOGAPとMULTILOCK_NOGAPの場合は、nstable::unlock() で直前の読み取り行のロックを解放することができます。必要な行のみロックを得たい場合に細かな制御を可能にします。なお、 nstable::find()など1回のオペレーションで複数の行を取得する場合は、ロックの解放はできません。細かくロック・アンロックの制御を行いたい場合は、seek系オペレーションを使用してください。

// ユーザートランザクション unlock()の例
db->beginTrn(MULTILOCK_NOGAP);
tb->setFVN("id", 1);
tb->seek();
if (tb->stat() == 0)
   tb->unlock();
...
db->endTrn();

マルチレコードロックの場合は、読み取りオペレーションの lockBias値にROW_LOCK_Sを指定することで、排他ロック(X)に替えて共有ロック(S)を使用することができます。更新を行わない行の読み取りにこれを使うことで、不要な排他ロック(X)を防止し、必要な行のみ排他ロック(X)にすることができます。lockBias値が指定できる読み取りオペレーションは、nastable::seek系とnastable::step系のオペレーションです。

// ユーザートランザクション 共有ロック(S)を指定する例
db->beginTrn(MULTILOCK_GAP);
tb->setFVN("id", 1);
tb->seek(ROW_LOCK_S); //<-- 共有ロック(S)にする
if (tb->stat() == 0)
...
db->endTrn();

スナップショット

スナップショットは読み取り専用トランザクションです。nsdatabase::beginSnapshot(bias=CONSISTENT_READ)メソッドで開始し、nsdatabase::endSnapshot()で終了します。 スナップショットは開始メソッドのbias値によって複数のロックタイプを選択できます。
以下はbias値に対するロック粒度とロックタイプの一覧表です。

beginSnapshotのbias値 ロック粒度 InnoDB分離レベル 行ロックタイプ
CONSISTENT_READ
(デフォルト)
対象外 REPEATABLE_READ ロックなし (consistent nonlocking reads)
MULTILOCK_NOGAP_SHARE マルチレコードロック READ_COMMITED row lock(S)
MULTILOCK_GAP_SHARE マルチレコードロック REPEATABLE_READ next key lock(S)

CONSISTENT_READはbiasのデフォルト値です。CONSISTENT_READは、REPEATABLE_READによる一貫性のある読み取りが行えます。集計処理などはもちろん、複数の読み取りオペレーションを実行する際にこれを使うと、自動トランザクションに比べて内部処理のオーバーヘッドが減ってより高速に処理できます。
MULTILOCK_NOGAP_SHAREの場合は、nstable::unlock() にて直前の読み取り行のロックを解放することができます。 必要な行のみロックを得たい場合に細かな制御を可能にします。なお、 nstable::find()など1回のオペレーションで複数の行を取得する場合は、ロックの解放はできません。細かくロック・アンロックの制御を行いたい場合は、seek系オペレーションを使用してください。

// スナップショット unlock()の例
db->beginSnapshot(MULTILOCK_NOGAP_SHARE);
tb->setFVN("id", 1);
tb->seek();
if (tb->stat() == 0)
   tb->unlock();
...
db->endSnapshot();

自動トランザクション時の行ロック

自動トランザクションの読み取りはデフォルトで nonlocking readsですが、lockBias値にROW_LOCK_Xを指定することで、排他ロック(X)を取得することができます。単純に1つのレコードを更新したい場合に使用します。

// 行ロックの例
tb->setFV("id", 1);
tb->seek(ROW_LOCK_X);// <-- 排他ロック(X)を取得
if (tb->stat() == 0)
{
    tb->setFV("name", "ABC");
    tb->update();
    if (tb->stat() == 0)
        ;// success!
}
...

自動トランザクションは、通常1つのオペレーションで終了してしまいますが、このロックオペレーションを行った場合に限り、次に続くオペレーションを行ってから終了します。これによりロストアップデートの無い更新・削除が行えます。
また、このロックは、テーブルごとに最後にアクセスしたレコードのみ有効で、次になんらかのオペレーションを行うとそのロックは解放されます。

ロックWait

ロックが競合した場合、InnoDBは自動的に一定時間内リトライを繰り返します。その間にロックが解放されればそのまま処理が進みます。解放されなかった場合は、Transactdが nstable::stat()にSTATUS_LOCK_ERRORを返します。リトライ時間は、サーバー側の my.cnf の、mysqldセクション transactd_lock_wait_timeout に秒で指定します。エントリが無い場合はデフォルト値 1(秒) が使用されます。my.cnfでの設定の詳細はTransactd 運用マニュアルを参照してください。
また、 nsdatabase::lockWaitCount()nsdatabase::lockWaitTime() によってクライアント側でリトライすることもできます。これらは、Transactdへのアクセスにおいてはデフォルトで共にゼロが設定され無効になっています。多くの場合は、InnoDBによるリトライの方が、通信のオーバーヘッドがなく効率的です。

更新処理のレコードアクセスアルゴリズム統一の重要性

同時実行性の向上は、不正な更新が起きるかも知れないであろうシナリオの推測と、更新処理のレコードアクセスアルゴリズムにかかっています。特にアルゴリズムは、処理ごとに統一されることがとても重要です。それができれば無限にある同時実行シナリオを限定的なものにできます。
処理ごとに更新アルゴリズムが1つであれば、ネクストキーロックをせずとも挿入ができなくなることは多々あります。また、マルチレコードロックでなくともシングルレコードロックで済むこともしかりです。
Transactdを使用した場合、レコードアクセスのインデックス・オペレーション・順序などはすべてプログラムコードで記述され、それが変わることはありません。しかしSQLの場合はそれらの多くをオプティマイザが決定しています。このためレコード数の増加やそのほかの条件によって変化する可能性があります。これもまた、ロックを多くしなければならない要因になります。

まとめ

Transactdでミッションクリティカルかつ同時実行性の高いアプリケーションを書く上で大切なことをまとめます。

  • アトミックな操作は、トランザクションで(beginTrn() ... endTrn())で行う。
  • 読み取りで一貫性が必要ならスナップショット(beginSnapshot(CONSISTENT_READ) ... endSnapshot())で行う。また、複数の読み取りオペレーションがある場合に使うと全体の実行速度も向上する。
  • 1行だけの更新・削除は、その読み取りオペレーションのbiasにROW_LOCK_Xを指定しロックして行う。
  • ファントムリードが問題になるトランザクションは beginTrn(MULTILOCK_GAP)で開始する。更新しないレコードの読み取り時は、その読み取りオペレーションのbiasにROW_LOCK_Sを指定して、同時実効性を良くする。
  • ファントムリードが問題にならないトランザクションは beginTrn(MULTILOCK_NOGAP)で開始する。
  • トランザクションの同時実効性を追求するときは、beginTrn(MULTILOCK_NOGAP)で不要なロックをunlock()をするか、beginTrn(SINGLELOCK_NOGAP)を検討する。
  • 更新処理ごとにレコードアクセスアルゴリズムを可能な限り統一する。
  • テーブルを占有したいときは、openTable()のmodeパラメータに、TD_OPEN_EXCLUSIVEかTD_OPEN_READONLY_EXCLUSIVEを指定する。ただし、テーブルロックはテーブルを占有してしまうので、マルチユーザー環境では特別な状況以外使用してはいけない。

いかがだったでしょうか?
Transactdはミッションクリティカルなアプリケーションを書けるのはもちろん、SQLに比べてトランザクションの同時実行性能を向上させることが可能です。
新しいプロダクトには、Transactdを使ってみませんか?

次回は、「実践ミッションクリティカル MySQL/Transactd」です。ロックのことはわかったけれども、実際どういうときにどうすればいいのかわからないという方のために、
いえ、自分のためにまとめてみたいと思います。