Redis 基本数据结构理解与使用

Redis 基本数据结构理解与使用

  Redis 提供了 5种数据结构,理解每种数据结构的特点对于 Redis 开发运维非常重要,同时掌握 Redis 的单线程命令处理机制,会使数据结构和命令的选择事半功倍

1. 概览

  在正式介绍 5种数据结构之前,了解一下 Redis 的一些全局命令、数据结构和内部编码、单线程命令处理机制是十分有必要的,它们能为后面内容的学习打下一个好的基础,主要体现在两个方面:

  • 第一、Redis 的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解 Redis 的一些机制,会发现这些命令有很强的通用性。
  • 第二、Redis 不是万金油,有些数据结构和命令 必须在特定场景下使用,一旦使用不当可能对Redis本身或者应用本身造成 致命伤害。

  

1.1 全局命令

  Redis 有 5种数据结构,它们是键值对中的值,对于键来说有一些通用的命令:

  

1.1.1 keys 查看所有键

127.0.0.1:6379> set hello world OK
127.0.0.1:6379> set java jedis OK
127.0.0.1:6379> set python redis-py OK

127.0.0.1:6379> keys * 1) "python"
2) "java"
3) "hello"

keys 命令会遍历所有键,所以它的时间复杂度是O(n),当 Redis 保存了大量键时,线上环境禁止使用。

  

1.1.2 dbsize 键总数

127.0.0.1:6379> rpush mylist a b c d e f g (integer) 7

127.0.0.1:6379> dbsize 
(integer) 4

dbsize 命令在计算键总数时不会遍历所有键,而是直接获取 Redis 内置的 键总数变量,所以dbsize命令的时间复杂度是O(1)。

  

1.1.3 exists 检查键是否存在

# 如果键存在则返回1,不存在则返回0:
127.0.0.1:6379> exists java (integer) 1
127.0.0.1:6379> exists not_exist_key (integer) 0

  

1.1.4 del 删除键

  del 是一个通用命令,无论值是什么数据结构类型,del 命令都可以将其 删除,例如下面将字符串类型的键 java 和列表类型的键 mylist 分别删除:

del key [key ...]

127.0.0.1:6379> del java 
(integer) 1
127.0.0.1:6379> exists java 
(integer) 0
127.0.0.1:6379> del mylist
(integer) 1
127.0.0.1:6379> exists mylist 
(integer) 0

# 返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回 0:
127.0.0.1:6379> del not_exist_key 
(integer) 0

# 同时del命令可以支持删除多个键:
127.0.0.1:6379> set a 1 OK
127.0.0.1:6379> set b 2 OK
127.0.0.1:6379> set c 3 OK
127.0.0.1:6379> del a b c (integer) 3

  

1.1.5 expire 键过期

  Redis 支持对键添加过期时间,当超过过期时间后,会自动删除键,例如为键hello 设置了 10秒过期时间

expire key seconds


127.0.0.1:6379> set hello world 
OK
127.0.0.1:6379> expire hello 10 
(integer) 1

ttl 命令会返回键的剩余过期时间,它有3种返回值:

  • 大于等于0的整数:键剩余的过期时间;
  • ·-1:键没设置过期时间;
  • ·-2:键不存在
#还剩7秒
127.0.0.1:6379> ttl hello 
(integer) 7
...
#还剩1秒
127.0.0.1:6379> ttl hello 
(integer) 1 #返回结果为-2,说明键hello已经被删除 
127.0.0.1:6379> ttl hello 
(integer) -2 
127.0.0.1:6379> get hello 
(nil)

  

1.1.6 type 键的数据结构类型

  例如键 hello 是字符串类型,返回结果为 string。键 mylist 是列表类型,返回结果为 list:

type key

127.0.0.1:6379> set a b 
OK
127.0.0.1:6379> type a 
string
127.0.0.1:6379> rpush mylist a b c d e f g 
(integer) 7
127.0.0.1:6379> type mylist
list

# 如果键不存在,则返回none:
127.0.0.1:6379> type not_exsit_key 
none

  

1.1.7 object encoding 命令查询内部编码

  每种数据结构都有自己底层的内部编码实现,而且是多种实现,可以通过 object encoding 命令查询内部编码:

127.0.0.1:6379> object encoding hello 
"embstr"
127.0.0.1:6379> object encoding mylist 
"ziplist"

  
  

1.2 数据结构和内部编码

  type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集 合),但这些只是 Redis 对外的数据结构。

在这里插入图片描述

  实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现, 这样 Redis 会在合适的场景选择合适的内部编码。在这里插入图片描述

  可以通过 object encoding 命令查询内部编码:

127.0.0.1:6379> object encoding hello 
"embstr"
127.0.0.1:6379> object encoding mylist 
"ziplist"

  
Redis这样设计有两个好处:

  • 第一,可以改进内部编码,而对外的数据 结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数 据结构和命令,例如 Redis3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了一种更为优秀的内部编码实现,而对外部用户来说基本感知不到;
  • 第二,多种内部编码实现可以在不同场景下发挥各自的优 势,例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为 linkedlist。

  

