};
struct attribute ((packed)) sdshdr16 {
uint16_t len; /* used /
uint16_t alloc; / excluding the header and null terminator /
unsigned char flags; / 3 lsb of type, 5 unused bits /
char buf[];
};
struct attribute ((packed)) sdshdr32 {
uint32_t len; / used /
uint32_t alloc; / excluding the header and null terminator /
unsigned char flags; / 3 lsb of type, 5 unused bits /
char buf[];
};
struct attribute ((packed)) sdshdr64 {
uint64_t len; / used /
uint64_t alloc; / excluding the header and null terminator /
unsigned char flags; / 3 lsb of type, 5 unused bits */
char buf[];
};
你会发现同样一组结构 Redis 使用泛型定义了好多次,为什么不直接使用 int 类型呢?
因为当字符串比较短的时候,len 和 alloc 可以使用 byte 和 short 来表示,Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。
①、SDS 与 C 字符串的区别
为什么不考虑直接使用 C 语言的字符串呢?因为 C 语言这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。我们知道,C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 '\0'
。(下图就展示了 C 语言中值为 “Redis” 的一 个字符数组)
这样简单的数据结构可能会造成以下一些问题:
- 获取字符串长度为 O(N) 级别的操作 → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组;
- 不能很好的杜绝 缓冲区溢出/内存泄漏 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题;
- C 字符串 只能保存文本数据 → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 ‘\0’ 可能会被判定为提前结束的字符串而识别不了;
我们以追加字符串的操作举例,Redis 源码如下:
/* Append the specified binary-safe string pointed by ‘t’ of ‘len’ bytes to the
- end of the specified sds string ‘s’.
- After the call, the passed sds string is no longer valid and all the
- references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
// 获取原字符串的长度
size_t curlen = sdslen(s);
// 按需调整空间,如果容量不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL; // 内存不足
memcpy(s+curlen, t, len); // 追加目标字符串到字节数组中
sdssetlen(s, curlen+len); // 设置追加后的长度
s[curlen+len] = ‘\0’; // 让字符串以 \0 结尾,便于调试打印
return s;
}
- 注:Redis 规定了字符串的长度不得超过 512 MB。
②、对字符串的基本操作
安装好 Redis,我们可以使用 redis-cli
来对 Redis 进行命令行的操作,当然 Redis 官方也提供了在线的调试器,你也可以在里面敲入命令进行操作:http://try.redis.io/#run
③、设置和获取键值对
SET key value
OK
GET key
“value”
正如你看到的,我们通常使用 SET
和 GET
来设置和获取字符串值。
值可以是任何种类的字符串(包括二进制数据),例如你可以在一个键下保存一张 .jpeg 图片,只需要注意不要超过 512 MB 的最大限度就好了。
当 key 存在时, SET
命令会覆盖掉你上一次设置的值:
SET key newValue
OK
GET key
“newValue”
另外你还可以使用 EXISTS
和 DEL
关键字来查询是否存在和删除键值对:
EXISTS key
(integer) 1
DEL key
(integer) 1
GET key
(nil)
④、批量设置键值对
SET key1 value1
OK
SET key2 value2
OK
MGET key1 key2 key3 # 返回一个列表
- “value1”
- “value2”
- (nil)
MSET key1 value1 key2 value2
MGET key1 key2
- “value1”
- “value2”
⑤、过期和 SET 命令扩展
可以对 key 设置过期时间,到时间会被自动删除,这个功能常用来控制缓存的失效时间。(过期可以是 任意数据结构)
SET key value1
GET key
“value1”
EXPIRE name 5 # 5s 后过期
… # 等待 5s
GET key
(nil)
等价于 SET
+ EXPIRE
的 SETEX
命令:
SETEX key 5 value1
… # 等待 5s 后获取
GET key
(nil)
SETNX key value1 # 如果 key 不存在则 SET 成功
(integer) 1
SETNX key value1 # 如果 key 存在则 SET 失败
(integer) 0
GET key
“value” # 没有改变
⑥、计数
如果 value 是一个整数,还可以对它使用 INCR
命令进行 原子性 的自增操作,这意味着及时多个客户对同一个 key 进行操作,也决不会导致竞争的情况:
SET counter 100
INCR counter
(integer) 101
INCRBY counter 50
(integer) 151
⑦、返回原值的 GETSET 命令
对字符串,还有一个GETSET比较让人觉得有意思,它的功能跟它名字一样:为 key 设置一个值并返回原值:
SET key value
GETSET key value1
“value”
这可以对于某一些需要隔一段时间就统计的 key 很方便的设置和查看,例如:系统每当由用户进入的时候你就是用 INCR
命令操作一个 key,当需要统计时候你就把这个 key 使用 GETSET
命令重新赋值为0,这样就达到了统计的目的。
2)列表list
Redis 的列表相当于 Java 语言中的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。
我们可以从源码的 adlist.h/listNode
来看到对其的定义:
/*
Node, List, and Iterator are the only data structures used currently. */
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
typedef struct listIter {
listNode *next;
int direction;
} listIter;
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
可以看到,多个 listNode 可以通过 prev
和 next
指针组成双向链表:
虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 adlist.h/list
结构来持有链表的话,操作起来会更加方便:
①、链表的基本操作
LPUSH
和RPUSH
分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素;LRANGE
命令可以从 list 中取出一定范围的元素;LINDEX
命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的get(int index)
操作;
示范:
rpush mylist A
(integer) 1
rpush mylist B
(integer) 2
lpush mylist first
(integer) 3
lrange mylist 0 -1 # -1 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有
- “first”
- “A”
- “B”
②、list 实现队列
队列是先进先出的数据结构,常用于消息排队和异步逻辑处理,它会确保元素的访问顺序:
RPUSH books python java golang
(integer) 3
LPOP books
“python”
LPOP books
“java”
LPOP books
“golang”
LPOP books
(nil)
③、list 实现栈
栈是先进后出的数据结构,跟队列正好相反:
RPUSH books python java golang
RPOP books
“golang”
RPOP books
“java”
RPOP books
“python”
RPOP books
(nil)
3)字典 hash
Redis 中的字典相当于 Java 中的HashMap,内部实现也差不多类似,都是通过 "数组 + 链表"的链地址法来解决部分哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。源码定义如dict.h/dictht
定义:
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值,总是等于size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void privdata;
// 内部有两个dictht结构
dictht ht[2];
long rehashidx; / rehashing not in progress if rehashidx == -1 /
unsigned long iterators; / number of iterators currently running */
} dict;
table
属性是一个数组,数组中的每个元素都是一个指向 dict.h/dictEntry
结构的指针,而每个dictEntry
结构保存着一个键值对:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
可以从上面的源码中看到,实际上字典结构的内部包含两个 hashtable,通常情况下只有一个hashtable 是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行 渐进式搬迁 (下面说原因)。
①、渐进式 rehash
大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个O(n) 级别的操作,作为单线程的Redis很难承受这样耗时的过程,所以 Redis使用渐进式 rehash小步搬迁:
渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,如上图所示,查询时会同时查询两个hash结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。
②、扩缩容的条件
正常情况下,当 hash 表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2 倍。不过如果 Redis 正在做 bgsave(持久化命令)
,为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做bgsave
。
③、字典的基本操作
hash 也有缺点,hash 结构的存储消耗要高于单个字符串,所以到底该使用 hash 还是字符串,需要根据实际情况再三权衡:
HSET books java “think in java” # 命令行的字符串如果包含空格则需要使用引号包裹
(integer) 1
HSET books python “python cookbook”
(integer) 1
HGETALL books # key 和 value 间隔出现
- “java”
- “think in java”
- “python”
- “python cookbook”
HGET books java
“think in java”
HSET books java “head first java”
(integer) 0 # 因为是更新操作,所以返回 0
HMSET books java “effetive java” python “learning python” # 批量操作 OK
4)集合 set
Redis 的集合相当于 Java 语言中的 HashSet,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。
①、集合 set 的基本使用
由于该结构比较简单,我们直接来看看是如何使用的:
SADD books java
(integer) 1
SADD books java # 重复
(integer) 0
SADD books python golang
(integer) 2
SMEMBERS books # 注意顺序,set 是无序的
- “java”
- “python”
- “golang”
SISMEMBER books java # 查询某个 value 是否存在,相当于 contains
(integer) 1
SCARD books # 获取长度
(integer) 3
SPOP books # 弹出一个
“java”
5)有序列表 zset
这可能使 Redis 最具特色的一个数据结构了,它类似于 Java 中 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。
它的内部实现用的是一种叫做 「跳跃表」 的数据结构,由于比较复杂,所以在这里简单提一下原理就好了:
想象你是一家创业公司的老板,刚开始只有几个人,大家都平起平坐。后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人又是员工又有组长的身份。
再后来,公司规模进一步扩大,公司需要再进入一个层级:部门。于是每个部门又会从组长中推举一位选出部长。
跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。最终形成了一个金字塔的结构。
想一下你目前所在的地理位置:亚洲 > 中国 > 某省 > 某市 > …,就是这样一个结构!
①、有序列表 zset 基础操作
ZADD books 9.0 “think in java”
ZADD books 8.9 “java concurrency”
ZADD books 8.6 “java cookbook”
ZRANGE books 0 -1 # 按 score 排序列出,参数区间为排名范围
- “java cookbook”
- “java concurrency”
- “think in java”
ZREVRANGE books 0 -1 # 按 score 逆序列出,参数区间为排名范围
- “think in java”
- “java concurrency”
- “java cookbook”
ZCARD books # 相当于 count()
(integer) 3
ZSCORE books “java concurrency” # 获取指定 value 的 score
“8.9000000000000004” # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题
ZRANK books “java concurrency” # 排名
(integer) 1
ZRANGEBYSCORE books 0 8.91 # 根据分值区间遍历 zset
- “java cookbook”
- “java concurrency”
ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
- “java cookbook”
- “8.5999999999999996”
- “java concurrency”
- “8.9000000000000004”
ZREM books “java concurrency” # 删除 value
(integer) 1
ZRANGE books 0 -1
- “java cookbook”
- “think in java”
二、跳跃表
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
最后
由于文案过于长,在此就不一一介绍了,这份Java后端架构进阶笔记内容包括:Java集合,JVM、Java并发、微服务、SpringNetty与 RPC 、网络、日志 、Zookeeper 、Kafka 、RabbitMQ 、Hbase 、MongoDB、Cassandra 、Java基础、负载均衡、数据库、一致性算法、Java算法、数据结构、分布式缓存等等知识详解。
本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。
a后端架构进阶笔记内容包括:Java集合,JVM、Java并发、微服务、SpringNetty与 RPC 、网络、日志 、Zookeeper 、Kafka 、RabbitMQ 、Hbase 、MongoDB、Cassandra 、Java基础、负载均衡、数据库、一致性算法、Java算法、数据结构、分布式缓存**等等知识详解。
[外链图片转存中…(img-7uwwEuUL-1711645902621)]
本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。
[外链图片转存中…(img-Igsj9c8k-1711645902621)]
[外链图片转存中…(img-Ubt99Giw-1711645902622)]