Redis数据库
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 | struct sdshdr { |
Redis 为了减少频繁扩容会一次多申请一点空间
Redis 为什么是线程安全的
尽管redis6.0引入多线程(网络读写改成多线程),但是仍是线程安全的,因为核心的内存数据读写依然是单线程,不涉及任何并发竞争。
Day2
Redis 五大数据类型
| 类型 | 特点 | 场景 |
|---|---|---|
| String | 最基础 | 缓存、token |
| List | 有序可重复 | 消息队列 |
| Hash | key-value对象 | 用户信息 |
| Set | 无序不重复 | 点赞、共同好友 |
| ZSet | 可排序 | 排行榜 |
list可做消息队列:
生产者:LPUSH
消费者:RPOP
Hash 本质
类似:
1 | { |
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 | INCR count |
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 | RedisA -> 0~5000 |
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 | Nginx缓存 |
四:限流降级
限流eg:每秒最多1000请求
降级eg:返回默认数据
双写一致性
缓存一致性问题就是redis和mysql的数据不一致问题
一般先更新数据库再更新缓存:避免脏数据
redis官方推荐删除缓存而不是更新缓存:因为缓存更新成本较高,并且容易出现并发不一致问题。删除缓存实现更简单,当下次查询时再从数据库读取并重建缓存即可。
延迟双删:
第一步更新数据库
第二步删除缓存
第三步:等待一小段时间
第四步再删一次缓存
防止旧数据重新写回redis
延迟双删解决的问题:
并发导致不一致的过程如下:
- 线程 A 请求更新数据,首先删除了 Redis 中的缓存。
- 线程 B 请求读取该数据,发现 Redis 缓存为空(Cache Miss)。
- 线程 B 去查询 MySQL 数据库,此时线程 A 还没来得及更新数据库,所以线程 B 查到了旧数据。
- 线程 A 将新数据更新到 MySQL 数据库。
- 线程 B 将刚才查到的旧数据写入 Redis 缓存。
结果: 数据库里是新数据,缓存里是旧数据。此后所有的读请求都会命中缓存里的旧数据,导致严重的数据不一致,直到缓存自然过期。
Day6
Redis 分布式锁
分布式锁:在分布式环境下,保证同一时间只有一个服务能够执行某段代码。
Redis 的 setnx 操作具有原子性,可以保证同一时间只有一个客户端加锁成功,因此适合实现分布式锁。
死锁
某个线程加锁后服务挂了,锁永远不释放
解决方案:
给锁设置过期时间
1 | SET lock value 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表示未到)