1.3 单线程架构

1.3.1 单线程模型

  Redis使用了单线程架构和 I/O 多路复用模型来实现高性能的内存数据库服务,现在开启了三个 redis-cli 客户端同时执行命令:

# 客户端1设置一个字符串键值对:
127.0.0.1:6379> set hello world
# 客户端2对counter做自增操作:
127.0.0.1:6379> incr counter
# 客户端3对counter做自增操作:
127.0.0.1:6379> incr counter

  Redis 客户端与服务端的模型可以简化成下图,每次客户端调用都经历了发送命令、执行命令、返回结果三个过程

在这里插入图片描述

  其中第2步是重点要讨论的,因为 Redis 是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。所以上面 3个客户端命令的执行顺序是不确定的,但是可以确定不会有两条命令被同时执行, 所以两条 incr 命令无论怎么执行最终结果都是 2,不会产生并发问题,这就是 Redis 单线程的基本模型。

  

1.3.2 为什么单线程还能这么快

  通常来讲,单线程处理能力要比多线程差,那么为什么 Redis 使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点:

  1. 第一,纯内存访问,Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 Redis 达到每秒万级别访问的重要基础;
  2. 第二,非阻塞I/O,Redis 使用 epoll作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间;

在这里插入图片描述

  1. 单线程避免了线程切换和竞态产生的消耗。

  因为单线程能带来几个好处:第一,单线程可以简化数据结构和算法的实现。第二,单线程避免了线程切换和竞态产生的消耗, 对于服务端开发来说,锁和线程切换通常是性能杀手。

  但是单线程会有一个问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于 Redis 这种高性能的服务来说是致命的,所以 Redis 是面向快速执行场景的数据库。

  

  

2. 字符串

  字符串类型是 Redis 最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。

在这里插入图片描述

  如图所示,字符串类型的值实际可以 是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字 (整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能 超过512MB。

  
  

2.1 命令

2.1.1 set 设置值

set key value [ex seconds] [px milliseconds] [nx|xx]

set命令有几个选项:

  • ex seconds:为键设置秒级过期时间。
  • px milliseconds:为键设置毫秒级过期时间。 ·
  • nx:键必须不存在,才可以设置成功,用于添- 加。
  • xx:与nx相反,键必须存在,才可以设置成功,用于更新。

  
除了set选项,Redis还提供了setex和setnx两个命令:

- setex key seconds value 
- setnx key value

  它们的作用和 ex 和 nx 选项是一样的。下面的例子说明了 set、setnx、set xx的区别

# 当前键hello不存在:
127.0.0.1:6379> exists hello
(integer) 0

# 下面操作设置键为hello,值为world的键值对,返回结果为OK代表设置 成功:
127.0.0.1:6379> set hello world OK

# 因为键hello已存在,所以setnx失败,返回结果为0:
127.0.0.1:6379> setnx hello redis
(integer) 0

# 因为键hello已存在,所以set xx成功,返回结果为OK:
127.0.0.1:6379> set hello jedis xx
OK

setnx 和 setxx 在实际使用中有什么应用场景吗?
以setnx命令为例子,由于 Redis 的单线程命令处理机制,如果有多个客户端同时执行 setnx key value, 根据 setnx 的特性只有一个客户端能设置成功,setnx 可以作为分布式锁的一种实现方案,Redis 官方给出了使用 setnx 实现分布式锁的方 法:http://redis.io/topics/distlock。

  

2.1.2 get 获取值

get key

# 下面操作获取键hello的值: 
127.0.0.1:6379> get hello
"world"
# 如果要获取的键不存在,则返回nil(空): 
127.0.0.1:6379> get not_exist_key
(nil)

  

2.1.3 mset 批量设置值

mset key value [key value ...]

# 下面操作通过mset命令一次性设置4个键值对:
127.0.0.1:6379> mset a 1 b 2 c 3 d 4
OK

  

2.1.4 mget 批量获取值

mget key [key ...]

# 下面操作批量获取了键a、b、c、d的值:
127.0.0.1:6379> mget a b c d 
1) "1"
2) "2"
3) "3"
4) "4"

# 如果有些键不存在,那么它的值为nil(空),结果是按照传入键的顺序返回:
127.0.0.1:6379> mget a b c f 
1) "1"
2) "2"
3) "3"
4) (nil)

批量操作命令可以有效提高开发效率,学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批 量操作所发送的命令数不是无节制的,如果数量过多可能造成 Redis 阻塞或 者网络拥塞。

  

2.1.5 incr 计数

incr 命令用于对值做自增操作,返回结果分为三种情况:

  • 值不是整数,返回错误。
  • 值是整数,返回自增后的结果。
  • 键不存在,按照值为0自增,返回结果为1。
