redis 源码阅读

个人redis基础八股

官网下载zip:
本文即是文件创建时间时候的版本~
在这里插入图片描述
redis代码注释也蛮多的,本文将围绕注释边理解边阅读代码。

目录结构

目录/文件说明
src/✅核心源代码目录,大多数逻辑都在这里
deps/依赖的第三方库,如 jemalloc、hiredis
tests/单元测试和集成测试代码
redis.conf默认的 Redis 配置文件
Makefile编译入口,可通过 make 编译 redis-server 等
README.md项目简介
utils/一些工具脚本,比如 create-cluster

/src

int main()

在这里插入图片描述

程序执行命令功能
redis-server./src/redis-server启动 Redis 【服务端】
redis-cli./src/redis-cli连接 Redis 的【命令行客户端】
redis-benchmark./src/redis-benchmark对 Redis 做压力测试
setproctitle开发/调试辅助工具设置 Linux 进程名用的工具模块
服务端 server.c
.h 声明实现的位置不只在同名.c

像 zsetAdd() 等,声明在server.h,实现不在server.c,而是在 t_zset.c :
在这里插入图片描述

取随机数:足够的熵值 entropy

在这里插入图片描述
为了确保获取到的随机数具有足够的熵值(entropy),我们不能仅依赖 time() 和 getpid()。

因为在 容器(如 Docker) 中运行多个 Redis 实例时:

  • 它们的 time()(当前时间戳 - 秒级)
  • 和 getpid()(进程号)

可能是一样的!
导致它们生成的随机数是一样的。

微秒tv_usec 几乎不可能重复 —— 一秒中可以有 100 万 个不同的微秒值(0~999999)

	struct timeval tv;
	...

/* To achieve entropy, in case of containers, their time() and getpid() can
     * be the same. But value of tv_usec is fast enough to make the difference */
    gettimeofday(&tv,NULL); //获取微秒级时间戳
    srand(time(NULL)^getpid()^tv.tv_usec); //种随机数种子
    srandom(time(NULL)^getpid()^tv.tv_usec);
    init_genrand64(((long long) tv.tv_sec * 1000000 + tv.tv_usec) ^ getpid()); //【初始化 Redis 自己实现的 Mersenne Twister 64 位随机数发生器】【梅森旋转算法】
    crc64_init(); //初始化 【CRC64 校验表】;Redis 中使用 CRC64 进行数据校验
存储umask掩码

在这里插入图片描述

存储 umask 值。因为 umask(2) 只提供设置并返回旧值的接口,
所以我们必须先设置一次再恢复它。
我们在启动早期执行这一步,是为了避免与可能正在创建文件或目录的线程之间产生竞态条件(race condition)。

umask 是 Unix 系统下用于设置默认新建文件权限的掩码(默认文件0666/目录0777,当 umask = 0022时(2 = 010),文件0644/目录0755)
(0777二进制: 0 111 111 111)

没有纯获取 umask 的接口,而 set 时可以返回之前的值
所以可以通过 umask(umask(0)) 的方式获取~

系统初始化*

在这里插入图片描述
ASAP:as soon as possible 尽快
sentinel mode: 哨兵模式 后面才初始化

strrchr() C标准库函数【string reverse chracter】:从右向左查找字符串中最后一次出现 ‘/’ 的位置,返回该位置的指针。
这里只想要执行的文件名

	uint8_t hashseed[16];
    getRandomBytes(hashseed,sizeof(hashseed));//Redis 自己的随机字节生成函数
    dictSetHashFunctionSeed(hashseed);// 初始化全局字典等结构中的哈希行为

    char *exec_name = strrchr(argv[0], '/');
    if (exec_name == NULL) exec_name = argv[0];
    server.sentinel_mode = checkForSentinelMode(argc,argv, exec_name);// 判断是否是 sentinel 模式,这是 Redis 支持的一种高可用模式。
    
    initServerConfig();// 初始化服务配置(比如监听端口、最大连接数、缓存设置等)

	//初始化 ACL(访问控制列表)子系统 【Redis 的权限控制系统】
    ACLInit(); /* The ACL subsystem must be initialized ASAP because the
                  basic networking code and client creation depends on it. */ // 后面的网络服务和客户端创建依赖它
    moduleInitModulesSystem();// 初始化模块管理框架 // Redis 支持以插件形式加载模块,比如 RedisGraph、RedisAI 等
    connTypeInitialize();// 初始化连接类型  // 用于注册不同类型的连接(TCP、Unix socket、TLS 等)
重启机制:保存执行数据 以便后续重启服务

