编辑
2023-10-17
缓存中间件
0
请注意,本文编写于 213 天前,最后修改于 51 天前,其中某些信息可能已经过时。

目录

1.1、对Redis单线程还是多线程的理解?
1.2、为什么Redis单线程也可以这么快?
1.3、Redis对于大key数据的删除应该怎么操作?
1.4、大key的优化,如何避免设计出大key?

1.1、对Redis单线程还是多线程的理解?

Redis在3.x版本中采用的是单线程架构,数据结构简单,能够避免锁的开销和上下文切换,并且可以有很高的QPS。

在4.x版本中,严格意义上讲已经不是单线程了。Redis负责处理客户端请求的线程是单线程的,但是也有多线程的应用,比如异步删除。

Redis 6.x版本中,使用了全新的多线程模型。因为Redis的瓶颈并不是COU消耗,而是在网络I/O模块和内存,所以再Redis 6.x中使用多线程来处理网络I/O部分,充分利用CPU资源,减少网络I/O阻塞带来的性能和可用性损耗。

关于6.x版本的多线程运用具体说明,一次Redis请求,要先建立连接,然后获取、解析操作命令,然后执行命令,最后将响应的结果写到socket上。在Redis的多线程模式下,获取、解析命令,和输出结果这两个过程,可以配置成多线程执行的,因为它们是主要得耗时点。对于命令的执行,是内存操作,依然使用单线程运行。所以Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行,保证线程安全,避免出现并发安全问题。

Redis 6.x多线程机制默认是关闭的,如果想要使用需要在redis.conf文件中完成响应配置:

1、将io-thread-do-reads配置为yes,表示启用多线程

2、设置线程个数io-threads,建议4核cpu线程数设置为2或者3,如果是8核设置为6,线程数最好

1.2、为什么Redis单线程也可以这么快?

1、基于内存操作,Redis的所有数据都存在内存中,因此所有的运算都是在内存级别的,性能很高。

2、数据结构简单,Redis的数据结构的查找和操作的大部分时间复杂度都是O(1)的。

3、多路复用和非阻塞I/O,Redis使用I/O多路复用功能监听多个socket客户端连接,这样就可以使用一个线程处理多个请求,减少线程切换带来的开销,同时也能避免I/O阻塞操作。

4、避免上下文切换,因为是单线程模型,因此避免了很多不必要的上下文切换和多线程竞争,省去了很多资源的消耗。

1.3、Redis对于大key数据的删除应该怎么操作?

如果在线上Redis出现大key,不能直接使用del去删除,因为大key的删除会造成线程阻塞。在阻塞期间,客户端所有的请求都可能会造成超时,当超时越来越多,会造成Redis连接池的耗尽,从而引发严重的事故。

从Redis 4.x开始,Redis的作者就对大key删除造成阻塞的问题做了考虑,于是出现了异步删除,对于异步删除分为用户主动和程序被动:

主动删除

unlink:对于主动删除,Redis提供了unlink替代del,当使用unlink的时候,Redis会先检查要删除元素的个数(比如集合),如果集合的元素小于等于64,会直接执行同步删除,因为这并不能算得上是一个大key,删除这样的key不会浪费太多的资源,但是如果集合的元素超过64的时候,Redis认为这是一大key的概率比较大,这个时候Redis会先将这个集合的key删除,真正的value删除会交给异步线程来操作,这样就能避免对主线程造成影响。

flushall、flushdb(正式环境不允许使用,会清空所有数据):在执行flushdb或者flushall的时候,增加了ASYNC选项,如 FLUSHALL [ASYNC],当用户没有设置ASYNC的时候,此时的flush操作时阻塞的,当设置了ASYNC的时候,会创建一个新的空字典,然后指向它,至于老字典则会交给异步线程,慢慢删除。

同步删除和异步删除源码

