Redis从入门到精通【redis基础】

一、认识分布式

1.认识Redis

什么是Redis,我们先看官方给的定义:

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.浅谈单机架构

​ 关于分布式系统,大家千万不要把所谓的“分布式”想的太复杂,太高大上了
​ 所谓的单机架构,就是只有一台服务器,这个服务器负责所有的工作。我们可以看下图:假定这是一个电商网站
image-20240627220852067

上图应用服务指的是我们写的服务器程序:
例如:C++的cpp-httplib ,java的Spring,还有MySQL的客户端程序

上图的数据库服务指的是:
MySQL的本体,即MySQL服务器(存储和组织数据的部分),因为MySQL是一个客户端服务器结构的程序!

Q:在单机程序中,能不能把数据库服务器也去掉,光一个应用服务器既负责业务,又负责数据存储呢?
也不是不可以,但是就是会比较麻烦

​ 大家千万不要瞧不上这个单机架构,绝大部分的公司的产品,都是这种单机架构!因为现在的计算机硬件,发展速度非常之快,哪怕只有一台主机,这一台主机的性能也是很高的。但如果业务进一步增长,用户量和数据量都水涨船高,一台主机难以应付的时候,这时候就需要引入更多的主机,引入更多的硬件资源。

3.浅谈分布式是什么

​ 我们知道,一台主机的硬件资源是有上限的!!!,包括不限于以下几种:1.cpu 2.内存 3.硬盘 4.网络 5….
​ 服务器每次收到一个请求,都是需要消耗上述的一些资源的,如果同一时刻,处理的请求多了,都可能会导致服务器处理请求的时间会变长,甚至于处理出错。

​ 如果我们真的遇到了这样的服务器不够用的场景,怎么处理呢?

  1. 开源:简单粗暴,添加更多的硬件资源,但一个主机上面能增加的硬件资源也是有限的,这取决于主板的扩展能力,当一台主机扩展到极限了,但是还不够,就只能引入多台主机了!不是说新的机器买来就直接可以解决问题了,也需要在软件上做出对应的调整和适配,一旦引入多台主机了,咱们的系统就可以称之为“分布式系统”。
  2. 节流:在软件上优化,(这就各凭本事了,需要通过性能测试,找到是哪个环节出现了瓶颈,再去对症下药)

注意:引入分布式,这是万不得已的无奈之举,系统的复杂度会大大提高,出现bug的概率也越高,加班的概率 & 丢失年终奖的概率也随之提高。

4.数据库分离和负载均衡

​ 引入多台服务器后,我们自然可以想到将应用服务和数据库服务分离。如下图:
image-20240627233238994

​ 对于应用服务器来说:里面可能会包含很多的业务逻辑,这可能会吃比较多的CPU和内容。

​ 对于数据库服务器,需要更大的硬盘空间,更快的数据访问速度,可以配置更大硬盘的服务器,甚至还可以上SSD硬盘。

​ 通过调整他们的配置,从而达到更高的性价比。一般来说我们会引入更多的应用服务器节点,因为应用服务器吃CPU和内存,如果CPU或者内存吃没了,此时应用服务器就顶不住了。

什么是负载均衡:

​ 用户的请求,先达到负载均衡器/网关服务器(这是一个单独的服务器),假设有1w个用户请求,有2个应用服务器,此时按照负载均衡的方式,就可以让每个应用服务器承担5k的访问量。这个事情就和之前讲过的“多线程”有点像。
​ 对于负载均衡器来说,也有很多的负载均衡具体的算法。例如:如果一个用户总数重复提出同一个请求,那么负载均衡器便会解析他的ip,将他的请求分配到同一个服务器上,以提高服务器的响应速度。

5.理解负载均衡

​ 还是上面那个1w请求的例子,虽然两个应用服务器各自承担5k的任务,但是这个负载均衡器,看起来不是承担了所有的请求嘛,这个东西能顶得住嘛???

​ 负载均衡器,对于请求量的承担能力,要远超过应用服务器的。就好比负载均衡器是领导,分配工作即可,而应用服务器是组员,需要执行任务。执行任务的压力和耗时肯定是远高于分配人物的。

​ 那是否会出现请求量达到连负载均衡器也扛不住了呢??也是有可能的!!!这时候可以考虑引入更多的负载均衡器(引入多个机房)

​ 举个例子:学校是服务同学的,每个学校有他们的教务团队,负责日常作业的跟进,跟进一个同学,势必要花一定的时间,一个助教老师,就是一个应用服务器。根进一个同学,就相当于处理一个请求。当同学数量很多时,就需要引入更多的助教老师。
​ 而引入负载均衡器,就承担了校长的角色,校长给每个助教老师分配任务,随着同学更多了,一个校长分配任务也分配不过来了,就可以划分出多个校区,每个校区都有各自的校长,每个校区里又有一批助教
​ 但是!当人变多了,管理成本就会提高,出现问题的概率也会提高,当机器也是如此。

6.数据库读写分离

​ 如上面讨论,增加应用服务器,确实能够处理更高的请求量,但是随之存储服务器要承担的请求量也就更多了!
​ 咋办呢?可以开源(引入更多的机器,简单粗暴)+节流(门槛高,更复杂),且看下图:
image-20240628015926355

​ 我们知道,在实际的应用场景中,数据库的读频率是要比写频率更高的!故数据库服务器可以采用读写分离的方式,主服务器一般是一个用于数据的更新修改操作,从服务器可以有多个(一主多从)负载数据的读取操作,主服务器实时或者定期的将数据同步到从服务器上,同时从服务器通过负载均衡的方式,让应用服务器进行访问。

7.引入缓存

​ 数据库天然有个问题,响应速度是更慢的!我们将数据区分“冷热”,热点数据放到缓存中,缓存的访问速度往往比数据库要快多了!
image-20240628021442485

​ 如上图,我们的主从服务器,存储的仍然是完整的全量数据,而我们引入的缓存服务器,只是放一小部分的热点数据(会频繁被访问到的数据),根据二八原则:20%的数据能够支持80%的访问量【出自经济学:20%的人持有80%的股份】,甚至更极端情况可以达到一九,实际场景不同会略有差异。这里引入的缓存服务器通常就是Redis,此时,缓存服务器就帮助数据库服务器负重前行!
​ 那么代价是?既然引入缓存,必然不会让其与主数据库直接进行同步数据,因为主数据库的效率会拉低缓存数据库的效率,那么缓存数据库中的数据如何保证其与主数据库一致呢?这是一个很大的问题。

8.数据库分库分表

​ 我们引入分布式系统,不光要能够去应对更高的请求量(并发量),同时也要能应对更大的数据量。那么是否会出现,一台服务器已经存不下数据了呢???当然会存在!!!
​ 虽然一个服务器,存储的数据量可以达到几十个TB,即使如此也可能会存不下(例如短视频文件),一台主机存不下,那么就需要多台主机来进行存储。我们见下图的结构:
image-20240628154022129

​ 我们针对数据库进一步的拆分,即分库分表操作。本来应该数据库服务器,这个数据库服务器上有多个数据库(这里数据库指的是逻辑上的数据集合,即create database创建的那个东西)
​ 现在就可以引入多个数据库服务器,每个数据库服务器存储一个或者一部分数据库。
​ 如果某个表特别大,大到一台主机存不下,也可以针对表进行拆分,具体分库分表如何实践?还是需要结合实际的业务场景来展开,业务是互联网公司最重要的东西,技术只是给业务提供支持的,业务决定了技术!

9.引入微服务

​ 我们来看一下什么是微服务架构
image-20240628160528111

​ 在之前的应用服务器中,一个服务器程序里面做了很多的业务(比如电商,一台服务器上既做用户和商品,又做电商功能),这就可以会导致这一个服务器的代码变的越来越复杂。
​ 为了更方便代码的维护,就可以把这样的一个负载的服务器,拆分成更多的,功能更单一,但是更小的服务器。这样的服务器叫做微服务,这种服务器的种类和数据就增加了。
​ 我们想想,引入微服务到底是在解决什么问题,其实本质上是在解决“人的问题”,啥时候会涉及到“人”的问题??大厂!!如果是小公司,就两三个开发,此时搞微服务就没有太大必要了。
​ 当应用服务器复杂了,势必就需要更多的人来维护了,当人多了,就需要配套的管理,把这些人组织好,通过划分组织结构,分成多个组(就需要进行分工),每个组分别配备领导进行管理。
​ 按照功能,拆分为多组微服务,就可以有利于上述人员的组织结构的分配了。

​ 那么引入微服务,解决了人的问题,付出的代价?

  1. 系统的性能下降(要想保证性能不下降太多,只能进入更多的机器,更多的硬件资源 => 充钱),因为拆出来更多的服务,多个功能之间更需要依赖网络通信,网络通信的速度很可能是比硬盘还慢的!!!(不过幸运的是,硬件技术的发展,网卡现在有万兆网卡,读写速度已经能够超过硬盘读写了,但也贵!)
  2. 系统复杂程度提高,可用性受到影响,服务器更多了,出现问题的概率就更大了,这就需要一系列的手段来保证系统的可用性,(比如更丰富的监控报警,以及配套的运维人员)

最后总结下微服务的优势:

  1. 解决了人的问题
  2. 使用微服务,可以更方便与功能的复用
  3. 可以给不同的服务进行不同的部署

10.补充概念

  1. 应用(Application)/系统(System)

    一个应用,就是一个/组 服务器程序

  2. 模块(Module)/组件(Component)

    一个应用,里面有很多个功能,每个独立的功能,就可以称为是一个模块/组件

  3. 分布式(Distriibuted)

    引入多个主机/服务器,协同配合完成一系列的工作,(这里的多个指的是物理上的多个主机)

  4. 集群(Cluster)

    引入多个主机/服务器,协同配合完成一系列的工作,(这里是逻辑上的多个主机)

  5. 主(Master)/从(Slave)

    分布式系统中一种比较典型的结构,多个服务器节点,其中一个是主,另外的是从,从节点的数据要从主节点这里同步过来

  6. 中间件(Middleware)

    和业务无关的服务(功能更通用的服务)

    1. 数据库
    2. 缓存
    3. 消息队列
  7. 可用性(Availablity)

    系统整体可用的时间/总的时间(这是一个系统的第一要务)

  8. 响应时长(Response Time RT)

    衡量服务器的性能,这个指标越小越好,通常是和具体服务器要做的业务是密切相关的。

  9. 吞吐(Throughput) vs 并发(Concurrent)

    衡量系统的处理请求的能力,衡量性能的一种方式

11.分布式总结

  1. 单机架构(应用程序+数据库服务器)

  2. 数据库和应用分离

    应用程序和数据库服务器,分别放到了不同主机上部署了

  3. 引入负载均衡优化应用服务器 => 集群

    通过负载均衡器,把请求比较均匀的分发给集群中的每个应用服务器,并且当集群中的某个主机挂了,其他主机仍然可以承担服务,提高了整个系统的可用性

  4. 引入读写分离,数据库主从结构

    一个数据库节点作为主节点,其他N个数据库节点作为从节点,主节点负责写数据,从节点负责读数据,主节点需要把修改过的数据同步给从节点。

  5. 引入缓存,冷热数据分离

    进一步提升了服务器针对请求的处理能力,依据的是二八原则。Redis在一个分布式系统中,通常就扮演着缓存这样的角色。但引入的问题是:数据库和缓存的数据一致性问题。

  6. 引入分库分表,数据库能够进一步扩展存储空间

  7. 引入微服务,从业务上进一步拆分应用服务器

    从业务功能的角度,把应用服务器,拆分成更多的功能更单一,更简单,更小的服务器

​ 上述的这样的几个演化的步骤,只是一个粗略的过程。实际上一个商业项目,真实的演化过程,都是和他的业务发展密切相关的,业务是更重要的,技术只是给业务提供支持的。所谓的分布式系统,其实就算想办法引入更多的硬件资源而已!

二、Redis背景知识

1.Redis特性介绍

​ Redis是一个在内存中存储数据的中间件,用于作为数据库,用于作为数据缓存,在分布式系统中能够大展拳脚,接下来我将讲解一些Redis的一些特性(优点):

  1. In-memory data structures

    在内存中存储数据,官方原话:support for strings,hashes,lists,sets,sorted sets,streams,and more.在Redis中,数据是按照一种kv的结构存储的,key都是string,而value则可以是上述的这些数据结构。

    这时我们可以对比MySQL:MySQL主要是通过“表”的方式来存储数据的,我们一般称之为“关系型数据库”

    而Redis主要是通过“键值对”的方式来存储组织数据的,我们称之为“非关系型数据库”

  2. Programmability

    官网:Server-side scripting with Lua and server-side storted procedures with Redis Functions 这里的Lua也是一个编程语言。针对Redis的操作,可以直接通过简单的交互式命令进行操作,也可以通过一些脚本的方式,批量执行一些操作(可以带有一些逻辑)

  3. 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支持更多的数据结构以及支持更多的命令。

  4. 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的内存恢复到重启前的状态)

  5. Clustering

    Horizonta scalability with hash-based sharding,scaling to millions of nodes with automatic repartitioning when growing the cluster. Redis作为一个分布式系统中的中间件,能够支持集群是很关键的,这个水平扩展,类似于是“分库分表”,一个Redis能存储的数据是有限的(内存空间有限),引入多个主机,部署多个Redis节点,每个Redis存储数据的一部分。

  6. High availability

    Replication with automatic failover for both standalone and clustered deployments 高可用 => 冗余/备份 Redis自身也是支持“主从”结构的,从节点就相当于主节点的备份了。

一个字总结Redis,那就是快!那为啥Redis更快呢?

  1. Redis数据在内存中,就比访问硬盘的数据库(点名MySQL)要快很多。
  2. Redis核心功能都是比较简单的逻辑,核心功能都是比较简单的操作内存的数据结构
  3. 从网络角度上,Redis使用了IO多路复用的方式(epoll,使用一个线程来管理很多个socket)
  4. Redis使用的是单线程模型(虽然更高版本的Redis引入了多线程),这样的单线程模型,减少了不必要的线程之间的竞争开销。因为多进程提高效率的前提是:CPU密集型的任务,使用多个线程可用充分的利用CPU多核资源。但是Redis的核心任务,主要就是操作内存的数据结构,不会吃很多的CPU。而使用多进程势必要考虑线程安全,锁等问题。
  5. [个人不认同,网上说的很多]Redis是使用C语言开发的,所以就快。但我觉得MySQL也是C语言开发的,为啥没那么快呢?

2.Redis的应用场景

​ Redis的Use cases主要分为下面三种情况:

  1. Real-time data store

    这种使用场景下把redis当做了数据库,在大多数情况下,考虑到数据存储,优先考虑的是“大”,但是仍然有一些场景,考虑的是“快”,就比如做一个搜索引擎 -> 广告搜索(商业搜索),对于性能要求是非常高的,搜索系统中没有用到MySQL这样的数据库。把所有需要检索的数据都存储在内存中,就使用的是类似Redis这样的内存数据库来完成的。当然,使用这样的内存数据库,存储大量的数据,需要不少的硬件资源的(需充值)。在这种使用场景下Redis存的是全量数据,这里的数据是不能随便丢的。

  2. Caching & session storage

    使用MySQL存数据,大,慢,根据二八原则,把热点数据拎出来,存储在redis中。在这种使用场景下,Redis存的是部分数据,全量数据都是以mysql为主的,哪怕Redis的数据都没了,还可以从mysql这边再加载回来,这便是Redis作为Cache的应用场景。当Redis作为会话存储时候,我们回想起学习http时,cookie => 实现用户身份信息的保存(只是在浏览器这边存储了一个用户的身份标识,一般称为sessionid),是需要session配合的(服务器这里真正的存储了用户数据),之前我们的session都是存储在应用服务器上的,那么我们的分布式服务器如何存储的呢?

    image-20240628234934825

    我们可能会遇到这样一个问题:用户第一次登录时,负载均衡器把会话放到了应用服务器1中,那么下一次会话,负载均衡器分配了应用服务器2来完成任务,那用户岂不是又要重新登录啦?如何解决这个问题那?

    1. 想办法让负载均衡器,把同一个用户的请求始终打到同一个机器上(不能轮询了,而是要通过userid之类的方式来分配机器)
    2. 把会话数据单独拎出来,放到一组独立的机器上存储(Redis),这样的话,当应用程序重启了,会话也不会丢失,这才是我们理想中的情况。
      image-20240629001521959
  3. 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系列,比较老~

image-20240629153357788

​ 此处我们需要安装额外的软件源。我们这里选择的是scl源,所以我们首先安装scl源,再去安装redis,(记得安装要以root身份,或者普通用户su root/sudo)

yum install centos-release-scl-rh

image-20240629153943343

​ 此时,我们再根据特定的命令,才能去安装我们的redis 5

yum install rh-redis5-redis

image-20240629154200170

​ 一路 y 下去就安装好啦。

2.创建符号链接

​ 我们发现,Redis 5 默认安装的目录为 /opt/rh/rh-redis5/root/usr/bin/ , 藏的太深了,不方便我们使用,我们可以通过符号链接, 把需要用到的关键内容设置到方便使用的目录中。符号链接,就是“快捷方式”,在linux下通过ln -s 指令。操作步骤为下面几个步骤:

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

image-20240629155415249

image-20240629155444267

执行完发现,这几个符号链接都已经创建成功了。针对配置文件,我们也需要创建符号链接,步骤如下:

cd /etc/
ln -s /etc/opt/rh/rh-redis5/ ./redis

image-20240629155901687

3.修改配置文件

​ 首先我们要进入到我们刚刚创建好的符号链接的redis这个目录,然后打开这个目录下的.conf配置文件。

cd redis
vim redis.conf

