Skip to content

セッションストアの改修案(Redis の不整合・UPSERT・クリーンアップ・抽象化) ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.0.01.5.8.1

本体の標準機能ではありません

このページは、プリザンター本体のセッション管理を改修する場合の設計メモです。現行の動きは セッション管理の実装 にまとめています。

前提にした現行実装 ​

1.5.8.1 のソースで確かめた、改修に関係する点です。詳しい根拠は セッション管理の実装 にリンクしています。

現状影響
SessionUtilities.Remove() は Sessions テーブルだけを消し、Redis のフィールドを消さないRedis を使う設定で、ロック情報・一時ファイルの許可などが TTL まで残る
userArea: true の値は Redis を使う設定でも Sessions テーブルに書くが、読み込みは Redis からRedis を使う設定で、Sessions API の Get / Delete が値を見つけられない
Redis のフィールド名 {キー}_{ページ} を Split('_') で分ける_ を含むキー(View_…、TableExclusive_…、TempFile_…)が正しく読めない
Sessions テーブルへの書き込みは「UPDATE して 0 行なら INSERT」で、トランザクションも指定しない同じキーへの同時書き込みで PRIMARY KEY 違反
DeleteOldSessions() は Web リクエストの中で 1 日 1 回、プロセスごとに走るアクセスの無い時間帯は削除されない。削除のコストが利用者のリクエストに載る
リクエストごとに SessionData と UserSessionData を全件読む使わない値も毎回読む
Redis への接続は ConnectionMultiplexer.Connect() を遅延生成するだけ(CacheForRedisConnection.cs)Redis が落ちると、セッションの読み書きが例外になる。RDB への切り替えは無い
Redis の操作は SessionUtilities・Context・AuthenticationTicketStore・UserUtilities などに直接書かれ、インターフェースが無いRedis 以外の KVS を足すには各所を直す必要がある

Redis との不整合を直す ​

Remove() で Redis のフィールドも消す ​

Remove() の中で、Redis を使う設定ならフィールドも消します。HDEL はキーの TTL を変えないので、残ったフィールドは今までどおり TTL で消えます。

csharp
public static void Remove(Context context, string key, bool page, string sessionGuid = null)
{
    var targetSessionGuid = sessionGuid ?? context.SessionGuid;
    Repository.ExecuteNonQuery(
        context: context,
        statements: Rds.PhysicalDeleteSessions(
            where: Rds.SessionsWhere()
                .SessionGuid(targetSessionGuid)
                .Key(key)
                .Page(context.Page, _using: page)));
    if (Parameters.Session.UseKeyValueStore)
    {
        var iDatabase = CacheForRedisConnection.Connection.GetDatabase();
        var fieldName = page && context.Page != null
            ? $"{key}_{context.Page}"
            : key;
        iDatabase.HashDelete(targetSessionGuid, fieldName);
        iDatabase.HashDelete($"{targetSessionGuid}_readOnce", fieldName);
    }
}
方式内容考えること
HDEL だけ(上の案)フィールドを消す空のハッシュが TTL まで残る
HDEL + EXPIRE消したあと TTL を付け直す削除でセッションが延命される
HDEL + 空なら DEL最後のフィールドならキーも消すHLEN の呼び出しが 1 回増える

削除はセッションの利用とは言えないので、TTL は付け直さない方が自然です。今ある ReturnOriginalUser() の HashDelete のような個別の対応は、この改修で不要になります。

userArea の値を読むときは RDB から読む ​

Get() で includeUserArea が true のとき、または SessionGuid が @ で始まるときは RDB から読むように分けます。書き込み先と読み込み先がそろい、Redis を使う設定でも Sessions API が動くようになります。

フィールド名の区切りを変える ​

方法内容
区切り文字を変えるキーやページに入らない文字で区切る
最後の _ で分けるLastIndexOf('_') で分ける。ページに _ が入らない前提
構造化するフィールド名を JSON({"key":"View","page":"items/1"})にする

区切りを変えると、切り替え前に Redis に入っていた値は読めなくなります。切り替えは TTL が切れる程度の期間を見込むか、切り替え時に Redis を空にします。

PRIMARY KEY 違反を防ぐ ​

対策内容効果
トランザクション + 再試行SetRds() を transactional: true にし、主キー違反なら UPDATE をやり直すREAD COMMITTED ではトランザクションだけでは競合は防げない。再試行が必要
DB の UPSERT 構文UpdateOrInsert を DB ごとの 1 文の UPSERT にする競合そのものが起きなくなる
RedisUseKeyValueStore: trueuserArea の値は RDB に残るので、その分は残る
DBUPSERT 構文
SQL Servermerge ... when matched then update when not matched then insert
PostgreSQLinsert ... on conflict ("SessionGuid", "Key", "Page") do update set ...
MySQLinsert ... on duplicate key update ...
sql
insert into "Sessions"
  ("Creator", "Updator", "SessionGuid", "Key", "Page", "Value", "ReadOnce", "UserArea")