incr key

# 例如对一个不存在的键执行incr操作后,返回结果是1:

127.0.0.1:6379> exists key 
(integer) 0 
127.0.0.1:6379> incr key 
(integer) 1

# 再次对键执行incr命令,返回结果是2:
127.0.0.1:6379> incr key
(integer) 2

# 如果值不是整数,那么会返回错误:
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range

  除了 incr命令,Redis 提供了 decr(自减)、incrby(自增指定数字)、 decrby(自减指定数字)、incrbyfloat(自增浮点数)

decr key
incrby key increment 
decrby key decrement 
incrbyfloat key increment

很多存储系统和编程语言内部使用 CAS 机制实现计数功能,会有一定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执行。

  

2.1.6 时间复杂度

在这里插入图片描述

  

2.2 内部编码

字符串类型的内部编码有3种:

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。

# 整数类型示例如下:
127.0.0.1:6379> set key 8653
OK
127.0.0.1:6379> object encoding key 
"int"

# 短字符串示例如下:
#小于等于39个字节的字符串:embstr 
127.0.0.1:6379> set key "hello,world" 
OK
127.0.0.1:6379> object encoding key 
"embstr"

# 长字符串示例如下:
#大于39个字节的字符串:raw
127.0.0.1:6379> set key "one string greater than 39 byte........." 
OK
127.0.0.1:6379> object encoding key
"raw"
127.0.0.1:6379> strlen key
(integer) 40

  

2.3 典型使用场景

2.3.1 缓存功能

  下图是比较典型的缓存使用场景,其中 Redis 作为缓存层,MySQL 作为存层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

在这里插入图片描述

UserInfo getUserInfo(long id){ 
	userRedisKey = "user:info:" + id value = redis.get(userRedisKey); 
	UserInfo userInfo;
	if (value != null) {
		userInfo = deserialize(value); } else {
		userInfo = mysql.get(id); if (userInfo != null)
		redis.setex(userRedisKey, 3600, serialize(userInfo));
	}
	return userInfo; 
}

设计合理的键名,有利于防止键冲突和项目的可维护性:比较推荐的方式是使用“业务名:对象名:ID:[属性]”作为键名(也可以不是分号):

  • 例如MySQL的数据库名为 vs,用户表名为user,那么对应的键可用"vs:user:1","vs:user:1: name"来表示,如果当前Redis只被一个业务使用,甚至可以去掉“vs:”。
  • 如果键名比较长,例如“user:{uid}:friends:messages:{mid}”,可以在能描述键含义的前提下适当减少键的长度,例如变为“u:{uid}:f:msg: {mid}”,从而减少由于键过长的内存浪费。

  

2.3.2 计数

  许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、 查询缓存的功能,同时数据可以异步落地到其他数据源。例如笔者所在团队的视频播放数系统就是使用Redis作为视频播放数计数的基础组件,用户每 播放一次视频,相应的视频播放数就会自增1:

long incrVideoCounter(long id) { 
	key = "video:playCount:" + id; 
	return redis.incr(key);
}

  

2.3.3 共享Session

  如图所示,一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可 能会发现需要重新登录,这个问题是用户无法容忍的。

在这里插入图片描述

  为了解决这个问题,可以使用 Redis 将用户的 Session 进行集中管理,如图所示,在这种模式下只要保证 Redis 是高可用和扩展性的,每次用户更新或者查询登录信息都直接从 Redis 中集中获取。

在这里插入图片描述

  

2.3.4 限速

  很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,如图所示。

在这里插入图片描述

phoneNum = "138xxxxxxxx";
key = "shortMsg:limit:" + phoneNum;
// SET key value EX 60 NX
isExists = redis.set(key,1,"EX 60","NX"); 
if(isExists != null || redis.incr(key) <=5){
	// 通过 
}else{
	// 限速 
}

  上述就是利用 Redis 实现了限速功能,例如一些网站限制一个 IP 地址不能在一秒钟之内访问超过 n次也可以采用类似的思路。

  
  

3. 哈希

  在 Redis 中,哈希类型是指键值本身又是一个键值对 结构,形如 value={{field1,value1},…{fieldN,valueN}},Redis 键值对和哈希类型二者的关系可以用图来表示。

在这里插入图片描述

  

3.1 命令

3.1.1 hset 设置值

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。

  

3.1.2 hget 获取值

hget key field

# 下面操作获取user:1的name域(属性)对应的值:
127.0.0.1:6379> hget user:1 name
"tom"

# 如果键或field不存在,会返回nil:
127.0.0.1:6379> hget user:2 name 
(nil)
127.0.0.1:6379> hget user:1 age 
(nil)

  

3.1.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

  

3.1.4 hlen 计算field个数

hlen key

