Redis 学习笔记

目录

一、认识 Redis

1、单机架构

2、分布式是什么

 2.1、负载均衡器

 2.2、数据库读写分离

2.3、二八原则!

2.4、数据库分库分表

 3、微服务

 3.1、为什么引入微服务?

 3.2、微服务的本质(通俗易懂)

 3.3、微服务的缺点(代价)

3.4、微服务的优势

4、概念补充

4.1、应⽤(Application)/ 系统(System)

4.2、模块(Module)/ 组件(Component)

4.3、分布式(Distributed)

4.4、集群(Cluster)

4.5、主(Master)/ 从(Slave)

4.6、中间件(Middleware)

4.7、衡量服务器的评价指标: 可用性、响应时长、吞吐 vs 并发

4.8、可⽤性(Availability)

4.9、响应时⻓(Response Time RT)

4.10、吞吐(Throughput)vs 并发(Concurrent)

4.11、未来在工作中会涉及到的几个环境

5、分布式系统小结

二、Redis 特性

4、持久化

5、支持集群

6、高可用

7、快!!!

三、Redis应用场景

1、把Redis当作数据库

2、缓存&身份信息

3、消息队列(服务器)

四、在Contos下安装Redis5

1、下载Redis5

2、创建符号链接(快捷方式)

3、修改配置文件

 4、设置工作目录和日志目录

 5、启动Redis

6、停止 Redis

 7、Redis的相关文件地址

8、进入Redis

五、Redis客户端(多种多样)

六、Redis 命令

1、get、set 

2、mget、mset

2、keys

3、exists

4、del

5、expire

6、ttl 

7、type 

8、 object  encoding

9、flushall(大写:FLUSHALL)

10、redis 文档语法说明

八、Redis的数据类型

1、redis 的数据类型

2、内部编码: 

九、redis 虽然是单线程模型,为啥效率这么高?速度这么快? 以及什么是 IO 复用

1、redis 虽然是单线程模型,为啥效率这么高?速度这么快?

2、IO 多路复用

 十、Redis 中的 String  详细解释  以及操作 字符串 的命令

set  详细解释

String 的应用场景

Redis + MySQL 组成的缓存存储结构

记录视频播放次数

Redis 集中管理 Session

短信验证码

十一、 hash 类型

哈希的应用

映射关系表表示用户信息

十二、list

!!! 此博客每两天必更新,一直到结束,预计更新至少三万字!!!


一、认识 Redis

Redis 是一个在内存中存储数据的中间件,是一个 客户端-服务器 结构的程序,用于作为内存数据库/数据缓存/消息队列来使用,在分布式系统中能够大展拳脚

Redis官方文档:https://redis.io/docs/icon-default.png?t=N7T8https://redis.io/docs/

1、单机架构

单机架构:只有一台服务器,这个服务器负责所有的工作 

千万不要瞧不上这个东西,绝大部分的公司的产品,都是这种单机架构!!!

如果业务进一步增长,用户量和数据量都水涨船高,一台主机难以应付的时候,就需要引入更多的主机,引入更多的资源

2、分布式是什么

引入多台主机,各自负责一部分模块,协同工作达成最终的结果(一般都是一台主机的资源不足以完成所需的任务时会使用分布式结构)

如下图: 应用服务器占用一台主机,存储服务器又占用一台主机

 当然,也可以引入更多的应用服务器节点

 所谓的 分布式 系统,就是想办法引入更多的硬件资源!

 2.1、负载均衡器

如上图的负载均衡器

负载均衡器用来处理到达应用服务器的请求,将其分配给合适的应用服务器

(负载均衡器对于请求量的承担能力,是远远超过应用服务器的)

相当于:

负载均衡器,是领导,分配工作

应用服务器,是组员,执行任务

 若出现请求量到负载均衡器也扛不住?

引入更多的负载均衡器

 2.2、数据库读写分离

 本质上是将数据库服务器部署到两台主机上,一个主要负责”读“操作,一个负责”写“操作

注意  数据同步

2.3、二八原则!

20% 的热点数据,能满足 80%的访问需求

这时候就可以把 Redis 和 MySQL 结合起来使用

为什么不单纯用 MySQL?

一个原因:慢!

Redis也可以作为数据库使用,快!劣势是存储空间是有限的!

缺点:系统的复杂程度提高,而且,如果数据发生修改,还涉及到 Redis 和 MySQL之间的数据同步问题

2.4、数据库分库分表

 一台主机存不下,就需要多台主机来存储

引入多个数据库服务器,每个数据库服务器存储一个或者一部分数据库

(如果某个表特别大,大到一台主机存不下,也可以针对表进行拆分)

 3、微服务

把复杂的服务器,拆分成更多的,功能更单一,但是更小的服务器(微服务)

 3.1、为什么引入微服务?

        之前应用服务器,一个服务器程序里面做了很多的业务,这就可能会导致这一个服务器的代码变的越来越复杂

        为了更方便于代码的维护,就可以把这样的一个复杂的服务器,拆分成更多的,功能更单一,但是更小的服务器(微服务)

 3.2、微服务的本质(通俗易懂)

微服务的本质是在解决 “人” 的问题

  •         当应用服务器复杂了,势必就需要更多的人来维护了,当人多了,就需要配套的管理,把这些人组织好
  •         划分组织结构,分成多个组,每个组分别配备领导进行管理
  •         分成多个组,就需要进行分工
  •         比如让某个人专精于某个模块,这样这个人就能将这个模块开发的非常优秀
 3.3、微服务的缺点(代价)

1、系统的性能下降

        拆出来更多的服务,多个功能之间要更依赖 网络通信

        网络通信的速度很可能是比硬盘还慢的!

        系统复杂程度提高,可用性受到影响 

2、系统复杂程度提高,可用性受到影响

        服务器更多了,出现问题的概率就更大了

        这就需要一系列的手段,来保证系统的可用性

        (比如更丰富的监控报警,以及配套的运维人员)

3.4、微服务的优势
  1. 解决了人的问题
  2. 使用微服务,可以更方便于功能的复用
  3. 可以给不同的服务进行不同的部署(比如硬件资源的配置)

