【大厂面试】面试官看了赞不绝口的Redis笔记

一、Redis简介

Redis(Remote Dictionary Server)是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库,也是于开发或者运维都是必须要掌握的非关系型数据库。

Redis可作为高性能 Key-Value服务器,拥有多种数据结构,并提供丰富的功能以及对高可用分布式的支持。

Redis的具有以下:1. 速度快;2. 功能丰富;3. 可持久化;4. 简单;5. 多种数据结构;6. 主从复制;7. 支持多种编辑语言;8. 高可用、分布式等。

二、Redis API的使用和理解

下面我们会依次详细探讨常用的Redis 的API。

再说之前,给大家推荐一个网站,用于查阅Redis API: http://redisdoc.com/index.html

文中的部分演示结果来源于该网站

传送门:Redis 命令参考

(一)通用命令

因为通用命令会涉及Redis的数据结构操作,而Redis的数据结构操作也会涉及到通用命令,所以这两部分要结合着看。

不同数据结构的操作内容在下面

1. KEYS

通用命令 KEYS pattern(pattern 为正则表达式)
功能描述 查找所有符合给定模式 pattern 的 key
时间复杂度 O(N), N 为数据库中 key 的数量。

功能描述方面举例:

  • KEYS * 匹配数据库中所有 key 。
  • KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
  • KEYS h*llo 匹配 hllo 和 heeeeello 等。
  • KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。

特殊符号用 \ 隔开。

示例

redis> MSET one 1 two 2 three 3 four 4  # 一次设置 4 个 key
OK

redis> KEYS *o*
1) "four"
2) "two"
3) "one"

redis> KEYS t??
1) "two"

redis> KEYS t[w]*
1) "two"

redis> KEYS *  # 匹配数据库内所有 key
1) "four"
2) "three"
3) "two"
4) "one"

在生产环境中,使用keys命令取出所有key并没有什么意义,而且Redis是单线程应用,如果Redis中存的key很多,使用keys命令会阻塞其他命令执行,所以keys命令一般不在生产环境中使用

2. DBSIZE

通用命令 DBSIZE
功能描述 返回当前数据库的 key 的数量。
时间复杂度 时间复杂度: O(1)

示例

redis> DBSIZE
(integer) 5

redis> SET new_key "hello_moto"     # 增加一个 key 试试
OK

redis> DBSIZE
(integer) 6

Redis内置一个计数器,可以实时更新Redis中key的总数,因此dbsize的时间复杂度为O(1),可以在线上使用。

3. EXISTS

通用命令 EXISTS key
功能描述 检查给定 key 是否存在。若 key 存在,返回 1 ,否则返回 0 。
时间复杂度 O(1)
redis> SET db "redis"
OK

redis> EXISTS db
(integer) 1

redis> DEL db
(integer) 1

redis> EXISTS db
(integer) 0

4. DEL

通用命令 DEL key [key …]
功能描述 删除给定的一个或多个 key 。不存在的 key 会被忽略。返回值是被删除 key 的数量。
时间复杂度 O(N), N 为被删除的 key 的数量,其中删除单个字符串类型的 key ,时间复杂度为O(1);删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为O(M), M 为以上数据结构内的元素数量。
#  删除单个 key
redis> SET name huangz
OK

redis> DEL name
(integer) 1


# 删除一个不存在的 key
redis> EXISTS phone
(integer) 0

redis> DEL phone # 失败,没有 key 被删除
(integer) 0


# 同时删除多个 key
redis> SET name "redis"
OK

redis> SET type "key-value store"
OK

redis> SET website "redis.com"
OK

redis> DEL name type website
(integer) 3

5. EXPIRE

通用命令 EXPIRE key seconds
功能描述 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。设置成功返回 1 。 当 key 不存在或者不能为 key 设置生存时间时(比如在低于 2.1.3 版本的 Redis 中你尝试更新 key 的生存时间),返回 0 。
时间复杂度 O(1)
redis> SET cache_page "www.google.com"
OK

redis> EXPIRE cache_page 30  # 设置过期时间为 30 秒
(integer) 1

redis> TTL cache_page    # 查看剩余生存时间
(integer) 23

redis> EXPIRE cache_page 30000   # 更新过期时间
(integer) 1

redis> TTL cache_page
(integer) 29996

6. TTL

通用命令 TTL key
功能描述 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。
时间复杂度 O(1)
redis> FLUSHDB
OK

redis> TTL key
(integer) -2

# key 存在,但没有设置剩余生存时间
redis> SET key value
OK

redis> TTL key
(integer) -1

# 有剩余生存时间的 key
redis> EXPIRE key 10086
(integer) 1

redis> TTL key
(integer) 10084

7. PERSIST

通用命令 PERSIST key
功能描述 移除给定 key 的生存时间,将这个 key 从“易失的”(带生存时间 key )转换成“持久的”(一个不带生存时间、永不过期的 key )。当生存时间移除成功时,返回 1 . 如果 key 不存在或 key 没有设置生存时间,返回 0 。
时间复杂度 O(1)
redis> SET mykey "Hello"
OK

redis> EXPIRE mykey 10  # 为 key 设置生存时间
(integer) 1

redis> TTL mykey
(integer) 10

redis> PERSIST mykey    # 移除 key 的生存时间
(integer) 1

redis> TTL mykey
(integer) -1

8. TYPE

通用命令 TYPE key
功能描述 返回 key 所储存的值的类型。
时间复杂度 O(1)

值的类型有:

  • none (key不存在)
  • string (字符串)
  • list (列表)
  • set (集合)
  • zset (有序集)
  • hash (哈希表)
  • stream (流)
# 字符串
redis> SET weather "sunny"
OK

redis> TYPE weather
string

# 列表
redis> LPUSH book_list "programming in scala"
(integer) 1

redis> TYPE book_list
list

# 集合
redis> SADD pat "dog"
(integer) 1

redis> TYPE pat
set

8. del

通用命令 DEL key [key …]
功能描述 删除给定的一个或多个 key 。不存在的 key 会被忽略。返回值是被删除 key 的数量。
时间复杂度 O(N), N 为被删除的 key 的数量,其中删除单个字符串类型的 key ,时间复杂度为O(1);删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为O(M), M 为以上数据结构内的元素数量。
#  删除单个 key
redis> SET name huangz
OK

redis> DEL name
(integer) 1

# 删除一个不存在的 key
redis> EXISTS phone
(integer) 0

redis> DEL phone # 失败,没有 key 被删除
(integer) 0


# 同时删除多个 key
redis> SET name "redis"
OK

redis> SET type "key-value store"
OK

redis> SET website "redis.com"
OK

redis> DEL name type website
(integer) 3

9. scan

通用命令 SCAN cursor [MATCH pattern] [LIMT count]
功能描述 查找(limit个)(符合给定模式 pattern )的 key ,返回值是符合 条件的key
时间复杂度 增量式迭代命令每次执行的复杂度为 O(1) , 对数据集进行一次完整迭代的复杂度为 O(N) , 其中 N 为数据集中的元素数量。

刚开始我们已经介绍过了keys,它的缺点非常明显:

  1. ⼀次性查出所有满⾜条件的 key,万⼀Redis中有⼏百 w 个 key 满⾜条件,满屏都是输出的结果,眼花缭乱。
  2. keys由于走的是遍历算法,复杂度是 O(n),如果Redis中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写Redis 的其它的指令都会被延后甚⾄会超时报错,因为 Redis是单线程程序,顺序执⾏所有指令,其它指令必须等到当前的keys 指令执⾏完了才可以继续。

为了解决这个问题, 2.8 版本中的Redis加⼊了scan。

scan 相⽐ keys 具备有以下特点:

  1. 复杂度虽然也是 O(n),但是它是通过游标(cursor,相当于位置)分步进⾏的,不会阻塞线程;
  2. 提供 limit 参数(可选),可以控制每次返回结果的最⼤条数(实际上是遍历的key的数量);
  3. 它也提供模式匹配功能;
  4. 服务器不需要为游标(cursor)保存状态,游标(cursor)的唯⼀状态就是 scan 返回给客户端的游标整数;
  5. 返回的结果可能会有重复,需要客户端去重复(重要);
  6. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  7. 单次返回的结果是空的并不意味着遍历结束,⽽要看返回的游标值是否为零;

scan 参数提供了三个参数,第⼀个是 cursor 整数值,第⼆个是key 的正则模式,第三个是遍历的limit。第⼀次遍历时,cursor 值为 0,然后将返回结果中第⼀个整数值作为下⼀次遍历的cursor。⼀直遍历到返回的 cursor 值为 0 时结束。

127.0.0.1:6379> scan 0 match key99* count 1000
1) "13976"
2) 1) "key9911"
   2) "key9974"
   3) "key9994"
   4) "key9910"
   5) "key9907"
   6) "key9989"
   7) "key9971"
   8) "key99"
127.0.0.1:6379> scan 13976 match key99* count
1000
1) "1996"
2) 1) "key9982"
   2) "key9997" 
   3) "key9963"
   4) "key996"
   5) "key9912"
   6) "key9999"
   7) "key9921"
   8) "key994"
   9) "key9956"
   10) "key9919"
