Redis技术入门
in Redis with 0 comment

Redis技术入门

in Redis with 0 comment

简介

背景

我们现在的项目架构中,基本上是Web服务器(Tomcat)和数据库独立部署,独占服务器资源,随着用户数的增长,并发读写数据库,会加大数据库访问压力,导致性能的下降,严重时直接导致系统宕机,例如:

image-1654691465048

此时,我们可以在Tomcat同服务器上中增加本地缓存,并在外部增加分布式缓存,缓存热门数据。也就是通过缓存能把绝大多数请求在读写数据库前拦截掉,大大降低数据库压力。例如:

image-1654691476013

基于这样的一种架构设计,于是类似redis的一些分布式数据库就诞生了。

Redis 概述

Redis(Remote Dictionary Server )即远程字典服务,是 C 语言开发的一个开源的key/value存储系统(官网:http://redis.io),是一个分布式缓存数据库。Redis 是一个开源的(遵守 BSD 协议)、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库。由于它是基于内存的所以它要比基于磁盘读写的数据库效率更快。在DB-Engines.com的数据库排行中, Redis上升排行第七,如图所示:

image-1654691487073

Redis 提供了一些丰富的数据结构,包括 lists、sets、ordered sets 以及 hashes ,当然还有和 Memcached 一样的 strings 结构。Redis 当然还包括了对这些数据结构的丰富操作。

Redis 常被称作是一款数据结构服务器(data structure server)。Redis 的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和 有序集合(sorted sets)等数据类型。

对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。

Redis框架如图:

Redis框架

Redis 的优点

Redis为什么这么快

Redis 到底有多快

大家可能都知道 Redis 很快,可是 Redis 到底能有多快呢,比如 Redis 的吞吐量能达到多少?我想这就不是每个人都能说的上来一个具体的数字了。

Redis 官方提供了一个测试脚本,可以供我们测试 Redis 的吞吐量:

下图就是我这边执行第一条命令的自测结果,可以看到大部分命令的吞吐量都可以达到 4 万以上,也就是说每秒钟可以处理 4 万次以上请求:

image-1654751714249

但是如果你以为这就是 Redis 的真实吞吐量,那就错了。实际上,Redis 官方的测试结果是可以达到 10 万的吞吐量,下图就是官方提供的一个基准测试结果(纵坐标就是吞吐量,横坐标是连接数):

image-1654751723388

Redis 是单线程还是多线程

这个问题比较经典,因为在很多人的认知里,Redis 就是单线程的。然而 Redis4.0 版本开始就有了多线程的概念,虽然处理命令请求的核心模块确实是保证了单线程执行,然而在其它许多地方已经有了多线程,比如:在后台删除对象,通过 Redis 模块实现阻塞命令,生成 dump 文件,以及 6.0 版本中网络 I/O 实现了多线程等,而且在未来 Redis 应该会有越来越多的模块实现多线程。

所谓的单线程,只是说 Redis 处理客户端的请求(即执行命令)时,是单线程去执行的,并不是说整个 Redis 都是单线程。

Redis 为什么选择使用单线程来执行请求

Redis 为什么会选择使用单线程呢?这是因为 CPU 成为 Redis 瓶颈的情况并不常见,成为 Redis 瓶颈的通常是内存或网络带宽。例如,在一个普通的 Linux 系统上使用 pipelining 命令,Redis 可以每秒完成 100 万个请求,所以如果我们的应用程序主要使用 O(N)O(log(N)) 复杂度的命令,它几乎不会使用太多的 CPU

那么既然 CPU 不会成为瓶颈,理所当然的就没必要去使用多线程来执行命令,这里需要明确的一个问题就是:多线程一定会比单线程快吗?答案是不一定。因为多线程也是有代价的,最直接的两个代价就是线程的创建和销毁(当然可以通过线程池来一定程度减少频繁的创建线程和销毁线程)以及线程的上下文切换。

在我们的日常系统中,主要可以区分为两种:CPU 密集型 和 IO 密集型:

  1. CPU 密集型:这种系统就说明 CPU 的利用率很高,那么使用多线程反而会增加上下文切换而带来额外的开销,所以使用多线程效率可能会不升反降。

举个例子:假如你现在在干活,你一直不停的在做一件事,需要 1 分钟可以做完,但是你中途总是被人打断,需要花 1 秒钟时间步行到旁边去做另一件事,假如这件事也需要 1 分钟,那么你因为反复切换做两件事,每切换一次就要花 1 秒钟,最后做完这 2 件事的时间肯定大于 2 分钟(取决于中途切换的次数),但是如果中途不被打断,你做完一件事再去做另一件事,那么你最多只需要切换 1 次,也就是 21 秒就能做完。

  1. IO 密集型:IO 操作也可以分为磁盘 IO 和网络 IO 等操作。大部分 IO 操作的特点是比较耗时且 CPU 利用率不高,所以 Redis 6.0 版本网络 IO 会改进为多线程。至于磁盘 IO,因为 Redis 中的数据都存储在内存(也可以持久化),所以并不会过多的涉及到磁盘操作。

举个例子:假如你现在给树苗浇水,你每浇完一次水之后就需要等别人给你加水之后你才能继续浇,那么假如这个等待过程需要 5 秒钟,也就是说你浇完一次水就可以休息 5 秒钟,而你切换去做另一件事来回只需要 2 秒,那么你完全可以先去做另一件事,做完之后再回来继续浇水,这样就可以充分利用你空闲的 5 秒钟时间,从而提升了效率。

使用多线程还会带来一个问题就是数据的安全性,所以多线程编程都会涉及到锁竞争,由此也会带来额外的开销。

什么是 IO 多路复用机制

I/O 指的是网络 I/O, 多路指的是多个 TCP 连接(如 Socket),复用指的是复用一个或多个线程。I/O 多路复用的核心原理就是不再由应用程序自己来监听连接,而是由服务器内核替应用程序监听。

Redis 中,其多路复用有多种实现,如:selectepollevportkqueue 等。

我们用去餐厅吃饭的例子来解释一下 I/O 多路复用机制(点餐人相当于客户端,餐厅的厨房相当于服务器,厨师就是线程)。

需要注意的是:在 IO 多路复用机制下,客户端可以阻塞也可以选择不阻塞(大部分场景下是阻塞 IO),这个要具体情况具体分析,但是在多路复用机制下,服务端就可以通过多线程(上面示例中可以多几个厨师同时炒菜)来提升并发效率。

Redis 中 I/O 多路复用的应用

Redis 服务器是一个事件驱动程序,服务器需要处理两类事件:文件事件和时间事件。

Redis 的文件事件处理器以单线程的方式运行,其内部使用了 I/O 多路复用程序来同时监听多个套接字(Socket)连接,提升了性能的同时又保持了内部单线程设计的简单性。下图就是文件事件处理器的示意图:

image-1654751778746

I/O 多路复用程序虽然会同时监听多个 Socket 连接,但是其会将监听的 Socket 都放到一个队列里面,然后通过这个队列有序的、同步的将每个 Socket 对应的事件传送给文件事件分派器,再由文件事件分派器分派给对应的事件处理器进行处理,只有当一个 Socket 所对应的事件被处理完毕之后,I/O 多路复用程序才会继续向文件事件分派器传送下一个 Socket 所对应的事件,这也可以验证上面的结论,处理客户端的命令请求是单线程的方式逐个处理,但是事件处理器内并不是只有一个线程。

Redis 为什么这么快

Redis 为什么这么快的原因前面已经基本提到了,现在我们再总结一下:

版本及参考说明

Redis的次版本号(第一个小数点后的数字)为偶数的版本是稳定版本(2.4、2.6等),奇数为非稳定版本(2.5、2.7),一般推荐在生产环境使用稳定版本。最新版本6.2.2,新增了stream的处理方式,性能更高。Redis官方是不支持windows平台的,windows版本是由微软自己建立的分支,基于官方的Redis源码上进行编译、发布、维护的,所以windows平台的Redis版本要略低于官方版本。

Redis 相关参考网址如下所示:

Bootnb 相关:https://www.runoob.com/redis/redis-tutorial.html
Redis 官网:https://redis.io/
源码地址:https://github.com/redis/redis
Redis 在线测试:http://try.redis.io/
Redis 命令参考:http://doc.redisfans.com/

Redis 安装(Linux)

Redis 的安装步骤

redis 手动安装的话非常简单,以 redis-4.0.9 版本为例。

首先进入 root 目录并下载 Redis 的程序包:

sudo su
cd
wget https://labfile.oss.aliyuncs.com/courses/106/redis-4.0.9.tar.gz

在目录下,解压安装包,生成新的目录 redis-4.0.9:

tar -xzvf redis-4.0.9.tar.gz

进入解压之后的目录,进行编译:

cd redis-4.0.9
make

说明:如果没有明显的错误,则表示编译成功。

操作截图:

image-1654751884781

查看重要文件

在 Redis 安装完成后,注意一些重要的文件,可用 ls 命令查看。

image-1654751894084

设置环境变量

为了今后能更方便地打开 Redis 服务器和客户端,可以将 src 目录下的 redis-server 和 redis-cli 添加进环境变量所属目录里面。

cp redis-server /usr/local/bin/
cp redis-cli /usr/local/bin/

添加完成后在任何目录下输入 redis-server 可启动服务器,输入 redis-cli 可启动客户端。

运行测试

在前面的步骤设置完成后可以运行测试(非必须),确认 Redis 的功能是否正常。

cd /root/redis-4.0.9
make test

如果提示 You need tcl 8.5 or newer in order to run the Redis test

可以安装 tcl,然后再进行测试

sudo apt-get install tcl -y

这是在我本地虚拟机运行 make test 的结果:

image-1654751929319

Redis安装(Windows)

一、下载windows版本的Redis

官网上不提供windows版本的,现在官网没有下载地址,只能在github上下载,官网只提供linux版本的下载 官网下载地址:

Github源码地址:

Github下载地址:

或者

Redis中文网:

这里我选择的是x64-3.2.100,下载的时候下载msi(不要下载zip的压缩包)

建议:在github上公开项目下载速度非常慢,我100M光钎每秒速度不到10K

可以在CSDN上下载 地址:

webp

选择第一个Redis-x64-3.2.100.msi

二、安装Redis

1.首先双击现在完的安装程序

1.2

2.点击next

1.3

3.点击接受 继续next

1.4

4.设置Redis的服务端口 默认为6379 默认就好,单击next

1.5

5.选择安装的路径,并且打上勾(这个非常重要),添加到path是把Redis设置成windows下的服务,不然你每次都要在该目录下启动命令redis-server redis.windows.conf,但是只要一关闭cmd窗口,redis就会消失,这样就比较麻烦。

1.6

6.设置Max Memory,然后next进入安装

如果redis的应用场景是作为db使用,那不要设置这个选项,因为db是不能容忍丢失数据的。

如果作为cache缓存那就得看自己的需要(我这里设置了1024M的最大内存限制)

指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区。

1.7

7.安装完成

三、测试所安装的Redis

如果你是和我一样通过msi文件的安装,你可以在计算机管理→服务与应用程序→服务 看到Redis正在运行

1.9

你也可以将它停止,(不停止会出现错误代码为18012的错误,表示本机端口6379被占用)

然后在cmd窗口进入Redis的安装路径的根目录

输入命令redis-server.exe redis.windows.conf,出现下图证明Redis服务启动成功

1.10

下面进行测试:

你可以在Redis的安装根目录下找到redis-cli.exe文件启动(我用的是这种方法)

或在cmd中先进入Redis的安装根目录用命令**redis-cli.exe -h 192.168.10.61 -p 6379(注意换成自己的IP)**的方式打开

测试方法:设置键值对 取出键值对 (我这里键值对是peng)

1.11

四、测试成功,安装完成

Mac安装Redis

Mac安装Redis

Redis初始操作

启动redis服务

Docker 环境下的启动(docker环境启动多个需要运行多个容器):

docker start redis01 #底层也是通过redis-server启动,start单词后的redis01为容器名

docker 中查看redis 服务

docker ps

查看启动的redis进程信息

ps -ef|grep redis
root      3511     1  0 16:29 ?   00:00:01 redis-server *:6379
root      3515     1  0 16:29 ?   00:00:01 redis-server 127.0.0.1:6380

进入redis容器

docker exec -it redis01 bash #redis01 为容器名

登陆redis服务

登陆本地redis

redis-cli
或者
redis-cli -p 6379
或者
redis-cli -p 6379 -a  password #-a后面为password,此操作需要开启redis.conf文件中的 requirepass选项

登陆远程redis

redis-cli  -h ip  -p 6379  -a  password

查看redis信息

首先登陆redis,然后输入info指令,例如

127.0.0.1:6379> info		#查看当前redis节点的详细配置信息

清空redis屏幕

清除redis屏幕内容

127.0.0.1:6379> clear

退出redis服务

退出redis服务,例如

127.0.0.1:6379> exit

关闭redis服务

关闭redis服务,例如:

127.0.0.1:6379> shutdown

系统帮助

可以基于help指令查看相关指令帮助,例如

127.0.0.1:6379> help
redis-cli 2.8.19
Type: "help @<group>" to get a list of commands in <group>
      "help <command>" for help on <command>
      "help <tab>" to get a list of possible help topics
      "quit" to exit
123456
127.0.0.1:6379> help type

  TYPE key
  summary: Determine the type stored at key
  since: 1.0.0
  group: generic

Redis数据存储操作

在 Redis 中,命令大小写不敏感。

简易数据存取

基于查看redis中的key

127.0.0.1:6379> keys *
(empty list or set)

基于key/value形式存储数据

127.0.0.1:6379> set test1 123
OK
127.0.0.1:6379> set test2 ab
OK
127.0.0.1:6379> keys *
1) "test1"
2) "test2"