4、概念补充

4.1、应⽤(Application)/ 系统(System)

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

4.2、模块(Module)/ 组件(Component)

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

4.3、分布式(Distributed)

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

4.4、集群(Cluster)

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

比如 一个主机上部署了多个服务器,服务器之间通过网络连接

4.5、主(Master)/ 从(Slave)

分布式系统中一种比较典型的结构

多个服务器节点,其中一个是主,另外的是从,从节点的数据要从主节点这里同步过来

4.6、中间件(Middleware)

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

比如: 数据库、缓存、消息队列......

4.7、衡量服务器的评价指标: 可用性、响应时长、吞吐 vs 并发

4.8、可⽤性(Availability)

系统整体可用的时间/总的时间 => 可用性

可用的时间表示系统能正常运行,完成所需业务的时间

比如一年有365天,它有360天是正常可用的,那么可用性就是 360/365

可用性是一个系统的第一要务

4.9、响应时⻓(Response Time RT)

衡量服务器的性能 —— 越小越好(一般来说是这样)

4.10、吞吐(Throughput)vs 并发(Concurrent)

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

吞吐 —— 单位时间内,系统可以成功处理请求的数量

并发 —— 系统同一时刻支持的请求最高量

4.11、未来在工作中会涉及到的几个环境

在下面这个博客里,直接点击就行:

https://blog.csdn.net/dream_ready/article/details/132783194


5、分布式系统小结

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

5.2、数据库和应用分离

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

5.3、引入负载均衡,应用服务器 => 集群

通过负载均衡器,把请求比较均匀的分发给集群中的每个应用服务器

当集群中的某个主机挂了,其他的主机仍然可以承担服务,提高了整个系统的可用性

5.4、引入读写分离,数据库主从结构

一个数据库节点作为主节点,其他N个数据库节点作为从节点

主节点负责写数据,从节点负责读数据

主节点需要把修改过的数据同步给从节点

5.5、引入缓存,冷热数据分离

进一步的提升了服务器针对请求的处理能力 —— 二八原则

Redis 在一个分布式系统中,通常就扮演着缓存这样的角色

引入的问题:数据库和缓存的数据一致性问题

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

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

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

二、Redis 特性

1、在内存中存储数据

MySQL 主要是通过“表”的方式来存储组织数据的    =>    “关系型数据库”

Redis 主要是通过“键值对”的方式来存储组织数据的     =>    “非关系型数据库”

key 都是 字符串(string)

value 则可以是字符串、哈希表、列表、集合、有序集合

2、可编程

针对 Redis 的操作,可以直接通过简单的交互式命令进行操作

也可以通过一些脚本的方式,批量执行一些操作(可以带有一些逻辑)

脚本经常用 Lua 语言来编写(也是一种编程语言,中文:“撸啊”)

3、扩展能力

Redis提供了一组 API,可以在 Redis 原有的功能基础上再进行扩展

通过 C,C++,Rust 这几个语言编写 Redis 扩展(本质上就是一个动态链接库)

作用:

        自己去扩展 Redis 的功能

        比如,Redis自身已经提供了很多的数据结构和命令,但没有我们想要使用的数据结构

        此时便可以通过扩展,让Redis支持更多的数据结构以及支持更多的命令

4、持久化

Redis把数据存储到内存中,而内存中的数据是“易失”的,比如进程退出/系统重启

所以Redis的持久化策略是: Redis会把数据存储到硬盘中,内存为主,硬盘为辅

硬盘相当于对内存中的数据备份了一下,如果Redis重启了,就会在重启时加载硬盘中的备份数据,使Redis的内存恢复到重启前的状态

5、支持集群

Redis作为一个分布式系统中的中间件,能够支持集群是很关键的

Redis支持水平扩展,这个水平扩展,类似于“分库分表”

一个Redis能存储的数据是有限的(内存空间有限)

引入多台主机,部署多个Redis节点,每个Redis存储数据的一部分

6、高可用

高可用 => 冗余/备份

Redis自身也是支持“主从”结构的,从节点就都相当于主节点的备份了

当一个从节点挂了时,主节点仍然可以正常运行

7、快!!!

1、Redis数据在内存中,就比访问硬盘的数据库,要快很多

2、Redis核心功能都是比较简单的逻辑,核心功能都是比较简单的操作内存的数据结构

3、从网络角度上,Redis使用了 IO多路复用 的方式(epoll)

IO多路复用:使用一个线程,管理很多个socket

4、Redis使用的是单线程模型(虽然更高版本的Redis引入了多线程)

这样的单线程模型,减少了不必要的线程之间的竞争开销

多线程提高效率的前提是,CPU密集型的任务

使用多个线程可以充分的利用 CPU多核资源

但是 Redis 的核心任务,主要就是操作内存的数据结构,不会吃很多CPU

5、Redis是使用C语言开发的,所以就快

个人不认可,但是网上这么多

个人不认可原因:MySQL也是C语言开发的

三、Redis应用场景

1、把Redis当作数据库

在大多数情况下,考虑到数据存储,优先考虑的是“大”,但是仍然有一些场景,考虑的是“快” => 这时候就可以考虑把Redis当作数据库,比如搜索引擎(对性能要求极高)

这种应用场景时,Redis存的是全量数据,这里的数据是不能随便丢的

2、缓存&身份信息

二八原则 => 把热点数据拎出来,存储到redis中

cookie=>实现用户身份信息的保存  需要session配合的

cookie只是在浏览器这边存储了一个用户的身份标识(sessionId)

session=>服务器这里真正的存储了用户数据=>此时咱便可以使用Redis来存储session

问题:当下面这种情况时,session存在一台应用服务器上时,当该用户的请求下次发到其他服务器上时,其他服务器没有该用户的session,该如何处理请求呢?

 解决办法:如下图>

        1、想办法让负载均衡器,把同一个用户的请求始终打到同一个机器上(不能轮询了,而是通过userid之类的方式来分配机器)

        2、把会话数据单独拎出来,放到一组独立的机器上存储(Redis)

应用程序重启了,会话不丢失

3、消息队列(服务器)

此时说到的消息队列,不是Linux进程间通信的那个消息队列

基于这个可以实现一个网络版本的生产者消费者模型

对于分布式系统来说,服务器和服务器之间,与时候也需要用到生产者消费者模型的

优势:

  1. 解耦合
  2. 削峰填谷

业界还有很多知名的消息队列,如 RabbitMQ,Kafka,RocketMQ......

真正使用Redis作为消息队列的场景并不多

四、在Contos下安装Redis5

1、下载Redis5

scl源就像 应用商店 一样

 若中间让输入y等符号,输入y即可


2、创建符号链接(快捷方式)

为什么要创建符号链接,因为自带的文件目录太深太麻烦了

这样就建立了几个符号链接,在bin目录下就可以直接使用

 对配置文件建立符号链接

etc目录通常就是操作系统存放配置文件的目录

3、修改配置文件

注:若在本机上下载Redis,不需要改

找到 bind,更改ip地址(本机不需要修改,默认就是本机)

更改为 bind 0.0.0.0


这里原本是yes,现更改为no(保护机制关闭,这样别人就可以访问你的Redis了)


这里原本是no,现在改为yes,这样就可以让Redis按照守护进程的方式运行了

Linux中的进程,分成前台 进程 和 后台 进程

前台进程会随着终端的关闭而随之被杀死

后台进程不会随着终端关闭而关闭


 4、设置工作目录和日志目录

比如后续Redis要进行持久化,持久化的文件就放在该目录里


 5、启动Redis

 查看端口号看是否启动成功:

启动成功! 

6、停止 Redis

先查询Redis的进程ID,然后直接KIll即可

 7、Redis的相关文件地址

日志文件地址:/var/log/redis/

查看日志文件 

 工作目录地址: /var/lib/redis/

 

Redis是在分布式系统中,才能发挥威力的,如果只是单机程序,直接通过变量存储数据的方式,是比使用 Redis 更优的选择

8、进入Redis

redis-cli

即可进入Redis内部

让 redis 显示汉字正常的字符形式

redis-cli --raw

五、Redis客户端(多种多样)

redis 是一个 客户端-服务器 结构的程序

1、自带了命令行的客户端

链接 Redis

redis-cli (直接连接本地Redis)

指定IP地址和端口号: redis-cli -h 127.0.0.1 -p 6379   (此处以本机地址作为演示)

2、图形化界面的客户端

比如:桌面程序,web程序

3、基于 redis 的 api 自行开发客户端(工作中最主要的形态)

非常类似于mysql的C语言API和JDBC

六、Redis 命令

redis 中的命令不区分大小写

1、get、set 

这是 Redis 中最核心的两个命令

前置知识 —— redis 是按照键值对的方式存储数据的

get          根据 key 来取 value

set          把 key 和 value 存储进去

 注:必须要先进入 redis-cli 客户端程序,才能输入 redis 命令

set key value

get key

注: 对于 get 来说,只是支持字符串类型的 value

        如果 value 是其他类型,使用 get 获取就会出错 

2、mget、mset

mget key [key ...]

mset key value [key value ...]

  • 一次设置或查询多个 key
  • 时间复杂度均是 O(N)   N 是 key 数量

        此处的 N 不是整个 redis 服务器中所有 key 的数量,而只是当前命令中,给出的 key 的数量

        由此此处的时间复杂度也可以看成O(1)

2、keys

语法: 

keys pattern

 pattern写法:

?     匹配任意一个字符

*      匹配0个或者多个任意字符

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

[^e]           排除e,只有e匹配不了,其他的都能匹配

[a-b]          匹配 a - b 这个范围内的字符,包含两侧边界

实操截图:

注意事项

keys 命令的时间复杂度是O(N)

所以,在生产环境上,一般都会禁止使用 keys 命令,尤其是大杀器 keys *

keys *   =>   查询 redis 中所有的 key

生产环境上的key可能会非常多!!!而redis是一个单线程的服务器,执行 keys* 的时间非常长,就使 redis 服务器被堵塞了,无法给其他客户端提供服务

3、exists

exists 判定 key 是否存在

exists key [key...]

返回值: key 存在的个数(针对多个 key 来说,是非常有用的)

时间复杂度: O(1)也可以说O(N)     检查几个key,就是O几

分开查询和合并查询有什么不同么?

封面开的写法,会产生更多轮次的网络通信

而网络通信和直接操作内存比,效率比较低,成本比较高

redis 自身也非常清楚上述问题,redis 的很多命令都是支持一次就能操作多个 key 的多种操作

4、del

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

del key [key...]

返回值: 删除掉的 key 的个数

时间复杂度: O(1) 也可以说O(N)     检查几个key,就是O几

5、expire

expire 作用是给指定的 key 设置过期时间   单位是秒

key 存活时间超过这个指定的值,就会被自动删除

expire key seconds

 时间复杂度: O(1)

返回值:1表示设置成功      0表示设置失败

此处的设定过期时间,必须是针对已经存在的 key 设置

感觉秒太慢的话,也有另一个命令,  pexpire key 毫秒

 redis 中的 key 过期策略是如何实现的?

https://blog.csdn.net/dream_ready/article/details/132789385

6、ttl 

查看当前 key 的过期时间还剩多少

ttl key

时间复杂度:O(1)

返回值:剩余过期时间    -1表示没有关联过期时间,-2表示key不存在 

 redis 中的 key 过期策略是如何实现的?

https://blog.csdn.net/dream_ready/article/details/132789385

7、type 

返回 key 对应 value 的数据类型

type key

 

8、 object  encoding

查看 value 真实的数据类型

 object encoding key

9、flushall(大写:FLUSHALL)

警告!警告!警告!

极度危险!删库跑路!

 flushall

作用:清楚 redis 上所有的数据 ~ 删库

可以把 redis 上所有的键值对都带走!

直接输入这个单词即可,这个单词便是一个完整的命令