127.0.0.1:6379> scan 1996 match key99* count 1000
1) "12594"
2) 1) "key9939"
   2) "key9941"
   3) "key9967"
   4) "key9938"
   5) "key9906"
   6) "key999"
   7) "key9909"
   ...
127.0.0.1:6379> scan 11687 match key99* count
1000
1) "0"
2) 1) "key9969"
   2) "key998"
   3) "key9986"
   4) "key9968"
   5) "key9965"
   6) "key9990"
   7) "key9915"
   8) "key9928"
   9) "key9908"

刚才也强调了,limit是遍历的key的个数,从上⾯的过程可以看到虽然提供的 limit 是 1000,但是返回的结果,有的只有 10 个。

scan 指令返回的游标就是第⼀维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就⾏了。limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每⼀次遍历都会将 limit 数量的槽位上挂接的所有链表元素进⾏模式匹配过滤后,⼀次性返回给客户端。

scan除了可以遍历所有的 key 之外,还可以对指定的容器集合进⾏遍历。
⽐如 zscan 遍历 zset 集合元素,hscan遍历 hash 字典的元素、sscan 遍历 set 集合的元素。

常用的通用命令已经介绍完了,下面我们探讨一个Redis总要面临的问题:

在 Redis 中有可能会形成很⼤的对象,⽐如⼀个⼀个很⼤的 zset。

这样的对象对 Redis 的集群数据迁移带来了挑战,在集群环境下,如果某个 key 太⼤,可能会导致数据迁移卡顿。另外在内存分配上,如果 key 太⼤,那么当它需要扩容时,会⼀次性申请更⼤的⼀块内存,这也可能会导致卡顿。如果这个⼤ key 被删除,内存会⼀次性回收,卡顿现象也有可能再产⽣。

在平时的开发中,尽量避免⼤ key 的产⽣。如果遇到 Redis 的内存⼤起⼤落的现象,有可能是因为⼤ key 导致的,这时候你就需要定位这个大 key,进⼀步定位出具体的业务来源,然后再改进相关业务代码设计。

关于大key的寻找,可以通过 scan 指令,对于扫描出来的每⼀个 key,使⽤ type 指令获得 key 的类型,然后使⽤相应数据结构的 size 或者 len ⽅法来得到它的⼤⼩,对于每⼀种类型,保留⼤⼩的前 N 名作为扫描结果展示出来。(需要编写脚本)

除此之外, Redis 官⽅在redis-cli 指令中提供了这样的扫描功能

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys

如果你担⼼这个指令会⼤幅抬升 Redis 的 ops ,还可以增加⼀个休眠参数。

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1
# 每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变⻓。

redis中的OPS 即operation per second 每秒操作次数。意味着每秒对Redis的持久化操作

(二)单线程架构

Redis内部使用单线程架构。Redis一个瞬间只能执行一条命令,不能执行两条命令

Redis单线程速度这么快的原因可大致归结三个:
1.纯内存

Redis把所有的数据都保存在内存中,而内存的响应速度是非常快的

2.非阻塞IO

Redis使用epoll异步非阻塞模型 ,Redis自身实现了事件处理

3.避免线程切换和竞态消耗

在使用多线程编程中,线程之间的切换也会消耗一部分CPU资源,如果不合理的实现多线程编程,可能比单线程还要慢

主要原因是 纯内存。

不过第二条和第三条倒是面试中经常会问到,尤其是第二条。为了便于大家,理解更深刻,我们这里探讨一下操作系统的IO

用户程序进行IO的读写,依赖于底层的IO读写,基本上会用到底层的read&write两大系统调用。

read系统调用,并不是直接从物理设备把数据读取到内存中;write系统调用,也不是直接把数据写入到物理设备。上层应用无论是调用操作系统的read,还是调用操作系统的write,都会涉及缓冲区。具体来说,调用操作系统的read,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区

缓冲区的目的,是为了减少频繁地与设备之间的物理交换。外部设备的直接读写,涉及操作系统的中断。发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。为了减少这种底层系统的时间损耗、性能损耗,于是出现了内存缓冲区。

有了内存缓冲区,上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(进程缓冲区);上层应用使用write系统调用时,仅仅把数据从进程缓冲区复制到内核缓冲区中。底层操作会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作,这种机制提升了系统的性能。至于什么时候中断(读中断、写中断),由操作系统的内核来决定,用户程序则不需要关心。

从数量上来说,在Linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序(进程),有自己独立的缓冲区,叫作进程缓冲区。所以,用户程序的IO读写程序,在大多数情况下,并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间直接进行数据的交换。

有了对操作系统IO的基本认识之后,还要提一下操作系统四种主要的IO模型

  1. 同步阻塞IO(Blocking IO)
  2. 同步非阻塞IO(Non-blocking IO)
  3. IO多路复用(IO Multiplexing)
  4. 异步IO(Asynchronous IO)

Redis的IO模型是IO多路复用,有意思的是Java 的NIO模型,也是IO多路复用(不是同步非阻塞IO)

有兴趣的可以查阅相关的资料,或者不着急的 可以等我接下来的博文(过段日子会有网络编程详解的博文,对IO作深入探究)

这里还要强调一下,由于Redis单线程一次只运行一条命令,我们要拒绝长(慢)命令

 	keys 
    flushall
    flushdb
    slow lua script
    mutil/exec
    operate

(三)数据结构和内部编码

Redis每种数据结构及对应的内部编码如下图所示
在这里插入图片描述
你会发现 数据结构 内部编码方式有不同的方式,其实这是时间换空间 空间换时间的做法,选择何种内部编码要结合实际情况。

(四)字符串

字符串 string 是 Redis 最简单的数据结构。Redis 所有的数据结构都是以唯⼀的 key 字符串作为名称,然后通过这个唯⼀ key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不⼀样。

字符串的value值类型有三种:1. 字符串;2. 整型;3.二进制。

Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采⽤预分配冗余空间的⽅式来减少内存的频繁分配,当字符串⻓度⼩于 1M时,扩容都是加倍现有的空间,如果超过 1M,扩容时⼀次只会多扩1M 的空间。需要注意的是字符串最⼤⻓度为 512M。

我们看一下它的常用API

1. GET

命令 GET key
功能描述 返回与键 key 相关联的字符串值。 如果键 key 不存在, 那么返回特殊值 nil ; 否则, 返回键 key 的值。如果键 key 的值并非字符串类型, 那么返回一个错误, 因为 GET 命令只能用于字符串值。
时间复杂度 O(1)

示例
对不存在的键 key 或是字符串类型的键 key 执行 GET 命令:

redis> GET db
(nil)

redis> SET db redis
OK

redis> GET db
"redis"

对不是字符串类型的键 key 执行 GET 命令:

redis> DEL db
(integer) 1

redis> LPUSH db redis mongodb mysql
(integer) 3

redis> GET db
(error) ERR Operation against a key holding the wrong kind of value

2. set

命令 SET key value [EX seconds] [PX milliseconds] [NX|XX]
功能描述 将字符串值 value 关联到 key 。如果 key 已经持有其他值, SET 就覆写旧值, 无视类型。当 SET 命令对一个带有生存时间(TTL)的键进行设置之后, 该键原有的 TTL 将被清除。
时间复杂度 O(1)

可选参数
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:

  • EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
  • PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
  • NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
  • XX : 只在键已经存在时, 才对键进行设置操作。

关于返回值

  • 在 Redis 2.6.12 版本以前, SET 命令总是返回 OK 。
  • 从 Redis 2.6.12 版本开始, SET 命令只在设置操作成功完成时才返回 OK ; 如果命令使用了 NX 或者 XX 选项, 但是因为条件没达到而造成设置操作未执行, 那么命令将返回空批量回复(NULL Bulk Reply)。

3. INCR

命令 INCR key
功能描述 为键 key 储存的数字值加上一。如果键 key 不存在, 那么它的值会先被初始化为 0 , 然后再执行 INCR 命令。如果键 key 储存的值不能被解释为数字, 那么 INCR 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。返回值是DECR 命令会返回键 key 在执行减一操作之后的值。
时间复杂度 O(1)

对储存数字值的键 key 执行 DECR 命令:

redis> SET page_view 20
OK

redis> INCR page_view
(integer) 21

redis> GET page_view    # 数字值在 Redis 中以字符串的形式保存
"21"

对不存在的键执行 DECR 命令:

redis> EXISTS count
(integer) 0

redis> DECR count
(integer) -1

4. DECR

命令 DECRBY key decrement
功能描述 将键 key 储存的整数值减去减量 decrement 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 DECRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。返回值是DECRBY 命令会返回键在执行减法操作之后的值。
时间复杂度 O(1)

对已经存在的键执行 DECRBY 命令:

redis> SET count 100
OK

redis> DECRBY count 20
(integer) 80

对不存在的键执行 DECRBY 命令:

redis> EXISTS pages
(integer) 0

redis> DECRBY pages 10
(integer) -10

5. INCRBY

