1. 项目背景

1.1 业务场景

电商系统接入 Redis 三个月后,团队在做大促前的代码审查时震惊了。DBA 随手查看 Redis key 列表,发现同样表示商品详情的 key 有三种写法:goods_1、product:1、p:detail:1。开发 A 说这是他负责的模块,开发 B 说他改过中间件但不确定是否影响。更糟的是,某些验证码 key 没有设置 TTL,三个月前的验证码数据还占着内存。有个缓存预热脚本被配置成了永不过期,一个多月就把 Redis 从 8GB 撑到了 64GB。

更隐蔽的问题也浮现了:测试环境和预发布环境共享一个 Redis 实例,测试同学执行 FLUSHALL 清数据时顺带把预发布的缓存也清了——线上立即出现缓存全部 miss,数据库 CPU 飙到 90%。还有一次,运维同学想排查商品详情缓存情况,在业务高峰期敲了 KEYS *product*,Redis 主线程阻塞 2.3 秒,导致所有接口延迟同步升高,P99 从 5ms 飙升到 2.5 秒。

1.2 痛点

问题类别 具体表现 后果
命名混乱 同一实体三种 key 写法,无法用前缀批量管理 排查时找不到 key,清理时漏删
TTL 缺失 缓存/验证码/限流 key 无过期时间 内存只涨不跌,最终 OOM
过期风暴 批量写入的 key 同时过期 数据库瞬间承受全部回源流量
环境混用 测试和预发共用 Redis,FLUSHALL 误清 生产缓存全 miss,数据库被打爆
危险命令 业务高峰用 KEYS * 排查 Redis 主线程阻塞,所有接口超时
db 编号隔离 用 SELECT 1 隔离业务,集群不支持多 db 迁移到 Cluster 时全部改造

1.3 本章目标

掌握 Redis key 管理和生命周期控制的五项核心能力:

  1. 建立统一的 key 命名规范(业务域+场景+对象类型+对象ID+字段)。
  2. 为不同类型的 key 设计合理的 TTL 策略(含随机抖动)。
  3. 理解 Redis 的过期删除机制(惰性删除+定期删除)及过期风暴的成因与应对。
  4. 掌握 SCAN 的生产安全巡检方法——发现不合规 key、无 TTL key 和疑似大 key。
  5. 建立多环境、多租户的隔离策略。

2. 项目设计

2.1 第一幕:key 就是一封没有地址的信

小胖:"key 命名有什么难的?user:1、product:1,简单明了。"

小白:"那你告诉我,user:1 存的是用户信息、用户 token 还是用户购物车?如果项目有 20 个模块共用 Redis,每个开发按自己习惯命名——你排查线上问题时怎么知道一个 key 是哪个模块写的?谁负责清理?"

大师在白板上写下格式:"key 命名规范不是为了好看,是为了排查、隔离、扫描和容量统计。我建议所有 key 遵守五段式结构:{业务域}:{场景}:{对象类型}:{对象ID}:{字段}。"

五段式 key 命名示例:

  mall:cache:product:1001              → 商城-缓存-商品-1001
  mall:captcha:login:13800000000       → 商城-验证码-登录-手机号
  mall:cart:user:9527                  → 商城-购物车-用户-9527
  mall:limit:sms:13800000000:daily     → 商城-限流-短信-手机号-日维度
  mall:rank:sales:20260718             → 商城-排行榜-销量-日期
  mall:lock:order:9001                 → 商城-锁-订单-9001
  crm:tag:user:1001:interest           → CRM-标签-用户-1001-兴趣

大师:"不是每个 key 都必须是五段,但必须让人一眼看出三个信息:哪个团队负责、什么业务含义、适合什么生命周期。冒号没有特殊语义,但它是 Redis 社区最通用的分隔符,配合 SCAN ... MATCH mall:* 做前缀巡检。"

小胖:"那如果多租户系统呢?比如两个商家都有用户 1。"

大师:"那就加租户维度:mall:{tenantId}:cache:product:1001。租户维度放在最前面,便于按租户维度隔离和管理。"