11、清除当前屏幕内容(实际是把当前内容往上移动了,并没有消除)

在当前会话操作许多命令后,屏幕会被占满,按下 Ctrl 和 l ,即可清除当前屏幕内容

10、redis 文档语法说明

redis 文档给出的语法格式说明:

[] 相当于一个独立的单元,表示可选项(可有可无的)

其中 | 表示“或者”的意思,多个只能出现一个

[] 和 [] 之间,是可以同时存在的

八、Redis的数据类型

1、redis 的数据类型

key 只能是 字符串

value 可以是字符串、哈希、列表、集合、有序集合

  • 字符串类似于C++中的std::string,java'中的String
  • 哈希类似于C++中的std::unordered_map,java中的HashMap
  • 列表类似于C++中的 std::deque,java中的 List
  • 集合类似于C++中的std::set,java中的 Set
  • 有序集合,相当于除了存储 member 之外,还需要存储一个 score(权重,分数)

Redis 底层在实现上述数据结构的时候,会在源码层面,针对上述实现特定的优化,来达到节省时间/节省空间的效果

特定的优化:

        内部的具体实现的数据结构/编码方式,还会有变数

        数据结构:redis 承诺给你的,也可以理解成数据类型

        编码方式:redis 内部底层的实现

Redis 承诺,现在我这有个 hash 表,你进行查询、插入、删除操作,都保证O(1)

但是,这个背后的实现,不一定就是一个标准的 hash 表

可能在特定的场景下,使用别的数据结构,但是仍然保证时间复杂度符合承诺

2、内部编码: 

  • raw              最基本的字符串,底层就是持有一个char数组(C++)或者 byte 数组(java)
  • int                当 value 就是一个整数的时候,此时可能 redis 会直接使用 int 来保存,常用来实现一些“计数” 这样的功能
  • embstr         针对短字符串进行特殊的优化
  • hashtable     最基本的哈希表,redis 内部哈希表的实现
  • ziplist            压缩列表,能够节省空间    在哈希表里面元素比较少的时候,可能就优化成 ziplist 了
  • quicklist        是一个链表,每个元素又是一个 ziplist , 把空间和效率都折衷的兼顾到    从redis3.2 开始,就不用 linklist 和 ziplist 了,list 都开始用 quicklist来实现了,同时兼顾了 linklist 和 ziplist 的优点 ,类似于C++中的 std::deque
  • intset            集合中存的都是整数
  • skiplist          跳表   跳表也是链表,不同于普通的链表,每个节点上有多个指针域,巧妙的搭配这些指针域的指向,就可以做到从跳表上查询元素的时间复杂度是O(logN) 

演示代码:

利用 object encoding key  查看 value 真实的数据类型

     


九、redis 虽然是单线程模型,为啥效率这么高?速度这么快? 以及什么是 IO 复用

1、redis 虽然是单线程模型,为啥效率这么高?速度这么快?

https://blog.csdn.net/dream_ready/article/details/132862844icon-default.png?t=N7T8https://blog.csdn.net/dream_ready/article/details/132862844

2、IO 多路复用

https://blog.csdn.net/dream_ready/article/details/132863383icon-default.png?t=N7T8https://blog.csdn.net/dream_ready/article/details/132863383

 十、Redis 中的 String  详细解释  以及操作 字符串 的命令

redis 所有的 key 都是字符串

value 的类型是存在差异的

Redis 中的字符串,直接就是按照二进制数据的方式存储的,不会做任何的编码转换,存的是啥,取出来还是啥

不仅可以存储文本数据,还可以存:

整数

普通的文本字符串

JSON

xml

二进制数据(图片,视频,音频)

音频视频,体积可能会比较大

Redis 对于 String 类型,限制了大小最大是 512M

Redis 单线程模型,希望进行的操作都能比较迅速

set  详细解释

比如 set key value ex 10

就相当于: expire key 10

ex 设置时间是 秒, px 则是 毫秒

nx     如果 key 不存在,才设置

         如果 key 存在,则不设置(返回 nil)

xx     如果 key 存在,才设置(相当于更新 key 的 value)

         如果 key 不存在,则不设置(返回 nil) 

set 操作:

        如果 key 不存在,创建新的键值对

        如果 key 存在,则是让新的 value 覆盖旧的 value,可能会改变原来的数据类型 

        原来这个 key 的 ttl(生存时间)也会失效

 

  • setnx       不存在才能设置,存在才能设置失败
  • setex       设置 key 的过期时间    单位是秒
  • psetex      设置 key 的过期时间    单位是毫秒
  • setnx key value
  • setex key time value
  • psetex key time value

time 是过期时间

以下这几个命令针对的 value (整数) 必须在 64位范围内

把 String 当作整数进行操作:

  • incr                     针对 value + 1
  • incrby                 针对 value + n
  • decr                    针对 value - 1
  • decrby                针对 value - n
  • incrbyfloat           针对 value +/- 小数

 incr    针对 value + 1

incr key 

此时 key 对应的 value 必须得是整数

此操作的返回值,就是 + 1 之后的返回值

incr 操作的 key 如果不存在,就会把这个 key 的 value 当作 0 来使用

incrby        针对 value + n

incrby  key n

n 可以是正负数

decr  针对 value - 1

decr key 

此时 key 对应的 value 必须得是整数

此操作的返回值,就是 - 1 之后的返回值

decr 操作的 key 如果不存在,就会把这个 key 的 value 当作 0 来使用

decrby 针对 value - n

decrby key n

n  可以是正负数

incrbyfloat           针对 value +/- 小数

incrbyfloat  key  n

把 key 对应的 value 进行 + - 运算

运算的操作数可以是浮点数

上述操作的时间复杂度,都是O(1)

由于 redis 处理命令的时候,是单线程模型,多个客户端同时针对同一个 key 进行 incr 操作,不会引起类似 "线程安全" 的问题

字符串,也支持一些常用的操作,拼接,获取/修改 字符串的部分内容,获取字符串长度

append

append  key  value

append  返回值,长度的单位是字节