命令 INCRBY key increment
功能描述 为键 key 储存的数字值加上增量 increment 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 INCRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 INCRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。返回值为在加上增量 increment 之后, 键 key 当前的值。
时间复杂度 O(1)

示例演示与上面类似

6. DECRBY

命令 DECRBY key decrement
功能描述 将键 key 储存的整数值减去减量 decrement 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 DECRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。 返回值DECRBY 命令会返回键在执行减法操作之后的值。
时间复杂度 O(1)

示例演示与上面类似

使用上面这一些命令,其实我们就可以做一些事情了。

应用

1.比如说记录每个用户博文的访问量

incr userid:pageview(单线程:无竞争)

在这里插入图片描述
2.缓存用户的基本信息(数据源在 MySQL中),信息被序列化存放在value中。
在这里插入图片描述

一般而言,需要通过我们自定义规则的key,从Redis获取value,如果key存在的话,则直接获取value使用;如果不存在的话,从Mysql中读取使用,然后存在Redis中。
主要的命令是 get 和 set
在这里插入图片描述
3.分布式id生成器
如果集群规模和运算不太复杂的话,可以用Redis生成分布式id,因为Redis单线程的特点,一次只执行一条指令,保证了id值的唯一。
主要的命令还是incr
在这里插入图片描述
7. SETNX

命令 SETNX key value
功能描述 只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。命令在设置成功时返回 1 , 设置失败时返回 0 。
时间复杂度 O(1)
redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 设置成功
(integer) 1

redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败
(integer) 0

redis> GET job                   # 没有被覆盖
"programmer"

8. SETEX

命令 SETNX key value
功能描述 将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。如果键 key 已经存在, 那么 SETEX 命令将覆盖已有的值。命令在设置成功时返回 OK 。 当 seconds 参数不合法时, 命令将返回一个错误。
时间复杂度 O(1)

SETEX 命令的效果和以下两个命令的效果类似:

SET key value
EXPIRE key seconds  # 设置生存时间

SETEX 和这两个命令的不同之处在于 SETEX 是一个原子(atomic)操作, 它可以在同一时间内完成设置值和设置过期时间这两个操作, 因此 SETEX 命令在储存缓存的时候非常实用。

这两个命令的典型应用就是分布式锁了。

⽐如⼀个操作要修改⽤户的状态,修改状态需要先读出⽤户的状态,在内存⾥进⾏修改,改完了再存回去。如果这样的操作同时进⾏了,就会出现并发问题。这个时候就要使⽤到分布式锁来限制程序的并发执⾏。Redis 分布式锁使⽤⾮常⼴泛,必须要掌握。

分布式锁本质上要实现的⽬标就是在 Redis ⾥⾯占⼀个“位置”,当别的进程也要来占时,发现“位置”被占了,就只好放弃或者稍后
再试。

占位置⼀般是使⽤ setnx(set if not exists) 指令,只允许被⼀个客户端占据。先来先占,⽤完了,再调⽤ del 指令释放位置。

如果逻辑执⾏到中间出现异常了,可能会导致 del指令没有被调⽤,这样就会陷⼊死锁,锁永远得不到释放。于是我们在拿到锁之后,再给锁加上⼀个过期时间。

但如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被⼈为杀掉的,就会导致expire 得不到执⾏,也会造成死锁。

原因是 setnx 和 expire 是两条指令⽽不能保证都一定成功执行。如果这两条指令可以⼀起执⾏就不会出现问题(要么成功,要么失败)。所以说setex是最佳的方案

上面就是分布式锁的基本思想。但是在真正投入使用的时候,还会面临一个常见的问题:超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执⾏的时间太⻓,超出了锁的超时限制,就会出现问题。这时候第⼀个线程持有的锁过期了,临界区的逻辑没有执⾏完,而第⼆个线程就提前重新持有了这把锁,导致临界区代码不能严格地串⾏执⾏。

为了避免这个问题,Redis 分布式锁不要⽤于较⻓时间的任务。

我们会在下篇文章,也就是分布式章节继续探讨分布式锁。

9. MSET

命令 MSET key value [key value …]
功能描述 同时为多个键设置值。如果某个给定键已经存在, 那么 MSET 将使用新值去覆盖旧值, 如果这不是你所希望的效果, 请考虑使用 MSETNX 命令, 这个命令只会在所有给定键都不存在的情况下进行设置。MSET 是一个原子性(atomic)操作, 所有给定键都会在同一时间内被设置, 不会出现某些键被设置了但是另一些键没有被设置的情况。MSET 命令总是返回 OK 。
时间复杂度 O(N),其中 N 为被设置的键数量。

同时对多个键进行设置:

redis> MSET date "2012.3.30" time "11:00 a.m." weather "sunny"
OK

redis> MGET date time weather
1) "2012.3.30"
2) "11:00 a.m."
3) "sunny"

覆盖已有的值:

redis> MGET k1 k2
1) "hello"
2) "world"

redis> MSET k1 "good" k2 "bye"
OK

redis> MGET k1 k2
1) "good"
2) "bye"

10 . MGET

命令 MGET key [key …]
功能描述 返回给定的一个或多个字符串键的值。如果给定的字符串键里面, 有某个键不存在, 那么这个键的值将以特殊值 nil 表示。MGET 命令将返回一个列表, 列表中包含了所有给定键的值。
时间复杂度 O(N),其中 N 为被设置的键数量。
redis> SET redis redis.com
OK

redis> SET mongodb mongodb.org
OK

redis> MGET redis mongodb
1) "redis.com"
2) "mongodb.org"

redis> MGET redis mongodb mysql     # 不存在的 mysql 返回 nil
1) "redis.com"
2) "mongodb.org"
3) (nil)

下面说说mset和mget的好处
不使用mget和mset::
在这里插入图片描述
客户端和服务器端可能不在同一个地方
n次get/set=n次网络时间+n次命令时间

一次mget/mset:
在这里插入图片描述
1次mget/mset=1次网络时间+n次命令时间

随着n的增大,差距一下子就体现出来了。

下面的命令不太常用,大体过一下:

10. GETSET
GETSET key value
将键 key 的值设为 value , 并返回键 key 在被设置之前的旧值。

11. STRLEN
STRLEN key
返回键 key 储存的字符串值的长度

12. APPEND
APPEND key value
如果键 key 已经存在并且它的值是一个字符串, APPEND 命令将把 value 追加到键 key 现有值的末尾。

13. INCRBYFLOAT
INCRBYFLOAT key increment
为键 key 储存的值加上浮点数增量 increment 。
如果键 key 不存在, 那么 INCRBYFLOAT 会先将键 key 的值设为 0 , 然后再执行加法操作。
如果命令执行成功, 那么键 key 的值会被更新为执行加法计算之后的新值, 并且新值会以字符串的形式返回给调用者。
无论是键 key 的值还是增量 increment , 都可以使用像 2.0e7 、 3e5 、 90e-2 那样的指数符号(exponential notation)来表示, 但是, 执行 INCRBYFLOAT 命令之后的值总是以同样的形式储存, 也即是, 它们总是由一个数字, 一个(可选的)小数点和一个任意长度的小数部分组成(比如 3.14 、 69.768 ,诸如此类), 小数部分尾随的 0 会被移除, 如果可能的话, 命令还会将浮点数转换为整数(比如 3.0 会被保存成 3 )。
此外, 无论加法计算所得的浮点数的实际精度有多长, INCRBYFLOAT 命令的计算结果最多只保留小数点的后十七位。
当以下任意一个条件发生时, 命令返回一个错误:

  • 键 key 的值不是字符串类型(因为 Redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型);
  • 键 key 当前的值或者给定的增量 increment 不能被解释(parse)为双精度浮点数。

14. GETRANGE
GETRANGE key start end
返回键 key 储存的字符串值的指定部分, 字符串的截取范围由 start 和 end 两个偏移量决定 (包括 start 和 end 在内)。
负数偏移量表示从字符串的末尾开始计数, -1 表示最后一个字符, -2 表示倒数第二个字符, 以此类推。
GETRANGE 通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。

15. SETRANGE
SETRANGE key offset value
从偏移量 offset 开始, 用 value 参数覆写(overwrite)键 key 储存的字符串值。
不存在的键 key 当作空白字符串处理。
SETRANGE 命令会确保字符串足够长以便将 value 设置到指定的偏移量上, 如果键 key 原来储存的字符串长度比偏移量小(比如字符串只有 5 个字符长,但你设置的 offset 是 10 ), 那么原字符和偏移量之间的空白将用零字节(zerobytes, “\x00” )进行填充。
因为 Redis 字符串的大小被限制在 512 兆(megabytes)以内, 所以用户能够使用的最大偏移量为 2^29-1(536870911) , 如果你需要使用比这更大的空间, 请使用多个 key 。


在对字符串类型有了整体的了解之后,我们看看它具体的结构

Redis 的字符串名字是SDS(Simple Dynamic String)。它的结构是⼀个带⻓度信息的字节数组。

struct SDS<T> {
	T capacity; // 数组容量
	T len; // 数组⻓度
	byte flags; // 特殊标识位,不理睬它
	byte[] content; // 数组内容
}