# 如user:1有3个field:
127.0.0.1:6379> hset user:1 
(integer) 1
127.0.0.1:6379> hset user:1 
(integer) 1
127.0.0.1:6379> hset user:1 
(integer) 1
127.0.0.1:6379> hlen user:1 
(integer) 3

  

3.1.5 hmset hmget 批量设置或获取 field-value

hmget key field [field ...]
hmset key field value [field value ...]

# hmset和hmget分别是批量设置和获取field-value
# hmset需要的参数是key 和多对field-value
127.0.0.1:6379> hmset user:1 name mike age 12 city tianjin 
OK

# hmget需要的参数是key和多个field
127.0.0.1:6379> hmget user:1 name city
1) "mike"
2) "tianjin"

  

3.1.6 hexists 判断 field 是否存在

hexists key field

# 例如,user:1包含name域,所以返回结果为1,不包含时返回0:
127.0.0.1:6379> hexists user:1 name
(integer) 1

  

3.1.7 hkeys 获取所有 field

hkeys key

# hkeys命令应该叫hfields更为恰当,它返回指定哈希键所有的field
127.0.0.1:6379> hkeys user:1 
1) "name"
2) "age"
3) "city"

  

3.1.8 hvals 获取所有 value

hvals key

# 下面操作获取user:1全部value:
127.0.0.1:6379> hvals user:1 
1) "mike"
2) "12"
3) "tianjin"

  

3.1.9 hgetall 获取所有的 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"

在使用 hgetall 时,如果哈希元素个数比较多,会存在阻塞 Redis 的可能:

  • 如果开发人员只需要获取部分 field,可以使用 hmget;
    如果一定要获取全部 field-value,可以使用 hscan 命令,该命令会渐进式遍历哈希类型。

  

3.1.10 hincrby hincrbyfloat

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

hincrby key field
hincrbyfloat key field

  

3.1.11 时间复杂度

在这里插入图片描述
在这里插入图片描述

  

3.2 内部编码

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

  • ziplist(压缩列表):
    • 当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认512个);
    • 同时所有值都小于 hash-max-ziplist-value 配置(默认64 字节)时,Redis会使用 ziplist 作为哈希的内部实现;
    • ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
  • hashtable(哈希表):
    • 当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。
# 当field个数比较少且没有大的value时,内部编码为ziplist:
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 
OK
127.0.0.1:6379> object encoding hashkey 
"ziplist"

# 当有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"

# 当field个数超过512,内部编码也会由ziplist变为hashtable:
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ...忽略... f513 v513
127.0.0.1:6379> object encoding hashkey 
"hashtable"

  

3.3 使用场景

  图为关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行。如果将其用哈希类型存储,如图所示。

在这里插入图片描述

在这里插入图片描述

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

  到目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析

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

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

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

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

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

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

  

  

4. 列表

  列表(list)类型是用来存储多个有序的字符串,如图所示,a、 b、c、d、e 五个元素从左到右组成了一个有序的列表:

  • 列表中的每个字符串称为元素(element),
  • 一个列表最多可以存储232-1个元素。
  • 在 Redis 中,可 以对列表两端插入(push)和弹出(pop),
  • 还可以获取指定范围的元素列表、获取指定索引下标的元素等;
  • 列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

在这里插入图片描述
  
列表类型有两个特点:

  • 第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表;
  • 第二、列表中的元素可以是重复的。

在这里插入图片描述

  

4.1 命令

  下面将按照对列表的5种操作类型对命令进行介绍

在这里插入图片描述

  

4.1.1 添加操作

4.1.1.1 rpush 从右边插入元素
rpush key value [value ...]

# 从右向左插入元素c、b、a:
127.0.0. 1:6379> rpush listkey c b a
(integer) 3

# lrange 0-1命令可以从左到右获取列表的所有元素:
127.0.0.1:6379> lrange listkey 0 -1 
1) "c"
2) "b"
3) "a"

  

4.1.1.2 lpush 从左边插入元素
# 使用方法和rpush相同,只不过从左侧插入
lpush key value [value ...]

  

4.1.1.3 linsert 向某个元素前或者后插入元素

  linsert 命令会从列表中找到等于 pivot 的元素,在其前 (before) 或者后 (after) 插入一个新的元素 value

linsert key before|after pivot value

# 例如下面操作会在列表的元素b前插入 java:
127.0.0.1:6379> linsert listkey before b java 
(integer) 4

# 返回结果为4,代表当前命令的长度,当前列表变为:
127.0.0.1:6379> lrange listkey 0 -1 
1) "c"
2) "java"
3) "b"
4) "a"

  

4.1.2 查找

4.1.2.1 lrange 获取指定范围内的元素列表

lrange 操作会获取列表指定索引范围所有的元素。索引下标有两个特点:

  • 第一,索引下标从左到右分别是 0 到 N-1,但是从右到左分别是 -1到 -N;
  • 第二,lrange 中的 end 选项包含了自身,这个和很多编程语言不包含 end 不太相同。
