Day1

Redis 是什么

Redis 是一个高性能的 NoSQL 数据库,数据主要存储在内存中,所以读写速度很快。它支持 String、Hash、List、Set、ZSet 等数据结构,实际项目里常用来做缓存、验证码、登录 token、分布式锁、排行榜等。

Redis 和 MySQL 有什么区别?

Redis 是内存型 key-value 数据库,速度快,适合做缓存;MySQL 是关系型数据库,数据主要持久化到磁盘,支持事务和复杂 SQL,适合做核心数据存储。实际项目中一般 MySQL 存真实数据,Redis 做缓存来提高访问速度。

Redis 为什么快

一:基于内存操作

Redis 数据:直接存在内存

而 MySQL:主要在磁盘

二:使用高效的数据结构

redis 不是:简单 HashMap

它内部做了大量优化。

三:单线程避免线程切换和锁竞争

Redis 为什么单线程还快:

纯内存操作: 它的读写都在内存里完成,没有任何磁盘 I/O 带来的硬件瓶颈,速度是纳秒级别的。

高效的 I/O 多路复用: 也就是上面的多路复用机制,主线程只管处理“已经准备好”的事件,绝不在某个阻塞的连接上浪费时间。

没有多线程的副作用: 避免了线程创建、销毁、高频切换上下文带来的 CPU 消耗,同时也省去了各种加锁、释放锁的逻辑。

四:使用 IO 多路复用模型

通过 epoll 同时监听多个连接,当连接就绪后再处理

五:网络模型设计优秀

Redis:

  • 请求简单
  • 协议轻量
  • 数据操作快

Redis String 底层 —— SDS

Redis 的 String 底层不是 C 字符串,而是SDS(简单动态字符串)

SDS 会额外维护 len 属性,记录字符串长度,因此获取长度时间复杂度是 O(1),而不是像c一样一个一个往后找

SDS 在修改字符串时会检查剩余空间,如果空间不足会自动扩容,因此不会出现缓冲区溢出问题。

SDS 不依赖 \0 判断字符串结束,而是通过 len 字段记录长度,因此可以存储任意二进制数据,这就是二进制安全。

SDS结构

1
2
3
4
5
struct sdshdr {
int len;//字符串长度
int free;//剩余空间
char buf[];//真正存数据
}

Redis 为了减少频繁扩容会一次多申请一点空间

Redis 为什么是线程安全的

尽管redis6.0引入多线程(网络读写改成多线程),但是仍是线程安全的,因为核心的内存数据读写依然是单线程,不涉及任何并发竞争。

Day2

Redis 五大数据类型

类型 特点 场景
String 最基础 缓存、token
List 有序可重复 消息队列
Hash key-value对象 用户信息
Set 无序不重复 点赞、共同好友
ZSet 可排序 排行榜

list可做消息队列:

生产者:LPUSH

消费者:RPOP

Hash 本质

类似:

1
2
3
4
{
"name":"张三",
"age":18
}

Hash 可以将对象的多个属性拆分存储,只修改某个字段时不需要整体序列化和反序列化,因此适合存对象

Set 元素不可重复,可以天然实现点赞去重。

ZSet 可以根据 score 自动排序,并且支持快速获取 TopN 数据,因此非常适合实现排行榜。

ZSet 底层 —— 跳表+hash

hash表作用:快速查找元素

跳表作用:排序,范围查询

什么是跳表

就是可以跳跃的链表

普通链表:

1
1 -> 2 -> 3 -> 4 -> 5 -> 6

找元素只能从前往后遍历 O(n)

redis在此基础上加了快捷通道,类似:

1
1 -------> 4 -------> 6

再找6速度会快得多,这就是SkipList O(logn)

其实不止一层,会有多层索引(层数随机),查找会先从高层开始跳

为什么redis用调表

一:实现简单,维护成本和复杂程度不像红黑树那么高

二:跳表更适合范围查询

List 底层 —— quicklist

quicklist就是:双向链表 + 压缩列表

普通链表每个节点不仅要存数据,还要存前后指针,会造成内存浪费;用数组的话插入太慢