capacity 表示所分配数组的⻓度,len 表示字符串的实际⻓度。前⾯API提到⽀持 append操作(字符串是可修改的)。如果数组没有冗余空间,那么追加操作必然涉及到分配新数组,然后将旧内容复制过来,再 append 新内容。如果字符串的⻓
度⾮常⻓,这样的内存分配和复制开销就会⾮常⼤。

/* Append the specified binary-safe string
pointed by 't' of 'len' bytes to the
* end of the specified sds string 's'
.
*
* After the call, the passed sds string is no
longer valid and all the
* references must be substituted with the new
pointer returned by the call.
*/
sds sdscatlen(sds s, const void *t, size_t len) {
	size_t curlen = sdslen(s); // 原字符串⻓度
	// 按需调整空间,如果 capacity 不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中
	s = sdsMakeRoomFor(s,len);
	if (s == NULL) return NULL; // 内存不⾜
	memcpy(s+curlen, t, len); // 追加⽬标字符串的内容到字节数组中
	sdssetlen(s, curlen+len); // 设置追加后的⻓度值
	s[curlen+len] ='\0'; // 让字符串以\0 结尾,便于调试打印,还可以直接使⽤ glibc 的字符串函数进⾏操作
	return s;
}

上⾯的 SDS 结构使⽤了范型 T,这是Redis 对内存做出的优化,不同⻓度的字符串使⽤不同的结构体来表示,字符串⽐较短时,len 和 capacity 可以使⽤ byte 和 short来表示。

Redis 规定字符串的⻓度不得超过 512M 字节。创建字符串时 len和 capacity ⼀样⻓,不会多分配冗余空间,这是因为绝⼤多数场景下我们不会使⽤ append 操作来修改字符串。

(五)hash (字典)

Redis 的字典结构为数组 +链表⼆维结构。第⼀维 hash 的数组位置碰撞时,就会将碰撞的元素使⽤链表串接起来。Redis 的字典的值只能是字符串。当字典很大的时候,会进行rehash,Redis 为了⾼性能,不能堵塞服务,采⽤了渐进式 rehash 策略。

渐进式 rehash 保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及hash 操作指令中,循序渐进地将旧 hash 的内容⼀点点迁移到新的hash 结构中。当搬迁完成了,就会使⽤新的hash结构取⽽代之。当 hash 移除了最后⼀个元素之后,该数据结构⾃动被删除,内存被回收。

在这里插入图片描述
下面我们看一下它的API, 所有hash的命令都是h开头
1. HSET hash field value

时间复杂度: O(1)

将哈希表 hash 中域 field 的值设置为 value 。如果给定的哈希表并不存在, 那么一个新的哈希表将被创建并执行 HSET 操作。如果域 field 已经存在于哈希表中, 那么它的旧值将被新值 value 覆盖。当 HSET 命令在哈希表中新创建 field 域并成功为它设置值时, 命令返回 1 ; 如果域 field 已经存在于哈希表, 并且 HSET 命令成功使用新值覆盖了它的旧值, 那么命令返回 0 。

设置一个新域:

redis> HSET website google "www.g.cn"
(integer) 1

redis> HGET website google
"www.g.cn"

对一个已存在的域进行更新:

redis> HSET website google "www.google.com"
(integer) 0

redis> HGET website google
"www.google.com"

2.HGET hash field
时间复杂度: O(1)

返回哈希表中给定域的值。HGET 命令在默认情况下返回给定域的值。如果给定域不存在于哈希表中, 又或者给定的哈希表并不存在, 那么命令返回 nil 。

域存在的情况:

redis> HSET homepage redis redis.com
(integer) 1

redis> HGET homepage redis
"redis.com"

域不存在的情况:

redis> HGET site mysql
(nil)

3.HDEL
HDEL key field [field …]
O(N), N 为要删除的域的数量。
删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。返回值为被成功移除的域的数量,不包括被忽略的域。

# 测试数据
redis> HGETALL abbr
1) "a"
2) "apple"
3) "b"
4) "banana"
5) "c"
6) "cat"
7) "d"
8) "dog"


# 删除单个域
redis> HDEL abbr a
(integer) 1


# 删除不存在的域
redis> HDEL abbr not-exists-field
(integer) 0


# 删除多个域
redis> HDEL abbr b c
(integer) 2

redis> HGETALL abbr
1) "d"
2) "dog"

4. HSETNX hash field value
时间复杂度: O(1)
当且仅当域 field 尚未存在于哈希表的情况下, 将它的值设置为 value 。如果给定域已经存在于哈希表当中, 那么命令将放弃执行设置操作。如果哈希表 hash 不存在, 那么一个新的哈希表将被创建并执行 HSETNX 命令。HSETNX 命令在设置成功时返回 1 , 在给定域已经存在而放弃执行设置操作时返回 0 。

域尚未存在, 设置成功:

redis> HSETNX database key-value-store Redis
(integer) 1

redis> HGET database key-value-store
"Redis"

域已经存在, 设置未成功, 域原有的值未被改变:

redis> HSETNX database key-value-store Riak
(integer) 0

redis> HGET database key-value-store
"Redis"

5. HLEN
时间复杂度:O(1)

返回哈希表 key 中域的数量。当 key 不存在时,返回 0 。

redis> HSET db redis redis.com
(integer) 1

redis> HSET db mysql mysql.com
(integer) 1

redis> HLEN db
(integer) 2

redis> HSET db mongodb mongodb.org
(integer) 1

redis> HLEN db
(integer) 3

6.HMSET
HMSET key field value [field value …]
时间复杂度:O(N), N 为 field-value 对的数量。

同时将多个 field-value (域-值)对设置到哈希表 key 中。此命令会覆盖哈希表中已存在的域。如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。如果命令执行成功,返回 OK 。当 key 不是哈希表(hash)类型时,返回一个错误。

redis> HMSET website google www.google.com yahoo www.yahoo.com
OK

redis> HGET website google
"www.google.com"

redis> HGET website yahoo
"www.yahoo.com"

7. HMGET
HMGET key field [field …]
时间复杂度:O(N), N 为给定域的数量。

返回哈希表 key 中,一个或多个给定域的值。
如果给定的域不存在于哈希表,那么返回一个 nil 值。因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。具体返回一个包含多个给定域的关联值的表,表值的排列顺序和给定域参数的请求顺序一样。

redis> HMSET pet dog "doudou" cat "nounou"    # 一次设置多个域
OK

redis> HMGET pet dog cat fake_pet             # 返回值的顺序和传入参数的顺序一样
1) "doudou"
2) "nounou"
3) (nil)                                      # 不存在的域返回nil值

8.HINCRBY
HINCRBY key field increment
时间复杂度:O(1)
为哈希表 key 中的域 field 的值加上增量 increment 。增量也可以为负数,相当于对给定域进行减法操作。如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。如果域 field 不存在,那么在执行命令前,域的值被初始化为 0 。对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。本操作的值被限制在 64 位(bit)有符号数字表示之内。执行 HINCRBY 命令之后,返回值哈希表 key 中域 field 的值。

# increment 为正数
redis> HEXISTS counter page_view    # 对空域进行设置
(integer) 0

redis> HINCRBY counter page_view 200
(integer) 200

redis> HGET counter page_view
"200"


# increment 为负数
redis> HGET counter page_view
"200"

redis> HINCRBY counter page_view -50
(integer) 150

redis> HGET counter page_view
"150"


# 尝试对字符串值的域执行HINCRBY命令
redis> HSET myhash string hello,world       # 设定一个字符串值
(integer) 1

redis> HGET myhash string
"hello,world"

redis> HINCRBY myhash string 1              # 命令执行失败,错误。
(error) ERR hash value is not an integer

redis> HGET myhash string                   # 原值不变
"hello,world"

9. HKEYS
时间复杂度:O(N), N 为哈希表的大小。

返回哈希表 key 中的所有域。即一个包含哈希表中所有域的表。当 key 不存在时,返回一个空表。

# 哈希表非空
redis> HMSET website google www.google.com yahoo www.yahoo.com
OK

redis> HKEYS website
1) "google"
2) "yahoo"

# 空哈希表/key不存在
redis> EXISTS fake_key
(integer) 0

redis> HKEYS fake_key
(empty list or set)

10.HVALS
HVALS key
时间复杂度:O(N), N 为哈希表的大小。
返回哈希表 key 中所有域的值。即一个包含哈希表中所有值的表。当 key 不存在时,返回一个空表。

# 非空哈希表
redis> HMSET website google www.google.com yahoo www.yahoo.com
OK

redis> HVALS website
1) "www.google.com"
2) "www.yahoo.com"


# 空哈希表/不存在的key
redis> EXISTS not_exists
(integer) 0

redis> HVALS not_exists
(empty list or set)

11.HGETALL
时间复杂度:O(N), N 为哈希表的大小。
HGETALL key
返回哈希表 key 中,所有的域和值。在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。返回值以列表形式返回哈希表的域和域的值。若 key 不存在,返回空列表。

