Redis 实战用法学习笔记
本文整理了 Redis 在实际项目中的核心用法,涵盖数据结构选型、分布式锁、消息队列、缓存策略、限流、排行榜等常见场景,每个场景附带思路分析和伪代码,适合面试准备和系统设计参考。
目录
- [String --- 分布式锁](#String — 分布式锁)
- [String --- 缓存与缓存策略](#String — 缓存与缓存策略)
- [String --- 限流/防重](#String — 限流/防重)
- [String --- 业务标记位/信号量](#String — 业务标记位/信号量)
- [String --- Token/会话管理](#String — Token/会话管理)
- [List --- 消息队列(异步解耦)](#List — 消息队列(异步解耦))
- [List --- 轮询调度池(负载均衡)](#List — 轮询调度池(负载均衡))
- [ZSet --- 排行榜/排队叫号](#ZSet — 排行榜/排队叫号)
- [ZSet --- 延迟队列](#ZSet — 延迟队列)
- [Hash --- 对象缓存](#Hash — 对象缓存)
- [Set --- 去重/共同好友/抽奖](#Set — 去重/共同好友/抽奖)
- [Lua 脚本 --- 原子操作](#Lua 脚本 — 原子操作)
- [Bitmap --- 签到/在线状态](#Bitmap — 签到/在线状态)
- [HyperLogLog --- UV统计](#HyperLogLog — UV统计)
- [Stream --- 消息队列(5.0+)](#Stream — 消息队列(5.0+))
- [Redis 持久化与高可用](#Redis 持久化与高可用)
- 缓存三大问题
- 面试串讲稿
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"?
先删缓存的问题:
- 线程A删缓存
- 线程B读缓存miss,从DB读到旧数据,回填缓存
- 线程A更新DB
- 结果:缓存是旧数据,DB是新数据,不一致
先更新DB再删缓存的问题:
- 线程A读缓存miss,查DB拿到旧数据
- 线程B更新DB,删缓存
- 线程A回填缓存(旧数据)
- 结果:缓存是旧数据,但这种情况概率极低(要在线程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用了四个场景:
- 分布式锁------SET NX EX加Lua脚本解锁,覆盖支付回调、库存扣减等并发路径
- 缓存------权限数据、用户信息、配置数据等,Cache Aside策略,先更新DB再删缓存
- 限流防重------SET NX做固定窗口限流,ZSet做滑动窗口限流
- Token会话------JWT+Redis管理登录状态,支持强制踢出
List用了两个场景:
消息队列 ------RPUSH入队+BLPOP阻塞消费,异步解耦合同回调、派单、分润等任务
轮询调度池------LPOP取一个+RPUSH放回队尾实现round-robin均匀派单
ZSet用了两个场景:
排行榜 ------score=分数,rank取排名,reverseRange取前N名
排队叫号/延迟队列------score=时间戳,rangeByScore取到期任务
Set做去重和集合运算 (共同好友、抽奖),Hash做对象字段级缓存 ,Lua脚本做原子操作 (权限校验、库存扣减),Bitmap做签到/在线统计 ,HyperLogLog做UV统计。
缓存三大问题都做了处理:穿透用缓存空值+布隆过滤器,击穿用互斥锁,雪崩用过期时间随机值+多级缓存。高可用方面用哨兵或Cluster部署。