当前位置: 首页 > 科技观察

为什么单线程Redis可以支持10w+QPS?

时间:2023-03-13 15:03:05 科技观察

单线程为什么能支持10w+QPS?我们经常听说Redis是一个单线程程序。准确的说Redis是一个多线程程序,只不过请求处理部分是用一个线程来实现的。阿里云对RedisQPS的测试结果如下:《Redis如何使用单线程实现每秒10万+QPS?》对非CPU密集型任务使用IO多路复用,对高效数据结构使用纯内存操作《只使用一个线程如何处理多个客户端连接?》这就需要IO多路复用技术,即Java中的NIO。当我们使用阻塞IO(Java中的BIO)时,调用read函数并传入参数n,表示线程读取n个字节后返回,否则一直阻塞。write方法一般不会阻塞,除非writebuffer满了,write会一直阻塞直到buffer有空间释放。当我们使用IO多路复用技术时,当没有数据读写时,客户端线程会直接返回,不会阻塞。这样Redis就可以用一个线程来监听多个Socket。当一个Socket可读或可写时,Redis读取请求,操作内存中的数据,然后返回。“在单线程的时候,不能使用多核CPU,但是Redis中的大部分命令都不是CPU密集型任务,所以CPU不是Redis的瓶颈。”高并发、大数据量的请放轻松。Redis的瓶颈主要体现在内存和网络带宽上,所以可以看到,为了节省内存,Redis在底层数据结构中占用的内存尽可能少,一种数据在不同的数据结构中使用不同的数据结构在不同的场景中。“所以Redis已经可以用单线程处理大量的请求了,就没必要再用多线程了。”另外,“使用单线程有以下优点”没有线程切换的性能开销各种操作不需要加锁(如果是多线程,访问共享资源需要加锁,增加开销)调试方便,可维护性高“最后,Redis是一个内存数据库,各种命令的读写操作都是基于内存的。”大家都知道,操作内存和操作磁盘的效率相差几个数量级。尽管Redis非常高效,但仍有一些缓慢的操作需要避免。Redis运行慢的原因有哪些?Redis的各种命令在一个线程中依次执行。如果一个命令在Redis中执行的时间过长,会影响整体性能,因为只有前面的请求处理完才能处理后面的请求。这些耗时的操作包括以下几个部分。Redis可以通过日志记录那些耗时的命令,使用下面的配置来执行#commandexecution记录慢日志需要5毫秒以上CONFIGSETslowlog-log-slower-than5000#只保留最后500条慢日志CONFIGSETslowlog-max-len500执行以下命令查询最新的慢日志(127.0.0.1:6379>SLOWLOGget51)1)(integer)32693#慢日志ID2)(integer)1593763337#执行时间戳3)(integer)5299#执行时间(微秒)4)1)"LRANGE"#具体的执行命令和参数2)"的使用在上一篇文章中,我们已经介绍了Redis的底层数据结构,它们的时间复杂度如下表所示。名称时间复杂度dict(字典)O(1)ziplist(压缩列表)O(n)zskiplist(跳跃列表)O(logN)quicklist(快速列表)O(n)intset(整数集)O(n)“单元素操作”:对集合中的元素进行增删改查,与底层数据结构相关。比如字典的增删改查时间复杂度为O(1),跳表的增删改查时间复杂度为O(logN)“范围操作”:遍历集合,比如Hash类型的HGETALL,Set类型的SMEMBERS,List类型的LRANGE,ZSet类型的ZRANGE,时间复杂度为O(n),避免使用,改用SCAN一系列命令(hscan用于hash,sscan用于set,zscanforzset)“聚合操作”:这类操作的时间复杂度通常大于O(n),比如SORT,SUNION,ZUNIONSTORE“统计操作”:当你想得到一个集合的个数时中元素的个数,比如LLEN或者SCARD,时间复杂度是O(1),因为它们的底层数据结构quicklist、dict、intset保存了元素个数。“边界操作”:list底层是通过quicklist实现的,quicklist保存了链表的头尾节点,所以对链表头尾节点进行操作的时间复杂度为O(1),比如LPOP,RPOP,LPUSH,RPUSH“当你想在Redis中获取key时,避免使用keys*”,Redis中存储的键值对存储在字典中(类似Java中的HashMap,也是实现的byarray+linkedlist),key的类型是string,value的类型可以是string、set、list等。例如,当我们执行下面的命令时,redis的字典结构是这样的:setbookNameredis;rushfruitsbananaapple;我们可以使用keys命令查询Redis中的特定key,如下所示#查询所有keykeys*#查询前缀为bookkeykeysbook*keys命令的复杂度为O(n),会遍历这个dict中的所有key,如果Redis中存储了很多key,所有读写Redis的命令都会被延迟,所以不要在生产环境中使用这个命令(如果你准备离开,祝你玩得开心)。“既然不能用钥匙,那肯定有办法,那就是扫描。”scan相比keys有以下特点:虽然也是O(n),但是通过cursor分布执行,不会阻塞线程。同keys,提供模式匹配功能从全量遍历开始到全量遍历结束,全量遍历返回所有一直存在于数据集中的元素,但是如果一个元素可能返回多次相同的元素在迭代过程中被添加到数据集中,或者在迭代过程中被从数据集中删除,那么这个元素可能会返回也可能不会返回有兴趣的小伙伴可以通过扫描源码的实现分析来了解这些特性。“用zscan遍历zset,hscan遍历hash,sscan遍历set,结构里面有dict。”操作bigkey“如果一个key对应的value非常大,那么这个key就叫做bigkey。写一个bigkey需要更长的时间来分配内存。同样,删除一个bigkey释放内存也需要更长的时间。”如果在slowlog中发现SET/DEL等复杂度不高的命令,应排查是否是写入bigkey导致的。“如何定位bigkey?”Redis提供扫描bigkey的命令$redis-cli-h127.0.0.1-p6379--bigkeys-i0.01...---------summary-------Sampled829675keysinthekeyspace!Totalkeylengthinbytesis10059825(avglen12.13)Biggeststringfound'key:291880'has10bytesBiggestlistfound'mylist:004'has40itemsBiggestsetfound'myset:2386'has38membersBiggesthashfound'myhash:3574'has37fieldsBiggestzsetfound'myzset:2704'has42members36313stringswith363130bytes(04.38%ofkeys,avgsize10.00)787393listswith896540items(94.90%ofkeys,AvgSize1.14)1994Setswith40052Members(00.24%的keys,avgsize20.09)1990hashswith39632Fields(00.24%的keys,avgsize19.92),199750.9750mbers(00.24%),000.24.24%992..key在内存中的个数、总占用内存、每个key平均占用内存、每种类型最大占用内存、key名称每种数据类型所占比例、平均大小。这个命令的原理是redis在内部执行scan命令遍历实例中所有的key,然后针对key的类型执行strlen,llen,hlen,scard,zcard命令获取长度字符串类型和容器类型(list、hash、set、zset)使用该命令时,需要注意以下两个问题。在对在线实例进行bigkey扫描时,为了避免ops(每秒操作数)突然增加,可以通过-i添加一个sleep参数。上面的意思是,每100扫描命令将休眠0.01秒。对于容器类型(list、hash、set、zset)来说,扫描到的key是元素最多的key,但是一个key中元素多并不一定代表它占用的内存多。性能问题?”尽量避免写入bigkey如果你使用redis4.0以上,可以使用unlink命令代替del,这个命令可以释放key内存操作,放到后台线程执行如果你是使用redis6.0以上版本可以开启免懒机制(lazyfree-lazy-user-delyes),del命令执行时,也会放到后台线程执行大量keyexpirations,我们可以给Redis中的key设置过期时间,那么当key过期了,什么时候删除呢?“如果让我们写Redis的过期策略,我们会想到下面三种方案”定时删除,同时设置key的过期时间,创建一个定时器,当key的过期时间到来时,立即对key执行删除操作,惰性删除,每次获取key时,判断key是否过期.如果过期,则删除密钥。er一段时间,检查一次keys,把里面过期的keys删除。定时删除策略对CPU不友好。当过期key较多时,使用Redis线程删除过期key,会影响正常请求的响应。定时删除策略对CPU不利。不友好,当过期key较多时,使用Redis线程删除过期key,会影响正常请求的响应。懒惰删除读CPU比较好,但是会浪费很多内存。如果一个key设置了过期时间,放到了内存中,但是没有被访问到,那么它就会一直存在于内存中。周期性删除策略对CPU和内存更友好。redisexpiredkey的删除策略选择了以下两种惰性删除和周期性删除。“惰性删除”客户端访问key时,会检查key的过期时间,过期则立即删除。「定期删除」Redis会将设置了过期时间的key放在一个独立的字典中,定期遍历这个字典,用于删除过期的key。遍历策略为:每秒扫描10次过期,每次从过期字典中随机选择20个key,删除这20个key中的过期key。如果过期键的比例超过1/4,则继续执行步骤1。每次扫描时间上限默认不超过25ms,避免线程卡死。“因为Redis中的过期key是主线程删除的,为了不阻塞用户的请求,删除过期key的时间是小次数。”。源码可以参考expire.c中的activeExpireCycle方法。为了防止主线程一直删除key,我们可以采用以下两种方案,给同时过期的key加上一个随机数,打散过期时间,减轻清除key的压力。如果使用redis4.0以上的redis版本可以开启lazy-free机制(lazyfree-lazy-expireyes)。删除过期key时,释放内存的操作放在后台线程执行内存限制,触发淘汰策略。图片Redis是一个内存数据库。当Redis使用的内存超过物理内存的限制时,内存数据会频繁地与磁盘进行交换,交换会导致Redis的性能急剧下降。所以在生产环境中我们通过配置参数maxmemoey来限制使用的内存大小。当实际使用的内存超过maxmemoey时,Redis提供了以下可选策略。noeviction:写请求返回错误随机删除volatile-ttl:按照过期时间的顺序删除,越早过期的先删除allkeys-lru:在所有键值对中,使用lru算法删除allkeys-lfu:在所有键值对中,使用lfu算法删除allkeys-random:随机删除所有键值对”Redis淘汰策略也在主线程中执行。但是当内存超过上限时Redis,有些key每次写入都需要淘汰,导致request时间变长”,可以通过增加内存或者将数据放在多个实例中,将淘汰策略改为随机淘汰来改善。一般来说,随机淘汰比lru要快很多,避免存储bigkeys,减少freeingmemory的写耗时。AOF日志方式总是。Redis的持久化机制包括RDB快照和AOF日志。在每次写入命令后,Redis提供了以下三种刷新机制:always:同步回写,写入命令执行后同步到磁盘everysec:每隔几秒回写一次,每次写入命令执行完后,只写入先记录到aof文件的内存缓冲区,每隔1秒将缓冲区的内容写入磁盘否:操作系统控制回写,每次执行写命令后,只将日志写入内存缓冲区首先读取aof文件,由操作系统决定何时将缓冲区内容写回磁盘。当aof的flush机制为always时,redis会将写命令flush到磁盘,只会返回。整个过程都在Redis主线程中进行,必然会拖慢redis的性能。当aof的刷盘机制为everysec时,redis写内存后会返回,刷盘操作放在后台线程。执行后,后台线程每隔1秒将内存中的数据刷新到磁盘中。当aof的diskflushing机制为no时,关机后可能会丢失部分数据,一般不会用到。“一般情况下,aof磁盘刷盘机制可以配置为everysec。”forktakestoolong让子进程执行的方式,主线程内存越大,阻塞时间越长。》可以通过以下方式优化控制Redis实例的内存大小,尽量控制在10g以内,因为内存越大阻塞时间越长。配置合理的持久化策略,比如在上面生成rdb快照从节点,本文转载自微信♂《Java知识殿堂》,作者李利民,转载请联系Java食堂公众号。