在这里插入图片描述
“将可执行文件路径和参数安全地保存下来,以便之后能够重新启动服务器。”

    /* Store the executable path and arguments in a safe place in order
     * to be able to restart the server later. */
    server.executable = getAbsolutePath(argv[0]); // 获取当前进程的可执行文件的绝对路径:argv[0] 通常是执行程序的名称或者路径
    server.exec_argv = zmalloc(sizeof(char*)*(argc+1)); // 为参数数组 exec_argv 分配内存
    server.exec_argv[argc] = NULL;
    
    for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]); // 将原始 argv 中的参数逐个复制(深拷贝)到 server.exec_argv 中

redis 的优雅重启机制:一旦重启时,可以直接调用 execv(server.executable, server.exec_argv) 实现“就地重启”。

哨兵模式 sentinel

在这里插入图片描述
我们现在就需要初始化 Sentinel,
因为在 Sentinel 模式下解析配置文件的过程中,会将需要监控的主节点信息填充到 Sentinel 的数据结构中。(所以new出来’数据结构’)

    /* We need to init sentinel right now as parsing the configuration file
     * in sentinel mode will have the effect of populating the sentinel
     * data structures with master nodes to monitor. */
    if (server.sentinel_mode) {	// 检查当前 Redis 是否以哨兵模式运行(通常通过命令行参数或配置文件设置 --sentinel)
        initSentinelConfig(); // 初始化哨兵【配置解析】相关内容。[哨兵配置文件会描述要监控的主节点(master),以及配置项如 down-after-milliseconds、quorum 等。]
        initSentinel(); // 初始化哨兵运行时的核心数据结构(如监控的 master 列表等)。(结合上面的 initSentinelConfig(),最终目的是:在加载配置文件时,把其中的哨兵监控项正确写入内存结构,准备后续运行。)
    }

哨兵模式介绍:
哨兵模式下,Redis 的行为和普通的主从模式不同,它主要用于自动:

  1. 监控(Monitoring):持续【检查】主节点和从节点是否可用;(哨兵模式是基于主从结构进行监控和故障转移的。)
  2. 通知(Notification):当某个节点不可达时,【通知】管理员或其他系统;
    (哨兵周期性地通过 PING 命令检查主从节点的可用性。无响应就判断为节点“主观下线” subjectively down)
  3. 自动故障转移(Automatic Failover):主节点宕机后,自动将某个从节点【升级】为主节点;
    (如果大多数哨兵都认为某个主节点不可达,称为“客观下线”(objectively down))(选举后,哨兵更新集群配置,并通知客户端。)
  4. 服务发现(Configuration Provider):客户端可以通过哨兵【获取】当前的主节点地址。

哨兵的部署架构
通常使用【多个哨兵(>=3 个)组成一个哨兵集群 + 【一个主节点(master)】 + 【多个从节点(slave)】:

sentinel.conf 中配置以告知哨兵谁是主节点:

sentinel monitor <master-name> <ip> <port> <quorum>

<master-name>:主节点的逻辑名称,客户端通过这个名字向 Sentinel 查询主节点地址
<quorum>:判定主节点下线需要多少哨兵确认

主从模式并不一定必须带上哨兵,但哨兵是 Redis 官方推荐的高可用方案之一。
主从模式本身没有自动故障转移能力。

检查 rdb aof

在这里插入图片描述
检查是否需要以 redis-check-rdb 或 redis-check-aof 模式启动。※
我们只是执行对应程序的主函数。
但是这两个程序是 Redis 可执行文件的一部分,
因此可以方便地在加载出错时执行 RDB 文件检查。

就是看执不执行,如果执行,就进入相应的“检查模式”,专门去检测 RDB 或 AOF 文件有没有问题。【官方的检查工具】

    /* Check if we need to start in redis-check-rdb/aof mode. We just execute
     * the program main. However the program is part of the Redis executable
     * so that we can easily execute an RDB check on loading errors. */
    if (strstr(exec_name,"redis-check-rdb") != NULL)
        redis_check_rdb_main(argc,argv,NULL);
    else if (strstr(exec_name,"redis-check-aof") != NULL)
        redis_check_aof_main(argc,argv);

strstr(str1, str2) (string.h) 检查str1里有没有str2

解析命令行参数
linux特有检查
守护进程
	/* Daemonize if needed */
    server.supervised = redisIsSupervised(server.supervised_mode); //supervise监督;判断 Redis 是否受外部服务管理
    int background = server.daemonize && !server.supervised; //只有在不被其他 supervisor 管理,且用户配置要 daemonize 的时候,才会真正后台化。
    if (background) daemonize();
serverLog 日志打印,打印方法
    serverLog(LL_NOTICE, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo");
    serverLog(LL_NOTICE,
        "Redis version=%s, bits=%d, commit=%s, modified=%d, pid=%d, just started",
            REDIS_VERSION,
            (sizeof(long) == 8) ? 64 : 32,
            redisGitSHA1(),
            strtol(redisGitDirty(),NULL,10) > 0,
            (int)getpid());

serverLog是一个宏,在server.h:

#define serverLog(level, ...) do {\
        if (((level)&0xff) < server.verbosity) break;\
        _serverLog(level, __VA_ARGS__);\
    } while(0)

