百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Redis模块学习(下):Why

mhr18 2024-11-23 19:13 25 浏览 0 评论

Why

在Redis源码学习之模块(上)这篇文章中提到过,所有的Redis模块的入口函数都是RedisModule_OnLoad()方法,那么这个方法什么时候被调用呢?传入的参数是什么样的呢?自定义的模块命令的实现是如何被调用的?这篇文章就一一为你揭秘。

模块加载

在前文中,我们提到了在Redis中有两种方式加载自定义的模块,一种是在Redis运行之前在配置文件中使用loadmodule /path/to/module.so指令,另一种是在Redis的运行时使用module load /path/to/module.so命令来加载。

配置文件加载

既然是从配置文件加载,那么入口肯定是读取配置文件。server.c的main()函数简化(只保留了跟模块相关的代码)如下 :

在main()中会调用loadServerConfig()方法从配置文件中读取配置信息:

loadServerConfig()调用loadServerConfigFromString()完成具体的配置信息的读取,loadServerConfigFromString()的代码如下:

可以看到,如果发现配置文件中的某一行配置的第一个参数是loadmodule,则会调用queueLoadModule()方法完成加载模块的信息的收集,

在queueLoadModule()方法会把需要加载的模块的信息(模块的路径、模块的参数、参数个数)封装成moduleLoadQueueEntry结构体,然后保存在server.loadmodule_queue列表中。

在读取完配置文件之后,main()会调用moduleLoadFromQueue()方法从队列找那个加载模块。

在moduleLoadFromQueue()方法中会循环遍历server.loadmodule_queue列表(配置文件中配置的需要加载的模块都保存在这个列表上),依次调用moduleLoad()方法来完成实际的模块加载动作。

在分析moduleLoad()方法之前,先来了解下在*nix平台上如何加载动态链接库。

在*nix平台上,使用gcc把源码编译成动态链接库的时候,需要使用-shared参数,一般同时也需要加上-fPIC选项,其中-fPIC选项的作用是:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的,所以动态载入时是通过代码拷贝的方式来满足不同的调用,而不能达到真正的代码段共享的目的。

同时*inx平台提供了dl*系列函数来使用动态链接库。

dlopen以指定模式打开指定的动态链接库文件,并返回一个句柄给调用线程,dlerror()返回出现的错误, dlsym通过句柄和连接符名称获取函数名和变量名,并返回对应的指针,dlclose()来卸载打开的库。其中dllopen支持的模式有:

  • RTLD_LAZY: 暂缓决定,等有需要时再解出符号。
  • RTLD_NOW: 立即决定,返回前解除所有未决定的符号。

继续来看moduleLoad()方法。

在moduleLoad()方法中,首先会调用dlopen()打开指定路径的动态链接库,获得句柄,然后调用dlsym()方法来查找是否存在RedisModule_OnLoad方法。如果存在的话,则获取RedisModule_OnLoad()函数的指针保存在onload指针上,接着调用onload()方法,实际上就是调用动态链接库中的RedisModule_OnLoad()方法。

这里就解释了为什么所有的Redis模块都需要实现RedisModule_OnLoad()方法:Redis模块系统默认会去查找动态链接库里面的RedisModule_OnLoad()方法然后进行调用。

如果onload()(也就是RedisModule_OnLoad())调用没有出错的话,接下来会调用dictAdd()方法把新加载的模块保存在modules这个静态字典上,key为模块的名字,值为对应的模块对象的指针。接着释放内存,返回结果。

运行时加载

运行时加载是指在Redis服务器已经启动成功之后执行module load /path/to/module.so指令来加载模块。执行上述命令的时候实际上是调用moduleCommand()方法:

从代码中可以很清楚地看到,moduleCommand()方法发现需要执行load操作时会直接调用moduleLoad()方法来执行具体的模块加载操作。

RedisModule_Init()

