Redis 核心实践指南:从数据结构到生产实战
前言
Redis 凭借其卓越的性能和丰富的数据结构,已经成为现代后端架构中不可或缺的一环。它既是性能加速器,也是解决复杂业务问题的利器。
本文将从实际开发角度出发,系统性地介绍 Redis 的核心数据结构、高级特性以及生产环境中常见问题的解决方案。
一、Redis 是什么?
根据 Redis 官方文档的定义:
Redis 是一个开源(BSD许可)的内存数据结构存储,可用作数据库、缓存、消息代理和流引擎。它提供字符串、哈希、列表、集合、带范围查询的有序集合、位图、HyperLogLog、地理空间索引和流等数据结构。
简单来说,Redis 是一个跑在内存里的"数据工具箱",它不仅能存数据,还能对数据进行各种高效操作。
核心能力一览
| 能力 | 说明 |
|---|---|
| 内存存储 | 数据主要在内存中,读写速度极快 |
| 数据结构丰富 | 不仅仅是 Key-Value,还有 List、Hash、Set 等 |
| 原子操作 | 所有操作都是原子的,支持复杂的数据操作 |
| 持久化 | 支持 RDB 快照和 AOF 日志两种持久化方式 |
| 高可用 | 支持主从复制、Sentinel 哨兵、Cluster 集群 |
二、快速部署
Docker 单机部署
docker run -itd -p 6379:6379 --restart always --name redis \
redis --requirepass "your_password"
Docker Compose 部署(推荐生产使用)
version: '3'
services:
redis:
image: redis:7.0
container_name: redis
volumes:
- ./data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf
- ./logs:/logs
command: redis-server /usr/local/etc/redis/redis.conf --requirepass your_password
ports:
- "6379:6379"
restart: always
常用运维命令
# 进入 Redis CLI
docker exec -it redis redis-cli -a your_password
# 查看所有 key(生产环境慎用)
KEYS *
# 查看 key 类型
TYPE key_name
# 设置过期时间(秒)
EXPIRE key 60
# 查看剩余过期时间(-1 表示永不过期,-2 表示不存在)
TTL key
# 删除 key
DEL key
# 清空当前库
FLUSHDB
# 清空所有库
FLUSHALL
# 批量删除指定前缀的 key
redis-cli -a password KEYS "prefix_*" | xargs redis-cli -a password DEL
三、核心数据结构
3.1 String(字符串)
底层实现:
- 整数值且可用 long 表示 → int 编码
- 字符串长度 ≤ 39 字节 → embstr 编码
- 字符串长度 > 39 字节 → raw 编码(SDS)
基本操作:
# 基础读写
SET hello world
GET hello
# 批量操作
MSET a 10 b 20 c 30
MGET a b c
# 带过期时间的写入(重要!)
SET key value EX 10 # 10秒后过期
SET key value PX 10000 # 10000毫秒后过期
# 条件写入(实现分布式锁的关键)
SET key value NX # 仅当 key 不存在时设置
SET key value XX # 仅当 key 存在时设置
# 组合使用:实现锁的最佳实践
SET lock_key unique_value NX EX 10
# 数值操作
INCR counter
DECR counter
INCRBY counter 100
# 判断存在
EXISTS key
应用场景:
- 缓存热点数据(用户信息、配置等)
- 分布式锁(SET NX EX 组合)
- 计数器(访问量、点赞数)
- 分布式 ID 生成
3.2 List(列表)
特点:有序、可重复、双端操作高效
底层实现:元素长度 < 64 字节且数量 < 512 时使用 ziplist,否则使用 linkedlist
# 插入
LPUSH list a b c # 左侧插入
RPUSH list x y z # 右侧插入
# 查询
LRANGE list 0 -1 # 获取全部
LRANGE list 0 10 # 分页查询
# 弹出(取出并删除)
LPOP list # 左侧弹出
RPOP list # 右侧弹出
# 阻塞弹出(实现消息队列的关键)
BLPOP list 5 # 阻塞5秒,没有元素则超时返回
BRPOP list 0 # 永久阻塞,直到有元素
# 长度
LLEN list
应用场景:
- 消息队列:LPUSH + BRPOP 实现生产者-消费者模式
- 分页数据:LRANGE 天然支持分页
- 最新消息/时间线:如微博最新动态
- 栈/队列:LPUSH/LPOP 实现栈,RPUSH/LPOP 实现队列
3.3 Hash(哈希)
特点:适合存储对象,支持对单个字段的操作
底层实现:字段长度 < 64 字节且数量 < 512 时使用 ziplist,否则使用 hashtable
# 设置字段
HSET user:1001 name "张三" age 25
HSET user:1001 email "zhangsan@example.com"
# 获取字段
HGET user:1001 name
HGETALL user:1001 # 获取全部(慎用,数据量大时性能差)
# 批量操作
HMSET user:1002 name "李四" age 30
HMGET user:1002 name age
# 数值操作
HINCRBY user:1001 age 1
# 判断字段存在
HEXISTS user:1001 name
# 删除字段
HDEL user:1001 email
应用场景:
- 对象存储:相比 String 存储 JSON,Hash 支持单独更新字段,减少网络传输
- 购物车:key=用户ID,field=商品ID,value=商品数量
- 计数器组:同一用户的多个计数指标
3.4 Set(集合)
特点:无序、不可重复、支持集合运算
底层实现:元素都是整数且数量 ≤ 512 时使用 intset,否则使用 hashtable
# 添加
SADD fruits apple banana orange
# 查询
SMEMBERS fruits # 获取全部(慎用)
SISMEMBER fruits apple # 判断存在
# 删除
SREM fruits apple
# 集合运算
SINTER set1 set2 # 交集
SUNION set1 set2 # 并集
SDIFF set1 set2 # 差集(set1 有但 set2 没有)
# 随机操作
SRANDMEMBER set 2 # 随机获取2个元素
SPOP set # 随机弹出并删除
# 数量
SCARD set
应用场景:
- 标签系统:用户标签、文章标签
- 社交关系:共同好友、关注/粉丝集合
- 抽奖/去重:随机抽取不重复用户
- 黑白名单:快速判断存在性
3.5 Sorted Set(有序集合)
特点:唯一成员 + 可排序分数,Redis 中最强大的数据结构之一
底层实现:元素数量 < 128 且成员长度 < 64 字节时使用 ziplist,否则使用 skiplist
# 添加(分数在前,成员在后)
ZADD leaderboard 100 "player1" 95 "player2" 88 "player3"
# 查询
ZRANGE leaderboard 0 -1 WITHSCORES # 升序
ZREVRANGE leaderboard 0 -1 WITHSCORES # 降序
# 按分数范围查询
ZRANGEBYSCORE leaderboard 90 100 # 90-100分
# 按排名查询
ZRANK leaderboard "player1" # 获取排名(0-based)
ZREVRANK leaderboard "player1" # 获取反向排名
# 获取分数
ZSCORE leaderboard "player1"
# 增加分数
ZINCRBY leaderboard 10 "player1"
# 删除
ZREM leaderboard "player3"
# 获取数量
ZCARD leaderboard
ZCOUNT leaderboard 90 100 # 分数区间内数量
应用场景:
- 排行榜:游戏积分榜、热搜榜、销量榜
- 带权重的队列:分数越高优先级越高
- 延迟队列:用时间戳作为分数
- 范围查询:地理位置距离、时间范围
3.6 HyperLogLog(基数统计)
特点:极省内存(每个 key 约 12KB),统计近似基数,误差率约 0.81%
# 添加元素
PFADD uv:2024-01-01 user1 user2 user3
# 统计基数
PFCOUNT uv:2024-01-01
# 合并多个 HyperLogLog
PFMERGE uv:2024-01 uv:2024-01-01 uv:2024-01-02
应用场景:
- UV 统计:网站独立访客数
- 去重计数:搜索关键词去重统计
- 大规模去重场景:牺牲一定精度换取内存
3.7 GEO(地理空间)
特点:存储地理位置信息并支持距离计算
# 添加地理位置(经度 纬度 名称)
GEOADD cities 116.405285 39.912835 "北京" 121.473701 31.230416 "上海"
# 获取位置
GEOPOS cities 北京 上海
# 计算距离(单位:m/km/mi/ft)
GEODIST cities 北京 上海 km
# 半径查询(以北京为中心,半径1000km内的城市)
GEORADIUS cities 116.405285 39.912835 1000 km WITHDIST
应用场景:
- 附近的人/店铺
- 打车/外卖距离计算
- 地图相关功能
3.8 Bitmap(位图)
特点:按位存储,极致节省空间,1个字节可存8个状态
# 设置某一位
SETBIT sign:202401 0 1 # 第0位设为1(第1天签到)
SETBIT sign:202401 1 1 # 第1位设为1(第2天签到)
# 获取某一位
GETBIT sign:202401 0
# 统计1的个数
BITCOUNT sign:202401
# 位运算
BITOP AND result key1 key2
应用场景:
- 签到系统:365天只需 46 字节
- 在线状态:用户 ID 作为偏移量
- 布隆过滤器:底层实现
四、高级特性
4.1 布隆过滤器(Bloom Filter)
概述:Bloom Filter 是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能在集合中。
核心原理:
- 一个二进制向量(初始全为 0)
- 多个 Hash 函数
- 添加元素时,将多个 Hash 值对应的位设为 1
- 查询时,如果任意一个对应位为 0,则元素一定不存在;如果全为 1,则元素可能存在
特点:
- ✅ 极省内存
- ✅ 查询速度快
- ❌ 存在误判率(可能把不存在判断为存在)
- ❌ 不支持删除
使用 RedisBloom 插件:
# 添加元素
BF.ADD bloom_key element
# 判断存在
BF.EXISTS bloom_key element
# 批量操作
BF.MADD bloom_key e1 e2 e3
BF.MEXISTS bloom_key e1 e2 e3
Java 使用 Redisson:
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setPassword("password");
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phone_blacklist");
// 预计元素 5 亿,误差率 3%
bloomFilter.tryInit(500000000L, 0.03);
bloomFilter.add("13800138000");
System.out.println(bloomFilter.contains("13800138000")); // true
应用场景:
- 缓存穿透防护:将存在的数据 ID 放入布隆过滤器,不存在直接拦截
- 黑名单校验:IP 黑名单、手机号黑名单
- 爬虫去重:URL 去重
4.2 内存淘汰策略
当 Redis 内存达到上限时,需要按照配置的策略淘汰数据。
| 策略 | 说明 |
|---|---|
noeviction |
不淘汰,写入操作报错(默认) |
allkeys-lru |
所有 key 中淘汰最近最少使用的 |
allkeys-random |
所有 key 中随机淘汰 |
volatile-lru |
设置了过期时间的 key 中淘汰 LRU |
volatile-random |
设置了过期时间的 key 中随机淘汰 |
volatile-ttl |
设置了过期时间的 key 中淘汰 TTL 最小的 |
allkeys-lfu |
(4.0+)所有 key 中淘汰最不经常使用的 |
volatile-lfu |
(4.0+)设置了过期时间的 key 中淘汰 LFU |
配置方式:maxmemory-policy allkeys-lru
五、生产常见问题与解决方案
5.1 缓存雪崩
现象:大量缓存同时失效,请求直接打到数据库,导致数据库压力骤增甚至宕机。
原因:
- 大量 key 设置了相同的过期时间
- Redis 实例宕机
解决方案:
| 方案 | 说明 |
|---|---|
| 过期时间打散 | 在基础时间上加随机偏移量 |
| 热点数据永不过期 | 通过后台任务异步更新 |
| 高可用部署 | 主从+哨兵/Cluster 集群 |
| 限流降级 | 使用 Sentinel/Hystrix 做熔断限流 |
// 过期时间打散示例
int baseExpire = 3600;
int randomOffset = new Random().nextInt(300); // 0-300秒随机
int finalExpire = baseExpire + randomOffset;
redisTemplate.expire(key, finalExpire, TimeUnit.SECONDS);
5.2 缓存穿透
现象:请求的数据在缓存和数据库中都不存在,每次请求都穿透到数据库。
解决方案:
| 方案 | 说明 |
|---|---|
| 参数校验 | 在接口层拦截无效参数(如 id ≤ 0) |
| 缓存空对象 | 将不存在的 key 也缓存,设置较短过期时间 |
| 布隆过滤器 | 提前将可能存在的数据放入 Bloom Filter |
// 缓存空对象方案
User user = dao.getUserById(id);
if (user == null) {
redisTemplate.opsForValue().set(cacheKey, "NULL", 60, TimeUnit.SECONDS);
return null;
}
5.3 缓存击穿
现象:某个热点 key 失效的瞬间,大量并发请求同时打到数据库。
解决方案:
| 方案 | 说明 |
|---|---|
| 热点数据永不过期 | 不设置过期时间,由后台任务主动更新 |
| 互斥锁 | 只允许一个线程去加载数据 |
// 互斥锁方案
public User getHotUser(Long id) {
String cacheKey = "user:" + id;
User user = redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
String lockKey = "lock:user:" + id;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 加载数据库
user = dao.getUserById(id);
redisTemplate.opsForValue().set(cacheKey, user, 3600, TimeUnit.SECONDS);
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 等待重试
Thread.sleep(100);
return getHotUser(id);
}
return user;
}
5.4 缓存与数据库双写一致性
问题:更新数据库后,如何保证缓存数据的一致性?
常见方案:
| 方案 | 适用场景 | 说明 |
|---|---|---|
| 先更新数据库,再删除缓存 | 大部分场景(推荐) | 最简洁,并发下有短暂不一致 |
| 先删除缓存,再更新数据库 | 能容忍短暂不一致 | 可能被旧数据回填 |
| 延迟双删 | 对一致性要求较高 | 更新后休眠再删一次 |
| 订阅 Binlog(Canal) | 强一致性要求 | 通过监听数据库变更同步缓存 |
// 推荐方案:先更新 DB,再删缓存
@Transactional
public void updateUser(User user) {
userDao.updateById(user);
redisTemplate.delete("user:" + user.getId());
}
5.5 热 Key / 大 Key 问题
热 Key:某个 key 被超高频率访问。
大 Key:key 对应的 value 过大(String 超过 10KB,集合元素超过 5000)。
| 问题类型 | 危害 | 解决方案 |
|---|---|---|
| 热 Key | 单节点 CPU 飙高 | 本地缓存、读写分离、多副本 |
| 大 Key | 网络阻塞、慢查询 | 拆分、压缩、换数据结构 |
大 Key 排查命令:
# 查看各 key 的内存占用
redis-cli --bigkeys
# 查看某个 key 的大小
MEMORY USAGE key_name
DEBUG OBJECT key_name
六、最佳实践总结
6.1 命名规范
[业务名]:[模块名]:[实体]:[ID]:[属性]
# 示例
order:pay:status:12345
user:profile:1001:basic
6.2 Key 设计原则
- ✅ 语义清晰,层级分明
- ✅ 控制长度,不要超过 1KB
- ✅ 设置合理的过期时间
- ❌ 避免使用 KEYS 命令
- ❌ 避免存储大 Value
- ❌ 避免使用 FLUSHALL/FLUSHDB
6.3 性能优化要点
- 使用 Pipeline:批量操作减少 RTT
- 使用连接池:避免频繁创建连接
- 合理设置过期时间:避免内存无限增长
- 选择合适的结构:Hash 代替 String 存储对象
- 读写分离:主写从读
七、总结
Redis 的精髓在于:用最合适的数据结构,解决最恰当的业务问题。
| 场景 | 推荐结构 |
|---|---|
| 缓存对象 | String(JSON)或 Hash |
| 计数器 | String(INCR) |
| 排行榜 | Sorted Set |
| 消息队列 | List(LPUSH + BRPOP) |
| 标签/好友 | Set |
| UV 统计 | HyperLogLog |
| 附近的人 | GEO |
| 签到 | Bitmap |
| 防缓存穿透 | Bloom Filter |
掌握 Redis 不仅仅是会用几个命令,更重要的是理解每个数据结构的底层特性和适用边界,这样才能在合适的场景做出最优的选择。