一、Redis概述
Redis是一种基于键值对(key-value)的NoSQL数据库,与许多其他键值对数据库不同之处在于,Redis中的值可以采用多种数据结构和算法组成,包括字符串、哈希、列表、集合、有序集合、位图、HyperLogLog、地理信息定位等。因此,Redis可以满足许多不同的应用场景。由于Redis将所有数据存储在内存中,因此具有出色的读写性能。此外,Redis还可以通过快照和日志的形式将内存中的数据保存到硬盘上,以防止数据丢失,这对于处理类似断电或机器故障的情况至关重要。除了上述功能外,Redis还提供了诸如键过期、发布订阅、事务、流水线、Lua脚本等附加功能
二、为什么要用Redis
在单机系统中,直接通过定义变量来存储数据可能是更好的选择,但是在分布式系统中,各个节点都有自己的内存空间,无法直接共享变量,而Redis是一个分布式数据存储系统,提供了分布式特性,可以在多个节点之间共享数据,并提供了数据持久化、高可用性、数据结构丰富等功能
三、Redis为什么这么快
1、数据存放在内存中:内存的读写速度远快于磁盘存储
2、采用C语言实现:C语言实现的程序与操作系统之间的接口更为紧密,执行速度更快
3、单线程架构:Redis采用单线程架构,避免了多线程可能产生的竞争和同步问题
4、优化的数据结构和算法:Redis在设计和实现过程中采用了一些高效的数据结构和算法
5、异步操作:Redis支持异步操作:例如异步持久化和异步复制,这些操作不会阻塞主线程的执行,从而提高了系统的并发能力和响应速度
6、网络通信机制和使用了IO多路复用(epoll):
高效的网络通信:
- TCP协议: Redis采用TCP/IP协议进行网络通信。TCP协议是一种可靠的、面向连接的传输层协议,它提供了数据的可靠传输、流量控制、拥塞控制等功能,适用于对数据传输有较高要求的场景。通过使用TCP协议,Redis能够保证数据的可靠性和稳定性。
- 异步非阻塞IO: Redis使用异步非阻塞IO模型进行网络通信。在传统的同步阻塞IO模型中,每个连接都需要一个线程来处理,当连接数量较多时,会导致系统资源消耗过大。而采用异步非阻塞IO模型,可以利用少量的线程处理大量的连接,提高系统的并发处理能力和资源利用率。
- 事件驱动模型: Redis基于事件驱动模型进行网络通信。通过监听和处理事件,当有数据到达时立即进行处理,而不是等待数据准备好后才进行处理。这样可以最大程度地减少系统的等待时间,提高了系统的响应速度和吞吐量。
IO多路复用机制:
IO多路复用是一种在单个线程中管理多个输入/输出通道的技术,它允许一个线程同时监听多个输入流(例如网络套接字、文件描述符),并在有数据可读或可写时进行相应的处理,而不需要为每个通道创建一个独立的线程
epoll:
epoll是Linux特有的IO多路复用机制,它使用一个内核事件表来管理和监听多个IO事件的就绪状态。应用程序需要将需要监听的文件描述符添加到内核事件表中,然后调用epoll_wait函数进行监听。当有文件描述符就绪时,epoll_wait函数会返回,并告知哪些文件描述符已经准备好进行读取或写入操作。与select和poll不同的是,epoll使用回调函数来处理就绪的IO事件,而不需要应用程序遍历事件列表。
四、Redis可以用来做什么
缓存、排行榜系统、计数器应用、社交网络、session存储、消息队列系统
五、Redis常用命令
1、GET 命令:
语法:GET key
用于获取指定key的值,如果key不存在则返回nil(在Redis中,nil表示键不存在或键值为空)
2、SET 命令
语法:SET key value [EX seconds] [PX milliseconds] [NX|XX]
保存键值对到Redis中,key为键,value为值(string类型),EX和PX用于设置键的过期时间,EX是秒级,PX是毫秒级,不设置时默认过期时间是-1,NX表示只有在键不存在时才设置值,XX表示只有在键存在时才设置值,他们都是可选参数
3、KEYS 命令
语法:KEYS pattern
pattern表示匹配规则,*表示匹配零个或多个字符,?匹配一个字符,[]匹配括号内的任意字符,类似于正则表达式
4、EXISTS 命令
语法:EXISTS key [key ...]
检查给定的键是否存在,可一次性检查多个key
5、DEL 命令
语法:DEL key [key ...]
一次性删除一个或多个key
6、EXPIRE 命令
语法:EXPIRE key seconds
设置过期时间,秒级
7、TTL 命令
语法:TTL key
查看指定key的剩余过期时间
8、TYPE 命令
语法:TYPE key
返回指定键对应的数据类型
六、Redis的键过期机制和内存淘汰策略
键过期机制
- 设置ttl:通过使用expire命令,为指定的键设置生存时间,到期自动删除
- 过期检查:Redis通过定时任务来检查键是否已经过期,在每次访问键时,都会先检查键是否已经过期,如果过期立即删除,此外,Redis还会在后台周期的扫描过期键,并删除已过期的键
- 惰性删除:为了提高性能,Redis采用了惰性删除的策略,当某个键过期后,不会立即删除,而是等到下次访问该键时才会删除。这样避免频繁地进行删除操作,提高了性能
内存淘汰策略
- LRU(Least Recently Used): 最近最少使用策略,Redis会根据键的最近访问时间来选择要删除的键,即删除最近最少被访问的键。
- LFU(Least Frequently Used): 最不经常使用策略,Redis会根据键被访问的频率来选择要删除的键,即删除访问频率最低的键。
- TTL(Time To Live): 过期时间策略,Redis会优先删除已经过期的键。
- Random(随机删除): 随机选择要删除的键。
基于优先队列和定时器的方式(Redis并未采用):
1、创建优先级队列:将所有设置了过期时间的键加入到一个优先队列中,优先级规则是过期时间越早的键优先级越高,即越早过期的键越靠前
2、启动定时器线程:启动一个单独的定时器线程,该线程负责检查队首元素的过期时间,并执行响应的任务
3、定时器线程主循环:
定时器线程在一个循环中执行以下操作:
(1)检查队首元素过期时间:查看队首元素是否过期
(2)执行任务:如果队首元素过期,执行删除操作
七、Redis采用单线程模型的优势
- 短平快的命令处理: Redis 的核心业务逻辑是基于内存的快速数据存储和处理,大多数命令操作都非常简单且耗时短暂。例如,GET、SET、INCR 等命令通常只涉及简单的内存读写操作,不会消耗过多的 CPU 资源。
- 非阻塞 I/O 操作: Redis 使用了非阻塞的 I/O 模型,通过事件驱动的方式处理网络请求。这意味着当 Redis 在等待网络 I/O 时,主线程可以继续处理其他请求,而不会因为等待而阻塞,从而最大程度地利用了 CPU 资源。
- 单线程的简单性: 单线程模型相对于多线程或多进程模型来说,实现起来更加简单,减少了线程间的同步和通信的复杂性。这使得 Redis 的代码更易于维护和调试。
- 避免了多线程的竞态条件和锁等问题: 在多线程环境下,需要考虑线程安全性和并发控制,例如竞态条件、死锁、资源争用等问题,而单线程模型可以避免这些问题的出现。
八、Redis中常见数据结构和内部编码
1、String
内部编码
- int:这种编码方式用于存储长整型数据。在内存中,长整型数据占用8个字节的空间。当存储的值可以表示为长整型时,Redis会使用这种编码方式。
- embstr:这是一种优化的编码方式,用于存储长度小于等于39个字节的字符串。在内存中,该编码方式只会消耗刚好所需的内存空间,避免了额外的内存开销。当存储的字符串较短时,Redis会选择这种编码方式。
- raw:当存储的字符串长度超过39个字节时,Redis会使用这种编码方式。它可以处理长度大于39个字节的字符串,但会占用更多的内存空间。
使用场景:缓存功能,计数功能,共享session,手机验证码,分布式锁,消息队列,分布式ID生成器,数据统计
2、Hash
内部编码
- ziplist(压缩列表): 当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)且所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 使用 ziplist 作为哈希的内部实现。Ziplist 使用更加紧凑的结构实现多个元素的连续存储,从而在节省内存方面优于 hashtable。
- hashtable(哈希表): 当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现。在这种情况下,ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。因此,当哈希元素较多或者元素值较大时,Redis会选择使用 hashtable。
使用场景:映射关系表示用户信息
3、List
内部编码
quciklist优点:
- 灵活性:可以根据列表的大小动态地调整节点的大小,以最大限度地减少内存消耗
- 性能:quicklist在处理大型列表时具有较低的内存消耗和较高的性能,而在处理小型列表时也能保持较高的效率
- 节点分割:quicklist将列表分割成多个节点,每个节点都是一个压缩列表,这样可以降低在执行插入和删除操作时的复杂度,并提高整体的性能
- 压缩列表优化:quicklist使用压缩列表作为节点的存储方式,这种紧凑的数据结构可以有效地节省内存空间,并提高数据访问的速度
使用场景:消息队列,微博Timeline
4、Set
内部编码
- intset(整数集合):当集合中的元素都是整数,并且元素的个数少于 Redis 配置参数 set-max-intset-entries(默认为 512 个)时,Redis 会选择 intset 作为集合的内部实现。intset 是一种紧凑且高效的数据结构,可以有效地节省内存空间。它以有序的方式存储整数元素,并通过压缩和编码来减少内存占用。
- hashtable(哈希表):如果集合中的元素不满足 intset 的条件,即包含非整数元素或元素个数超过了 set-max-intset-entries 的限制,Redis 就会使用 hashtable 作为集合的内部实现。hashtable 提供了更灵活的存储方式,适用于各种类型的元素以及大规模的集合数据。它采用哈希表的方式存储元素,通过哈希算法来实现快速的元素查找和插入,但相比于 intset,它会占用更多的内存空间。
使用场景:存储标签和用户画像;求交集,如共同好友,共同兴趣标签;统计UV
五、Zset
内部编码
- ziplist(压缩列表):在ziplist内部实现中,元素个数小于配置的 zset-max-ziplist-entries(默认 128 个)且每个元素的值都小于配置的 zset-max-ziplist-value(默认 64 字节)时,Redis会选择使用ziplist作为有序集合的内部实现。Ziplist采用紧凑的数据结构,通过将多个小的元素合并在一起以节省内存空间。它是一种节省内存的方法,但在大型集合上的性能可能会受到影响。
- skiplist(跳表):当ziplist的条件不满足时,即元素个数超过了配置值或者元素的值超过了配置大小,有序集合会使用skiplist作为内部实现。Skiplist是一种随机化的数据结构,用于在有序集合中维护元素的顺序,并支持快速的插入、删除和查找操作。它通过多层链表结构实现,每层链表都是元素的子集,从而加快了查找速度。
跳跃表skiplist:
是一种基于有序列表的数据结构,通过添加多层索引来加速查找操作,它是一种随机化数据结构,可以在平均情况下实现较快的搜索、插入和删除操作。跳跃表在Redis中被广泛应用于实现有序集合和其他数据结构的底层实现
结构特点:
- 多层索引:跳跃表通过维护多层索引来加速查找操作,每一层索引都是原始链表的一个子集,最底层索引包含所有元素,而上层索引 则包含部分元素,每个元素在不同层级的索引中出现的概率是随机的
- 升维结构:每个节点包含多个指针,指向同一层中的下一个节点,以及可能指向其他层级中的节点,这种升维结构使得跳跃表的查找操作可以跳过多个元素,从而实现快速查找
- 平衡性:跳跃表的高度是对数级别的,并且每一层的节点数量都尽量保持平衡,使得在平均情况下查找操作的时间复杂度为O(logN),其中N为跳跃表中的元素数量
操作:
- 查找:跳跃表的查找操作类似于二分查找,从顶层索引开始,逐层向下查找,直到找到目标元素或者到达原始链表的底层
- 插入:插入操作首先需要执行查找操作,待找到插入位置的前一个节点,然后在相应的层级上插入新节点,并更新相应的指针
- 删除:删除操作也需要执行查找操作,找到待删除节点中前一个节点,然后在相应的层级上删除该节点,并更新相应的指针
优点:
- 快速查找: 跳跃表的平均查找时间复杂度为 O(logN),比较适用于有序集合等需要频繁查找的场景。
- 简单高效: 跳跃表的实现相对简单,插入和删除操作的时间复杂度也是 O(logN),在实际应用中表现良好。
- 支持范围查询: 跳跃表可以方便地支持范围查询操作,例如查找某个范围内的元素或者统计某个范围内的元素数量。
缺点:
- 空间复杂度高:跳跃表的多层索引会占用较多的额外空间,对于存储空间较为敏感的场景可能不太合适。
- 不支持动态扩容:跳跃表通常需要预先确定最大层数,因此不支持动态扩容,需要根据实际情况预先分配足够的空间
使用场景:排行榜系统