基于key获取redis中存储的数据

127.0.0.1:6379> get test1
"123"
127.0.0.1:6379> get test2
"ab"
127.0.0.1:6379> get test3
(nil)
127.0.0.1:6379>

清除redis中的数据

删除指定key对应的数据

127.0.0.1:6379> del test1  #test1为key的名字
OK

清除当前数据库数据

127.0.0.1:6379> flushdb
OK

清除所有数据库数据

127.0.0.1:6379> flushall
OK

适合全体类型的常用命令

启动 redis 服务和 redis-cli 命令界面继续后续实验:

sudo service redis-server start
sudo su
cd
redis-cli

EXISTS and DEL

exists key:判断一个 key 是否存在,存在返回 1,否则返回 0。

del key:删除某个 key,或是一系列 key,比如:del key1 key2 key3 key4。成功返回 1,失败返回 0(key 值不存在)。

> set mykey hello
> exists mykey
> del mykey
> exists mykey

操作截图:

image-1654836205561

TYPE and KEYS

type key:返回某个 key 元素的数据类型(none:不存在,string:字符,list:列表,set:元组,zset:有序集合,hash:哈希),key 不存在返回空。

keys key—pattern:返回匹配的 key 列表,比如:keys foo* 表示查找 foo 开头的 keys。