技术映射:key 命名的核心是让 key 本身携带元信息——谁创建的、什么用途、能不能删。好的命名规范是 Redis 治理的第一步。

2.2 第二幕:TTL 不是清洁工,是合同条款

小胖:"那我把所有 key 都设个 TTL 不就完了?比如全部 1 天过期,Redis 自动清理,天下太平。"

小白:"验证码 5 分钟就失效了,你设 1 天的 TTL 意味着过期验证码还要占 1 天内存。购物车应该保留 30 天,你一天就清掉——用户明天回来购物车是空的,投诉到客服。更可怕的是,如果你批量预热了 10 万条商品缓存,全设同样的 600 秒 TTL,10 分钟后它们同时过期——数据库瞬间被 10 万个回源请求打满。"

大师:"TTL 不是随便填的数字。它是对数据生命周期的合同——'这份数据最多留存多久,过期后 Redis 有权清除'。我根据数据类别制定 TTL 策略:"

商城系统 TTL 规范表:

  ┌─────────────────────┬────────────┬────────────────────────────┐
  │ Key 类型             │ 基础 TTL    │ 说明                        │
  ├─────────────────────┼────────────┼────────────────────────────┤
  │ 登录验证码           │ 300 秒      │ 5 分钟,和短信网关约定一致   │
  │ 短信发送冷却         │ 60 秒       │ 60 秒内不允许重复发送        │
  │ 短信日发送次数       │ 86400 秒   │ 到第二天 0 点               │
  │ 商品详情缓存         │ 600+random  │ 10 分钟+随机 0-120 秒       │
  │ 购物车               │ 30 天       │ 支持长期未登录恢复           │
  │ 用户会话 token       │ 7200 秒    │ 2 小时,配合登录态续期       │
  │ 分布式锁             │ 10 秒       │ 必须短,必须有 value 校验    │
  │ 销量排行榜           │ 30 天       │ 活动结束后保留一个月          │
  │ 功能开关             │ 不过期      │ 长期存在,由运营手动删除      │
  │ 空值缓存(防穿透)    │ 60 秒       │ 不存在的数据短 TTL           │
  └─────────────────────┴────────────┴────────────────────────────┘

大师:"注意商品缓存的 TTL——600 + random(0, 120)。这个随机抖动就是防止过期风暴的。如果 10 万条商品缓存都精确在第 600 秒同时过期,10 分钟后就是 10 万个数据库查询。加上 0 到 120 秒的随机,它们会在 600 到 720 秒之间分散过期,数据库压力被平滑了。"

小胖追问:"那 Redis 具体是怎么删除过期 key 的?"

大师:"两种机制配合。惰性删除——每次访问 key 时检查是否过期,过期则删。定期删除——Redis 每秒运行 10 次 activeExpireCycle,每次从设置了过期时间 key 的字典中随机抽样 20 个,删除其中过期的。如果过期的比例超过 25%,继续循环抽样——直到比例降到 25% 以下或超时。这样做的好处是 CPU 开销可控,不会因为清理过期而长时间阻塞主线程。"

技术映射:过期删除是惰性+定期两种策略的折中。这意味着 key 过期后不会立刻消失,可能在内存中额外存活一小段时间。TTL 策略要从"数据最多留多久"和"过期后多久会被真正删除"两个维度考虑。

2.3 第三幕:SCAN 比 KEYS 好在哪?

小胖:"要找某个前缀的 key,KEYS mall:cache:* 不就行了?"

小白:"KEYS 的复杂度是 O(N),N 是 Redis 中所有 key 的总数。它一次性遍历整个键空间,在百万 key 的 Redis 里会阻塞主线程几百毫秒甚至几秒。这期间所有其他命令都在排队等待。"

大师:"SCAN 是渐进式迭代——它不一次性返回所有匹配的 key,而是每次返回一小批和一个游标。下次调用时传回游标,继续下一批。这是一个'非阻塞、非完整快照'的迭代方式:"

