redis
2020-12-18 18:42:00 0 举报
AI智能生成
redis
作者其他创作
大纲/内容
基本数据结构
strings
字符串
字符串
命令
内部编码
int ,存储8个字节的长整形(long, 2^63-1)
embstr, 代表embstr格式的SDS(Simple Dynamic String 简单动态字符串),存储小于44个字节的字符串
raw, 存储大于44个字节的字符串(3.2版本之前是39字节)
使用方式
全局ID incrby,利用原子性
计数器 INCR方法
限流 INCR方法
位统计 String类型BITCOUNT(1.6.6的bitmap数据结构介绍)
字符是以8位二进制存储的
字符是以8位二进制存储的
应用场景
热点数据缓存,对象缓存, 全页缓存
hashes
散列
散列
命令
内部编码
Redis的HASH本身也是一个KV结构,外层的哈希(Redis KV的实现)只用到了hashtable。当存储hash数据类型时,我们把它叫做内层的哈希。内层的哈希底层可以使用两种数据结构实现:
ziplist: OBJ_ENCODING_ZIPLIST(压缩列表)
hashtable: OBJ_ENCODING_HT(哈希表)
ziplist: OBJ_ENCODING_ZIPLIST(压缩列表)
hashtable: OBJ_ENCODING_HT(哈希表)
应用场景
一个Key对应多条数据
lists
列表
列表
命令
内部编码
早起版本,数据量较小使用ziplist存储,达到临界值时转换为linkedlist进行存储,分别对应OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_LICKEDLIST
3.2版本之后,统一用quicklist来存储。quicklist存储了一个双向链表,每个节点都是一个ziplist.
3.2版本之后,统一用quicklist来存储。quicklist存储了一个双向链表,每个节点都是一个ziplist.
应用场景
用户时间线
类型
存储有序的字符串(从左到右),元素可以重复,可以充当队列和栈的角色
sets
集合
集合
命令
内部编码
Redis用intset或 hashtable 存储set. 如果元素都是整数类型,就用inset存储 如果不是整数类型,就用hashtable(数组+链表)
应用场景
抽奖、点赞、打卡
类型
String类型的无序集合美最大存储数量 2^32 -1(40亿左右)
sorted sets
有序集合
有序集合
应用场景
排行榜
内部编码
ziplist
存储实现条件
元素数量小于128个
所有member的长度都小于64个字节
按照score排序递增来存储,插入的时候需要移动数据
redis.conf:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
skiplist
类型
有序Set集合,每个元素有个score, score相同时,按照key的ASCII码排序
bitmaps
hyperloglogs
geospatial
常用问题
1. 什么是SDS
Redis中字符串的实现,3.2以后版本中,SDS又有多种结构(sds.h): sdshdr5、 sdshdr8、 sdshdr16、
sdshdr32、sdshdr64, 用于存储不同的长度的字符串,分别代表2^5=32byte, 2^8=256byte, 2^16 = 65536byte=64KB, 2^32byte = 4GB。
sdshdr32、sdshdr64, 用于存储不同的长度的字符串,分别代表2^5=32byte, 2^8=256byte, 2^16 = 65536byte=64KB, 2^32byte = 4GB。
2. 为什么Redis要用SDS实现字符串?
C语言本身没有字符串类型(只能用字符数组char[]实现)
1、 使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出
2、 如果要获取字符长度,必须遍历字符数组,时间复杂度是O(n)
3、 C字符串长度的变更会对字符数组做内存重分配
4、 通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全
1、 使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出
2、 如果要获取字符长度,必须遍历字符数组,时间复杂度是O(n)
3、 C字符串长度的变更会对字符数组做内存重分配
4、 通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全
SDS的特点:
1、 不用担心内存溢出问题,如果需要会对SDS进行扩容
2、 获取字符串长度时间复杂度为O(1),因为定义了len属性
3、 通过“空间预分配”(sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存
4、 判断是否结束的标志是len属性(它同样以'\0'结尾是因为这样就可以使用C语言中函数库操作字符串的函数了),可以包含'\0'.
1、 不用担心内存溢出问题,如果需要会对SDS进行扩容
2、 获取字符串长度时间复杂度为O(1),因为定义了len属性
3、 通过“空间预分配”(sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存
4、 判断是否结束的标志是len属性(它同样以'\0'结尾是因为这样就可以使用C语言中函数库操作字符串的函数了),可以包含'\0'.
3. embstr和 raw的区别
embstr的使用只分配一次内存空间(因为RedisObject和SDS是连续的), 而raw需要分配两次内存空间(分别为RedisObject和SDS分配空间)
因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一次,寻找方便
而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个RedisObject和SDS都需要重新分配空间,因此Redis中的embstr实现为只读
因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一次,寻找方便
而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个RedisObject和SDS都需要重新分配空间,因此Redis中的embstr实现为只读
4. int 和 embstr什么时候转化为raw?
当int数据不再是整数,或大小超过了long的范围(2^63-1 = 9223372036854775807)时,自动转化为embstr。
5. 明明没有超过阈值,为什么变成raw了?
对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw在进行修改。因此只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44字节
6.当长度小于阈值是,会还原么?
关于Redis内部编码的转化,都符合以下规律:编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新Set)。
7. 为什么要对底层的数据结构进行一层包装呢?
通过封装,可以根据对象的类型动态地选择存储结构和可以使用的命令,实现节省空间和优化查询速度
什么时候使用ziplist
当hash对象同时满足以下两个条件的时候,使用ziplist编码:
所有的键值对的键和值的字符串长度都小于等于64byte(一个英文字母一个字节)
哈希对象保存的键值对数量小于512个
redis.conf配置:
hash-max-ziplist-value 64//ziplist中最大能存放的值长度
hash-max-ziplist-entries 512//ziplist中最多能存放entry节点数量
hash-max-ziplist-value 64//ziplist中最大能存放的值长度
hash-max-ziplist-entries 512//ziplist中最多能存放entry节点数量
为什么要定义两个哈希表呢?ht[2]
redis的hash默认使用的是ht[0], ht[1]不会初始化和分配空间。
哈希表dictht是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决于它的大小(size属性)和它所保存的节点的数量(used属性)之间的比率:
比率在1:1时(一个哈希表ht只存储一个节点entry),哈希表的性能最好
如果节点数量比哈希表的大小要大很多的话(这个比列用ratio表示,5表示平均一个ht存储5个entry),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在
在这种情况下需要扩容。redis里面的这种操作叫做rehash
比率在1:1时(一个哈希表ht只存储一个节点entry),哈希表的性能最好
如果节点数量比哈希表的大小要大很多的话(这个比列用ratio表示,5表示平均一个ht存储5个entry),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在
在这种情况下需要扩容。redis里面的这种操作叫做rehash
rehash步骤
为字符ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对的数量
扩展: ht[1]的大小为第一个大于等于ht[0].used*2
扩展: ht[1]的大小为第一个大于等于ht[0].used*2
将所有的ht[0]上的节点rehash到ht[1]上, 重新计算hash值和索引,然后放入指定的位置
当ht[0]全部迁移到了ht[1]之后,释放ht[0]的空间,将ht[1]设置为ht[0]表,并创建新的ht[1],为下次rehash做准备
什么时候触发扩容
负载因子
dict.c
static int dict_can_resize = 1
static unsigned int dict_force_resize_ratio = 5
static int dict_can_resize = 1
static unsigned int dict_force_resize_ratio = 5
ratio = used/size,已使用节点与字典大小的比例
dict_can_resize 为1并且dict_force_resize_ratio 已使用节点数和字典大小之间的比率超过1:5, 触发扩容
dict_can_resize 为1并且dict_force_resize_ratio 已使用节点数和字典大小之间的比率超过1:5, 触发扩容
如果基于传统LRU算法实现Redis LRU会有什么问题
需要额外的数据结构存储,消耗内存
Redis LUR对传统的LRU算法进行了改良,通过随机采样来调整算法的精度
如果淘汰策略是LRU,则根据配置的采样值maxmemory_samples(默认是5个),随机从数据库中选择m个key,淘汰其中热度最低的key对应的缓存数据,所以采样参数m配置的数据越大,就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率降低
Redis LUR对传统的LRU算法进行了改良,通过随机采样来调整算法的精度
如果淘汰策略是LRU,则根据配置的采样值maxmemory_samples(默认是5个),随机从数据库中选择m个key,淘汰其中热度最低的key对应的缓存数据,所以采样参数m配置的数据越大,就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率降低
如何找出热度最低的数据?
Redis中所有对象结构都有一个lru字段,且使用了unsigned的低24位,这个字段用来记录对象的热度。对象被创建时会记录lru值。在被访问的时候也会更新lru的值。但是不是获取系统当前的时间戳,而是设置为全集变量server.lruclock的值
数据都是实时持久化到磁盘吗?
由于操作系统的缓存机制,AOF数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓存区的内容写入到AOF文件?
appendfsync everysec
AOF持久化策略(硬盘缓存到磁盘),默认 everysec
no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全
always 表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低
everysec 表示每秒执行一次fsync,可能会导致丢失这1s数据。通常选择everysec,兼顾安全性和效率
appendfsync everysec
AOF持久化策略(硬盘缓存到磁盘),默认 everysec
no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全
always 表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低
everysec 表示每秒执行一次fsync,可能会导致丢失这1s数据。通常选择everysec,兼顾安全性和效率
文件越来越大,怎么办?
由于AOF持久化是Redis不断将写命令记录到AOF文件中,随着Redis不断的进行,AOF的文件会越来越大,文件越大,占用服务器内存越大以及AOF恢复要求时间越长。为了解决这个问题,Redis新增了重写机制,当AOF文件的大小超过锁设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。 可以使用命令bgrewriteaof来重写。
AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件
AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件
auto-aof-rewrite-percentage 100
默认值为100。AOF自动重写配置,当目前aof文件大小超过上一次重写的AOF文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程
默认值为100。AOF自动重写配置,当目前aof文件大小超过上一次重写的AOF文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程
默认是64M。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
重写过程中,AOF文件被更改了怎么办?
当子线程在执行AOF重写时,主进程需要执行以下三个工作:
1.处理命令请求
2.将写命令追加到现有的AOF文件中
3.将写命令追加到AOF重写缓存中
1.处理命令请求
2.将写命令追加到现有的AOF文件中
3.将写命令追加到AOF重写缓存中
no-appendfsync-on-rewrite
在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后在写入,默认为NO,建议修改为yes。 Linux的默认fsync策略是30秒。可能丢失30秒数据。
在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后在写入,默认为NO,建议修改为yes。 Linux的默认fsync策略是30秒。可能丢失30秒数据。
aof-load-truncated
aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在而ext4文件系统没有加上data=ordered选项,出现这种现象。redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。 如果是no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为yes.
aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在而ext4文件系统没有加上data=ordered选项,出现这种现象。redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。 如果是no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为yes.
数据一致性问题
先操作Redis的数据再操作数据库的数据
先操作数据库的数据再操作Redis的数据
高并发问题
用户集中访问的数据
在数据进行分片的情况下,负载不均衡,超过了单个服务器的承受能力
热点数据发现
客户端进行Key的统计
不知道要存多少个Key,可能会发生内存泄漏的问题
会对客户端的代码造成入侵
只能统计当前客户端的热点key
代理层
在代理端实现,比如TwemProxy或者Codis
服务端
Redis 有个monitor的命令,可以监控到所有的Redis执行的命令(JedisMonitor)
Facebook的开源项目redis-faina就是基于这个原理实现的
问题
monitor命令在高并发的场景下,会影响性能,所以不适合长时间使用
只能统计一个Redis节点的热点Key
机器层面
TCP抓包(ELK packetbeat)
缓存雪崩、缓存击穿
含义
Redis的大量热点数据同时过期(失效),因为设置了相同的过期时间,刚好这个时候Redis请求的并发量又很大,就会导致所有的请求落到数据库
解决方案
加互斥所或者使用队列,针对同一个key只允许一个线程到数据库查询
缓存定时预先更新,避免同时失效
通过加随机数,使的key在不同时间过期
缓存永不过期
缓存穿透
含义
查询不存在的数据,导致每次查询都到数据库
解决方案
缓存空数据
缓存特殊字符串
布隆过滤器BitMap
非关系型数据库特点
存储非结构化的数据,比如文本、图片、音频、视频
表与表之间没有关联,可扩展性强
保证数据的最终一致性,遵循BASE(碱)理论。Basically Available(基本可用); Soft-state(软状态);Eventually Consistent(最终一致性)
支持海量数据的存储和高并发的高效读写
支持分布式 ,能够对数据进行分片存储,扩缩容简单
LRU算法
maxmemory设置redis用来存放数据的最大内存大小,一旦超出这个内存大小之后,就会立即使用LRU算法清理掉部分数据
对于64 bit的机器, 如果maxmemory设置为0,那么就默认不限制内存的使用,知道耗尽机器中所有的内存为止;但是对于32 bit的机器,有一个隐式的限制就是3GB
对于64 bit的机器, 如果maxmemory设置为0,那么就默认不限制内存的使用,知道耗尽机器中所有的内存为止;但是对于32 bit的机器,有一个隐式的限制就是3GB
noeviction
如果内存使用达到了maxmemory,client还要继续写入数据,那么就直接报错给客户端
allkeys-lru
就是我们常说的LRU算法,移除掉最近最少使用的那些keys对应的数据
volatile-lru
也是采取LRU算法,但是仅仅针对那些设置了指定存活时间(TTL)的key才会清理掉
allkeys-random
随机选择一些key来删除掉
volatile-random
随机选择一些设置了TTL的key来删除掉
volatile-ttl
移除掉部分keys,选择那些TTL时间比较短的keys
LUR Least Recently Used:最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰
LFU Least Frequently Used 最不常用 4.0版本新增
事务
相关命令
开启事务 multi
执行事务 exec
取消事务 discard
监视 watch
执行顺序
通过multi 的命令开启事务。事务不能嵌套,多个multi命令效果一样
multi执行后,客户端可以继续向服务器发送任意多条命令,这些命令不会立刻被执行,而是被放在一个队列中,当exec命令被调用时,所有队列的命令才会被执行
通过exec的命令执行事务,如果没有执行exec,所有的命令都不会被执行
watch
可以为redis事务提供CAS乐观锁行为,也就是多个线程更新变量的时候,会更原值做比较,只有它没有被其他线程修改的情况下,才更新成新的值
watch可以监视一个或多个key,如果开启事务之后,至少有一个被监视key键在exec执行之前被修改了,那么整个事务都会被取消(key提前过期除外),可以用unwatch取消
问题
exec之前发生错误
事务会被拒绝,也就是队列中所有的命令都不会得到执行
exec之后发生错误
只有错误的命令没有被执行 但是其他命令没有收到影响
持久化机制
RDB快照(Redis DataBase)
简介
redis默认的持久化方案。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。 Redis重启会通过加载dump.rdb文件恢复数据。
触发方式
自动触发
配置规则触发
redis.conf,SNAPSHOTTING
SAVE 900 1 #900秒内至少有一个key被修改(包括添加)
save 300 10 #300秒内至少有10个key被修改
(配置不冲突,只要满足任意一个都会触发)
SAVE 900 1 #900秒内至少有一个key被修改(包括添加)
save 300 10 #300秒内至少有10个key被修改
(配置不冲突,只要满足任意一个都会触发)
shutdown触发 保证服务器正常关闭
flushall, RDB文件是空的,没什么意义
手动触发
重启服务获取迁移数据,这个时候就需要手动触发RDB快照保存
save
save在生成快照的时候会阻塞当前Redis服务器,Redis不能处理其他命令。如果内存中的数据比较多,会造成Redis长时间的阻塞。生产环境不建议使用这个命令
bgsave
执行bgsave时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求
具体操作是Redis进程执行fork操作创建子进程(copy-on-write),RDB持久化过程由子进程负责,完成后自动结束。它不会记录fork之后后续的命令。阻塞只发生在fork阶段,一般时间很短
具体操作是Redis进程执行fork操作创建子进程(copy-on-write),RDB持久化过程由子进程负责,完成后自动结束。它不会记录fork之后后续的命令。阻塞只发生在fork阶段,一般时间很短
用lastsave命令查看最近一次成功生成快照的时间
优势和劣势
优势
RDB是一个非常紧凑(compact)的文件,它保存了redis在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复
生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作
RDB在恢复大数据集时的速度比AOF的恢复速度要快
劣势
RDB方式数据没办法做到实时持久化/秒级持久化。 因为bgsave每次运行都要执行fork操作创建子进程,频繁执行成本过高
在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)
AOF(Append Only File)
简介
Append Only File
默认不开启。AOF采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改Redis数据的命令时,就会把命令写入到AOF文件中。
Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作
默认不开启。AOF采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改Redis数据的命令时,就会把命令写入到AOF文件中。
Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作
配置
redis.conf
appendonly no #开关
appendfilename "appendonly.aof" #文件名
appendonly no #开关
appendfilename "appendonly.aof" #文件名
优势和劣势
优势
AOF持久化方案提供了多种同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1秒的数据而已
劣势
对于具有相同数据的Redis,AOF文件通常会比RDF文件体积更大(RDB存的是数据快照)
虽然AOF提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB比AOF具有更好的性能保证
序列化协议(Redis Serialization Protocol)
特点: 容易实现、解析快、可读性强
存储实现原理
dictEntry
redis 是K、V的数据库,它是通过hashtable来实现的(外层的Hash),每个键值都对应一个dictEntry
redis 是K、V的数据库,它是通过hashtable来实现的(外层的Hash),每个键值都对应一个dictEntry
typedef struct dictEntry {
void *key; //key void*表示任意类型指针
union { //联合体中对于数字类型提供了专门的类型优化
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; //next指针
} dictEntry;
void *key; //key void*表示任意类型指针
union { //联合体中对于数字类型提供了专门的类型优化
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; //next指针
} dictEntry;
K存储到:SDS
Value存储到:redisObject
typedef struct redisObject {
unsigned type:4; /*对象的类型, 包括:OBJ_STRING, OBJ_LIST, OBJ_HASH, OBJ_SET, OBJ_ZSET*/
unsigned encoding:4; /*具体的数据结构*/
unsigned lru:REDIS_LRU_BITS; /* 24位, 对象最后一次被命令程序访问的时间,与内存回收有关*/
int refcount;/*引用计数。当refcount为0 的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
void *ptr;/*指向对象实际的数据结构*/
} robj;
unsigned type:4; /*对象的类型, 包括:OBJ_STRING, OBJ_LIST, OBJ_HASH, OBJ_SET, OBJ_ZSET*/
unsigned encoding:4; /*具体的数据结构*/
unsigned lru:REDIS_LRU_BITS; /* 24位, 对象最后一次被命令程序访问的时间,与内存回收有关*/
int refcount;/*引用计数。当refcount为0 的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
void *ptr;/*指向对象实际的数据结构*/
} robj;
next:指向下一个dictEntry
ziplist 压缩列表(ziplist.c的文件头部注释)
The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop operations on either side of the list in O(1) time.
The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop operations on either side of the list in O(1) time.
ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面
从宏观上看,ziplist的内存结构如下:
<zlbytes><zltail><zllen><entry>...<entry><zlend>
<zlbytes><zltail><zllen><entry>...<entry><zlend>
<zlbytes>: 32bit,表示ziplist占用的字节总数(也包括<zlbytes>本身占用的4个字节)
<zltail>: 32bit,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。<zltail>的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作
<zllen>: 16bit, 表示ziplist中数据项(entry)的个数。zllen字段因为只有16bit,所以可以表达的最大值为2^16-1。这里需要特别注意的是,如果ziplist中数据项个数超过了16bit能表达的最大值,ziplist仍然可以来表示。那怎么表示呢?这里做了这样的规定:如果<zllen>小于等于2^16-2(也就是不等于2^16-1),那么<zllen>就表示ziplist中数据项的个数;否则,也就是<zllen>等于16bit全为1的情况,那么<zllen>就不表示数据项个数了,这时候要想知道ziplist中数据项总数,那么必须对ziplist从头到尾遍历各个数据项,才能计数出来
<entry>: 表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它自己的内部结构
<prevrawlen><len><data>
<prevrawlen>: 表示前一个数据项占用的总字节数。这个字段的用处是为了让ziplist能够从后向前遍历(从后一项的位置,只需向前偏移prevrawlen个字节,就找到了前一项)。这个字段采用变长编码
它有两种可能,或者是1个字节,或者是5个字节
它有两种可能,或者是1个字节,或者是5个字节
如果前一个数据项占用字节数小于254,那么<prevrawlen>就只用一个字节来表示,这个字节的值就是前一个数据项的占用字节数
如果前一个数据项占用字节数大于等于254,那么<prevrawlen>就用5个字节来表示,其中第1个字节的值是254(作为这种情况的一个标记),而后面4个字节组成一个整型值,来真正存储前一个数据项的占用字节数
<len>: 表示当前数据项的数据长度(即<data>部分的长度)。也采用变长编码
<zlend>: ziplist最后1个字节,是一个结束标记,值固定等于255
hashtable(dict)
称为字典(dictionary),它是一个数组+链表的结构
称为字典(dictionary),它是一个数组+链表的结构
dictEntry进行了多层的封装
dictEntry放到了dictht(hashtable里面)
typedef struct dictht {
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小,即哈希表数组大小
unsigned long sizemask; //哈希表大小掩码,总是等于size-1,主要用于计算索引
unsigned long used; //已使用节点数,即已使用键值对数
}dictht;
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小,即哈希表数组大小
unsigned long sizemask; //哈希表大小掩码,总是等于size-1,主要用于计算索引
unsigned long used; //已使用节点数,即已使用键值对数
}dictht;
ht放到了 dict里面:
typedef struct dict {
dictType *type; //类型特定函数
void *privdata; //私有数据
dictht ht[2]; //2个哈希表,哈希表负载过高进行rehash的时候才会用到第2个哈希表
int rehashidx; //rehash目前进度,当哈希表进行rehash的时候用到,其他情况下为-1
}dict;
dictType *type; //类型特定函数
void *privdata; //私有数据
dictht ht[2]; //2个哈希表,哈希表负载过高进行rehash的时候才会用到第2个哈希表
int rehashidx; //rehash目前进度,当哈希表进行rehash的时候用到,其他情况下为-1
}dict;
从最底层到最高层流程
dictEntry-dictht-dict-OBJ-ENCODEING-HT
quicklist 快速列表
是ziplist和linkedlist的结合体
是ziplist和linkedlist的结合体
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
}
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
}
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
...
}
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
...
}
zl指向的就是ziplist
skiplist 跳表
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist
Level的算法
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
0xFFFF是 65535
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
基本命令
查看类型 type [key]
redis 的优势
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速
数据存储结构的优势
String类型通过 int、SDS(simple dynamic string)作为结构存储,int用来存放整型数据,sds存放字 节/字符串和浮点型数据。在C的标准字符串结构下进行了封装,用来提升基本操作的性能,同时也充分利用已有的 C的标准库,简化实现逻
表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素或者获得列表的某一个片段。列表类型内部使用双向链表实现,所以向列表两端添加元素的时间复杂度为O(1), 获取越接近两端的元素速度就越 快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是很快的
Map是K-V模式数据结构,查询速度
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
使用多路I/O复用模型,非阻塞IO
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
缓存过期的处理机制
惰性删除(延迟失效机制)
Key过期的时候,不会立即删除缓存。而是当缓存被访问的时候,先检查key是否到期,若到期则删除,同时向用户返回nil。这种处理方式能尽量减少CPU的占用,但是如果有大量的key过期,并且这些缓存永远都不会被用户访问的情况下,会存在内存回收的问题,那么内存浪费严重
get 堆栈:
getCommand
-> getGenericCommand
-> lookupKeyReadOrReply
-> lookupKeyRead
-> expireIfNeede
getCommand
-> getGenericCommand
-> lookupKeyReadOrReply
-> lookupKeyRead
-> expireIfNeede
关键的地方是expireIfNeed,Redis对key的get操作之前会判断key关联的值是否失效,我们看看expireIfNeeded的流程,大致如下:
1、从expires中查找key的过期时间,如果不存在说明对应key没有设置过期时间,直接返回。
2、如果是slave机器,则直接返回,因为Redis为了保证数据一致性且实现简单,将缓存失效的主动权交给Master机器,slave机器没有权限将key失效。
3、如果当前是Master机器,且key过期,则master会做两件重要的事情:1)将删除命令写入AOF文件。2)通知Slave当前key失效,可以删除了。
4、master从本地的字典中将key对于的值删除。
1、从expires中查找key的过期时间,如果不存在说明对应key没有设置过期时间,直接返回。
2、如果是slave机器,则直接返回,因为Redis为了保证数据一致性且实现简单,将缓存失效的主动权交给Master机器,slave机器没有权限将key失效。
3、如果当前是Master机器,且key过期,则master会做两件重要的事情:1)将删除命令写入AOF文件。2)通知Slave当前key失效,可以删除了。
4、master从本地的字典中将key对于的值删除。
流程
惰性删除策略流程:
1. 在进行get或setnx等操作时,先检查key是否过期;
2. 若过期,删除key,然后执行相应操作; 若没过期,直接执行相应操作;
1. 在进行get或setnx等操作时,先检查key是否过期;
2. 若过期,删除key,然后执行相应操作; 若没过期,直接执行相应操作;
定期删除(主动失效机制)
创建守护线程,守护线程每隔一段时间对key进行一次扫描,发现过期的key则将缓存删除。这种处理方案也许是内存和CPU之间比较好的平衡,它既不会像定时器方案一样创建大量的定时器占用内存和CPU,也不会像惰性删除那样存在内存泄露的潜在问题。但是这种方案存在最大的问题是用户仍然可能会访问到过期的缓存,并且这种方案难点在于守护线程触发频率的选择上,如果频率太高,并且存在数百万甚至上千万key的时候,那么守护线程占用CPU时间也会特别长可能会影响用户查询,如果执行频率太低,失效的缓存会占用太多的内存
定时器(定期过期)
如果key被设置了过期时间,那么便为key创建一个定时器,在缓存键到期的时候,触发一个任务,将该key对应的缓存删除。虽然能保证key对应的键在到达指定的时间时删除并释放缓存占用的内存,但是这种处理方案会创建非常多的定时器,定时器本身会占用非常多的资源,如果遇上缓存雪崩,定时任务在同一时间点被触发,那么在这一时间点会占用非常多的CPU资源
Java客户端
Jedis
工作模式
单节点
Jedis
分片
ShardedJedisPool
哨兵
JedisSentinelPool
集群
JedisClusterConnectionHandler
问题
如何存储slot和Redis连接池的关系
程序启动初始化集群环境,读取配置文件中的节点配置,无论是主从或者多少个,只拿第一个,获取Redis连接实例
用获取的redis连接实例执行clusterSlots()方法,实际执行redis服务端cluster slots命令,获取虚拟槽信息( List<Object> slots = jedis.clusterSlots();), 该集合的基本信息为[long,long,List,List],第一、二个元素是该节点负责槽点的起始位置,第三个元素是主节点信息,第四个元素为主节点对应的从节点信息。该list的基本信息为[string,int,string],第一个为host信息,第二个为port信息,第三个为唯一ID
获取有关节点的槽点信息后,调用getAssignedSlotArray(slotinfo)来获取所有的槽点值
在获取主节点的地址信息,调用generateHostAndPort(hostinfo)方法,生成一个ostAndPort对象
再根据节点地址信息来设置节点对应的JedisPool,即设置Map<String,JedisPool> nodes的值
存取值
把key作为参数,执行CRC16算法,获取key对应的slot值
通过该slot值,去slots的map集合中获取jedisPool实例
通过jedisPool实例获取jedis实例,最终完成redis数据存取工作
请求模式
Client
Pipeline
jedis-pipeline的client-buffer限制: 8192bytes,客户端堆积的命令超过8192bytes时,会发送给服务端
事务
lettuce
特点
Lettuce是一个可伸缩的线程安全的Redis客户端,支持同步、异步和响应式(Reactive)模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题
Lettuce是Spring Boot 2.x默认的客户端,替换了Jedis。
它基于Netty框架构建,支持Redis的高级功能,如Pipeline、发布订阅、事务、Sentinel、集群、支持连接池
Redisson
本质
Redisson是一个在Redis的基础上实现的Java驻内存数据网络(In-Memory-Data Grid),提供了分布式和可扩展的Java数据结构
特点
基于Netty实现,采用非阻塞IO,性能高
支持异步请求
支持连接池、Pipeline、LUA Scripting、Redis Sentinel、Redis Cluster
不支持事务,官方建议以LUA Scripting 代替事务
主从、哨兵、集群都支持、Spring也可以配置和注入RedissonClient
分布式锁
redissonClient.getLock
获得RLock之后,只需要一个tryLock方法,里面有三个参数:
1. watiTime: 获取锁的最大等待时间,超过这个时间不再尝试获取锁
2. leaseTime: 如果没有调用unlock,超过了这个时间会自动释放锁
3. TimeUnit: 释放时间单位
1. watiTime: 获取锁的最大等待时间,超过这个时间不再尝试获取锁
2. leaseTime: 如果没有调用unlock,超过了这个时间会自动释放锁
3. TimeUnit: 释放时间单位
Lua
缓存Lua脚本
为什么要缓存
脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给Redis服务端,会产生比较大的网络开销,为了解决这个问题,Redis提供了EVALSHA命令,允许开发者通过脚本内容的SHA1摘要来执行脚本
如何缓存
Redis在执行script load命令时会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:“NOSCRIPT No matching script Please use EVAL”
脚本超时
lua-time-limit 5000 redis.conf配置文件中
运行超时过这一限制后,Redis将开始接受其他命令单不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误
script kill
如果当前执行的LUA脚本对Redis的数据进行了修改(SET、DEL等),那么通过Script kill 命令是不能终止脚本运行的
shutdown nosave
强行终止redis
不会进行持久化操作
分布式方案
Redis主从复制(replication)
配置
redis.conf
slaveof 从节点IP port
slaveof 从节点IP port
主从切换,配置重写
replicaof 从节点IP port
介绍
从节点不能写入数据(只读),只能从master节点同步数据。get成功 set失败
复制原理
连接阶段
slave node 启动时,会在自己本地保存master node的信息,包括master node 的 host 和 IP
slave node内部有个定时任务replicationCron(源码replication.c),每隔1秒钟检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接,如果连接成功,从节点为该socket建立一个专门处理复制公告的文件事件处理器,负责后续的复制工作,如接受RDB文件、接受命令传播等
数据同步阶段
master node第一次执行全量复制,通过bgsave命令在本地生成一份RDN快照,将RDB快照文件发给slave node(如果超时会重连,可以调大repl-timeout的值) slave node 首先清除自己的旧数据,然后用RDB文件加载数据
命令传播阶段
master node持续将写命令异步复制给slave node,延迟是不可避免的,只能通过优化网络
repl-disable-tcp-nodelay no
repl-disable-tcp-nodelay no
不足
RDB文件过大的情况下,同步非常耗时
在一主一从或者一主多从的情况下,如果主服务器挂掉了,对外提供的服务就不可用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换为主服务器,这个比较费时费力,还会造成一定时间的服务不可用
Sentinel
原理
创建一台监控服务器来监控所有的redis服务节点的状态,比如master节点超过一定时间没有给监控服务器发送心跳报文,就把master标记为下线,然后把某一个slave变成master.应用每一次都是从这个监控服务器拿到master的地址
监控流程
服务下线
Sentinel默认以每秒钟1次的频率向Redis服务节点发送PING命令。 如果在down-after-milliseconds 内都没有收到有效回复,Sentinel会将该服务器标记为下线(主观下线)
# sentinel.conf
sentinel down-after-milliseconds <master-name><milliseconds>
# sentinel.conf
sentinel down-after-milliseconds <master-name><milliseconds>
Sentinel节点会继续询问其他的Sentinel节点,确认这个节点是否下线,如果多数Sentinel节点都认为master下线,master才真正确认被下线(客观下线),这个时候就需要重新选举master
故障转移
第一步就是在Sentinel集群选择一个Leader,由leader完成故障转移流程。Sentinel通过Raft算法,实现Sentinel选举
Raft算法
领导选举
数据复制
Sentinel的Raft算法和Raft论文不同
master客观下线触发选举,而不是过了election timeout时间开始选举
Leader并不会把自己成为Leader的消息发给其他的Sentinel。其他Sentinel等待Leader从slave选出master后,检测到新的master正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。
选举因素
断开连接时长
如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权
优先级排序
拥有选举权,就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高
复制数量
如果优先级相同。就看谁从master中复制的数据最多(复制偏移量最大),选最多的那个
进程ID
如果复制数量也相同,就选择进程ID最小的那个
功能总结
监控
Sentinel会不断检查主服务器和从服务器时候正常运行
通知
如果某一个被监控的实例出现问题,Sentinel可以通过API发出通知
自动故障转移(failover)
如果主服务器发生故障,Sentinel可以启动故障转移过程。把某台服务器升级为主服务器,并发出通知
配置管理
客户端连接到Sentinel,获取当前的Redis主服务器的地址
实战
Sentinel配置
Sentinel也需要做集群部署,集群中至少需要是哪个Sentinel实例(推荐奇数个,防止脑裂)
protected-mode 是否允许外部网络访问
dir snetinel的工作目录
sentinel monitor snetinel监控的redis节点
down-after-milliseconds(毫秒) master宕机多久,才会被Sentinel主观认为下线
sentinel failover-timeout(毫秒)
同一个sentinel对同一个master两次failover之间的间隔时间
当一个slave从一个错误的master哪里同步数据开始计算时间,直到slave被纠正为向正确的master哪里同步数据时
当想要取消一个正在进行的failover所需要的时间
当进行failover时,配置所有的slaves指向新的master所需的最大时间
parallel-syncs
这个配置项指定了在 发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字 越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为1来保证每次只有一个slave处于不能处理命令请求的状态
不足
主从切换的过程中会丢失数据,因为只有一个master
只能单点写,没有解决水平扩容的问题
如果数据量非常大,这个时候我们需要多个master-slave的group,把数据分布到不同的group中
分片
客户端Sharding
代理Proxy
Twemproxy
Codis
Redis Cluster
0 条评论
下一页
为你推荐
查看更多