quicklist 思想:每个链表节点里,不只放一个元素,而是放一批元素。

外层:双向链表

内层:listpack(紧凑列表)

Hash底层

小数据量情况:ziplist(旧版),listpack(新版)

大数据量情况:hashtable

为什么小数据不用hashtable:因为hashtable要指针和哈希桶,而小数据指针开销较大,不合适

Redis String 和 Hash 存对象有什么区别:

String 一般存整个 JSON,对象读取比较方便,但修改某个字段时需要整体更新。Hash 可以将对象属性拆分存储,只修改单个字段即可,因此更适合频繁修改属性的场景。

Set底层

整数、小数据量:intset(本质就是整数数组,用于优化整数存储)

大数据量:hashtable

常用命令

数据类型 核心核心命令 典型应用场景 一句话大白话解释
string (字符串) set / get 基础缓存、token 令牌 最基础的存和取。
setnx 分布式锁核心 只有不存在时才成功(抢锁)。
incr / decr 文章阅读量、点赞计数 线程安全的数字自增/自减 1。
list (列表) lpush / rpop 简单消息队列 左边塞进去,右边拿出来(先进先出)。
brpop 消息队列(阻塞版) 右边拿数据,要是没有就死等,不空转。
hash (哈希) hset / hget 存储对象(如用户信息) 类似 map<string, map<field, value>>
hexists 判断属性是否存在 检查这个对象里有没有某个字段。
set (集合) sadd / sismember 独立 ip 去重、标签系统 往集合塞数据(自动去重);判断在不在集合里。
sinter 共同好友、共同关注 求两个集合的交集
zset (有序集合) zadd 排行榜、热搜榜、延时队列 存入元素的同时,必须绑一个分数
zrevrange 获取前 n 名(降序) 按分数从大到小排序,拉取前几名。

Day3

Redis持久化

因为redis的数据都存在内存,所以断电即丢失,redis把数据保存到磁盘,防止redis重启后数据丢失就叫持久化

redis有两种持久化:

持久化 原理
RDB 数据快照
AOF 记录命令

RDB:

优点:恢复快,直接整体加载

​ 保存压缩后的数据,文件小

缺点:可能丢数据

AOF:

优点:更安全,数据丢失少

缺点:记录大量命令,文件更大,恢复更慢

混合日志:

前半部分RDB,后半部分AOF

RDB原理

RDB本质:某一时刻内存数据的快照

RDB文件:dump.rdb

Redis怎么生成RDB?

命令 特点
save 同步
bgsave 异步(常用)

save:立即生成RDB,生成期间redis被阻塞(因为redis是单线程)

bgsave:后台上传RDB

Redis 怎么后台生成:fork

fork:Linux 系统调用,作用是复制一个子进程

当redis执行bgsave时,会fork一个子线程,父线程继续处理客户端请求,子线程则生成RDB文件

fork为什么这么快?

fork刚开始是父子进程共享内存,只有修改数据时才真正复制,即写时复制:Copy-On-Write(COW)

fork 后父子进程 initially 共享内存,只有当某一方修改数据时,操作系统才会复制对应内存页,从而减少内存复制开销。

同时fork是会阻塞redis的,如果redis内存很大fork就会卡顿

RDB 自动触发:

Redis 可以自动:定时生成快照

配置:

1
save 900 1

意思:900秒内至少1次修改就生成 RDB。

AOF原理

为什么 AOF 比 RDB 更安全:因为 AOF 会记录每一次写命令,而 RDB 是定时生成快照,因此 Redis 崩溃时 AOF 丢失的数据通常更少。

Redis 什么时候把 AOF 写入磁盘:appendfsync

appendfsync有三种模式:

1.always:appendfsync always

每次写命令都会立刻刷盘,最安全,性能最差,因为磁盘io太麻烦

2.everysec(最常用):appendfsync everysec

redis每秒刷盘一次,平衡最好,生产最常用

3.no:appendfsync no

redis不主动刷盘,由系统决定,性能最好但是可能丢失大量数据

AOF最大问题:AOF 文件越来越大

