面试官:Redis新版本开始引入多线程谈谈你的看法_第1页
面试官:Redis新版本开始引入多线程谈谈你的看法_第2页
面试官:Redis新版本开始引入多线程谈谈你的看法_第3页
面试官:Redis新版本开始引入多线程谈谈你的看法_第4页
面试官:Redis新版本开始引入多线程谈谈你的看法_第5页
已阅读5页,还剩11页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

面试官:Redis新版本开始引入多线程,谈谈你的看法?Redis作为一个基于内存的缓存系统,一直以高性能著称,因没有上下文切换以及无锁操作,即使在单线程处理情况下,读速度仍可达到11万次/s,写速度达到8.1万次/s。但是,单线程的设计也给Redis带来一些问题:只能使用CPU一个核;如果删除的键过大(比如Set类型中有上百万个对象),会导致服务端阻塞好几秒;QPS难再提高。针对上面问题,Redis在4.0版本以及6.0版本分别引入了LazyFree以及多线程IO,逐步向多线程过渡,下面将会做详细介绍。单线程原理都说Redis是单线程的,那么单线程是如何体现的?如何支持客户端并发请求的?为了搞清这些问题,首先来了解下Redis是如何工作的。Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象;服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作,比如连接accept,read,write,close等;时间事件:Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象,比如过期键清理,服务状态统计等。事件调度如上图,Redis将文件事件和时间事件进行抽象,时间轮训器会监听I/O事件表,一旦有文件事件就绪,Redis就会优先处理文件事件,接着处理时间事件。在上述所有事件处理上,Redis都是以单线程形式处理,所以说Redis是单线程的。此外,如下图,Redis基于Reactor模式开发了自己的I/O事件处理器,也就是文件事件处理器,Redis在I/O事件处理上,采用了I/O多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数,通过一个线程实现了多客户端并发处理。多路复用件正因为这样的设计,在数据处理上避免了加锁操作,既使得实现上足够简洁,也保证了其高性能。当然,Redis单线程只是指其在事件处理上,实际上,Redis也并不是单线程的,比如生成RDB文件,就会fork一个子进程来实现,当然,这不是本文要讨论的内容。LazyFree机制如上所知,Redis在处理客户端命令时是以单线程形式运行,而且处理速度很快,期间不会响应其他客户端请求,但若客户端向Redis发送一条耗时较长的命令,比如删除一个含有上百万对象的Set键,或者执行flushdb,flushall操作,Redis服务器需要回收大量的内存空间,导致服务器卡住好几秒,对负载较高的缓存系统而言将会是个灾难。为了解决这个问题,在Redis4.0版本引入了LazyFree,将慢操作异步化,这也是在事件处理上向多线程迈进了一步。如在其博客中所述,要解决慢操作,可以采用渐进式处理,即增加一个时间事件,比如在删除一个具有上百万个对象的Set键时,每次只删除大键中的一部分数据,最终实现大键的删除。但是,该方案可能会导致回收速度赶不上创建速度,最终导致内存耗尽。因此,Redis最终实现上是将大键的删除操作异步化,采用非阻塞删除(对应命令UNLINK),大键的空间回收交由单独线程实现,主线程只做关系解除,可以快速返回,继续处理其他事件,避免服务器长时间阻塞。以删除(DEL命令)为例,看看Redis是如何实现的,下面就是删除函数的入口,其中,lazyfree_lazy_user_del是是否修改DEL命令的默认行为,一旦开启,执行DEL时将会以UNLINK形式执行。void

delCommand(client

*c)

{

delGenericCommand(c,server.lazyfree_lazy_user_del);

}

/*

This

command

implements

DEL

and

LAZYDEL.

*/

void

delGenericCommand(client

*c,

int

lazy)

{

int

numdel

=

0,

j;

for

(j

=

1;

j

<

c->argc;

j++)

{

expireIfNeeded(c->db,c->argv[j]);

//

根据配置确定DEL在执行时是否以lazy形式执行

int

deleted

=

lazy

?

dbAsyncDelete(c->db,c->argv[j])

:

dbSyncDelete(c->db,c->argv[j]);

if

(deleted)

{

signalModifiedKey(c,c->db,c->argv[j]);

notifyKeyspaceEvent(NOTIFY_GENERIC,

"del",c->argv[j],c->db->id);

server.dirty++;

numdel++;

}

}

addReplyLongLong(c,numdel);

}同步删除很简单,只要把key和value删除,如果有内层引用,则进行递归删除,这里不做介绍。下面看下异步删除,Redis在回收对象时,会先计算回收收益,只有回收收益在超过一定值时,采用封装成Job加入到异步处理队列中,否则直接同步回收,这样效率更高。回收收益计算也很简单,比如String类型,回收收益值就是1,而Set类型,回收收益就是集合中元素个数。/*

Delete

a

key,

value,

and

associated

expiration

entry

if

any,

from

the

DB.

*

If

there

are

enough

allocations

to

free

the

value

object

may

be

put

into

*

a

lazy

free

list

instead

of

being

freed

synchronously.

The

lazy

free

list

*

will

be

reclaimed

in

a

different

bio.c

thread.

*/