redis> HSET people jack "Jack Sparrow"
(integer) 1

redis> HSET people gump "Forrest Gump"
(integer) 1

redis> HGETALL people
1) "jack"          # 域
2) "Jack Sparrow"  # 值
3) "gump"
4) "Forrest Gump"

小心单线程 数据量大的话 会比较慢

12. hsetnx
时间复杂度: O(1)

当且仅当域 field 尚未存在于哈希表的情况下, 将它的值设置为 value 。如果给定域已经存在于哈希表当中, 那么命令将放弃执行设置操作。如果哈希表 hash 不存在, 那么一个新的哈希表将被创建并执行 HSETNX 命令。HSETNX 命令在设置成功时返回 1 , 在给定域已经存在而放弃执行设置操作时返回 0 。

域尚未存在, 设置成功:

redis> HSETNX database key-value-store Redis
(integer) 1

redis> HGET database key-value-store
"Redis"

域已经存在, 设置未成功, 域原有的值未被改变:

redis> HSETNX database key-value-store Riak
(integer) 0

redis> HGET database key-value-store
"Redis"

13. hincrbyfloat
HINCRBYFLOAT key field increment
时间复杂度:O(1)
为哈希表 key 中的域 field 加上浮点数增量 increment 。如果哈希表中没有域 field ,那么 HINCRBYFLOAT 会先将域 field 的值设为 0 ,然后再执行加法操作。如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表,再创建域 field ,最后再执行加法操作。当以下任意一个条件发生时,返回一个错误:

  • 域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型)
  • 域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number)
    返回值为返回值:执行加法操作之后 field 域的值。
# 值和增量都是普通小数
redis> HSET mykey field 10.50
(integer) 1
redis> HINCRBYFLOAT mykey field 0.1
"10.6"


# 值和增量都是指数符号
redis> HSET mykey field 5.0e3
(integer) 0
redis> HINCRBYFLOAT mykey field 2.0e2
"5200"


# 对不存在的键执行 HINCRBYFLOAT
redis> EXISTS price
(integer) 0
redis> HINCRBYFLOAT price milk 3.5
"3.5"
redis> HGETALL price
1) "milk"
2) "3.5"


# 对不存在的域进行 HINCRBYFLOAT
redis> HGETALL price
1) "milk"
2) "3.5"
redis> HINCRBYFLOAT price coffee 4.5   # 新增 coffee 域
"4.5"
redis> HGETALL price
1) "milk"
2) "3.5"
3) "coffee"
4) "4.5"

知道上面的命令后,就可以做一些事情了。

应用
类似于字符串,我们可以记录网站每个用户个人主页的访问量

hincrby user chenxiao  pageviewCount

当然还有缓存用户信息。

对于记录个人主页的访问量,自然字符串要比hash更好点。

但是对于缓存用户新信息这种逻辑要好好斟酌一下

字符串Key:Value的结构:(第一种方案 String-v1)

key: 'user:userId'
value:
{
	"name": "chenxiao",
 	"age":100,
  "pageview": 8000000
}

value是序列化的结果

字符串Key:Value的结构:(第二种方案 String-v2)

key: user:userId:name
value: chenxiao

key: user:userId:age
value: 100

key: user:userId:pageView
value: 800000

相比上面的方案更新属性更方便 只需要一条

再看看hash形式的方案(hash)

key:user:userId

field:name
value:chenxiao

field:age
value:100

field:pageView
vale:80000

3种方案比较:

方案 优点 缺点
String v1 编程简单,可能节约内存 1.序列化开销 2.设置属性要操作整个数据。
String v2 直观,可以部分更新 1.内存占用较大 2.key较为分散
hash 直观 节省空间 可以部分更新 1.编程稍微复杂 2.ttl不好控制

(六)列表

Redis 的列表相当于 Java 语⾔⾥⾯的 LinkedList,数据结构形式为链表,插⼊和删除操作⾮常快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为 O(n)。

当列表弹出了最后⼀个元素之后,该数据结构⾃动被删除,内存被回收。

插入元素后,各元素的相对位置确定,遍历的结果也与之保持一致。链表元素可以重复。下面我们看看它的API

1.LPUSH
LPUSH key value [value …]
时间复杂度: O(1)

将一个或多个值 value 插入到列表 key 的表头。如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头: 比如说,对空列表 mylist 执行命令 LPUSH mylist a b c ,列表的值将是 c b a ,这等同于原子性地执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。当 key 存在但不是列表类型时,返回一个错误。正常返回列表的长度。

# 加入单个元素
redis> LPUSH languages python
(integer) 1


# 加入重复元素
redis> LPUSH languages python
(integer) 2

redis> LRANGE languages 0 -1     # 列表允许重复元素
1) "python"
2) "python"


# 加入多个元素
redis> LPUSH mylist a b c
(integer) 3

redis> LRANGE mylist 0 -1
1) "c"
2) "b"
3) "a"

2.RPUSH
RPUSH key value [value …]
时间复杂度: O(1)

将一个或多个值 value 插入到列表 key 的表尾(最右边)。如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c ,等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。当 key 存在但不是列表类型时,返回一个错误。正常返回列表的长度。

# 添加单个元素
redis> RPUSH languages c
(integer) 1


# 添加重复元素
redis> RPUSH languages c
(integer) 2

redis> LRANGE languages 0 -1 # 列表允许重复元素
1) "c"
2) "c"


# 添加多个元素
redis> RPUSH mylist a b c
(integer) 3

redis> LRANGE mylist 0 -1
1) "a"
2) "b"
3) "c"

3.LINSERT
LINSERT key BEFORE|AFTER pivot value
时间复杂度: O(N), N 为寻找 pivot 过程中经过的元素数量。

将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。当 pivot 不存在于列表 key 时,不执行任何操作。当 key 不存在时, key 被视为空列表,不执行任何操作。如果 key 不是列表类型,返回一个错误。如果命令执行成功,返回插入操作完成之后,列表的长度。 如果没有找到 pivot ,返回 -1 。 如果 key 不存在或为空列表,返回 0 。

redis> RPUSH mylist "Hello"
(integer) 1

redis> RPUSH mylist "World"
(integer) 2

redis> LINSERT mylist BEFORE "World" "There"
(integer) 3

redis> LRANGE mylist 0 -1
1) "Hello"
2) "There"
3) "World"


# 对一个非空列表插入,查找一个不存在的 pivot
redis> LINSERT mylist BEFORE "go" "let's"
(integer) -1                                    # 失败


# 对一个空列表执行 LINSERT 命令
redis> EXISTS fake_list
(integer) 0

redis> LINSERT fake_list BEFORE "nono" "gogogog"
(integer) 0                                      # 失败

4.LPOP
LPOP key
时间复杂度: O(1)

移除并返回列表 key 的头元素。返回列表的头元素。 当 key 不存在时,返回 nil 。

redis> LLEN course
(integer) 0

redis> RPUSH course algorithm001
(integer) 1

redis> RPUSH course c++101
(integer) 2

redis> LPOP course  # 移除头元素
"algorithm001"

5.RPOP
RPOP key
时间复杂度: O(1)

移除并返回列表 key 的尾元素。返回列表的尾元素。 当 key 不存在时,返回 nil 。

redis> RPUSH mylist "one"
(integer) 1

redis> RPUSH mylist "two"
(integer) 2

redis> RPUSH mylist "three"
(integer) 3

redis> RPOP mylist           # 返回被弹出的元素
"three"

redis> LRANGE mylist 0 -1    # 列表剩下的元素
1) "one"
2) "two"

6.LREM
LREM key count value

时间复杂度: O(N), N 为列表的长度。
根据参数 count 的值,移除列表中与参数 value 相等的元素。

count 的值可以是以下几种:

  • count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
  • count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
  • count = 0 : 移除表中所有与 value 相等的值。

返回值被移除元素的数量。 因为不存在的 key 被视作空表(empty list),所以当 key 不存在时, LREM 命令总是返回 0 。

# 先创建一个表,内容排列是
# morning hello morning helllo morning

redis> LPUSH greet "morning"
(integer) 1
redis> LPUSH greet "hello"
(integer) 2
redis> LPUSH greet "morning"
(integer) 3
redis> LPUSH greet "hello"
(integer) 4
redis> LPUSH greet "morning"
(integer) 5

redis> LRANGE greet 0 4         # 查看所有元素
1) "morning"
2) "hello"
3) "morning"
4) "hello"
5) "morning"

redis> LREM greet 2 morning     # 移除从表头到表尾,最先发现的两个 morning
(integer) 2                     # 两个元素被移除

redis> LLEN greet               # 还剩 3 个元素
(integer) 3

redis> LRANGE greet 0 2
1) "hello"
2) "hello"
3) "morning"

redis> LREM greet -1 morning    # 移除从表尾到表头,第一个 morning
(integer) 1

redis> LLEN greet               # 剩下两个元素
(integer) 2

redis> LRANGE greet 0 1
1) "hello"
2) "hello"

redis> LREM greet 0 hello      # 移除表中所有 hello
(integer) 2                    # 两个 hello 被移除