怎么解决:AOF Rewrite(重写)

就是用最少命令重建当前数据。

eg:

rewrite前:

1
2
3
4
5
INCR count
INCR count
INCR count
...
100万次

rewrite后

1
SET count 1000000

rewrite不会阻塞redis

与RDB类似采用后台处理方式:子进程后台重写

混合持久化

Redis 在 AOF Rewrite 时,先写入 RDB 快照,再追加增量 AOF 命令。

RDB 恢复快但容易丢数据,而 AOF 数据更安全但恢复较慢,因此 Redis 引入混合持久化,结合两者优点,提高恢复速度并减少数据丢失。

使用

1
aof-use-rdb-preamble yes

开启

Day4

Redis 主从复制

一个主节点(Master)负责写,多个从节点(Slave)复制主节点数据。

工作方式:读写分离

主节点负责写操作,从节点负责读操作

好处:减轻压力(多个Slave分担读);提高可用性(主挂了还有从)

主从复制流程

第一阶段:全量复制

第一次连接slave为空,所以master把全部数据都发给slave

流程:

第一步slave发送psync同步请求

第二步master执行bgsave生成RDB

第三步master把RDB文件发给slave

第四步slave加载RDB

第五步,因为master在生成RDB的时候可能还有新写请求,所以master会把新增写命令缓存,最后发给slave确保数据一致

第二阶段:增量复制

如果网络断开后每次都全量复制,那么代价太大,所以要有增量复制

增量复制只同步断线期间丢失的数据

实现原理:repl_backlog_buffer

什么是 backlog buffer?

就是master维护的一个环形缓冲区,里面保存最近写命令

当slave断线重连时会根据offset告诉master同步位置,master只发送缺失部分即可

Redis 主从复制本质是异步的,Master 写成功后不会等待 Slave 完成同步,因此主从之间可能存在短暂数据不一致。

Redis Sentinel(哨兵)

Sentinel 是 Redis 的高可用监控系统,用于监控 Redis 节点并在 Master 故障时自动完成故障转移。

功能 作用
监控 检查Redis是否正常
通知 节点异常报警
自动故障转移 Master挂了自动切换

判断原理

sentinel会不断ping redis节点

如果master挂了,sentinel会自动选择一个slave升级成master(故障转移)

Sentinel 怎么判断 Redis 挂了?

第一阶段:主观下线

就是某个sentinel发现master一直没响应,它会主观认为master挂了,但不会真的切换

第二阶段:客观下线

就是多个sentinel都认为master挂了,达到某个数量后正式确认

quorum就是投票人数

1
quorum = 3

代表至少要三个sentinel认为master挂了才会故障转移

故障转移流程

master挂了后sentinel开始failover

第一步:

选一个sentinel当leader

第二步:

从多个slave选一个升级为新master

第三步:

通知其他 Slave去复制新Master

第四步:

客户端连接到新master

Sentinel 怎么选新 Master?

第一:优先级 priority:配置高的优先。

第二:复制 offset

谁数据更新:谁优先

第三:runid

随机比较。

Redis Cluster(集群)

Redis Cluster 是 Redis 的分布式集群方案,通过数据分片将数据分散到多个 Redis 节点中。(因为所有写请求都是在一个master中处理,数据太多会放不下)

集群:大容量,高性能,高可用

Redis Cluster 核心:Hash Slot

Redis Cluster没有直接:按节点存key,而是先分16384个槽位,每个节点负责一部分slot

eg:

1
2
3
RedisA -> 0~5000
RedisB -> 5001~10000
RedisC -> 10001~16383

key怎么定位?

计算

1
CRC16(key) % 16384  //CRC16 哈希计算,然后对 16384 取模

得到slot编号,然后找到负责这个slot的节点

为什么是16384?

一:足够均匀

二:节省网络开销

节点之间要相互通信,会发送slot位图,slot太多网络包太大,所以16384平衡最好

Cluster 为什么去中心化?

redis cluster是没有中心节点的,每个节点都知道slot分布,能避免单点故障

同时Redis Cluster 不需要 Sentinel