在Redis源码学习之模块(上)这篇文章中提到过在RedisModule_OnLoad()方法中首先要调用RedisModule_Init()方法,那么为什么呢?RedisModule_Init()主要执行什么操作呢?

别急,且听我娓娓道来。

首先看RedisModule_Init()方法,在redismodule.h文件中定义:

在RedisModule_Init()方法中首先把传入的RedisModuleCtx对象ctx的首地址赋值给getapifuncptr指针,那么ctx的首地址指向哪里呢?

ctx对象是在调用RedisModule_OnLoad()方法时传入的,初始化为REDISMODULE_CTX_INIT

可以看到,ctx的首地址指向RM_GetApi()方法,RM_GetApi定义如下:

RM_GetApi方法从server.moduleapi字典中查找对应的方法,那么server.moduleapi字段的数据是在什么时候赋值的呢?

在module.c文件中有一个函数moduleRegisterCoreAPI():

在这个方法中会初始化server.moduleapi列表,并且会调用REGISTER_API宏来向moduleapi字典中添加需要导出的方法,REGISTER_API宏定义如下:

这个宏定义会把传入的名称进行转换,添加RedisModule_前缀作为方法名,RM_前缀的方法作为回调方法(比如传入的名称是Alloc,则转换之后的方法名是RedisModule_Alloc,对应的回调方法为RM_Alloc)调用moduleRegisterApi方法,

moduleRegisterApi()直接往moduleapi字典中添加数据。 那么moduleRegisterCoreAPI()方法是在什么时候被调用的呢?

答:在server.c的main()函数中,调用initServerConfig()初始化服务端配置之后,会新调用moduleInitModuleSystem()方法,

在moduleInitModulesSystem()方法中会调用moduleRegisterCoreAPI()完成导出方法的注册。

先总结一下:

  1. Redis服务器在启动的时候会调用moduleRegisterCoreAPI()方法,把Redis模块所有需要导出的方法注册到server.moduleapi字典上。
  2. 在调用RedisModule_OnLoad()方法时传入的RedisModuleCtx对象ctx的首地址指针指向RM_GetApi()方法,而RM_GetApi()方法的作用是从server.moduleapi字典中查找指定名称的方法。

现在重新回到RedisModule_Init()方法中来:

在把ctx的首地址指针赋值给RedisModule_GetApi(函数指针)之后,接下来是一长串的REDISMODULE_GET_API()调用。REDISMODULE_GET_API是个宏定义:

这个宏定义会把传入的名字加上RedisModule_前缀,然后调用RedisModule_GetApi()方法查找对应的方法,如果查找到的话,把查找到的结果保存在以RedisModule_为前缀的函数指针上,亦即导出了一个API。

假如传入REDISMODULE_GET_API()的是Realloc,则最终会调用RedisModule_GetApi("RedisModule_Realloc",(void**)&RedisModule_Realloc)。 其效果就是在server.moduleapi字典中查找方法名为RedisModule_Realloc的方法,如果找到的话,则会把对应的函数指针赋值给RedisModule_Realloc字段上。

在执行完这句代码之后,RedisModule_Realloc实际上就是指向了RM_Realloc()方法。

换句话说,RM_GetApi()方法和REDISMODULE_GET_API()宏完成了Redis对外导出的方法的方法名称的映射:

模块开发中可使用的API Redis本身的实现 RedisModule_IsModuleNameBusy RM_IsModuleNameBusy RedisModule_SetModuleAttribs RM_SetModuleAttribs ... ...

在导出了一系列API之后,RedisModule_Init()方法紧接着会调用RedisModule_IsModuleNameBusy()(实际上就是RM_IsModuleNameBusy())判断传入的模块名称是否已经存在,如果已经存在的话则直接报错返回,说明模块名称不能重复。 接着会继续调用RedisModule_SetModuleAttribs()(RM_SetModuleAttribs())完成RedisModule对象的的初始化:

在这个方法中会初始化RedisModule对象,并把指针保存在ctx->module字段中。