接下来主要修改下面几个部分:

  1. 设置 ip 地址

    bind 0.0.0.0
    
  2. 关闭保护模式

    protected-mode no
    

    image-20240629160625460

  3. 启动守护进程

    daemonize yes
    

    ​ 什么是守护进程呢?我们的服务器程序,一般都会以“后台进程”(守护进程)的方式运行,Linux中的进程,分为“前台”和“后台”进程,前台进程会随着终端的关闭而随之被杀死,后台进程不会随着终端的关闭而关闭。
    image-20240629161300354

  4. 设置工作目录

    ​ 我们先创建一个工作目录(这个目录是用于保存redis运行是生成的文件的)

    mkdir -p /var/lib/redis
    

    ​ 再在配置文件中,设置工作目录

    dir /var/lib/redis
    

    image-20240629162010857

  5. 设置日志目录

    和上面操作差不多,我就不演示了,先创建日志目录

    mkdir -p /var/log/redis/
    

    再在配置文件中,设置日志目录

    logfile /var/log/redis/redis-server.log
    

4.启动Redis

​ 终于配置好了,接下来我们可以直接通过命令来启动Redis服务器啦!

redis-server /etc/redis/redis.conf

我们可以通过 netstat 指令,来看一下redis服务器的启动情况。

netstat -anp | grep redis

image-20240629163100647

5.停止Redis

​ 直接通过进程id ,使用kill指令来停止redis服务器的运行

image-20240629163349528

​ 我们上面时候需要停止(重启)Redis呢?当我们每次修改redis的配置文件时,都需要重启才能使得新的配置生效!

​ 最后,我们来看一看redis生成的日志吧

image-20240629163812783

​ 我们再去看一下redis的工作目录吧:
image-20240629163950698

​ 以上就是我们在centos 7上安装Redis服务器的全部过程啦!如果你是centos 8,那么安装会方便很多,不用这么麻烦滴。因为博主没有ubantu的云服务器环境,这里就不再演示ubantu的安装了,都是大同小异的,后续我们会通过使用Docker进行安装,就不必再考虑这些平台问题啦。

3.Redis客户端介绍

​ 我们的redis和MySQL一样,他也是一个客户端-服务器 结构的程序!redis 客户端和服务器可以在同一个主机上,也可以在不同的主机上(当前阶段,大家一般只有一台机器,此时客户端和服务器就是在同一台机器上的)
image-20240629181007847

​ 而Redis的客户端也有很多种形态,例如:

  1. 自带了命令行客户端【当前学习时主要使用这个客户端】

    redis-cli:有两种连接方式

    redis-cli
    redis-cli -h 127.0.0.1 -p 6379
    

    image-20240629181351847

  2. 图形化界面的客户端(桌面程序,web程序)
    像这样的图形化程序,依赖windows系统,而未来在实际工作中,你用来办公的windows系统,连接到服务器可能会有诸多限制(中间可能会经历很多的跳板机,堡垒机,权限校验等),你的windows上的图形化界面客户端能不能连上你们的服务器里的redis,这是一个未知数!(这也和MySQL同理)

  3. 基于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的命令非常多,需要我们:

  1. 掌握常用命令(多操作多练习)
  2. 学会使用redis文档

2.get和set指令

​ Redis 中最核心的两个命令,get 是根据 key 来取 value,set 是把key 和 value 存储进去。在redis中,是按照键值对的方式来存储数据的。
​ 我们必须先进入redis-cli 客户端程序,才能输入 redis 命令。

image-20240629231745417

​ 对于上述这里的 key value,不需要加上引号,就是表示字符串的类型。当然,如果要是给key 和 value 加上引号,也是可以的(单引号或者双引号都行),并且在redis中,命令是不区分大小写的。

​ 而 get 命令直接输入key,就能得到value。如果当前key不存在,就会返回nil,和null/NULL是一个意思。

3.全局命令keys

​ Redis支持很多种数据结构,整体来说,redis 是键值对结构,key 固定就是字符串,value实际上会有很多种类型(字符串,哈希表,列表,集合,有序集合),操作不同的数据结构就会有不同的命令。而全局命令,就是能够搭配任意一个数据结构来使用的命令。

​ 关于keys指令:用来查询当前服务器上匹配的key,通过一些特殊符号(通配符)来描述key的模样,匹配上述模样的key,就能被查询出来。常用命令:|

KEYS pattern

​ 其中pattern:是包含特殊符号的字符串,有的地方翻译成“样式”或者“模式”,存在的意义就是去描述另外的字符串长啥样的。那pattern具体是咋写的呢,我们来举一些例子,首先我们向数据库中插入一些数据。
image-20240630000808694

  1. ?:匹配任意一个字符
    image-20240630000851727

  2. *:匹配0个或者多个任意字符
    image-20240630000929440

  3. [abcde] 只能匹配到 a b c d e,别的不行,相当于给出固定的选项了
    image-20240630001025958

  4. [^e] :排除e,只有 e匹配不了,其他的都能匹配
    image-20240630001131695

  5. [a-b]:匹配a-b这个范围的字符,包括两侧边界

    image-20240630001223453

上述的匹配规则,大家都不要刻意的去背;用的时候差一下就好啦

注意事项:
keys命令的时间复杂度是O(N),所以,在生产环境上,一般都会禁止使用keys命令,尤其是大杀器keys *(查询redis中所有的keys!!!)

​ 因为:生产环境上的key可能会非常多!而redis是一个单线程的服务器,执行keys * 的时间非常长,就使redis服务器被阻塞了,无法给其他客户提供服务!(这样的后果可能是灾难性的),因为redis经常会被用于缓存,挡在mysql的前面,替mysql负重前行的人,万一redis被一个keys * 阻塞住了,此时其他的查询redis 的操作就超时了,此时这些请求就会直接查数据库,突然一大波请求过来,mysql措手不及,就容易挂了。整个系统就基本瘫痪了。如果你要是没能及时发现,及时恢复的话,那就麻烦啦!

4.生产环境的概念

​ 未来在工作中会涉及到几个环境:

  1. 办公环境(入职公司之前,公司给你发个电脑)

    笔记本(windows,mac)/台式机,现在办公电脑,一般8核16G内存512G磁盘

  2. 开发环境,有的时候,开发环境和办公环境是一个,一般是做前端/做客户端的。有的时候,开发环境是单独的服务器(28核128G内存,4T硬盘这样的)一般是后端程序,为什么呢?

    1. 编译一次时间特别久(C++) => C++ 23才引入 module,这个问题 #include 要接锅,所以考虑使用高性能服务器进行编译。
    2. 有的程序一启动要消耗很多的cpu和内存资源,办公电脑难以支撑。一般的商业搜索项目,启动起来要吃100G的内存
    3. 有的程序比较依赖linux,在windows环境搭不起来。
  3. 测试环境(测试工程师使用的,一般也是28核128G4T配置)

  4. 线上环境/生产环境

    (办公环境,开发环境,测试环境,也统称为线下环境),外界用户无法访问到的。线上环境则是外界用户能够访问到的。一旦生产环境上出问题,一定会对用户的使用产生影响。未来咱们去操作线上环境的任何一个设备/程序都要怀着12分的谨慎!

5.exists指令

​ exists 指令是判定 key 是否存在的。语法格式如下:

EXISTS key [key ... ]

​ 返回值:key 存在的个数。键值对存储的体系中(类似哈希表),key 得是唯一的呀,但是针对多个key来说,这是非常有用的。

​ 时间复杂度:O(1),因为redis组织这些key就是按照哈希表的方式来组织的。

redis支持很多数据结构 => 指的是一个value 可以是一些复杂的数据结构,redis自身的这些键值对,是通过哈希表的方式来组织的,redis具体的某个值,又可以是一些数据结构。

​ 我们要注意:redis是一个客户端-服务器 结构的程序,客户端和服务器之间通过网络来进行通信!所以我们考虑下下面两条指令有什么不同之处:

image-20240630141349664

​ 区别在于:分开的写法,会产生更多轮此时的网络通信。网络通信和直接操作内存相比,效率比较低,成本比较高。因为网络通信时需要封装和分用。
​ 进行网络通信的时候,发送方发送一个数据,这个数据从应用层,到物理层,层层封装(每一层协议都要加上报头或者报尾)=> 发一个快递,要包装一下,要包装好几层。
​ 接收方收到一个数据,这个数据要从物理层,到应用层层层分用(把每一层协议的报头或者尾给拆掉) => 收到一个快递,要拆快递,要拆很多层。而且网卡是 IO 设备,更何况你的客户端和服务器都不一定在一个主机上,中间可能隔着很远。
​ redis自身也非常清楚上述问题,所以redis的很多命令都是支持一次就能操作多个key 的/多种操作。

6.del指令

​ del(delete) 删除指定的 key,可以一次删除一个或者多个

DEL key [key ... ]

​ 时间复杂度:O(1)

​ 返回值:删除掉的key的个数
image-20240630161152935

​ 值得注意的是:之前学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指令格式:

EXPIRE key seconds
PEXPIRE key 毫秒级别,对于计算机来说,秒是一个非常长的时间

​ 注意:此处的设定过期时间,必须是针对已经存在的key设置。设置成功返回1,设置失败返回0

​ 时间复杂度:也是O(1)

image-20240630163720607

​ 对于ttl(time to live)指令,作用是查看当前key的过期时间还剩多少(以秒为单位),与此对应的还有pttl(以毫秒为单位)。我们之前学习网络原理,IP协议的时候,IP协议的报头中,就有一个字段,TTL,不过IP中的TTL不是用时间来衡量过期的,而是通过次数。

image-20240630163833658

8.Redis的过期策略【经典面试题】

​ redis的key的过期策略是怎么实现的?【经典面试题】

​ 一个redis中可能同时存在很多很多key,这些key中可能有很大一部分都有过期时间,此时,redis服务器咋知道是哪些key已经过期要被删除,哪些key还没过期??

​ 如果直接遍历所有的key,显然是行不通的,效率非常低。所以redis整体的策略就是:

  1. 定期删除

    ​ 每次抽取一部分,进行验证过期时间,保证这次抽取检查的过程足够块,为啥这里对于定期删除的时间,有明确的要求呢?因为redis是单线程的程序,主要的任务(处理每个命令的任务,扫描过期key …),如果扫描过期key消耗的时间太多了,就可能导致正常处理请求命令就被阻塞了(产生了类似于执行keys * 这样的效果)

  2. 惰性删除

    ​ 假设这个key已经到过期时间了,但是暂时还没删他,key还存在。紧接着,后面又一次访问,正好用到了这个key,于是这次访问就会让redis服务器触发删除key的操作,同时再返回一个nil

  3. 内存淘汰策略

    ​ 虽然有了上述两种策略结合,但整体的效果一般,仍然可能会有很多过期的key被残留了,没有及时删除掉。redis为了对上述进行补充,还提供了一些列的内存淘汰策略。这个暂时不介绍。

  4. 定时删除【网上的错误说法】

    ​ 含义:在设置Key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。【×】

    1. redis中并没有采取定时器的方式来实现过期key删除
    2. 如果有多个key过期,也可以通过一个定时器(基于优先级队列或者基于时间轮都可以实现比较高效的定时器)来高效/节省cpu的前提下来处理多个Key。

    为啥redis没有采取这种定时器的方式呢?很难考证为啥,个人猜测,基于定时器的实现,势必就要引入多线程了,redis早期版本就是奠定了单线程的基调,引入多线程就打破了作者的初衷。

9.定时器实现原理【扩展】