KEYS vs SCAN 对比:

  KEYS mall:*:
    → 一次性扫描所有 key,过滤,返回完整列表
    → O(N),阻塞主线程
    → 返回结果是精确快照

  SCAN 0 MATCH mall:* COUNT 100:
    → 从游标 0 开始,返回约 100 个 key 和新游标
    → O(1) 每次,不阻塞主线程
    → 期间新增/删除的 key 可能被遗漏或重复
    → 调用方需要去重

大师:"SCAN 的最大代价是——它不保证一致性。迭代期间如果有增删,新 key 可能被扫到也可能被遗漏,删掉的 key 可能已经存在于返回列表中。所以用 SCAN 的脚本要有去重逻辑,也不能依赖'一定会扫到所有 key'。"

技术映射:SCAN 是生产巡检的唯一安全选择。KEYS 只能在学习环境和紧急调试(确认 key 很少时)使用。

2.4 第四幕:多环境隔离的正确方式

小胖:"生产、预发、测试三套环境,我用 SELECT 0、SELECT 1、SELECT 2 区分不就行了?"

大师:"不行。第一,Redis Cluster 不支持多 db——如果以后要迁移到集群,所有 SELECT 都要改。第二,db 编号只是逻辑上的隔离,内存、CPU、持久化、慢查询都是共享的——测试环境的 FLUSHDB 也可能影响预发(如果连错了的话)。第三,安全性和权限管理无法基于 db 做精细化控制。"

多环境隔离方案对比:

  方案 1: 不同 db(不推荐)
    SELECT 0 → 生产, SELECT 1 → 预发
    问题: 集群不支持, 物理资源共享, 容易误操作

  方案 2: key 前缀隔离(中等)
    prod:cache:product:1001
    staging:cache:product:1001
    问题: 仍在同一实例内, 没有资源隔离

  方案 3: 独立 Redis 实例(推荐)
    生产环境 → redis-prod:6379
    预发环境 → redis-staging:6379
    测试环境 → redis-test:6379
    优点: 资源隔离, 独立配置, 互不影响

大师:"小团队可以共用实例但用前缀区分;中大型团队建议至少生产环境独立实例,测试和预发可以共用端口区分。最好的方案是 Docker Compose 为每个环境启动独立服务——这正是我们第 2 章做过的。"


3. 项目实战

3.1 环境准备

docker exec -it redis-lab redis-cli -a redis123

3.2 实战一:建立 key 命名规范并写入

# 按五段式规范写入各类 key

# 商品缓存(带 TTL 随机抖动基础值)
127.0.0.1:6379> SET mall:cache:product:1001 '{"id":1001,"name":"机械键盘","price":299}' EX 660

# 购物车
127.0.0.1:6379> HSET mall:cart:user:9527 sku:1001 2 sku:1002 1
(integer) 2
127.0.0.1:6379> EXPIRE mall:cart:user:9527 2592000

# 验证码
127.0.0.1:6379> SET mall:captcha:login:13800000000 746281 EX 300

# 短信冷却
127.0.0.1:6379> SET mall:cooldown:sms:13800000000 1 EX 60

# 日发送次数
127.0.0.1:6379> INCR mall:limit:sms:13800000000:daily
(integer) 1
127.0.0.1:6379> EXPIRE mall:limit:sms:13800000000:daily 86400

# 排行榜
127.0.0.1:6379> ZADD mall:rank:sales:20260718 1500 1001 1200 1002 900 1003
(integer) 3
127.0.0.1:6379> EXPIRE mall:rank:sales:20260718 2592000

# 分布式锁(演示)
127.0.0.1:6379> SET mall:lock:order:9001 "req-uuid-001" NX EX 10
OK

# 故意写几个不合规的 key(用于后续巡检)
127.0.0.1:6379> SET badkey "no-prefix-and-no-ttl"
127.0.0.1:6379> SET product_1002 '{"id":1002,"name":"鼠标"}' EX 120

3.3 实战二:TTL 查看与管理

