Redis项目实战整理

Redis 实战用法学习笔记

本文整理了 Redis 在实际项目中的核心用法,涵盖数据结构选型、分布式锁、消息队列、缓存策略、限流、排行榜等常见场景,每个场景附带思路分析和伪代码,适合面试准备和系统设计参考。


目录

  1. [String --- 分布式锁](#String — 分布式锁)
  2. [String --- 缓存与缓存策略](#String — 缓存与缓存策略)
  3. [String --- 限流/防重](#String — 限流/防重)
  4. [String --- 业务标记位/信号量](#String — 业务标记位/信号量)
  5. [String --- Token/会话管理](#String — Token/会话管理)
  6. [List --- 消息队列(异步解耦)](#List — 消息队列(异步解耦))
  7. [List --- 轮询调度池(负载均衡)](#List — 轮询调度池(负载均衡))
  8. [ZSet --- 排行榜/排队叫号](#ZSet — 排行榜/排队叫号)
  9. [ZSet --- 延迟队列](#ZSet — 延迟队列)
  10. [Hash --- 对象缓存](#Hash — 对象缓存)
  11. [Set --- 去重/共同好友/抽奖](#Set — 去重/共同好友/抽奖)
  12. [Lua 脚本 --- 原子操作](#Lua 脚本 — 原子操作)
  13. [Bitmap --- 签到/在线状态](#Bitmap — 签到/在线状态)
  14. [HyperLogLog --- UV统计](#HyperLogLog — UV统计)
  15. [Stream --- 消息队列(5.0+)](#Stream — 消息队列(5.0+))
  16. [Redis 持久化与高可用](#Redis 持久化与高可用)
  17. 缓存三大问题
  18. 面试串讲稿

1. String --- 分布式锁

1.1 适用场景

多个服务实例(多台JVM)同时操作同一个资源时,防止并发冲突。典型场景:

  • 支付回调并发处理同一笔订单
  • 库存扣减防超卖
  • 风控审核多人同时点击通过
  • 领款/核销防重复

1.2 实现思路

复制代码
加锁:SET key value NX EX 30秒
  - NX:key不存在才设置(原子操作,保证只有一个线程能设置成功)
  - EX:设置过期时间,防止进程宕机导致死锁
  - value:存线程唯一标识(用于解锁时校验身份)

业务处理

解锁:校验value == 自己的标识,一致才删除(防误删别人的锁)

1.3 伪代码

java 复制代码
/**
 * 分布式锁工具
 */
public class DistributedLock {

    private StringRedisTemplate redisTemplate;

    // 加锁
    public boolean tryLock(String lockKey, String ownerId, long timeoutSeconds) {
        // SET lockKey ownerId NX EX timeout
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, ownerId, timeoutSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }

    // 解锁(Lua脚本保证原子性:判断+删除在一条命令里完成)
    public boolean unlock(String lockKey, String ownerId) {
        String luaScript =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";

        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(script, List.of(lockKey), ownerId);
        return result != null && result == 1L;
    }
}

1.4 使用示例

java 复制代码
// 支付回调场景
String lockKey = "lock:pay:callback:" + orderId;
String ownerId = UUID.randomUUID().toString();

if (distributedLock.tryLock(lockKey, ownerId, 30)) {
    try {
        // 1. 查询订单状态
        // 2. 判断是否已支付(幂等)
        // 3. 更新订单状态为已支付
        // 4. 扣减库存
    } finally {
        distributedLock.unlock(lockKey, ownerId);
    }
} else {
    log.warn("获取锁失败,可能已有其他实例在处理该订单");
}

1.5 面试要点

问题 回答
为什么不用setnx+expire两条命令? 两条命令不原子,setnx成功但expire失败会死锁。SET NX EX是一条命令原子执行
锁过期了但业务没执行完怎么办? 可以用看门狗机制(Redisson默认30秒续期),或者业务层加幂等校验兜底
解锁为什么要用Lua? get+判断+del三条命令不原子,用Lua在Redis端一次执行保证原子性
可重入锁怎么做? value里记录线程ID+重入次数,重入时+1解锁时-1,归零才删key

2. String --- 缓存与缓存策略

2.1 适用场景

高频读取但变化不频繁的数据:用户信息、配置数据、字典数据、权限数据、区域信息等。

2.2 常用模式

基础缓存:

java 复制代码
// 写缓存
redisTemplate.opsForValue().set("user:" + userId, userJson, 30, TimeUnit.MINUTES);

// 读缓存
String json = redisTemplate.opsForValue().get("user:" + userId);
User user = json != null ? parseJson(json, User.class) : null;

缓存空值(防穿透):

java 复制代码
User user = userMapper.selectById(userId);
if (user != null) {
    redisTemplate.opsForValue().set("user:" + userId, user, 30, MINUTES);
} else {
    // 缓存空值,过期时间短一点(如5分钟)
    redisTemplate.opsForValue().set("user:" + userId, NULL_PLACEHOLDER, 5, MINUTES);
}

布隆过滤器(防穿透,大数据量场景):

java 复制代码
// 初始化时把所有userId放入布隆过滤器
// 查询前先判断
if (!bloomFilter.mightContain(userId)) {
    return null; // 一定不存在,直接返回
}
// 可能存在,再去查缓存/DB

2.3 缓存更新策略(面试高频)

策略 思路 适用场景
Cache Aside 读:先缓存,miss查DB回填;写:先更新DB,再删缓存 最常用,通用场景
Read Through 缓存层自动从DB加载(应用只跟缓存交互) 缓存框架支持
Write Behind 写入缓存后异步批量写DB 写多读少,允许短暂不一致

Cache Aside 为什么"先更新DB再删缓存"而不是"先删缓存再更新DB"?

先删缓存的问题:

  1. 线程A删缓存
  2. 线程B读缓存miss,从DB读到旧数据,回填缓存
  3. 线程A更新DB
  4. 结果:缓存是旧数据,DB是新数据,不一致

先更新DB再删缓存的问题:

  1. 线程A读缓存miss,查DB拿到旧数据
  2. 线程B更新DB,删缓存
  3. 线程A回填缓存(旧数据)
  4. 结果:缓存是旧数据,但这种情况概率极低(要在线程A查DB之后、回填之前,线程B刚好完成更新+删缓存)

2.4 面试要点

问题 回答
缓存和DB一致性怎么保证? Cache Aside策略,先更新DB再删缓存,结合延迟双删或Canal监听binlog兜底
大Key怎么处理? 拆分key、压缩value、避免存大JSON,用Hash分片
热Key怎么处理? 本地缓存(Caffeine)+Redis二级缓存、key加随机后缀打散

3. String --- 限流/防重

3.1 固定窗口限流

java 复制代码
// 某个接口10秒内只能调用1次
public boolean isAllowed(String apiName) {
    String key = "rateLimit:" + apiName;
    Boolean result = redisTemplate.opsForValue()
        .setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(result);
}

3.2 滑动窗口限流(更精确)

java 复制代码
// 用ZSet实现滑动窗口:score=请求时间戳
public boolean isAllowedSlidingWindow(String apiName, int maxCount, int windowSeconds) {
    String key = "sliding:" + apiName;
    long now = System.currentTimeMillis();
    long windowStart = now - windowSeconds * 1000L;

    Pipeline pipeline = redisTemplate.pipelined();
    // 1. 移除窗口外的请求
    pipeline.opsForZSet().removeRangeByScore(key, 0, windowStart);
    // 2. 统计窗口内请求数
    pipeline.opsForZSet().zCard(key);
    // 3. 添加当前请求
    pipeline.opsForZSet().add(key, String.valueOf(now), now);
    // 4. 设置key过期
    pipeline.expire(key, windowSeconds, TimeUnit.SECONDS);

    List<Object> results = pipeline.exec();
    long count = (Long) results.get(1);
    return count < maxCount;
}

3.3 防重复提交(业务幂等)

java 复制代码
// 提交表单/下单等场景,同一个用户短时间内不能重复提交
public boolean tryAcquire(String bizKey) {
    // key包含业务含义 + 用户ID + 时间窗口
    String key = "idempotent:" + bizKey;
    return Boolean.TRUE.equals(
        redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.MINUTES)
    );
}

4. String --- 业务标记位/信号量

4.1 适用场景

分布式环境下的"开关"信号:

  • 导出锁(防止并发导出导致内存溢出)
  • 系统维护开关(一键停服)
  • 缓存刷新锁
  • 分布式任务标记(某任务已开始/已完成)

4.2 伪代码

java 复制代码
// 导出锁:同时只允许一个导出任务执行
public void exportExcel(Long userId) {
    String lockKey = "export:lock:device";
    try {
        if (!redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.MINUTES)) {
            throw new RuntimeException("导出任务进行中,请稍后再试");
        }
        // 执行耗时导出...
    } finally {
        redisTemplate.delete(lockKey);
    }
}

// 系统维护开关
public boolean isSystemMaintenance() {
    return redisTemplate.hasKey("sys:maintenance:flag");
}

// 运维一键开启维护
public void enableMaintenance() {
    redisTemplate.opsForValue().set("sys:maintenance:flag", "1");
}

5. String --- Token/会话管理

5.1 适用场景

JWT无状态Token + Redis有状态管理(支持强制踢出、Token黑名单)。

5.2 伪代码

java 复制代码
// 登录时:生成Token并存入Redis
String token = JwtUtil.generateToken(userId, role);
redisTemplate.opsForValue().set(
    "token:" + token,      // key
    userId.toString(),      // value
    2, TimeUnit.HOURS       // 过期时间=Token有效期
);

// 每次请求拦截器校验
String token = request.getHeader("Authorization");
String userId = redisTemplate.opsForValue().get("token:" + token);
if (userId == null) {
    throw new UnauthorizedException("Token已过期或已失效");
}

// 踢人下线:直接删Token
redisTemplate.delete("token:" + token);

// 修改密码/退出:删Token
redisTemplate.delete("token:" + token);

6. List --- 消息队列(异步解耦)

6.1 适用场景

将耗时操作从主流程剥离,异步处理:

  • 支付成功后异步发送通知
  • 下单后异步生成合同
  • 回调数据异步入库
  • 异步计算分润/提成

6.2 架构

复制代码
生产者(主线程)                 Redis List                消费者(后台线程)
     |                           |                          |
  RPUSH ──→  [msg1][msg2][msg3]  ←── BLPOP(超时) ──→  handler.execute(msg)
           (FIFO先进先出)                              单条异常不影响后续

6.3 伪代码

生产者:

java 复制代码
// 入队:RPUSH到列表尾部
public void enqueue(String queueKey, Object message) {
    redisTemplate.opsForList().rightPush(queueKey, serialize(message));
}

消费者(应用启动时注册,停机时销毁):

java 复制代码
public class QueueConsumer implements ApplicationRunner, DisposableBean {

    private final Map<String, Future<?>> consumers = new ConcurrentHashMap<>();

    @Override
    public void run(ApplicationArguments args) {
        // 启动消费者线程
        startConsumer("queue:contract", this::handleContract);
        startConsumer("queue:notify", this::handleNotify);
        startConsumer("queue:bonus", this::handleBonus);
    }

    private void startConsumer(String queueKey, Consumer<Object> handler) {
        Future<?> future = executor.submit(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // BLPOP阻塞获取,超时10秒
                    Object item = redisTemplate.opsForList()
                        .leftPop(queueKey, 10, TimeUnit.SECONDS);
                    if (item != null) {
                        handler.accept(item);  // 业务处理
                    }
                } catch (Exception e) {
                    log.error("消费异常", e);
                    Thread.sleep(1000);  // 异常后短暂休眠防热循环
                }
            }
        });
        consumers.put(queueKey, future);
    }

    @Override
    public void destroy() {
        // 优雅停机:发送中断信号
        consumers.forEach((key, future) -> future.cancel(true));
        consumers.clear();
    }
}

6.4 与MQ(RabbitMQ/Kafka)的对比

维度 Redis List RabbitMQ/Kafka
复杂度 低,Redis自带 高,需要额外部署
可靠性 一般(消费失败无重试,需自己实现) 高(ACK机制、死信队列、重试)
吞吐量 万级/秒 Kafka百万级/秒
适用场景 中小规模、对可靠性要求不高 大规模、金融级可靠性

面试话术: 项目里选Redis List是因为规模可控,核心是合同回调、派单分配、分润计算等异步任务。如果后续规模增长或需要可靠投递,可以平滑迁移到RabbitMQ/Kafka。


7. List --- 轮询调度池(负载均衡)

7.1 适用场景

任务分配需要均匀分摊到多个处理人:

  • 风控审核自动派单
  • 客服工单自动分配
  • 呼叫中心座席轮询

7.2 实现思路

复制代码
List模拟环形队列:
初始: [审核员A, 审核员B, 审核员C]
第1单: LPOP(A) → 分配A → RPUSH(A) → [B, C, A]
第2单: LPOP(B) → 分配B → RPUSH(B) → [C, A, B]
第3单: LPOP(C) → 分配C → RPUSH(C) → [A, B, C]
→ 每个人均匀分配,round-robin

7.3 伪代码

java 复制代码
// 初始化:从DB加载审核员到Redis List
public void initAuditorPool() {
    String poolKey = "pool:auditor";
    redisTemplate.delete(poolKey);
    List<Auditor> auditors = auditorMapper.selectActive();
    for (Auditor a : auditors) {
        redisTemplate.opsForList().rightPush(poolKey, a.getId());
    }
}

// 分配:LPOP取一个,RPUSH放回队尾
public Long assignAuditor() {
    String poolKey = "pool:auditor";
    Long auditorId = (Long) redisTemplate.opsForList().leftPop(poolKey);
    redisTemplate.opsForList().rightPush(poolKey, auditorId);
    return auditorId;
}

7.4 带配额的轮询(进阶)

java 复制代码
// 额度小的单子 → 沿用初审人(不轮转)
// 额度中等   → 轮转终审池
// 额度大     → 兜底给主管
public Long assignFinalAuditor(Long quota) {
    if (quota <= LOW_THRESHOLD) {
        return previousAuditorId;  // 沿用
    } else if (quota <= HIGH_THRESHOLD) {
        Long id = redisTemplate.opsForList().leftPop("pool:auditor:final");
        redisTemplate.opsForList().rightPush("pool:auditor:final", id);
        return id;
    } else {
        return managerId;  // 主管兜底
    }
}

8. ZSet --- 排行榜/排队叫号

8.1 排行榜

场景: 销售业绩排行榜、游戏积分榜、直播间热度榜

java 复制代码
// 更新分数(score=业绩金额)
redisTemplate.opsForZSet().add("rank:sales", userId, totalSalesAmount);

// 查询某人排名(从0开始,0=第一)
Long rank = redisTemplate.opsForZSet().rank("rank:sales", userId);

// 查询前N名
Set<ZSetOperations.TypedTuple<String>> top10 =
    redisTemplate.opsForZSet().reverseRangeWithScores("rank:sales", 0, 9);

// 查询某人的分数
Double score = redisTemplate.opsForZSet().score("rank:sales", userId);

8.2 排队叫号

场景: 客户提交风控/审批后排队,前端展示"前面还有几人"

java 复制代码
// 入队:score=提交时间戳(按时间排序公平调度)
redisTemplate.opsForZSet().add("queue:pending",
    orderId, System.currentTimeMillis());

// 查询排名
Long position = redisTemplate.opsForZSet().rank("queue:pending", orderId);
Long totalWaiting = redisTemplate.opsForZSet().zCard("queue:pending");
// "您前面还有 N 人等待审核"

// 处理完移除
redisTemplate.opsForZSet().remove("queue:pending", orderId);

9. ZSet --- 延迟队列

9.1 适用场景

  • 订单超时未支付自动关闭
  • 定时提醒/通知
  • 延迟重试任务

9.2 实现思路

复制代码
score = 执行时间戳(当前时间 + 延迟秒数)
消费者轮询ZSet,取score <= 当前时间的成员执行

9.3 伪代码

java 复制代码
// 生产者:延迟30分钟执行
public void enqueueDelay(String orderId, long delaySeconds) {
    double executeTime = System.currentTimeMillis() + delaySeconds * 1000;
    redisTemplate.opsForZSet().add("delay:queue", orderId, executeTime);
}

// 消费者:轮询取到期任务
public void consumeDelayQueue() {
    while (true) {
        long now = System.currentTimeMillis();
        Set<String> readyTasks = redisTemplate.opsForZSet()
            .rangeByScore("delay:queue", 0, now);

        for (String orderId : readyTasks) {
            // 取出的同时删除(防重复消费)
            Long removed = redisTemplate.opsForZSet()
                .remove("delay:queue", orderId);
            if (removed > 0) {
                // 执行业务:检查订单是否已支付,未支付则关闭
                processTimeoutOrder(orderId);
            }
        }
        Thread.sleep(1000);  // 1秒轮询一次
    }
}

10. Hash --- 对象缓存

10.1 适用场景

结构化对象的字段级缓存,可以只读写部分字段而不用序列化整个对象。

java 复制代码
// 缓存用户信息(Hash结构)
Map<String, String> userMap = Map.of(
    "name", "张三",
    "phone", "138****1234",
    "role", "admin"
);
redisTemplate.opsForHash().putAll("user:1001", userMap);
redisTemplate.expire("user:1001", 30, TimeUnit.MINUTES);

// 读取单个字段(不需要反序列化整个对象)
String name = (String) redisTemplate.opsForHash().get("user:1001", "name");

// 更新单个字段
redisTemplate.opsForHash().put("user:1001", "role", "superAdmin");

10.2 与String存JSON的对比

维度 Hash String(JSON)
部分更新 只更新某个字段 需要读出来→改→写回去
网络开销 HGET单个字段,数据量小 GET整个JSON,数据量大
序列化 不需要 每次读写都要序列化/反序列化
适用场景 对象字段多、更新频繁 对象结构简单、整体读写

11. Set --- 去重/共同好友/抽奖

11.1 消息/内容去重

java 复制代码
// 同一内容18000秒内不重复发送
String dedupKey = "dedup:notify:" + contentHash;
if (!redisTemplate.opsForSet().isMember(dedupKey, contentHash)) {
    sendNotification(content);
    redisTemplate.opsForSet().add(dedupKey, contentHash);
    redisTemplate.expire(dedupKey, 18000, TimeUnit.SECONDS);
}

11.2 共同好友/共同关注

java 复制代码
// 用户A的关注列表
redisTemplate.opsForSet().add("follow:A", "user1", "user2", "user3");
// 用户B的关注列表
redisTemplate.opsForSet().add("follow:B", "user2", "user3", "user4");

// 共同关注(交集)
Set<Object> common = redisTemplate.opsForSet().intersect("follow:A", "follow:B");
// → ["user2", "user3"]

11.3 抽奖

java 复制代码
// 参与抽奖
redisTemplate.opsForSet().add("lottery:活动ID", userId1, userId2, userId3);

// 抽N个人
Set<Object> winners = redisTemplate.opsForSet().randomMembers("lottery:活动ID", 3);

// 从中奖结果中移除(不重复中奖)
redisTemplate.opsForSet().remove("lottery:活动ID", winners.toArray());

12. Lua 脚本 --- 原子操作

12.1 适用场景

多个Redis操作需要原子执行:分布式锁解锁、库存扣减、权限校验等。

12.2 扣库存示例

lua 复制代码
-- 原子扣减库存:判断+扣减在一条命令里完成
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil then
    return -1  -- 库存key不存在
end
if stock < tonumber(ARGV[1]) then
    return 0   -- 库存不足
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1       -- 扣减成功
java 复制代码
// Java调用
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(script,
    List.of("stock:sku:1001"),   // KEYS
    String.valueOf(quantity)      // ARGV
);

12.3 权限校验示例

lua 复制代码
-- 一次请求完成多个权限检查
-- 检查系统是否维护中
if redis.call('GET', KEYS[1]) then
    return 'MAINTENANCE'
end
-- 按角色优先级查权限
for i = 1, #ARGV do
    local perm = redis.call('GET', ARGV[i])
    if perm then return perm end
end
return 'NO_PERMISSION'

12.4 面试要点

Lua脚本在Redis中是单线程原子执行的,所有命令作为一个整体执行,中间不会被其他客户端打断。适合"读取-判断-写入"这种需要原子性的复合操作。


13. Bitmap --- 签到/在线状态

13.1 每日签到

java 复制代码
// 用户签到(key=年月,offset=日期,value=bit)
redisTemplate.opsForValue().setBit("sign:user:1001:202401", dayOfMonth - 1, true);

// 查询某天是否签到
Boolean signed = redisTemplate.opsForValue().getBit("sign:user:1001:202401", dayOfMonth - 1);

// 统计当月签到天数
Long count = redisTemplate.execute(
    redis -> redis.bitCount("sign:user:1001:202401")
);

空间效率: 365天签到只占365bit ≈ 46字节,百万用户一年签到数据约46MB。

13.2 在线用户统计

java 复制代码
// 用户上线
redisTemplate.opsForValue().setBit("online:users", userId, true);

// 用户下线
redisTemplate.opsForValue().setBit("online:users", userId, false);

// 统计在线人数
Long onlineCount = redisTemplate.execute(
    redis -> redis.bitCount("online:users")
);

14. HyperLogLog --- UV统计

14.1 适用场景

统计网站UV(独立访客)、页面访问量,允许误差(约0.81%),内存固定12KB。

java 复制代码
// 记录用户访问
redisTemplate.opsForHyperLogLog().add("uv:page:home", userId1, userId2, userId3);

// 统计UV
Long uv = redisTemplate.opsForHyperLogLog().size("uv:page:home");

// 合并多个页面的UV(如首页+商品页的总UV)
redisTemplate.opsForHyperLogLog().union("uv:total", "uv:page:home", "uv:page:product");

与Set的对比: 1亿个UV用Set需要约800MB,HyperLogLog只需要12KB,但有0.81%误差。


15. Stream --- 消息队列(5.0+)

15.1 适用场景

Redis 5.0引入的原生消息队列,支持消费者组、消息确认、持久化,是List队列的升级版。

15.2 伪代码

java 复制代码
// 生产者:发送消息
Map<String, String> msg = Map.of("orderId", "123", "action", "pay_success");
redisTemplate.opsForStream().add("stream:order", msg);

// 消费者组创建
redisTemplate.opsForStream().createGroup("stream:order", "group:notify");

// 消费者:读取消息
List<MapRecord<String, Object, Map<Object, Object>>> messages =
    redisTemplate.opsForStream().read(Consumer.from("group:notify", "consumer-1"),
        StreamReadOptions.empty().count(10),
        StreamOffset.create("stream:order", ReadOffset.lastConsumed())
    );

// 确认消息(ACK)
for (MapRecord record : messages) {
    redisTemplate.opsForStream().acknowledge(
        "stream:order", "group:notify", record.getId()
    );
}

15.3 与List队列的对比

维度 List Stream
消费确认 无(POP即消费) 支持ACK,消息可重试
消费者组 不支持 支持,多消费者分摊
消息持久化 不持久化 AOF/RDB持久化
消息回溯 不支持 支持从任意位置重新消费

16. Redis 持久化与高可用

16.1 持久化

方式 原理 特点
RDB 定时快照(fork子进程) 恢复快、文件紧凑,但两次快照间数据可能丢失
AOF 每条写命令追加到日志 数据更安全,但文件大、恢复慢
混合 RDB + AOF Redis 4.0+,兼顾两者优点

16.2 高可用方案

方案 原理 适用场景
主从 一主多从,读写分离 读多写少
哨兵 自动故障转移,监控主从 中小规模,不想用Cluster
Cluster 数据分片(16384槽位),多主多从 大数据量、高并发写入

17. 缓存三大问题

17.1 缓存穿透

问题: 查询的数据DB里也没有,每次都穿透到DB

解决方案:

  • 缓存空值(短TTL,如5分钟)
  • 布隆过滤器(快速判断key是否可能存在)
  • 参数校验(拦截非法请求)

17.2 缓存击穿

问题: 某个热点key突然过期,大量请求同时打到DB

解决方案:

  • 互斥锁(只放一个请求回源DB,其他等待)
  • 热点key永不过期(异步刷新)
  • 逻辑过期(value里存过期时间,过期后异步更新,返回旧数据)
java 复制代码
// 互斥锁防击穿
String data = redis.get(key);
if (data == null) {
    if (redis.setIfAbsent("lock:" + key, "1", 10, SECONDS)) {
        try {
            data = db.query(key);          // 回源DB
            redis.set(key, data, 30, MINUTES);
        } finally {
            redis.delete("lock:" + key);
        }
    } else {
        Thread.sleep(50);                  // 等待其他线程回填
        data = redis.get(key);             // 重试读缓存
    }
}

17.3 缓存雪崩

问题: 大量key同时过期,或Redis宕机,请求全部打到DB

解决方案:

  • 过期时间加随机值(防止同时过期)
  • 多级缓存(本地缓存 + Redis + DB)
  • 限流降级(Redis挂了走降级策略)
  • Redis高可用(哨兵/Cluster)
java 复制代码
// 过期时间加随机值
long baseTTL = 30 * 60;  // 30分钟基础过期
long randomTTL = ThreadLocalRandom.current().nextLong(0, 5 * 60);  // 0~5分钟随机
redisTemplate.expire(key, baseTTL + randomTTL, TimeUnit.SECONDS);

18. 面试串讲稿

项目里Redis用了八个维度,按数据类型来说:

String用了四个场景:

  1. 分布式锁------SET NX EX加Lua脚本解锁,覆盖支付回调、库存扣减等并发路径
  2. 缓存------权限数据、用户信息、配置数据等,Cache Aside策略,先更新DB再删缓存
  3. 限流防重------SET NX做固定窗口限流,ZSet做滑动窗口限流
  4. Token会话------JWT+Redis管理登录状态,支持强制踢出

List用了两个场景:

  1. 消息队列 ------RPUSH入队+BLPOP阻塞消费,异步解耦合同回调、派单、分润等任务

  2. 轮询调度池------LPOP取一个+RPUSH放回队尾实现round-robin均匀派单

ZSet用了两个场景:

  1. 排行榜 ------score=分数,rank取排名,reverseRange取前N名

  2. 排队叫号/延迟队列------score=时间戳,rangeByScore取到期任务

Set做去重和集合运算 (共同好友、抽奖),Hash做对象字段级缓存Lua脚本做原子操作 (权限校验、库存扣减),Bitmap做签到/在线统计HyperLogLog做UV统计

缓存三大问题都做了处理:穿透用缓存空值+布隆过滤器,击穿用互斥锁,雪崩用过期时间随机值+多级缓存。高可用方面用哨兵或Cluster部署。

相关推荐
kyrie_sakura1 小时前
MySQL数据库学习笔记1 -- 基础概念,sql语句
数据库·学习·mysql
凌云若寒1 小时前
CODESOFT试用流程
linux·运维·网络·数据库·sentinel
云贝贝贝2 小时前
Oracle 运维高频 6 坑:硬日期、AUTOEXTEND、闪回、锁、FRA、双引号
运维·数据库·oracle
九月要晚安2 小时前
从数据库到湖库一体
数据库
正在走向自律2 小时前
FastGPT高并发登录故障根治方案:50人并发报错、PostgreSQL、MongoDB数据库深度优化手册
数据库·mongodb·postgresql
姜穆澜2 小时前
时空数据处理全攻略
数据库
OceanBase数据库官方博客2 小时前
深度拆解seekdb:AI Native Database 的技术架构与核心能力
数据库·人工智能·架构
爱吃鱼的喵️2 小时前
云数据库怎么选?主流云厂商横向对比与选型指南
数据库·阿里云·云计算
维克兜率天2 小时前
4.3.2.1 日常监控:上线只是开始
服务器·数据库·python·区块链·php·量化