总结一下,在RedisModule_Init()方法中,主要完成了下面两件事:

  1. 导出了Redis模块系统提供的API。
  2. 完成RedisModule对象的初始化并保存在上下文对象RedisModuleCtx的module字段中。

RedisModule_CreateCommand

在RedisModule_OnLoad()实现中,调用完RedisModule_Init()之后紧接着调用了RedisModule_CreateCommand()来注册自定义的命令。

RedisModule_CreateCommand()实际上指向RM_CreateCommand()方法:RM_CreateCommand()主要是初始化RedisModuleCommandProxy对象

在进行了相应的参数检查之后,就是对RedisModuleCommandProxy对象进行初始化,RedisModuleCommandProxy表示一个Redis模块命令的代理,用来把RedisModule、模块的回调实现跟RedisComand关联在一起。结构如下:

对RedisModuleCommandProxy对象的初始化,主要就是生成RedisCommand对象,如果熟悉Redis源码的同学,对这个数据结构应该很熟悉,RedisCommand代表一个Redis的命令,每个RedisCommand对象都有个字段proc指向命令的回调方法,所有的Redis命令实际上最终都是调用proc指向的函数。

在RM_CreateCommand()中生成的RedisCommand对象的proc函数指针被赋值为RedisModuleCommandDispatcher。也就是说所有的模块命令执行的时候,最终都是调用RedisModuleCommandDispatcher()。

同时,生成的RedisModuleCommandDispatcher对象的指针被强制转换成redisGetKeysProc*类型保存在RedisCommand的getkeys_proc字段中。

其实这里的实现有一些巧妙:getkeys_proc指向的redisGetKeysProc类型的函数本意是用来从命令行参数中获取key,(一般只有在firstkey、lastkey和keystep这几个参数都不确定的情况下才需要使用,大部分命令都是NULL)。而在这里的实现中,把RedisModuleCommandDispatcher对象强制转换成redisGetKeysProc*保存在getkeys_proc字段中,相当于强制把RedisComand对象跟生成的RedisModuleCommandProxy关联在一起了(RedisModuleCommandProxy对象的rediscmd字段只是把RedisModuleCommandProxy跟RedisCommand进行了单向的关联)。因为当执行命令的时候,只能根据命令名称定位到对应的RedisCommand对象,而自定义的模块的回调保存在RedisModuleCommandProxy对象的func字段中,因此需要能够根据RedisCommand对象回溯到func函数。 这里巧妙的地方在于复用了getkeys_proc字段,把这个字段当做一个指针容器,用来关联RedisCommand和RedisModuleCommandProxy。

在完成RedisModuleCommandProxy的初始化之后,会调用dictAdd()把新生成的RedisCommand对象保存到server.commands字典中,熟悉Redis源码的同学应该都知道,这个字典中保存了所有的Redis支持的命令,在接收到命令请求的时候都需要根据命令名称到server.commands字段中查找对应的实现,然后执行proc()。

在执行完RM_CreateCommand()方法之后,内存中的结构如下:

前面说了,所有的Redis的命令,实际上都是执行proc()方法,而自定义的模块命令的proc()方法实际上指向RedisModuleCommandDispatcher()。RedisModuleCommandDispatcher 实现如下:

在RedisModuleCommandDispatcher()的实现中,首先会把getkeys_proc字段指向的数据强类型转换成RedisModuleCommandProxy对象,然后调用RedisModuleCommandProxy对象的func函数指针指向的方法。而func指针正是指向了调用RedisModule_CreateCommand()时传入的模块命令的回调函数,在本例中,实际上就是执行ListExtendFilter_RedisCommand()方法。

因此,执行自定义模块命令的时候实际上执行的就是模块实现的回调方法。

Reference

  1. Redis 5.X under the hood: 3 — Writing a Redis Module
  2. Redis Modules: an introduction to the API
  3. Modules API reference

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: