redis是一个可基于内存可持久化的日志型,key-value数据库,提供多种语言的API,是单线程的,为什么redis是单线程的,本文主要以java为例。
redis的特性包括:数据访问速度快(存在内存中),有数据持久化机制,支持集群模式(容量可以线性扩展),支持丰富的数据结构,可以按Key设置过期时间,过期自动删除,支持事务。
redis缺点:数据库容量受物理内存的限制,不能用作海量数据的高性能读写,场景主要局限在较小数据量的高性能操作和运算上。
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如10K的命令,读回复,然后再发送另一个10k的命令,等等。这样速度几乎是相同的,但是在回复这10k命令队列需要非常大量的内存用来组织返回数据内容。
redis支持主从的模式。原则:Master会将数据同步到slave,而slave不会将数据同步到master。Slave启动时会连接master来同步数据。
这是一个典型的分布式读写分离模型。我们可以利用master来插入数据,slave提供检索服务。这样可以有效减少单个机器的并发访问数量。
通过增加Slave DB的数量,读的性能可以线性增长。为了避免Master DB的单点故障,集群一般都会采用两台Master DB做双机热备,所以整个集群的读和写的可用性都非常高。
读写分离架构的缺陷在于,不管是Master还是Slave,每个节点都必须保存完整的数据,如果在数据量很大的情况下,集群的扩展能力还是受限于单个节点的存储能力,而且对于Write-intensive类型的应用,读写分离架构并不适合。
为了解决读写分离模型的缺陷,可以将数据分片模型应用进来。
可以将每个节点看成都是独立的master,然后通过业务实现数据分片。
结合上面两种模型,可以将每个master设计成由一个master和多个slave组成的模型。
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…
这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。
redis如何实现延时队列?使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
Redis其实只适合作为缓存,而不是数据库或是存储。它的持久化方式适用于救救急啥的,不太适合当作一个普通功能来用。应为dump时候,会影响性能,数据量小的时候还看不出来,当数据量达到百万级别,内存10g左右的时候,非常影响性能。
假如有多个消费者同时监听一个队列,其中一个出队了一个元素,另一个则获取不到该元素
Redis的队列应用场景是一对多或者一对一的关系,即有多个入队端,但是只有一个消费端(出队)
redis崩溃的时候队列功能失效
如果入队端一直在塞数据,而出队端没有消费数据,或者是入队的频率大而多,出队端的消费频率慢会导致内存暴涨
优点:1.保存了某个时间点的数据集,如每天保存过去30天的数据集,可根据需要恢复到不同版本的数据集。
2.RDB是一个紧凑的单一文件,方便传送到另一个远端数据中心,适用于灾难恢复。
3.RDB只需fork出一个子进程,接下来的工作由子进程完成,父进程不需其他IO操作,可最大化redis性能。
4.与AOF相比,在恢复大的数据集的时候,RDB会更快。
缺点:1.会丢失一部分数据,因为是每隔一段时间进行保存。
2.RDB需要经常fork子进程来保存数据集到硬盘上,数据集较大时,fork过程非常耗时,会导致redis在一些毫秒级内不能响应客户端请求,AOF也需要fork,但可以调节重写日志文件的频率来提高数据集的耐久度。
优点:1.会让redis更加耐久,可以使用不同的fsync策略,每秒fsync,每次写时fsync,每秒fsync一旦出现故障最多丢失一秒数据。(fsync是由后台线程进行处理)
2.AOF文件是只进行追加的日志文件,不需要写入seek,即使因为故障未执行完整的写入命令,也可使用redis-check-aof工具修复这些问题。
3.AOF文件体积过大时,redis可自动在后台对AOF重写,重写后的AOF文件包含恢复当前数据集所需的最小命令集合,整个重写过程安全,即使重写过程中发生停机,现有的AOF文件也不会丢失,一旦新AOF文件创建完毕,redis就会从旧AOF文件切换到新AOF文件,并开始对新AOF文件进行追加操作。
4.AOF文件有序保存了对数据库执行的所有写入操作,易读,可进行分析,导出。
缺点:1.AOF文件体积通常大于RDB文件。
2.根据使用的fsync策略,AOF速度可能会慢于RDB,大处理巨大的写入载入时,RDB可以提供更有保证的最大延迟时间。
字符串是一种最基本的Redis值类型。Redis字符串是二进制安全的,这意味着一个Redis字符串能包含任意类型的数据,例如: 一张JPEG格式的图片或者一个序列化的Ruby对象。
一个字符串类型的值最多能存储512M字节的内容
Redis列表是简单的字符串列表,按照插入顺序排序。 你可以添加一个元素到列表的头部(左边)或者尾部(右边)
从时间复杂度的角度来看,Redis列表主要的特性就是支持时间常数的 插入和靠近头尾部元素的删除,即使是需要插入上百万的条目。 访问列表两端的元素是非常快的,但如果你试着访问一个非常大 的列表的中间元素仍然是十分慢的,因为那是一个时间复杂度为 O(N) 的操作。
Redis集合是一个无序的字符串合集。你可以以O(1) 的时间复杂度(无论集合中有多少元素时间复杂度都为常量)完成 添加,删除以及测试元素是否存在的操作。
Redis集合有着不允许相同成员存在的优秀特性。向集合中多次添加同一元素,在集合中最终只会存在一个此元素。实际上这就意味着,在添加元素前,你并不需要事先进行检验此元素是否已经存在的操作。
一个Redis列表十分有趣的事是,它们支持一些服务端的命令从现有的集合出发去进行集合运算。 所以你可以在很短的时间内完成合并(union),求交(intersection), 找出不同元素的操作
Redis Hashes是字符串字段和字符串值之间的映射,所以它们是完美的表示对象(eg:一个有名,姓,年龄等属性的用户)的数据类型。
一个拥有少量(100个左右)字段的hash需要 很少的空间来存储,所有你可以在一个小型的 Redis实例中存储上百万的对象。
小散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。
Redis有序集合和Redis集合类似,是不包含 相同字符串的合集。它们的差别是,每个有序集合 的成员都关联着一个评分,这个评分用于把有序集 合中的成员按最低分到最高分排列。
使用有序集合,你可以非常快地(O(log(N)))完成添加,删除和更新元素的操作。 因为元素是在插入时就排好序的,所以很快地通过评分(score)或者 位次(position)获得一个范围的元素。 访问有序集合的中间元素同样也是非常快的,因此你可以使用有序集合作为一个没用重复成员的智能列表。 在这个列表中, 你可以轻易地访问任何你需要的东西: 有序的元素,快速的存在性测试,快速访问集合中间元素!
实际上是基于字符串的基本类型的数据类型,但有自己的语义。
redis是用C语言写的,字符串用的是自己构建的一个名为简单动态字符串的抽象类型。
struct sdshdr {
int len; //记录buf数组中已使用字节的数量
int free; //记录buf数组中未使用字节的数量
char buf[]; //用于保存字符串的字节数组
}
由于len属性的存在,获取字符串长度时间复杂度为O(1),而对于C语言,获取字符串长度通常是通过遍历计数来实现的。
在进行字符串拼接时,可以先根据记录的len属性和free属性检查内存空间是否满足需求,不满足则进行相应的扩容再修改,避免出现缓冲区溢出。
C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。
而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:
1、空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
2、惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)
typedef struct listNode{
struct listNode *prev;
struct listNode *next;
void *value;
}listNode
typedef struct list {
listNode *head;
listNode *tail;
unsigned long len;
void (*copy) (void *ptr); //节点值复制函数
void (*free) (void *ptr); //节点值释放函数
int (*match) (void *ptr,void *key); //节点值对比函数
}list;
redis链表特性:双端,无环,带链表长度计数器,多态(链表节点使用void*指针来保存节点值,可以保存各种不同类型的值)。
typedef struct dictht{
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小
unsigned long sizemask; //哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long used; //该哈希表已有节点的数量
}dictht
typedef struct dictEntry{
void *key;
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
struct dictEntry *next;
}dictEntry
哈希表是由数组table组成,table中每个元素都指向一个dictEntry。
每一个键key都是唯一的,这里采用的链地址法解决哈希冲突,通过next将多个哈希值相同的键值对连接在一起。
每次扩容或收缩都会建一个新的表,然后重新计算哈希值将键值对存入新的表中,所有键值对存入完后释放原来的哈希表,redis采用渐进式rehash,就是考虑到当键值对有上百万个时,扩容和收缩都是分多次,渐进式的完成的,在复制过程中,删除查找更新等操作可能会在两个表中进行,但增加操作是在新的哈希表上进行的。
typedef struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度
}level[];
struct zskiplistNode *backward;//后退指针
double score;//分值
robj *obj;//成员对象
}zskiplistNode
typedef struct zskiplist{
struct zskiplistNode *header,*tail;
unsigned long length;
int level;//表中层数最大的节点的层数
}zskiplist;
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。具有如下性质:
1、由很多层结构组成;
2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
3、最底层的链表包含了所有的元素;
4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;
跳跃表的动画展示:跳跃表
typedef struct intset{
uint32_t encoding; //编码方式
uint32_t length;
int8_t contents[];
}intset;
用于保存整数值的集合抽象数据类型,保证集合中不会出现重复元素。
contents数组按照从小到大的顺序排列。
redis为了节省内存而开发的,有一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值,压缩列表只是将数据按照一定的规则编码在一块连续的内存区域,目的是节省内存。并没有进行压缩。
①、previous_entry_ength:记录压缩列表前一个字节的长度。previous_entry_ength的长度可能是1个字节或者是5个字节,如果上一个节点的长度小于254,则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于254,则previous length的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。利用此原理即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置,压缩列表可以从尾部向头部遍历。这么做很有效地减少了内存的浪费。
②、encoding:节点的encoding保存的是节点的content的内容类型以及长度,encoding类型一共有两种,一种字节数组一种是整数,encoding区域长度为1字节、2字节或者5字节长。
③、content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定.
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放,可以设置set的参数将set和expire合为一个指令。