Redis内存数据库
更新说明:此文第一版是2019年4月份写得,当时也对Redis没有太多的认识,写了较为基础的一些东西,如今已到年末,对Redis的认识也多了很多,特此更新2.0版本。
新增的内容有:
1、排版优化
2、新增基础数据类型的底层实现
3、新增高级数据类型
4、新增了Redis扩容
5、新增Redis混合备份的方式
6、优化Redis集群主从通信的细节
7、对一些bug进行了优化
(目前更新进度80%,2019/12/11)
第二篇原创技术博客就贡献给Redis,做缓存这块,Redis内存数据库极其擅长,记得在Redis之前还有个Memcached,但支持的数据类型较少只有一种,而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)。
2.为什么要用Redis
请看上一段,Redis是内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件
- 数据库
Redis本质是内存数据库,所以自然可以当做数据库来使用,但要注意的是内存空间是极其有限的,可不像硬盘那样浩瀚无垠,所以大多数情况下我们还是用关系型数据库+Redis缓存的方式运用Redis - 缓存
比如Mysql,可承担的并发访问量有多大呢?答案是几百左右就会扛不住了,所以我们为了支持更高的并发,会使用缓存,为数据库筑起一道护盾,让大多数请求都发生在缓存这一层。Redis是把数据存储在内存上的,访问数据速度相当快,很适合做缓存。 - 消息中间件
Redis支持发布/订阅消息,当然真正的MQ我们一般在Rabbit,Rocket,卡夫卡之间选一个,这并不是Redis的强项
3.Redis的使用方式
这一块分为两个部分,第一部分是把Redis部署在linux上,我们在linux使用Redis。
第二部分是通过SpringBoot来操作使用Redis。
3.1 Linux上使用
3.1.1 redis启动
1、备份redis.conf(此文件为Redis配置文件,非常重要):拷贝一份redis.conf到其他目录
2、修改redis.conf文件将里面的daemonize no 改成 yes,让服务在后台启动
3、启动命令:执行 redis-server /myredis/redis.conf(后面那个是配置文件的位置)
4、用客户端访问: Redis-cli
多个端口可以 Redis-cli –p 6379
5、测试验证: ping 若成功启动会返回 pong!
3.1.2 redis关闭
单实例关闭:Redis-cli shutdown
也可以进入终端后再关闭 shutdown
多实例关闭,指定端口关闭:Redis-cli -p 6379 shutdown
3.1.3 Redis–key/value
Redis作为Nosql数据库,数据都以键值对的形式存储,Value内置了5大数据类型,
在看Value操作之前,先看一看Key的操作
keys * 查询当前库的所有键
exists 判断某个键是否存在
type 查看键的类型
del 删除某个键
expire 为键值设置过期时间,单位秒。
ttl 查看还有多少秒过期,-1表示永不过期 (-2表示已过期)
dbsize 查看当前数据库的key的数量
Flushdb 清空当前库(慎用!)
Flushall 通杀全部库(删库跑路!!!忘了这个命令吧)
4 Redis五大数据类型
4.1 Redis五大数据类型–String
Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
4.1.2 关于String底层实现
Redis 的字符串叫着「SDS」,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组。所以以后有人在面试的时候问你:请聊一聊SDS。就不要傻乎乎的问人家“Redis有名为SDS的数据结构么?”
上图为容量与长度的关系,是不是简单明了。
SDS的结构:
struct SDS {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}
SDS 结构使用了范型 T,为什么不直接用 int 呢,这是因为当字符串比较短时,len 和 capacity 可以使用 byte 和 short 来表示,Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。所以别人才那么快嘛,太精细了!
注意:创建字符串时 len 和 capacity 一样长,不会多分配冗余空间,这是因为绝大多数场景下我们不会使用 append 操作来修改字符串。
4.1.3 String常用命令
get 查询对应键值
set 添加键值对
append 将给定的 追加到原值的末尾
strlen 获得值的长度
setnx 只有在 key 不存在时设置 key 的值
incr
将 key 中储存的数字值增1
只能对数字值操作,如果为空,新增值为1
decr
将 key 中储存的数字值减1
只能对数字值操作,如果为空,新增值为-1
incrby / decrby <步长>
将 key 中储存的数字值增减。自定义步长。
mset …
同时设置一个或多个 key-value对
mget …
同时获取一个或多个 value
msetnx …
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
getrange <起始位置> <结束位置>
获得值的范围,类似java中的substring
setrange <起始位置>
用 覆写 所储存的字符串值, 从<起始位置>开始。
setex <过期时间>
设置键值的同时,设置过期时间,单位秒。
getset
以新换旧,设置了新值同时获得旧值。
4.2 Redis五大数据类型–list
单键多值
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。操作的时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)
4.2.1 底层实现原理
上面也讲了底层是个双向链表,但还是太模糊了,具体一点,其实底层用的是ZipList(压缩链表)和QuickList(快速链表)。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。(感觉好像数组啊,连续的内存空间)
当数据量比较多的时候才会改成 quickList。那么什么是quickList呢?Redis 将链表和 ziplist 结合起来组成了 quickList。如下图所示:
4.2.1.1 压缩链表
-
压缩链表结构:
struct ziplist {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一个元素,然后倒着遍历。 -
Entry的数据结构:
entry 块随着容纳的元素类型不同,也会有不一样的结构。
struct entry {
int prevlen; // 前一个 entry 的字节长度
int encoding; // 元素类型编码
optional byte[] content; // 元素内容
}
encoding字段是Redis自己定义的N种编码类型,罗列出来也无意义。只需要知道可以根据编码字段来确定元素内容的类型即可。 -
增加元素:
因为 ziplist 都是紧凑存储,没有冗余空间 (对比一下 Redis 的字符串结构)。意味着插入一个新的元素就需要调用 realloc 扩展内存。取决于内存分配器算法和当前的 ziplist 内存大小,realloc 可能会重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也可能在原有的地址上进行扩展,这时就不需要进行旧内容的内存拷贝。
如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗。所以 ziplist 不适合存储大型字符串,存储的元素也不宜过多。
4.2.1.2 快速链表
先看下最基础的双向链表结构:
// 链表的节点
struct listNode {
listNode* prev;
listNode* next;
T value;
}
// 链表
struct list {
listNode *head;
listNode *tail;
long length;
}
链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率
为了解决上述问题,就引出了quicklist:
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
zipList因为是连续的内存空间,所以不需要prev 和 next 指针,空间占用大幅减少。同样,比起原来每个节点的一盘散沙,现在zipList是一坨一坨的沙砖,碎片化的问题也解决了。
- 快速链表数据结构:
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
…
}
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算