redis> LLEN greet
(integer) 0

7.LTRIM
LTRIM key start stop
时间复杂度: O(N), N 为被移除的元素的数量。
对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。

这个从字面意思不容易理解 ,我们图解一下:

在这里插入图片描述
运行命令

Itrim listkey 1 4

结果
在这里插入图片描述
上面的图能一下子让你看懂,它是做什么的了,下面演示一下

# 情况 1: 常见情况, start 和 stop 都在列表的索引范围之内
redis> LRANGE alpha 0 -1       # alpha 是一个包含 5 个字符串的列表
1) "h"
2) "e"
3) "l"
4) "l"
5) "o"

redis> LTRIM alpha 1 -1        # 删除 alpha 列表索引为 0 的元素
OK

redis> LRANGE alpha 0 -1       # "h" 被删除了
1) "e"
2) "l"
3) "l"
4) "o"


# 情况 2: stop 比列表的最大下标还要大
redis> LTRIM alpha 1 10086     # 保留 alpha 列表索引 1 至索引 10086 上的元素
OK

redis> LRANGE alpha 0 -1       # 只有索引 0 上的元素 "e" 被删除了,其他元素还在
1) "l"
2) "l"
3) "o"


# 情况 3: start 和 stop 都比列表的最大下标要大,并且 start < stop
redis> LTRIM alpha 10086 123321
OK

redis> LRANGE alpha 0 -1        # 列表被清空
(empty list or set)


# 情况 4: start 和 stop 都比列表的最大下标要大,并且 start > stop
redis> RPUSH new-alpha "h" "e" "l" "l" "o"     # 重新建立一个新列表
(integer) 5

redis> LRANGE new-alpha 0 -1
1) "h"
2) "e"
3) "l"
4) "l"
5) "o"

redis> LTRIM new-alpha 123321 10086    # 执行 LTRIM
OK

redis> LRANGE new-alpha 0 -1           # 同样被清空
(empty list or set)

8.LINDEX
LINDEX key index
时间复杂度:O(N), N 为到达下标 index 过程中经过的元素数量。因此,对列表的头元素和尾元素执行 LINDEX 命令,复杂度为O(1)。

返回列表 key 中,下标为 index 的元素。下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。如果 key 不是列表类型,返回一个错误。返回列表中下标为 index 的元素。 如果 index 参数的值不在列表的区间范围内(out of range),返回 nil 。

redis> LPUSH mylist "World"
(integer) 1

redis> LPUSH mylist "Hello"
(integer) 2

redis> LINDEX mylist 0
"Hello"

redis> LINDEX mylist -1
"World"

redis> LINDEX mylist 3        # index不在 mylist 的区间范围内
(nil)

9.LLEN
LLEN key
时间复杂度: O(1)

返回列表 key 的长度。如果 key 不存在,则 key 被解释为一个空列表,返回 0 .如果 key 不是列表类型,返回一个错误。

# 空列表
redis> LLEN job
(integer) 0


# 非空列表
redis> LPUSH job "cook food"
(integer) 1

redis> LPUSH job "have lunch"
(integer) 2

redis> LLEN job
(integer) 2

10.LSET
LSET key index value
时间复杂度:对头元素或尾元素进行 LSET 操作,复杂度为 O(1)。其他情况下,为 O(N), N 为列表的长度。

将列表 key 下标为 index 的元素的值设置为 value 。当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。操作成功返回 ok ,否则返回错误信息。

# 对空列表(key 不存在)进行 LSET

redis> EXISTS list
(integer) 0

redis> LSET list 0 item
(error) ERR no such key


# 对非空列表进行 LSET

redis> LPUSH job "cook food"
(integer) 1

redis> LRANGE job 0 0
1) "cook food"

redis> LSET job 0 "play game"
OK

redis> LRANGE job  0 0
1) "play game"


# index 超出范围

redis> LLEN list                    # 列表长度为 1
(integer) 1

redis> LSET list 3 'out of range'
(error) ERR index out of range

下面我们看一下它的应用:

最典型的就是时间轴

以CSDN APP的bink推荐为例,
在这里插入图片描述
当有一个bink发表的时候,要将这个blink插入时间轴第一个位置。可以是LPUSH的操作。这样子每次你就能从列表中,看到最新的blink,越往下时间越远。
在这里插入图片描述

11. BLPOP
BLPOP key [key …] timeout
时间复杂度: O(1)

BLPOP 是列表的阻塞式(blocking)弹出原语。它是 LPOP key 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。

12.BRPOP
BRPOP key [key …] timeout
时间复杂度: O(1)

BRPOP 是列表的阻塞式(blocking)弹出原语。它是 RPOP key 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。

下面再看一个Redis的应用:简易的消息队列

Redis 的简易的消息队列不是专业的消息队列,它没有⾮常多的⾼级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使⽤。但是对于那些只有⼀组消费者的消息队列,使⽤ Redis 就可以⾮常轻松的搞定。

后面会谈到Redis的高级特性–发布订阅。这个也是消息队列

异步消息队列
Redis 的 list(列表) 数据结构常⽤来作为异步消息队列使⽤,使⽤rpush/lpush操作⼊队列,使⽤lpop 和 rpop来出队列。

客户端是通过队列的 pop 操作来获取消息,然后进⾏处理。处理完了再接着获取消息,再进⾏处理。如此循环往复。

但是如果队列空了,客户端就会陷⼊ pop 的死循环,不停地 pop。这样不但拉⾼了客户端的 CPU,redis 的 QPS(每秒执行次数) 也会被拉⾼,如果这样不断轮询的客户端有⼏⼗来个,资源被大量无效占用。通常我们使⽤ sleep 来解决这个问题,让线程睡⼀会,睡个 1s 钟就可以了。不但客户端的 CPU 能降下来,Redis 的 QPS 也降下来了。不过这样也并不是很好的手段,

延时队列
运用blpop/brpop构建延时队列是一个非常好的方式。这两个指令的前缀字符b代表的是blocking,也就是阻塞读。

阻塞读在队列没有数据的时候,会⽴即进⼊休眠状态,⼀旦数据到来,则⽴刻醒过来。消息的延迟⼏乎为零。不过这也带来一个问题:空闲连接。如果线程⼀直阻塞在哪⾥,Redis 的客户端连接就成了闲置连接,闲置过久,服务器⼀般会主动断开连接,减少闲置资源占⽤。这个时候blpop/brpop会抛出异常来。编写客户端消费者的时候要⼩⼼,注意捕获异常,还要重试。

在上面简单说明分布式锁的时候,没有提到客户端在处理请求时加锁没加成功怎么办。
对于这种情况,有 3 种策略来处理加锁失败:

  1. 直接抛出异常,通知⽤户稍后重试;
  2. sleep ⼀会再重试;
  3. 将请求转移⾄延时队列,过⼀会再试;

(七)Set集合

Redis 的集合相当于 Java 语⾔⾥⾯的 HashSet,它内部的键值对是⽆序的唯⼀的。它的内部实现相当于⼀个特殊的字典,字典中所有的value 都是⼀个值NULL。当集合中最后⼀个元素移除之后,数据结构⾃动删除,内存被回收。

插入之后,元素的先后位置是不固定的,遍历的时候无序。

下面我们看一下它的API

1.SADD
SADD key member [member …]
时间复杂度: O(N), N 是被添加的元素的数量。

将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。假如 key 不存在,则创建一个只包含 member 元素作成员的集合。当 key 不是集合类型时,返回一个错误。正常返回被添加到集合中的新元素的数量,不包括被忽略的元素。

# 添加单个元素
redis> SADD bbs "discuz.net"
(integer) 1

# 添加重复元素
redis> SADD bbs "discuz.net"
(integer) 0

# 添加多个元素
redis> SADD bbs "tianya.cn" "groups.google.com"
(integer) 2

redis> SMEMBERS bbs
1) "discuz.net"
2) "groups.google.com"
3) "tianya.cn"

2.SCARD
SCARD key
时间复杂度: O(1)
返回集合 key 的基数(集合中元素的数量)。当 key 不存在时,返回 0 。

redis> SADD tool pc printer phone
(integer) 3

redis> SCARD tool   # 非空集合
(integer) 3

redis> DEL tool
(integer) 1

redis> SCARD tool   # 空集合
(integer) 0

3.SMEMBERS
SMEMBERS key
时间复杂度: O(N), N 为集合的基数。返回集合 key 中的所有成员。不存在的 key 被视为空集合。

# key 不存在或集合为空

redis> EXISTS not_exists_key
(integer) 0

redis> SMEMBERS not_exists_key
(empty list or set)


# 非空集合

redis> SADD language Ruby Python Clojure
(integer) 3

redis> SMEMBERS language
1) "Python"
2) "Ruby"
3) "Clojure"

可以看出来,插入顺序,与返回顺序不同。要小心使用

4.srandmember和spop
SRANDMEMBER key [count]
时间复杂度: 只提供 key 参数时为 O(1) 。如果提供了 count 参数,那么为 O(N) ,N 为返回数组的元素个数。

