掌握 Redis 核心概念与实战操作,为黑马点评项目打好基础
📌 前言
这篇学习记录涵盖了 Redis 的基础概念、数据类型与操作、Java 集成(Jedis + SpringDataRedis)、主从复制与哨兵机制以及缓存三大经典问题(穿透/击穿/雪崩)。
学完这些内容,就可以无缝衔接到黑马点评项目了。
一、Redis 概述
1.1 什么是 Redis?
Redis 是一个开源(BSD 许可)、内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。
核心特点:
| 特性 | 说明 |
|---|---|
| 基于内存 | 读写速度极快(读 11w/s,写 8.1w/s) |
| 持久化 | 支持 RDB 和 AOF 两种持久化方式 |
| 数据结构丰富 | String、Hash、List、Set、SortedSet 等 |
| 高可用 | 支持主从复制、哨兵监控、集群模式 |
| 支持多语言 | Java、Python、Go、C++、PHP 等 |
1.2 Redis 历史
- 2009 年由 Salvatore Sanfilippo 开发完成
- 2010 年起由 VMware 主持开发
- 2013 年起由 Pivotal 赞助
1.3 关系型 vs 非关系型
| 对比项 | 关系型数据库 | 非关系型数据库(Redis) |
|---|---|---|
| 存储位置 | 硬盘 | 内存 |
| 速度 | 慢 | 快 |
| 格式 | 固定表结构 | K-V、文档、图片等 |
| 扩展性 | 困难(多表关联) | 容易(数据间无耦合) |
| 事务 | 强一致性 | 最终一致性 |
| 代表 | MySQL、Oracle | Redis、MongoDB |
二、Redis 安装与配置
2.1 安装步骤
# 1. 下载安装包
wget http://download.redis.io/releases/redis-5.0.3.tar.gz
# 2. 解压
tar -xzvf redis-5.0.3.tar.gz
cd redis-5.0.3
# 3. 创建安装目录
mkdir -p /usr/local/redis
# 4. 编译安装(指定安装路径)
make PREFIX=/usr/local/redis/ install
# 5. 安装完成后目录结构
# /usr/local/redis/bin/
# ├── redis-server # 服务端
# ├── redis-cli # 客户端
# ├── redis-benchmark
# └── redis-sentinel
2.2 启动 Redis
# 前台启动(默认)
/usr/local/redis/bin/redis-server
# 后台启动(修改配置文件)
cp redis.conf /usr/local/redis/bin/
vim /usr/local/redis/bin/redis.conf
# 将 daemonize 修改为 yes
# 指定配置文件启动
/usr/local/redis/bin/redis-server /usr/local/redis/bin/redis.conf
2.3 配置文件关键项
# 允许所有 IP 访问(注释掉 bind)
# bind 127.0.0.1
# 关闭保护模式
protected-mode no
# 设置密码
requirepass root
# 后台启动
daemonize yes
# 修改数据库数量(默认 16)
databases 32
2.4 使用 Redis Desktop Manager 连接
# 配置文件修改三项
bind 0.0.0.0 # 或注释掉 bind
protected-mode no # 关闭保护模式
requirepass root # 设置认证密码
# 重启 Redis
kill -9 <pid>
/usr/local/redis/bin/redis-server /usr/local/redis/bin/redis.conf
三、Redis 命令行操作(redis-cli)
3.1 连接 Redis
# 基本连接
redis-cli -h 192.168.10.100 -p 6379 -a root
# 指定数据库(默认 0)
redis-cli -h 192.168.10.100 -p 6379 -a root -n 1
3.2 String(字符串)
# 添加单条
set username zhangsan
# 获取
get username
# 添加多条
mset address bj sex 1
# 获取多条
mget username address sex
# 设置过期时间(秒)
set code test ex 180
3.3 Hash(哈希)
# 添加单条
hset userInfo name lisi
# 添加多条
hmset userInfo age 20 sex 1
# 获取单条
hget userInfo name
# 获取多条
hmget userInfo age sex
# 获取所有
hgetall userInfo
# 删除
hdel userInfo name age
3.4 List(列表)
# 左添加(头)
lpush students wangwu lisi
# 右添加(尾)
rpush students zhaoliu
# 获取(0 到 2)
lrange students 0 2
# 获取总条数
llen students
# 删除(删除 1 个 lisi)
lrem students 1 lisi
3.5 Set(集合,无序)
# 添加
sadd letters aaa bbb ccc ddd eee
# 获取所有
smembers letters
# 获取总条数
scard letters
# 删除
srem letters aaa bbb
3.6 Sorted Set(有序集合)
# 添加(分数在前,值在后)
zadd score 7 zhangsan 3 lisi 5 wangwu 6 zhaoliu 2 tianqi
# 获取(按分数从小到大)
zrange score 0 4
# 获取总条数
zcard score
# 删除
zrem score lisi
3.7 层级目录存储
# 使用 : 分隔层级
set user:01 zhangsan
set user:02 lisi
set order:2024:001 xxx
3.8 通用命令
# 设置过期时间(秒)
expire key 180
# 查看剩余时间(-1 永久,-2 已过期)
ttl key
# 删除
del key
# 获取所有 key
keys *
四、Java 操作 Redis(Jedis)
4.1 引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
4.2 基本连接
// 创建连接
Jedis jedis = new Jedis("192.168.10.100", 6379);
jedis.auth("root");
jedis.select(1); // 选择数据库
// 测试连接
String result = jedis.ping(); // 返回 PONG
// 操作数据
jedis.set("username", "zhangsan");
String username = jedis.get("username");
// 释放资源
jedis.close();
4.3 连接池方式
// 创建连接池
JedisPool jedisPool = new JedisPool(
new JedisPoolConfig(),
"192.168.10.100",
6379,
10000,
"root"
);
// 获取连接
Jedis jedis = jedisPool.getResource();
// ... 操作
jedis.close(); // 归还连接池
4.4 JedisUtil 封装
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(1024);
config.setMaxIdle(200);
config.setMinIdle(5);
config.setMaxWaitMillis(10000);
return new JedisPool(config, host, port, 10000, password);
}
}
4.5 Jedis 操作五种数据类型
@Autowired
private JedisPool jedisPool;
private Jedis jedis;
@Before
public void init() {
jedis = jedisPool.getResource();
}
@After
public void close() {
if (jedis != null) jedis.close();
}
String:
jedis.set("username", "zhangsan");
jedis.mset("address", "bj", "sex", "1");
String name = jedis.get("username");
List<String> list = jedis.mget("username", "age");
Hash:
jedis.hset("userInfo", "name", "lisi");
Map<String, String> map = new HashMap<>();
map.put("age", "20");
jedis.hmset("userInfo", map);
String name = jedis.hget("userInfo", "name");
Map<String, String> all = jedis.hgetAll("userInfo");
List:
jedis.lpush("students", "wangwu", "lisi");
jedis.rpush("students", "zhaoliu");
List<String> list = jedis.lrange("students", 0, 2);
Long total = jedis.llen("students");
Set:
jedis.sadd("letters", "aaa", "bbb", "ccc");
Set<String> letters = jedis.smembers("letters");
Long total = jedis.scard("letters");
Sorted Set:
Map<String, Double> scoreMembers = new HashMap<>();
scoreMembers.put("zhangsan", 7D);
scoreMembers.put("lisi", 3D);
jedis.zadd("score", scoreMembers);
Set<String> scores = jedis.zrange("score", 0, 4);
4.6 其他操作
// 层级目录
jedis.set("user:01", "zhangsan");
// 设置过期时间(秒)
jedis.expire("code", 180);
jedis.setex("code", 180, "test");
// 事务
Transaction tx = jedis.multi();
tx.set("tel", "10010");
tx.exec(); // 提交
// tx.discard(); // 回滚
// 获取所有 key
Set<String> keys = jedis.keys("*");
五、Spring Data Redis
5.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
5.2 配置文件(application.yml)
spring:
redis:
host: 192.168.10.100
port: 6379
password: root
database: 0
timeout: 10000ms
jedis:
pool:
max-active: 1024
max-wait: 10000ms
max-idle: 200
min-idle: 5
5.3 Jedis vs Lettuce
| 对比项 | Jedis | Lettuce |
|---|---|---|
| 线程安全 | ❌ 非线程安全(需连接池) | ✅ 线程安全 |
| 连接方式 | 直连 | 基于 Netty NIO |
| 性能 | 一般 | 更好(异步/响应式) |
| 适用场景 | 简单同步操作 | 高并发、异步场景 |
5.4 自定义序列化模板
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
LettuceConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// Key 使用 String 序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 使用 JSON 序列化
template.setValueSerializer(
new GenericJackson2JsonRedisSerializer()
);
template.setHashValueSerializer(
new GenericJackson2JsonRedisSerializer()
);
template.setConnectionFactory(factory);
return template;
}
}
5.5 Spring Data Redis 操作五种数据类型
String:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void testString() {
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
ops.set("username", "zhangsan");
ops.set("user:01", "lisi");
Object name = ops.get("username");
// 设置过期时间
ops.set("code", "abcd", 180, TimeUnit.SECONDS);
}
Hash:
HashOperations<String, String, String> ops = redisTemplate.opsForHash();
ops.put("userInfo", "name", "lisi");
Map<String, String> map = new HashMap<>();
map.put("age", "20");
ops.putAll("userInfo", map);
String name = ops.get("userInfo", "name");
List:
ListOperations<String, Object> ops = redisTemplate.opsForList();
ops.leftPush("students", "lisi");
ops.rightPush("students", "zhaoliu");
List<Object> list = ops.range("students", 0, 2);
Set:
SetOperations<String, Object> ops = redisTemplate.opsForSet();
ops.add("letters", "aaa", "bbb", "ccc");
Set<Object> members = ops.members("letters");
Sorted Set:
ZSetOperations<String, Object> ops = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
tuples.add(new DefaultTypedTuple<>("zhangsan", 7D));
ops.add("score", tuples);
Set<Object> scores = ops.range("score", 0, 4);
获取所有 key 和删除:
Set<String> keys = redisTemplate.keys("*");
redisTemplate.delete("score");
redisTemplate.expire("code", 180, TimeUnit.SECONDS);
Long ttl = redisTemplate.getExpire("code");
六、Redis 主从复制与哨兵
6.1 读写分离架构
6.2 搭建步骤
1. 创建目录结构
/opt/redis/
├── conf/
│ ├── redis-common.conf
│ ├── redis-6379.conf
│ ├── redis-6380.conf
│ └── redis-6381.conf
├── data/
│ ├── dump-6379.rdb
│ ├── dump-6380.rdb
│ └── dump-6381.rdb
└── log/
├── redis-6379.log
├── redis-6380.log
└── redis-6381.log
2. 公共配置 redis-common.conf
# 注释 bind
# bind 127.0.0.1
# 关闭保护模式
protected-mode no
# 后台启动
daemonize yes
# 从服务器只读(默认)
slave-read-only yes
# 设置密码(主从都要设置)
requirepass root
masterauth root
3. 各节点私有配置
# redis-6379.conf(主)
include /opt/redis/conf/redis-common.conf
port 6379
pidfile /var/run/redis-6379.pid
logfile "/opt/redis/log/redis-6379.log"
dbfilename dump-6379.rdb
# redis-6380.conf(从)
include /opt/redis/conf/redis-common.conf
port 6380
pidfile /var/run/redis-6380.pid
logfile "/opt/redis/log/redis-6380.log"
dbfilename dump-6380.rdb
slaveof 127.0.0.1 6379
4. 启动主从
/usr/local/redis/bin/redis-server /opt/redis/conf/redis-6379.conf
/usr/local/redis/bin/redis-server /opt/redis/conf/redis-6380.conf
/usr/local/redis/bin/redis-server /opt/redis/conf/redis-6381.conf
5. 查看主从状态
redis-cli -p 6379 -a root info replication
# role:master
# connected_slaves:2
redis-cli -p 6380 -a root info replication
# role:slave
# master_link_status:up
6.3 哨兵机制(主备切换)
1. 哨兵公共配置
# sentinel-common.conf
protected-mode no
daemonize yes
# 监听端口
# port 26379
# 监控主节点(至少 1 个哨兵同意才切换)
sentinel monitor mymaster 127.0.0.1 6379 1
# 设置密码
sentinel auth-pass mymaster root
# 心跳超时时间(毫秒)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时
sentinel failover-timeout mymaster 180000
2. 各哨兵私有配置
# sentinel-26379.conf
include /opt/redis/conf/sentinel-common.conf
port 26379
pidfile /var/run/sentinel-26379.pid
logfile "/opt/redis/log/sentinel-26379.log"
3. 启动哨兵
/usr/local/redis/bin/redis-sentinel /opt/redis/conf/sentinel-26379.conf
/usr/local/redis/bin/redis-sentinel /opt/redis/conf/sentinel-26380.conf
/usr/local/redis/bin/redis-sentinel /opt/redis/conf/sentinel-26381.conf
4. 测试主备切换
# 杀掉主节点
kill -9 <6379-pid>
# 查看哨兵是否选举新主
redis-cli -p 6380 -a root info replication
# role:master ✅
# 重启原主节点,会自动变为从节点
/usr/local/redis/bin/redis-server /opt/redis/conf/redis-6379.conf
6.4 Spring Data Redis 整合哨兵
spring:
redis:
password: root
database: 0
timeout: 10000ms
lettuce:
pool:
max-active: 1024
max-wait: 10000ms
max-idle: 200
min-idle: 5
sentinel:
master: mymaster
nodes: 192.168.10.100:26379,192.168.10.100:26380,192.168.10.100:26381
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration() {
RedisSentinelConfiguration config = new RedisSentinelConfiguration();
config.master("mymaster");
config.sentinel("192.168.10.100", 26379);
config.sentinel("192.168.10.100", 26380);
config.sentinel("192.168.10.100", 26381);
config.setPassword("root");
return config;
}
七、缓存三大经典问题
7.1 过期淘汰机制
Redis 的 Key 过期删除策略:
1. 定期删除
- 每 100ms 随机抽取设置了过期时间的 Key 进行检查
- 随机抽取是为了避免扫描全部 key 造成 CPU 压力
2. 惰性删除
- 获取 Key 时检查是否过期,过期则删除
- 弥补定期删除可能遗漏的问题
3. 内存淘汰策略(6 种)
| 策略 | 说明 |
|---|---|
volatile-lru | 过期 Key 中淘汰最近最少使用的 ⭐推荐 |
volatile-ttl | 过期 Key 中淘汰即将过期的 |
volatile-random | 过期 Key 中随机淘汰 |
allkeys-lru | 所有 Key 中淘汰最近最少使用的 |
allkeys-random | 所有 Key 中随机淘汰 |
no-enviction | 默认,内存不足时写入报错 |
7.2 缓存击穿
定义: 高并发下,某个热门 Key 突然过期,大量请求直接打到数据库
解决方案:
- 热点数据永不过期 + 异步更新
- 互斥锁保证缓存单线程写入
- 逻辑过期(对象内维护过期时间)
7.3 缓存穿透
定义: 查询缓存和数据库都不存在的数据(如恶意查询 id=-1)
解决方案:
- 布隆过滤器:快速判断 Key 是否可能存在于数据库
- 缓存空对象:查询为空也缓存,过期时间设短(如 60 秒)
- 请求参数校验:拦截非法参数
7.4 缓存雪崩
定义: 大量缓存同时过期,所有请求落到数据库
解决方案:
- 过期时间加随机值:
expire_time = base + random(0, 300) - 热点数据永不过期
- 双缓存机制:缓存 A 设过期时间,缓存 B 不设过期时间
- 集群部署:热点数据均匀分布在不同的 Redis 节点
7.5 三种问题对比
| 问题 | 原因 | 特点 |
|---|---|---|
| 缓存击穿 | 热点 Key 过期 | 针对单个 Key |
| 缓存穿透 | 查询不存在的数据 | 大量请求打到 DB |
| 缓存雪崩 | 大量 Key 同时过期 | 范围广,压力大 |
面试回答参考
-
第一段:开场 + 总述(15秒)
“面试官您好,关于缓存穿透、击穿和雪崩这三个问题,我在项目中都有遇到过,也做过相应的优化方案。我分别来讲一下它们的定义、区别以及解决方案。” -
第二段:缓存穿透(40秒)
“首先缓存穿透,它指的是查询一个缓存和数据库中都不存在的数据。
举个例子,我们商品查询的接口,正常id都是正数,但攻击者故意传一个负数的id去查,因为缓存里没有,每次请求都会穿透缓存直接打到数据库,导致数据库压力骤增。
针对穿透,我有两套解决方案:
第一套是布隆过滤器,我们可以在查询之前先用布隆过滤器判断这个key是否可能存在,如果判断不存在就直接返回,连缓存都不查了。但布隆过滤器有个特点,它只能100%判断不存在,不能100%判断存在,存在一定的误判率,所以需要定期重建。
第二套是缓存空对象,就是从数据库查不到数据时,也把这个key缓存起来,value设成空值,过期时间设短一些,比如60秒。这样短时间内同样的恶意请求就会命中缓存,不会再打到数据库。
这两种方案我们项目里是结合使用的,布隆过滤器做第一层拦截,缓存空值做兜底。”
- 第三段:缓存击穿(50秒)
“然后是缓存击穿,它和穿透不一样。击穿针对的是某一个热点key突然过期的情况。
比如微博上某个明星突然爆出大新闻,这个热点数据的key正好在那一瞬间过期了,那成千上万的请求就会同时绕过缓存,全部打到数据库上,瞬间造成数据库压力陡增。
解决击穿的核心思路就是保证热点数据永远可用。我了解两种方案:
第一种是互斥锁。当发现缓存失效时,不是所有请求都去查数据库,而是只有第一个请求去查,然后重建缓存,其他请求等待或者轮询。实现上可以用Redis的setnx命令来加锁,但要注意设置超时时间,防止持锁线程挂了导致死锁。
第二种是逻辑过期。就是不设置物理过期时间,而是在value里面存一个过期时间戳。查询的时候判断这个时间戳,如果发现逻辑过期了,就返回旧数据,同时异步发起一个线程去更新缓存。这种方案用户体验更好,因为不需要等待,但实现会复杂一些。
我在实际项目中,对于真正热点的数据,一般是直接设置永不过期,然后由定时任务主动去刷新缓存。”
- 第四段:缓存雪崩(40秒)
“然后是缓存雪崩,它和击穿的区别在于:击穿是一个热点key过期,雪崩是大量key在同一时间段集中过期。
比如我们给所有缓存数据都设置了统一的60分钟过期时间,那一个小时之后所有数据同时失效,所有请求全部打到数据库,数据库可能直接就宕机了。
针对雪崩,解决方案主要有三个方向:
第一个是过期时间加随机值。比如设置过期时间时,在60分钟基础上加上一个0到5分钟的随机数,让过期时间分散开,避免集体失效。
第二个是热点数据永不过期,核心数据不设过期时间,由后台任务主动刷新。
第三个是双缓存机制,就是缓存A设过期时间,缓存B不设过期时间。当A失效的时候从B读取数据,同时异步去更新A缓存。
我们项目里是三个方案结合使用,核心数据永不过期,一般数据加随机过期时间,同时做了多级缓存兜底。”
- 第五段:熔断降级 + 兜底(30秒)
“除了上面三个问题的针对性方案之外,我们还在应用层做了熔断降级作为兜底。
降级就是当系统压力过大或者缓存出现问题的时候,牺牲一些非核心功能,保证核心功能可用。比如查询商品详情的时候,如果缓存和数据库都出了问题,可以降级返回缓存的老数据或者默认数据,而不是直接报错。
熔断就是当某个服务的错误率超过一定阈值时,直接切断对这个服务的调用,快速失败返回,避免连锁反应导致整个系统崩溃。
这块我们用的是阿里开源的Sentinel,配置了错误率熔断和慢调用熔断,同时设置了降级返回的兜底数据。这样即使缓存彻底挂了,系统也不会完全不可用,用户体验的损失是可控的。”
- 第六段:项目实践经验(40秒)- 高级加分项
“我在实际项目中还积累了一些经验:
第一个是,我们当时上线之前做了压测,发现没有加随机过期时间的时候,QPS从1000瞬间跌到50左右,加了随机值之后,QPS稳定在800以上,效果非常明显。
第二个是,我们用布隆过滤器的时候,是用Google的Guava本地布隆,把热点key加载到本地缓存里,每次查询前先判断,误判率控制在1%以内。
第三个就是,我们还做了多级缓存,客户端缓存加上Nginx缓存加上Redis缓存再加上本地缓存,每一层都做降级兜底,这样即使Redis挂了,本地缓存还能撑一段时间。
第四个是,所有缓存key的过期时间,我们都集中管理在配置中心,方便统一调整和紧急刷新。
最后我觉得,这三个问题本质上都是缓存命中率下降导致的数据库压力问题,所以核心思路就是尽可能提高缓存命中率,同时做好降级和限流兜底,保证系统的高可用。”
- 📋 关键词速记卡(自己背的时候用)
穿透:查不存在的数据 → 布隆过滤器 + 缓存空值
击穿:热点key过期瞬间 → 互斥锁 + 逻辑过期 + 永不过期
雪崩:大量key同时过期 → 随机过期时间 + 永不过期 + 双缓存
熔断:出问题了直接拒绝请求,快速失败
降级:出问题了返回简化数据,保住核心功能
兜底:多级缓存 + Sentinel 熔断降级 + 默认值返回
关键点:压测数据 + 布隆实现 + 多级缓存 + 配置中心管理
八、总结
8.1 学习路径回顾
- ✅ Redis 基本概念与安装配置
- ✅ redis-cli 操作五种数据类型
- ✅ Jedis 原生 API 操作
- ✅ Spring Data Redis 集成
- ✅ 主从复制 + 哨兵高可用
- ✅ 缓存三大经典问题及解决方案
8.2 下一步:黑马点评项目
学习完以上内容后,你已经具备了:
- Redis 核心 API 操作能力
- Java 集成 Redis 的开发能力
- 高可用架构的搭建能力
- 缓存问题的应对能力
现在可以开始黑马点评项目了,重点实践:
- 短信验证码登录(Session 共享 + Redis)
- 缓存穿透/击穿/雪崩实战
- 分布式锁(Redisson)
- 秒杀系统(Lua 脚本 + 事务)
- 附近商户(GEO 功能)
- 关注推送(Feed 流)
- 好友关注(Set 交集)
:基础篇&spm=1001.2101.3001.5002&articleId=163879388&d=1&t=3&u=4adb02fa55e648e3bd8d706e18f64f2e)
1419

被折叠的 条评论
为什么被折叠?