> set mykey x
> type mykey
> keys my*
> del mykey
> keys my*
> type mykey

操作截图:

image-1654836215390

RANDOMKEY and CLEAR

randomkey:随机获得一个已经存在的 key,如果当前数据库为空,则返回空字符串。

> randomkey

操作截图:

image-1654836225108

clear:清除界面。

> clear

RENAME and RENAMENX

rename oldname newname:更改 key 的名字,新键如果存在将被覆盖。 renamenx oldname newname:更改 key 的名字,新键如果存在则更新失败。

比如这里 randomkey 结果为 mylist,将此 key 值更名为 newlist。

> randomkey
> rename mylist newlist
> exists mylist
> exists newlist

操作截图:

image-1654836237293

DBSIZE

dbsize:返回当前数据库的 key 的总数。

> dbsize

操作截图:

image-1654836249600

Redis 时间相关命令/Key有效时间设计

下面我们将会学习 Redis 时间相关命令。

实际工作中我们经常要控制redis中key的有效时长,例如秒杀操作的计时,缓存数据的有效时长等。

限定 key 生存时间

这同样是一个无视数据类型的命令,对于临时存储很有用处。避免进行大量的 DEL 操作。

Expire (设置生效时长-单位秒)

语法:EXPIRE key seconds

127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 10
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 5
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 2
127.0.0.1:6379> ttl bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

其中,TTL查看key的剩余时间,当返回值为-2时,表示键被删除。
当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。

expire:设置某个 key 的过期时间(秒),比如:expire bruce 1000 表示设置 bruce 这个 key 1000 秒后系统自动删除,注意:如果在还没有过期的时候,对值进行了改变,那么那个值会被清除。

> set key some-value
> expire key 10
> get key     # 马上执行此命令
> get key     # 10s后执行此命令

操作截图:

image-1654836266163

结果显示:执行 expire 命令后,马上 get 会显示 key 存在;10 秒后再 get 时,key 已经被自动删除。

查询 key 剩余生存时间

限时操作可以在 set 命令中实现,并且可用 ttl 命令查询 key 剩余生存时间。

ttl:查找某个 key 还有多长时间过期,返回时间单位为秒。

> set key 100 ex 30
> ttl key
> ttl key

操作截图:

image-1654836273889

清除 key

flushdb:清空当前数据库中的所有键。 flushall:清空所有数据库中的所有键。

> flushdb
> flushall

取消时长设置

Persist (取消时长设置)

通过persist让对特定key设置的生效时长失效。

语法:PERSIST key

127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 60
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 49
127.0.0.1:6379> persist bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -1
127.0.0.1:6379>

其中,设置新的数据时需要重新设置该key的生存时间,重新设置值也会清除生存时间。

单位毫秒

pexpire (单位毫秒)

pexpire 让key的生效时长以毫秒作为计量单位,这样可以做到更精确的时间控制。例如,可应用于秒杀场景。

语法:PEXPIRE key milliseconds

127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> pexpire bomb 10000
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 6
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

Redis 设置相关命令

Redis 有其配置文件,可以通过 client-command 窗口查看或者更改相关配置。下面介绍相关命令。

CONFIG GET and CONFIG SET

config get:用来读取运行 Redis 服务器的配置参数。 config set:用于更改运行 Redis 服务器的配置参数。 auth:认证密码。

下面针对 Redis 密码的示例:

> config get requirepass  # 查看密码
> config set requirepass test123  # 设置密码为 test123
> config get requirepass  # 报错,没有认证
> auth test123  # 认证密码
> config get requirepass

操作截图:

image-1654836285893

由结果可知,刚开始时 Reids 并未设置密码,密码查询结果为空。然后设置密码为 test123,再次查询报错。经过 auth 命令认证后,可正常查询。

可以通过修改 Redis 的配置文件 redis.conf 修改密码。

config get 命令是以 list 的 key-value 对显示的,如查询数据类型的最大条目:

> config get *max-*-entries*

操作截图:

image-1654836294458

重置报告

config resetstat:重置数据统计报告,通常返回值为“OK”。

> CONFIG RESETSTAT

操作截图:

image-1654836307216

查询信息

info [section]:查询 Redis 相关信息。

