源码文件简介
文件 | 功能 | 备注 |
adlist.h/adlist.c | 双向链表 | |
ae.h/ae.c | 事件驱动 | |
ae_epoll.c | epoll接口, | Linux下的IO接口 |
ae_kqueue.c | kqueue接口 | Freebsd |
ae_select.c | select接口 | Windows |
ae_evport.c | event ports接口 | Illumos(OpenSolaris的衍生版本) |
anet.h/ anet.c | 网络处理 | 为Server/Client通信的基础封装 |
aof.c | AOF文件相关处理 | |
asciilogo.h | Redis的字符logo | |
bio.h/ bio.c | Background I/O 服务 | |
bitops.c | Bit操做函数 | |
config.h/ config.c | 配置文件解析 | |
crc64.c | crc64算法 | |
db.c | DB处理 | |
debug.c | 用于调试使用 | |
dict.h/dict.c | hash表 | |
endianconv.h/endianconv.c | 大小端转换 | |
fmacros.h | 用于Mac下的兼容性处理 | |
help.h | 命令的提示信息 | |
intset.h/intset.c | 转换为数字类型数据 | |
lzf.h/lzf_c.c | LZF压缩算法 | 用于本地数据库的保存 |
lzf_d.c | LZF压缩算法 | 用于本地数据库的保存 |
lzfP.h | LZF压缩算法 | 用于本地数据库的保存 |
memtest.c | | |
migrate.c | DUMP, RESTORE and MIGRATE 命令 | |
multi.c | 事务 | |
networking.c | 读取、解析和处理客户端命令 | 网络协议传输方法定义 |
object.c | 各种对像的创建与销毁,string、list、set、zset、hash | |
pqsort.h/pqsort.c | 排序算法 | |
pubsub.c | 发布-订阅系统 | |
rand.h/rand.c | 随机数产生 | |
rdb.h/rdb.c | redis数据文件处理 | |
redisassert.h | 断言 | |
redis-benchmark.c | redis性能测试 | |
redis-check-aof.c | 用于更新日志检查的实现 | |
redis-check-dump.c | 用于本地数据库检查的实现 | |
redis-cli.c | 客户端程序 | |
redis.c | redis的主文件 | main函数 |
redis.h | redis的主文件头 | |
release.c | 用于发布使用 | |
replication.c | 数据同步master-slave | |
rio.h/rio.c | Rio对象 | |
scripting.c | 脚本 | |
sds.h/sds.c | 动态字符串 | |
sentinel.c | 警戒模式 | |
sha1.h/sha1.c | sha算法的实现 | |
slowlog.h/slowlog.c | 慢日志 | |
solarisfixes.h | Solaris系统的兼容性实现 | |
sort.c | 用于list、set、zset排序 | |
syncio.c | 用于同步Socket和文件I/O操作 | |
t_hash.c | hash类型处理 | |
t_list.c | list类型处理 | |
t_set.c | set类型处理 | |
t_string.c | string类型处理 | |
t_zset.c | 有序sort | |
testhelp.h | 一个C风格的小型测试框架 | |
util.h/util.c | 通用工具 | |
version.h | Redis版本号定义 | |
ziplist.h/ziplist.c | 压缩列表 | |
zipmap.h/zipmap.c | 压缩hash | |
zmalloc.h/zmalloc.c | 内存管理 | |
一、main()函数
zmalloc_enable_thread_safeness()打开内存分配线程安全标志,这个函数只有一句代码即zmalloc_thread_safe =1,zmalloc_thread_safe是zmalloc.c的一个静态变量,这个值为1的情况下,redis内存的申请、释放、使用都会加锁。
【注】此处值得一提的是,在i386CPU和AMD64CPU上,在GCC库版本号大于40100的情况下,会直接使用原子指令代替锁操作,原子指令时CPU层面的支持,无需内核锁对象介入,性能大大提高,redis针对系统环境和硬件进行效率优化。这个原子指令就是__sync_add_and_fetch,一个指令实现增加并获取一个值,熟悉汇编的人应该不陌生。
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
设置内存分配失败后的回调函数,如果不主动设置回调函数,redis将会会记录文件“zmalloc:Out of memory trying to allocate xxxbytes”,并退出线程。在这里,redis没有使用默认处理方式,而是选择记录Log,并主动报告一个OMM错误。
srand(time(NULL)^getpid()); 用当前时间随机种子值,用当前时间与进程ID进行按位异或,增大随机种子的随机概率。
gettimeofday(&tv,NULL);
dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
取得当前时间(秒和微秒),用秒数、微秒及进程ID值三者之间的按位异或,作为hash函数的种子值。这个函数也只有一句代码,dict_hash_function_seed,redis给这个变量提供了一个默认值5381。这个值用于hash数据结构,以后讲到hash数据结构即可。
server.sentinel_mode = checkForSentinelMode(argc,argv); 服务器是否用监视模式运行,监视模式下,redis将记录监视主节点(master nodes)的一些数据。
这个函数如果第一个参数是"redis-sentinel",或者其他参数中有“—sentinel”都会返还1,否则0。这句话完全可以放入initServerConfig()函数。
initServerConfig();
初始化服务器配置。配置内容很多,详细介绍见下文。
if(server.sentinel_mode) {initSentinelConfig();initSentinel();}
initSentinelConfig();位于sentinel.c文件,设置监视端口为26379;
initSentinel();位于sentinel.c文件,清空服务器的命令表,并加入SENTINEL命令。并初始化sentinel全局变量,这个变量时一个sentinelState类型,分析到监视模式会详细深入。
if (argc >= 2){…} 这部分进行读入选项和配置文件,并修改服务器配置,如果第二个启动参数是“-v”或者”--version”, 输出当前redis版本号后退出进程; 如果第二个参数是"--help"或"-h",则调用usage() 输出控制台服务器一些帮助信息后退出;如果第二个参数是"--test-memory",那么根据第三个参数进行内存测试后,退出。
if(argv[j][0] != '-' || argv[j][1] != '-')
{
configfile = argv[j++];
}
记住,C程序启动后,第一个启动参数是默认的,即是你的启动路径,第2~n个参数才是你配置的。只有是第二个参数不是以“--”开头,就认为是配置文件。
while(j != argc){} 解析其他配置选项,记录到options这个sds变量,并以“ ”结尾(关于sds,详见redis内部数据结构暂时理解为字符串)。如果是以“--”开头的参数,例如 “--port 6380”这个启动参数,将给options追加入“\nport 6380 ”。
options= sdscat(options,argv[j]+2); // 这个+2目的是为了去掉”--”。
如果是以“-”开头的参数,则需要进入sdscatrepr()函数特殊处理,并追加入options变量。这里的特殊处理是指对转义字符的处理,例如“-xx\nyy\t””将被处理成“xx\\nyy\\t”。最后两句,resetServerSaveParams(); //定义于 config.c ,释放并置server.saveparams,server.saveparamslen=0。
loadServerConfig(configfile,options);
//压栈分析loadServerConfig()函数
根据配置文件和传入的选项,修改server 变量(服务器配置),如果配置文件名是以“-\0”开头,认为配置从标准输入。
loadServerConfigFromStri
loadServerConfigFromStri
lines = sdssplitlen(config,strlen(config),"\n",1,&totlines);
在这一句根据“\n”为分割符,把config字符串分割,然后返回一个sds数组指针。然后遍历这个数组,然后根据参数做server常数的配置。
if (server.daemonize) {daemonize();}
// 如果服务器配置是守护形式启动,就创建守护进程Daemonize()函数里面 if ((fd =open("/dev/null",O_RDWR, 0)) != -1){} //所有输出都放在 /dev/null下, 如果redis工作在守护线程下,配置文件的'stdout'设置为'logfile',就不能正常记录日志。如果日志权限不足,就会进行以下行为:
initServer(); initServer()的详细详见下文
if(server.daemonize) createPidFile();
createPidFile()函数的内部实现 FILE *fp =fopen(server.pidfile,"w"),以可写的方式打开server.pidfile配置的文件。
redisAsciiArt();
这个函数的内部实现是
#include "asciilogo.h"
char *buf = zmalloc(1024*16);
char *mode = "stand alone";
if (server.cluster_enabled) mode = "cluster";
else if (server.sentinel_mode) mode = "sentinel";
snprintf(buf,1024*16,ascii_logo, REDIS_VERSION,redisGitSHA1(),strtol(redisGitDirty(),NULL,10) > 0,(sizeof(long) == 8) ?"64" : "32",mode, server.port,(long) getpid() );
redisLogRaw( REDIS_NOTICE|REDIS_LOG_RAW, buf );
zfree(buf);
打印ASCII图片(实际是日志),就是把 logo、版本、redis_git_sha1、redis_git_dirty、系统bits、redis运行模式、服务器端口、进程ID等信息写入日志文件。ascii_logo是一串字符串,用一些符号绘制的redis的logo,非常蛋疼。另外一个有意思的事情是,在函数里包含头文件,一个不错的思路。redisLogRaw()详细见下文。
if(!server.sentinel_mode)
{
……
loadDataFromDisk();
……
}
如果不在后台模式下,会输出一些信息。这里只看一个函数loadDataFromDisk(),如果server.aof_state这个配置了REDIS_AOF_ON,要通过loadAppendOnlyFile()载入AOF文件,否则加载RDB文件,如果RDB加载失败,则记录报错日志并退出程序,loadAppendOnlyFile()函数分析见aof.c,rdbLoad()函数分析见rdb.c。
if(server.maxmemory > 0 && server.maxmemory < 1024*1024)
{
redisLog(…);
}
打印内存限制警告
server.el->beforesleep= beforeSleep;
beforeSleep()这个方法在Redis每次进入sleep/wait去等待监听的端口发生I/O事件之前被调用。beforeSleep函数分析,见下文。
aeMain(server.el);
启动事件主循环,这个函数内部实现为
eventLoop->stop = 0;
while (!eventLoop->stop)
{
if (eventLoop->beforesleep!=NULL)
{ eventLoop->beforesleep(eventLoop); }
aeProcessEvents(eventLoop,AE_ALL_EVENTS);
}
如果有需要在事件处理前执行的函数,那么其回调函数,接着执行事件aeProcessEvents(),这个函数详见ae.c。
aeDeleteEventLoop(server.el);
主循环结束后会运行到这,关闭服务器,删除事件。内部实现为
aeApiFree(eventLoop);
zfree(eventLoop->events);
zfree(eventLoop->fired);
zfree(eventLoop);
aeApiFree()在win、linux、FreeBSD都是不同的,拿linux下
aeApiState*state = eventLoop->apidata;
close(state->epfd);
zfree(state->events);
zfree(state);
没什么难理解的,都是关闭epoll,释放事件。
二、initServerConfig()
Server是一个redisServer类型的全局变量,也是redis最重要的一个全局变量,大部分的服务器配置和一些重要状态都保存在这里。其结构详见附录。initServerConfig()函数是初始化Server的部分成员变量。这里只部分介绍,没有什么值得详细分析的。
server.arch_bits =(sizeof(long) == 8) ? 64 : 32;
一个经典的办法,判断当前执行环境是多少位。
server.port = REDIS_SERVERPORT;
TCP/IP连接的默认端口为REDIS_SERVERPORT即6379,这个6379在手机上字母组合Merz对应的数字是6379,Merz是redis之父的网名,嚯嚯。
server.dbnum = REDIS_DEFAULT_DBNUM; 默认数据库(实际相当于数据名称空间)个数16个
server.verbosity = REDIS_NOTICE; 日志详细级别,默认是NOTICE级别
server.maxidletime = REDIS_MAXIDLETIME; 客户端连接过期时间,默认永不过期
server.client_max_querybuf_len = REDIS_MAX_QUERYBUF_LEN; 客户端查询缓存最大长度,默认1G
server.syslog_ident = zstrdup("redis"); 系统日志识别字符串
server.syslog_facility = LOG_LOCAL0;
在这个函数中,对server变量进行了部分成员的初始化,其中:runid:运行该redis服务器端程序的唯一标识,即每次启动都会一个唯一ID,用来区分不同的redis服务器端程序。maxidletime:最大空闲时间,就是client连接到server时,如果超出这个值,就会被自动断开,当然,master和slave节点不包括。如果client有阻塞命令在运行,也不会断开。saveparams:这个存储的是redis服务器端程序从配置文件中读取的持久化参数
lruclock:是redis实现LRU算法所需的,每个redis object都带有一个lruclock,用来从内存中移除空闲的对象。commands:是redis命令的字符数组。sentinel_mode:是否开启redis的哨兵模式,也就是是否监测,通知,自动错误恢复,是用来管理多个redis实例的方式。
三、initServer()
dup2(fd, STDIN_FILENO); 赋给fd标准输入句柄
dup2(fd, STDOUT_FILENO); 关闭标准输入,赋给fd标准输出句柄
dup2(fd, STDERR_FILENO); 关闭标准输出,赋给fd标准错误输出句柄
if(fd >STDERR_FILENO)close(fd); 关闭标准错误输出
signal(SIGHUP, SIG_IGN);让系统忽略SIGHUP信号(控制终端信号),作为守护进程运行,不会有控制终端,所以忽略掉SIGHUP信号。
signal(SIGPIPE, SIG_IGN);让系统忽略SIGHUP信号(控制终端信号),SIGPIPE信号是在写管道发现读进程终止时产生的信号,向已经终止的SOCK_STREAM套接字写入也会产生此信号。redis作为server,不可避免的会遇到各种各样的client,client意外终止导致产生的信号也应该在server启动后忽略掉。
setupSignalHandlers();
setupSignalHandlers函数处理的信号分两类:1)SIGTERM。SIGTERM是kill命令发送的系统默认终止信号。也就是我们在试图结束server时会触发的信号。对这类信号,redis并没有立即终止进程,其处理行为是,设置一个server.shutdown_asap,然后在下一次执行serverCron时,调用prepareForShutdown做清理工作,然后再退出程序。这样可以有效的避免盲目的kill程序导致数据丢失,使得server可以优雅的退出。2)SIGSEGV、SIGBUS、SIGFPE、SIGILL。这几个信号分别为无效内存引用(即我们常说的段错误),实现定义的硬件故障,算术运算错误(如除0)以及执行非法硬件指令。这类是非常严重的错误,redis的处理是通过sigsegvHandler,记录出错时的现场、执行必要的清理工作,然后kill自身。除上面提到的7个信号意外,redis不再处理任何其他信号,均保留默认操作。
if(server.syslog_enabled)
{
openlog(server.syslog_ident,LOG_PID| LOG_NDELAY | LOG_NOWAIT, server.syslog_facility);
}
如果服务器配置中启动了日志,那么就打开系统日志。openlog属于linux系统函数,如果不知道用法自行搜索。
server.current_client = NULL; 设置当前处理的客户端为空
server.clients = listCreate(); 客户端链表,用adlist.c的listCreate()创建了链表(实际分配头结点内存)。
server.clients_to_close = listCreate(); 要被关闭的客户端链表,同上。
server.slaves = listCreate(); slave节点链表
server.monitors = listCreate(); monitor客户端链表
server.unblocked_clients = listCreate(); 被取消阻塞的客户端链表
server.ready_keys = listCreate(); 已就绪key链表
createSharedObjects(); 初始化共享对象,主要是设置redis.c里的全局对象structsharedObjectsStruc
adjustOpenFilesLimit(); 获取最大打开文件数目,根据这个打开文件最大数,适当调整同时支持的客户端数server.maxclients 变量。
server.el = aeCreateEventLoop(server.maxclients+1024); 创建事件循环aeCreateEventLoop ()函数先创建一个结构aeEventLoop的指针变量eventLoop,并从堆中申请内存,把这个结构的一些成员赋初始值,创建aeApiState结构指针变量state,并创建epool,并把fd赋给state->epfd,最后把state赋给eventLoop->apidata。
server.db = zmalloc(sizeof(redisDb)*server.dbnum); 根据服务器配置分配数据库内存
if (server.port != 0)
{
server.ipfd= anetTcpServer(server.neterr,server.port,server.bindaddr);
if (server.ipfd == ANET_ERR)
{
redisLog(REDIS_WARNING, "Openingport %d: %s", server.port,server.neterr);
exit(1);
}
}
如果server.port设置了值,根据srver配置,创建SOCK_STREAM套接字,并监听server.port这个端口, 如果创建失败,则结束服务器程序。
if (server.unixsocket != NULL)
{
unlink(server.unixsocket);
server.sofd=anetUnixServer(server.neterr,server.unixsocket,server.unixsocketperm);
if (server.sofd == ANET_ERR)
{
redisLog(REDIS_WARNING, "Openingsocket: %s",server.neterr);
exit(1);
}
}
if (server.ipfd < 0 && server.sofd < 0)
{
redisLog(REDIS_WARNING, "Configuredto not listen anywhere,exiting.");
exit(1);
}
建立unix socket(本地无名套接字),流程同上。如果配置两种连接方式都没设置,服务器程序也会退出。
// ------------------------根据配置初始化数据库-----------------------
for (j = 0; j
{
server.db[j].dict=dictCreate(&dbDictType,NULL); key哈希
server.db[j].expires=dictCreate(&keyptrDictType,NULL);
过期key hash,存储会过期的key以及相应过期时间,即一对(key,time_t)的kv组合,也是一个。
server.db[j].blocking_keys= dictCreate(&keylistDictType,NULL);
// --阻塞键hash
server.db[j].ready_keys=dictCreate(&setDictType,NULL);
// --收到push命令的阻塞键hash
server.db[j].watched_keys= dictCreate(&keylistDictType,NULL);
// --被WATCH命令监视的键hash
server.db[j].id = j;
// --数据库ID,数据库的id是从0开始递增。
}
server.pubsub_channels=dictCreate(&keylistDictType,NULL);
订阅频道的hash,用来记录所有订阅的client。
server.pubsub_patterns=listCreate();
初始化订阅-发布模式列表
server.pubsub_patterns->free=freePubsubPattern;
发布-订阅函数的释放函数指针
server.pubsub_patterns->match=listMatchPubsubPattern;
匹配函数
server.cronloops= 0; // --CRON执行计数
server.rdb_child_pid= -1; // --BGSAVE 执行指示变量
server.aof_child_pid= -1; // --BGREWRITEAOF 执行指示变量
aofRewriteBufferReset(); // --初始化AOF 重写缓存
server.aof_buf=sdsempty();
server.lastsave=time(NULL); // --最后一次成功保存的时间
server.rdb_save_time_last=-1; // --结束 SAVE 的时间
server.rdb_save_time_start=-1; // --开始 SAVE 的时间
server.dirty =0; // --用来后续计算server维护的数据是否有更新,如果有,需要记录aof和通知replication.
//--接下来,是一些统计变量
server.stat_numcommands = 0; // --命令数
server.stat_numconnections = 0; // --连接数
server.stat_expiredkeys = 0; // --过期键
server.stat_evictedkeys = 0;
server.stat_starttime = time(NULL);
server.stat_keyspace_misses = 0;
server.stat_keyspace_hits = 0;
server.stat_peak_memory = 0;
server.stat_fork_time = 0;
server.stat_rejected_conn = 0;
memset(server.ops_sec_samples,0,sizeof(server.ops_sec_samples));
server.ops_sec_idx = 0;
server.ops_sec_last_sample_time = mstime();
server.ops_sec_last_sample_ops = 0;
server.unixtime = time(NULL); // 用于时间值保留,其精度为s,类似于一个缓存。redis的代码中有很多需要时间值的地方,只要其精度要求不是很高,server.unixtime又有合理的机制进行更新,就可以避免在每次需要时间值的时候执行昂贵的time系统调用。
server.lastbgsave_status = REDIS_OK;
server.stop_writes_on_bgsave_err = 1;
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
【非常重要】创建一个ae定时事件,加到server.el->timeEventHead的头部,并将serverCron设置为这个定时事件的处理函数。这是redis的核心循环,该过程是serverCron,每秒调用次数由一个叫REDIS_HZ的宏决定,默认是每10微秒超时,即每10微秒该ae时间事件处理过程serverCron会被过期调用。serverCron详解见下文。
if (server.ipfd > 0 &&aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,
acceptTcpHandler,NULL)== AE_ERR)
{
redisPanic("Unrecoverable error creating server.ipfd fileevent.");
}
创建ae文件事件,对redis的TCP或者unixsocket端口进行监听,使用相应的处理函数注册。每次得到clients连接后,都会触发ae文件事件,异步接收命令。如果server.ipfd(tcp/ip文件描述,类似于windows下的handle)有值,也即tcp/ip套接字创建成功的情况下,创建网络事件。在有连接请求进来后,acceptTcpHandler将会被调用,该函数调用accept接收连接,然后用accept函数返回的文件描述符创建一个client桩(一个redisClient对象),在server端代表连接进来的真正client。在创建client桩的时候,会将返回的这个描述符同样添加进事件监控列表,监控READABLE事件,事件发生代表着客户端发送数据过来,此时调readQueryFromClient接收客户端的query。
if (server.aof_state == REDIS_AOF_ON)
{
server.aof_fd =open(server.aof_filename,
O_WRONLY|O_APPEND|O_CREAT,0644);
if (server.aof_fd == -1)
{
redisLog(REDIS_WARNING,
"Can't openthe append-only file: %s", strerror(errno));
exit(1);
}
}
如果server设置了aof模式做持久化,将会打开或创建对应的打开或创建 AOF 文件,保存相关的描述符。
设置内存限制。
if(server.arch_bits == 32 && server.maxmemory == 0)
{
redisLog(REDIS_WARNING,"Warning: 32 bit instance detected but nomemory limit set.
Setting 3 GB maxmemory limit with 'noeviction' policy now.");
server.maxmemory = 3072LL*(1024*1024);
server.maxmemory_policy =REDIS_MAXMEMORY_NO_EVICTION;
}
32位系统,如果没有显式内存设置,默认设置为3G,并把maxmemory_policy设置为REDIS_MAXMEMORY_NO_EVICTION,在程序达到最大内存限制后,拒绝后续会增大内存使用的客户端执行的命令。
if(server.cluster_enabled)
{
clusterInit();
}
如果集群模式已打开,那么初始化集群。对于clusterInit()函数的细节分析,详见cluster.c的剖析。
scriptingInit();
初始化脚本系统。Redis的脚本系统用的是lua语言。在scriptingInit()函数主要是初始化lua环境,对于在C/C++下使用过lua脚本的人来说,非常熟悉,具体细节将在scripting.c文件分析时展开。
slowlogInit();
初始化slowlog。slowlog是redis提供的进行query分析的工具。它将执行时间长的命令统一以list形式保存在内存之中,使用者可以通过slowlog命令查看这些慢query,从而分析系统瓶颈。
bioInit();
初始化后台IO服务。
四、serverCron()函数
前面介绍过,这个函数非常重要。serverCron是一个主循环事件的回调处理函数,redis每个循环都会执行该函数。
REDIS_NOTUSED(eventLoop);
REDIS_NOTUSED(id);
REDIS_NOTUSED(clientData);
REDIS_NOTUSED这个宏很有意思,#define REDIS_NOTUSED(V) ((void) V)
目的是为了去掉编译器对未使用的局部变量的警告。
if(server.watchdog_period)
{
watchdogScheduleSignal(server.watchdog_period);
}
如果配置设置了server.watchdog_period,则启动看门狗信号:如果系统在一定时间内不运行到这里,看门狗程序会触发一个SIGALRM信号到信号处理器。
structitimerval it;
it.it_value.tv_sec= period/1000; // --如果period为0,那么停止计时器
it.it_value.tv_usec=(period00)*1000;
it.it_interval.tv_sec= 0; // --不要自动重启
it.it_interval.tv_usec= 0;
setitimer(ITIMER_REAL,&it,NULL); // --以系统真实的时间来计算,它送出SIGALRM信号。
server.unixtime= time(NULL);
// --将UNIX 时间保存在服务器状态中,减少对time(NULL) 的调用,获取一个全局变量快于一个调用time(NULL)。
server.mstime= mstime();
这一句在2.6之前的版本是没有的,记录当前时间的毫秒数(是毫秒,不是微秒)。
run_with_period(100)trackOperationsPerSecond
这个run_with_period是一个宏,看一下宏定义#define run_with_period(_ms_) if ((_ms_<= 1000/server.hz)||!(server.cronloops%((_ms_)/(1000/server.hz)))),
如果_ms_<=1000/server.hz 或者 server.cronloops不能整除_ms_/(1000/server.hz)的结果,就执行trackOperationsPerSecond
这句很不好理解,但是大致可以这么理解,按照一定周期执行trackOperationsPerSecond
updateLRUClock();
更新服务器的 LRU 时间。函数只有一句代码server.lruclock =(server.unixtime/REDIS_LRU_CLOCK_RESOLUTION)& REDIS_LRU_CLOCK_MAX;
后续在执行lru淘汰策略时,作为比较的基准值。redis默认的时间精度是10s(#defineREDIS_LRU_CLOCK_RESOLUTION 10),保存lruclock的变量共有22bit。换算成总的时间为1.5year(每隔1.5年循环一次)。不知为何在最初设计的时候,为lruclock只给了22bit的空间。还是读一下原始的注释吧:
Wehave just 22 bitsper object for LRU information.So we use an (eventually wrapping) LRUclock with10 seconds resolution.2^22 bits with 10 seconds resolution ismore or less 1.5years.
Note that even ifthis will wrap after1.5 years it's not a problem,everything will still work butjust someobject will appear younger to Redis. But for this to happen agivenobject should never be touchedfor 1.5 years.
Note that you canchange the resolutionaltering the REDIS_LRU_CLOCK_RESOLUTION define.
if (zmalloc_used_memory()>server.stat_peak_memory) { server.stat_peak_memory = zmalloc_used_memory();}
记录服务器启动以来的内存最高峰
size_tzmalloc_used_memory(void)
{
size_t um;
if (zmalloc_thread_safe)
{
#ifdefHAVE_ATOMIC
um =__sync_add_and_fetch(&used_memory,0);
#else
pthread_mutex_lock(&used_memory_mutex);
um = used_memory;
pthread_mutex_unlock(&used_memory_mutex);
#endif
}
else
{
um = used_memory;
}
return um;
}
if (server.shutdown_asap)
{
if(prepareForShutdown(0) == REDIS_OK)
{
exit(0);
}
redisLog(REDIS_WARNING,"SIGTERMreceivedbut
errors trying toshut down the server,
check the logs formoreinformation");
server.shutdown_asap= 0;
}
redis在SIG_TERM信号的处理函数中并没有立即终止进程的执行,而是选择了标记shutdown_asap flag,然后在serverCron中通过执行prepareForShutdown函数,优雅的退出。prepareForShutdown()函数主要是处理了rdb、aof记录文件退出的情况,最后保存了一次rdb文件,关闭了相关的文件描述符以及删除了保存pid的文件(server.pidfile),详细见下文。
run_with_period(5000)
{
for (j = 0; j
{
long long size, used,vkeys;
size =dictSlots(server.db[j].dict);
used =dictSize(server.db[j].dict);
vkeys =dictSize(server.db[j].expires);
if (used || vkeys)
{
redisLog(REDIS_VERBOSE,"DB%d:%lld keys (%lld volatile) in %lld slots HT.", j,used,vkeys,size);
}
}
}
每5秒输出一次redis每个数据库的统计信息: 使用的key数目、设置过期的key数目、以及当前的hashtabale的槽位数。
if (!server.sentinel_mode)
{
run_with_period(5000) {
redisLog(REDIS_VERBOSE,
"�lients connected (%dslaves), %zu bytes in use",
listLength(server.clients)-listLength(server.slaves),
listLength(server.slaves),
zmalloc_used_memory());
}
}
在非后台模式下,每5秒输出一次,client数目,slaves数目,以及总体的内存使用情况
clientsCron();
clientCron()的代码如下:
Make sure to processat least1/(server.hz*10) of clients per call.
Since this functionis called server.hztimes per second we are sure that
in the worst case weprocess all theclients in 10 seconds.
In normal conditions(a reasonablenumber of clients) we process
all the clients in ashorter time.
次调用clientCron例程,这是一个对server.clients列表进行处理的过程。再每次执行clientCron时,会对 server.clients进行迭代,并且保证 1/(REDIS_HZ*10)ofclients per call。也就是每次执行clientCron,如果clients过多,clientCron不会遍历所有clients,而是遍历一部分 clients,但是保证每个clients都会在一定时间内得到处理。处理过程主要是检测client连接是否idle超时,或者block超时,然后 会调解每个client的缓冲区大小。
int numclients =listLength(server.clients);
int iterations =numclients/(server.hz*10);
if (iterations < 50)iterations= (numclients < 50) ? numclients : 50;
while(listLength(server.clients)&&iterations--)
{
redisClient *c;
listNode *head;
listRotate(server.clients);
将当前处理的客户端调到表头,这样在要删除客户端时,复杂度就是O(1)而不是O(N)
head = listFirst(server.clients);
c = listNodeValue(head);
The followingfunctions do differentservice checks on the client.The protocol is that theyreturnnon-zero if the client was terminated.
检查客户端是否超时,如果是的话删除它的连接如果客户端正因 BLPOP/BRPOP/BLPOPRPUSH 阻塞,那么检查阻塞是否超时,是的话就退出阻塞状态
if(clientsCronHandleTimeout
// --释放客户端查询缓存多余的空间
if(clientsCronResizeQueryBu
}
检查连接是否超时,以及清理多余的查询缓存,最多处理50个迭代,
databasesCron();
if (server.rdb_child_pid == -1&& server.aof_child_pid == -1&& server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBac
}
如果用户在此期间,执行BGREWRITEAOF 命令的话,在后台执行AOF重写。
rewriteAppendOnlyFileBac
if (server.rdb_child_pid !=-1 ||server.aof_child_pid != -1)
{
int statloc;
pid_t pid;
if ((pid =wait3(&statloc,WNOHANG,NULL))!= 0)
{
int exitcode =WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc))bysignal = WTERMSIG(statloc);
if (pid == server.rdb_child_pid)
{
backgroundSaveDoneHandle
}
elseif (pid == server.aof_child_pid)
{
backgroundRewriteDoneHan
}
else
{
redisLog(REDIS_WARNING,"Warning,
detected child with unmatched pid: %ld",(long)pid);
}
updateDictResizePolicy();
}
}
如果有server.rdb_child_pid或server.aof_child_pid配置:调用wait3获取子进程状态。此wait3为非阻塞(设置了WNOHANG flag)。注意:APUE2在进程控制章节其实挺不提倡用wait3和wait4接口的,不过redis的作者貌似对这个情有独钟。如果后台进程刚好退出,调用backgroundSaveDoneHandle
// --否则,如果没有后台的save rdb操作及rewrite操作:首先,根据saveparams规定的rdb save策略,如果满足条件,执行后台rdbSave操作;其次,根据aofrewrite策略,如果当前aof文件增长的规模,要求触发rewrite操作,则执行后台的rewrite操作。
else
{
for (j = 0; j
{
struct saveparam *sp=server.saveparams+j;
/* Save if we reachedthe given amount ofchanges,
* the given amountofseconds, and if the latest bgsave was
* successful or if,incase of an error, at least
*REDIS_BGSAVE_RETRY_DELAYseconds already elapsed. */
if (server.dirty>=sp->changes &&
server.unixtime-server.lastsave>sp->seconds &&
(server.unixtime-server.lastbgsave_try>REDIS_BGSAVE_RETRY_DELAY || server.lastbgsave_status == REDIS_OK) )
{
redisLog(REDIS_NOTICE,"�hangesin %d seconds. Saving...",sp->changes, sp->seconds);
rdbSaveBackground(server.rdb_filename);
break;
}
}
if (server.rdb_child_pid== -1&&
server.aof_child_pid== -1&&
server.aof_rewrite_perc&&
server.aof_current_size>server.aof_rewrite_min_size)
{
long long base =server.aof_rewrite_base_size?
server.aof_rewrite_base_size: 1;
long long growth=(server.aof_current_size*100/base) - 100;
if (growth >= server.aof_rewrite_perc){
redisLog(REDIS_NOTICE,"Startingautomaticrewriti
rewriteAppendOnlyFileBac
}
}
}
if(server.aof_flush_postponed_start) flushAppendOnlyFile(0);
如果我们延缓执行aof缓冲flush,则在此进行flush操作,调用flushAppendOnlyFile函数
freeClientsInAsyncFreeQu
函数内部代码为
while(listLength(server.clients_to_close))
{
listNode *ln =listFirst(server.clients_to_close);
redisClient *c =listNodeValue(ln);
c->flags &=~REDIS_CLOSE_ASAP;
freeClient(c);
listDelNode(server.clients_to_close,ln);
}
run_with_period(1000)replicationCron();
每秒执行一次replicationCron(),这个函数用来master重连和传输失败检查
run_with_period(100)
{
if(server.sentinel_mode)sentinelTimer();
}
如果再监视模式下,则每100ms运行一次监视Time
server.cronloops++;
统计服务器循环计数
return 1000/server.hz;
返回 1000/server.hz,意味着server将会在100ms后重新调用这个函数
五、databasesCron()函数
databasesCron()函数主要用于在后台做一些需要逐渐实现的操作,比如key的过期,哈希重置等操作。
if(server.active_expire_enabled && server.masterhost == NULL)
{
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
}
activeExpireCycle()这个函数主要是用于过期值的处理, 详情见本文后面的分析。这个函数只有在active_expire_enabled这个配置为true,而且本库是master的的情况下执行,slave会等候由master传递的DEL消息,保证master-slave在过期值处理上的一致。redis是随机抽取过期值的,所以master和slave可能抽取不同的值,故通过DEL消息实现同步,同时这种expire机制也是不可靠的expire,即key超时后有可能不会被删除。
if(server.rdb_child_pid == -1 && server.aof_child_pid == -1) {…}
这句代码经常见,如果没有其进程进行DB存储的时候,才能进行hash重置,因为这会引起很多的写实复制内存页。
static unsignedint resize_db = 0;
static unsignedint rehash_db = 0;
unsigned intdbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
unsigned int j;
if (dbs_per_call> server.dbnum)
{
dbs_per_call = server.dbnum;
}
不能超过数据库数目,默认为16。
for (j = 0; j
{
tryResizeHashTables(resize_db %server.dbnum);
resize_db++;
}
遍历每个数据库都进行Resize操作,tryResizeHashTables()的全部代码如下:
if (htNeedsResize(server.db[dbid].dict))
{
dictResize(server.db[dbid].dict);
}
if (htNeedsResize(server.db[dbid].expires))
{
dictResize(server.db[dbid].expires);
}
函数分别检查检查key空间和过期key空间。
htNeedsResize()的内部实现
long long size, used;
size = dictSlots(dict);
used = dictSize(dict);
return (size && used && size >DICT_HT_INITIAL_SIZE && (used*100/size < REDIS_HT_MINFILL));
size是两个字典结构的桶的数目总和,而used是两个字典结构已经使用的桶总和。
如果满足used*100/size< REDIS_HT_MINFILL,就进行dictResize()。dictResize()内部实现见dict.c。
if(server.activerehashing
{
for (j = 0; j < dbs_per_call; j++)
{
int work_done =incrementallyRehash(rehash_db % server.dbnum);
rehash_db++;
if (work_done) {break;}
}
}
遍历每个数据库,进行rehash操作,每次调用只rehash一个。incrementallyRehash()的内部实现如下:
if(dictIsRehashing(server.db[dbid].dict))
{
dictRehashMilliseconds(server.db[dbid].dict,1);
return 1;
}
if(dictIsRehashing(server.db[dbid].expires))
{
dictRehashMilliseconds(server.db[dbid].expires,1);
return 1;
}
return 0;
dictIsRehashing ()的宏定义,dictIsRehashing(ht) ((ht)->rehashidx != -1)。dictRehashMilliseconds()细节见dict.c。
六、activeExpireCycle()函数
★★★这个函数也非常重要,牵涉到了过期主键淘汰(清除)机制,我将它比作redis的GC。Redis支持主键可以有过期时间,这种机制方便用户使用,比如一些缓存策略,过期的主键会占用大量内存资源,这就必要要求redis有及时从内存中清理失效主键。
Redis的主键淘汰有两种策略:
被动策略(passive way),在访问主键的时候,如果发现主键已经过期,就清除掉。
主动策略(active way),周期性地从过期主键空间中淘汰一部分失效的主键。
本函数则代表着redis过期主键淘汰机制的主动策略,本函数是由时间事件来驱动执行。Redis的淘汰算法是灵活适应的。如果有很少的过期key,就用很少的CPU循环;否则就会占用更多CPU循环用于淘汰计算,以防止很多可删除键空间占用大量内存。每次迭代DB数目不能超过REDIS_DBCRON_DBS_PER_CALL。
淘汰原理:遍历服务器每个数据库expires字典,从中尝试着随机抽样不超过ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP(20)个限时主键,检查其是否过期,如果过期则删除。如果过期的限时主键个数占本次抽样个数的比例超过1/4,Redis 会认为当前数据库中的失效主键依然很多,所以它会继续进行下一轮的随机抽样和删除,直到刚才的比例低于25%才停止对当前数据库的处理,转向下一个数据 库。这里我们需要注意的是,activeExpireCycle函数不会试图一次性处理Redis中的所有数据库,而是最多只处理REDIS_DBCRON_DBS_PER_CALL(默认值为16),此外 activeExpireCycle 函数还有处理时间上的限制,不是想执行多久就执行多久,凡此种种都只有一个目的,那就是避免失效主键删除占用过多的CPU资源
static unsigned intcurrent_db = 0;
static int timelimit_exit = 0;
static long longlast_fast_cycle = 0;
unsigned int j, iteration = 0;
unsigned int dbs_per_call =REDIS_DBCRON_DBS_PER_CALL; // --每次最多处理DB个数
long long start = ustime(), timelimit;
注意一下这几个静态变量,current_db,用来保存每次函数调用处理的最后一个Redis数据库的编号,因为activeExpireCycle不是一次性遍历所有数据库,而是渐进式的,下次执行本函数时,从current_db继续开始处理。timelimit_exit保存上一次调用本函数时,是否到达指定期限(这个期限下文有详细说明)last_fast_cycle用于保存上一次快速循环的执行时间。
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
{
if(!timelimit_exit) return;
if(start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2 ) return;
last_fast_cycle = start;
}
如果传入参数是ACTIVE_EXPIRE_CYCLE_FAST,则在快速循环执行期间内不再重复执行快速循环,且在距离上次快速循环执行时间小于2倍EXPIRE_FAST_CYCLE_DURATION微秒 则不执行。如果参数是ACTIVE_EXPIRE_CYCLE_SLOW,则执行普通过期循环。
if (dbs_per_call > server.dbnum || timelimit_exit){ dbs_per_call = server.dbnum; }
通常每次迭代要通常每次迭代要检测REDIS_DBCRON_DBS_PER_CALL(16)个db,有两种例外情况:1)实际数据库数量小于REDIS_DBCRON_DBS_PER_CAL;2)上一次调用activeExpireCycle()函数到达指定期限,这种情况说明数据库的过期主键很多,占用大量内存,所以需要处理全部数据库。
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;
if (type == ACTIVE_EXPIRE_CYCLE_FAST){ timelimit =ACTIVE_EXPIRE_CYCLE_FAST_DURATION;}
每次迭代执行activeExpireCycle()的指定期限(单位为微秒),普通循环(参数是ACTIVE_EXPIRE_CYCLE_SLOW)的期限是REDIS_HZ的百分比即(1000000 *(REDIS_EXPIRELOOKUPS_TIME_PERC / 100)) / server.hz;而快速循环的指定期限是ACTIVE_EXPIRE_CYCLE_FAST_DURATION(1000微秒)。
for (j = 0; j < dbs_per_call; j++) {…}
不用细讲,循环遍历固定数目的DB。看循环里面的代码:
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
current_db++;
此处立刻就将current_db加一,这样可以保证即使这次无法在时间限制内删除完所有当前数据库中的失效主键,下一次调用activeExpireCycle一样会从下一个数据库开始处理,从而保证每个数据库都有被处理的机会。
do{ … }while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
这个循环处理当前数据库中的失效主键,直到过期的限时主键个数占本次抽样个数(20)的比例小于等于1/4,则不重复循环。看do{}里的代码:
unsigned long num, slots;
long long now, ttl_sum;
ttl_samples;
if ((num = dictSize(db->expires)) == 0)
{
db->avg_ttl = 0;
break;
}
如果expires字典表大小为0,说明该数据库中没有设置失效时间的主键,直接检查下一数据库。
slots = dictSlots(db->expires); // --记录当前数据库限时主键hash的桶数
now = mstime(); // --记录当前时间
if (num && slots > DICT_HT_INITIAL_SIZE &&(num*100/slots< 1)){ break;}
如果expires字典表不为空,但是其填充率不足1%,那么随机选择主键进行检查的代价会很高,所以这里直接检查下一数据库。
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
{
num =ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
}
如果expires字典表中的entry个数大于ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP,抽样次数为ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP。
while (num--)
{
dictEntry *de;
long long ttl;
if((de = dictGetRandomKey(db->expires)) == NULL)
{
break;
}
// --随机获取一个设置了失效时间的主键
ttl =dictGetSignedIntegerVal(de)-now;
if ( activeExpireCycleTryExpi
{
expired++; // --如果过期,则增加统计
}
if(ttl < 0) ttl = 0;
ttl_sum += ttl;
ttl_samples++;
}
下面来看看activeExpireCycleTryExpi
long long t = dictGetSignedIntegerVal(de);
if (now > t)
{
sds key =dictGetKey(de);
robj*keyobj = createStringObject(key,sdslen(key));
propagateExpire(db,keyobj);// --通知AOF文件和客户端都删除该主键,详见db.c。
dbDelete(db,keyobj); // --删除此主键
decrRefCount(keyobj);
server.stat_expiredkeys++;
return 1;
} else
{
return 0;
}
if (ttl_samples)
{
longlong avg_ttl = ttl_sum/ttl_samples;
if(db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl =(db->avg_ttl+avg_ttl)/2;
}
给DB更新平均TTL状态
iteration++; // --每次抽样后增加iteration
if ((iteration & 0xf) == 0 && (ustime()-start) > timelimit ){timelimit_exit = 1;}
if (timelimit_exit) { return; }
iteration & 0xf) == 0这句主要是检测是否到了16次抽样,每经过16次抽样就判断一次执行时间是否已经达到指定时间限制,如果已达到时间限制,则把timelimit_exit这个静态变量设置为1,并退出函数。我很奇怪的是,这句if (timelimit_exit)完全可以省略,直接写成if ((iteration & 0xf) == 0 && (ustime()-start) >timelimit ){ timelimit_exit = 1; return; }
通过以上对 Redis 主键失效机制的介绍,我们知道虽然 Redis 会定期地检查设置了失效时间的主键并删除已经失效的主键,但是通过对每次处理数据库个数的限制、activeExpireCycle 函数在一秒钟内执行次数的限制、分配给activeExpireCycle 函数CPU时间的限制、继续删除主键的失效主键数百分比的限制,Redis 已经大大降低了主键失效机制对系统整体性能的影响,但是如果在实际应用中出现大量主键在短时间内同时失效的情况还是会使得系统的响应能力降低,所以这种情况无疑应该避免。