因为cluster自己内置故障转移

为什么 Redis 大 key 不好?

因为 Redis 是单线程模型,大 key 会导致命令执行时间变长,阻塞 Redis。同时大 key 还会增加网络传输开销,并影响 RDB/AOF fork 性能。

Day5

缓存穿透

查询一个缓存和数据库中都不存在的数据,导致请求每次都会打到数据库。

解决方案

一:缓存null

如果要查询的数据数据库中也没有,在redis中缓存一个null,并设置短TTL(过期时间)

问题:

1.占内存,大量null浪费redis空间

2.短暂不一致

二:布隆过滤器

什么是布隆过滤器:用来快速判断一个数据“可能存在”还是“一定不存在”。

先查布隆过滤器,如果不存在直接拦截

布隆过滤器可能会误判,把实际不存在的判断为存在的,但是不会漏判

缓存击穿

某个热点 key 在失效瞬间,大量并发请求同时访问数据库,导致数据库压力瞬间增大。

与缓存穿透的对比:

问题 数据存在吗
缓存穿透 不存在
缓存击穿 存在

解决方案:

一:互斥锁

即缓存失效后只允许一个线程查数据库,其他线程等待

二:逻辑过期

逻辑过期是指不给 Redis key 设置真实过期时间,而是在数据中保存逻辑过期时间。数据过期后仍然返回旧数据,并由后台线程异步更新缓存。

缓存雪崩

大量缓存 key 在同一时间失效,或者 Redis 宕机,导致大量请求直接访问数据库,从而造成数据库压力过大。

vs缓存击穿:一个是大量key同时过期,一个是一个热点key过期

缓存雪崩原因:

一:TTL一样

二:redis宕机

解决方案:

一:TTL随机化

eg:30分钟+随机值

二:redis高可用

eg:主从复制,监控,集群

三:多级缓存

eg:

1
2
3
4
5
Nginx缓存
+
Redis缓存
+
本地缓存

四:限流降级

限流eg:每秒最多1000请求

降级eg:返回默认数据

双写一致性

缓存一致性问题就是redis和mysql的数据不一致问题

一般先更新数据库再更新缓存:避免脏数据

redis官方推荐删除缓存而不是更新缓存:因为缓存更新成本较高,并且容易出现并发不一致问题。删除缓存实现更简单,当下次查询时再从数据库读取并重建缓存即可。

延迟双删:

第一步更新数据库

第二步删除缓存

第三步:等待一小段时间

第四步再删一次缓存

防止旧数据重新写回redis

延迟双删解决的问题:

并发导致不一致的过程如下:

  1. 线程 A 请求更新数据,首先删除了 Redis 中的缓存
  2. 线程 B 请求读取该数据,发现 Redis 缓存为空(Cache Miss)。
  3. 线程 B 去查询 MySQL 数据库,此时线程 A 还没来得及更新数据库,所以线程 B 查到了旧数据
  4. 线程 A 将新数据更新到 MySQL 数据库
  5. 线程 B 将刚才查到的旧数据写入 Redis 缓存

结果: 数据库里是新数据,缓存里是旧数据。此后所有的读请求都会命中缓存里的旧数据,导致严重的数据不一致,直到缓存自然过期。

Day6

Redis 分布式锁

分布式锁:在分布式环境下,保证同一时间只有一个服务能够执行某段代码。

Redis 的 setnx 操作具有原子性,可以保证同一时间只有一个客户端加锁成功,因此适合实现分布式锁。

死锁

某个线程加锁后服务挂了,锁永远不释放

解决方案:

给锁设置过期时间

1
2
3
SET lock value NX EX 30
//NX:不存在才设置
//EX:30秒自动过期

为什么 unlock 要用 Lua

解锁:第一步判断是不是自己的锁,第二步删除锁

但这两步不是原子的,可能A的锁过期了B抢到了新锁,但是A执行DEL lock把B的删了,所以要用lua脚本来解决

因为redis的lua脚本原子执行

Redis 分布式锁缺点

业务执行太久导致锁过期,其他线程抢到锁出现并发执行的情况

