BIO即background I/O service,后台I/O服务,redis将一些可能会堵塞主线程的操作放到后台线程去执行。
我们通常说redis是单线程的,但是redis并不是单线程的,单线程指的是redis的主要任务单线程的。redis的主线程主要是处理网络IO、命令执行、定时器任务。目前,7.0版本,redis的后台IO任务有3个:关闭文件描述符close(2)系统调用、AOF磁盘同步fsync,大键bigkey惰性删除。
/* Background job opcodes */
#define BIO_CLOSE_FILE 0 /* Deferred close(2) syscall. */
#define BIO_AOF_FSYNC 1 /* Deferred AOF fsync. */
#define BIO_LAZY_FREE 2 /* Deferred objects freeing. */
#define BIO_NUM_OPS 3
redis之所以将close(2)加入BIO主要是这个原因:如果服务器是某个文件的最后一个拥有者时,关闭一个文件就代表要 unlinking 这个文件,并且删除文件非常慢,会阻塞系统。
This is needed as when the process is the last owner of areference to a file closing it
means unlinking it, and the deletion of the file is slow, blocking the server.
如果AOF持久化设置为每秒进行一次磁盘同步的时候,fsync操作也是放到后台线程去执行,理由很简单,磁盘同步可能会比较慢,可能会造成堵塞。
这里简单说明一下为什么要进行磁盘同步。redis持久化就是指要将数据写入到磁盘,因为redis是内存数据库,如果主机一掉电内存中所有的数据都会丢失。
当redis将AOF日志写入到磁盘文件时,操作系统不会马上将数据写入磁盘,而是先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列。这种输出方式被称为延迟写(delayed write),主要是为了减少了磁盘读写次数,提升磁盘io性能,但是却带来了数据丢失的风险。
所以Linux操作系统系供了sync、fsync和fdatasync这几个系统调用来进行磁盘同步。redis是使用fsync来控制磁盘同步,避免数据丢失。redis磁盘同步有三种模式always、everysec和no。其中always是对每条AOF日志都进行磁盘同步,毫无疑问这会严重影响性能。所以redis还提供了每秒进行一次磁盘同步的选项,性能会有提升但是依然存在数据丢失的风险,但是最多只会丢失最近一秒钟内的日志,这就要使用者自己权衡使用哪种方案,要性能还是数据安全性。至于No选项,完全将磁盘同步交给操作系统决定,风险比较大,一般不推荐使用这种配置。
if (server.aof_fsync == AOF_FSYNC_ALWAYS)
redis_fsync(newfd);
else if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
aof_background_fsync(newfd);
第三种后台任务是惰性删除,惰性删除lazy free是在4.0版本引入,主要用来解决大键big key删除问题。big key主要是指包含对象比较多的key,比如list类型,hash类型,集合类型,有序集合,可能包含有大量的对象,如果采用同步删除可能要消耗大量的时间,所以如果配置了惰性删除,redis对于big key是采用异步删除策略。
/* This is a wrapper whose behavior depends on the Redis lazy free
* configuration. Deletes the key synchronously or asynchronously. */
/* 如果配置惰性删除则采用异步删除*/
int dbDelete(redisDb *db, robj *key) {
return server.lazyfree_lazy_server_del ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
当要删除一个key的时候redis会先衡量一下删除工作的工作量,如果工作量超过一个阈值就把删除操作放到后台线程去执行。
size_t free_effort = lazyfreeGetFreeEffort(val);//评估删除工作量
if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
atomicIncr(lazyfree_objects,1);
bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
dictSetVal(db->dict,de,NULL);
}
LAZYFREE_THRESHOLD默认值是64.
由下面这段代码可以看出,big key指的不是占用内存多的key,而是指内存分配次数多的key。比如,一个string类型,即使可能会占用很大的内存,但是它是内存连续的,所以只需要分配一次内存,不属于big key。其它数据类型还要看具体的编码格式,比如一个hash,或者zset,当包含的对象比较少的时候是用的压缩列表编码,这种编码内存也是连续的,所以也不算big key。对于set,包含对象少的时候是用的intset整数集合编码,内存也是连续。而list对象,6.0版本用的是quick list快表编码,内存是不连续的。
size_t lazyfreeGetFreeEffort(robj *obj) {
if (obj->type == OBJ_LIST) {
quicklist *ql = obj->ptr;
return ql->len;//如果是list类型直接返回list长度,即包含的节点数量
} else if (obj->type == OBJ_SET && obj->encoding == OBJ_ENCODING_HT) {
dict *ht = obj->ptr;
return dictSize(ht);//如果是集合类型,而且编码格式是哈希桶编码
} else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST){
zset *zs = obj->ptr;
return zs->zsl->length;//有序集合,而且是跳表编码
} else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HT) {
dict *ht = obj->ptr;
return dictSize(ht);//hash对象,哈希桶编码
} else {
return 1; /* Everything else is a single allocation. 其它类型都是只分配一次内存*/
}
}