文章目录
Redis的基本特性
- 非关系型数据库,是以
键值对
的形式进行存储的,可以根据key以O(1)的时间复杂度去除或者插入关联值。 - Redis的数据是存储在
内存
中的 - 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的。
- 键值对中的值类型可以是string,hash,list,set,sorted set等
- Redis内置了复制,磁盘持久化,LUA脚本,事务,SSL,ACLs,客户端缓存,客户端代理功能
- 通过Redis哨兵和Redis Cluster模式提供了高可用性
在redis中所有的键key都是string类型的,我们一般说的redis的五种类型一般都是说其值的类型
Redis的应用场景
缓存
可以看到我们客户端拿数据就会先从db中去取数据,拿到数据之后放入到redis中,下次客户端再来取数据就可以直接从redis中取,而不是从磁盘上取。在大并发的情况下还能保护我们的数据库。
计数器
- 可以对String类型进行自增自减的运算,从而实现计数器功能。Redis这种内存型数据库的读写性能是非常高的,很适合存储频繁读写的计数量。
分布式ID生成
- 利用自增特性,一次请求一个大一点的步长如incr 2000,缓存在本地使用,用完再请求。
海量的数据统计
- 位图(bitmap)存储是否参加过某次活动,是否已读谋篇文章,用户是否为会员,日活统计。
会话缓存(session)
- 可以使用Redis来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用以及可伸缩性。
分布式队列/阻塞队列
- list是一个双向链表,可以通过lpush/rpush和rpop/lpop写入和读取消息。可以通过使用brpop/blpop来实现阻塞队列。
分布式锁实现
- 在分布式场景下,无法使用基于进程的锁来对多个节点上的进程进行同步。可以使用Redis自带的SETNX命令来实现分布式锁
热点数据存储
- 最新评论,最新文章列表,使用list存储,ltrim取出热点数据,删除老数据。
社交类需求
- Set可以实现交集,从而实现共同好友等功能,Set通过求差集,可以进行好友推荐,文章推荐。
排行榜
- sorted_set可以实现有序性操作,从而实现排行榜等功能。
延迟队列
- 使用sorted_set,使用【当前时间戳+需要延迟的时长】做score,消息内容作为元素,调用zadd来生产消息,消费者使用zrangbyscore获取当前时间之前的数据做轮训处理,消费完再删除任务rem key member
redis的相关源码介绍
- redis中我们处理命令的时候,都是将其处理成一个流,然后发送到服务端,这个服务端,将我们的redis的key,全部解析成字符串的形式,那么我们的redis中的字符串String类型又是以何种形式存储的呢?
Redis中的字符串
- 我们的redis源码是用C语言写的,所以我们先看看C语言中的字符串是如何实现的:
char data[] = "hello world";
可以看到我们的C语言中的字符串是利用字符数组实现的。
- 虽然我们的C语言是这么实现的,但是Redis是自己实现了一个字符串,我们redis中的字符串是
SDS
(Simple Dynamic String,简单的动态字符串)
- 问题:为什么我们的redis会自己定义一个数据结构用来存储String呢,而不是直接用C语言的字符数组呢?
- 答:redis会和各个语言进行交互,且数据是不可控的,可能是各种各样的。首先我们需要明白一点,C语言的字符数组在最后会默认加上
\0
表示结束,占用一个字节。如果我们客户端传过来的数据带有\0那岂不是接收的数据就会不完整,就会丢弃\0之后的字符。
Redis的字符串结构SDS
struct sdshdr{
int len;
int free;
char buf[];
}
- len:这个是表示长度,表示我们的数据的长度(就是存储的数据长度,表示buf[]已经使用的长度),该字段保证了不会因为内容中有结束符\0而截取出不完整的内容
- buf[]:是一个字符数组,该数组存储我们发送的内容
- free:表示buf[]中还有多少的剩余空间
- 我们每次会先从free中看我们要增加的字符长度够不够,如果不够才会进行扩容,如果足够使用则不会进行扩容
- redis的扩容是
(len + addlen) * 2
,其中len就是我们的数据的长度,addlen就是我们这次要新增的长度(例如本身存储的是abc,现在要存储abcd,所以addlen的长度是1),但是请注意,当我们的len到达1024*1024,即1M的时候,不会再成倍扩容,只会每次扩容加1M的空间
- 我们只会扩容不会缩容,这个结构就是用空间换取时间的典型。
小结
- SDS是Redis实现String的数据结构
(1)二进制安全的数据结构
(2)提供了内存预分配机制,避免了频繁的内存分配(小于1M的时候直接扩大两倍,大于1M的时候只会扩容1M)
(3)兼容C语言的函数库(因为其内容的存放是一个字符数组,buf[]完全遵循C语言,结束以\0结束)
Redis的字符串的代码实现
- 3.2之前和3.2之后是有一定的区别的,我们之前说的都是3.2之前的实现
- 3.2之前:
struct sdshdr{
int len;
int free;
char buf[];
}
- 3.2之后:
typedef char *sds;
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 {
......
- 注意
3.2之后这么实现,就是可以根据存储的数据的大小选择更合适的数据结构,hdr5表示的就是存储的长度可以是0-(2^5 - 1),依次类推。
- len:还是表示buf[]中已经使用的大小,表示内容的长度
- alloc:表示我们给buf[]的总大小,给其分配的大小,我们这里就不需要free,因为alloc - len = free
- flag:占用一个字节,前三位表示类型,后5位表示的是长度(只有sdshdr5中才表示长度),其他的后五位是闲置的
Redis的value的存储
- 我们之前说过了我们redis的key都是字符串类型的,其存储结构上面也都介绍了
- 我们redis的value是可以有不同的类型的:
(1)string
(2)hash
(3)set
(4)sorted set
(5)list - 我们一提起键值对,我们就会想到java中的map,在C中我们是用
dict
这个类型实现的。 - 对于我们的数据的存储,我们一般也就有这么几个结构
(1)数组:O(1)
(2)链表:O(N)
(3)树:log(N) - 我们的redis只使用了数组 + 链表,与java的map相似,hash(key)% 长度做为数组下标,hash碰撞就用链表追加。 当然源码里是用&进行计算下标的。
我反正个人就得这就是用C实现了一个HashMap
- redis一共有0-15,共计16个数据库,其实这个就是我们上面说的数组的长度是16
RedisDb的存储结构
- 16个数据库的存储结构
typedef struct redisDb {
// 键值对就是存在这个里面的
dict *dict;
// 过期时间,某个key的过期时间什么的都是在这里设置
dict *expires;
// 阻塞队列处理,维护了key和客户端连接之间的关系
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
// 数据库索引id,select 0其实就是选择的这个属性
int id;
long long avg_ttl;
unsigned long expires_cursor;
list *defrag_later;
} redisDb;
dict结构存储
- 重要的就是我们的
dict
这个属性 ,我们来详细了解下dict
的结构 - 这个dict的结构就是我们的key-value键值对的存储
typedef struct dict {
// 指定类型
dictType *type;
void *privdata;
// hashtable的一个缩写,长度为2,是为了实现渐进式的rehash
// 重要
dictht ht[2];
long rehashidx;
unsigned long iterators;
} dict;
渐进式rehash我理解就是两个链表,一个往另一个搬。就是h[0]长度是4,h[1]是null。现在需要扩容,我们就给h[1]长度变为8,然后把h[0]的一个个的搬到h[1],最后将h[0]指向h[1],h[1]变为null
typedef struct dictht {
// 重要
dictEntry **table;
unsigned long size;
unsigned long sizemask;
// 有多少个元素
unsigned long used;
} dictht;
当used:size = 1:1的时候我们就会对其进行扩容,扩容就是成倍扩容,直接翻一倍完事。
typedef struct dictEntry {
// 这个就是key
// key指向的就是一个sds对象,是一个string对象
void *key;
// 这个就是key对应的value
union {
// 由他来指向具体的值,可以指向任意一个类型,其指向的也是一个结构体,指向robj
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指明下一个节点,这就是解决hash冲突的链表
struct dictEntry *next;
} dictEntry;
- 上面的dictEntry这个结构指向的是一个redis的具体的value值,其value也是一个结构体
typedef struct redisObject {
// type是类型,string,list等 用来进行约束
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
// 用来进行引用计数法 C需要自己释放内存
int refcount;
// 真正的数据实际的地址
void *ptr;
} robj;
我们之所以不能对string类型的key执行lpush操作就是因为上面的type,type字段对其进行了约束。