lrange key start end

# 获取列表的第2到第4个元
127.0.0.1:6379> lrange listkey 1 3 
1) "java"
2) "b"
3) "a"

  

4.1.2.2 lindex 获取列表指定索引下标的元素
lindex key index

# 当前列表最后一个元素为a:
127.0.0.1:6379> lindex listkey -1
"a"

  

4.1.2.3 llen 获取列表长度
llen key

# 当前列表长度为4:
127.0.0.1:6379> llen listkey
(integer) 4

  

4.1.3 删除

4.1.3.1 lpop 从列表左侧弹出元素
lpop key

# 如下操作将列表最左侧的元素c会被弹出,弹出后列表变为java、b、 a:
127.0.0.1:6379>t lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1 
1) "java"
2) "b" 
3) "a"

  

4.1.3.2 从列表右侧弹出
# 它的使用方法和lpop是一样的,只不过从列表右侧弹出
rpop key

  

4.1.3.3 lrem 删除指定元素

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

  • count>0,从左到右,删除最多 count 个元素。
  • count<0,从右到左,删除最多 count 绝对值个元素。
  • count=0,删除所有。
lrem key count value

# 向列表从左向右插入5个a,将从列表左边开始删除4个为a的元素:
127.0.0.1:6379> lrem listkey 4 a 
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1 
1) "a"
2) "java" 
3) "b"
4) "a"

  

4.1.3.4 ltrim 按照索引范围修剪列表
ltrim key start end

# 下面操作会只保留列表listkey第2个到第4个元素:
127.0.0.1:6379> ltrim listkey 1 3 
OK
127.0.0.1:6379> lrange listkey 0 -1 
1) "java"
2) "b"
3) "a"

  

4.1.4 修改

4.1.4.1 lset 修改指定索引下标的元素
lset key index newValue

# 下面操作会将列表listkey中的第3个元素设置为python:
127.0.0.1:6379> lset listkey 2 python 
OK
127.0.0.1:6379> lrange listkey 0 -1 
1) "java"
2) "b"
3) "python"

  

4.1.5 阻塞操作

blpop key [key ...] timeout 
brpop key [key ...] timeout

  blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,它们除了弹出方向不同,使用方法基本相同,所以下面以 brpop 命令进行说明,brpop 命令包含两个参数:

  • key[key…]:多个列表的键;
  • timeout:阻塞时间(单位:秒)。
  1. 列表为空:如果 timeout=3,那么客户端要等到 3秒后返回,如果 timeout=0,那么客户端一直阻塞等下去:
127.0.0.1:6379> brpop list:test 3 
(nil)
(3.10s)
127.0.0.1:6379> brpop list:test 0 
...阻塞...

  如果此期间添加了数据 element1,客户端立即返回:

127.0.0.1:6379> brpop list:test 3 
1) "list:test"
2) "element1"
(2.06s)
  1. 列表不为空:客户端会立即返回
127.0.0.1:6379> brpop list:test 0 
1) "list:test"
2) "element1"

在使用brpop时,有两点需要注意:

  • 第一点,如果是多个键,那么 brpop 会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回;
    第二点,如果多个客户端对同一个键执行 brpop,那么最先执行 brpop 命令的客户端可以获取到弹出的值。

  

4.1.6 时间复杂度

在这里插入图片描述

  

4.2 内部编码

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

  • ziplist(压缩列表):
    • 当列表的元素个数小于 list-max-ziplist-entries 配置 (默认512个);
    • 同时列表中每个元素的值都小于 list-max-ziplist-value 配置时 (默认64字节);
    • Redis 会选用 ziplist 来作为列表的内部实现来减少内存的使用。
  • linkedlist(链表):
    • 当列表类型无法满足 ziplist 的条件时,Redis 会使用 linkedlist 作为列表的内部实现。
# 当元素个数较少且没有大元素时,内部编码为ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3 (integer) 3
127.0.0.1:6379> object encoding listkey 
"ziplist"

# 当元素个数超过512个,内部编码变为linkedlist:
127.0.0.1:6379> rpush listkey e4 e5 ...忽略... e512 e513 (integer) 513
127.0.0.1:6379> object encoding listkey
"linkedlist"

# 或者当某个元素超过64字节,内部编码也会变为linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte............... ................."
(integer) 4
127.0.0.1:6379> object encoding listkey
"linkedlist"

Redis3.2版本提供了quicklist 内部编码,简单地说它是以一个 ziplist 为节点的 linkedlist,它结合了 ziplist和 linkedlis t两者的优势,为列表类型提供了一种更为优秀的内部编码实现。

  

4.3 使用场景

4.3.1 消息队列

  Redis 的 lpush+brpop 命令组合即可实现阻塞队列,生产者客户端使用 lrpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令 阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用 性。

在这里插入图片描述

  

4.3.2 文章列表

  每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

  • 每篇文章使用哈希结构存储,例如每篇文章有3个属性title、timestamp、content;
