面试官:Redis新版本开始引入多线程,谈谈你的看法?
Posted xhmj12
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官:Redis新版本开始引入多线程,谈谈你的看法?相关的知识,希望对你有一定的参考价值。
相关阅读:2T架构师学习资料干货分享
来源:juejin.cn/post/6928407842009546766
Redis作为一个基于内存的缓存系统,一直以高性能著称,因没有上下文切换以及无锁操作,即使在单线程处理情况下,读速度仍可达到11万次/s,写速度达到8.1万次/s。但是,单线程的设计也给Redis带来一些问题:
针对上面问题,Redis在4.0版本以及6.0版本分别引入了Lazy Free以及多线程IO,逐步向多线程过渡,下面将会做详细介绍。
都说Redis是单线程的,那么单线程是如何体现的?如何支持客户端并发请求的?
为了搞清这些问题,首先来了解下Redis是如何工作的。
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
正因为这样的设计,在数据处理上避免了加锁操作,既使得实现上足够简洁,也保证了其高性能。当然,Redis单线程只是指其在事件处理上,实际上,Redis也并不是单线程的,比如生成RDB文件,就会fork一个子进程来实现,当然,这不是本文要讨论的内容。
如上所知,Redis在处理客户端命令时是以单线程形式运行,而且处理速度很快,期间不会响应其他客户端请求,但若客户端向Redis发送一条耗时较长的命令,比如删除一个含有上百万对象的Set键,或者执行flushdb,flushall操作,Redis服务器需要回收大量的内存空间,导致服务器卡住好几秒,对负载较高的缓存系统而言将会是个灾难。为了解决这个问题,在Redis 4.0版本引入了Lazy Free,将慢操作异步化,这也是在事件处理上向多线程迈进了一步。搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
以删除(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);
/* 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;
通过引入a threaded lazy free,Redis实现了对于Slow Operation的Lazy操作,避免了在大键删除,FLUSHALL,FLUSHDB时导致服务器阻塞。当然,在实现该功能时,不仅引入了lazy free线程,也对Redis聚合类型在存储结构上进行改进。因为Redis内部使用了很多共享对象,比如客户端输出缓存。当然,Redis并未使用加锁来避免线程冲突,锁竞争会导致性能下降,而是去掉了共享对象,直接采用数据拷贝,如下,在3.x和6.x中ZSet节点value的不同实现。搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
// 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;
去掉共享对象,不但实现了lazy free功能,也为Redis向多线程跨进带来了可能,正如作者所述:
多线程I/O及其局限性
实现原理
正如官方以前的回复,Redis的性能瓶颈并不在CPU上,而是在内存和网络上。因此6.0发布的多线程并未将事件处理改成多线程,而是在I/O上,此外,如果把事件处理改成多线程,不但会导致锁竞争,而且会有频繁的上下文切换,即使用分段锁来减少竞争,对Redis内核也会有较大改动,性能也不一定有明显提升。
如上图红色部分,就是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;
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线程只能同时执行读或者同时执行写操作,期间事件处理线程一直处于等待状态,并非流水线模型,有很多轮训等待开销。搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
Tair多线程实现原理
相较于6.0版本的多线程,Tair的多线程实现更加优雅。如下图,Tair的Main Thread负责客户端连接建立等,IO Thread负责请求读取、响应发送、命令解析等,Worker Thread线程专门用于事件处理。IO Thread读取用户的请求并进行解析,之后将解析结果以命令的形式放在队列中发送给Worker Thread处理。Worker Thread将命令处理完成后生成响应,通过另一条队列发送给IO Thread。为了提高线程的并行度,IO Thread和Worker Thread之间采用无锁队列和管道进行数据交换,整体性能会更好。
小结
Redis 4.0引入Lazy Free线程,解决了诸如大键删除导致服务器阻塞问题,在6.0版本引入了I/O Thread线程,正式实现了多线程,但相较于Tair,并不太优雅,而且性能提升上并不多,压测看,多线程版本性能是单线程版本的2倍,Tair多线程版本则是单线程版本的3倍。在作者看来,Redis多线程无非两种思路,I/O threading和Slow commands threading,正如作者在其博客中所说:
Redis作者更倾向于采用集群方式来解决I/O threading,尤其是在6.0版本发布的原生Redis Cluster Proxy背景下,使得集群更加易用。此外,作者更倾向于slow operations threading(比如4.0版本发布的Lazy Free)来解决多线程问题。后续版本,是否会将IO Thread实现的更加完善,采用Module实现对慢操作的优化,着实值得期待。
以上是关于面试官:Redis新版本开始引入多线程,谈谈你的看法?的主要内容,如果未能解决你的问题,请参考以下文章
面试官:Redis 单线程已经很快,为何 6.0要引入多线程?有啥优势?