一、认识分布式
1.认识Redis
什么是Redis,我们先看官方给的定义:
tex
The open source,in-memory data store used by millions of developers as a database,cache,streaming engine,and message broker.
1.什么叫做in-memory data store?
在内存中存储数据,那我们之前在学习语言的时候,我们定义一个变量,那不也是在内存中存储数据嘛?是这样的,如果只是单机程序的话,直接通过变量存储数据的方式确实是比使用Redis更优的选择,我们的主角Redis主要是用在分布式系统当中,在之前的学习中,我们知道,进程与进程之间是具有隔离性的,两个不相干的进程是无法访问他们相互之间的数据的,Redis就算基于网络,可以把自己内存中的变量给别的进程,甚至别的主机的进程进行使用,即:Redis通过网络来完成两个进程之间的通信过程。
2.什么叫做database,cache?
我们对比MySQL,MySQL最大的问题就在于,访问速度比较慢,在很多互联网产品中对于性能要求是很高的。 而Redis也可以作为数据库使用,他的最大优点就算快!因为访问内存的速度是越高与访问硬盘速度的。所以在定性的角度,我们可以知道Redis要快很多,但是很难定量衡量。 但和MySQL相比,Redis最大的劣势就在于,他的存储空间是有限的!因为互联网对于性能要求高的产品笔记占少数,大部分对于性能要求还是没有那么高的。 那有没有存储空间又大,速度又快的方案呢?有的,典型的方案就是可以把Redis和MySQL结合起来使用,根据"二八原则",一般情况下20%热点数据就能够满足80%的访问需求。所以我们可以只把热点数据存放到Redis当中,把所有的数据存放到MySQL中,将Redis当中一种"cache"来使用。但这样的话系统的复杂程度就大大提升了,而且,如果数据发生了修改,还要涉及到Redis和MySQL之间的数据同步的问题。
3.什么是streaming和message broker呢?
这个Redis的初心,最初就是用来作为一个"消息中间件"的(消息队列),作为一种分布式系统下的生产者消费者模型。但是当前很少人会直接使用Redis作为消息中间件了(因为业界有更多更专业的消息中间件使用)
2.浅谈单机架构
关于分布式系统,大家千万不要把所谓的"分布式"想的太复杂,太高大上了 所谓的单机架构,就是只有一台服务器,这个服务器负责所有的工作。我们可以看下图:假定这是一个电商网站
上图应用服务指的是我们写的服务器程序: 例如:C++的cpp-httplib ,java的Spring,还有MySQL的客户端程序
上图的数据库服务指的是: MySQL的本体,即MySQL服务器(存储和组织数据的部分),因为MySQL是一个客户端服务器结构的程序!
Q:在单机程序中,能不能把数据库服务器也去掉,光一个应用服务器既负责业务,又负责数据存储呢? 也不是不可以,但是就是会比较麻烦
大家千万不要瞧不上这个单机架构,绝大部分的公司的产品,都是这种单机架构!因为现在的计算机硬件,发展速度非常之快,哪怕只有一台主机,这一台主机的性能也是很高的。但如果业务进一步增长,用户量和数据量都水涨船高,一台主机难以应付的时候,这时候就需要引入更多的主机,引入更多的硬件资源。
3.浅谈分布式是什么
我们知道,一台主机的硬件资源是有上限的!!!,包括不限于以下几种:1.cpu 2.内存 3.硬盘 4.网络 5.... 服务器每次收到一个请求,都是需要消耗上述的一些资源的,如果同一时刻,处理的请求多了,都可能会导致服务器处理请求的时间会变长,甚至于处理出错。
如果我们真的遇到了这样的服务器不够用的场景,怎么处理呢?
- 开源:简单粗暴,添加更多的硬件资源,但一个主机上面能增加的硬件资源也是有限的,这取决于主板的扩展能力,当一台主机扩展到极限了,但是还不够,就只能引入多台主机了!不是说新的机器买来就直接可以解决问题了,也需要在软件上做出对应的调整和适配,一旦引入多台主机了,咱们的系统就可以称之为"分布式系统"。
- 节流:在软件上优化,(这就各凭本事了,需要通过性能测试,找到是哪个环节出现了瓶颈,再去对症下药)
注意:引入分布式,这是万不得已的无奈之举,系统的复杂度会大大提高,出现bug的概率也越高,加班的概率 & 丢失年终奖的概率也随之提高。
4.数据库分离和负载均衡
引入多台服务器后,我们自然可以想到将应用服务和数据库服务分离。如下图:
对于应用服务器来说:里面可能会包含很多的业务逻辑,这可能会吃比较多的CPU和内容。
对于数据库服务器,需要更大的硬盘空间,更快的数据访问速度,可以配置更大硬盘的服务器,甚至还可以上SSD硬盘。
通过调整他们的配置,从而达到更高的性价比。一般来说我们会引入更多的应用服务器节点,因为应用服务器吃CPU和内存,如果CPU或者内存吃没了,此时应用服务器就顶不住了。
什么是负载均衡:
用户的请求,先达到负载均衡器/网关服务器(这是一个单独的服务器),假设有1w个用户请求,有2个应用服务器,此时按照负载均衡的方式,就可以让每个应用服务器承担5k的访问量。这个事情就和之前讲过的"多线程"有点像。 对于负载均衡器来说,也有很多的负载均衡具体的算法。例如:如果一个用户总数重复提出同一个请求,那么负载均衡器便会解析他的ip,将他的请求分配到同一个服务器上,以提高服务器的响应速度。
5.理解负载均衡
还是上面那个1w请求的例子,虽然两个应用服务器各自承担5k的任务,但是这个负载均衡器,看起来不是承担了所有的请求嘛,这个东西能顶得住嘛???
负载均衡器,对于请求量的承担能力,要远超过应用服务器的。就好比负载均衡器是领导,分配工作即可,而应用服务器是组员,需要执行任务。执行任务的压力和耗时肯定是远高于分配人物的。
那是否会出现请求量达到连负载均衡器也扛不住了呢??也是有可能的!!!这时候可以考虑引入更多的负载均衡器(引入多个机房)
举个例子:学校是服务同学的,每个学校有他们的教务团队,负责日常作业的跟进,跟进一个同学,势必要花一定的时间,一个助教老师,就是一个应用服务器。根进一个同学,就相当于处理一个请求。当同学数量很多时,就需要引入更多的助教老师。 而引入负载均衡器,就承担了校长的角色,校长给每个助教老师分配任务,随着同学更多了,一个校长分配任务也分配不过来了,就可以划分出多个校区,每个校区都有各自的校长,每个校区里又有一批助教 但是!当人变多了,管理成本就会提高,出现问题的概率也会提高,当机器也是如此。
6.数据库读写分离
如上面讨论,增加应用服务器,确实能够处理更高的请求量,但是随之存储服务器要承担的请求量也就更多了! 咋办呢?可以开源(引入更多的机器,简单粗暴)+节流(门槛高,更复杂),且看下图:
我们知道,在实际的应用场景中,数据库的读频率是要比写频率更高的!故数据库服务器可以采用读写分离的方式,主服务器一般是一个用于数据的更新修改操作,从服务器可以有多个(一主多从)负载数据的读取操作,主服务器实时或者定期的将数据同步到从服务器上,同时从服务器通过负载均衡的方式,让应用服务器进行访问。
7.引入缓存
数据库天然有个问题,响应速度是更慢的!我们将数据区分"冷热",热点数据放到缓存中,缓存的访问速度往往比数据库要快多了!
如上图,我们的主从服务器,存储的仍然是完整的全量数据,而我们引入的缓存服务器,只是放一小部分的热点数据(会频繁被访问到的数据),根据二八原则:20%的数据能够支持80%的访问量【出自经济学:20%的人持有80%的股份】,甚至更极端情况可以达到一九,实际场景不同会略有差异。这里引入的缓存服务器通常就是Redis,此时,缓存服务器就帮助数据库服务器负重前行! 那么代价是?既然引入缓存,必然不会让其与主数据库直接进行同步数据,因为主数据库的效率会拉低缓存数据库的效率,那么缓存数据库中的数据如何保证其与主数据库一致呢?这是一个很大的问题。
8.数据库分库分表
我们引入分布式系统,不光要能够去应对更高的请求量(并发量),同时也要能应对更大的数据量。那么是否会出现,一台服务器已经存不下数据了呢???当然会存在!!! 虽然一个服务器,存储的数据量可以达到几十个TB,即使如此也可能会存不下(例如短视频文件),一台主机存不下,那么就需要多台主机来进行存储。我们见下图的结构:
我们针对数据库进一步的拆分,即分库分表操作。本来应该数据库服务器,这个数据库服务器上有多个数据库(这里数据库指的是逻辑上的数据集合,即create database创建的那个东西) 现在就可以引入多个数据库服务器,每个数据库服务器存储一个或者一部分数据库。 如果某个表特别大,大到一台主机存不下,也可以针对表进行拆分,具体分库分表如何实践?还是需要结合实际的业务场景来展开,业务是互联网公司最重要的东西,技术只是给业务提供支持的,业务决定了技术!
9.引入微服务
我们来看一下什么是微服务架构
在之前的应用服务器中,一个服务器程序里面做了很多的业务(比如电商,一台服务器上既做用户和商品,又做电商功能),这就可以会导致这一个服务器的代码变的越来越复杂。 为了更方便代码的维护,就可以把这样的一个负载的服务器,拆分成更多的,功能更单一,但是更小的服务器。这样的服务器叫做微服务,这种服务器的种类和数据就增加了。 我们想想,引入微服务到底是在解决什么问题,其实本质上是在解决"人的问题",啥时候会涉及到"人"的问题??大厂!!如果是小公司,就两三个开发,此时搞微服务就没有太大必要了。 当应用服务器复杂了,势必就需要更多的人来维护了,当人多了,就需要配套的管理,把这些人组织好,通过划分组织结构,分成多个组(就需要进行分工),每个组分别配备领导进行管理。 按照功能,拆分为多组微服务,就可以有利于上述人员的组织结构的分配了。
那么引入微服务,解决了人的问题,付出的代价?
- 系统的性能下降(要想保证性能不下降太多,只能进入更多的机器,更多的硬件资源 => 充钱),因为拆出来更多的服务,多个功能之间更需要依赖网络通信,网络通信的速度很可能是比硬盘还慢的!!!(不过幸运的是,硬件技术的发展,网卡现在有万兆网卡,读写速度已经能够超过硬盘读写了,但也贵!)
- 系统复杂程度提高,可用性受到影响,服务器更多了,出现问题的概率就更大了,这就需要一系列的手段来保证系统的可用性,(比如更丰富的监控报警,以及配套的运维人员)
最后总结下微服务的优势:
- 解决了人的问题
- 使用微服务,可以更方便与功能的复用
- 可以给不同的服务进行不同的部署
10.补充概念
-
应用(Application)/系统(System)
一个应用,就是一个/组 服务器程序
-
模块(Module)/组件(Component)
一个应用,里面有很多个功能,每个独立的功能,就可以称为是一个模块/组件
-
分布式(Distriibuted)
引入多个主机/服务器,协同配合完成一系列的工作,(这里的多个指的是物理上的多个主机)
-
集群(Cluster)
引入多个主机/服务器,协同配合完成一系列的工作,(这里是逻辑上的多个主机)
-
主(Master)/从(Slave)
分布式系统中一种比较典型的结构,多个服务器节点,其中一个是主,另外的是从,从节点的数据要从主节点这里同步过来
-
中间件(Middleware)
和业务无关的服务(功能更通用的服务)
- 数据库
- 缓存
- 消息队列
- ...
-
可用性(Availablity)
系统整体可用的时间/总的时间(这是一个系统的第一要务)
-
响应时长(Response Time RT)
衡量服务器的性能,这个指标越小越好,通常是和具体服务器要做的业务是密切相关的。
-
吞吐(Throughput) vs 并发(Concurrent)
衡量系统的处理请求的能力,衡量性能的一种方式
11.分布式总结
-
单机架构(应用程序+数据库服务器)
-
数据库和应用分离
应用程序和数据库服务器,分别放到了不同主机上部署了
-
引入负载均衡优化应用服务器 => 集群
通过负载均衡器,把请求比较均匀的分发给集群中的每个应用服务器,并且当集群中的某个主机挂了,其他主机仍然可以承担服务,提高了整个系统的可用性
-
引入读写分离,数据库主从结构
一个数据库节点作为主节点,其他N个数据库节点作为从节点,主节点负责写数据,从节点负责读数据,主节点需要把修改过的数据同步给从节点。
-
引入缓存,冷热数据分离
进一步提升了服务器针对请求的处理能力,依据的是二八原则。Redis在一个分布式系统中,通常就扮演着缓存这样的角色。但引入的问题是:数据库和缓存的数据一致性问题。
-
引入分库分表,数据库能够进一步扩展存储空间
-
引入微服务,从业务上进一步拆分应用服务器
从业务功能的角度,把应用服务器,拆分成更多的功能更单一,更简单,更小的服务器
上述的这样的几个演化的步骤,只是一个粗略的过程。实际上一个商业项目,真实的演化过程,都是和他的业务发展密切相关的,业务是更重要的,技术只是给业务提供支持的。所谓的分布式系统,其实就算想办法引入更多的硬件资源而已!
二、Redis背景知识
1.Redis特性介绍
Redis是一个在内存中存储数据的中间件,用于作为数据库,用于作为数据缓存,在分布式系统中能够大展拳脚,接下来我将讲解一些Redis的一些特性(优点):
-
In-memory data structures
在内存中存储数据,官方原话:support for strings,hashes,lists,sets,sorted sets,streams,and more.在Redis中,数据是按照一种kv的结构存储的,key都是string,而value则可以是上述的这些数据结构。
这时我们可以对比MySQL:MySQL主要是通过"表"的方式来存储数据的,我们一般称之为"关系型数据库"
而Redis主要是通过"键值对"的方式来存储组织数据的,我们称之为"非关系型数据库"
-
Programmability
官网:Server-side scripting with Lua and server-side storted procedures with Redis Functions 这里的Lua也是一个编程语言。针对Redis的操作,可以直接通过简单的交互式命令进行操作,也可以通过一些脚本的方式,批量执行一些操作(可以带有一些逻辑)
-
Extensibility
A module API for building custom extensions to Redis in C,C++,and Rust 即:可以在Redis原有的功能基础上再进行扩展,Redis提供了一组API,可以通过上述的几个语言编写Redis扩展(本质上就是一个动态链接库,好比Windows上的dll,可以让exe去调用里面包含的很多代码,Linux上的动态库是.so 虽然和dll格式不同,但本质是一样的)。我们可以自己去扩展Redis的功能,比如:Redis自身已经提供了很多的数据结构和命令,通过扩展,让Redis支持更多的数据结构以及支持更多的命令。
-
Persistence
Keep the dataset in memory for fast access but can also persist all writes to permanent storage to survive reboosts and system failures. 持久化,Redis是把数据存储在内存上的,但是进行的退出或者系统重启会导致内存上的数据"丢失",所以Redis也会把数据存储在硬盘上一份,内存为主,硬盘为辅。(硬盘相当于对内存的数据备份了一下,如果Redis重启了,就会在重启时加载硬盘中的备份数据,使Redis的内存恢复到重启前的状态)
-
Clustering
Horizonta scalability with hash-based sharding,scaling to millions of nodes with automatic repartitioning when growing the cluster. Redis作为一个分布式系统中的中间件,能够支持集群是很关键的,这个水平扩展,类似于是"分库分表",一个Redis能存储的数据是有限的(内存空间有限),引入多个主机,部署多个Redis节点,每个Redis存储数据的一部分。
-
High availability
Replication with automatic failover for both standalone and clustered deployments 高可用 => 冗余/备份 Redis自身也是支持"主从"结构的,从节点就相当于主节点的备份了。
一个字总结Redis,那就是快!那为啥Redis更快呢?
- Redis数据在内存中,就比访问硬盘的数据库(点名MySQL)要快很多。
- Redis核心功能都是比较简单的逻辑,核心功能都是比较简单的操作内存的数据结构
- 从网络角度上,Redis使用了IO多路复用的方式(epoll,使用一个线程来管理很多个socket)
- Redis使用的是单线程模型(虽然更高版本的Redis引入了多线程),这样的单线程模型,减少了不必要的线程之间的竞争开销。因为多进程提高效率的前提是:CPU密集型的任务,使用多个线程可用充分的利用CPU多核资源。但是Redis的核心任务,主要就是操作内存的数据结构,不会吃很多的CPU。而使用多进程势必要考虑线程安全,锁等问题。
- [个人不认同,网上说的很多]Redis是使用C语言开发的,所以就快。但我觉得MySQL也是C语言开发的,为啥没那么快呢?
2.Redis的应用场景
Redis的Use cases主要分为下面三种情况:
-
Real-time data store
这种使用场景下把redis当做了数据库,在大多数情况下,考虑到数据存储,优先考虑的是"大",但是仍然有一些场景,考虑的是"快",就比如做一个搜索引擎 -> 广告搜索(商业搜索),对于性能要求是非常高的,搜索系统中没有用到MySQL这样的数据库。把所有需要检索的数据都存储在内存中,就使用的是类似Redis这样的内存数据库来完成的。当然,使用这样的内存数据库,存储大量的数据,需要不少的硬件资源的(需充值)。在这种使用场景下Redis存的是全量数据,这里的数据是不能随便丢的。
-
Caching & session storage
使用MySQL存数据,大,慢,根据二八原则,把热点数据拎出来,存储在redis中。在这种使用场景下,Redis存的是部分数据,全量数据都是以mysql为主的,哪怕Redis的数据都没了,还可以从mysql这边再加载回来,这便是Redis作为Cache的应用场景。当Redis作为会话存储时候,我们回想起学习http时,cookie => 实现用户身份信息的保存(只是在浏览器这边存储了一个用户的身份标识,一般称为sessionid),是需要session配合的(服务器这里真正的存储了用户数据),之前我们的session都是存储在应用服务器上的,那么我们的分布式服务器如何存储的呢?
我们可能会遇到这样一个问题:用户第一次登录时,负载均衡器把会话放到了应用服务器1中,那么下一次会话,负载均衡器分配了应用服务器2来完成任务,那用户岂不是又要重新登录啦?如何解决这个问题那?
- 想办法让负载均衡器,把同一个用户的请求始终打到同一个机器上(不能轮询了,而是要通过userid之类的方式来分配机器)
- 把会话数据单独拎出来,放到一组独立的机器上存储(Redis),这样的话,当应用程序重启了,会话也不会丢失,这才是我们理想中的情况。
-
Streaming & messaging
Redis作为消息队列(这里是一种服务器,此处咱们说到的消息队列,不是Linux进程间通信的那个消息队列)。基于这个可以实现一个网络版本的生产者消费者模型。对于分布式系统来说,服务器和服务器之间,有时候也需要使用到生产者消费者模型的,优势在于:1.解耦合 2.削峰填谷。业界也有很多知名的消息队列,比如RabbitMQ,Kafka,RocketMQ等,Redis也是提供了消息队列的功能的。如果在当前场景中,对于消息队列的功能依赖不是很多,并且又不想引入额外的依赖了,Redis可以作为一个选择。
3.Redis背景知识小结
我们刚刚说了Redis的这么多优点,那有没有Redis他不能做的事情呢?那就是存储大规模数据。根据之前所学内容,对于分布式系统,我们已经有了一个初步的认识,要想进一步了解,一定要去公司!(中厂,大厂),因为在不同的业务场景下,分布式系统的具体实践方式是差异很大的。
我们一句话来总结Redis就是,他是一个使用内存存储数据的中间件,一般被用作 内存数据库/缓存/消息队列 来使用。
三、Redis环境搭建
1.版本选择说明
我这里选择的Redis 5系列,在Linux中进行安装。因为Redis官方是不支持Windows版本的,如果想使用Windows版本的Redis,可以考虑看看微软维护的Windows版本的Redis分支。
我这里开始学习的时候,先在本机上安装(Centos 和 Ubuntu),后面学习到Redis的集群相关的功能的时候,再使用Docker。
2.在centos上安装Redis
在 Centos上安装Redis 5(2018年底发布,用的多),如果是Centos8,yum 仓库中默认的redis版本就是5,直接yum install 即可 ~
1.换源安装Redis 5
如果是Centos7 ,yum 仓库中默认的redis 版本是3系列,比较老~
此处我们需要安装额外的软件源。我们这里选择的是scl源,所以我们首先安装scl源,再去安装redis,(记得安装要以root身份,或者普通用户su root/sudo)
shell
yum install centos-release-scl-rh
此时,我们再根据特定的命令,才能去安装我们的redis 5
shell
yum install rh-redis5-redis
一路 y 下去就安装好啦。
2.创建符号链接
我们发现,Redis 5 默认安装的目录为 /opt/rh/rh-redis5/root/usr/bin/
, 藏的太深了,不方便我们使用,我们可以通过符号链接, 把需要用到的关键内容设置到方便使用的目录中。符号链接,就是"快捷方式",在linux下通过ln -s 指令。操作步骤为下面几个步骤:
shell
cd /usr/bin
ln -s /opt/rh/rh-redis5/root/usr/bin/redis-server ./redis-server
ln -s /opt/rh/rh-redis5/root/usr/bin/redis-sentinel ./redis-sentinel
ln -s /opt/rh/rh-redis5/root/usr/bin/redis-cli ./redis-cli
执行完发现,这几个符号链接都已经创建成功了。针对配置文件,我们也需要创建符号链接,步骤如下:
shell
cd /etc/
ln -s /etc/opt/rh/rh-redis5/ ./redis
3.修改配置文件
首先我们要进入到我们刚刚创建好的符号链接的redis这个目录,然后打开这个目录下的.conf配置文件。
shell
cd redis
vim redis.conf
接下来主要修改下面几个部分:
-
设置 ip 地址
texbind 0.0.0.0
-
关闭保护模式
texprotected-mode no
-
启动守护进程
texdaemonize yes
什么是守护进程呢?我们的服务器程序,一般都会以"后台进程"(守护进程)的方式运行,Linux中的进程,分为"前台"和"后台"进程,前台进程会随着终端的关闭而随之被杀死,后台进程不会随着终端的关闭而关闭。
-
设置工作目录
我们先创建一个工作目录(这个目录是用于保存redis运行是生成的文件的)
shellmkdir -p /var/lib/redis
再在配置文件中,设置工作目录
texdir /var/lib/redis
-
设置日志目录
和上面操作差不多,我就不演示了,先创建日志目录
shellmkdir -p /var/log/redis/
再在配置文件中,设置日志目录
texlogfile /var/log/redis/redis-server.log
4.启动Redis
终于配置好了,接下来我们可以直接通过命令来启动Redis服务器啦!
shell
redis-server /etc/redis/redis.conf
我们可以通过 netstat
指令,来看一下redis服务器的启动情况。
shell
netstat -anp | grep redis
5.停止Redis
直接通过进程id ,使用kill指令来停止redis服务器的运行
我们上面时候需要停止(重启)Redis呢?当我们每次修改redis的配置文件时,都需要重启才能使得新的配置生效!
最后,我们来看一看redis生成的日志吧
我们再去看一下redis的工作目录吧:
以上就是我们在centos 7上安装Redis服务器的全部过程啦!如果你是centos 8,那么安装会方便很多,不用这么麻烦滴。因为博主没有ubantu的云服务器环境,这里就不再演示ubantu的安装了,都是大同小异的,后续我们会通过使用Docker进行安装,就不必再考虑这些平台问题啦。
3.Redis客户端介绍
我们的redis和MySQL一样,他也是一个客户端-服务器 结构的程序!redis 客户端和服务器可以在同一个主机上,也可以在不同的主机上(当前阶段,大家一般只有一台机器,此时客户端和服务器就是在同一台机器上的)
而Redis的客户端也有很多种形态,例如:
-
自带了命令行客户端【当前学习时主要使用这个客户端】
redis-cli:有两种连接方式
shellredis-cli redis-cli -h 127.0.0.1 -p 6379
-
图形化界面的客户端(桌面程序,web程序) 像这样的图形化程序,依赖windows系统,而未来在实际工作中,你用来办公的windows系统,连接到服务器可能会有诸多限制(中间可能会经历很多的跳板机,堡垒机,权限校验等),你的windows上的图形化界面客户端能不能连上你们的服务器里的redis,这是一个未知数!(这也和MySQL同理)
-
基于Redis的api自行开发客户端【工作中最主要的形态】
非常类似于MySQL的C语言API和JDBC
咱们谈到的redis的快,是相对于MySQL这样的关系型数据库的,但是如果是直接和内存中的操作变量相比,就没有优势了,甚至更慢了。我们举个例子:在一个单机系统中,应用程序要存储一些数据,比如存储一下用户点赞数,视频id,点赞个数,按照键值对格式来存储。那么,我们是用一个redis来存储,还是直接在内存中搞一个hash map来存储呢?? 我们知道,使用hash map是直接操作内存,而使用redis是先通过网络!再操作内存 在上述场景中,是否要使用redis?要结合实际的需求来确定!引入redis的缺点,会变慢,但是有了redis之后,就可以把数据单独存储,后续应用服务器重启,不会影响到数据内容,在未来要扩展成分布式系统,使用redis是更佳的
四、Redis通用命令
1.使用官网文档
接下来我们来学习Redis的实战操作,通过redis-cli 客户端和redis服务器交互,涉及到很多的redis命令,redis的命令非常多,需要我们:
- 掌握常用命令(多操作多练习)
- 学会使用redis文档
2.get和set指令
Redis 中最核心的两个命令,get 是根据 key 来取 value,set 是把key 和 value 存储进去。在redis中,是按照键值对的方式来存储数据的。 我们必须先进入redis-cli 客户端程序,才能输入 redis 命令。
对于上述这里的 key value,不需要加上引号,就是表示字符串的类型。当然,如果要是给key 和 value 加上引号,也是可以的(单引号或者双引号都行),并且在redis中,命令是不区分大小写的。
而 get 命令直接输入key,就能得到value。如果当前key不存在,就会返回nil,和null/NULL是一个意思。
3.全局命令keys
Redis支持很多种数据结构,整体来说,redis 是键值对结构,key 固定就是字符串,value实际上会有很多种类型(字符串,哈希表,列表,集合,有序集合),操作不同的数据结构就会有不同的命令。而全局命令,就是能够搭配任意一个数据结构来使用的命令。
关于keys指令:用来查询当前服务器上匹配的key,通过一些特殊符号(通配符)来描述key的模样,匹配上述模样的key,就能被查询出来。常用命令:|
shell
KEYS pattern
其中pattern:是包含特殊符号的字符串,有的地方翻译成"样式"或者"模式",存在的意义就是去描述另外的字符串长啥样的。那pattern具体是咋写的呢,我们来举一些例子,首先我们向数据库中插入一些数据。
-
?:匹配任意一个字符
-
*:匹配0个或者多个任意字符
-
[abcde] 只能匹配到 a b c d e,别的不行,相当于给出固定的选项了
-
[^e] :排除e,只有 e匹配不了,其他的都能匹配
-
[a-b]:匹配a-b这个范围的字符,包括两侧边界
上述的匹配规则,大家都不要刻意的去背;用的时候差一下就好啦
注意事项: keys命令的时间复杂度是O(N),所以,在生产环境上,一般都会禁止使用keys命令,尤其是大杀器keys *(查询redis中所有的keys!!!)
因为:生产环境上的key可能会非常多!而redis是一个单线程的服务器,执行keys * 的时间非常长,就使redis服务器被阻塞了,无法给其他客户提供服务!(这样的后果可能是灾难性的),因为redis经常会被用于缓存,挡在mysql的前面,替mysql负重前行的人,万一redis被一个keys * 阻塞住了,此时其他的查询redis 的操作就超时了,此时这些请求就会直接查数据库,突然一大波请求过来,mysql措手不及,就容易挂了。整个系统就基本瘫痪了。如果你要是没能及时发现,及时恢复的话,那就麻烦啦!
4.生产环境的概念
未来在工作中会涉及到几个环境:
-
办公环境(入职公司之前,公司给你发个电脑)
笔记本(windows,mac)/台式机,现在办公电脑,一般8核16G内存512G磁盘
-
开发环境,有的时候,开发环境和办公环境是一个,一般是做前端/做客户端的。有的时候,开发环境是单独的服务器(28核128G内存,4T硬盘这样的)一般是后端程序,为什么呢?
- 编译一次时间特别久(C++) => C++ 23才引入 module,这个问题 #include 要接锅,所以考虑使用高性能服务器进行编译。
- 有的程序一启动要消耗很多的cpu和内存资源,办公电脑难以支撑。一般的商业搜索项目,启动起来要吃100G的内存
- 有的程序比较依赖linux,在windows环境搭不起来。
-
测试环境(测试工程师使用的,一般也是28核128G4T配置)
-
线上环境/生产环境
(办公环境,开发环境,测试环境,也统称为线下环境),外界用户无法访问到的。线上环境则是外界用户能够访问到的。一旦生产环境上出问题,一定会对用户的使用产生影响。未来咱们去操作线上环境的任何一个设备/程序都要怀着12分的谨慎!
5.exists指令
exists 指令是判定 key 是否存在的。语法格式如下:
shell了
EXISTS key [key ... ]
返回值:key 存在的个数。键值对存储的体系中(类似哈希表),key 得是唯一的呀,但是针对多个key来说,这是非常有用的。
时间复杂度:O(1),因为redis组织这些key就是按照哈希表的方式来组织的。
redis支持很多数据结构 => 指的是一个value 可以是一些复杂的数据结构,redis自身的这些键值对,是通过哈希表的方式来组织的,redis具体的某个值,又可以是一些数据结构。
我们要注意:redis是一个客户端-服务器 结构的程序,客户端和服务器之间通过网络来进行通信!所以我们考虑下下面两条指令有什么不同之处:
区别在于:分开的写法,会产生更多轮此时的网络通信。网络通信和直接操作内存相比,效率比较低,成本比较高。因为网络通信时需要封装和分用。 进行网络通信的时候,发送方发送一个数据,这个数据从应用层,到物理层,层层封装(每一层协议都要加上报头或者报尾)=> 发一个快递,要包装一下,要包装好几层。 接收方收到一个数据,这个数据要从物理层,到应用层层层分用(把每一层协议的报头或者尾给拆掉) => 收到一个快递,要拆快递,要拆很多层。而且网卡是 IO 设备,更何况你的客户端和服务器都不一定在一个主机上,中间可能隔着很远。 redis自身也非常清楚上述问题,所以redis的很多命令都是支持一次就能操作多个key 的/多种操作。
6.del指令
del(delete) 删除指定的 key,可以一次删除一个或者多个
shell
DEL key [key ... ]
时间复杂度:O(1)
返回值:删除掉的key的个数
值得注意的是:之前学mysql的时候,当时强调,删除类的操作,比如 drop database,drop table,delete from ...,都是非常危险的操作,一旦删除了之后,数据就没有了。 但是redis主要的应用场景,就是作为缓存。此时redis里存的只有一个热点数据,全局数据是在mysql数据库中,此时,如果把redis中的key删除了几个,一把来说,问题不大。 但是,当然如果把所以数据或者一大半数据一下子都干没了,这种影响就会很大,(本来redis是帮MySQL负重前行,redis没数据了,大部分的请求就直接打给mysql,然后就容易把mysql 搞挂掉)。相比之下,如果是mysql这样的数据,哪怕误删了一个数据,都可能是影响很大的。 如果是把redis作为数据库,此时误删数据的影响就大了! 如果是把redis作为消息队列(mq),这种情况误删数据的影响大不大,就要具体问题具体分析了。
7.expire和ttl指令
expire作用是给指定的key设置过期时间,key存活时间超出这个指定的值,就会被自动删除。设置的时间单位是秒。 为什么会有这个指令呢?因为很多业务场景,是有时间限制的,比如我们的手机验证码,通常都是5分钟有效。点外卖,优惠券,也都是指定时间之内有效。 包括后面基于redis实现分布式锁,为了避免出现不能正确解锁的情况,通常都会在加锁的时候设置一下过期时间(所谓的使用redis 作为分布式锁,就是给redis 里写一个特殊的key value) expire指令格式:
shell
EXPIRE key seconds
PEXPIRE key 毫秒级别,对于计算机来说,秒是一个非常长的时间
注意:此处的设定过期时间,必须是针对已经存在的key设置。设置成功返回1,设置失败返回0
时间复杂度:也是O(1)
对于ttl(time to live)指令,作用是查看当前key的过期时间还剩多少(以秒为单位),与此对应的还有pttl(以毫秒为单位)。我们之前学习网络原理,IP协议的时候,IP协议的报头中,就有一个字段,TTL,不过IP中的TTL不是用时间来衡量过期的,而是通过次数。
8.Redis的过期策略【经典面试题】
redis的key的过期策略是怎么实现的?【经典面试题】
一个redis中可能同时存在很多很多key,这些key中可能有很大一部分都有过期时间,此时,redis服务器咋知道是哪些key已经过期要被删除,哪些key还没过期??
如果直接遍历所有的key,显然是行不通的,效率非常低。所以redis整体的策略就是:
-
定期删除
每次抽取一部分,进行验证过期时间,保证这次抽取检查的过程足够块,为啥这里对于定期删除的时间,有明确的要求呢?因为redis是单线程的程序,主要的任务(处理每个命令的任务,扫描过期key ...),如果扫描过期key消耗的时间太多了,就可能导致正常处理请求命令就被阻塞了(产生了类似于执行keys * 这样的效果)
-
惰性删除
假设这个key已经到过期时间了,但是暂时还没删他,key还存在。紧接着,后面又一次访问,正好用到了这个key,于是这次访问就会让redis服务器触发删除key的操作,同时再返回一个nil
-
内存淘汰策略
虽然有了上述两种策略结合,但整体的效果一般,仍然可能会有很多过期的key被残留了,没有及时删除掉。redis为了对上述进行补充,还提供了一些列的内存淘汰策略。这个暂时不介绍。
-
定时删除【网上的错误说法】
含义:在设置Key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。【×】
- redis中并没有采取定时器的方式来实现过期key删除
- 如果有多个key过期,也可以通过一个定时器(基于优先级队列或者基于时间轮都可以实现比较高效的定时器)来高效/节省cpu的前提下来处理多个Key。
为啥redis没有采取这种定时器的方式呢?很难考证为啥,个人猜测,基于定时器的实现,势必就要引入多线程了,redis早期版本就是奠定了单线程的基调,引入多线程就打破了作者的初衷。
9.定时器实现原理【扩展】
定时器:在某个时间到达之后,执行指定的任务,我们来看看定时器如何实现
-
基于优先级队列/堆
正常的队列是先进先出,优先级队列则是按照指定的优先级先出
啥叫优先级高?这是我们自定义的
在redis过期的key的场景中,就可以通过"过期时间越早,就是优先级越高"
现在假定有很多key设置了过期时间,就可以把这些key加入到一个优先级队列中,指定优先级规则是过期时间早的,先出队列,优先级队列的队首元素,就是最早的要过期的key!!!举个例子:key1:12:00 key2:13:00 key3:14:00 此时定时器中只要分配一个线程,让这个线程去检查队首元素,看是否过期即可!!! 如果队首元素还没过期,后续元素一定没过期。此时扫描线程不需要遍历所有key,只盯住这一个队首元素即可!!另外在扫描线程检查队首元素过期时间的时候,也不能检查的太频繁。此时做法就是可以根据当前时刻和队首元素的过期时间,设置一个等待。当时间差不多到了,系统再唤醒这个线程。 此时 扫描线程 不需要高频扫描队首元素,把cpu的开销也节省下来了。 万一在线程休眠的时候,来了一个新的任务,是11:30 要执行。可以在新任务添加的时候,唤醒一下刚才的线程,重新检查一下队首元素,再根据时间差距重新调整阻塞时间即可。
-
基于时间轮实现的定时器
首先,我们要把时间划分为很多个小段(划分的粒度,要看实际需求),每个小段上都挂着一个链表(通过函数指针实现),每个链表都代表一个要执行的任务。比如我们把时间粒度设置为100ms,假设需要添加一个key,这个 key 在300 ms之后过期,那么他将会被插入到第三个位置,如下图:
此时这个起始位置的这个指针,就会每隔固定的间隔(此处是约定100ms),每次走到一个格子,就会把这个格子上的链表的任务尝试执行一下,对于时间轮来说,每个格子是多少时间,一共多少个格子,都是需要根据实际场景,灵活调配的。假设指定的过期时间特别特别长,比如3000ms,就可以通过对格子的数目取模 来确定位置。
此处大家一定要注意了!!! Redis并没有采取上述的方案!!但是要了解这两种方案,都是属于高效的定时器的实现方式,很多场景可能都会用到,至于为什么redis没有采用这个机制呢,博主觉得是因为redis最初还是想通过单线程来实现,引入这些机制,可能需要引入多进程才能实现,违背了初心。在redis源码中,有一个比较核心的机制,是事件循环
10.type指定
type:返回key对应的数据类型,此处redis 所有的key 都是string ,key 对应的value可能会存在多种类型。存在【none,string(字符串),list(列表),set(集合),zset(有序集合),hash(哈希) and stream(这个是redis作为消息队列的时候,使用这个类型的value)】,在redis中,上述类型操作方式差异很大,使用的命令,都是完全不同的。我们来操作示范一下:
11.小结
此前已经学习了redis 中几个基本的全局命令,这里我们来总结一下:
-
keys:用来查看匹配规则的key(要注意使用keys的风险)
-
exists:用来判定指定key是否存在
-
del:删除指定的key(注意del的风险)
-
expire:给key设置过期时间(redis key过期策列如何实现,三步骤)
-
ttl:查询key的过期时间(定时器的实现思路)
-
type:查询key对应的value的类型
-
围绕每个数据结构来介绍相关命令
当前版本的redis支持10个数据类型,非常通用的数据结构【strings,lists,sets,hashes,sorted sets】,针对特殊场景下的特殊数据类型【streams,geospatial,hyperloglog,bitmaps,bitfields】
五、Redis常用数据结构
1.认识数据类型和编码方式
redis中存在多种数据类型,见下图:
redis底层在实现上述数据结构的时候,会在源码层面,针对上述实现进行特定的优化(内部的具体实现的数据结构(编码方式),还会有变数),来达到 节省时间/节省空间 的效果。就好比指鼠为鸭:鸭脖店承诺卖给你的这个东西吃起来和鸭脖是一样的,但是内部的数据结构,是否真的是鸭脖,可能会根据实际情况,做出一定的优化。
redis承诺,现在我这有个hash表,你要进行查询,插入,删除,操作,都保证O(1),但是这个背后的实现,不一定就是一个标准的hash表,可能在特定场景下,使用别的数据结构实现,但是仍然保证时间复杂度符合承诺!!!
数据结构:redis承诺给你的,也可以理解成数据类型。 编码方式:redis内部底层的实现。同一个数据类型,背后可能的编码方式是不同的,会根据特定场景优化。redis会自动适应,程序员在适应redis的时候一般感知不到。
数据类型 | 内部编码 | 说明 |
---|---|---|
string | raw | 最基本的字符串(底层就是持有一个char数组) |
int | redis通常也可以用来实现一些"计数"这样的功能,当value就是一个整数的时候此时可能redis会直接使用int来保存 | |
embstr | 针对短字符串进行的特殊优化 | |
hash | hashtable | 最基本的哈希表 |
ziplist | 在哈希表里面元素比较少的时候,可能就优化为ziplist了,压缩列表,能够节省空间 | |
list | listedlist | 链表 |
ziplist | 压缩链表,从redis3.2开始,引入了新的实现方式quicklist,同时兼顾了linkedlist和ziplist的优点,quicklist就是一个链表,每个元素又是一个ziplist,把空间和效率都折中的兼顾到,类似C++的std::deque | |
set | hashtable | |
inset | 集合中存的都是整数 | |
zset | skiplist | 跳表,经典题目"复杂链表复制",跳表也是链表,不同的是,每个节点上有多个指针域,巧妙的搭配这些指针的指向,就可以做到查询元素的时间复杂度为O(logN) |
ziplist |
为啥有些结构需要压缩呢?因为redis上有很多很多key,可能是某些key 的value 是hash,此时,如果key特别多,对应的hash也特别多,但是每个hash又不太大的情况,就尽量去压缩,压缩之后就可以让整体占用的内存更小了。
redis会自动根据当前的实际情况选择内部的编码方式,自动适应的。那我们是否要记住说明时候用啥编码方式呢?只记思想,不记数字!!!
网上的说法,如果字符串长度小于39字节,使用embstr,超过39字节使用raw(记数字,没有任何意义!因为数字都是可配置的,像比于知道数字,更要知道数字是怎么得到的)
redis还提供了object encoding key
指令来查看key 对应的value的实际编码方式。
2.关于单线程模型
redis的单线程模型,redis只使用一个线程,处理所有的命令请求,不是说redis服务器进程内部真的就只有一个线程,其实也有多个线程,多个线程是在处理网络IO
假设,有很多客户端(假定为两个),同时操作一个redis服务器,同时适应increase操作想把key对应的value进行+1操作,会引发线程安全问题(在多线程中,针对类似于这样的场景,两个线程尝试同时对一个遍历进行自增,表面上看是自增两次,实际上可能只自增了一次)。当前这两个客户端,也相当于"并发"的发起了上述的请求,此时就意味这是否服务器这边也会存在类似的线程安全的问题呢?
幸运的是,并不会,因为redis服务器实际上是单线程模型,保证了当前收到的这多个请求是串行执行的!!!多个请求同时到达redis服务器,也是要先在队列中排队的,再等待redis服务器一个一个的取出里面的命令再执行。微观上讲,redis服务器是串行/顺序 执行这多个命令的。就比如:想象学校食堂,只有一个打饭窗口,每次一到放学,在校学生就赶紧往食堂跑,宏观上,我们一群人,往食堂跑是并发执行的,微观上,进了食堂的门之后,还是需要排队的。
redis能够使用单线程模型 很好的工作,原因主要在于redis的核心业务逻辑,都是短平快的,不太消耗cpu资源,也就不太吃多核了。弊端在于redis必须要特别小心,某个操作占用时间上,就会阻塞其他命令的执行!!!
3.redis单线程快的原因【重要面试题】
redis虽然是单线程模型,但为啥效率这么高呢?速度这么快呢?【参照物是其他数据库(mysql,oracle,sql server)】,原因有以下几点:
-
redis访问内存,其他数据库则是访问硬盘
-
redis的核心功能,比数据库的核心功能更简单。
数据库对于数据的插入删除查询,都有更复杂的功能支持,这样的功能势必要花费更多的开销,比如:针对插入删除,数据库中的各种约束,都会使数据库做额外的工作。redis干的活少,提供的功能相比于Mysql也是少了不少。
-
单线程模型,避免了一些不必要的线程竞争开销
redis每个基本操作,都是短平快的,就是简单操作一下内存数据,都不是什么特别消耗cpu的操作。就算搞多个线程也提升不大。
-
处理网络IO的时候,使用了epoll这样的IO多路复用机制
一个线程,就可以管理多个socket,针对TCP来说,服务器这边每次要服务一个客户端,都需要给这个客户端安排一个socket,一个服务器服务多个客户端,同时据有很多的socker,这些socket上都是无时不刻的在传输数据吗???其实很多情况下,每个客户端和服务器之间的通信也没那么频繁,此时这么多socket大部分时间都是静默的,上面是没有数据需要传输的。同一时刻,只有少数socket是活跃的。
而最开始介绍TCP服务器的时候,有一个版本就是每个客户端给分配一个线程,客户端多了,线程就多了,系统开销就大了。而通过IO多路复用技术,就可以实现用一个线程来处理多个socket!!!(操作系统给程序员提供的机制,提供了一套api,内部的功能都是操作系统内核来实现的)
linux上提供的IO多路复用,主要是三套API:select,poll,epoll(这个是运行效率最高的机制)
那么epoll机制是如何运行的呢?这里可以举一个生活中的例子:校区门口有很多小摊,有一天晚上我想吃蛋炒饭,我妈想吃肉夹馍,我弟想吃饺子,于是我们有以下几种方案:
- 我自己去,先买蛋炒饭,等,再买肉夹馍,等,再买饺子,等,需要等三次!,这是效率最低的方案
- 我们三个一起去,我去买蛋炒饭,我妈去买肉夹馍, 我弟去买饺子,各等各的,效率大大提升了,但是系统开销太大了。
- 我自己去,先买蛋炒饭的时候,等的时候回过头就买肉夹馍,在等肉夹馍的时候再去饺子,这三份饭,哪个先做好了,对应的老板就可以喊我一声(epoll事件通知/回调机制),此时就能让我一个线程同时去做三件事,能高效的完成这三件事的前提是,这三件事的交互都不频繁,大部分事件都是在等的。如果这三件事都是交互特别频繁的,还是老老实实的多搞几个线程靠谱,一个线程就容易忙不过来,就比如你这边打游戏呢,女朋友的电话来了!!!
4.string类型及基本命令
1.string类型基本介绍
redis所有的key都是字符串,而value的类型是存在差异的。
Redis中的string(字符串),直接就是按照二进制数据的方式存储的(不会做任何的编码转换,存的是啥,取出来就还是啥),所以一般来说,redis遇到乱码的问题概率是更小的。讲mysql的时候,知道mysql默认的字符集,是拉丁文,所以插入中文就会失效。而redis不仅仅可以存储文本数据,整数,普通的文本字符串,JSON,xml,二进制数据(图片,视频,音频 ...),音频视频体积可能会比较大,redis对于string类型,现在了大小最大为512M,因为redis是单线程模型,希望进行的操作都能比较快速。
2.set和get
shell
SET key value [expiration EX seconds | PX milliseconds] [NX|XX]
NX:如果Key不存在,才设置,如果key存在,则不设置
XX:如果key存在,才设置(相当于更新key的value),如果key不存在,则不设置(返回nil)
SET key value NX ==> SETNX key value,同理还有SETEX,SETPX
所以,set key value ex 10 【更推荐,减少网络请求数量】==> set key value 然后 expire key 10
注意redis文档给出的语法格式说明:[ ]相当于一个独立的单元,表示可选项(可有可无的),其中 | 表示"或者"的意思,多个时只能出现一个,[ ] 和 [ ] 之间,是可以同时存在的。
注意我们set操作的一个小细节(默认不加选项时):如果key不存在,则创建新的键值对,如果key存在,则是让新的value 覆盖旧的value,可能会改变原来的数据类型,原来这个key的(生存事件)也会失效
接下来介绍一个快速失去年终奖的小技巧:清楚redis上的所有的数据 ==> 删库,相当于mysql里的drop database。FLUSHALL 可以把redis上所有的键值对都带走 。以后在公司,尤其在生产环境的数据库,千万千万不敢敲!!当前学习阶段在自己电脑上敲一敲还好。
接下来我们再来看一下get指令
shell
GET key
对于GET来说,只是支持字符串类型的value,如果value是其他类型,使用GET获取就会出错!!!
3.mset和mget
这两个指令的作用,一次操作多组键值对。
语法格式:
shell
MGET key [key ... ]
MSET key value [key value ...]
事件复杂度:O(N) N是key的数量,此处的N不是整个redis服务器中所有key的数量,而只是当前命令中,给出的key的数量。所以就可以认为是O(1)
4.setnx,setex和psetex
SETNX:不存在才能设置,存在则设置失效。 SETEX:[SETEX key seconds value ] 设置key的过期时间,单位是秒
PSETEX:设置key的过期时间,单位是毫秒
redis针对set的一些常见用法,进行了缩写,之所以这样搞,就是为了让操作更符合人的直觉。
5.incr和incrby等指令
- incr:针对 value + 1
- incrby:针对 value + n
- decr:针对 value - 1
- decrby:针对 value - n
- incrbyfloat:针对 value +/- 小数
incr:针对 value + 1,注意此时 key 对应的value 必须得是整数(在redis中,是使用64位/8字节表示,相当于C++中的long long或者java中的long,表示范围还是非常非常大的)。此操作的返回值就是 +1 之后的值。
incr 操作的 key 如果不存在,就会把这个key 的value当作0来使用。
incrby:可以针对key对应的value进行 +n 操作。
decr把key对应的value 进行 -1 操作。注意:key对应的value必须是整数,在64位的范围内,如果这个key 对应的value不存在,则当作0处理。decr的运算结果,也是计算之后的值。
decrby 就是把key对应的value进行 -n 的操作。
incrbyfloat:把key对应的value 进行 + - 运算,运算的操作数可以是浮点数。这个指令只能通过加上负数的形式来实现减法。虽然此处没有提供减法版本,但是使用redis进行的计数操作,一般都是针对整数来进行的。
上述操作的时间复杂度,都是O(1),由于redis处理命令的时候,是单线程模型,多个客户端同时针对同一个key进行incr操作,内部是通过一个执行队列来维护的,不会引起"线程安全"问题。
6.append等指令
字符串,也支持一些常见的操作,比如拼接,获取/修改 字符串的部分内容,获取字符串长度。append操作语法如下:
shell
APPEND KEY VALUE
append返回值:长度的单位是字节!redis的字符串,不会对字符编码做任何的处理(redis不认识字符,只认识字节) 当前咱们的xshell终端,默认的字符编码是utf8,在终端中输入汉字之后,也就是按utf8来编码的。一个汉字在utf8字符集中,通常是3个字节的。在启动redis客户端的时候,加上一个 - -raw 这样的选项,就可以使redis客户端能够自动的把二进制数据尝试翻译。
setrange指令的语法:
shell
SETRANGE key offset value
offset:偏移量(从第几个字节,开始进行替换)
返回值:替换之后新的字符串的长度
setrange 针对不存在的key也是可以操作的,不过会把offset之前的内容填充成0x00
strlen 获取到字符串的长度,单位是字节,语法格式:
shell
STRLEN key
我们在将Mysql的时候,varchar(N),此处N的单位就是字符,mysql中的字符,也是完整的汉字,这样的一个字符,也可能是多个字节。
C++中,字符串的长度本身就是用字节为单位的,java中,字符串的长度则是以字符为单位的,java中的一个char == 2字符 ,java中的char 是基于unicode 这样的编码方式,就能够表示中文等符号。 刚刚说了半天,一个汉字通常是3个字符(编码方式为utf8),java里面咋一个2字节的char就能表示汉字呢??? 因为java中的char是用unicode,一个汉字使用两个字节 java中的string,则是用的utf8,一个汉字就是3个字节了,java的标准库内部,在进行上述的操作过程中,程序员一般是感知不到编码方式的变换的。
5.string编码方式
string内部有三种编码方式:
- int:64位/8字节 的整数
- embstr:压缩字符串,适用于表示比较段的字符串
- raw:普通字符串,适用于表示更长的字符串,只是单纯的持有字节数组、
我们之前介绍过,通过使用object enconding来查看数据结构的真实编码方式:
redis的string类型,一般是大于39个字节就使用raw的编码方式来存储,但不建议大家去记长度大于39这样的数字。
假如有个业务场景,都会有很多很多的key,类型都是string,但是每个value的string长度都是100左右。为了关注整体的内存空间,这样的字符串使用embstr来存储也不是不能考虑,上述效果具体怎么实现呢?
- 先看redis是否提供了对应的配置项,可以修改39这个数字
- 如果没有提供配置项,就需要针对redis的源码进行魔改了
这也是因为为啥很多大厂,往往都是自己造轮子,而不是直接使用业界成熟的呢? 开源的组件,往往考虑的都是通用性,但是大厂往往会遇到一些极端的业务场景,往往就需要根据当前的极端业务,针对上述的开源组件进行定制化。
6.string类型的应用场景
1.缓存(Cache)功能
见下图:
整体的思路:应用服务器访问数据的时候,先查询redis,如果redis上数据存在了,就直接从redis上去取数据交给应用服务器,不继续访问数据库了。
如果redis上的数据不存在,再读取Mysql,把读到的结果,返回给应用服务器的同时,把这个数据也写入到redis中。
像redis这样的缓存,经常用来存储"热点"数据(高频被使用的数据)。
刚才上述描述的过程,相当于把最近使用到的数据作为热点数据。(暗含了一层假设,某个数据一旦被用到了,那么很可能在最近这段时间就会被反复用到)
上述策略,存在一个明显的问题:随着时间的推移,肯定是会有越来越多的key在redis上访问不到,从而要去从mysql上读取并写入redis了,此时redis中的数据不是就越来越多嘛??有以下两种解决方案:
- 在把数据写给redis的同时,给这个key设置一个过期时间
- redis也在内存不足的时候,提供了淘汰机制(之后会细说)
伪代码实现功能:
-
假设业务是根据用户uid获取用户信息
c++UserInfo getUserInfo(long uid) { ... }
-
首先从 Redis 获取用户信息,我们假设用户信息保存在 "user:info: <uid>" 对应的键中:
c++// 根据 uid 得到 Redis 的键 String key = "user:info:" + uid; // 尝试从 Redis 中获取对应的值 String value = Redis 执⾏命令:get key; // 如果缓存命中(hit) if (value != null) { // 假设我们的⽤⼾信息按照 JSON 格式存储 UserInfo userInfo = JSON 反序列化(value); return userInfo; }
-
如果没有从redis中得到用户信息,及缓存miss,则进一步从mysql中获取对应的信息,随后写入缓存并返回:
c++// 如果缓存未命中(miss) if (value == null) { // 从数据库中,根据 uid 获取⽤⼾信息 UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid> // 如果表中没有 uid 对应的⽤⼾信息 if (userInfo == null) { 响应 404 return null; } // 将⽤⼾信息序列化成 JSON 格式 String value = JSON 序列化(userInfo); // 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒) Redis 执⾏命令:set key value ex 3600 // 返回⽤⼾信息 return userInfo; }
2.计数(Counter)功能
企业为啥老乐意收集用户的数据呢???为了统计 =》 进一步明确用户的需求 =》根据需求改进和迭代产品。但redis并不擅长统计!
比如,想在redis中,统计播放量前100的视频有哪些,基于redis搞就很麻烦,相比之下,如果是mysql来存储上述数据,一个sql就搞定了。
redis如何存储统计数据见下图:
伪代码:
c++
// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
key = "video:" + vid;
long count = Redis 执⾏命令:incr key
return counter;
许多应用都会使用Redis 作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。如上图所示,例如视频网站的视频播放次数可以使用Redis 来完成:用户每播放⼀次视频,相应的视频播放数就会自增1。 实际中要开发⼀个成熟、稳定的真实计数系统,要面临的挑战远不止如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。
3.共享会话(Session)
Cookie(浏览器存储数据的机制),Session(服务器这边存储数据的机制) 举个取医院看病的例子,我是一个病人,相当于一个客户端,医生要给我诊病,医生要给我提供服务,相当于服务器。 由于病人需要多次访问服务器,服务器这边就需要很好的了解到病人每次复查时候的状态。医院有多个医生,即有多个服务器,这些服务器,是以负载均衡的方式来提供服务的 ,同一个病人,两次来医院看病,遇到的医生可能是不同的,此时,如果每个医生只是靠自己的记忆,来存储病人的状态,此时两次访问的是不同的医生,医生就很难知道病人之前的情况了。医院正确的做法是使用一套系统,记录病人的病历,诊断结果,治疗情况(这个就是所谓的会话,客户端和服务器在交互过程中产生的一些专属于该客户端的中间状态的数据),让多个医生共享。
如果每个应用服务器,维护自己的会话数据,此时彼此之家不共享,用户请求访问到不同的服务器上,就可能会出现一些不能正确处理的情况了。
此时所有的会话数据(病人的信息),都被各个服务器(医生)共享了,这时候就不会出现问题了。
4.收集验证码
-
生产验证码:用户输入一下手机号,点击获取验证码
-
限制1分钟之内,最多获取5次验证码
主要还是怕用户频繁获取验证码,对于我们服务器压力过大
-
每次获取验证码必须间隔30秒
验证码存在一个有效时间,此处假定是五分钟
-
-
检查验证码:把短信收到的验证码这一串数,提交到系统中,系统进行验证验证码是否正确。
上述的过程可以用下面的伪代码来说明:
c++
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;
}
}
7.hash类型介绍及基本命令
1.hash类型的基本介绍
哈希表【是我们之前学过的所有的数据结构中,最最重要的】
- 日常开发中,出场频率非常高
- 面试中也是非常重要的考点
我们知道,redis自身已经是键值对结构了,redis自身的键值对其实就是通过哈希的方式来组织的!!!把key这一层组织完成之后,到了value这一层,value的其中一种类型还可以再是哈希。
2.hset和hget
设置一个哈希的关键字为 HSET 关键字,语法格式如下:
shell
HSET key fidld value [field value ... ]
这里的value通常为字符串
返回值:是设置成功的 键值对(field - value)的个数
如何拿到value的值呢(指的是field 中的value的值),通过使用HGET命令,语法如下:
shell
HGET key field
3.hexists和hdel
HEXISTS指令用于判断,相关的key和field对应的值是否存在。语法格式如下:
shell
HEXISTS key field
HDEL指令:用于删除hash中指定的 字段。语法格式:
shell
HDEL key field [field ...]
注意:del删除的是key;hdel删除的是 field【有什么区别呢,在下面操作中展示】
返回值:本次操作删除的字段个数
4.hkeys和hvals
HKEYS 这个操作,先根据key找到对应的hash,时间复杂度为O(1),然后再遍历整个hash,这一步的时间复杂度为O(N),这里N是hash的元素个数。语法:
shell
HKEYS key
注意:这个操作也是存在一定的风险的!类似之前介绍过的 keys *,因为主要咱们也不知道某个hash中是否会存在大量的field。
HVALS 这个指令和HKEYS相对,能够获取到hash中所有的value。语法格式为:
shell
HVALS key
这个操作的时间复杂读,也是O(N),这个N是哈希的元素个数,如果哈希非常大,这个操作就可能会导致redis服务器被阻塞住。
H 系统的命令,必须要保证key对应的value得是 哈希类型的!!
5.hgetall和hmget
HGETALL这个指令,获取key对应的所有的field和value。这个指令的时间复杂度还是蛮大的,一般情况下,我们不需要查询所有的field,可能只查其中的几个field。
HMGET,这个操作类似于之前的MGET,可以一次查询多个field。HGET一次只能查一个field。
注意:多个value的顺序和field的顺序是匹配的。
有没有hmset,一次设置多个field和value呢???是有的,但是并不需要使用,hset已经支持一次设置多个field和value了。
上述的hkeys,hvals,hgetall都是存在一定的风险的,hash的元素个数太多,执行的耗时会比较长,从而阻塞redis。
可以考虑使用hscan遍历redis的hash,因为他属于"渐进式遍历",敲一次指令,遍历一小部分,再敲一次,再遍历一小部分,,连续执行多次,就可以完成整个的遍历过程了。这个思想也叫做化整为零。
6.hlen等指令
HLEN指令,用于获取hash的元素个数,这个是不需要遍历的,所以时间复杂度为O(1)。语法个数如下:
shell
HLEN key
HSETNX key field value,这个指令类似于setnx,key和field不存在的时候,才能设置成功,如果存在,则失败。
hash这里的value,也可以当作数字来处理, hincrby就可以加减整数,hincrbyfloat就可以加减小数【使用频率不算很高,redis没有提供类似的incr ,decr ...】 ,这几个操作的时间复杂度都是O(1)。
8.hash的编码方式
我们常见的压缩:rar,zip,gzip,7z ....都是一些具体的压缩算法。压缩的本质,就是针对数据进行重新编码。不同的数据,就有不同的特点,结合这些特点,进行精妙的涉及,重新编码之后,就可以缩小体积。 比如举一个粗糙的编码方式:abbcccddddeeee可以重新编码为1a2b3c4d5e。
ziplist也是同理,内部的数据结构也是精心设计的。目的是为了节省空间。因为如果表示一个普通的hash表,可能会浪费一定的空间。(hash首先是一个数组,数组上有些位置有元素,有些位置没有元素),ziplist付出的代价,进行读写元素,速度是比较慢的。如果元素个数少,慢的并不明显,如果元素个数太多了,慢的就会雪上加霜。
所以,如果:
- 哈希中的元素个数比较少,使用ziplist表示,元素个数比较多时,使用hashtable来表示 可以在hash-max-ziplist-entries配置(默认512字节)。
- 每个value的值长度都比较短,使用ziplist表示,如果某个value的长度太长了,也会转换成hashtable 可以在hash-max-zip-value配置(默认64字节)
这个配置项就是可以写到redis.conf文件中的。
9.哈希的应用
1.作为缓存
之前介绍过,string也是可以作为缓存的,但是当我们存储结构化的数据(类似于 数据库 表 这样的结构)的时候,使用hash类型更加合适一些。
比如下面是两条用户信息,用户的属性表现为表的列,每条用户的信息表现为行,如果映射关系表示这两个用户信息。则:
相比于使用json格式的字符串缓存用户信息,哈希类型则变得更加直观,并且在更新操作上变得更加灵活。可以将每个用户的id定义为键后缀,多对field-value对应用户的各个属性,类似如下伪代码:
c++
UserInfo getUserInfo(long uid) {
// 根据 uid 得到 Redis 的键
String key = "user:" + uid;
// 尝试从 Redis 中获取对应的值
userInfoMap = Redis 执⾏命令:hgetall key;
// 如果缓存命中(hit)
if (value != null) {
// 将映射关系还原为对象形式
UserInfo userInfo = 利⽤映射关系构建对象(userInfoMap);
return userInfo;
}
// 如果缓存未命中(miss)
// 从数据库中,根据 uid 获取⽤⼾信息
UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid>
// 如果表中没有 uid 对应的⽤⼾信息
if (userInfo == null) {
响应 404
return null;
}
// 将缓存以哈希类型保存
Redis 执⾏命令:hmset key name userInfo.name age userInfo.age city
userInfo.city
// 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
Redis 执⾏命令:expire key 3600
// 返回⽤⼾信息
return userInfo;
}
如果使用string(json)的格式来表示UserInfo,万一只想获取其中的某个field,或者修改某个field,就需要把整个json都读出来,解析成对象,操作field,再重写转成json字符串,再写回去。 如果使用hash的方式来表示UserInfo,就可以使用field表示对象的每个属性(数据表的每个列),此时就可以非常方便的修改/获取任何一个属性的值了。
使用hash的方式,确实读写field更加直观高效,但是付出的是空间的代价。需要控制哈希在ziplist和hashtable两种内部编码的转换,可能会造成内存的较大消耗。
原生字符串类型 --- 使用字符串类型,每个属性一个键,这种方式,相当于把同一个数据的各个属性,给分散开表示了。这就是低内聚
shell
//原生字符串类型存储表数据
set user:1:name James
set user:1:age 23
set user:1city Beijing
高内聚就是把有关联的东西放在一起,最好能放到指定的地方。
耦合:两个模块/代码之间的关联关系,关联关系越大,就容易相互影响,认为就是耦合越大。我们追求低耦合,避免"牵一发动全身",这边一改出bug,影响到了其他的地方。
我们再来看一下之前使用哈希存储表数据的时候:
这里这个uid不存可以吗,是否就直接使用key中的id就可以进行区分了呢,这样是不是存储空间又进一步的节省了呢??
如果确实不想存这个uid也可以,但是在工程实践中,一般都会把uid在value中也再存一份,再存一份的化,后续写到相关代码,使用起来会比较方便。
10.list的基本介绍及基本命令
1.list类型的基本介绍
列表(List)相当于数组或者顺序表。不过要注意:list内部的结构(编码方式)并非是一个简单的数组,而是更接近于"双端队列"(deque),redis中的list的结构图如下:
约定最左侧元素下标是0,redis的下标支持负数下标,这个负数下标可以支持使用getrange函数。列表中的元素是有序的(这里指的是顺序很关键,如果把元素位置颠倒,顺序调换,此时得到的新的list和之前的list是不等价的)
区分获取和删除的区别:
- lindex 能获取到元素的值
- lrem 也能返回被删除元素的值
列表中的元素是允许重复的,像hash这样的类型,他的field是不能重复的。因为当前的list,头和尾都能高效的插入删除元素,就可以把这个list当作一个 栈/队列 来使用了。Redis有一个典型的应用场景,就是作为消息队列。最早的时候,就是通过list类型。后来redis又提供了一个stream类型,这个类型比list类型更加适合作为消息队列来使用。
2.lpush和lrange
lpush指令:一次可以插入一个元素,也可以插入多个元素。语法格式如下:
shell
LPUSH key element [element ...]
比如我现在需要按照顺序 lpush:1 2 3 4 全部插入完毕,4 是在最前面的!
时间复杂度:O(1),返回值是list的长度。
如果key已经存在,并且key对应的value类型,不是list,此时lpush命令就要报错【redis中所有的这些各种数据类型的操作,都是类似的效果】
lrange指令用来查看list中指定范围的元素。语法格式如下:
shell
LRANGE key start stop
此处描述的区间也是闭区间,下标支持负数。
谈到下标,我们往往会关注超出范围的情况, C++中,下标超出范围,一把会认为这是一个"未定义行为"(可能会导致程序崩溃,也可能会得到一个不合法的数据,还可能会得到一个看起来合法,但是错误的数据,也有可能得到一个符合要求的数据 ... 开盲盒)。缺点:程序员不一定能第一时间发现问题!优点:效率是最高的! 在java中,下标超出范围,一般会抛出异常,相比C++会多出一部下标合法性的检验,缺点:速度比上面要慢,优点:出现问题能及时发现。
在redis中并没有采用上述的两种设定。redis的做法是:直接尽可能的获取到给定区间的元素,如果给定区间非法,比如超出下标,就会尽可能的获取对应的内容。此处对于越界下标的处理方式,更接近于python的处理方式。(python的切片)
3.lpushx等指令
这里x的意思是exists的意思,lpushx的语法格式如下:
shell
LPUSHX key element [element ... ]
rpush指令,尾插,语法格式如下:
shell
RPUSH key element [element ... ]
LPUSH => left push 对应的是 lpop RPUSH => right push 对应的是 rpop 但是我们要注意了:lrange对应的是list range 而不是left range
同时还有RPUSHX 指令,X -> exists。语法格式如下:
shell
RPUSHX key element [element ... ]
接下来我们来介绍lpop和rpop这两个指令。在当前的redis 5版本中,都是没有count参数的,从redis 6.2版本,新增加了一个count 参数。count这个参数用于描述这次要删几个元素。这两条指令的语法格式如下:
shell
LPOP key
RPOP key
reids中的list的一个双端队列,从两头插入/删除 元素都是非常高效的O(1),搭配使用rpush(尾插)和lpop(头删),就相当于队列了。搭配使用rpush(尾插)和rpop(尾删),就相当于栈了。
4.lindex等指令
给定下标,获取到对应的元素。这个指令的时间复杂度是O(N),这里的N指的是list中的元素个数。如果下标非法,返回的是nil。这个指令的语法格式如下:
shell
LINDEX key index
linsert指令,返回值是插入之后,得到的新的list的长度。指令格式如下:
shell
LINSERT key <BEFORE> | <AFTER> pivot element
万一要插入的列表,基准值存在多个,该怎么办呢?linsert 在进行插入的时候,要根据基准值,找到对应的位置,从左往右找,找到第一个符合基准值的位置即可。时间复杂度为O(N),N表示列表的长度。
接下来是LLEN命令,这个命令的格式如下:
shell
LLEN key
5.lrem等指令
lrem rem => remove 这个指令的格式为:
shell
LREM key count element
这个count参数表示:要删除的个数,这个element表示要删除的值。有以下几种可能:
- count > 0:从前往后删count个
- count < 0:从后往前删count个
- count = 0:删除所有
举个例子:如果列表中现在存放的是 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4
- 当count = 2,element=1 时:返回值为 2 3 4 2 3 4 1 2 3 4 1 2 3 4
- 当count=-2,element = 1时:返回值为 1 2 3 4 1 2 3 4 2 3 4 2 3 4
- 当count=0,element = 1时:返回值为 2 3 4 2 3 4 2 3 4 2 3 4
6.ltrim和lset指令
接下来我们再来看看LTRIM指令,这个指令用于保留start和stop之间区间内的元素,(区间外面两边的元素就直接被删除了),语法格式如下:
shell
LTRIM key start stop
lset指令,根据下标,修改元素。格式如下:
perl
LSET key index element
值得注意的是:这个指令的时间复杂度是O(N)的,而且,lindex这个指令可以很好的处理下标越界的情况,如果没有就直接返回nil,lset这个则会报错,但不会像js那样,直接在越界的下标处搞出一个元素来。
7.blpop和brpop等指令
在谈这两个指令之前,外面先谈一谈阻塞队列(BlockingQueue),多线程的时候,讲到了一个东西,叫生产者消费者模型。是使用队列作为中间的"交易场所(broker)"。我们期望这个队列有两个特性:
- 线程安全
- 阻塞
- 如果队列为空,尝试出队列,就会产生阻塞,直到队列不空,阻塞解除。
- 如果队列为满,尝试入队列,也会产生阻塞,直到队列不满,阻塞解除。
redis 中的list也相当于 阻塞队列一样,线程安全是通过单线程模型 来支持的。阻塞,则只支持 "队列为空"的情况,不考虑 队列满 。但阻塞版本 会根据timeout,阻塞一段时间,期间redis 可以执行其他命令(此处blpop 和 brpop看起来好像耗时很久,但实际上并不会对redis服务器产生负面影响!),使用brpop 和blpop 的时候,这里是可以显示的设置阻塞时间的 !(不一定是无休止的等待!)
命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。
blpop和brpop都是可以取尝试获取多个key的列表的元素的,多个key对应多个list,这多个list哪个有元素了,就会返回哪个元素。
如果多个客户端同时对一个键执行pop,则最先执行命令的客户端会得到弹出的元素。命令的语法格式:
shell
BLPOP key [key ... ] timeout
此处 可以指定一个key,或者多个key,每个key都对应一个list
- 如果这些list有任何一个非空,blpop都能够把这里的元素给获取到,立即返回
- 如果这些list都为空,此时就需要阻塞等待,等待其他客户端往这些list中插入元素了。
- 此处还可以设定超市时间,单位是秒,(Redis 6中,超时时间允许设定为小数,Redis 5中,超时时间得是整数)
-
针对一个非空的列表进行操作:
返回值的结果相当于一个pair(二元组),一方面是告诉我们当前的数据是来自于哪个key,一方面是告诉我们取到的数据是啥。
-
针对一个空的列表进行操作
我们先开一个客户端,对一个空的列表(键),进行blpop操作,查看效果:
此时程序看上去就跟卡了一样,我们赶紧再开另一个redis客户端,通过新来的客户端,向key8中插入元素:
此时我们再去看看第一个客户端上的响应:
-
针对多个key操作
此时这些键都是没有list列表的,这时候blpop就在同时等待多个列表。
我们随便选取一个列表,比如key11,在别的客户端上,向这个key11中插入元素,我们查看效果:
我们看看第一个客户端,对于blpop指令的响应:
总结:brpop和blpop完全一样(这里就是尾删了),此处的这俩阻塞命令,用途主要就是用来作为"消息队列",当前这俩指令虽然可以一定程度上满足"消息队列"这样的需求,整体来说,这俩命令的功能还是比较有限。blpop和brpop 有些英雄迟暮。
11.list的编码方式
- ziplist(压缩列表),把数据按照更紧凑的压缩形式进行表示的,节省空间。当元素个数多了,操作起来效率会降低。
- linkedlist(链表)
- quicklist:相当于是链表 和压缩列表的结合。整体还是一个链表,链表的每个节点,是一个压缩列表。每个压缩列表,都不让它太大,同时再把多个压缩列表通过链式结构连起来。
我们可以来进行测试一下:
这里测试了一些情况,发现都是通过使用quicklist来存储的。
12.list的应用场景
1.存储数据
用list作为"数组"这样的结构,来存储多个元素。但是我们redis提供的查询功能,不像mysql这么强大。
比如在mysql中,表示学生和班级信息时:如下图:
在这种表结构中,就可以和方便的通过外键来"查询指定班级中有哪些同学"。
而在redis中,存储结构则可以是这样的。
2.使用redis作为消息队列
作为生产者消费者模型使用。
这些消费者,谁先执行到这个brpop命令(这个操作,是阻塞操作,当列表为空的时候,brpop就会阻塞等待,一直等到其他客户端向列表中push 了元素),谁就能拿到这个新来的元素。像这样的设定,就能构成一个"轮询"式的效果。假设消费者执行顺序是 1 2 3 ,当新元素到达之后,首先是消费者1 拿到元素(按照执行brpop命令的先后顺序来决定谁先获取到的),消费者1 拿到元素之后,也就从brpop中返回了(相当于这个命令就执行完了),如果消费者1 还想继续消费,就需要重新执行brpop。此时,再来一个新的元素过来,就是消费者2 还想继续消费,也需要重新执行brpop。再来一个新元素,也就是消费者3 拿到这个元素了。
3.Redis 分频道阻塞消息队列模型
多个列表/频道 channel / topic ,这种场景是非常常见的,日常使用的一些程序,比如抖音:有一个通道,来传输短视频数据,还有一个通道来传输弹幕,还可以有频道,来传输 点赞,转发,收藏数据,还可以有频道来传输评论数据。搞成多个频道,就可以在某种数据发生问题的时候,不会对其他数据造成影响(解耦合)。
每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
分页获取用户的 Timeline,例如获取用户1 的前 10 篇微博:
c++
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
当前一页中有多少数据,不确定时,可能会导致下面的循环比较大,从而会触发很多次的hgetall,也就是很多的 网络请求。此方案在实际中可能存在两个问题:
-
1 + n 问题。即如果每次分页获取的微博个数较多,需要执⾏多次 hgetall 操作,此时可以考虑使⽤ ,pipeline(流水线)模式批量提交命令【pipeline(流水线,管道),虽然咱们是多个redis命令,但是把这些命令合并成一个 网络请求 进行通信,就可以大大降低客户端 和服务器之间的交互次数了。】,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。
-
分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分【假设某个用户发了 1w个微博,list的长度就是1w,就可以把这1w 个微博拆成10份,每个就是1k,如果是想获取 到5k 个左右的微博。】。