c++
// 同步删除就很简单了,直接在字典里进行切断引用并释放空间 int dbSyncDelete(redisDb *db, robj *key) { if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr); if (dictDelete(db->dict,key->ptr) == DICT_OK) { // 如果是集群,删除对应的槽分配数据 if (server.cluster_enabled) slotToKeyDel(key); return 1; } else { return 0; } } // 异步删除 int dbAsyncDelete(redisDb *db, robj *key) { // 同步删除过期键空间key数据,并释放空间 if (dictSize(db->expires) > 0) dictDelete(db->expires, key->ptr); // 因为数据库使用字典实现的,所以把存储k-v数据的dictEntry节点取出来, // 并且在取出来的同时,切断这个节点和键空间的引用。 // 所以称把键空间存储的key-value删除,但是未释放key-value空间 dictEntry *de = dictUnlink(db->dict, key->ptr); if (de) { robj *val = dictGetVal(de); // 获取value的大小(这个大小不是占用的字节数,而是redis内部定义的算法,根据不同类型规定的大小) size_t free_effort = lazyfreeGetFreeEffort(val); // 只有大小大于64才进行异步释放,小于64进行同步释放空间。 // 这里就说明了,异步删除只是对value的空间进行异步释放。 if (free_effort > LAZYFREE_THRESHOLD) { bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL); // 这里把数据节点de的value设置成null,因为value的空间释放交给异步线程去做了。 dictSetVal(db->dict,de,NULL); } } if (de) { // 释放节点的key-value空间,如果上面已经进行异步删除了,这里 // 释放节点的key空间 dictFreeKey(d, he); // 释放节点的value空间,如果上面已经进行异步删除释放了,这里就什么都不做。 dictFreeVal(d, he); // 释放节点空间 zfree(he); // 如果是集群,删除对应的槽分配数据 if (server.cluster_enabled) slotToKeyDel(key); return 1; } else { return 0; } } // 计算对象的大小,用于判断是否可以异步释放空间。这个大小是从需要申请多少次空间来定义的。 // 对于为什么每种类型还要判断实现的数据结构? // 因为Redis的每中类型的底层存储的数据结构都不止一种。 size_t lazyfreeGetFreeEffort(robj *obj) { // list 取链表长度 if (obj->type == OBJ_LIST) { return obj->ptr->len; // set并且数据结构是字典,取字典长度 } else if (obj->type == OBJ_SET && obj->encoding == OBJ_ENCODING_HT) { return dictSize(obj->ptr); // zset并且数据结构是跳跃链表,取跳表长度 } else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST){ return obj->ptr->zsl->length; // hash并且数据结构是字典,取字典长度 } else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HT) { return dictSize(obj->ptr); } else { return 1; /* Everything else is a single allocation. */ } }

被动删除

可选的策略配置有:

lazyfree-lazy-eviction:针对Redis有设置内存到达maxmemory的淘汰策略时,会启动异步删除,此场景异步删除的缺点就是如果删除不及时,内存可能不能及时得到释放。

lazyfree-lazy-expire:对于有ttl的key,在被Redis清理的时候,不执行同步删除,加入异步线程执行删除操作。

replica-lazy-flush:在slave节点加入进来时,会执行flush操作清空自己的数据,如果flush耗时比较久,那么复制缓冲区堆积的数据会越来越多,后面slave同步数据会变得比较慢,开启replica-lazy-flush后,slave的flush可以由异步线程来处理,从而提高同步的速度。

lazyfree-lazy-server-del:这个选项是针对一些指令,比如rename一个一个字段的时候 RENAME KEY NEWKEY,如果这是NEWKEY是存在的,对于RENAME来说,它要删除这个NEWKEY的value,如果这个NEWKEY是一个大key,那么就会造成阻塞,开启这个选项时也会将删除操作交给异步线程去操作,避免阻塞主线程。

1.4、大key的优化,如何避免设计出大key?

big key的定义

字符串类型:体现在单个value值很大,一般认为超过10kb就是big key。

非字符串类型:哈希、列表、集合、有序列表体现在元素个数太多。

一般来说,string类型控制在10kb以内,hash、list、set、zset元素不要超过5000。

优化big key

1、

big list:list1、list2、... listN

big hash:可以将数据分段存储,比如一个大的key,加入存了一百万的用户数据,可以拆分成200个key,每个key下面存放5000个用户数据。

2、如果big key不可避免,也要思考一下要不要每次都把所有元素取出来(例如,有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式去处理。

3、选择合适的数据类型

例如:实体类型(要合理空中和使用数据结构,但是也要注意节省内存和性能之间的平衡)

坏味道

shell
set user:1:name tom set user:1:age 19 set user:1:favor football

好味道

shell
hmset user:1 name tom age 19 favor football

4、控制key的生命周期,Redis不是垃圾桶。建议使用expire设置过期时间(条件允许的话尽量打散过期时间,避免集中过期)

本文作者:whitebear

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!