参考资料:《redis设计与实现》
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
上面一段话是redis中文网的官方介绍。
有几个关键的点值得注意:
学一门技术之前往往会出现这么一个问题,它好在哪里,为什么值得学?值得用?
官方给出了以下几点优势:
我们看到上面第一点就是性能极高!!那么问题来了,redis是怎么做到这么快的读写速度的?这个问题我当时面试的时候也被问到过…说白了就是让你说清楚redis为什么很快??
关于redis为什么快的问题,有两点我们其实很容易想到:
我们知道一般数据库的读写都是需要经过磁盘的,而磁盘的读写速度相对于内存来说绝对是算慢的了,所以我们需要用缓存,我们希望不要每次进行读写都跑去数据库进行操作。mybatis为什么要设置一级缓存,二级缓存?主要还是为了不希望每次读取数据都要到数据库去读取,以提升性能。
但是除了上述两点,还有什么特点是提升了redis的性能的呢?
其实还有一点很重要:redis采用了非阻塞的网络IO多路复用技术来保证多连接的时候系统的高吞吐量。也就是指多个socket网络连接复用同一个线程。
另外还有两点:
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
数据类型 | 可以存储的值 | 操作 |
---|---|---|
STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 |
LIST | 列表 | 从两端压入或者弹出元素 对单个或者多个元素 进行修剪,只保留一个范围内的元素 |
SET | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 |
HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 |
ZSET | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 |
redis的存储形式为key-value,其中key为字符串,value可以是string、list、hash、set、zset。redis并没有直接使用这些数据结构来构建key-value数据库,而是基于这些数据结构构建了一个对象系统。每个相应的键对象、值对象都有自己的类型、编码、和指向底层数据结构的指针。
string是redis最基本的类型,一个key对应一个value。
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
实例
127.0.0.1:6379> set name "xiaohuang"
OK
127.0.0.1:6379> get name
"xiaohuang"
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> get name
(nil)
redis自己构建了一种叫做简单动态字符串(SDS)的抽象类型,并将SDS用作redis的默认字符串表示。
每个sdshdr结构表示一个SDS值:
struct sdshdr {
//记录buf数组中已使用的字节数量
int len;
//记录buf数组中未使用的字节数量
int free;
//字节数组,用来保存字符串
char buf[];
}
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API不安全,可能会造成缓冲区溢出 | API安全,不会造成缓冲区溢出 |
修改字符串长度n次必然要进行N次内存重分配 | 修改字符串N次最多需要N次内存重分配 |
只能保存文本数据 | 可以保存文本或二进制 |
可以使用所有 |
可以使用部分 |
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。
实例:
127.0.0.1:6379> lpush list-key redis
(integer) 1
127.0.0.1:6379> lpush list-key mongodb
(integer) 2
127.0.0.1:6379> rpush list-key java
(integer) 3
127.0.0.1:6379> lrange list-key 0 5
1) "mongodb"
2) "redis"
3) "java"
redis列表键的底层实现之一为链表
每个链表结点listnode的结构表示:
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//结点值
void *value;
}listNode;
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 hash 是一个键值对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
实例:
127.0.0.1:6379> hset hash-key field1 value1
(integer) 1
127.0.0.1:6379> hset hash-key field2 value2
(integer) 1
127.0.0.1:6379> hset hash-key field3 value3
(integer) 1
127.0.0.1:6379> hset hash-key field1 value1
(integer) 0
127.0.0.1:6379> hgetall hash-key
1) "field1"
2) "value1"
3) "field2"
4) "value2"
5) "field3"
6) "value3"
127.0.0.1:6379> hdel hash-key field1
(integer) 1
127.0.0.1:6379> hget hash-key field2
"value2"
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long sizemask;
//该哈希表已有节点数量
unsigned long used;
}dictht;
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
typedef struct dict{
dictType *type;
void *privdata;
dictht ht[2];
int trehashidx; /* rehashing not in progress if rehashidx == -1 */
}dict;
typedef struct dictType {
//计算哈希值
unsigned int (*hashFunction) (const void *key);
//复制键
void *(*keyDup) (void *privdata, const void *key);
//复制值
void *(*valDup) (void *privdata, const void *obj);
//对比建
int (*keyCompare) (void *privdata, const void *key1, const void *key2);
//销毁键
void (*keyDestructor) (void *privdata, void *key);
//销毁值
void (*valDestructor) (void *privdata, void *obj);
}dictType;
当我们要插入新的键值对到字典里面的时候,首先要根据键值对的键计算得到哈希值,然后根据哈希值得到索引值,最后将相应包含键值对的哈希表节点放到哈希数组的制定索引位置。整个过程你会发现和Java里的HashMap很相似。
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;
当两个或两个以上的键被分配到哈希表数组的同一个索引出,我们认为此时发生了hash冲突。和HashMap相似,redis的哈希表也是使用链地址法来解决哈希冲突。被分配到同一个索引的多个结点可以利用next指针构成一个单链表,并且为了提升性能,总是将新节点添加到链表的表头位置(复杂度为O(1)),
但是rehash(重新散列)redis和Java并不同,redis采用的是渐进式rehash,而不是一次性完成。因为如果数据量过大,一次性rehash会导致较大的计算延时,可能会导致服务器在一段时间内停止服务。
具体的rehash步骤:
hash表扩展和收缩的条件:
负载因子 = 哈希表以保存结点数量 / 哈希表大小
另外,在进行渐进式rehash操作时,字典的插入、删除、跟新操作会在新旧两个hash表上进行,查找一个键时,会先在旧表里面查找,如果找不到,再到新表里面去找。新增一个键值对时,则直接增加到新表上,确保旧表的键值对数量只减不增。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
实例:
127.0.0.1:6379> sadd set-key redis
(integer) 1
127.0.0.1:6379> sadd set-key java
(integer) 1
127.0.0.1:6379> sadd set-key mysql
(integer) 1
127.0.0.1:6379> sadd set-key redis
(integer) 0
127.0.0.1:6379> smembers set-key
1) "java"
2) "redis"
3) "mysql"
127.0.0.1:6379> sismember set-key redis
(integer) 1
127.0.0.1:6379> srem set-key mysql
(integer) 1
127.0.0.1:6379> smembers set-key
1) "java"
2) "redis"
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且元素数量不多,redis会使用整数集合作为集合键的底层实现。
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
关于整数集合如何升级,可以自行查阅黄建宏的《redis设计与实现》
redis的整数集合底层为有序,无重复的数组,有需要时,程序会改变数组类型。
整数集合只支持升级操作,不支持降级操作
实例:
127.0.0.1:6379> zadd zset-key 11 member1
(integer) 1
127.0.0.1:6379> zadd zset-key 9 member2
(integer) 1
127.0.0.1:6379> zadd zset-key 13 member3
(integer) 1
127.0.0.1:6379> zadd zset-key 13 member3
(integer) 0
127.0.0.1:6379> zrange zset-key 0 -1 withscores
1) "member2"
2) "9"
3) "member1"
4) "11"
5) "member3"
6) "13"
127.0.0.1:6379> zrem zset-key member1
(integer) 1
127.0.0.1:6379> zrange zset-key 0 -1 withscores
1) "member2"
2) "9"
3) "member3"
4) "13"
127.0.0.1:6379>
跳跃表是一种有序数据结构,属于有序集合键的底层实现之一,它通过在每个节点中维持多个指向其他节点的指针,从而能够快速访问节点。
平均复杂度O(logN),最坏O(N)复杂度,
如果一个有序集合包含的元素比较多,或者有序集合中元素的成员是比较长的字符串时,redis就会使用跳跃表来作为有序集合键的底层实现。
跳跃表除了在有序集合中用到,在redis的集群节点中也有用作内部数据结构。
跳跃表节点结构
typedef struct zskiplistNode {
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
}zskiplistNode;
跳跃表结构
typedef struct zskiplist {
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
与红黑树等平衡树相比,跳跃表的优势在于: