天天看点

Redis开发与运维. 2.3 哈希

<b>2.3 哈希</b>

几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组。在redis中,哈希类型是指键值本身又是一个键值对结构,形如value={{field1,value1},...{fieldn,valuen}},redis键值对和哈希类型二者的关系可以用图2-14来表示。

图2-14 字符串和哈希类型对比

哈希类型中的映射关系叫作field-value,注意这里的value是指field对应的值,不是键对应的值,请注意value在不同上下文的作用。

<b>2.3.1 命令</b>

(1)设置值

hset key field value

下面为user:1添加一对field-value:

127.0.0.1:6379&gt; hset user:1 name tom?

(integer) 1

如果设置成功会返回1,反之会返回0。此外redis提供了hsetnx命令,它们的关系就像set和setnx命令一样,只不过作用域由键变为field。

(2)获取值

hget key field

例如,下面操作获取user:1的name域(属性)对应的值:

127.0.0.1:6379&gt; hget user:1 name?

"tom"

如果键或field不存在,会返回nil:

127.0.0.1:6379&gt; hget user:2 name?

(nil)?

127.0.0.1:6379&gt; hget user:1 age?

(nil)

(3)删除field

hdel key field [field ...]

hdel会删除一个或多个field,返回结果为成功删除field的个数,例如:

127.0.0.1:6379&gt; hdel user:1 name?

127.0.0.1:6379&gt; hdel user:1 age?

(integer) 0

(4)计算field个数

hlen key

例如user:1有3个field:

(integer) 1?

127.0.0.1:6379&gt; hset user:1 age 23

127.0.0.1:6379&gt; hset user:1 city tianjin

127.0.0.1:6379&gt; hlen user:1

(integer) 3

(5)批量设置或获取field-value

hmget key field [field ...]?

hmset key field value [field value ...]

hmset和hmget分别是批量设置和获取field-value,hmset需要的参数是key和多对field-value,hmget需要的参数是key和多个field。例如:

127.0.0.1:6379&gt; hmset user:1 name mike

age 12 city tianjin

ok?

127.0.0.1:6379&gt; hmget user:1 name city

1) "mike"

2) "tianjin"

(6)判断field是否存在

hexists key field

例如,user:1包含name域,所以返回结果为1,不包含时返回0:

127.0.0.1:6379&gt; hexists user:1 name

(7)获取所有field

hkeys key

hkeys命令应该叫hfields更为恰当,它返回指定哈希键所有的field,例如:

127.0.0.1:6379&gt; hkeys user:1

1) "name"?

2) "age"?

3) "city"

(8)获取所有value

hvals key

下面操作获取user:1全部value:

127.0.0.1:6379&gt; hvals user:1?

1) "mike"?

2) "12"?

3) "tianjin"

(9)获取所有的field-value

hgetall key

下面操作获取user:1所有的field-value:

127.0.0.1:6379&gt; hgetall user:1

1) "name"

2) "mike"

3) "age"

4) "12"

5) "city"

6) "tianjin"

在使用hgetall时,如果哈希元素个数比较多,会存在阻塞redis的可能。如果开发人员只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型,hscan将在2.7节介绍。

(10)hincrby hincrbyfloat

hincrby key field

hincrbyfloat key field

hincrby和hincrbyfloat,就像incrby和incrbyfloat命令一样,但是它们的作用域是filed。

(11)计算value的字符串长度(需要redis 3.2以上)

hstrlen key field

例如hget user:1 name的value是tom,那么hstrlen的返回结果是3:

127.0.0.1:6379&gt; hstrlen user:1 name

表2-3是哈希类型命令的时间复杂度,开发人员可以参考此表选择适合的命令。

表2-3 哈希类型命令的时间复杂度

命  令         时间复杂度

hset key field value  o(1)

hget key field   o(1)

hdel key field [field ...]      o(k),k是field个数

hlen key   o(1)

hgetall key        o(n),n是field总数

hmget field [field ...]         o(k),k是field的个数

hmset field value [field value ...]      o(k),k是field的个数

hexists key field        o(1)

hkeys key o(n),n是field总数

hvals key  o(n),n是field总数

hsetnx key field value       o(1)

hincrby key field increment      o(1)

hincrbyfloat key field increment      o(1)

hstrlen key field        o(1)?

<b>2.3.2 内部编码</b>

哈希类型的内部编码有两种:

ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。

hashtable(哈希表):当哈希类型无法满足ziplist的条件时,redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为o(1)。

下面的示例演示了哈希类型的内部编码,以及相应的变化。

1)当field个数比较少且没有大的value时,内部编码为ziplist:

127.0.0.1:6379&gt; hmset hashkey f1 v1 f2

v2

ok

127.0.0.1:6379&gt; object encoding hashkey

"ziplist"

2.1)当有value大于64字节,内部编码会由ziplist变为hashtable:

127.0.0.1:6379&gt; hset hashkey f3

"one string is bigger than 64 byte...忽略..."

"hashtable"

2.2)当field个数超过512,内部编码也会由ziplist变为hashtable:

v2 f3 v3 ...忽略... f513 v513

有关哈希类型的内存优化技巧将在8.3节中详细介绍。

<b>2.3.3 使用场景</b>

图2-15为关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行。

如果将其用哈希类型存储,如图2-16所示。

相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对field-value对应每个用户的属性,类似如下伪代码:

userinfo getuserinfo(long id){?

// 用户id作为key后缀

userrediskey = "user:info:" + id;

// 使用hgetall获取所有用户信息映射关系

userinfomap = redis.hgetall(userrediskey);

userinfo userinfo;?

if (userinfomap != null) {?

// 将映射关系转换为userinfo

userinfo = transfermaptouserinfo(userinfomap);?

    }

else {

// 从mysql中获取用户信息

userinfo = mysql.get(id);

// 将userinfo变为映射关系使用hmset保存到redis中

redis.hmset(userrediskey, transferuserinfotomap(userinfo));

// 添加过期时间?

redis.expire(userrediskey, 3600);

}?

return userinfo;?

}

但是需要注意的是哈希类型和关系型数据库有两点不同之处:

哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为null),如图2-17所示。

关系型数据库可以做复杂的关系查询,而redis去模拟关系型复杂查询开发困难,维护成本高。

图2-17 关系型数据库稀疏性

开发人员需要将两者的特点搞清楚,才能在适合的场景使用适合的技术。到目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点

分析。

1)原生字符串类型:每个属性一个键。

set user:1:name tom

set user:1:age 23?

set user:1:city beijing

优点:简单直观,每个属性都支持更新操作。

缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。

2)序列化字符串类型:将用户信息序列化后用一个键保存。

set user:1 serialize(userinfo)

优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。

缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到redis中。

3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。

hmset user:1 name tom?age 23 city beijing

优点:简单直观,如果使用合理可以减少内存空间的使用。

缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。