排他制御をKeyedMutex::Memcachedでやった件

おはようございます。 若林(@nqounet)です。

memcached を使って排他制御したい事ってありますよね? ないですか?

私は、つい最近そういうことがあったので KeyedMutex::Memcached を使ってみました。

あらすじ

掲示板時代からそうですが、更新処理が複数同時に実行された場合、排他制御(ロック)をしていないとデータが壊れます。

ファイルのロックについていろいろ考えていた時期もありました。

データベースを使っている場合、排他制御はあまり意識しなくても壊れなかった(むしろロックされすぎてエラーになったり)のですが、ある時を境によく壊れるようになりました。

セッション管理に memcached を使っていたので、それを使って排他制御ができないかな〜と CPAN を見ていたところ、使えそうなモジュールがありました。

その中で IPC::Lock::Memcached は少し古いのと、インストールの失敗数がそこそこあるので回避し、KeyedMutex::Memcached を使うことにしました。

KeyedMutex::Memcached を使ってみる

使い方を見て、ほぼそのまま使えました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use KeyedMutex::Memcached;

my $key   = 'query:XXXXXX';
my $cache = Cache::Memcached::Fast->new( ... );
my $mutex = KeyedMutex::Memcached->new( cache => $cache );

until ( my $value = $cache->get($key) ) {
  {
    if ( my $lock = $mutex->lock( $key, 1 ) ) {
      #locked read from DB
      $value = get_from_db($key);
      $cache->set($key, $value);
      last;
    }
  };
}

new するときに Cache::Memcached::Fast のインスタンスを渡せるので、既に利用しているインスタンスを再利用することができるのが良いですね。

lock の第二引数は use_raii のフラグですが、これを使うと $lock がなくなった時にロックをはずしてくれます。 よくわからない場合は、黙って上のコードのように書くと良いと思います。 (use_raiiをしなかった場合は、if文の中で$mutex->release としてロックをはずす必要があるので、上のコードのように書いておくのが間違いないでしょう。この機構は Scope::Guard を利用しているのですが、こういうのは便利だなと思いました)

本来の使い方としては、上記のようにキャッシュをセットする時のロックとして使用するのが良いのでしょうが、重い処理を何度も動作しないように使ったりもできるようです。

システムへの組み込み方

今回は、更新するためのオブジェクトの中に、ロックを取得する機能をつけて、そこで使用しました。

イメージは以下の様な感じです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package Entry;
use Moo;

has id => (is => 'rw', predicate => 1);
has mutex => (is => 'lazy');

sub _build_mutex {
    my $cache = Cache::Memcached::Fast->new( ... );
    return KeyedMutex::Memcached->new( cache => $cache );
}

sub create_mutex {
    my $self = shift;

    return 1 unless $self->has_id;
    my $key = join ':', 'entry', $self->id;
    return $self->mutex->lock($key, 1);
}

ロックをかけたいところで if (my $mutex = $entry->create_mutex) { ... } という感じで使えるかなと。

IDがない場合はロック不要(insertするのでDBが適切に処理してくれる)とみなして何もせずに真を返しています。

RAIIを使うと、明示的にロックをはずす必要がなくなるので、ロックの実体がなくてもロックを取得したものとして動作させることができます。

仕組み

ソースを見ると、 memcachedadd が成功したかどうかでロックが取得できたかどうかを判定しています。

排他制御を確実に行うには、「ロック操作ができるかどうかの判定」と「ロック操作」が同時にできる必要があります。add はそれが可能なのです。

ファイルシステムを使ったロックの場合、mkdirrenameを使うと思いますが、それにはちゃんとした理由があるのでした。

実は車輪の再発明しかけた

実は、CPANで探す前に簡単に書いてみたのです。

とりあえず排他制御として使えそうな物(ただしキーの指定ができない)ができたのですが、そこまで書いた時点で「さすがに誰か書いてるんじゃないか」と思って調べたら、わりとすぐに見つかったと。

まとめ

CPAN は偉大です。 最初から探しておけばよかった。

参考になる資料

comments powered by Disqus
Built with Hugo
テーマ StackJimmy によって設計されています。