一些基础概念。
性能 | performance
是基于内存的数据库(in-memory data store as a database)。读写快。
适用场景:缓存(cache)、消息队列(streaming engine)、分布式锁(message broker)。
对数据的操作是原子性(atomic)。单线程执行命令,不存在并发竞争(Concurrent competition)。
和 Memcached 比较
same:
- 基于内存 in-memory
- 有过期策略 Expiration Policies
- 性能高 high performance
diff:
- Memcached的type只有key-value
- redis支持数据持久化 | data persistence
- redis有原生集群模式 | native cluster mode
- 支持发布订阅、事务功能。| sub/pub, transaction
为什么作为mysql缓存:
高性能 高并发 High performance Concurrency
QPS 是mysql的10倍
数据类型及数据结构
数据类型和数据结构是不同的东西!
数据类型 | TYPE
-
基础类型:
包括字符串( String ) ,哈希( Hash ),列表( List ),集合( Set )、有序集合( Zset )五种- string:缓存对象、常规计数、分布式锁、共享 session 信息
- list:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)
- hash:缓存对象、购物车等
- set:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等
- zset:排序场景,比如排行榜、电话和姓名排序等
-
新增类型
BitMap(2.2 版+)、HyperLogLog(2.8 版+)、GEO(3.2 版+)、Stream(5.0 版+)- bitmap:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等
- HyperLogLog:海量数据基数统计的场景,比如百万级网页 UV 计数等
- geo:存储地理位置信息的场景,比如滴滴叫车
- stream:消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据
数据结构 | STRUCTURE:
SDS,double linklist → quicklist,ziplist → listpack,hashtable,int set,skiplist
(此处待补图)
-
SDS:<c的str结构> → <redis的改进>
- 以
/0
结尾,所以内容有限制 → 可以存文本&二进制 - 顺序存,所以len(n)要O(n) → 有单独存len的字段
- 可能缓存溢出 → 自动扩容 安全
- 数据结构:| len | alloc | flags | buf[] |
- len:存长度代替
/0
,判断结束 - alloc:分配内存长度,和len结合可自动扩容
- flags:记录类型,节省内存
- buf[]: data
同时有优化对齐,按实际占用内存进行对齐
- len:存长度代替
- 以
-
linklist:(c没有的链表)
listnode 双向链表 + list结构缺点:list的head开销大,每个节点内存不连续浪费内。
改进 👇 ziplist -
ziplist:占用连续内存
数据结构:
| zlbytes | zltail | zllen | ……………………………………… | 结束点 |总bytes offset | prelen | typelen | data |
prelen & typelen : 动态分配内存
data:可以是int 也可以是string缺点:因为存了pre的len,所以插入一个就要全部重新分配内存,适合数据量少的。
改进 👇 listpack -
hashtable
O(1) 但冲突 → 链式hash → 一条链上data多了会O(n) → rehash
rehash: redis中有一个结构体内有两个hash表
负载因子触发rehash → 给backup分配更大内存+转移数据 →
copy时间过长 → 渐进式rehash:rehash时两个表都写入
负载因子 = 已存数量 / 表大小 -
int set
是set的底层之一
有连续内存空间
encoding类型有三种:int8 int16 int32
如果新数更大,则触发升级,之前的全生 | 不支持降级
升级时不分配新space,在原数组扩展:节省内存 -
skiplist
支持O(logN) 查找
只有zset用它。zset用两个- skiplist :范围查询
- hash: O(1)获取
规则:
① 从最高level开始找,比权重
② 权重大了往前,小了往后
③ 当前level无,降一层level层数设置:
相邻两层2:1创建node时在0-1中随机生成数x: while x <0.25: level += 1
-
quicklist
双向链表 + 压缩列表
add node时会check该ziplist能否容纳,不能就新建quicklistNode
通过控制每个node的大小or个数,规避连锁更新问题
-
liskpack
| totalbyte | len | ………………………………… | end |
| encoding | data | len |不记pre长度,只记当前len
数据类型和数据结构的关系
string : SDS
list: 双向链表 + 压缩列表 (content < 512 & value < 64b)⇒ quicklist
hash: 哈希表 + ziplist (content < 512 & value < 64b)⇒ listpack
set:哈希表 + 整数set (e:int & count < 512)
zset: skiplist + hash
ziplist(content < 512 & value < 64b) ⇒ listpack
redis 是单线程吗
主流程是单线程:接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端
redis程序不是单线程。关闭文件、AOF 刷盘、释放内存有单独线程。
free memory
因为耗时。
6.0之后引入多线程:
随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。所以对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理。
默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求。
持久化
AOF + RDB or 混合
RDB:加载恢复数据快,但是易丢。
AOF:恢复时丢的数据少,但是体积大会慢。
选择:当丢数据也可以时用RDB,否则选AOF
应用:一般主从+读写分离时,master的持久化要关掉,slave开启AOF,关闭RDB,并记录备份的时间点。
AOF 日志过大,会触发什么机制?
当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
RDB实现:
手动两种:save和bgsave
普通save的流程:
①按save
②redis的主线程就阻塞
③生成RDB文件
和bgsave区别是,在主线程阻塞后,bgsave就fork()一个子进程来进行持久化,生成RDB文件,完成之后会给主线程一个通知。#bgsave只有在fork()子进程时短暂阻塞。
save 会阻塞主线程;bgsave 不阻塞
自动三种:
- 命令设置save m n m时间内如有n个键变化则rdb
- flushall清空全部时自动备份
- 主从同步触发。当从全量复制时,主bgsave,并将RBD文件发给从
主从 哨兵 集群
主从 master-slaver
异步。master操作完就返回,不等slave,不强一致。
读所有server;写只master,master同步给slave
缺点:有单点问题。解决 👇
哨兵 sentinel:
sentinel ping每一个server,监控主从服务器,并且提供主从节点故障转移的功能
数据量大到一台存不下时,解决👇
集群 cluster:
data和node之间的映射:hash slot
crc16(key) % 16384 取模,结果代表一个哈希槽
映射到node节点:可均分可手动
过期删除和内存淘汰
Expire policy
惰性 + 定期 ,删除的对象是已过期的 key。
惰性 Lazy deletion:
有请求才检查
tradeoff:对性能好,对内存不好
定期 periodic deletion
隔一段时间随机抽一批检查,设阈值,如果过期量高,循环抽取
tradeoff:省内存。执行频率难把握
内存淘汰
解决内存过大的问题,内存满了就触发
持久化时,对过期键会如何处理?
RDB:
-
文件生成阶段
不影响。会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中。
-
加载阶段
- 主服务器: 不影响。载入时,会检查键,过期键「不会」被载入到数据库中
- 从服务器:不影响。载入时不检查全载入,但主从服务器在进行数据同步时,从服务器的数据会被清空。
AOF:
-
写入阶段
影响。如果写入时过期键还没删除,就保留,过期键删除后,追加一条DEL 命令来显式地删除该键值
-
重写阶段
不影响。重写时,会检查键值,已过期的不写入。
主从模式中,对过期键会如何处理
从库不会进行过期扫描,从库对过期的处理是被动的
主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key
缓存雪崩、热点失效、缓存穿透
缓存穿透 | cache penetration
程序要访问的缓存key不在缓存key的取值范围里。
原因:误操作 & 黑客
方案:
- 非法请求的限制
- 设置空值或者默认值
- 使用布隆过滤器
热点失效 | hotspot invalid
缓存的key还是存在的,只是这个热点key的数据失效了。
方案:
- 互斥锁
要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 - 热点数据单独设置
要么不设过期时间,要么过期前提前通知处理
缓存雪崩 | cache avalanche
大量缓存都失效了,像雪崩一样
方案:
- 将缓存失效时间随机打散
- 设置缓存不过期
缓存更新策略
通常有三种:
Cache Aside:
适合读多写少。写多影响缓存命中率。
直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」
- 写:先更db再删cache
- 读:读到了,直接返回;没读到,读db,再写回cache
Read/Write Through:
应用程序只和缓存交互,不和数据库交互。缓存和数据库交互。
适合本地缓存
-
Read Through:
cache有直接返回,没有cache问db,写入cache,再返回 -
Write Through:
写时先查cache,如果在,更新cache,cache更新db;如果不在,直接更db。
write back
只更新缓存,同时将缓存数据设置为脏。然后立马返回,不会更新数据库。批量异步更新db
适用计算机体系结构,CPU OS;写多时
不强一致,数据可能丢失。