verbosity(冗长,赘述) 日志等级:所以只有 ≥ 日志等级的才会记录。
在这里插入图片描述
... 是可变参数包。
__VA_ARGS__ 表示在使用 serverLog 宏时传入的可变参数部分。功能是像使用函数那样传多个参数进去

_serverLog在上面有声明:

void _serverLog(int level, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));			// GCC 提供的一个编译器扩展,用于 格式校验。


server.c中有实现:
/* Like serverLogRaw() but with printf-alike support. This is the function that
 * is used across the code. The raw version is only used in order to dump
 * the INFO output on crash. */
void _serverLog(int level, const char *fmt, ...) {
    va_list ap;
    char msg[LOG_MAX_LEN];

    va_start(ap, fmt);
    vsnprintf(msg, sizeof(msg), fmt, ap);
    va_end(ap);

    serverLogRaw(level,msg);
}

和 serverLogRaw() 类似,但支持类似 printf 的格式化功能。这个函数(指 _serverLog)是整个代码中最常用的日志输出函数。而 serverLogRaw() 仅在程序崩溃时用于输出 INFO 日志信息的转储。

  • 崩溃时用 Raw:崩溃时内存和栈可能不可靠,vprintf 风格的格式化函数会用到更多资源,容易再度出错;此时使用 Raw 更安全。
  • 平时用格式化版本:开发调试、日志分析时更友好,结构清晰。
/* Low level logging. To use only for very big messages, otherwise
 * serverLog() is to prefer. */
void serverLogRaw(int level, const char *msg) {
    const int syslogLevelMap[] = { LOG_DEBUG, LOG_INFO, LOG_NOTICE, LOG_WARNING };
    const char *c = ".-*#";
    FILE *fp;
    char buf[64];
    int rawmode = (level & LL_RAW);
    int log_to_stdout = server.logfile[0] == '\0';

    level &= 0xff; /* clear flags */
    if (level < server.verbosity) return;

    fp = log_to_stdout ? stdout : fopen(server.logfile,"a");
    if (!fp) return;

    if (rawmode) {
        fprintf(fp,"%s",msg);
    } else {
        int off;
        struct timeval tv;
        int role_char;
        int daylight_active = 0;
        pid_t pid = getpid();

        gettimeofday(&tv,NULL);
        struct tm tm;
        atomicGet(server.daylight_active, daylight_active);
        nolocks_localtime(&tm,tv.tv_sec,server.timezone,daylight_active);
        off = strftime(buf,sizeof(buf),"%d %b %Y %H:%M:%S.",&tm);
        snprintf(buf+off,sizeof(buf)-off,"%03d",(int)tv.tv_usec/1000);
        if (server.sentinel_mode) {
            role_char = 'X'; /* Sentinel. */
        } else if (pid != server.pid) {
            role_char = 'C'; /* RDB / AOF writing child. */
        } else {
            role_char = (server.masterhost ? 'S':'M'); /* Slave or Master. */
        }
        fprintf(fp,"%d:%c %s %c %s\n",
            (int)getpid(),role_char, buf,c[level],msg);
    }
    fflush(fp);

    if (!log_to_stdout) fclose(fp);
    if (server.syslog_enabled) syslog(syslogLevelMap[level], "%s", msg);
}

在这里插入图片描述


t_zset.c
/*-----------------------------------------------------------------------------
 * Sorted set API
 *----------------------------------------------------------------------------*/

/* ZSETs are ordered sets using two data structures to hold the same elements
 * in order to get O(log(N)) INSERT and REMOVE operations into a sorted
 * data structure.
 *
 * The elements are added to a hash table mapping Redis objects to scores.
 * At the same time the elements are added to a skip list mapping scores
 * to Redis objects (so objects are sorted by scores in this "view").
 *
 * Note that the SDS string representing the element is the same in both
 * the hash table and skiplist in order to save memory. What we do in order
 * to manage the shared SDS string more easily is to free the SDS string
 * only in zslFreeNode(). The dictionary has no value free method set.
 * So we should always remove an element from the dictionary, and later from
 * the skiplist.
 *
 * This skiplist implementation is almost a C translation of the original
 * algorithm described by William Pugh in "Skip Lists: A Probabilistic
 * Alternative to Balanced Trees", modified in three ways:
 * a) this implementation allows for repeated scores.
 * b) the comparison is not just by key (our 'score') but by satellite data.
 * c) there is a back pointer, so it's a doubly linked list with the back
 * pointers being only at "level 1". This allows to traverse the list
 * from tail to head, useful for ZREVRANGE. */

