一、Redis核心概念
1.1 什么是Redis?
Redis(Remote Dictionary Server) 是一个基于内存的键值对存储系统,具有以下特点:
- 内存存储:数据存储在内存中,读写速度极快(10万+QPS)
- 持久化:支持RDB和AOF两种持久化方式
- 多数据结构:支持String、Hash、List、Set、ZSet等数据结构
- 单线程模型:命令执行单线程,避免并发问题
- 高性能:基于IO多路复用技术,支持高并发
核心理解:
传统数据库:磁盘存储 → 慢速(毫秒级)
Redis:内存存储 → 极快(微秒级)
1.2 为什么Redis这么快?
三大核心原因:
-
纯内存操作
- 内存访问速度:ns级别
- 磁盘访问速度:ms级别
- 速度差距:100万倍
-
单线程模型
优势: - 避免线程切换开销 - 避免锁竞争 - 避免上下文切换 为什么单线程还快? - 内存操作快,CPU不是瓶颈 - IO多路复用处理并发 -
IO多路复用
传统IO:一个连接一个线程 多路复用:一个线程处理多个连接 使用epoll机制: - 监听多个Socket - 有数据就处理 - 无数据不阻塞
二、Redis作用与应用场景
2.1 核心作用
| 作用 | 说明 | 性能提升 |
|---|---|---|
| 缓存 | 减轻数据库压力 | 读性能提升100倍+ |
| 分布式锁 | 解决并发问题 | 保证数据一致性 |
| 消息队列 | 异步解耦 | 削峰填谷 |
| 计数器 | 高并发计数 | 支持亿级并发 |
| 排行榜 | 实时排序 | 毫秒级响应 |
| Session共享 | 分布式会话 | 无状态服务 |
2.2 典型应用场景
场景1:缓存热点数据
java
// 查询商品详情
public Product getProduct(Long id) {
// 1. 先查缓存
String key = "product:" + id;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
return JSON.parseObject(json, Product.class);
}
// 2. 缓存未命中,查数据库
Product product = productDao.selectById(id);
// 3. 写入缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(product),
30, TimeUnit.MINUTES);
return product;
}
为什么这样做?
数据库QPS:1000
Redis QPS:100000+
查询流程:
┌─────────┐ Cache Hit (99%) ┌───────┐
│ Client │ ─────────────────────→ │ Redis │
└─────────┘ └───────┘
│ Cache Miss (1%)
└──────────────────────────→ ┌──────────┐
│ Database │
└──────────┘
效果:数据库压力降低99%
场景2:分布式锁(秒杀场景)
java
@Service
public class SeckillService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean seckill(Long userId, Long productId) {
String lockKey = "lock:product:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 获取锁,设置过期时间防止死锁
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (!success) {
return false; // 获取锁失败
}
// 业务逻辑:扣减库存
Integer stock = (Integer) redisTemplate.opsForValue()
.get("stock:" + productId);
if (stock <= 0) {
return false; // 库存不足
}
// 库存扣减
redisTemplate.opsForValue().decrement("stock:" + productId);
// 创建订单(异步或同步)
createOrder(userId, productId);
return true;
} finally {
// 释放锁(Lua脚本保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), requestId);
}
}
}
核心要点:
- 设置过期时间:防止死锁
- 使用唯一ID:防止误删其他线程的锁
- Lua脚本释放锁:保证原子性
场景3:排行榜(ZSet应用)
java
// 游戏积分排行榜
public class RankService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 更新积分
public void updateScore(String userId, double score) {
redisTemplate.opsForZSet().add("rank:score", userId, score);
}
// 获取前100名
public Set<ZSetOperations.TypedTuple<String>> getTop100() {
return redisTemplate.opsForZSet()
.reverseRangeWithScores("rank:score", 0, 99);
}
// 获取用户排名
public Long getUserRank(String userId) {
return redisTemplate.opsForZSet()
.reverseRank("rank:score", userId);
}
}
三、Redis数据类型详解
3.1 String(字符串)
底层实现:SDS(Simple Dynamic String)
应用场景:
- 缓存对象(JSON序列化)
- 计数器(incr/decr)
- 分布式锁(setnx)
- Session共享
常用命令:
bash
SET key value [EX seconds] [NX|XX]
GET key
INCR key # 自增1
DECR key # 自减1
INCRBY key delta # 增加指定值
代码示例:
java
// 计数器:文章浏览量
public void incrViewCount(Long articleId) {
String key = "article:view:" + articleId;
redisTemplate.opsForValue().increment(key);
}
// 获取浏览量
public Long getViewCount(Long articleId) {
String key = "article:view:" + articleId;
Object count = redisTemplate.opsForValue().get(key);
return count == null ? 0L : Long.parseLong(count.toString());
}
3.2 Hash(哈希)
底层实现:哈希表(ziplist或hashtable)
应用场景:
- 对象存储(用户信息、商品信息)
- 购物车
常用命令:
bash
HSET key field value
HGET key field
HGETALL key
HMSET key field1 value1 field2 value2
HINCRBY key field delta
代码示例:
java
// 购物车实现
public class CartService {
// 添加商品到购物车
public void addCart(Long userId, Long productId, Integer count) {
String key = "cart:" + userId;
redisTemplate.opsForHash().increment(key, productId.toString(), count);
}
// 获取购物车
public Map<Object, Object> getCart(Long userId) {
String key = "cart:" + userId;
return redisTemplate.opsForHash().entries(key);
}
// 删除商品
public void removeProduct(Long userId, Long productId) {
String key = "cart:" + userId;
redisTemplate.opsForHash().delete(key, productId.toString());
}
}
为什么用Hash而不是String?
String方式:
- 整个对象序列化为JSON
- 修改一个字段需要读取整个对象
- 占用空间大
Hash方式:
- 字段独立存储
- 修改单个字段高效
- 占用空间小
3.3 List(列表)
底层实现:quicklist(双向链表+ziplist)
应用场景:
- 消息队列
- 最新列表(朋友圈)
- 评论列表
常用命令:
bash
LPUSH key value # 左侧插入
RPUSH key value # 右侧插入
LPOP key # 左侧弹出
RPOP key # 右侧弹出
LRANGE key start end # 范围查询
BLPOP key timeout # 阻塞弹出
代码示例:
java
// 消息队列实现
public class MsgQueueService {
// 生产消息
public void sendMsg(String queue, String msg) {
redisTemplate.opsForList().rightPush(queue, msg);
}
// 消费消息(阻塞)
public String receiveMsg(String queue) {
List<String> result = redisTemplate.opsForList()
.leftPop(queue, 10, TimeUnit.SECONDS);
return result != null && !result.isEmpty() ? result.get(0) : null;
}
}
3.4 Set(集合)
底层实现:intset或hashtable
应用场景:
- 标签系统
- 共同关注
- 抽奖系统
- 去重
常用命令:
bash
SADD key member # 添加元素
SREM key member # 删除元素
SISMEMBER key member # 是否存在
SINTER key1 key2 # 交集
SUNION key1 key2 # 并集
SDIFF key1 key2 # 差集
SRANDMEMBER key count # 随机返回
代码示例:
java
// 抽奖系统
public class LotteryService {
// 添加参与者
public void addUser(Long activityId, Long userId) {
String key = "lottery:" + activityId;
redisTemplate.opsForSet().add(key, userId.toString());
}
// 抽取N个中奖者
public List<String> draw(Long activityId, int count) {
String key = "lottery:" + activityId;
return redisTemplate.opsForSet().randomMembers(key, count);
}
// 共同关注
public Set<String> commonFollows(Long userId1, Long userId2) {
String key1 = "follows:" + userId1;
String key2 = "follows:" + userId2;
return redisTemplate.opsForSet().intersect(key1, key2);
}
}
3.5 ZSet(有序集合)
底层实现:ziplist或skiplist+hashtable
应用场景:
- 排行榜
- 延迟队列
- 范围查询
常用命令:
bash
ZADD key score member # 添加元素
ZRANGE key start end # 正序范围查询
ZREVRANGE key start end # 倒序范围查询
ZRANK key member # 正序排名
ZREVRANK key member # 倒序排名
ZINCRBY key increment member # 增加分数
代码示例:
java
// 热搜榜实现
public class HotSearchService {
private static final String HOT_SEARCH_KEY = "hot:search";
// 搜索时增加热度
public void search(String keyword) {
redisTemplate.opsForZSet().incrementScore(HOT_SEARCH_KEY, keyword, 1);
}
// 获取热搜榜Top10
public Set<ZSetOperations.TypedTuple<String>> getTop10() {
return redisTemplate.opsForZSet()
.reverseRangeWithScores(HOT_SEARCH_KEY, 0, 9);
}
}
四、Redis使用方法
4.1 基本操作命令
通用命令
bash
# 键操作
KEYS pattern # 查找键(生产禁用,用SCAN代替)
EXISTS key # 判断键是否存在
DEL key # 删除键
EXPIRE key seconds # 设置过期时间
TTL key # 查看剩余时间
TYPE key # 查看类型
# 数据库操作
SELECT index # 切换数据库(0-15)
FLUSHDB # 清空当前库
FLUSHALL # 清空所有库
# 性能相关
PING # 测试连接
INFO # 服务器信息
4.2 Java客户端使用
Jedis(传统方式)
java
public class JedisExample {
public static void main(String[] args) {
// 创建连接
Jedis jedis = new Jedis("localhost", 6379);
// String操作
jedis.set("name", "张三");
String name = jedis.get("name");
// Hash操作
jedis.hset("user:1", "name", "李四");
jedis.hset("user:1", "age", "25");
Map<String, String> user = jedis.hgetAll("user:1");
// List操作
jedis.rpush("list", "a", "b", "c");
List<String> list = jedis.lrange("list", 0, -1);
// 关闭连接
jedis.close();
}
}
Lettuce(推荐,Spring Boot 2.x默认)
java
public class LettuceExample {
public static void main(String[] args) {
// 创建客户端
RedisClient client = RedisClient.create("redis://localhost:6379");
StatefulRedisConnection<String, String> connection = client.connect();
RedisCommands<String, String> commands = connection.sync();
// 操作
commands.set("key", "value");
String value = commands.get("key");
// 关闭
connection.close();
client.shutdown();
}
}
五、Redis与Spring集成
5.1 Spring Boot集成Redis
步骤1:添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
步骤2:配置文件
yaml
spring:
redis:
# 单机模式
host: localhost
port: 6379
password: 123456
database: 0
# 连接池配置
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: 1000ms # 最大等待时间
# 超时配置
timeout: 3000ms
connect-timeout: 3000ms
步骤3:配置类(可选)
java
@Configuration
public class RedisConfig {
/**
* RedisTemplate配置
* 解决序列化问题
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON序列化配置
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
// String序列化
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// Key使用String序列化
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value使用JSON序列化
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
5.2 使用RedisTemplate
java
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// ======== String操作 ========
public void setUser(User user) {
redisTemplate.opsForValue().set("user:" + user.getId(), user);
}
public User getUser(Long id) {
return (User) redisTemplate.opsForValue().get("user:" + id);
}
// ======== Hash操作 ========
public void setUserField(Long userId, String field, Object value) {
redisTemplate.opsForHash().put("user:" + userId, field, value);
}
public Object getUserField(Long userId, String field) {
return redisTemplate.opsForHash().get("user:" + userId, field);
}
// ======== List操作 ========
public void addToList(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
}
public Object popFromList(String key) {
return redisTemplate.opsForList().leftPop(key);
}
// ======== Set操作 ========
public void addToSet(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public Set<Object> getSet(String key) {
return redisTemplate.opsForSet().members(key);
}
// ======== ZSet操作 ========
public void addToZSet(String key, Object value, double score) {
redisTemplate.opsForZSet().add(key, value, score);
}
public Set<Object> getZSetRange(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
}
5.3 使用StringRedisTemplate
java
@Service
public class CacheService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 设置缓存(带过期时间)
*/
public void set(String key, String value, long timeout, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 获取缓存
*/
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 删除缓存
*/
public Boolean delete(String key) {
return stringRedisTemplate.delete(key);
}
/**
* 设置过期时间
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return stringRedisTemplate.expire(key, timeout, unit);
}
}
5.4 Spring Cache注解
java
@Service
@CacheConfig(cacheNames = "user")
public class UserCacheService {
@Autowired
private UserMapper userMapper;
/**
* @Cacheable: 查询时使用缓存
* 如果缓存存在,直接返回
* 如果缓存不存在,执行方法并缓存结果
*/
@Cacheable(key = "#id")
public User getById(Long id) {
System.out.println("查询数据库...");
return userMapper.selectById(id);
}
/**
* @CachePut: 更新缓存
* 总是执行方法,并将结果更新到缓存
*/
@CachePut(key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
return user;
}
/**
* @CacheEvict: 删除缓存
*/
@CacheEvict(key = "#id")
public void delete(Long id) {
userMapper.deleteById(id);
}
/**
* 条件缓存
*/
@Cacheable(key = "#id", condition = "#id > 0", unless = "#result == null")
public User getByIdWithCondition(Long id) {
return userMapper.selectById(id);
}
}
配置Cache:
java
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// 默认配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 过期时间30分钟
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // 不缓存null值
// 针对不同缓存的个性化配置
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("user", config.entryTtl(Duration.ofHours(1))); // 用户缓存1小时
configs.put("product", config.entryTtl(Duration.ofMinutes(10))); // 商品缓存10分钟
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configs)
.build();
}
}
六、Redis核心原理
6.1 持久化机制
RDB(快照)
工作原理:
1. 定时将内存数据生成快照
2. 保存到dump.rdb文件
3. 恢复时加载文件
触发方式:
- save 900 1 # 900秒内至少1个key变化
- save 300 10 # 300秒内至少10个key变化
- save 60 10000 # 60秒内至少10000个key变化
优点:
✓ 文件小,恢复快
✓ 性能高(fork子进程)
缺点:
✗ 可能丢失最后一次快照后的数据
✗ fork子进程消耗内存
配置:
bash
# redis.conf
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir ./
AOF(追加文件)
工作原理:
1. 记录每个写操作
2. 追加到appendonly.aof文件
3. 恢复时重新执行命令
同步策略:
- always # 每次写入立即同步(慢但安全)
- everysec # 每秒同步一次(推荐)
- no # 由OS决定(快但不安全)
优点:
✓ 数据更安全(最多丢失1秒)
✓ 可读性好
缺点:
✗ 文件大
✗ 恢复慢
✗ 性能相对低
配置:
bash
# redis.conf
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
# AOF重写(压缩)
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
如何选择?
生产环境推荐:RDB + AOF 混合使用
- RDB做全量备份(快速恢复)
- AOF做增量备份(数据安全)
配置:
appendonly yes
aof-use-rdb-preamble yes # AOF重写时使用RDB格式
6.2 过期策略
三种策略:
-
定时删除
设置过期时间时创建定时器
到期自动删除优点:内存友好
缺点:CPU不友好(大量定时器)
Redis不采用 -
惰性删除
访问key时检查是否过期
过期则删除优点:CPU友好
缺点:内存不友好(过期key占用内存)
Redis采用 -
定期删除
每隔一段时间随机检查部分key
删除过期的keyRedis采用:每秒10次,每次检查20个key
Redis实际策略:
惰性删除 + 定期删除
过程:
1. 客户端访问key时检查(惰性)
2. 定期随机抽查删除(定期)
3. 内存不足时触发内存淘汰(兜底)
6.3 内存淘汰策略
8种淘汰策略:
bash
# redis.conf
maxmemory 2gb # 最大内存
maxmemory-policy allkeys-lru # 淘汰策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰,写入返回错误 | 纯缓存,不允许丢失 |
| allkeys-lru | 所有key,淘汰最少使用 | 通用缓存(推荐) |
| allkeys-lfu | 所有key,淘汰最少访问频率 | 热点数据缓存 |
| allkeys-random | 所有key,随机淘汰 | 均匀访问 |
| volatile-lru | 有过期时间的key,LRU | 业务数据缓存 |
| volatile-lfu | 有过期时间的key,LFU | 业务热点缓存 |
| volatile-random | 有过期时间的key,随机 | - |
| volatile-ttl | 有过期时间的key,优先淘汰快过期的 | 时效性数据 |
LRU vs LFU:
LRU(Least Recently Used):
- 淘汰最久未使用的
- 适合:周期性访问
LFU(Least Frequently Used):
- 淘汰访问频率最低的
- 适合:热点数据
推荐:allkeys-lru(Redis默认)
6.4 事务机制
Redis事务特点:
✓ 支持:批量执行、原子性(单线程)
✗ 不支持:回滚、隔离性
命令:
bash
MULTI # 开启事务
command1
command2
EXEC # 提交事务
DISCARD # 放弃事务
WATCH key # 监听key,乐观锁
UNWATCH # 取消监听
示例:
java
// 使用SessionCallback实现事务
public void transfer(String from, String to, int amount) {
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations)
throws DataAccessException {
operations.watch(from); // 监听
operations.multi(); // 开启事务
operations.opsForValue().decrement(from, amount);
operations.opsForValue().increment(to, amount);
return operations.exec(); // 提交
}
});
}
Pipeline(管道):
java
// 批量执行,减少网络RTT
public void batchSet(Map<String, String> data) {
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection)
throws DataAccessException {
data.forEach((k, v) -> {
connection.set(k.getBytes(), v.getBytes());
});
return null;
}
});
}
七、Redis高可用方案
7.1 主从复制
架构:
┌──────┐
│Master│ (读写)
└───┬──┘
│
┌───────┼───────┐
│ │ │
┌───▼──┐ ┌──▼──┐ ┌──▼──┐
│Slave1│ │Slave2│ │Slave3│ (只读)
└──────┘ └─────┘ └─────┘
特点:
- Master负责写
- Slave负责读
- 异步复制
- 读写分离
配置:
bash
# slave节点配置
replicaof 192.168.1.100 6379
masterauth 123456
Java实现读写分离:
java
@Configuration
public class RedisConfig {
@Bean
public LettuceClientConfigurationBuilderCustomizer
lettuceClientConfigurationBuilderCustomizer() {
return clientConfigurationBuilder -> {
clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
// REPLICA_PREFERRED: 优先从slave读,slave不可用时从master读
};
}
}
7.2 哨兵模式(Sentinel)
架构:
┌─────────┐ 监控 ┌──────┐
│Sentinel1├────────→│Master│
└─────────┘ └──┬───┘
│
┌─────────┐ │ ┌──────┐
│Sentinel2├─────────────┼─────→│Slave1│
└─────────┘ │ └──────┘
│
┌─────────┐ │ ┌──────┐
│Sentinel3├─────────────┴─────→│Slave2│
└─────────┘ └──────┘
功能:
1. 监控:检查master和slave是否正常
2. 通知:故障时通知管理员
3. 故障转移:master宕机时自动选举新master
4. 配置提供:客户端连接哨兵获取master地址
配置:
yaml
spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.1.101:26379
- 192.168.1.102:26379
- 192.168.1.103:26379
password: 123456
7.3 集群模式(Cluster)
架构:
┌──────────────────────────┐
│ Hash Slot (0-16383) │
└───────────┬────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌───▼────┐ ┌────▼───┐ ┌────▼───┐
│Master1 │ │Master2 │ │Master3 │
│Slot │ │Slot │ │Slot │
│0-5460 │ │5461- │ │10923- │
│ │ │10922 │ │16383 │
└───┬────┘ └────┬───┘ └────┬───┘
│ │ │
┌───▼────┐ ┌────▼───┐ ┌────▼───┐
│Slave1 │ │Slave2 │ │Slave3 │
└────────┘ └────────┘ └────────┘
特点:
- 数据分片存储(16384个slot)
- 支持水平扩展
- 自动故障转移
- 去中心化
配置:
yaml
spring:
redis:
cluster:
nodes:
- 192.168.1.101:6379
- 192.168.1.102:6379
- 192.168.1.103:6379
- 192.168.1.104:6379
- 192.168.1.105:6379
- 192.168.1.106:6379
max-redirects: 3
password: 123456
对比:
| 方案 | 数据分片 | 故障转移 | 扩展性 | 复杂度 |
|---|---|---|---|---|
| 主从 | ✗ | 手动 | 低 | 低 |
| 哨兵 | ✗ | 自动 | 低 | 中 |
| 集群 | ✓ | 自动 | 高 | 高 |
八、Redis性能优化
8.1 缓存三大问题
问题1:缓存穿透
定义:查询不存在的数据,缓存和数据库都没有
影响:
- 每次都查数据库
- 数据库压力大
- 可能被恶意攻击
场景:
Client → Redis (miss) → DB (miss) → 返回null
问题:下次还是穿透
解决方案:
方案1:缓存空值
java
public User getUser(Long id) {
String key = "user:" + id;
// 1. 查缓存
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
if ("null".equals(json)) {
return null; // 缓存的空值
}
return JSON.parseObject(json, User.class);
}
// 2. 查数据库
User user = userMapper.selectById(id);
// 3. 缓存结果(包括null)
if (user == null) {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set(key, "null", 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
30, TimeUnit.MINUTES);
}
return user;
}
方案2:布隆过滤器
java
@Configuration
public class BloomFilterConfig {
@Bean
public BloomFilter<Long> userBloomFilter() {
// 预计元素数量100万,误判率0.01%
BloomFilter<Long> filter = BloomFilter.create(
Funnels.longFunnel(),
1000000,
0.0001
);
// 初始化:加载所有用户ID
List<Long> userIds = userMapper.selectAllIds();
userIds.forEach(filter::put);
return filter;
}
}
@Service
public class UserService {
@Autowired
private BloomFilter<Long> userBloomFilter;
public User getUser(Long id) {
// 1. 布隆过滤器判断
if (!userBloomFilter.mightContain(id)) {
return null; // 一定不存在
}
// 2. 查缓存
// 3. 查数据库
// ...
}
}
问题2:缓存击穿
定义:热点key过期,大量请求同时访问
影响:
- 瞬间大量请求打到数据库
- 数据库压力激增
- 可能导致雪崩
场景:
热点商品缓存过期 → 1万并发同时查数据库
问题:数据库扛不住
解决方案:
方案1:互斥锁
java
public User getUser(Long id) {
String key = "user:" + id;
// 1. 查缓存
User user = getFromCache(key);
if (user != null) {
return user;
}
// 2. 缓存未命中,加锁
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
try {
// 尝试获取锁
Boolean getLock = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (getLock) {
// 获取锁成功,查数据库
user = userMapper.selectById(id);
// 写入缓存
setToCache(key, user);
return user;
} else {
// 获取锁失败,等待后重试
Thread.sleep(50);
return getUser(id); // 递归重试
}
} finally {
// 释放锁(Lua脚本保证原子性)
releaseLock(lockKey, lockValue);
}
}
方案2:热点数据永不过期
java
// 逻辑过期:缓存中存储过期时间
public class CacheData {
private Object data;
private LocalDateTime expireTime;
}
public User getUser(Long id) {
String key = "user:" + id;
// 查缓存
CacheData cacheData = getFromCache(key);
if (cacheData == null) {
// 缓存未命中,重建缓存
return rebuildCache(id);
}
// 检查逻辑过期
if (cacheData.getExpireTime().isBefore(LocalDateTime.now())) {
// 过期了,异步重建缓存
threadPool.submit(() -> rebuildCache(id));
}
// 返回旧数据(保证可用性)
return (User) cacheData.getData();
}
问题3:缓存雪崩
定义:大量缓存同时过期,或Redis宕机
影响:
- 大量请求打到数据库
- 数据库崩溃
- 整个系统不可用
场景:
凌晨0点,昨天设置的所有缓存同时过期
解决方案:
方案1:过期时间打散
java
// 不好的做法
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
// 好的做法:加随机值
int randomSeconds = ThreadLocalRandom.current().nextInt(300); // 0-300秒
redisTemplate.opsForValue().set(key, value,
30 * 60 + randomSeconds, TimeUnit.SECONDS);
方案2:Redis高可用
- 主从 + 哨兵
- Redis集群
- 多级缓存(本地缓存 + Redis)
方案3:限流降级
java
@Service
public class UserService {
// 限流:每秒最多1000个请求
@RateLimiter(value = 1000, timeout = 100)
public User getUser(Long id) {
// ...
}
// 降级:Redis挂了,返回默认值
@HystrixCommand(fallbackMethod = "getUserFallback")
public User getUserWithFallback(Long id) {
return getFromRedis(id);
}
public User getUserFallback(Long id) {
return new User(id, "默认用户");
}
}
8.2 性能优化建议
1. 合理使用数据结构
String:简单key-value
Hash:对象存储(节省内存)
List:队列、栈
Set:去重、交并差集
ZSet:排行榜
2. 避免大key
String:不超过10KB
List/Set/ZSet:元素不超过5000
Hash:field不超过1000
大key危害:
- 阻塞其他命令
- 内存碎片
- 主从同步慢
- 过期删除慢
3. 批量操作
java
// 不好:多次网络IO
for (String key : keys) {
redisTemplate.opsForValue().get(key);
}
// 好:一次网络IO
List<Object> values = redisTemplate.opsForValue().multiGet(keys);
// 更好:Pipeline
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
keys.forEach(key -> connection.get(key.getBytes()));
return null;
});
4. 避免使用KEYS命令
bash
# 生产禁用
KEYS user:*
# 使用SCAN
SCAN 0 MATCH user:* COUNT 100
5. 设置合理的过期时间
java
// 根据业务设置
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
// 热点数据可以长一些
redisTemplate.opsForValue().set(hotKey, value, 24, TimeUnit.HOURS);
九、Redis面试高频问题
9.1 基础问题
Q1:Redis为什么这么快?
答:三大原因
1. 纯内存操作:ns级别的访问速度
2. 单线程模型:避免线程切换和锁竞争
3. IO多路复用:epoll机制处理并发连接
补充:
- 高效的数据结构(SDS、跳表等)
- 单线程避免上下文切换
Q2:Redis单线程为什么还能支持高并发?
答:
1. CPU不是瓶颈,内存操作极快
2. IO多路复用处理网络请求
3. 单线程避免了并发开销
类比:
一个人(单线程)在图书馆(内存)找书 vs
一群人(多线程)在仓库(磁盘)找书
Q3:Redis有哪些数据类型?
答:5种基本类型 + 3种特殊类型
基本类型:
1. String:字符串
2. Hash:哈希表
3. List:列表
4. Set:集合
5. ZSet:有序集合
特殊类型:
1. Bitmap:位图
2. HyperLogLog:基数统计
3. Geo:地理位置
9.2 原理问题
Q4:Redis持久化方式有哪些?
答:RDB + AOF
RDB(快照):
- 定时保存内存快照
- 文件小,恢复快
- 可能丢数据
AOF(追加):
- 记录每个写命令
- 数据更安全
- 文件大,恢复慢
推荐:混合持久化
- RDB做全量备份
- AOF做增量备份
Q5:Redis过期键的删除策略?
答:惰性删除 + 定期删除
惰性删除:
- 访问时检查是否过期
- CPU友好,内存不友好
定期删除:
- 定期随机检查
- 每秒10次,每次20个key
兜底:内存淘汰策略
Q6:Redis内存淘汰策略有哪些?
答:8种策略
推荐:allkeys-lru
- 所有key
- 淘汰最少使用的
- 适合通用缓存
其他:
- volatile-lru:只淘汰有过期时间的key
- allkeys-lfu:淘汰访问频率最低的
- noeviction:不淘汰,写入报错
9.3 高可用问题
Q7:Redis集群方案有哪些?
答:主从 + 哨兵 + 集群
主从复制:
- 读写分离
- 手动故障转移
哨兵模式:
- 自动故障转移
- 无数据分片
集群模式:
- 数据分片(16384 slot)
- 水平扩展
- 自动故障转移
Q8:Redis主从同步原理?
答:全量同步 + 增量同步
全量同步(初次):
1. Slave发送SYNC命令
2. Master生成RDB快照
3. Master发送RDB给Slave
4. Slave加载RDB
5. Master发送同步期间的写命令
增量同步(断线重连):
1. Master维护replication buffer
2. Slave发送offset
3. Master发送offset后的命令
9.4 缓存问题
Q9:什么是缓存穿透?如何解决?
答:查询不存在的数据
影响:
- 每次都打数据库
- 可能被攻击
解决:
1. 布隆过滤器(推荐)
2. 缓存空值
3. 参数校验
Q10:什么是缓存击穿?如何解决?
答:热点key过期,大量请求同时访问
影响:
- 瞬间大量请求打数据库
解决:
1. 互斥锁(保证一致性)
2. 热点数据永不过期(保证可用性)
3. 提前异步刷新
Q11:什么是缓存雪崩?如何解决?
答:大量缓存同时过期
影响:
- 数据库瞬间压力暴增
解决:
1. 过期时间打散(加随机值)
2. Redis高可用(集群)
3. 限流降级
4. 多级缓存
9.5 分布式锁问题
Q12:如何实现Redis分布式锁?
java
答:SETNX + 过期时间 + 唯一标识 + Lua脚本
// 获取锁
SET lock:key uuid EX 10 NX
// 释放锁(Lua脚本)
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
要点:
1. NX:不存在才设置
2. EX:防止死锁
3. UUID:防止误删
4. Lua:保证原子性
Q13:Redis分布式锁有什么问题?
答:主要问题和解决方案
问题1:主从同步延迟
- 场景:Master获取锁后宕机,Slave未同步
- 解决:Redlock算法(多个Master)
问题2:锁超时
- 场景:业务执行超过锁超时时间
- 解决:看门狗机制(Redisson)
问题3:不可重入
- 场景:同一线程无法再次获取锁
- 解决:Hash结构记录重入次数
推荐:使用Redisson框架
9.6 实战问题
Q14:如何保证缓存与数据库一致性?
答:先更新数据库,再删除缓存(推荐)
方案对比:
1. 先删缓存,再更新DB:可能不一致
2. 先更新DB,再删缓存:推荐
3. 先更新DB,再更新缓存:浪费性能
延迟双删:
1. 删除缓存
2. 更新数据库
3. 延迟500ms后再删除缓存
最终方案:
- 业务代码:先更新DB,再删缓存
- 兜底:订阅binlog,异步更新缓存
Q15:Redis如何实现延迟队列?
答:使用ZSet
原理:
- score存储执行时间戳
- 定时扫描score < 当前时间的任务
示例:
ZADD delay:queue <timestamp> <task>
ZRANGEBYSCORE delay:queue 0 <now>
ZREM delay:queue <task>
Q16:Redis如何实现限流?
答:多种方案
方案1:计数器(简单)
INCR key
EXPIRE key 1
方案2:滑动窗口(ZSet)
ZADD key <timestamp> <request_id>
ZREMRANGEBYSCORE key 0 <now-60s>
ZCARD key
方案3:令牌桶(Lua脚本)
推荐使用Redisson的RateLimiter