对于Redis主要的性能瓶颈是内存或者网络带宽而并非 CPU
io-thread-do-reads 配置项为 yes,表示启动多线程http://doc.redisfans.com/index.html
微信抽奖小程序
微信朋友圈点赞
微博社交关系
可能认识的人
sdiff a b:在a集合中而不在b集合中的sdiff b a:在b集合中而不在a集合中的sinter a b:a、b集合公用的根据商品销售对商品进行排序显示
抖音热搜
用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型,位图本质是数组,它是基于String数据类型的按位的操作,该数组由多个二进制位组成,每个二进制位都对应一个偏移量,我们可以称之为个索引或者位格
Bitmap支持的最大位数是2的32次方,它可以极大的节约存储空间,使用 512M 内存就可以存储最大42.9亿字节信息
签到
getbit
setbit
strlen
bitcount
bitop
去重统计的基数估计算法
在 Redis 中,每个 HyperLogLog 键只需要花费 12KB 内存就可以计算接近 2 的64次方个不同元素的基数
http://redis.cn/commands/geoadd.html
Redis 集群并没有使用一致性 hash 而是引入了哈希槽的概念,Redis 集群有16384个哈希糟,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽,但为什么哈希槽的数量是16384,2的14次方呢
CRC16算法产生的 hash 值有 16bit,该算法可以产生 2^16=65536 个值,换句话说值是分布在 0~65535 之间,那作者在做 mod 运算的时候,为什么不 mod65536,而选择mod6384?
如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大
在消息头中最占空问的是 myslots[CLUSTER_SLOTS/8],当槽位 为 655361 时,这块的大小是:65536/8/1024=8kb,因为每秒钟,Redis 节点需要发送一定数量的 ping 消息作为心跳包,如果档位为 65536,这个 ping 消息的消息头太大了,浪费带宽
Redis 的集群主节点数最基本不可能超过1000个,集群节点越多,心跳包的消息体内携带的数据就越多,如果节点过 1000 个,也会导致网络拥堵,因此 redis 作者不建议 redis cluster 节点数量超过1000个,那么,对于节点数在 1000 以内的 redis cluster 集群,16384个槽位够用了,没有必要拓展到 65536 个
槽位越小,节点少的情况下,压缩比高,容易传输,Redis 主节点的配置信息中它所负责的哈希槽是通过一张 bitmap 的形式保存的,在传输过程中会对 bitmap 进行压缩,但是如果 bitmap 的填充率 slots/n 很高的话 (n表示节点数),bitmap的压缩率就很低,如果节点数很少,而哈希槽数很多的话,bitmap的压缩率也低
初始化
布隆过滤器本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1位值的列表)组成,最初所有的值均设置为 0
添加
当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个 hash 函数对 key 进行运算,算得一个下标索引值,然后对位数组
长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置,再把位数组的这几个位置都置为 1 就完成了 add 操作
判断
向布隆过滤器查询某个 key 是否存在时,先把这个key 通过相同的多个hash 两数进行运算,查看对应的位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在
如果这几个位置全都是 1,那么说明极有可能存在,因为这些位置的 1 可能是因为其他的 key 存在导致的,也就是前面说过的 hash冲突
总结
使用时 最好不要让实际元素数量远大于初始化数量
当实际元素数量超过初始化数量时,应这对布隆过滤器进行重建,重新分配 size 更大的过滤器,再将所有的历史元素批量 add 进去
优点:高效的插入和查询,占用空间少
缺点:不能删除元素,存在误判
布谷鸟过滤器
聚合统计
排序统计
list
zset
二值统计
基数统计
热点 key 失效,大量请求到 MySQL
N(集群数量)= 2 * X(宕机数量)+ 1
该方案也是基于 setnx 加锁、lua 脚本解锁进行改良的,大致方案如下
假设我们有 N 个 Redis 主节点,例如 N=5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统
为了取到锁客户端执行以下操作
该方案为了解决数据不一致的问题,直按舍弃了异步复制只使用 master 节点,同时由于舍弃了slave,为了保证可用性,引入了N个节点,官方建议是5
客户端只有在满足以下2个条件才被认为加锁成功
客户端 A 加锁成功,就会启动一个 watch dog 看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端 A 还持有锁 key,那么就会不断的延长锁 key 的生存时间,默认每次续命又从30秒新开始
Redis 默认第一个策略,推荐:allkeys-lru
typedef struct redisObject {
// 刚刚好32 bits
// 对象的类型,字符串/列表/集合/哈希表
unsigned type:4;
// 未使用的两个位
unsigned notused:2; /* Not used */
// 编码的方式,(int、embstr、raw)Redis 为了节省空间,提供多种方式来保存一个数据
// 譬如:“123456789” 会被存储为整数123456789
unsigned encoding:4;
// 上次访问的时间戳,当内存紧张,淘汰数据的时候用到
unsigned lru:22; /* lru time (relative to server.lruclock) */
// 引用计数, 用于垃圾回收
int refcount;
// 数据指针,指向对象实际的数据结构
void *ptr;
} robj;
Redis 没有直接复用 C 语言的字符串,而是新建了属于自己的结构 SDS,在 Redis 数据库里,包含字符串值的键值对都是由 SDS 实现的Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现
SDS 结构
SDS 有多种结构
字符串长度处理
\0 为止,时间复杂度 O(N)内存重新分配
二进制安全
\0\0 会结束, 而 SDS 根据 len 长度来判断字符串结束的,不存在二进制的安全问题| 类型 | 备注 |
|---|---|
| int | Long 类型整数时,RedisObject中的 ptr 指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销 |
| embstr | 当保存的是字符串数据且字符串小于等于 44 字节时,embstr 类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含 redisObject 与 sdshdr 两个数据结构,让元数据、指针和 SDS 是一块连续的内存区城,这样就可以避免内存碎片 |
| raw | 当字符串大于 44 字节时,SDS 的数据变多变大了,SDS 和 RedisObject 布局分家各自过,会给 SDS 分配多的空间并用指针指向 SDS 结构,raw 类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObjiect 结构,而另一块用于包含 sdshdr 结构 |
ziplist 压缩列表是一种紧凑编码格式,总体思想是多花时问来换取节约空间,即以部分读写性能为代价,来换取极高的内存空间利用率,因此只会用于字段个数少,且字段值也较小的场景,压缩列表内存利用率极高的原因与其连续内存的特性是分不开的
想想我们的学过的一种 GC 垃圾回收机制:标记 - 压缩算法,当一个 hash 对象只包含少量键值对且每个键值对的健和值要么就是小整数要么就是长度比较短的字符串,那么它用 ziplist 作为底层实现
ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过栖牲部分读写性能,来换取高效的内存空问利用率,节约内存,是一种时问换空间的思想,只用在字段个数少,字段值小的场景里面
压缩列表节点构成

