Reids说明
对于Reids来说单CPU的性能更优于多CPU,因为CPU缓存(L1,L2,L3)一致性的问题涉及到CPU之间的通性
支持的数据结构
- string
- hashMap
- list
- set
- zSet
使用type查看key是哪种类型,使用encoding判断是什么对象类型,可以知道底层使用的哪种数据结构
所有的value都是RedisObject对象。
Redis为什么快?
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速
2、采用单线程,避免了不必要的上下文切换和竞争条件,并且不需要考虑锁的问题
3、使用多路I/O复用模型,非阻塞IO
4、采用多种合适的优化的数据结构,比如字符串存储使用sds等
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
(encoding表示 ptr 指向的具体数据结构,即这个对象使用了什么数据结构作为底层实现)
encoding的取值,如图:
不同的数据类型,底层存储结构:
(Redis的List类型中多了一种quickList,在3.2之后)
Reids底层存储结构(value)
String
底层以int、raw、embstr作为结构存储,redis中规定了string的最大长度为512M
什么时候使用什么类型?
-
如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型标识,那么字符串对象会讲整数值保存在 ptr 属性中,并将 encoding 设置为 int
-
如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于 32 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw
-
如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于 32 字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串。
既然有了 raw 的编码方式,为什么还会有 embstr 的编码方式呢?
因为 embstr 的编码方式有一些优点,如下: embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
释放 embstr 编码的字符串对象只需要调用一次内存释放函数,而释放 raw 编码的字符串对象需要调用两次内存释放函数。
因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起 raw ,编码的字符串对象能够更好地利用缓存带来的优势。
sds结构说明
struct sdshdr {
// 记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
如:
为什么使用SDS这种结构,而不是直接存字符串?
缺点:占用无用内容,典型的用空间换时间
HashMap
哈希对象的编码有两种,分别是:ziplist、hashtable。
当哈希对象保存的键值对数量小于 512,并且所有键值对的长度都小于 64 字节时,使用压缩列表存储;否则使用 hashtable 存储。
HashMap渐进式扩容?
List
列表对象的编码有两种,分别是:ziplist、linkedlist
ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在数据量小的时候使用压缩列表存储。
当列表的长度小于 512,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 linkedlist 存储。
考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
quickList 是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
Set
集合对象的编码有两种,分别是:intset、hashtable
intset(整数集合)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在数据量小的时候使用整数集合存储。
当集合的长度小于 512,并且所有元素都是整数时,使用整数集合存储;否则使用 hashtable 存储。
ZSet
有序集合对象的编码有两种,分别是:ziplist、skiplist
当有序集合的长度小于 128,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 skiplist 存储。
AOF
AOF策略就是每次执行修改命令的时候都将命令写到AOF文件中,AOF是有缓存的,什么时候真正落文件,Redis提供了三种策略
redis通过appendfsync配置提供了三种写盘的方式:
- appendfsync=always,每次修改事件都将aof buffer的内容写到aof文件并同步aof文件,这种同步策略的安全性最高,效率最低,发生系统奔溃时最多丢失一个事件循环的数据。
- appendfsync=everysec,每次修改都aof buffer内容写到aof文件,每秒同步一次aof文件,这种同步策略与每次修改都同步的策略相比,安全性差一些,但性能高一些,是性能与安全性的折中,发生系统奔溃时最多会丢失1s的数据。
- appendsync=no,每次修改都将aof buffer内容写到aof文件,同步动作的进行时机交给操作系统来决定,redis不管,这种安全性最低,但性能最好。操作系统系统一般考虑繁忙程度以及其它一些因素综合判断同步时机。
AOF重写
AOF随着操作的增加会越来越大,就需要重写(把对一个key的多条操作合并成一条)来减少文件的大小
Redis通过fork一个子进程来进行文件的重写,在重写的过程中如果有新的操作,则会把操作记录写两份,一份在源AOF的缓存区(避免重写失败导致数据的丢失),一份在重写缓存区,重写完成后将重写缓存区的信息写入重写文件,将原来的文件进行替换
在重写过程中,fork子进程以及重写完成后进行合并替换都会阻塞Reids的主线程的操作
为什么开启子进程而不是线程?
线程的创建和销毁会比子进程快的多,但是子进程会更稳定些,对于需要频繁创建和销毁的场景可以使用线程,如果追求稳定,使用子进程比较合适
内存快照(RDB)
AOF只要不是设置为always策略,对性能不会造成太大影响,但是由于记录的是命令而不是实际数据,所以AOF在恢复的时候就会比较缓慢
内存快照就是指内存中的数据在某一时刻的状态记录,类似于照片,快照对于恢复数据很友好但会有两个问题
- 对哪些数据做快照?关系到执行效率的问题
- 做快照时,数据还能增删改查吗?关系到Reids是否被阻塞
- 多久做一次快照
对哪些数据做快照?
Redis数据都是在缓存,为了提供可靠性必须对所有数据进行快照,执行全量快照
Reids提供两种快照方法
- SAVE 在主线程中执行会导致阻塞
- BGSAVE 创建子进程,专门用于写入RDB文件,避免阻塞,也是默认的配置
做快照时数据可以修改吗?
读操作不用关心,和写快照没有冲突,写操作时Reids借助操作系统提供的写时复制技术,在执行快照的同时正常处理写操作(COW)
多久做一次快照?
如果太频繁会影响主进程导致Reids性能下降,如果时间长则会丢失的数据比较多,所以这个时间需要根据具体情况来定,不好把控,Reids4.0提出了一个混合使用AOF和内存快照的方法,简单的来说,内存快照以一定的频率执行,在两次快照之间使用AOF记录这期间的操作(建议在实践中使用)
缓存淘汰策略
参考二八原则,缓存设置比较合理是在总数据量的15%-30%,不过缓存写满是不可避免的,Redis又有哪些淘汰策略?
- noevction策略不会淘汰数据,在写满后再有写请求时,报错
- volatile-ttl 在筛选时,对越早过期的越优先删除
- volatile-random 在设置了过期时间的键值对中进行随机删除
- volatile-lru 使用LRU算法对设置了过期时间的键值对中删除
- volatile-lfu 使用LFU算法对设置了过期时间的键值对中删除
- allkeys-random 所有键值对随机删除
- allkeys-lru 使用LRU对所有键值对删除
- allkeys-lfu 使用LFU对所有键值对删除
LRU和LFU都需要链表来记录最新访问或最频繁访问的数据,这样会对性能造成影响,Reids的做法是在ReidsObject中记录了时间和次数,在淘汰的时候随机选一批进行LFU或者LRU的淘汰,不会维护整个链表,所以是不公平的淘汰策略
LRU的时间字段和LFU的次数都是用一个字段来保存的,只使用了8bit来保存访问次数(最大255),在实现LFU的时候并没有采用每次访问都把对应的counter的值加一,而是,采用一种算法,在访问到一定的量级才counter的值会到某个值
算法源码
double r = (double)rand()/RAND_MAX;
...
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
次数和counter的对应关系