-
概念:
- Redis是一个分部式非关系性数据库,它是一种分布式缓存中间件,基于内存的,以key-value的形式存储,能够提供海量数据的的存储访问,便于我们进行水平拓展。
-
技术选型:
- Ehcache
- 优点
- 基于java开发
- 基于JVM内存
- 简单、轻巧、方便
- 缺点
- 不支持集群和分布式缓存。
- 优点
- Memcache
- 优点:
- 简单的key-value存储
- 内存使用率较高
- 多核多线程处理
- 缺点:
- 无法进行容灾,无法进行持久化策略
- 优点:
- Redis
- 优点:
- 丰富的数据结构
- 支持RDB和AOF的持久化策略
- 主从同步和故障转移
- 内存数据库
- 缺点:
- 单线程,大数据量效率较低
- 优点:
- Ehcache
-
redis客户端常用命令
- 关闭redis
- redis-cli -a "password" shutdown
- ./redis stop (注意需要修改/etc/init.d/redis配置文件, stop修改$CLIEXEC -a "imooc" -p $REDISPORT shutdown)
- 进入redis里面
- redis-cli -a “password”
- redis-cli
- auth "password"
- 查看redis是否存活
- redis-cli -a "password" ping
- 关闭redis
-
Redis高性能原理:
- 1:完全基于内存实现
- 基于磁盘读取
- 基于内存读取
- 内存直接由 CPU 控制,也就是 CPU 内部集成的内存控制器,
- 基于磁盘读取
- 2:
- redis hash字典
- Redis 整体就是一个 哈希表来保存所有的键值对。哈希表,本质就是一个数组,每个元素被叫做哈希桶,不管什么数据类型,每个桶里面的 entry 保存着实际具体值的指针。而哈希表的时间复杂度是 O(1),只需要计算每个键的哈希值,便知道对应的哈希桶位置
- 图解hash表
- 图解hash表
- Redis哈希冲突
- Redis 通过链式哈希解决冲突:也就是同一个 桶里面的元素使用链表保存。但是当链表过长就会导致查找性能变差可能,所以 Redis 为了追求快,使用了两个全局哈希表。用于 rehash 操作,增加现有的哈希桶数量,减少哈希冲突。
- Redis 渐进式reHash
- redis默认使用两个全局hash表,采用渐进式的rehash方式,如果一次性把哈希表 1 中的数据都迁移完,因为Redis的工作线程只有1个,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。
- 过程:
- 1:为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 2:在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
- 3:在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1
- 4:随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
- 5:如果有增删改查操作时,如果index大于rehashindex,访问ht[0],否则访问ht[1]。
- 图解:渐进式rehash
- Redis 整体就是一个 哈希表来保存所有的键值对。哈希表,本质就是一个数组,每个元素被叫做哈希桶,不管什么数据类型,每个桶里面的 entry 保存着实际具体值的指针。而哈希表的时间复杂度是 O(1),只需要计算每个键的哈希值,便知道对应的哈希桶位置
- 不同的数据类型,底层采用不同数据结构存储
- sds简单动态字符串
- redis没有使用C的字符串,而是构建了一种简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作redis的默认字符串表示。
- SDS数据结构
- struct SDS {
- int len; // buf数组中已使用字节数,并未算上’\0’字符
- int free; // buf数组中未使用字节数
- char buf[]; // 用于保存字符串
- };
- SDS数据结构
- SDS 与 C 字符串区别
- O(1) 时间复杂度获取字符串长度
- C 语言中字符串的获取 「MageByte」的长度,要从头开始遍历,直到 「\0」为止
- SDS 中 len 保存这字符串的长度,O(1) 时间复杂度。
- 空间预分配
- SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间
- 分配规则如下:如果对 SDS 修改后,len 的长度小于 1M,那么程序将分配和 len 相同长度的未使用空间。举个例子,如果 len=10,重新分配后,buf 的实际长度会变为 10(已使用空间)+10(额外空间)+1(空字符)=21。如果对 SDS 修改后 len 长度大于 1M,那么程序将分配 1M 的未使用空间。
- SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间
- 惰性空间释放
- 当对 SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配
- 二进制安全
- 在 Redis 中不仅可以存储 String 类型的数据,也可能存储一些二进制数据。
- 二进制数据并不是规则的字符串格式,其中会包含一些特殊的字符如 '\0',在 C 中遇到 '\0' 则表示字符串的结束,但在 SDS 中,标志字符串结束的是 len 属性。
- O(1) 时间复杂度获取字符串长度
- redis没有使用C的字符串,而是构建了一种简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作redis的默认字符串表示。
- LinkedList双端链表
- 数据结构
- Redis 的链表实现的特性可以总结如下:
- 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的复杂度都是 O(1)
- 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点
- 带表头指针和表尾指针:通过 list 结构的 head 指针和 tail 指针,程序获取链表的表头节点和表尾节点的复杂度为 O(1)
- 带链表长度计数器:程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数,程序获取链表中节点数量的复杂度为 O(1)
- 多态:链表节点使用 void* 指针来保存节点值,并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
- 数据结构
- zipList 压缩列表
- 压缩列表是 List 、hash、 sorted Set 三种数据类型底层实现之一
- ziplist 是由一系列特殊编码的连续内存块组成的顺序型的数据结构,ziplist 中可以包含多个 entry 节点,每个节点可以存放整数或者字符串。ziplist 在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表占用字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
- 数据结构:
- struct ziplist<T> {
- int32 zlbytes; // 整个压缩列表占用字节数
- int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
- int16 zllength; // 元素个数
- T[] entries; // 元素内容列表,挨个挨个紧凑存储
- int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
- }
- 如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N)
- quicklist
- 后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
- skipList 跳跃表
- sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。
- 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
- 跳跃表支持平均 O(logN)、最坏 O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
- 跳表在链表的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:
- 整数数组(intset)
- 当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现
- 数据结构:
- contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。length 属性记录了整数集合包含的元素数量,也即是 contents 数组的长度。
- 合理的数据编码
- Redis 使用对象(redisObject)来表示数据库中的键值,当我们在 Redis 中创建一个键值对时,至少创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。
- 图解
- 其中 type 字段记录了对象的类型,包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
- 图解
- 不同的数据类型的编码转化:
- String:存储数字的话,采用 int 类型的编码,如果是非数字的话,采用 raw 编码;
- List:List 对象的编码可以是 ziplist 或 linkedlist,字符串长度 < 64 字节且元素个数 < 512 使用 ziplist 编码,否则转化为 linkedlist 编码;
- 注意:这两个条件是可以修改的,在 redis.conf 中
- list-max-ziplist-entries 512
- list-max-ziplist-value 64
- 注意:这两个条件是可以修改的,在 redis.conf 中
- Hash:Hash 对象的编码可以是 ziplist 或 hashtable
- 当 Hash 对象同时满足以下两个条件时,Hash 对象采用 ziplist 编码 否则就是 hashtable 编码
- Hash 对象保存的所有键值对的键和值的字符串长度均小于 64 字节。
- Hash 对象保存的键值对数量小于 512 个
- 当 Hash 对象同时满足以下两个条件时,Hash 对象采用 ziplist 编码 否则就是 hashtable 编码
- Set:Set 对象的编码可以是 intset 或 hashtable,intset 编码的对象使用整数集合作为底层实现,把所有元素都保存在一个整数集合里面。保存元素为整数且元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码;
- Zset:Zset 对象的编码可以是 ziplist 或 zkiplist,当采用 ziplist 编码存储时,每个集合元素使用两个紧挨在一起的压缩列表来存储。
- Ziplist 压缩列表第一个节点存储元素的成员,第二个节点存储元素的分值,并且按分值大小从小到大有序排列。
- 当 Zset 对象同时满足一下两个条件时,采用 ziplist 编码,否则使用:zplist 就会转化为 zkiplist 编码
- Zset 保存的元素个数小于 128。
- Zset 元素的成员长度都小于 64 字节。
- 这两个条件是可以修改的,在 redis.conf 中
- zset-max-ziplist-entries 128
- zset-max-ziplist-value 64
- Redis 使用对象(redisObject)来表示数据库中的键值,当我们在 Redis 中创建一个键值对时,至少创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。
- 3:单线程模型
- Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。
- 多线程的优势:
- 使用多线程,通常可以增加系统吞吐量,充分利用 CPU 资源
- 多线程弊端:
- 多线程并行修改共享数据的时候,为了保证数据正确,需要加锁机制就会带来额外的性能开销,面临的共享资源的并发访问控制问题
- 单线程的优势
- 不会因为线程创建导致的性能消耗
- 避免上下文切换引起的 CPU 消耗,没有多线程切换的开销
- 避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁问题
- 多线程的优势:
- Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。
- 4:I/O多路复用
- Redis 采用 I/O 多路复用技术,并发处理连接。采用了 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 IO 上浪费一点时间
- 基本 IO 模型
- 和客户端建立建立 accept;
- 从 socket 种读取请求 recv;
- 解析客户端发送的请求 parse;
- 执行 get 指令;
- 响应客户端数据,也就是 向 socket 写回数据。
- IO 多路复用
- 多路指的是多个 socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll 是最新的也是目前最好的多路复用技术。它的基本原理是,内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符
- Redis线程模型 :
- 当客户端运行时,它将生成具有不同事件类型的套接字。在服务器端,I / O 多路复用程序(I / O 多路复用模块)会将消息放入队列(也就是 下图的 I/O 多路复用程序的 socket 队列),然后通过文件事件分派器将其转发到不同的事件处理器。
- Redis 单线程情况下,内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器。所以 Redis 一直在处理事件,提升 Redis 的响应性能。
- 5:总结:
- 1:纯内存操作,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在 IO 上,所以读取速度快
- 2:整个 Redis 就是一个全局 哈希表,他的时间复杂度是 O(1),而且为了防止哈希冲突导致链表过长,Redis 会执行 rehash 操作,扩充 哈希桶数量,减少哈希冲突。并且防止一次性 重新映射数据过大导致线程阻塞,采用 渐进式 rehash。巧妙的将一次性拷贝分摊到多次请求过程后总,避免阻塞。
- 3:Redis 使用的是非阻塞 IO:IO 多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,Redis 采用自己实现的事件分离器,效率比较高。
- 4:采用单线程模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
- 5:Redis 全程使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
- 6:根据实际存储的数据类型选择不同编码
- 1:完全基于内存实现
Redis基础和高性能原理分析
最新推荐文章于 2021-08-25 11:04:03 发布