前提
关系型数据库
关系数据库是指采用了关系模型来组织数据的数据库,它通过行(记录)和列(字段)的形式存储数据,这些一系列的行和列被称为表,一个表就是一个关系,而一组表组成了数据库。
优点
- 通过事务处理保持数据的一致性
- 支持SQL语言,可以进行 JOIN 等复杂查询
- 相比于其他模型,它使用的二维表结构贴近于现实世界,易于理解
缺点
- 当数据量达到一定规模时,由于关系型数据库的系统逻辑非常复杂,为了维护一致性,使得其非常容易发生死锁等的并发问题,导致其读写速度下滑严重。
- 当有较大的结构变更时 表结构更新就会变得比较复杂
- 当访问网站的用户并发性非常高,此时当读写请求数量非常高,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。
非关系型数据库
关系型数据在一些数据敏感的应用中表现了糟糕的性能,例如为巨量文档创建索引、高流量网站的网页服务等。因此关系型数据库主要用于执行规模小而读写频繁,或者大批量极少写访问的事务。
非关系型数据库(NoSQL)是对不同于传统的关系数据库的数据库管理系统的统称。相比起前者,它提出了很多不同于前者的理念:
(1)以键值对存储,结构不固定;
(2)每一个元组(记录)可以有不一样的字段,且可以根据需要增加键值对,因此其数据结构不局限于固定的表格模式,并且不使用SQL作为查询语言。
非关系型数据库分类
非关系型数据库可以分为如下几类:
(1)面向高性能并发读写的 key-value 数据库:Redis,Tokyo Cabinet属于这一类型
(2)面向海量数据访问的面向文档数据库:它可以在海量的数据中快速的查询数据,典型代表为 MongoDB 以及 CouchDB
(3)面向可扩展性的分布式数据库:这种数据库希望解决传统数据库中可扩展性上的缺陷,它可以适应数据量的增加以及数据结构的变化。
非关系型数据库优点
(1)读写性能:比如redis,它无需经过 SQL 层的解析,且是纯内存操作,因此每秒可以处理超过10万次读写操作;
(2)存储格式多:支持key-value形式、文档形式、图片形式,而关系型数据库则只支持基础类型;
(3)简单的扩展:基于键值对,数据没有耦合性(即关联性),容易扩展。
非关系型数据库缺点
(1)支持的特性不够丰富:现有产品所提供的功能都比较有限,大多数 NoSQL 数据库都不支持事务
(2)现有产品的不够成熟:大多数产品都还处于初创期,而关系型数据库已经完善了几十年。
什么是redis
Redis是一个开源,基于内存的数据结构存储,它可用作数据库、缓存、消息中间件。
(1)一般的数据库的读写都是经过磁盘的,相比于内存,磁盘的速度非常慢。而如果我们有了缓存,查询请求就会先在缓存中判断是否有结果,这样查询速度就会更快;在一些并发度高的场景,缓存也可以分担部分请求。
(2)Redis实现的是分布式缓存,若存在多个实例(机器),则每个实例都可共享一份缓存
不用安装redis即可尝试redis命令:try.redis.io/
Redis的数据结构
Redis的存储是key-value的形式的,其中key是字符串,而value可以是string、list、hash、set、sortset。
redis并不是直接只用string list set等来直接实现键值对数据库,而是根据这些数据结构创建了一个对象系统,这个系统包含了redis中五种数据结构。并且redis对象中记录最后一次访问时间,服务器会根据这个时间删除对象,从而释放内存(如果启用了maxmemory功能的情况下)。
每次我们在Redis数据库中新创建一个键值对时,至少会创建出两个对象。一个是键对象,一个是值对象。每个对象的结构如下:
typedef struct redisObject {
unsigned type:4; //对象类型
unsigned encoding:4; //对象编码格式
unsigned lru:REDIS_LRU_BITS;
int refcount;
void *ptr; // 指向底层实现数据结构的指针
} robj;
字符串
Redis使用sdshdr结构来表示字符串对象,并没有直接使用C语言的字符串。
struct sdshdr {
int len;
int free; //存储字节数组中未使用的字节数量
char buf[]; //字节数组中会有\0结束符,该结束符不会记录在len
};
SDS与C的字符串的比较
(1)sdshdr数据结构中用len属性记录了字符串的长度,获取字符串长度只需O(1)
(2)sds内部实现了动态扩展机制:若修改SDS时,空间不足。先会扩展空间,再进行修改
(3)sds内部有空间预分配机制:在扩展空间时,除了分配修改时所必要的空间,还会分配额外的空闲空间
(4)sds是二进制安全的,所有sds API都会以处理二进制的方式来处理存放在buf数组里的数据。
常用命令
SET key value 设置指定key值,它会覆盖已存在key对应的的value
GET key 获取指定key值
EXISTS key 是否存在键值对
DEL key
MGET key1 key2 key3 批量返回三个key对应的value
MGET key1 val1 key2 val2 设置两个键值对
EXPIRE keyname 3 设置keyname键值对 3s后过期
链表
Redis的链表是无环双向链表,通过void *指针保存不同类型的节点值
redis使用listNode表示节点
typedef strcut listNode{
//前置节点
strcut listNode *pre;
//后置节点
strcut listNode *next;
//节点的值
void *value;
}listNode
list表示链表
typedef struct list{
//表头结点
listNode *head;
//表尾节点
listNode *tail;
//链表长度
unsigned long len;
//节点值复制函数
void *(*dup) (viod *ptr);
//节点值释放函数
void (*free) (viod *ptr);
//节点值对比函数
int (*match) (void *ptr,void *key);
}list
使用示例
LPUSH list A 向list头部添加一个新元素,尾部则是RPUSH
LRANGE list 0 3 从list取出[0,3]范围的value值。其中0 -1表示取出全部元素
LINDEX list 2 从list取出下标2的value值
集合set
Redis的集合是无序,唯一的,相当于Java的HashSet。
整数集合(intset)是set集合的底层数据结构之一,当一个Set集合只包含整数元素且数量不多时,redis就会采用整数集合(intset)作为set(集合)的底层实现。
整数集合保证了元素是不会出现重复的,并且是从小到大的序列,其结构如下:
typeof struct intset {
// 编码方式
unit32_t encoding;
// 集合包含的元素数量
unit32_t lenght;
// 保存元素的数组,保存的数值类型取决于redisObject的encoding属性的值
int8_t contents[];
} intset;
contents数组可以存放的元素类型为:
- INTSET_ENC_INT16
- INTSET_ENC_INT32
- INTSET_ENC_INT64
16,32,64编码对应能存放的数字范围是不一样的。16最少,64最大。若希望存放的数值更大,则需要编码升级,其中步骤为:
(1)根据新元素类型来扩展整数集合底层数组空间
(2)将底层数组现有的全部元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位上,同样也要维持有序。
(3)将新元素添加到底层数组。
不支持降级操作
命令示例
SADD bools kava
SISMEMBER books java 查询是否存在某个value
SCARD books 获取长度
SPOP books 随机返回一个
字典
redis的字典相当于Java的HashMap,同样也采用了数组+链表解决哈希冲突。
redis使用dictEntry表示节点(键值对)
typedef struct dictEntry {
void *key;
//值
union {
void *value;
uint64_tu64;
int64_ts64;
}v;
//指向下个哈希节点,组成链表。
//当产生哈希冲突时,新节点放在表头
struct dictEntry *next;
}dictEntry;
dictht结构来定义哈希表
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemark;
//哈希表已有节点数量
unsigned long used;
}dictht
此外redis还再封装了一层,即外部的字典:
typedef struct dict {
//指向dictType结果的指针,它实现了一些操作键值对的函数
dictType *type;
//私有数据,这是需要传给上面特定函数的可选参数
void *privdata;
//两张哈希表,ht[0]是原生哈希表,ht[1]是用于扩容的哈希表
dictht ht[2];
//当rehash不进行时,值为-1
int rehashidx;
}dict;
typedef struct dictType{
//计算哈希值
unsigned int (*hashFunction)(const void * key);
//复制键
void *(*keyDup)(void *private, const void *key);
//复制值
void *(*valDup)(void *private, const void *obj);
//对比键
int (*keyCompare)(void *privdata , const void *key1, const void *key2)
//销毁键
void (*keyDestructor)(void *private, void *key);
//销毁值
void (*valDestructor)(void *private, void *obj);
}dictType
大致结构如下:
rehash过程
正常情况下,当hash表中元素个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原来的2倍。此外如果hash表因元素逐渐删除而变得稀疏,那么当元素个数低于数组长度的10%时,redis会开始缩容。
在对哈希表进行扩展或者收缩操作时,reash过程并不是一次性地完成的,而是渐进式地完成的,这是为了防止数据量过大导致服务器停止服务。
rehash具体过程如下:
(1)将字典的属性rehashidx设置为0,表示rehash开始,它会记录某一时刻rehash的下标。
(2)在rehash期间,它会将ht[0]中rehashidx索引对应的值rehash到ht[1]。此时也可以对字典进行增删改查操作,它会在ht[0]和ht[1]完成操作,比如查询操作是在两个数组里完成;添加操作是只在ht[1]完成。
(3)所有键值对完成rehash后,将rehashidx设置为-1,表示rehash完成。
示例过程可参照下图:
命令示例
HSET books java "think in java"
HGETALL books key和value是换行出现的
HGET books java
HMSET books java "effetive java" puthon "learn python" 批量设置
有序列表(zset)
zset类似Java中SortedSet和HashMap的结合体,即它存储的value是唯一性的,另一方面每个value都有一个代表排序的权重score值。
它的内部实现是用跳跃表数据结构。
跳跃表
什么是跳表
对于一个有序链表,如果希望查找某一个数,则需要从头到尾去匹配。为了提高搜索速度,我们可以在原有的链表上,再加一个链表
此处新链表是由上一层链表的偶数个节点构成,当我们执行查找时,先在最高层链表进行查找,若没有找到则到下一层去查找。例如此时如果我们希望查找节点17,搜寻顺序是6->9->17,发现下一节点21比19大,则我们跑到下一层查找,即可找到19。
上面的例子中上层节点数与下一层节点数比例是1:2,但实际使用中,链表是要经过多次添加/删除的,因此跳表(skip list)不强制要求 1:2,一个节点要不要被索引,建几层的索引,都在节点插入时由抛硬币决定。虽然索引的节点、索引的层数是随机的,但为了保证搜索的效率,要大致保证每层的节点数目与上节的结构相当。
redis的跳跃表
redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成。其中zskiplistNode则表示跳跃表的节点,zskiplist保存跳跃表的信息(表头,表尾节点,长度)。
typeof struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
跳表节点结构大致为:
跳表结构
typeof struct zskiplist {
// 表头节点,表尾节点
struct skiplistNode *header,*tail;
// 表中节点数量
unsigned long length;
// 表中最大层数
int level;
} zskiplist;
整个跳表示例图如下: