版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
面试官: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. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 幼儿园2021年度安全工作总结范文(9篇)
- 担保书承诺书7篇
- 非暴力沟通心得体会三篇
- 高中学习计划(合集14篇)
- 养殖场房屋租赁合同电子版(31篇)
- 关于防溺水的演讲稿(30篇)
- 2024届黑龙江省哈尔滨市第九中学校高三下学期第五次模拟考试语文试题
- 竞选班干部的演讲稿【常用15篇】
- 河南省孟州市职业教育中心2023-2024学年上学期期末高一年级《园艺植物生产技术》试卷(附答案)
- 2023-2024学年湖南省岳阳市湘阴县杨林寨乡中学九年级(上)第一次月考语文试卷
- 电子商务实训总结反思报告
- 第六单元 大单元教学设计(表格式) 2023-2024学年统编版高中语文必修下册
- 广东省东莞市重点中学2024届中考英语模拟试题含答案
- 国开2024年《班级管理》形考作业1-3答案
- 老小区消防改造工程施工方案
- 2024年贵阳信用增进投资有限责任公司招聘笔试参考题库附带答案详解
- 2024-2029年中国冬虫夏草行业发展分析及投资风险预测分析报告
- 产品解决方案部门职责
- 送料机械手设计说明书
- 粮油配送方案
- 酒吧的创业计划书
评论
0/150
提交评论