ZSET 是一种有序集合,Redis 使用两种数据结构同时存储相同的元素,以便在有序结构中实现 O(log N) 的插入和删除操作。

元素同时存入:
一个哈希表,用来将元素(Redis 对象)映射到分数(score);
一个跳表(skip list),用于将分数映射到对象(以实现排序视图)。

注意,哈希表和跳表中存储的是同一个 SDS 字符串(代表元素;simple dynamic string),以节省内存。为了方便管理共享的 SDS 字符串,只在 zslFreeNode() 中释放这块内存。哈希表本身不设置自动释放函数(value free method),因此删除元素时必须先从字典(hash)中删,再从跳表中删。
(本人读英文就没理解到这个意思,以为全局相同字符串都是静态的,,“representing the element”作为主语多好理解)

Redis 的跳表实现几乎是 William Pugh 论文中算法的 C 语言翻译,但有三个修改:

  • 支持重复分数 (不同元素可以有相同分数)
  • 不仅比较分数,还比较元素 (分数相同时可以进一步比较元素)
  • level 1 上有后向指针,支持从尾到头遍历 (ZREVRANGE 命令)

论文链接:《Skip Lists: A Probabilistic Alternative to Balanced Trees》

GEO

在这里插入图片描述

Morton Code

geohash.c

下面方法把32位的纬度经度在64位上交替排列。

通过二分的方法逐步分散
(如B[4] 这一步把x的前16位和后16位分开了)

/* Interleave lower bits of x and y, so the bits of x
 * are in the even positions and bits from y in the odd;
 * x and y must initially be less than 2**32 (4294967296).
 * From:  https://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN
 */
static inline uint64_t interleave64(uint32_t xlo, uint32_t ylo) {
    static const uint64_t B[] = {0x5555555555555555ULL, 0x3333333333333333ULL,
                                 0x0F0F0F0F0F0F0F0FULL, 0x00FF00FF00FF00FFULL,
                                 0x0000FFFF0000FFFFULL};
    static const unsigned int S[] = {1, 2, 4, 8, 16};

    uint64_t x = xlo;
    uint64_t y = ylo;

    x = (x | (x << S[4])) & B[4];
    y = (y | (y << S[4])) & B[4];

    x = (x | (x << S[3])) & B[3];
    y = (y | (y << S[3])) & B[3];

    x = (x | (x << S[2])) & B[2];
    y = (y | (y << S[2])) & B[2];

    x = (x | (x << S[1])) & B[1];
    y = (y | (y << S[1])) & B[1];

    x = (x | (x << S[0])) & B[0];
    y = (y | (y << S[0])) & B[0];

    return x | (y << 1);
}
查询坐标周围的单位

georadiusGeneric

geo.c

#define SORT_NONE 0
#define SORT_ASC 1
#define SORT_DESC 2

#define RADIUS_COORDS (1<<0)    /* Search around coordinates. */
#define RADIUS_MEMBER (1<<1)    /* Search around member. */
#define RADIUS_NOSTORE (1<<2)   /* Do not accept STORE/STOREDIST option. */
#define GEOSEARCH (1<<3)        /* GEOSEARCH command variant (different arguments supported) */
#define GEOSEARCHSTORE (1<<4)   /* GEOSEARCHSTORE just accept STOREDIST option */

//0.GEO命令:

/* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]
 *                               [COUNT count [ANY]] [STORE key|STOREDIST key]
 * GEORADIUSBYMEMBER key member radius unit ... options ...
 * GEOSEARCH key [FROMMEMBER member] [FROMLONLAT long lat] [BYRADIUS radius unit]
 *               [BYBOX width height unit] [WITHCOORD] [WITHDIST] [WITHASH] [COUNT count [ANY]] [ASC|DESC]
 * GEOSEARCHSTORE dest_key src_key [FROMMEMBER member] [FROMLONLAT long lat] [BYRADIUS radius unit]
 *               [BYBOX width height unit] [COUNT count [ANY]] [ASC|DESC] [STOREDIST]
 *  */
