redis后台线程BIO源码分析1-使用后台线程的场景

Redis使用BIO服务将可能阻塞主线程的I/O操作移到后台,如关闭文件描述符、AOF磁盘同步和大键惰性删除。AOF的fsync操作可配置为每秒同步,减少性能影响但可能丢失最近1秒数据。惰性删除在删除大键时采用异步策略,减少删除时间。Redis判断大键不仅考虑内存大小,还关注内存分配次数。
摘要由CSDN通过智能技术生成

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. 其它类型都是只分配一次内存*/
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值