当前位置:首页 > 问答 > 正文

Redis原理 源码解析 深入分析Redis设计原理及源码,redis设计与源码详解

Redis深度探险:从设计哲学到源码解密

当购物车遇上百万并发

记得去年双十一,我负责的电商平台购物车服务差点崩溃,每秒百万级的写入请求让传统数据库不堪重负,直到我们引入Redis集群——这个神奇的内存数据库不仅扛住了流量洪峰,响应时间还保持在毫秒级,究竟是什么让Redis如此强大?今天我们就一起剥开它的内核,看看这个"数据结构服务器"的设计奥秘。

Redis的设计哲学:简单即美

1 单线程模型的智慧

"单线程怎么处理高并发?"这是很多人对Redis的第一个疑问,Redis采用单线程处理命令请求是经过深思熟虑的:

  • 避免锁开销:没有多线程竞争,天然线程安全
  • 消除上下文切换:CPU不用在不同线程间疲于奔命
  • 顺序执行:命令严格按照到达顺序处理,保证原子性
// redis.c中的事件循环核心
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

2 内存优先策略

Redis所有数据常驻内存的设计带来了惊人速度,但也面临内存管理挑战:

  • 渐进式rehash:字典扩容时新旧哈希表并存,避免一次性迁移卡顿
  • 内存淘汰策略:从LRU到LFU,8种策略应对不同场景
  • 对象共享:0~9999的整数对象全局共享,节省内存
// object.c中的对象共享实现
if (value >= 0 && value < OBJ_SHARED_INTEGERS) {
    decrRefCount(o);
    incrRefCount(shared.integers[value]);
    return shared.integers[value];
}

核心数据结构实现揭秘

1 动态字符串SDS

Redis没有直接使用C字符串,而是自研了SDS(Simple Dynamic String):

struct sdshdr {
    int len;    // 已用长度
    int free;   // 剩余空间
    char buf[]; // 实际存储
};

这种设计带来三大优势:

Redis原理 源码解析 深入分析Redis设计原理及源码,redis设计与源码详解

  1. O(1)时间复杂度获取长度
  2. 杜绝缓冲区溢出
  3. 减少内存重分配次数(空间预分配+惰性释放)

2 哈希表的艺术

Redis字典采用链式哈希解决冲突,但有两个精妙设计:

  1. 增量式rehash:在ht[0]和ht[1]之间渐进迁移
  2. 哈希种子随机化:防止HashDoS攻击
// dict.c中的哈希表定义
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

3 跳表的魔法

有序集合(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)复杂度且更易维护,典型空间换时间。

持久化机制的权衡

1 RDB:内存快照

SAVE命令会阻塞主线程,而BGSAVE则fork子进程处理:

Redis原理 源码解析 深入分析Redis设计原理及源码,redis设计与源码详解

// 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);
    ...
}

优势:紧凑二进制格式,恢复速度快 风险:可能丢失最后一次快照后的数据

2 AOF:操作日志

通过记录写命令实现持久化,支持三种同步策略:

  1. always:每个命令都fsync(最安全但最慢)
  2. everysec:每秒同步(折中方案)
  3. no:由操作系统决定(最快但可能丢失1秒以上数据)
// 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文件

多机协作原理

1 主从复制流程

  1. 全量同步:主节点执行BGSAVE生成RDB发送给从节点
  2. 部分同步:基于复制偏移量和积压缓冲区
  3. 命令传播:持续将写命令发送给从节点
// 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) {
        ...
    }
}

2 Cluster分片设计

Redis集群采用虚拟槽(16384 slots)分片:

Redis原理 源码解析 深入分析Redis设计原理及源码,redis设计与源码详解

  • Gossip协议:节点间通过PING/PONG交换状态
  • MOVED重定向:客户端请求错误分片时返回正确节点地址
  • ASK转向:迁移过程中临时重定向
// 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;
}

源码中的性能优化技巧

1 内存分配优化

  • zmalloc:封装系统malloc,记录内存使用情况
  • 内存池:对特定大小对象进行预分配
  • 字符串优化:embstr编码节省内存
// 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;
}

2 事件循环优化

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如何在简单与高效之间找到平衡点,它的设计哲学给我们三点启示:

  1. 专注核心场景:把内存数据结构和持久化做到极致
  2. 规避复杂性:单线程模型避免并发陷阱
  3. 渐进式改进:从2.x到7.x每个版本都有实质性优化

下次当你使用Redis的SET/GET命令时,不妨想想背后这个精妙的系统是如何运转的,理解这些原理,能帮助我们在实际开发中做出更合理的技术决策。

发表评论