官网下载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 的行为和普通的主从模式不同,它主要用于自动:
- 监控(Monitoring):持续【检查】主节点和从节点是否可用;(哨兵模式是基于主从结构进行监控和故障转移的。)
- 通知(Notification):当某个节点不可达时,【通知】管理员或其他系统;
(哨兵周期性地通过 PING 命令检查主从节点的可用性。无响应就判断为节点“主观下线” subjectively down) - 自动故障转移(Automatic Failover):主节点宕机后,自动将某个从节点【升级】为主节点;
(如果大多数哨兵都认为某个主节点不可达,称为“客观下线”(objectively down))(选举后,哨兵更新集群配置,并通知客户端。) - 服务发现(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只是本次编码用到的精度。)