Redis的C源码实现(1)--redisDB讲解

Redis的基本特性

  1. 非关系型数据库,是以键值对的形式进行存储的,可以根据key以O(1)的时间复杂度去除或者插入关联值。
  2. Redis的数据是存储在内存中的
  3. 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的。
  4. 键值对中的值类型可以是string,hash,list,set,sorted set等
  5. Redis内置了复制,磁盘持久化,LUA脚本,事务,SSL,ACLs,客户端缓存,客户端代理功能
  6. 通过Redis哨兵和Redis Cluster模式提供了高可用性
  7. 在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字段对其进行了约束。

总结redisDB主题数据结构

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值