压缩列表是 Redis 为节约空问而实现的一系列特殊编码的连线内存块组成的顺序型数据结构,本质上是字节数组,在模型上将这些连续的数组分为 3 大部分,分別是 header + tentry 集合 + end,其中 header 是由 zlbytes + zltail + zllen 组成,entry 是节点,zlend 是一个单字节 255,用作 ZipList 的结尾标识符
| 属性 | 类型 | 长度 | 用途 |
|---|---|---|---|
| zlbytes | uint_32t | 4B | 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配或者计算 zlend 的位置时使用 |
| zltail | uint_32t | 4B | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址 |
| zllen | uint_16t | 2B | 记录了压缩列表包含的节点数量: 当这个属性的值小于UINT16_ MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量,当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出 |
| entryX | 列表节点 | 不定 | 压缩列表包含的各个节点,节点的长度由节点保存的内容决定 |
| zlend | uint_8t | 1B | 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端 |
zlentry 节点结构
/* 在内存中并没有存下下面结构体zlentry的内容,只是为了方便,在代码中定义了这样的结构体,包含了一些其他信息
*/
typedef struct zlentry {
// 上一个链表节点占用的长度
unsigned int prevrawlensize;
// 存储上一个链表节点的长度数值所需要的字节数
unsigned int prevrawlen;
// 存储当前链表节点的长度数值所需要的字节数
unsigned int lensize;
// 当前链表节点占用的长度
unsigned int len;
// 当前链表节点的头部大小
unsigned int headersize;
// 编码方式
unsigned char encoding;
// 压缩链表以字符串的形式保存,该指针指向当前节点起始位置
unsigned char *p;
} zlentry;
压缩列表的通历
通过指向表尾节点的位置指针 p1,减去节点的 previous_entry_llength,得到前一个节点起始地址的指针,如此循环,从表尾遍历到表头节点,从表尾向表头遍历操作就是使用这一原理实现的,只要我们拥有了一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的 previous_entry_length 属性,程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点
hash-max-ziplist-entries
hash-max-ziplist-value
Hash 类型键的字段个数小于 hash-max-ziplist-entries 并且每个字段名和字段值的长度小于 hash-max-zjplistvalue 时,Redis 才会使用 OBJ_ENCODING_ZIPLIST 来存储该键,前述条件任意一个不满足则会转换为 OBJ_ENCODING_HT 的编码方式
ziplist 升级到 hashtable 之后就无法再降级到 ziplist
hashtable 是由数组加链表构成
list-compress-depth 0
表示一个 quickist 两端不被压缩的节点个数,这里的节点是指 quickist 双向链表的节点,而不是指 ziplist 里面的数据项个数
参数 list-compress-depth 的取值含义如下
list-max-ziplist-size -2
当取正值的时候,表示按照数据项个数来限定每个 quicklist 节点上的 ziplist 长度。比如,当这个参数配置成 5 的时候,表示每个quicklist 节点的 ziplist 最多包含 5 个数据项,当取负值的时候,表示按照占用字节数来限定每个 quicklist 节点上的 ziplist 长度,这时它只能取 -1 到 -5 这五个值
(默认值)quicklist 结构体
typedef struct quicklist{
//指向双向链表的表头
quicklistNode *head;
//指向双向链表的表尾
quicklistNode *tail;
// 所有的ziplist存储的元素数量
unsigned long count
// 双向链表的长度,node的数量
unsigned long len
int fill : 16
// 压缩深度,1代表不压缩
unsigned int compress : 16
} quicklist
当有序集合中包含的元素数量超过服务器属性 server.zset_ max_ziplist_entries 的值,默认值为 128,或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值,默认值为 64 时,Redis 会使用跳跃表作为有序集合的底层实现,否则会使用 ziplist 作为有序集合的底层实现
字符串
哈希
列表
集合
有序集合
zipist压缩列表):当有序集合的元素个数小于 zset_max_ziplist_enties 配置 (默认128个),同时每个元素的值都小于 zset_max_ziplist_valve 配置(默认64字节时),Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用
skiplist (跳跃表):当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时ziplist 的读写效率会下降
| 名称 | 时间复杂度 |
|---|---|
| 哈希表 | O(1) |
| 跳表 | O(logN) |
| 双向链表 | O(N) |
| 压缩列表 | O(N) |
| 整数数组 | O(N) |
https://github.com/alibaba/canal/wiki/QuickStart
先更新数据库,再更新缓存
先删除缓存,再更新数据库
过程
解决方案
双删产生的问题
旁路缓存模式:先更新数据库,再删除缓存
在大多数业务场景下,我们会把 Redis 作为只读级存使用,假如定位是只读缓存来说,理论上我们既可以先删除缓存值再更新数据库,也可以先更新数据库再删除缓存,但是没有完美方案,两害趋其轻的原则
个人建议是,优先使用先更新数据库,再删除缓存的方案,理由如下
如果使用先更新数据库,再删除的方案
如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在 Redis 缓存客户端暂存并发读请求,等数据库更新完,缓存值删除后,再读取数据,从而保证数据一致性
假设剩余红包金额为 M,剩余人数为 N,那么有如下公式
每次抢到的金额为:(0,(M/N)*2))
Redis 利用 epoll 来实现 IO 多路复用,将连接信息和事件放到队列里,依次放到文件事件分派器,事件分派器将事件分发给事件处理器
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 IO 操作在一情况下往往不能直接返回,这会导致某一文件的 I0 阻塞导致整个进程无法对其它客户提供服务,而 IO 多路复用就是为了解决这个问题而
出现,所谓 IO 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪,一般是读就绪或写就绪,能够通知程序进行相应的读写操作,这种机制的使用需要 select、poll、epoll 来配合,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接,当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务理
Redis 服务采用 Reactor 的方式来实现文件事件处理器,每一个网络连接其实都对应一个文件描述符,Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器,它的组成结构为 4 部分
因为文件事件分派器队列的消费是单线程的,所以 Redis 才叫单线程模型