redis 的字符串,不会对字符编码做任何处理(redis 不认识字符,只认识字节)

当前咱们的 xshell 终端,默认的字符编码是 utf8

在终端中输入汉字之后,也就是按照 utf8 编码的

一个汉字在 utf8 字符集中,通常是3个字节的

此时,若 get key 得到的是汉字的字节序列

\x 表示转义字符,表示这是个16进制的数据

在启动 redis 客户端的时候,加上一个 --raw 这样的选项

就可以使 redis 客户端能够自动的把二进制数据尝试翻译

getrange    字串切分 => 查找 key 对应的value,并将value切割再返回

getrange key start end

切割范围由 start 和 end 确定(左闭右闭)

redis 中指定的区间,是闭区间

正常下标都是从 0 开始的整数

redis 的下标是可以支持负数的

-1 是倒数第一个元素

下标为 len -1 的元素

如果字符串中保存的是汉字,此时进行子串切分,很可能切出来的就不是完整的汉字了

补:

上述的代码,是强行切出了中间的中间的四个字节

随便这么一切,切除的结果在 utf8 码表上不知道能查出啥了

上述问题,在C++ 中同样存在

C++ 中字符串的基本单位是字节

java 中就没事

java 中字符串的基本单位,是字符(java 的字符,占两个字节的字符)

setrange     替换某个位置之后的字符串内容

setrange key offset value

offset  是  偏移量

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

如果当前咱们 value 是一个中文字符串,进行 setrange 的时候,是可能会搞出问题的

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

凭空生成了一个字节,这个字节里的内容就是0x00 

aaa 就被追加到 0x00 的后面了

strlen   获取到字符串的长度  单位是字节

strlen  key

补充:

redis 存储小数,区别与存储整数的情况区别挺大的:

String 的应用场景

Redis + MySQL 组成的缓存存储结构

注: 这就是所说的 Redis 常用来存储 “热点数据”

 整体的思路:

应用服务器访问数据的时候,先查询 Redis

如果 Redis 上数据存在了,就直接从 Redis 取数据交给应用服务器,不继续访问数据库了

如果 Redis 上数据不存在,再读取 MySQL,把读到的结果,返回给应用服务器

同时,把这个数据也写入到 Redis 中

上述策略,存在一个明显的问题:

随着时间的推移,肯定是会有越来越多的 key 在 redis 上访问不到

从而从 mysql 读取并写入 redis了

此时 redis 中的数据不是就越来越多嘛

1、在把数据写给 redis 的同时,给这个 key 设置一个过期时间

2、Redis 也在内存不足的时候,提供了 淘汰策略

记录视频播放次数

这里写入统计数据仓库(可能是mysql等)的步骤是异步的

而且不是说,来一个播放请求,这里就必须立即马上写一个数据

Redis 集中管理 Session

Session 会话

Cookie  =>  浏览器存储数据的机制  (键值对)  

Session  =>  服务器这边存储数据的机制   (键值对)

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

此时所有的会话数据(病人的信息)都被各个服务器(医生)共享了

生活举例:

短信验证码

主要是用上了 redis 的 key 过期策略 

1、生成验证码

用户输入一下手机号,点击获取验证码 

2、检查验证码

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

十一、 hash 类型

字符串和哈希类型对比

Redis 自身已经是键值对结构了

Redis 自身的键值对就是通过 哈希 的方式来组织的

把 key 这一层组织完成之后,到了 value 这一层

value 的其中一种类型还可以是哈希

hset

hset  key  field  value  [field  value...]

返回值,是设置成功的 键值对 (field - value) 的个数

field 是 字段 的意思

hget

hget  key  field

hexists

hexists  key  field

 

1表示存在,0表示不存在

hedl

hdel key field [field ...]

删除 hash 中指定的字段

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

补:注意区分:

del 删除的是 key

hdel 删除的是 field

  

hkeys

hkeys key

获取 hash 中所有的 field

这个操作,先根据 key 找到对应的 hash,O(1) 

然后再遍历 hash   O(N)  N是hash的元素个数

 注意!!    这个操作也是存在一定风险的!! 类似于之前介绍过的 keys *

hvals 

hvals key

获取 hash 中所有的 value

时间复杂度是O(N)  N 是哈希的元素个数

如果 哈希 非常大,这个操作就可能导致 redis 服务器被阻塞住

hgetall

hgetall key

获取 hash 中的所有字段以及对应的值

hmget

hmget  key  field  [field ...]

类似之前的 mget,可以一次查询多个 field

hlen

hlen  key

获取 hash 中的所有字段(field)的个数

hsetnx

hsetnx  key  field  value

在字段不存在的情况下,设置 hash 中的字段和值 

类似于 setnx 不存在的时候,才能设置成功 如果存在,则失败

hash 这里的 value,也可以当作数字来处理

hincrby   就可以加减整数

hincrbyfloat   就可以加减小数

hincrby  key  field  increment

hincrbyfloat  key  field  increment

        使用频率不算高,redis 没有提供类似于 incr decr...

时间复杂度都是O(1)

哈希的应用

映射关系表表示用户信息

关系型数据表是这样保存用户信息的:(比如 MySQL)

原生字符串类型 —— 使用字符串类型,每个属性一个键 

 原生字符串类型一般不用,因为这种方式,相当于把同一个数据的各个属性,给分散开表示了(低内聚)

什么叫做高内聚,低耦合?

https://blog.csdn.net/dream_ready/article/details/132921437

  • 如果使用 String (json) 的格式来表示 UserInfo                                                                  万一只想获取某中的某个 field,或者修改某个 field                                                        就需要把整个 json 都读出来,解析成对象,操作 field,再重写转成 json 字符串,再写回去

  • 如果使用 hash 的方式来表示 UserInfo                                                                              就可以使用 field 表示对象的每个属性(数据表的每个列)                                                    此时就可以非常方便的修改/获取到任何一个属性的值了

使用 hash 的方式,确实读写 field 更直观高效,但是付出的是空间的代价

需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗

十二、list

列表(List) 相当于 数组 或者 顺序表

约定最左侧元素下标是0