void georadiusGeneric(client *c, int srcKeyIndex, int flags) {
    robj *storekey = NULL;
    int storedist = 0; /* 0 for STORE, 1 for STOREDIST. */

	//1.GEO是一个zset,获取
	
    /* Look up the requested zset */
    kvobj *zobj = lookupKeyRead(c->db, c->argv[srcKeyIndex]);
    if (checkType(c, zobj, OBJ_ZSET)) return;

	//2.根据flags判断,如 RADIUS_COORDS → 中心点是 (x, y) 坐标
    
    /* Find long/lat to use for radius or box search based on inquiry type */
    int base_args;
    GeoShape shape = {0};
    if (flags & RADIUS_COORDS) {
        /* GEORADIUS or GEORADIUS_RO */
        base_args = 6;
        shape.type = CIRCULAR_TYPE;
        if (extractLongLatOrReply(c, c->argv + 2, shape.xy) == C_ERR) return;
        if (extractDistanceOrReply(c, c->argv+base_args-2, &shape.conversion, &shape.t.radius) != C_OK) return;
    } else if ((flags & RADIUS_MEMBER) && !zobj) {
        /* We don't have a source key, but we need to proceed with argument
         * parsing, so we know which reply to use depending on the STORE flag. */
        base_args = 5;
    } else if (flags & RADIUS_MEMBER) {
        /* GEORADIUSBYMEMBER or GEORADIUSBYMEMBER_RO */
        base_args = 5;
        shape.type = CIRCULAR_TYPE;
        robj *member = c->argv[2];
        if (longLatFromMember(zobj, member, shape.xy) == C_ERR) {
            addReplyError(c, "could not decode requested zset member");
            return;
        }
        if (extractDistanceOrReply(c, c->argv+base_args-2, &shape.conversion, &shape.t.radius) != C_OK) return;
    } else if (flags & GEOSEARCH) {
        /* GEOSEARCH or GEOSEARCHSTORE */
        base_args = 2;
        if (flags & GEOSEARCHSTORE) {
            base_args = 3;
            storekey = c->argv[1];
        }
    } else {
        addReplyError(c, "Unknown georadius search type");
        return;
    }

	//3. 解析其他参数

    /* Discover and populate all optional parameters. */
    int withdist = 0, withhash = 0, withcoords = 0;
    int frommember = 0, fromloc = 0, byradius = 0, bybox = 0;
    int sort = SORT_NONE;
    int any = 0; /* any=1 means a limited search, stop as soon as enough results were found. */
    long long count = 0;  /* Max number of results to return. 0 means unlimited. */
    if (c->argc > base_args) {
        int remaining = c->argc - base_args;
        for (int i = 0; i < remaining; i++) {
            char *arg = c->argv[base_args + i]->ptr;
            if (!strcasecmp(arg, "withdist")) {
                withdist = 1;
            } else if (!strcasecmp(arg, "withhash")) {
                withhash = 1;
            } else if (!strcasecmp(arg, "withcoord")) {
                withcoords = 1;
            } else if (!strcasecmp(arg, "any")) {
                any = 1;
            } else if (!strcasecmp(arg, "asc")) {
                sort = SORT_ASC;
            } else if (!strcasecmp(arg, "desc")) {
                sort = SORT_DESC;
            } else if (!strcasecmp(arg, "count") && (i+1) < remaining) {
                if (getLongLongFromObjectOrReply(c, c->argv[base_args+i+1],
                                                 &count, NULL) != C_OK) return;
                if (count <= 0) {
                    addReplyError(c,"COUNT must be > 0");
                    return;
                }
                i++;
            } else if (!strcasecmp(arg, "store") &&
                       (i+1) < remaining &&
                       !(flags & RADIUS_NOSTORE) &&
                       !(flags & GEOSEARCH))
            {
                storekey = c->argv[base_args+i+1];
                storedist = 0;
                i++;
            } else if (!strcasecmp(arg, "storedist") &&
                       (i+1) < remaining &&
                       !(flags & RADIUS_NOSTORE) &&
                       !(flags & GEOSEARCH))
            {
                storekey = c->argv[base_args+i+1];
                storedist = 1;
                i++;
            } else if (!strcasecmp(arg, "storedist") &&
                       (flags & GEOSEARCH) &&
                       (flags & GEOSEARCHSTORE))
            {
                storedist = 1;
            } else if (!strcasecmp(arg, "frommember") &&
                      (i+1) < remaining &&
                      flags & GEOSEARCH &&
                      !fromloc)
            {
                /* No source key, proceed with argument parsing and return an error when done. */
                if (zobj == NULL) {
                    frommember = 1;
                    i++;
                    continue;
                }

                if (longLatFromMember(zobj, c->argv[base_args+i+1], shape.xy) == C_ERR) {
                    addReplyError(c, "could not decode requested zset member");
                    return;
                }
                frommember = 1;
                i++;
            } else if (!strcasecmp(arg, "fromlonlat") &&
                       (i+2) < remaining &&
                       flags & GEOSEARCH &&
                       !frommember)
            {
                if (extractLongLatOrReply(c, c->argv+base_args+i+1, shape.xy) == C_ERR) return;
                fromloc = 1;
                i += 2;
            } else if (!strcasecmp(arg, "byradius") &&
                       (i+2) < remaining &&
                       flags & GEOSEARCH &&
                       !bybox)
            {
                if (extractDistanceOrReply(c, c->argv+base_args+i+1, &shape.conversion, &shape.t.radius) != C_OK)
                    return;
                shape.type = CIRCULAR_TYPE;
                byradius = 1;
                i += 2;
            } else if (!strcasecmp(arg, "bybox") &&
                       (i+3) < remaining &&
                       flags & GEOSEARCH &&
                       !byradius)
            {
                if (extractBoxOrReply(c, c->argv+base_args+i+1, &shape.conversion, &shape.t.r.width,
                        &shape.t.r.height) != C_OK) return;
                shape.type = RECTANGLE_TYPE;
                bybox = 1;
                i += 3;
            } else {
                addReplyErrorObject(c,shared.syntaxerr);
                return;
            }
        }
    }

    /* Trap options not compatible with STORE and STOREDIST. */
    if (storekey && (withdist || withhash || withcoords)) {
        addReplyErrorFormat(c,
            "%s is not compatible with WITHDIST, WITHHASH and WITHCOORD options",
            flags & GEOSEARCHSTORE? "GEOSEARCHSTORE": "STORE option in GEORADIUS");
        return;
    }

    if ((flags & GEOSEARCH) && !(frommember || fromloc)) {
        addReplyErrorFormat(c,
            "exactly one of FROMMEMBER or FROMLONLAT can be specified for %s",
            (char *)c->argv[0]->ptr);
        return;
    }

    if ((flags & GEOSEARCH) && !(byradius || bybox)) {
        addReplyErrorFormat(c,
            "exactly one of BYRADIUS and BYBOX can be specified for %s",
            (char *)c->argv[0]->ptr);
        return;
    }

    if (any && !count) {
        addReplyError(c, "the ANY argument requires COUNT argument");
        return;
    }

    /* Return ASAP when src key does not exist. */
    if (zobj == NULL) {
        if (storekey) {
            /* store key is not NULL, try to delete it and return 0. */
            if (dbDelete(c->db, storekey)) {
                signalModifiedKey(c, c->db, storekey);
                notifyKeyspaceEvent(NOTIFY_GENERIC, "del", storekey, c->db->id);
                server.dirty++;
            }
            addReply(c, shared.czero);
        } else {
            /* Otherwise we return an empty array. */
            addReply(c, shared.emptyarray);
        }
        return;
    }

    /* COUNT without ordering does not make much sense (we need to
     * sort in order to return the closest N entries),
     * force ASC ordering if COUNT was specified but no sorting was
     * requested. Note that this is not needed for ANY option. */
    if (count != 0 && sort == SORT_NONE && !any) sort = SORT_ASC;


	//4.计算 geohash 范围

    /* Get all neighbor geohash boxes for our radius search */
    GeoHashRadius georadius = geohashCalculateAreasByShapeWGS84(&shape);

	//5.查找匹配点

    /* Search the zset for all matching points */
    geoArray *ga = geoArrayCreate();
    membersOfAllNeighbors(zobj, &georadius, &shape, ga, any ? count : 0);

    /* If no matching results, the user gets an empty reply. */
    if (ga->used == 0 && storekey == NULL) {
        addReply(c,shared.emptyarray);
        geoArrayFree(ga);
        return;
    }

    long result_length = ga->used;
    long returned_items = (count == 0 || result_length < count) ?
                          result_length : count;
    long option_length = 0;

    /* Process [optional] requested sorting */
    if (sort != SORT_NONE) {
        int (*sort_gp_callback)(const void *a, const void *b) = NULL;
        if (sort == SORT_ASC) {
            sort_gp_callback = sort_gp_asc;
        } else if (sort == SORT_DESC) {
            sort_gp_callback = sort_gp_desc;
        }

        if (returned_items == result_length) {
            qsort(ga->array, result_length, sizeof(geoPoint), sort_gp_callback);
        } else {
            pqsort(ga->array, result_length, sizeof(geoPoint), sort_gp_callback,
                0, (returned_items - 1));
        }
    }

    if (storekey == NULL) {
        /* No target key, return results to user. */

        /* Our options are self-contained nested multibulk replies, so we
         * only need to track how many of those nested replies we return. */
        if (withdist)
            option_length++;

        if (withcoords)
            option_length++;

        if (withhash)
            option_length++;

        /* The array len we send is exactly result_length. The result is
         * either all strings of just zset members  *or* a nested multi-bulk
         * reply containing the zset member string _and_ all the additional
         * options the user enabled for this request. */
        addReplyArrayLen(c, returned_items);

        /* Finally send results back to the caller */
        int i;
        for (i = 0; i < returned_items; i++) {
            geoPoint *gp = ga->array+i;
            gp->dist /= shape.conversion; /* Fix according to unit. */

            /* If we have options in option_length, return each sub-result
             * as a nested multi-bulk.  Add 1 to account for result value
             * itself. */
            if (option_length)
                addReplyArrayLen(c, option_length + 1);

            addReplyBulkSds(c,gp->member);
            gp->member = NULL;

            if (withdist)
                addReplyDoubleDistance(c, gp->dist);

            if (withhash)
                addReplyLongLong(c, gp->score);

            if (withcoords) {
                addReplyArrayLen(c, 2);
                addReplyDouble(c,gp->longitude);
                addReplyDouble(c,gp->latitude);
            }
        }
    } else {
        /* Target key, create a sorted set with the results. */
        robj *zobj;
        zset *zs;
        int i;
        size_t maxelelen = 0, totelelen = 0;

        if (returned_items) {
            zobj = createZsetObject();
            zs = zobj->ptr;
        }

        for (i = 0; i < returned_items; i++) {
            zskiplistNode *znode;
            geoPoint *gp = ga->array+i;
            gp->dist /= shape.conversion; /* Fix according to unit. */
            double score = storedist ? gp->dist : gp->score;
            size_t elelen = sdslen(gp->member);

            if (maxelelen < elelen) maxelelen = elelen;
            totelelen += elelen;
            znode = zslInsert(zs->zsl,score,gp->member);
            serverAssert(dictAdd(zs->dict,gp->member,&znode->score) == DICT_OK);
            gp->member = NULL;
        }

        if (returned_items) {
            zsetConvertToListpackIfNeeded(zobj,maxelelen,totelelen);
            setKey(c,c->db,storekey,&zobj,0);
            notifyKeyspaceEvent(NOTIFY_ZSET,flags & GEOSEARCH ? "geosearchstore" : "georadiusstore",storekey,
                                c->db->id);
            server.dirty += returned_items;
        } else if (dbDelete(c->db,storekey)) {
            signalModifiedKey(c,c->db,storekey);
            notifyKeyspaceEvent(NOTIFY_GENERIC,"del",storekey,c->db->id);
            server.dirty++;
        }
        addReplyLongLong(c, returned_items);
    }
    geoArrayFree(ga);
}