hmset acticle:1 title xx timestamp 1476536196 content xxxx
hmset acticle:k title yy timestamp 1476512536 content yyyy ...
  • 向用户文章列表添加文章,user:{id}:articles 作为用户文章列表的 键;
lpush user:1:acticles article:1 article3 ...
lpush user:k:acticles article:5
...
  • 分页获取用户文章列表,例如下面伪代码获取用户 id=1 的前10篇文章。
articles = lrange user:1:articles 0 9 
for article in {articles}
	hgetall {article}

  

  

5. 集合

  集合 (set) 类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合型,能在实际开发中解决很多实际问题。

在这里插入图片描述

  

5.1 命令

5.1.1 sadd 添加元素

sadd key element [element ...]

# 返回结果为添加成功的元素个数,例如:
127.0.0.1:6379> exists myset 
(integer) 0
127.0.0.1:6379> sadd myset a b c 
(integer) 3
127.0.0.1:6379> sadd myset a b 
(integer) 0

  

5.1.2 srem 删除元素

srem key element [element ...]

# 返回结果为成功删除元素个数,例如:
127.0.0.1:6379> srem myset a b 
(integer) 2
127.0.0.1:6379> srem myset hello 
(integer) 0

  

5.1.3 计算元素个数

  scard 的时间复杂度为 O(1),它不会遍历集合所有元素,而是直接用 Redis 内部的变量。

scard key

127.0.0.1:6379> scard myset 
(integer) 1

  

5.1.4 sismember 判断元素是否在集合中

sismember key element

# 如果给定元素element在集合内返回1,反之返回0
127.0.0.1:6379> sismember myset c
(integer) 1

  

5.1.5 随机从集合返回指定个数元素

srandmember key [count]

# [count]是可选参数,如果不写默认为1
127.0.0.1:6379> srandmember myset 2 
1) "a"
2) "c"
127.0.0.1:6379> srandmember myset 
"d"

  

5.1.6 spop 从集合随机弹出元素

spop key

# spop操作可以从集合中随机弹出一个元素,例如下面代码是一次spop 后,集合元素变为"d b a"
127.0.0.1:6379> spop myset
"c"
127.0.0.1:6379> smembers myset 
1) "d"
2) "b" 
3) "a"

需要注意的是 Redis 从 3.2 版本开始,spop 也支持 [count] 参数。
srandmember 和 spop 都是随机从集合选出元素,两者不同的是 spop 命令执行后,元素会从集合中删除,而 srandmember 不会。

  

5.1.7 smembers 获取所有元素

smembers key

# 获取集合myset所有元素,并且返回结果是无序的:
127.0.0.1:6379> smembers myset 
1) "d"
2) "b"
3) "a"

smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存在阻塞 Redis 的可能性,这时候可以使用 sscan 来完成

  

5.1.8 sinter 多个集合的交集

sinter key [key ...]

# 现在有两个集合,它们分别是user:1:follow和user:2:follow:

127.0.0.1:6379> sadd user:1:follow it music his sports 
(integer) 4
127.0.0.1:6379> sadd user:2:follow it news ent sports 
(integer) 4

# 求user:1:follow和user:2:follow两个集合的交集, 返回结果是sports、it:
127.0.0.1:6379> sinter user:1:follow user:2:follow 
1) "sports"
2) "it"

  

5.1.9 suinon 多个集合的并集

suinon key [key ...]

127.0.0.1:6379> sunion user:1:follow user:2:follow 
1) "sports"
2) "it"
3) "his"
4) "news" 
5) "music" 
6) "ent"

  

5.1.10 sdiff 多个集合的差集

sdiff key [key ...]

127.0.0.1:6379> sdiff user:1:follow user:2:follow
1) "music" 
2) "his"

  

5.1.11 将交集、并集、差集的结果保存

  集合间的运算在元素较多的情况下会比较耗时,所以 Redis 提供了下面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination key 中

sinterstore destination key [key ...] 
suionstore destination key [key ...] 
sdiffstore destination key [key ...]


127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow 
(integer) 2
127.0.0.1:6379> type user:1_2:inter
set
127.0.0.1:6379> smembers user:1_2:inter 
1) "it"
2) "sports"

  

5.1.12 时间复杂度

在这里插入图片描述

  

5.2 内部编码

集合类型的内部编码有两种:

  • intset(整数集合):
    • 当集合中的元素都是整数且元素个数小于 set-max- intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用;
  • hashtable(哈希表):
    • 当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。
# 当元素个数较少且都为整数时,内部编码为intset:
127.0.0.1:6379> sadd setkey 1 2 3 4 
(integer) 4
127.0.0.1:6379> object encoding setkey 
"intset"

