Redis 实战指南:缓存、分布式锁、持久化
Redis 是 CSDN 上后端技术流量最高的主题之一,也是大厂面试必考。本文从数据结构讲起,覆盖缓存三大问题(穿透/击穿/雪崩)、分布式锁、持久化、集群、性能调优,全部带可运行代码。
一、Redis 是什么
Redis 是内存数据库——数据存在内存里,读写纳秒级。相比 MySQL(磁盘),它快几个数量级,代价是数据不是主要存储。
用途:
- 缓存(加速读);
- 分布式锁(并发控制);
- 计数器(点赞、库存);
- 消息队列(List/Stream);
- 排行榜(ZSet);
- 会话共享(Session 存 Redis)。
核心特点:
- 单线程执行命令(6.0 后多线程处理网络 IO,但命令执行仍单线程)——所以命令都是原子的;
- 丰富的数据结构;
- 支持持久化(RDB/AOF);
- 支持主从、哨兵、集群。
二、五种基本数据结构
2.1 String
最常用。存 JSON、计数、分布式锁。
SET user:1001'{"name":"张三","age":25}'GET user:1001 INCR visit:count# 自增SETNX lock:order1# 不存在才设置(分布式锁用)EXPIRE user:10013600# 设置过期2.2 Hash
适合存对象字段,可单独更新某个字段。
HSET user:1001 name 张三 age25HGET user:1001 name HINCRBY user:1001 score10为什么比 String 好:对象场景下,Hash 只改一个字段不用整体反序列化,内存占用也低。
2.3 List
双向链表。消息队列、时间线。
LPUSH news:tech"Spring Boot 3 发布"LRANGE news:tech0-1LPOP news:tech2.4 Set
去重、集合运算。关注关系、标签。
SADD user:1001:following20012002SISMEMBER user:1001:following2001# 是否关注SINTER user:1001:following user:1002:following# 共同关注2.5 ZSet(有序集合)
带分数的集合。排行榜、延迟队列。
ZADD rank:game1000"playerA"ZADD rank:game800"playerB"ZRANGE rank:game0-1WITHSCORES# 升序ZREVRANGE rank:game0-1# 降序(排行榜)ZINCRBY rank:game100"playerA"# 加分数三、缓存三大经典问题
这是面试必问、生产必踩的三座山。
3.1 缓存穿透
现象:查一个不存在的 key——缓存没有,数据库也没有。每次请求都打到数据库。
危害:恶意请求用不存在的 id 刷接口,数据库直接被打挂。
解决:
- 缓存空值:查询结果为空也缓存,过期时间设短(如 5 分钟);
- 布隆过滤器:请求先过 Bloom Filter,不存在的 key 直接拦截(不用查库)。
// 方案一:缓存空值publicUsergetUser(Longid){Stringkey="user:"+id;Useruser=redis.get(key);if(user!=null)returnuser;user=userMapper.selectById(id);if(user==null){redis.set(key,"",300);// 缓存空值 5 分钟returnnull;}redis.set(key,user,3600);returnuser;}3.2 缓存击穿
现象:某个热点 key 过期的瞬间,大量请求同时打到数据库。
区别:穿透是"查不存在",击穿是"查一个存在但缓存刚好过期"。
解决:
- 互斥锁:只有一个线程去查库,其他线程等待;
- 逻辑过期:key 永不过期,value 里带过期时间,异步刷新。
// 方案:互斥锁publicUsergetUser(Longid){Useruser=redis.get("user:"+id);if(user!=null)returnuser;StringlockKey="lock:user:"+id;booleanlocked=redis.setIfAbsent(lockKey,"1",30,TimeUnit.SECONDS);if(!locked){Thread.sleep(50);returngetUser(id);// 递归等待}try{user=userMapper.selectById(id);// 只有一个线程进来redis.set("user:"+id,user,3600);returnuser;}finally{redis.delete(lockKey);}}3.3 缓存雪崩
现象:大量 key 同时过期,或 Redis 宕机,请求全部打到数据库。
解决:
- 过期时间加随机值:
set key val (3600 + random(0,600)),避免同时过期; - 多级缓存:本地缓存(Caffeine)+ Redis;
- Redis 高可用:主从 + 哨兵;
- 限流降级:数据库扛不住时直接降级。
三者对比:
| 问题 | 场景 | 核心解决 |
|---|---|---|
| 穿透 | 查不存在的 key | 空值缓存 / 布隆过滤器 |
| 击穿 | 热点 key 过期瞬间 | 互斥锁 / 逻辑过期 |
| 雪崩 | 大量 key 同时过期 / Redis 宕机 | 随机过期 / 高可用 / 多级缓存 |
四、分布式锁
4.1 为什么需要分布式锁
单体时代用synchronized,多实例部署后(两个 JVM 各持一把锁)就不管用了——需要跨进程的锁。
场景:防止重复下单、库存扣减、定时任务重复执行。
4.2 基于 Redis 的分布式锁
版本一:简单版(不推荐生产用)
// 加锁Booleanok=redis.setIfAbsent("lock:order:1001","1",30,TimeUnit.SECONDS);if(ok){try{doBiz();}finally{redis.delete("lock:order:1001");}}问题:
- 误删锁:线程 A 的锁过期了,线程 B 拿到锁,A 执行完把 B 的锁删了——要校验 value 是自己的;
- 过期时间太短:业务没执行完锁就过期了。
版本二:校验 value + 续期
Stringtoken=UUID.randomUUID().toString();Booleanok=redis.setIfAbsent(lockKey,token,30,TimeUnit.SECONDS);if(ok){// 看门狗续期(Redisson 自动做)try{doBiz();}finally{// 只删自己的锁if(token.equals(redis.get(lockKey))){redis.delete(lockKey);}}}生产建议:直接用Redisson,它实现了看门狗自动续期、可重入锁、公平锁,别自己造轮子:
RLocklock=redisson.getLock("lock:order:"+orderId);if(lock.tryLock(3,30,TimeUnit.SECONDS)){try{doBiz();}finally{lock.unlock();}}4.3 Redlock 与主从问题
Redis 主从切换时,锁可能丢(主挂了,从没同步到锁)。极端场景用 Redlock(多节点加锁),但复杂且有争议。大多数业务场景:单节点 Redis + 哨兵 + 合理过期时间就够,别过度设计。
五、持久化
Redis 数据在内存,重启会丢——持久化解决这个问题。
5.1 RDB(快照)
定时把内存数据 dump 到磁盘。
# redis.conf save 900 1 # 900秒内1次写就触发 save 300 10 save 60 10000优点:文件小、恢复快、适合备份。
缺点:可能丢最后一次快照后的数据。
5.2 AOF(追加日志)
每次写命令追加到日志文件。
appendonly yes appendfsync everysec # 每秒刷盘(推荐)优点:最多丢 1 秒数据。
缺点:文件大、恢复慢(可 AOF rewrite 压缩)。
5.3 怎么选
- 能丢 1 分钟数据:RDB 够;
- 不能丢数据(订单、支付):AOF everysec;
- 生产标配:RDB + AOF 都开。
六、高可用架构
6.1 主从复制
一个主节点写,多个从节点读(读写分离)。
# 从节点配置replicaof192.168.1.106379- 主节点写命令同步到从节点;
- 读压力大时,读请求走从节点;
- 缺点:主挂了要手动切换(用哨兵解决)。
6.2 哨兵(Sentinel)
监控主节点,挂了自动选新主:
Sentinel 集群(3 个以上) ↓ 监控 Redis 主节点 ←→ 从节点1 ←→ 从节点2redis-sentinel sentinel.conf作用:故障自动切换、通知客户端新主地址。
6.3 Cluster 集群
数据量大 + 写量大,用 Cluster 分片:
- 16384 个 hash slot,key 按
CRC16(key) % 16384分配到节点; - 每个节点管一部分 slot;
- 支持自动扩容/缩容。
选型建议:
| 规模 | 方案 |
|---|---|
| 单机 < 10G | 主从 + 哨兵 |
| 读写分离 | 主从 |
| 数据量大/写量大 | Cluster |
| 云厂商 | 直接用云 Redis(自带高可用) |
七、Redis 性能调优
7.1 慢查询
SLOWLOG GET10# 看最近慢命令configsetslowlog-log-slower-than10000# 阈值 10ms常见慢命令:KEYS *(全量扫)、大 key 的HGETALL、SMEMBERS。
禁止 KEYS:生产环境KEYS *会阻塞 Redis 几秒。用SCAN游标式遍历。
7.2 大 Key 问题
单个 key 值超过 10KB 或集合元素超过 1 万个叫大 key:
- 读写慢、阻塞;
- 删除时卡顿。
处理:拆分 key、压缩值、删除用UNLINK(异步删除)。
7.3 热点 Key
某个 key 被超高并发访问,单节点扛不住:
- 本地缓存:JVM 里缓存热点数据,Redis 再挂也是走本地;
- 读写分离:读走多个从节点;
- key 分片:
key:1、key:2…分散到不同节点。
7.4 内存淘汰策略
maxmemory 4gb maxmemory-policy allkeys-lru # 淘汰最近最少使用策略:
allkeys-lru:所有 key 按 LRU 淘汰(常用);volatile-lru:只淘汰有过期时间的;allkeys-lfu:按访问频率(热数据稳定场景)。
八、Spring Boot 集成 Redis
8.1 依赖与配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>spring:data:redis:host:localhostport:6379lettuce:pool:max-active:208.2 封装工具类
@ComponentpublicclassRedisUtil{@AutowiredprivateStringRedisTemplateredis;publicvoidset(Stringkey,Stringvalue,longtimeout){redis.opsForValue().set(key,value,timeout,TimeUnit.SECONDS);}publicStringget(Stringkey){returnredis.opsForValue().get(key);}publicbooleansetIfAbsent(Stringkey,Stringvalue,longtimeout){returnredis.opsForValue().setIfAbsent(key,value,timeout,TimeUnit.SECONDS);}publicvoiddelete(Stringkey){redis.delete(key);}}坑:RedisTemplate默认 JDK 序列化,存进去的 key 带乱码前缀。用StringRedisTemplate或配置 JSON 序列化器。
九、实战案例:商品详情缓存
@ServicepublicclassProductService{// 商品详情:本地缓存 → Redis → 数据库publicProductDetailgetDetail(LongproductId){// 1. 本地缓存ProductDetaillocal=localCache.getIfPresent(productId);if(local!=null)returnlocal;// 2. Redis(带逻辑过期防击穿)Stringkey="product:detail:"+productId;ProductDetailredisData=redis.get(key);if(redisData!=null){if(redisData.isExpired()){// 逻辑过期:异步刷新,先返回旧数据asyncRefresh(productId,key);}localCache.put(productId,redisData);returnredisData;}// 3. 互斥锁查库StringlockKey="lock:product:"+productId;booleanlocked=redis.setIfAbsent(lockKey,"1",30,TimeUnit.SECONDS);if(!locked){Thread.sleep(50);returngetDetail(productId);}try{ProductDetaildetail=productMapper.selectDetail(productId);if(detail==null){redis.set(key,"",300);// 缓存空值防穿透returnnull;}redis.set(key,detail,3600);localCache.put(productId,detail);returndetail;}finally{redis.delete(lockKey);}}}这一套组合拳同时防了穿透、击穿、雪崩:空值缓存防穿透,互斥锁防击穿,随机过期+本地缓存防雪崩。
本章小结
Redis 看似简单(set/get),但生产级的难点全在"缓存一致性、高可用、性能边界"这些细节上。掌握三大问题 + 分布式锁 + 持久化 + 集群,就掌握了 Redis 的核心战场。
下一篇讲 Docker——部署的标准化,和 Redis 一样是 CSDN 热门。
(全文完)