一些基础概念。

性能 | 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
        同时有优化对齐,按实际占用内存进行对齐
  • 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;写多时
不强一致,数据可能丢失。