介绍一下redis的历史、SQL和NoSQL的一些区别、redis的安装、常用的五种数据类型的存储原理,其它4中数据类型只是简单做了一下了解。希望能够从中学习到一些数据结构设计的思想。关于命令的操作文中有链接,其中的命令描述非常详细,这里就不在写了。
2008年意大利的一个叫做antirez的老哥,搭建了一个可以记录网站访问情况的一个网站LLOOGG.com,最初它最多查看一万条最新的记录,它为每一个网站创建了一个列表,不同网站的访问情况对应不同的列表,这些列表存储在了Mysql中。但是当列表满了的时候,它会删除掉最早的记录,并且在用户增加的情况下,维护列表的数量也会增加,那么记录和删除的操作也会比较频繁,由于数据是存储在Mysql中,也就是磁盘中,限制了网站的性能,因此antirez就考虑将数据放在内存中,这样添加和删除的效率将会大大提高。
Redis的全称是REmote DIctionary Service,也就是远程字典服务的意思。
首先了解一下SQL和NoSQL的一些区别:
1.数据是以表格的形式存储,也就是行存储,是一个二维的结构,存储结构化的数据。
2.表格有相应的结构
3.表格之间是有关联的
4.基本上都支持SQL标准
5.支持事务的操作,事务特性ACID
1、扩容只能向上扩容,水平扩容需要使用分库分表技术
2、数据存储的格式有限制,也就是表结构固定后,修改比较麻烦
3、高并发、数据量大的情况下,基于磁盘的读写压力比较大
non-relational | not only SQL
1、存储的非结构化的数据,可以存储图片、视频、音频等数据。
2、表与表之间没有关联,扩展性强
3、没有事务特性,遵循BASE理论
4、数据不是存储在磁盘,支持海量存储,支持高并发
5、分布式,实现水平扩容和分片存储更加简单
1、丰富的数据结构
2、进程内与跨进程;单机与分布式
3、提供了丰富的功能和高级特性,比如发布订阅、持久化、内存淘汰(过期)策略等
4、客户端支持多种编程语言
5、高可用性, 主从、哨兵
1、配置文件redis.conf
2、启动参数 --require
3、config set命令动态修改
Vmware 14的CentOs 7上安装的Redis-5.0.5的版本
由于Redis是C语言开发的,在安装之前要先安装gcc编译器,使用yum命令安装
yum install gcc
第一步:下载redis
cd /usr/local/software
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
第二步:解压
tar -zxvf redis-5.0.5.tar.gz
第三步:编译安装
cd redis-5.0.5
make MALLOC=libc
cd src
make install
第四步:修改配置文件
cd redis-5.0.5
vim redis.conf
后台启动参数
daemonize no 改为 daemonize yes
主机绑定参数,如果不修改只能本机访问服务端
bind 127.0.0.1 改为0.0.0.0
rdb快照文件目录
dir ./ 改为dir /usr/local/software/redis-5.0.5
其它参数后面用到再说
服务端和客户端启动的命令太长,可以配置别名
alias redis='/usr/local/software/redis-5.0.5/src/redis-server /usr/local/software/redis-5.0.5/redis.conf'
alias rcli='/usr/local/softeware/redis-5.0.5/src/redis-cli'
关闭服务端
ps -aux | grep redis
kill -9 pid
或者在客户端中关闭
192.168.75.26> shutdown
Redis是字典结构的存储方式,也就是K-V的形式存储,K和V的最大长度限制都是512M,Redis提供了丰富的数据类型,根据这些数据类型的特点,可以被应用到不同的业务场景中。
默认是16个数据库,数据库索引值为0-15,默认使用第一个0,数据库的个数可以在配置文件中修改dababases参数的值
databases 16
切换数据库
set 1
清空当前数据库
flushdb
清空所有数据库
flushall
查看所有键
keys *
获取键的总数
dbsize
查看键是否存在
exists key_test
重命名键
rename key_test key_test_rename
查看键类型
type key_test
这里只介绍简单常用的命令,详细的命令可以参考http://redisdoc.com/index.html
存储类型:可以存储字符串、整数、浮点数。官网中字符串类型描述的是binary safe String(二进制安全字符串)原因后面解释。
存储原理:因为Redis是KV型的数据库,每个键值对都是一个dictEntry,里面存储了指向Key和Value的指针,next指向了下一个dictEntry。key是作为字符串存储的,但是并没有直接使用C的字符数组,而是使用自定义的SDS(Simple Dynamic Strings)存储,value既不是直接作为字符串存储,也不是直接存储在SDS中,而是存储在redisObject中,redisObjec中存储了指向value的指针。
下面是dictEntry的定义,源码位置在src/dict.h,
typedef struct dictEntry {
void *key; /* key的定义 */
union {
void *val;/* value的定义 */
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;/* 指向下一个键值对 */
} dictEntry;
下面是redisObject的定义,源码位置在src/server.h
常用的五种基本数据类型都是通过redisObject来存储的。
typedef struct redisObject {
unsigned type:4;/* 对象的类型 */
unsigned encoding:4;/* 具体的数据结构 */
unsigned lru:LRU_BITS; /* 24位,与内存回收有关 */
int refcount;/* 引用计数 */
void *ptr;/* 指向对象实际的数据类型 */
} robj;
各个字段的含义:
/* type字段:4位存储,可以通过type命令查看数据类型*/
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
/* encoding字段:表示该类型的物理编码方式,同一数据类型可能有不同的编码方式,可以通过object encoding命令查看编码方式*/
#define REDIS_ENCODING_RAW 0 /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
字符串的内部编码,由小内存编码->大内存编码
1、int:存储8个字节的长整型
2、embstr:代表embstr格式的SDS,存储小于44字节的字符串,3.2版本之前是39字节
3、raw:存储大于44字节的字符串
几个常见的问题:
1、embstr和raw的区别
embstr的使用只分配一次内存(因为redisObject和SDS是连续的),而raw需要分配两次内存(分别为redisObject和SDS分配空间),因此embr和raw相比,创建时少分配一次内存和删除时少释放一次内存,对象的所有数据连在一起,方便查找,embstr如果字符串的长度增加时需要重新分配内存,整个redisObject和SDS都需要重新分配,因此Redis中embstr实现为只读。
2、int和embstr转化到row的条件
当int数据不再是整数或者大小超出了2^63-1时转化为embstr,只要是修改了embstr类型的对象,无论是否达到了44字节的限制,都会转化为row,因为embstr是只读的,对其进行修改时,都会先转化为row。
3、当长度小于阈值时,会还原吗
Redis内部编码的转换只能从小内存编码向大内存编码转换,编码转换在写入数据时完成,转换过程不可逆
4、为什么Redis对底层的数据结构进行了一次封装
通过封装,可以根据对象的类型动态的选择存储结构、动态的选择可以使用的命令,实现节省空间和优化查询速度
SDS介绍
C语言中字符串是以字符数组实现,使用字符数组必须先给目标变量分配足够的空间,否 则可能会溢出;获取字符串长度时,就要遍历字符数组,时间负责度O(n);字符串长度的变化会对字符数组进行内存重分配;二进制不安全:字符串的结尾是以\0来标记的,因此不能存储图片、音频、视频、压缩文件等二进制保存的内容。所以Redis对其进行了改造。SDS不用担心内存溢出问题,如果需要会对SDS进行扩容;采用预分配冗余空间和惰性空间释放的方式来减少内存的频繁分配;定义了len属性,获取字符串长度的时间复杂度为O(1);判断是否结束的标志是len属性,它同样以\0结尾,这样就部分兼容了C。
在3.2之后的版本,SDS又有了多种结构体:sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64用于存储不同长度的字符串,以sdshdr16为例,介绍结构体中的参数含义(源码在src/sds.h):
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used 字符数组长度 */
uint16_t alloc; /* 字符数组总共分配的内存大小 */
unsigned char flags; /* 字符数组的属性,标识是sdshr16 */
char buf[];/* 存储字符 */
};
应用场景
缓存:热点数据缓存,提高访问速度
分布式数据共享:分布式session,比如sping-session-data-redis
分布式锁:setnx命令
全局ID:incyby命令,利用这个命令的原子性
计数器:incy命令
位统计:bit相关命令
存储类型
包含键值对的无序散列表,value只能是字符串
hash和string的区别?hash是把相关的值聚集到一个key上,节省内存空间;只使用一个key,减少哈希冲突;批量获取值的时候,使用一个命令,减少IO次数。hash类型的field字段不能单独设置过期时间,没有位操作,value值非常大的时候,不能分布到多个节点。
存储原理
Reids本身是一个KV的结构,使用了hashtable(数组+链表)的存储结构,而hash类型的数据,Value本身也是一个KV结构,这个就是内层的哈希,底层使用了两种数据结构实现:ziplist 也就是压缩列表或hashtable 哈希表。
1、ziplist介绍
ziplist是一个经过特殊编码的连续内存块组成的顺序性链表结构,但它并没有存储指向上一个或下一个链表节点的指针,而是存储的是上一个节点的长度和当前节点的长度,是通过时间换空间的思想来提高内存的空间利用率。只用在字段个数少,字段值小的场景。
ziplist的布局定义在src/ziplist.c中:
/* ... */
/*
记录整个压缩列表占用的内存字节数,在内存重分配或者计算zlend时使用
记录压缩列表尾节点到头节点有多少字节,可以快速定位到尾部节点
记录压缩列表的节点数量
压缩列表的节点
标记压缩列表末端
*/
zlentry的结构定义:
typedef struct zlentry {
unsigned int prevrawlensize; /* 保存前一个节点长度所需的长度*/
unsigned int prevrawlen; /* 前一节点的长度,用于从后往前遍历使用 */
unsigned int lensize; /* 保存当前节点长度所需的长度 */
unsigned int len; /* 当前节点的长度 */
unsigned int headersize; /* 当前节点头部信息长度=参数1+参数3,表示非数据域的大小 */
unsigned char encoding; /* 编码*/
unsigned char *p; /* 压缩列表以字符串的形式保存,该指针指向当前节点的起始位置 */
} zlentry;
编码定义:
#define ZIP_STR_06B (0 << 6)/*长度小于等于63字节*/
#define ZIP_STR_14B (1 << 6)/*长度小于等于16383字节*/
#define ZIP_STR_32B (2 << 6)/*长度小于等于4294967295字节*/
当hash对象键和值的长度都超过64字节或者哈希对象保存的键值对数量超过512个时,底层会转化为hashtable存储。
配置文件中的参数(redis.conf)
hash-max-ziplist-value 64
hash-max-ziplist-entries 512
2、hashtable介绍
前面我们知道了Redis的KV结构是通过一个dictEntry实现的,dictEntry又被放到了一个dictht(hashtable)里面,而这个dictht又被放到了dict中,下面简单了解一下这几个结构体:
typedef struct dictht {
dictEntry **table;/* 哈希表数组 */
unsigned long size;/* 哈希表大小 */
unsigned long sizemask;/* 掩码大小,用于计算索引值=size-1*/
unsigned long used;/*已有节点数*/
} dictht;
typedef struct dict {
dictType *type;/* 字典类型 */
void *privdata;/* 私有数据 */
dictht ht[2];/* 一个字典有两个哈希表 */
long rehashidx; /* rehash索引 */
unsigned long iterators; /* 当前正在使用的迭代器数量 */
} dict;
hash默认使用的是h[0],h[1]不会初始化和分配空间,dictht使用拉链法来解决哈希碰撞的,当数组的每一个位置只挂载一个dictEntry时,性能最高,如果挂载的dictEntry变多的时候,性能就会下降,这里用一个参数ratio表示哈希表的大小(size属性)和它保存的节点数量(used)的比率,当ratio值太大的时候就会触发扩容(rehash),定义两个哈希表就跟JVM运行时数据区的from区和to区进行数据迁移的原理相同。首先为h[1]分配内存,然后把h[0]的节点rehash到h[1]上,最后释放h[0]的空间并将h[1]设置为h[0],创建新的h[1],为下次rehash做准备。
扩容操作:当ratio值大于1:5时触发扩容
缩容操作:
应用场景
存储对象类型的数据:节省了Key的空间,数据便于集中管理
存储类型
存储有序的字符串,元素可以重复。
列表的头在左边
rpush+lpop命令结合相当于队列操作
lpush+lpop命令结合相当于栈操作
存储原理
在3.2版本之前,数据量较小时用ziplist存储,达到临界值时转换为linkedlist存储,3.2版本之后统一用quicklist存储。
quicklist介绍:
quicklist存储了一个双向链表,每个节点都是一个ziplist,它结合了压缩列表和链表的特点。结构定义如下:
typedef struct quicklist {
quicklistNode *head;/* 链表头 */
quicklistNode *tail;/* 链表尾 */
unsigned long count;/* 所有的ziplist中总共存储了多少个元素 */
unsigned long len;/* 双向链表的长度,quicklistNode的数量 */
int fill : 16;/* 单个node的填充因子 */
unsigned int compress : 16; /* 压缩深度 */
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; /* 前一个节点 */
struct quicklistNode *next; /* 后一个节点 */
unsigned char *zl; /* 指向实际的ziplist */
unsigned int sz; /* 当前ziplist占用的字节 */
unsigned int count : 16; /* 当前ziplist中存储了多少个元素,占16位,也就是做多为65536个 */
unsigned int encoding : 2; /* 是否采用了LZF压缩算法RAW==1 or LZF==2 */
unsigned int container : 2; /* 未来可能支持其他结构存储NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* 当前ziplist是不是已经被解压出来用作临时使用 */
unsigned int attempted_compress : 1; /* 测试时使用 */
unsigned int extra : 10; /* 预留给未来扩展使用 */
} quicklistNode;
list-max-ziplist-size 正数表示单个ziplist最多包含的entry个数,负数表示单个ziplist的大小,默认为8k。-1:4K;-2:8K;-3:16K;-4:32K;-5:64K。
list-compress-depth 0 压缩深度,默认为0,1表示首尾的ziplist不压缩,2表示第一个第二个和倒数第一个倒数第二个不压缩,以此类推
至于ziplist这里就不再重复说明了。
应用场景
时间线:根据list有序的特点
消息队列:blpop和brpop命令是阻塞式的弹出操作,还可以设置超时时间
存储类型
存储的是String类型的无序集合,最大存储数量2^32-1个,元素不允许重复
存储原理
如果元素都是整数类型就用intset存储
如果不是整数类型,就用hashtable存储
注意:key就是存储的元素的值,value为null
如果元素的个数超过了512个,就是用hashtable存储
相关参数配置信息redis.conf:
set-max-intset-entries 512
应用场景
抽奖:spop命令随机弹出一个元素
打卡、点赞:sadd命令、srem、sismember、smembers、scard
筛选:sdiff 、sinter、sunion取差集、交集、并集操作
存储类型
顾名思义,存储的是有序的set集合,每个元素都有一个score,score相同时,按照key的ASCII码排序,元素不能重复
存储原理
当元素数量少于128个并且所有元素的长度小于64字节时,采用的是ziplist编码存储,按照score递增的顺序存储,插入数据时要移动之后的数据。
当上面两个条件不满足的时候采用skiplist+dict存储
skiplist介绍
跳跃表使用概率均衡技术而不是使用强制性均衡,因此,对于插入和删除结点比传统上的平衡树算法更为简洁高效。与有序数组通过二分查找的思想类似,通过分段的思想,使得查找更加高效。
看一下源码的定义src/server.h:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;/*指向跳跃表的头节点和尾节点*/
unsigned long length;/*跳跃表的节点数*/
int level;/*最大的层数*/
} zskiplist;
typedef struct zskiplistNode {
sds ele;/*zset的元素*/
double score;/*分值*/
struct zskiplistNode *backward;/*后退指针*/
struct zskiplistLevel {
struct zskiplistNode *forward;/*前进指针,对应具体level的下一个节点*/
unsigned long span;/*从当前节点到下一个节点跨越的节点数*/
} level[];/*分层值的集合,*/
} zskiplistNode;
typedef struct zset {
dict *dict;/*dictEntry*/
zskiplist *zsl;/*zskiplist*/
} zset;
这里看一下节点获取分层值的函数src/t_zset.c:
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
应用场景
排行榜:利用有序的特点
范围查找:
提供了一种不太准确的基数统计方法,这种方法存在一定的误差
可以将经纬度格式的地理坐标信息存储在Redis中,并且可以对这些坐标执行距离计算、范围查找等。
在字符串类型上定义的一些位操作。
Redis5.0新推出的类型,支持多播的可持久化的消息队列,可以实现发布订阅的功能,借鉴了kafka的设计
*/
} zset;
这里看一下节点获取分层值的函数src/t_zset.c:
```c
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level
应用场景
排行榜:利用有序的特点
范围查找:
提供了一种不太准确的基数统计方法,这种方法存在一定的误差
可以将经纬度格式的地理坐标信息存储在Redis中,并且可以对这些坐标执行距离计算、范围查找等。
在字符串类型上定义的一些位操作。
Redis5.0新推出的类型,支持多播的可持久化的消息队列,可以实现发布订阅的功能,借鉴了kafka的设计