values
  (@U, @U, @SessionGuid, @Key, @Page, @Value, @ReadOnce, @UserArea)
on conflict ("SessionGuid", "Key", "Page")
do update set
  "Updator" = @U,
  "UpdatedTime" = now() at time zone 'UTC',
  "Value" = @Value,
  "ReadOnce" = @ReadOnce,
  "UserArea" = @UserArea;

UpdateOrInsert は Sessions 以外でも使われているので、ISqlCommandText.CreateUpdateOrInsert() 自体を変えるか、Sessions 専用の SQL を足すかを選びます。

古いセッションの削除をバックグラウンドに移す ​

DeleteOldSessions() を Context.SetData() から外し、Quartz.NET のタイマーで決まった時刻に実行します。既存の DeleteUnusedRecordTimer と同じく ClusterExecutionTimerBase([DisallowConcurrentExecution] 付き。ClusterExecutionTimerBase.cs)を継承し、BackgroundService.json に有効・無効と実行時刻の設定を足します。

csharp
public class DeleteOldSessionsTimer : ClusterExecutionTimerBase
{
    public override async Task Execute(IJobExecutionContext context)
    {
        await Task.Run(() =>
        {
            var ctx = CreateContext();
            Rds.ExecuteNonQuery(
                context: ctx,
                statements: Rds.PhysicalDeleteSessions(
                    where: Rds.SessionsWhere()
                        .UpdatedTime(
                            DateTime.Now.AddMinutes(Parameters.Session.RetentionPeriod * -1),
                            _operator: "<")
                        .Add(raw: "( \"SessionGuid\" not like '@%' )")));
        }, context.CancellationToken);
    }
}

利用者のリクエストから削除のコストが無くなり、アクセスの無い時間帯でも削除されます。削除件数が多い環境では、DeleteUnusedRecordTimer と同じように件数を区切って繰り返します。

読み込みを減らす ​

案内容考えること
必要になったときに読むSessionData を、キーを初めて参照したときに読むクラスにする参照箇所が多いので、SessionData.Get() の呼び出し方は変えずに中身だけ差し替える
必要なキーだけまとめて読むKey in (...) / HMGET で指定したキーだけ読む呼び出し元がキーを知っている必要がある

RDB の場合、今の Get() は SELECT と ReadOnce の DELETE を 1 回の呼び出しで送っているので、往復の回数ではなく読み込む行の量を減らすのが目的になります。

Redis の障害に備える ​

Redis の操作を、失敗が続いたら一定時間 RDB に切り替える仕組み(サーキットブレーカー)で包みます。

図を読み込み中…

切り替えている間に RDB に書いた値は、Redis に戻ったあとは読まれません。ログインが切れる、表示中のビューが戻るといった影響が出るので、許容できるかを決めてから入れます。切り替えと復帰は SysLogs に残します。

Redis 以外の KVS を使えるようにする ​

今の実装は StackExchange.Redis に直接依存しているので、ほかの KVS を足すには抽象化が先に必要です。

図を読み込み中…

csharp
public interface ISessionStore
{
    Dictionary<string, string> Get(string sessionGuid, string page, bool includeUserArea);
    void Set(string sessionGuid, string key, string value, string page, bool readOnce, TimeSpan expiry);
    void Remove(string sessionGuid, string key, string page);
    void Clear(string sessionGuid);
    void Rename(string oldSessionGuid, string newSessionGuid);
}
変える箇所内容
Models/Sessions/SessionUtilities.cs分岐を ISessionStore の呼び出しにする
Libraries/Redis/CacheForRedisConnection.csRedisSessionStore の中に移す
Libraries/Requests/Context.csログアウト・SessionAbandon()・RotateSessionGuid() の Redis 操作
Libraries/Security/AuthenticationTicketStore.csRemoveAsync() の Redis 操作
Models/Users/UserUtilities.csReturnOriginalUser() の HashDelete
App_Data/Parameters/Kvs.jsonプロバイダーの種類を足す(例: "Provider": "Redis")

ハッシュ型・キー単位の TTL・キーの付け替えなど、Redis の機能を前提にした処理が多いので、他の KVS ではこれらの代わりをどう作るかが課題になります。

関連ページ ​

変更履歴

第1版セッションの仕組みとデータベースのテーブル構成の解説を追加し、性能とスケールアウトの説明を修正