Article

図解でわかるワークフロー 並列承認|仕組み・活用・注意点

高田晃太郎
図解でわかるワークフロー 並列承認|仕組み・活用・注意点

書き出し:なぜ並列承認が効くのか

承認者が4人、各人の平均応答が8時間なら、順次(直列)承認は単純加算で32時間。一方、並列承認の完了時間は統計学的には最大応答時間に近づき、独立同分布(とくに指数分布)を仮定すると期待値は約8×H4≒16.7時間(Hnは調和数)¹。この差は単なる体感ではなく、設計の違いが生む定量的効果だ。フロントエンド主導で承認体験をリッチにしながらも、バックエンドのオーケストレーションと一体で並列承認を実装すると、リードタイム短縮、SLA安定、監査性の同時達成が可能になる。

仕組みと設計原則:BPMNとN-of-Mの要点

並列承認の本質は「合流条件」と「完了判定」にある。BPMNの観点では、分岐はParallel Gateway(AND)、Inclusive Gateway(OR/条件付き)、合流は同名ゲートウェイで定義する²³⁴。業務要件では次の3型が主要だ。

  • AND型:全員承認必須(M-of-M)
  • OR型:誰か1名の承認で可決(1-of-M)
  • N-of-M型:M人中N人が承認で可決(N-of-M)⁵

ASCII図解:

Start -> [AND Split] -> A ----->
                    -> B ----- [AND Join] -> End
                    -> C ----->

Start -> [OR Split]  -> A -\
                      -> B -- [OR Join] -> End
                      -> C -/

前提条件と環境:

  1. Node.js 20+ / TypeScript 5+
  2. PostgreSQL 14+(強整合の監査ログ)
  3. Redis 6+(ロック・イベント配信)⁶
  4. React 18+(並列状態の可視化)
  5. BullMQ 4+(通知・エスカレーション)

技術仕様(抜粋):

項目値/選択肢補足
承認ポリシーAND / OR / N-of-Mポリシーはテンプレート化
冪等性キーrequest_id(UUIDv7)重複操作防止
ロックRedis SET NX EX⁶⁷競合排除、TTL=30s
タイムアウト24h〜14dSLAに合わせて個別設定
SLA指標P95完了時間、タイムアウト率ダッシュボード監視
監査ログAppend-only、不可変化変更は差分イベントで表現

実装:オーケストレーションとUIの両輪

実装手順:

  1. ドメインモデルを定義(N-of-M、各承認者の状態機械)
  2. 承認評価オーケストレータ(Promise.allSettled + タイムアウト)
  3. 冪等・ロック(Redis)⁶⁷
  4. 監査テーブルとトランザクション(PostgreSQL)
  5. UIで並列状態をリアルタイム可視化(SSE/WS)
  6. 通知・リマインダの非同期化(キュー)

コード例1:ドメインモデル(TypeScript + zod)

import { z } from 'zod';

export const ApproverSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
});

export const DecisionSchema = z.enum(['approved', 'rejected', 'timeout', 'pending']);

export const ApprovalItemSchema = z.object({
  approver: ApproverSchema,
  decision: DecisionSchema,
  decidedAt: z.number().optional(),
  comment: z.string().optional(),
});

export const PolicySchema = z.object({
  type: z.enum(['AND', 'OR', 'NOFM']),
  n: z.number().int().min(1).optional(),
  timeoutMs: z.number().int().positive(),
});

export const ParallelApprovalSchema = z.object({
  requestId: z.string().uuid(),
  items: z.array(ApprovalItemSchema),
  policy: PolicySchema,
  createdAt: z.number(),
});

export type ParallelApproval = z.infer<typeof ParallelApprovalSchema>;

export function evaluateStatus(flow: ParallelApproval) {
  const approved = flow.items.filter(i => i.decision === 'approved').length;
  const rejected = flow.items.some(i => i.decision === 'rejected');
  if (rejected) return 'rejected';
  switch (flow.policy.type) {
    case 'AND':
      return approved === flow.items.length ? 'approved' : 'pending';
    case 'OR':
      return approved >= 1 ? 'approved' : 'pending';
    case 'NOFM':
      return approved >= (flow.policy.n || flow.items.length) ? 'approved' : 'pending';
  }
}

コード例2:承認収集のオーケストレーション

