Redis

基础

redis是什么?

一个用c语言实现的非关系型的键值对数据库,数据存储在内存上。

redis和MEmcached区别

共同点:都是基于内存

区别

  1. MEmcached只有key-value的数据结构
  2. redis有持久化机制,MEmcached没有,重启或者挂掉数据就没了
  3. redis原生就支持集群
  4. redis有发布订阅、lua脚本、事务,MEmcached没有

redis的优点

本质上就三点:高性能、高灵活、高可用

高性能

内存读写速度快

高灵活

多种数据结构:

Hash存储用户信息,键值对形式,可以单个字段的更新

Zset实现排行榜,支持范围查询和排序

List还能充当简单的MQ

高可用

一、完整的持久化机制

1.RDB快照:定期生成内存数据的全量快照

2.AOF:记录每一条写操作的指令,支持每秒每次操作同步

详细见持久化

二、集群、主从复制、哨兵

读写策略

旁路缓存

读多写少

以DB的数据为准

先更新DB,然后删除cache

先从cache中读,有就返回,没有就DB中返回,然后数据放到cache中

缺陷

  1. 首次请求肯定不在cache里->热点数据提前放
  2. 写频繁会导致cache频繁删除,命中率变很低

    1. 强一致场景:更新db的时候同时更新cache,必须加(分布式)锁保证cache里面没有线程不安全问题
    2. 短期可以不一致:更新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 不太可能

读的过程的一半出现的问题

解决:延时双删,更数据库-->删除缓存-->延迟删除缓存

删除失败情况

增加重试机制:

  1. 发送异步消息到息队列中,让系统监听消息队列
  2. 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区别

特性StringHash
数据结构简单的 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 类型的底层数据结构;
压缩列表

场景:我们要找车厢里第一个包裹。

  1. 定位车厢头部:

    • Redis 找到这块连续的内存区域(火车车厢)。
    • 车厢最前面有三个小标签:

      • zlbytes:整节车厢有多长(方便快速跳过它)。
      • zltail:最后一个包裹在哪里(方便快速找尾部)。
      • zllen:车厢里一共有几个包裹。
  2. 找到第一个包裹:

    • 第一个包裹紧跟在这些小标签后面。
    • 包裹结构: 每个包裹(Entry)不是裸数据,它也自带两个标签:

      • 前一个包裹有多长(prevlen):这是最关键的!它可以让我们知道从当前包裹往回退多少距离,就能找到前一个包裹,实现了“双向”功能。
      • 包裹的内容和长度(encoding + content):实际的数据。
  3. 取出数据:

    • 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。

image-20251213214155246
image-20251213214155246

应用

redis 还能干啥?

  1. 分布式锁:见项目记录
  2. 限流:Redis+lua实现,
  3. 消息队列:List可做(加了订阅广播),还有Stream类型
  4. 延时队列:Redisson内置延迟队列(zset实现)
  5. 分布式登录的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 日志。

好处:

  1. 不需要额外的检查的开销---先记录日志万一语法错了还得检查
  2. 阻塞当前的写操作的运行---写操作执行成功后,才记录日志

风险:

  1. 写操作和记录日志是两个操作有前后之分,还没存到硬盘,间隙间宕机,就丢失数据
  2. 阻塞,不会阻塞当前的写操作,但是会阻塞下一个写操作

能看出来两个风险都跟日志写回磁盘的时候有关,所以详解写回的策略

写回策略

区别

image-20251009175236953
image-20251009175236953

过程图

image-20251009182959367
image-20251009182959367

实际

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格式,可读性低。

异同

区别AOFRDB
内容存储的每一次写命令,存储压缩后的二进制数据,
恢复速度一条一条执行,解析还原就行,
恢复程度秒级记录,丢的少分钟级记录,丢的多

选择

丢一点没关系,就用单独RDB

丢一些有点伤,建议全开或者混合持久化

不单独开AOF,开启RDB有助于快速重启,数据库备份。

机制描述默认状态开启方式
RDB时间点快照(Snapshot)默认启用自动执行(基于配置)或手动命令(SAVE/BGSAVE
AOF写命令日志(Log)默认关闭主动修改配置文件,设置 appendonly yes

主从复制

原理

见canal同步mysql的原理部分

作用

1、实现读写分离,从库主要负责读操作,主库主要负责写操作

2、数据备份,通过Sentinel实时监控主从节点

哨兵监控