info 命令可以查询 Redis 几乎所有的信息,其命令选项有如下:

若命令参数为空,info 命令返回所有信息。

> info keyspace
> info server

操作截图:

image-1654836333460

image-1654836339091

Redis安全性设置

涉及到客户端连接是需要指定密码的(由于 redis 速度相当的快,一秒钟可以 150K 次的密码尝试,所以需要设置一个强度很大的密码)。

设置密码的方式有两种:

输入认证的方式也有两种:

设置密码

第一种密码设置方式在上一个实验中已经提到(在 CONFIG SET 命令讲解的实例),此处我们来看看第二种方式设置密码。

首先需要进入 Redis 的安装目录,然后修改配置文件 redis.conf。根据 grep 命令的结果,使用 vim 编辑器修改 “# requirepass foobared” 为 “requirepass test123”,然后保存退出。

sudo grep -n requirepass /etc/redis/redis.conf
sudo vim /etc/redis/redis.conf

编辑 redis.conf 的结果:

image-1654839338005

重启 redis-server 与 redis-cli

重启 redis server:

sudo service redis-server restart

进入到 redis-cli 交互界面进行验证:

$ redis-cli
> info
> auth test123
> info
> exit

操作截图:

image-1654839374990

结果表明第一次 info 命令失败,在 auth 认证之后 info 命令正常返回,最后退出 redis-cli。

另外一种密码认证方式:

$ redis-cli -a test123
> info

操作截图:

image-1654839382900

常用数据类型

Redis作为一种key/value结构的数据存储系统,为了便于对数据进行进行管理,提供了多种数据类型。然后,基于指定类型存储我们项目中产生的数据,例如用户的登陆信息,购物车信息,商品详情信息等等。

Reids中基础数据结构包含字符串、散列,列表,集合,有序集合。工作中具体使用哪种类型要结合具体场景。

现在我们一一讲解:Redis keys 是采用二进制安全,这就意味着你可以使用任何二进制序列作为重点,比如:“foo” 可以联系一个 JPEG 文件;空字符串也是一个有效的密钥。

一个简单的字符串,为什么 Redis 要设计的如此特别

Redis 的 9 种数据类型

Redis 中支持的数据类型到 6.0.6 版本,一共有 9 种。分别是:

  1. Binary-safe strings(二进制安全字符串)
  2. Lists(列表)
  3. Sets(集合)
  4. Sorted sets(有序集合)
  5. Hashes(哈希)
  6. Bit arrays (or simply bitmaps)(位图)
  7. HyperLogLogs
  8. geospatial
  9. Streams

虽然这里列出了 9 种,但是基础类型就是前面 5 种。后面的 4 种是基于前面 5 种基本类型及特定的算法来实现的特殊类型。

而在 5 种基础类型之中,又尤其以字符串类型最为常用,且 key 值只能为字符串对象,所以要想深入的了解 Redis 的特性,字符串对象是首先需要学习的。

String类型操作实践

字符串类型是redis中最简单的数据类型,它存储的值可以是字符串,其最大字符串长度支持到512M。基于此类型,可以实现博客的字数统计,将日志不断追加到指定key,实现一个分布式自增iid,实现一个博客的的点赞操作等

二进制安全字符串

Redis 是基于 C 语言进行开发的,而 C 语言中的字符串是二进制不安全的,所以 Redis 就没有直接使用 C 语言的字符串,而是自己编写了一个新的数据结构来表示字符串,这种数据结构称之为简单动态字符串(Simple dynamic string),简称 sds

什么是二进制安全的字符串

C 语言中,字符串采用的是一个 char 数组(柔性数组)来存储字符串,而且字符串必须要以一个空字符串 \0 来结尾。字符串并不记录长度,所以如果想要获取一个字符串的长度就必须遍历整个字符串,直到遇到第一个 \0 为止(\0 不会计入字符串长度),故而获取字符串长度的时间复杂度为 O(n)

正因为 C 语言中是以遇到的第一个空字符 \0 来识别是否到了字符串末尾,因此其只能保存文本数据,不能保存图片、音频、视频和压缩文件等二进制数据,否则可能出现字符串不完整的问题,所以其是二进制不安全的。

Redis 中为了实现二进制安全的字符串,对原有 C 语言中的字符串实现做了改进。如下所示就是一个旧版本的 sds 字符串的结构定义:

struct sdshdr{
  int len;//记录buf数组已使用的长度,即SDS的长度(不包含末尾的'\0')
  int free;//记录buf数组中未使用的长度
  char buf[];//字节数组,用来保存字符串
}

经过改进之后,如果想要获取 sds 的长度不用去遍历 buf 数组了,直接读取 len 属性就可以得到长度,时间复杂度一下就变成了 O(1),而且因为判断字符串长度不再依赖空字符 \0,所以其能存储图片、音频、视频和压缩文件等二进制数据,不用担心读取到的字符串不完整。

需要注意的是,sds 依然遵循了 C 语言字符串以 \0 结尾的惯例,这么做是为了方便复用 C 语言字符串原生的一些 API,换言之就是在 C 语言中会以碰到的第一个 \0 字符作为当前字符串对象的结尾,所以如果一些二进制数据就可能会出现读取字符串不完整的现象,而 sds 会以长度来判断是否到字符串末尾。

Redis 3.2 之后的版本,Redissds 又做了优化,按照存储空间的大小拆分成为了 sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64,分别用来存储大小为:32 字节(25 次方),256 字节(28 次方),64KB216 次方),4GB 大小(232 次方)以及 264 次方大小的字符串(因为目前版本 keyvalue 都限制了最大 512MB,所以 sdshdr64 暂时并未使用到)。 sdshdr5 只被应用在了 Rediskey 中,value 中不会被使用到,因为 sdshdr5 和其它类型也不一样,其并没有存储未使用空间,所以比较适用于使用大小固定的场景(比如 key 值):

image-1654753169637

任意选择其中一种数据类型,其字段代表含义如下:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; //已使用空间大小
    uint8_t alloc; //总共申请的空间大小(包括未使用的)
    unsigned char flags; //用来表示当前sds类型是sdshdr8还是sdshdr16等
    char buf[]; //真实存储字符串的字节数组
};

可以看到相比较于 Redis 3.2 版本之前的 sds,主要是修改了 free 属性然后新增了一个 flags 标记来区分当前的 sds 类型。

sds 空间分配策略

C 语言中因为字符串内部没有记录长度,所以如果扩充字符串非常容易造成缓冲区溢出(buffer overflow)

