本文为笔者对在学习Redis过程中所收集资料的一个总结,目的是为了以后方便回顾相关的知识,大部分为非原创内容。特此声明!
Redis是什么?
Redis是一个开源的key-value存储系统,由于拥有丰富的数据结构,又被其作者戏称为数据结构服务器。它属于NoSQL(Not Only SQL)数据库中的键值(Key-Value)存储数据库,即它属于与MySQL和Oracle等关系型数据库不同的非关系型数据库。它与memcached类似,但是优于memcached。
Redis的应用场景
- 用于做持久化存储:由于Redis拥有丰富的数据结构,所以可以存储多种类型的数据。同时,它在存储与获取某些数据的效率方面也优于关系型数据库。例如微博在存储关注列表和粉丝列表时,就可以使用Redis中的hash sets数据类型用于存储;在记录用户发言数以及粉丝数时,就可以使用Redis中的string(counter)进行存储,避免了关系型数据库中的select count(*) from....,减小了系统开销。但是在实际生产中,一般不会单独使用Redis作为数据库。
- 用于数据缓存:Redis最适合所有数据in-momory的场景。
Redis的优点
- 性能很高:Redis支持超过100K每秒的读写速率
- 丰富的数据类型(相对于memcached来讲):Strings,Lists,Hashes,Sets,Ordered Sets
- 所有操作都是原子性的,不用担心发生并发问题。并且Redis还支持对几个操作合并之后的原子性操作
- 拥有其他丰富的特性,例如给Key设置expire过期时间等
Redis对大小写不敏感
数据类型
Strings
字符串是Redis的一种最基本的数据类型。Redis字符串是二进制安全的,这意味着一个Redis字符串能包含任意类型的数据。一个字符串类型的变量最多能存储512M字节的内容。
对String常用的操作命令
- set(key, value):给数据库中名称为key的string赋予值value
- get(key):返回数据库中名称为key的string的value
- getset(key, value):给名称为key的string赋予上一次的value
- mget(key1, key2,…, key N):返回库中多个string(它们的名称为key1,key2…)的value
- setnx(key, value):如果不存在名称为key的string,则向库中添加string,名称为key,值为value
- setex(key, time, value):向库中添加string(名称为key,值为value)同时,设定过期时间time
- mset(key1, value1, key2, value2,…key N, value N):同时给多个string赋值,名称为key i的string赋值value i
- msetnx(key1, value1, key2, value2,…key N, value N):如果所有名称为key i的string都不存在,则向库中添加string,名称key i赋值为value i
- incr(key):名称为key的string增1操作
- incrby(key, integer):名称为key的string增加integer
- decr(key):名称为key的string减1操作
- decrby(key, integer):名称为key的string减少integer
- append(key, value):名称为key的string的值附加value
- substr(key, start, end):返回名称为key的string的value的子串
Lists
Redis中的List是简单的字符串列表,可以按照插入的顺序排序。我们可以添加一个元素到列表的左边(头部)或者是右边(尾部)。对应的命令为LPUSH和RPUSH。
- rpush(key, value):在名称为key的list尾添加一个值为value的元素
- lpush(key, value):在名称为key的list头添加一个值为value的 元素
- llen(key):返回名称为key的list的长度
- lrange(key, start, end):返回名称为key的list中start至end之间的元素(下标从0开始,下同)
- ltrim(key, start, end):截取名称为key的list,保留start至end之间的元素
- lindex(key, index):返回名称为key的list中index位置的元素
- lset(key, index, value):给名称为key的list中index位置的元素赋值为value
- lrem(key, count, value):删除count个名称为key的list中值为value的元素。count为0,删除所有值为value的元素,count>0从头至尾删除count个值为value的元素,count<0从尾到头删除|count|个值为value的元素。 lpop(key):返回并删除名称为key的list中的首元素 rpop(key):返回并删除名称为key的list中的尾元素 blpop(key1, key2,… key N, timeout):lpop命令的block版本。即当timeout为0时,若遇到名称为key i的list不存在或该list为空,则命令结束。如果timeout>0,则遇到上述情况时,等待timeout秒,如果问题没有解决,则对keyi+1开始的list执行pop操作。
- brpop(key1, key2,… key N, timeout):rpop的block版本。参考上一命令。
- rpoplpush(srckey, dstkey):返回并删除名称为srckey的list的尾元素,并将该元素添加到名称为dstkey的list的头部
Hashes
Hash是字符串字段和字符串值之间的映射,因此他们是展现对象的完美数据类型。一个带有一些字段的hash仅仅需要一块很小的空间存储,因此我们可以存储数以百万计的对象在一个小小的redis实例当中。
- hset(key, field, value):向名称为key的hash中添加元素field<—>value
- hget(key, field):返回名称为key的hash中field对应的value
- hmget(key, field1, …,field N):返回名称为key的hash中field i对应的value
- hmset(key, field1, value1,…,field N, value N):向名称为key的hash中添加元素field i<—>value i
- hincrby(key, field, integer):将名称为key的hash中field的value增加integer
- hexists(key, field):名称为key的hash中是否存在键为field的域
- hdel(key, field):删除名称为key的hash中键为field的域
- hlen(key):返回名称为key的hash中元素个数
- hkeys(key):返回名称为key的hash中所有键
- hvals(key):返回名称为key的hash中所有键对应的value
- hgetall(key):返回名称为key的hash中所有的键(field)及其对应的value
Sets(无序集合)
Redis集合(Set)是一个无序的字符串集合。我们可以在O(1)的时间复杂度(无论集合中有多少元素时间复杂度都是常量)完成添加、删除或者是查看元素是否存在。Redis集合拥有令人满意的不允许包含相同成员的属性。多次添加相同的元素,最终在集合里面只会有一个元素。实际上说这些就是意味着在添加元素的时候无须检测元素是否存在。一个关于Redis集合非常有趣的事情就是它支持一些服务端的命令从现有的集合出发去进行集合运算,因此我们可以在非常短的时间内合并(unions),求交集(intersections),找出不同的元素(difference of sets)。
- sadd(key, member):向名称为key的set中添加元素member
- srem(key, member) :删除名称为key的set中的元素member
- spop(key) :随机返回并删除名称为key的set中一个元素
- smove(srckey, dstkey, member) :将member元素从名称为srckey的集合移到名称为dstkey的集合
- scard(key) :返回名称为key的set的基数
- sismember(key, member) :测试member是否是名称为key的set的元素
- sinter(key1, key2,…key N) :求交集
- sinterstore(dstkey, key1, key2,…key N) :求交集并将交集保存到dstkey的集合
- sunion(key1, key2,…key N) :求并集
- sunionstore(dstkey, key1, key2,…key N) :求并集并将并集保存到dstkey的集合
- sdiff(key1, key2,…key N) :求差集
- sdiffstore(dstkey, key1, key2,…key N) :求差集并将差集保存到dstkey的集合
- smembers(key) :返回名称为key的set的所有元素
- srandmember(key) :随机返回名称为key的set的一个元素
Soted Sets(有序集合)
Redis有序集合和普通集合非常类似,是一个没有重复元素的字符串集合。不同之处在于有序集合的所有成员都关联了一个评分,这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。使用有序集合我们可以用非常快的速度(O(logN))添加、删除以及更新元素。因为元素是有序的,所以我们也可以很快地根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此我们能够使用有序集合作为一个没有重复成员的智能列表。在有序集合中,我们可以很快捷地访问一切我们所需要的东西:有序的元素、快速的存在性测试、快速访问集合的中间元素。简而言之,使用有序集合我们可以完成许多对性能有极端要求的任务,而这些任务是使用其他类型的数据库很难完成的。
- zadd(key, score, member):向名称为key的zset中添加元素member,score用于排序。如果该元素已经存在,则根据score更新该元素的顺序。
- zrem(key, member) :删除名称为key的zset中的元素member
- zincrby(key, increment, member) :如果在名称为key的zset中已经存在元素member,则该元素的score增加increment;否则向集合中添加该元素,其score的值为increment
- zrank(key, member) :返回名称为key的zset(元素已按score从小到大排序)中member元素的rank(即index,从0开始),若没有member元素,返回“nil”
- zrevrank(key, member) :返回名称为key的zset(元素已按score从大到小排序)中member元素的rank(即index,从0开始),若没有member元素,返回“nil”
- zrange(key, start, end):返回名称为key的zset(元素已按score从小到大排序)中的index从start到end的所有元素
- zrevrange(key, start, end):返回名称为key的zset(元素已按score从大到小排序)中的index从start到end的所有元素
- zrangebyscore(key, min, max):返回名称为key的zset中score >= min且score <= max的所有元素 zcard(key):返回名称为key的zset的基数 zscore(key, element):返回名称为key的zset中元素element的score zremrangebyrank(key, min, max):删除名称为key的zset中rank >= min且rank <= max的所有元素 zremrangebyscore(key, min, max) :删除名称为key的zset中score >= min且score <= max的所有元素
- zunionstore / zinterstore(dstkeyN, key1,…,keyN, WEIGHTS w1,…wN, AGGREGATE SUM|MIN|MAX):对N个zset求并集和交集,并将最后的集合保存在dstkeyN中。对于集合中每一个元素的score,在进行AGGREGATE运算前,都要乘以对于的WEIGHT参数。如果没有提供WEIGHT,默认为1。默认的AGGREGATE是SUM,即结果集合中元素的score是所有集合对应元素进行SUM运算的值,而MIN和MAX是指,结果集合中元素的score是所有集合对应元素中最小值和最大值。
系统管理
- exists key:判断一个key是否存在。存在返回1,否则返回0
- del key:删除一个key。成功返回,失败返回0
- type key:返回key的数据类型:string、list、set、zset、hash。key不存在返回none
- keys key-pattern:将所有能够匹配key-pattern的key都列出来
- randomkey:随机返回一个key,如果此时数据库是空的,则返回一个为空的字符串
- clear:清除界面
- rename oldname newname:将key由原来的oldname改为newname,不管此时newname存不存在
- renamenx oldname newname:将key由原来的oldname改为newname.如果此时newname存在则更改失败
- dbsize:返回当前数据库中key的总数
- expire key :限定key的生存时间,命令的一般形式如expire name 30,意思是值为name的key只能存活30秒
- ttl key:返回key剩余存活时间
- flushdb:清除当前数据库中所有的key
- flushall:清除数据库中所有的key
- config get:读取Redis此时的配置参数
- config set:设置Redis此时的配置参数
- auth:密码认证
- info:可以查询Redis几乎所有的信息
底层数据结构
- 简单动态字符串
- 链表
- 字典(Map)
- 跳表
- 整数集合
- 压缩列表
- 对象
简单动态字符串(Simple Dynamic String)
虽然Redis是使用C语言编写的,但是Redis中的字符串类型并不是直接搬用C语言的字符串。
/*字符串对象底层结构*/ struct sds{ int len;//buf已占用的空间长度 int free;//buf中剩余空间长度 char buf[];//数据存储空间 }
- 获取字符串长度时间更快(SDS为O(1)/C语言字符串为O(n))
- 避免了缓冲区溢出问题:当我们在对SDS进行修改之前Redis会预先检查所操作的SDS空间够不够。如果不够,则会拓展对应SDS空间之后再进行拼接等操作。
- 减少修改字符串时带来的内存分配问题
- 二进制安全
- 兼容部分C语言中有关字符串的函数
链表
Redis中的list底层使用的是双向链表
字典(Map)
在字典中,一个key和一个value关联,并且字典中的每个key都是独一无二的。
Redis字典使用的哈希表底层结构:
typedef struct dictht { //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 unsigned long sizemask; //该哈希表已有节点的数量 unsigned long used; }
哈希表节点:
typeof struct dictEntry{ //键 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; } struct dictEntry *next; }
这一部分主要涉及三个点:
- 根据hash算法算出key的hash值然后分配存储空间(由于哈希表中没有记录链表尾节点的位置,所以是在链表的head插入新的节点);
- 链地址法解决hash地址冲突
- 随着哈希表中节点数量的增加,适当时候会进行rehash将哈希表的负载因子保持在一个合理的范围
跳表(skiplist)
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美 ——查找、删除、添加等操作都可以在对数期望时间下完成,并且比起平衡树来说,跳跃表的实现要简单直观得多。Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是在集群节点中用作内部数据结构。
只有当有序集合中的元素个数大于128时才会使用跳表,否则将使用后面提到的压缩列表
- 跳跃表是有序集合的底层实现之一
- 主要有zskiplist 和zskiplistNode两个结构组成
- 每个跳跃表节点的层高都是1至32之间的随机数
- 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的对象必须是唯一的
- 节点按照分值的大小从大到小排序,如果分值相同,则按成员对象大小排序
zskiplist(链表)数据结构:
typedef struct zskiplist { //表头节点和表尾节点 structz skiplistNode *header,*tail; //表中节点数量 unsigned long length; //表中层数最大的节点的层数 int level; }zskiplist;
zskiplistNode(节点)数据结构:
typedef struct zskiplistNode{ //层 struct zskiplistLevel{ //前进指针 struct zskiplistNode *forward; //跨度 unsigned int span; } level[]; //后退指针 struct zskiplistNode *backward; //分值 double score; //成员对象 robj *obj; }
整数集合(Intset)
整数集合是集合建的底层实现之一,当一个集合中只包含整数,且这个集合中的元素数量不多时,redis就会使用整数集合intset作为集合的底层实现。我们可以这样理解整数集合,他其实就是一个特殊的集合,里面存储的数据只能够是整数,并且数据量不能过大。
整数集合数据结构
typedef struct intset{ //编码方式 uint32_t enconding; // 集合包含的元素数量 uint32_t length; //保存元素的数组 int8_t contents[]; }
- encoding:用于定义整数集合的编码方式
- length:用于记录整数集合中变量的数量
- contents:用于保存元素的数组,虽然我们在数据结构图中看到,intset将数组定义为int8_t,但实际上数组保存的元素类型取决于encoding
在上述数据结构图中我们可以看到,intset 在默认情况下会帮我们设定整数集合中的编码方式,但是当我们存入的整数不符合整数集合中的编码格式时,就需要使用到Redis 中的升级策略来解决Intset 中升级整数集合并添加新元素共分为三步进行:
- 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
- 将底层数组现有的所有元素都转换成新的编码格式,重新分配空间
- 将新元素加入到底层数组中
整数集合升级不仅可以提高灵活性,还能节约内存。整数集合是集合键的底层实现之一。整数集合的底层实现为数组,这个数组以有序,无重复的范式保存集合元素,在有需要时,程序会根据新添加的元素类型改变这个数组的类型。同时,升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存。但是整数集合只支持升级操作,不支持降级操作。
压缩列表
压缩列表是列表键和哈希键的底层实现之一。当一个列表键只含少量列表项(一般是少于128)时并且每个列表项要么就是小整数,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
- zlbytes:用于记录整个压缩列表占用的内存字节
- zltail:用于记录列表尾节点距离压缩列表的起始地址有多少字节
- zllen:用于记录压缩列表包含的节点数量
- entry*:列表包含的节点
- zlend:用于标记压缩列表的末端
关于压缩列表的几点总结:
- 压缩列表是一种为了节约内存而开发的顺序型数据结构
- 压缩列表被用作列表键和哈希键的底层实现之一
- 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者是整数值
- 添加新节点到压缩列表,可能会引发连锁更新操作
持久化
Redis直接将数据存储在内存当中,但是并不是所有的数据都一直存储在内存中的(这是和memcached相比最大的一个区别)。Redis会缓存所有的key的信息,但是如果Redis发现内存的使用量超过了某一个阈值,就会触发swap操作。Redis会计算出哪些key对应的value需要swap到磁盘,然后再将这些key对应的value持久化到磁盘中同时清除内存中存储的对应的value。这种特性使得Redis可以保持超过其机器本身内存大小的数据。但是机器本身的内存必须可以有足够的空间存储所有的key,因为key是不会进行swap操作的。由于Redis将内存中的数据swap到磁盘中时,提供服务的主线程和进行swap操作的子线程会共享这部分内存。如果更新需要swap的数据,Redis将阻塞这个操作,直至子线程完成swap操作之后才可以进行修改。
当从Redis中读取数据的时候,如果读取的key对应的value不在内存中,那么Redis就要从swap文件加载相应的数据,然后再返回给请求数据的一方。此时存在一个I/O线程池的问题。在默认情况下,Redis会出现阻塞,它要完成所有的swap文件的加载之后才会响应。这样的策略在客户端数量较少,进行批量操作的时候比较合适。但是如果将Redis应用在一个大型的网站中,这显然是无法满足高并发的需求的。所有Redis允许我们设置I/O线程池的大小,对需要从swap文件中加载对应数据的请求进行并发操作,减少阻塞时间。
Redis提供两种持久化的机制
-
RDB快照:Redis支持将当前的快照存储为一个数据文件的持久化机制,即当前提到的RDB快照。Redis借助了fork命令的copy on write机制(私有内存非共享内存)。在生成快照时,将当前进程fork出一个子进程。接着在子进程中迭代循环所有的数据同时将数据存储到一个临时文件当中。当数据全部处理完之后,就通过原子性rename系统调用将临时文件重命名为RDB文件。值得注意的是,这样的文件生成机制可以保证RDB文件不会坏掉,即Redis的RDB文件总是可用的。但是,RDB有明显的不足-----一旦数据库出现问题,那么我们的RDB文件中保存的数据并不是全新的,从上次RDB文件生成到Redis停机这段时间的数据全部丢掉了。在某些业务下,这是可以忍受的,我们也推荐这些业务使用RDB的方式进行持久化,因为开启RDB的代价并不高。但是对于另外一些对数据安全性要求极高的应用,无法容忍数据丢失的应用,RDB就无能为力了,所以Redis引入了另一个重要的持久化机制:AOF日志。
我们可以通过Redis的save指令来配置快照生成的时机
save 900 1 #900秒内有一条key数据被修改就生成RDB文件
- AOF(Append Only File)日志
AOF日志是追加写入的日志文件。和一般数据库的binlog不同的是,AOF文件是可读性较强的纯文本。其中保存的内容即Redis一条条的标准指令。但是并不是所有的Redis指令都会记录在AOF文件中,只有会导致数据发生修改的指令才会追加到AOF文件中。随着记录的指令越来越多,文件会变得越来越大。此时Redis提供了一个叫做AOF rewrite的功能,可以重新生成一份新的并且更小的AOF文件。新的文件中针对同一条key只记录了最新的一次数据修改的指令。AOF的文件生成机制和RDB快照类似。再写入新文件的过程中,所有操作日志还是会写到旧的文件当中,同时会记录在内存缓冲区中。当rewrite操作完成后,会将所有缓冲区中的日志一次性写入临时文件并调用原子性的rename命令将新的AOF文件覆盖旧的AOF文件。
appendfsync:控制AOF文件写入磁盘的时机
- appendfsync no:当设置appendfsync为no时,Redis不会主动调用fsync去将AOF日志内容同步到磁盘,这完全依赖于操作系统。对于大多数Linux系统来说,一般是每30秒进行一次fsync,将缓冲区中的数据同步到磁盘上;
- appendfsync everysec:当设置appendfsync为everysec的时候,Redis会默认每隔一秒进行一次fsync调用,将缓冲区中的数据写到磁盘。但是当这一次的fsync调用时长超过1秒时。Redis会采取延迟fsync的策略,再等一秒钟。也就是在两秒后再进行fsync,这一次的fsync就不管会执行多长时间都会进行。这时候由于在fsync时文件描述符会被阻塞,所以当前的写操作就会阻塞。所以结论就是,在绝大多数情况下,Redis会每隔一秒进行一次fsync。在最坏的情况下,两秒钟会进行一次fsync操作。这一操作在大多数数据库系统中被称为group commit,就是组合多次写操作的数据,一次性将日志写到磁盘。
- appendfsync always:
当设置appendfsync为always时,每一次写操作都会调用一次fsync,这时数据是最安全的,当然,由于每次都会执行fsync,所以其性能也会受到影响。
主从复制
为了保证单点故障下的数据可用性,Redis引入了Master节点和Slave节点。
- master节点可拥有多个slave节点
- 除了多个slave连到相同的master之外,slave也可以连接其他slave形成图状结构
- 主从复制不会阻塞master。也就是说,当一个或多个slave与master进行初次同步数据时,master可以继续处理client发来的请求。相反slave在初次同步数据时则会阻塞,不能处理client的请求
- 主从复制可以用来提高系统的可伸缩性,我们可以用多个slave专门用于client的读请求,比如sort操作可以使用slave来处理。也可以用来做简单的数据冗余
- 可以在master禁用数据持久化,只需要注释掉master配置文件中的所有save配置,然后只在slave上配置数据持久化
主从复制过程
当设置好slave服务器后,slave会建立和master的连接,接着发送sync命令。无论是第一次同步建立的连接还是连接断开后的重新连接,master都会启动一个后台进程,将数据库快照保存到文件中,同时master主进程会开始收集新的写命令并缓存起来。后台进程完成写文件后,master就发送文件给slave,slave将文件保存到磁盘上,然后加载到内存并恢复数据库快照到磁盘上。接着master就会把缓存的命令转发给slave,而且后续master收到的写命令都会通过开始建立的连接发送给slave。从master到slave的同步数据的命令和从client发送的命令使用相同的协议格式。当master和slave的连接断开时slave可以自动重新建立连接。如果master同时收到多个slave发来的同步连接命令,只会启动一个进程来写数据库镜像,然后发送给所有slave。