# 查看各 key 的 TTL
127.0.0.1:6379> TTL mall:cache:product:1001
(integer) 628
127.0.0.1:6379> TTL mall:cart:user:9527
(integer) 2591964
127.0.0.1:6379> TTL badkey
(integer) -1       # 没有 TTL!
127.0.0.1:6379> TTL not_exist_key
(integer) -2       # key 不存在

# TTL 返回值含义:
# 正数 = 剩余秒数
# -1   = key 存在但没有过期时间(永不过期)
# -2   = key 不存在

# 改 TTL
127.0.0.1:6379> EXPIRE mall:cache:product:1001 3600
(integer) 1
127.0.0.1:6379> TTL mall:cache:product:1001
(integer) 3598

# 取消过期(变为永不过期)
127.0.0.1:6379> PERSIST mall:cache:product:1001
(integer) 1
127.0.0.1:6379> TTL mall:cache:product:1001
(integer) -1

# 毫秒级 TTL
127.0.0.1:6379> PEXPIRE mall:cache:product:1001 5000
(integer) 1
127.0.0.1:6379> PTTL mall:cache:product:1001
(integer) 4872

# 在指定时间点过期(Unix 时间戳)
127.0.0.1:6379> EXPIREAT mall:cache:product:1001 1715000000
(integer) 1

3.4 实战三:过期风暴模拟与抖动策略

# 场景:批量为 5 个商品设置缓存

# 危险做法:相同 TTL(600 秒后同时过期 → 过期风暴)
127.0.0.1:6379> MSET mall:cache:product:2001 '{"name":"A"}' mall:cache:product:2002 '{"name":"B"}' mall:cache:product:2003 '{"name":"C"}'
OK
127.0.0.1:6379> EXPIRE mall:cache:product:2001 600
127.0.0.1:6379> EXPIRE mall:cache:product:2002 600
127.0.0.1:6379> EXPIRE mall:cache:product:2003 600
# → 600 秒后它们同时过期!

# 安全做法:每个 key 的 TTL 加随机抖动
# 基础 600 秒 + random(0, 120) → 分散在 600~720 秒之间
127.0.0.1:6379> DEL mall:cache:product:2001 mall:cache:product:2002 mall:cache:product:2003
(integer) 3
127.0.0.1:6379> SET mall:cache:product:2001 '{"name":"A"}' EX 618
127.0.0.1:6379> SET mall:cache:product:2002 '{"name":"B"}' EX 687
127.0.0.1:6379> SET mall:cache:product:2003 '{"name":"C"}' EX 642

Java 实现的 TTL 抖动:

public class CacheService {

    private static final int BASE_TTL = 600;  // 基础 10 分钟
    private static final int JITTER_MAX = 120; // 最多加 2 分钟

    private final ThreadLocalRandom random = ThreadLocalRandom.current();

    public void cacheProduct(Product product) {
        String key = buildKey("mall:cache:product:", product.getId());
        String json = objectMapper.writeValueAsString(product);
        int ttl = BASE_TTL + random.nextInt(JITTER_MAX);
        redis.set(key, json, Duration.ofSeconds(ttl));
        log.debug("缓存商品 {} TTL={}s", product.getId(), ttl);
    }

    public void cacheProducts(List<Product> products) {
        // 批量写入时也可以逐个计算随机 TTL
        redis.executePipelined((RedisCallback<Object>) connection -> {
            for (Product p : products) {
                String key = buildKey("mall:cache:product:", p.getId());
                int ttl = BASE_TTL + random.nextInt(JITTER_MAX);
                connection.setEx(key.getBytes(), ttl, serialize(p));
            }
            return null;
        });
    }
}

3.5 实战四:SCAN 安全巡检脚本

步骤 1:基础 SCAN 使用

# 单次扫描匹配 mall: 前缀
127.0.0.1:6379> SCAN 0 MATCH mall:* COUNT 10
1) "14"              # 下一次迭代的游标
2) 1) "mall:cache:product:1001"
   2) "mall:cart:user:9527"
   3) "mall:captcha:login:13800000000"
   ...

# 继续迭代(使用上一轮返回的游标)
127.0.0.1:6379> SCAN 14 MATCH mall:* COUNT 10
1) "0"               # 游标为 0 表示迭代结束
2) ...