​ 定时器:在某个时间到达之后,执行指定的任务,我们来看看定时器如何实现

  1. 基于优先级队列/堆

    正常的队列是先进先出,优先级队列则是按照指定的优先级先出

    啥叫优先级高?这是我们自定义的

    在redis过期的key的场景中,就可以通过“过期时间越早,就是优先级越高”

    ​ 现在假定有很多key设置了过期时间,就可以把这些key加入到一个优先级队列中,指定优先级规则是过期时间早的,先出队列,优先级队列的队首元素,就是最早的要过期的key!!!举个例子:key1:12:00 key2:13:00 key3:14:00
    ​ 此时定时器中只要分配一个线程,让这个线程去检查队首元素,看是否过期即可!!!
    ​ 如果队首元素还没过期,后续元素一定没过期。此时扫描线程不需要遍历所有key,只盯住这一个队首元素即可!!另外在扫描线程检查队首元素过期时间的时候,也不能检查的太频繁。此时做法就是可以根据当前时刻和队首元素的过期时间,设置一个等待。当时间差不多到了,系统再唤醒这个线程。
    ​ 此时 扫描线程 不需要高频扫描队首元素,把cpu的开销也节省下来了。
    ​ 万一在线程休眠的时候,来了一个新的任务,是11:30 要执行。可以在新任务添加的时候,唤醒一下刚才的线程,重新检查一下队首元素,再根据时间差距重新调整阻塞时间即可。

  2. 基于时间轮实现的定时器

    ​ 首先,我们要把时间划分为很多个小段(划分的粒度,要看实际需求),每个小段上都挂着一个链表(通过函数指针实现),每个链表都代表一个要执行的任务。比如我们把时间粒度设置为100ms,假设需要添加一个key,这个 key 在300 ms之后过期,那么他将会被插入到第三个位置,如下图:
    image-20240630213635513

    ​ 此时这个起始位置的这个指针,就会每隔固定的间隔(此处是约定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中,上述类型操作方式差异很大,使用的命令,都是完全不同的。我们来操作示范一下:
image-20240630220913263

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中存在多种数据类型,见下图:
image-20240701154614727

​ redis底层在实现上述数据结构的时候,会在源码层面,针对上述实现进行特定的优化(内部的具体实现的数据结构(编码方式),还会有变数),来达到 节省时间/节省空间 的效果。就好比指鼠为鸭:鸭脖店承诺卖给你的这个东西吃起来和鸭脖是一样的,但是内部的数据结构,是否真的是鸭脖,可能会根据实际情况,做出一定的优化。

​ redis承诺,现在我这有个hash表,你要进行查询,插入,删除,操作,都保证O(1),但是这个背后的实现,不一定就是一个标准的hash表,可能在特定场景下,使用别的数据结构实现,但是仍然保证时间复杂度符合承诺!!!

​ 数据结构:redis承诺给你的,也可以理解成数据类型。
​ 编码方式:redis内部底层的实现。同一个数据类型,背后可能的编码方式是不同的,会根据特定场景优化。redis会自动适应,程序员在适应redis的时候一般感知不到。

数据类型内部编码说明
stringraw最基本的字符串(底层就是持有一个char数组)
intredis通常也可以用来实现一些“计数”这样的功能,当value就是一个整数的时候此时可能redis会直接使用int来保存
embstr针对短字符串进行的特殊优化
hashhashtable最基本的哈希表
ziplist在哈希表里面元素比较少的时候,可能就优化为ziplist了,压缩列表,能够节省空间
listlistedlist链表
ziplist压缩链表,从redis3.2开始,引入了新的实现方式quicklist,同时兼顾了linkedlist和ziplist的优点,quicklist就是一个链表,每个元素又是一个ziplist,把空间和效率都折中的兼顾到,类似C++的std::deque
sethashtable
inset集合中存的都是整数
zsetskiplist跳表,经典题目“复杂链表复制”,跳表也是链表,不同的是,每个节点上有多个指针域,巧妙的搭配这些指针的指向,就可以做到查询元素的时间复杂度为O(logN)
ziplist

​ 为啥有些结构需要压缩呢?因为redis上有很多很多key,可能是某些key 的value 是hash,此时,如果key特别多,对应的hash也特别多,但是每个hash又不太大的情况,就尽量去压缩,压缩之后就可以让整体占用的内存更小了。

​ redis会自动根据当前的实际情况选择内部的编码方式,自动适应的。那我们是否要记住说明时候用啥编码方式呢?只记思想,不记数字!!!

​ 网上的说法,如果字符串长度小于39字节,使用embstr,超过39字节使用raw(记数字,没有任何意义!因为数字都是可配置的,像比于知道数字,更要知道数字是怎么得到的)

​ redis还提供了object encoding key 指令来查看key 对应的value的实际编码方式。
image-20240701161833746

2.关于单线程模型

​ redis的单线程模型,redis只使用一个线程,处理所有的命令请求,不是说redis服务器进程内部真的就只有一个线程,其实也有多个线程,多个线程是在处理网络IO

​ 假设,有很多客户端(假定为两个),同时操作一个redis服务器,同时适应increase操作想把key对应的value进行+1操作,会引发线程安全问题(在多线程中,针对类似于这样的场景,两个线程尝试同时对一个遍历进行自增,表面上看是自增两次,实际上可能只自增了一次)。当前这两个客户端,也相当于“并发”的发起了上述的请求,此时就意味这是否服务器这边也会存在类似的线程安全的问题呢?

​ 幸运的是,并不会,因为redis服务器实际上是单线程模型,保证了当前收到的这多个请求是串行执行的!!!多个请求同时到达redis服务器,也是要先在队列中排队的,再等待redis服务器一个一个的取出里面的命令再执行。微观上讲,redis服务器是串行/顺序 执行这多个命令的。就比如:想象学校食堂,只有一个打饭窗口,每次一到放学,在校学生就赶紧往食堂跑,宏观上,我们一群人,往食堂跑是并发执行的,微观上,进了食堂的门之后,还是需要排队的。

​ redis能够使用单线程模型 很好的工作,原因主要在于redis的核心业务逻辑,都是短平快的,不太消耗cpu资源,也就不太吃多核了。弊端在于redis必须要特别小心,某个操作占用时间上,就会阻塞其他命令的执行!!!
image-20240701165642509

3.redis单线程快的原因【重要面试题】

​ redis虽然是单线程模型,但为啥效率这么高呢?速度这么快呢?【参照物是其他数据库(mysql,oracle,sql server)】,原因有以下几点:

  1. redis访问内存,其他数据库则是访问硬盘

  2. redis的核心功能,比数据库的核心功能更简单。

    ​ 数据库对于数据的插入删除查询,都有更复杂的功能支持,这样的功能势必要花费更多的开销,比如:针对插入删除,数据库中的各种约束,都会使数据库做额外的工作。redis干的活少,提供的功能相比于Mysql也是少了不少。

  3. 单线程模型,避免了一些不必要的线程竞争开销

    ​ redis每个基本操作,都是短平快的,就是简单操作一下内存数据,都不是什么特别消耗cpu的操作。就算搞多个线程也提升不大。

  4. 处理网络IO的时候,使用了epoll这样的IO多路复用机制

    ​ 一个线程,就可以管理多个socket,针对TCP来说,服务器这边每次要服务一个客户端,都需要给这个客户端安排一个socket,一个服务器服务多个客户端,同时据有很多的socker,这些socket上都是无时不刻的在传输数据吗???其实很多情况下,每个客户端和服务器之间的通信也没那么频繁,此时这么多socket大部分时间都是静默的,上面是没有数据需要传输的。同一时刻,只有少数socket是活跃的。

    ​ 而最开始介绍TCP服务器的时候,有一个版本就是每个客户端给分配一个线程,客户端多了,线程就多了,系统开销就大了。而通过IO多路复用技术,就可以实现用一个线程来处理多个socket!!!(操作系统给程序员提供的机制,提供了一套api,内部的功能都是操作系统内核来实现的)

    ​ linux上提供的IO多路复用,主要是三套API:select,poll,epoll(这个是运行效率最高的机制)

    ​ 那么epoll机制是如何运行的呢?这里可以举一个生活中的例子:校区门口有很多小摊,有一天晚上我想吃蛋炒饭,我妈想吃肉夹馍,我弟想吃饺子,于是我们有以下几种方案:

    1. 我自己去,先买蛋炒饭,等,再买肉夹馍,等,再买饺子,等,需要等三次!,这是效率最低的方案
    2. 我们三个一起去,我去买蛋炒饭,我妈去买肉夹馍, 我弟去买饺子,各等各的,效率大大提升了,但是系统开销太大了。
    3. 我自己去,先买蛋炒饭的时候,等的时候回过头就买肉夹馍,在等肉夹馍的时候再去饺子,这三份饭,哪个先做好了,对应的老板就可以喊我一声(epoll事件通知/回调机制),此时就能让我一个线程同时去做三件事,能高效的完成这三件事的前提是,这三件事的交互都不频繁,大部分事件都是在等的。如果这三件事都是交互特别频繁的,还是老老实实的多搞几个线程靠谱,一个线程就容易忙不过来,就比如你这边打游戏呢,女朋友的电话来了!!!

4.string类型及基本命令

1.string类型基本介绍

​ redis所有的key都是字符串,而value的类型是存在差异的。

​ Redis中的string(字符串),直接就是按照二进制数据的方式存储的(不会做任何的编码转换,存的是啥,取出来就还是啥),所以一般来说,redis遇到乱码的问题概率是更小的。讲mysql的时候,知道mysql默认的字符集,是拉丁文,所以插入中文就会失效。而redis不仅仅可以存储文本数据,整数,普通的文本字符串,JSON,xml,二进制数据(图片,视频,音频 …),音频视频体积可能会比较大,redis对于string类型,现在了大小最大为512M,因为redis是单线程模型,希望进行的操作都能比较快速。

2.set和get

SET key value [expiration EX seconds | PX milliseconds] [NX|XX]

NX:如果Key不存在,才设置,如果key存在,则不设置
image-20240702133316786

XX:如果key存在,才设置(相当于更新key的value),如果key不存在,则不设置(返回nil)
image-20240702133355292

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的(生存事件)也会失效
image-20240702133820629

​ 接下来介绍一个快速失去年终奖的小技巧:清楚redis上的所有的数据 ==> 删库,相当于mysql里的drop database。FLUSHALL 可以把redis上所有的键值对都带走。以后在公司,尤其在生产环境的数据库,千万千万不敢敲!!当前学习阶段在自己电脑上敲一敲还好。
image-20240702134411494

​ 接下来我们再来看一下get指令

GET key

​ 对于GET来说,只是支持字符串类型的value,如果value是其他类型,使用GET获取就会出错!!!
image-20240702134605633

3.mset和mget

​ 这两个指令的作用,一次操作多组键值对。
image-20240702134801049

​ 语法格式:

MGET key [key ... ]
MSET key value [key value ...]

​ 事件复杂度:O(N) N是key的数量,此处的N不是整个redis服务器中所有key的数量,而只是当前命令中,给出的key的数量。所以就可以认为是O(1)
image-20240702135259578

4.setnx,setex和psetex

​ SETNX:不存在才能设置,存在则设置失效。
​ SETEX:[SETEX key seconds value ] 设置key的过期时间,单位是秒
image-20240702135814721

​ 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来使用。
image-20240702141635099

​ incrby:可以针对key对应的value进行 +n 操作。

​ decr把key对应的value 进行 -1 操作。注意:key对应的value必须是整数,在64位的范围内,如果这个key 对应的value不存在,则当作0处理。decr的运算结果,也是计算之后的值。

​ decrby 就是把key对应的value进行 -n 的操作。

​ incrbyfloat:把key对应的value 进行 + - 运算,运算的操作数可以是浮点数。这个指令只能通过加上负数的形式来实现减法。虽然此处没有提供减法版本,但是使用redis进行的计数操作,一般都是针对整数来进行的。
image-20240702142745941

​ 上述操作的时间复杂度,都是O(1),由于redis处理命令的时候,是单线程模型,多个客户端同时针对同一个key进行incr操作,内部是通过一个执行队列来维护的,不会引起“线程安全”问题。

6.append等指令

​ 字符串,也支持一些常见的操作,比如拼接,获取/修改 字符串的部分内容,获取字符串长度。append操作语法如下:

APPEND KEY VALUE

​ append返回值:长度的单位是字节!redis的字符串,不会对字符编码做任何的处理(redis不认识字符,只认识字节)
​ 当前咱们的xshell终端,默认的字符编码是utf8,在终端中输入汉字之后,也就是按utf8来编码的。一个汉字在utf8字符集中,通常是3个字节的。在启动redis客户端的时候,加上一个 - -raw 这样的选项,就可以使redis客户端能够自动的把二进制数据尝试翻译。
image-20240702143955675

​ setrange指令的语法:

SETRANGE key offset value

​ offset:偏移量(从第几个字节,开始进行替换)

​ 返回值:替换之后新的字符串的长度

image-20240702144220340

​ setrange 针对不存在的key也是可以操作的,不过会把offset之前的内容填充成0x00

image-20240702145522642

​ strlen 获取到字符串的长度,单位是字节,语法格式:

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的标准库内部,在进行上述的操作过程中,程序员一般是感知不到编码方式的变换的。

image-20240702144307048

7.string编码方式

​ string内部有三种编码方式:

  1. int:64位/8字节 的整数
  2. embstr:压缩字符串,适用于表示比较段的字符串
  3. raw:普通字符串,适用于表示更长的字符串,只是单纯的持有字节数组、

我们之前介绍过,通过使用object enconding来查看数据结构的真实编码方式:
image-20240702155839200

​ redis的string类型,一般是大于39个字节就使用raw的编码方式来存储,但不建议大家去记长度大于39这样的数字。

​ 假如有个业务场景,都会有很多很多的key,类型都是string,但是每个value的string长度都是100左右。为了关注整体的内存空间,这样的字符串使用embstr来存储也不是不能考虑,上述效果具体怎么实现呢?

  1. 先看redis是否提供了对应的配置项,可以修改39这个数字
  2. 如果没有提供配置项,就需要针对redis的源码进行魔改了

​ 这也是因为为啥很多大厂,往往都是自己造轮子,而不是直接使用业界成熟的呢?
开源的组件,往往考虑的都是通用性,但是大厂往往会遇到一些极端的业务场景,往往就需要根据当前的极端业务,针对上述的开源组件进行定制化。

8.string类型的应用场景

1.缓存(Cache)功能

​ 见下图:
image-20240702161920421

​ 整体的思路:应用服务器访问数据的时候,先查询redis,如果redis上数据存在了,就直接从redis上去取数据交给应用服务器,不继续访问数据库了。

​ 如果redis上的数据不存在,再读取Mysql,把读到的结果,返回给应用服务器的同时,把这个数据也写入到redis中。

​ 像redis这样的缓存,经常用来存储“热点”数据(高频被使用的数据)。

​ 刚才上述描述的过程,相当于把最近使用到的数据作为热点数据。(暗含了一层假设,某个数据一旦被用到了,那么很可能在最近这段时间就会被反复用到)

​ 上述策略,存在一个明显的问题:随着时间的推移,肯定是会有越来越多的key在redis上访问不到,从而要去从mysql上读取并写入redis了,此时redis中的数据不是就越来越多嘛??有以下两种解决方案:

  1. 在把数据写给redis的同时,给这个key设置一个过期时间
  2. redis也在内存不足的时候,提供了淘汰机制(之后会细说)

伪代码实现功能:

  1. 假设业务是根据用户uid获取用户信息

    UserInfo getUserInfo(long uid) {
    ...
    }
    
  2. 首先从 Redis 获取用户信息,我们假设用户信息保存在 “user:info: <uid>” 对应的键中:

    // 根据 uid 得到 Redis 的键
    String key = "user:info:" + uid;
    // 尝试从 Redis 中获取对应的值
    String value = Redis 执⾏命令:get key;
    // 如果缓存命中(hit)
    	if (value != null) {
    	// 假设我们的⽤⼾信息按照 JSON 格式存储
    	UserInfo userInfo = JSON 反序列化(value);
    	return userInfo;
    }
    
  3. 如果没有从redis中得到用户信息,及缓存miss,则进一步从mysql中获取对应的信息,随后写入缓存并返回:

    // 如果缓存未命中(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如何存储统计数据见下图:
image-20240702162349266

​ 伪代码:

// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
	key = "video:" + vid;
	long count = Redis 执⾏命令:incr key
	return counter;

​ 许多应用都会使用Redis 作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。如上图所示,例如视频网站的视频播放次数可以使用Redis 来完成:用户每播放⼀次视频,相应的视频播放数就会自增1。
​ 实际中要开发⼀个成熟、稳定的真实计数系统,要面临的挑战远不止如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。

3.共享会话(Session)

​ Cookie(浏览器存储数据的机制),Session(服务器这边存储数据的机制)
举个取医院看病的例子,我是一个病人,相当于一个客户端,医生要给我诊病,医生要给我提供服务,相当于服务器。
​ 由于病人需要多次访问服务器,服务器这边就需要很好的了解到病人每次复查时候的状态。医院有多个医生,即有多个服务器,这些服务器,是以负载均衡的方式来提供服务的 ,同一个病人,两次来医院看病,遇到的医生可能是不同的,此时,如果每个医生只是靠自己的记忆,来存储病人的状态,此时两次访问的是不同的医生,医生就很难知道病人之前的情况了。医院正确的做法是使用一套系统,记录病人的病历,诊断结果,治疗情况(这个就是所谓的会话,客户端和服务器在交互过程中产生的一些专属于该客户端的中间状态的数据),让多个医生共享。
image-20240702163243404

​ 如果每个应用服务器,维护自己的会话数据,此时彼此之家不共享,用户请求访问到不同的服务器上,就可能会出现一些不能正确处理的情况了。

image-20240702163431168

​ 此时所有的会话数据(病人的信息),都被各个服务器(医生)共享了,这时候就不会出现问题了。

4.收集验证码
  1. 生产验证码:用户输入一下手机号,点击获取验证码

    1. 限制1分钟之内,最多获取5次验证码

      主要还是怕用户频繁获取验证码,对于我们服务器压力过大

    2. 每次获取验证码必须间隔30秒

      验证码存在一个有效时间,此处假定是五分钟

  2. 检查验证码:把短信收到的验证码这一串数,提交到系统中,系统进行验证验证码是否正确。

上述的过程可以用下面的伪代码来说明:

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;
	}
}

5.hash类型介绍及基本命令

1.hash类型的基本介绍

哈希表【是我们之前学过的所有的数据结构中,最最重要的】

  1. 日常开发中,出场频率非常高
  2. 面试中也是非常重要的考点

​ 我们知道,redis自身已经是键值对结构了,redis自身的键值对其实就是通过哈希的方式来组织的!!!把key这一层组织完成之后,到了value这一层,value的其中一种类型还可以再是哈希。
image-20240703143749199

2.hset和hget

​ 设置一个哈希的关键字为 HSET 关键字,语法格式如下:

HSET key fidld value [field value ... ]

​ 这里的value通常为字符串

​ 返回值:是设置成功的 键值对(field - value)的个数
image-20240703140343126

​ 如何拿到value的值呢(指的是field 中的value的值),通过使用HGET命令,语法如下:

HGET key field

image-20240703140535084

3.hexists和hdel

​ HEXISTS指令用于判断,相关的key和field对应的值是否存在。语法格式如下:

HEXISTS key field

image-20240703141027071

​ HDEL指令:用于删除hash中指定的 字段。语法格式:

HDEL key field [field ...]

​ 注意:del删除的是key;hdel删除的是 field【有什么区别呢,在下面操作中展示】

​ 返回值:本次操作删除的字段个数

image-20240703142534095

4.hkeys和hvals

​ HKEYS 这个操作,先根据key找到对应的hash,时间复杂度为O(1),然后再遍历整个hash,这一步的时间复杂度为O(N),这里N是hash的元素个数。语法:

HKEYS key

image-20240703143107430

​ 注意:这个操作也是存在一定的风险的!类似之前介绍过的 keys *,因为主要咱们也不知道某个hash中是否会存在大量的field。

​ HVALS 这个指令和HKEYS相对,能够获取到hash中所有的value。语法格式为:

HVALS key

​ 这个操作的时间复杂读,也是O(N),这个N是哈希的元素个数,如果哈希非常大,这个操作就可能会导致redis服务器被阻塞住。
image-20240703143519220

​ H 系统的命令,必须要保证key对应的value得是 哈希类型的!!

5.hgetall和hmget

​ HGETALL这个指令,获取key对应的所有的field和value。这个指令的时间复杂度还是蛮大的,一般情况下,我们不需要查询所有的field,可能只查其中的几个field。
image-20240703143933692

​ HMGET,这个操作类似于之前的MGET,可以一次查询多个field。HGET一次只能查一个field。
image-20240703144053312

​ 注意:多个value的顺序和field的顺序是匹配的。

​ 有没有hmset,一次设置多个field和value呢???是有的,但是并不需要使用,hset已经支持一次设置多个field和value了。

​ 上述的hkeys,hvals,hgetall都是存在一定的风险的,hash的元素个数太多,执行的耗时会比较长,从而阻塞redis。

​ 可以考虑使用hscan遍历redis的hash,因为他属于“渐进式遍历”,敲一次指令,遍历一小部分,再敲一次,再遍历一小部分,,连续执行多次,就可以完成整个的遍历过程了。这个思想也叫做化整为零。

6.hlen等指令

​ HLEN指令,用于获取hash的元素个数,这个是不需要遍历的,所以时间复杂度为O(1)。语法个数如下:

HLEN key

image-20240703145241643

​ HSETNX key field value,这个指令类似于setnx,key和field不存在的时候,才能设置成功,如果存在,则失败。

image-20240703145517431

​ hash这里的value,也可以当作数字来处理,
​ hincrby就可以加减整数,hincrbyfloat就可以加减小数【使用频率不算很高,redis没有提供类似的incr ,decr …】 ,这几个操作的时间复杂度都是O(1)。

image-20240703150305390

7.hash的编码方式

​ 我们常见的压缩:rar,zip,gzip,7z ….都是一些具体的压缩算法。压缩的本质,就是针对数据进行重新编码。不同的数据,就有不同的特点,结合这些特点,进行精妙的涉及,重新编码之后,就可以缩小体积。
​ 比如举一个粗糙的编码方式:abbcccddddeeee可以重新编码为1a2b3c4d5e。

​ ziplist也是同理,内部的数据结构也是精心设计的。目的是为了节省空间。因为如果表示一个普通的hash表,可能会浪费一定的空间。(hash首先是一个数组,数组上有些位置有元素,有些位置没有元素),ziplist付出的代价,进行读写元素,速度是比较慢的。如果元素个数少,慢的并不明显,如果元素个数太多了,慢的就会雪上加霜。

​ 所以,如果:

  1. 哈希中的元素个数比较少,使用ziplist表示,元素个数比较多时,使用hashtable来表示 可以在hash-max-ziplist-entries配置(默认512字节)。
  2. 每个value的值长度都比较短,使用ziplist表示,如果某个value的长度太长了,也会转换成hashtable 可以在hash-max-zip-value配置(默认64字节)

这个配置项就是可以写到redis.conf文件中的。

image-20240703153234482

8.哈希的应用

1.作为缓存

​ 之前介绍过,string也是可以作为缓存的,但是当我们存储结构化的数据(类似于 数据库 表 这样的结构)的时候,使用hash类型更加合适一些。

​ 比如下面是两条用户信息,用户的属性表现为表的列,每条用户的信息表现为行,如果映射关系表示这两个用户信息。则:
image-20240703165414328

image-20240703165559846

​ 相比于使用json格式的字符串缓存用户信息,哈希类型则变得更加直观,并且在更新操作上变得更加灵活。可以将每个用户的id定义为键后缀,多对field-value对应用户的各个属性,类似如下伪代码:

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两种内部编码的转换,可能会造成内存的较大消耗。

​ 原生字符串类型 — 使用字符串类型,每个属性一个键,这种方式,相当于把同一个数据的各个属性,给分散开表示了。这就是低内聚

