初识redis
- Ubuntu下安装redis
- 什么是redis?
- redis的一些特性
- 为什么redis快?
- redis的应用场景
- redis客户端形态
- redis实战操作
- redis支持的数据类型
- redis单线程模型
- redis单线程模型快的原因
- 详解数据类型
- 其它数据类型
- 数据库管理
Ubuntu下安装redis
c++
//默认下载5.0.7版本(初学的话也推荐从版本5开始学习)
sudo apt install redis -y
//修改一下redis的配置文件
sudo vim /etc/redis/redis.conf
//将redis服务器放在公网进行访问
修改bind 127.0.0.1 为bind 0.0.0.0
修改protected-mode yes 为protected-mode no
//修改配置文件过后需要重启redis服务器
sudo service redis-server start(启动redis服务器)
sudo service redis-server stop (停止redis服务器)
sudo service redis-server restart(重启redis服务器)
什么是redis?
Redis是⼀种基于键值对(key-value)的NoSQL数据库,与很多键值对数据库不同的是,Redis
中的值可以是由string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据结构和算法组成,因此Redis可以满⾜很多的应⽤场景,⽽且因为Redis会将所有数据都存放再内存中,所以它的读写性能⾮常惊⼈。不仅如此,Redis还可以将内存的数据利⽤快照和⽇志的形式保存到硬盘上,这样在发⽣类似断电或者机器故障的时候,内存中的数据不会"丢失"。除了上述功能以外,Redis还提供了键过期、发布订阅、事务、流⽔线、
redis的一些特性
在内存中存储数据
相比于MySQL来说,redis是在内存中存储数据,而MySQL是将数据存在硬盘中的,如果某一天,用户想要查询一条数据,那么肯定是使用redis存储的数据查询效率比较高,因为redis查询是直接在内存中进行操作,而MySQL查询不是,MySQL查询会先将数据从硬盘加载进内存,再进行查询,也就是相比与redis来说,要多进行一次IO,查询效率上肯定没有那么块。
可编程的
针对redis的操作,可以直接通过简单的交互式命令来进行操作,也可以通过一些脚本的方式,批量执行一些操作(可以带一点逻辑)。
可扩展性
可以在redis原有功能的基础上再进行扩展,redis提供了一组API。
自己去扩展redis的功能,比如,redis自身已经提供很多的数据结构和命令了,通过扩展,可以让redis支持更多的数据结构和命令。
持久化
redis将数据存储在内存中速度自然快,可是内存有个极大的缺点,就是掉电易失。要是某一时刻我们的redis程序突然崩溃或者突然停电了,那么我们存储在redis中的数据自然也就不翼而飞了,为了避免这个问题,redis会将内存中的数据全部在硬盘上备份一遍,这样就算掉电了或redis重启了,能够及时的从硬盘中读取数据来进行恢复。当然redis还是以内存为主,硬盘为辅。
集群
redis作为一个分布式系统中的中间件,能够支持集群是很关键的。因为一个redis能存储的数据是有限的,为此我们可以引入多个主机,部署多个redis节点,每个redis存储一部分数据,类似"分库分表"。
高可用
redis 自身是支持"主从"结构的,从节点就相当于主节点的备份,当主节点挂掉过后,从节点可以快速的切换为主节点。
小结
- MySQL主要是通过"表"的方式来存储和组织数据的,属于关系型数据库。而redis主要是通过"键值"对的方式来存储和组织数据的,属于非关系型数据库。
- redis的key值类型只能是string,而value的类型可以是:字符串、哈希、列表、集合、有序集合等。
为什么redis快?
- redis将数据存储在内存中的,而MySQL是将数据存储在硬盘中的。那么对于读取同一条数据的化,redis直接对内存进行操作,而MySQL需要将数据从硬盘读取到内存过后再进行操作,要比redis多一次IO,而IO又是比较消耗时间的,你说redis能不快吗?
- redis核心功能都是比较简单的逻辑,核心功能都是比较简单的操作内存的数据结构。
- 从网络上来说,redis采取了IO多路复用技术(epoll),使用一个线程管理多个socket。相比于MySQL的多线程,避免了锁的竞争,减少了 不必要的线程之间的竞争开销。
redis的应用场景
数据库
redis可以当作数据库来使用,在大多数情况下考虑到存储数据,优先考虑到的是"大",那么这时候采用MySQL来进行存储是个不错的选择,而在某些特殊场景下,我们需要优先考虑"快",那么这时候redis是个不错的选择。只不过这时候redis里面存的是全量数据,不能轻易丢弃!
缓存和会话存储
首先,我们可以将redis当作一个缓存来使用,在这个缓存中我们可以放一些热点数据,让后将全量数据存入在MySQL中,因为在互联网这个世界中有一个原则就是"二八原则",也就是20%的数据能够满足80%的需求,极端情况下就是一九原则,10%的数据可以满足90%的需求,我们可以将这20%的数据放入redis缓存中,让后将全量数据放入MySQL中进行存储,这样即使redis缓存丢失,也可以从MySQL中进行恢复。日次就是将redis用来存储session对象;
就比如现在有如下架构服务:
现在有一个用户首次访问了我们的服务,那么用户的请求会被先打到负载均衡层,然后由负载均衡层将这个用户请求分配到应用程序,假设就分配到了0号吧,于是0号服务器,接收到这个请求过后,发现用户的请求中没有sessionID字段,那么该服务器就判定当前用户是首次访问我们的服务器,那么他就会要求用户进行登录或注册,于是用户按照服务器的要求做后,服务器会将收集到的用户信息放在一个sessio对象中进行存储,并且把这个session对象存储在自己的服务器内部,并给客户端返回该session对象的sessionID,于是乎,后续这个客户端的请求中就都会带上这个sessionID,来供服务端进行身份验证。于是这个客户端开启了自己的第二次访问(本次的请求中携带了sessioID),可是好巧不巧这次用户被负载均衡层分配到了2号服务器,于是2号服务器就开始了"秉公执法",先检查你有没有sessionID,哎,发现你有,于是乎,他遍根据这个sessionID去自己存储的session对象中查找是否有与目标sessionID一致的session对象,结果找了一般半天,他发现没找到,于是他认为这个客户端的sessionID过期了,遍让其重新进行登录,这时候用户就不干了,"什么垃圾网站,我登录过了,还要我登录",用户愤愤的说道。
为此,为了解决这个问题,我们有两个方案:
- 负载均衡层根据userID将通过一个用户请求分配到原先使用过的服务器上。
eg:此时用户id为0,那么第一次经过负载均衡采用userID%3的方式分配到了0号服务器,那么第二次的时候,同一个用户ID也会被分配到相同的服务器上去,这样就能解决用户多次登录的问题了;可是这样的方法有个缺点,就是它是将用户信息存储在服务器内部的,如果这个服务器访问量比较高的化,那么在服务器内部就会出现大量session对象,会大大的降低服务器的性能,为此我们可以考虑将服务器内部的session对象,单独放在一个服务器上进行存储,这样应用服务器的性能就得到了释放,同时对于存储session对象的服务器来说,我们要求查询速度快,那么这时候redis的作用不就出来了。- 采用redis来存储session对象
这样的话也能解决,用户需要多次登录的问题。
消息队列
这里的消息队列不是Linux下进程间通信的那个消息队列,而是生产者消费者模型中消息队列。
基于这个,可以实现一个网络版本的生产者消费者模型。
当然业界也有许多知名的消息队列,RabbitMQ、Kafka、RocketMQ...
如果当前场景中,对于消息队列的功能依赖不是很多,并且又不想额外引进其它消息队列组件,那么redis是个不错的选择。
redis客户端形态
- 命令行客户端
命令行客户端主要是我们学习redis的是否进行使用的;
redis客户端名称为redis-cli
redis-cli -h {redis所在ip} -p {redis绑定的端口号}
不写-h、-p默认redis在当前服务器上,端口号默认为6379
- 图形化界面
桌面程序,web程序。
像这样的图形化界面,依赖Windows操作系统,而在未来实际工作中,我们用来办公的windows系统,连接道服务器可能会有诸多限制,具有一定的风险!
- 基于redis提供的API自行开发开发的客户端
这是我们未来工作中经常用到的方式!非常类似于MySQL C语言的函数接口。
redis实战操作
set命令
语法: set key val
介绍:设置一个键值对进行存储,key为key值,val为value值,redis最核心的命令之一。
get命令
语法:get key
介绍:获取key值对应的value值,如果key值不存在,则返回nil,也就是null。这也是redis的核心命令之一!
ping
语法 ping [message]
介绍:用于测试客户端和服务端的连通性,如果ping后面没有跟任何内容,服务端会返回pong;如果后面跟了内容的,那么返回用户设置的内容;
keys
语法:keys [格式]
介绍:匹配满足指定格式要求的key值,并返回;
其中:
*
表示通配符,可以表示零个或多个字符,?
表示匹配一个字符eg:
h*llo
会匹配hello、hallo、hllo、hadsfafllo等格式的字符串。
h?llo
会匹配hello、hallo、hcllo等格式的字符串。
h[abcd]llo
会匹配hallo、hbllo、hcllo、hdllo等格式字符串,不回匹配hello、hfllo等字符串。
h[a-d]llo
会匹配hallo、hbllo、hcllo、hdllo等格式字符串,不回匹配hello、hfllo等字符串。
h[^e]llo
会匹配第二个字符不是e的格式字符串,比如hallo、hcllo等。
exists
语法:exists key1 key2 key3....
介绍:检测一个key值是否存在,如果存在则返回1,如果不存在则返回0;
如果exists去检测多个key值,那么exists会返回这些key值的总的出现次数,如果参数中多次提及相同的现有键,则该键会被多次计数。
del
语法:del key1 key2 key3....
介绍:给定key删除对应键值对,返回值为删掉的key值的个数;
expire
语法 expire key {seconds}
介绍:为已存在的key值添加过期时间,成功返回1;如果key值不存在则返回0;
key值过期过后会被自动删除。
ttl
语法:ttl key
介绍:获取key值的过期剩余时间;如果key不存在则返回-2,如果key值没有关联过期时间则返回-1;
EXPIRE和TTL命令都有对应的⽀持毫秒为单位的版本:PEXPIRE和PTTL,详细⽤法就不再
介绍了。
键的过期机制
一个redis中会存在大量的key值,同时也会存在大量过期的key值,那么redis是如何区别出那些是过期key值并清除的呢?
最暴力的方法:遍历检测每一个key值,删除过期key值,这样毫无疑问是比较耗时间的,同时我们的redis是单线程服务器,如果key值比较多的化,那么遍历时间就会比较就,那么就无法及时给予后续客户端响应。
为此redis的整体策略是:
- 定期删除
redis服务器每个一段时间就会去抽取一些key值来进行检测,然后删除抽取中的过期的key值。每次抽取的数量不能太多,避免造成redis不能及时响应客户端。
优点 :可以通过设置检测时长来限制删除操作对于CPU的占用率,保证了redis服务器能够及时处理来自客户端的请求。
缺点 :
如果定期删除执行的比较频繁,那么redis的大部分精力都会花在删除操作上,不能够及时的处理来自客户端的请求。
如果定期删除执行的比较少,那么在内存中就会存在大量的过期key占用着内存资源,对内存使用率不是很友好。- 惰性删除
当一个key值过期过后,redis并不着急去删除它,而是将其先暂时保存在内存中,当用户再次访问这个key值的时候,我们会对这个key值先进行过期检测,如果该key已经过期了,则删除该key值,并向客户端表示访问失败;反之,则向客户端表示访问成功!redis主要就是综合这两种策略来进行定期键的删除的,当然仅仅这两种策略还是不能完全删除过期键的,为此为了对于上述进行补充,redis还提供了一系列的内存淘汰策略。
注意:
redis并没有采取定时器的策略来删除过期key!!(定时器:就是到达某个时间后执行特定任务)
个人猜测 :基于定时器的删除策略,势必就要引入多线程了,而redis早期版本早就奠定了单线程基调,引入多线程就打破作者初衷了。
那么假设,redis采用了定时器的策略那么我们可以怎么做呢?我们可以基于优先级队列或时间轮实现比较高效的定时器。
- 基于优先级队列的定时器;
首先,我们可以更具过期key的过期时间建一个小根堆,然后由检测线程根据当前时间与堆顶的时间进行对比,如果当前时间早于堆顶时间,那么就将检测线程阻塞,阻塞时间为当前时间与堆顶时间的差值,当检测线程被唤醒过后,那么它就可以删除堆顶元素了;如果当前时间晚于堆顶时间,那么说明堆顶元素早就过期了,那么我们可以安全的删除这个堆顶key;同时,如果在我们检测线程阻塞期间,又有新的元素插入进小堆中,那么我们的检测线程需要被唤醒,并且重新计算自己的阻塞时间。- 基于时间轮的定时器
逻辑结构如下:
redis会将上述圆环切分成多个小格子,每个小格子都是一样大的,都代表着相同的时间间隔,比如100ms,在每个小格子上可以挂一个链表,在这个链表上可以挂定时事件;当过了一个间隔时间过后,指针就会往后走,但是在往后走之前,检测线程会检查一下当前槽所对应的链表上的定时事件,有哪些是已经到期了的,检测线程会执行这些到期事件的回调函数,来处理这些到期事件,并将其从链表中移除,做完这些过后,检测线程才将指针往后移动,继续等待下一个间隔时间的到来。当有定时时间要被插入的时候,redis会计算这个定时事件的到期时间,然后根据到期时间计算出该定时事件应该放在那个小格子的链表上。
进一步理解时间轮,说百了所谓时间轮其实就是一个长长的时间轴被卷成了一个圆形:
从这个角度来理解时间轮,就比较好理解了;
注意:
redis实际上并没有采用我们上述所说的两种定时器方案来实现定期键的删除,但是这两种确实是比较优秀和高效的定时器,我们需要了解它。
type
语法:type key
返回key值对应的val的数据类型;如果key值不存在则返回none;
redis支持的数据类型
对于redis来说,key的类型只有string类型,而value的类型可以是,以下几种:
其中比较常用的有:string、list、set、hash、sorted set(有序集合)。
其它几种数据类型都是针对特殊场景下的特殊数据类型/数据结构。
其中redis中的string就类似于我们C++中的string,list类似于我们C++中的deque、set相当于C++中的set,hash相当于C++中的unordered_map,而对于有序集合来说,我们除了存储一个member之外,还需要存储一个score(权重、分数等数据)来帮助有序集合进行排序。
但是redis在底层设计这些数据结构的时候,会进行一定的优化,以达到节省时间、节省空间的效果。
这个一定的优化,就表现在:比如当用户传给我一个value为string类型,且value值为12345的时候,redis底层可能会用一个int来存储这个12345,而不是使用string。但是redis做的这些操作,我们上层用户是感知不到的,因为redis在表层表现给我们的就是,我这个value是string类型的,但是底层到底是不是使用string来实现的就取决于redis的策略了,redis这个意思,有点类似于挂羊头买狗肉了,redis明面上表示这个value的值我会用string来存储,但是底层具体存储的时候就又会根据实际情况来选择最优方案来进行存储。
当然不止string类型会是这样,其它常见结构也会这样。
redis数据结构和内部编码
数据类型 | 内部编码 |
---|---|
string | raw |
string | int |
string | embstr |
hash | hashtable |
hash | ziplist |
list | linkedlist |
list | ziplist |
set | hashtable |
set | intset |
zset | skiplist |
zset | ziplist |
上述这个表格中就记录了,redis表面上的数据类型和底层实际采用的数据类型;
我们现在来具体介绍一下,每个内部编码方式:
- string
int :当value是一个纯数字字符串,并且组成的数字比较小时,redis底层并不会对这个数字字符串,采用字符串的方式来进行存储,而是将其转换为数字过后在进行存储。
embstr :当value是一个比较短小的普通字符串时,比如字符串长度小于等于39时,那么redis会对其采用embstr进行编码。其实这个数字是没有意义的,因为,当字符串长度小于等于多少时,该采用embstr编码,我们应该取决于我们的应用场景。
raw :当value是一个比较长的字符串时,那么redis会对这个字符串采用raw的方式进行编码,也就是类似于C++中的char数组来存储。- hash
ziplist :value是个hash类型,并且,这个hash结构里面元素比较少的时候,redis就会采用ziplist的编码方式来编码这个value,ziplist本质上是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一字节数组或者一个整数值。为什么要进行压缩呢?因为普通的编码方式是在内存中随机找块节点来进行存储,元素稍微少点还好,元素一旦多起来,内存碎片就比较严重,内存利用率就比较低,要知道redis之所以快,主要原因就是因为它是针对与内存进行操作的,内存利用率变少了,redis工作效率就变低了;
hashtable :基本的hash结构,与C++中的unordered_map类似;- list
linkedlist :普通的双向链表;
ziplist: 见上述;
quicklist :quicklist同样采用了linkedlist的双端列表特性,然后quicklist中的每个节点又是一个ziplist,所以quicklist就是综合平衡考虑了空间碎片和读写性能两个维度。- set
hashtable :基本的hash结构;
inset: 当value是set类型并且集合里面元素比较少且都是整数的时候,那么redis会将这个value采用intset的方式进行编码;实际上,intset是一个由整数组成的有序集合,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化。- zset
skiplist :在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。
ziplist: 见上述;
接下来,我们通过实验的方式来展示,redis内部实现的编码:使用
object encoding key
可以查看key值对应的value的具体编码方式;
1. string
2. hash
编码为hashtable的方式,需要value中有很多元素才行,这里不好演示。
3. list
现在list的编码方式普遍都是quicklist了,因为quicklist结合了linkedlist和ziplist两者编码方式的优点,只有少数情况才会出现ziplist和linkedlist编码方式;
4. set
5. zset
编码方式为skiplist的,需要插入大量元素才能演示出来,读者可以自行验证。
redis这样设计有两个好处:
- 可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如redis3.2提供了quicklist编码方式,结合了ziplist和linkedlist两者的优势,为list类型提供了一种更为优秀的内部编码的实现,而对于用户来说基本无感知。
- 多种内部编码实现,可以在不同的场景下发挥各自的优势,例如ziplist比较节约内存,但是在列表元素比较多的情况下,性能下降,这时候redis会根据配置选项将列表类型内部实现转换为linkedlist,整个过程用户同样无感知。
redis单线程模型
redis单线程模型是指redis只是用一个线程来处理命令,而不是说redis服务器内部就真的只有一个线程!实际上redis服务器内部也有很多个线程,多个线程处理网络IO。
假设,现在开启三个redis客户端,并且,三个客户端都同时向服务器发起命令:客户端1设置⼀个字符串键值对:
set key1 111
客户端2对键值为key2的value做自增操作:
incr key2
客户端3对键值为key2的value做自增操作;
incr key2
这三个命令同时向服务端发起,并且同时被服务端接收到,那么redis在执行的时候会存在线程安全的问题吗?
实际上,并不会,我们举个简单的例子来说明这个情况,eg:
我记得我在上高中的时候,我们学校的人非常多,并且全校都是一起放学去干饭,因此,如果是后面去吃饭的人,可能就会面临没饭吃的尴尬场景,为了避免这种场景,我们一般是下课铃一响,全班就一起冲向食堂,但是来到食堂后,打饭的窗口就那么一个,为了快速的打到饭,我们只能一个个排队等候。
在这个故事中,食堂就相当于我们的redis服务器,打饭窗口就相当于redis中真正处理命令的线程,而学生就相当于客户端,无论我们这个学生什么时候开始跑,和那几个学生一起到的食堂?我们到了打饭窗口就得排队打饭!redis客户端亦是如此,无论客户端是同时出发还是同时到达,只要是需要被处理命令,那么就需要在队列中进行排队等待,redis线程的处理,因为redis处理命令的线程只有一个!
从宏观上来看,客户端是同时到达服务器的,但是从微观上来看,即使是同时到达的客户端,也是有先后次序之分!
这其实也说明的redis虽然是单线程模型,但是他能保证到来的任务是串行化执行的!!!
redis之所以能够用单线程模型来进行工作,主要依赖于redis的核心业务就是短平快的!不太消耗CPU资源,也不太吃多核!
redis单线程模型快的原因
- redis的大多数的操作都是基于内存来完成的。相比于MySQL这种关系型数据库来说,当redis接收到指令过后,那么就直接在内存中操作数据了,而MySQL接收到指令过后,会首相将数据从硬盘IO到内存,然后再基于内存进行操作,这一次IO就拖慢了MySQL不少效率。
- redis的核心功能都是短平快的。而MySQL的核心功能比较复杂,比如插入数据之前,需要检查约束,插入结束过后又需要分组分页来维护自己的索引。在查询功能上MySQL也需要先将数据从硬盘读取到磁盘,再进行操作,等MySQL经过着一些复杂的操作过后,redis似乎早已经完成了自己的工作。
- redis在处理业务的时候采用的是单线程模式,避免了无谓的锁的竞争和资源消耗。而对于MySQL来说,MySQL在处理业务的时候是存在锁冲突和锁竞争的,这是会消耗大量资源和大量时间的。同时在单线程情况下,redis也避免了线程切换所带来的消耗。
- 处理网络IO的时候,使用了epoll这样的机制。一个线程就可以管理多个socket,工作线程一次性可以处理更多的客服端。因为对于一个服务器来说,同一时刻可能会有很多客户端连接它,但是并不是所有的客户端都是活跃的,相当,在服务器器中会存在大量静默的客户端,如果我们采用一个客户端一个线程去处理的方式的话,那么服务器内部就会有大量线程阻塞,这对于服务器来说是不能容忍的,这不典型的站着茅坑不拉屎嘛,服务器的资源就白白的被浪费了。而我们采用epoll的话,我们redis服务器只有当有活跃客户端或者活跃事件的时候,redis才会开启真正的核心工作,这样以事件驱动的方式,降低了服务器的开销,提高了服务器的运行效率。
redis的缺陷:虽然单线程给Redis带来很多好处,但还是有⼀个致命的问题:对于单个命令的执⾏时间都是有
要求的。如果某个命令执⾏过⻓,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客⼾
端的阻塞,对于Redis这种⾼性能的服务来说是⾮常严重的,所以Redis是⾯向快速执⾏场景的数据
库。
详解数据类型
string
字符串类型是redis最基础的数据类型,关于字符串我们需要特别注意:
①首先redis中所有的键的类型都是字符串类型,没有hash、set、list等类型的说法;而且其它几种数据结构也都是在字符串类似基础上进行构建的,例如列表和集合的元素类型都是字符串类型,所以字符串类型能为其他4种数据结构的学习奠定基础。
②字符串类型实际存储的值可以是普通字符串、JSON格式字符串、XML格式字符串、二进制数据(eg:图片、音频、视频等)不过一个字符串最大不能超过521M。
③redis内部存储字符串完全是按照二进制流的形式来进行保存的,所以redis是不解决编码问题的,客户端提交上来的数据是什么形式,就按照什么形式进行存储。比如:客户端1上传了一个GBK编码格式的字符串,那么redis不回对该二进制序列做任何处理,而是直接进行存储,后面客户端1在读取数据的时候,也方便,不需要做编码转换,比MySQL简洁多了。
相关命令
set key value [ex|px] [nx|xx]
功能: 设置value类型为string的键值对,当要设置的key已经存在的时候,那么set就是覆盖的功能,并且该键的过期时间也会失效。
return val :成功返回ok其中[] 表示可选项,可带可不带;
[a|b] 表示a和b只能选一个;
[ex|px] 设置这个键的过期时间,ex单位秒,px单位毫秒.
[nx|xx] nx表示如果键值不存在,则进行插入,存在则插入失败,返回nil;xx表示如果键值存在则进行替换,替换过后该键的过期时间将失效,如果键值不存在,则替换失败,返回nil。
演示:
①设置键值key1 v1 的过期时间为10s 不存在就进行插入:
set key1 v1 ex 10 nx
②替换key1 对应的键值对,将其替换为key1 v2
set key1 v2 xx
当然redis也为我们集成了这些命令:
setnx 等价于 set key val nx
setex 等价于 set key val ex
psetex 等价于 set key val px
get key
功能 : 获取key值对应的val值;
return val: 如果key不存在,则返回nil;如果key存在则返回对应的val值;
通过实验我们可以发现,当我们插入的val是中文时,那么我们在使用get去获取的时候,实际上获取到的是该中文的二进制编码(为了方便我们人查看,redis将其使用十六进制显示),并不会直接显示对应的字符,如果我们想要其显示对应的字符,那么我们在使用redis-cli客户端登录redis服务器的时候,我们可以加上--raw的选项,这样我们在使用get获取key3的时候就能看到对应的字符,而不是二进制序列;
mget key [key...]
功能: 获取多个key对应的val值;
return val: 返回获取成功的key对应的val值,如果某个key值不存在,则该key值对应的val值在返回的时候,会返回nil;
与之对应的操作还有,mset;
mset key val [key val....]
功能: 一次性设置多个键值对
return val:
实际上,上面说的mset、mget都是批量化操作,可以理解为是多次set、get的行为,但是一次mset、mget在效率和消耗上是比多次set、get要小的;
通过上图,我们可以很清楚的分析到,n次get/set操作会进行n次网路传输,而一次mget/mset只会进行一次网络传输,我们都知道网络传输是很慢的,很消耗时间的,比文件IO还慢,其次的话,mget/mset与单次get/set在redis服务器中被处理的消耗实际上是相当的,那么很明显n次get/set虽然也能得到结果,但是消耗会更大;
因此,如果要进行批量化,操作,我们更推荐使用mget/mset;
下面是1000次get和1次mget的对比:
操作 | 时间 |
---|---|
1000次get | 1000 * 0.1+1000 * 0.1=1100 ms |
1次mget1000个键 | 1 * 1+1000 * 0.1=101 ms |
setnx key val
功能: 如果key值不存在,则插入;
return val: 如果成功,则返回ok;失败返回0;
incr key
功能: 将对应的key所对应的val加1,当然key所对应的val必须是合法整数;
return val: 如果key存在,则将val自增1,返回自增过后的结果;如果key不存在,在插入key,val初始化为0,然后在自增1,也就是返回1;
与之对应的还有
decr
,该命令的功能是将key对应的val自减1;
incrby key num
功能: 将key所对应的val+=num,然后返回;如果key不存在,则插入key,然后用0初始化val,然后再+=num,最后返回num;其中num必须是整数!;
return val: 返回加上num过后的结果;
与之对应的还有
decrby
将key对应的val 减去num;
incrbyfloat key num
功能: 将key对应的val加上一个小数;
return val: 与incr类似
append key str
功能: 在key值对应的val值后面追加一个str;
return val 如果key存在,则返回追加过后的值的长度(所占字节数);如果key不存在,那么此时append就相当于做了一次set;
getrange key start end
功能: 获取key所对应的val字符串的[start,end]区间的字串;
return val: 返回对应区间的字串;如果key不存咋,则返回空;
在redis中不就n-1可以表示最后一个元素,-1也可以表示最后一个元素,然后-2,-3...以此类推;
setrange key offset str
功能: 从offset偏移量开始,设置str;
return val: 返回设置成功过后的结果的字节数; 如果key不存在,那么setrange就相当于一次set
strlen key
功能: 计算key值对应的字符串的字节数;
return val: key对应的val的字节数;
应用场景
1. 缓存功能
下图是个比较典型的缓存使用场景,其中redis作为缓冲层,MySQL作为存储层,绝大部分的请求都是从redis中获取数据,由于redis具有支持高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用;
2. 计数功能许多应用都会使用redis作为计数的基础工具,他可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其它数据源
3. 共享session一个分布式Web服务将用户的Session信息保存在各自的服务器内部,这样会造成一个问题就是:出于负载均衡的考虑,负载均衡层会将用户的访问请求均衡到不同的服务器上面,并且通常无法保证用户每次请求都会被均衡到同一台服务器,这样当用户刷新一次访问,可能发现需要重新登录,这个问题对于用户来说是极其不友好的。为了解决这个问题,可以使用redis将Session信息进行集中管理,这种模式下只要保证 Redis 是⾼可⽤和可扩展性的,⽆论⽤⼾被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息。
4. 手机验证码很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机号发送验证码,然后让用户输入收到的验证码进行验证,从而确定是否是用户本人。为了使短信接口不回被频繁的访问到,会限制用户每分钟获取验证码的频率,eg: 一分钟不能超过5次;
具体思路可以参考如下伪代码:
cpp
String 发送验证码(phoneNumber) {
key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟(60 秒)
// 使⽤ NX,只在不存在 key 时才能设置成功
bool r = Redis 执⾏命令:set key 1 ex 60 nx
if (r == false) {
// 说明之前设置过该⼿机的验证码了
long c = Redis 执⾏命令:incr key
if (c > 5) {
// 说明超过了⼀分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过⼿机的验证码;要么次数没有超过 5 次
String validationCode = ⽣成随机的 6 位数的验证码();
validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟(300 秒)内有效
Redis 执⾏命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过⼿机短信发送给⽤⼾
return validationCode ;
}
// 验证⽤⼾输⼊的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
validationKey = "validation:" + phoneNumber;
String value = Redis 执⾏命令:get validationKey;
if (value == null) {
// 说明没有这个⼿机的验证码记录,验证失败
return false;
}
if (value == validationCode) {
return true;
} else {
return false;
}
}
hash
⼏乎所有的主流编程语⾔都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数
组、映射。在Redis中,哈希类型是指值本⾝⼜是⼀个键值对结构,形如key="key",value={{
field1, value1 }, ..., {fieldN, valueN } },Redis 键值对和哈希类型⼆者的关系可以⽤图2-15来表⽰。
其中field和value的类型只能是string类型,并且对于value类型不允许在嵌套其它的数据类型!
并且,hash中的field值不允许设置过期时间,过期时间只能针对redis的key值来进行设置!
常用命令
hset key field1 value1 [field2 value2 ....]
功能: 针对val插入一系列键值对
return val: 返回插入成功插入的数目;
hget key field
功能: 获取key对应的hash结构中值为field对应的val;
return val: 返回key对应的hash结构中值为field对应的val;
当然,对于hget操作中,如果key值或field值有一个不存在,fget返回的就是空值:
hdel key field [field1.....]
功能: 删除hash结构中对应的键值
retuan val: 返回成功删除的个数:
hkeys key
功能: 获取key值对应的hash结构中,所有的field值
return val: 返回获取到的field值
当然,如果key值不存在,返回值就是空:
hvals key
功能: 获取key值对应的hash结构中,所有的field值
return val 返回获取到的所有field值
同理,如果操作的key值不存在,那么返回空:
注意,hvals在获取field值的时候,即使field值由重复,他也会将重复的值加入结果集中进行返回:
hgetall key
功能: 获取key值对应的hash结构的所有{field,value}键值对
return val: 返回key值对应的hash结构的所有{field,value}键值对;
hmset key f1 v1 [f2 v2....]
功能: 批量插入{field,value}键值对
return val: 返回成功插入的个数:
hmget key f1 [f2...]
功能: 获取key对应的hash结构中f1、f2...等对应的val值
return val: 返回key对应的hash结构中f1、f2...等对应的val值;
在操作中,如果key值不存在或者某个field值不存在,那么会返回空集;
hlen key
功能: 获取key值对应的hash结构的元素个数;
return val: 返回hash结构中元素的个数;
hsetnx key field value
功能: 如果field不存在则插入,{field,value};存在则插入失败;
return val : 成功返回1,失败返回0;
hincrby key field num
功能: hash结构中field对应的value值加等num;
return val: 返回加上num过后的值;
hincrbyfloat key field num
功能: 将hash中field对应的值加等一个num(num为小数);
return val: 返回加等过后的结果;
总结:
- 针对上面的命令中,hkeys、hvals、hgetall命令我们需要慎用,因为这些命令时间复杂度都是O(N) 会遍历整个hash,而我们的redis处理命令的模型是单线程,如果一个hash中元素太多,那么就可能导致,后续命令被阻塞,严重影响redis的性能!为此,如果想要获取部分的field,我们可以用hmget,如果我们非要获取全部的field,那么我们可以用hscan,该命令采取渐进式遍历hash结构,每次遍历都只会获取一小部分field,也就是我们每敲一次hscan就会获取到一段field,我们只需要循环的敲field,就能获取到全部的field,这样的话,遍历成本就转嫁到客户端这边了,缓解了redis服务器的压力,同时由于每次都只是获取一小部分,也不会造成redis服务器阻塞;
应用场景
- 使用关系型数据库保存用户信息如下:
- 使用key-value类型数据库存储,以上信息:
- 相比于使用Json格式的字符串缓存用户信息,使用hash类型会变得更加直观,并且在操作上变的更加灵活;
- 哈希类型是稀疏的,而关系型数据库是完全结构化的,比如说:hash类型可以拥有不同的field值,但是对于关系型数据库来说,如果新增加了一列,那么其原先所有已经插入的数据,该列都要设置值,即使为null;
- 关系型数据库可以维护更加复杂的关系查询,比如:多表查询、聚合查询、外键等等,如果redis想要去维护这些关系的话,会付出很大的代价;
- 缓存方式对比: 截至目前为止,在redis中我们已经拥有了3中缓存用户信息的方式;
4.1 直接使用原生字符串来进行缓存;
eg:
set user:0001 uid:0001;
set user:0001 name:Tom;
set user:0001 age:19;
set user:0001 city:BeiJing;
优点: 实现简单,定位更加准确;
缺点: 如果用户想要获取到一个用户的全部数据,那么需要经过多次网络IO,消耗较大;其次用户数据过于分散、低内聚。
4.2 将用户信息先序列化成Json字符串,然后再进行存储;
eg:set user:0001 Json序列化过后的字符串;
优点: 将用户信息聚集了起来,具有高内聚的特点,同时也只需要一次网络IO就能获取到用户的全部数据,比较高效;
缺点: 当我们想要只想修改用户某一字段的属性时,我们只能先将用户全部信息获取到,然后反序列化, 然后进行修改,然后又序列化,然后再重新存储,比较浪费客户端内存空间,同时时间消耗上也比较大;获取单一字段的值不够灵活;
4.3 hash类型存储用户信息;
eg:hset user:0001 uid 0001 name Tom age 19 city BeiJing;
优点: 高内聚、获取某一字段的值比较灵活同时修改某一字段的值也比较方便,只需一次网络IO就能获取到用户的全部信息;
缺点: 需要控制hash在ziplist编码和hashTable编码方式之间的转换,这两个编码方式之间的转换,消耗比较大!
list
在redis中list更像是C++中的dequeue(双端数组),支持随机访问,头插、头删、尾插、尾删时间复杂度都是O(1);
列表类型是⽤来存储多个有序的字符串,如图9所⽰,a、b、c、d、e五个元素从左到右组成了⼀个有序的列表,列表中的每个字符串称为元素(element),⼀个列表最多可以存储32 2 − 1个元素。在Redis中,可以对列表两端插⼊(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是⼀种⽐较灵活的数据结构,它可以充当栈和队列的⻆⾊,在实际开发上有很多应⽤场景。
列表具有以下特点:
- 列表中的元素是有序的,这里的有序不是指升序、降序那个有序,而是说当我们改变list中元素的顺序过后得到的新list与原来的list是两个不同的list,eg:[1,2,3,4] 和[2,3,1,4] 是两个不同的list。
- 对于list来说,删除元素和获取元素都会返回元素的值,唯一的区别就是一个会真删除列表中的元素,而一个只是读取;
- 列表中的元素允许重复;
常用命令
lpush key e1 [e2....]
功能: lpush全称是left push 也就是头插;
return val: 返回插入过后的列表的长度;
rpush key e1 [e2...]
功能: rpush全称right push 也就是尾插;
return val: 返回插入元素过后的列表的长度;
lpop key [count]
功能: lpop全称left pop 也就是头删;当然在redis 6.0 以上版本中支持指定头删的元素个数;
return val 返回删除的元素的值;
如果删除的列表是个空,那么会返回空;
rpop key [count]
功能: rpop全称right pop 也就是尾删;在redis 6.0 版本中支持指定尾删的元素个数;
return val: 返回删除的元素的值;
lpushx key e1 [e2]
功能: 如果key值存在,则等价于lpush;如果key值不存在,则插入失败;
return val: 如果插入成功,返回插入过后的列表的长度;如果插入失败,那么返回0;
rpushx key e1 [e2....]
功能: 如果key值存在,那么rpushx等价于rpush;如果key值不存在,那么尾插入失败;
return val: 插入成功,返回插入成功过后的列表的长度;插入失败,返回0;
插入失败:
lrange key start end
功能: 获取列表[start,end]区间的元素;
return val: 返回[start,end]区间的元素;
在redis中列表下标,从左往右是从0开始,然后一次增长;当然也可以是从右往左从-1开始递减;
因此最后一个元素的下标,也可以用-1来表示;
[0,-1]就是获取整个列表区间的元素;
当然如果获取的区间不合法,那么redis会尽量将我们要访问的区间自动校准成合法的,但是如果是在校准不过来,那么就直接返回空:
lindex key index
功能: 获取列表中下标为index的元素;
return val: 如果index合法,那么返回对应位置的元素,如果index不合法,那么直接返回空;
linsert key [before|after] p e
功能: 在p元素[之前|之后]插入元素e;
return val: 返回插入过后的列表的长度;如果插入失败,或者要查找的元素不存在,则返回-1;
注意: 整个linsert命令是针对元素的之前之后插入,不是针对下标!!!
插入失败:
lrem key count val
功能:count>0: 从左向右进行搜索,删除count个值为val的元素;
count<0: 从右向左进行搜索,删除count个值为val的元素;
count=0: 全表搜索,删除所有值为val的元素;
return val: 被移除的元素的数量;列表不存在时返回0;
ltrim key start end
功能: 保留列表[start,end]区间的值,其余区间的值全部删除;
return val: 执行成功返回ok;
lset key insex e
功能: 将下标为index的值修改为e;
return val: 操作成功,返回ok;失败返回错误信息;
llen key
功能: 获取列表的大小;
return val: 返回列表的长度;如果key不存在,则返回0;
blpop key [key2...] timeout
功能: 头删;与lpop不一样的是,这个是阻塞版本的头删;当列表中没有元素的时候,那么此时头删操作会被阻塞,其中timeout为其阻塞时间,在阻塞时间内,如果列表中有新元素到来,那么则直接完成删除;如果在阻塞时间内没有元素到来,那么它会结束本次阻塞,然后返回空;
return val:
当然,blpop也支持同时头删多个key对应的列表,但是最后都只会返回第一个被头删的值:
与blpop与之对应的就是brpop尾删,功能与blpop一样,就是删除位置不一样;
应用场景
- 消息队列
redis可以使用lpush+brpop组合或rpush+blpop组合实现网络版本的阻塞式的生产者消费者模型,生产者客户端使用lpush向列表中插入一个商品,消费者客户端使用brpop命令来从列表中获取商品,当列表为空,那么消费客户端就会被阻塞,并且这个消息队列是保证线程安全的,因为redis服务器内部处理命令的线程是单线程,并且是一个命令一个命令的处理,因此对于消费者客户端来说就是轮询式的访问这个消息队列;
set
集合类型也是保存多个字符串类型的元素,但是和列表类型不同的是集合中元素之间是无序的,这句话的意思就是说,集合中改变元素的顺序得到的集合与原集合是一样的,eg:[1,2,3,4] 与[2,3,1,4] 表示的是同一个集合;
同时集合不允许存在重复元素,一个集合最多可以存储2 ^32 -1 个元素。redis除了支持集合内的增删查改操作,同时还支持多个集合取交集、并集、差集。
常用命令
sadd key m1[m2.....]
功能: 向一个set中插入一个或多个元素;
return val: 成功插入的元素个数;
smembers key
功能: 查看key所对应的set的结果;
return val: 返回key所对应的set的结果集;
spop key [count]
功能: 随机删除一个元素,如果count被设置,那么随机删除count个;这里的随机删除,分为两步来理解就是:先随机生成count个元素,然后将这count个元素进行删除,这也是spop被叫做随机删除的原因;
return val: 返回被删除的元素;
sismember key m
功能: 检查元素m是否在集合中;
return val: 如果元素存在,则返回1;如果元素不存在则返回0;如果key不存在则返回0;
scard key
功能: 返回集合的元素个数;
return val: 返回集合元素个数;
smove sour dest m
功能: 将sour集合中的m元素移动到dest集合中;
return val: 移动成功,返回1;移动失败返回0;
srem key m
功能: 删除集合中的元素m
return val: 成功删除返回1;删除失败返回0;
sinter key1 key2 [key3....]
功能: 对key1、key2、key3...等集合取交集
return val: 返回上面几个集合的交集;
sinterstore dest key1 key2 [key3....]
功能: 对key1、key2、key3...取交集,并且将交集结果放在dest中;
return val: 交集的元素个数;
sunion key1 key2 [key3....]
功能: 对key1、key2、key3求并集;
return val: key1、key2、key3的并集
sunionstore dest key1 key2 [key3..]
功能: 对key1、key2、key3...求并集,并且将结果放入dest;
return val: 并集的元素个数;
sdiff key1 key2 [key3...]
功能: 查看key1、key2、key3的差集;
return val: 返回key1、key2、key3的差集;其中注意一下,差集是不满足交换律的,也就是说,A、B的差集是A有的,B没有的;而B、A的差集是B有的,A没有的;
sdiffstore dest key1 key2 [key3..]
功能: 将key1、key2、key3等的差集放在dest中;
return val: 返回差集的元素个数;
srandmember key [count]
功能: 随机获取count个元素;如果count没有被设置,那么就是随机获取一个元素;
return val: 返回获取到的元素;
应用场景
- 标签
就比如说刷抖音,A用户爱看:计算机相关的知识、足球、动漫、新闻、甜妹;而B用户喜欢看:新闻、音乐、动漫、学习、篮球; 那么抖音就会根据用户浏览的这些信息,来为用户生成对应的标签;
eg:
A用户的标签就是:计算机、足球、动漫、新闻、甜妹;
B用户的标签就是:新闻、音乐、动漫、学习、篮球;
然后抖音会将这些用户标签给存储起来:
eg:
sadd userA 计算机 足球 动漫 新闻 甜妹
sadd userB 新闻 音乐 动漫 学习 篮球
后续抖音推送的大多数内容都是围绕着这些标签来进行推送的;
同时,如果某一天用户A和用户B想要使用抖音的"一起看"功能的话,那么抖音会将用户A和用户B的标签做交集,eg:sinter userA userB; 然后根据这些交集标签来进行推送大家都爱看的视频;
zset
有序集合它保留了集合不能有重复元素的特点,但与集合不同的是:有序集合的每个元素都有一个浮点数类型的分数与之关联,redis通过这个分数来维护zset的有序性,eg:
有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利⽤有序集合,可
以帮助我们在实际开发中解决很多问题。
有序集合中的元素是不能重复的,但分数允许重复。类⽐于⼀次考试之后,每个⼈⼀定有⼀
个唯⼀的分数,但分数允许相同。zset中的元素和分数的关系更像是C++中的std::pair<元素,分数>的关系,而不是键值对的关系!
常用命令
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]
功能: 向zset中添加元素;NX: 如果元素不存在则进行添加,存在则添加失败
XX: 如果元素存在则替换,不存在则替换失败;
GT: 如果新分数大于当前的分数,那么就更新;当然如果元素不存在则失败;(redis5.0 中没有)
LT: 如果新分数小于当前的分数,那么就更新;当然如果元素不存在则失败;(redis5.0 中没有)
CH: 修改zadd返回值的意义,在没添加CH之前,zadd的返回值表示成功添加的新元素的个数,加上CH过后zadd的返回值,表示添加成功的元素个数+更新成功的元素个数的总和;
INCR: 将指定元素的分数加上一个整数;
注意: NX、GT、LT三个选项互斥
return val: 没加CH,表示成功添加的元素个数,加上CH过后,就表示添加成功的元素的个数+更新成功的元素个数;eg:
zrange key start stop [withscores]
功能: 获取[start,end]区间的元素withscores: 该选项表示在最后的结果集中是否带上元素对应的分数;
return val: 返回对应区间的元素;
zrevrange key start end [withscores]
功能: 以降序的方式获取[start,end]区间的元素withscores: 该选项表示最后的结果集中是否带上分数;
return val: 返回[start,end]区间的元素
zcard key
功能: 获取有序集合中的元素个数;
return val: 有序集合中的元素个数;
zcount key min max
功能: 获取一下[min,max] 分数区间的元素;
return val: 返回符合条件的元素个数;
如果我们想要获取(93,110]区间的元素个数,那么我们可以这样写:
当然,如果我们想要获取(93,110)区间的元素,我们可以这样写:
zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
功能: 根据分数来获取对应的元素;获取[min,max]区间分数的元素,结果集按照分数进行升序排序,如果分数相同,则对元素按照字典序进行升序排序;其中,-inf被redis视为负无穷大,inf被视为正无穷大,因此,如果我们想要根据分数获取全部元素的话,那么只需将获取区间改为[-inf,inf]即可;
return val: 返回获取到的结果集;
zpopmax key [count]
功能: 删除并返回分数最高的count个元素;
return val: 返回被删除的count个元素;
bzpopmax key [key....] timeout
功能: 阻塞式删除有序集合中分数最大的元素;如果有序集合为空,那么在timeout时间内,该删除操作会被阻塞;
return val 返回被删除的元素;
类似的命令还有:
zopomin、bzpopmin
zrank key member
功能: 获取指定元素在升序排序中的排名;
return val 返回指定元素在升序排序中的排名;
zrevrank key member
功能: 获取key有序集合中,member成员在降序排序中的排名;
return val: 返回member在降序结构集中的排名;
zscore key member
功能: 返回member元素对应的分数
return val: 返回menber元素对应的分数;
zrem key member[member...]
功能: 删除指定元素;
return val: 返回本次删除的元素个数;
zremrangebyrank key start end
功能: 删除升序排名在[start,end]区间的元素;
return val 本次删除的个数;
zremrangebyscore key start end
功能: 删除分数在[start,end]区间的元素;
return val 返回本次删除的元素个数;
zincrby key num member
功能: 对member元素对应的分数+=num分;
return val 返回加上num分过后的分数;
zinterstore dest numkey key1 [key2...] [weights weight [weight....]] [aggregate <min|max|sum>]
功能: 对key1 key2...多个有序集合求交集,并且将最后的结果放在dest有序集合中;numkey: 表示要进行求交集的有序集合的个数;
weights: 表示权重,最终取出来的分数会乘以这个权重,得到新分数;
aggregate: 表示相同元素的分数怎么取:min表示取较小值、max表示取较大值、sum表示求和;
eg:
return val: 返回dest中交集的元素个数;
zunionstore dest numkey key1 [key2] [weights weight [weight...]] [aggregate <min|max|sum>]
功能: 求key1、key2... 中的并集;dest: 存放结果的有序集合
numkey: 需要求并集的有序集合的个数;
weights: 每个集合所占的权重;
aggregate: 求取相同元素对应的分数的方式;
retrun val: 返回并集的元素个数;
应用场景
1. 排行榜系统;
eg:热搜榜、王者荣耀的巅峰赛排行榜等等;
对于排行榜来说,我们可以将用户文章id或用户id作为有序集合中的元素,然后将热榜分数或者巅峰分作为分数,然后我们可以根据这个"分数"对元素进行排名,从而选出热搜前几名和巅峰赛前几的玩家;
渐进式遍历
redis使用scan系列命令来进行渐进式遍历,从而解决直接使用keys 获取所有键时可能出现的阻塞问题。
什么是渐进式遍历?答:渐进式遍历,实际上就是每次都遍历获取一小部分数据,然后经过多次渐进式遍历,来完成整个键的遍历,其中由于每次渐进式遍历都是遍历的一小部分数据,因此这不会造成redis阻塞,并且事件复杂度可以近似的看成O(1);
scan cursor [match pattern] [COUNT count] [TYPE type ]
功能: 从cursor位置开始最多获取count个元素,默认获取10个;cursor: 本次开始渐进式遍历的位置,由上一次的返回值确定;
match pattern: 渐进式遍历过程中匹配什么格式的key值;
COUNT count: 这一次最多获取count个元素;注意:这个count仅仅是起建议的作用,redis服务器可能会返回多于或者少于count个元素回来;
TYPE type: 在渐进式遍历的过程中,匹配值为type类型的key;(redis 5.0版本不支持)
return val: 返回值由两部分组成,第一部分为下一次的cursor字符串,还有一部分是匹配到的key值; 当返回值中cursor为0,表示本次是最后一次遍历;
- 除了scan之外,面对hash有hscan、set有sscan、zset有zscan,用法与scan一致;
- 渐进式遍历虽然解决了阻塞问题,但是如果在遍历期间进行了增、删、改,那么可能造成最后遍历出来的结果有遗漏或者重复,这在实际开发中务必重视!
其它数据类型
当然,redis除了给我们提供了类似于:string、list、hash、set、zset这五种基本数据类型,还针对特定的场景为我们提供了一些特俗的数据类型;
- stream
该类型通常被用来当作消息队列使用:
- bitmaps
许多开发语言都提供了操作位的功能,合理的使用位能够有效的提高内存利用率;redis 提供的bitmaps这个数据结构,可以实现对位操作。
- bitmaps本身不是一种数据结构,实际上他就是string类型,但是它可以对string的位进行操作;
- bitmaps 单独提供了一套命令,所以在redis种bitmaps和使用字符串的方法不太相同,可以把bitmaps想象成一个以位为单位的数组,数组中的每个单元只能存储0和1,数组的下标在bitmaps中叫做偏移量;
- geospatial
redis 3.2 版本提供了GEO(地理位置定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能;- HyperLogLog
HyperLogLog并不是一种新的数据类型(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的空间完成独立总数的统计;但是用小空间估算巨大的数据,必然不是100%的正确,其中存在一定的误差,redis官方给出的数字是0.81%的失误率。- bitfields
redis中的bitfields结构实际上就相当于C语言中的位段,本质上是让我们精确操作位的一种方法;
bitfields这个东西相对于之前的string、hash来说,目的仍然是节省空间!
数据库管理
redis 实际上也是有类似于MySQL那样的数据库的概念;
只不过redis中的数据库并不像MySQL中的数据库那样自由,我们用户不能自主创建、销毁redis中的数据库,而是只能使用redis自身提供的16个数据库,这16个数据库,由redis使用0~15的序号来表示;
针对redis数据库的操作,redis提供了一些关于数据库相关的操作:
1. 切换数据库;
select index
其中index表示数据库的编号(0~15),默认情况下,我们所处的数据库是0号数据库;
redis虽然支持多数据库的操作,但是在实际开发中,我们并不建议使用它的多数据库特性;如果真的要使用两套完全隔离的数据库,那么我们建议直接部署两个redis实例,而不是在一个redis实例中使用两份数据库;这是因为,redis本身并没有为多数数据库提供太多的特性,其次无论是否有多个数据库,redis都是使用的单线程模型,所以彼此之间还需要排队等待命令的执行,同时多个数据库之间的切换也是有消耗的,redis作为一个高效的数据库,是不允许有任何不高效行为的!同时多数据库还会让开发、调试、运维变的复杂,所以在实践中使用0号数据库是一个不错的选择!这样既保证了CPU的高效利用,也保证了redis服务本身的高效性;
2. 查看当前数据库的键的个数
dbsize
3. 清除数据库
flushdb/flushall
flushdb: 清除当前数据库中的元素;
flushall: 清除所有数据库中的元素;
注意: 永远不要在线上环境,执行这两个命令,这是两个非常危险的命令!