程序地带

Redis设计与实现(一)——数据结构与对象


Redis中的数据结构
简单动态字符串链表字典跳跃表整数集合压缩列表对象
1.简单动态字符串

Redis底层是用C语言实现的,所以,在很多数据结构上可以直接使用C语言中已经存在的数据结构和库函数。


Redis的字符串数据结构并没有直接使用C语言的字符数组,而是用了一个结构体,名为简单动态字符串(SDS),这是Redis默认实现字符串的数据结构。


SDS的定义

SDS由一个C语言结构体定义,里面包含当前已使用长度、未使用长度和字节数组。


struct sdshdr
{
// 记录buf中已经使用的字节数量
int len;
// 记录buf中未使用的字节数量
int free;
// 字节数据,用于保存字符串
char buf[];
};

结构图示如下: 在这里插入图片描述 因为Redis使用C语言实现,同时为了可以使用C中的很多库函数,所以,需要遵照C数组的惯例,在数组末尾用一个’/0’表示数组结尾。因此,给字符串分配空间的时候都会多一个字节。


SDS的功能
保存字符串值。用作缓冲区,包括AOF模块的缓冲区,客户端状态中的输入缓冲区。
SDS的特点
1.常数时间获取字符串的长度

在C语言环境下,如果需要获取一个数组的长度,就需要遍历这个数组,并进行计数,直到遍历到末尾的结尾字符,就能计算出长度。这样做的问题就是每次想要获取字符串的长度时间复杂度都是O(n)的,对于需要经常获取字符串长度的应用,就会造成很大的开销。


而SDS中,利用空间换时间,通过维护结构体中的一个整型,保存当前结构体所表示的字符串的长度,就可以在O(1)的时间获取长度。这样做的代价就是需要增加一个整型的内存,同时也需要在变更字符串的时候,需要对这个整型进行维护,也会带来开销。


2.防止缓冲区溢出

如果仅仅对字符数组进行操作,给数组添加了大于其分配了的内存的数据,就会导师溢出,将数据覆盖到了后面的数组上,造成错误。


而SDS中,对添加数据的操作都会进行一个扩容检查,如果原有的空间无法装下添加的数据,就会对数组进行一次扩容操作。


3.减少字符串修改带来的内存重分配次数

首先,对于内存的重分配是一个比较耗时的工作。


如果每次添加数据和删减数据,都对数据存储数组进行一个重分配修改,那么造成的开销就很大。SDS采用以下两种策略进行一个优化:


空间预分配 对于数组的扩容操作,每次都会扩得多一点,而不是刚刚好。如果数组大小小于1MB,每次增加一倍;如果数组大小大于30MB,那么每次扩容增加1MB。这样下一次数据增加的时候,直接添加就完了,不需要立马又要扩容。惰性空间释放 对于数据的数据删除,不需要马上减少数组的空间,而是动一动表示使用数据长度和空闲数据长度的两个值就行了。但也需要避免空间浪费,SDS提供API实现真正的空间释放。
4.二进制安全

Redis中的字符串图片、音频等很多数据,其数据基本单元是字节而不是字符,所以,不能让字符数组中的’/0’表示数组的结尾。


SDS的中的数组不只是可以用来保存字符,而是通过字节形式保存一系列二进制数据,通过len和free,就可以避免使用’/0’结尾。


5.兼容C字符串函数库

对于很多字符串的操作,SDS的字符数组和C的普通字符数组是一样的,也都有’/0’,那么就可以重用<string.h>中的部分库函数,避免了代码的重复。


总结

比起C字符数组,SDS有以下优点:


常数复杂度获取字符串的长度;杜绝缓冲区溢出;减少修改字符串长度时需要内存重新分配的次数;二进制安全;兼容部分C字符串函数。
2.链表

C语言中没有实现链表的数据结构,所以Redis构建了自己的链表实现。这里的链表就是很普通的双向链表。


链表的定义

链表由链表节点和链表构成。其中链表节点的定义如下:


