为什么要区分内核空间和用户空间?
早期是不区分内核和用户的,带来的问题是程序可以访问任意内存空间,如果程序不稳定,容易把系统搞崩溃。
后来按cpu指令的重要程度对指令进行了分级,一共4个级别:Ring0~Ring3,linux只使用了Ring0和Ring3两个级别;
用户态使用Ring3级别运行,只访问用户空间,Ring0运行在内核态,可以访问任何程序空间
内核空间
用户空间
fd(file descriptors文件描述符)
指向内核为对应进程维护打开文件记录表的索引值,进程唯一,也可以理解为文件指针
file table
inode table
一切皆文件(进程、设备、通道等)
抽象了一组标准接口,每个进程有3个标准文件
fd可以重复利用
socket的概念
bind()
listen()
fork()
io多路复用
/**
* @brief ae的几种实现
* redis按照性能从上到下排序
*
* evport: 支持Solaris
* epoll: 支持linux
* kqueue: 支持FreeBSD 系统 如macos
* select: 都不包含就是select
*/
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
redis 对于事件模型的封装
/**
* @brief
* 根据不同的操作系统会有不同的实现
* 对于select来说应该是就是初始化fdset,用于select的相关调用;
* 对于epoll来说,需要创建epoll的fd以及epoll使用的events数组
* @param eventLoop
* @return int
*/
static int aeApiCreate(aeEventLoop *eventLoop) {}
/**
* @brief 注册事件到 到操作系统,每个操作系统针对读写的事件类型不同
* 对于evport来说,往npending里增加fd
* 对于kqueue来说,就是往kqfd里增加fd
* 对于select来说,就是往对应读写类型的fd_set里面增加fd
* 对于epoll来说,就是在events中增加/修改感兴趣的事件
* @param eventLoop 是为了接收回调数据
* @param fd 对应监听的fd值
* @param mask 类型
* @return int
*/
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {}
/**
* @brief 单位时间内监听到的事件数量
* @param eventLoop
* @param tvp 单位时间
* @return int 返回待处理的事件数量
*/
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {}
//删除事件
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {}
static void aeApiFree(aeEventLoop *eventLoop) {}
//获取实现的名称
static char *aeApiName(void) {}
select和poll的缺点:
epoll改进了select模式,避免了以上的几个缺点(事件驱动)
如上图:
核心代码
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
//只要没有停止,就循环执行,这个是主线程
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
//每次循环前执行beforesleep
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
/**
* @brief tcp处理器
* @param el
* @param fd 当前tcp的fd
* @param privdata 对应epoll数据
* @param mask
*/
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
/**
* cport 当前的端口
* cfd 当前的fd
* max 一次最多处理1000个
*/
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
//取tcp请求
while(max--) {
/**
* @brief 监听tcp socket ,获取一个新的fd,后续再研究下这里 TODO
* 新的fd就是一个有效的链接
*/
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
//针对新监听到的请求处理(创建一个client并将新生成的cfd与其绑定)
acceptCommonHandler(cfd,0,cip);
}
}
typedef struct dictEntry {
void *key;
// v使用联合体,共用头部指针,正常是4选一,可以理解为java的泛型
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
/**
* @brief redis对象的数据结构体,在监听到数据后,都会将key和value包装成这个对象
* OBJ_STRING -> OBJ_ENCODING_INT 使用整数值实现的字符串对象
* OBJ_STRING -> OBJ_ENCODING_RAW 使用sds实现的字符串对象
* OBJ_STRING -> OBJ_ENCODING_EMBSTR 使用embstr编码的sds实现的字符串
* OBJ_LIST -> OBJ_ENCODING_QUICKLIST 使用快速列表实现(混合了压缩列表和双端链表)
* OBJ_SET -> OBJ_ENCODING_HT 使用字典实现的集合
* OBJ_SET -> OBJ_ENCODING_INTSET 使用整数集合实现的集合
* OBJ_ZSET -> OBJ_ENCODING_ZIPLIST 使用压缩列表实现的有序集合
* OBJ_ZSET -> OBJ_ENCODING_SKIPLIST 使用跳表实现的有序集合
* OBJ_HASH -> OBJ_ENCODING_HT 使用字典实现的hash
* OBJ_HASH -> OBJ_ENCODING_ZIPLIST 使用压缩列表实现的hash
* OBJ_MODULE
* OBJ_STREAM -> OBJ_ENCODING_STREAM 使用stream实现
*/
typedef struct redisObject {
//robj存储的对象类型,sting、list、set、zset等
unsigned type:4; //4位
// 编码,OBJ_ENCODING_RAW 0 OBJ_ENCODING_INT 1
unsigned encoding:4; //4位
/**
* @brief 24位
* LRU的策略下:lru存储的是 秒级时间戳的低24位,约194天会溢出
* LFU的策略下:24位拆为两块,高16位(最大值65535)低8位(最大值255)
* 高16存储的是 存储的是分钟级&最大存储位的值,要溢出的话,需要65535%60%24 约 45天溢出
* 低8位存储的是近似统计位
* 在lookupKey进行更新
*/
unsigned lru:LRU_BITS;
//引用次数,当为0的时候可以释放就,c语言没有垃圾回收的机制,通过这个可以释放空间
int refcount; //4字节
/**
* 指针有两个属性
* 1,指向变量/对象的地址;
* 2,标识变量/地址的长度;
* void 因为没有类型,所以不能判断出指向对象的长度
*/
void *ptr; // 8字节
} robj;//一个robj 占16字节
typedef struct client {
uint64_t id; /* Client incremental unique ID. */
int fd; /* Client socket. */
redisDb *db; /* Pointer to currently SELECTed DB. */
robj *name; /* As set by CLIENT SETNAME. */
//初始的时候,是一个空的sds,客户端累计的查询缓冲区大小,后续每次处理扩容16kb
sds querybuf; /* Buffer we use to accumulate client queries. */
//从querybuf读取的位置
size_t qb_pos; /* The position we have read in querybuf. */
//待同步到从库的缓冲区到小
sds pending_querybuf; /* If this client is flagged as master, this buffer
represents the yet not applied portion of the
replication stream that we are receiving from
the master. */
//上次查询缓冲区使用的大小
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size. */
//参数数量
int argc; /* Num of arguments of current command. */
//参数的redisObject 数组
robj **argv; /* Arguments of current command. */
//客户端要执行的命令
struct redisCommand *cmd, *lastcmd; /* Last command executed. */
int reqtype; /* Request protocol type: PROTO_REQ_* */
int multibulklen; /* Number of multi bulk arguments left to read. */
long bulklen; /* Length of bulk argument in multi bulk request. */
/**
* @brief 链表对象是里面的节点对象是clientReplyBlock
* clientReplyBlock是一个数组
* 因为不知道缓冲区有多大,为了
*/
list *reply; /* List of reply objects to send to the client. */
unsigned long long reply_bytes; /* Tot bytes of objects in reply list. */
size_t sentlen; /* Amount of bytes already sent in the current
buffer or object being sent. */
time_t ctime; /* Client creation time. */
/**
* @brief 上次交互的时间,用于判断超时
*/
time_t lastinteraction; /* Time of the last interaction, used for timeout */
time_t obuf_soft_limit_reached_time;
.....
/* Response buffer */
int bufpos;
char buf[PROTO_REPLY_CHUNK_BYTES];
} client;
Redis是一个开源,内存存储的数据结构服务,可用作数据库(不建议),高速缓存和消息队列等。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglog、stream等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
通过sds动态字符串编码实现。
struct __attribute__ ((__packed__)) sdshdr8 {
//1字节 max= 255 已用空间(不同类型len占用的长度不同)
uint8_t len; /* used */
//1字节 申请的buf的总空间,max255(不包含flags、len、alloc这些)(不同类型len占用的长度不同)
uint8_t alloc; /* excluding the header and null terminator */
// 1字节 max= 255
unsigned char flags; /* 3 lsb of type, 5 unused bits */
// 字节数组+1结尾\0
char buf[];
};//4+n 长度
struct __attribute__ ((__packed__)) sdshdr16 {
// 2字节 16位 max 65535(不同类型len占用的长度不同)
uint16_t len; /* used */
// 2字节 16位 申请的buf的总空间max 65535
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
sds高效在哪了?
必须了解下c语言的字符串。
sds做了哪些优化?
应用场景
redis3.2以后将list的压缩列表(ziplist)和双端链表(linkedList)改成了quicklist了。
quicklist融合了ziplist和linkedlist的功能;
默认一个quicklistNode是一个ziplist对象;
ziplist的大小有限制
ziplist更新会比较麻烦(比如更新值不等于当前元素内存大小时,需要扩缩容,或挪移,如果多个连续更新,想下效率)
先看下原来ziplist的的创建,以及结构
/**
* @brief 创建一个空的压缩列表
*
* @return unsigned char*
*/
unsigned char *ziplistNew(void) {
// ...
//压缩列表的结构大小 12+1
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
//申请13字节的空间,ziplist的 head + 结束标识的大小
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0;
//将列表尾设置为255
zl[bytes-1] = ZIP_END;
return zl;
}
再看下quicklist的源码和结构
typedef struct quicklist {
//头节点指针
quicklistNode *head;
//尾节点指针
quicklistNode *tail;
//元素个数总和
unsigned long count; /* total count of all entries in all ziplists */
//快速列表的节点个数
unsigned long len; /* number of quicklistNodes */
//压缩列表的最大大小,初始化时用的,list-max-ziplist-size的值
int fill : 16; /* fill factor for individual nodes */
//结点压缩深度,初始化时用的 list-compress-depth 的值,0表示不压缩
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
/**
* 快速列表的节点结构
*/
typedef struct quicklistNode {
//前驱节点指针
struct quicklistNode *prev;
//后继节点指针
struct quicklistNode *next;
//指向压缩列表的指针(当前节点被压缩,指向一个quicklistLZF结构的指针)
unsigned char *zl;
//压缩列表所占字节总数
unsigned int sz; /* ziplist size in bytes */
//压缩列表中的元素数量
unsigned int count : 16; /* count of items in ziplist */
//编码,原生字节数组为1,压缩存储为2
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
//表示quicklistNode 节点是否采用ziplist结构保存数据,
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
//是否再次压缩,不设置,表示ziplist结构,设置为1表示quicklistLZF,
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklistLZF {
//表示被压缩后的ziplist的大小
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
如上:
应用场景
/**
* 创建set的工厂方法
* @param value
* @return
*/
robj *setTypeCreate(sds value) {
//是一个long类型的,创建成intset
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}
/**
* 创建一个普通hash表
* @return
*/
robj *createSetObject(void) {
dict *d = dictCreate(&setDictType,NULL);
robj *o = createObject(OBJ_SET,d);
o->encoding = OBJ_ENCODING_HT;
return o;
}
/**
* 创建一个intset
* @return
*/
robj *createIntsetObject(void) {
intset *is = intsetNew();
robj *o = createObject(OBJ_SET,is);
o->encoding = OBJ_ENCODING_INTSET;
return o;
}
/**
* intset的数据结构
*/
typedef struct intset {
//编码方式,可以是16位整数,32位整数,64位整数
uint32_t encoding;
//元素个数
uint32_t length;
//存储的是数组指针,按从小到大排列
int8_t contents[];
} intset;
这里只介绍下intset
应用场景
唯一性
共同(好友、独立ip、标签)
主要讲解下跳跃表结构,压缩表不讲解
/**
* 跳跃表节点
*/
typedef struct zskiplistNode {
//member对象
sds ele;
//权重分值
double score;
//后退指针
struct zskiplistNode *backward;
//层级描述
struct zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨越节点的数量
unsigned long span;
} level[];
} zskiplistNode;
/**
* zset的数据结构跳跃表
*/
typedef struct zskiplist {
//头尾节点指针
struct zskiplistNode *header, *tail;
//节点数量
unsigned long length;
//最大层数
int level;
} zskiplist;
/**
* 跳表结构的zset
*/
typedef struct zset {
//kv形式,存储所有的member和对应的score
dict *dict;
//跳跃表
zskiplist *zsl;
} zset;
我们从源码zadd看下zset命令(精简后的源码),在t_zset.c中
void zaddGenericCommand(client *c, int flags) {
//不存在,就创建
if (zobj == NULL) {
/**
* 根据redis的配置,如果有序集合不使用ziplist存储或者第一次插入元素的个数大于设置的ziplist最大长度,则使用跳表
*/
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr)){
//这里创建了一个score为0,层级为64的元素为null的 头节点
zobj = createZsetObject();
} else {
zobj = createZsetZiplistObject();
}
//插入entry到hash表
dbAdd(c->db,key,zobj);
} else {//存在,校验类型,不是zset,报错
if (zobj->type != OBJ_ZSET) {
addReply(c,shared.wrongtypeerr);
goto cleanup;
}
}
//遍历所有的
for (j = 0; j < elements; j++) {
double newscore;
score = scores[j];
int retflags = flags;
//获取元素数据的指针
ele = c->argv[scoreidx+1+j*2]->ptr;
//添加元素到zset,在zsetAdd 方法里进行了类型区分
int retval = zsetAdd(zobj, score, ele, &retflags, &newscore);
if (retval == 0) {
addReplyError(c,nanerr);
goto cleanup;
}
//根据操作类型计数
if (retflags & ZADD_ADDED) added++;
if (retflags & ZADD_UPDATED) updated++;
if (!(retflags & ZADD_NOP)) processed++;
score = newscore;
}
//计数
server.dirty += (added+updated);
}
/**
* zset添加元素
* @param zobj zset的存储结构
* @param score 添加的分值
* @param ele 元素对象
* @param flags
* @param newscore 添加成功后的分值
* @return
*/
int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) {
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
//存在先删后插
}else if (!xx) {
//超过配置长度,就将压缩链表转到了跳跃表
if (zzlLength(zobj->ptr)+1 > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value ||
!ziplistSafeToAdd(zobj->ptr, sdslen(ele)))
{
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
} else {
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
if (newscore) *newscore = score;
*flags |= ZADD_ADDED;
return 1;
}
}
}
if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
//zset 指针
zset *zs = zobj->ptr;
zskiplistNode *znode;
dictEntry *de;
//从zset的全局hash表中查找对应的key,找到说明已经存在,如果需要更新就操作,不需要就返回
de = dictFind(zs->dict,ele);
if (de != NULL) {
if (score != curscore) {
//这里先删除,然后重新插入,单线程保证了一致性,最后插入还是走的zslInsert
znode = zslUpdateScore(zs->zsl,curscore,ele,score);
//更新全局hash表里的权重分值
dictGetVal(de) = &znode->score; /* Update score ptr. */
*flags |= ZADD_UPDATED;
}
}else if (!xx) {
//将元素压缩成一个紧凑型的sds
ele = sdsdup(ele);
//插入元素
znode = zslInsert(zs->zsl,score,ele);
//将元素插入到对应的hash表中
serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
*flags |= ZADD_ADDED;
if (newscore) *newscore = score;
return 1;
}
}
}
通过源码:
在插入的元素的时候逻辑如下:
我们重点看下跳跃表的操作。
跳跃表论文 https://www.cl.cam.ac.uk/teaching/2005/Algorithms/skiplists.pdf
/**
* 创建一个跳跃表(具体实现)
*/
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl));
zsl->level = 1;
zsl->length = 0;
// header是一个权重分值为0,元素为NULL的对象
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
//新创建的跳跃表的header是一个64层级的空表
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
zsl->tail = NULL;
return zsl;
}
/**
* 跳表结构插入一条数据
* @param zsl 从zset上获取到跳跃表
* @param score 权重分值
* @param ele 元素
* @return
*/
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
/**
* update 保存对应层级小于插入权重分值的前一个节点,如果没有为header
* 新添加层级保存的是跳跃表的header指针
* x 表示zskiplistNode节点指针
*/
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
/**
* 每一层对应到update对应层级那个位置的跨度
*/
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
//最开始为头节点
x = zsl->header;
//逆序遍历当前的所有层级,找到新插入权重分值每一层左侧的数据
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
//最上层 rank为0,否则为i+1(相当于逆序了)
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
/**
* 前驱指针存在,
* 并且(当前指针对应的分值小于插入分值 或者(当前分值等于插入分值 并且现有元素和插入元素不相同))
* 比如当前权重分值为 20,跨度5,插入权重分值为30 ,或者 权重分值都为20,但是元素长度不相同(分值相同的话看元素长度大小,小的在前)
* 继续往下一个节点走,会记录下满足条件的跨度
* 通过这块,可以看到在zset里是根据分数权重,然后根据元素的长度大小升序排序
*/
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
//将符合条件的层级跨度收集起来
rank[i] += x->level[i].span;
//链表往下走
x = x->level[i].forward;
}
//将每一层的要插入值的最近一个节点更新到update数组里
update[i] = x;
}
/**
* 随机层级
* 1层级的概率为 100%;
* 2层级的概率为 1/4
* 3层级的概率为 1/4 * 1/4
* 后续每增加一层级的概率都是指数级上升
*/
level = zslRandomLevel();
// 扩容层级,随机出来的层级> 当前层级
if (level > zsl->level) {
//这块增加的可能1层,也可能多层,最多(64-当前层级)
for (i = zsl->level; i < level; i++) {
//新添加的层级rank都为0
rank[i] = 0;
//新添加层级取的是zskplist的header对应的指针
update[i] = zsl->header;
//新添加层级的跨度就是元素的总数
update[i]->level[i].span = zsl->length;
}
//更新层级数
zsl->level = level;
}
//给新插入的元素和权重分值创建zskiplistNode,层级为刚随机出来的层级
x = zslCreateNode(level,score,ele);
/**
* 把新插入的节点,插入到每一层级中
* 更新每一层级的链表结构
* 并将新插入节点对应层级的前驱指针和跨度维护进去
*/
for (i = 0; i < level; i++) {
//链表插入节点
x->level[i].forward = update[i]->level[i].forward;
/**
* 新的层级update[i]为 header节点
*/
update[i]->level[i].forward = x;
/* update span covered by update[i] as x is inserted here */
//计算每一层级的跨度并更新进去
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
/**
* 对于没有达到的层级,增加1
*/
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
//新插入节点的回退指针为最底层的前一个节点
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
}
/**
* 随机层级
* 1层级的概率为 100%;
* 2层级的概率为 1/4
* 3层级的概率为 1/4 * 1/4
* 后续每增加一层级的概率都是指数级上升
* @return
*/
int zslRandomLevel(void) {
int level = 1;
/**
* 完全靠随机, 0xFFFF = 65535 , ZSKIPLIST_P = 0.25
* 如果随机每次随机出来都小于 0.25*65535 level都加1,直到随机出大于
*/
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
总结一下:
上图
ADD添加对应的元素
# 语法
ZADD key score member [[score member] [score member] …]
# 添加
zadd yxkong 50 'a' 60 'b' 75 'c' 88 'd' 90 'e' 100 'f'
ZRANGEBYSCORE 获取分值在某个区间的元素(以跳跃表为例)
ZRANGEBYSCORE key min max WITHSCORES limit offset num
# min 可以是具体的数值,也可以是-inf(负无穷),也可以是(10
# max 可以是具体的数值,也可以是+inf(正无穷) 也可以是(30
# WITHSCORES 返回元素带score
# limit 限制返回的数量
# offset 从哪个索引开始
# num 返回的数量
如:
office:0>ZRANGEBYSCORE yxkong 55 80 WITHSCORES
1) "b"
2) "60"
3) "c"
4) "75"
查看下源码
/**
* 获取指定score区间的元素
* @param c
* @param reverse 是否取反,0表示不取反,1表示取反
*/
void genericZrangebyscoreCommand(client *c, int reverse) {
//将分值区间解析到range中
if (zslParseRange(c->argv[minidx],c->argv[maxidx],&range) != C_OK) {
addReplyError(c,"min or max is not a float");
return;
}
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
......
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
//最终都是找链表上的起始点
if (reverse) {
//查找终点 80 50
ln = zslLastInRange(zsl,&range);
} else {
//查找起点 比如 50 80
ln = zslFirstInRange(zsl,&range);
}
//遍历链表,如果有limit就递减,0为假
while (ln && limit--) {
/* Abort when the node is no longer in range. */
//如果获取到的对象权重分值,已经不在范围内了,直接break
if (reverse) {
if (!zslValueGteMin(ln->score,&range)) break;
} else {
if (!zslValueLteMax(ln->score,&range)) break;
}
rangelen++;
//添加到回复缓冲区
addReplyBulkCBuffer(c,ln->ele,sdslen(ln->ele));
if (withscores) {
//需要带权重分值,将权重分值添加到回复缓冲区
addReplyDouble(c,ln->score);
}
/* Move to next node */
//就是正反序遍历链表
if (reverse) {
//回退,永远是level[0]
ln = ln->backward;
} else {
//最底层前进
ln = ln->level[0].forward;
}
}
}
}
/**
* 获取第一个分值所在的节点
* @param zsl
* @param range 50 ~ 80
* @return
*/
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {
zskiplistNode *x;
int i;
if (!zslIsInRange(zsl,range)) return NULL;
x = zsl->header;
//从上到下遍历所有的层级,定位到最小的元素
for (i = zsl->level-1; i >= 0; i--) {
/* Go forward while *OUT* of range. */
//定位最小元素所在的位置
while (x->level[i].forward &&
!zslValueGteMin(x->level[i].forward->score,range))
x = x->level[i].forward;
}
x = x->level[0].forward;
serverAssert(x != NULL);
/* Check if score <= max. */
if (!zslValueLteMax(x->score,range)) return NULL;
return x;
}
查找过程
阅读了redis的源码,redis好多地方都是用的近似算法,LFU、LRU、淘汰策略、以及这次的zset,跳跃表的索引也是近似,一但出现了极端情况,跳跃表就直接退化为了链表。
应用场景
我们先看下源码
void hsetCommand(client *c) {
//遍历所有的kv
for (i = 2; i < c->argc; i += 2)
// 创建hash的key val
created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
}
int hashTypeSet(robj *o, sds field, sds value, int flags) {
//ziplist处理逻辑
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
}else if (o->encoding == OBJ_ENCODING_HT) {
dictEntry *de = dictFind(o->ptr,field);
if (de) {
//如果存在,先释放,再把新的sds的指针赋值过去
sdsfree(dictGetVal(de));
if (flags & HASH_SET_TAKE_VALUE) {
dictGetVal(de) = value;
value = NULL;
} else {
dictGetVal(de) = sdsdup(value);
}
} else {
dictAdd(o->ptr,f,v);
}
}
}
对应的图,可以参考下存储的基本结构那
应用场景
redis中有事务的概念,但是大家不要把这个事务往mysql的事务上靠。
multi
和exec
来实现事务
multi
指令告诉server中的client开启事务exec
命令 后server中的client执行命令我们看下源码:
/**
* server接收到multi指令后的动作
* @param c
*/
void multiCommand(client *c) {
//已开启,直接拦截
if (c->flags & CLIENT_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
//将客户端标记为CLIENT_MULTI
c->flags |= CLIENT_MULTI;
addReply(c,shared.ok);
}
在server.c中
/**
* @brief 命令执行
* @param c 客户端
* @return int
*/
int processCommand(client *c) {
/**
*
* 开启了事务(CLIENT_MULTI)直接放入Multi队列
*
*/
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand){
queueMultiCommand(c);
addReply(c,shared.queued);
}
}
/**
* 添加命令到事务队列
* @param c
*/
void queueMultiCommand(client *c) {
multiCmd *mc;
int j;
c->mstate.commands = zrealloc(c->mstate.commands,
sizeof(multiCmd)*(c->mstate.count+1));
mc = c->mstate.commands+c->mstate.count;
mc->cmd = c->cmd;
mc->argc = c->argc;
mc->argv = zmalloc(sizeof(robj*)*c->argc);
//最终将客户端接收的命令都copy到mc->argv
memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
for (j = 0; j < c->argc; j++)
incrRefCount(mc->argv[j]);
c->mstate.count++;
c->mstate.cmd_flags |= c->cmd->flags;
}
/**
* 命令执行
* @param c
*/
void execCommand(client *c) {
//关键点,遍历,并执行,由于redis命令执行是单线程处理,所以在多个客户端的时候,能保证串行执行
for (j = 0; j < c->mstate.count; j++) {
c->argc = c->mstate.commands[j].argc;
c->argv = c->mstate.commands[j].argv;
c->cmd = c->mstate.commands[j].cmd;
call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);
/* Commands may alter argc/argv, restore mstate. */
c->mstate.commands[j].argc = c->argc;
c->mstate.commands[j].argv = c->argv;
c->mstate.commands[j].cmd = c->cmd;
}
discardTransaction(c);
}
redis怎么用lua脚本保证操作的原子性呢,看一个示例,
/**
* redis自增并续期过期时间的原子操作
* @param key
* @param expireTime
* @return
*/
public static Long incrByAtom(String key,int expireTime){
StringBuffer luaScript = new StringBuffer();
//lua脚本
luaScript.append("local count = redis.call('incrby', KEYS[1],1) ")
.append(" redis.call('expire', KEYS[1],ARGV[1]) ")
.append(" return count ")
;
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript.toString(), Long.class);
//redisTemplate 调用
Object result = redisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(expireTime));
if (Objects.isNull(result)){
return null;
}
return Long.valueOf(result.toString());
}
也可以直接使用redisson,它内部封装了很多的lua脚本(不会写lua脚本,直接翻它的代码)
redis是一个内存数据库,当内存满了,redis是如何淘汰的呢?
Redis的几种淘汰策略:
noeviction 无过期策略,内存满了就直接异常
volatile-lru 对有过期时间的key进行lru淘汰(越长时间没有被访问,越容易被淘汰)
allkeys-lru 对全局的key按LRU进行淘汰(越长时间没有被访问,越容易被淘汰)
volatile-lfu 对有过期时间的key进行lfu淘汰(经常不被访问的,越容易被淘汰)
allkeys-lfu 对全局的key进行lfu淘汰(经常不被访问的,越容易被淘汰)
volatile-random 对有过期时间的key进行随机淘汰
allkeys-random 对有所有的key进行随机淘汰
volatile-ttl 按时间进行过期淘汰
在freeMemoryIfNeeded中
对于LRU/LFU/TTL evictionPoolPopulate 函数是核心,核心思想就是随机采样后,计算采样数据的idle值
对LRU,idle是现在到上次访问的时间差,操作val对象的robj,这个值是记录在robj中的lru里
对LFU,idle是255-counter,counter是根据访问计算出来的衰减值,操作val对象的robj
对TTL, idle是db->expires里存储的dictEntry,val是到期日期
//所有的操作都会调用lookupKey,在这里会更新LFU的访问频次或LRU的时钟
robj *lookupKey(redisDb *db, robj *key, int flags) {
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
//更新访问频次
updateLFU(val);
} else {
//更新LRU的时钟,这个简单
val->lru = LRU_CLOCK();
}
}
unsigned int LRU_CLOCK(void) {
unsigned int lruclock;
/**
* 相当于每1毫秒获取一次时钟
*/
if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
atomicGet(server.lruclock,lruclock);
} else {
lruclock = getLRUClock();
}
return lruclock;
}
/**
* @brief 更新LFU高16位的时钟和后8位记录的数
* @param val
*/
void updateLFU(robj *val) {
//获取counter,用了计数衰减的
unsigned long counter = LFUDecrAndReturn(val);
//更新LRU的后8位,也就是LFU的counter,LFU 使用近似计数法,counter越大,使用了对数的思想
counter = LFULogIncr(counter);
/**
* 获取分钟级的时间,左移8位(占高16位)
* 低8位couter占用
*/
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
/**
* @brief 更新LRU的后8位,也就是LFU的counter
* LFU 使用近似计数法,counter越大,使用了对数的思想
* @param counter
* @return uint8_t
*/
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
/**
* @brief rand()随机生成一个0~RAND_MAX 的随机数
* r的范围是0~1
*/
double r = (double)rand()/RAND_MAX;
//
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
/**
* server.lfu_log_factor 默认为10
* baseval 越大 p的值就越小
*/
double p = 1.0/(baseval*server.lfu_log_factor+1);
/**
* r是随机生成的0~1
* counter 是以5为起始点
* baseval=0 时: p的值为1 r的在1以下的概率为100%
* baseval=1 时: p的值为0.0909 r的在0.09以下的概率只有约9% 10次counter+1
* baseval=2 时: p的值为0.0476 r的在0.0476以下的概率只有约4.8% 20次counter+1
* baseval=3 时: p的值为0.0322 r的在0.0322以下的概率只有约3.2% 30次counter+1
* baseval=4 时: p的值为0.0244 r的在0.0244以下的概率只有约2.4% 40次counter+1
* baseval=5 时: p的值约0.0196 r的在0.0196 以下的概率只有约2% 50次counter+1
* baseval=10 时:p的值约0.0099 r的在0.0099 以下的概率只有约1% 100次才可能加1次
* baseval=100时:p的值约0.000999 r的在0.000999 以下的概率只有约0.1% 1000次才可能加1
* baseval=200时:p的值约0.0005 r的在0.0005 以下的概率只有约0.05% 2000次才可能加1
* 想达到100的baseval总次数为(10+20+30+40+...+1000)=49*1000+500 约 5万次
* 想达到200的baseval总次数为 (10+20+30+40+...+2000) = 99*2000+1000 约20万次
*/
if (r < p) counter++;
return counter;
}
三种持久化模式
rdb文件示意图
在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)
rdb 是一个非常紧凑的二进制文件
rdb适合灾难恢复,主从复制
异步rdb可以最大化redis的性能,rdb操作是会从主进程fork一个子进程(fork的过程中创建快照表会阻塞主进程),不会阻塞主进程,但是占用磁盘IO;
rdb写的只是当时那个时间点的快照,并不会追加增量数据
rdb写后处理通过下个周期触发
通过参数配置
# 表示900秒内有一个键改动,就会执行rdb
save 900 1
# 表示300秒内有10个键改动,就会执行rdb
save 300 10
# 表示60秒内有1万个键改动,就会执行rdb
save 60 10000
# redis通过LZF算法对RDB文件进行压缩,会消耗子进程的cpu资源(多核,物理核、逻辑核有区别),如果是单核就会影响主进程
rdbcompression yes
# redis默认使用CRC64的算法对RDB文件进行完整性校验
rdbchecksum yes
rdb处理流程
aof有几个场景的写入:
触发时机:
aof重写过程:
redis为什么要进行aof重写呢?
为什么aof和rdb要后台子进程运行?
ps: 当有持久化任务时不会触发
有三种模式:主从模式、哨兵模式、集群模式;
redis最原始的模式,master宕机需要手动配置将slave转为master。
主从复制一共有三种模式:
syncWithMaster
进行认证slaveTryPartialResynchronization
发送psync和接收psync
FULLRESYNC
会进行重新全量复制CONTINUE
会进行增量同步replicationFeedSlaves
feedReplicationBacklog
加入到server.repl_backlog
addReplyMultiBulkLen
函数进行实时同步slaveTryPartialResynchronization
返回CONTINUE
进行增量同步replicationResurrectCachedMaster
里
readQueryFromClient
注册一个FileEvent事件,用来读取接收slave客户端到的信息sendReplyToClient
注册一个FileEvent响应给master这里就不贴源码了,看下主从复制的时序图,以及对应的状态流转
对应的时序图,两步:
数据类型选择不合理
过期key订阅
大key优化
数据倾斜
脑裂
缓存击穿:并发量比较大的key,在某个时间点过期,导致流量都打到了db上;
缓存穿透:缓存中不存在对应的数据,导致没所有的请求都打到了db上;
这种情况下主要是DDOS攻击
业务代码没有使用缓存
缓存雪崩:缓存服务宕机或者大批量缓存某一时刻过期,流量达到DB上后,会导致系统崩溃;
阻塞
共用redis,非核心业务系统执行命令阻塞redis,导致核心业务不可用;
redis系列文章
redis源码阅读-入门篇
redis源码阅读二-终于把redis的启动流程搞明白了
redis源码阅读三-终于把主线任务执行搞明白了
redis源码阅读四-我把redis6里的io多线程执行流程梳理明白了
redis源码阅读五-为什么大量过期key会阻塞redis?
redis源码六-redis中的缓存淘汰策略处理分析
redis源码阅读-之哨兵流程
redis源码阅读-持久化之RDB
redis源码阅读-持久化之aof
redis源码阅读-rehash详解
redis源码阅读-发布与订阅pub/sub
redis源码阅读-zset
阅读redis源码的时候一些c知识
阅读redis持久化RDB源码的时候一些c知识
linux中的文件描述符与套接字socket
redis中的IO多路复用select和epoll
Reactor模式详解及redis如何使用
redis的key过期了还能取出来?
本文是Redis源码剖析系列博文,有想深入学习Redis的同学,欢迎star和关注; Redis中文注解版:https://github.com/yxkong/redis/tree/5.0 如果觉得本文对你有用,欢迎一键三连; 同时可以关注微信公众号5ycode获取第一时间的更新哦;