作者 | 一洺
Nosql = not only sql(不仅仅是SQL)。
关系型数据库:列+行,同一个表下数据的结构是一样的。
非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。
NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0大数据时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,redis是发展最快的。
传统RDBMS和NoSQL
RDBMS
- 组织化结构
- 固定SQL
- 数据和关系都存在单独的表中(行列)
- DML(数据操作语言)、DDL(数据定义语言)等
- 严格的一致性(ACID): 原子性、一致性、隔离性、持久性
- 基础的事务
NoSQL
- 不仅仅是数据
- 没有固定查询语言
- 键值对存储(redis)、列存储(HBase)、文档存储(MongoDB)、图形数据库(不是存图形,放的是关系)(Neo4j)
- 最终一致性(BASE):基本可用、软状态/柔性事务、最终一致性
Redis = Remote Dictionary Server,即远程字典服务。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis是一个开源,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
由于redis类型大家很熟悉,且网上命令使用介绍很多,下面重点介绍五大基本类型的底层数据结构与应用场景,以便当开发时,可以熟练使用redis。
(1)String类型是redis的最基础的数据结构,也是最经常使用到的类型。 而且其他的四种类型多多少少都是在字符串类型的基础上构建的,所以String类型是redis的基础。
(2)String 类型的值最大能存储 512MB,这里的String类型可以是简单字符串、 复杂的xml/json的字符串、二进制图像或者音频的字符串、以及可以是数字的字符串。
(1)缓存功能:String字符串是最常用的数据类型,不仅仅是redis,各个语言都是最基本类型,因此,利用redis作为缓存,配合其它数据库作为存储层,利用redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
(2)计数器:许多系统都会使用redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
(3)统计多单位的数量:eg,uid:gongming count:0 根据不同的uid更新count数量。
(4)共享用户session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存cookie,这两种方式做有一定弊端,1)每次都重新登录效率低下 2)cookie保存在客户端,有安全隐患。这时可以利用redis将用户的session集中管理,在这种模式只需要保证redis的高可用,每次用户session的更新和获取都可以快速完成。大大提高效率。
list类型是用来存储多个有序的字符串的,列表当中的每一个字符看做一个元素
一个列表当中可以存储有一个或者多个元素,redis的list支持存储2^32次方-1个元素。
redis可以从列表的两端进行插入(pubsh)和弹出(pop)元素,支持读取指定范围的元素集,
或者读取指定下标的元素等操作。redis列表是一种比较灵活的链表数据结构,它可以充当队列或者栈的角色。
redis列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的。
意味着它可以根据链表的下标获取指定的元素和某个范围内的元素集。
(1)消息队列:reids的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
(2)文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。
.redis集合(set)类型和list列表类型类似,都可以用来存储多个字符串元素的集合。
但是和list不同的是set集合当中不允许重复的元素。而且set集合当中元素是没有顺序的,不存在元素下标。
redis的set类型是使用哈希表构造的,因此复杂度是O(1),它支持集合内的增删改查,
并且支持多个集合间的交集、并集、差集操作。可以利用这些集合操作,解决程序开发过程当中很多数据集合间的问题。
(1)标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。
(2)共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。
(3)统计网站的独立IP。利用set集合当中元素不唯一性,可以快速实时统计访问网站的独立IP。
数据结构
set的底层结构相对复杂写,使用了intset和hashtable两种数据结构存储,intset可以理解为数组。
redis有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,但是不同的是,有序集合给每个元素多设置了一个分数。
redis有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,但是不同的是,
有序集合给每个元素多设置了一个分数,利用该分数作为排序的依据。
(1)排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
(2)用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
Redis hash数据结构 是一个键值对(key-value)集合,它是一个 string 类型的 field 和 value 的映射表,
redis本身就是一个key-value型数据库,因此hash数据结构相当于在value中又套了一层key-value型数据。
所以redis中hash数据结构特别适合存储关系型对象
(1)由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是redis中哈希类型最常用的场景。一条记录作为一个key-value,把每列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。
(2)经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当中读取,提高系统性能。
在学习基本类型底层数据存储结构前,首先看下redis整体的存储结构。
redis内部整体的存储结构是一个大的hashmap,内部是数组实现的hash,key冲突通过挂链表去实现,每个dictEntry为一个key/value对象,value为定义的redisObject。
结构图如下:
dictEntry是存储key->value的地方,再让我们看一下dictEntry结构体。
/*
* 字典
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
// 指向具体redisObject
void *val;
//
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
我们接着再往下看redisObject究竟是什么结构的。
/*
* Redis 对象
*/
typedef struct redisObject {
// 类型 4bits
unsigned type:4;
// 编码方式 4bits
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock) 24bits
unsigned lru:22;
// 引用计数 Redis里面的数据可以通过引用计数进行共享 32bits
int refcount;
// 指向对象的值 64-bit
void *ptr;
} robj;
*ptr指向具体的数据结构的地址;type表示该对象的类型,即String,List,Hash,Set,Zset中的一个,但为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种,encoding 表示对象底层所使用的编码。
redis对象底层的八种数据结构
REDIS_ENCODING_INT(long 类型的整数)
REDIS_ENCODING_EMBSTR embstr (编码的简单动态字符串)
REDIS_ENCODING_RAW (简单动态字符串)
REDIS_ENCODING_HT (字典)
REDIS_ENCODING_LINKEDLIST (双端链表)
REDIS_ENCODING_ZIPLIST (压缩列表)
REDIS_ENCODING_INTSET (整数集合)
REDIS_ENCODING_SKIPLIST (跳跃表和字典)
好了,通过redisObject就可以具体指向redis数据类型了,总结一下每种数据类型都使用了哪些数据结构,如下图所示:
前期准备知识已准备完毕,下面分每种基本类型来讲。
String类型的转换顺序
它只分配一次内存空间,redisObject和sds是连续的内存,查询效率会快很多,也正是因为redisObject和sds是连续在一起,伴随了一些缺点:当字符串增加的时候,它长度会增加,这个时候又需要重新分配内存,导致的结果就是整个redisObject和sds都需要重新分配空间,这样是会影响性能的,所以redis用embstr实现一次分配而后,只允许读,如果修改数据,那么它就会转成raw编码,不再用embstr编码了。
embstr和raw都为sds编码,看一下sds的结构体:
/* 针对不同长度整形做了相应的数据结构
* 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底层使用c语言实现,可能会有疑问为什么不用c语言的字符串呢,而是用sds结构体。
1) 低复杂度获取字符串长度:由于len存在,可以直接查出字符串长度,复杂度O(1);如果用c语言字符串,查询字符串长度需要遍历整个字符串,复杂度为O(n);
2) 避免缓冲区溢出:进行两个字符串拼接c语言可使用strcat函数,但如果没有足够的内存空间。就会造成缓冲区溢出;而用sds在进行合并时会先用len检查内存空间是否满足需求,如果不满足,进行空间扩展,不会造成缓冲区溢出;
3)减少修改字符串的内存重新分配次数:c语言字符串不记录字符串长度,如果要修改字符串要重新分配内存,如果不进行重新分配会造成内存缓冲区泄露;
redis sds实现了空间预分配和惰性空间释放两种策略。
空间预分配:
1)如果sds修改后,sds长度(len的值)将于1mb,那么会分配与len相同大小的未使用空间,此时len与free值相同。例如,修改之后字符串长度为100字节,那么会给分配100字节的未使用空间。最终sds空间实际为 100 + 100 + 1(保存空字符'