tomcat7 之前使用 BIO 多线程连接问题
当用户进程发出 read 操作作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个error,从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果
用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作,如果 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回,所以,NIO 特点是用户进程需要不断的主动询问内核数据是否准备完毕
在非阻塞式 IO 模型中,应用程序把一个套接口设置为非阻塞,就是告诉内核,当所请求的 IO 操作无法完成时,不要将进程睡眠而是返回
一个“错误”,应用程序基于 IO 操作函数将不断的轮询数据是否己经准备好,如果没有准备好,继续轮询,直到数据准备好为止
优点:不会阻塞在内核的等待数据过程,每次发起的 IO 请求可以立即返回,不用阻塞等待,实时性较好
缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 IO 模型
结论:让 Linux 内核搞定上述需求,将一批文件描述符通过一次系统调用传给内核由内核层去遍历,才能真正解决这个问题,IO 多路复用应运而生,将上述工作直接放进 Linux 内核,不再两态转换而是直接从内核获得结果,因为内核是非阻塞的
IO mutiplexing 就是我们说的 select、poll、epoll,有些地方也称这种 IO 方式为 event driven IO 即事件驱动 IO,就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪,一般是读就绪或者写就绪,能够通知程序进行相应的读写操作,可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程,每个文件描述符一个线程,每次 new 一个线程,这样可以大大节省系统资源,所以,IO 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select 函数就可以返回
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用+内核层遍历这些文件描述符
多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
| select | poll | epoll | |
|---|---|---|---|
| 操作方式 | 遍历 | 遍历 | 回调 |
| 数据结构 | bitmap | 数组 | 红黑树 |
| 最大连接数 | 1024/2048,操作系统位数决定 | 无上限 | 无上限 |
| 最大支持文件描述符数 | 有最大值限制 | 65535 | 65535 |
| fd 拷贝 | 每次调用都需要将 fd 集合从用户态拷贝到内核态 | 每次调用都需要将 fd 集合从用户态拷贝到内核态 | 首次调用需要epoll_ctl拷贝,后续使用epoll_wait无需拷贝 |
| 工作效率 | 每次调用都进行线性遍历,时间复杂度为 O(N) | 每次调用都进行线性遍历,时间复杂度为 O(N) | 事件通知方式,每次 fd 就绪,系统注册的回调函数就会被调用,将就绪的 fd 放到就绪列表,时间复杂度为 O(1) |