起立!这份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 命令参考

(一)通用命令

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

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

1. KEYS

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

功能描述方面举例:

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

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

示例

redis> DBSIZE
(integer) 5

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

redis> DBSIZE
(integer) 6

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

3. EXISTS

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

redis> SET db "redis"
OK

redis> EXISTS db
(integer) 1

redis> DEL db
(integer) 1

redis> EXISTS db
(integer) 0
1234567891011

4. DEL

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

#  删除单个 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

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

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

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

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

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

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

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

值的类型有:

  • 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

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

#  删除单个 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

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

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

⼀次性查出所有满⾜条件的 key,万⼀Redis中有⼏百 w 个 key 满⾜条件,满屏都是输出的结果,眼花缭乱。

keys由于走的是遍历算法,复杂度是 O(n),如果Redis中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写Redis 的其它的指令都会被延后甚⾄会超时报错,因为 Redis是单线程程序,顺序执⾏所有指令,其它指令必须等到当前的keys 指令执⾏完了才可以继续。

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

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

复杂度虽然也是 O(n),但是它是通过游标(cursor,相当于位置)分步进⾏的,不会阻塞线程;

提供 limit 参数(可选),可以控制每次返回结果的最⼤条数(实际上是遍历的key的数量);

它也提供模式匹配功能;

服务器不需要为游标(cursor)保存状态,游标(cursor)的唯⼀状态就是 scan 返回给客户端的游标整数;

返回的结果可能会有重复,需要客户端去重复(重要);

遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;

单次返回的结果是空的并不意味着遍历结束,⽽要看返回的游标值是否为零;

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模型

同步阻塞IO(Blocking IO)

同步非阻塞IO(Non-blocking IO)

IO多路复用(IO Multiplexing)

异步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

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

示例对不存在的键 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

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

可选参数

从 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

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

对储存数字值的键 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

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

对已经存在的键执行 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

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

示例演示与上面类似

6. DECRBY

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

示例演示与上面类似

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

应用

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

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

2.缓存用户的基本信息(数据源在 MySQL中),信息被序列化存放在value中。

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

一般而言,需要通过我们自定义规则的key,从Redis获取value,如果key存在的话,则直接获取value使用;如果不存在的话,从Mysql中读取使用,然后存在Redis中。
主要的命令是 get 和 set

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

3.分布式id生成器
如果集群规模和运算不太复杂的话,可以用Redis生成分布式id,因为Redis单线程的特点,一次只执行一条指令,保证了id值的唯一。
主要的命令还是incr

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

7. SETNX

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

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

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

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

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

同时对多个键进行设置:

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

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

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::

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

客户端和服务器端可能不在同一个地方n次get/set=n次网络时间+n次命令时间

一次mget/mset:

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

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 移除了最后⼀个元素之后,该数据结构⾃动被删除,内存被回收。

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

下面我们看一下它的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种方案比较:

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

(六)列表

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

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

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

1.LPUSH

LPUSH key value [value …]

猜你喜欢

转载自blog.csdn.net/java_xiaoo/article/details/110563887
今日推荐