【第二章 API的理解和使用】2.3、哈希

in with 0 comment

哈希

  1. 命令

    (1)设置值

    hset key field value

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

    127.0.0.1:6379> hset user:1 name tom
    (integer) 1
    

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

    (2)获取值

    hget key field

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

    127.0.0.1:6379> hget user:1 name
    "tom"
    

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

    127.0.0.1:6379> hget user:2 name
    (nil)
    127.0.0.1:6379> hget user:1 age
    (nil)
    

    (3)删除field

    hdel key field [field...]

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

    127.0.0.1:6379> hdel user:1 name 
    (integer) 1
    127.0.0.1:6379> hdel user:1 age
    (integer) 0
    

    (4)计算field个数

    hlen key

    例如user:1有3个field

    127.0.0.1:6379> hset user:1 name tom
    (integer) 1
    127.0.0.1:6379> hset user:1 age 23
    (integer) 1
    127.0.0.1:6379> hset user:1 city tianjin
    (integer) 1
    127.0.0.1:6379> 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> hmset user:1 name mike age 12 city tianjin
    OK
    127.0.0.1:6379> hmget user:1 name city
    1) "mike"
    2) "tianjin"
    

    (6)判断field是否存在

    hexists key filed

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

    127.0.0.1:6379> hexists user:1 name
    (integer) 1
    

    (7)获取所有filed

    hkeys key

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

    127.0.0.1:6379> hkeys user:1
    1) "name"
    2) "age"
    3) "city"
    

    (8)获取所有value

    hvals key

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

    127.0.0.1:6379> hvals user:1
    1) "mike"
    2) "12"
    3) "tianjin"
    

    (9)获取所有的field-value

    hgetall key

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

    127.0.0.1:6379> hgetall user:1
    1) "name"
    2) "mike"
    3) "age"
    4) "12"
    5) "city"
    6) "tianjin"
    

    (10)hinerby hincybyfloat

    hincrby key field hincrbyfloat key field

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

    (11)计算value的字符串长度

    hstrlen key field

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

    127.0.0.1:6379> hstrlen user:1 name
    (integer) 3
    

    下表是哈希类型命令的时间复杂度

    命令时间复杂度
    hset key field valueO(1)
    hget key fieldO(1)
    hdel key field [field ...]O(k),k是field的个数
    hlen keyO(1)
    hgetall keyO(n),n是field的总数
    hmget field [field ...]O(k),k是field的个数
    hmset field value [field value ...]O(k),k是field的个数
    hexists key fieldO(1)
    hkeys keyO(n),n是field的总数
    hvals keyO(n),n是field的总数
    hstenx key field valueO(1)
    hincrby key field incrementO(1)
    hincrbyfloat key filed incrementO(1)
    hstrlen key fieldO(1)
  2. 内部编码

    哈希类型的编码有两种:

    • 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> hmset hashkey f1 v1 f2 v2
    OK
    127.0.0.1:6379> objet encoding hashkey
    "ziplist"
    

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

    127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 byte ...忽略..."
    OK
    127.0.0.1:6379> object encoding hashkey
    "hashtable"
    

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

    127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ...忽略... f513 v513
    OK
    127.0.0.1:6379> object encoding hashkey
    "hashtable"
    
  3. 使用场景

    下表为关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信 息作为行

    idnameagecity
    1tom23beijing
    2mike30tianjin

    如果将其用哈希类型存储,如下表

    user:1

    fieldvalue
    id1
    nametom
    age23
    citybeijing

    user:2

    fieldvalue
    id2
    namemike
    age30
    citytianjing

    相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操 作上会更加便捷。可以将每个用户的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 usreInfo;
    }
    

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

    • 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可 以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值 (即使为NULL),如下表:
    idnamefavorcityagegender
    1tomsprotbeijingNULLNULL
    2mikeNULLNULL30male

    user:1

    fieldvalue
    id1
    nametom
    favorsport
    citybeijing

    user:2

    fieldvalue
    id2
    namemike
    age30
    gendermale

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

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

    set user:1:name tom
    set user:1:age 23
    set user:1:city beijing 
    

    优点:简单直观,每个属性都支持更新操作。 缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种 方案一般不会在生产环境使用。

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

    set user:1 serialize(usreInfo)

    优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。 缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取 出进行反序列化,更新后在序列化到Redis中。

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

    hmset user:1 name tom age 23 city beijing

    优点:简单直观,如果使用合理可以减少内存空间的使用。 缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。