Redis源码
2024-07-20 18:40:07 0 举报
AI智能生成
Redis
作者其他创作
大纲/内容
线程模型
seconds秒数范围内,执行了times指定的操作数量,|在900s内执行了超过50次操作,会触发,生成rdb快照【bgsave,后台去进行rdb快照持久化的过程】
save seconds times
参数配置
1 fork子进程
2 将完整的内存快照写入磁盘
每次执行很耗时
问题
业务系统对redis访问压力是最低的时候执行再上传到hdfs上去,做冷备份
场景
rdb定时快照
写命令追加到aof_buf缓冲区
根据刷盘策略去进行刷盘,aof文件越来越大,定期对aof文件进行rewrite,缩小aof文件
每次你往redis里写入一个命令,他都把这个命令,异步追加到aof日志文件里
默认是关闭的
appendonly no
AOF启动的配置
always: 每次写入一条数据
everysec: 每秒将os cache中的数据fsync到磁盘【常用,生产环境】
no: 将数据写入os cache就撒手不管了,然后后面os自己会时不时有自己的策略将数据刷入磁盘,不可控了。
AOF的fsync策略配置
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)用新的日志文件替换掉旧的日志文件
流程
AOF rewrite 策略配置
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】
选择服务器压力小的时候执行如凌晨
内存不够,可以选择扩容
建议
持久化fork操作与aof阻塞优化
持久化机制
struct sdshdr { int len; // 已使用字节数 = 字符串长度 int free; // 未使用字节数 char buf[]; // 字符串本身内容}对于字符串长度的获取,直接读取len就可以了,O(1)复杂度
结构模型
时间复杂度优化
对于c语言来说,读取的时候,一定要按照\\0来截断。而对于二进制数据来说,图片、视频、音频、压缩文件,这种数据都是以一大堆的二进制串来存储的,里面肯定会有很多\\0空字符因此原生的c语言字符串没法安全的存储二进制数据
原因
len有2个作用,直接读取len,strlen,O(1)对于我们来说,redis里的sds存储二进制的数据,图片、音视频,就算buf里包含了很多\\0空字符,根据len来读取,不是根据\\0来截断,redis sds可以实现二进制安全性,安全的保存二进制数据
解决
二进制的安全存储
sds长度小于1mb,就把free设置为跟len一样,做一个双倍预分配,如果扩容后len大于1mb了,就把free设置为1mb
free可以实现内存预分配【空间换时间】
数组里先保留free,等到你需要释放的时候,可以调用sds API释放
惰性释放
解决缓冲区溢出
string
基本数据结构
sds【simple dynaic string简单动态字符串】
list
有序列表
typedef struct listNode { struct listNode * prev; struct listNode * next; void * value;} listNode;
无序
头和尾的获取时间复杂度都是O(1)
链表元素个数的时间复杂度是O(1)
lrange -> 遍历 -> O(n)
时间复复杂度
双向链表数据结构
hash
set
typedef struct 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;
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 = dict->tpe->hashFunction(key);
算法
不同的hash值算出来的数组里的index位置,有可能是一样的
冲突问题
基于dictEntry的单向链表,来挂载一个位置的多个key
hash算法、key冲突与rehash
搞了一个新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负载不会太高【可触发】
触发条件
数组扩容流程
hashtable
允许你根据你自己制定的规则,进行自定义的排序
zset
typedef struct 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以上
生产配置
配置和实现
slowlog get n,指定获取多少条慢查询日志
命令
监控慢查询
网络耗时图
基于pipeline和lua的多指令性能优化
默认是8个Jedis连接【可根据实际情况配置】
如果8个连接都被占用了,默认会等待maxWaitMillis时长来等待,一直等待不到,那就异常了,could not get a resource
如果设置了blockWhenExhausted=false,那就是资源直接耗尽,立马就异常了,都不等待了
could not get a resource
blockWhenExhausted=true
maxWaitMillis=1000ms
解决异常【通常设置】
一般来说,8个jedis连接其实够了,可以算一下,假设业务系统有200线程,每秒大概有几百个并发,TPS在每秒几百,4核8g机器,4核,每秒跑几百并发,就差不多了,每秒钟,会有几百次线程拿到一个Jedis,执行一些指令,执行完毕了以后,再把你的Jedis给人家还回去
10ms -> 几十ms -> 8个Jedis实例可以提供几百次的调用和使用【分析】
JedisPool参数设置
客户端连接池的生产环境优化
指令速度
增加8个Jedis连接个数
优化思路
性能调优实践
maxmemory :最大使用内存
maxmemory-policy :淘汰策略
maxmemory-samples: LRU 算法中采样集合大小
lfu_log_factor:计数器增长因子
lfu-decay-time:LFU 算法计数器衰减因子
查询这条数据的时候,redis惰性检查一下,这条数据是否过期了,如果说过期了,就把他给删除掉,惰性删除,过期数据,下次查询他的时候,才会删除他
惰性删除
定时删除
expired过期删除策略
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 策略的核心思想:如果一个数据刚刚被访问,那么这个数据肯定是热数据,还会被再次访问
根据过期时间的淘汰策略
淘汰策略
expired过期和LRU淘汰机制
设置为1,意思就是说在rdb和aof rewrite进行fork进程的时候,避免 说失败,要让他成功,sysctl vm.overcommit_memory=1
overcommit
这个就是物理内存不够的时候,就swap到磁盘空间,可以避免进程死掉,但是会导致性能降低,linux有oom killer机制,内存不够就杀死一些进程,默认值是60,就是比较倾向于进行swap,如果内存不够就会swap
swappiness
默认的进程可以打开文件句柄是4096,需要调大
maxclients
/etc/rc.local里加入echo never > /sys/kernel/mm/transparent_hugepage/enabled,关闭大内存页机制,默认开启了,会导致fork的内存消耗变大,而且速度变慢,所以要给他关了,避免fork速度太慢
Redis服务器的linux内核参数
Redis源码
0 条评论
回复 删除
下一页