# 扫描所有 key
127.0.0.1:6379> SCAN 0 COUNT 50

步骤 2:Java 安全巡检工具

public class RedisKeyInspector {

    // 巡检结果
    @Data
    public static class InspectionResult {
        private List<String> noTtlKeys = new ArrayList<>();    // 无 TTL 的临时 key
        private List<String> nonCompliantKeys = new ArrayList<>(); // 命名不合规
        private List<BigKeyInfo> bigKeys = new ArrayList<>();  // 大 key
    }

    @Data
    public static class BigKeyInfo {
        private String key;
        private String type;
        private long sizeBytes;
    }

    // 执行全量巡检
    public InspectionResult inspect(String prefixPattern, int scanCount) {
        InspectionResult result = new InspectionResult();
        String cursor = "0";

        do {
            ScanResult<String> scanResult = redis.scan(cursor,
                ScanOptions.scanOptions().match(prefixPattern).count(scanCount).build());

            for (String key : scanResult.getValues()) {
                inspectSingleKey(key, result);
            }

            cursor = scanResult.getCursor();
        } while (!"0".equals(cursor));

        return result;
    }

    private void inspectSingleKey(String key, InspectionResult result) {
        // 1. 命名合规检查
        if (!key.startsWith("mall:") && !key.startsWith("crm:") && !key.startsWith("ops:")) {
            result.getNonCompliantKeys().add(key);
        }

        // 2. TTL 检查
        Long ttl = redis.ttl(key);
        String keyLower = key.toLowerCase();
        boolean isTempKey = keyLower.contains(":captcha:")
            || keyLower.contains(":cache:")
            || keyLower.contains(":limit:")
            || keyLower.contains(":cooldown:");

        if (ttl != null && ttl == -1 && isTempKey) {
            result.getNoTtlKeys().add(key + " (无TTL的临时key)");
        }

        // 3. 大 key 检查(可选,性能开销大,建议低峰执行)
        Long memoryUsage = redis.memoryUsage(key);
        if (memoryUsage != null && memoryUsage > 1024 * 1024) { // > 1MB
            String type = redis.type(key);
            result.getBigKeys().add(new BigKeyInfo(key, type, memoryUsage));
        }
    }

    // 生成巡检报告
    public String generateReport(InspectionResult result) {
        StringBuilder report = new StringBuilder();
        report.append("===== Redis Key 巡检报告 =====\n");
        report.append("命名不合规 key: ").append(result.getNonCompliantKeys().size()).append(" 个\n");
        result.getNonCompliantKeys().forEach(k -> report.append("  - ").append(k).append("\n"));
        report.append("无 TTL 的临时 key: ").append(result.getNoTtlKeys().size()).append(" 个\n");
        result.getNoTtlKeys().forEach(k -> report.append("  - ").append(k).append("\n"));
        report.append("疑似大 key (>1MB): ").append(result.getBigKeys().size()).append(" 个\n");
        result.getBigKeys().forEach(k -> report.append("  - ").append(k.getKey())
            .append(" type=").append(k.getType())
            .append(" size=").append(k.getSizeBytes()).append("B\n"));
        return report.toString();
    }
}

3.6 实战五:过期删除实测

# 观察过期删除过程

# 先查看初始过期统计
127.0.0.1:6379> INFO stats | grep expired_keys
# expired_keys:42

# 写入一批 10 秒过期的 key
127.0.0.1:6379> SET expire:test:1 "a" EX 10
127.0.0.1:6379> SET expire:test:2 "b" EX 10
127.0.0.1:6379> SET expire:test:3 "c" EX 10

# 立刻查看 TTL
127.0.0.1:6379> TTL expire:test:1
(integer) 8

# 10 秒后——惰性删除触发
127.0.0.1:6379> GET expire:test:1
(nil)       # 访问时发现过期,被惰性删除

# 再等几秒(如果定期删除已触发)
127.0.0.1:6379> EXISTS expire:test:2
(integer) 0  # 可能已被定期删除清掉