用redissson解决:Redisson 是 Redis 的 Java 客户端,内部封装了分布式锁实现。

Redisson 最大特点:watchdog

如果线程还在执行,watchdog会续期锁时间,防止锁提前过期

Redis 过期策略

Redis 删除过期 key有两种策略

一:惰性删除

访问 key 时才检查是否过期,对cpu友好,不用一直扫描,对内存不友好

二:定期删除

redis每隔一段时间随机抽查部分key,如果发现过期就删除

redis的过期删除策略则是同时使用两种,平衡CPU和内存

redis不用定时删除防止缓存雪崩

Day7

redis秒杀系统

为什么redis适合秒杀?

因为 Redis:

  • 内存快
  • 单线程原子(避免超卖)
  • 高并发强

为了避免库存为0还继续扣减的情况,使用lua脚本,让判断库存 + 扣减库存一起执行

秒杀建议redis+mq

Redis 扣库存后可以通过 MQ 异步处理订单,从而实现削峰填谷,保护后端系统。

redis怎么解决一人一单问题?

可以使用 Redis Set 保存已下单用户 ID,利用 Set 天然去重特性实现一人一单。

Redis 排行榜

ZSet 可以根据 score 自动排序,并且支持快速获取 TopN 数据,因此非常适合实现排行榜。

1
ZADD rank 100 zhangsan//张三得了 100 分

ZSET 常用操作命令表:

命令 核心作用 典型业务场景 语法示例
zadd 添加成员或更新已有成员的分数 玩家首次上榜、刷新历史最高分 zadd rank 100 zhangsan
zincrby 在原有分数基础上增加(或减少) 游戏中每击杀一怪加10分、点赞数+1 zincrby rank 10 zhangsan
zscore 获取指定成员的当前分数 用户在个人主页查看自己的积分 zscore rank zhangsan
zrevrank 获取降序排名(分数由大到小,从0开始计算) 用户查看自己在全服的具体名次 zrevrank rank zhangsan
zrevrange 获取指定降序排名区间内的成员 首页展示全服 Top 10 排行榜 zrevrange rank 0 9 (withscores//可选参数。如果不加这个参数,Redis 只会返回成员的名字(比如只返回“张三”、“李四”)。加上它之后,会连同分数一起返回(“张三”、100、“李四”、85))
zrem 从集合中彻底移除指定成员 玩家注销账号、清除作弊者成绩 zrem rank zhangsan
zcard 统计当前集合中的总人数 展示“已有 10 万人参与排名” zcard rank

Redis Feed 流

就是用户打开首页时看到的内容流。

内容怎么推给用户

推模式:

例如张三有100粉丝,张三发动态直接写入这100人feed

优点:

用户打开首页:直接读Redis非常快。

缺点:

如果粉丝太多发一次动态系统压力太大

拉模式:

用户打开首页时:实时查询关注的人

优点:写入压力小

缺点:比较慢,因为每次都要实时聚合

推拉结合:

推模式读取快但写扩散严重,而拉模式写入压力小但读取较慢,因此实际项目中通常采用推拉结合方案:普通用户使用推模式,大 V 使用拉模式。

Redis 在 Feed 流里怎么用

使用zset

因为 Feed 流需要按照发布时间排序,而 Redis ZSet 支持根据 score 排序,因此非常适合实现 Feed 流。

Redis 其他经典项目场景

token 登录

Redis 读写速度快,并且支持过期时间,适合存储登录状态等临时数据。

验证码

验证码属于短时临时数据,而 Redis 支持高性能读写和自动过期,因此非常适合存储验证码。

限流

防止接口被打爆

可以使用 Redis 的 incr 作为计数器,对请求次数进行统计,并结合过期时间实现固定时间窗口限流。

1
expire key seconds//设置过期时间

GEO

Redis GEO 可以存储地理位置数据,适用于附近的人、附近商家、外卖配送等场景。

Bitmap

Redis Bitmap 适用于签到、用户在线状态、活跃统计等场景,因为 Bitmap 使用 bit 存储数据,内存占用非常小。

(1表示签到,0表示未到)