redis 的下标支持负数下标

注意,list 内部的结果(编码方式),并非是一个简单的数组,而是更接近于“双端队列”(deque)

列表的元素是有序的

“有序”的含义,要根据上下文区分

有的时候,谈到有序,指的是“升序”,“降序”

有的时候,谈到的有序,指的是,顺序很关键

顺序很关键:

  •         如果把元素的位置颠倒,顺序调换,
  •         此时得到的新的 List 和之前的 List 是不等价的

同样一个词,怎么理解,务必要结合上下文,结合具体场景

栈/堆 (数据结构的,操作系统的,还是 JVM 的)

同步 (同步和互斥的同步,还是同步和异步的同步)

lpush

lpush  key  element  [element ...]

按照顺序,依次头插这几个元素,全部插入完毕,4是在最前面的

一次可以插入一个元素,也可以插入多个元素

key 存在,则插入,不存在,则创建

时间复杂度是O(1)

返回值是 list 的长度

如果 key 已经存在,并且 key 对应的 value 类型,不是 list,此时 lpush 命令就要报错

lrange

lrange  key  start  stop

查看 list 中指定范围的元素

此时描述的区间也是闭区间,下标支持负数

在 C++ 中,下标超出范围,虽灵活,但有一定危险性

java 中,下标超出范围,一般会抛出异常

在 Redis 中并没有采用上述的两种设定

Redis 的做法,是直接尽可能的获取到给定空间的元素,如果给定区间非法,比如超出下标,就会尽可能的获取对应的内容

 

 此处对于越界下标的处理方式,更接近于 python 的处理方法

lpushx

lpushx  key  element  [element ...]

 在 key 存在时,将一个或多个元素从左侧放入 (头插) 到 list 中。不存在,直接返回0

返回值:插入后 list 的长度

rpush

rpush  key  element  [element ...]

 将一个或者多个元素从右侧放入(尾插)到 list 中

返回值:插入后 list 的长度

rpushx

rpushx  key  element  [element ...]

当 key 存在时,将一个或者多个元素从右侧放入(尾插)到 list 中 

lpop

lpop  key

从 list 左侧取出元素(即头删)

返回值:取出的元素或者nil

rpop

rpop  key

从 list 右侧取出元素(即尾删)

lpop 和 rpop

在当前的 redis 5 版本中,都是没有 count 参数的

Redis 中的 list 是一个双端队列

从两头插入/删除元素都是非常高效O(1)

搭配使用 rpush 和 lpop,就相当于队列了

搭配使用 rpush 和 rpop,就相当于栈了

lindex

lindex  key  index

获取从左数第 index 位置的元素(给定下标,获取到对应的元素)

linsert

linsert  key  <before | after> pivot element

 在特定位置插入元素 

before 和 after 二选一

注:此处的位置不是下标!!! 而是 基准值(即 value 值)

注意看,位置不是 下标,而是 value!!!

万一要插入的列表中,基准值,存在多个,咋办?

linsert 进行插入的时候,要根据基准值,找到对应的位置,从左往右找,找到第一个符合基准值的位置即可

lrem

lrem key count element

count :要删除的个数                  element:要删除的值

count > 0 :  从左往右删 count 个 element

count < 0 :  从右往左删 count 个 element

count = 0 :  删除所有的 element

 ltrim

ltrim  key  start  stop

保留 start 和 stop 之间区间内的元素(区间外面两边的元素就直接被删除了) 

lset

lset  key  index  element

 修改指定下标位置的元素

阻塞

阻塞:当前的线程,不走了,代码不继续执行了

           会在满足一定的条件之后,被唤醒

redis 中的 list 也相当于 阻塞队列一样

线程安全是通过单线程模型支持的

阻塞,则只支持“队列为空”的情况下,不考虑“队列满”

blpop       lpop

brpop      rpop

        如果 list 中存在元素,blpop 和 brpop 就和 lpop 以及 rpop作用完全相同

        如果 list 中为空,blpop 和 brpop 就会产生阻塞,一直阻塞到队列不空为止

 blpop

blpop  key  [key ...]  timeout

此处可以指定一个 key 或者 多个 key

每个 key 都对应一个 list

如果这些 list 有任何一个非空,blpop 都能够把这里的元素给获取到,立即返回

如果这些 list 都为空,此时就需要阻塞等待,等待其他客户端往这些 list 中插入元素了

此处还可以指定超时时间

        单位是 秒 (Redis6,超时时间允许设定成小数,Redis5 中,超时时间,得是整数 )

brpop 效果 和 blpop 完全一样,只不过变成尾删了

阻塞版本会根据  timeout , 阻塞一段时间,期间 Redis 可以执行其他命令

使用 brpop 和 blpop 的时候,这里是可以显式设置阻塞时间的!!!(不一定是无休止的等待!)

此处的 blpop 和 brpop 看起来,好像耗时很久,但是实际上,并不会对 redis 服务器产生负面影响!

blpop 和 brpop 都是可以同时去尝试获取多个 key 的列表的元素的

        多个 key 对应多个 list

        这多个 list 哪个有元素了,就会返回哪个元素

补充(权限知识):

list 的应用场景 

1、用 list 作为“数组”这样的结构,来存储多个元素

 具体 redis 中的数据如何组织,都要根据实际的业务情况来决定

使用 redis 作为消息队列(生产者消费者模型)

Redis 分频道阻塞队列模型

十二、set  集合

集合的定义:

集合就是把一些有关联的数据放到一起

        1、集合中的元素是无序的

        2、集合中的元素是不能重复的(唯一的)

此处说的 无序 和 list 的 有序 是对应的

有序:顺序很重要,变换一下顺序,就是不同的 list 了

无序:顺序不重要,变化一下顺序,集合还是那个集合

和 list 类似,集合中的每个元素,也都是 string 类型

(可以使用 json 这样的格式让 string 也能存储 结构化 数据)

sadd

sadd key member [member ...]

集合中的元素,叫 member

就像 hash 类型中,叫做 field 类似,只是叫法不同

时间复杂度: O(1)