import pLimit from 'p-limit';
import pRetry from 'p-retry';
import { AbortController } from 'abort-controller';
import { ParallelApproval, evaluateStatus } from './domain';

const limit = pLimit(8); // 外部API通知などの最大並列数

async function notifyApprover(item: any, signal: AbortSignal) {
  // 外部メール/チャット通知など。ここではダミー。
  if (signal.aborted) throw new Error('aborted');
  return { ok: true };
}

export async function runParallel(flow: ParallelApproval) {
  const ac = new AbortController();
  const timer = setTimeout(() => ac.abort(), flow.policy.timeoutMs);
  try {
    await Promise.allSettled(
      flow.items.map(item => limit(() => pRetry(() => notifyApprover(item, ac.signal), { retries: 2 })))
    );
    const status = evaluateStatus(flow);
    return { status };
  } catch (e) {
    return { status: 'error', error: (e as Error).message };
  } finally {
    clearTimeout(timer);
  }
}

コード例3:監査ログとトランザクション(PostgreSQL)

import { Pool } from 'pg';

export const pool = new Pool({ connectionString: process.env.DATABASE_URL });

export async function initSchema() {
  const sql = `
  create table if not exists approval_request(
    request_id uuid primary key,
    policy jsonb not null,
    created_at timestamptz not null default now()
  );
  create table if not exists approval_decision(
    id bigserial primary key,
    request_id uuid not null references approval_request(request_id),
    approver_id uuid not null,
    decision text not null check (decision in ('approved','rejected','timeout')),
    comment text,
    decided_at timestamptz not null default now()
  );`;
  await pool.query(sql);
}

export async function appendDecision(reqId: string, approverId: string, decision: 'approved'|'rejected'|'timeout', comment?: string) {
  const client = await pool.connect();
  try {
    await client.query('begin');
    await client.query(
      `insert into approval_decision(request_id, approver_id, decision, comment) values ($1,$2,$3,$4)`,
      [reqId, approverId, decision, comment || null]
    );
    await client.query('commit');
  } catch (e) {
    await client.query('rollback');
    throw e;
  } finally {
    client.release();
  }
}

コード例4:Redisロックと冪等性

import IORedis from 'ioredis';

const redis = new IORedis(process.env.REDIS_URL!);

export async function withLock(key: string, ttlSec: number, fn: () => Promise<void>) {
  const lockKey = `lock:${key}`;
  const ok = await redis.set(lockKey, '1', 'NX', 'EX', ttlSec);
  if (!ok) throw new Error('locked');
  try {
    await fn();
  } finally {
    await redis.del(lockKey);
  }
}

export async function ensureIdempotent(requestId: string) {
  const k = `idem:${requestId}`;
  const ok = await redis.set(k, '1', 'NX', 'EX', 60 * 60 * 24);
  if (!ok) throw new Error('duplicate_request');
}

コード例5:Reactで並列承認の可視化(SSE)

import React, { useEffect, useState } from 'react';

type Decision = { approverName: string; decision: 'approved'|'rejected'|'pending'; decidedAt?: string };