#define

LAZYFREE_THRESHOLD

64

int

dbAsyncDelete(redisDb

*db,

robj

*key)

{

/*

Deleting

an

entry

from

the

expires

dict

will

not

free

the

sds

of

*

the

key,

because

it

is

shared

with

the

main

dictionary.

*/

if

(dictSize(db->expires)

>

0)

dictDelete(db->expires,key->ptr);

/*

If

the

value

is

composed

of

a

few

allocations,

to

free

in

a

lazy

way

*

is

actually

just

slower...

So

under

a

certain

limit

we

just

free

*

the

object

synchronously.

*/

dictEntry

*de

=

dictUnlink(db->dict,key->ptr);

if

(de)

{

robj

*val

=

dictGetVal(de);

//

计算value的回收收益

size_t

free_effort

=

lazyfreeGetFreeEffort(val);

/*

If

releasing

the

object

is

too

much

work,

do

it

in

the

background

*

by

adding

the

object

to

the

lazy

free

list.

*

Note

that

if

the

object

is

shared,

to

reclaim

it

now

it

is

not

*

possible.

This

rarely

happens,

however

sometimes

the

implementation

*

of

parts

of

the

Redis

core

may

call

incrRefCount()

to

protect

*

objects,

and

then

call

dbDelete().

In

this

case

we'll

fall

*

through

and

reach

the

dictFreeUnlinkedEntry()

call,

that

will

be

*

equivalent

to

just

calling

decrRefCount().

*/

//

只有回收收益超过一定值,才会执行异步删除,否则还是会退化到同步删除

if

(free_effort

>

LAZYFREE_THRESHOLD

&&

val->refcount

==

1)

{

atomicIncr(lazyfree_objects,1);

bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);

dictSetVal(db->dict,de,NULL);

}

}

/*

Release

the

key-val

pair,

or

just

the

key

if

we

set

the

val

*

field

to

NULL

in

order

to

lazy

free

it

later.

*/

if

(de)

{

dictFreeUnlinkedEntry(db->dict,de);

if

(server.cluster_enabled)

slotToKeyDel(key->ptr);

return

1;

}

else

{

return

0;

}

}通过引入athreadedlazyfree,Redis实现了对于SlowOperation的Lazy操作,避免了在大键删除,FLUSHALL,FLUSHDB时导致服务器阻塞。当然,在实现该功能时,不仅引入了lazyfree线程,也对Redis聚合类型在存储结构上进行改进。因为Redis内部使用了很多共享对象,比如客户端输出缓存。当然,Redis并未使用加锁来避免线程冲突,锁竞争会导致性能下降,而是去掉了共享对象,直接采用数据拷贝,如下,在3.x和6.x中ZSet节点value的不同实现。//

3.2.5版本ZSet节点实现,value定义robj

*obj

/*

ZSETs

use

a

specialized

version

of

Skiplists

*/

typedef

struct

zskiplistNode

{

robj

*obj;

double

score;

struct

zskiplistNode

*backward;

struct

zskiplistLevel

{

struct

zskiplistNode

*forward;

unsigned

int

span;

}

level[];

}

zskiplistNode;

//

6.0.10版本ZSet节点实现,value定义为sds

ele

/*

ZSETs

use

a

specialized

version

of

Skiplists

*/

typedef

struct

zskiplistNode

{

sds

ele;

double

score;

struct

zskiplistNode

*backward;

struct

zskiplistLevel

{

struct

zskiplistNode

*forward;

unsigned

long

span;

}

level[];

}