//原生字符串类型存储表数据
set user:1:name James
set user:1:age 23
set user:1city Beijing

​ 高内聚就是把有关联的东西放在一起,最好能放到指定的地方。

​ 耦合:两个模块/代码之间的关联关系,关联关系越大,就容易相互影响,认为就是耦合越大。我们追求低耦合,避免“牵一发动全身”,这边一改出bug,影响到了其他的地方。

​ 我们再来看一下之前使用哈希存储表数据的时候:
image-20240703170910350

​ 这里这个uid不存可以吗,是否就直接使用key中的id就可以进行区分了呢,这样是不是存储空间又进一步的节省了呢??

​ 如果确实不想存这个uid也可以,但是在工程实践中,一般都会把uid在value中也再存一份,再存一份的化,后续写到相关代码,使用起来会比较方便。

6.list的基本介绍及基本命令

1.list类型的基本介绍

​ 列表(List)相当于数组或者顺序表。不过要注意:list内部的结构(编码方式)并非是一个简单的数组,而是更接近于“双端队列”(deque),redis中的list的结构图如下:
image-20240704140358757

​ 约定最左侧元素下标是0,redis的下标支持负数下标,这个负数下标可以支持使用getrange函数。列表中的元素是有序的(这里指的是顺序很关键,如果把元素位置颠倒,顺序调换,此时得到的新的list和之前的list是不等价的)

​ 区分获取和删除的区别:

  1. lindex 能获取到元素的值
  2. lrem 也能返回被删除元素的值

​ 列表中的元素是允许重复的,像hash这样的类型,他的field是不能重复的。因为当前的list,头和尾都能高效的插入删除元素,就可以把这个list当作一个 栈/队列 来使用了。Redis有一个典型的应用场景,就是作为消息队列。最早的时候,就是通过list类型。后来redis又提供了一个stream类型,这个类型比list类型更加适合作为消息队列来使用。

2.lpush和lrange

​ lpush指令:一次可以插入一个元素,也可以插入多个元素。语法格式如下:

LPUSH key element [element ...]

​ 比如我现在需要按照顺序 lpush:1 2 3 4 全部插入完毕,4 是在最前面的!
image-20240704142158757

​ 时间复杂度:O(1),返回值是list的长度。

​ 如果key已经存在,并且key对应的value类型,不是list,此时lpush命令就要报错【redis中所有的这些各种数据类型的操作,都是类似的效果】
image-20240704142234809

​ lrange指令用来查看list中指定范围的元素。语法格式如下:

LRANGE key start stop

​ 此处描述的区间也是闭区间,下标支持负数。
image-20240704142835924

​ 谈到下标,我们往往会关注超出范围的情况,
​ C++中,下标超出范围,一把会认为这是一个“未定义行为”(可能会导致程序崩溃,也可能会得到一个不合法的数据,还可能会得到一个看起来合法,但是错误的数据,也有可能得到一个符合要求的数据 … 开盲盒)。缺点:程序员不一定能第一时间发现问题!优点:效率是最高的!
​ 在java中,下标超出范围,一般会抛出异常,相比C++会多出一部下标合法性的检验,缺点:速度比上面要慢,优点:出现问题能及时发现。

​ 在redis中并没有采用上述的两种设定。redis的做法是:直接尽可能的获取到给定区间的元素,如果给定区间非法,比如超出下标,就会尽可能的获取对应的内容。此处对于越界下标的处理方式,更接近于python的处理方式。(python的切片)
image-20240704143545509

3.lpushx等指令

​ 这里x的意思是exists的意思,lpushx的语法格式如下:

LPUSHX key element [element ... ]

image-20240704144239679

​ rpush指令,尾插,语法格式如下:

RPUSH key element [element ... ]

​ LPUSH => left push 对应的是 lpop
​ RPUSH => right push 对应的是 rpop
但是我们要注意了:lrange对应的是list range 而不是left range
image-20240704145038461

​ 同时还有RPUSHX 指令,X -> exists。语法格式如下:

RPUSHX key element [element ... ]

​ 接下来我们来介绍lpop和rpop这两个指令。在当前的redis 5版本中,都是没有count参数的,从redis 6.2版本,新增加了一个count 参数。count这个参数用于描述这次要删几个元素。这两条指令的语法格式如下:

LPOP key
RPOP key

image-20240704145608023

​ reids中的list的一个双端队列,从两头插入/删除 元素都是非常高效的O(1),搭配使用rpush(尾插)和lpop(头删),就相当于队列了。搭配使用rpush(尾插)和rpop(尾删),就相当于栈了。

4.lindex等指令

​ 给定下标,获取到对应的元素。这个指令的时间复杂度是O(N),这里的N指的是list中的元素个数。如果下标非法,返回的是nil。这个指令的语法格式如下:

LINDEX key index

image-20240704150433143

​ linsert指令,返回值是插入之后,得到的新的list的长度。指令格式如下:

LINSERT key <BEFORE> | <AFTER> pivot element

image-20240704150708848

​ 万一要插入的列表,基准值存在多个,该怎么办呢?linsert 在进行插入的时候,要根据基准值,找到对应的位置,从左往右找,找到第一个符合基准值的位置即可。时间复杂度为O(N),N表示列表的长度。

​ 接下来是LLEN命令,这个命令的格式如下:

LLEN key

image-20240704170213217

5.lrem等指令

​ lrem rem => remove 这个指令的格式为:

LREM key count element

​ 这个count参数表示:要删除的个数,这个element表示要删除的值。有以下几种可能:

  1. count > 0:从前往后删count个
  2. count < 0:从后往前删count个
  3. count = 0:删除所有

举个例子:如果列表中现在存放的是 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4

  1. 当count = 2,element=1 时:返回值为 2 3 4 2 3 4 1 2 3 4 1 2 3 4
  2. 当count=-2,element = 1时:返回值为 1 2 3 4 1 2 3 4 2 3 4 2 3 4
  3. 当count=0,element = 1时:返回值为 2 3 4 2 3 4 2 3 4 2 3 4

6.ltrim和lset指令

​ 接下来我们再来看看LTRIM指令,这个指令用于保留start和stop之间区间内的元素,(区间外面两边的元素就直接被删除了),语法格式如下:

LTRIM key start stop

image-20240704171913460

​ lset指令,根据下标,修改元素。格式如下:

LSET key index element

​ 值得注意的是:这个指令的时间复杂度是O(N)的,而且,lindex这个指令可以很好的处理下标越界的情况,如果没有就直接返回nil,lset这个则会报错,但不会像js那样,直接在越界的下标处搞出一个元素来。
image-20240704172304886

7.blpop和brpop等指令

​ 在谈这两个指令之前,外面先谈一谈阻塞队列(BlockingQueue),多线程的时候,讲到了一个东西,叫生产者消费者模型。是使用队列作为中间的“交易场所(broker)”。我们期望这个队列有两个特性:

  1. 线程安全
  2. 阻塞
    1. 如果队列为空,尝试出队列,就会产生阻塞,直到队列不空,阻塞解除。
    2. 如果队列为满,尝试入队列,也会产生阻塞,直到队列不满,阻塞解除。

​ redis 中的list也相当于 阻塞队列一样,线程安全是通过单线程模型 来支持的。阻塞,则只支持 “队列为空”的情况,不考虑 队列满 。但阻塞版本 会根据timeout,阻塞一段时间,期间redis 可以执行其他命令(此处blpop 和 brpop看起来好像耗时很久,但实际上并不会对redis服务器产生负面影响!),使用brpop 和blpop 的时候,这里是可以显示的设置阻塞时间的 !(不一定是无休止的等待!)

​ 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。

​ blpop和brpop都是可以取尝试获取多个key的列表的元素的,多个key对应多个list,这多个list哪个有元素了,就会返回哪个元素。

​ 如果多个客户端同时对一个键执行pop,则最先执行命令的客户端会得到弹出的元素。命令的语法格式:

BLPOP key [key ... ] timeout

​ 此处 可以指定一个key,或者多个key,每个key都对应一个list

  • 如果这些list有任何一个非空,blpop都能够把这里的元素给获取到,立即返回
  • 如果这些list都为空,此时就需要阻塞等待,等待其他客户端往这些list中插入元素了。
  • 此处还可以设定超市时间,单位是秒,(Redis 6中,超时时间允许设定为小数,Redis 5中,超时时间得是整数)
  1. 针对一个非空的列表进行操作:

    ​ 返回值的结果相当于一个pair(二元组),一方面是告诉我们当前的数据是来自于哪个key,一方面是告诉我们取到的数据是啥。

    image-20240704173907534

  2. 针对一个空的列表进行操作

    ​ 我们先开一个客户端,对一个空的列表(键),进行blpop操作,查看效果:
    image-20240704174039539

    此时程序看上去就跟卡了一样,我们赶紧再开另一个redis客户端,通过新来的客户端,向key8中插入元素:

    image-20240704174209155

    此时我们再去看看第一个客户端上的响应:
    image-20240704174232819

  3. 针对多个key操作

    ​ 此时这些键都是没有list列表的,这时候blpop就在同时等待多个列表。image-20240704174341376

    我们随便选取一个列表,比如key11,在别的客户端上,向这个key11中插入元素,我们查看效果:
    image-20240704174501450

    我们看看第一个客户端,对于blpop指令的响应:
    image-20240704174543664

​ 总结:brpop和blpop完全一样(这里就是尾删了),此处的这俩阻塞命令,用途主要就是用来作为”消息队列“,当前这俩指令虽然可以一定程度上满足“消息队列”这样的需求,整体来说,这俩命令的功能还是比较有限。blpop和brpop 有些英雄迟暮。

8.list的编码方式

  • ziplist(压缩列表),把数据按照更紧凑的压缩形式进行表示的,节省空间。当元素个数多了,操作起来效率会降低。
  • linkedlist(链表)
  • quicklist:相当于是链表 和压缩列表的结合。整体还是一个链表,链表的每个节点,是一个压缩列表。每个压缩列表,都不让它太大,同时再把多个压缩列表通过链式结构连起来。

我们可以来进行测试一下:
image-20240705085505841

​ 这里测试了一些情况,发现都是通过使用quicklist来存储的。

9.list的应用场景

1.存储数据

​ 用list作为”数组“这样的结构,来存储多个元素。但是我们redis提供的查询功能,不像mysql这么强大。

​ 比如在mysql中,表示学生和班级信息时:如下图:
image-20240705085940101

​ 在这种表结构中,就可以和方便的通过外键来”查询指定班级中有哪些同学“。

而在redis中,存储结构则可以是这样的。
image-20240705090120507

2.使用redis作为消息队列

​ 作为生产者消费者模型使用。
image-20240705103632555

​ 这些消费者,谁先执行到这个brpop命令(这个操作,是阻塞操作,当列表为空的时候,brpop就会阻塞等待,一直等到其他客户端向列表中push 了元素),谁就能拿到这个新来的元素。像这样的设定,就能构成一个”轮询“式的效果。假设消费者执行顺序是 1 2 3 ,当新元素到达之后,首先是消费者1 拿到元素(按照执行brpop命令的先后顺序来决定谁先获取到的),消费者1 拿到元素之后,也就从brpop中返回了(相当于这个命令就执行完了),如果消费者1 还想继续消费,就需要重新执行brpop。此时,再来一个新的元素过来,就是消费者2 还想继续消费,也需要重新执行brpop。再来一个新元素,也就是消费者3 拿到这个元素了。

3.Redis 分频道阻塞消息队列模型

image-20240705173427120

​ 多个列表/频道 channel / topic ,这种场景是非常常见的,日常使用的一些程序,比如抖音:有一个通道,来传输短视频数据,还有一个通道来传输弹幕,还可以有频道,来传输 点赞,转发,收藏数据,还可以有频道来传输评论数据。搞成多个频道,就可以在某种数据发生问题的时候,不会对其他数据造成影响(解耦合)。

​ 每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

​ 分页获取用户的 Timeline,例如获取用户1 的前 10 篇微博:

keylist = lrange user:1:mblogs 0 9
for key in keylist {
	hgetall key
}

​ 当前一页中有多少数据,不确定时,可能会导致下面的循环比较大,从而会触发很多次的hgetall,也就是很多的 网络请求。此方案在实际中可能存在两个问题:

  1. 1 + n 问题。即如果每次分页获取的微博个数较多,需要执⾏多次 hgetall 操作,此时可以考虑使⽤ ,pipeline(流水线)模式批量提交命令【pipeline(流水线,管道),虽然咱们是多个redis命令,但是把这些命令合并成一个 网络请求 进行通信,就可以大大降低客户端 和服务器之间的交互次数了。】,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。

  2. 分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分【假设某个用户发了 1w个微博,list的长度就是1w,就可以把这1w 个微博拆成10份,每个就是1k,如果是想获取 到5k 个左右的微博。】。

7.set类型的基本介绍及基本命令

1.set类型的基本介绍

​ 我们谈及一个术语,这个术语往往可能有多种含义,比如我们谈及set,可能想到的是(1)集合,(2)设置(和get相对应的那个)。

​ 这里我们的set指的是(1)集合,集合就把一些有关联的数据放到一起,

  1. 集合中的元素是无序的!
    此处说的无序和前面list这里的有序,是相对应的。
    有序:顺序很重要,变换一下顺序,就是不同的list了。无序:顺序不重要,变换一下顺序,集合还是那个集合。比如:list:[1,2,3]和[2,1,3]是两个不同的list。而set:[1,2,3] 和[2,1,3] 是同一个集合。
  2. 集合中的元素是不能重复的(唯一的)

​ 咱们的set和list类似,集合中的每个元素,也都是string类型。(可以使用json这样的格式,让string也能存储结构化数据)

2.sadd等指令

​ 首先我们要知道如何才能向一个set中添加元素,命令的语法如下:

SADD key member [member . . .]

​ 咱们把 集合 中的元素,叫做member,就像hash类型中,哈希的元素叫做field类似。

​ 时间复杂度:O(1),返回值:表示本次操作后,添加成功了几个元素。
QQ_1720532813203

​ 如何查看,set中含有哪些元素呢,可以看下smembers指令。指令的格式如下:

SMEMBERS key

QQ_1720532930693

​ 那我们应该如何判定,当前元素是否在集合当中呢,使用sismember命令:

SISMEMBER key member

​ 我们发现,集合的操作指令都是带有S前缀的,当前这个指令是用于判断当前的元素是否在集合当中。

​ 我们如何知道当前key对应的set当中有多少个元素啊,总不能每次都把所有的元素都看一遍吧!即如何得到 set 中的元素个数。使用的是SCARD指令。指令格数如下:

SCARD key

QQ_1720533263442

3.spop等指令

​ pop 一般表示 ”从末尾“ 删除一个元素,但是集合中的元素是无序的,此时 哪个元素 是 ”末尾“ ?? 使用 spop 删除的元素的时候其实是随机删除的。指令格式如下:

SPOP key [count]

​ 不指定这个 count 的时候,是随机删除一个,指定这个count的时候,就是写几个就删几个。我们先来演示一下删除的过程:
QQ_1720533524855

​ 之前我们在key所对应的集合中存储的是 ”1,2,3,4“。这时候使用spop逐个元素进行删除的时候得到的删除顺序为“ 4 3 2 1 ”,如果这个时候我再次构造一个 1 2 3 4 这样的集合,此时还是通过 4 次spop进行删除,删除顺序是否还是 1 4 3 2 呢,肯定不是了!如果每次删除,都是 1 4 3 2,这还能叫做随机嘛?? 当前的spop 每次确实 是随机删除了 一个元素!!!

​ 返回值:从set 中删除并返回一个或者多个元素。注意,由于set 内的元素是无序的,所以取出哪个元素实际上是未定义行为,即可以看作是随机的。我们来看下官方文档是怎么描述的,见下:(官方文档承诺了咱们是随机的!!)

Removes and returns one or more random members from the set value store at key.

This operation is simliar to SRANDMEMBER, that returns one or more random elements from a set but does not remove it.
(这两条指令的底层实现逻辑是一样的)

​ 其实是人家在 源码 中,针对 spop 实现 的时候就采取了“生成随机数” 的方式。

QQ_1720534344038

4.smove等指令

​ 这条指令也是用于删除集合上的元素的,不过与spop随机删除不同,smove是指定式的删除。指定的格式如下:

SMOVE source destination member

​ 这条指令的作用是把member从source上删除,再插入到destination中。
QQ_1720534893699

​ 我们可以发现,本来key1 中的元素是“ 1 2 3 4”,key2 中的元素是 “5 6 7 8”,我们将指定的元素“1” 从key1中删除掉了,然后插入到了key2 当中。

​ 那,如果我给key里再添加一个1 ,再次把这个1 移动给key2 ,会怎么呢?
QQ_1720535109063

​ 我们发现,针对这种情况,smove并不会视为出错,也会按照 删除 - 插入 进行执行。

​ 那如果是要移动的元素 在source中 不存在呢?此时我们发现就会返回 0 表示移动失败了。
QQ_1720535293097

​ 接下来我们再来看下,srem指令的作用,这个指令一次删除一个member,也可以一次删除多个member这条指令的语法格式如下:

SREM key member [member . . .]

​ 返回值表示删除成功的元素的个数。
QQ_1720535557410

5.sinter等指令

​ 我们的集合通常来说会存在很多操作,比如求交集(inter),并集(union),差集(diff)。
​ 交集:最终结果同时出现在两个集合当中。比如A:1 2 3 4,B 3 4 5 6,这时候A和B的交集为:3 和 4。
​ 并集:把多个集合中的数据都集中放在一起,如果元素有重复,也最终只保留一份,比如此时A 和 B的并集:1 2 3 4 5 6
​ 差集:A 和 B做差集,就是找出哪些元素,在A中存在,同时在B中不存在。A 和 B 的差集:1 2 ,B 和 A 的差集:5 6。

