Redis 面试笔记整理
一、Redis 基础知识
1. Redis 概述
Redis 是什么?主要特点有哪些?
- Redis(Remote Dictionary Server)是一个开源的、基于内存的键值存储系统
- 主要特点:
- 高性能:数据存储在内存中,读写速度极快(10万+ QPS)
- 支持多种数据结构:String、Hash、List、Set、ZSet等
- 持久化:支持RDB和AOF两种持久化方式
- 高可用:支持主从复制、哨兵、集群模式
- 原子操作:支持事务和Lua脚本保证原子性
Redis 和 Memcached 的区别是什么?
特性 | Redis | Memcached |
---|---|---|
数据类型 | 支持多种数据结构 | 仅支持简单的key-value |
持久化 | 支持RDB和AOF持久化 | 不支持持久化 |
集群模式 | 原生支持Cluster模式 | 需要客户端实现分布式 |
线程模型 | 单线程(6.0+支持多线程I/O) | 多线程 |
内存管理 | 支持内存淘汰策略 | 固定大小内存,LRU淘汰 |
适用场景 | 缓存、消息队列、计数器等复杂场景 | 简单的键值缓存场景 |
Redis 是单线程还是多线程?为什么单线程还能高效?
- Redis 核心网络模型是单线程的(6.0之前是完全单线程)
- 高效的原因:
- 基于内存操作,没有磁盘I/O瓶颈
- 单线程避免了多线程的上下文切换和竞争条件
- 非阻塞I/O多路复用(epoll/kqueue)
- 精心优化的数据结构(如跳表、哈希表)
Redis 6.0 之后的多线程模型是怎样的?
- Redis 6.0 引入了多线程I/O(默认关闭,需配置)
- 模型特点:
- 主线程仍处理命令执行(保持单线程特性)
- 新增I/O线程(默认4个)负责:
- 网络读:解析客户端请求
- 网络写:返回结果给客户端
- 配置参数:
io-threads 4 # 启用4个I/O线程 io-threads-do-reads yes # 启用读多线程
- 注意:
- 实际命令执行仍是单线程
- 性能提升主要在超高并发网络I/O场景
2. Redis 数据类型
Redis 支持的数据类型及使用场景
String(字符串)
- 存储结构:二进制安全的字符串,最大512MB
- 典型场景:
- 缓存对象(JSON序列化存储)
- 计数器(INCR/DECR命令)
- 分布式锁(SETNX实现)
- 会话管理(Session存储)
- 特殊操作:
SETEX key seconds value # 带过期时间设置
GETSET key new_value # 设置新值返回旧值
MSET/MGET # 批量操作
Hash(哈希)
- 存储结构:field-value映射表
- 典型场景:
- 存储对象属性(用户资料)
- 购物车数据(用户ID+商品ID)
- 配置信息集合
- 优势:
- 比String更节省空间(避免重复key)
- 支持部分字段操作
List(列表)
- 存储结构:双向链表
- 典型场景:
- 消息队列(LPUSH+BRPOP)
- 最新消息排行(朋友圈)
- 数据分页(LRANGE)
- 特点:
LPUSH/RPUSH # 左右插入
BLPOP/BRPOP # 阻塞式弹出
Set(集合)
- 存储结构:无序唯一集合
- 典型场景:
- 好友关系(共同好友SINTER)
- 抽奖活动(SRANDMEMBER)
- 标签系统
- 核心命令:
SADD/SREM # 增删元素
SISMEMBER # 判断存在
SUNION/SDIFF # 并集/差集
ZSet(有序集合)
- 存储结构:跳表+哈希表
- 典型场景:
- 排行榜(ZREVRANGE)
- 延迟队列(时间戳作为score)
- 优先级任务
- 特点:
ZADD key score member # 带分数插入
ZRANGEBYSCORE # 按分数范围查询
Bitmap(位图)
- 存储结构:String的位操作
- 典型场景:
- 用户签到(SETBIT)
- 活跃用户统计
- 布隆过滤器实现
- 示例:
SETBIT sign:2023:user1 1 1 # 第1天签到
BITCOUNT sign:2023:user1 # 签到总数
HyperLogLog(基数统计)
- 存储结构:概率算法
- 典型场景:
- UV统计(去重计数)
- 大规模数据去重
- 特点:
- 固定12KB存储空间
- 误差率约0.81%
PFADD/PFCOUNT # 添加/计数
GEO(地理空间)
- 存储结构:ZSet扩展
- 典型场景:
- 附近的人
- 位置搜索
- 距离计算
- 核心命令:
GEOADD key 经度 纬度 member
GEORADIUS # 半径查询
Stream(消息流)
- 存储结构:持久化消息队列
- 典型场景:
- 消息队列(替代List)
- 事件溯源
- 日志收集
- 特点:
XADD/XREAD # 生产/消费
Consumer Groups # 消费者组支持
底层实现原理
ZSet实现(跳表+哈希表)
- 跳表结构:
- 多层链表,查询复杂度O(logN)
- 支持范围查询
- 哈希表:
- 存储member到score的映射
- O(1)时间复杂度查询单个元素
- 内存优化:
- 元素小于128字节且数量<512时使用ziplist
Hash实现(压缩列表或哈希表)
- ziplist条件:
- 所有field/value长度<64字节
- field数量<512
- hashtable:
- 字典结构(数组+链表)
- 渐进式rehash策略
- 转换阈值:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
3. Redis 持久化
Redis 的持久化机制
Redis 提供两种持久化方式:
- RDB(Redis Database):定时生成内存快照
- AOF(Append Only File):记录所有写操作命令
RDB 和 AOF 的区别与优缺点
特性 | RDB | AOF |
---|---|---|
持久化方式 | 二进制快照 | 命令追加日志 |
文件大小 | 较小(压缩存储) | 较大(持续增长) |
恢复速度 | 快 | 慢(需重放命令) |
数据安全性 | 可能丢失最后一次快照后的数据 | 可配置不同刷盘策略保证数据安全 |
性能影响 | 保存时影响性能 | 写入性能影响较小 |
恢复优先级 | 低 | 高(默认优先使用AOF恢复) |
RDB 触发方式
- 手动触发:
SAVE # 同步保存,阻塞主线程
BGSAVE # 后台异步保存(fork子进程)
- 自动触发(redis.conf配置):
save 900 1 # 900秒内至少1个key变化
save 300 10 # 300秒内至少10个key变化
save 60 10000 # 60秒内至少10000个key变化
AOF 重写机制
作用:
- 压缩AOF文件体积(去除冗余命令)
- 重建更优的持久化命令序列
触发方式:
- 手动触发:
BGREWRITEAOF
- 自动触发(需配置):
- 手动触发:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
- 优化策略:
- 使用AOF重写缓冲区
- 配置合理的重写触发阈值
- 开启AOF-RDB混合模式(4.0+)
生产环境配置建议
选择策略:
- 数据安全性要求高:AOF(appendfsync always)
- 快速恢复优先:RDB
- 折中方案:同时开启RDB+AOF
推荐配置:
appendonly yes # 开启AOF
appendfsync everysec # 折中刷盘策略
aof-use-rdb-preamble yes # 开启混合持久化(4.0+)
save 900 1 # 保留RDB触发条件
- 注意事项:
- 同时开启时重启优先加载AOF文件
- 混合模式结合两者优势(4.0+版本)
- 监控持久化文件大小和性能影响
二、Redis 高可用与集群
1. 主从复制
Redis 主从复制原理
全量同步过程:
- 从节点发送SYNC命令
- 主节点执行BGSAVE生成RDB文件
- 主节点将RDB文件发送给从节点
- 从节点清空数据后加载RDB
- 主节点将缓冲区的写命令发送给从节点
增量同步过程:
- 主节点维护复制积压缓冲区(repl_backlog)
- 从节点断线重连后发送PSYNC命令
- 主节点发送缓冲区中缺失的命令
关键参数:
repl-backlog-size 1mb # 复制积压缓冲区大小
repl-backlog-ttl 3600 # 缓冲区保留时间(秒)
主从复制常见问题
数据延迟:
- 原因:网络延迟、主节点写入量过大
- 监控:INFO replication查看slave_repl_offset
主从切换问题:
- 故障转移时可能丢失部分数据
- 新主节点选举期间服务不可用
其他问题:
- 全量同步导致主节点内存峰值
- 从节点过期key处理不及时
- 网络闪断导致频繁全量同步
主从复制性能优化
- 网络优化:
- 主从节点同机房部署
- 增大复制缓冲区大小:
repl-backlog-size 256mb
配置优化:
- 关闭从节点AOF(除非需要)
- 设置合理的主节点保存间隔:
save 300 100
其他优化:
- 使用SSD磁盘提高RDB传输速度
- 适当增大TCP缓冲区大小
- 监控复制延迟(redis-cli --latency)
特殊场景优化:
- 级联复制减轻主节点压力
- 读写分离时控制从节点数量
2. Redis Sentinel(哨兵)
Redis Sentinel 的作用
核心功能:
- 监控:持续检查主从节点运行状态
- 通知:通过API向管理员发送故障报警
- 自动故障转移:主节点故障时提升从节点为新主节点
- 配置中心:客户端自动获取最新主节点地址
故障转移流程:
- 检测主节点失效(主观下线→客观下线)
- 选举领头Sentinel
- 领头Sentinel执行故障转移:
- 选择最优从节点
- 将其提升为主节点
- 配置其他从节点复制新主节点
- 通知客户端配置变更
故障检测机制
主观下线(SDOWN):
- 单个Sentinel实例检测到主节点无响应
- 判定条件(可配置):
down-after-milliseconds 5000 # 5秒无响应
客观下线(ODOWN):
- 多个Sentinel达成共识认为主节点不可用
- 判定条件:
quorum 2 # 需要至少2个Sentinel同意
Sentinel 选举机制
选举触发条件:
- 主节点被判定为客观下线
- 需要执行故障转移操作
选举规则:
- 基于Raft算法实现
- 每个发现主节点下线的Sentinel都会要求其他Sentinel选举自己为leader
- 先到先得原则(先发送选举请求的优先)
- 获得多数票(>N/2+1)的Sentinel成为leader
选举相关配置:
sentinel monitor mymaster 127.0.0.1 6379 2 # 监控的主节点名称、IP、端口、quorum数
sentinel failover-timeout mymaster 180000 # 故障转移超时时间(毫秒)选举失败处理:
- 若选举超时(failover-timeout)仍未选出leader
- 等待一段时间后重新发起选举
3. Redis Cluster(集群)
Redis Cluster 数据分片
哈希槽分配:
- 整个集群被划分为16384个哈希槽(slot)
- 每个key通过CRC16算法计算后取模:
slot = CRC16(key) % 16384
- 每个节点负责一部分哈希槽范围
槽分配示例:
- 节点A:0-5460
- 节点B:5461-10922
- 节点C:10923-16383
数据分布特性:
- 支持key哈希标签:
{user1000}.profile
和{user1000}.account
会被分配到相同slot - 迁移过程中允许部分key访问
- 支持key哈希标签:
节点通信机制
Gossip协议:
- 节点间通过PING/PONG消息保持通信
- 每个节点随机选择部分节点进行通信(默认每秒10次)
- 传播的信息包括:
- 节点状态
- 哈希槽分配
- 集群配置epoch
通信优化:
cluster-node-timeout 15000 # 节点超时时间(毫秒)
cluster-require-full-coverage yes # 是否需要所有槽被覆盖
集群扩缩容
- 扩容流程:
# 添加新节点
redis-cli --cluster add-node new_host:new_port existing_host:existing_port
# 重新分配槽
redis-cli --cluster reshard existing_host:existing_port
# 输入要迁移的槽数量
# 选择接收这些槽的目标节点ID
# 选择从哪些源节点迁移槽
- 缩容流程:
# 迁移待删除节点的槽到其他节点
redis-cli --cluster reshard del_node_host:del_node_port
# 删除空节点
redis-cli --cluster del-node del_node_host:del_node_port node_id
- 自动平衡:
redis-cli --cluster rebalance --use-empty-masters
请求重定向机制
MOVED重定向:
- 当客户端访问错误的节点时
- 节点返回:
MOVED 1234 10.0.0.2:6379
- 表示key属于槽1234,应由10.0.0.2:6379处理
- 客户端应更新本地slot缓存
ASK重定向:
- 发生在集群迁移过程中
- 返回格式:
ASK 1234 10.0.0.2:6379
- 表示key正在迁移,临时访问目标节点
- 客户端需先发送ASKING命令
智能客户端处理:
- 维护slot与节点的映射关系
- 自动处理重定向请求
- 定期更新集群拓扑信息
三、Redis 缓存问题
1. 缓存穿透、缓存雪崩、缓存击穿
缓存穿透
问题定义:大量请求查询不存在的数据,绕过缓存直接访问数据库
解决方案:
布隆过滤器(Bloom Filter):
- 前置过滤器拦截不存在key的请求
- 优点:内存占用极小
- 缺点:存在误判率(可配置)
空值缓存:
- 对查询结果为null的key也进行缓存
- 设置较短过期时间:
SET key_null "" EX 300
其他方案:
- 接口层增加参数校验
- 用户权限校验前置
缓存雪崩
问题定义:大量缓存同时失效,导致请求直接冲击数据库
解决方案:
过期时间随机化:
- 基础时间 + 随机偏移量
SET key value EX ${1800 + random(300)}
- 基础时间 + 随机偏移量
多级缓存架构:
- 本地缓存(Caffeine) + Redis集群
- 不同层级设置不同过期策略
其他方案:
- 热点数据永不过期(后台异步更新)
- 熔断降级机制(Hystrix/Sentinel)
缓存击穿
问题定义:热点key突然失效,大量并发请求直接访问数据库
解决方案:
- 互斥锁(分布式锁):
- 使用SETNX实现锁机制
if (redis.setnx("lock_key", 1, "EX", 10)) {
try {
// 查询数据库
// 更新缓存
} finally {
redis.del("lock_key")
}
}
热点数据永不过期:
- 不设置过期时间
- 后台定时任务异步更新
其他方案:
- 逻辑过期时间(value中存储过期时间戳)
- 请求合并(将多个并发查询合并为单个查询)
2. 缓存一致性
如何保证缓存与数据库的数据一致性
先更新数据库再删缓存(推荐方案)
操作流程:
- 先更新数据库
- 再删除缓存
- 后续读取时重新加载缓存
优点:
- 实现简单
- 避免大多数并发问题
潜在问题及解决方案:
- 删除缓存失败 → 加入重试机制(消息队列)
- 读请求在删除前加载旧数据 → 设置较短过期时间
延迟双删策略
操作流程:
- 先删除缓存
- 再更新数据库
- 延迟一定时间后再次删除缓存
典型实现:
// 第一次删除
redis.del(key)
// 更新数据库
db.update(data)
// 延迟删除(如500ms后)
Thread.sleep(500)
redis.del(key)
- 适用场景:
- 对一致性要求高的场景
- 能容忍短暂延迟
其他保障措施
版本号机制:
- 在缓存value中加入数据版本号
- 更新时校验版本号
异步监听binlog:
- 通过canal监听数据库变更
- 自动更新/删除缓存
缓存更新策略
Cache Aside(旁路缓存)
读流程:
- 先查缓存,命中则返回
- 未命中则查DB,并写入缓存
写流程:
- 直接更新DB
- 删除对应缓存
Read/Write Through
读流程:
缓存作为主要数据源,自动从DB加载写流程:
先写缓存,由缓存系统负责同步写入DB
Write Behind
- 写流程:
先写缓存,异步批量更新DB - 特点:
高性能但可能丢失数据
3. 缓存淘汰策略
Redis内存淘汰策略
不淘汰:
noeviction
→ 内存满时拒绝写入全局淘汰:
allkeys-lru
→ 最近最少使用
allkeys-lfu
→ 最不经常使用
allkeys-random
→ 随机淘汰有过期时间的key淘汰:
volatile-lru
→ 在过期key中LRU
volatile-lfu
→ 在过期key中LFU
volatile-ttl
→ 淘汰剩余TTL最短的
volatile-random
→ 随机淘汰过期key
策略选择建议
常规场景:
allkeys-lru
(平衡性好)热点数据明显:
allkeys-lfu
(更精准)严格保证数据不过期:
noeviction
+ 监控混合使用场景:
volatile-lru
+ 合理设置TTL配置方式:
maxmemory-policy allkeys-lru
maxmemory 4gb # 设置最大内存
四、Redis 高级特性与优化
1. Redis 事务与 Lua 脚本
Redis 事务(MULTI/EXEC)工作机制
- 基本流程:
- 使用MULTI开始事务
- 将多个命令入队
- 使用EXEC执行所有命令
- 示例:
MULTI
SET key1 value1
INCR key2
EXEC
ACID特性支持:
- 原子性(A):单条命令原子执行,但事务不保证(某条失败不影响其他命令)
- 一致性(C):总能保证数据一致性
- 隔离性(I):单线程模型天然隔离
- 持久性(D):取决于持久化配置
事务特点:
- 不支持回滚(与关系型数据库不同)
- 命令执行期间不会被其他客户端打断
WATCH 命令(乐观锁实现)
- 工作原理:
- 监视一个或多个key
- 如果在EXEC前这些key被修改,则事务失败
- 示例:
WATCH balance
val = GET balance
MULTI
SET balance val-100
EXEC
典型应用场景:
- 账户余额修改
- 库存扣减
- 需要CAS(Compare-And-Swap)的场景
注意事项:
- 需要配合重试机制使用
- 不适用于高竞争场景(可能导致大量重试)
Lua 脚本优势及原子性保证
推荐使用原因:
- 减少网络开销(多个操作合并)
- 避免竞态条件(脚本整体原子执行)
- 复杂操作封装(如分布式锁实现)
- 执行效率高(脚本会被缓存)
原子性保证机制:
- Redis单线程执行Lua脚本
- 脚本执行期间不会处理其他命令
- 示例原子操作:
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
return redis.call('SET', KEYS[1], ARGV[2])
end
- 最佳实践:
- 控制脚本复杂度(避免长时间阻塞)
- 使用SCRIPT LOAD预加载脚本
- 避免在脚本中使用随机写操作
2. Redis 性能优化
Redis Pipeline 机制
基本原理:
- 将多个命令打包一次性发送
- 减少网络往返时间(RTT)
- 服务端按顺序执行并批量返回结果
性能提升方式:
- 典型提升5-10倍吞吐量
- 适合批量写入/读取场景
- 示例实现:
Pipeline p = jedis.pipelined();
for(int i=0; i<10000; i++){
p.set("key"+i, "value"+i);
}
p.sync();
- 注意事项:
- 单次Pipeline不宜包含过多命令
- 不适用于有命令依赖的场景
- 需要合理设置超时时间
BigKey 问题处理
- 排查方法:
- 使用内置命令:
redis-cli --bigkeys
MEMORY USAGE key
- 扫描分析工具:
redis-rdb-tools分析RDB文件
优化方案:
- 大Key拆分:
将hash拆分为多个小hash - 数据压缩:
使用Snappy等算法压缩value - 存储优化:
改用更适合的数据结构
- 大Key拆分:
预防措施:
- 设置监控告警
- 避免存储超大value(>10KB)
- 定期扫描清理
慢查询分析
- 配置与查看:
- 设置阈值(微秒):
slowlog-log-slower-than 10000
- 保留条数:
slowlog-max-len 128
- 查看慢日志:
SLOWLOG GET 10
常见优化方向:
- 避免O(N)命令操作大数据集
- 优化复杂Lua脚本
- 合理使用索引
分析工具:
- redis-faina分析监控日志
- 可视化工具展示慢查询趋势
连接数优化
连接池配置:
- 最大连接数:
maxTotal 50
- 最大空闲连接:
maxIdle 10
- 最小空闲连接:
minIdle 5
- 最大连接数:
优化建议:
- 合理设置超时时间:
timeout 3000
- 使用连接池复用连接
- 避免短连接操作
- 合理设置超时时间:
问题排查:
- 查看当前连接:
CLIENT LIST
- 分析连接来源:
INFO clients
- 紧急断开连接:
CLIENT KILL
- 查看当前连接:
3. Redis 网络与安全
Redis 通信协议 (RESP)
协议特点:
- 二进制安全的文本协议
- 简单易实现
- 支持多种数据类型编码
协议格式:
- 简单字符串: “+OK\r\n”
- 错误: “-Error message\r\n”
- 整数: “:1000\r\n”
- 批量字符串: “$6\r\nfoobar\r\n”
- 数组: “*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n”
工作方式:
- 客户端-服务端基于TCP通信
- 默认端口6379
- 支持管道和批量操作
安全认证配置
密码认证设置:
redis.conf配置
requirepass your_strong_password
客户端认证
AUTH your_strong_password
防护措施:
- 修改默认端口
- 绑定指定IP:
bind 127.0.0.1
- 禁用危险命令:
rename-command FLUSHALL ""
- 启用保护模式:
protected-mode yes
网络安全:
- 使用SSL隧道
- 配置防火墙规则
- 启用ACL(Redis 6.0+)
SCAN vs KEYS 命令
SCAN命令特点:
- 增量式迭代
- 不阻塞服务器
- 可能返回重复key
- 语法:
SCAN cursor [MATCH pattern] [COUNT count]
KEYS命令风险:
- 一次性返回所有匹配key
- 大数据量时会阻塞服务
- 导致性能骤降
生产环境建议:
- 永远禁用KEYS *
- 使用SCAN替代大数据量查询
- 监控脚本示例:
local cursor = "0"
repeat
local reply = redis.call("SCAN", cursor, "MATCH", "user:*")
cursor = reply[1]
-- 处理返回的keys
until cursor == "0"
五、Redis 应用场景
1. 常见业务场景
分布式锁实现
核心方案:
基础实现:
SET lock_key unique_value NX EX 10
- NX: 仅当key不存在时设置
- EX: 设置过期时间(秒)
- unique_value: 唯一标识(通常使用UUID)
优化版(Lua脚本保证原子性):
if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then
return redis.call("EXPIRE", KEYS[1], ARGV[2])
else
return 0
end
- 注意事项:
- 必须设置过期时间
- 解锁需验证value值
- 考虑锁续期问题
限流方案
INCR计数器方案:
- 固定窗口:
local counter = redis.call("INCR", KEYS[1])
if counter == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return counter <= tonumber(ARGV[2])
- 令牌桶算法:
- 使用Hash存储:last_time, tokens
- Lua脚本计算当前可用令牌数
- 补充速率控制
参数示例:
- KEYS[1]: rate_limiter:user1
- ARGV[1]: 时间窗口(如60秒)
- ARGV[2]: 最大请求数(如100)
排行榜实现
ZSet核心操作:
更新分数:
ZADD leaderboard 100 "user1"
获取排名:
ZREVRANGE leaderboard 0 9 WITHSCORES
用户排名:
ZREVRANK leaderboard "user1"
优化技巧:
- 定期持久化到数据库
- 分片存储超大排行榜
- 使用ZUNIONSTORE合并多个榜单
消息队列方案
List基础方案:
生产者:
LPUSH orders "order_data"
消费者:
BRPOP orders 30
**Stream方案(Redis 5.0+):
生产消息:
XADD mystream * field1 value1 field2 value2
消费消息:
XREAD BLOCK 0 STREAMS mystream $
消费者组:
XGROUP CREATE mystream mygroup $
Session共享
实现方案:
存储结构:
SETEX session:sessionid 3600 "user_data"
Spring集成配置:
spring.session.store-type=redis
server.servlet.session.timeout=3600
集群方案:
- 相同TTL配置
- 合理设置序列化方式
秒杀系统实现
库存扣减方案:
- Lua脚本保证原子性:
local stock = tonumber(redis.call("GET", KEYS[1]))
if stock > 0 then
redis.call("DECR", KEYS[1])
return 1
end
return 0
- 优化措施:
- 库存预热
- 分段锁减少竞争
- 异步扣减+结果通知
参数示例:
- KEYS[1]: seckill:stock:123
- ARGV[1]: 无(可扩展传用户ID)
2. 高级应用
布隆过滤器实现
RedisBloom模块:
- 加载模块:
redis-server --loadmodule /path/to/redisbloom.so
- 基本命令:
BF.ADD myfilter item1
BF.EXISTS myfilter item1
- 参数配置:
BF.RESERVE myfilter 0.01 100000
- 加载模块:
原生实现方案:
- 使用Bitmap+多个哈希函数
- 示例SETBIT操作:
SETBIT bloom:filter hash1(item) 1
SETBIT bloom:filter hash2(item) 1
HyperLogLog统计UV
基础命令:
- 添加访问记录:
PFADD uv:20230515 user1 user2 user3
- 获取统计结果:
PFCOUNT uv:20230515
- 合并多日数据:
PFMERGE uv:week uv:day1 uv:day2
- 添加访问记录:
精度说明:
- 标准误差0.81%
- 固定使用12KB内存
- 适合大数据量去重
GEO地理位置
核心命令:
- 添加位置:
GEOADD locations 116.404 39.915 "user1"
- 查询附近的人:
GEORADIUS locations 116.404 39.915 10 km WITHDIST
- 计算距离:
GEODIST locations user1 user2 km
- 添加位置:
实现原理:
- 基于ZSet存储
- 使用Geohash编码
- 有效距离范围:-180到180经度
BitMap签到功能
每日签到:
- 用户签到:
SETBIT sign:user1:202305 15 1
- 查询签到:
GETBIT sign:user1:202305 15
- 统计签到:
BITCOUNT sign:user1:202305
- 用户签到:
连续签到计算:
- 获取位图数据:
GET sign:user1:202305
- 客户端解析连续天数
- 支持多维度统计:
BITOP AND result sign:user1:202305 sign:user1:202306
- 获取位图数据:
六、Redis 与其他技术结合
Spring Boot 集成 Redis
- 基础配置:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=yourpassword
spring.redis.lettuce.pool.max-active=8
- 核心组件:
RedisTemplate
: 提供各种数据结构操作StringRedisTemplate
: 字符串专用模板LettuceConnectionFactory
: 默认连接池(优于Jedis)
- 典型用法:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 操作示例
redisTemplate.opsForValue().set("key", "value");
redisTemplate.opsForHash().put("user", "id", 1001);
Redis + MySQL 配合方案
- 缓存策略:
- Cache Aside Pattern:
- 读: 先查缓存→未命中查DB→回填缓存
- 写: 更新DB→删除缓存
- 双写一致性方案:
- 延迟双删:
- 删除缓存
- 更新数据库
- 延迟500ms再次删除
- 监听binlog变更(通过canal)
Redis 和本地缓存(Caffeine、Guava Cache)如何选择?
选择依据对比
特性 | Redis | Caffeine/Guava Cache |
---|---|---|
数据范围 | 全集群共享 | 仅单机有效 |
性能 | 微秒级(网络IO影响) | 纳秒级(内存访问) |
容量 | 支持GB/TB级别 | 通常MB级别(受JVM堆限制) |
数据结构 | 支持丰富的数据结构 | 简单KV结构 |
持久化 | 支持 | 不支持 |
适用场景 | 分布式缓存、共享数据 | 高频热点数据、临时数据 |
电商分层缓存实战示例
场景:商品详情页缓存
架构设计:
用户请求 → Nginx → 本地缓存(Caffeine)→ Redis集群 → MySQL代码实现:
// 初始化多级缓存
@Bean
public Cache<String, Product> productCache() {
return Caffeine.newBuilder()
.maximumSize(10_000) // 本地缓存1万个商品
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
// 查询逻辑
public Product getProduct(Long id) {
String key = "product:" + id;
// 先查本地缓存
Product product = productCache.getIfPresent(key);
if (product != null) return product;
// 再查Redis
product = redisTemplate.opsForValue().get(key);
if (product == null) {
product = productMapper.selectById(id); // 查数据库
// 异步回填缓存
executor.execute(() -> {
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
productCache.put(key, product);
});
} else {
productCache.put(key, product); // 回填本地缓存
}
return product;
}
优势分析:
- 本地缓存:扛住90%以上的商品查询请求
- Redis层:保证集群各节点的数据一致性
- 数据库:最终数据源,流量削峰达99%
关键配置:
- 本地缓存TTL(5分钟)< Redis TTL(1小时)
- 本地缓存最大条目数根据内存调整
- 使用异步线程回填避免阻塞主流程
选型建议
必须使用Redis的场景
- 需要跨服务共享的数据(如库存)
- 需要持久化的关键数据
- 复杂数据结构需求(如排行榜)
优先本地缓存的场景
- 极端性能要求(秒杀系统)
- 不要求强一致性的数据(商品描述)
- 单机高频访问数据(用户基础信息)
七、Redis 底层实现
Redis 的 SDS(Simple Dynamic String)和 C 字符串区别
特性 | SDS | C 字符串 |
---|---|---|
长度获取 | O(1) 直接读取len属性 | O(n) 需要遍历计算 |
缓冲区安全 | 自动扩容,不会缓冲区溢出 | 容易缓冲区溢出 |
二进制安全 | 可以存储任意二进制数据 | 只能存储文本,遇’\0’终止 |
内存分配 | 预分配+惰性释放策略 | 每次修改需重新分配 |
兼容性 | 末尾保留’\0’兼容C函数 | - |
Redis 字典(Hash 表)实现
核心结构:
- 哈希表数组 + 链表解决冲突
- 包含两个哈希表(ht[0]和ht[1])用于rehash
渐进式rehash过程:
- 扩容时机:负载因子 > 1 且允许rehash,或负载因子 > 5 强制rehash
- 步骤:
- 分配ht[1]空间(大小为第一个大于等于ht[0].used*2的2^n)
- 维护rehashidx计数器(初始为0)
- 每次CRUD操作时迁移ht[0][rehashidx]的键值对
- 全部迁移完成后用ht[1]替换ht[0]
优化特性:
- 单次迁移一个桶(链表)
- 期间查询会同时查两个表
- 定时任务辅助迁移
Redis 跳跃表(Skip List)工作原理
结构特点:
- 多层有序链表(默认最大32层)
- 每个节点包含:
- 成员对象(ele)
- 分值(score)
- 后退指针(BW)
- 层数组(level[]包含前进指针和跨度)
查找过程:
- 从最高层开始遍历
- 当前节点分值 < 目标分值 → 继续前进
- 当前节点分值 ≥ 目标分值 → 下降一层
- 时间复杂度:O(logN)
插入流程:
- 随机确定节点层数(幂次定律)
- 逐层更新前后节点指针
- 更新跨度信息
Redis 过期键删除策略
惰性删除:
- 触发时机:访问键时检查过期时间
- 优点:CPU友好
- 缺点:内存不及时释放
定期删除:
- 工作流程:
- 随机抽取20个键检查
- 删除其中已过期的键
- 如果超过25%键过期则重复过程
- 配置参数:
hz 10 # 每秒执行次数
- 工作流程:
内存淘汰触发删除:
- 当内存不足时,按策略淘汰键
- 相关配置:
maxmemory-policy volatile-lru