请看下面这张图,假设下图就是内存里面的连续空间,可以很明显的看到,此时 wolfRedis 两个字符串之间只有三个空位,那么这时候如果我们要将 wolf 字符串修改为 lonelyWolf,那么就需要 6 个空间,这时候下面这个空间是放不下的,必须要重新申请空间。但是假如说程序员忘了申请空间,或者说申请到的空间依然不够,那么就会出现后面的 Redis 字符串中的 Red 被覆盖了:

image-1654753202791

同样的,假如要缩小字符串的长度,那么也需要重新申请释放内存。否则,字符串一直占据着未使用的空间,会造成内存泄露

C 语言避免缓存区溢出和内存泄露完全依赖于人为,很难把控,但是使用 sds 就不会出现这两个问题,因为当我们操作 sds 时,其内部会自动执行空间分配策略,从而避免了上述两种情况的出现。

空间预分配

空间预分配指的是当我们通过 apisds 进行扩展空间时,假如未使用空间不够用,那么程序不仅会为 sds 分配必须要的空间,还会额外分配未使用空间,未使用空间分配大小主要有两种情况:

执行空间预分配策略的好处是**提前分配了未使用空间备用后,就不需要每次增大字符串都需要分配空间,减少了内存重分配的次数。

惰性空间释放

惰性空间释放指的是当我们需要通过 api 减小 sds 长度的时候,程序并不会立即释放未使用的空间,而只是更新 free 属性的值,这样空间就可以留给下一次使用。而为了防止出现内存溢出的情况,sds 单独提供给了 api 让我们在有需要的时候去真正的释放内存。

sds 和 C 语言字符串区别

下面表格中列举了 Redis 中的 sdsC 语言中实现的字符串的区别:

C 字符串 SDS
只能保存文本类不含空字符串 \0 数据 可以保存文本或者二进制数据,允许包含空字符串 \0
获取字符串长度的复杂度为 O(n) 获取字符串长度的复杂度为 O(1)
操作字符串可能会造成缓冲区溢出 不会出现缓冲区溢出情况
修改字符串长度 N 次,必然需要 N次内存重分配 修改字符串长度 N 次,最多需要 N 次内存重分配
可以使用 C 字符串相关的所有函数 可以使用 C 字符串相关的部分函数

sds 是如何被存储的

Redis 中所有的数据类型都是将对应的数据结构进行了再一次包装,创建了一个字典对象来存储,sds 也不例外。每次创建一个 key-value 键值对,Redis 都会创建两个对象,一个是键对象,一个是值对象。而且需要注意的是Redis 中,值对象并不是直接存储,而是被包装成 redisObject 对象,并同时将键对象和值对象通过 dictEntry 对象进行封装,如下就是一个 dictEntry 对象:

typedef struct dictEntry {
    void *key;//指向key,即sds
    union {
        void *val;//指向value
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;//指向下一个key-value键值对(哈希值相同的键值对会形成一个链表,从而解决哈希冲突问题)
} dictEntry;

redisObject 对象的定义为:

typedef struct redisObject {
    unsigned type:4;//对象类型(4位=0.5字节)
    unsigned encoding:4;//编码(4位=0.5字节)
    unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节)
    int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节)
    void *ptr;//指向底层实际的数据存储结构,如:sds等(8字节)
} robj;

当我们在 Redis 客户端中执行命令 set name lonely_wolf ,就会得到下图所示的一个结构(省略了部分属性):

image-1654753348088

看到这个图想必大家会有疑问,这里面的 typeencoding 到底是什么呢?其实这两个属性非常关键,Redis 就是通过这两个属性来识别当前的 value 到底属于哪一种基本数据类型,以及当前数据类型的底层采用了何种数据结构进行存储。

type 属性

type 属性表示对象类型,其对应了 Redis 当中的 5 种基本数据类型:

类型属性 描述 type 命令返回值
REDIS_STRING 字符串对象 string
REDIS_LIST 列表对象 list
REDIS_HASH 哈希对象 hash
REDIS_SET 集合对象 set
REDIS_ZSET 有序集合对象 zset

可以看到,这就是对应了我们 5 种常用的基本数据类型。

encoding 属性

Redis 当中每种数据类型都是经过特别设计的,相信大家看完这个系列也会体会到 Redis 设计的精妙之处。字符串在我们眼里是非常简单的一种数据结构了,但是 Redis 却把它优化到了极致,为了节省空间,其通过编码的方式定义了三种不同的存储方式:

编码属性 描述 object encoding命令返回值
OBJ_ENCODING_INT 使用整数的字符串对象 int
OBJ_ENCODING_EMBSTR 使用 embstr 编码实现的字符串对象 embstr
OBJ_ENCODING_RAW 使用 raw 编码实现的字符串对象 raw

讲了半天理论,接下来让我们一起来验证下这些结论。首先启动 Redis

sudo redis-server /etc/redis/redis.conf
redis-cli

连接上 Redis 之后依次输入以下命令:

set name lonely_wolf
type name
object encoding name

得到如下所示结果:

image-1654753361903

可以发现当前的数据类型就是 string,普通字符串因为长度小于 44,所以采用的是 embstr 编码。

再依次输入如下命令:

set num 1111111111
type num
object encoding num
set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa   //长度 44(注释不要输入)
object encoding address
set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  //长度 45(注释不要输入)
object encoding address

最后得到如下所示效果:

image-1654753370616

可以发现,当输入纯数字的时候,采用的是 int 编码,而字符串小于等于 44 则为 embstr,大于 44 则为 raw 编码。

字符串对象中除了上面提到的纯整数和字符串,还可以存储浮点型类型,所以字符串对象可以存储以下三种类型:

而当我们的 value 为整数时,还可以使用原子自增命令来实现 value 的自增,这个命令在实际开发过程中非常实用。

image-1654753378864

不过这两个命令只能用在 value 为整数的场景,当 value 不是整数时则会报错。

embstr 编码为什么从 39 位修改为 44 位

embstr 编码中,redisObjectsds 是连续的一块内存空间,这块内存空间 Redis 限制为了 64 个字节,而 redisObject 固定占了 16 字节(上面定义中有标注),Redis 3.2 版本之前的 sds 占了 8 个字节,再加上字符串末尾 \0 占用了 1 个字节,所以:64-16-8-1=39 字节。

Redis 3.2 版本之后 sds 做了优化,对于 embstr 编码会采用 sdshdr8 来存储,而 sdshdr8 占用的空间只有 24 位:3 字节(len + alloc + flag)+ \0 字符(1 字节),所以最后就剩下了:64-16-3-1=44 字节。