返回值表示本次操作,添加成功了几个元素(集合中的元素不可重复,重复的元素只添加一个)

smembers

smembers key

查询集合中的元素 

sismember 

sismember  key  member

 集合的操作都是带有 S 前缀

判定当前的元素是否在集合中

spop

spop  key  [count]

随机删除元素!!!

count 不写的时候,随机删除一个;写的时候,就是写几个就删几个

srandmember

srandmember  key  [count]

在集合中随机取 member,不删除(除了不删除以外其他和 spop 一样)

smove

smove  source  destination  member

把 member 从 source 上删除,再插入到 destination 中

将元素从一个集合移动到另一个集合中

如果我给 key1 里再添加一个 1,再次把这个 1 移动给 key2 会咋样?

针对上述的情况,smove不会视为出错,也会按照 删除 插入进行执行

但 key2 中已经有1了(注意集合元素不能重复),所以仅仅是不出错,仅此而已

如果要移动的元素在 source 不存在呢

此处就会返回 0 表示移动失败了

srem

srem  key  member  [member ...]

可以一次删除一个 member,也可以一次删除多个 member

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

sinter

sinter  key  [key ...]

求多个key中元素的交集

此处每个 key 都对应一个集合

返回值就是 最终交集的数据

O(N * M),N 是最小的集合元素个数,M是最大的集合元素个数

sinterstore

sinterstore destination key [key ...]

直接把算好的交集,放到 destination 这个 key 对应的集合中

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

sunion

sunion  key  [key ...]

求多个key中元素的并集

返回值是并集的结果数据

时间复杂度 O(N)  N指的是总的元素个数

sunionstore

sunionstore  destination  key  [key  ...]

直接把并集的结果存储到 destination 对应的集合中

返回值并集的元素个数

sdiff

sdiff  key  [key ...]

返回值是差集的结果

时间复杂度:O(N)

注意,key 之间换顺序意义是一样的

差集是指前者key中有,后者key中没有的元素集合

有顺序,注意使用区别 

sdiffstore

sdiffstore  destination  key  [key  ...]

直接把差集的结果存储到 destination 对应的集合中

返回值是差集的元素个数

时间复杂度:O(N)

Set 的应用场景

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

用户画像

        分析出你这个人的一些特征,分析清楚特征之后,再投其所好 —— 千人千面

特征比如:性别,年龄,居住地,爱好等

通过上述过程,搜集到的用户特征,就会转换成“标签”  =>  简短的字符串  =>  保存到 redis 的 set 中了

用 set 存储标签的好处:容易计算交集

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

        基于 “集合求交集”

3、使用 Set 统计 UV

        “去重”

一个互联网产品,如何衡量用户量,用户规模?

主要的指标,是两方面:

1、PV  page view        用户每次访问该服务器,每次访问都会产生一个 pv

2、UV user view          每个用户,访问服务器,都会产生一个 uv,但是同一个用户多次访问,不                                       会使 uv 增加

uv 需要按照用户进行去重,上述的去重过程,就可以使用 set 来实现

十三、zset   有序集合

此处指的是 升序/降序

排序的规则是啥?

        给 zset 中的 member 同时引入了一个属性

        分数(score),浮点类型

        每个 member 都会安排一个分数

        进行排序的时候,就是依照此处的分数大小来进行升序/降序排序 

zset  中的 member 和 score 的关系,不能理解成 “键值对”

实际上 zset 内部就是按照升序方式来排列的 

 Zset 中的 member 仍然要求是唯一的!(score 则可以重复)

 列表、集合、有序集合间的区别:

zadd

zadd key [nx | xx] [gt | lt] [ch] [incr] score member [score member ...]

 往有序集合中,添加元素和分数

时间复杂度:O(logN)    注:N是有序集合中的元素个数

 ZADD的相关选项:
• XX:仅仅⽤于更新已经存在的元素,不会添加新元素。
• NX:仅⽤于添加新元素,不会更新已经存在的元素。

• lt    :更新分数时,若给定的新的分数比之前的分数小,此时就更新成功,否则就不更新

•gt    :更新分数时,若给定的新的分数比之前的分数大,此时就更新成功,否则就不更新
• CH:默认情况下,zadd 返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更新的元素的个数。
• INCR:此时命令类似 ZINCRBY 的效果,将元素的分数加上指定的分数。此时只能指定⼀个元素和分数。

 不加 nx | xx 选项的时候:

        如果当前 member 不存在,此时就会达到 “添加新 member” 的效果

        如果当前 member 已经存在,此时就会更新分数

分数相同,再按照元素自身字符串的字典序来排列

分数不同,仍然按照分数来排

zadd 也可以修改分数

和添加的格式一样,注意修改操作是修改 score,而不是 member

返回值是0正常,因为 zadd 返回新增的元素个数

用 ch,让其返回值里面既包含新增,也包含修改

zrange  

zrange  key  start  stop  [withscores]

查询集合中的元素,按照升序 ,可指定区间(start开始,stop结束,闭区间,支持负数)

带上 withscores 可以带上分数一起查询

incr

zcrad

zcard  key

获取当前元素的个数(按照 member 个数计算)

zcount

zcount  key  min  max

返回分数在 min 和 max 之间的元素个数

默认是一个闭区间