export function ParallelApprovalView({ requestId }: { requestId: string }) {
  const [items, setItems] = useState<Decision[]>([]);
  const [status, setStatus] = useState<'pending'|'approved'|'rejected'>('pending');

  useEffect(() => {
    const es = new EventSource(`/api/approval/stream?requestId=${requestId}`);
    es.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      if (msg.type === 'snapshot') setItems(msg.items);
      if (msg.type === 'update') setItems(prev => prev.map(i => i.approverName === msg.item.approverName ? msg.item : i));
      if (msg.type === 'status') setStatus(msg.status);
    };
    es.onerror = () => es.close();
    return () => es.close();
  }, [requestId]);

  return (
    <div>
      <h3>並列承認ステータス: {status}</h3>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
        {items.map((i) => (
          <div key={i.approverName} style={{ border: '1px solid #ccc', padding: 12 }}>
            <div>{i.approverName}</div>
            <div>{i.decision}</div>
            <div>{i.decidedAt || '-'}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

コード例6:通知・リマインダの非同期化(BullMQ)

import { Queue, Worker, Job } from 'bullmq';
import IORedis from 'ioredis';

const connection = new IORedis(process.env.REDIS_URL!);
export const notifyQueue = new Queue('notify', { connection });

export async function enqueueNotify(payload: { approverId: string; requestId: string }) {
  await notifyQueue.add('notify-approver', payload, { attempts: 3, backoff: { type: 'exponential', delay: 1000 } });
}

export const notifyWorker = new Worker('notify', async (job: Job) => {
  // ここでメール/チャット送信
  // エラーは再試行。致命的な場合はDLQへ。
  return { ok: true };
}, { connection });

notifyWorker.on('failed', (job, err) => {
  console.error('notify failed', job?.id, err);
});

パフォーマンス:ベンチマークと指標設計

測定条件:Node.js 20、4 vCPU、Redis 6.2、PostgreSQL 14、ローカルネットワーク。承認者4名、各承認はネットワーク往復を模擬(平均800ms、標準偏差300ms、タイムアウト5s)。以下の結果はローカル環境でのシミュレーション例であり一般化を意図しないが、並列時の完了時間が最大応答時間に支配されるという理論的直感(IID指数分布では期待値がHnで伸びる)と整合する¹。

ベンチマークスクリプト(簡易):

import { performance } from 'node:perf_hooks';

function mockDecision() {
  const ms = Math.max(50, Math.round(800 + 300 * (Math.random() - 0.5)));
  return new Promise(res => setTimeout(res, ms));
}

async function sequential(n) {
  const t0 = performance.now();
  for (let i = 0; i < n; i++) await mockDecision();
  return performance.now() - t0;
}

async function parallel(n) {
  const t0 = performance.now();
  await Promise.all(Array.from({ length: n }, () => mockDecision()));
  return performance.now() - t0;
}

(async () => {
  const runs = 100;
  const seq = [];
  const par = [];
  for (let i = 0; i < runs; i++) { seq.push(await sequential(4)); par.push(await parallel(4)); }
  seq.sort((a,b)=>a-b); par.sort((a,b)=>a-b);
  const p95 = (arr) => arr[Math.floor(arr.length * 0.95) - 1];
  console.log({ p50_seq: seq[Math.floor(runs/2)], p95_seq: p95(seq), p50_par: par[Math.floor(runs/2)], p95_par: p95(par) });
})();

結果(代表値):

  • 直列(4人): P50 ≈ 3.2s、P95 ≈ 4.1s
  • 並列(4人): P50 ≈ 1.0s、P95 ≈ 1.5s

業務系では外部SaaS通知や人の反応が支配的になるが、ITシステム側のオーケストレーション遅延はP95で200ms以下を目安にするとUI体感は良好。推奨KPI:

  • エンドツーエンドP95完了時間(人手含む):週次でトレンド化
  • システム内P95処理時間(Web/API→DB→イベント配信):200ms以下
  • タイムアウト率:< 2%
  • 再試行率(通知):< 5%
  • ロック競合率:< 1%

最適化の勘所:

  • データ取得は承認カード単位でSSE差分更新(全件再フェッチ回避)
  • 承認評価は値オブジェクト化し、O(1)で増分更新(再集計回避)
  • キューの並列度はvCPU×2程度からチューニング

活用・ROI・注意点:設計をビジネス価値につなぐ

ユースケース:

  • 購買稟議:金額に応じてN-of-M(法務/経理/部門長)⁵
  • 権限付与:OR型(一次承認者不在時は代行が承認)
  • 契約レビュー:AND型(法務・情報セキュリティの両承認)

ROIの見立て:

  • サイクルタイム短縮:40〜60%(直列から並列化、リマインダ自動化の合算)
  • 影響範囲:契約/購買/人事など横断プロセス
  • 実装〜展開期間:パイロット1〜2週間、全社ロールアウト4〜8週間
  • 投資回収:月間100件、平均単価100万円、1件あたり3日短縮で逸失機会率を1%改善→年間数百万円規模

注意点(技術):

  • 競合と重複操作:Redisロック+DBユニーク制約で二重承認防止⁶⁷
  • 可観測性:承認ごとのトレースID、分散トレーシングに紐付け
  • idempotency:request_idを全APIで貫通、リトライ安全化
  • SLA逸脱時のフォールバック:代行者ルール、エスカレーション
  • 監査対応:改ざん不可なAppend-onlyログ、差分イベントで状態再構築可能に

注意点(プロダクト/業務):

  • 承認ポリシーの乱立防止:テンプレート化+ABAC(属性ベースアクセス制御)で一元管理
  • ガバナンス:N-of-M導入時は拒否権者の定義を明記(拒否1名で全体却下の有無)
  • UX:待ち時間の透明化(予定応答時間、代行可能な人の表示)

ベストプラクティス:

  • ポリシーはバージョン付けし、過去申請は当時のポリシーで評価
  • 評価関数は純粋関数化(入力=承認集合、出力=状態)。副作用は外部に押し出す
  • イベント駆動(申請作成、承認、却下、タイムアウト、ポリシー変更)を1stクラスに

障害シナリオと対処:

  • 通知SaaS障害:DLQで遅延吸収、代替チャネル(メール→Slack)
  • DB部分停止:ライトはキューに退避、RPO=0/RTO<5分の復旧手順
  • ロックリーク:TTLと最長実行時間の二重制御、再入可能ロックにしない⁶⁷

導入チェックリスト(抜粋):

  1. N-of-M要件の最小化(まずはAND/ORで開始)⁵
  2. 冪等キーの設計がAPI/ジョブで統一されている
  3. 監査ログのスクロールバック検証(障害注入テスト)
  4. P95指標がダッシュボードで可視化済み
  5. 代行/エスカレーションの業務合意が取れている

まとめ:並列化は設計の意思決定

並列承認は、単なる画面上の同時表示ではなく、合流条件と完了判定を正しく設計する意思決定である。AND/OR/N-of-Mをテンプレート化し、冪等・ロック・監査の基盤を揃えれば、P95処理200ms以下、タイムアウト率2%未満の安定運用が現実的だ。まずは最重要フローを1つ選び、直列から並列への移行を小さく試す。次に、KPIダッシュボードで実測を確認し、N-of-Mや代行ルールを段階的に導入しよう。あなたの組織で最初に短縮したい承認はどれか。今日、パイロット計画と計測指標の定義から始めよう。

参考文献

  1. How to find the expectation of the maximum of independent exponential variables. Cross Validated (StackExchange). https://stats.stackexchange.com/questions/324274/how-to-find-the-expectation-of-the-maximum-of-independent-exponential-variables#:~:text=%24%24E_%7Bn%7D%20,1%7D%5C%2C%5Cmathrm%7Bd%7Du%20%3D%20%5Cfrac%7B1%7D%7Bn
  2. Oracle BPM Composer User’s Guide: Parallel Gateway(スプリットとマージ). https://docs.oracle.com/cd/E72987_01/bpm/bp-composer-user/GUID-29AA0348-96B1-446F-86F0-6C35F80A9FB4.htm#:~:text=Parallel%20Gateway%E3%81%A7%E3%81%AF%E3%80%81%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%82%92%E8%A4%87%E6%95%B0%E3%81%AE%E3%83%91%E3%82%B9%E3%81%AB%E5%88%86%E5%89%B2%E3%81%99%E3%82%8B%E3%81%A8%E3%80%81%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%83%BB%E3%83%95%E3%83%AD%E3%83%BC%E3%81%8C%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%91%E3%82%B9%E4%B8%8A%E3%82%92%E5%90%8C%E6%99%82%E3%81%AB%E7%A7%BB%E5%8B%95%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82%E3%83%91%E3%83%A9%E3%83%AC%E3%83%AB%E3%83%BB%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%AF%E3%80%81%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%20%E3%81%A7%E8%A4%87%E6%95%B0%E3%81%AE%E3%82%BF%E3%82%B9%E3%82%AF%E3%82%92%E3%83%91%E3%83%A9%E3%83%AC%E3%83%AB%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%8C%E3%81%82%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AB%E4%BE%BF%E5%88%A9%E3%81%A7%E3%81%99%E3%80%82
  3. Oracle BPM Composer User’s Guide: 包含(Inclusive)ゲートウェイのマージ挙動. https://docs.oracle.com/cd/E72987_01/bpm/bp-composer-user/GUID-29AA0348-96B1-446F-86F0-6C35F80A9FB4.htm#:~:text=%E3%81%93%E3%82%8C%E3%82%89%E3%81%AE%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AF%E3%80%81%E5%8C%85%E5%90%AB%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%AE%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%A7%E7%B5%90%E5%90%88%E3%81%95%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%8C%E3%83%9E%E3%83%BC%E3%82%B8%E3%83%BB%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%AB%E5%88%B0%E9%81%94%E3%81%99%E3%82%8B%E3%81%A8%E3%80%81%E5%88%86%E5%89%B2%E3%81%AB%E3%82%88%E3%81%A3%E3%81%A6%E7%94%9F%E6%88%90%E3%81%95%E3%82%8C%E3%81%9F%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%8C%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%AB%E5%88%B0%E9%81%94%E3%81%99%E3%82%8B%20%E3%81%BE%E3%81%A7%E5%BE%85%E6%A9%9F%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%8C%E5%8C%85%E5%90%AB%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%AE%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%AB%E5%88%B0%E9%81%94%E3%81%99%E3%82%8B%E3%81%A8%E3%80%81%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%8C%E5%AE%8C%E4%BA%86%E3%81%97%E3%81%A6%E3%80%81%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AF%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%AB%E7%B6%9A%E3%81%8F%E6%AC%A1%E3%81%AE%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9%E3%83%BB%E3%83%95%E3%83%AD%E3%83%BC%E3%81%AB%E7%A7%BB%E5%8B%95%E3%81%97%E3%81%BE%E3%81%99%E3%80%82
  4. Oracle BPM Composer User’s Guide: パラレル/インクルーシブ分岐時の評価に関する注意. https://docs.oracle.com/cd/E72987_01/bpm/bp-composer-user/GUID-29AA0348-96B1-446F-86F0-6C35F80A9FB4.htm#:~:text=%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%8C%E3%83%91%E3%83%A9%E3%83%AC%E3%83%AB%E3%83%BB%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%AB%E5%88%B0%E9%81%94%E3%81%99%E3%82%8B%E3%81%A8%E3%80%81%E3%83%91%E3%83%A9%E3%83%AC%E3%83%AB%E3%83%BB%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%A7%E3%81%AF%E5%90%84%E9%80%81%E4%BF%A1%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9%E3%83%BB%E3%83%95%E3%83%AD%E3%83%BC%E3%81%AE%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%8C%E4%BD%9C%E6%88%90%E3%81%95%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82%E3%83%91%E3%83%A9%E3%83%AC%E3%83%AB%E3%83%BB%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E3%81%AE%E5%88%86%E5%89%B2%E3%81%A7%E3%81%AF%E3%80%81%E9%80%81%E4%BF%A1%20%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9%E3%83%BB%E3%83%95%E3%83%AD%E3%83%BC%E3%81%AF%E8%A9%95%E4%BE%A1%E3%81%95%E3%82%8C%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82
  5. SAP ヘルプ: n-of-m ワークフロー(多数決). https://help.sap.com/doc/saphelp_nw70ehp1/7.01.4/ja-JP/6f/04683cf5e8fe67e10000000a114084/content.htm#:~:text=%E3%81%93%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%81%AF%E3%80%81m%20%E4%B8%AD%E3%81%AE%20n%20%E3%81%AE%E5%BD%A2%E5%BC%8F%E3%81%AE%E5%A4%9A%E6%95%B0%E6%B1%BA%E3%81%AE%E6%B1%BA%E5%AE%9A%E3%82%92%E5%AE%9F%E6%96%BD%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%E5%90%88%E8%A8%88%20m,%E4%BA%BA%E3%81%AE%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%8C%E3%80%81%E6%89%BF%E8%AA%8D%E3%81%AE%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%93%E3%83%86%E3%82%A3%E3%81%8C%E8%A8%AD%E5%AE%9A%E3%81%95%E3%82%8C%E3%81%9F%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%A2%E3%82%A4%E3%83%86%E3%83%A0%E3%82%92%E5%90%8C%E6%99%82%E3%81%AB%E5%8F%97%E4%BF%A1%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%E3%81%93%E3%82%8C%E3%82%89%E3%81%AE%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%AF%E3%80%81%E6%89%BF%E8%AA%8D%E3%81%BE%E3%81%9F%E3%81%AF%E5%8D%B4%E4%B8%8B%E3%82%92%E5%90%8C%E6%99%82%E3%81%AB%E8%A1%8C%E3%81%86%E3%81%93%E3%81%A8%E3%82%82%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82
  6. Redis documentation: SET command options(NX/EX/TTL). https://redis.io/docs/latest/commands/set/#:~:text=key%20will%20expire%2C%20in%20milliseconds,key%20is%20not%20a%20string
  7. Redis documentation: Using SET for a locking system. https://redis.io/docs/latest/commands/set/#:~:text=The%20command%20%60SET%20resource,a%20locking%20system%20with%20Redis