embstr 编码和 raw 编码的区别

embstr 编码是一种优化的存储方式,其在申请空间的时候因为 redisObjectsds 两个对象是一个连续空间,所以只需要申请 1 次空间(同样的,释放内存也只需要 1 次),而 raw 编码因为 redisObjectsds 两个对象的空间是不连续的,所以使用的时候需要申请 2 次空间(同样的,释放内存也需要 2 次)。但是使用 embstr 编码时,假如需要修改字符串,那么因为 redisObjectsds 是在一起的,所以两个对象都需要重新申请空间,为了避免这种情况发生,embstr 编码的字符串是只读的,不允许修改。

依次输入如下命令来验证一下编码转换:

set addr china
object encoding addr
append addr -beijing    //在addr所对应的值后面追加“-beijing”(注释不要输入)
get addr
object encoding addr

得到如下所示结果:

image-1654753388719

上图中的示例我们看到,对一个 embstr 编码的字符串对象进行 append 操作时,长度还没有达到 45,但是编码已经被修改为 raw 了,这就是因为 embstr 编码是只读的,如果需要对其修改,Redis 内部会将其修改为 raw 编码之后再操作。同样的,如果是操作 int 编码的字符串之后,导致 long 类型无法存储时(int 类型不再是整数或者长度超过 263 次方减 1 时),也会将 int 编码修改为 raw 编码。

PS:需要注意的是,编码一旦升级(int–>embstr–>raw),即使后期再把字符串修改为符合原编码能存储的格式时,编码也不会回退。

Redis strings

字符串是一种最基本、最常用的 Redis 值类型。

Redis 对字符串对象进行了特别设计,这就是 sds,一种二进制安全的字符串对象。

Redis 字符串是二进制安全的,这意味着一个 Redis 字符串能包含任意类型的数据,例如: 一张经过 base64 编码的图片或者一个序列化的 Ruby 对象。通过这样的方式,Redis 的字符串可以支持任意形式的数据,但是对于过大的文件不适合存入 redis,一方面系统内存有限,另外一方面字符串类型的值最多能存储 512M 字节的内容。

启动 redis-cli 来看看 Redis strings 数据类型。

$ sudo service redis-server start
$ sudo su
$ cd
$ redis-cli
> set mykey somevalue
> get mykey

操作截图:

image-1654753461761

如上例所示,可以使用 set 和 get 命令来创建和检索 strings。注意:set 命令将取代现有的任何已经存在的 key。set 命令还有一个提供附加参数的选项,我们能够让 set 命令只有在没有相同 key 的情况下成功,反之亦然,可以让 set 命令在有相同 key 值的情况下成功:

> set mykey newval nx
> set mykey newval xx

操作截图;

image-1654753469457

即使 string 是 Redis 的基本类型,也可以对其进行一些有趣的操作,例如加法器:

> set counter 100 # 初始化
> incr counter   # +1
> incr counter   # +1
> incrby counter 50 # +50 自定义计数

操作截图:

image-1654753478827

incr 命令让 the value 成为一个整数,运行一次 incr 便加一。incrby 命令便是一个加法运算。类似的命令如减法运算为: decr 和 decrby。

Redis 可以运用 mset 和 mget 命令一次性完成多个 key-value 的对应关系,使用 mget 命令,Redis 返回一个 value 数组:

> mset a 10 b 20 c 30
> mget a b c

操作截图:

image-1654753486522

incr/incrby

当存储的字符串是整数时,redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递增后的值。
语法:INCR key

127.0.0.1:6379> set num 1
(integer) 1
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> keys *
1) "num"	
127.0.0.1:6379> incr num
127.0.0.1:6379>

说明,如果num不存在,则自动会创建,如果存在自动+1。

指定增长系数

语法:INCRBY key increment

127.0.0.1:6379> incrby num 2
(integer) 5
127.0.0.1:6379> incrby num 2
(integer) 7
127.0.0.1:6379> incrby num 2
(integer) 9
127.0.0.1:6379>
1234567

decr/decrby

减少指定的整数
DECR key 按照默认步长(默认为1)进行递减
DECRBY key decrement 按照指定步长进行递减

127.0.0.1:6379> incr num
(integer) 10
127.0.0.1:6379> decr num
(integer) 9
127.0.0.1:6379> decrby num 3

append

向尾部追加值。如果键不存在则创建该键,其值为写的value,即相当于SET key value。返回值是追加后字符串的总长度。
语法:APPEND key value

127.0.0.1:6379> keys *
1) "num"
2) "test1"
3) "test"
127.0.0.1:6379> get test
"123"
127.0.0.1:6379> append test "abc"
(integer) 6
127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379>

strlen

字符串长度,返回数据的长度,如果键不存在则返回0。注意,如果键值为空串,返回也是0。
语法:STRLEN key

127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379> strlen test
(integer) 6
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> set tnt ""
OK
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> exists tnt
(integer) 1
127.0.0.1:6379>

mset/mget

同时设置/获取多个键值
语法:MSET key value [key value …]
MGET key [key …]

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c
1) "1"
2) "2"
3) "3"
127.0.0.1:6379>

小节面试分析

Hash类型应用实践

Redis散列类型相当于Java中的HashMap,实现原理跟HashMap一致,一般用于存储对象信息,存储了字段(field)和字段值的映射,一个散列类型可以包含最多232-1个字段。

Redis Hashes

Redis Hashes 是字符串字段和字符串值之间的映射,因此它们是展现对象的完整数据类型。例如一个有名、姓、年龄等等属性的用户:一个带有一些字段的 hash 仅仅需要一块很小的空间存储,因此你可以存储数以百万计的对象在一个小的 Redis 实例中。哈希主要用来表现对象,它们有能力存储很多对象,因此你可以将哈希用于许多其它的任务。

> hmset user:1000 username antirez birthyear 1977 verified 1
> hget user:1000 username
> hget user:1000 birthyear
> hgetall user:1000

操作截图:

image-1654753664240

hmset 命令设置一个多域的 hash 表,hget 命令获取指定的单域,hgetall 命令获取指定 key 的所有信息。hmget 类似于 hget,只是返回一个 value 数组。

> hmget user:1000 username birthyear no-such-field

操作截图:

image-1654753673083

同样可以根据需要对 hash 表的表项进行单独的操作,例如 hincrby: (原本 birthyear 为 1977,见上一图)