typedef struct listNode
{
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
}

从这里可以看出,redis中的链表是双向链表。


链表的结构定义如下:


typedef struct list
{
// 头节点
listNode *head;
// 尾节点
listNode *tail;
// 链表节点数
unsigned long len;
// 节点的复制函数
void *(*dup) (void *ptr);
// 节点的释放函数
void *(*free) (void * ptr);
// 节点的比较函数
int (*match) (void *ptr, void *key);
}

其中,dump函数用于复制表系欸但所保存的值;free用于释放链表节点所保存的值;match函数用于对比链表节点所保存的值与另一个输入值是否相等。


链表结构示例如下图所示: 在这里插入图片描述


链表的功能

链表在Redis中使用很广泛,提供了如下特性:


高效的节点重排;顺序性的节点访问方式;通过增删节点来灵活地调整链表的长度。

在Redis中,使用链表数据结构的地方主要有:


链表键发布与订阅慢查询监视器保存多个客户端的状态信息构建客户端输出缓冲区
链表的特点
双向:每个节点都有前置指针和后置指针,这样获取某个节点的前置和后置节点的时间复杂度都是O(1)的。无环:表头节点前置指针和表尾的后置指针指向空,这样对链表的访问以null为终点。具有表头节点和表尾节点:这样可以分别从表头或者表尾开始对标进行遍历。存有表节点的个数:这样获取表长度的时间复杂度是O(1)的。多态:表节点中值的指针是用过void*来定义的,并且还可以通过dup、free、match三个属性给节点值设置类型的特定函数,所以,可以用链表保存不同类型的数据。
3.字典

字典就是映射,在python中也称为字典,是用于保存键值对的数据结构,键唯一,值可以重复。


字典的定义

Redis字典使用哈希表作为底层的实现,其实大致的原理和JDK1.8之前的HashMap差不多,都采用基于链表的拉链法解决哈希冲突。


字典中的结构定义主要分为哈希表、哈希表节点以及字典的实现。


1.哈希表
typedef struct dictht
{
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表存储节点的数量
unsigned long used;
} dictht;

其中table就是一个存储哈希链表头节点的数组。


