Redis源码
2024-07-20 18:40:07 0 举报
AI智能生成
Redis
作者其他创作
大纲/内容
线程模型
持久化机制
rdb定时快照
参数配置
save seconds times
seconds秒数范围内,执行了times指定的操作数量,
|在900s内执行了超过50次操作,会触发,生成rdb快照【bgsave,后台去进行rdb快照持久化的过程】
|在900s内执行了超过50次操作,会触发,生成rdb快照【bgsave,后台去进行rdb快照持久化的过程】
参数配置
问题
每次执行很耗时
1 fork子进程
2 将完整的内存快照写入磁盘
场景
业务系统对redis访问压力是最低的时候执行
再上传到hdfs上去,做冷备份
再上传到hdfs上去,做冷备份
aof异步日志【默认关闭】
每次你往redis里写入一个命令,他都把这个命令,异步追加到aof日志文件里
写命令追加到aof_buf缓冲区
根据刷盘策略去进行刷盘,aof文件越来越大,定期对aof文件进行rewrite,缩小aof文件
AOF启动的配置
appendonly no
默认是关闭的
AOF的fsync策略配置
always: 每次写入一条数据
everysec: 每秒将os cache中的数据fsync到磁盘【常用,生产环境】
no: 将数据写入os cache就撒手不管了,然后后面os自己会时不时有自己的策略将数据刷入磁盘,不可控了。
AOF rewrite 策略配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
然后就会接着128mb继续写AOF的日志,如果发现增长的比例,超过了之前的100%,256mb,就可能会去触发一次rewrite,但是此时还要去跟min-size,64mb去比较,256mb > 64mb,才会去触发rewrite 一般这两个参数保持默认基本不用去动
流程
(1)redis fork一个子进程
(2)子进程基于当前内存中的数据,构建日志,开始往一个新的临时的AOF文件中写入日志
(3)redis主进程,接收到client新的写操作之后,在内存中继续写入旧的AOF文件
(4)子进程写完新的日志文件之后,redis主进程将内存中的新日志再次追加到新的AOF文件中
(5)用新的日志文件替换掉旧的日志文件
持久化fork操作与aof阻塞优化
分析
根据生产实践来说,就是redis内存越大,耗时就越多,一般来说,1个GB的redis进行fork的时候,会耗时20ms,如果是10个GB的话,就会耗费几百ms去执行一次fork,这会导致高并发的redis实例,每秒几万请求的话,可能上万请求都会被拖慢,这会导致线上生产环境的性能抖动和不稳定
就是在进行rdb和aof rewrite的时候,需要做内存快照,还要做磁盘io,这都会很耗费机器性能,包括cpu、内存、磁盘io,都会有开销和影响,都会影响线上机器的性能
主线程一旦发现现在比上次fsync时间大于2s,就会把主线程自动给阻塞住,这要来确保最多可能会丢失2s的数据,因为这个时候就不接受新写入了,aof_buf里阻塞了2s的数据,要一直等到fsync完成 才行
建议
控制redis实例的内存,通常都是建议redis实例不要超过10gb
【因为每次rdb和aof rewrite都很耗时了,要几百毫秒,甚至上1s】
【因为每次rdb和aof rewrite都很耗时了,要几百毫秒,甚至上1s】
选择服务器压力小的时候执行如凌晨
内存不够,可以选择扩容
数据结构
sds【simple dynaic string
简单动态字符串】
简单动态字符串】
结构模型
struct sdshdr {
int len; // 已使用字节数 = 字符串长度
int free; // 未使用字节数
char buf[]; // 字符串本身内容
}
对于字符串长度的获取,直接读取len就可以了,O(1)复杂度
int len; // 已使用字节数 = 字符串长度
int free; // 未使用字节数
char buf[]; // 字符串本身内容
}
对于字符串长度的获取,直接读取len就可以了,O(1)复杂度
时间复杂度优化
c语言字符串在获取长度的时候 [r,e,d,i,s,\0] -> strlen -> C语言遍历字符串的数组,
一个一个元素的遍历,一直遍历到\0空字符。复杂度为O(n)
一个一个元素的遍历,一直遍历到\0空字符。复杂度为O(n)
二进制的安全存储
原因
对于c语言来说,读取的时候,一定要按照\0来截断。
而对于二进制数据来说,图片、视频、音频、压缩文件,
这种数据都是以一大堆的二进制串来存储的,里面肯定会有很多\0空字符
因此原生的c语言字符串没法安全的存储二进制数据
而对于二进制数据来说,图片、视频、音频、压缩文件,
这种数据都是以一大堆的二进制串来存储的,里面肯定会有很多\0空字符
因此原生的c语言字符串没法安全的存储二进制数据
解决
len有2个作用,直接读取len,strlen,O(1)
对于我们来说,redis里的sds存储二进制的数据,图片、音视频,
就算buf里包含了很多\0空字符,根据len来读取,不是根据\0来截断,
redis sds可以实现二进制安全性,安全的保存二进制数据
对于我们来说,redis里的sds存储二进制的数据,图片、音视频,
就算buf里包含了很多\0空字符,根据len来读取,不是根据\0来截断,
redis sds可以实现二进制安全性,安全的保存二进制数据
解决缓冲区溢出
原因
c语言原生字符串可能搞出缓冲区溢出的问题,那就是说,r,e,d,i,s,s,p,a,r,k,连续两个字符串,redis和spark,结果我们要是用c语言的strcat函数,搞了一个strcat(str, ‘sentinel’),想要把sentinel拼接到redis字符串后面去,又忘了给sentinel提前分配内存空间,就会导致,r,e,d,i,s,s,e,n,t,i,n,e,l,直接覆盖掉spark了,这就是缓冲区溢出。
sds为了避免这种频繁内存重分配的问题,设计了free
sds为了避免这种频繁内存重分配的问题,设计了free
解决
free可以实现内存预分配【空间换时间】
sds长度小于1mb,就把free设置为跟len一样,
做一个双倍预分配,如果扩容后len大于1mb了,就把free设置为1mb
做一个双倍预分配,如果扩容后len大于1mb了,就把free设置为1mb
惰性释放
数组里先保留free,等到你需要释放的时候,可以调用sds API释放
基本数据结构
string
双向链表数据结构
基本数据结构
list
结构模型
有序列表
typedef struct list {
listNode * head;
listNode * tail;
unsigned long len;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
} list;
listNode * head;
listNode * tail;
unsigned long len;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
} list;
无序
typedef struct listNode {
struct listNode * prev;
struct listNode * next;
void * value;
} listNode;
struct listNode * prev;
struct listNode * next;
void * value;
} listNode;
时间复复杂度
头和尾的获取时间复杂度都是O(1)
链表元素个数的时间复杂度是O(1)
lrange -> 遍历 -> O(n)
hashtable
基本数据结构
hash
set
结构模型
typedef struct dict {
dictType *type; // 类型特性的函数,保存了操作特定类型kv的函数
void *privdata; // 私有数据,传递给类型特定函数的可选的参数
dictht ht[2]; // 哈希表,两个dictht哈希表,默认就用一个,另外一个是在rehash的时候用的,ht[0]->在使用的hashtable,ht[1]->备用的hashtable
int trehashidx; // rehash索引,默认是-1,因为没有进行rehash
}
typedef struct dictht {
dictEntry **table; // 数组,放我们的一个一个的key-value对,就是放在数组里
unsigned long size; // 数组大小
unsigned long sizemask; // 掩码,根据key hash计算数组里面的索引值,size-1
unsigned long used; // 已有的节点数量
} dictht;
dictEntry **table; // 数组,放我们的一个一个的key-value对,就是放在数组里
unsigned long size; // 数组大小
unsigned long sizemask; // 掩码,根据key hash计算数组里面的索引值,size-1
unsigned long used; // 已有的节点数量
} dictht;
typedef struct dictEntry {
void *key; // key值
union { // value值
void *val;
uint64_tu64;
int64_ts64;
} v;
struct dictEntry *next; // 指向下一个节点,单向链表结构,解决 hash冲突问题的
} dictEntry;
void *key; // key值
union { // value值
void *val;
uint64_tu64;
int64_ts64;
} v;
struct dictEntry *next; // 指向下一个节点,单向链表结构,解决 hash冲突问题的
} dictEntry;
typedef struct dict {
dictType *type; // 类型特性的函数,保存了操作特定类型kv的函数
void *privdata; // 私有数据,传递给类型特定函数的可选的参数
dictht ht[2]; // 哈希表,两个dictht哈希表,默认就用一个,另外一个是在rehash的时候用的,ht[0]->在使用的hashtable,ht[1]->备用的hashtable
int trehashidx; // rehash索引,默认是-1,因为没有进行rehash
}
dict-->dictht-->dictEntry
hash算法、key冲突与rehash
算法
hash = dict->tpe->hashFunction(key);
冲突问题
不同的hash值算出来的数组里的index位置,有可能是一样的
解决
基于dictEntry的单向链表,来挂载一个位置的多个key
数组扩容流程
流程
搞了一个新hash表ht[1], 大小大于等于ht[0].used * 2
可以把ht[0]里的所有key都rehash到ht[1]
解决key冲突问题,全部迁移完毕之后,就会释放掉ht[0]了
把ht[1]设置为ht[0]
触发条件
负载因子 load_factor = ht[0].used / ht[0].size
rdb和aof执行的过程中【不可触发】
redis没有执行rdb和aof的持久化动作,redis负载不会太高【可触发】
跳跃表结构
基本数据结构
zset
允许你根据你自己制定的规则,进行自定义的排序
结构模型
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
struct zskiplistNode *backward;
double score;
robj *obj;
} zskiplistNode;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
struct zskiplistNode *backward;
double score;
robj *obj;
} zskiplistNode;
时间复杂度
O(logn),最坏情况下才会到O(n)
压缩列表
原理
压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
存储优化建议
对于大量分散的key,注意,是大量分散的key,要使用hash来作为一个table的概念,用一个hash来存放大量同类型的kv数据,利用hash结构,可以在底层大幅度的优化内存使用,因为hash保存的数据条数在1000条以内,在底层是可以用skiplist的,这会比hashtable更加节约内存
一般来说,每个hash里保存1000条数据,如果有100w数据,就散列到1000个hash里去就可以了,每个hash控制数据条数,就会用skiplist结构来存储,内存节约量可以达到几十%,但是一般要求value小于1kb,否则就没太大用了,而且这样会导致客户端的开发量增加
性能调优实践
合理选择对应的数据机构
key->values(list、hash、set、sorted set),控制你的values数量
hkeys,切记切记,hashtable里放了多少数据,不可放过多的数据,否则执行该命令会造成堵塞
注意你使用的数据结构,指令时间复杂度
监控慢查询
利用redis提供了慢查询功能,会生成slow log,慢查询日志
配置和实现
slowlog-log-slower-than【默认10ms】
slowlog-max-len【列表的最大长度,超过长度,就移除最早的慢查询slow log移除掉】
依靠双向链表实现
生产配置
slowlog-max-len【1000以上】
slowlog-log-slower-than【调小】
因为redis并发能力根据每个指令的执行耗时来的,假设超时时间耗时达到1ms,
redis并发就只有1000了,如果是0.1ms,那么并发就是1w,
redis单机一般都要求在1w以上
redis并发就只有1000了,如果是0.1ms,那么并发就是1w,
redis单机一般都要求在1w以上
命令
slowlog get n,指定获取多少条慢查询日志
基于pipeline和lua的多指令性能优化
网络耗时图
客户端连接池的生产环境优化
JedisPool参数设置
默认是8个Jedis连接【可根据实际情况配置】
could not get a resource
如果8个连接都被占用了,默认会等待maxWaitMillis时长来等待,
一直等待不到,那就异常了,could not get a resource
一直等待不到,那就异常了,could not get a resource
如果设置了blockWhenExhausted=false,那就是资源直接耗尽,立马就异常了,都不等待了
解决异常【通常设置】
blockWhenExhausted=true
maxWaitMillis=1000ms
10ms -> 几十ms -> 8个Jedis实例可以提供几百次的调用和使用【分析】
一般来说,8个jedis连接其实够了,可以算一下,假设业务系统有200线程,每秒大概有几百个并发,TPS在每秒几百,4核8g机器,4核,每秒跑几百并发,就差不多了,每秒钟,会有几百次线程拿到一个Jedis,执行一些指令,执行完毕了以后,再把你的Jedis给人家还回去
一般来说,8个jedis连接其实够了,可以算一下,假设业务系统有200线程,每秒大概有几百个并发,TPS在每秒几百,4核8g机器,4核,每秒跑几百并发,就差不多了,每秒钟,会有几百次线程拿到一个Jedis,执行一些指令,执行完毕了以后,再把你的Jedis给人家还回去
优化思路
指令速度
指令速度
增加8个Jedis连接个数
expired过期和LRU淘汰机制
参数配置
maxmemory :最大使用内存
maxmemory-policy :淘汰策略
maxmemory-samples: LRU 算法中采样集合大小
lfu_log_factor:计数器增长因子
lfu-decay-time:LFU 算法计数器衰减因子
expired过期删除策略
惰性删除
查询这条数据的时候,redis惰性检查一下,这条数据是否过期了,如果说过期了,就把他给删除掉,惰性删除,过期数据,下次查询他的时候,才会删除他
定时删除
淘汰策略
noeviction:不淘汰任何数据,内存溢出时直接返回 OOM 错误信息
在redis3.0之前,默认是noeviction;在redis3.0之后(包括3.0),默认淘汰策略则是noeviction
所有数据范围内的淘汰策略
allkeys-random:针对所有 Key 进行随机移除;
allkeys-lru:针对所有 Key 使用 LRU(Least Recently Used) 算法移除;
allkeys-lfu:针对所有 Key 使用 LFU(Least Frequently Used) 算法移除。
根据过期时间的淘汰策略
volatile-ttl:针对带有过期时间的 Key,移除离过期时间最近的 Key(越早过期越先移除)
volatile-lru:针对带有过期时间的 Key,使用 LRU(Least Recently Used) 算法筛选移除;
volatile-lfu:针对带有过期时间的 Key,使用 LFU(Least Frequently Used) 算法筛选移除
volatile-random:随机移除设置了过期时间的 Key;
LRU 策略的核心思想:如果一个数据刚刚被访问,那么这个数据肯定是热数据,还会被再次访问
Redis服务器的linux内核参数
overcommit
设置为1,意思就是说在rdb和aof rewrite进行fork进程的时候,避免 说失败,要让他成功,sysctl vm.overcommit_memory=1
swappiness
这个就是物理内存不够的时候,就swap到磁盘空间,可以避免进程死掉,
但是会导致性能降低,linux有oom killer机制,内存不够就杀死一些进程,
默认值是60,就是比较倾向于进行swap,如果内存不够就会swap
但是会导致性能降低,linux有oom killer机制,内存不够就杀死一些进程,
默认值是60,就是比较倾向于进行swap,如果内存不够就会swap
maxclients
默认的进程可以打开文件句柄是4096,需要调大
/etc/rc.local里加入echo never > /sys/kernel/mm/transparent_hugepage/enabled,关闭大内存页机制,默认开启了,会导致fork的内存消耗变大,而且速度变慢,所以要给他关了,避免fork速度太慢
/etc/rc.local里加入echo never > /sys/kernel/mm/transparent_hugepage/enabled,关闭大内存页机制,默认开启了,会导致fork的内存消耗变大,而且速度变慢,所以要给他关了,避免fork速度太慢
0 条评论
下一页