Redis
基础
redis是什么?
一个用c语言实现的非关系型的键值对数据库,数据存储在内存上。
redis和MEmcached区别
共同点:都是基于内存
区别:
- MEmcached只有key-value的数据结构
- redis有持久化机制,MEmcached没有,重启或者挂掉数据就没了
- redis原生就支持集群
- redis有发布订阅、lua脚本、事务,MEmcached没有
redis的优点
本质上就三点:高性能、高灵活、高可用
高性能
内存读写速度快
高灵活
多种数据结构:
Hash存储用户信息,键值对形式,可以单个字段的更新
Zset实现排行榜,支持范围查询和排序
List还能充当简单的MQ
高可用
一、完整的持久化机制
1.RDB快照:定期生成内存数据的全量快照
2.AOF:记录每一条写操作的指令,支持每秒每次操作同步
详细见持久化
二、集群、主从复制、哨兵
读写策略
旁路缓存
读多写少
以DB的数据为准
写
先更新DB,然后删除cache
读
先从cache中读,有就返回,没有就DB中返回,然后数据放到cache中
缺陷
- 首次请求肯定不在cache里->热点数据提前放
写频繁会导致cache频繁删除,命中率变很低
- 强一致场景:更新db的时候同时更新cache,必须加(分布式)锁保证cache里面没有线程不安全问题
- 短期可以不一致:更新db的时候同时更新cache,给缓存一个短的过期时间
读写穿透
意思在于:应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互
侧重cache
写
先查cache
- cache中不存在,直接更新DB
- cache中存在,先更新cache,然后cache自己更新DB
读
查cache
- cache中存在,直接返回
- cache中不存在,先通过缓存组件从DB加载,写到cache里再返回
思考:特点是由缓存组件和数据库打交道,而不是应用程序和缓存打交道。
异步缓存写入
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。
但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
一致性策略
线程1来写,线程2来读
先删缓存再更新数据库
问题:线程1删除缓存然后更新Mysql中间,网络延迟,此时线程2查询发现缓存没有去Mysql查,查到旧数据,更新到redis中,
最后结果是redis一直是旧数据,Mysql一直是新数据
写的过程的一半出现的问题
解决:延迟双删,也就是说线程1删除缓存更新数据库再删除缓存,实现了最终一致性
注意延迟时间,根据业务找到线程2查询+更新redis这个过程的耗时,在这之后延迟删除
先更新数据库再删缓存
问题:线程2查询数据,redis的数据过期了,去Mysql查,查数据之后更新redis之前,线程1做完了更新Mysql+删除缓存,线程2才更新到redis数据,
最后结果是redis一直是旧数据,Mysql一直是新数据
概率很小很小,因为 更新Mysql+删除redis<读Mysql+写入redis 不太可能
读的过程的一半出现的问题
解决:延时双删,更数据库-->删除缓存-->延迟删除缓存
删除失败情况
增加重试机制:
- 发送异步消息到息队列中,让系统监听消息队列
- Canal解耦,Mysql数据变动,立马通知到Canal,删除缓存和重试操作都Canal来做
数据类型结构
List、String、hash、Set、Zset
List:信息流---最新文章
String:
Hash:对象存储---文章信息
Set:无序唯一集合---网站数据统计,比如点赞、转发;共同好友(交集)
Zset:同set,加了一个score权重----排行榜
String
key-value形
底层实现:通过int和SDS(简单动态字符串),下面详细的SDS的结构和C的对比
struct sdshdr {
int len; // 已用长度(比如"abc"的len是3)
int free; // 剩余空间(比如buf能存5字节,已用3,free=2)
char buf[]; // 存数据的数组(末尾有`\0`兼容C字符串)
};- SDS保存文本数据,还可以保存二进制数据(\0兼容);C只存文本
- SDS获取字符串长度的时间复杂度是O(1)(有len);C语言字符串不能记录长度复杂度O(N)
- SDS可以自动扩容,C不可以
Hash
Hash 在字段较少且值较小时,底层使用 ziplist (压缩列表) 。这种结构在内存中是连续的,没有指针开销,非常紧凑。当字段增多时,会自动转为 hashtable。
S和H区别
| 特性 | String | Hash |
|---|---|---|
| 数据结构 | 简单的 Key-Value。Value 可以是字符串、整数或序列化的对象。 | 一个 Key 对应一个 field-value 映射表,类似 Java 的 HashMap。 |
| 内存占用 | 较高。每个 Key 都有额外的元数据开销(如过期时间、LRU 信息等)。 | 较低。多个字段共用一个 Key 的元数据,且小对象会使用 ziplist 压缩存储。 |
| 操作粒度 | 粗粒度。修改对象需取出整个字符串,反序列化修改后再存回。 | 细粒度。可以独立操作(HSET/HGET)对象中的某个属性。 |
String 的适用场景
String 是 Redis 最基础的类型,适合“一存一取”的简单场景。
- 缓存简单对象:如果对象结构固定且总是整体读写(例如:Session 信息、配置信息),直接将对象 JSON 序列化存为 String 最快。
- 大文本/二进制存储:如存储一段 HTML 页面片段或一张图片的 Base64。
Hash 的适用场景
Hash 适合存储对象,尤其是对象属性经常需要部分更新的情况。
存储用户信息/商品详情:
- 例如
user:1001作为 Key,内部 field 包括name,age,email。 - 场景:用户只修改了
age,你只需HSET user:1001 age 25,无需读写整个对象,节省网络带宽。
- 例如
- 购物车:
cart:user_id作为 Key,sku_id作为 field,count作为 value。
Zset
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
- 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
- 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
压缩列表
场景:我们要找车厢里第一个包裹。
定位车厢头部:
- Redis 找到这块连续的内存区域(火车车厢)。
车厢最前面有三个小标签:
zlbytes:整节车厢有多长(方便快速跳过它)。zltail:最后一个包裹在哪里(方便快速找尾部)。zllen:车厢里一共有几个包裹。
找到第一个包裹:
- 第一个包裹紧跟在这些小标签后面。
包裹结构: 每个包裹(Entry)不是裸数据,它也自带两个标签:
- 前一个包裹有多长(
prevlen):这是最关键的!它可以让我们知道从当前包裹往回退多少距离,就能找到前一个包裹,实现了“双向”功能。 - 包裹的内容和长度(
encoding+content):实际的数据。
- 前一个包裹有多长(
取出数据:
- Redis 读出第一个包裹的内容(例如:"Java"),任务完成!
缺点:连锁更新
想象一下:
- 包裹 A 比较小,它后面的 包裹 B 用一个 1 厘米的小标签记录了 A 的长度。
- 现在你把 包裹 A 换成一个巨大的包裹。
- 包裹 B 发现它那个 1 厘米的小标签不够写 A 的新长度了,它必须换成一个 5 厘米的大标签。
- 换标签(从 1 变 5)会挤占它后面 包裹 C 的位置。
- 如果 包裹 C 的位置变了,它后面 包裹 D 记录 C 位置的标签可能也要更新...
- 结果就是,一个包裹变大,可能导致后面所有包裹的标签都要更新和移动,浪费大量计算资源。
这就是 “连锁更新” 问题。
跳表
结构:
查询:
SDS大小:也就是字典序
权重是Zset的Score字段
层数增加的方式:
跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。
Redis具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低,层高最大限制是 64。
应用
redis 还能干啥?
- 分布式锁:见项目记录
- 限流:Redis+lua实现,
- 消息队列:List可做(加了订阅广播),还有Stream类型
- 延时队列:Redisson内置延迟队列(zset实现)
- 分布式登录的session
Redis线程模型
单线程部分
单线程是说Redis的主线程是单线程
负责部分:
6.0之前是执行所有客户端发来的命令(set之类的),从连接到命令执行都是
6.0之后是把网络IO读写扔给了IO线程池,其他不变
为什么?保证原子性,什么命令都是串行,不需要加锁,避免了线程上下文开销
多线程部分
多线程是说的IO读写(6.0之后有的)和后台任务的BIO
负责部分:
1、IO线程池:让多个IO线程并行从多个客户端中读取数据,并行把响应数据返回给客户端
2、异步线程BIO:执行不影响核心的耗时任务---AOP、RDB
为什么?IO的目的是并行处理数据读取实现快速到执行步骤,提高吞吐量,异步BIO线程执行持久化等耗时的操作,防止阻塞主线程执行
单线程怎么监听的大量客户端连接的?
IO多路复用机制!
内核代替应用程序去监控多个 I/O 流
主线程向内核注册它感兴趣的所有Socket(包括监听 Socket 和已连接 Socket)
当Socket要进行操作的时候, 内核会将这些已就绪的套接字列表返回给 Redis 主线程,并把主线程休眠中唤醒。
Redis 6.0 之前为什么使用单线程
性能瓶颈不在CPU上,在内存和网络上
单线程编程更容易
容易维护
多线程就存在死锁,线程上下文切换问题
Redis 6.0 之后为什么引入了多线程
随着网络硬件的提升,Redis的性能瓶颈有时候出现在网络IO上-->万兆网卡之类的
所以对于网络IO来说采取多线程客户端读写更快
但是!对于命令还是单线程执行的
Redis内存淘汰
为什么要有内存淘汰就是因为避免 Redis 因为内存耗尽而崩溃
触发时机:当触发新内存申请命令的时候,并且当前内存的使用量超过了配置的maxmemory参数。
策略:触发时机发生的时候,根据maxmemory-policy这个参数设置的策略进行淘汰,举例几个:
1、LRU:最近最少使用算法
2、LFU:淘汰最不经常使用的
3、随机淘汰
4、淘汰过期时间剩的最短的
Redis事务
Redis是有事务的,通过一些关键词就能开启。
用Lua脚本而不是用redis原生事务,原因:
| 特性 | Redis 事务 | Lua 脚本 |
|---|---|---|
| 逻辑控制 | 不能用中间结果进行判断 | 可以在服务端执行ifelse等 |
| 原子性实现 | 依赖 WATCH 实现乐观锁,需客户端重试。 | 脚本整体原子执行,无需 WATCH。 |
AOF持久化机制
是什么
把写操作保存到日志,注意只记录写操作
顺序是先执行写操作命令后,才将该命令记录到 AOF 日志。
好处:
- 不需要额外的检查的开销---先记录日志万一语法错了还得检查
- 不会阻塞当前的写操作的运行---写操作执行成功后,才记录日志
风险:
- 写操作和记录日志是两个操作有前后之分,还没存到硬盘,间隙间宕机,就丢失数据
- 阻塞,不会阻塞当前的写操作,但是会阻塞下一个写操作
能看出来两个风险都跟日志写回磁盘的时候有关,所以详解写回的策略
写回策略
区别
过程图
实际
Always策略就是每次写入AOF文件后,就执行fsync ()
Everysec策略就是创建一个异步任务执行fsync ()
No策略就是永不执行fsync ()
重写机制
AOF日志越写越大总得清除,避免越写越大,重写
举个例子就是说多个set语句,只保留最新状态对应的set语句到日志里
重写完成之后就把这个新的日志文件覆盖掉旧的,实现压缩效果
为什么不复写旧的而是写新的覆盖?
因为如果重写失败了,就会污染现有的AOF文件
重写的过程是由后台子进程完成的,不影响主进程接着用
AOF恢复是很慢的,因为Redis命令是单线程的,一条一条执行恢复。
RDB持久化机制
是什么
通过创建快照来存储内存的数据在某一个时间上的副本。
怎么用
save命令:在主进程生成RDB文件,所以写入RDB时候会堵塞主线程
bgsave命令:创建一个子进程来生成 RDB 文件,不堵塞主线程
注意:RDB的快照是全量快照,很重,如果频繁干,影响性能,所以一般分钟级设置
混合持久化与异同
混合持久化
4.0之后,可以开启混合持久化,就是把RDB中的内容写到AOP文件开头。缺点就是AOF里面的RDB部分是压缩格式而不是AOF格式,可读性低。
异同
区别 AOF RDB 内容 存储的每一次写命令,大 存储压缩后的二进制数据,小 恢复速度 一条一条执行,慢 解析还原就行,快 恢复程度 秒级记录,丢的少 分钟级记录,丢的多
选择
丢一点没关系,就用单独RDB
丢一些有点伤,建议全开或者混合持久化
不单独开AOF,开启RDB有助于快速重启,数据库备份。
| 机制 | 描述 | 默认状态 | 开启方式 |
|---|---|---|---|
| RDB | 时间点快照(Snapshot) | 默认启用 | 自动执行(基于配置)或手动命令(SAVE/BGSAVE) |
| AOF | 写命令日志(Log) | 默认关闭 | 主动修改配置文件,设置 appendonly yes |
主从复制
原理
见canal同步mysql的原理部分
作用
1、实现读写分离,从库主要负责读操作,主库主要负责写操作
2、数据备份,通过Sentinel实时监控主从节点
哨兵监控