​ 我们首先来看看sinter 指令,该指令的格式如下:

SINTER key [key . . .]

​ 此处每个 key 都对应一个集合,返回值就是 最终交集的数据。

​ 时间复杂度:O(N*M),N 是最小的集合元素个数,M 是最大的集合元素个数。
操作演示如下:
QQ_1720537367599

​ 我们再看下sinterstore指令,这条指令的格式如下:

SINTERSTORE destination key [key . . .]

​ 这条指令,会直接把算好的交集,放到destination这个key对应的集合当中了。

​ 返回值就是交集的元素个数。

​ 要想知道交集的内容,直接按照集合的方式访问 destination 这个 key 即可。

QQ_1720537584102

6.sunion等指令

​ 我们来看如何求并集,指令的个数如下:

SUNION key [key . . .]

​ sunion 返回的就是 并集 的结果数据。

​ 时间复杂度为O(N),N指的是总的元素个数。
QQ_1720537823990

​ sunionstore指令,这条指令的格式为:

SUNIONSTORE destination key [key . . .]

​ 直接把并集的结果存储到destination对应的集合当中,返回值为并集中的元素个数。
QQ_1720538071703

​ 我们再来看下sdiff指令,这条指令的格式见下:

SDIFF key [key . . . ]

​ 这条指令的返回值是差集的结果,时间复杂度为O(N)。比如我们之前探讨过A 和 B 与B 和 A之间差集的不同之处,这里我们通过redis再来验证下结果。
QQ_1720538634998

​ 根据之前的经验,那我们的sdiffstore 指令,指令格式为:

SDIFFSTORE destination key [key . . .]

​ 返回值为差集的元素个数。时间复杂度为O(N)。
QQ_1720538776663

​ 上述的这些命令,对应的时间复杂度,都是怎么计算出来的呢?如果你想知道,可以去看看 集合 内部的源码实现了。大家可以看看redis对应的这部分的源码。

7.set的内部编码方式

​ set内部的 编码方式有两种,一种是intset,还有一种是hashtable(哈希表)

  1. intset(整数集合):为了节省空间,做出的特定优化,当元素均为整数,并且元素个数不是很多的时候、
  2. hashtable(哈希表)

​ C++中的std::set 背后的数据结构是红黑树,java中的set 本身是一个接口,这个接口后面的实现,可以是treeset,也可以是hashset。接下来我们来操作检验下:
QQ_1720539723952

10.set的应用场景

  1. 使用set来保存用户的“标签”

    ​ 比如用户画像:分析出你这个人的一些特征【性别,年龄,居住地,爱好 …】,分析清楚特征之后,再投其所好。“千人千面(窥探用户的隐私)”,会根据用户的一些历史行为,就能看出来。这些用户数据,很多公司之间都在共享。通过上述过程,搜集到的用户特征,就会转换成“标签”(简短的字符串),此时就可以将这些字符串(标签)保存到redis的set当中。这样set可以自动的避免用户id重复的问题。

  2. 使用set来计算用户之间的共同好友

    基于“集合求交并”,比如QQ的共同好友功能。基于上述还可以做一些好友推荐。

  3. 使用set统计UV

    ​ 一个互联网产品,如何衡量用户量,用户规模??主要的指标是两个方面:

    1. PV page view:用户每次访问该服务器,每次访问都会产生一个pv
    2. UV user view:每个用户,访问服务器,都会产生一个uv,但是用一个用户多次访问,不会使uv增加,uv需要按照用户进行去重,上述去重的过程,就可以通过使用set来实现。

8.zset类型的基本介绍及基本指令

1.zset类型的基本介绍

​ zset是一种有序集合(升序/降序),这里的排序的规则是啥? 给zset中的member同时引入了一个属性分数(score),浮点类型。每个member 都会安排一个分数,进行排序的时候,就是依照此处的分数的大小来进行升序/降序 排序。
​ zset中的member 仍然要求是唯一的!(score则可以重复),因为zset主要还是用来存储member的!score在其中只是辅助。比如我们要对一个班所有的同学的分数进行排序的时候,member可以是每个同学的姓名或者学号,分数则作为辅助存储信息。

​ 我们现在可以总结下我们目前遇到的数据结构及应用;

数据结构是否允许重复元素是否有序有序依据应用
列表索引下标时间轴、消息队列
集合标签、社交
有序集合分数排行榜系统

2.zadd等指令

​ 使用zadd往有序集合中添加元素和分数,member和score 称为是一个“pair”,这个东西类似于C++里谈到的std::pair,不要把member 和score理解为“键值对”(key-value pair),键值对中,是有明确的“角色区分”,谁是键,谁是值,这都是非常明确的,一定的根据 键 -> 值。对于有序集合来说,是既可以通过member找到对应的score,又可以通过score找到匹配的member。加下来我们看下这条指令的格式:

ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]

​ 要注意的是:添加的时候,既要添加元素,又要添加分数。

XX: Only updata elements that already exist.Don`t add new elements.
NX: Only add new elements.Don`t updata already existing elements.

​ 这里的elements指的就是我们之前提到的member,不加 NX | XX 选项的时候,如果当前member不存在,此时就会达到“添加新member”的效果,如果当前member已经存在,此时就会更新分数。

LT:Only updata existing elements if the new score is less than the current score.This flag doesn`t prevent adding new elements
GT: Only updata existing elements if the new score is greater than the current score,This flag doesn`t prevent adding new elements.

​ 现在要更新分数了,发现现在要给定的新的分数,比之前的分数小,此时就更新成功,否则不更新。

CH:Modify the return value from the number of new elements added,to the total number of elements changed(CH is an abbreviation of changed).Changed elements are new elements added and elements already existing for which the score was updated.so elements specified in the command line having the same score as they had in the past are not counted.Note:normally the return value of ZADD only counts the number of new elements added.

​ 影响到zadd的返回值了,本来zadd返回的是新增元素个数。

INCR:when this option is specified ZADD acts like ZINCRBY.Only one score-element pair can be specified in this mode.

​ 时间复杂度:O(log(N)),之前讲hash,set,list的时候,我们知道很多时候,添加一个元素,复杂度都是O(1),此处,Zset则是logN。因为zset是有序结构,要求新增的元素,要放到合适的位置上(找位置),当然之所以是logN 不是N,也是充分利用了集合有序的这样的特点,(当然,zset内部的数据结构是通过跳表来实现的)

multiple elements have the same score,they are ordered lexicographically(they are still ordered by score as a first key),however,locally,all the elements with the same score.

​ 当我们的分数相同时,会再根据元素自身字符串的字典序来排序,分数不同,仍然会按照分数来排序。
QQ_1720593629737

Sorted sets are sorted by their score in an ascending way.

​ 前面说”有序“只是升序或者降序,实际上zet内部就是按照升序方式来排列的!!!我们来操作插入的过程演示下:
QQ_1720594729371

​ zrange 查看 有序集合中的元素详情了(类似于lrange,可以指定一对下标构成的区间),有序集合,本身元素就是有先后顺序的,谁在前,谁在后,都是很明确的!因此也就可以给这个有序集合赋予 下标 这样的概念了。我们来演示下这个操作:
QQ_1720596096725

​ 我们发现,当前的查询结果,它就是有序的(升序的),如果我们查询的时候需要带着score显示呢,也很简单,只需要带上withscores即可,操作示范如下:
QQ_1720599023548

​ 如果修改分数,影响到了之前的顺序,这时候就会自动给的一定元素位置,保持原有的升序顺序不变!比如这时候我们修改”zhoutai”的分数为100,然后再次查询,它是否会排到最下面去呢?
QQ_1720599207114

​ 这里我们想到了,C++ 的std::set 存储的元素也是有序的呀(升序/降序),但是这个set里面的值,是不能进行修改的。

​ 最后我们再来讨论下zadd指令的返回值,zadd指令在默认情况下,返回值就是新增成功的元素个数。如果我们对元素的score进行修改,并没有进行新增的化,返回值就是0,这个特点在上图我们也看到了。

​ 最后我们再来讨论下关于redis汉字编码的问题,redis内部存储数据的时候,是按照二进制的方式来存储的,这就意味着,redis服务器不负责”字符编码“的,我们如果需要把二进制字节对应回到汉字,还需要客户端来支持。即 redis-cli - -raw

3.zcard等指令

​ zcard这个指令用于统计当前的这个key对应多少个集合元素,指令的格式如下:

ZCARD key

​ 返回值为:存在的集合的个数。
QQ_1720600009116

​ 我们再看下zcount指令,这条指令的格式如下:

ZCOUNT key min max

