Redis是一个开源的,内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。它是key,value结构的存储系统,它支持多种数据类型的数据结构,如字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets)等。它可以通过Redis哨兵和自动分区提供高可用性。引入Redis缓存机制可以有效的降低用户访问物理设备的频次,提高响应速度。
为什么要用Redis?
Q: redis中存储数据是以key-value形式, 那么为什么不用java的map 容器代替呢?
答:
- java仅实现的是本地缓存, 如果有多台机器的话, 都需要各自保存一份数据, 不具有一致性
- Redis是分布式缓存, 每个机器都共享一份数据, 具有一致性
- java的map不是专业做缓存的, JVM的内存太大的时候容易挂掉, 且随着JVM结束而销毁, 一般用于临时存储数据, 且如果需要过期策略等机制都需要额外手写!
- redis是专业缓存的, 可以有几十个G做缓存, 且能存入硬盘, 一旦重启也可将数据恢复, 自带丰富的数据结构, 和过期策略等机制
- Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了
- Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了
- Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里
- Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象
- Redis 缓存有过期机制,Map 本身无此功能
- Redis 有丰富的 API,Map 就简单太多了
缓 解 数 据 库 压 力 !
1s读 10000次 写5000次
1.操作内存 :直接操作内存速度会相对较快。
2.5种数据结构的存在
SDS : 它的定义是不单单是一个char 数组构成,每个sds都会比它真实占用的字符长度都长,通过一个空闲标识符表示sds当前空闲字符有多少,如此设计,在一定长度范围的内的字符串都可以使用此sds,而且不会频繁的进行内存分配,直到此sds不能容纳分配的字符串,如果遇到这种情况情况,才需要进行扩扩容;这是redis的最基础的,所有的redis k-v 中的字符串都是依托于sds;
dict : 类似java中的hashmap( 数组, 负载因子, hash算法 )
不过dict 有两个数组 其中一个用作扩容 ( dict的扩容是渐进式的,不会影响当前使用, 一点点迁移, 直到迁移完成 不像hashmap是new 一个更大的然后全部移走 )
3.跳跃表 : 跳跃表使用了历史上最屌的算法:抛硬币
在跳跃表是由N层链表组成,最底层是最完整的的数据,每次数据插入,率先进入到这个链表(有序的),插入完成后,通过抛硬币的算法,判断是否将数据向上层跑,如果是1的话,就抛到上层,然后继续抛硬盘,判断是否继续向上层抛,直到抛出了0结束整个操作,每抛到一层的时候,如果当前层没有数据,就构造一个链表,将数据放进去,然后使用指针指向来源地址,就这样依次类推,形成了跳跃表,每次查询,从最上层遍历查询,如果找到就返回结果,否则就在此层找到最接近查询的值,将查询操作移到另外一层,就是刚才说到来源地址,所在层,重复查询
4.使用单线程+多路 I/O 复用模型
但要值得注意的是:Redis并没有直接使用上图的数据结构来实现key-value数据库,而是基于这些数据结构创建了一个对象系统。
简单来说:Redis使用对象来表示数据库中的键和值。每次我们在Redis数据库中新创建一个键值对时,至少会创建出两个对象。一个是键对象,一个是值对象。
Redis中的每个对象都由一个RedisObject
结构来表示:
typedef struct redisObject{
// 对象的类型
unsigned type 4:;
// 对象的编码格式
unsigned encoding:4;
// 指向底层实现数据结构的指针
void * ptr;
//.....
}robj;
简单来说就是Redis对key-value
封装成对象,key是一个对象,value也是一个对象。每个对象都有type(类型)、encoding(编码)、ptr(指向底层数据结构的指针)等 来表示。
简单动态字符串(Simple dynamic string,SDS)
Redis中的字符串跟C语言中的字符串,是有点差距的。
Redis使用sdshdr结构来表示一个SDS值:
struct sdshdr{
// 字节数组,用于保存字符串
char buf[];
// 记录buf数组中已使用的字节数量,也是字符串的长度
int len;
// 记录buf数组未使用的字节数量
int free;
}
使用SDS的好处
SDS与C的字符串表示比较
Redis中的链表是怎么实现的:
使用listNode结构来表示每个节点:
typedef strcut listNode{
//前置节点
strcut listNode *pre;
//后置节点
strcut listNode *pre;
//节点的值
void *value;
}listNode
使用listNode是可以组成链表了,Redis中使用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
Redis链表的特性
Redis的链表有以下特性:
void *
指针来保存节点值,可以保存各种不同类型的值在Redis中,key-value
的数据结构底层就是哈希表来实现的。对于哈希表来说,我们也并不陌生。在Java中,哈希表实际上就是数组+链表的形式来构建的。下面我们来看看Redis的哈希表是怎么构建的吧。
在Redis里边,哈希表使用dictht结构来定义:
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemark;
//哈希表已有节点数量
unsigned long used;
}dictht
从结构上看,我们可以发现:Redis实现的哈希表和Java中实现的是类似的。只不过Redis多了几个属性来记录常用的值:sizemark(掩码)、used(已有的节点数量)、size(大小)。
同样地,Redis为了更好的操作,对哈希表往上再封装了一层(参考上面的Redis实现链表),使用dict结构来表示:
typedef struct dict {
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不进行时,值为-1
int rehashidx;
}dict;
//-----------------------------------
typedef struct dictType{
//计算哈希值的函数
unsigned int (*hashFunction)(const void * key);
//复制键的函数
void *(*keyDup)(void *private, const void *key);
//复制值得函数
void *(*valDup)(void *private, const void *obj);
//对比键的函数
int (*keyCompare)(void *privdata , const void *key1, const void *key2)
//销毁键的函数
void (*keyDestructor)(void *private, void *key);
//销毁值的函数
void (*valDestructor)(void *private, void *obj);
}dictType
所以,最后我们可以发现,Redis所实现的哈希表最后的数据结构是这样子的:
从代码实现和示例图上我们可以发现,Redis中有两个哈希表:
Redis中哈希算法和哈希冲突跟Java实现的差不多,它俩差异就是:
rehash 就是 重新的分配元素并加入新的桶内,这称为rehash 就是扩容
下面来具体讲讲Redis是怎么rehash的,因为我们从上面可以明显地看到,Redis是专门使用一个哈希表来做rehash的。这跟Java一次性直接rehash是有区别的。
在对哈希表进行扩展或者收缩操作时,reash过程并不是一次性地完成的,而是渐进式地完成的。
Redis在rehash时采取渐进式的原因:数据量如果过大的话,一次性rehash会有庞大的计算量,这很可能导致服务器一段时间内停止服务。
Redis具体是rehash时这么干的:
跳跃表(shiplist)是实现sortset(有序集合)的底层数据结构之一!
Redis的跳跃表实现由zskiplist
和 zskiplistNode
两个结构组成。其中zskiplist保存跳跃表的信息(表头,表尾节点,长度),zskiplistNode则表示跳跃表的节点。
按照惯例,我们来看看zskiplistNode跳跃表节点的结构是怎么样的:
typeof struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
typeof struct zskiplist {
// 表头节点,表尾节点
struct skiplistNode *header,*tail;
// 表中节点数量
unsigned long length;
// 表中最大层数
int level;
} zskiplist;
最后我们整个跳跃表的示例图如下:
整数集合(intset)是set(集合)的底层数据结构之一。当一个set(集合)只包含整数值元素,并且元素的数量不多时,Redis就会采用整数集合(intset)作为set(集合)的底层实现。
整数集合(intset)保证了元素是不会出现重复的,并且是有序的(从小到大排序),intset的结构是这样子的:
typeof struct intset {
// 编码方式
unit32_t encoding;
// 集合包含的元素数量
unit32_t lenght;
// 保存元素的数组
int8_t contents[];
} intset;
压缩列表(ziplist)是 List 和 Hash 的底层实现之一。
如果list的每个都是小整数值,或者是比较短的字符串,压缩列表(ziplist)作为list的底层实现。
压缩列表(ziplist)是Redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序性数据结构。
压缩列表结构图例如下:
下面我们看看节点的结构图:
压缩列表从表尾节点倒序遍历,首先指针通过 zltail
偏移量指向表尾节点,然后通过指向节点记录的前一个节点的长度依次向前遍历访问整个压缩列表。
在上面的图我们知道string类型有三种编码格式:
int:整数值,这个整数值可以使用long类型来表示
如果是浮点数,那就用embstr或者raw编码。具体用哪个就看这个数的长度了
embstr:字符串值,这个字符串值的长度小于32字节
raw:字符串值,这个字符串值的长度大于32字节
embstr和raw的区别:
raw分配内存和释放内存的次数是两次,embstr是一次
embstr编码的数据保存在一块连续的内存里面
编码之间的转换:
在上面的图我们知道list类型有两种编码格式:
ziplist:字符串元素的长度都小于64个字节&&总数量少于512个
linkedlist:字符串元素的长度大于64个字节||总数量大于512个
ZipList 编码的列表结构:
redis > RPUSH numbers 1 "three" 5
(integer) 3
LinkedList编码的列表结构:
LinkedList编码的列表结构
编码之间的转换:
ziplist
编码的,如果保存的数据长度太大或者元素数量过多,会转换成 linkedlist 编码的。在上面的图我们知道Hash类型有两种编码格式:
ziplist编码的哈希结构:
压缩列表:
HashTable编码的哈希结构:
编码之间的转换:
在上面的图我们知道set类型有两种编码格式:
&&
总数量小于512||
总数量大于512intset编码的集合结构:
hashtable编码的集合结构:
编码之间的转换:
在上面的图我们知道set类型有两种编码格式:
&&
总数量小于128||
总数量大于128ziplist编码的有序集合结构:
压缩列表:
skiplist编码的有序集合结构:
有序集合(sortset)对象同时采用SkipList和哈希表来实现:
编码之间的转换:
可以使用debug object key_name
来查看数据类型内部结构
服务器在执行某些命令的时候,会先检查给定的键的类型能否执行指定的命令。
比如我们的数据结构是sortset,但你使用了list的命令。这是不对的,服务器会检查一下我们的数据结构是什么才会进一步执行命令
Redis的对象系统带有引用计数实现的内存回收机制。
对象不再被使用的时候,对象所占用的内存会释放掉
Redis会共享值为0到9999的字符串对象
对象会记录自己的最后一次被访问时间,这个时间可以用于计算对象的空转时间。
Redis之所以用跳表来实现有序集合
插入、删除、查找以及迭代输出有序序列这几个操作,红黑树都能完成,时间复杂度跟跳表是一样的。但是按照区间来查找数据,红黑树的效率就没有跳表高
跳表更加灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗
在实现方面,红黑树实现更加复杂,跳跃表实现比较简单,也更加直观,更加灵活。
红黑树/平衡二叉树这种树形结构,每次每隔两个节点建一个索引,而跳跃表可以多个节点,不限于两个节点。
跳跃表插入或删除操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,而平衡二叉树则需要左旋或者右旋实现平衡。
从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
用户高并发环境下访问数据库和缓存中都不在的数据称为穿透现象。
解决方法:
布隆过滤器: **是一个很长的二进制向量和一系列随机映射函数。**可以用于检索一个元素是否在一个集合中,优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
用法: 由二进制向量,hash函数组合.
作用: 判断一个元素是否存在于集合中.
优点: 占用空间更小/效率更高
缺点: 有一定的误判率(hash碰撞), 删除困难.由于hash碰撞问题,可能有多个key有相同的位置,可以得出结论 :
布隆过滤器认为数据存在,那么数据可能存在,如果认为数据不存在,那么一定不存在。优化方法 : 可以用扩容二进制向量位数和增加hash函数的个数来降低hash碰撞的几率。
布隆过滤器应用场景
说明:当用户查询服务器时,首先查询布隆过滤器,如果查询存在该数据,则执行后续的流程,
如果查询没有该数据,则直接返回.无需执行后续流程.
布隆过滤器算法介绍
关于布隆过滤器优化说明
根据hash原则 数据存在hash碰撞的概率. 则使用布隆过滤器容器造成误判. 如何解决?
**优化hash碰撞概率-增加hash函数个数 ** 优化hash碰撞概率-增加二进制向量
当某一个热点数据在缓存中突然失效,导致大量用户直接访问数据库,导致并发压力过高造成异常,这种情况称为击穿.
解决方法:
在缓存服务器中,由于大量缓存数据失效导致用户访问的命中率过低,导致直接访问数据库。
解决方法:
设定多级缓存,设定超时时间使用随机算法。
Redis中将数据都保存到了内存中,但是内存的特点断电及擦除. 为了保证redis中的缓存数据不丢失,则需要将内存数据定期进行持久化操作.
持久化: 将内存数据,写到磁盘中.
特点:
备份命令:
save
会阻塞用户操作bgsave
异步的方式进行持久化操作 不会阻塞.关于持久化配置
save 900 1
: 900秒内,用户执行了一次更新操作时,那么就持久化一次save 300 10
:300秒内,用户执行了10次更新操作. 那么就持久化一次save 60 10000
:60秒内,用户执行了10000次的更新操作,则持久化一次.save 1 1
: 1秒内 1次更新 持久化一次!! 性能特别低.关于持久化文件名称设定
默认的条件下,持久化文件名称 dump.rdb
文件存储目录
./
代表当前文件目录. 意义使用绝对路径的写法.
特点 :
AOF配置
开启AOF模式
持久化策略 :
always
: 用户更新一次,则持久化一次.
everysec
: 每秒持久化一次 效率更高
no
: 不主动持久化. 操作系统有关. 几乎不用.
企业策略:既要满足效率,又不能丢失数据.
主从结构:主机RDB,从机AOF.
场景1: redis中的服务只开启了默认的持久策略 RDB模式.
解决方案:
关闭现有的redis服务器.
检查RDB文件是否被覆盖. 如果文件没有覆盖.则重启redis即可.(希望渺茫)
如果flushAll命令,同时执行了save操作,则RDB模式无效.
场景2: redis中的服务开启了AOF模式.
解决方案:
众所周知,Redis的所有数据都存储在内存中,但是内存是一种有限的资源,所以为了防止Redis无限制的使用内存,在启动Redis时可以通过配置项maxmemory
来指定其最大能使用的内存容量。例如可以通过以下配置来设置Redis最大能使用 1G 内存:maxmemory 1G
**当Redis使用的内存超过配置的 maxmemory 时,便会触发数据淘汰策略。**Redis提供了多种数据淘汰的策略,如下:
volatile-lru
: 最近最少使用算法,从设置了过期时间的键中选择空转时间最长的键值对清除掉volatile-lfu
: 最近最不经常使用算法,从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉volatile-ttl
: 从设置了过期时间的键中选择过期时间最早的键值对清除volatile-random
: 从设置了过期时间的键中,随机选择键进行清除allkeys-lru
: 最近最少使用算法,从所有的键中选择空转时间最长的键值对清除allkeys-lfu
: 最近最不经常使用算法,从所有的键中选择某段时间之内使用频次最少的键值对清除allkeys-random
: 所有的键中,随机选择键进行删除noeviction
: 不做任何的清理工作,在redis的内存超过限制之后,所有的写入操作都会返回错误;但是读操作都能正常的进行可以在启动Redis时,通过配置项maxmemory_policy
来指定要使用的数据淘汰策略。例如要使用volatile-lru
策略可以通过以下配置来指定:maxmemory_policy volatile-lru
LRU算法
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面(数据)予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
计算维度: 自上一次以来所经历的时间T.
说明:LRU算法是内存优化中最好用的算法.
LFU算法
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
维度: 引用次数
常识: 计算机左移 扩大倍数
计算机右移 缩小倍数
随机算法
随机删除数据.
TTL算法
说明:将剩余存活时间排序,将马上要被删除的数据,提前删除.
Redis默认的内存优化策略
说明1: Redis中采用的策略定期删除+惰性删除策略
说明2:
定期删除: redis默认每隔100ms 检查是否有过期的key, 检查时随机的方式进行检查.(不是检查所有的数据,因为效率太低.)
问题: 由于数据众多,可能抽取时没有被选中.可能出现 该数据已经到了超时时间,但是redis并没有马上删除数据.
惰性策略: 当用户获取key的时候,首先检查数据是否已经过了超时时间. 如果已经超时,则删除数据.
问题: 由于数据众多, 用户不可能将所有的内存数据都get一遍.必然会出现 需要删除的数据一直保留在内存中的现象.占用内存资源.
可以采用上述的内存优化手段,主动的删除.
Redis LRU的具体实现
算法介绍
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 [1] 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 [2] 。
作用: 解决缓存数据,在哪存储的问题…
算法说明
常识:
①平衡性是指hash的结果应该平均分配到各个节点,这样从算法上解决了负载均衡问题 [4] 。
说明:通过虚拟节点实现数据的平衡
②单调性是指在新增或者删减节点时,不影响系统正常运行 [4] 。
原则: 如果节点新增/减少 应该尽可能保证原始数据尽可能不变.
③分散性是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不必每个节点都存储所有的数据 [4]
将数据分散存储,即使将来服务器宕机,则影响只是一部分,.而不是全部.
谚语: 鸡蛋不要放到一个篮子里.
如果redis分片中有一个节点宕机了,则可能会影响整个服务的运行,redis分片没有实现高可用,所以就需要使用哨兵来监控主机.
原理说明:
1.哨兵监控主机的运行的状态. 通过心跳检测机制(PING-PONG)如果连续3次节点没有响应,则断定主机宕机,哨兵开始进行选举.
2.哨兵通过链接主机,获取主机的相关配置信息(包含主从结构),挑选链接当前主机的从机.根据随机算法挑选出新的主机. 并且将其他的节点设置为新主机的从.
1.分片机制: 可以实现内存数据的扩容. 但是本身没有实现高可用的效果.
2.哨兵机制: 哨兵可以实现redis节点的高可用.但是哨兵本身没有实现高可用的效果.
需求:
宕机条件: Redis中的主机缺失时,并且没有从机替补,Redis内存数据丢失.这时Redis集群崩溃了.
问题1: 6台redis 3主3从(1主1从分为3组). 至少Redis宕机几台集群崩溃. 至少2台 集群崩溃.
问题2: 9台redis 3主6从(1主2从分为3组). 至少宕机几台Redis集群崩溃. 至少5台 集群崩溃.
集群宕机的条件: 当主机的数量不能保证时集群崩溃.
特点:集群中如果主机宕机,那么从机可以继续提供服务,
当主机中没有从机时,则向其它主机借用多余的从机.继续提供服务.如果主机宕机时没有从机可用,则集群崩溃.
答案:9个redis节点,节点宕机5-7次时集群才崩溃.
Hash槽算法 分区算法.
说明: RedisCluster采用此分区,所有的键根据哈希函数(CRC16[key]%16384)映射到0-16383槽内,共16384个槽位,每个节点维护部分槽及槽所映射的键值数据.根据主节点的个数,均衡划分区间.
算法:哈希函数: Hash()=CRC16[key]%16384
当向redis集群中插入数据时,首先将key进行计算.之后将计算结果匹配到具体的某一个槽的区间内,之后再将数据set到管理该槽的节点中.
问题:一个数据很大.一个槽位不够存怎么办??? 错误?? A 逻辑错误 B. 有道理
解答:
1.一致性hash算法 hash(key) 43亿 按照顺时针方向找到最近的节点 进行set操作.
2.Hash槽算法 crc16(key)%16384 (0-16383) 计算的结果归哪个节点管理,则将数据保存到节点中.
核心知识: 一致性hash算法/hash槽算法 都是用来确定 数据归谁管理的问题. 最终的数据都会存储到node节点中.
问: Redis集群中一共可以存储16384个数据? A 对 B 错 为什么???
小明猜想: 由于redis中共有16384个槽位,所以每个槽位存储一个key.那么不就是16384个key吗??
答案: 错误
原因: Redis集群中确实有16384个槽位.但是这些槽位是用来划分数据归谁管理的.不是用来存储数据的. 并且根据hash计算的规则肯能出现碰撞的问题.比如
hash(key1)%16384=3000
hash(key2)%16384=3000
说明key1和key2归同一个node管理.
node.set(key1,value1);
node.set(key2,value2);
由于槽位只是用来区分数据,数据到底能存储多少个完成由redis内存决定.
问题: 为Redis集群中最多有多少台主机?? 16384台主机
公式:存活节点>N/2
算数计算:
1个节点 1-1>1/2 假的
2个节点 2-1>2/2 假的
3个节点 3-1>3/2 真的
结论:集群搭建最小服务器数:3台
原因:
3个节点和4个节点都是允许宕机1台,他们的容灾效果相同,所以一般是奇数个
说明:
由于集群工作过程中主机意外宕机,之后集群开始进行选举,如果出现多次连续平票状态时,则可能出现脑裂现象.
脑裂发生的概率是:1/8=12.5%