"""
Ch12 配套代码 4 / 4 —— 延迟双删（Delayed Double Delete）

场景：
  写线程更新 DB 时，恰好有读线程已经查到了 DB 旧值正准备写回缓存。
  - 单删（先更 DB → 删缓存）：读线程把旧值写回，缓存脏
  - 双删（更 DB → 删 → sleep → 再删）：第二次删兜底，把脏值清掉

本脚本通过线程编排「精准复现」单删失效的场景，再演示双删如何修复。

依赖：
  pip install redis
"""

import time
import threading
import redis


r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


_db = {"user:1": "v_old"}
_db_lock = threading.Lock()


def db_query(key: str) -> str:
    with _db_lock:
        return _db[key]


def db_update(key: str, value: str) -> None:
    with _db_lock:
        _db[key] = value


def section(title: str) -> None:
    print("\n" + "=" * 60)
    print(title)
    print("=" * 60)


def reset() -> None:
    _db["user:1"] = "v_old"
    r.delete("user:1")


def slow_reader(delay_before_writeback: float, label: str) -> None:
    """模拟慢读线程：cache miss → 读 DB → sleep → 写回缓存。"""
    if r.get("user:1") is None:
        v = db_query("user:1")
        time.sleep(delay_before_writeback)
        r.set("user:1", v, ex=600)
        print(f"  [{label}] 读线程把 DB 值 {v!r} 写回了缓存")


def writer_single_delete() -> None:
    db_update("user:1", "v_new")
    r.delete("user:1")
    print("  [单删] 写线程：更新 DB 为 v_new + 删缓存")


def writer_double_delete(delay: float = 0.3) -> None:
    db_update("user:1", "v_new")
    r.delete("user:1")
    print("  [双删] 写线程：更新 DB 为 v_new + 第一次删缓存")
    time.sleep(delay)
    r.delete("user:1")
    print(f"  [双删] 写线程：sleep {delay}s 后第二次删缓存（清理可能的脏数据）")


def demo_single_delete_fail() -> None:
    section("Demo 1: 仅「先更 DB → 再删缓存」—— 复现脏数据场景")
    reset()

    reader_thread = threading.Thread(target=slow_reader, args=(0.2, "单删"))
    reader_thread.start()
    time.sleep(0.05)

    writer_single_delete()

    reader_thread.join()

    cached = r.get("user:1")
    in_db = db_query("user:1")
    print("\n  最终状态：")
    print(f"    DB    = {in_db!r}")
    print(f"    Cache = {cached!r}")
    print(f"  → {'❌ 不一致' if cached != in_db else '✅ 一致'}（缓存被读线程写回了 v_old）")


def demo_double_delete_fix() -> None:
    section("Demo 2: 延迟双删 —— 修复同样的并发场景")
    reset()

    reader_thread = threading.Thread(target=slow_reader, args=(0.2, "双删"))
    reader_thread.start()
    time.sleep(0.05)

    writer_thread = threading.Thread(target=writer_double_delete, args=(0.3,))
    writer_thread.start()

    reader_thread.join()
    writer_thread.join()

    cached = r.get("user:1")
    in_db = db_query("user:1")
    print("\n  最终状态：")
    print(f"    DB    = {in_db!r}")
    print(f"    Cache = {cached!r}")
    if cached is None:
        print("  → ✅ 一致（缓存被双删清理为空，下次读会重新从 DB 加载 v_new）")
    elif cached == in_db:
        print("  → ✅ 一致")
    else:
        print("  → ❌ 不一致（说明 sleep 间隔还不够，需调大 delay）")


def demo_async_double_delete() -> None:
    section("Demo 3: 工程实战写法 —— 第二次删走异步")
    reset()

    def update_async_double_delete(key, value, delay=0.3):
        db_update(key, value)
        r.delete(key)
        threading.Timer(delay, lambda: r.delete(key)).start()

    reader_thread = threading.Thread(target=slow_reader, args=(0.15, "异步双删"))
    reader_thread.start()
    time.sleep(0.05)

    update_async_double_delete("user:1", "v_new", delay=0.3)
    print("  [异步双删] 主线程：更 DB + 删缓存后立即返回（第二次删 0.3s 后异步执行）")

    reader_thread.join()
    time.sleep(0.4)

    cached = r.get("user:1")
    in_db = db_query("user:1")
    print("\n  0.4s 后状态：")
    print(f"    DB    = {in_db!r}")
    print(f"    Cache = {cached!r}")
    if cached is None or cached == in_db:
        print("  → ✅ 一致")
    else:
        print("  → ❌ 不一致")


if __name__ == "__main__":
    try:
        demo_single_delete_fail()
        demo_double_delete_fix()
        demo_async_double_delete()

        print("\n" + "=" * 60)
        print("总结：")
        print("  - 延迟双删的「延迟」要大于读线程「DB 查询 + 写回缓存」的总耗时")
        print("  - 工程上第二次删通常走异步线程或 MQ，避免阻塞主流程")
        print("  - 这是「概率优化」，不是强一致保证。要 100% 一致请用 Binlog 订阅")
        print("=" * 60)

        r.delete("user:1")
    except redis.ConnectionError as e:
        print(f"Redis 连接失败: {e}")