​ 这里默认是一个闭区间,如果想排除边界值,可以加上括号。但是此处的表示非常奇葩,这里是实验你( 表示开区间,使用 [ 表示闭区间。
QQ_1720600215602

​ 但,加上区间检索就很奇怪了。
QQ_1720601226912

​ zcount 的时间复杂度是O(logN),
​ zcount指定min和max分数区间的,这里是先根据min找到对应的元素,再根据max找到对应的元素。整体复杂度为2*logN,实际上zset内部,会记录每个元素当前的 ” 排名/次序 “(下标),查询到元素,就直接知道了元素所在的 ”次序“ ,就可以直接把max 对应的元素次序 和 min 对应的元素次序,做减法即可【注意不是遍历,如果遍历,复杂度是O(logN+M)】。min 和 max 是可以写成 浮点数(zset 分数本身就是浮点数),在浮点数中,存在两个特殊的数值:inf:无穷大,-inf:负无穷大,zset 中分数也是支持使用 inf 和 -inf 作为 max 和 min 的。这里我们可以操作演示下看看:
QQ_1720602508251

4.zrange等指令

​ 这条指令我们先前已经使用过了,这里主要讨论下时间复杂度为:O(log(N) +M),这里的M是从start - stop 区间的元素个数。此处要根据下标来找到边界值。从 start 对应 的位置起,接下来就需要遍历了。
​ 我们再来看一下zrevrange指令,这里的是reverse 逆序的意思,按照分数降序进行遍历并打印了。我们来实操看一下:
QQ_1720603811540

​ 我们再来看一下zrangebyscore指令,这个指令是按照分数来找元素的,相当于和刚才的zcount类似。指令的格式如下:

ZRANGEBYSCORE key min max [WITHSCORES]

​ 这条指令的时间复杂度为O(logN+M),这个指令可能在 6.2.0 之后会废弃,并且功能合并到zrange中。
QQ_1720604305643

5.zpopmax等指令

​ 删除并返回分数最高的 count 个元素。语法:

ZPOPMAX key [count]

​ 这里本质上就是一个topK问题:数据结构 => 优先级队列,返回值就是被删除的元素(包括member和score),如果存在多个元素,分数相同【分数虽然是主要因素,如果分数相同会按照member字符串的字典序(这里可以回顾C语言,字符串和内存函数,strcmp(介绍字典序))决定先后!!!】,同时为最大值,zpopmax 在删的时候,仍然只删除其中的一个元素!!!

​ 时间复杂度:O(log(N) * M),N是有序集合的元素个数,count要删除的元素个数。此处删除的是 最大值!!!有序集合,最大值就相当于最后一个元素(尾删)。

​ 既然是尾删,为什么不把这最后一个元素的位置特殊记录下来,后续直接删除不就可以了O(1)复杂度,省去了查找的过程。可以讲O(logN) => O(1)。这个是很有可能可以实现的!

​ 但是很遗憾,目前redis并没有这样做,事实上,redis的源码中,针对有序集合,确实是记录了 尾部 这样的特定位置,但是实际删除的时候,并没有用上这个特性,而是直接调用了 一个 ”通用的删除函数“(这个函数给定一个member的值,进行查找,找到位置之后再删除),虽然存在这样的优化空间,但未来也不一定真的这样优化,因为优化这种活,要优化到刀刃上,优化一般是要先找到性能瓶颈,再做针对性能的优化。毕竟当前这个logN的速度其实是不慢的,如果N不是非常夸张的大,基本是可以近似看做O(1)的。
QQ_1720605486268

QQ_1720605515502

6.bzpopmax等指令

​ 这个指令跟我们的blpop 以及 brpop(针对list) 一样,是实现类似于阻塞队列的那种效果。我们先来看一下这条指令的使用方法。

BZPOPMAX key [key . . .] timeout

​ 咱们这里的“有序集合”也可以视为是一个“优先级队列”,有的时候,也需要一个带有“阻塞功能”的优先级队列。这个key都是一个有序的集合,阻塞也是在有序集合为空的时候触发阻塞,阻塞到有其他客户端插入元素时。timeout 表示超时时间,最多阻塞多久,单位是 s,支持小数形式,写作0.1也就是100ms。一开始redis里面没有key,我们使用bzpopmax,指令进入等待状态。
QQ_1720674031648 然后我们立马再开一个端口,向redis中加入元素。
QQ_1720674133148 然后再观察我们bzpopmax指令对应的端口,发现指令就以及允许结束了。
QQ_1720674249737
​ 这里的key 标识了有序集合,bzpopmax可以同时读多个有序集合的。如果有序集合中已经有了元素了,直接就能返回,不会阻塞了。

​ 时间复杂度:O(logN),就是删除最大值所花费的时间。如果当前bzpopmax同时监听了多个key,假设key是M个,此时时间复杂度是O(log(N) * M)吗? 不是的!!啥时候*M,每个这样的key上面都删除一次元素。而我们监听这么多key只是从若干key中删除一个而已。

​ 我们再来看看zpopmin指令,这条指令的作用是删除有序集合中,最小的元素。这条指令的格式如下:

zpopmin key [count]

​ 时间复杂度O(logN*M),此处的zpopmin 和 上面的zpopmax的逻辑是一致的(同一个函数实现的),虽然redis的有序集合记录了开头的元素,但是删除的时候使用的是通用的删除函数,导致了出现了重新查找的过程。
QQ_1720676538383

​ 还有一个是bzpopmin指令,这个指令的用法是和bzpopmax是一样的,这条指令的格式如下:

BZPOPMIN key [key . . .] timeout

​ 时间复杂度也是O(logN),这里就不再演示了。

7.zrank等指令

​ 这条指令最主要的是 有一个 查询位置 的过程。指令的格式如下:

ZRANK key member

​ 这条指令的时间复杂度为O(logN),相比较于zcount,zcount在计算的时候,就是先根据分数找到元素,再根据元素获取到排名,再把排名一减,得到了元素个数。zrank得到的下标,是从前往后算的(升序)。
image-20240711135534561

​ 相应的还有zrevrank指令,这条指令也是获取到member的下标,但是是反着算的。

ZREVRANK key member

QQ_1720677505424

​ 再接着是zscore指令了,这条指令的语法格式:

ZSCORE key member

​ 时间复杂度:O(1),前面,根据member找元素,都是logN,这里也是要先找元素呀,此处相当于redis对于这样的查询操作做了特殊优化,付出了额外的空间代价,针对这里进行了优化到了O(1)实现。

QQ_1720677924082

8.zrem等指令

​ 这条指令的作用是删除,指令的格式为:

ZREM key member [member . . .]

​ 时间复杂度为:O(logN * M),这里的N 是整个有序集合中的元素个数,M是参数中member的个数。
QQ_1720678576768

​ 我们再来看下zremrangebyrank指令,这条指令的语法格式为:

ZREMRANGBYRANK key start stop

​ 使用这个下标描述的范围进行删除,闭区间。时间复杂度:O(logN +M),N 整个有序集合的元素个数,M是start - stop 区间中元素个数。此处查找位置,只需要进行一次(不需要重复进行),接下来进行操作示范:
QQ_1720679064674

​ 再者我们看一下zremrangebyscore这个指令,指定一个删除的区间,通过分数来描述,是闭区间的 (排除边界值
image-20240711151029670

​ 时间复杂度为O(logN + M)

9.zincrby等指令

​ 这条指令不光会修改分数内容,也能同时移动元素位置,保持整个哟许集合仍然是升序的。指令的个数如下:

ZINCRBY key increment member

QQ_1720682071752

10.zinterstore等指令

​ 求交集,并集,差集的指令分别为sinter,sunion,sdiff,而zinter,zunion,zdiff这几个指令都是从redis 6.2 版本才开始支持的,咱现在使用的是 5 版本,此处暂时不涉及。

​ zinterstore:求交集,将结果保存到另一个key中,zunionstore:并集,结果保存到另一个key 中。指令的格式如下:

ZINTERSTORE destination numkeys key [key . . .] [WEIGHTS  weight [weight . . .]] [AGGREGATE <SUM> | MIN | MAX >]

​ 其中的numkeys 是整数,描述了后续有几个key参与交集运算,这里的destination是要把结果存储到哪个key对应的zset中,这里的WEIGHTS是权重参数,因为咱们的集合是一个有序集合,是带有分数的,此处指定的权重,相当于一个系数,会乘以当前的分数。

​ 前面介绍的命令,不涉及到类似于此处的设定,很多命令也是支持多个key的呀,比如zadd,mget,mset等等,这里这样做的主要原因是,numkeys描述出key的个数之后,就可以明确的知道,后面的“选项”是从哪里开始的了,避免选项和keys 混淆。此处的设定,特别像我们之前学过的两个知识点(本质上是一个),HTTP协议,首行,请求头(键值对)【Content-Length 描述了 正文的长度!】,空行,正文。如果没有这个content-length字段,那么这里数据错了就容易产生粘包问题【HTTP在传输层,是基于TCP的,而TCP是面向字节流的,粘包问题,是面向字节流这种的一个普遍存在的问题】。如何解决粘包问题?【1.明确包的长度,2.明确包的边界】

​ 接下来我们来看一个操作示范:这里key1当中是【zhangsan:10,lisi:20,wangwu:30】,key2是【zhangsan:15,lisi:25,zhaoliu:30】,在有序集合中,我们要知道member才是元素的本体,score只是辅助排序的工具人,因此,在进行比较“相同”的时候,只要member相同即可,score不一样就不一样呗,如果member相同,score不同,在进行交集合并之后,最终的分数是怎么算的呢,我们来演示看下:

QQ_1720686062539

​ 根据结果可以看出,score是两个对应的member的相加之和。再者我们来讨论下这条指令的时间复杂度:O(N*K)+O(M*log(M)),这里这个N是输入若干有序集合,里面元素最小的个数,而K是输入的有序集合的个数,也即我们输入了几个有序集合【把几个有序集合求交集】,M是最终结果的有序集合的个数。这个看起来很复杂,却决于redis的源码是咋写的。我们可以对其化简一下得到一个近似值,K近似看作2,化简一下,认为N和M是接近的(同一个数量级的)(这个假设并不严谨,只是近似看来),所以可以近似成O(M)+O(M*logM) => O(M*logM)

​ 最后还有我们的zunionstore指令,这条指令在用法上和zinterstore基本一致,时间复杂度为O(N)+O(M*log(M)),就不做过多介绍了。

11.zset编码方式

​ 如果有序集合中的元素个数比较少,或者单个元素的体积比较小,一般使用ziplist来存储【压缩列表,节省内存空间】。

​ 如果当前元素个数比较多,或者单个元素体积非常大,使用就skiplist来存储了。同样的,zset-max-ziplist-entries和zset-max-ziplist-value中配置。我们和之前一样再用object encoding指令来测试下。
QQ_1720692040606

​ 关于跳表,简单来说,跳表是一个“复杂链表”,查询元素,时间复杂度为logN,相比于树形结构(B+树),这个结构更加适合按照范围获取元素

12.zset的应用场景

​ 最关键的应用场景,排行榜系统(关键要点,用来排行的“分数”是实时变化的),虽然是实时变化,也能够高效的更新排名。

  1. 微博热搜
  2. 游戏天体排名
  3. 成绩排名

​ 如果我们使用zset来完成上述操作,就非常简单,比如游戏天梯排行,只需要把玩家信息和对应的分数给放到有序集合中即可,自动就形成了一个排行榜,随时可以按照排行(下标),按照分数,进行范围查询。
​ 随着分数发生变化,也可以比较方便的,使用zincrby指令来修改分数,排行顺序也能自动调整(logN)

​ 但,游戏玩家这么多,此时都用这个zset来存(内存),能存下嘛?比如如果有一个游戏有百万玩家,就已经是大的游戏了,假如有1亿玩家,假如userld 4个字节,score 8个字节,来表示一个玩家,大概是12 个字节。12 亿字节 => 1.2GB ,我们在内存中也是完全可以存的下的!

​ 最后是关于游戏排行榜,这里的先后顺序非常容易确定,但是有的排行榜就比较复杂,比如微博热搜,这里要考虑的是综合数值:

  1. 浏览量
  2. 点赞量
  3. 转发量
  4. 评论量

​ 具体有多少个维度,每个维度权重怎么分配,以及怎么设定是最优的,这些都是公司有专门的团队来做这块的事情,通过一些人工智能的方式来进行计算,预测的。最后我们根据每个维度,计算得到的综合评分,得到热度!此时就可以借助zinterstore / zunionstore 按照加权的方式来处理啦!

​ 此时,就可以把上述每个维度的数值都放到一个有序集合中,member就是微博的id,score就是各自维度的数值,通过zinterstore或者zunionstore把上述有序集合按照约定好的权重,进行集合间运算即可,得到的结果集合的得分就是热度(排行榜也就做出来了!)

​ 总结就是:zset是一个选择,不是说非得用redis的zset,有些场景确实可以用到有序集合,又不方便使用redis,那就可以考虑使用其他的有序集合。

9.其他类型补充

​ Redis最关键的五个数据类型【string,list,hash,set,zset】,应用广泛,频繁使用。而其他类型比如【streams,geospatial,hyperloglog,bitmaps,bitfieds】等指令都是在特定的 场景中才会去使用的。

1.stream类型

​ 官方文档的意思,就是stream类型,就可以用来模拟实现这种事件传播的机制。

  • 事件,epoll / io 多路复用
    每个网卡 / io 多路复用,每次网卡/socket 上有可读可写的数据,都会通过这种事件回调机制来通知到咱们的应用程序代码。

  • js (做界面)

    ​ 点击事件,键盘事件,窗口大小改变 / 位置改变事件【什么是事件?有些操作,我们也不知道它啥时候出现,只能等这个事情出现了之后,再采取动作】。

​ 我们的stream就是一个队列(阻塞队列),是redis作为一个消息队列的重要支持,属于是list blpop/brpop 的升级版本。

2.geospatial类型

​ 这个类型主要用于经纬度,用来存储坐标的。存储一些点之后,就可以让用户给定一个坐标,去从刚才存储的点里面进行查找(按照半径,矩形区域),这个功能在“地图”应用中非常重要。比如下图是某导航的定位功能:
QQ_1720793977877

3.hyperloglog类型

​ 这个类型的应用场景只有一个,估算集合中的元素个数。我们还记不记得set有一个应用场景,用来统计服务器的UV(用户访问次数),使用set当然可以统计uv,但是存在的最大的问题在于,如果uv的数据量非常大,set就会消耗很多的内存空间。假设set存储userid,每个userid按照8个字节算,那么1亿的UV => 8亿的字节 =》0.8G的空间,但是如果我们使用HyperLogLog(这个类型并不是redis专有的)可以最多使用12kb字节空间就能实现上述的效果。1亿的UV算下来也就是800MB,这可太划算了老铁。

​ 之所以set需要消耗这么大的空间是因为set需要存储每个元素,而HyperLogLog不存储元素的内容,但是能够记录“元素的特征”【比如只是用来计数(记录出当前集合中有多少个不同的元素,但是不能告诉你这些元素都是啥)】,从而在新增元素的时候,能够知道当前新增的元素是一个已经存在的元素,还是一个新的元素,这个东西具体还是得分析源码,核心思路是“位操作” => 精准性~~(存在一定的 误差),如果想了解可以1.分析源码 2.数学方式来进行计算

4.bitmaps类型

​ 这个类型是通过使用Bit位来表示整数,因为计算机进行位运算,一般都是比较高效的。位图本质上,就还剩一个集合,属于set类型针对整数的特化版本,目的是 节省空间,但为啥此时又不使用我们各个说的Hyperloglog来节省空间了啊,这个类型不是更节省空间吗,但是这个Hyperlohlog类型,它是不存储元素内容,只是计数效果,hyperloglog存储元素的时候,提取特征的时候,是不可逆的!(信息量丢失了),就好比如我给你个猪肉,能把它做成火腿肠,但给你个火腿肠,能还原回猪肉吗?

5.bitfield类型

​ 这个类型是位域,C进阶的时候,我们学过一个叫做位段的东西,也叫做位域,比如一个:

struct Test{
	int a:8;
	int b:16;
	int c:8;此处的数字,就描述这个成员实际占几个bit位!位域的本质上是让我们精确的进行位操作的一种方法。
}

​ 上述 redis中的bitfield 和 C中 的位域,本质上是非常相似的!bitfield可以理解成一串二进制序列(字节数组),同时可以把这个字节数组中的某几个位,赋予特定的含义,并且可以进行 读取/修改/算数运算等相关操作。位域这个东西,相比于之前的string/hash 来说,目的仍然是节省空间

6.渐进式遍历

​ keys 一次性的把整个redis中所有的key都获取到,如 keys *,但是这个操作是比较危险的,可能会一下子得到太多的key,阻塞redis服务器!我们可以通过渐进式的遍历【不是一个命令,把所有的key都拿到,而是每执行一次命令,只获取到其中的一小部分,这样的话保证当前这一次操作不会太卡,想要得到所有的Key就需要多次遍历了,多次执行渐进式遍历命令】,就可以做到,既能够获得所有的Key,同时又不会卡死服务器。这里其实是一种“化整为零”的思想。

​ 渐进式遍历其实是一组命令,这一组命令的使用方法是一样的,其中代表命令 scan,语法的格式【精简版】为:

scan cursor count

​ 此处涉及到关键概念,光标,光标就指向了当前遍历的位置。比如这里我使用 scan 0 count 3这个指令,返回值第一项为:告诉你,下次继续遍历,光标要从哪里开始。真正遍历到的key的内容在第二段的返回结果当中。下次我们再进行渐进式遍历的时候,如果上一次的返回值为2,那么这次应该输入的就是scan 2 count 3,此时的返回值就应该是7了。注意cursor不能理解成是下标,它不是一个连续递增的整数!这条指令的完整版格式如下:

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

​ 这里的[MATCH pattern]指的是通配,和之前介绍给的keys命令是一样的,这时候我们的keys *这个命令就可以忘掉了。[COUNT count] 这个是限制一次遍历能够获取到多少元素,默认是10,但是注意!此处的count和mysql 的limit 不一样,此处的count只是给redis服务器一个“提示”/“建议”,写入的count和实际返回的key的个数不一定的完全相同的,但不会相差太多。而mysql的limit则是一个非常精确的指令。[TYPE type] redis 里的key都是string,但是value的类型不一样!5个通用的类型,5个特殊场景使用的类型。接下来我们便来进行一些演示:
QQ_1720800022457

​ 如果这里第一行返回的是0,那么说明遍历结束啦!!!注意count这里的数字,不是说每次遍历都得设置成一样,这里的渐进式遍历,在遍历过程中,不会在服务器这边存储任何的状态信息。此处的遍历是随时可以终止的,不会对服务器产生任何的副作用。因为redis服务器并不会保留任何的状态信息,所以redis的遍历是可以随时中断的。打个比方:我们去超市买东西,推个购物车,装很多东西,扫了一半了,你突然说,后面的你不要了,你想直接走,其他没有付款的直接推掉就可以了。

​ 我们的渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏,这点务必在实际开发中考虑。如果C++,这里可能就崩溃了,redis虽然不会给你崩溃,但是可能会出现遗漏重复。不仅仅是redis,遍历其他内容的时候,也是比较忌讳,一边遍历,一边修改的。C++ STL,遍历 + 修改/新增/删除 => 迭代器失效 ~,比如我们看看下面的这段代码:

std::vector<int> v = {1,2,3,4};
for(auto it=v.begin(); it != v.end(); ++it){
	v.erase(it);
}

​ 大部分编程语言中,++都是后置写的,C++更偏好前置++,在C++中更偏好前置 ++,因为在C++ 里前置 ++ 比后置 ++ 性能更高(少了一次临时对象的构造)。注意上面这样的代码,就会导致“迭代器失效”,当删除完成之后,it这个迭代器指向哪里,已经不知道了!此时循环体结束之后,再次 ++it,不一定就能指向下一个元素了。同理java中也是一样的。虽然scan比keys要好点,但是使用的时候也还是有很多注意事项,很多的出错的可能的,我们真的需要遍历redis所有的key嘛?

六、数据库管理命令

​ mysql中有一个重要的概念,database:一个mysql服务器上可以有很多个database,一个database上可以有很多个表,在mysql上我们可以随心所欲的创建 / 删除 数据库。但在我们之前学习redis的过程中,感觉好像就是上来就是 key value的,其实redis也是有database 这样的概念的,只不过不像mysql那样随意,redis中的database是现成的,咱们用户不能创建新的数据库,也不能删除已有的数据库。

​ 默认的redis 给咱们提供了 16个数据库,编号为 0 - 15,这16个数据库中的数据是隔离的(相互之前不会有影响),默认情况下使用的数据库就是0号,如果需要切换数据库可以使用下面的指令:

select dbIndex

​ 实际上使用redis很少会关注数据库,一般都是默认就用0号就可以了,这里我们需要注意下,flushdb这条指令是删除当前数据库中所有的key,而flushall这条指令则是删除所有数据库中的所有的key。以后在公司千万不敢乱用,尤其是在生产环境上。

​ 这时我们再介绍一个指令可以获取当前数据库所有key的个数。它就是DBSIZE指令:

DBSIZE

​ 事件复杂度为O(1)

QQ_1720852252132

七、redis客户端

1.认识redis客户端

​ 我们前面学习的主要是各种redis的基本操作/命令,都是在redis命令行客户端,手动执行的。但这种操作方式并不是我们日常开发中主要的形式。更多的时候,我们是使用redis的api,来实现定制化的redis客户端程序,进一步操作redis服务器。我们回顾下以前学习MySQL的时候,也会涉及到,关于使用程序来操作mysql服务器,C++我们使用MySQL的原生API,java中我们使用 JDBC & MyBatis

​ redis提供的命令行客户端 / 第三方的图形化客户端,他们本质上都是属于是“通用的客户端程序”,相比下我们在工作中更希望使用到的是“专用的” “定制化” 的客户端程序。

​ 是否前面学习的这些redis命令,就没有价值了呢?当然不是的!redis的命令是相当于使用代码来执行,所以他本身的命令我们也是需要会的。因为博主主修的是C++语言,所以后面主要通过使用C++客户端来操作redis。

​ 为什么我们能编写出一个自定义的redis客户端呢?咱有这本事的化能不能编写出一个qq自定义客户端/王者荣耀的自定义客户端/xxxx的自定义客户端呢?【答案是不能的!因为他们没有公开自己使用的自定义协议!!】,虽然腾讯没公开qq协议,但是我看到网上还是有一些开源项目去实现自定制的qq客户端的呀,(这里基本都是通过一些抓包/逆向手段,猜测是qq的应用层协议是啥样),这个过程全靠猜 => 猜多猜少,猜对猜错,取决于水平+运气,依靠的是逆向工程。

​ 网络通信过程中,会用到很多的“协议”,就比如数据链路层:以太网,网络层:IP协议,【这些协议都是固定好的,是在系统内核或者驱动程序中实现的,咱们程序员只能选择,不能修改】。传输层:TCP/UDP协议,还有应用层【虽然业界有很多成熟的应用层协议,HTTP等,但是此处更多的时候,都会“自定义”应用层协议,redis此处应用层协议,就是自定义的协议】(传输层还是基于TCP),客户端按照这里的应用层协议,发送请求,服务器按照这个协议构造响应,客户端再去解析这个响应。之所以上述通信能够完成,是因为开发客户端的人和开发服务器的人,都清楚的知道协议的细节。

​ 咱们作为第三方,想要开发一个redis客户端,也就需要知道redis的应用层协议,因为redis的官网上是开源了的,所以我们是可以知道的。(redis自定义的应用层协议就相当于一个暗号)

2.认识resp协议

​ resp协议就是一个场景的redis的应用层协议,这个协议他1.简单好实现,2.快速进行解析,3.肉眼可读。传输层这里是基于TCP,但是和TCP也没有强耦合。请求和响应之间的通信模型是一问一答的形式,客户端给服务器发一个请求,服务器返回一个响应。RESP讲这个主要是希望大家能了解redis底层的通信原理,即便不了解,也没关系,不影响后续代码的编写。

​ 接下来我们来看一下这个协议的具体规定的细节:

Redis uses RESP as a request-response protocol in the following way:
	Clients send commands【前面学习的redis命令】 to a Redis server as a RESP Array of Bulk Strings【客户端给服务器发送的是redis命令,(bulk string 数组的形式发送的)】
	The server replies with one of the RESP types according to the command implementation【不同的命令,返回结果不一样】

​ 我们再来看一下这个协议对于redis中不同的类型的处理:

In RESP,the first byte determines the data type:
	For Simple Strings,the first byte of the reply is "+"
	For Errors,the first byte of the reply is “-”
	For Integers,the first byte of the reply is "$"
	For Arrays,the first byte of the reply is "*"

​ 就比如,如果是Simple Strings,服务器就把“+OK\r\n”这个字符写入到tcp socket中即可。如果是Errors,服务器就写如“- Error message\r\n”,服务器把这个字符串也写道tcp socket中,因此,redis客户端和服务器要做的工作就是,1.按照上述格式,构造出字符串,往socket中写入。2.从socket中读取字符串,按照上述格式进行解析。

	Simple Strings are used to transmit non binary-safe strings with minimal overhead.
	In order to send binary-safe strings,use RESP Bulk Strings instead.

​ 总结就是:Simple String 只能用来传输文本,Bulk String 可以传输二进制数据。一会咱们写代码,真的需要按照上述的协议,解析/构造字符串嘛? 不用的,这套协议公开已久,已经有很多大佬,实现了这套协议的解析/构造,只要使用这些大佬们提供的库,就可以比较简单方便的来完成和redis服务器通信的操作了。

3.C++客户端实现helloworld程序

​ 接下来我们就去github上看一看这个大佬写好的库吧。

QQ_1720859184685

​ redis-plus-plus 依赖了hiredis(C语言的redis客户端库),关于hiredis的安装,可以通过源码安装,也可以直接使用包管理器来安装**【yum install hiredis-devel.x86_64】**
QQ_1720861561130

​ 我这里也是使用包管理器yum很轻松的就安装好了,注意安装时需要yum权限。

git clone https://github.com/redis/hiredis.git
cd hiredis
make
make install

​ 但是对于redis-plus-plus本体,我们只能编译安装了,如果是编译安装,使用ubantu 会比使用centos简单很多。

​ redis-plus-plus是使用cmake作为构建工具的,cmake相当于Makefile的升级版【C语言和汇编语言的区别】。Makefile本身功能比较简陋,比较原始,写起来也比较麻烦,实际开发中很少会手写makefile,我们一般都是通过程序去生成makefile,cmake就是一个生成makefile的工具,先不管Cmake的语法结构,重点看如何使用cmake编译程序。很多C++中的库,都是需要编译才能安装的,具体的操作步骤都是类似的!

​ 接下来我们来在我们的centos服务器上演示操作一下,首先是因为centos上的make版本太低,我们必须先安装一下cmake3,来升级一下我们的cmake版本:

yum install cmake3

​ 然后是去github官网上,通过git clone下载好我们的代码
QQ_1720861874302

​ 里面的结构是这样的:
QQ_1720861924210

​ 注意这里的CMakeLists.txt,这个就是我们cmake去自动化生成makfile的配置文件。我们先不管Cmake的语法结构,主要关注如何使用cmake编译程序。

mkdir build
cd build

​ 创建一个build目录是习惯做法,并非必须,目的是为了让编译生成的临时文件都放到build下,避免污染源代码目录

cmake3 ..

​ 这个操作是生成makefile,此处 … 指向的是刚才CMakeLists.txt文件所在的目录。
QQ_1720862323216

make

​ 进行编译~(makefile已经在上一步生成好了),注意这里编译还是有一点慢的
QQ_1720862354670

​ 我们编译成功之后得到的内容如下如:
QQ_1720862444569

​ 此处是我们编译生成的内容,主要就是.a和.so都是咱们当前编译的目录下的,后续写代码不一定能找到这些库,更推荐大家把这些库拷贝到系统目录中。

make install

​ 把刚才的库拷贝到系统目录下
QQ_1720862532702

​ 到这里我们也就安装好了,环境安装其实是一个大活(必须要花费很多时间),我们现在编写代码,看看能不能打印出一个hello world出来,使用redis-plus-plus 连接一下咱们的redis服务器,再使用ping 命令,检测一下连通性,可以通过VSCode远程连接咱们的linux服务器。代码分为下面三个部分:

  1. 包含redis-plus-plus的头文件

    ​ 这个头文件叫做redis++.h,使用这个库,入口头文件就是包含这个/usr/local/include/sw/redis++,和/usr/local/lib/libredis++.a,我们通过#include <>在系统目录中搜索头文件。如果万一刚才安装的日志找不到,也不知道装哪里了,可以使用find命令进行查找。所以这里对应的代码是:

    #include<sw/redis++/redis++.h>
    
  2. 创建一个redis对象

    ​ redis类就是刚才这个redis++.h 中提供的类,咱现在的环境中,咱们的redis客户端和redis服务器在同一个主机上,如果是不同主机,就写对应主机的ip即可。如果你确实是使用不同主机,要确保1.写的ip是否可达 2.还需要保证端口可访问(防火墙/安全组),所以对应的代码是:

    sw::redis::Redis redis("tcp://127.0.0.1:6379");
    

    我们要知道的是,URL并非是专属于HTTP的功能,RESP协议,他是基于TCP的应用层协议,最后的6379是redis服务器的默认端口,中间部分则是URL的唯一资源定位符。

  3. 进行ping操作

    ​ 代码如下:

    int main(){
    	//创建redis对象的时候,需要在构造函数中,指定redis服务器的地址和端口
    	sw::redis::Redis redis("tcp://127.0.0.1:6379");
    	//调用ping方法,让客户端给服务器发了一个PING,然后服务器就会返回一个PONG,就通过返回值获取到
    	string result = redis.ping();//前面学习过的各种redis命令,在这里就会对应到这个redis对象的各种方法。
    	std::cout<<result<<std::endl;
    	return 0;
    }
    
  4. 使用Makefile编译程序

    ​ 编译程序的时候,需要引入库文件,就比如:

    1. redis++ 自己的静态库【/usr/local/lib/libredis++.a】

    2. hiredis的静态库

      ​ 这里有一个巨坑,在博主的环境中,尝试使用find指令并没有找到hiredis的静态库【libhiredis.a】,所以只能退而求其次使用动态库进行连接,博主机器上动态库的位置在【/usr/lib64/libhiredis.so】

    3. 线程库【-pthread】

    关键就是要能够知道这几个库的路径是啥,要么就用软连接

​ 这里给出我们完整的makefile代码:

[ljh@VM-8-16-centos myredis_code]$ cat Makefile
hello:hello.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread

.PHONY:clean
clean:
	rm -rf hello

​ 最后我们来看看打印出helloworld的完整版代码,如下:

#include<iostream>
#include<vector>
#include<string>
#include<unordered_map>
#include<sw/redis++/redis++.h>
using std::cout;
using std::end;
using std::vector;
using std::string;
using std::unordered_map;
int main(){
  //创建Redis对象的时候,需要在构造函数中,指定redis服务器的地址和端口
  sw::redis::Redis redis("tcp://127.0.0.1:6379");
  //调用ping方法,让客户端给服务器发了一个PING,然后服务器就会返回一个PONG,就通过返回值获取到
  string result=redis.ping();
  std::cout<<result<<std::endl;
  return 0;
}

​ 编译,允许我们的程序,得到程序的运行结果,我们就大功告成啦!
QQ_1720938052378

4.C++客户端实现通用指令

​ redis的通用指令有以下这些指令:1.get/set 2.exists 3.del 4.keys 5.expire/ttl 6.type

1.get和set如何实现

​ 我们首先来看下代码:

// get 和 set
void test1(sw::redis::Redis& redis) {
    std::cout << "get 和 set 的使用" << std::endl;
    // 清空一下数据库, 避免之前残留的数据有干扰. 
    redis.flushall();
    // 使用 set 设置 key
    redis.set("key1", "111");
    redis.set("key2", "222");
    redis.set("key3", "333");
    // 使用 get 获取到 key 对应的 value
    auto value1 = redis.get("key1");
    // optional 可以隐式转成 bool 类型, 可以直接在 if 中判定. 如果是无效元素, 就是返回 false
    if (value1) {
        std::cout << "value1=" << value1.value() << std::endl;
    }
    auto value2 = redis.get("key2");
    if (value2) {
        std::cout << "value2=" << value2.value() << std::endl;
    }
    auto value3 = redis.get("key3");
    if (value3) {
        std::cout << "value3=" << value3.value() << std::endl;
    }
    auto value4 = redis.get("key4");
    if (value4) {
        std::cout << "value4=" << value4.value() << std::endl;
    }
}

​ 这里有很多需要注意的地方:首先我们来看下set这个函数的参数说明:

bool set(const sw::redis::StringView &key,const sw::redis::StrinView &val,
bool keepttl, sw::redis::UpdataType type=sw::redis::UpdataType::ALWAYS)

​ 这里的这些参数,和之前的set命令的选项有很大关联关系,这里用到的StringView这样的类型(是在sw::redis命名空间里的),我们C++的std::string 是可修改的,既可读,也能写,而StringView是只读的(不能修改,针对只读操作,做很大的优化工作,效率比std::string更高),在C++17的标准库中,也提供了一个std::string_view,(在java中String就是类似于StringView,java中要向你使用可修改的字符串,要使用StringBuilder或者StringBuffer),而StringView中的各种操作和string类似,只不过只是包含了一些只读方法。

​ 接下来是get接口的返回值,我们发现,get接口的返回值为:

sw::redis::OptionalString(作者自己封装的)

​ 此处的Optional可以表示“非法值”/“无效值”,nil => 无效值 【如果就直接使用std::string来表示,不方便来表现这个nil(无效值),如果使用std::string* 来表示,是可以用nullptr表示无效,但是返回指针又涉及到内存归谁管】,在boost中,很早就引入了option类型,在C++14版本中也正式纳入标准库了,那为什么这个库的作者还要自己去封装一个呢,因为我们知道,redis-plus-plus是支持C++11,14,17三个版本的,虽然说14,17已经支持了option类型,但是我们的C++11目前还不支持啊

​ 最后还有一点需要注意:
QQ_1720943097518

​ 此处的value1 - value4 都是 optional类型,但我们的sw::redis::OptionalString 是不支持 << 运算符的重载的,此处不需要给这个optional搞一个 << 的重载,因为这没什么必要,我们只需要把optional里面的元素给取出来就可以了(optional可以当作只包含一个元素的容器)

​ 还有一个问题,我们在使用get命令的时候,如果key不存在。就比如说我们代码中的key4,我们并没有对key4进行set,value4其实是optional的非法状态,对于非法状态的optional,进行取值操作,就会抛出异常。
QQ_1720943429528

​ 这个操作就是触发了操作系统的那个SIGABRT信号,怎么处理上述的情况呢?1.catch 2.访问value之前判定一下。但实际上,C++中不太会经常使用try catch,原因主要有以下两点:

  1. try catch 会存在额外的运行时开销,对于性能追求极致的C++ 来说不太合适
  2. try catch相比于其他语言来说,有点弱。

最后我们修改以下我们的makefile,见下面的代码:

.PHONY:all
all:hello generic
hello:hello.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
generic:generic.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
.PHONY:clean
clean:
	rm -rf hello generic

​ 我们的运行结果如下图:

QQ_1720944282937

​ 为了检测是否真的连接上了我们的redis,此时我们再打开redis默认提供的客户端,输入keys *进行查看,发现确实设置成功 。
QQ_1720944407513

2.expire和del指令的实现

​ 接下来我们来看看expire和del指令如何实现,代码如下:

void test2(sw::redis::Redis& redis) {
    std::cout << "exists" << std::endl;
    redis.flushall();
    redis.set("key", "111");
    redis.set("key3", "111");
    auto ret = redis.exists("key");//存在的
    std::cout << ret << std::endl;
    ret = redis.exists("key2");//不存在的
    std::cout << ret << std::endl;
    ret = redis.exists({"key", "key2", "key3"});//构造函数的重载,初始化列表版本
    std::cout << ret << std::endl;
}
void test3(sw::redis::Redis& redis) {
    std::cout << "del" << std::endl;
    // 清除库非常必要的! 
    redis.flushall();

    redis.set("key", "111");
    redis.set("key2", "111");

    // redis.del("key");

    auto ret = redis.del({"key", "key2", "key3"});
    std::cout << ret << std::endl;

    ret = redis.exists({"key", "key2"});
    std::cout << ret << std::endl;
}

​ 上述的代码运行结果如下:
QQ_1721024536823

​ 接下来是del指令的实现,这部分代码很简单,看看就能懂了。

void test3(sw::redis::Redis& redis) {
    std::cout << "del" << std::endl;
    // 清除库非常必要的! 
    redis.flushall();
    redis.set("key", "111");
    redis.set("key2", "111");
    // redis.del("key");
    auto ret = redis.del({"key", "key2", "key3"});
    std::cout << ret << std::endl;
    ret = redis.exists({"key", "key2"});
    std::cout << ret << std::endl;
}

​ 运行结果为:
QQ_1721025444485

​ 这里有一个问题:进行单元测试的时候,清除数据的操作是放在开始,还是结束呢?我认为放在开始更好点,如果放在结束,一旦执行的程序中间出现了异常,就可能会导致清理部分的代码没有执行到!就比如上面的代码,第6行代码如果这里出现异常,该函数就立即结束了。那么如果清理函数写在了函数结尾,那么这次清理将会执行不到,那么下次执行就会受到数据残留的影响。

​ 清理操作放在开始,当执行完一个操作的时候,此时redis里仍然是有数据的,也方便程序员手工来检查数据的内容【方便单步调试】。上述代码的执行结果见下图。

3.通用指令keys

​ 这里的keys命令也没什么好说的,直接上代码:

void test4(sw::redis::Redis& redis) {
    std::cout << "keys" << std::endl;
    redis.flushall();
    redis.set("key", "111");
    redis.set("key2", "222");
    redis.set("key3", "333");
    redis.set("key4", "444");
    redis.set("key5", "555");
    redis.set("key6", "666");
    // keys 的第二个参数, 是一个 "插入迭代器". 咱们需要先准备好一个保存结果的容器. 
    // 接下来再创建一个插入迭代器指向容器的位置. 就可以把 keys 获取到的结果依次通过刚才的插入迭代器插入到容器的指定位置中了. 
    vector<string> result;
    auto it = std::back_inserter(result);//这里通过使用迭代器来进行插入
    redis.keys("*", it);
    printContainer(result);
}

​ 为了方便打印各种容器之间的数,我们单独再写一个工具类,工具类中写一个通用的打印函数【通过使用函数模板】,这个打印函数可以打印所有类型的容器中的数据。

template<typename T>
inline void printContainer(const T& container) {
    for (const auto& elem : container) {
        std::cout << elem << std::endl;
    }
}

​ 运行结果如下图:
QQ_1721027244133
​ 这里我们再复习以下什么是迭代器,stl中五种迭代器的类型,1.输入迭代器 2.输出迭代器 3.前向迭代器 4.双向迭代器 5.随机访问迭代器 ,为什么没有插入迭代器呢?插入迭代器,本质上也是一种“输出迭代器”,通常一个迭代器主要是表示一个“位置”,插入迭代器则是“位置”+“动作”。比如 font_insert_iterator 是从区间的开头,往前面插入,back_insert_iterator 是从区间的末尾,往后面插入,insert_iterator是从区间的任意位置,往该位置上插入。我们刚刚代码中的**std::back_inserter(result)**就是用来构造back_insert_iterator迭代器的。我们一般都不会直接构造出这几个对象(构造函数写起来比较麻烦),一般会使用一些辅助的 函数来进行构造。

​ 对于插入迭代器来说,*,++都是啥都不干,主要的核心操作就是 =(把另一个迭代器,赋值给这个插入迭代器),就比如it 是插入迭代器,it2是普通迭代器,it=it2,相当于是获取到it2指向的元素,然后把元素按照it当前插入迭代器的位置和动作来进行指向插入操作,比如it是一个back_insert_iterator,就是把it2指向 的元素插入到it指向的容器末尾(相当于调用了一次push_back)

​ 我们代码中的redis.keys(“*”,it);这里直接使用容器作为参数,keys内部直接操作容器,进行插入不就行了嘛,为啥要通过迭代器绕一个大圈子呢?主要还是为了“解耦合“。与此相比,java中虽然也有迭代器这样的概念,但是java的迭代器,功能和作用相比于C++就小了不少。

4.通用指令expire和ttl指令

​ linux的sleep(s),windows的Sleep(ms),系统函数是和系统相关的 ,同样的功能,在不同的系统上可能是完全不同的函数,更好的选择,是使用标注库的函数,C++也提供了thread库中的sleep_for函数。我们在使用的时候只需要包含头文件#include<thread>,然后调用sleep_for(3s),这里的3s是一个字面值常量。接下来我们看看代码:

void test5(sw::redis::Redis& redis) {
    using namespace std::chrono_literals;
    std::cout << "expire and ttl" << std::endl;
    redis.flushall();
    redis.set("key", "111");
    // 10s => std::chrono::seconds(10)
    redis.expire("key", 10s);
    std::this_thread::sleep_for(3s);
    auto time = redis.ttl("key");
    std::cout << time << std::endl;
}

​ 运行结果如下:
QQ_1721028646754

5.通用指令type

​ 这个指令比较简单,直接上代码:

void test6(sw::redis::Redis& redis) {
    std::cout << "type" << std::endl;
    redis.flushall();
    redis.set("key", "111");
    string result = redis.type("key");
    std::cout << "key: " << result << std::endl;
    redis.lpush("key2", "111");
    result = redis.type("key2");
    std::cout << "key2: " << result << std::endl;
    redis.hset("key3", "aaa", "111");
    result = redis.type("key3");
    std::cout << "key3: " << result << std::endl;
    redis.sadd("key4", "aaa");
    result = redis.type("key4");
    std::cout << "key4: " << result << std::endl;
    redis.zadd("key5", "吕布", 99);
    result = redis.type("key5");
    std::cout << "key5: " << result << std::endl;
}

​ 运行结果如下图:
QQ_1721029171406

6.string的相关指令

1.string的get和set

​ 这里都和我们直接讲解的命令行式的指令差别不大,所以这里直接上代码:

[ljh@VM-8-16-centos myredis_code]$ cat string.cc
#include<iostream>
#include<vector>
#include<string>
#include<unordered_map>
#include<chrono>
#include<thread>
#include<sw/redis++/redis++.h>
#include"util.hpp"
using std::cout;
using std::endl;
using std::vector;
using std::string;
using std::unordered_map;
using sw::redis::Redis;
using namespace std::chrono_literals;
void test1(Redis& redis){
  std::cout<<"get和set"<<std::endl;
  redis.flushall();
  redis.set("key","111");
  auto value=redis.get("key");
  if(value){
    std::cout<<"value: "<<value.value()<<std::endl;
  }
  redis.set("key","222");
  value=redis.get("key");
  if(value){
    std::cout<<"value: "<<value.value()<<std::endl;
  }
}
void test2(Redis& redis){
  std::cout<<"set带有超时时间"<<std::endl;
  redis.flushall();
  redis.set("key","111",10s);
  std::this_thread::sleep_for(3s);
  long long time =redis.ttl("key");
  std::cout<<"time: "<<time<<std::endl;
}
void test3(Redis& redis){
  std::cout<<"set NX和XX"<<std::endl;
  redis.flushall();
  redis.set("key","111");
  //set的重载版本中,没有单独提供NX和XX的版本,必须搭配
  //过期时间的版本来使用
  redis.set("key","222",0s,sw::redis::UpdateType::EXIST);
  auto value=redis.get("key");
  if(value){
    std::cout<<"value: "<<value.value()<<std::endl;
  }else{
    std::cout<<"key 不存在!"<<std::endl;
  }
}
int main(){
  Redis redis("tcp://127.0.0.1:6379");
  test1(redis);
  return 0;
}

​ test1函数的运行结果:
QQ_1721035699728

​ test2函数的运行结果:
QQ_1721035753344

​ test3函数的运行结果:
QQ_1721035800561

2.string的mget和mset函数

​ 也比较简单的两个指令,直接上代码,原理是通过构造函数的初始化列表:

void test4(Redis& redis) {
    std::cout << "mset" << std::endl;
    redis.flushall();
    // 第一种写法, 使用初始化列表描述多个键值对
    // redis.mset({ std::make_pair("key1", "111"), std::make_pair("key2", "222"), std::make_pair("key3", "333") });
    // 第二种写法, 可以把多个键值对提前组织到容器中. 以迭代器的形式告诉 mset
    vector<std::pair<string, string>> keys = {
        {"key1", "111"},
        {"key2", "222"},
        {"key3", "333"}
    };
    redis.mset(keys.begin(), keys.end());
    auto value = redis.get("key1");
    if (value) {
        std::cout << "value: " << value.value() << std::endl;
    }
    value = redis.get("key2");
    if (value) {
        std::cout << "value: " << value.value() << std::endl;
    }
    value = redis.get("key3");
    if (value) {
        std::cout << "value: " << value.value() << std::endl;
    }
}

​ 运行结果如下:
QQ_1721057482991

​ 再接下来据说mget函数,代码如下:

void test5(Redis& redis) {
    std::cout << "mget" << std::endl;
    redis.flushall();
    vector<std::pair<string, string>> keys = {
        {"key1", "111"},
        {"key2", "222"},
        {"key3", "333"}
    };
    redis.mset(keys.begin(), keys.end());
    vector<sw::redis::OptionalString> result;
    auto it = std::back_inserter(result);
    redis.mget({"key1", "key2", "key3", "key4"}, it);
    printContainerOptional(result);
}
//打印的代码
template<typename T>
inline void printContainerOptional(const T& container) {
    for (const auto& elem : container) {
        // 此处预期 elem 是一个 optional 类型的元素, 打印之前, 先判定一下, 看是否有效
        if (elem) {
            std::cout << elem.value() << std::endl;
        } else {
            std::cout << "元素无效" << std::endl;
        }
    }
}

​ 运行结果如下图:
QQ_1721058224452

3.使用string的getrange和setrange

​ 和之前一样,直接上代码:

void test6(Redis& redis) {
    std::cout << "getrange 和 setrange" << std::endl;
    redis.flushall();
    redis.set("key", "abcdefghijk");
    string result = redis.getrange("key", 2, 5);
    std::cout << "result: " << result << std::endl;
    redis.setrange("key", 2, "xyz");
    auto value = redis.get("key");
    std::cout << "value: " << value.value() << std::endl;
}

​ 运行结果如下:
QQ_1721058910621

4.使用string的incr和decr

​ incr和decr得到的是long long类型【使用这个更多一些】,get得到的是OptionalString类型=>需要手动转成数字【C++种把字符串转成数字,有很多种方式】

void test7(Redis& redis) {
    std::cout << "incr 和 decr" << std::endl;
    redis.flushall();
    redis.set("key", "100");
    long long result = redis.incr("key");
    std::cout << "result: " << result << std::endl;
    auto value = redis.get("key");
    std::cout << "value: " << value.value() << std::endl;
    result = redis.decr("key");
    std::cout << "result: " << result << std::endl;
    value = redis.get("key");
    std::cout << "value: " << value.value() << std::endl;
}

​ 运行结果如下:
QQ_1721059339608

​ 至此,string类型通过C++客户端操作就都讲解完了。

7.list的相关指令

1.lpush和lrange指令

​ 我们直接上代码,并在代码中做了相应的注解:

void test1(Redis& redis) {
    std::cout << "lpush 和 lrange" << std::endl;
    redis.flushall();
    // 插入单个元素
    redis.lpush("key", "111");
    // 插入一组元素, 基于初始化列表
    redis.lpush("key", {"222", "333", "444"});
    // 插入一组元素, 基于迭代器
    vector<string> values = {"555", "666", "777"};
    redis.lpush("key", values.begin(), values.end());
    // lrange 获取到列表中的元素
    vector<string> results;
    auto it = std::back_inserter(results);
    redis.lrange("key", 0, -1, it);
    printContainer(results);
}

​ 运行结果如下图:
QQ_1721111513565

​ 要注意的是,lpush是头插,每次新的元素都是插入到list开头,越后来的元素,就越是排在前面!

2.rpush指令

​ 直接看代码:

void test2(Redis& redis) {
    std::cout << "rpush" << std::endl;
    redis.flushall();
    // 插入单个元素
    redis.rpush("key", "111");
    // 插入多个元素, 基于初始化列表
    redis.rpush("key", {"222", "333", "444"});
    // 插入多个元素, 基于容器
    vector<string> values = {"555", "666", "777"};
    redis.rpush("key", values.begin(), values.end());
    // 使用 lrange 获取元素
    vector<string> results;
    auto it = std::back_inserter(results);
    redis.lrange("key", 0, -1, it);
    printContainer(results);
}

​ 运行结果如下图:
QQ_1721111949474

3.lpop和rpop指令

​ 看代码:

void test3(Redis& redis) {
    std::cout << "lpop 和 rpop" << std::endl;
    redis.flushall();
    // 构造一个 list
    redis.rpush("key", {"1", "2", "3", "4"});
    auto result = redis.lpop("key");
    if (result) {
        std::cout << "lpop: " << result.value() << std::endl;
    }
    result = redis.rpop("key");
    if (result) {
        std::cout << "rpop: " << result.value() << std::endl;
    }
}

​ 运行结果如下:
QQ_1721112379817

4.blpop指令

​ 代码如下:

void test4(Redis& redis) {
    using namespace std::chrono_literals;
    std::cout << "blpop" << std::endl;
    redis.flushall();
    auto result = redis.blpop({"key", "key2", "key3"}, 10s);
    if (result) {
        std::cout << "key:" << result->first << std::endl;
        std::cout << "elem:" << result->second << std::endl;
    } else {
        std::cout << "result 无效!" << std::endl;
    }
}

​ 这里的blpop函数的返回值类型为:OptionalStringPair,这个返回类型,既是optional,里面还包裹了一个pair,pair里面是string,blpop要返回的内容是两个部分

  1. 这个元素从属于哪个list,blpop可以同时监听多个list
  2. 当前被删除的元素

​ blpop还可以设定超时时间,如果在指定空间内,还没有其他客户端往指定的list中插入元素,此时blpop就直接返回了,此时返回的就是一个无效值了。

​ 对于std::optional类型来说,可以直接使用 -> 来访问optional内部包含的元素的成员。

​ 代码的运行结果如下:此时我们还需要再开启一个redis终端,在这个程序等待的过程中,使用另一个终端向redis中插入元素,得到返回结果。
QQ_1721113296104

5.llen指令

​ 代码如下:

void test5(Redis& redis) {
    std::cout << "llen" << std::endl;
    redis.flushall();
    redis.lpush("key", {"111", "222", "333", "444"});
    long long len = redis.llen("key");
    std::cout << "len: " << len << std::endl;
}

​ 运行结果如下:
QQ_1721114028584

​ 看了这么多函数接口了,我们可以发现,对于redis-plus-plus这个库来说,接口风格的设计,是非常统一的。
​ 当一个函数,参数需要传递多个值的时候,往往都是支持初始化列表或者一对迭代器的方式,来进行实现的。当一个函数,返回值需要表示多个数据的时候,也往往会借助插入迭代器,来实现往一个容器中添加元素的效果。当某些场景涉及到无效值的时候,往往会搭配std::optional来进行使用。很多C++的代码,都是按照上述的方式来设计的。

8.set的相关指令

1.sadd和smembers指令

​ 代码如下:

void test1(Redis& redis) {
    std::cout << "sadd 和 smembers" << std::endl;
    redis.flushall();
    // 一次添加一个元素
    redis.sadd("key", "111");
    // 一次添加多个元素(使用初始化列表)
    redis.sadd("key", {"222", "333", "444"});
    // 一次添加多个元素(使用迭代器)
    set<string> elems = {"555", "666", "777"};
    redis.sadd("key", elems.begin(), elems.end());
    // 获取到上述元素
    // 此处用来保存 smembers 的结果, 使用 set 可能更合适. 
    set<string> result;
    // auto it = std::back_inserter(result);
    // 由于此处 set 里的元素顺序是固定的. 指定一个 result.end() 或者 result.begin() 或者其他位置的迭代器, 都无所谓~~
    auto it = std::inserter(result, result.end());
    redis.smembers("key", it);
    printContainer(result);
}

​ 注意这里如果是想用set当容器的话,要使用inserter来构造迭代器,因为set和前面的几个不一样,他里面没有push_back函数!
QQ_1721118242530

​ 那我们能不能使用auto it = std::back_inserter(result)呢,这个也是不行的,因为back_insert_iterator是把末尾位置和push_back这俩操作绑定到一起了。但是此处set又没有push_back。但是我们的set有insert方法,和insert对应的就是 insert_iterator!QQ_1721117731350

2.sismember,spop,scard等指令

​ 我们直接看sismember的相关实现代码:

void test2(Redis& redis) {
    std::cout << "sismember" << std::endl;
    redis.flushall();
    redis.sadd("key", {"111", "222", "333", "444"});
    bool result = redis.sismember("key", "555");
    std::cout << "result: " << result << std::endl;
}

​ 代码的运行结果如下:
QQ_1721137047999

​ 我们再来看scard的相关实现代码:

void test3(Redis& redis) {
    std::cout << "scard" << std::endl;
    redis.flushall();
    redis.sadd("key", {"111", "222", "333"});
    long long result = redis.scard("key");
    std::cout << "result: " << result << std::endl;
}

​ 代码的运行结果如下,这个结果说明,key所对应的set中,含有三个元素
QQ_1721137425025

​ 接下来是spop的相关实现代码:

void test4(Redis& redis) {
    std::cout << "spop" << std::endl;
    redis.flushall();
    redis.sadd("key", {"111", "222", "333", "444"});
    auto result = redis.spop("key");
    if (result) {
        std::cout << "result: " << result.value() << std::endl;
    } else {
        std::cout << "result 无效!" << std::endl;
    }
}

​ 代码的运行结果如下:
QQ_1721137681914

3.sinter,sinterstore指令

​ sinter的代码如下:

void test5(Redis& redis) {
    std::cout << "sinter" << std::endl;
    redis.flushall();
    redis.sadd("key1", {"111", "222", "333"});
    redis.sadd("key2", {"111", "222", "444"});
    set<string> result;
    auto it = std::inserter(result, result.end());
    redis.sinter({"key1", "key2"}, it);
    printContainer(result);
}

​ 代码的运行结果如下:
QQ_1721138016600

​ 还有我们的sinterstore指令,代码如下:

void test6(Redis& redis) {
    std::cout << "sinterstore" << std::endl;
    redis.flushall();
    redis.sadd("key1", {"111", "222", "333"});
    redis.sadd("key2", {"111", "222", "444"});
    long long len = redis.sinterstore("key3", {"key1", "key2"});
    std::cout << "len: " << len << std::endl;
    set<string> result;
    auto it = std::inserter(result, result.end());
    redis.smembers("key3", it);
    printContainer(result);
}

​ 代码的运行结果如下:
QQ_1721138355288

9.hash的相关操作

1.hset和hget

​ 代码如下:

void test1(Redis& redis) {
    std::cout << "hset 和 hget" << std::endl;
    redis.flushall();
    redis.hset("key", "f1", "111");
    redis.hset("key", std::make_pair("f2", "222"));
    // hset 能够一次性插入多个 field-value 对!!
    redis.hset("key", {
        std::make_pair("f3", "333"),
        std::make_pair("f4", "444")
    });
    vector<std::pair<string, string>> fields = {
        std::make_pair("f5", "555"),
        std::make_pair("f6", "666")
    };
    redis.hset("key", fields.begin(), fields.end());
    auto result = redis.hget("key", "f3");
    if (result) {
        std::cout << "result: " << result.value() << std::endl;
    } else {
        std::cout << "result 无效!" << std::endl;
    }
}

​ 代码的运行结果如下:
QQ_1721139503625

2.hexists,hdel,hlen指令

​ hexist指令的C++客户端代码实现如下:

void test2(Redis& redis) {
    std::cout << "hexits" << std::endl;
    redis.flushall();
    redis.hset("key", "f1", "111");
    redis.hset("key", "f2", "222");
    redis.hset("key", "f3", "333");
    bool result = redis.hexists("key", "f4");
    std::cout << "result: " << result << std::endl;
}

​ hdel和hlen指令的C++客户端代码实现如下:

void test3(Redis& redis) {
    std::cout << "hdel" << std::endl;
    redis.flushall();
    redis.hset("key", "f1", "111");
    redis.hset("key", "f2", "222");
    redis.hset("key", "f3", "333");
    long long result = redis.hdel("key", "f1");
    std::cout << "result: " << result << std::endl;
    result = redis.hdel("key", {"f2", "f3"});
    std::cout << "result: " << result << std::endl;
    long long len = redis.hlen("key");
    std::cout << "len: " << len << std::endl;
}
3.hkeys,hvals,hmset,hmget等指令

​ 直接上代码,感兴趣的读者可以自行实现

void test4(Redis& redis) {
    std::cout << "hkeys 和 hvals" << std::endl;
    redis.flushall();
    redis.hset("key", "f1", "111");
    redis.hset("key", "f2", "222");
    redis.hset("key", "f3", "333");
    vector<string> fields;
    auto itFields = std::back_inserter(fields);
    redis.hkeys("key", itFields);
    printContainer(fields);
    vector<string> values;
    auto itValues = std::back_inserter(values);
    redis.hvals("key", itValues);
    printContainer(values);
}
void test5(Redis& redis) {
    std::cout << "hmget 和 hmset" << std::endl;
    redis.flushall();
    redis.hmset("key", {
        std::make_pair("f1", "111"),
        std::make_pair("f2", "222"),
        std::make_pair("f3", "333")
    });
    vector<std::pair<string, string>> pairs = {
        std::make_pair("f4", "444"),
        std::make_pair("f5", "555"),
        std::make_pair("f6", "666")
    };
    redis.hmset("key", pairs.begin(), pairs.end());
    vector<string> values;
    auto it = std::back_inserter(values);
    redis.hmget("key", {"f1", "f2", "f3"}, it);
    printContainer(values);
}

10.zset的相关指令

1.zadd,zrange指令

​ 代码如下:

void test1(Redis& redis) {
    std::cout << "zadd 和 zrange" << std::endl;
    redis.flushall();
    redis.zadd("key", "吕布", 99);
    redis.zadd("key", {
        std::make_pair("赵云", 98),
        std::make_pair("典韦", 97)
    });
    vector<std::pair<string, double>> members = {
        std::make_pair("关羽", 95),
        std::make_pair("张飞", 93)
    };
    redis.zadd("key", members.begin(), members.end());
    // zrange 支持两种主要的风格:
    // 1. 只查询 member, 不带 score
    // 2. 查询 member 同时带 score
    // 关键就是看插入迭代器指向的容器的类型. 
    // 指向的容器只是包含一个 string, 就是只查询 member
    // 指向的容器包含的是一个 pair, 里面有 string 和 double, 就是查询 member 同时带有 score
    vector<string> memberResults;
    auto it = std::back_inserter(memberResults);
    redis.zrange("key", 0, -1, it);
    printContainer(memberResults);
    vector<std::pair<string, double>> membersWithScore;
    auto it2 = std::back_inserter(membersWithScore);
    redis.zrange("key", 0, -1, it2);
    printContainerPair(membersWithScore);
}
2.zcard,zrem,zscore,zrank指令

​ 代码如下:

void test2(Redis& redis) {
    std::cout << "zcard" << std::endl;
    redis.flushall();
    redis.zadd("key", "zhangsan", 90);
    redis.zadd("key", "lisi", 91);
    redis.zadd("key", "wangwu", 92);
    redis.zadd("key", "zhaoliu", 93);
    long long result = redis.zcard("key");
    std::cout << "result: " << result << std::endl;
}
void test3(Redis& redis) {
    std::cout << "zrem" << std::endl;
    redis.flushall();
    redis.zadd("key", "zhangsan", 90);
    redis.zadd("key", "lisi", 91);
    redis.zadd("key", "wangwu", 92);
    redis.zadd("key", "zhaoliu", 93);
    redis.zrem("key", "zhangsan");
    long long result = redis.zcard("key");
    std::cout << "result: " << result << std::endl;
}
void test4(Redis& redis) {
    std::cout << "zscore" << std::endl;
    redis.flushall();
    redis.zadd("key", "zhangsan", 90);
    redis.zadd("key", "lisi", 91);
    redis.zadd("key", "wangwu", 92);
    redis.zadd("key", "zhaoliu", 93);
    auto score = redis.zscore("key", "zhangsan");
    if (score) {
        std::cout << "score: " << score.value() << std::endl;
    } else {
        std::cout << "score 无效" << std::endl;
    }
}
void test5(Redis& redis) {
    std::cout << "zrank" << std::endl;
    redis.flushall();
    redis.zadd("key", "zhangsan", 90);
    redis.zadd("key", "lisi", 91);
    redis.zadd("key", "wangwu", 92);
    redis.zadd("key", "zhaoliu", 93);
    auto rank = redis.zrank("key", "zhaoliu");
    if (rank) {
        std::cout << "rank: " << rank.value() << std::endl;
    } else {
        std::cout << "rank 无效" << std::endl;
    }
}

​ 最后,附赠一份上面代码对应的完整的makefile文件:

.PHONY:all
all:hello generic string list set hash zset
zset:zset.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
hash:hash.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
set:set.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
string:string.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
hello:hello.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
generic:generic.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
list:list.cc
	g++ -std=c++17 -o $@ $^ /usr/local/lib64/libredis++.a -lhiredis -pthread
.PHONY:clean
clean:
	rm -rf hello generic string list set hash zset

​ 以上便是redis基础使用的全部内容了,祝大家学习顺利!

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Li小李同学Li

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值