# 查看过期统计增长
127.0.0.1:6379> INFO stats | grep expired_keys
# expired_keys:45 (增加了 3)

3.7 实战六:TYPE、OBJECT、MEMORY 诊断三件套

# TYPE: 查看 key 的数据类型
127.0.0.1:6379> TYPE mall:cache:product:1001
string
127.0.0.1:6379> TYPE mall:cart:user:9527
hash

# OBJECT: 查看内部编码和空闲时间
127.0.0.1:6379> OBJECT ENCODING mall:cache:product:1001
"embstr"
127.0.0.1:6379> OBJECT IDLETIME mall:cache:product:1001
(integer) 120   # 120 秒未被访问过

# MEMORY USAGE: 精确计算 key 占用内存
127.0.0.1:6379> MEMORY USAGE mall:cache:product:1001
(integer) 89

3.8 实战七:key 迁移与重命名的安全操作

当需要将旧命名的 key 迁移到新规范时,直接 RENAME 可能带来风险——如果目标 key 已存在会被覆盖。

# 危险!如果 new_key 已存在,会被静默覆盖
127.0.0.1:6379> RENAME product_1002 mall:cache:product:1002
OK

# 安全!RENAMENX:仅当目标 key 不存在时才执行
127.0.0.1:6379> RENAMENX goods_1001 mall:cache:product:1001
(integer) 1

# 批量迁移脚本(Java 实现):
public class KeyMigrationTool {
    public int migrateKeys(String oldPattern, String newPrefix) {
        String cursor = "0";
        int migrated = 0;

        do {
            ScanResult<String> scanResult = redis.scan(cursor,
                ScanOptions.scanOptions().match(oldPattern).count(100).build());

            for (String oldKey : scanResult.getValues()) {
                // 提取对象 ID 构建新 key
                String objectId = extractId(oldKey);
                String newKey = newPrefix + objectId;

                // 安全重命名(避免覆盖)
                Boolean ok = redis.renameNx(oldKey, newKey);
                if (Boolean.TRUE.equals(ok)) {
                    migrated++;
                    // 复制 TTL
                    Long ttl = redis.ttl(newKey);
                    if (ttl != null && ttl > 0) {
                        redis.expire(newKey, Duration.ofSeconds(ttl));
                    }
                } else {
                    log.warn("目标 key 已存在,跳过: {} → {}", oldKey, newKey);
                }
            }

            cursor = scanResult.getCursor();
        } while (!"0".equals(cursor));

        log.info("迁移完成: {} 个 key", migrated);
        return migrated;
    }
}

4. 项目总结

4.1 核心命名规范速查

key 模板 数据类型 TTL 策略 说明
mall:cache:product:{id} String 600+random(0,120) 商品详情缓存
mall:cache:product:{id}:null String 60 商品不存在的空值缓存
mall:cart:user:{uid} Hash 2592000 (30天) 用户购物车
mall:captcha:login:{phone} String 300 (5分钟) 登录验证码
mall:cooldown:sms:{phone} String 60 短信发送冷却
mall:limit:sms:{phone}:daily String 86400 短信日发送次数
mall:rank:sales:{date} ZSet 2592000 销量日榜
mall:lock:order:{oid} String 10 订单处理锁
mall:switch:{feature} String -1 (不过期) 功能开关

4.2 优点与缺点

维度 详细说明
优点1:排查快 看到 key 就知道哪个模块、什么场景,不用全局搜索代码
优点2:清理方便 SCAN ... MATCH mall:cache:* 精准定位缓存类 key
优点3:容量统计 按前缀聚合 MEMORY USAGE,知道每类 key 占用多少内存
优点4:环境隔离清晰 前缀可区分环境,不会误删生产数据
缺点1:key 变长 五段式比简写更长,但对于 Redis 的哈希表查找来说,key 长度不影响查找性能(O(1))
缺点2:需要强制执行 规范靠自觉不靠谱,需要 Code Review 和自动巡检
缺点3:历史兼容 老 key 的命名无法自动迁移,需要逐步淘汰

4.3 常见踩坑经验