zskiplistNode;去掉共享对象,不但实现了lazyfree功能,也为Redis向多线程跨进带来了可能,正如所述:Nowthatvaluesofaggregateddatatypesarefullyunshared,andclientoutputbuffersdon’tcontainsharedobjectsaswell,thereisalottoexploit.ForexampleitisfinallypossibletoimplementthreadedI/OinRedis,sothatdifferentclientsareservedbydifferentthreads.Thismeansthatwe’llhaveagloballockonlywhenaccessingthedatabase,buttheclientsread/writesyscallsandeventheparsingofthecommandtheclientissending,canhappenindifferentthreads.多线程I/O及其局限性Redis在4.0版本引入了LazyFree,自此Redis有了一个LazyFree线程专门用于大键的回收,同时,也去掉了聚合类型的共享对象,这为多线程带来可能,Redis也不负众望,在6.0版本实现了多线程I/O。搜索后端架构师公众号回复“架构整洁”,送你一份惊喜礼包。实现原理正如官方以前的回复,Redis的性能瓶颈并不在CPU上,而是在内存和网络上。因此6.0发布的多线程并未将事件处理改成多线程,而是在I/O上,此外,如果把事件处理改成多线程,不但会导致锁竞争,而且会有频繁的上下文切换,即使用分段锁来减少竞争,对Redis内核也会有较大改动,性能也不一定有明显提升。多线程IO实现如上图红色部分,就是Redis实现的多线程部分,利用多核来分担I/O读写负荷。在事件处理线程每次获取到可读事件时,会将所有就绪的读事件分配给I/O线程,并进行等待,在所有I/O线程完成读操作后,事件处理线程开始执行任务处理,在处理结束后,同样将写事件分配给I/O线程,等待所有I/O线程完成写操作。以读事件处理为例,看下事件处理线程任务分配流程:int

handleClientsWithPendingReadsUsingThreads(void)

{

...

/*

Distribute

the

clients

across

N

different

lists.

*/

listIter

li;

listNode

*ln;

listRewind(server.clients_pending_read,&li);

int

item_id

=

0;

//

将等待处理的客户端分配给I/O线程

while((ln

=

listNext(&li)))

{

client

*c

=

listNodeValue(ln);

int

target_id

=

item_id

%

server.io_threads_num;

listAddNodeTail(io_threads_list[target_id],c);

item_id++;

}

...

/*

Wait

for

all

the

other

threads

to

end

their

work.

*/

//

轮训等待所有I/O线程处理完

while(1)

{

unsigned

long

pending

=

0;

for

(int

j

=

1;

j

<

server.io_threads_num;

j++)

pending

+=

io_threads_pending[j];

if

(pending

==

0)

break;

}

...

return

processed;

}I/O线程处理流程:void

*IOThreadMain(void

*myid)

{

...

while(1)

{

...

//

I/O线程执行读写操作

while((ln

=

listNext(&li)))

{

client

*c

=

listNodeValue(ln);

//

io_threads_op判断是读还是写事件

if

(io_threads_op

==

IO_THREADS_OP_WRITE)

{

writeToClient(c,0);

}

else

if

(io_threads_op

==

IO_THREADS_OP_READ)

{

readQueryFromClient(c->conn);

}

else

{

serverPanic("io_threads_op

value

is

unknown");

}

}

listEmpty(io_threads_list[id]);

io_threads_pending[id]

=

0;

if

(tio_debug)

printf("[%ld]

Done\n",

id);

}

}局限性从上面实现上看,6.0版本的多线程并非彻底的多线程,I/O线程只能同时执行读或者同时执行写操作,期间事件处理线程一直处于等待状态,并非流水线模型,有很多轮训等待开销。Tair多线程实现原理相较于6.0版本的多线程,Tair的多线程实现更加优雅。如下图,Tair的MainThread负责客户端连接建立等,IOThread负责请求读取、响应发送、命令解析等,WorkerThread线程专门用于事件处理。IOThread读取用户的请求并进行解析,之后将解析结果以命令的形式放在队列中发送给WorkerThread处理。WorkerThread将命令处理完成后生成响应,通过另一条队列发送给IOThread。为了提高线程的并行度,IOThread和WorkerThread之间采用无锁队列和管道进行数据交换,整体性能会更好。小结Redis4.0引入LazyFree线程,解决了诸如大键删除导致服务器阻塞问题,在6.0版本引入了I/OThread线程,正式实现了多线程,但相较于Tair,并不太优雅,而且性能提升上并不多,压测看,多线程版本性能是单线程版本的2倍,Tair多线程版本则是单线程版本的3倍。在看来,Redis多线程无非两种思路,I/Othreading和Slowcommandsthreading,正如在其博客中所说:I/OthreadingisnotgoingtohappeninRedisAFAIK,becauseaftermuchconsiderationIthinkit’salotofcomplexitywithoutagoodreason.ManyRedissetupsarenetworkormemoryboundactually.AdditionallyIreallybelieveinashare

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论