> hincrby user:1000 birthyear 10
> hincrby user:1000 birthyear 10

操作截图:

image-1654753681274

hset/hget/hgetall

语法结构

HSET key field value
HGET key field
HMSET key field value [field value…]
HMGET key field [field]
HGETALL key

HSET和HGET赋值和取值

127.0.0.1:6379> hset user username chenchen
(integer) 1
127.0.0.1:6379> hget user username
"chenchen"
127.0.0.1:6379> hset user username chen
(integer) 0
127.0.0.1:6379> keys user
1) "user"
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379> 
127.0.0.1:6379> hset user age 18
(integer) 1
127.0.0.1:6379> hset user address "xi'an"
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
3) "age"
4) "18"
3) "address"
4) "xi'an"
127.0.0.1:6379>

HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0。

hincrby

127.0.0.1:6379> hdecrby article total 1		#执行会出错
127.0.0.1:6379> hincrby article total -1		#没有hdecrby自减命令
(integer) 1
127.0.0.1:6379> hget article total			#获取值

hmset/hmget

HMSET和HMGET设置和获取对象属性

127.0.0.1:6379> hmset person username tony age 18
OK
127.0.0.1:6379> hmget person age username
1) "18"
2) "tony"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>

注意:上面HMGET字段顺序可以自行定义

hexists

属性是否存在
127.0.0.1:6379> hexists killer
(error) ERR wrong number of arguments for 'hexists' command
127.0.0.1:6379> hexists killer a
(integer) 0
127.0.0.1:6379> hexists user username
(integer) 1
127.0.0.1:6379> hexists person age
(integer) 1
127.0.0.1:6379>

hdel

删除属性

127.0.0.1:6379> hdel user age
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>

hkeys/hvals

只获取字段名HKEYS或字段值HVALS

127.0.0.1:6379> hkeys person
1) "username"
2) "age"
127.0.0.1:6379> hvals person
1) "tony"
2) "18"
2.3.8	hlen
元素个数
127.0.0.1:6379> hlen user
(integer) 1
127.0.0.1:6379> hlen person
(integer) 2
127.0.0.1:6379>

小节面试分析

List类型应用实践

Redis的list类型相当于java中的LinkedList,其原理就就是一个双向链表。支持正向、反向查找和遍历等操作,插入删除速度比较快。经常用于实现热销榜,最新评论等的设计。

Redis Lists

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边),lpush 命令插入一个新的元素到头部,而 rpush 命令插入一个新元素到尾部。当这两个操作中的任一操作在一个空的 Key 上执行时就会创建一个新的列表。相似的,如果一个列表操作清空一个列表,那么对应的 key 将被从 key 空间删除。

push 一类的命令的返回值为 list 的长度。这里有一些类表操作和结果的例子:

> rpush mylist A
> rpush mylist B
> lpush mylist first
> lrange mylist 0 -1

操作截图:

image-1654753577166

注意:lrange 需要两个索引,0 表示 list 开头第一个,-1 表示 list 的倒数第一个,即最后一个。-2 则是 list 的倒数第二个,以此类推。

这些命令都是可变的命令,也就是说你可以一次加入多个元素放入 list:

> rpush mylist 1 2 3 4 5 "foo bar"
> lrange mylist 0 -1

操作截图:

image-1654753585682

在 Redis 的命令操作中,还有一类重要的操作 pop,它可以弹出一个元素,简单的理解就是获取并删除第一个元素,和 push 类似的是它也支持双边的操作,可以从右边弹出一个元素也可以从左边弹出一个元素,对应的指令为 rpoplpop

> del mylist
> rpush mylist a b c
> rpop mylist
> lrange mylist 0 -1
> lpop mylist
> lrange mylist 0 -1

操作截图:

image-1654753595745

一个列表最多可以包含 4294967295(2 的 32 次方减一)个元素,这意味着它可以容纳海量的信息,最终瓶颈一般都取决于服务器内存大小。

事实上,在高级的企业架构当中,会把缓存服务器分离开来,因为数据库服务器和缓存服务器的特点各异,比如对于数据库服务器应该用更快、更大的硬盘,而缓存专用服务器则偏向内存性能,一般都是 64GB 起步。

List 阻塞操作

理解阻塞操作对一些请求操作有很大的帮助,关于阻塞操作的作用,这里举一个例子。

假如你要去楼下买一个汉堡,一个汉堡需要花一定的时间才能做出来,非阻塞式的做法是去付完钱走人,过一段时间来看一下汉堡是否做好了,没好就先离开,过一会儿再来,而且要知道可能不止你一个人在买汉堡,在你离开的时候很可能别人会取走你的汉堡,这是很让人烦的事情。

阻塞式就不一样了,付完钱一直在那儿等着,不拿到汉堡不走人,并且后面来的人统统排队。

Redis 提供了阻塞式访问 brpopblpop 命令。用户可以在获取数据不存在时阻塞请求队列,如果在时限内获得数据则立即返回,如果超时还没有数据则返回 nil。

在终端执行:

brpop list 10
brpop mylist 10

image-1654753607877

List 常见应用场景

分析 List 应用场景需要结合它的特点,List 元素是线性有序的,很容易就可以联想到聊天记录,你一言我一语都有先后,因此 List 很适合用来存储聊天记录等顺序结构的数据。

lpush

在key对应list的头部添加字符串元素

redis 127.0.0.1:6379> lpush mylist "world"
(integer) 1
redis 127.0.0.1:6379> lpush mylist "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>

其中,Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推

rpush

在key对应list的尾部添加字符串元素

redis 127.0.0.1:6379> rpush mylist2 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist2 "world"
(integer) 2
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>

del

清空集合元素,例如

redis 127.0.0.1:6379> del mylist

linsert

在key对应list的特定位置之前或之后添加字符串元素

redis 127.0.0.1:6379> rpush mylist3 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist3 "world"
(integer) 2
redis 127.0.0.1:6379> linsert mylist3 before "world" "there"
(integer) 3
redis 127.0.0.1:6379> lrange mylist3 0 -1
1) "hello"
2) "there"
3) "world"
redis 127.0.0.1:6379>

lset

设置list中指定下标的元素值(一般用于修改操作)

redis 127.0.0.1:6379> rpush mylist4 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist4 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist4 "three"
(integer) 3
redis 127.0.0.1:6379> lset mylist4 0 "four"
OK
redis 127.0.0.1:6379> lset mylist4 -2 "five"
OK
redis 127.0.0.1:6379> lrange mylist4 0 -1
1) "four"
2) "five"
3) "three"
redis 127.0.0.1:6379>