如果命令执行时,只提供了 key 参数,那么返回集合中的一个随机元素。
从 Redis 2.6 版本开始, SRANDMEMBER 命令接受可选的 count 参数:

  • 如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。如果 count 大于等于集合基数,那么返回整个集合。
  • 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。
# 添加元素
redis> SADD fruit apple banana cherry
(integer) 3

# 只给定 key 参数,返回一个随机元素
redis> SRANDMEMBER fruit
"cherry"

redis> SRANDMEMBER fruit
"apple"

# 给定 3 为 count 参数,返回 3 个随机元素
# 每个随机元素都不相同
redis> SRANDMEMBER fruit 3
1) "apple"
2) "banana"
3) "cherry"

# 给定 -3 为 count 参数,返回 3 个随机元素
# 元素可能会重复出现多次
redis> SRANDMEMBER fruit -3
1) "banana"
2) "cherry"
3) "apple"

redis> SRANDMEMBER fruit -3
1) "apple"
2) "apple"
3) "cherry"

# 如果 count 是整数,且大于等于集合基数,那么返回整个集合
redis> SRANDMEMBER fruit 10
1) "apple"
2) "banana"
3) "cherry"

# 如果 count 是负数,且 count 的绝对值大于集合的基数
# 那么返回的数组的长度为 count 的绝对值
redis> SRANDMEMBER fruit -10
1) "banana"
2) "apple"
3) "banana"
4) "cherry"
5) "apple"
6) "apple"
7) "cherry"
8) "apple"
9) "apple"
10) "banana"

# SRANDMEMBER 并不会修改集合内容
redis> SMEMBERS fruit
1) "apple"
2) "cherry"
3) "banana"

# 集合为空时返回 nil 或者空数组
redis> SRANDMEMBER not-exists
(nil)

redis> SRANDMEMBER not-eixsts 10
(empty list or set)
讨论 

SPOP key
时间复杂度: O(1)
移除并返回集合中的一个随机元素。如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER key [count] 命令。

redis> SMEMBERS db
1) "MySQL"
2) "MongoDB"
3) "Redis"

redis> SPOP db
"Redis"

redis> SMEMBERS db
1) "MySQL"
2) "MongoDB"

redis> SPOP db
"MySQL"

redis> SMEMBERS db
1) "MongoDB"

spop从集合弹出 srandmembe不会破坏集合

下面我们看看应用:
CSDN的点赞状态就可以用set集合进行处理。类似于点赞、踩这种场景,状态不重复,就可以set集合。
在这里插入图片描述
在这里插入图片描述
还有CSDN发表博文的时候,给文章打标签,标签就可以存储在set集合中
在这里插入图片描述
除此之外,set 结构可以⽤来存储活动中奖的⽤户 ID,因为有去重功能,可以保证同⼀个⽤户不会中奖两次。

下面看看集合间的API

5.sdiff sinter sunion

分别是差集 交集 并集。

这个玩法就比较多了。

比如微信上,将每个人拥有的群id都存储在每个人的set集合中

我们只要对两个人的集合取交集,就可以得出 我和他的公共群聊的个数。
在这里插入图片描述

(八)zset (有序集合)

zset 类似于 Java 的 SortedSet 和HashMap的结合体,

⼀⽅⾯它是⼀个 set,保证了内部 value 的唯⼀性,另⼀⽅⾯它可以给每个 value 赋予⼀个 score,代表这个 value 的排序权重。它的内部实现⽤的是⼀种叫做「跳跃列表」的数据结构。zset 中最后⼀个 value 被移除后,数据结构⾃动删除,内存被回收。

有序集合结构:
key: user

score value
1 tom
91 jerry
102 jeffery

集合 VS 有序集合
集合:无重复元素、无序、element
有序集合 : 无重复元素、有序、element + score
有序集合相比集合时间复杂度较高

1.ZADD
ZADD key score member [[score member] [score member] …]
时间复杂度: O(M*log(N)), N 是有序集的基数, M 为成功添加的新成员的数量。

将一个或多个 member 元素及其 score 值加入到有序集 key 当中。如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值,并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。score 值可以是整数值或双精度浮点数。如果 key 不存在,则创建一个空的有序集并执行 ZADD 操作。当 key 存在但不是有序集类型时,返回一个错误。正常返回被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。

# 添加单个元素
redis> ZADD page_rank 10 google.com
(integer) 1


# 添加多个元素
redis> ZADD page_rank 9 baidu.com 8 bing.com
(integer) 2

redis> ZRANGE page_rank 0 -1 WITHSCORES
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"


# 添加已存在元素,且 score 值不变
redis> ZADD page_rank 10 google.com
(integer) 0

redis> ZRANGE page_rank 0 -1 WITHSCORES  # 没有改变
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"


# 添加已存在元素,但是改变 score 值
redis> ZADD page_rank 6 bing.com
(integer) 0

redis> ZRANGE page_rank 0 -1 WITHSCORES  # bing.com 元素的 score 值被改变
1) "bing.com"
2) "6"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"

2.ZREM
ZREM key member [member …]
时间复杂度: O(M*log(N)), N 为有序集的基数, M 为被成功移除的成员的数量。

移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。当 key 存在但不是有序集类型时,返回一个错误。正常返回值
被成功移除的成员的数量,不包括被忽略的成员。

# 测试数据
redis> ZRANGE page_rank 0 -1 WITHSCORES
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"


# 移除单个元素
redis> ZREM page_rank google.com
(integer) 1

redis> ZRANGE page_rank 0 -1 WITHSCORES
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"

# 移除多个元素
redis> ZREM page_rank baidu.com bing.com
(integer) 2

redis> ZRANGE page_rank 0 -1 WITHSCORES
(empty list or set)

# 移除不存在元素
redis> ZREM page_rank non-exists-element
(integer) 0

3.zscore
ZSCORE key member
时间复杂度: O(1)

返回有序集 key 中,成员 member 的 score 值(member 成员的 score 值,以字符串形式表示)。如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。

1) "tom"
2) "2000"
3) "peter"
4) "3500"
5) "jack"
6) "5000"

redis> ZSCORE salary peter              # 注意返回值是字符串
"3500"

4.ZINCRBY
ZINCRBY key increment member
时间复杂度: O(log(N))

为有序集 key 的成员 member 的 score 值加上增量 increment 。可以通过传递一个负数值 increment ,让 score 减去相应的值,比如 ZINCRBY key -5 member ,就是让 member 的 score 值减去 5 。当 key 不存在,或 member 不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member 。当 key 不是有序集类型时,返回一个错误。score 值可以是整数值或双精度浮点数。正常返回值为member 成员的新 score 值,以字符串形式表示。

redis> ZSCORE salary tom
"2000"

redis> ZINCRBY salary 2000 tom   # tom 加薪啦!
"4000"

5.zcard
ZCARD key
时间复杂度: O(1)
当 key 存在且是有序集类型时,返回有序集的基数。 当 key 不存在时,返回 0 。

redis > ZADD salary 2000 tom    # 添加一个成员
(integer) 1

redis > ZCARD salary
(integer) 1

redis > ZADD salary 5000 jack   # 再添加一个成员
(integer) 1

redis > ZCARD salary
(integer) 2

redis > EXISTS non_exists_key   # 对不存在的 key 进行 ZCARD 操作
(integer) 0

redis > ZCARD non_exists_key
(integer) 0

6.ZRANGE
ZRANGE key start stop [WITHSCORES]
时间复杂度: O(log(N)+M), N 为有序集的基数,而 M 为结果集的基数。

返回有序集 key 中,指定区间内的成员。其中成员的位置按 score 值递增(从小到大)来排序。具有相同 score 值的成员按字典序(lexicographical order )来排列。如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE key start stop [WITHSCORES] 命令。下标参数 start 和 stop 都以 0 为底,也就是说,以 0 表示有序集第一个成员,以 1 表示有序集第二个成员,以此类推。 你也可以使用负数下标,以 -1 表示最后一个成员, -2 表示倒数第二个成员,以此类推。超出范围的下标并不会引起错误。 比如说,当 start 的值比有序集的最大下标还要大,或是 start > stop 时, ZRANGE 命令只是简单地返回一个空列表。 另一方面,假如 stop 参数的值比有序集的最大下标还要大,那么 Redis 将 stop 当作最大下标来处理。可以通过使用 WITHSCORES 选项,来让成员和它的 score 值一并返回,返回列表以 value1,score1, …, valueN,scoreN 的格式表示。 客户端库可能会返回一些更复杂的数据类型,比如数组、元组等。返回为指定区间内,带有 score 值(可选)的有序集成员的列表。

redis > ZRANGE salary 0 -1 WITHSCORES             # 显示整个有序集成员
1) "jack"
2) "3500"
3) "tom"
4) "5000"
5) "boss"
6) "10086"

redis > ZRANGE salary 1 2 WITHSCORES              # 显示有序集下标区间 1 至 2 的成员
1) "tom"
2) "5000"
3) "boss"
4) "10086"

redis > ZRANGE salary 0 200000 WITHSCORES         # 测试 end 下标超出最大下标时的情况
1) "jack"
2) "3500"
3) "tom"
4) "5000"
5) "boss"
6) "10086"