案例 表象 根因 解法
缓存同步过期 每 10 分钟数据库 CPU 飙一次 批量预热时所有 TTL 相同,集中过期形成雪崩 TTL 加随机抖动(基础+random)
业务高峰期 KEYS 所有接口超时 2 秒 运维用 KEYS 查 key,阻塞主线程 禁用 KEYS,全员培训 SCAN
FLUSHALL 清错环境 预发缓存全清,线上回源流量暴增 测试和预发共用实例,db 编号不够隔离 独立 Redis 实例或至少 key 前缀区分
key 命名混乱 商品缓存命中率只有 30%(明明写入了) 写入时用 p:detail:1,读取时用 mall:cache:product:1 统一 KeyBuilder 工具类,禁止业务代码手写 key

4.4 思考题

  1. 设计题:一个跨国电商平台,需要支持 10 个国家站的商品缓存。如果所有国家站都用同一个 Redis 实例,key 应该如何设计才能实现按国家站隔离、按国家站统计、按国家站清理?如果各国法律要求数据不能跨国传输,key 设计需要怎么调整?

  2. 排查题:线上 Redis 的 expired_keys 指标持续增长(每秒新增数百个过期 key),CPU 使用率也同步升高。请分析可能的原因,并设计一套定期删除的健康检查方案。

(答案提示将在下一章末尾给出。)

4.5 推广协作建议

  • 开发团队:建立 RedisKeyBuilder 工具类,强制所有 key 通过统一入口生成。纳入 Code Review 检查项——直接手写 key 字符串的代码须标红。
  • 测试团队:编写自动化巡检脚本,每日执行一次——扫描不合规 key、无 TTL key 和疑似大 key,失败发告警。
  • 运维团队:监控 expired_keys 增长率、evicted_keys 累积值、keyspace 中无 TTL key 的占比。高峰期配置告警阈值。
  • 架构师:制定多环境 Redis 隔离标准和迁移到 Cluster 的 key 兼容性审计。

上一章思考题答案提示(第10章):

第1题(配送范围覆盖):不能用 GEO 直接搜"3 公里内的所有餐厅"——因为餐厅的配送范围是 3-5 公里,用户必须在这个范围内才行。解法:反向思维——查询"覆盖用户位置的餐厅"。每个餐厅的配送范围对应一个 GEO key(geo:delivery:{restaurantId} 存配送范围圆周上的几个点),用户定位后,用"用户 3 公里内的餐厅"做粗筛,再对候选逐家判断 GEODIST 用户到餐厅的距离 ≤ 餐厅配送半径。数量不多时直接遍历 + 距离判断即可。

第2题(三角定位防护):① 距离模糊化——返回 "约 500m" 而非 "523m",加大三角定位误差;② 随机偏移——每次返回位置时在真实坐标上加一个小偏移(如 ±200 米);③ 限制搜索频率——同一用户短时间内只能搜索有限次;④ 只显示相对方向——"在东边约 500m"而非具体坐标。

延伸阅读与资源

Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
MySQL Server 9从入门到进阶的实战之旅
Nacos 3.x 注册配置中心实战修炼:从入门到源码扩展
Elasticsearch从入门到进阶的实战之旅
RabbitMQ从入门到进阶的实战之旅:从单机到大促高可用架构
Celery 入门到进阶之路:从异步任务到自研调度平台
LangGraph 生产级实战进阶:从零到生产级Agent工作流开发
Dify 从入门到源码:LLM 应用平台实战修炼
从零到生产级:FastAPI 异步高并发、源码与 SRE 实战
实战SQLAlchemy 2.0: 从 CRUD 到生产级架构
从零打造企业级 AI 助手:LangChain RAG、Agent 与生产实战
后端工程师 AI 转型课:Ollama 私有化大模型从入门到生产
MongoDB 实战进阶与内核修炼
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化/性能调优)
Milvus向量数据库实战修炼:从 0 到 1 精通向量检索与生产落地
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经

posted on 2026-11-05 10:43  一天不进步,就是退步  阅读(9)  评论(0)    收藏  举报