# 当元素个数超过512个,内部编码变为hashtable:
127.0.0.1:6379> sadd setkey 1 2 3 4 5 6 ... 512 513 
(integer) 509
127.0.0.1:6379> scard setkey
(integer) 513
127.0.0.1:6379> object encoding listkey 
"hashtable"

# 当某个元素不为整数时,内部编码也会变为hashtable:
127.0.0.1:6379> sadd setkey a 
(integer) 1
127.0.0.1:6379> object encoding setkey 
"hashtable"

  

5.3 使用场景

5.3.1 标签

  集合类型比较典型的使用场景是标签(tag)。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共 同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。

下面使用集合类型实现标签功能的若干功能:

  1. 给用户添加标签
sadd user:1:tags tag1 tag2 tag5 sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4 ...
  1. 给标签添加用户
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3 ...
sadd tagk:users user:1 user:2
...

用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致

  1. 计算用户共同感兴趣的标签
sinter user:1:tags user:2:tags

  

5.3.2 其他场景

集合类型的应用场景通常为以下几种:

  • spop/srandmember=Random item(生成随机数,比如抽奖)
  • sadd+sinter=Social Graph(社交需求)

  

  

6. 有序集合

  有序集合,它保留了集合不能有重复成员的特性, 但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为 排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依 据。有序集合提供了获取指定 分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助 我们在实际开发中解决很多问题。

在这里插入图片描述
  
下面给出了列表、集合、有序集合三者的异同点
在这里插入图片描述

  

6.1 命令

6.1.1 zadd 添加成员

zadd key score member [score member ...]

# 向有序集合user:ranking添加用户tom和他的分数251
127.0.0.1:6379> zadd user:ranking 251 tom
(integer) 1

# 返回结果代表成功添加成员的个数:
127.0.0.1:6379> zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin (integer) 5

有关 zadd 命令有两点需要注意:Redis3.2 为 zadd 命令添加了nx、xx、ch、incr 四个选项:

  • nx:member必须不存在,才可以设置成功,用于添加。
  • xx:member必须存在,才可以设置成功,用于更新。
  • ch:返回此次操作后,有序集合元素和分数发生变化的个数
  • incr:对score做增加,相当于后面介绍的zincrby。
      

有序集合相比集合提供了排序字段,但是也产生了代价,zadd 的时间复杂度为O(log(n)),sadd的时间复杂度为O(1)。

  

6.1.2 zcard 计算成员个数

zcard key

# 操作返回有序集合user:ranking的成员数为5,和集合类型的 scard命令一样,zcard的时间复杂度为O(1)。
127.0.0.1:6379> zcard user:ranking 
(integer) 5

  

6.1.3 计算某个成员的分数

zscore key member

# tom的分数为251,如果成员不存在则返回nil:

127.0.0.1:6379> zscore user:ranking tom 
"251"
127.0.0.1:6379> zscore user:ranking test 
(nil)

  

6.1.4 zrank 计算成员的排名

# zrank是从分数从低到高返回排名
zrank key member
# zrevrank反之
zrevrank key member

# tom 的排名
127.0.0.1:6379> zrank user:ranking tom 
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom 
(integer) 0

  

6.1.5 zrem 删除成员

zrem key member [member ...]

# 将成员mike从有序集合user:ranking中删除
127.0.0.1:6379> zrem user:ranking mike
(integer) 1

  

6.1.6 zincrby 增加成员的分数

zincrby key increment member

# 操作给tom增加了9分,分数变为了260分:
127.0.0.1:6379> zincrby user:ranking 9 tom
"260"

  

6.1.7 zrange 返回指定排名范围的成员

# zrange是从低到高返回
zrange key start end [withscores]
zrevrange key start end [withscores]

127.0.0.1:6379> zrange user:ranking 0 2 withscores 
1) "kris"
2) "1"
3) "frank"
4) "200"
5) "tim"
6) "220"
127.0.0.1:6379> zrevrange user:ranking 0 2 withscores 
1) "tom"
2) "260"
3) "martin" 4) "250"
5) "tim"
6) "220"

  

6.1.8 zrangebyscore 返回指定分数范围的成员

# zrangebyscore按照分数从低到高返回,
# withscores选项会同时返回每个 成员的分数。
# [limit offset count]选项可以限制输出的起始位置和个数:
zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]

127.0.0.1:6379> zrangebyscore user:ranking 200 tinf withscores 
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrevrangebyscore user:ranking 221 200 withscores 
1) "tim"
2) "220"
3) "frank"
4) "200"

  同时 min 和 max 还支持开区间(小括号)和闭区间(中括号),-inf 和 +inf 分别代表无限小和无限大:


127.0.0.1:6379> zrangebyscore user:ranking (200 +inf withscores 
1) "tim"
2) "220"
3) "martin"
4) "250" 
5) "tom" 
6) "260"

  

6.1.9 zcount 返回指定分数范围成员个数

zcount key min max

# 返回200到221分的成员的个数
127.0.0.1:6379> zcount user:ranking 200 221
(integer) 2

  

