"""
Ch12 配套代码 3 / 4 —— TTL 随机抖动防雪崩

场景：
  10000 个商品缓存同时被预热。
  - 固定 TTL：3600s 后所有 key 同一秒过期 → 雪崩
  - 抖动 TTL：3600s + rand(0, 600) → 过期时间在 3600~4200s 内均匀分布 → 平滑

本脚本通过统计「每秒过期的 key 数」绘制出对比直方图（ASCII）。

依赖：
  pip install redis
"""

import random
import collections
import redis


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


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


def histogram(buckets: collections.Counter, label: str, max_width: int = 50) -> None:
    print(f"\n  [{label}]  X 轴: 过期时间偏移(秒)  Y 轴: 同秒过期 key 数")
    if not buckets:
        print("    （无数据）")
        return
    max_count = max(buckets.values())
    sorted_keys = sorted(buckets.keys())
    for k in sorted_keys:
        count = buckets[k]
        bar_len = int(count / max_count * max_width)
        bar = "█" * bar_len if bar_len else "▏"
        print(f"    {k:>5}s | {bar} {count}")


def demo_fixed_ttl(n: int = 10000, base_ttl: int = 3600) -> None:
    section(f"Demo 1: 固定 TTL = {base_ttl}s ({n} 个 key)")

    pipe = r.pipeline()
    for i in range(n):
        pipe.set(f"demo:fixed:{i}", "x", ex=base_ttl)
    pipe.execute()

    pipe = r.pipeline()
    for i in range(n):
        pipe.ttl(f"demo:fixed:{i}")
    ttls = pipe.execute()

    buckets: collections.Counter = collections.Counter()
    for t in ttls:
        if t > 0:
            buckets[t // 60 * 60] += 1

    histogram(buckets, "固定 TTL（X 轴单位：分钟刻度）")
    print(f"\n  💥 全部集中在 {base_ttl}s 这一刻过期 → 单秒过期峰值 = {max(buckets.values())}")

    pipe = r.pipeline()
    for i in range(n):
        pipe.delete(f"demo:fixed:{i}")
    pipe.execute()


def demo_random_ttl(n: int = 10000, base_ttl: int = 3600, jitter: int = 600) -> None:
    section(f"Demo 2: 抖动 TTL = {base_ttl} + rand(0, {jitter})  ({n} 个 key)")

    pipe = r.pipeline()
    for i in range(n):
        pipe.set(f"demo:rand:{i}", "x", ex=base_ttl + random.randint(0, jitter))
    pipe.execute()

    pipe = r.pipeline()
    for i in range(n):
        pipe.ttl(f"demo:rand:{i}")
    ttls = pipe.execute()

    buckets: collections.Counter = collections.Counter()
    for t in ttls:
        if t > 0:
            buckets[t // 60 * 60] += 1

    histogram(buckets, "抖动 TTL（X 轴单位：分钟刻度）")
    peak = max(buckets.values())
    avg = sum(buckets.values()) / len(buckets)
    print(f"\n  ✅ 过期时间均匀分散 → 峰值 = {peak}，均值 = {avg:.1f}")
    print(f"  ✅ 相比固定 TTL，DB 压力降为 ~ {peak / n * 100:.2f}%")

    pipe = r.pipeline()
    for i in range(n):
        pipe.delete(f"demo:rand:{i}")
    pipe.execute()


if __name__ == "__main__":
    try:
        demo_fixed_ttl()
        demo_random_ttl()

        print("\n" + "=" * 60)
        print("结论：")
        print("  - 固定 TTL 在某一秒会有峰值过期，导致下一秒大量请求 miss → 打 DB")
        print("  - 抖动 TTL 把过期时间打散到一个区间，DB 压力恒定且可控")
        print("  - 经验值：jitter = base_ttl 的 5%~20%")
        print("=" * 60)
    except redis.ConnectionError as e:
        print(f"Redis 连接失败: {e}")
