记得去年双十一,我负责的电商平台购物车服务差点崩溃,每秒百万级的写入请求让传统数据库不堪重负,直到我们引入Redis集群——这个神奇的内存数据库不仅扛住了流量洪峰,响应时间还保持在毫秒级,究竟是什么让Redis如此强大?今天我们就一起剥开它的内核,看看这个"数据结构服务器"的设计奥秘。
"单线程怎么处理高并发?"这是很多人对Redis的第一个疑问,Redis采用单线程处理命令请求是经过深思熟虑的:
// redis.c中的事件循环核心 void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
Redis所有数据常驻内存的设计带来了惊人速度,但也面临内存管理挑战:
// object.c中的对象共享实现 if (value >= 0 && value < OBJ_SHARED_INTEGERS) { decrRefCount(o); incrRefCount(shared.integers[value]); return shared.integers[value]; }
Redis没有直接使用C字符串,而是自研了SDS(Simple Dynamic String):
struct sdshdr { int len; // 已用长度 int free; // 剩余空间 char buf[]; // 实际存储 };
这种设计带来三大优势:
Redis字典采用链式哈希解决冲突,但有两个精妙设计:
// dict.c中的哈希表定义 typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
有序集合(ZSET)使用跳表+字典的双索引结构:
// server.h中的跳表节点定义 typedef struct zskiplistNode { sds ele; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned long span; } level[]; } zskiplistNode;
跳表通过概率平衡替代严格平衡,实现O(logN)复杂度且更易维护,典型空间换时间。
SAVE
命令会阻塞主线程,而BGSAVE
则fork子进程处理:
// rdb.c中的保存逻辑 int rdbSave(char *filename) { ... snprintf(tmpfile,256,"temp-%d.rdb", (int)getpid()); fp = fopen(tmpfile,"w"); rioInitWithFile(&rdb,fp); rdbSaveData(&rdb); fclose(fp); rename(tmpfile,filename); ... }
优势:紧凑二进制格式,恢复速度快 风险:可能丢失最后一次快照后的数据
通过记录写命令实现持久化,支持三种同步策略:
// aof.c中的写入逻辑 void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) { ... if (server.aof_state == AOF_ON) aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf)); ... }
AOF重写:通过bgrewriteaof命令生成紧凑的新AOF文件
// replication.c中的同步处理 void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { ... if (server.repl_state == REPL_STATE_SEND_PSYNC) { if (masterTryPartialResynchronization(fd,0) == PSYNC_CONTINUE) return; } server.repl_state = REPL_STATE_TRANSFER; if (rdbSaveBackground(server.rdb_filename) != C_OK) { ... } }
Redis集群采用虚拟槽(16384 slots)分片:
// cluster.c中的槽位分配 int clusterAddSlot(clusterNode *n, int slot) { if (server.cluster->slots[slot]) return C_ERR; server.cluster->slots[slot] = n; n->slots[slot/8] |= (1<<(slot%8)); n->numslots++; return C_OK; }
// zmalloc.c中的内存分配 void *ztrymalloc_usable(size_t size, size_t *usable) { void *ptr = malloc(size+PREFIX_SIZE); *((size_t*)ptr) = size; update_zmalloc_stat_alloc(size+PREFIX_SIZE); if (usable) *usable = size; return (char*)ptr+PREFIX_SIZE; }
Redis基于epoll/kqueue实现I/O多路复用:
// ae_epoll.c中的事件处理 static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) { aeApiState *state = eventLoop->apidata; int retval, numevents = 0; retval = epoll_wait(state->epfd,state->events,eventLoop->setsize, tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1); if (retval > 0) { numevents = retval; for (int j = 0; j < numevents; j++) { int mask = 0; struct epoll_event *e = state->events+j; ... } } return numevents; }
通过这次源码之旅,我们看到了Redis如何在简单与高效之间找到平衡点,它的设计哲学给我们三点启示:
下次当你使用Redis的SET/GET命令时,不妨想想背后这个精妙的系统是如何运转的,理解这些原理,能帮助我们在实际开发中做出更合理的技术决策。
本文由 严三姗 于2025-07-30发表在【云服务器提供商】,文中图片由(严三姗)上传,本平台仅提供信息存储服务;作者观点、意见不代表本站立场,如有侵权,请联系我们删除;若有图片侵权,请您准备原始证明材料和公证书后联系我方删除!
本文链接:https://vps.7tqx.com/wenda/487887.html
发表评论