2021 年我在一家社交 App 公司负责后端 用 Redis 做缓存层 20 多个微服务都接它。第一版我们就 Set Get 用得很爽 大家觉得 Redis 是银弹 啥都可以缓存。然后业务起飞日活 500 万 缓存层就开始陆续出问题 一周内连续两次大面积故障 缓存击穿 缓存雪崩 缓存穿透 全踩了一遍。然后我们陆续踩了一堆坑。第一种最让我傻眼 一次热门主播开播 10 万人涌入查他的资料 主播缓存刚好过期 10 万请求同时打到 MySQL 直接把数据库打挂 整个 App 5 分钟不可用。第二种最难缠 我们设置了所有缓存 30 分钟统一过期 凌晨 3 点一次定时任务批量写缓存 30 分钟后所有缓存集体失效 MySQL QPS 瞬间冲到 5 万直接超时雪崩。第三种最离谱 攻击者拿大量随机用户 ID 来查 这些 ID 都不存在 缓存不缓存空值 每次都打 MySQL 30 万次/秒查询全是 NULL MySQL CPU 100%。第四种最致命 我们用 Redis 做分布式锁 但代码里 SET 完没设过期时间 锁服务器一次重启 所有锁都还在 业务永久阻塞 凌晨被运维电话叫起来。第五种最莫名其妙 我们 Redis 用 4GB 内存 用到 3.5GB 时性能急剧下降 RDB 持久化 fork 子进程要复制全部内存 服务器 OOM Redis 直接挂。我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 Redis 就是个 KV 存储 SET GET 就能用 可这个认知是错的真正能扛业务的 Redis 缓存是一个 缓存策略设计 加 击穿雪崩穿透防范 加 分布式锁正确姿势 加 内存与持久化管理 加 监控与限流 加 高可用部署 的整套工程方法论 任何一环没做都可能让你的缓存层成为压死后端的最后一根稻草本文从头梳理 Redis 缓存设计的工程要点 三大缓存问题怎么解 分布式锁怎么做 内存怎么管 高可用怎么搭 以及一些把 Redis 用扎实要避开的工程坑
问题背景:为什么默认 Redis 用法是地雷
Redis 上手简单 SET GET 几行代码就能用 性能也是真的好 单机 10 万 QPS 起步。但简单背后藏着很多看起来不起眼的坑 等业务起来才发现 没设过期时间的 key 把内存吃满 缓存与 DB 不一致让用户看到错误数据 分布式锁误删让两个进程同时改一个资源。问题的根源在于:
- 缓存击穿:热点 key 过期瞬间:大量请求穿透到 DB 单 key 防范要做。
- 缓存雪崩:大量 key 同时过期:打到 DB 整体雪崩 过期时间要打散。
- 缓存穿透:查询不存在 key:绕过缓存直接打 DB 必须缓存 NULL 或布隆过滤器。
- 分布式锁有正确姿势:SETNX + EX + UUID + Lua 释放 缺一不可 否则锁失效或误删。
- 持久化与内存协同:RDB AOF 各有适用 内存达 50% 必须考虑 fork 影响。
- 高可用不只是主从:Sentinel Cluster 各自适用场景 单主从是脆弱的。
一 缓存击穿:热点 key 的保护
缓存击穿是指某个热点 key 在缓存过期的瞬间 大量并发请求都打到 DB 把 DB 打挂。常见场景是热门商品 网红主播 热搜话题。解决方案有三种 互斥锁 永不过期(逻辑过期)预热 + 提前刷新 各有适用场景。
# 1 互斥锁方案 推荐 简单可靠
import redis
import time
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def get_with_mutex(key, loader, ttl=300):
"""带互斥锁的缓存获取 防击穿"""
value = r.get(key)
if value is not None:
return json.loads(value)
# 缓存不存在 用 SETNX 获取互斥锁
lock_key = f"lock:{key}"
locked = r.set(lock_key, "1", nx=True, ex=10)
if locked:
try:
# 拿到锁 查 DB 加载数据
value = loader()
r.set(key, json.dumps(value), ex=ttl)
return value
finally:
r.delete(lock_key)
else:
# 没拿到锁 等待 50ms 后重试
time.sleep(0.05)
return get_with_mutex(key, loader, ttl)
# 使用
def load_anchor_info(anchor_id):
# 实际从 MySQL 查
return {"name": "主播1", "fans": 100000}
info = get_with_mutex(f"anchor:{12345}", lambda: load_anchor_info(12345))
# 2 逻辑过期方案 适合极热点 永不返回 NULL
def get_with_logical_expire(key, loader, ttl=300):
"""逻辑过期 缓存永不过期 用过期字段判断"""
value = r.get(key)
if value is None:
# 第一次访问 同步加载
data = loader()
r.set(key, json.dumps({
"data": data,
"expire_at": time.time() + ttl
}))
return data
cached = json.loads(value)
if cached["expire_at"] > time.time():
# 没过期 直接返回
return cached["data"]
# 逻辑过期 异步刷新 但本次仍返回旧数据
lock_key = f"lock:{key}"
if r.set(lock_key, "1", nx=True, ex=10):
# 异步刷新 用线程池
import threading
def refresh():
try:
data = loader()
r.set(key, json.dumps({
"data": data,
"expire_at": time.time() + ttl
}))
finally:
r.delete(lock_key)
threading.Thread(target=refresh).start()
return cached["data"] # 返回旧数据 不让用户等
互斥锁方案适合一般热点 user 看不见的请求等待 50ms 影响小;逻辑过期方案适合极热点 比如开播主播 不能让任何用户等 后台异步刷新。两种方案配合使用 可以应对大部分击穿场景。
二 缓存雪崩:过期时间的艺术
缓存雪崩是大量 key 同时过期 或者 Redis 实例宕机 导致所有请求都打到 DB。雪崩比击穿严重 不是单个 key 而是整体崩。防范主要是过期时间打散 + 多级缓存 + 限流降级。
# 1 过期时间打散 加随机
import random
def set_with_jitter(key, value, base_ttl=3600, jitter_ratio=0.2):
"""加 20% 随机抖动 防止同时过期"""
jitter = int(base_ttl * jitter_ratio * random.random())
actual_ttl = base_ttl + jitter
r.set(key, json.dumps(value), ex=actual_ttl)
# 2 永不过期 + 异步刷新(对极热点)
# 见上文逻辑过期
# 3 多级缓存 本地 Caffeine + Redis
# Java 示例代码注释
# Cache<String, Object> localCache = Caffeine.newBuilder()
# .maximumSize(10000)
# .expireAfterWrite(60, TimeUnit.SECONDS)
# .build();
#
# public Object get(String key) {
# // L1: 本地缓存
# Object value = localCache.getIfPresent(key);
# if (value != null) return value;
# // L2: Redis
# value = redis.get(key);
# if (value != null) {
# localCache.put(key, value);
# return value;
# }
# // L3: DB
# value = db.load(key);
# redis.set(key, value, 3600);
# localCache.put(key, value);
# return value;
# }
除了 TTL 打散与多级缓存之外 还有一种更主动的防雪崩手段 限流降级 + 预热。Redis 突然挂了或者大量 key 同时失效 这两种兜底机制能让 DB 不被瞬时洪峰打挂。下面是 Token Bucket 限流与系统启动预热的标准实现。
# 4 限流降级 防 Redis 挂时雪崩到 DB
import time
from collections import defaultdict
class TokenBucket:
def __init__(self, capacity, refill_rate):
self.capacity = capacity
self.tokens = capacity
self.refill_rate = refill_rate
self.last_refill = time.time()
def allow(self):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
db_limiter = TokenBucket(capacity=1000, refill_rate=200) # 1000 容量 每秒补 200
def get_with_fallback(key, loader):
try:
value = r.get(key)
if value is not None:
return json.loads(value)
except redis.ConnectionError:
# Redis 挂了 降级直接打 DB 但限流
pass
if not db_limiter.allow():
# 限流 返回兜底数据
return {"name": "default", "fans": 0}
return loader()
# 5 预热 系统启动时主动加载
def preheat():
"""预热热点 key"""
hot_keys = r.zrevrange("hot_keys", 0, 999) # 取 top 1000
for key in hot_keys:
# 异步加载
threading.Thread(target=lambda k=key: load_to_cache(k)).start()
实战经验:过期时间打散 + 多级缓存 是基础;主动预热 + 限流降级 是兜底。这套组合下来 即使 Redis 短暂不可用 DB 也不会被打挂 用户最多看到稍微旧的数据。
三 缓存穿透:布隆过滤器与空值缓存
缓存穿透是查询根本不存在的数据 比如恶意构造的随机用户 ID 商品 ID。这些请求 Redis 没有缓存 直接打 DB DB 查不到返回空 下次又打。攻击者可以用这个把 DB 打挂。
# 1 缓存 NULL 简单粗暴
def get_with_null_cache(key, loader, ttl=300, null_ttl=60):
value = r.get(key)
if value is not None:
if value == "NULL":
return None
return json.loads(value)
data = loader()
if data is None:
# 缓存 NULL 但短期 避免长期占内存
r.set(key, "NULL", ex=null_ttl)
return None
r.set(key, json.dumps(data), ex=ttl)
return data
# 2 布隆过滤器 防止恶意构造
# pip install pybloom-live
from pybloom_live import ScalableBloomFilter
# 启动时加载所有有效 ID
user_bloom = ScalableBloomFilter(initial_capacity=10000000, error_rate=0.001)
def init_bloom():
cursor = 0
while True:
ids = db.query("SELECT id FROM users LIMIT %s, 10000", cursor)
if not ids:
break
for id in ids:
user_bloom.add(str(id))
cursor += 10000
# 查询前先查布隆
def get_user_with_bloom(user_id):
if str(user_id) not in user_bloom:
return None # 肯定不存在 直接拒绝
return get_with_null_cache(f"user:{user_id}", lambda: db.get_user(user_id))
# 3 Redis 4.0+ 内置 Bloom 模块
# redis-cli MODULE LOAD /path/to/redisbloom.so
# 或者用 Redis Stack
# BF.RESERVE user_bloom 0.001 10000000
# BF.ADD user_bloom 12345
# BF.EXISTS user_bloom 12345
# 4 参数校验前置 第一道防线
def get_user_safe(user_id):
# 参数合法性检查
if not isinstance(user_id, int) or user_id <= 0 or user_id > 10_000_000_000:
return None
return get_user_with_bloom(user_id)
布隆过滤器是穿透最佳防线 缺点是有误判率(说有的可能没有 但说没的一定没)。错误率 0.001 即 1000 个不存在的 ID 里有 1 个会漏到 DB 这个量级 DB 完全扛得住。生产推荐用 RedisBloom 模块 比 Python 内存 Bloom 更省内存而且共享。
四 分布式锁的正确姿势
分布式锁是 Redis 最常用又最容易写错的场景。错误的实现可能让锁失效(永远拿不到)或者误删(A 的锁被 B 释放)。下面是经过血泪教训的正确实现。
# 1 错误示例 1: SET 没设过期时间
# r.set(lock_key, "1") # 服务挂了锁永远不释放
# 2 错误示例 2: SETNX + EXPIRE 两步
# r.setnx(lock_key, "1")
# r.expire(lock_key, 10) # 这两步之间 client 挂了 锁永远不过期
# 3 正确实现 1: 原子操作 + UUID + Lua 释放
import uuid
def acquire_lock(lock_key, timeout=10):
"""获取分布式锁 返回 token 用于释放"""
token = str(uuid.uuid4())
if r.set(lock_key, token, nx=True, ex=timeout):
return token
return None
# 释放锁必须用 Lua 保证原子 检查 token 一致才删
RELEASE_LOCK_LUA = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
def release_lock(lock_key, token):
"""安全释放 防止误删别人的锁"""
return r.eval(RELEASE_LOCK_LUA, 1, lock_key, token)
# 使用
def transfer_money(from_id, to_id, amount):
lock_key = f"lock:transfer:{from_id}"
token = acquire_lock(lock_key, timeout=10)
if not token:
raise Exception("get lock failed")
try:
# 业务逻辑
do_transfer(from_id, to_id, amount)
finally:
release_lock(lock_key, token)
# 4 锁续期 防业务执行超过锁过期时间
import threading
import time
class LockWatchdog:
def __init__(self, lock_key, token, ttl=10):
self.lock_key = lock_key
self.token = token
self.ttl = ttl
self.running = True
self.thread = threading.Thread(target=self._watch)
self.thread.start()
def _watch(self):
while self.running:
time.sleep(self.ttl / 3) # 1/3 ttl 续期
# Lua: 检查 token 一致才续期
renew_lua = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
end
return 0
"""
r.eval(renew_lua, 1, self.lock_key, self.token, self.ttl)
def stop(self):
self.running = False
对于金融级强一致场景 还有一种更严苛的分布式锁方案 RedLock 多 Redis 实例容错。它通过多数派(majority)算法 避免单 Redis 主从切换时锁失效。但业界对 RedLock 有争议 Martin Kleppmann 与 antirez 还公开撕过 不到金融场景没必要上。
# 5 RedLock 多实例容错(慎用 业界有争议)
# 5 个独立 Redis 节点 大多数(3 个)成功才算获取锁
# pip install redlock-py
from redlock import Redlock
dlm = Redlock([
{"host": "node1", "port": 6379, "db": 0},
{"host": "node2", "port": 6379, "db": 0},
{"host": "node3", "port": 6379, "db": 0},
])
# 加锁 返回 None 表示失败
lock = dlm.lock("payment:order:123", 10000) # 10 秒 TTL
if lock:
try:
# 业务逻辑
do_payment()
finally:
dlm.unlock(lock)
# RedLock 的核心思想:
# - N 个独立的 Redis 节点(推荐 5)
# - 在大多数节点(N/2 + 1)上获取锁成功才算获取成功
# - 总耗时必须小于锁 TTL 否则也算失败
# - 释放锁时所有节点都释放(不管获取成功与否)
# - 单节点失败可以容忍 N/2 个节点同时挂仍可用
实战经验:单 Redis 主从架构用 SETNX + UUID + Lua 释放 + Watchdog 续期 这套已经足够;只有金融级强一致场景才上 RedLock。Watchdog 续期是必备的 业务一旦超过 10 秒锁就过期 不续期就出问题。
五 内存管理与持久化
Redis 是内存数据库 内存管理与持久化策略直接决定可用性。内存满了写不进 fork RDB 时复制全部内存可能 OOM AOF 重写时性能抖动 这些都是生产高频踩坑点。
# 1 maxmemory 与淘汰策略 必配
# redis.conf
maxmemory 8gb
maxmemory-policy allkeys-lru # 推荐 LRU 淘汰最久未用
# 其他策略
# noeviction 默认 内存满就报错 别用
# allkeys-lru 所有 key 走 LRU 推荐
# allkeys-lfu 4.0+ 频率优先 适合热点明显
# volatile-lru 只淘汰带 TTL 的 适合混合使用
# volatile-ttl 淘汰 TTL 短的 也行
# allkeys-random 随机 别用
# 2 内存使用监控
redis-cli INFO memory
# used_memory_human:5.2G
# used_memory_peak_human:6.1G
# mem_fragmentation_ratio:1.05 内存碎片率 > 1.5 需要 MEMORY PURGE
# maxmemory_human:8.00G
# 3 大 key 排查 高频问题
redis-cli --bigkeys
# Biggest string found 'user:123:profile' has 5242880 bytes
# Biggest hash found 'session:abc' has 50000 fields
# 大 key 都要拆 或者单独存
# 4 慢查询排查
redis-cli SLOWLOG GET 10
# 1) (integer) 14
# (integer) 1640000000
# (integer) 12000 12ms
# 1) "KEYS" 危险命令
# 2) "user:*"
# 5 持久化 RDB vs AOF
# RDB: 定时快照 启动快 但崩溃丢数据多 适合缓存场景
# AOF: 追加日志 启动慢 数据更安全 适合持久存储
# 推荐组合: appendonly yes + save 关闭 RDB
# 或者 RDB 主用 AOF 兜底
appendonly yes
appendfsync everysec # 推荐 每秒 fsync 性能与安全平衡
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 6 fork 影响 大内存机器特别注意
# RDB fork 时 子进程会复制父进程页表(不是数据)
# 但写时复制 父进程修改时会触发 copy
# 30GB Redis fork 可能瞬间使用 60GB 内存(峰值)
# 操作系统必须开 vm.overcommit_memory=1
# 大内存机器建议关 RDB 用 AOF only
实战经验:缓存用途的 Redis maxmemory 必须设 不然内存满了直接报错 业务全挂;淘汰策略选 allkeys-lru 适合 90% 场景;大 key 是 Redis 头号杀手 一个 100MB 的 hash 操作能阻塞整个 Redis 几秒;持久化按场景选 纯缓存用 RDB 或者关持久化 重要数据用 AOF。
[mermaid]
flowchart TD
A[业务请求] --> B[参数校验]
B --> C{布隆过滤器存在}
C -->|否| Z[直接返回 null]
C -->|是| D[本地 Caffeine 缓存]
D -->|命中| R[返回数据]
D -->|未命中| E[Redis 查询]
E -->|命中| F[回填本地缓存]
F --> R
E -->|未命中| G{获取互斥锁}
G -->|失败| H[等待 50ms 重试]
H --> E
G -->|成功| I[查询 MySQL]
I --> J{数据存在}
J -->|否| K[缓存 NULL 60s]
J -->|是| L[缓存数据 + 随机 TTL]
L --> M[释放锁]
M --> R
六 高可用部署:Sentinel 与 Cluster
单 Redis 实例是脆弱的 一台机器挂了业务全部不可用。生产必须考虑高可用 主流方案是 Sentinel 哨兵 或 Cluster 集群 两者适用场景不同。
# 1 主从复制 最基础 但不能自动 failover
# slave 配置
replicaof master_ip 6379
replica-read-only yes
# 2 Sentinel 哨兵 自动 failover 中小规模适用
# sentinel.conf 至少 3 个哨兵节点
port 26379
sentinel monitor mymaster 192.168.1.10 6379 2 # 2 表示 quorum
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
# 客户端连接 Sentinel
# python redis-py
from redis.sentinel import Sentinel
sentinel = Sentinel([
('sentinel1', 26379),
('sentinel2', 26379),
('sentinel3', 26379)
], socket_timeout=0.5)
master = sentinel.master_for('mymaster', socket_timeout=0.5)
slave = sentinel.slave_for('mymaster', socket_timeout=0.5)
# 3 Redis Cluster 分片 大规模场景
# 16384 个槽位 自动分配到节点
# 至少 3 主 3 从 共 6 节点
# cluster.conf
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-require-full-coverage no # 部分节点挂了仍服务
# 初始化集群
redis-cli --cluster create \
192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \
192.168.1.13:6379 192.168.1.14:6379 192.168.1.15:6379 \
--cluster-replicas 1
# 4 Cluster 客户端 必须用 cluster 模式
# python redis-py-cluster
from rediscluster import RedisCluster
startup_nodes = [{"host": "192.168.1.10", "port": "6379"}]
rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True)
rc.set("key", "value")
# 5 Cluster 使用约束
# - 不支持 multi-key 操作跨槽位
# - mset/mget 必须 key 在同槽位 用 hash tag {tag}:key
rc.mset({"{user:1}:name": "alice", "{user:1}:age": "30"}) # 同 tag 同槽
# rc.mset({"name": "alice", "age": "30"}) # 报错 跨槽位
# 6 监控 必备
# - redis_exporter + Prometheus + Grafana
# - 关键指标: ops_per_sec used_memory hit_rate connected_clients
选型建议:数据量 < 100GB QPS < 10 万 用 Sentinel 简单;数据量 > 100GB 或 QPS > 10 万 用 Cluster 分片。云上推荐直接用云厂商托管版(阿里云 Tair AWS ElastiCache)省心 别自己搭。
关键概念速查
| 概念 | 说明 | 推荐 | 备注 |
|---|---|---|---|
| 缓存击穿 | 热点 key 过期 | 互斥锁 / 逻辑过期 | 单 key 防范 |
| 缓存雪崩 | 大量 key 同过期 | TTL 加随机抖动 | 过期时间打散 |
| 缓存穿透 | 查不存在数据 | 布隆过滤器 + NULL 缓存 | 防恶意攻击 |
| 分布式锁 | 跨进程同步 | SETNX + UUID + Lua | + Watchdog 续期 |
| maxmemory | 最大内存 | 必设 | 不设默认无限 |
| 淘汰策略 | 满了怎么删 | allkeys-lru | 缓存场景首选 |
| 大 key | 单 key 过大 | 必拆 | --bigkeys 排查 |
| 慢查询 | 阻塞操作 | SLOWLOG 排查 | 禁用 KEYS |
| Sentinel | 哨兵高可用 | 中小规模 | 3 哨兵节点起 |
| Cluster | 分片集群 | 大规模 | 3 主 3 从起 |
避坑清单
- 不要不设 maxmemory 不然内存满了 Redis 直接报错业务全挂。
- 不要不设 TTL 永久 key 越来越多内存爆。
- 不要给所有 key 设相同 TTL 必须加随机抖动避免雪崩。
- 不要不缓存 NULL 否则攻击者用不存在 ID 直接打 DB。
- 不要用 KEYS 在生产 阻塞整个 Redis 数秒 用 SCAN。
- 不要存大 key 单 key > 10KB 就要警惕 > 100KB 必拆。
- 不要 SETNX + EXPIRE 两步加锁 用 SET NX EX 原子操作。
- 不要无 token 释放锁 必须 Lua 检查 token 一致才删。
- 不要忽视 fork 影响 大内存 Redis RDB fork 可能 OOM。
- 不要单实例 生产至少主从 + Sentinel 或 Cluster。
总结
把 Redis 缓存这套从我们踩过的所有坑里反过来看 你会发现真正影响业务稳定性的不是 Redis 性能 而是工程化的全栈能力。同样一个 Redis 集群 不防击穿主播开播 5 分钟全站不可用 防了击穿 + 多级缓存 + 限流降级 10 万人涌入毫无压力;同样一个分布式锁 SETNX + EXPIRE 写错了凌晨被叫起来 SET NX EX + UUID + Lua + Watchdog 写对了再大压力都不出事。Redis 不是 SET GET 几行代码就够的玩具 它是一个 三大缓存问题防范 + 分布式锁 + 内存管理 + 持久化 + 高可用 + 监控 的完整系统工程。
另一个常见的认知误区是把 Redis 当成万能锤子 啥都往里塞。但 Redis 是内存数据库 内存比磁盘贵 100 倍 不该缓存的数据(冷数据 大对象 长会话)放 Redis 就是浪费钱。该缓存的是高频读 + 计算昂贵的数据 不该缓存的是冷数据与频繁变化的数据(命中率 < 30% 反而拖累)。判断该不该缓存比怎么缓存更重要。
打个比方 Redis 在系统里像便利店的冷柜。冷柜空间有限 maxmemory 是冷柜容量 淘汰策略是先扔哪些不常买的商品 缓存穿透防范是不让小偷进店刷货架 缓存击穿防范是热销品断货时不让所有顾客同时跑去仓库取货 缓存雪崩防范是不让所有商品同时过期需要补货 分布式锁是收银台只能一个一个结账 持久化是夜间盘点账本 高可用是冷柜坏了备用冷柜马上接管。哪一项缺了 便利店都会出问题 即使你的冷柜本身性能再好。
所以下一次再有人跟你说 Redis 就是 SET GET 你可以反问他 你的击穿防住了吗 雪崩散开了吗 穿透防了吗 分布式锁正确吗 maxmemory 设了吗 大 key 排了吗 高可用搭了吗 这些工作没做完 Redis 只是一个能让你跑通 demo 的玩具 不是一个能扛业务的缓存层。从踩坑到投产 中间隔着一整套工程方法论 这条路没有捷径 但走完之后 你的缓存层会从压力来临就崩变成压力越大越稳 从凌晨被叫醒变成稳如老狗。
—— 别看了 · 2026