目录
[1. 通用命令](#1. 通用命令)
[⭕Redis 过期键处理和内存淘汰策略](#⭕Redis 过期键处理和内存淘汰策略)
[2. 数据类型](#2. 数据类型)
[3. 内部编码](#3. 内部编码)
[1. 单线程模型](#1. 单线程模型)
[Redis 单线程模型为何效率高](#Redis 单线程模型为何效率高)
[1. 办公环境](#1. 办公环境)
[2. 开发环境](#2. 开发环境)
[3. 测试环境](#3. 测试环境)
[4. 线上环境/生产环境](#4. 线上环境/生产环境)
[单线程模式 vs 单例模式](#单线程模式 vs 单例模式)
1. 通用命令
SET
- 功能:设置一个键值对。
- 使用方法:
set key value
- 注意事项:
key
和value
都是字符串,可选地使用单引号或双引号包围。
GET
- 功能:获取键对应的值。
- 使用方法:
get key
- 特点:如果查询的键不存在,则返回
nil
。
KEYS
- 功能:获取所有符合特定模式的键。
- 使用方法:
keys pattern
- 常见模式:
-
?
:匹配任何一个字符。*
:匹配任意数量的任意字符。[abc]
:匹配a
、b
、c
中的一个。[^a]
:排除a
,匹配其他任何字符。[a-c]
:匹配a
到c
范围内的字符。
注意:时间复杂度O ( N ) , 在生产环境上,一般都会禁用KEYS命令,尤其是KEYS *
- 生产环境上的key可能会非常多,而Redis是一个单线程服务器,执行KEYS *的时间非常长,就会使Redis服务器被阻塞,无法给其他客户端提供服务
- 此时其他的查询Redis操作超时了,就直接会去查数据库,突然一大波数据突然来了,MySQL可能措手不及,就直接挂了
EXISTS
- 功能:检查一个或多个键是否存在。
- 使用方法:
exists key [key ...]
- 特点:可以同时检查多个键,返回存在的键的数量。由于使用哈希表存储数据,此操作的时间复杂度为 O(1)。
EXISTS key1 key2****和分开写 EXISTS key1**、** EXISTS key2****有什么区别?
- 分开的写法,会产生更多的轮次的网络通信
- 此时跟直接操作内存相比,效率比较低,成本比较高
- 所以,建议合着写
DEL
- 功能:删除一个或多个键。
- 使用方法:
del key [key ...]
- 特点:支持一次删除多个键。
EXPIRE
- 功能:为指定的键设置过期时间(以秒为单位)。
- 使用方法:
expire key seconds
- 返回值:成功返回 1,失败(例如键不存在)返回 0。
TTL
- 功能:获取键的剩余生存时间(以秒为单位)。
- 使用方法:
ttl key
- 返回值:
-
-1
:键没有设置过期时间。-2
:键不存在。- 其他:键的剩余生存时间。
另外的,ttl也有毫秒级别的指令pttl。
⭕那么Redis是如何实现定期删除的?
此处采用了定期删除+惰性删除的策略:
- 定期删除: 每隔一段时间,抽取一部分数据检查,看是否有过期的数据,将其删除
- 惰性删除: 当用户操作数据时,检测一遍这个数据是否过期,如果过期就删除,再给用户返回key不存在
- 因为Redis内部要存储不少数据,轮询一遍所有数据要浪费很多时间,所以不会遍历所有数据判断过期。
- 而是等待用户访问数据才删除,或者抽样检查删除,以降低删除过期数据带来的时间浪费。
总结: 抽检+触发****的删除
思考
⭕Redis 过期键处理和内存淘汰策略
- 当前策略的效果
- 现有策略:Redis 采用了定期删除和惰性删除相结合的策略。
- 效果:尽管这两种策略结合使用,整体效果仍不尽如人意,可能会导致许多过期的键未被及时删除。
- 内存淘汰策略补充
- 补充措施:为了进一步优化过期键的处理,Redis 提供了一系列内存淘汰策略。后文会进行讲解~
- 定时器方式的考虑
- 未采用定时器:Redis 并没有采取定时器的方式来实现过期键的删除。
- 多键过期处理:如果有多个键过期,可以通过一个定时器在高效节省 CPU 的前提下处理多个键。
- 常见的高效的定时器实现方法:基于优先级队列或时间轮可以实现高效的删除。
🎢定时器的实现
1. 优先级队列的应用
- 优先级:可以自定义,但在Redis过期key的场景下,"过期时间越早,优先级越高"。
- 当多个key设置了过期时间,可以将这些key加入到一个优先级队列中,规定过期时间早的key具有更高的优先级。
- 队首元素:总是最早到期的key。
key示例:
- key1: 12:00
- key2: 13:00
- key3: 14:00
🔷定时器的工作原理
- 单线程检查:只需要一个线程检查队首元素是否已过期。
- 高效性:如果队首元素未过期,则后续元素肯定也未过期,因此只需关注队首元素。
- 避免频繁检查:基于当前时间和队首元素的过期时间设置适当的等待时间,以减少CPU开销。
- 动态调整:如果有新的更早过期的任务(如11:30),则需唤醒线程重新评估并调整等待时间。
2. 时间轮实现的定时器
- 时间分割:将时间划分为多个小段,每段可以挂载一个或多个待执行任务的链表。
- 任务表示:每个链表节点可以是一个函数指针及其参数,或等效的对象形式(如Java中的对象)。
指针移动:设定固定的时间间隔(例如100ms)移动指针,直到到达指定位置触发相应key的执行。
- redis 为什么不采用定时器
原因:
- 多线程引入:++基于定时器实现势必会引入多线程。++
- 单线程基础:Redis 早期版本奠定了单线程的基础,引入多线程会打破作者的初衷。
通过一些策略和方法,Redis 基本在处理过期键和内存管理方面取得了平衡,既保持了单线程的简洁性和高效性,又尽量减少了过期键的残留。
TYPE
- 功能:返回键对应的值的类型。
- 使用方法:
type key
- 返回值:none(不存在), string, list, set, zset, hash, stream
- 时间复杂度:O ( 1 )
FLUSHALL
- 功能:删除Redis中所有数据库的数据。
- 使用方法:
flushall
- 注意:在生产环境中谨慎使用此命令,以免造成数据丢失。
2. 数据类型
Redis 提供了多种数据类型:
- String:字符串,是最简单的类型,可以存储文本或数字。
- Hash :哈希,存储键值对的映射。
- List:列表,存储有序的字符串列表。
- Set :集合,存储无序且不重复的字符串集合。
- Sorted Set :有序集合,类似于集合,但每个成员有权重,用于排序。
此外还有:
- Stream:流,用于处理消息队列。
- Bitmap:位图,用于高效地存储布尔值。
- Bitfield:位字段,用于直接操作位。
- Geospatial:地理信息,用于处理地理位置相关的数据。
- ......
3. 内部编码
TYPE
命令实际返回的就是当前键的数据结构类型,但这些只是Redis对外的数据结构- 实际上Redis针对每种数据结构都有⾃⼰的底层内部编码实现,⽽且是多种实现 ,这样Redis会在合适的场景选择合适的内部编码
Redis 对不同数据类型使用不同的内部编码以提高性能和内存使用效率:
|----------|------------|
| 数据类型 | 内部编码 |
| string | raw |
| string | int |
| string | embstr |
| hash | hashtable |
| list | ziplist |
| list | linkedlist |
| list | ziplist |
| set | hashtable |
| set | intset |
| zset | skiplist |
| zset | ziplist |
String
- raw:最基本的字符串,底层实现为字符数组。
- int :当值为整数时,使用
int
类型存储,占用8字节。 - embstr:针对短字符串的优化,减少内存分配次数。
-
- 默认情况下,值以字符串形式传入,如果Redis检测到字符串为数字,则转换为
int
存储,从而节省空间。例如,字符串++"1234567890"若作为字符串存储需要10字节,而转换为int
后仅需8字节。++
- 默认情况下,值以字符串形式传入,如果Redis检测到字符串为数字,则转换为
💡 对于"短"的数字界限,不用太记忆,我们学的是设计思想,参数在许多时候都是可调的
Hash
- hashtable:标准哈希表实现。
- ziplist:当哈希表元素较少时,采用压缩列表以节省空间。
List
- linklist:标准链表实现。
- zip****list:当链表元素较少时,采用压缩列表以节省空间。
-
- Redis 3.2 后,List统一使用quicklist ,结合了
linklist
和ziplist
的优点,类似于 C++中的 deque, 即基本结构为链表,每个链表节点包含一个压缩列表。
- Redis 3.2 后,List统一使用quicklist ,结合了
Set
- hashtable:基于哈希表实现的集合,提供O(1)时间复杂度的操作。
- i ntset:如果集合成员全部为整数,则使用整数集合进行优化。
-
- 在现代编程语言中,常用平衡二叉搜索树(如红黑树)实现Set,时间复杂度为O(log N),而Redis选择哈希表实现Set以获得更高的效率。
ZSet (Sorted Set)
- skiplist:跳表,用于快速查找排序后的元素,时间复杂度为O(log N)。
- zip****list:当有序集合元素较少时,使用压缩列表以节省空间。
-
- 跳表是一种高效的搜索结构,与平衡二叉搜索树处于同一级别的时间复杂度,但提供了更好的并发性能。
Redis根据不同数据类型的特性和应用场景,选择了合适的内部编码方式,以实现高效的数据存储和检索。例如:
- 对于整数和短字符串,Redis通过特殊编码(如
int
和embstr
)来节省内存; - 而对于集合和有序集合,Redis通过哈希表和跳表等数据结构来提高操作的效率。
查看编码
使用 object encoding key
命令可以查看指定键的具体编码方式。
通过合理选择数据类型和理解内部编码机制,可以有效地利用 Redis 实现高性能的应用场景。
内部编码的好处:
- 可以改进内部编码,⽽对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码, ⽆需改动外部数据结构和命令
- 例如:Redis3.2提供了 quicklist,结合了ziplist和linkedlist 两者的优势 ,为列表类型提供了⼀种更为优秀的内部编码实现,⽽对⽤⼾来说基本⽆感知
- 多种内部编码实现可以在不同场景下发挥各⾃的优势
- 例如:ziplist⽐较节省内存,但是在列表元素⽐较多的情况下,性能会下降,这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist,整个过程⽤⼾同样⽆感知
4.单线程
1. 单线程模型
- 单线程处理请求:Redis 使用一个线程处理所有的客户端请求,但这并不意味着Redis服务器内部只有一个线程。
- 内部多线程:Redis 内部确实有多个线程,但这些线程主要用于处理网络 I/O。
- 命令执行顺序:尽管多个客户端看起来是同时发起请求,但从微观角度看,这些命令仍然是按线性顺序执行的,且命令的执行顺序是不确定的,但绝不会有两个命令同时执行。
- 感性理解
- 并发请求与串行处理 :虽然多个客户端并发发起请求,但Redis在处理这些请求时是单线程模型,确保所有请求在内部依然串行执行。
- 请求排队 :多个请求同时到达Redis服务器时,会先在队列中排队,然后由Redis服务器逐个取出并执行。
- 所以不存在之前学习的 多线程安全的 数据同步及加锁解决,有兴趣的 uu 可以看一下这篇前文 [Linux#42][线程] 锁的接口 | 原理 | 封装与运用 | 线程安全
Redis 为何效率高
1. 以数据库为参照
- 参照对象:说Redis快,是以传统数据库为参照的。没有参照对象,单纯说"快"是没有意义的。
- 存储介质:Redis使用纯内存访问,而传统数据库通常是访问硬盘,内存访问速度远超硬盘。
2. 核心功能简单
- 功能对比:Redis的核心功能相对简单,主要涉及键值对的增删查改操作,而传统数据库的功能更为复杂,包括复杂的查询、事务管理等。
- 单线程优势:单线程模型避免了多线程之间的竞争和同步开销,减少了上下文切换的开销,提高了整体效率。
- 操作特点:Redis的每个基本操作都是短平快的,主要是简单的内存操作,不消耗大量CPU资源。即使引入多线程,性能提升也不会很大。
3. 简化数据结构和算法
- 数据结构:单线程模型使得Redis可以使用更高效的数据结构和算法,因为不需要考虑多线程同步问题。
4. 高效的网络I/O处理
- 最开始介绍 TCP 服务器的时候,有一个版本就是每个客户端给分配一个线程。客户端多了,线程就多了,系统开销就大了
- IO多路复用 :Redis在处理网络I/O时使用了
epoll
等IO多路复用机制,实现了非阻塞I/O,进一步提升了性能。 - epoll 事件通知 / 回调机制
- 适用场景:客户端的交互不频繁,大部分时间都在等
自己写服务器网络安排时,就可以模仿参考 进行设计~
注意
Redis有⼀个致命的问题 :对于单个命令的执⾏时间都是有要求的
- 如果某个命令执⾏过⻓,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客⼾端的阻塞,对于Redis这种⾼性能的服务来说是⾮常严重的
- 所以Redis是⾯向快速执⾏场景的数据库
补充:
四种工作环境:
1. 办公环境
- 设备:笔记本(Windows, Mac)或台式机
- 配置:通常为8核16GB内存512GB存储
- 用途 :日常办公和开发工作
2. 开发环境
- 配置:
-
- 有时与办公环境相同
- 有时是单独的服务器,配置为28核128GB内存4TB存储
- 用途:
-
- 前端和客户端开发通常在办公环境中进行
- 后端开发可能需要单独的服务器
- 复杂后端程序的需求:
-
- 长时间编译:C++等语言的编译时间较长,需要高性能服务器
- 高资源消耗:某些程序启动时需要大量CPU和内存资源,办公电脑难以支撑
- 依赖Linux:某些程序在Windows环境下难以搭建
3. 测试环境
- 配置:28核128GB内存4TB存储
- 用途:测试工程师使用,进行功能和性能测试
4. 线上环境/生产环境
- 定义:外界用户能够访问到的环境
- 重要性:
-
- 任何问题都会直接影响用户的使用体验
- 直接影响公司营收,特别是依赖广告收入的公司
- 注意事项:
-
- 操作生产环境的任何设备或程序都需要极其谨慎
- 把程序"上线"是开发工作的最终目标
- 上线次数是衡量程序员工作绩效的重要指标
- 实习生的转正留用也与此相关,频繁上线是稳定的表现
单线程模式 vs 单例模式
单线程模式
- 定义:单线程模式是指程序在执行过程中只使用一个执行线程来处理所有任务。
- 特点:
-
- 简单直接 :同一时间只能处理一个任务,任务按照顺序依次执行。
- 编写和理解容易 :++不需要考虑复杂的线程同步++问题。
- 效率问题 :在处理多个可以同时进行的任务时,特别是多核处理器环境下,不能充分利用硬件资源。
- 适用场景:
-
- 简单任务:例如简单的脚本程序,按照顺序读取文件、处理数据、输出结果。
单例模式
- 定义 :单例模式是一种创建型设计模式,确保一个类只有一个实例存在,并提供一个全局访问点来访问这个实例。
- 特点:
-
- 唯一实例:确保在整个应用程序中只有一个实例存在。
- 全局访问 :提供一个全局访问点,方便其他对象访问该实例。
- 节省资源 :避免频繁创建和销毁对象,例如数据库连接池。
- 线程安全:需要考虑线程安全问题,尤其是在多线程环境下。
- 实现:
-
- 私有构造函数:防止外部直接实例化。
- 静态方法:提供一个全局访问点来获取唯一的实例。
- 适用场景:
-
- 资源共享:例如数据库连接池、日志记录器等。
- 全局状态:需要维护全局状态的场景,如用户会话管理。
主要区别
单线程模式:
- 侧重于:程序执行流程的方式。
- 目标 :确保任务的顺序执行和避免多线程竞争。
单例模式:
- 侧重于:对象的创建和管理。
- 目标 :保证类实例的唯一性,提供全局访问点。
tip: markdown 下的无序嵌套列表,一级序号 tab 即可实现 二级序号