1.概述
hash是一个string类型的field和value的映射表。添加,删除操作都是o(1)。hash特别适合于存储对象。相对于将对象的每个字段存成单个string类型。将一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对象。省内存的原因是新建一个hash对象时开始时用zipmap(又称为small hash)来存储的。这个zipmap其实并不是hash table,但是zipmap相比正常的hash实现可以节省不少hash本身需要的一些元数据存储开销。尽管zipmap的添加,删除,查找都是o(n),但那时由于一般对象的field数量都不太多,所以使用zipmap也是很快的,也就是说添加删除还是o(1)。如果field或者value的大小查过一定限制后,redis会在内部自动将zipmap替换成正常的hash实现。这个限制可以再配置文件中指定。
hash-max-zipmap-entries 64 #配置字段最多64个
hash-max-zipmap-value 512 #配置value最大为512字节
hash-max-zipmap-entries含义是当value这个Map内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即value内部有64个以下的成员就是使用线性紧凑存储,超过该值自动转成真正的HashMap。
hash-max-zipmap-value 含义是当 value这个Map内部的每个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。
以上2个条件任意一个条件超过设置值都会转换成真正的HashMap,也就不会再节省内存了,那么这个值是不是设置的越大越好呢,答案当然是否定的,HashMap的优势就是查找和操作的时间复杂度都是O(1)的,而放弃Hash采用一维存储则是O(n)的时间复杂度,如果
成员数量很少,则影响不大,否则会严重影响性能,所以要权衡好这个值的设置,总体上还是最根本的时间成本和空间成本上的权衡。
同样类似的参数还有:
list-max-ziplist-entries 512
说明:list数据类型多少节点以下会采用去指针的紧凑存储格式。
list-max-ziplist-value 64
说明:list数据类型节点值大小小于多少字节会采用紧凑存储格式。
set-max-intset-entries 512
说明:set数据类型内部数据如果全部是数值型,且包含多少节点以下会采用紧凑格式存储。
Redis内部实现没有对内存分配方面做过多的优化,在一定程度上会存在内存碎片,不过大多数情况下这个不会成为Redis的性能瓶颈。
如果在Redis内部存储的大部分数据是数值型的话,Redis内部采用了一个shared integer的方式来省去分配内存的开销,即在系统启动时先分配一个从1~n 那么多个数值对象放在一个池子中,存储的数据恰好是这个数值范围内的数据,则直接从池子里取出该对象,并且通过引用计数的方式来共享,这样在系统存储了大量数值下,也能一定程度上节省内存并且提高性能,这个参数值n的设置需要修改源代码中的一行宏定义REDIS_SHARED_INTEGERS,该值默认是10000,可以根据自己的需要进行修改,修改后重新编译就可以了。
2.存储结构
在源文件dict.h/dict.c中实现了hashtable的操作,数据结构的定义如下
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
}v;
struct dictEntry *next;
} dictEntry;
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;
/* This is our hash table structure. Everydictionary has two of this as we
*implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned longsizemask;
unsigned longused;
} dictht;
typedef structdict {
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx; /* rehashing not in progressif rehashidx == -1 */
int iterators; /* number of iteratorscurrently running */
} dict;
/* If safe isset to 1 this is a safe iterator, that means, you can call
* dictAdd, dictFind, and other functions againstthe dictionary even while
* iterating. Otherwise it is a non safeiterator, and only dictNext()
* should be called while iterating. */
typedef structdictIterator {
dict *d;
int table, index, safe;
dictEntry *entry, *nextEntry;
} dictIterator;
· 我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口,如下图:
也就是说,Key仍然是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。
这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
实现方式:
上面已经说到RedisHash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
3.命令列表
命令原型 | 时间复杂度 | 命令描述 | 返回值 |
HSET key field value | O(1) | 为指定的Key设定Field/Value对,如果Key不存在,该命令将创建新Key以参数中的Field/Value对,如果参数中的Field在该Key中已经存在,则用新值覆盖其原有值。 | 1表示新的Field被设置了新值,0表示Field已经存在,用新值覆盖原有值。 |
HGET key field | O(1) | 返回指定Key中指定Field的关联值。 | 返回参数中Field的关联值,如果参数中的Key或Field不存,返回nil。 |
HEXISTSkey field | O(1) | 判断指定Key中的指定Field是否存在。 | 1表示存在,0表示参数中的Field或Key不存在。 |
HLEN key | O(1) | 获取该Key所包含的Field的数量。 | 返回Key包含的Field数量,如果Key不存在,返回0。 |
HDEL key field [field ...] | O(N) | 时间复杂度中的N表示参数中待删除的字段数量。从指定Key的Hashes Value中删除参数中指定的多个字段,如果不存在的字段将被忽略。如果Key不存在,则将其视为空Hashes,并返回0. | 实际删除的Field数量。 |
HSETNXkey field value | O(1) | 只有当参数中的Key或Field不存在的情况下,为指定的Key设定Field/Value对,否则该命令不会进行任何操作。 | 1表示新的Field被设置了新值,0表示Key或Field已经存在,该命令没有进行任何操作。 |
HINCRBYkey field increment | O(1) | 增加指定Key中指定Field关联的Value的值。如果Key或Field不存在,该命令将会创建一个新Key或新Field,并将其关联的Value初始化为0,之后再指定数字增加的操作。该命令支持的数字是64位有符号整型,即increment可以负数。 | 返回运算后的值。 |
HGETALLkey | O(N) | 时间复杂度中的N表示Key包含的Field数量。获取该键包含的所有Field/Value。其返回格式为一个Field、一个Value,并以此类推。 | Field/Value的列表。 |
HKEYSkey | O(N) | 时间复杂度中的N表示Key包含的Field数量。返回指定Key的所有Fields名。 | Field的列表。 |
HVALSkey | O(N) | 时间复杂度中的N表示Key包含的Field数量。返回指定Key的所有Values名。 | Value的列表。 |
HMGET key field [field ...] | O(N) | 时间复杂度中的N表示请求的Field数量。获取和参数中指定Fields关联的一组Values。如果请求的Field不存在,其值返回nil。如果Key不存在,该命令将其视为空Hash,因此返回一组nil。 | 返回和请求Fields关联的一组Values,其返回顺序等同于Fields的请求顺序。 |
HMSET key field value [field value ...] | O(N) | 时间复杂度中的N表示被设置的Field数量。逐对依次设置参数中给出的Field/Value对。如果其中某个Field已经存在,则用新值覆盖原有值。如果Key不存在,则创建新Key,同时设定参数中的Field/Value。 |
|
1. 命令示例
redis 127.0.0.1:6379> hset website google "www.google.com"
(integer) 1
redis 127.0.0.1:6379> hsetwebsite google "www.g.cn"
(integer) 0
redis 127.0.0.1:6379> hget website
(error) ERR wrong number ofarguments for 'hget' command
redis 127.0.0.1:6379> hget website google
"www.g.cn"
redis 127.0.0.1:6379> hsetnx student "yangyan" 30
(integer) 1
redis 127.0.0.1:6379> hsetnx student "yangyan" 29
(integer) 0
redis 127.0.0.1:6379> hget student "yangyan"
"30"
redis 127.0.0.1:6379> hmset student "peiweijun" 32 "yangmin" 29
OK
redis 127.0.0.1:6379> hget student "pweiweijun"
(nil)
redis 127.0.0.1:6379> hget student "peiweijun"
"32"
redis 127.0.0.1:6379> hget student "yangmin"
"29"
redis 127.0.0.1:6379> hmget student "peiweijun" "yangmin"
1) "32"
2) "29"
redis 127.0.0.1:6379> hgetall
(error) ERR wrong number ofarguments for 'hgetall' command
redis 127.0.0.1:6379> hgetall student
1) "yangyan"
2) "30"
3) "peiweijun"
4) "32"
5) "yangmin"
6) "29"
redis 127.0.0.1:6379> hdel student "yangmin"
(integer) 1
redis 127.0.0.1:6379> hgetall student
1) "yangyan"
2) "30"
3) "peiweijun"
4) "32"
redis 127.0.0.1:6379> hdel student "yangyan" "peiweijun"
(integer) 2
redis 127.0.0.1:6379> hgetall student
(empty list or set)
redis 127.0.0.1:6379> hlen website
(integer) 1
redis 127.0.0.1:6379> hgetall website
1) "google"
2) "www.g.cn"
redis 127.0.0.1:6379> hset website baidu "www.baidu.com" csdn "www.csdn.com"
(error) ERR wrong number ofarguments for 'hset' command
redis 127.0.0.1:6379> hmset website baidu "www.baidu.com" csdn "www.csdn.com"
OK
redis 127.0.0.1:6379> hgetall website
1) "google"
2) "www.g.cn"
3) "baidu"
4) "www.baidu.com"
5) "csdn"
6) "www.csdn.com"
redis 127.0.0.1:6379> hlen website
(integer) 3
redis 127.0.0.1:6379>
redis 127.0.0.1:6379> hexists student "yangyan"
(integer) 0
redis 127.0.0.1:6379> hexists website baidu
(integer) 1
redis 127.0.0.1:6379> hexists student "yangyan"
(integer) 0
redis 127.0.0.1:6379> hincrby student yangyan 30
(integer) 30
redis 127.0.0.1:6379> hget student yangyan
"30"
redis 127.0.0.1:6379> hincrby student yangyan -2
(integer) 28
redis 127.0.0.1:6379> hincrby website baidu 2
(error) ERR hash value is not aninteger
redis 127.0.0.1:6379>