1. 引言
在当今互联网高并发场景下,数据库的性能瓶颈往往成为系统扩展的最大障碍。Redis 作为一款高性能的内存数据库,凭借其丰富的数据结构、极快的读写速度和灵活的持久化机制,已经成为 Java 后端开发中不可或缺的组件。
本文将系统性地介绍 Redis 的核心知识体系,包括五种基本数据结构、持久化机制、集群部署模式,以及面试中高频出现的缓存穿透、缓存雪崩和缓存击穿三大经典问题。无论你是准备面试还是实际项目开发,这篇文章都能为你提供完整的知识框架。
2. Redis 概述
2.1 什么是 Redis
Redis(Remote Dictionary Server)是一个开源的、基于内存的键值对存储系统,由 Salvatore Sanfilippo 于 2009 年开发。它支持多种数据结构,如字符串、哈希、列表、集合和有序集合,并提供了丰富的操作命令。
2.2 Redis 的核心特性
- 高性能:基于内存操作,单线程模型避免了并发竞争,读写速度可达 10 万次/秒以上
- 丰富的数据结构:支持 String、Hash、List、Set、ZSet 五种基本类型及 Bitmap、HyperLogLog、Geo 等高级类型
- 持久化机制:支持 RDB 快照和 AOF 日志两种持久化方式
- 高可用:支持主从复制、哨兵模式和集群模式
- 原子性操作:所有命令都是原子操作,支持事务和 Lua 脚本
2.3 Redis 的应用场景
- 缓存系统:减轻数据库压力,提升系统响应速度
- 分布式锁:基于 SETNX 命令实现分布式锁
- 排行榜:利用 ZSet 实现实时排行榜
- 计数器:利用 INCR/DECR 实现访问计数、库存扣减
- 消息队列:利用 List 的 LPUSH/BRPOP 实现简单的消息队列
- 会话管理:存储用户登录状态和 Session 信息
3. Redis 数据结构详解
3.1 String(字符串)
String 是 Redis 最基本的数据类型,可以存储字符串、整数或浮点数,最大容量为 512MB。
常用命令:
SET key value# 设置键值GET key# 获取值INCR key# 值加1DECR key# 值减1SETEX key seconds value# 设置带过期时间的键值MSET key1 value1 key2 value2# 批量设置Java 使用示例:
// 使用 Spring Data Redis@AutowiredprivateStringRedisTemplatestringRedisTemplate;// 设置缓存stringRedisTemplate.opsForValue().set("user:1001","张三",30,TimeUnit.MINUTES);// 获取缓存Stringuser=stringRedisTemplate.opsForValue().get("user:1001");// 计数器操作Longcount=stringRedisTemplate.opsForValue().increment("page:view:count");应用场景:缓存对象、计数器、分布式锁、Session 管理。
3.2 Hash(哈希)
Hash 是一个键值对集合,适合存储对象类型的数据,可以单独修改某个字段而不影响其他字段。
常用命令:
HSET key field value# 设置字段值HGET key field# 获取字段值HGETALL key# 获取所有字段和值HDEL key field# 删除字段HINCRBY key field increment# 字段值增加Java 使用示例:
@AutowiredprivateRedisTemplate<String,Object>redisTemplate;// 存储用户对象HashOperations<String,String,Object>hashOps=redisTemplate.opsForHash();Map<String,Object>userMap=newHashMap<>();userMap.put("name","李四");userMap.put("age",25);userMap.put("email","lisi@example.com");hashOps.putAll("user:1002",userMap);// 获取单个字段Objectname=hashOps.get("user:1002","name");// 获取整个对象Map<String,Object>user=hashOps.entries("user:1002");应用场景:存储对象信息、购物车、用户资料。
3.3 List(列表)
List 是一个双向链表,支持从两端插入和弹出元素,适合实现队列和栈。
常用命令:
LPUSH key value# 从左侧插入RPUSH key value# 从右侧插入LPOP key# 从左侧弹出RPOP key# 从右侧弹出LRANGE key start stop# 获取范围内的元素LLEN key# 获取列表长度Java 使用示例:
// 实现消息队列ListOperations<String,String>listOps=redisTemplate.opsForList();// 生产者:发送消息listOps.rightPush("message:queue","消息1");listOps.rightPush("message:queue","消息2");// 消费者:接收消息(阻塞式)Stringmessage=listOps.leftPop("message:queue",10,TimeUnit.SECONDS);应用场景:消息队列、最新消息列表、时间线。
3.4 Set(集合)
Set 是无序的字符串集合,元素唯一,支持集合运算(交集、并集、差集)。
常用命令:
SADD key member# 添加元素SREM key member# 删除元素SMEMBERS key# 获取所有元素SISMEMBER key member# 判断元素是否存在SINTER key1 key2# 求交集SUNION key1 key2# 求并集Java 使用示例:
SetOperations<String,String>setOps=redisTemplate.opsForSet();// 添加标签setOps.add("user:1001:tags","Java","Redis","Spring");// 判断是否包含某元素BooleanhasTag=setOps.isMember("user:1001:tags","Java");// 求共同关注Set<String>common=setOps.intersect("user:1001:follows","user:1002:follows");应用场景:标签系统、好友关系、去重操作。
3.5 ZSet(有序集合)
ZSet 在 Set 的基础上增加了分数(score)属性,元素按分数排序,适合实现排行榜。
常用命令:
ZADD key score member# 添加元素及分数ZRANGE key start stop# 按分数升序获取ZREVRANGE key start stop# 按分数降序获取ZSCORE key member# 获取元素分数ZINCRBY key increment member# 增加元素分数ZRANK key member# 获取元素排名Java 使用示例:
ZSetOperations<String,String>zSetOps=redisTemplate.opsForZSet();// 添加排行榜数据zSetOps.add("ranking:game","玩家A",100);zSetOps.add("ranking:game","玩家B",85);zSetOps.add("ranking:game","玩家C",95);// 获取前三名Set<String>top3=zSetOps.reverseRange("ranking:game",0,2);// 增加分数zSetOps.incrementScore("ranking:game","玩家B",20);应用场景:排行榜、延时队列、优先级队列。
4. Redis 持久化机制
4.1 RDB(Redis DataBase)持久化
RDB 是 Redis 默认的持久化方式,通过创建快照(Snapshot)将内存中的数据保存到磁盘。
工作原理:
- 在指定的时间间隔内,将内存中的数据集快照写入磁盘
- 默认文件名为
dump.rdb - 通过 fork 子进程完成快照生成,不影响主进程服务
触发条件:
# 配置文件 redis.conf 中的默认配置save9001# 900秒内至少1次修改save30010# 300秒内至少10次修改save6010000# 60秒内至少10000次修改优点:
- 文件紧凑,适合备份和灾难恢复
- 恢复速度快
- 对性能影响小
缺点:
- 可能丢失最后一次快照后的数据
- fork 子进程时可能短暂阻塞
4.2 AOF(Append Only File)持久化
AOF 通过记录每次写操作命令来持久化数据,以追加的方式写入日志文件。
工作原理:
- 将每次写命令追加到
appendonly.aof文件 - 支持三种同步策略:always、everysec、no
- 支持 AOF 重写机制压缩文件大小
配置方式:
# 开启 AOFappendonlyyes# 同步策略appendfsync always# 每次写入都同步,最安全但性能最差appendfsync everysec# 每秒同步一次,推荐appendfsync no# 由操作系统决定同步时机优点:
- 数据安全性高,最多丢失 1 秒数据
- 日志文件可读,便于排查问题
缺点:
- 文件体积较大
- 恢复速度较慢
- 相同数据集下 AOF 文件比 RDB 大
4.3 混合持久化
Redis 4.0 引入了混合持久化机制,结合 RDB 和 AOF 的优点:
- 使用 RDB 作为基础快照
- 在快照之间使用 AOF 记录增量操作
- 兼顾恢复速度和数据安全性
# 开启混合持久化aof-use-rdb-preambleyes5. Redis 集群模式
5.1 主从复制模式
主从复制是最基础的集群模式,通过将主节点的数据同步到从节点,实现读写分离和数据备份。
架构特点:
- 一个主节点(Master)负责写操作
- 多个从节点(Slave)负责读操作
- 从节点自动同步主节点数据
配置方式:
# 在从节点配置文件中指定主节点replicaof192.168.1.1006379# 或使用命令动态设置SLAVEOF192.168.1.1006379复制原理:
- 从节点发送 SYNC 命令
- 主节点执行 BGSAVE 生成 RDB 快照
- 主节点将快照发送给从节点
- 从节点加载快照
- 主节点将增量命令发送给从节点
优点:
- 实现读写分离,提升读性能
- 数据冗余备份
- 配置简单
缺点:
- 主节点故障时无法自动切换
- 从节点过多时同步压力大
5.2 哨兵模式(Sentinel)
哨兵模式在主从复制基础上增加了自动故障转移能力,实现高可用。
架构特点:
- 哨兵进程监控主从节点的运行状态
- 主节点故障时自动选举新的主节点
- 客户端通过哨兵获取当前主节点地址
哨兵配置:
# sentinel.confsentinel monitor mymaster192.168.1.10063792sentinel down-after-milliseconds mymaster5000sentinel failover-timeout mymaster15000故障转移流程:
优点:
- 自动故障转移,高可用
- 无需人工干预
- 支持监控和通知
缺点:
- 只有一个主节点,写性能受限
- 配置相对复杂
5.3 Cluster 集群模式
Cluster 模式是 Redis 3.0 引入的分布式解决方案,支持数据分片和水平扩展。
架构特点:
- 数据自动分片到 16384 个哈希槽
- 每个节点负责一部分哈希槽
- 支持多主多从,无中心化架构
- 支持在线扩容和缩容
哈希槽分配:
# 集群节点分配哈希槽redis-cli-p7001cluster addslots0-5460 redis-cli-p7002cluster addslots5461-10922 redis-cli-p7003cluster addslots10923-16383集群配置示例:
# 创建集群(至少3个主节点)redis-cli--clustercreate192.168.1.101:7001192.168.1.102:7002\192.168.1.103:7003 --cluster-replicas1数据分片原理:
优点:
- 支持水平扩展,突破单机内存限制
- 高可用,支持自动故障转移
- 无中心化,避免单点瓶颈
缺点:
- 不支持多键操作(跨槽位)
- 客户端需要支持集群协议
- 数据迁移期间可能影响性能
6. 缓存穿透、缓存雪崩与缓存击穿
6.1 缓存穿透
问题描述:查询一个不存在的数据,缓存和数据库中都没有,导致每次请求都直接打到数据库。
产生原因:
- 恶意攻击,构造大量不存在的 key
- 业务逻辑缺陷,查询了不存在的 ID
解决方案:
方案一:缓存空值
publicObjectgetData(Stringkey){// 先从缓存查询Objectvalue=redisTemplate.opsForValue().get(key);if(value!=null){returnvalue;}// 缓存未命中,查询数据库ObjectdbValue=queryFromDatabase(key);if(dbValue==null){// 缓存空值,设置较短的过期时间redisTemplate.opsForValue().set(key,"",60,TimeUnit.SECONDS);returnnull;}// 缓存真实数据redisTemplate.opsForValue().set(key,dbValue,30,TimeUnit.MINUTES);returndbValue;}方案二:布隆过滤器
// 使用 Guava 布隆过滤器BloomFilter<String>bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000000,// 预计元素数量0.01// 误判率);// 初始化时加载所有存在的 keybloomFilter.put("user:1001");bloomFilter.put("user:1002");publicObjectgetData(Stringkey){// 布隆过滤器判断if(!bloomFilter.mightContain(key)){returnnull;// 一定不存在,直接返回}// 可能存在,继续查询缓存和数据库// ...}6.2 缓存雪崩
问题描述:大量缓存同时过期或 Redis 服务宕机,导致所有请求直接打到数据库,造成数据库压力骤增甚至崩溃。
产生原因:
- 缓存设置了相同的过期时间
- Redis 服务宕机
- 缓存服务器重启
解决方案:
方案一:过期时间加随机值
// 设置过期时间时加入随机值,避免同时过期intbaseExpire=30*60;// 基础过期时间30分钟intrandomExpire=newRandom().nextInt(300);// 随机0-300秒redisTemplate.opsForValue().set(key,value,baseExpire+randomExpire,TimeUnit.SECONDS);方案二:多级缓存
// 一级缓存:本地缓存(Caffeine)Cache<String,Object>localCache=Caffeine.newBuilder().expireAfterWrite(5,TimeUnit.MINUTES).maximumSize(10000).build();// 二级缓存:RedispublicObjectgetData(Stringkey){// 先查本地缓存ObjectlocalValue=localCache.getIfPresent(key);if(localValue!=null){returnlocalValue;}// 再查 RedisObjectredisValue=redisTemplate.opsForValue().get(key);if(redisValue!=null){localCache.put(key,redisValue);returnredisValue;}// 最后查数据库ObjectdbValue=queryFromDatabase(key);if(dbValue!=null){redisTemplate.opsForValue().set(key,dbValue,30,TimeUnit.MINUTES);localCache.put(key,dbValue);}returndbValue;}方案三:Redis 高可用
- 使用哨兵模式或集群模式
- 配置主从复制,实现故障自动转移
- 使用持久化机制,重启后快速恢复
6.3 缓存击穿
问题描述:某个热点 key 在缓存过期的瞬间,大量并发请求同时访问该 key,导致所有请求都打到数据库。
产生原因:
- 热点数据过期
- 高并发访问同一数据
解决方案:
方案一:互斥锁
publicObjectgetData(Stringkey){// 先从缓存查询Objectvalue=redisTemplate.opsForValue().get(key);if(value!=null){returnvalue;}