
redis复盘
redis是基于内存的 键值对nosql数据库,c语言编写,单线程模型
高性能,读写快,支持多数据类型,持久化,主从集群,事务,lua脚本
单线程模型:内存操作+IO多路复用+避免线程切换开销
IO多路复用:基于epoll/kqueue实现
基础命令
#启动
redis-server [配置文件]
#客户端连接
redis-cli -h 地址 -p 端口 -a 密码
#停止
shutdown
常用命令
#查看全部key 遍历,效率低
keys *
#key是否存在
exists key
#删除key
del key
#设置key过期时间,单位秒
expire key
#查看剩余过期时间-1永久 -2已过期
ttl key
#移除过期时间
persist key
#查看key对应的数据类型
type key
#切换数据库,集群仅支持db0
select db [0-15]
#重命名key
rename key
#移动key到指定db
move key
#替代keys 渐进式遍历key 避免阻塞
scan
# scan 用法(游标式遍历,count仅为提示,非精确值)
scan 0 match user* count 100
数据类型
| 数据类型 | 底层实现 | 典型应用场景 | 核心常用命令 |
|---|---|---|---|
| String 字符串 | 动态字符串 SDS,最大容量 512MB | 普通数据缓存、计数器、分布式 ID、限流 | set/get/mset/mget(设置 / 获取 / 批量)incr/incrby/decr(自增自减,原子操作)append(追加字符串)strlen(获取长度)setnx(不存在则设置,分布式锁基础)setex(设置值+过期时间,原子操作)getset(先获取再设置,适合计数器重置) |
| List 列表 | 双向链表,有序、可重复 | 消息队列、时间线、栈 / 队列实现 | lpush/rpush(左右追加)lpop/rpop(左右弹出)lrange(范围查询)llen(获取长度)blpop/brpop(阻塞弹出,避免队列空轮询)ltrim(裁剪列表,保留指定范围) |
| Hash 哈希 | 数组 + 链表(哈希表) | 用户信息、商品信息等对象存储 | hset/hget/hmset/hmget``hgetall(获取所有字段值)hkeys/hvals(获取所有键 / 值)hlen(获取字段数量) |
| Set 集合 | 哈希表,无序、元素唯一 | 数据去重、交集 / 并集 / 差集运算(共同好友、共同关注) | sadd(添加元素)smembers(获取所有元素)sismember(判断元素是否存在)sinter/sunion/sdiff(交 / 并 / 差集) |
| ZSet 有序集合 | 跳表 + 哈希表,元素唯一、按分值排序 | 排行榜、延时任务、权重排序 | zadd(添加带分值元素)zrange/zrevrange(正序 / 倒序范围查询)zscore(获取元素分值)zrank(获取元素排名)zremrangebyrank/zremrangebyscore(按排名 / 分值删除)、zincrby(分值自增,适合排行榜加分); |
| Bitmap 位图 | 本质是 String,按位操作 | 签到打卡、用户状态标记、日活统计 | setbit(设置位值)getbit(获取位值)bitcount(统计 1 的个数) |
| HyperLogLog 基数统计 | 概率数据结构,误差率≈0.81%,极度省内存 | 页面 UV 统计、独立访客统计 | pfadd(添加元素)pfcount(统计基数)pfmerge(合并多个 HyperLogLog) |
| GeoHash 地理位置 | 基于 GeoHash 编码的有序集合 | 附近门店、附近的人、距离计算 | geoadd(添加地理位置)georadius(查询指定范围内的位置) |
| 通用运维 | info(查看 Redis 状态,如info memory/info stats/info replication)、config get/set(动态修改配置)、slowlog(慢查询日志,排查慢命令) |
java整合redis
redisTemplate
- springboot自动配置RedisAutoConfiguration,默认提供RedisTemplate,StringRedisTemplate
- 默认JDK序列化,乱码,可读性差
- 一般自定义序列化器:Jackson2JsonRedisSerializer,统一用Json序列化,key为String
常用API
// 操作String
redisTemplate.opsForValue().set/get()
// 操作Hash
redisTemplate.opsForHash().put/get()
// 操作List/Set/ZSet 对应 opsForList/opsForSet/opsForZSet
Spring Cache
| 注解 | 核心作用 | 缓存一致性模式 |
|---|---|---|
@Cacheable | 触发缓存写入操作,将方法返回值存入缓存 | 读模式(先查缓存,无则查库并回写) |
@CacheEvict | 触发缓存删除操作,根据 key 删除指定缓存 | 失效模式(更新数据库后删除缓存) |
@CachePut | 触发缓存更新操作,每次执行方法后同步更新缓存 | 双写模式(更新数据库后同步写缓存) |
@Caching | 组合多个缓存操作,支持同时进行增删改 | 复合模式 |
@CacheConfig | 在类级别统一配置缓存公共属性 | 全局配置 |
SpEL常用表达式
// 引用方法参数:#id / #user.id
@Cacheable(value = "user", key = "#id")
// 引用返回值:#result.id(仅@CachePut/@CacheEvict可用)
@CacheEvict(value = "user", key = "#result.id")
// 组合key:拼接多个参数
@Cacheable(value = "order", key = "#userId + ':' + #orderType")
1. @Cacheable(value, key, sync)
作用:加在查询方法上,将方法返回结果存入缓存。下次调用时若缓存命中,直接返回缓存数据,不再执行方法体。
核心参数:
value:缓存分区名(必填),用于逻辑隔离不同业务的缓存key:缓存键名(必填),支持 SpEL 表达式sync:是否开启本地锁(布尔值),**开启后可解决缓存击穿问题
// 查询一级分类,缓存到catalog分区,key为catelogLevel1
@Cacheable(value = "catalog", key = "'catelogLevel1'", sync = true)
public List<CategoryEntity> getLevel1Categories() {
return categoryMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
2. @CacheEvict(value, key, allEntries)
作用:加在更新 / 删除方法上,执行方法后删除指定缓存,保证数据一致性(失效模式)。
核心参数:
value:缓存分区名(必填)key:要删除的缓存键名allEntries:是否删除整个分区的所有缓存(布尔值),设为true时无需指定key
示例:
// 删除单个缓存
@CacheEvict(value = "catalog", key = "'catelogLevel1'")
public void updateCategory(CategoryEntity category) {
categoryMapper.updateById(category);
}
// 删除整个catalog分区的所有缓存
@CacheEvict(value = "catalog", allEntries = true)
public void deleteCategory(Long id) {
categoryMapper.deleteById(id);
}
3. @CachePut(value, key)
作用:加在更新方法上,每次都会执行方法体,并将返回结果同步更新到缓存中(双写模式)。
注意:方法必须有返回值,否则无法更新缓存。
示例:
@CachePut(value = "user", key = "#user.id")
public UserEntity updateUser(UserEntity user) {
userMapper.updateById(user);
return user; // 返回更新后的对象,写入缓存
}
4. @Caching
作用:组合多个缓存操作,适用于一个方法需要同时操作多个不同缓存的场景。
示例:
// 同时删除两个不同的缓存键
@Caching(evict = {
@CacheEvict(value = "catalog", key = "'catelogLevel1'"),
@CacheEvict(value = "catalog", key = "'catelogjson'")
})
public void saveOrUpdateCategory(CategoryEntity category) {
// 业务逻辑
}
5. @CacheConfig
作用:加在类上,统一配置该类中所有缓存注解的公共属性,避免重复代码。
示例:
@RestController
@RequestMapping("/category")
@CacheConfig(cacheNames = "catalog") // 统一指定缓存分区为catalog
public class CategoryController {
// 此处无需再写value = "catalog"
@Cacheable(key = "'catelogLevel1'")
public List<CategoryEntity> getLevel1Categories() {
// 业务逻辑
}
}
spring cache 默认不设置过期时间,需要加配置
@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 Jackson2JsonRedisSerializer<>(Object.class)));
// 针对不同分区设置不同过期时间
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("catalog", config.entryTtl(Duration.ofHours(1)));
configMap.put("user", config.entryTtl(Duration.ofDays(1)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configMap)
.build();
}
注意事项
⚠️ SpEL 表达式中使用纯字符串作为 key 时,必须再包一层单引号
- ✅ 正确写法:
key = "'catelogLevel1'" - ❌ 错误写法:
key = "catelogLevel1"(会被解析为变量名而非字符串)
⚠️ 注解基于 AOP 实现,同类内部方法调用会导致注解失效
缓存三大问题
缓存穿透
现象:查询DB不存在的数据,未命中缓存,请求直达数据库
解决:
- 布隆过滤器:前置过滤非法Key
- 空值缓存,缓存空值并设置短过期时间
- 接口层校验:参数合法性拦截
缓存击穿
现象:热点接口过期期间瞬时,大量请求直连数据库
解决:
- 互斥锁(分布式锁)
- 逻辑过期(不设过期时间,后台异步更新缓存,请求直接返回旧数据
缓存雪崩
现象:大量key同时过期或者redis宕机,请求直连数据库
解决:
- 错开过期时间,避免同时失效
- redis集群
- 服务层熔断限流降级
- 多级缓存(本地缓存+redis)
缓存更新策略
- 读,先查缓存->无缓存->查库->回写缓存
- 写,先更新数据库->再删缓存
锁机制
本地锁
sync,reentrantlock,单机可用,分布式环境下无效
分布式锁
首选redisson
- 基于lua脚本,保证原子性
- 核心特性:
- 可重入锁(同一线程可多次加锁)
- 看门狗机制(未释放自动续签避免过期)
- 可重试,防误删,支持公平锁
- redlock红锁:多节点部署,解决 Redis 单点故障,提升锁可靠性(有业界争议,了解即可)
事务和lua脚本
事务
multi开启->批量命令->exec执行/discard放弃
- 命令入队,执行阶段生效
- 不保证原子性,单命令报错后续仍然执行
- 无回滚机制
lua脚本【推荐】
多条命令原子执行,替代事务
场景:分布式锁、限流、批量操作、复杂原子逻辑
redis执行lua脚本命令:eval
内存淘汰策略,持久化,集群
一.内存淘汰策略
配置文件中maxmemory,生产环境必须配置
若不配置maxmemory,redis会使用所有可用内存,若触发oom则会被kill掉
内存淘汰策略类型
| 类别 | 名称 | 核心逻辑 | 适用场景 |
|---|---|---|---|
| 不淘汰 | noeviction | 拒绝所有新的写请求,返回oom错误 | 绝不允许丢数据场景,极少 |
| 所有key中淘汰 | allkeys-lru | 淘汰最近最少使用的key | 绝大多数场景 |
| 所有key中淘汰 | alllkeys-lfu | 淘汰访问频率最低的key | 热点数据明显的场景,如电商,商品 |
| 所有key中淘汰 | allkeys-random | 随机淘汰key | 极少用 |
| 过期key中淘汰 | volatile-lru | 设置了过期时间key中,最近最少使用 | 同时存在永久和临时数据的场景 |
| 过期key中淘汰 | volatile-lfu | 设置了过期时间key中,访问频率最低 | 同上,热点区分明显 |
| 过期key中淘汰 | volatile-random | 设置了过期时间key中,随机淘汰 | 同上,访问频率均匀 |
| 过期key中淘汰 | volatile-ttl | 设置了过期时间key中,最早过期的 | 有明确过期优先级场景 |
LRU(最近最少使用)
- 传统LRU,双向链表,每次访问key移到表头,删除表尾(内存开销大)
- redis近似LRU:随机采样5个key(maxmemory-samples),淘汰其中最久未使用的
- 优点:实现简单
- 缺点:无法区别 偶尔访问一次,和高频访问key
LFU(访问频率最低)
- 为每一个key维护一个访问计数器和最后访问时间,淘汰计数器小的key
- 计数器会随时间衰减,避免历史热点永不淘汰
- 优点:比LRU更精准
- 缺点:需要额外维护计数器
使用建议
- 大部分情况用allkeys-lru
- 电商场景 热点区分明显用lfu
- 不要用默认的noeviction,占满会OOM
- 合理设置maxmemory:默认5,可设置10,越高越接近真实lru,性能略降
衍生:大Key和热Key
大Key
-
定义:key过大(> 1MB)或元素过多
-
危害:网络卡顿,内存溢出,集群迁移阻塞,慢查询
-
解决方案:
- 拆分大Key,拆成多个小Hash
- 精简数据:只存储必要字段
- 异步清理:unlink代替del,后台异步删除大Key
-
排查:
-
redis-cli --bigkeys # 扫描大Key redis-cli -p 6379 info memory | grep used_memory # 查看内存使用
-
热Key
- 定义:被超高并发访问的key
- 危害:单节点cpu打满,集群负载不匀
- 解决方案:
- 本地缓存,本地使用caffeine等,减少redis缓存
- key分片,一个热key复制为多个副本,分散到不同节点
- 集群负载均衡,避免热key在单个节点
- 排查:
- 结合
info stats(查看 key 命中数)、第三方工具(如 Redis Insight、阿里云 Redis 控制台)。
- 结合
二.持久化
RDB【快照持久化】
原理:
定期将redis内存中的全量数据生成一个二进制快照文件(默认名dump.rdb),保存在磁盘上
触发方式:
-
自动触发
-
在配置文件中设置save规则,如
-
save 900 1 # 900秒内至少有1个key发生变化,就生成快照 save 300 10 # 300秒内至少有10个key发生变化,就生成快照 save 60 10000 # 60秒内至少有10000个key发生变化,就生成快照
-
-
手动触发
- bgsave:后台异步生成快照,不阻塞redis主线程
- save:前台同步生成快照,会阻塞主线程
执行流程:
- redis主进程fork出一个子进程
- 子进程将内存数据写入临时rdb文件
- 完成写入后,将临时替换旧rdb文件
- 主进程继续处理客户端请求
优点
- 文件紧凑,体积小,适合备份和恢复
- 恢复速度快
- 性能影响小
缺点
- 数据丢失风险大
- 大内存实例fork子进程耗时较长,可能导致毫秒级卡顿
AOF【追加日志持久化】
原理:
记录redis每一条写指令,以文本形式追加到aof文件(默认appendonly.aof)重启时会重新执行aof所有命令
开启方式:
appendonly yes # 开启AOF持久化
appendfilename "appendonly.aof" # AOF文件名
刷盘策略
appendfsync
| 策略 | 配置值 | 刷盘时机 | 数据安全性 | 性能 |
|---|---|---|---|---|
| 总是 | always | 写一条刷一条 | 高 | 差 |
| 每秒【默认】 | everysec | 每秒刷一次 | 中,最多丢一秒数据 | 中 |
| 系统控制刷盘 | no | 操作系统决定何时刷盘 | 最低,可能丢大量 | 高 |
重写机制
时间推移,AOF会越来越大,需要定期重写,压缩AOF文件
原理:分析当前内存中数据,生成新的aof文件,只包含恢复数据需要的最小命令
触发方式:
- 自动触发:配置auto-aof-rewrite-percentage,auto-aof-rewrite-min-size
- 手动触发:执行
BGREWRITEAOF命令。
优点
- 数据安全性高,默认配置下最多丢失1秒数据
- aof是文本格式,可读性好
缺点
- 文件体积大
- 恢复速度慢
- 对性能有一定影响
混合持久化【建议使用】
原理
结合了RDB和AOF的优点:在AOF重写时,将当前内存中的数据以RDB格式写入AOF文件开头,然后将后续的写命令以AOF格式追加到文件末尾
触发方式
aof-use-rdb-preamble yes # 开启混合持久化
优点
- 恢复速度快
- 数据安全性高
- 文件体积小
缺点
-
redis4.0+ 支持
-
可读性差
持久化文件校验
- RDB:
redis-check-dump dump.rdb检查rdb完整性 - AOF:
redis-check-aof --fix appendonly.aof修复损坏的aof文件
持久化性能优化
- RDB:避免在高峰期触发bgsave
- AOF:关闭
appendfsync always用everysec;定义手动触发BGREWRITEAOF
三.集群
主从同步
架构
一个主节点,带多个从节点,主负责写,从负责读,数据单向主同步到从
作用
- 读写分离,提高吞吐量
- 备份
- 数据转移基础,主节点挂了,从节点提升为主节点
同步流程
全量同步【初次复制】
- 从节点向主节点发送PSYNC命令
- 主节点执行bgsave生成rdb快照
- 主节点将rdb发送给从节点
- 从节点根据rdb文件同步数据
- 主节点将执行期间的命令发送给从节点,补充间隔命令
增量同步【后续复制】
- 主节点维护一个复制偏移量和复制缓冲区
- 从节点断开后,向主节点发送自己的复制偏移量
- 如果偏移量在缓冲区内->主节点只发送偏移量之后的命令
- 不在缓冲区->全量复制
缺点
- 无自动故障转移,主节点宕机需要手动把从节点提升
- 主从延迟:高并发下数据不一致
- 写能力有限,写操作瓶颈仍然在主节点
哨兵模式【高可用,解决主从同步无法自动故障转移问题】
架构
在主从复制的基础上,增加哨兵(至少3个,奇数),用来监控主从节点状态,实现自动故障转移
三大职责
- 监控:哨兵节点不断检查主从节点是否正常运行
- 通知:某个节点出现故障时,通知其他哨兵和客户端
- 自动故障转移:主节点宕机时,哨兵选举一个从节点作为新的主节点,并将其他节点挂载到主节点
核心概念
- 主观下线:单个哨兵认为该节点故障
- 客观下线:半数以上哨兵认为该节点故障,确认真正宕机
执行流程
- 多个哨兵发现主节点主观下线,投票确认客观下线。
- 哨兵之间选举一个领导者,负责执行故障转移
- 领导者从 从节点中选举一个新的新的主节点(优先级高,复制偏移量大,运行ID小)
- 领导者向其他从节点发送命令,挂载到新的主节点上
- 领导者把原来的主节点标记为从节点,等恢复后自动挂载到新的主节点
优点
实现了自动故障转移,解决了主从复制的单故障问题
缺点
写能力仍然有限,存储能力有限
分片集群【多主多从,数据分片】
架构
无中心节点,多个主从节点组成(至少3主3从),每个节点负责一部分数据,从节点是主节点的副本
核心原理
数据分片
- 将所有数据分为16384个hash槽
- 每个主节点负责一部分的槽位,例如节点A负责0-5460,B负责5461-10922,C负责10923-16383
- 数据路由规划:CRC16(key) % 16384,计算出key对应的槽位,路由到对应节点
节点通信
节点之间使用Gossip协议进行通信,互相交换状态信息
每个节点都会维护整个集群的槽位分配信息,客户端可以连接任意节点,获取集群信息后路由到正确节点
核心特性
- 动态扩容/缩容:可以随时增加/删除节点,槽位自动迁移,但是不会自动均匀分配
- 删除节点,会把该节点数据分配到另一个节点
redis-cli --cluster del-node - 新增节点,不会自动分配节点
redis-cli --cluster add-node - 需要手动执行
redis-cli --cluster reshard 任意节点IP:端口
- 删除节点,会把该节点数据分配到另一个节点
- 自动转移故障:主节点宕机后,从节点自动升级为主节点
- 读写分离:可以将读请求路由到从节点,提高读性能
- 检查集群健康
redis-cli --cluster check
限制
不支持多key跨槽操作:例如mget mset,事务等
不支持数据库:只能使用db0
批量操作受限:只能对同一槽位的key操作
使用建议
建议3主3从起步,每个主最多带2个从
均匀分配槽位
使用支持redis cluster的客户端,如 JedisCluster、Lettuce)。
定期备份数据,监控集群状态,槽位分布。
脑裂问题
定义:主从/哨兵之间网络断了,在断网期间,旧主仍然以为自己是主,而另一边从已经选举了新的主节点,导致恢复后旧主数据错乱 丢失
引发问题:
- 双主同时写入:覆盖数据,丢失
- 网络恢复后
- 旧主变成从,被迫降级,全量同步,本地数据被flush清空,脑裂期间写入数据全部消失
解决方案:
-
主从/哨兵模式
# 主至少有1个正常从才允许写 min-slaves-to-write 1 # 从延迟超过10秒,认为不健康 min-slaves-max-lag 10 -
集群模式
- 至少3主3从,奇数节点
- 开启
cluster-require-full-coverage yes(槽必须全覆盖) - 设置节点超时
cluster-node-timeout 15000
监控指标
- 槽位命中率(100%为正常)
- 主从同步延迟(info replication master_repl_offset)
- 节点内存使用率,CPU使用率
常见故障排查
| 故障方向 | 排查方向 |
|---|---|
| Redis连接超时 | 防火墙,maxclient设置,节点是否宕机,集群槽位是否正常 |
| 内存占用飙升 | 是否有大Key,过期策略是否失效,持久化阻塞,慢命令(slowlog get) |
| 主从同步失败 | 主从密码不一致,网络不同,RDB/AOF文件损坏,复制缓冲区不足(repl-backlog-size) |
| 集群槽位丢失 | 节点宕机未自动故障转移,手动迁移槽位未完成,Gossip协议通信异常 |
| 命令执行慢 | 大key操作,复杂lua脚本,cpu密集型操作,持久化fork阻塞 |
配置优化(redis.conf)
# 基础优化
daemonize yes # 后台运行
pidfile /var/run/redis_6379.pid
port 6379
bind 0.0.0.0 # 生产建议仅绑定内网IP
protected-mode yes # 保护模式
requirepass yourpassword # 密码(生产必须设置)
maxclients 10000 # 最大连接数(根据服务器配置)
maxmemory 8GB # 内存上限(根据服务器配置)
maxmemory-policy allkeys-lru # 内存淘汰策略
# 持久化优化
save 3600 1 300 100 60 10000 # 降低快照频率,减少fork次数
rdbcompression yes # RDB压缩(节省磁盘,轻微CPU开销)
aof-use-rdb-preamble yes # 开启混合持久化
auto-aof-rewrite-percentage 100 # AOF增长100%触发重写
auto-aof-rewrite-min-size 64mb # AOF最小重写大小
# 集群优化
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000 # 节点超时时间
备份和恢复
rdb文件:定时拷贝dump.rdb
恢复:停止redis,替换rdb/aof文件,启动redis(优先加载aof)
跨环境同步:redis-cli -pipe 批量导入/导出数据
Redis新版本特性
6.0+特性
-
多线程IO:仅针对网络IO,命令仍然单线程,通过
io-threads配置 -
ACL权限控制:细粒度控制用户对key/命令的权限
-
RESP3协议:支持更多数据类型
7.0+特性
-
hash可设置单个字段过期时间
-
函数和触发器:
- 直接在redis运行js/ts代码
- 事件触发:key变更,写入,过期
- 用途:数据校验,自动归档,实时聚合
8.0+特性
-
支持JSON类型
jsonsetjsongetjsonarrappend- 支持jsonpath,嵌套修改,性能比老模块高50%,内存省90%
-
(Vector Set)向量数据类型,支持相似度搜索
- 用做推荐,搜索,RAG知识库
-
概率数据结构(内置)
- 布隆过滤器,布谷鸟过滤器,Count-min sketch,Top-K
- 直接命令:BF.ADDredis是基于内存的 键值对nosql数据库,c语言编写,单线程模型
高性能,读写快,支持多数据类型,持久化,主从集群,事务,lua脚本
单线程模型:内存操作+IO多路复用+避免线程切换开销
IO多路复用:基于epoll/kqueue实现
基础命令
#启动
redis-server [配置文件]
#客户端连接
redis-cli -h 地址 -p 端口 -a 密码
#停止
shutdown
常用命令
#查看全部key 遍历,效率低
keys *
#key是否存在
exists key
#删除key
del key
#设置key过期时间,单位秒
expire key
#查看剩余过期时间-1永久 -2已过期
ttl key
#移除过期时间
persist key
#查看key对应的数据类型
type key
#切换数据库,集群仅支持db0
select db [0-15]
#重命名key
rename key
#移动key到指定db
move key
#替代keys 渐进式遍历key 避免阻塞
scan
# scan 用法(游标式遍历,count仅为提示,非精确值)
scan 0 match user* count 100
数据类型
| 数据类型 | 底层实现 | 典型应用场景 | 核心常用命令 |
|---|---|---|---|
| String 字符串 | 动态字符串 SDS,最大容量 512MB | 普通数据缓存、计数器、分布式 ID、限流 | set/get/mset/mget(设置 / 获取 / 批量)incr/incrby/decr(自增自减,原子操作)append(追加字符串)strlen(获取长度)setnx(不存在则设置,分布式锁基础)setex(设置值+过期时间,原子操作)getset(先获取再设置,适合计数器重置) |
| List 列表 | 双向链表,有序、可重复 | 消息队列、时间线、栈 / 队列实现 | lpush/rpush(左右追加)lpop/rpop(左右弹出)lrange(范围查询)llen(获取长度)blpop/brpop(阻塞弹出,避免队列空轮询)ltrim(裁剪列表,保留指定范围) |
| Hash 哈希 | 数组 + 链表(哈希表) | 用户信息、商品信息等对象存储 | hset/hget/hmset/hmget``hgetall(获取所有字段值)hkeys/hvals(获取所有键 / 值)hlen(获取字段数量) |
| Set 集合 | 哈希表,无序、元素唯一 | 数据去重、交集 / 并集 / 差集运算(共同好友、共同关注) | sadd(添加元素)smembers(获取所有元素)sismember(判断元素是否存在)sinter/sunion/sdiff(交 / 并 / 差集) |
| ZSet 有序集合 | 跳表 + 哈希表,元素唯一、按分值排序 | 排行榜、延时任务、权重排序 | zadd(添加带分值元素)zrange/zrevrange(正序 / 倒序范围查询)zscore(获取元素分值)zrank(获取元素排名)zremrangebyrank/zremrangebyscore(按排名 / 分值删除)、zincrby(分值自增,适合排行榜加分); |
| Bitmap 位图 | 本质是 String,按位操作 | 签到打卡、用户状态标记、日活统计 | setbit(设置位值)getbit(获取位值)bitcount(统计 1 的个数) |
| HyperLogLog 基数统计 | 概率数据结构,误差率≈0.81%,极度省内存 | 页面 UV 统计、独立访客统计 | pfadd(添加元素)pfcount(统计基数)pfmerge(合并多个 HyperLogLog) |
| GeoHash 地理位置 | 基于 GeoHash 编码的有序集合 | 附近门店、附近的人、距离计算 | geoadd(添加地理位置)georadius(查询指定范围内的位置) |
| 通用运维 | info(查看 Redis 状态,如info memory/info stats/info replication)、config get/set(动态修改配置)、slowlog(慢查询日志,排查慢命令) |
java整合redis
redisTemplate
-
springboot自动配置RedisAutoConfiguration,默认提供RedisTemplate,StringRedisTemplate
-
默认JDK序列化,乱码,可读性差
-
一般自定义序列化器:Jackson2JsonRedisSerializer,统一用Json序列化,key为String
常用API
// 操作String
redisTemplate.opsForValue().set/get()
// 操作Hash
redisTemplate.opsForHash().put/get()
// 操作List/Set/ZSet 对应 opsForList/opsForSet/opsForZSet
Spring Cache
| 注解 | 核心作用 | 缓存一致性模式 |
|---|---|---|
@Cacheable | 触发缓存写入操作,将方法返回值存入缓存 | 读模式(先查缓存,无则查库并回写) |
@CacheEvict | 触发缓存删除操作,根据 key 删除指定缓存 | 失效模式(更新数据库后删除缓存) |
@CachePut | 触发缓存更新操作,每次执行方法后同步更新缓存 | 双写模式(更新数据库后同步写缓存) |
@Caching | 组合多个缓存操作,支持同时进行增删改 | 复合模式 |
@CacheConfig | 在类级别统一配置缓存公共属性 | 全局配置 |
SpEL常用表达式
// 引用方法参数:#id / #user.id
@Cacheable(value = "user", key = "#id")
// 引用返回值:#result.id(仅@CachePut/@CacheEvict可用)
@CacheEvict(value = "user", key = "#result.id")
// 组合key:拼接多个参数
@Cacheable(value = "order", key = "#userId + ':' + #orderType")
1. @Cacheable(value, key, sync)
作用:加在查询方法上,将方法返回结果存入缓存。下次调用时若缓存命中,直接返回缓存数据,不再执行方法体。
核心参数:
-
value:缓存分区名(必填),用于逻辑隔离不同业务的缓存 -
key:缓存键名(必填),支持 SpEL 表达式 -
sync:是否开启本地锁(布尔值),**开启后可解决缓存击穿问题
// 查询一级分类,缓存到catalog分区,key为catelogLevel1
@Cacheable(value = "catalog", key = "'catelogLevel1'", sync = true)
public List<CategoryEntity> getLevel1Categories() {
return categoryMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
2. @CacheEvict(value, key, allEntries)
作用:加在更新 / 删除方法上,执行方法后删除指定缓存,保证数据一致性(失效模式)。
核心参数:
-
value:缓存分区名(必填) -
key:要删除的缓存键名 -
allEntries:是否删除整个分区的所有缓存(布尔值),设为true时无需指定key
示例:
// 删除单个缓存
@CacheEvict(value = "catalog", key = "'catelogLevel1'")
public void updateCategory(CategoryEntity category) {
categoryMapper.updateById(category);
}
// 删除整个catalog分区的所有缓存
@CacheEvict(value = "catalog", allEntries = true)
public void deleteCategory(Long id) {
categoryMapper.deleteById(id);
}
3. @CachePut(value, key)
作用:加在更新方法上,每次都会执行方法体,并将返回结果同步更新到缓存中(双写模式)。
注意:方法必须有返回值,否则无法更新缓存。
示例:
@CachePut(value = "user", key = "#user.id")
public UserEntity updateUser(UserEntity user) {
userMapper.updateById(user);
return user; // 返回更新后的对象,写入缓存
}
4. @Caching
作用:组合多个缓存操作,适用于一个方法需要同时操作多个不同缓存的场景。
示例:
// 同时删除两个不同的缓存键
@Caching(evict = {
@CacheEvict(value = "catalog", key = "'catelogLevel1'"),
@CacheEvict(value = "catalog", key = "'catelogjson'")
})
public void saveOrUpdateCategory(CategoryEntity category) {
// 业务逻辑
}
5. @CacheConfig
作用:加在类上,统一配置该类中所有缓存注解的公共属性,避免重复代码。
示例:
@RestController @RequestMapping("/category") @CacheConfig(cacheNames = "catalog") // 统一指定缓存分区为catalog public class CategoryController {
// 此处无需再写value = "catalog"
@Cacheable(key = "'catelogLevel1'")
public List<CategoryEntity> getLevel1Categories() {
// 业务逻辑
}
}
spring cache 默认不设置过期时间,需要加配置
@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 Jackson2JsonRedisSerializer<>(Object.class))); // 针对不同分区设置不同过期时间 Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put("catalog", config.entryTtl(Duration.ofHours(1))); configMap.put("user", config.entryTtl(Duration.ofDays(1))); return RedisCacheManager.builder(factory) .cacheDefaults(config) .withInitialCacheConfigurations(configMap) .build(); }
注意事项
⚠️ SpEL 表达式中使用纯字符串作为 key 时,必须再包一层单引号
-
✅ 正确写法:
key = "'catelogLevel1'" -
❌ 错误写法:
key = "catelogLevel1"(会被解析为变量名而非字符串)
⚠️ 注解基于 AOP 实现,同类内部方法调用会导致注解失效
缓存三大问题
缓存穿透
现象:查询DB不存在的数据,未命中缓存,请求直达数据库
解决:
-
布隆过滤器:前置过滤非法Key
-
空值缓存,缓存空值并设置短过期时间
-
接口层校验:参数合法性拦截
缓存击穿
现象:热点接口过期期间瞬时,大量请求直连数据库
解决:
-
互斥锁(分布式锁)
-
逻辑过期(不设过期时间,后台异步更新缓存,请求直接返回旧数据
缓存雪崩
现象:大量key同时过期或者redis宕机,请求直连数据库
解决:
-
错开过期时间,避免同时失效
-
redis集群
-
服务层熔断限流降级
-
多级缓存(本地缓存+redis)
缓存更新策略
-
读,先查缓存->无缓存->查库->回写缓存
-
写,先更新数据库->再删缓存
锁机制
本地锁
sync,reentrantlock,单机可用,分布式环境下无效
分布式锁
首选redisson
-
基于lua脚本,保证原子性
-
核心特性:
-
可重入锁(同一线程可多次加锁)
-
看门狗机制(未释放自动续签避免过期)
-
可重试,防误删,支持公平锁
-
redlock红锁:多节点部署,解决 Redis 单点故障,提升锁可靠性(有业界争议,了解即可)
-
事务和lua脚本
事务
multi开启->批量命令->exec执行/discard放弃
-
命令入队,执行阶段生效
-
不保证原子性,单命令报错后续仍然执行
-
无回滚机制
lua脚本【推荐】
多条命令原子执行,替代事务
场景:分布式锁、限流、批量操作、复杂原子逻辑
redis执行lua脚本命令:eval
内存淘汰策略,持久化,集群
一.内存淘汰策略
配置文件中maxmemory,生产环境必须配置
若不配置maxmemory,redis会使用所有可用内存,若触发oom则会被kill掉
内存淘汰策略类型
| 类别 | 名称 | 核心逻辑 | 适用场景 |
|---|---|---|---|
| 不淘汰 | noeviction | 拒绝所有新的写请求,返回oom错误 | 绝不允许丢数据场景,极少 |
| 所有key中淘汰 | allkeys-lru | 淘汰最近最少使用的key | 绝大多数场景 |
| 所有key中淘汰 | alllkeys-lfu | 淘汰访问频率最低的key | 热点数据明显的场景,如电商,商品 |
| 所有key中淘汰 | allkeys-random | 随机淘汰key | 极少用 |
| 过期key中淘汰 | volatile-lru | 设置了过期时间key中,最近最少使用 | 同时存在永久和临时数据的场景 |
| 过期key中淘汰 | volatile-lfu | 设置了过期时间key中,访问频率最低 | 同上,热点区分明显 |
| 过期key中淘汰 | volatile-random | 设置了过期时间key中,随机淘汰 | 同上,访问频率均匀 |
| 过期key中淘汰 | volatile-ttl | 设置了过期时间key中,最早过期的 | 有明确过期优先级场景 |
LRU(最近最少使用)
-
传统LRU,双向链表,每次访问key移到表头,删除表尾(内存开销大)
-
redis近似LRU:随机采样5个key(maxmemory-samples),淘汰其中最久未使用的
-
优点:实现简单
-
缺点:无法区别 偶尔访问一次,和高频访问key
LFU(访问频率最低)
-
为每一个key维护一个访问计数器和最后访问时间,淘汰计数器小的key
-
计数器会随时间衰减,避免历史热点永不淘汰
-
优点:比LRU更精准
-
缺点:需要额外维护计数器
使用建议
-
大部分情况用allkeys-lru
-
电商场景 热点区分明显用lfu
-
不要用默认的noeviction,占满会OOM
-
合理设置maxmemory:默认5,可设置10,越高越接近真实lru,性能略降
衍生:大Key和热Key
大Key
-
定义:key过大(> 1MB)或元素过多
-
危害:网络卡顿,内存溢出,集群迁移阻塞,慢查询
-
解决方案:
-
拆分大Key,拆成多个小Hash
-
精简数据:只存储必要字段
-
异步清理:unlink代替del,后台异步删除大Key
-
-
排查:
- redis-cli --bigkeys # 扫描大Key redis-cli -p 6379 info memory | grep used_memory # 查看内存使用
热Key
-
定义:被超高并发访问的key
-
危害:单节点cpu打满,集群负载不匀
-
解决方案:
-
本地缓存,本地使用caffeine等,减少redis缓存
-
key分片,一个热key复制为多个副本,分散到不同节点
-
集群负载均衡,避免热key在单个节点
-
-
排查:
- 结合
info stats(查看 key 命中数)、第三方工具(如 Redis Insight、阿里云 Redis 控制台)。
- 结合
二.持久化
RDB【快照持久化】
原理:
定期将redis内存中的全量数据生成一个二进制快照文件(默认名dump.rdb),保存在磁盘上
触发方式:
-
自动触发
-
在配置文件中设置save规则,如
-
save 900 1 # 900秒内至少有1个key发生变化,就生成快照 save 300 10 # 300秒内至少有10个key发生变化,就生成快照 save 60 10000 # 60秒内至少有10000个key发生变化,就生成快照
-
-
手动触发
-
bgsave:后台异步生成快照,不阻塞redis主线程
-
save:前台同步生成快照,会阻塞主线程
-
执行流程:
-
redis主进程fork出一个子进程
-
子进程将内存数据写入临时rdb文件
-
完成写入后,将临时替换旧rdb文件
-
主进程继续处理客户端请求
优点
-
文件紧凑,体积小,适合备份和恢复
-
恢复速度快
-
性能影响小
缺点
-
数据丢失风险大
-
大内存实例fork子进程耗时较长,可能导致毫秒级卡顿
AOF【追加日志持久化】
原理:
记录redis每一条写指令,以文本形式追加到aof文件(默认appendonly.aof)重启时会重新执行aof所有命令
开启方式:
appendonly yes # 开启AOF持久化 appendfilename "appendonly.aof" # AOF文件名
刷盘策略
appendfsync
| 策略 | 配置值 | 刷盘时机 | 数据安全性 | 性能 |
|---|---|---|---|---|
| 总是 | always | 写一条刷一条 | 高 | 差 |
| 每秒【默认】 | everysec | 每秒刷一次 | 中,最多丢一秒数据 | 中 |
| 系统控制刷盘 | no | 操作系统决定何时刷盘 | 最低,可能丢大量 | 高 |
重写机制
时间推移,AOF会越来越大,需要定期重写,压缩AOF文件
原理:分析当前内存中数据,生成新的aof文件,只包含恢复数据需要的最小命令
触发方式:
-
自动触发:配置auto-aof-rewrite-percentage,auto-aof-rewrite-min-size
-
手动触发:执行
BGREWRITEAOF命令。
优点
-
数据安全性高,默认配置下最多丢失1秒数据
-
aof是文本格式,可读性好
缺点
-
文件体积大
-
恢复速度慢
-
对性能有一定影响
混合持久化【建议使用】
原理
结合了RDB和AOF的优点:在AOF重写时,将当前内存中的数据以RDB格式写入AOF文件开头,然后将后续的写命令以AOF格式追加到文件末尾
触发方式
aof-use-rdb-preamble yes # 开启混合持久化
优点
-
恢复速度快
-
数据安全性高
-
文件体积小
缺点
-
redis4.0+ 支持
-
可读性差
持久化文件校验
-
RDB:
redis-check-dump dump.rdb检查rdb完整性 -
AOF:
redis-check-aof --fix appendonly.aof修复损坏的aof文件
持久化性能优化
-
RDB:避免在高峰期触发bgsave
-
AOF:关闭
appendfsync always用everysec;定义手动触发BGREWRITEAOF
三.集群
主从同步
架构
一个主节点,带多个从节点,主负责写,从负责读,数据单向主同步到从
作用
-
读写分离,提高吞吐量
-
备份
-
数据转移基础,主节点挂了,从节点提升为主节点
同步流程
全量同步【初次复制】
-
从节点向主节点发送PSYNC命令
-
主节点执行bgsave生成rdb快照
-
主节点将rdb发送给从节点
-
从节点根据rdb文件同步数据
-
主节点将执行期间的命令发送给从节点,补充间隔命令
增量同步【后续复制】
-
主节点维护一个复制偏移量和复制缓冲区
-
从节点断开后,向主节点发送自己的复制偏移量
-
如果偏移量在缓冲区内->主节点只发送偏移量之后的命令
-
不在缓冲区->全量复制
缺点
-
无自动故障转移,主节点宕机需要手动把从节点提升
-
主从延迟:高并发下数据不一致
-
写能力有限,写操作瓶颈仍然在主节点
哨兵模式【高可用,解决主从同步无法自动故障转移问题】
架构
在主从复制的基础上,增加哨兵(至少3个,奇数),用来监控主从节点状态,实现自动故障转移
三大职责
-
监控:哨兵节点不断检查主从节点是否正常运行
-
通知:某个节点出现故障时,通知其他哨兵和客户端
-
自动故障转移:主节点宕机时,哨兵选举一个从节点作为新的主节点,并将其他节点挂载到主节点
核心概念
-
主观下线:单个哨兵认为该节点故障
-
客观下线:半数以上哨兵认为该节点故障,确认真正宕机
执行流程
-
多个哨兵发现主节点主观下线,投票确认客观下线。
-
哨兵之间选举一个领导者,负责执行故障转移
-
领导者从 从节点中选举一个新的新的主节点(优先级高,复制偏移量大,运行ID小)
-
领导者向其他从节点发送命令,挂载到新的主节点上
-
领导者把原来的主节点标记为从节点,等恢复后自动挂载到新的主节点
优点
实现了自动故障转移,解决了主从复制的单故障问题
缺点
写能力仍然有限,存储能力有限
分片集群【多主多从,数据分片】
架构
无中心节点,多个主从节点组成(至少3主3从),每个节点负责一部分数据,从节点是主节点的副本
核心原理
数据分片
-
将所有数据分为16384个hash槽
-
每个主节点负责一部分的槽位,例如节点A负责0-5460,B负责5461-10922,C负责10923-16383
-
数据路由规划:CRC16(key) % 16384,计算出key对应的槽位,路由到对应节点
节点通信
节点之间使用Gossip协议进行通信,互相交换状态信息
每个节点都会维护整个集群的槽位分配信息,客户端可以连接任意节点,获取集群信息后路由到正确节点
核心特性
-
动态扩容/缩容:可以随时增加/删除节点,槽位自动迁移,但是不会自动均匀分配
-
删除节点,会把该节点数据分配到另一个节点
redis-cli --cluster del-node -
新增节点,不会自动分配节点
redis-cli --cluster add-node -
需要手动执行
redis-cli --cluster reshard 任意节点IP:端口
-
-
自动转移故障:主节点宕机后,从节点自动升级为主节点
-
读写分离:可以将读请求路由到从节点,提高读性能
-
检查集群健康
redis-cli --cluster check
限制
不支持多key跨槽操作:例如mget mset,事务等
不支持数据库:只能使用db0
批量操作受限:只能对同一槽位的key操作
使用建议
建议3主3从起步,每个主最多带2个从
均匀分配槽位
使用支持redis cluster的客户端,如 JedisCluster、Lettuce)。
定期备份数据,监控集群状态,槽位分布。
脑裂问题
定义:主从/哨兵之间网络断了,在断网期间,旧主仍然以为自己是主,而另一边从已经选举了新的主节点,导致恢复后旧主数据错乱 丢失
引发问题:
-
双主同时写入:覆盖数据,丢失
-
网络恢复后
- 旧主变成从,被迫降级,全量同步,本地数据被flush清空,脑裂期间写入数据全部消失
解决方案:
-
主从/哨兵模式
# 主至少有1个正常从才允许写 min-slaves-to-write 1
从延迟超过10秒,认为不健康
min-slaves-max-lag 10
-
集群模式
-
至少3主3从,奇数节点
-
开启
cluster-require-full-coverage yes(槽必须全覆盖) -
设置节点超时
cluster-node-timeout 15000
-
监控指标
-
槽位命中率(100%为正常)
-
主从同步延迟(info replication master_repl_offset)
-
节点内存使用率,CPU使用率
常见故障排查
| 故障方向 | 排查方向 |
|---|---|
| Redis连接超时 | 防火墙,maxclient设置,节点是否宕机,集群槽位是否正常 |
| 内存占用飙升 | 是否有大Key,过期策略是否失效,持久化阻塞,慢命令(slowlog get) |
| 主从同步失败 | 主从密码不一致,网络不同,RDB/AOF文件损坏,复制缓冲区不足(repl-backlog-size) |
| 集群槽位丢失 | 节点宕机未自动故障转移,手动迁移槽位未完成,Gossip协议通信异常 |
| 命令执行慢 | 大key操作,复杂lua脚本,cpu密集型操作,持久化fork阻塞 |
配置优化(redis.conf)
# 基础优化 daemonize yes # 后台运行 pidfile /var/run/redis_6379.pid port 6379 bind 0.0.0.0 # 生产建议仅绑定内网IP protected-mode yes # 保护模式 requirepass yourpassword # 密码(生产必须设置) maxclients 10000 # 最大连接数(根据服务器配置) maxmemory 8GB # 内存上限(根据服务器配置) maxmemory-policy allkeys-lru # 内存淘汰策略
持久化优化
save 3600 1 300 100 60 10000 # 降低快照频率,减少fork次数 rdbcompression yes # RDB压缩(节省磁盘,轻微CPU开销) aof-use-rdb-preamble yes # 开启混合持久化 auto-aof-rewrite-percentage 100 # AOF增长100%触发重写 auto-aof-rewrite-min-size 64mb # AOF最小重写大小
集群优化
cluster-enabled yes cluster-config-file nodes-6379.conf cluster-node-timeout 15000 # 节点超时时间
备份和恢复
rdb文件:定时拷贝dump.rdb
恢复:停止redis,替换rdb/aof文件,启动redis(优先加载aof)
跨环境同步:redis-cli -pipe 批量导入/导出数据
Redis新版本特性
6.0+特性
-
多线程IO:仅针对网络IO,命令仍然单线程,通过
io-threads配置 -
ACL权限控制:细粒度控制用户对key/命令的权限
-
RESP3协议:支持更多数据类型
7.0+特性
-
hash可设置单个字段过期时间
-
函数和触发器:
-
直接在redis运行js/ts代码
-
事件触发:key变更,写入,过期
-
用途:数据校验,自动归档,实时聚合
-
8.0+特性
-
支持JSON类型
-
jsonsetjsongetjsonarrappend -
支持jsonpath,嵌套修改,性能比老模块高50%,内存省90%
-
-
(Vector Set)向量数据类型,支持相似度搜索
- 用做推荐,搜索,RAG知识库
-
概率数据结构(内置)
-
布隆过滤器,布谷鸟过滤器,Count-min sketch,Top-K
-
直接命令:BF.ADD
-