lrem

从key对应list中删除count个和value相同的元素,count>0时,按从头到尾的顺序删除

redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist5 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist5 2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "foo"
2) "hello"
redis 127.0.0.1:6379>
count<0时,按从尾到头的顺序删除
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist6 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist6 -2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist6 0 -1
1) "hello"
2) "foo"
redis 127.0.0.1:6379>
count=0时,删除全部
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist7 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist7 0 "hello"
(integer) 3
redis 127.0.0.1:6379> lrange mylist7 0 -1
1) "foo"
redis 127.0.0.1:6379>

ltrim

保留指定key 的值范围内的数据

redis 127.0.0.1:6379> rpush mylist8 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist8 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist8 "three"
(integer) 3
redis 127.0.0.1:6379> rpush mylist8 "four"
(integer) 4
redis 127.0.0.1:6379> ltrim mylist8 1 -1
OK
redis 127.0.0.1:6379> lrange mylist8 0 -1
1) "two"
2) "three"
3) "four"
redis 127.0.0.1:6379>

lpop

从list的头部删除元素,并返回删除元素

redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> lpop mylist
"hello"
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "world"
redis 127.0.0.1:6379>

rpop

从list的尾部删除元素,并返回删除元素:

redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> rpop mylist2
"world"
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
redis 127.0.0.1:6379>

llen

返回key对应list的长度:

redis 127.0.0.1:6379> llen mylist5
(integer) 2
redis 127.0.0.1:6379>

lindex

返回名称为key的list中index位置的元素:

redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "three"
2) "foo"
redis 127.0.0.1:6379> lindex mylist5 0
"three"
redis 127.0.0.1:6379> lindex mylist5 1
"foo"
redis 127.0.0.1:6379>

rpoplpush

从第一个list的尾部移除元素并添加到第二个list的头部,最后返回被移除的元素值,整个操作是原子的.如果第一个list是空或者不存在返回nil:
rpoplpush lst1 lst1
rpoplpush lst1 lst2

小节面试分析

Set类型应用实践

Redis的Set类似Java中的HashSet,是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

Redis 无序集合

Redis 集合(Set)是一个无序的字符串集合。你可以以 O(1) 的时间复杂度(无论集合中有多少元素时间复杂度都是常量)完成添加、删除以及测试元素是否存在。

Redis 集合拥有令人满意的不允许包含相同成员的属性,多次添加相同的元素,最终在集合里只会有一个元素,这意味着它可以非常方便地对数据进行去重操作。一个 Redis 集合的非常有趣的事情是它支持一些服务端的命令从现有的集合出发去进行集合运算,因此你可以在非常短的时间内进行合并(unions),求交集(intersections),找出不同的元素(differences of sets)。

> sadd myset 1 2 3
> smembers myset

sadd 命令产生一个无序集合,返回集合的元素个数。smembers 用于查看集合。

操作截图:

image-1654753749641

sismember 用于查看集合是否存在,匹配项包括集合名和元素(用于查看该元素是否是集合的成员)。匹配成功返回 1,匹配失败返回 0。

> sismember myset 3
> sismember myset 30
> sismember mys 3

操作截图:

image-1654753756288

Redis 有序集合

Redis 有序集合与普通集合非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每一个成员都关联了一个权值,这个权值被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是权值可以是重复的。

使用有序集合你可以以非常快的速度 O(log(N)) 添加、删除和更新元素。因为元素是有序的,所以你也可以很快的根据权值(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。在有序集合中,你可以很快捷的访问一切你需要的东西:有序的元素,快速的存在性测试,快速访问集合的中间元素!简而言之使用有序集合你可以完成许多对性能有极端要求的任务,而那些任务使用其它类型的数据库真的是很难完成的。

zadd 与 sadd 类似,但是在元素之前多了一个参数,这个参数便是用于排序的。形成一个有序的集合。

> zadd hackers 1940 "Alan Kay"
> zadd hackers 1957 "Sophie Wilson"
> zadd hackers 1953 "Richard Stallman"
> zadd hackers 1949 "Anita Borg"
> zadd hackers 1965 "Yukihiro Matsumoto"
> zadd hackers 1914 "Hedy Lamarr"
> zadd hackers 1916 "Claude Shannon"
> zadd hackers 1969 "Linus Torvalds"
> zadd hackers 1912 "Alan Turing"

查看集合:zrange 是查看正序的集合,zrevrange 是查看反序的集合。0 表示集合第一个元素,-1 表示集合的倒数第一个元素。

> zrange hackers 0 -1
> zrevrange hackers 0 -1

操作截图:

image-1654753766503

使用 withscores 参数返回记录值。

> zrange hackers 0 -1 withscores

操作截图:

image-1654753773125

sadd

添加元素,重复元素添加失败,返回0

127.0.0.1:6379> sadd name tony
(integer) 1
127.0.0.1:6379> sadd name hellen
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 0

smembers

获取集合中成员,例如
127.0.0.1:6379> smembers name

  1. “hellen”
  2. “rose”
  3. “tony”

spop

移除并返回集合中的一个随机元素

127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> spop internet
"rabbitmq"
127.0.0.1:6379> spop internet
"nginx"
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"

scard

获取集合中的成员个数

127.0.0.1:6379> scard name
(integer) 3

smove

移动一个元素到另外一个集合

127.0.0.1:6379> sadd internet amoeba nginx redis
(integer) 3
127.0.0.1:6379> sadd bigdata hadopp spark rabbitmq
(integer) 3
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
3) "rabbitmq"
127.0.0.1:6379> smove bigdata internet rabbitmq
(integer) 1
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
127.0.0.1:6379>

sunion

实现集合的并集操作

127.0.0.1:6379> sunion internet bigdata
1) "redis"
2) "nginx"
3) "rabbitmq"
4) "amoeba"
5) "hadopp"
6) "spark"

小节面试分析

总结(Summary)

常见问题分析

Redis分布式缓存技术应用

1)一种性能优化策略?(从内存加载数据)
2)两种缓存(内存)应用套路?(本地缓存和分布式缓存)
3)Redis中的5种数据类型(string,hash,list,set,zset,…)
4)两套Java客户端API?(Jedis,RedisTemplate)
5)3种数据可靠性方案?(持久化,事务,主从架构,哨兵,集群)
6)结合菜单模块实现两种综合性缓存应用解决方案?(RedisTemplate+redis,Aop+redis)