目的
排他制御のためだけに Redis 渋々使ってませんか?データベース単独でアドバイザリーロックできるよ!という記事をみて、ちょうどポイントを賞品に交換するというストアードプロシージャが複雑なロックが必要になったので、勧告的ロックを使ってみました。
関数
pg_try_advisory_xact_lock_sharedの引数は1つのBIGINTまたは2つのINTになります。自分はテーブルの主キーをUUIDにしているのでそのまま使うことができません。なんらかの方法で変換する必要があります。色々探したところ以下のようなコードを見つけました。
('x' || translate(gen_random_uuid()::TEXT, '-', ''))::bit(64)::BIGINT
UUIDは128bitでBIGINTは64bitなので、半分捨ててしまうことになりますが、本人が同時にポイント交換させないという目的には十分だと考えました。
コード
賞品を交換する雰囲気のストアードプロシージャは以下のようになります。
CREATE OR REPLACE FUNCTION exchange_item (
p_user_uuid UUID DEFAULT NULL
,p_item_uuid UUID DEFAULT NULL
) AS $FUNCTION$
DECLARE
BEGIN
-- 勧告的ロック
IF NOT pg_try_advisory_xact_lock(
('x' || translate(p_user_uuid::TEXT, '-', ''))::bit(64)::BIGINT
) THEN
RAISE SQLSTATE 'U0001' USING MESSAGE = 'advisory lock';
END IF;
-- 複雑な状況チェック
-- ポイント交換処理
END;
$FUNCTION$ LANGUAGE plpgsql;
追記
このままのコードだとシステム全体で1っか所でしか使えないので、複数で使いたい場合、数字を足せばよいです。
例えば4か所で使うなら使う場所ごとに+1, +2, +3, +4すれば同じuser_uuidでも同時に使えます。
同じ人に同時に処理させたくないという目的ならこれで問題無いと思います。
IF NOT pg_try_advisory_xact_lock(
('x' || translate(p_user_uuid::TEXT, '-', ''))::bit(64)::BIGINT + 1
) THEN
RAISE SQLSTATE 'U0001' USING MESSAGE = 'advisory lock';
END IF;