redis > ZRANGE salary 200000 3000000 WITHSCORES   # 测试当给定区间不存在于有序集时的情况
(empty list or set)

7.ZRANGEBYSCOR
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

时间复杂度: O(log(N)+M), N 为有序集的基数, M 为被结果集的基数。

返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。具有相同 score 值的成员按字典序(lexicographical order)来排列(该属性是有序集提供的,不需要额外的计算)。

可选的 LIMIT 参数指定返回结果的数量及区间(就像SQL中的 SELECT LIMIT offset, count ),注意当 offset 很大时,定位 offset 的操作可能需要遍历整个有序集,此过程最坏复杂度为 O(N) 时间。

可选的 WITHSCORES 参数决定结果集是单单返回有序集的成员,还是将有序集成员及其 score 值一起返回。 该选项自 Redis 2.0 版本起可用。

区间及无限
min 和 max 可以是 -inf 和 +inf ,这样一来,你就可以在不知道有序集的最低和最高 score 值的情况下,使用ZRANGEBYSCORE 这类命令。默认情况下,区间的取值使用闭区间 (小于等于或大于等于),你也可以通过给参数前增加 ( 符号来使用可选的开区间 (小于或大于)。

(integer) 0
redis> ZADD salary 5000 tom
(integer) 0
redis> ZADD salary 12000 peter
(integer) 0

redis> ZRANGEBYSCORE salary -inf +inf               # 显示整个有序集
1) "jack"
2) "tom"
3) "peter"

redis> ZRANGEBYSCORE salary -inf +inf WITHSCORES    # 显示整个有序集及成员的 score 值
1) "jack"
2) "2500"
3) "tom"
4) "5000"
5) "peter"
6) "12000"

redis> ZRANGEBYSCORE salary -inf 5000 WITHSCORES    # 显示工资 <=5000 的所有成员
1) "jack"
2) "2500"
3) "tom"
4) "5000"

redis> ZRANGEBYSCORE salary (5000 400000            # 显示工资大于 5000 小于等于 400000 的成员
1) "peter"

8.zcount
ZCOUNT key min max

时间复杂度: O(log(N)), N 为有序集的基数。
返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。关于参数 min 和 max 的详细使用方法,请参考 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 命令。返回值为score 值在 min 和 max 之间的成员的数量。

redis> ZRANGE salary 0 -1 WITHSCORES    # 测试数据
1) "jack"
2) "2000"
3) "peter"
4) "3500"
5) "tom"
6) "5000"

redis> ZCOUNT salary 2000 5000          # 计算薪水在 2000-5000 之间的人数
(integer) 3

redis> ZCOUNT salary 3000 5000          # 计算薪水在 3000-5000 之间的人数
(integer) 2

9.zremrangebyrank
ZREMRANGEBYRANK key start stop
时间复杂度: O(log(N)+M), N 为有序集的基数,而 M 为被移除成员的数量。

移除有序集 key 中,指定排名(rank)区间内的所有成员。区间分别以下标参数 start 和 stop 指出,包含 start 和 stop 在内。

下标参数 start 和 stop 都以 0 为底,也就是说,以 0 表示有序集第一个成员,以 1 表示有序集第二个成员,以此类推。 你也可以使用负数下标,以 -1 表示最后一个成员, -2 表示倒数第二个成员,以此类推。返回值为被移除成员的数量。

redis> ZADD salary 2000 jack
(integer) 1
redis> ZADD salary 5000 tom
(integer) 1
redis> ZADD salary 3500 peter
(integer) 1

redis> ZREMRANGEBYRANK salary 0 1       # 移除下标 0 至 1 区间内的成员
(integer) 2

redis> ZRANGE salary 0 -1 WITHSCORES    # 有序集只剩下一个成员
1) "tom"
2) "5000"

10.zremrangebyscore
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

时间复杂度: O(log(N)+M), N 为有序集的基数, M 为结果集的基数。

返回有序集 key 中, score 值介于 max 和 min 之间(默认包括等于 max 或 min )的所有的成员。有序集成员按 score 值递减(从大到小)的次序排列。具有相同 score 值的成员按字典序的逆序(reverse lexicographical order )排列。
除了成员按 score 值递减的次序排列这一点外, ZREVRANGEBYSCORE 命令的其他方面和 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 命令一样。
返回值为指定区间内,带有 score 值(可选)的有序集成员的列表。

redis > ZADD salary 10086 jack
(integer) 1
redis > ZADD salary 5000 tom
(integer) 1
redis > ZADD salary 7500 peter
(integer) 1
redis > ZADD salary 3500 joe
(integer) 1

redis > ZREVRANGEBYSCORE salary +inf -inf   # 逆序排列所有成员
1) "jack"
2) "peter"
3) "tom"
4) "joe"

redis > ZREVRANGEBYSCORE salary 10000 2000  # 逆序排列薪水介于 10000 和 2000 之间的成员
1) "peter"
2) "tom"
3) "joe"

我们看一下它具体的应用:
首先最容易想到的就是排行榜了。
(博主在long long ago 也曾第一过)
在这里插入图片描述
类似的,zset 还可以⽤来存储学⽣的成绩,value 值是学⽣的 ID,score 是他的考试成绩。我们可以对成绩按分数进⾏排序就可以得到他的名次。
除此之外,zset 可以⽤来存粉丝列表,value 值是粉丝的⽤户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进⾏排序。

不太常见API有这些,文章上面已经给大家推荐查看API的网站了,用到的时候查一查哈
◆ zrevrank
◆ zrevrange
◆ zrevrangebyscore
◆ interstore
◆ zunionstore

上面的API可以分成这三类
在这里插入图片描述

三、Redis 客户端操作

带大家过一遍Redis 的API之后,这部分非常非常简单了。

所有语言的操作,其实都是对Redis API的封装,封装前的东西你都知道了,封装后的不是易如反掌?

以Java为例吧,基于Java语言的客户端比较热的是Jedis

在连接方面有两种方式,一种是直连、一种是线程池(一般都要用到线程池)

直连方式

// 1.生成一个 Jedis对象,这个对象负责和指定Reds节点进行通信
Jedis jedis = new Jedis("localhost", 6379);
// Sting  set get 操作
jedis.set("Jedis", "hello jedis!");
jedis.get("Jedis");
// 对结果进行自增
jedis.incr("counter")

构造方法最多有四个参数
在这里插入图片描述
hash操作

jedis hset("myhash",f1",v1);
jedis hset("myhash",f2,v2);
// 输出结果:{1=v1,f2=V2}
jedis.hgetAll(myhash");

是不是很熟悉,其他的类似。

Jedis连接池
直连的弊端很明显:消耗资源。如果在不同方面,频繁用到Redis,编写程序也很麻烦。
在这里插入图片描述
所以,连接池是非常重要的的。
在这里插入图片描述
方案对比

方案 优点 缺点
直连 简单方便;适用于少量长期连接的场景 1.存在每次新建/关闭TCP开销;2.资源无法控制,存在连接泄露的可能;3.Jedis对象线程不安全
连接池 Jedis预先生成,降低开销使用连接池的形式保护和控制资源的使用 相对于直连,使用相对麻烦尤其在资源的管理上需要很多参数来保证,一旦规划不合理也会出现问题。
// 初始化 Jedis连接池,通常来讲 Jedis Pool是单例的。
 GenericObjectPoolConf poolConfig new GenericObjectPoolConfigO Jedis Pool jedis Pool new JedisPool(poolConfig, 127.0.0. 1",6379;

简单使用:

Jedis jedis = null;
try{
	//1.从连接池获取 jedis对象
	jedis = jedisPool.getResource();
	//2.执行操作
	jedis set(hello","world");
	} catch(Exception e){
		eprintStackTrace();
	}
	finally {
		if (jedis != null)
		//如果使用 JedisPool,cose操作不是关闭连接,代表归还连接池jedis closed
		jedis.close();
	}
}

说明

唉,写得太长了,CSDN编辑器不允许我在一篇文章上继续发挥了。

这是下一篇文章。

传送门:【大厂面试】面试官看了赞不绝口的Redis笔记(二)

目录:
在这里插入图片描述
这五个专题串过之后,你会对Redis单体,有着非常好的理解了,后面再走就是看源码了。相信你到这一步已经可以独当一面了。

我再往下面写,就是Redis分布式领域相关的东西了,比如说Redis的主从复制、哨兵机制、 Redis cluster特性等。

传送门: 【大厂面试】面试官看了赞不绝口的Redis笔记(三)分布式篇

目录:
在这里插入图片描述
最后还有一些拓展的内容需要补充。


对了,兄dei,如果你觉得这篇文章可以的话,给俺点个赞再走,管不管?这样可以让更多的人看到这篇文章,对我来说也是一种激励。

如果你有什么问题的话,欢迎留言或者CSDN APP直接与我交流。

发布了205 篇原创文章 · 获赞 3758 · 访问量 59万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 岁月 设计师: pinMode

分享到微信朋友圈

×

扫一扫,手机浏览