本文将介绍Redis 基本安装操作,以及基本指令,以及基础数据类型的介绍。
安装部署您可以在Redis官网上下载最新的 Redis https://redis.io/
源码安装方式使用 tar xzf 用于解压下载好的Redis安装包。我们一般会通过ln -s的方式指定一个工作目录,有利于后续版本升级。接下来使用make进行安装的操作,(此命令需要依赖gcc环境,如果您的系统没有安装的话,需要先进行安装的操作)。安装完成后可以使用redis-cli -v 的指令查询所安装的Redis的版本。
配置, 启动,操作,停止接下来我们将介绍一下Redis安装成功之后的几个重要的可执行文件,您可以在当前的目录下的src中或者/usr/local/bin目录下找到它们。
| 可执行文件 | 作用 |
|---|---|
| redis-server | 启动 Redis 服务 |
| redis-cli | Redis 命令行客户端 |
| redis-benchmark | Redis 基准测试工具 |
| redis-check-aof | Redis AOF持久化文件检测和修复工具 |
| redis-check-dump | Redis RDB持久化文件检测和修复工具 |
| redis-sentinel | 启动 Redis Sentinel |
如上所见,就是几个重要的可执行文件的简单介绍,我们通常称它们为Redis shell。
启动接下来我们尝试启动一个Redis服务,您可以执行以下指令
redis-server
看到如下日志,证明您的Redis-server服务端已经启动成功了
4454:M 23 Dec 2021 09:06:58.756 * Ready to accept connections
现在您可以新开一个terminal窗口来使用它了
在新的窗口您可以使用如下指令连接到刚刚启动的服务
redis-cli
您可以看到已经成功连接到Redis服务
[root@d4d3d679abb8 shell]# redis-cli 127.0.0.1:6379>
这样重新开启新的窗口然后连接,是不是觉得有点麻烦呢,这是因为当前的Redis是默认前台启动的,我们可以使它后台启动,这样它就会在后台挂起,并且不影响您正常使用。首先我们先关闭刚刚打开的客户端,然后执行ctrl+c的命令,可以退出当前正在运行的Redis服务端。
我们可以通过如下几种方式修改Redis的启动方式,您可以使用上述的方式来测试它是否启动成功。
-
1, 我们可以在启动的时候配置启动参数,没有指定的话它会使用默认的配置
redis-server --daemonize yes
当然我们还可以指令其他的一些配置,我们在最后会进行介绍
-
2,(推荐)我们还可以修改配置文件,并且在启动的时候指定我们修改的配置文件启动。
现在修改 Redis 的启动方式为后台启动 将 daemonize no 修改为 daemonize yes然后执行启动的指令
redis-server /home/shell/redis/redis.conf
接下来我们简单的介绍一些常见`Redis`的配置项(`Redis`共有60多个配置 详情可见本章节的附件部分)
| 配置名 | 配置说明 |
|---|---|
| port | 端口 |
| logfile | 日志文件 |
| dir | Redis 工作目录(存放持久化文件和日志文件) |
| daemonize | 是否以守护进程的方式启动 |
以下是我们的配置文件所在的目录:
/home/shell/redis/redis.conf操作
接下来我们将介绍它的两种连接客户端的方式
-
1,交互式方式
通过 redis-cli -h { host } -p { port } 的方式连接到 Redis 服务,之后的所有操作都是通过交互式的方式实现,例如:(-h默认为 127.0.0.1 -p默认为 6379 所以刚刚的redis-cli是此命令的缩写)
redis-cli -h 127.0.0.1 -p 6379
-
2,命令方式
使用 redis-cli -h { host } -p { port } {command} 可以直接得到命令的返回结果
[root@86318cd7ea26 shell]# redis-cli -h 127.0.0.1 -p 6379 select 1OK
您可以通过如下指令停止redis-sever服务
redis-cli shutdown常用指令,基本特性介绍
本章节主要介绍一下Redis的一些全局命令,数据库的基本操作,key的基本操作,以及了解一下Redis的单线程命令处理机制,主要是为了后面内容的学习打下一个比较好的基础。主要体现在两个方面:
- 第一、Redis的命令上百个,如果纯靠死记硬背比较困难,但是如果理解Redis的一些机制,会发现这些命令有很强的通用性。
- 第二、Redis不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能会对Redis本身或者应用本身造成致命伤害。
为了您可以更好的学习Redis接下来的课程,我们先简单介绍一些通用指令。 如果您执行了关闭redis-server服务操作的话,您需要重新启动redis-server服务,并且使用redis-cli进入客户端才能继续以下的课程。
1,数据库操作
首先我们先了解一下Redis默认拥有16个数据库,默认使用的是第一个也就是0号数据库。各个数据库之间是相互独立的,互不影响,没有任何关联。Redis提供了几个面向Redis数据库的操作,它们分别是select,flushdb/flushall命令,我们依次来介绍一下
-
1,切换数据库
我们已经了解到 使用redis-cli进入Redis客户端后,默认所处的数据库为0号数据库,如果我们想切换所处的数据库的话可以使用如下命令(默认配置是拥有16个数据库,您如果不修改配置文件的话能切换的选择范围是(0-15))
select 1
我们可以通过查看当前的输入行前缀查看当前当前所处的数据库,当不位于0号数据库之下时,会在前面[ ]里边显示数据库的编号。
目前Redis已经逐渐弱化这个功能,例如Redis的分布式实现RedisCluster只允许使用0号数据库, 只不过为了向下兼容老版本的数据库功能,该功能没有完全废弃掉, 下面分析一下为什么要废弃掉这个“优秀”的功能呢? 总结起来有三点
-
Redis是单线程的,如果使用多个数据库的话,这些数据库仍然是使用的同一个cpu彼此间还是会受到影响的
-
多数据库的使用方式, 会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在, 依然会影响其他数据库, 这样会使得别的业务方定位问题非常的困难 。
-
部分Redis的客户端根本就不支持这种方式。 即使支持, 在开发的时候来回切换数字形式的数据库, 很容易弄乱。
笔者建议如果要使用多个数据库功能, 完全可以在一台机器上部署多个Redis实例, 彼此用端口来做区分, 因为现代计算机或者服务器通常是有多个CPU的。 这样既保证了业务之间不会受到影响, 又合理地使用了CPU资源。
-
-
2,flushdb/flushall
flushdb/flushall命令用于清除数据库,两者的区别的是flushdb只清除当前数据库,flushall会清除所有的数据库。
*建议大家使用这两个命令的时候一定要谨慎,慎重。
2,键操作
Redis有5种基本数据结构分别是String,Hash,List,Set,Zset, 它们是键值对中的值, 对于键来说有一些通用的命令
-
1,查看所有键
我们可以通过key*来查询当前数据库下的所有键值
*注意生产环境禁止使用此命令,此命令会检索所有的 key 如果 Redis 存储的量过多的话会造成 Redis 的阻塞,在实验中我们会对Redis的遍历键做简单的分析说明
-
键总数
我们可以通过dbsize来查询当前数据库下的键总数
*dbsize 指令在计算键总数的时候不会遍历所有键,而是直接获取 Redis 内置的键总数变量,所以 dbsize 的时间复杂度是 0(1) 。
-
3,检查键是否存在
我们可以用exists来检查指定键是否存在,如果键存在则返回 1 否则返回 0
-
4,删除键
我们可以通过del来删除指定键值,操作结果为删除键的个数,支持删除多个。
-
5,键过期
Redis支持对指定键添加过期事件,当超过指定时间后,会自动删除如下(单位时间为秒)
expire key3 100
此方法会返回受响应的键的数量,我们可以通过ttl命令来查询当前键的剩余过期时间,如下
ttl key3
返回字段为剩余时间,它有两种特殊的返回
- 如果返回-1,证明当前查询的键没有设置过期事件
- 如果返回-2,则证明当前的键不存在
-
6 ,值的数据结构类型
我们可以通过type命令来查询指定键的对应值的数据类型如:
type key3
我们将得到返回值的基本数据类型
如上type指令返回的就是当前值的基本数据类型。基本数据类型分别是 string (字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但这些仅仅是 Redis 对外的数据结构,如下图。
如图所示,这样设计的好处
-
1.可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。
-
2.多重内部编码实现可以在不同场景下发挥各自的优势,例如 zipList 比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为 linkedList 。
Redis 使用了单线程架构和I/O多路复用模型,来实现高性能的内存数据库服务。本节简单介绍一下Redis的单线程命令处理机制,接着简单分析一下Redis单线程模型为什么性能如果之高,最终您会对Redis的单线程模型有初步的了解。
-
一.单线程处理机制
Redis是单线程来处理命令的,所以一条命令从客户端到达服务端,会进入一个队列中,然后逐个执行,不会存在两条指令同时执行的情况,如图:
-
二.为什么单线程还能这么快。
-
1,纯内存访问,Redis 将所有数据都放在内存中,内存的响应时长大约为100纳秒 这是 Redis 达到每秒百万级的重要基础
-
2,非阻塞 IO ,Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 Redis 本身的时间处理模型将 epoll 中的连接,读写,关闭都转换为事件,不在网络 I/O 上浪费过多的时间
-
3,单线程避免了线程切换和竞态产生的消耗。
*单线程可以简化数据结构和算法的实现,其次避免了线程切换和竞态产生的消耗。但是单线程对于每个命令的执行时间是有要求的,如果某个指令执行时间过长的话,会造成其他命令的阻塞,对于Redis这种高性能的服务是致命的,所以 Redis 是面向快速执行场景的数据库。
-
首先介绍的是Sring类型,首先键都是字符串类型的,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串能为其他四种数据结构的学习奠定基础。 如图所示,字符串类型的值实际可以是字符串(简单的字符串,复杂的字符串(例如 JSON ,XML ),数字(整数,浮点数),甚至二进制(图片,音频,视屏))但是值最大不能超过 512MB 。
常用命令我们这里只介绍一些详细的命令,在实验中提供了String类型所有的命令,以及示例。
-
1,单个设置值,获取值
Redis可以通过set命令来创建一个String类型的键值对:
set key value
我们可以通过get命令来获取我们保存的数据:
get key
我们也可以为我们的数据设置过期事件,我们只需要在set命令后添加EX[过期时间];
set key value ex 5
这里的时间单位是秒,我们可以等待5秒之后再去查询,就会发现get key的结果为(nil)这个意思是不存在的意思。
当然我们也可以使用毫秒为单位来设置过期时间,如果我们希望使用毫秒作为过期的时间,我们需要使用px:
set key value px 5000
有些情况下,我们希望设置一个具体的时间,比如说2021-12-31 23:59:59, 很可惜我们不能直接使用这个时间,我们需要用所谓的时间戳。时间戳是计算机用来表达时间的一种方式,它是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。2021-12-31 23:59:59用时间戳表示就是:1640966399:
set key value EXAT 1640966399
这样,我们键为key的这个数据会在2021-12-31 23:59:59到了后过期。
同样,我们也可以设置毫秒级别的到期时间,这样会更加准确一些,毫秒级别的到期时间用的是PXAT:
set key value PXAT 1640966399000
有时候为了避免覆盖已经存在的数据,我们可以在设置数据的时候,增加一个NX的标志,NX也就是NOT EXISTS的意思。这时候如果我们准备保存的键值对在Redis数据库中已经存在,它就会报错
set key value NX
与NX相反的是,还有一个标志是XX,它的作用是要求这个键必须存在:
set key-not-exist value XX
(nil)的意思就是没有成功的意思。
相反,我们重新设置一下键为key的值:
set key new-value XX
当然,Redis也为我们提供了语法糖,比如set key value NX 等价于setnx key value,set key value XX等价于setxx key value。
-
2,批量设置值,获取值
Redis提供了mset来批量创建多个键值对:
mset demo1 dsds demo2 dededed demo3 ededede
它会返回你创建成功的个数。同时 Redis也提供了mget用于批量获取值,我们可以用来获取获取我们刚刚存储的所有值:
mget demo1 demo2 demo3
批量操作可以有效的提高开发效率,可以替换n次的get命令的操作,下述我们简单做了两种命令的时间消耗对比
操作 时间 1000次get 1000 * 1+1000 * 0.1=1000毫秒=1.1秒 1次mget(组装1000和键值对) 1*1+1000 * 0.1=101毫秒=0.101秒 *需要的注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成 Redis 阻塞或者网络阻塞。
-
3,计数
我们在开头的时候了解到String类型可以存储整数或者浮点数,Redis对于值的类型为整数或者浮点数的键值对,提供了计数的相关命令,可以对值进行递增,递减的操作。
给大家介绍的是incr命令,它可以对值进行递增的操作。
incr key
如果我们递增了一个不存在的键,它会先创建一个键值对值为0,随后对这个键值对进行递增的操作,返回递增后的结果。
接下来我们来了解一下字符串类型的内部编码。
- 1,int:8个字节的长整型
- 2,embstr:小于等于39个字节的字符串
- 3, raw:大于39个字节的字符串
Redis会根据你存储的值的字符串长度,选取合适的内部编码实现。这里我们只做一下基础的了解。
常见使用场景-
1.缓存功能
Redis 具有支撑高并发的特性,所以一些热点信息使用缓存,可以起到加速读写和降低后端压力的作用
例如,存储用户信息,商品信息等一些热点信息。一般采用 Json 格式存储
-
2.计数
很多应用会使用 Redis 作为计数的基本工具,它可以实现快速计数,查询缓存的功能。同时数据可以异步落地到其他持久化的数据源。
例如:电商系统中,商品的浏览量,购买量等都是以天为单位 在 Redis 中计数,然后使用定时任务同步到 ES 分布式搜索引擎 或者 Mysql 中持久化保存。
-
3.共享Session
分布式 Web 服务为了 实现单点登录 可以采用 分布式 Session共享的方式实现。其原理就是将用户的 Session 进行集中管理,而不是分布在各个服务端 进行管理如图:
-
4.限速,降频
Redis 可以给指定 key 设置过期时间,通过检查指定 key 的存在情况,可以实现一下定时的业务场景。
例如:常见的 app 进行短信登录的时候会对多次短信发送,做时间间隔的操作,常见的实现方式,发送短信之前,在 Redis 中查看上次发送的短信是否过期。
这里只介绍一些常用的命令,在实验中提供了Hash类型所有的命令,以及示例。
-
1,单个设置值,获取值
Redis可以通过hset命令来创建一个Hash类型的键值对,它可以一次设置多个field:
hset hash1 field1 val1 field2 val2
返回的是您新建成功的个数,我们可以通过hget来获取具体的field的值
hget hash1 field1
当前如果您想获取到指定key下边所有的filed和value的话,您也可以使用如下的命令
hgetall hash1
-
2,删除操作
我们在开始的基础设置以及了解到del可以删除指定的key,Redis针对Hash类型来提供了删除指定field的命令:
hdel hash1 field1
您可以使用这个命令删除指定的field,当然也支持同时删除多个
hdel hash1 field1 field2
-
3,计算field的个数
Redis也提供了指定key计算field个数的方法
hlen hash1
返回的为field的个数。
-
4,批量设置,获取field-value
同样Redis也提供了Hash类型批量设置和获取的方法hmset和hmget,我们来简单使用一下
hmset hash1 field1 val1 field2 val2
大家可能会发现这个方法使用和hset方法一致。这个方法在Redis 4.0.0的时候弃用了,如果您的Redis版本低于4.0.0还是需要使用hmset来存储多个field-value的,如下是官方文档介绍
As per Redis 4.0.0, HMSET is considered deprecated. Please use HSET in new code. 根据Redis 4.0.0,HMSET被视为已弃用。请在新代码中使用HSET。
获取多个field-value目前还需要使用hmget如下
hmget hash1 field1 field2
接下来我们来了解一下Hash类型的内部编码。
-
1,zipList (压缩列表)
当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认512个)、同时所有值小于 hash-max-ziplist-value 配置(默认64字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hshtable 更加优秀
-
2,hashtable(哈希表)
当哈希类型无法满足 ziplist 的所有条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为0(1)
-
1,用于缓存
Redis 的 hash 类型的(key,field,value)的结构与对象的(对象id,属性,值)的结构相似,可以用来存储热点的对象数据。与 string 类型存储的区别如下
String+json hash 效率 很高 高 容量 低 低 灵活性 低 高 序列化 简单 复杂 当对象的某个属性需要频繁修改时,不合适用 string + json ,因为它不够灵活,每次修改都需要重新将整个对象序列化并赋值,如果使用 hash 类型则可以针对某个属性单独修改。另外使用 string + json 格式存储查询检索方式单一,只能通过 key 的方式查询,如果使用 hash 类型存储则可以使用 key +field 的方式进行检索。
综上: 一般对象使用 string + json 存储,对象中如果有某些频繁变化的属性建议使用 hash 存储
List类型,列表(List) 类型是用来存储多个有序的字符串, 如图所示, a、b、 c、 d、 e五个元素从左到右组成了一个有序的列表, 列表中的每个字符串称为元素(element) , 一个列表最多可以存储232-1个元素。 在Redis中, 可以对列表两端插入(push) 和弹出(pop) , 还可以获取指定范围的元素列表、 获取指定索引下标的元素等。 列表是一种比较灵活的数据结构, 它可以充当栈和队列的角色, 在实际开发上有很多应用场景。
列表类型有两个特点:
- 列表中的元素是有序的;
- 列表中的元素是可以重复的;
我们先按照对 列表的5种操作类型 ,对所有的api进行了一个分类,然后按照分类进行介绍
| 操作类型 | 操作 |
|---|---|
| 增加 | rpush、lpush、linsert |
| 查 | lrange 、lindex 、llen |
| 删除 | lpop 、rpop 、lrem 、ltrim |
| 修改 | lset |
| 阻塞操作 | blpop 、brpop |
-
1,添加操作
首先我们了解到,Redis中的List是一个有序的列表,我们可以在列表的右侧,左侧,或者某个元素的位置上进行插入,相对的Redis给我们提供了以上三个场景的命令,分别是rpush、lpush、linsert三个命令,我们依次来演示一下
-
1,rpush从右侧新增元素(支持单个或者多个),该命令会返回当前所有元素的个数,此时新建的元素在队列的末尾:
rpush list1 value1 value2 value3
您可以通过lrange list1 0 -1指令来查询列表:
lrange list1 0 -1
可以看到新插入的数据在列表的末尾。
-
2,lpush从左侧新增元素(支持单个或者多个),该命令会返回当前所有元素的个数,此时新增的元素在队列的开头位置:
lpush list1 left1 left2
我们再来查询一下列表:
lrange list1 0 -1
可以发现新增的元素按照插入时间的倒叙在列表的开头位置。
-
3,linsert是在指定元素的前或者后插入元素,该命令会返回当前所有元素的个数。首先我们先在上述list1的value1前方插入一个元素linsert1
linsert list1 before value1 linsert1
再次查询列表:
lrange list1 0 -1
可以看到 linsert1确实位于位于value1的上方
我们再向上述list1的value1后方插入一个元素linsert1
linsert list1 after value1 linsert1
再查询一下:
lrange list1 0 -1
如您所见value1的下方也出现了一个linsert1,这也只能的List中的元素是可以重复的
-
-
2,查找
Redis提供了多种检索List列表的方法,同上述我们使用到的lrange检索指定范围内的元素列表,还提供了获取指定索引元素的方法lindex,以及查询List列表长度的方法llen,下述我们依次介绍一下
-
1,lrange获取指定范围内的元素列表
这个方法我们在上边已经使用过了,我们来解析一下刚刚使用的命令lrange list1 0 -1其中list1是查询的指定键,后面跟的两个参数分别是开始的索引start,和结束的索引end。Redis中的索引下标有两个特:第一, 索引下标从左到右分别是0到N-1, 但是从右到左分别是-1到-N;第二, lrange中的end选项包含了自身, 这个和很多编程语言不包含end不太相同。 例如想获取列表的第2到第4个元素, 可以执行如下操作:
lrange list1 1 3
-
lindex可以获取指定索引的元素
这个方法简单,就不赘述了,我们来一个示例
lindex list1 1
-
llen这个方法可以获取列表长度
这个方法同我们前面课程介绍的dbsize一样获取的系统参数,并不是检索的内容,时间复杂度为0(1)
llen list1
-
-
3,删除
同样的Redis也提供了多重删除List中元素的方法分别是:lpop从左侧弹出,rpop从右侧弹出,lrem删除指定元素,ltrim删除指定范围内的元素。老规矩我们还是依次来介绍一下:
-
1,lpop从列表的左侧弹出元素,可以指定弹出元素的个数,此结果会返回删除的元素值
先查询当前的列表
lrange list1 0 -1
执行指令
lpop list1 1
再次查询列表:
lrange list1 0 -1
-
2,rpop从列表的右侧弹出元素,同样可以指定弹出元素的个数
rpop list1 1
查询列表:
lrange list1 0 -1
可以发现在列表的末尾部分,少了一个
-
lrem删除指定元素
lrem可以删除指定的元素,它的语法是这样的lrem key count value 这个命令会从列表中找到等于value的元素进行删除, 根据count的不同分为三种情况:
- count>0, 从左到右, 删除最多count个元素。
- count<0, 从右到左, 删除最多count绝对值个元素
- count=0, 删除所有。
例如:新建一个列表中从左向右插入5个a和两个b。
lpush list2 a a a a a b b
通过查询可以得知那么当前列表变为“ b b a a a a a”:
lrange list2 0 -1
下面操作将从列表左边开始删除4个为a的元素:
lrem list2 4 a
随后我们再次查询列表,看看是否和我们猜想的一样变成了"b b a"
lrange list2 0 -1
-
修改操作
Redis提供了修改指定索引下标元素的方法
lset list1 0 newval
-
阻塞操作
blpop key [key ...] timeout brpop key [key ...] timeout
阻塞式弹出有两个命令`blpop`和`brpop`这个两个方法是`lpop`和`rpop`的阻塞版本,它们除了弹出方向不同之后,使用方法基本相同,下面以`blpop`为例子 首先解释一下上述语法:这个方法就是当给定的列表内没有任何元素可供弹出的时候,连接将被 `BLPOP `命令阻塞,直到等待超时或发现可弹出元素为止。
- key [key …] 多个列表的键
- timeout 阻塞时间(单位秒)
我们来一个简单的例子实践一下
首先有两种情况(列表为空和列表不为空的情况)
我们先创建一个列表
lpush list3 val3
可以查看一下当前list3中有一个元素val3
lrange list3 0 -1
我们执行一下BLPOP命令让他阻塞10秒
blpop list3 10
我们可以发现当列表不为空的时候,它会立即返回结果,并没有进入阻塞等待,返回值为,指定的key以及弹出的值。我现在再来看一下list3的情况
lrange list3 0 -1
可以发现当前的list3是空的,这时候我们再执行BLPOP命令:
blpop list3 10
可以看到它在阻塞了10秒之后返回了空,在这期间如果我们插入了元素,那么它会停止阻塞,立即返回,我们来试一下。
首先我们还是使它处理阻塞等待状态,这次时间稍微长点设置30秒
blpop list3 30
首先打开新的 terminal窗口点击我打开
使用指令给list3新建一个元素
redis-cli lpush list3 new
随后切换为成我们原先的 terminal窗口可以看到,命令并没有阻塞等待30秒自动返回,而是立即返回了我们刚刚新增的new元素
-
接下来我们来了解一下List类型的内部编码。
-
ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认512个),同时列表中每个元素都小于 list-max-ziplist-value 配置时(默认64字节),redis 会选取 ziplist 来作为列表的内部实现来减少内存的使用。
-
linkedlist (链表):当列表类型无法满足 ziplist 的条件是,Redis 会使用 linkedlist 作为列表的内部实现。
Redis 3.2 版本提供了 quicklist 内部编码,简单地说它是一个 ziplist 为节点的 linkedlist,它结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了一种更加优秀的内部编码实现,它的设置原理可以参考 Redis 的另一个作者 Matt Stancliff的博客: https://matt.sh/redis-quicklist
-
消息队列
Redis 的 lpush + brpop 命令组合即可实现阻塞队列,生产者客户端使用 lrpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
-
文章列表
对于热点数据的文章列表可以考虑使用 Redis 的List类型存储,因为列表不但是有序的,同时支持按照索引范围获取元素。可能存在问题如下
- 如果每次获取的文章个数较多,需要执行多次 hgetall 操作 ,此时可以考虑使用 pipeline (管道)批量获取
- lrange 命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表作二级拆分,或者使用 Redis3.2 的 quicklist 内部编码实现,它结合 ziplist 和 linkedlist 的特点,获取列表中间范围的元素时也可以高效完成。
实际上列表的使用场景很多,下述是指令组合能达到的 类似数据类型特点
-
lpush + lpop =Stack (栈)
-
lpush + rpop= Queue (队列)
-
lpush + ltrim =Capped Collection (有限集合)
-
lpush + brpop = Message Queue(消息队列)
Set类型,集合(Set) 类型也是用来保存多个的字符串元素,但是和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,也不是通过索引下标获取元素。如图所示 集合user: 1: follow包含着"it"、 "music"、 "his"、 "sports"四个元素, 一个集合最多可以存储232-1个元素。 Redis除了支持集合内的增删改查, 同时还支持多个集合取交集、 并集、 差集, 合理地使用好集合类型, 能在实际开发中解决很多实际问题
常用命令下面将按照集合内和集合之间两个维度对集合的常用命令进行介绍
-
一,集合内操作
-
1,单个添加,查询元素
Redis提供了sadd用户添加Set类型的元素
sadd set1 demo1
您可以使用smembers命令来返回当前Set中的所有元素,返回的元素是无序的。
smembers set1
*类似smembers和List中的Lrange,Hash中的hgetall都是比较重的命令,如果元素过多的话存在阻塞Redis的可能性,这时候可以使用scan来完成,在实验的附件中介绍,可以参考。
-
2,计算元素的个数
使用scard可以计算当前集合中的元素的个数
scard set1
scard的时间复杂度为0(1),它不会遍历集合所有元素,而是直接使用Redis内部的变量
-
3,判断元素是否在集合中
Redis提供的键是否存在的函数为exists,同时提供了sismemeber用于判断集合中是否有指定的元素
sismember set1 demo1
如果给定元素 element 在集合内返回1,反之返回0
-
4,随机从集合中返回指定个数的元素
Redis提供srandmember从集合中随机返回指定个数的元素
srandmember set1 1
-
5,从集合中随机弹出元素
Redis提供spop从集合中随机弹出指定个数的元素(注意这里边是弹出就是删除)(最后的个数可以选择不填默认是1个)
spop set1 1
-
6,删除元素
Redis提供的删除键的方法是del,Redis提供了srem用于删除集合中的指定元素(我们先添加一个)
sadd set1 demo1 demo2
srem set1 demo1
此方法会返回删除成功的个数
-
-
二,集合间的操作
Redis提供了多个集合间求交集,并集,差集,以及将这些结果保存的命令,下面我们依次介绍一下
-
1,求多个集合的交集sinter
首先我们先新建两个集合
sadd set4 a h b hb
sadd set5 b e berb rer ebrer
然后我们可以使用sinter来得到两个集合的交集
sinter set4 set5
-
2,求多个集合的并集
sunion可以得到多个集合的并集
sunion set4 set5
-
3,求多个集合的差集
sdiff可以得到多个集合的差集,
sdiff set4 set5
-
4,将交集、并集、差集的结果保存
sinterstore destination key [key ...] sunionstore destination key [key ...] sdiffstore destination key [key ...]
集合间的运算在元素较多的情况下比较耗时,所以 Redis 提供了上面三个命令,(原命令+ store)将集合间交集,并集,差集的结果保存在 destination key 中。
我们先执行sunionstore方法将结果输出到myset1中
sunionstore myset1 set5 set4
我们查询一样结果是否输出到指定键中
smembers myset1
这里仅仅列举了一个的使用方法,其他两者使用方法一致。
-
接下来我们来了解一下Set类型的内部编码。
集合的内部编码有两种:
- intset ( 整数结合 ) :当集合中的元素都是整数且元素个数小于 set-max-intset-entries 配置(默认512个)时,Redis 会选用intset 来作为集合的内部实现,从而减少内存的使用。
- hashtable( 哈希表):当集合类型无法满足 intset 条件时, Redis 会使用 hashtable 作为集合的内部实现
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、 体育比较感兴趣, 另一个用户可能对历史、 新闻比较感兴趣, 这些兴趣点就是标签。 有了这些数据就可以得到喜欢同一个标签的人, 以及用户的共同喜好的标签, 这些数据对于用户体验以及增强用户黏度比较重要。 例如一个电子商务的网站会对不同标签的用户做不同类型的推荐, 比如对数码产品比较感兴趣的人, 在各个页面或者通过邮件的形式给他们推荐最新的数码产品, 通常会为网站带来更多的利益 。
下面使用集合类型实现标签功能的若干功能示例。大家可以尝试一下。
-
给用户添加标签
sadd user1:tages tag1 tag3
sadd user2:tages tag3 tag3
-
给标签添加用户
sadd tag1 user1 user2 user3
-
删除用户下的标签
srem user1:tages tag1
-
删除标签下的用户
srem tag1 user2 user3
-
计算用户共同感兴趣的标签
sinter user1:tages user2:tages
上述只是给出了使用 Redis 结合类型实现标签的基本思路,实际上会复杂很多,不过集合类型的应用场景通常为以下几种:
-
sadd = Tagging (标签)
-
spop/srandmember = Random item (生成随机数,比如抽奖)
-
sadd + sinter = Social Graph (社交需求)
-
本章节我们简单介绍一下`Zset`(有序集合)类型,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。下图给出了列表、集合、有序集合三者的异同点。
| 数据结构 | 是否允许重复元素 | 是否有序 | 有序实现方式 | 应用场景 |
|---|---|---|---|---|
| 列表 | 是 | 是 | 索引下标 | 时间轴、消息对列表等 |
| 集合 | 否 | 否 | 无 | 标签、社交等 |
| 有序结合 | 否 | 是 | 分值 | 排行榜系统、社交等 |
本节依旧按照集合内和集合外两个维度对有序集合的命令进行介绍。
-
一,集合内操作
-
1,设置元素
Redis提供了zadd的方法用于添加Zset类型的键值对。我们先介绍一下它的基本语法
zadd key score member [sore memeber ...]
score代表元素的分值,member列表存储的元素,它支持一次性设置多个元素,返回结果为成功添加成员的个数
下面的操作将向有序集合zset:user中添加用户tom和他的分数251 ,以及他的小伙伴tom1和tom2他两的分数分别为262和263
zadd zset:user1 200 tom 201 tom1 253 tom2
有关add命令有两点需要注意的:
-
Redis后续版本对zadd方法添加了nx,xx,ch,incr,gt,lt;六个可选项我们来简单了解一下
-
1)首先nx与xx这两个选项我们在string的章节中已经介绍过了,nx表示目标的元素必须不存在才可以设置成功,而xx表示只有目标元素存在才可以设置成功,用于元素的更新。
我们简要的使用下述指令测试其中nx使用
我们先存储一个已经存在的
zadd zset:user1 nx 261 tom
可以看到没有新增成功返回结果为0 接下来我们新增一个不存在的键
zadd zset:user1 nx 264 tom4
-
2)其次incr在之前的章节中了解到用于自增的命令,这里是用于对 score做增加并且返回增加成功的值,增加的数量我们可以自己指定,下述指令中为1。我们简单使用一下
zadd zset:user1 incr 1 tom
Redis也提供了相应的语法糖zincrby key score value member 下面是简单的示例
zincrby zset:user1 1 tom
通过返回值可以观察到在上次增加后的结果上又增加了1。我们也可以使用zrange的命令查询指定key所有元素,这个指令我们在接下来会详细介绍
zrange zset:user1 0 -1 withscores
我们可以观察到tom的分值确实是263
-
3)接下来我们了解一下ch选项,这个选项会返回成功修改的个数,请注意原始命令返回的是成功新增的元素个数,我们了解到zset是有序集合,集合中的元素是唯一的,如果添加了相同的元素,但是分值不同会进行替换的操作,这时新增的个数为0,我们可以通过如下来进行测试
首先设置一个值
zadd zset:user2 123 tom
通过ch设置和刚刚相同的值
zadd zset:user2 ch 124 tom
我们再使用普通的方式重新设置一下值
zadd zset:user2 125 tom
我们可以发现,使用ch设置值会返回覆盖操作的元素个数,而普通方式不会。
-
4)gt以及lt这两个选项代表的含义是:如果元素存储在的话,修改的分数必须大于或者小于原先的分数才能修改成功。其中gt表示大于,lt表示小于。请注意这两个选项,与[nx/xx]不可以共用。同样我们来进行简单的测试
首先设置一个值
zadd zset:user3 166 tom
然后我们使用gt方式来修改刚刚新建的值为165
zadd zset:user3 gt ch 165 tom
最后我们使用zrange来查询集合的所有元素
zrange zset:user3 0 -1 withscores
最后我们可以发现tom的分数还是166 可以确定由于修改的分值小于原先的分值,所以修改并没有生效。
-
-
2,计算成员个数
我们可以通过zcard来查询指定键的元素的个数,当然您值的类型必须是zset类型,否则查询会报错。和集合类型的scard命令一样,zcard的时间复杂度为0(1)
例如下面的操作我们将返回有序集合zset:user1的成员数。
zcard zset:user1
-
3,计算某个成员的分数
您可以通过zscore命令来查询指定成员的分数。
例如 zset:user3中tom的分数为166,:
zscore zset:user3 tom
我们再查询zset:user3中不存在的用户,会返回(nil)空
zscore zset:user3 test
-
4,计算成员的排名
Redis提供了两个查询成员排名的命令:分别是zrank(将分数从低到高返回排名)和zrevrank(将分数从高到低返回排名)。
例如下边的操作,查询tom在zset:user1中的两种排名
从低到高查询排名 请注意排名是从0开始计数的,通过存在分值一样的会根据元素的字典值来进行排序,具体的可见附件部分。
zrank zset:user1 tom1
从高到低查询排名
zrevrank zset:user1 tom1
-
5,删除成员
Redis提供了zrem用于删除指定成员(支持删除多个)
以下操作会将tom从zset:user1中删除,返回的为成功删除的个数
zrem zset:user1 tom
-
6,返回指定排名范围的成员
有序集合是按照分值排名的,`zrange`是从低到高返回,`zrevrange`反之。它的索引排序依旧是`[0,-1]`。如果加上`withscores`选项,同时返回成员的分数。
例如下面示例返回zset:user1中排名最低的三个成员
zrange zset:user1 0 2 withscores
-
7,返回指定分数范围的成员或个数
同时Redis还提供了查询指定分数范围的方法zrangebyscore 以及zrevrangebyscore 其中前者是按照分数从低到高排序,后者反之。同样提供了winthscores选项用于返回成员的分数。
同样我们先查询一下zset:user1 中分数范围是500 -600 的元素名称,发现返回空
zrangebyscore zset:user1 500 700
我们来查询和260-262中的元素。返回为此区间的元素个数
zrangebyscore zset:user1 200 300
-
8,我们还可以使用zcount查询指定分数范围内的成员个数。
我们使用它的查询260-265中的元素个数
zcount zset:user1 200 300
-
-
-
二,集合间的操作
-
首先插入两个有序集合在 Redis 中如图所示
zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
zadd user:ranking:2 8 james 77 mike 625 martin 888 tom
-
1,交集
首先介绍一下语法
zinterstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]
这个命令的参数比较多,下面我们分别介绍说明一下:
-
destination: 交集计算结果保存到这个键 。
-
numkeys: 需要做交集计算键的个数 。
-
key[key...]: 需要做交集计算的键。
-
weights weight[weight...]: 每个键的权重, 在做交集计算时, 每个键中的每个member会将自己分数乘以这个权重, 每个键的权重默认是1。
-
aggregate sum|min|max: 计算成员交集后, 分值可以按照sum( 和) 、min( 最小值) 、 max( 最大值) 做汇总, 默认值是sum。
下面我们列举几个场景来对这些参数加深一下了解
-
1)下面操作对user: ranking: 1和user: ranking: 2做交集,
zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
我们可以发现返回结果为3 这是因为weights和aggregate使用了默认配置,对目标user: ranking: 1_inter_2对分值做了sum操作
我们来通过zrange命令来查询是否输出到指定键
zrange user:ranking:1_inter_2 0 -1 withscores
-
2)如果想让user: ranking: 2的权重变为0.5, 并且聚合效果使用max, 可以执行如下操作:
zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2 weights 1 0.5
同样我们查询一下是否输出到指定键
zrange user:ranking:1_inter_2 0 -1 withscores
-
-
2,并集
我们还是先来了解一下语法
zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]
该命令的所有参数和zinterstore是一致的, 只不过是做并集计算, 例如下面操作是计算user: ranking: 1和user: ranking: 2的并集, weights和aggregate使用了默认配置, 可以看到目标键user: ranking: 1_union_2对分值做了sum操作:
zunionstore user:ranking:1_union_2 2 user:ranking:1 user:ranking:2
我们可以发现返回结果为7 这是因为weights和aggregate使用了默认配置,对目标user: ranking: 1_inter_2对分值做了sum操作
我们来通过zrange命令来查询是否输出到指定键
zrange user:ranking:1_union_2 0 -1 withscores
-
接下来我们来了解一下Zset类型的内部编码。
有序结合类型的内部编码有两种
- ziplist (压缩列表):当有序集合的元素个数小于 zet-max-ziplist-entries 配置(默认128个),同时每个元素的值都小于 zet-max-ziplist-value 配置 (默认64字节)时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用
- skiplist (跳跃表) : 当 ziplist 条件不满足时,有序列表会使用 skiplist 作为内部实现,因为此时 ziplist 的读写效率会下降。
有序结合比较典型的应用场景就是排行榜系统,例如视屏网站需要对用户上传的视屏做排行榜,榜单的维度可能是多个方面的: 按照时间、按照播放数量、按照获得的赞数、本节使用赞数这个维度,记录每天用户是上传视屏的排行榜。主要需要实现以下4个功能。
-
1,添加用户赞数
用户 mike 上传一个视屏,并获得了3个赞,可以使用有序集合的 zadd 和 zincrby 功能:
zadd user:ranking:2021_12_20 3 mike
如果之后再获得了一个赞,可以使用 zincrby
zincrby user:ranking:2021_12_20 1 mike
2,取消用户赞数
由于各种原因(例如用户注销,用户作弊)需要将用户删除,可以使用 zrem
zrem user:ranking:2021_12_20 mike
-
3,展示获取赞数最多的十个用户
此功能使用 zrevange 命令实现:
zervangebyrank user:ranking:2021_12_20 0 9
4,展示用户信息以及用户分数
此功能将用户名作为键的后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用 zscore 和 zrank 两个功能
hgetall user:info:tom zscore user:ranking:2021-12_20 mike zrank user:ranking:2021_12_20 mike