搜索逻辑

其实最关心怎么找邻居格子,尤其是刚开始就没划分到一起的。实现方法是geohashNeighbors

#define CIRCULAR_TYPE 1
#define RECTANGLE_TYPE 2
typedef struct {
    int type; /* search type */
    //经纬度
    double xy[2]; /* search center point, xy[0]: lon, xy[1]: lat */
    double conversion; /* km: 1000 */
    double bounds[4]; /* bounds[0]: min_lon, bounds[1]: min_lat
                       * bounds[2]: max_lon, bounds[3]: max_lat */
    union {
        /* CIRCULAR_TYPE */
        double radius;
        /* RECTANGLE_TYPE */
        struct {
            double height;
            double width;
        } r;
    } t;
} GeoShape;

typedef struct {
    GeoHashBits hash; //中心点 geohash
    GeoHashArea area; //中心点周围的 8 个 geohash 区域
    GeoHashNeighbors neighbors;
} GeoHashRadius;
/* Calculate a set of areas (center + 8) that are able to cover a range query
 * for the specified position and shape (see geohash.h GeoShape).
 * the bounding box saved in shaple.bounds */
GeoHashRadius geohashCalculateAreasByShapeWGS84(GeoShape *shape) {
    GeoHashRange long_range, lat_range;
    GeoHashRadius radius;
    GeoHashBits hash;
    GeoHashNeighbors neighbors;
    GeoHashArea area;
    double min_lon, max_lon, min_lat, max_lat;
    int steps;

	//1.最小包围矩形
    geohashBoundingBox(shape, shape->bounds);
    min_lon = shape->bounds[0];
    min_lat = shape->bounds[1];
    max_lon = shape->bounds[2];
    max_lat = shape->bounds[3];

    double longitude = shape->xy[0];
    double latitude = shape->xy[1];
    /* radius_meters is calculated differently in different search types:
     * 1) CIRCULAR_TYPE, just use radius.
     * 2) RECTANGLE_TYPE, we use sqrt((width/2)^2 + (height/2)^2) to
     * calculate the distance from the center point to the corner */
    
    //2.计算搜索半径
	double radius_meters = shape->type == CIRCULAR_TYPE ? shape->t.radius :
            sqrt((shape->t.r.width/2)*(shape->t.r.width/2) + (shape->t.r.height/2)*(shape->t.r.height/2));
    radius_meters *= shape->conversion;

	//3.估算 geohash 精度
    steps = geohashEstimateStepsByRadius(radius_meters,latitude);

	//4.编码中心点,获取8个邻居geohash
    geohashGetCoordRange(&long_range,&lat_range);
    geohashEncode(&long_range,&lat_range,longitude,latitude,steps,&hash);
    geohashNeighbors(&hash,&neighbors);
    geohashDecode(long_range,lat_range,hash,&area);

    /* Check if the step is enough at the limits of the covered area.
     * Sometimes when the search area is near an edge of the
     * area, the estimated step is not small enough, since one of the
     * north / south / west / east square is too near to the search area
     * to cover everything. */
    int decrease_step = 0;
    {
        GeoHashArea north, south, east, west;

        geohashDecode(long_range, lat_range, neighbors.north, &north);
        geohashDecode(long_range, lat_range, neighbors.south, &south);
        geohashDecode(long_range, lat_range, neighbors.east, &east);
        geohashDecode(long_range, lat_range, neighbors.west, &west);

        if (north.latitude.max < max_lat) 
            decrease_step = 1;
        if (south.latitude.min > min_lat) 
            decrease_step = 1;
        if (east.longitude.max < max_lon) 
            decrease_step = 1;
        if (west.longitude.min > min_lon)  
            decrease_step = 1;
    }

    if (steps > 1 && decrease_step) {
        steps--;
        geohashEncode(&long_range,&lat_range,longitude,latitude,steps,&hash);
        geohashNeighbors(&hash,&neighbors);
        geohashDecode(long_range,lat_range,hash,&area);
    }

    /* Exclude the search areas that are useless. */
    if (steps >= 2) {
        if (area.latitude.min < min_lat) {
            GZERO(neighbors.south);
            GZERO(neighbors.south_west);
            GZERO(neighbors.south_east);
        }
        if (area.latitude.max > max_lat) {
            GZERO(neighbors.north);
            GZERO(neighbors.north_east);
            GZERO(neighbors.north_west);
        }
        if (area.longitude.min < min_lon) {
            GZERO(neighbors.west);
            GZERO(neighbors.south_west);
            GZERO(neighbors.north_west);
        }
        if (area.longitude.max > max_lon) {
            GZERO(neighbors.east);
            GZERO(neighbors.south_east);
            GZERO(neighbors.north_east);
        }
    }
    radius.hash = hash;
    radius.neighbors = neighbors;
    radius.area = area;
    return radius;
}
void geohashNeighbors(const GeoHashBits *hash, GeoHashNeighbors *neighbors) {
    neighbors->east = *hash;
    neighbors->west = *hash;
    neighbors->north = *hash;
    neighbors->south = *hash;
    neighbors->south_east = *hash;
    neighbors->south_west = *hash;
    neighbors->north_east = *hash;
    neighbors->north_west = *hash;

    geohash_move_x(&neighbors->east, 1);
    geohash_move_y(&neighbors->east, 0);

    geohash_move_x(&neighbors->west, -1);
    geohash_move_y(&neighbors->west, 0);

    geohash_move_x(&neighbors->south, 0);
    geohash_move_y(&neighbors->south, -1);

    geohash_move_x(&neighbors->north, 0);
    geohash_move_y(&neighbors->north, 1);

    geohash_move_x(&neighbors->north_west, -1);
    geohash_move_y(&neighbors->north_west, 1);

    geohash_move_x(&neighbors->north_east, 1);
    geohash_move_y(&neighbors->north_east, 1);

    geohash_move_x(&neighbors->south_east, 1);
    geohash_move_y(&neighbors->south_east, -1);

    geohash_move_x(&neighbors->south_west, -1);
    geohash_move_y(&neighbors->south_west, -1);
}

static void geohash_move_x(GeoHashBits *hash, int8_t d) {
    if (d == 0)
        return;

    uint64_t x = hash->bits & 0xaaaaaaaaaaaaaaaaULL;
    uint64_t y = hash->bits & 0x5555555555555555ULL;

    uint64_t zz = 0x5555555555555555ULL >> (64 - hash->step * 2);

    if (d > 0) {
        x = x + (zz + 1);
    } else {
        x = x | zz;
        x = x - (zz + 1);
    }

    x &= (0xaaaaaaaaaaaaaaaaULL >> (64 - hash->step * 2));
    hash->bits = (x | y);
}

static void geohash_move_y(GeoHashBits *hash, int8_t d) {
    if (d == 0)
        return;

    uint64_t x = hash->bits & 0xaaaaaaaaaaaaaaaaULL;
    uint64_t y = hash->bits & 0x5555555555555555ULL;

    uint64_t zz = 0xaaaaaaaaaaaaaaaaULL >> (64 - hash->step * 2);
    if (d > 0) {
        y = y + (zz + 1);
    } else {
        y = y | zz;
        y = y - (zz + 1);
    }
    y &= (0x5555555555555555ULL >> (64 - hash->step * 2));
    hash->bits = (x | y);
}

给 经度x +1,我们可以把 空位先填上,直接+1即可。
(step只是本次编码用到的精度。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值