《Redis设计与实现》学习笔记
Redis
本文会有一些Redis和Java容器对象的对比,一个是分布式数据库,一个是JVM内部数据容器,应用场景不同,仅仅是为了加深对Redis”数据库“的认识,加深对Redis使用场景的认识。
1.什么是Redis
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)
redis中文官方网站
2. Redis数据类型有哪些?
提供给用户的数据类型 有String,List,Hash,Set,ZSet
Redis内部数据结构有:SDS(简单动态字符串),链表,字典,跳跃表,整数集合,压缩列表
那么数据类型和内部数据结构的对应关系是什么样子的呢?
2.1 Redis数据类型与内部数据结构的关系是什么样的?
Redis没有直接使用基础数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这些对象是供用户直接使用的。Redis数据库中的每个键值对的键和值都是一个对象,五种类型的对象至少都支持两种或者两种以上的基础数据结构,不同的数据结构可以在不同的使用场景上优化对象的使用效率。
String是唯一一个会被其它四种对象嵌套使用的对象。List、Hash、Set、ZSet之所以会虚线指向SDS也是嵌套使用String的原因。
3.什么是SDS?
3.1 SDS数据结构是什么样的?
Redis 3.0及以前:
Redis没有直接使用C语言字符串,而是自己创建了一个名为简单动态字符串(Simple Dynamic String)的抽象类型来表示字符串。
SDS的基本结构:
struct sdshdr{int len;int free;char buf[];
}
Redis 3.0及以后: Redis 的SDS分为了5种数据结构,分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。以为sdsshdr8举例:
struct sdshdr8 {// 当前字符串长度,最长支持2^8-1=255个字符(不包括\0)uint8_t len;// 记录了当前字节数组总共分配的内存大小(不包括\0)uint8_t alloc; // 3 bit 表示类型,5 bit空闲unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
3.2 以下操作之后共生成了几个SDS对象呢?
redis> rpush fruits "apple","banana","cherry"
答案:4个,第一个SDS保存“fruits”,第二"apple",这里其实并不是List里面直接放的SDS,而是String对象,后面会讲到。
3.3 SDS内存如何扩容?
SDS内存扩容流程如下,SDS采用空间预分配的方式,即SDS的修改程序不仅仅会分配修改所需的空间,还会为SDS分配额外未使用的空间,1M以内2倍快速扩容,1M以上线性扩容。
3.4 SDS为什么要采用空间预分配的方式?
减少修改字符串时带来的内存重分配次数
C字符串在每次增加和缩短的时候总是进行一次内存重分配,在Java里面,String类型的字符串也是不可变变量,每次修改都是重新申请空间,拷贝的过程。Redis作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串长度都需要执行一次内存重分配的话,频繁修改的话影响效率。
3.5 SDS空间预分配和StringBuilder的空间预分配有什么异同点?
SDS之于C语言字符串,StringBuilder之于String。StringBuilder又是如何进行空间扩容的呢?
StringBuilder类(只包含部分信息)如下:
public final class StringBuilder extends AbstractStringBuilderimplements java.io.Serializable, CharSequence{//存放字符串 char[] value;// value 数组中真正存在值的元素个数int count;// 最大的字符串长度 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;// 当有初始化字符串时,初始长度为16 public StringBuilder() {super(16);}// 当有初始化字符串时,初始长度为 字符串长度 + 16public StringBuilder(String str) {super(str.length() + 16);append(str);}
}
这里你是不是也有个疑问? 有count记录当前实际元素的个数,为什么没有元素记录数组的大小?不管是java的StringBuilder 或者是Redis的SDS,还是其他语言的可变字符串,大概都要解决:
1.如何O(1)时间复杂度获取字符数组长度?
2.如何杜绝内存溢出(快速获得空闲空间个数)?
3.如何减少内存重分配提高读写效率?
如果StringBuilder也有属性记录容量大小,那么跟Redis SDS就非常相似了。这是因为Java语言里面数组的length是非常特殊的,既不是数组的属性也不是数组的方法,length有对应的Java字节码arraylength指令,获取长度是常数级别的,所以Java StringBuilder里面没有记录数组容量的大小。
StringBuilder扩容的流程如下:一直如下分配,直到扩容到MAX_ARRAY_SIZE。
O(1)时间复杂度获取字符数组长度:两者都可以O(1)时间复杂度获得字符数组长度。
杜绝内存溢出:两者在字符串改变之前均会检查剩余空间是否满足插入。
减少内存重分配提高读写效率:
1.StringBuilder初始化时是len(str)+16,是有空间预留的,SDS初始化时是len(str)*2
2.扩容时SDS和StringBuilder均有两倍增长来预分配空间,SDS1M以内,2倍于插入之后的数组长度,StringBuilder 2倍于当前数组的长度,在两倍当前数组和(当前数组的长度+ 申请空间长度)之间取最大值。
这里我们可以看到,不管SDS和StringBuilder都包含2倍增长阶段,为什么StringBuilder和SDS要实现2倍增长呢?于是就有了下面的问题:
3.6 为什么StringBuilder和SDS要实现2倍增长呢?
1.空间预分配可以减少内存申请和内存拷贝的时间消耗,将n次扩容,减少为最多n次扩容。
2. 倍数增长 VS 固定长度增长
倍数增长:
假设字符数组中包含a个元素,倍增因子是m,经历x次扩容 可以生成一个n个元素的数组。
那么经历x次扩容,总共发生的插入次数如下:
参照等比数列:N^0+N^1+N^2+N^3........N^n=[N^(n+1)-1]/(N-1)
由上可知:插入的次数时间复杂度为O(n)
固定长度增长:
假设字符数组中包含a个元素,每次固定增长m,经历x次扩容 可以生成一个n个元素的数组。
那么经历x次扩容,总共发生的插入次数如下:
由上可知:插入的次数时间复杂度为O(n^2)
可见,低倍数增长既能保证时间复杂度是个常数,又能有效的利用的空间。
这里还有个遗留的问题?为什么是2倍
2倍 可以实现快速位运算获得新长度。
3.7 为什么SDS在1M以上就停止2倍增长?StringBuilder为什么不这样?
Redis不适合存储1M以上的数据,1M以上的数据建议使用其它存储介质。大部分数据是在1M以下的,停止2倍增长牺牲了大数据的写入性能,但是有利于节约内存,也非常适合Redis的使用场景。StringBuilder JVM对象,大小依据业务,并没有1M的限制。
SDS有空间预分配的策略,释放空间又采用什么策略呢?
3.7 SDS采用什么策略释放空间?
Redis采用惰性空间释放用于优化SDS字符串缩短操作,即:字符缩短的时候不立即释放空间,仅仅使用属性记录当前空间使用情况。那么SDS是不是就永远不会释放这部分空间?不会,SDS提供了相应的API,可以在需要的时候,真正释放SDS未使用的空间。
3.8 SDS字符串较使用C字符串比有哪些优点?
1.常数复杂度获取字符串长度
2.杜绝缓冲区溢出
3.减少修改字符串长度时所需的内存重分配次数
4.二进制安全
5.兼容部分C字符串函数,Redis可以使用C的部分库函数
3.9 String对象底层全部都使用SDS吗?
字符串对象的编码可以是int,raw,或者是embstr
如果一个字符串对象保存的是整数值,并且这个整数值可以用long表示,字符串对象是直接保存数字的,如果字符串对象保存的是一个字符串值,才会使用SDS。
紧接着们了解一下什么是String对象,在了解String对象前,我们需要先看一下什么是对象?
4. 什么是对象?
Redis通过5种不同类型的对象,Redis可以在执行命令之前,根据对象类型来判断一个对象是否可以执行给定的命令。使用对象的另外一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。Redis里面每一个对象都是由一个redisObject结构标识,如下:
typedef struct redisObject{// type:类型,eg:REDIS_STRING,REDIS_LIST,REDIS_HASH,REDIS_SET,REDIS_ZSET
unsigned type:4;// encoding:对象所使用的编码,也即这个对象使用什么数据结构作为对象的底层实现,
// Redis中每种type至少支持两种编码方式
unsigned encoding:4;//lru 表示程序最近访问该对象的时间
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */// 表示引用计数,每当被引用,引用计数加一,反之减一。当引用计数为零会被回收
int refcount;// ptr指向对象的底层实现数据结构,这些数据结构是ecoding属性决定的
void *ptr;}
从这里我们也可以看到,redisObject除了type,还有ecoding,一种类型的对象不直接绑定一种实现,而是根据ecoding动态设置实现,而且支持从一种编码转换为另外一种编码。
eg:列表对象在包含元素比较少的时候,Redis使用压缩列表作为底层实现,因为压缩列表比双端列表更节约内存,并且在元素较少的情况下,在内存中连续保存的压缩列表要比双端列表可以更快的加载到缓存中。随着列表的元素越来越多,使用压缩列表的优势逐渐消失,对象会将底层实现从压缩列表转向功能更强、也适合大量元素的双端链表。
上面我们讲了SDS和对象,下面对字符串对象做详细的介绍
5. 什么是字符串对象?
type : REDIS_STRING
encoding : int, raw, embstr
ptr指向SDS 或者直接保存一个long类型的数。
5.1 什么时候encoding是int?
字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,ptr会直接保存数字。
5.2 什么时候encoding是raw?
1.Redis3.0及以前字符串对象保存的是字符串值,并且字符串值的长度大于39字节。
2.对int编码,embstr编码的字符串执行了Append命令,encoding会从int、embstr转化为raw。
5.3 什么时候encoding是embstr?
embstr 专门用于保存短字符串(字符串长度小于等于39字节)的一种优化编码方式,与raw的区别是:raw通过一次内存分配创建redisObject一次内存分配创建sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr。
embstr保存字符串的好处:
1.内存分配次数从raw的两次降低为一次
2.embstr释放字符串对象只需要调用一次内存释放函数,而释放raw编码需要两次
3.embstr都在一块连续的内存中,提高查询效率
5.4 为什么要有39字节的限制?
本文主要学习了黄健宏的《Redis设计与实现》,39字节是Redis3.0的限制。具体怎么来的可以看下图:
Linux的一个CPU缓存行默认是64字节,64字节-16字节(redisObject)-8字节(sdshdr) - 1字节(字符串‘\0’字符)=39字节。
Redis3.0以上,sdshdr有所改变,但是计算方式同上,以为sdsshdr8为例:
我们也可以看到相对于Redis3.0的数据结构,新版本在节约内存上是更细致了。
6.什么是链表?
6.1 链表的使用场景?
链表可以提供高效的插入,顺序性的节点访问,并且可以通过增删节点来灵活的调整链表的长度。
1.列表键的底层实现之一,当列表键包含的元素数量比较多,或者列表包含的元素都是比较长的字符串时,Redis就会使用列表键的底层实现。
2.除了列表,发布预定于,慢查询,监视器等功能也用到了链表
3.Redis服务器本身还是要使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓存区。
6.2 链表节点的数据结构?
Node节点:
typedef struct listNode{
// 前置节点
listNode *prev;
// 后置节点
listNode *next;
// 节点值
void *value;}
list:
typedef struct list{
// 表头节点
listNode *head;// 表尾节点
listNode *tail;// 链表包含的节点数量
unsigned long len;// 节点值复制函数
void *(*dup)(void *ptr);// 节点值释放函数
void (*free)(void *ptr);// 节点值对比函数
int (*match)(void *ptr,void *key);}
Redis链表具有双端、无环、带表头指针和表尾指针(获取表头和表尾的时间复杂度是O(1))、带链表长度计数器(获得节点数量的时间复杂度是O(1))、多态(可以保存不同类型的值)。
7.什么是压缩列表?
7.1 压缩列表的使用场景?
1. 列表键:当列表键只包含少量的列表项,并且每个列表项是小整数值或者是短字符串,Redis就会使用压缩列表来实现列表键。
2.哈希键:当哈希键只包含少量的键值对,并且每个键值对的键和值是小整数值或者短字符串,Redis就会使用压缩列表来实现哈希键。
7.2 压缩列表的数据结构?
压缩列表使用连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者整数值。
previous_entry_length:前一个entry节点的字节长度,此属性的长度可以是1字节或者是5字节,可以表示的长度:
1字节:前一个节点真实长度为0~254
5字节:1字节固定值(0xFE),后面4个字节表示真实长度。
previous_entry_length的设置可以快速计算前一节点的起始指针,方便快速从后往前遍历。
encoding:记录content的类型和长度。一字节、两字节或者五字节。encoding定义如下:b代表长度的二进制。
编码 | 编码长度 | content保存长度 | 备注 |
00bbbbbb | 1字节 | 长度小于等于 | |
01bbbbbb bbbbbbbb | 2字节 | 长度小于等于2^14 -1 | |
10_ _ _ _ _ _ bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb |
5字节 |
长度小于等于2^32 -1 |
从压缩列表各个组成部分可知,一个压缩列表占用的内存总数用4个字节表示 如果链表只有一个entry节点就到达上限,那么一个entry的content的长度最长为 2^32 - 4-4-2-1 -5-5字节。 |
11000000 | 1字节 | int16 | |
11010000 | 1字节 | int32 | |
11100000 | 1字节 | int64 | |
11110000 | 1字节 | 24位有符号整数 | |
11111110 | 1字节 | 8位有符号整数 | |
1111xxxx | 1字节 | 没有content属性,直接用xxxx来表示entry节点值 |
7.3 压缩列表有没有内嵌SDS对象?
从上面”压缩列表的数据结构?“分析我们可以知道,没有使用SDS,原因应该也比较好理解:
1:省内存
2:连续存储,提高存取效率
那么就引出了下面的问题:
1:列表的entry的个数(新增/删除)一般是会变的,Redis作为数据库更需要考虑列表元素数量变化的问题,压缩列表会像Java的List一样,设置初始容量,负载因子,然后1.5倍扩容吗?也就是压缩列表如何进行扩容的问题
2:修改的问题,压缩列表每一个Entry的content有没有空间预分配呢?
两个问题答案都是否定的。
7.4 压缩列表新增/删除元素,内存空间如何变化?
针对7.3的问题1,答案是Redis的压缩列表没有空间预分配,每次新增和删除都会移动其位置后元素。为什么没有预分配机制?
1.压缩列表在Redis的数据结构中,定位是存储少量的,短小数据,为了节约内存空间而设计的,大量的 或者是 大数据节点就会转化为使用 链表或者其它的数据结构作为存储结构。
2.Java的List来实现列表数据存储,既要考虑小数据量场景,也要考虑大数据量场景,因此需要动态扩容来实现。
Redis 节点插入/删除都会引起连锁更新,更新逻辑如下图所示:
新插入一个节点进行空间分配并复制时间复杂度是O(N),在最坏的情况下,引发N次连锁重分配,时间复杂度最坏是O(N^2)。 删除原理同上。
7.4 连锁更新或者删除会不会影响性能?
不会,
1. 恰好多个连续的长度介于250字节和253字节的几率非常低
2. 即使出现连锁更新只要节点数量不多,就不会造成性能影响
3. 个人理解:压缩列表本身定位有关系,压缩列表节点数量少,节点小,即使连锁更新也会很快,不涉及大数据量的拷贝。这一点在列表对象编码转换的条件中也可以看到。
8.什么是列表对象?
type : REDIS_LIST
encoding :REDIS_ENCODING_ZIPLIST / REDIS_ENCODING_LINKEDLIST
ptr指向压缩列表或者链表。
8.1 什么时候列表对象底层实现用压缩列表 ?
1.列表对象保存的所有的字符串元素的长度都小于64字节。
2.列表对象保存的元素的数量小于512个。
1 && 2
这里留一个疑问:压缩列表只有在元素长度小于64字节的情况下才使用,那压缩列表本身定义的三种字节数组长度是怎么回事? 可以参照”7.2 压缩列表的数据结构?“ encoding有一字节、两字节、五字节长度? 感觉一个字节就够了。
9.什么是字典?
字典是一种用于保存键值对的抽象数据结构。字典使用哈希表作为底层实现,一个哈希表里面可以包含多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
一般情况下,只有哈希表0是使用中的,只有rehash的过程中,才两个哈希表都会使用到。
哈希表结构定义如下:
typedef struct dictht{// Hash 表数组 dictEntry ** table;// 哈希表大小unsigned long size;// 哈希表大小掩码,用于计算索引值,总是等于size -1unsigned long sizemask;// 该hash表已有节点数量 usigned long used;
}
9.1 如何解决Hash冲突? 与Java的HashMap有哪些异同点?
Redis采用链地址法来解决冲突(解决冲突的方法基本上有链地址法,开放定址法,再哈希法等),与Java的HashMap的异同点:绿色为相同点,橙色为不同点。
为什么Redis字典没有使用红黑树来提高查询效率?
1. Redis负载因子是使用了 已经存在的节点数量/数组长度,基本上链表节点个数不会超过数组的长度。Java HashMap负载因子是使用的数组中已有节点数量/数组长度。
2. Redis的链表结构已经做了优化,红黑树插入新节点时要进行颜色切换,树扭转,更消耗空间。
Java中也有使用跳表实现的数据结构:ConcurrentSkipListMap,ConcurrentSkipListSet
《Redis设计与实现》学习笔记相关推荐
- 第二行代码学习笔记——第六章:数据储存全方案——详解持久化技术
本章要点 任何一个应用程序,总是不停的和数据打交道. 瞬时数据:指储存在内存当中,有可能因为程序关闭或其他原因导致内存被回收而丢失的数据. 数据持久化技术,为了解决关键性数据的丢失. 6.1 持久化技 ...
- 第一行代码学习笔记第二章——探究活动
知识点目录 2.1 活动是什么 2.2 活动的基本用法 2.2.1 手动创建活动 2.2.2 创建和加载布局 2.2.3 在AndroidManifest文件中注册 2.2.4 在活动中使用Toast ...
- 第一行代码学习笔记第八章——运用手机多媒体
知识点目录 8.1 将程序运行到手机上 8.2 使用通知 * 8.2.1 通知的基本使用 * 8.2.2 通知的进阶技巧 * 8.2.3 通知的高级功能 8.3 调用摄像头和相册 * 8.3.1 调用 ...
- 第一行代码学习笔记第六章——详解持久化技术
知识点目录 6.1 持久化技术简介 6.2 文件存储 * 6.2.1 将数据存储到文件中 * 6.2.2 从文件中读取数据 6.3 SharedPreferences存储 * 6.3.1 将数据存储到 ...
- 第一行代码学习笔记第三章——UI开发的点点滴滴
知识点目录 3.1 如何编写程序界面 3.2 常用控件的使用方法 * 3.2.1 TextView * 3.2.2 Button * 3.2.3 EditText * 3.2.4 ImageView ...
- 第一行代码学习笔记第十章——探究服务
知识点目录 10.1 服务是什么 10.2 Android多线程编程 * 10.2.1 线程的基本用法 * 10.2.2 在子线程中更新UI * 10.2.3 解析异步消息处理机制 * 10.2.4 ...
- 第一行代码学习笔记第七章——探究内容提供器
知识点目录 7.1 内容提供器简介 7.2 运行权限 * 7.2.1 Android权限机制详解 * 7.2.2 在程序运行时申请权限 7.3 访问其他程序中的数据 * 7.3.1 ContentRe ...
- 第一行代码学习笔记第五章——详解广播机制
知识点目录 5.1 广播机制 5.2 接收系统广播 * 5.2.1 动态注册监听网络变化 * 5.2.2 静态注册实现开机广播 5.3 发送自定义广播 * 5.3.1 发送标准广播 * 5.3.2 发 ...
- 第一行代码学习笔记第九章——使用网络技术
知识点目录 9.1 WebView的用法 9.2 使用HTTP协议访问网络 * 9.2.1 使用HttpURLConnection * 9.2.2 使用OkHttp 9.3 解析XML格式数据 * 9 ...
- 安卓教程----第一行代码学习笔记
安卓概述 系统架构 Linux内核层,还包括各种底层驱动,如相机驱动.电源驱动等 系统运行库层,包含一些c/c++的库,如浏览器内核webkit.SQLlite.3D绘图openGL.用于java运行 ...
最新文章
- fusionchart图表遮挡Ext下拉控件或日期控件解决办法(IE下有问题firefox与chrome正常)...
- IT真的很重要,还是会被边缘化?
- 剑指offer 用2个栈实现队列
- 学习 SQL 语句 - Select(7): 分组统计之 Avg()、Sum()、Max()、Min()、Count()
- NET问答: using 和 await using 有什么不同?
- java项目经验行业_行业研究以及如何炫耀您的项目
- Hive的伴奏_Position Music顶级背景音乐合集243CD
- 如何让word中清晰的图片无损导出为pdf?
- img居中以及等比缩放
- 3D建模学习对于电脑配置要求高不高?用台式机好还是笔记本电脑好?显卡内存等全方面解析,小白福音
- 使用GSM6315模块,采用http或者https协议与服务器通信笔记
- OPC UA IO模块对工业物联网的影响
- 最适合深夜失眠听的歌,听了最容易入睡的歌曲推荐
- python发红包(转载)
- Python 的dict几种遍历方式
- AJAX都有哪些优点和缺点
- opencv 图像操作,常用 OpenCV 内置函数
- 六张卡片来猜数(哈利波特之心灵感应魔法)
- WEB前端学习 (7)CSS复习六(布局-定位)
- Android App 网络接入实时监控
热门文章
- 苹果耳机airpods2需要激活?_苹果耳机三兄弟,谁才是安卓手机的绝配?
- 2018大数据培训学习路线图(详细完整版)
- OpenCV实现角点检测(cornerHarris)
- 罗克韦尔AB PLC安装Studio 5000 V35的具体步骤演示
- 米拓5.3 mysql支持off,Metinfo 5.3.17 前台SQL注入漏洞分析
- 【数论定理】卢卡斯定理
- 光猫修改上报服务器地址,怎样改光猫的ip地址!急求!!
- 【Android Dialog】Dialog
- Redis常用的命令(一)-------启动、配置等
- 织梦官方幻灯片调用以及幻灯片模糊的处理办法