6.1.10 zremrangebyrank 删除指定排名内的升序元素

zremrangebyrank key start end

# 删除第start到第end名的成员
127.0.0.1:6379> zremrangebyrank user:ranking 0 2
(integer) 3

  

6.1.11 zremrangebyscore 删除指定分数范围的成员

zremrangebyscore key min max

# 将250分以上的成员全部删除,返回结果为成功删除的个数
127.0.0.1:6379> zremrangebyscore user:ranking (250 +inf
(integer) 2

  

6.1.12 交集

127.0.0.1:6379> zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
(integer) 6
127.0.0.1:6379> zadd user:ranking:2 8 james 77 mike 625 martin 888 tom (integer) 4
zinterstore destination numkeys key [key ...] [weights weight [weight ...]]
[aggregate sum|min|max]

这个命令参数较多,下面分别进行说明:

  • destination:交集计算结果保存到这个键。
  • numkeys:需要做交集计算键的个数。
  • key[key…]:需要做交集计算的键。
  • weights weight[weight…]:每个键的权重,在做交集计算时,每个键中 的每个member会将自己分数乘以这个权重,每个键的权重默认是1。
  • aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、 min(最小值)、max(最大值)做汇总,默认值是sum。
# 对user:ranking:1和user:ranking:2做交集,weights和 aggregate使用了默认配置,
# 可以看到目标键user:ranking:1_inter_2对分值 做了sum操作

127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores 
1) "mike"
2) "168"
3) "martin"
4) "875"
5) "tom"
6) "1139"

  

6.1.13 并集

zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]

  

6.1.14 时间复杂度

在这里插入图片描述

  

6.2 内部编码

有序集合类型的内部编码有两种:

  • ziplist(压缩列表):
    • 当有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个);
    • 同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时;
    • Redis会用ziplist来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
  • skiplist(跳跃表):
    • 当ziplist条件不满足时,有序集合会使用skiplist作 为内部实现,因为此时ziplist的读写效率会下降。
# 当元素个数较少且每个元素较小时,内部编码为skiplist
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 
(integer) 3
127.0.0.1:6379> object encoding zsetkey 
"ziplist"

# 当元素个数超过128个,内部编码变为ziplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 12 e4 ...忽略... 84 e129 
(integer) 129
127.0.0.1:6379> object encoding zsetkey
"skiplist"

# 当某个元素大于64字节时,内部编码也会变为hashtable:
127.0.0.1:6379> zadd zsetkey 20 "one string is bigger than 64 byte............. ..................."
(integer) 1
127.0.0.1:6379> object encoding zsetkey
"skiplist"

  

6.3 使用场景

  有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。

  1. 添加用户赞数
zadd user:ranking:2016_03_15 mike 3

  

  1. 取消用户赞数
zrem user:ranking:2016_03_15 mike
  1. 增加获得一个赞,可以使用 zincrby
zincrby user:ranking:2016_03_15 mike 1
  1. 展示获取赞数最多的十个用户
zrevrangebyrank user:ranking:2016_03_15 0 9
  1. 展示用户信息以及用户分数
      将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用 zscore 和 zrank 两个功能
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike zrank user:ranking:2016_03_15 mike

  

  

7. 键管理

7.1 单个键管理

  针对单个键的命令,前面已经介绍过一部分了,例如type、del、object、exists、expire等,下面将介绍剩余的几个重要命令:

  

7.1.1 键过期

除了expire、ttl命令以外,Redis 还提供了 expireat、pexpire、pexpireat、pttl、persist 等一系列命令,下面分别进行说明:

  • expire key seconds:键在seconds秒后过期;
  • expireat key timestamp:键在秒级时间戳timestamp后过期。

  expireat 命令可以设置键的秒级过期时间戳,例如如果需要将键 hello 在 2016-08-0100:00:00(秒级时间戳为1469980800)过期,可以执行如下操作:

127.0.0.1:6379> expireat hello 1469980800 
(integer) 1

除此之外,Redis2.6版本后提供了毫秒级的过期方案:

  • pexpire key milliseconds:键在milliseconds毫秒后过期;
  • pexpireat key milliseconds-timestamp:键在毫秒级时间戳timestamp后过期。

对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易 在开发中被忽视

127.0.0.1:6379> expire hello 50 
(integer) 1
127.0.0.1:6379> ttl hello 
(integer) 46
127.0.0.1:6379> set hello world 
OK
127.0.0.1:6379> ttl hello 
(integer) -1

  Redis 不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过期时间设置。

  setex 命令作为 set+expire 的组合,不但是原子执行,同时减少了一次网络通讯的时间。

  

7.2 遍历键

  Redis 提供了两个命令遍历所有的键,分别是 keys和 scan,这里将对它们介绍并简要分析。

除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫 描遍历命令,解决诸如 hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan,它们的用法和 scan 基本类似.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值