"Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker." —— Redis是一个开放源代码(BSD许可)的内存中数据结构存储,用做数据库,缓存和消息代理。 (摘自官网)html
Redis 是一个开源,高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的 Web 应用程序。Redis 也被做者戏称为 数据结构服务器 ,这意味着使用者能够经过一些命令,基于带有 TCP 套接字的简单 服务器-客户端 协议来访问一组 可变数据结构 。(在 Redis 中都采用键值对的方式,只不过对应的数据结构不同罢了)java
如下是 Redis 的一些优势:python
这一步比较简单,你能够在网上搜到许多满意的教程,这里就再也不赘述。git
给一个菜鸟教程的安装教程用做参考:https://www.runoob.com/redis/redis-install.html程序员
当你安装完成以后,你能够先执行 redis-server
让 Redis 启动起来,而后运行命令 redis-benchmark -n 100000 -q
来检测本地同时执行 10 万个请求时的性能:github
固然不一样电脑之间因为各方面的缘由会存在性能差距,这个测试您能够权当是一种 「乐趣」 就好。golang
Redis 有 5 种基础数据结构,它们分别是:string(字符串)、list(列表)、hash(字典)、set(集合) 和 zset(有序集合)。这 5 种是 Redis 相关知识中最基础、最重要的部分,下面咱们结合源码以及一些实践来给你们分别讲解一下。web
Redis 中的字符串是一种 动态字符串,这意味着使用者能够修改,它的底层实现有点相似于 Java 中的 ArrayList,有一个字符数组,从源码的 sds.h/sdshdr 文件 中能够看到 Redis 底层对于字符串的定义 SDS,即 Simple Dynamic String 结构:redis
/* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; 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 为了对内存作极致的优化,不一样长度的字符串使用不一样的结构体来表示。
为何不考虑直接使用 C 语言的字符串呢?由于 C 语言这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。咱们知道,C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,而且字符数组最后一个元素老是 '\0'
。(下图就展现了 C 语言中值为 "Redis" 的一个字符数组)
这样简单的数据结构可能会形成如下一些问题:
'\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,咱们可使用 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 # 返回一个列表 1) "value1" 2) "value2" 3) (nil) > MSET key1 value1 key2 value2 > MGET key1 key2 1) "value1" 2) "value2"
能够对 key 设置过时时间,到时间会被自动删除,这个功能经常使用来控制缓存的失效时间。(过时能够是任意数据结构)
> SET key value1 > GET key "value1" > EXPIRE name 5 # 5s 后过时 ... # 等待 5s > GET key (nil)
等价于 SET
+ EXPIRE
的 SETNX
命令:
> SETNX key 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 count (interger) 101 > INCRBY counter 50 (integer) 151
对字符串,还有一个 GETSET
比较让人以为有意思,它的功能跟它名字同样:为 key 设置一个值并返回原值:
> SET key value > GETSET key value1 "value"
这能够对于某一些须要隔一段时间就统计的 key 很方便的设置和查看,例如:系统每当由用户进入的时候你就是用 INCR
命令操做一个 key,当须要统计时候你就把这个 key 使用 GETSET
命令从新赋值为 0,这样就达到了统计的目的。
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 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即全部 1) "first" 2) "A" 3) "B"
队列是先进先出的数据结构,经常使用于消息排队和异步逻辑处理,它会确保元素的访问顺序:
> RPUSH books python java golang (integer) 3 > LPOP books "python" > LPOP books "java" > LPOP books "golang" > LPOP books (nil)
栈是先进后出的数据结构,跟队列正好相反:
> RPUSH books python java golang > RPOP books "golang" > RPOP books "java" > RPOP books "python" > RPOP books (nil)
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,而后进行 渐进式搬迁 (下面说缘由)。
大字典的扩容是比较耗时间的,须要从新申请新的数组,而后将旧字典全部链表中的元素从新挂接到新的数组下面,这是一个 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 间隔出现 1) "java" 2) "think in java" 3) "python" 4) "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
Redis 的集合至关于 Java 语言中的 HashSet,它内部的键值对是无序、惟一的。它的内部实现至关于一个特殊的字典,字典中全部的 value 都是一个值 NULL。
因为该结构比较简单,咱们直接来看看是如何使用的:
> SADD books java (integer) 1 > SADD books java # 重复 (integer) 0 > SADD books python golang (integer) 2 > SMEMBERS books # 注意顺序,set 是无序的 1) "java" 2) "python" 3) "golang" > SISMEMBER books java # 查询某个 value 是否存在,至关于 contains (integer) 1 > SCARD books # 获取长度 (integer) 3 > SPOP books # 弹出一个 "java"
这可能使 Redis 最具特点的一个数据结构了,它相似于 Java 中 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的惟一性,另外一方面它能够为每一个 value 赋予一个 score 值,用来表明排序的权重。
它的内部实现用的是一种叫作 「跳跃表」 的数据结构,因为比较复杂,因此在这里简单提一下原理就行了:
想象你是一家创业公司的老板,刚开始只有几我的,你们都分庭抗礼。后来随着公司的发展,人数愈来愈多,团队沟通成本逐渐增长,渐渐地引入了组长制,对团队进行划分,因而有一些人又是员工又有组长的身份。
再后来,公司规模进一步扩大,公司须要再进入一个层级:部门。因而每一个部门又会从组长中推举一位选出部长。
跳跃表就相似于这样的机制,最下面一层全部的元素都会串起来,都是员工,而后每隔几个元素就会挑选出一个表明,再把这几个表明使用另一级指针串起来。而后再在这些表明里面挑出二级表明,再串起来。最终造成了一个金字塔的结构。
想一下你目前所在的地理位置:亚洲 > 中国 > 某省 > 某市 > ....,就是这样一个结构!
> ZADD books 9.0 "think in java" > ZADD books 8.9 "java concurrency" > ZADD books 8.6 "java cookbook" > ZRANGE books 0 -1 # 按 score 排序列出,参数区间为排名范围 1) "java cookbook" 2) "java concurrency" 3) "think in java" > ZREVRANGE books 0 -1 # 按 score 逆序列出,参数区间为排名范围 1) "think in java" 2) "java concurrency" 3) "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 1) "java cookbook" 2) "java concurrency" > ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 表明 infinite,无穷大的意思。 1) "java cookbook" 2) "8.5999999999999996" 3) "java concurrency" 4) "8.9000000000000004" > ZREM books "java concurrency" # 删除 value (integer) 1 > ZRANGE books 0 -1 1) "java cookbook" 2) "think in java"
- 本文已收录至个人 Github 程序员成长系列 【More Than Java】,学习,不止 Code,欢迎 star:https://github.com/wmyskxz/MoreThanJava
- 我的公众号 :wmyskxz,坚持原创输出,下方扫码关注,2020,与您共同成长!
很是感谢各位人才能 看到这里,若是以为本篇文章写得不错,以为 「我没有三颗心脏」有点东西 的话,求点赞,求关注,求分享,求留言!
创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!