《Redis开发与运维》第二章 API的理解和使用(中)读书笔记


哈希

在Redis中提供了哈希(hash)类型,哈希类型是指键值本身又是一个键值对结构。形如value={{field1,value1},...,{fieldN,valueN}}

Redis的键值对和哈希类型的关系如下图:
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记
  注意:哈希类型的映射关系叫做field-value,这里的value是指field对应的值,不是键对应的值。

命令

  • 设置值
## 命令:hset key filed value
## 例如:为user:1设置一对field-value
172.17.236.250:6379> hset user:1 name tom
(integer) 1

  设置成功则返回1,设置失败返回0。Redis也提供了hsetnx,它们的关系就和setsetnx一样,只是作用域由键变成了field

  • 获取值
## 命令:hget key field
## 例如:获取user:1的name属性的值
172.17.236.250:6379> hget user:1 name
"tom"
## 如果键,或者field不存在则返回 nil
172.17.236.250:6379> hget user:2 name
(nil)
172.17.236.250:6379> hget user:1 age
(nil)
  • 删除field
## 命令:hdel key field \[field ... \]
## 例如:删除存在的 field name 和不存在的 field age
## hdel 可以删除一个或多个 field ,返回结果是成功删除field的个数。
172.17.236.250:6379> hdel user:1 name
(integer) 1
172.17.236.250:6379> hdel user:1 age
(integer) 0
  • 计算 field 的个数
## 命令:hlen key
## 例如:获取 user:1的 field 的个数
172.17.236.250:6379> hlen user:1
(integer) 1
  • 批量设置或获取field-value
## 批量获取命令:hmget key field \[field...\]
## 批量设置命令:hmset key value \[field value ... \]
## 例如:批量设置 user:1 的 field-value
172.17.236.250:6379> hmset user:1 name mike age 12 city tianjin
OK
## 例如:批量获取 user:1 的 value
172.17.236.250:6379> hmget user:1 name city age
1) "mike"
2) "tianjin"
3) "12"
  • 判断field是否存在
## 命令:hexists key field
## 例如:user:1 包含属性 name ,所以返回结果 1,否则返回 0
172.17.236.250:6379> hexists user:1 name
(integer) 1
  • 获取所有 field
## 命令:hkeys key
## 例如:返回指定 hash 键所有的 field
172.17.236.250:6379> hkeys user:1
1) "name"
2) "age"
3) "city"
  • 获取所有的 value
## 命令: hvals key
## 例如:获取 user:1 全部 value
172.17.236.250:6379> hvals user:1
1) "mike"
2) "12"
3) "tianjin"
  • 获取所有的 field-value
## 命令:hgetall key
## 例如:获取 user:1 键所有的 field-value
172.17.236.250:6379> hgetall user:1
1) "name"
2) "mike"
3) "age"
4) "12"
5) "city"
6) "tianjin"

  注意:在使用hgetall时,如果hash元素个数较多,会存在Redis阻塞的可能。只需获取部分field,可以使用 hmget,若一定要获取所有的field-value可以使用hscan命令,该命令会渐进式遍历hash类型。

  • hash 表中的属性 field 添加 increment
## 命令:hincrby key field increment
## 命令:hincrbyfloat key field increment
## 例如:给 user:1 的 age 做自增操作
172.17.236.250:6379> hmget user:1 age
1) "12"
172.17.236.250:6379> hincrby user:1 age 2
(integer) 14
172.17.236.250:6379> hincrbyfloat user:1 age 6.9
"20.9"

  如果键不存在,则创建新的hash表,并执行hincrby。如果field不存在,在执行命令前则属性的值初始化为0。

  • 计算值得字符串长度(基于Redis3.2版本及以上)
## 命令:hstrlen key field

  下面两张图片是Redis的hash类型命令的时间复杂度,咱们可以根据这两张图片的表格并结合实际开发场景选择合适命令使用。
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记

内部编码

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

  • ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有的值小于hash-max-ziplist-value配置(默认64个字节)时。Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
  • hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写复杂度为O(1)。