2.哈希表节点
typedef struct dictEntry
{
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;

这里值的定义很奇怪,是一个C语言的联合体,什么是联合体呢,就是里面的变量是共享一块空间的,彼此之间可能会相互覆盖对方,所以,同时只能使用其中的一个类型。这里为了实现值的多态,所以,采用这种联合体的方式,就能实现不同类型数据的存储。


next用于形成链表,用于解决哈希冲突的问题。


3.字典
typedef struct dict
{
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;

其中type属性和privdata是用于不同类的键值对,实现多态字典而设置的。


typedef struct dictType
{
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;

dictType结构体中主要是对键值进行操作的函数指针,可以依据不同的键值类型,传入对于的操作函数,实现了多态。


回到上面的字典,ht包含了两个哈希表,在一般的情况下,数据存储在ht[0]中,ht[1]主要用于哈希表的rehash。rehashidx记录当前rehash的进度,如果没有在rehash,该值就是-1。


字典的结构示例如下图所示。 在这里插入图片描述


字典的功能
Redis的数据库底层就是使用字典来实现的,对数据库的增删改查都是在对字典的操作。还可以用来实现哈希键,当一个哈希键包含的键值对比较多,或者键值对的元素较长,就会使用字典作为哈希键的底层实现。还有其他的功能。
字典的算法
1.解决哈希冲突

Redis采用的链地址法来解决。键冲突的节点就用过链表,一个个连在一起,等待遍历查询。


和JDK1.7中的HashMap一样,Redis采用的是头插法,总是将新的节点添加到链表头的位置,这样可以提高速度,但是会导致在多线程下的死循环。在JDK1.8中采用了尾插法。


2.rehash

当节点变多的时候,会造成大量的哈系冲突,这样哈希表的效率就下降了,所以,需要对数组进行扩容,减少哈希冲突。同时节点变少的时候,需要减少数组的大小,节约空间。


rehash就是用来在扩容或者收缩的时候,解决原有哈希值映射到新表上的哈希值的问题。


扩容的大小每次是2的n次幂。这些原理上和Java的实现几乎一致。将当前表中的节点重新计算哈希值,然后放到新表h1中的指定位置上。最后将h0指向h1,再将h1指向空。rehash的具体流程如下:


为ht[1]分配空间:如果执行扩容操作,则ht[1]的大小为第一个大于等于ht[0].used*2的2**n(2的n次方);如果执行收缩操作,则ht[1]的大小为第一个大于等于ht[0].used的2 **n。将ht[0]上的元素rehash到ht[1]上,即重新计算哈希值,将元素保存到ht[1]中。当元素全部迁移完成后,释放ht[0],将ht[1]设置为ht[0],并为ht[1]新创建一个空白的哈希表。

扩容的条件是大于其设定的负载因子。


服务器当前未执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。服务器目前正在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。

不同于Java的HashMap,Redis的字典具有缩容功能,用于节省内存。 参考Java HashMap


3.渐进式rehash

在将h0的数据复制到h1时,可能不是一次性将数据复制过去的,而是分多次实现,这样做的原因是当节点数量太多的时候,防止复制节点造成的系统停顿。


在复制的过程中,通过rehashidx来保存复制的进度,每次完成一部分的时候,就更新rehashidx的值,直到完成所有的复制,就可以将rehash置为-1。如果是查找修改操作,会先在h0中进行查找,没有找到再去h1;如果是添加操作,则一律添加到h1中。


总结
字典被广泛用于Redis的各种功能,其中包括数据库和哈希键。Redis字典采用哈希表作为底层实现,每个字典有两个哈希表,一个平时用,另一个仅仅在rehash的时候使用。通过拉链法解决哈希冲突的问题。扩容或者缩容不是一次性完成,而是分批次,渐进式完成的。
4.跳跃表

跳跃表是一种有序的数据结构,通过在每个节点中位置多个指向其他节点的指针,实现快速访问。


跳跃表的定义

Redis跳跃表由跳跃表和跳跃表节点两个结构实现。


1.跳跃表节点
typedef struct zskiplistNode
{
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
层:一个节点有很多层,每一个层中包含指向其他节点的前进指针和该指针指向的节点的跨度。根据幂次定律随机生成一个介于1和32的值作为层数。节点中存储的数值越大,层数就会越少。前进指针:指向下面的节点。跨度:用于记录两个节点之间的距离。如果跨度为0,那么表示该层指向的是null。后退指针:用于指向前一个节点,只能退一个位置。分值:用于给节点进行排序,分值可以相等,如果分值相等,那么就根据成员对象的字典序进行排序。成员对象:它指向一个字符串对象,而字符串对象中保存着一个SDS值。
2.跳跃表
typedef struct zskiplist
{
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;

跳跃表结构示例如下图所示: 在这里插入图片描述


跳跃表的功能

Redis中只有两个地方用到跳跃表。


实现有序集合键。集群节点中作为内部数据结构。
跳跃表的特点
支持平均O(logN),最坏O(N)时间复杂度的节点查找。可以通过顺序性操作来批量处理节点。
5.整数集合

整数是用来保存整数值的数据结构,并且集合内不会出现重复元素,且有序排列。


整数集合的定义
typedef struct intset
{
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组,一个字节位单位存储
uint8_t contents[];
} intset;

在这里插入图片描述


其中,encoding表示数组中数据的真正类型,可以是16位、32位或者64位的整型。


整数集合的功能

是集合键的底层实现之一,当一个集合只包含整数值的元素,而且元素数量不多,Redis就会采用整数集合作为集合键的底层实现。


整数集合的升级

当新添加的元素类型超出了现有的类型长度,就需要对集合进行升级,也就是提升存储数组的类型。再添加元素进去。


升级整数集合并添加新元素的步骤如下:


根据新元素类型,扩容底层的数组空间大小,并为新元素分配空间;将底层所有的元素都转化为新元素的相同的类型,并放在正确的位置上,还要维持元素的有序性;添加新元素到数组中。

对于原有元素类型的升级和移动,其实就是再数组中根据前面元素偏移的位置进行一个再偏移。其中可能需要空出新元素的位置。


升级的好处:


提升灵活性,可以随意添加不同类型的整型到集合中。节约内存,尽可能用最小的类型来保存元素。

不支持降级的操作。


6.压缩列表
压缩列表的功能

是列表键和哈希键的底层实现之一,当一个列表键只包含少量列表项,并且每个列表项中只有整数或者长度比较短的字符串,那么Redis就会采用压缩列表来做列表键的底层实现。


压缩列表的构成

一个压缩列表中包含多个节点,每个节点中保存一个字节数组或者整型。


列表中包含的属性:


zlbytes:记录整个列表占用的内存数量,用于对列表的内存再分配。zltail:记录列表尾节点到起始地址之间有多少个字节,通过这个偏移量,就不需要遍历就能获得尾节点的地址。zllen:记录列表包含的节点数量。entryX:包含的各个节点。zlend:用于标记列表的末端,0xFF。

节点中包含:


previous_entry_length:记录前一个节点的长度。encoding:表示当前节点存储数据的类型,有不同长度的字节数组和不同整型等。content:保存系欸但的值,值得类型和长度由encoding决定。
对象

Redis并没有直接使用上面讲的数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统主要包含五类对象:


字符串列表哈希集合有序集合

使用对象的好处:


可以根据不同的使用场景,为对象设置不同的数据结构,从而优化不同场景下的效率。Redis对象系统实现了基于引用计数法的垃圾回收机制,同时可以让多个数据库键共享一个对象来节约内存。对象具有访问时间记录,这样可以实现类似LRU的内存换页机制。
对象的类型和编码

Redis中对象都由一个redisObject结构表示:


typedef struct redisObject
{
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层数据结构的指针
void *ptr;
}

其中,类型就是上面提到的五类对象。对象的ptr指向了底层的数据结构,而这些数据结构就是由编码属性来决定的。实现同一对象类型可以有很多不同的数据结构,一次适应实际的场景。


字符串对象

字符串对象的编码有三种:


int:如果字符串保存的整数,那么就用这种编码,里面用了一个long来保存。raw:如果保存的是一个字符串值,而且字符串的长度大于32,那么就用会这种编码,里面就是用了SDS。embstr:如果保存的字符串值长度小于32,就会用这种编码。

在对字符串操作过程中,如果发生了修改使其不满足原先编码的条件,那就就会进行编码的转换。


String 类型的注意事项 数据操作不成功的反馈与数据正常操作之间的差异


表示运行结果是否成功  (integer)0–>false 失败  (integer)1–>true 成功表示运行结果值  (integer)3–>3  (integer)1–>1数据未获取到 (nil)等同于null数据最大存储量 512MB数值计算最大范围(java中的long的最大值)
列表对象

列表对象的编码有两种:


ziplist:利用压缩列表作为底层实现。列表中保存的所有的字符串长度小于53字节,列表中保存的元素数量小于612。linkedlist:利用双端链表作为底层实现。如果压缩列表的条件不满足,才会使用链表。

list类型数据操作注意事项


list 中保存的数据都是string类型的,数据总容量是有限的,最多2^32-1个元素(4294967295)。list具有索引的概念,但是操作数据时候通常以队列的形式进行入队出队操作,或以栈的形式进入栈出栈的操作。获取全部数据操作结束索引设置为-1。list 可以对数据进行分页操作,通过第1页的信息来自list,第2页及更多的信息通过数据库的形式加载。
哈希对象

哈希对象的编码两种:


ziplist:使用压缩列表作为底层实现。保存键值对的时候,分别生成键和值的两个节点,键节点在前,值节点在后,一起从表尾压缩列表。如果所有键和值的字符串长度小于64字节,同时,键值对数量小于512个,就会采用这种编码。hashtable:使用字典作为底层实现,每个键值对都使用字典键值来保存,每个键和值都是字符串对象。

hash类型数据操作的注意事项


hash类型下的value只能存储字符串,不允许存储其他类型数据,不存在嵌套现象。如果数据未获取到,对应的值为(nil)。每个hash可以存储232-1个键值对。hash类型十分贴近对象的数据存储形式,并且可以灵活添加删除对象属性。但hash设计初中不是为了存储大量对象而设计的,切记不可滥用,更不可以将hash作为对象列表使用。hgetall操作可以获取全部属性,如果内部fiekd过多,遍历整体数据效率就会很低,有可能成为数据访问瓶颈。
集合对象

集合对象的编码有两种:


intset:使用整数集合作为底层实现,所有元素都保存在整数集合中。保存的元素都是整数,同时保存元素的个数小于等于512个,就会采用这种编码。hashtable:使用字典作为底层实现。

Set类型数据操作的注意事项


set类型不允许数据重复,如果添加的数据在set中已经存在,将只保留一份。set虽然与hash的存储结构相同,但是无法启用hash中存储值的空间。
有序集合对象

有序结合对象编码有两种:


ziplist:采用压缩列表作为底层的实现,每个集合元素用两个挨在一起的压缩列表节点来保存,第一个保存元素的成员,第二个保存元素的分值。压缩列表内的元素从小到达进行排序。skiplist:使用zset结构作为底层实现,一个zset结构包含一个字典和一个跳跃表。 在这里插入图片描述

为什么需要同时使用字典和跳跃表的方式来实现? 如果只用字典,那么无法保证集合有序,范围型操作就需要排序,这样时间复杂度就是nlongn的;如果只使用跳跃表,那么查询的时候就要遍历查询跳表,时间复杂度是longn的。所以,同时使用这两种数据结构,就能将查询和范围操作的时间复杂度都为O1,空间换时间。


sorted_set 类型数据操作的注意事项


score 保存的数据存储空间是64位。score保存的数据也可以是一个双精度的double值,基于双精度浮点数的特征,可能会丢失精度,使用时侯要慎重。sorted_set底层存储还是基于set结构的,因此数据不能重复,如果重复添加相同的数据,score值将被反复覆盖,保留最后一次修改的结果。
Bitmap

Redis允许使用二进制数据的Key(binary keys) 和二进制数据的Value(binary values)。Bitmap就是二进制数据的value。Redis的 setbit(key, offset, value)操作对指定的key的value的指定偏移(offset)的位置1或0,时间复杂度是O(1)。


Bitmap实际上就是字符串,但是可以对字符串的位进行操作。通过 bitcount可以很快速的统计,比传统的关系型数据库效率高很多。


在应对缓存穿透问题的时候,可以利用bitmap做一个布隆过滤器,通过bitmap来过滤无效的请求,拒绝访问不存在信息。


常用命令:


getbit key offset 用于获取Redis中指定key对应的值,中对应offset的bit。setbit key offset value 用于修改指定key对应的值,中对应offset的bit。bitcount key [start end] 用于统计字符串被设置为1的bit数。bitop and/or/xor/not destkey key [key …] 用于对多个key求逻辑与/逻辑或/逻辑异或/逻辑非。
HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。 所谓基数,就是不重复的数字。


在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。


但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。


HyperLogLog 的基本命令:


PFADD key element [element …] 添加指定元素到 HyperLogLog 中。PFCOUNT key [key …] 返回给定 HyperLogLog 的基数估算值。PFMERGE destkey sourcekey [sourcekey …] 将多个 HyperLogLog 合并为一个 HyperLogLog

HyperLogLog数据操作的注意事项


用于进行基数统计,不是集合,不保存数据,只是记录数据的数量而不是数据本身。核心是基数估算算法,最终数值存在一定误差。误差范围:基数估计结果具有0.81标准错误的近似值。占用空间小,每个HyperLogLog key只有12k。pfadd不是一次性分配12k,而是随着基数的增加而增加。pfmerg合并后占用的空间就是12k,不论合并之后有多少数据。
GEO

Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。


Redis GEO 操作方法有:


GEOADD key longitude latitude member [longitude latitude member …] 添加地理位置的坐标。GEOPOS key member [member …] 获取地理位置的坐标。GEODIST key member1 member2 [m|km|ft|mi] 计算两个位置之间的距离。GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] 返回一个或多个位置对象的 geohash 值。
Stream

Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。


简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。


而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。


消息队列相关命令:


XADD - 添加消息到末尾XTRIM - 对流进行修剪,限制长度XDEL - 删除消息XLEN - 获取流包含的元素数量,即消息长度XRANGE - 获取消息列表,会自动过滤已经删除的消息XREVRANGE - 反向获取消息列表,ID 从大到小XREAD - 以阻塞或非阻塞方式获取消息列表

消费者组相关命令:


XGROUP CREATE - 创建消费者组XREADGROUP GROUP - 读取消费者组中的消息XACK - 将消息标记为"已处理"XGROUP SETID - 为消费者组设置新的最后递送消息IDXGROUP DELCONSUMER - 删除消费者XGROUP DESTROY - 删除消费者组XPENDING - 显示待处理消息的相关信息XCLAIM - 转移消息的归属权XINFO - 查看流和消费者组的相关信息;XINFO GROUPS - 打印消费者组的信息;XINFO STREAM - 打印流信息
内存回收

Redis采用引用计数法进行垃圾回收,在每个对象中添加了引用计数,如果计数值为0,说明对象不会再被使用,那么对象就会被释放。


怎么解决循环引用的问题?


对象共享

Redis会共享值为0到9999的字符串对象。有点像JVM的常量池。


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_36263268/article/details/111246184

随机推荐

SpringMVC中视图解析器源码分析

SpringMVC中视图解析器源码分析该篇博客只分析视图解析器源码,如果想看DispatcherServlet的具体流程,请看SpringMVC中DispatcherSer...

柳吉晴 阅读(409)

Filter+Listener(过滤器和拦截器)

Filter+ListenerFilter本意为”过滤“的含义,是JavaWeb的三大组件之一,三大组件为:Servlet、Filter、Listen...

小星亮晶晶 阅读(960)

MySQL基础学习笔记

MySQL基础开启cmd窗口Ctrl+R开启服务窗口services.msc启动命令netstartmysql关闭命令netstopmysql登陆mysql-uroot-proot/mysql...

无欲则刚*<(¦Q[▓▓ 呼呼呼。。。 阅读(658)

NDK入门——第一个JNI程序

NDK入门——第一个JNI程序

1.什么是NDK、JNI?NDK全称是NativeDevelopmentKit,NDK提供了一系列的工具,帮助开发者快速开发C(或C+&...

鑫宇_ 阅读(133)

python常用的内建函数

常用的内建函数在Python中,内建函数是被自动加载的,可以随时调用这些函数,不需要定义,极大地简化了编程。1.eval()函数eval()函数...

是张可爱吖 阅读(837)

数据结构与算法——堆排序

原文链接:https://jiang-hao.com/articles/2020/algorithms-algorithms-heap-sort.html文章目录算法介绍算法步骤算法实现...

Heriam 阅读(705)

JAVA工具类(自定义)

1、发送HttpClient请求工具类importorg.apache.http.Consts;importorg.apache.http.HttpEntity;importorg.apache.ht...

蒙眼脚趾敲代码 阅读(224)

锁屏界面_系统小技巧:Windows 10锁屏界面玩通透

锁屏界面_系统小技巧:Windows 10锁屏界面玩通透

此前,本刊文章中不少讲述关于聚焦锁屏图片的内容,但聚焦锁屏只是锁屏内容的一部分。关于Windows10锁屏界面的使用方法和技巧,其实还有许多内容需要我们去好好...

Rayzmoon 阅读(377)