使用( 表示开区间,使用[ 表示闭区间

注意:是左括号,右括号不对

时间复杂度: O(logN)

并不是通过遍历的方法计算的

zrange

zrange  key  start  stop  [withscores]

返回指定区间的元素,分数按照升序

时间复杂度:O(log(N)+M)

从 start 对应的位置开始遍历

zrevrange

ZREVRANGE key start stop [WITHSCORES]

返回指定区间的元素,分数按照降序

zrangebyscore

zrangebyscore  key  min  max  [withscores]

 闭区间,按照分数来查找元素,和 zcount 类似

这个命令可能在6.2.0之后废弃,并且功能合并到 zrange 中

zpopmax

zpop  key  [count]

删除并返回分数最高的 count 个元素(member 和 score 都显示)

如果存在多个元素,分数相同,同时为最大值,zpopmax 删的时候,仍然只删除其中一个元素

如果分数相同会按照 member 字符串的字典序决定先后! 

bzpopmax

bzpopmax 的阻塞版本

bzpopmax  key  [key ...]  timeout

有序集合 也可以视为是一个 “优先级队列”

有的时候,也需要一个带有 “阻塞功能” 的优先级队列

每个 key 都是一个有序集合

阻塞也是在有序集合为空的时候触发阻塞,阻塞到其他客户端插入元素

如果有序结合中已经有了元素,直接就能返回,不会阻塞了

timeout 表示超时时间,最多阻塞多久

        单位是 s,支持小数形式,写作 0.1 就是 100 ms

zpopmin

zpopmin  key  [count]

删除有序集合中,最小的元素

时间复杂度: O(logN * M)

虽然 redis 的有序集合记录了开头的元素,但是删除的时候使用的是通用的删除函数,导致出现了重新查找的过程

bzpopmin

bzpopmin  key  [key ...]  timeout

用法和 bzpopmax 一样的

zrank

zrank  key  member

zrank  得到的下标,是从前往后算的

zrevrank

zrevrank  key  member

获取下标,从后往前算的

zscore

zscore  key  member

根据 member 查询 score

时间复杂度:O(1)

此处相当于 redis 对于这样的查询操作做了特殊优化,付出了额外的空间代价,针对这里进行了优化到O(1) 实现

zrem

zrem  key  member  [member ...]

返回值是删除成功的元素个数

zremrangebyrank

zremrangerank  key  start  stop

删除该区间内的元素,通过下标来决定(闭区间)

时间复杂度: O(logN + M)

zremrangebyscore

zremrangebyscore  key  min  max

删除该区间内的元素,闭区间,通过分数来描述

删除该区间内的元素,通过下标来决定(闭区间)

时间复杂度: O(logN + M)

zincrby

zincrby  key  increment  member

 修改 member 的 score

increment 是增减的 改变量          可以是正数,小数,负数等

不光会修改分数内容,也能同时移动元素位置,保持整个有序集合仍然是升序的

zinter, zunion, zdiff

上面这几个命令是从 Redis 6.2 开始支持的

zinterstore

交集,结果保存到另一个 key 中

权重可以是小数

时间复杂度:

zunionstore

求并集

zset 的应用场景

最关键的应用场景:排行榜系统

像 微博热搜、 游戏天梯排行、 成绩排行 等

十三、其他不常用的数据类型(简单介绍)

1、stream 就是一个队列(阻塞队列)

redis 作为一个消息队列的重要支撑

属于是 List blpop/brpop 升级版本

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

2、Redis geospatial

用来存储坐标(类似于地球的经纬度)

存储一些点之后,就可以让用户给定一个坐标,去从刚才存储的点里进行查找(按照半径、矩形区域等)

哲哥功能在“地图”中非常重要

3、Redis HyperLogLog

应用场景只有一个,估算集合中的元素个数

误差大约在 0.81% (不需要记)

注:该数据类型不是 Redis 专有的,因此也可以在其他地方运用该类型(比如引用第三方库等)

4、 Redis bitmaps

位图

作用:使用 bit 位来表示整数

位图本质上,就还是一个集合。 属于是 Set 类型针对整数的特化版本

虽然 HyperLogLog 更省空间,但是它不存储元素, bitmp 类型存储元素了

5、Redis bitfields

位域(位段)

上述 Redis 中的 bitfield 和 C 中的位域非常相似

bitfield 可以理解成一串二进制序列(字节数组)

同时可以把这个字节数组中的某几个位,赋予特定的含义,并且可以进行 读取/修改/算术运算 相关操作

十四、scan —— 渐进式遍历

前言:

 渐进式遍历其实是一组命令,这一组命令的使用方法是一样的

其中代表命令 scan:

scan 命令语法格式介绍:

注: count 不是下标!!! 

 若图片看不清,可放大观看

注意:scan 返回的下次光标的值是下次遍历的起始位置

           当返回 0 时说明遍历完了

会产生副作用的生活例子:

我去吃烧烤

不会产生副作用的生活例子:

忌讳:边遍历,边增加/修改/删除

 redis 和 mysql 操作区别分析以及操作命令

 十五、Redis 客户端

对 Redis 客户端及其相关进行介绍

为什么我们能编写出一个自定义的 redis 客户端

因为 Redis 公开了自己的应用层协议的名字: resp

Redis 公开了自己的应用层协议的名字: resp 

resp

数据类型:

  • 带 + 号,是字符串
  • 带 - 号,是错误
  • 带 . 号,是整数
  • 带 ¥ 号,是 Bulk Strings
  • 带 * 号,是数组

 

 

!!! 此博客每两天必更新,一直到结束,预计更新至少三万字!!!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
尚硅谷是一个教育机构,他们提供了一份关于Redis学习笔记。根据提供的引用内容,我们可以了解到他们提到了一些关于Redis配置和使用的内容。 首先,在引用中提到了通过执行命令"vi /redis-6.2.6/redis.conf"来编辑Redis配置文件。这个命令可以让你进入只读模式来查询"daemonize"配置项的位置。 在引用中提到了Redis会根据键值计算出应该送往的插槽,并且如果不是该客户端对应服务器的插槽,Redis会报错并告知应该前往的Redis实例的地址和端口。 在引用中提到了通过修改Redis的配置文件来指定Redis的日志文件位置。可以使用命令"sudo vim /etc/redis.conf"来编辑Redis的配置文件,并且在文件中指定日志文件的位置。 通过这些引用内容,我们可以得出结论,尚硅谷的Redis学习笔记涵盖了关于Redis的配置和使用的内容,并提供了一些相关的命令和操作示例。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Redis学习笔记--尚硅谷](https://blog.csdn.net/HHCS231/article/details/123637379)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Redis学习笔记——尚硅谷](https://blog.csdn.net/qq_48092631/article/details/129662119)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

dream_ready

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

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

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

打赏作者

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

抵扣说明:

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

余额充值