使用场景

  下图为关系型数据库存储两条用户的记录信息:
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记
  下图为使用哈希类型缓存用户信息:
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记
  相对于使用字符串序列化缓存用户信息,哈希类型更加直观,并且在更新操作上更加方便。将每个用户的id作为键后缀,多对field-value对应每个用户的属性。伪代码如下:

UserInfo getUserInfo(long id){
    // 用户 id 作为后缀
    userRedisKey = "user:info:"+id;
    // 使用 hgetall 获取所有用户信息关系映射
    userInfoMap = redis.hgetAll(userRedisKey);
    UserInfo userInfo;
    if(userInfo != 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)。如下图:
    《Redis开发与运维》第二章 API的理解和使用(中)读书笔记

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

  我们暂时可以使用三种方式缓存用户信息,下面分析一下这三种方案。

  • 原生字符串类型:每个属性一个键。
set user:1:name tom
set user:1:age 12
set user:1:city beijing

优点:简单直观、每个属性都支持更新操作。
缺点:占用键过多,内存占用量大,用户信息内聚性差(生产环境不会使用)。

  • 序列化字符串类型:将用户信息序列化后用一个字符串保存。
set user:1 serialize(userInfo)

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

  • 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。
hmset user:1 name tomage 23 city beijing

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


列表

  列表类型是用来存储多个有序的字符串。如下图:
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记
  列表中的每一个字符串称为元素。在Redis中可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的列表、获取指定索引下标的元素等。如下图:
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记

列表类型的特点:

  • 元素有序。
  • 元素可以重复。

命令

  列表的五种操作类型,命令如下图:
《Redis开发与运维》第二章 API的理解和使用(中)读书笔记

  • 从右边插入元素
## 命令:rpush key value \[value ... \]
## 例如:从右向左插入元素 c、b、a
172.17.236.250:6379> rpush listkey c b a
(integer) 3
  • 从左到右获取全部元素
## 命令:lrange key 0 -1
## 例如:从左到右获取 listkey 的全部元素
172.17.236.250:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
  • 从左边插入元素
## 命令:lpush key value \[value ... \]
  • 在某个元素前或者后插入元素
## 命令:linsert key before | after pivot value
## 例如:在列表的元素 b 前插入 java
172.17.236.250:6379> linsert listkey before b java
(integer) 4
172.17.236.250:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"
4) "a"
  • 获取指定范围内的元素列表
## 命令:lrange key start end
## 例如:获取 listkey 的第二到第四个元素
172.17.236.250:6379> lrange listkey 1 3
1) "java"
2) "b"
3) "a"

  lrange操作会获取列表指定索引范围的所有元素。索引下标从左到右分别是 0 到 N-1,但是从右到左分别是 -1 到 -N,lrangeend选项包含了自身。

  • 获取列表指定索引下标元素
## 命令:lindex key index
## 例如:获取 keylist 的最后一个元素
172.17.236.250:6379> lindex listkey -1
"a"
  • 获取列表长度
## 命令:llen key
## 例如:获取 listkey 的长度
172.17.236.250:6379> llen listkey
(integer) 4
  • 从列表左侧删除元素
## 命令:lpop key 
## 例如:将 listkey 列表最左侧的元素删除
172.17.236.250:6379> lpop listkey
"c"
172.17.236.250:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
  • 从列表右侧弹出
命令:rpop key
  • 删除指定元素
命令:lrem key count value
例如:删除4个为a的元素
172.17.236.250:6379> lpush listkey a a a a a
(integer) 8
172.17.236.250:6379> lrange listkey 0 -1
1) "a"
2) "a"
3) "a"
4) "a"
5) "a"
6) "java"
7) "b"
8) "a"
172.17.236.250:6379> lrem listkey 4 a
(integer) 4
172.17.236.250:6379> lrange listkey 0 -1
1) "a"
2) "java"
3) "b"
4) "a"

lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:

  1. count > 0:从左到右,删除最多 count 个元素
  2. count < 0:从右到左,删除最多 count 绝对值个元素
  3. count = 0:删除所有

  • 按照索引范围修剪列表
## 命令:ltrim key start end
## 例如:保留列表 listkey 的第二个到第四个元素
172.17.236.250:6379> ltrim listkey 1 3
OK
172.17.236.250:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
  • 修改指定索引下标的元素
## 命令:lset key index newValue
## 例如:将列表 listkey 中的第三个元素设置为 python
172.17.236.250:6379> lset listkey 2 python
OK
172.17.236.250:6379> lindex listkey 2
"python"
  • 阻塞式弹出
## 命令:blpop | brpop key \[key ... \] timeout

  blpopbrpoplpoprpop 的阻塞版本,它们除了弹出方向不同,使用方法基本相同。以brpop 进行说明。
brpop 包含两个参数:

  1. key [key … ]:多个列表的键
  2. timeout:阻塞时间(单位:秒)
  • 列表为空:如果 timeout = 3 ,那么客户端等3秒后返回,如果timeout = 0,则一直处于阻塞状态:
172.17.236.250:6379> brpop list:test 3
(nil)
(3.00s)
172.17.236.250:6379> brpop list:test 0
··· 阻塞 ···
  • 如果在此期间列表添加了元素,客户端会立刻返回
172.17.236.250:6379> brpop list:test 0 
1) "list:test" 
2) "element1"
  • 列表不为空时,客户端会立刻返回
172.17.236.250:6379> brpop listkey 3
1) "listkey"
2) "python"

注意:

  • 如果是多个键,那么 brpop 会从左至右遍历键,一但有一个键能弹出元素,客户端立即返回。
  • 如果多个客户端对同一个键执行 brpop ,那么最先执行 brpop命令的客户端可以获取到弹出的值。
      下表示列表的命令时间复杂度,可以参考这个表选择适合的命令。
    《Redis开发与运维》第二章 API的理解和使用(中)读书笔记

内部编码

列表类型的内部编码有两种。

  • ziplist(压缩列表): 当列表的元素个数小于 list-max-ziplist-entries 配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value 配置时(默认64个字节),Redis会选用 ziplist 作为列表的内部实现。
  • linkedlist(链表): 当列表类型无法满足ziplist的条件时,Redis会使用 linkedlist 作为列表的内部实现。

使用场景

  • 消息队列
      Redis的 lpush+brpop 命令组合即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。如下图:
    《Redis开发与运维》第二章 API的理解和使用(中)读书笔记
  • 文章列表
      每一个用户都有自己的文章列表,需要分页展示文章列表。这时就可以使用列表,因为列表有序且支持按照索引范围获取元素。
      每一篇文章使用哈希结构存储,例如:每篇文章有三个属性 title、 timestamp、content:
172.17.236.250:6379> hmset acticle:1 title xx timestamp 1476536196 content xxx
OK
172.17.236.250:6379> hmset acticle:1 title yy timestamp 1476536196 content yyy
OK

  向用户文章列表添加文章,user:{id}:articles 作为用户文章列表的键:

172.17.236.250:6379> lpush user:1:articles article:1
(integer) 1

  分页获取用户的文章列表,伪代码如下:

articles = lrange user:1: articles 0 9
for article in {articles}
    hgetall {article}

使用列表类型保存和获取文章列表存在两个问题:

  • 每次分页获取文章个数多,需要多次执行 hgetall ,此时可以考虑使用 pipeline 批量获取,或者将文章数据序列化为字符串类型,使用 mget 批量获取。
  • 分页获取文章列表,lrange 命令在列表过大的情况下,获取列表中间的元素性能会变差,此时可以考虑将列表做二级拆分。

在实际场景中列表的使用场景很多,选择时可以参考一下口诀:

  • lpush + lpop = Stack(栈)
  • lpush + rpop = Queue(队列)
  • lpush + ltrim = Capped Connection(有限集合)
  • lpush + brpop = Message Queue(消息队列)