一篇关于Redis的好文章

时间:2021-11-24 04:28:31

Redis作为缓存使用,在值大小为1k的情况下,可以支持到每秒近十万次的set操作,可见redis的运行效率是非常的高的。但是为什么我们还会有的时候会遇到redis的瓶颈呢?一般来说,都是因为我们没有对redis的所有的操作有一个全面直观的了解。
Redis运行模式
Redis是运行在单线程下的,也就是说,一个redis实例最多也就只能占用一个cpu,当redis实例线程运行所使用的cpu到达100%后,就无法进行更多的操作了,甚至从操作系统层面就开始拒绝连接了。然后我们就遇到了最迷茫的事情,为什么我设置了很多的连接,但是都无法连接到redis服务呢,原因大抵如此。

Redis在执行一个操作的时候,其他所有的操作,包括系统的,后台的等等,都会出现阻塞,一旦有阻塞,就会出现返回变慢的情况。

虽然说redis的运行时非常快的,但是也会受到很多其他因素的影响,在分析过程中,要注意都有什么耗时的操作在霸占cpu(依旧是单线程的问题),这些操作能不能分解,这些操作能不能再做缓存,这些操作是否是必要的,在优化处理时都应该考虑这样的问题。

总而言之,redis是以单线程来应对上层多线程的调用,在编码过程中,使用到redis就要考虑到这一层。
Redis操作中需要规避的一些问题
Redis提供了比较多的数据结构,让单纯的kv结构更加丰富,但随之而来的问题就是,数据结构越是复杂,操作时间越长,记住redis只能单线程!

当然,redis就是为了大量快速地读写一些数据而生的,放心大胆地去用,不然搭建一个redis干嘛呢?

为了放心地使用redis,下面列举一些需要特别注意,必要时要规避这些操作。这些操作有时会很慢,但是具体有多慢呢?我们不能用通常的模式去理解,如果说这个操作只需要10ms呢?是否算慢呢?当一个操作需要10ms时,这个redis的实例,在1s中只能执行100次同样的操作,这样算快还是慢呢?当redis的调用量上来以后,这个操作就成了系统的瓶颈了。

SISMEMBER
这个操作是查看集合中的成员。在集合较大的情况下,会变得比较慢。所以,一般不要直接返回整个集合中的成员,而是采用标准的调用方式SISMEMBER key member,查看集合是否具有该成员,从而避免大量的无用的数据处理。

SINTER
返回一个集合的全部成员,该集合是所有给定集合的交集。当两个巨大的集合进行交集的运算,其效果也可以是毁灭性的。

LPUSH & RPUSH
在队列头部/尾部插入一串key。这个操作本身并无什么大问题,但是我们又考虑到redis是一个单线程,那么我们一次性插入了大量的值,就会造成阻塞,那么就有必要把这一次操作分散为多次的操作,循环去做了,虽然觉得循环会耗时,但是在没有读写分离的情况下,这种处理方式总归是比直接阻塞redis要来得好的。建议一次性增加200个左右为佳。但是这也受限于key值的大小。具体的问题具体分析,在调试和测试中要注意这部分的耗时,可以控制在10ms以内即可(redis默认超过10ms的操作对于redis是慢的)。

MSET & MGET
批量设置和获取一串key的值。此操作与上面的也是一样的,要考虑到整体的耗时,在需要的时候要做到循环设置/获取,标准也是尽量控制一次操作在10ms以内。

KEYS
KEYS操作,查询并枚举redis中与给定规则相符的key。此物为大杀器,看似每个操作只需要40-70ms,在编码和功能性的测试中,根本无法感知到其带来的恶果,因为70ms实在是很难感知到到底有多长,但是用另一个指标来衡量,就很明显看出来了。取平均值,假设KEYS操作耗时50ms,那么一个redis实例的qps只有可怜的20,还是在不做其他动作的情况下。
此处要着重说明之前的一个线上问题的解决,就是因为KEYS操作引起,最后调用者临时做了读写分离,并且将redis的实例拉到了恐怖的21个才勉强支持起线上的调用量,堪称本司有史以来最豪华redis,并且这个还是在用户数没有暴增的情况下。
所以切记,KEYS是禁止使用的!(后续再新机房中的redis会直接改掉这个操作,具体怎么再来调用KEYS操作,我就不告诉你!)

CONFIG GET & CONFIG SET
非管理人员,禁止使用!

Redis需要注意的操作其实并不多,并且很多情况下,根本不会造成压力,但是这些情况都是需要在编码时注意的,去衡量并正确高效地使用redis。记住一句,我们是单线程!我们是运行在毫秒级别的服务!
Redis的设计中需要注意的
Redis本身没有强的模式,在使用中,所有人都可以随意地设置redis的存储,那么我们要如何才能高效地使用redis呢?有一些问题就需要在存储的设计时要注意了。

Key的命名要简短,且有意义。Key在redis中也是要占用空间的,并且内存是很宝贵的,所以要节约使用。当key名有意义,那么在程序的逻辑处理过程中,就有可能直接获得key名,从而直接去获取到相应的value了。

Value值不宜过大。当value在1k的时候,redis可以支持到10w/s左右,那当value的大小达到10k,那么redis就只能支持到1w/s左右了。可见value的大小对于redis的效率还是很有影响的。所以,每个key的value值要尽量精简,哪怕是冗余一些多余的key也不要制造一个超级大的key因为那样的话,还不如使用其他的存储来得实惠了。

返回的数据要尽量紧凑,小型。虽然redis没有对客户端做什么限制,但是他是预留了功能的,可以在一次请求超过了多久,或者是总数据量超过多少,还有就是规定时间内发送数据超过多少的情况下,就可以直接杀死链接,没有任何返回,也没有任何道理。所以要注意,只返回有用的东西。

对于list要活学活用。我们不能做KEYS,那么当我们又需要将某种类型的KEY进行计数的时候,我们就应该去使用list了。将一些相关的key都存放于一个list中,我们就可以轻松地使用LLEN来获取到list的长度,其效果可以抵消一部分KEYS的应用场景了。对于list也并不是万灵药,虽然在很多情况下,list可以消耗更少的资源来做到和key相同的效果,但是也要注意,list不适宜过长,要做到可控。

Set(集合)的运用,与list类似,就不多描述了,参见一下redis的命令列表吧。这也是一种非常有用的数据结构。

对于list和set还是要说两句,正因为有了这样的数据结构,我们得以用更小的代价管理更加庞大的key。

要用SETEX去设置key的过期时间。Redis本身就是一个缓存,只是一个缓存,在需要持久化的场景下,不要妄想能在redis中做的存储大量的值。所以redis中尽量存储一些不需要持久化的东西,并且如果关联到持久化的数据的话,最好能有缓存载入的脚本,能手工载入一些缓存数据,在出现整个redis集群完全崩溃的情况下可以减少crash到数据库的时间。

我们是单线程!又说到这个话题了,redis就是个单线程,我觉得也不太可能改造了。那么我们怎么来应对大量并发呢?
其实并不是没招,招有的是。
方法一,我们可以使用队列,将请求放入队列中,再取出处理,将大量的并发序列化,持续地,高效地在redis中处理。
方法二,读写分离。在有可能的情况下一定要具备读写分离功能。当序列化后依然无法响应大量的并发,那么我们就需要做读写分离了。这样只要在量上来时,多拉实例,并添加到负载均衡即可了。

要为分布式做好准备。现在本司在redis的分布式上选型是twemproxy,在分布式的情景下,很多操作会受到限制,所以,当可以预见到redis在将来会增长到需要做分布式的场景下,就需要注意了,在编码中就要有意去规避这些操作,以减少将来修改的成本。

如何去界定一个redis是否需要做成分布式,建议当redis的数据量超过15G时就需要更换为分布式,当redis的数据量达到8G,并且有增长的趋势时,就需要考虑分布式的方案了。

就本司当前情况来看,只有少量的一些应用会达到分布式的要求,其他的,因为历史原因,不适合马上切换为分布式。大量的应用还是处于读写分离即可支持的情况下,所以不一定上线,但一定要有读写分离的方案,或者是预案。
后续的一些展望
在后续的规划中,redis更加倾向于小型化,轻量化的部署。这里有两条路,一是集中化的分布式redis,虽然将数据打散了,这样解决了容量的问题,但是,但是!我们特么还是单线程!同时,集群化,也限制了一些特殊的操作,但是这些小技巧有的时候又是非常nice的。那么我们就提出了第二个方案,二级缓存。在持久化层上有几个集群,这些集群又可做读写分离,在这个大容量的缓存上每个服务,甚至是每个进程都对应一个自己的超级轻量化redis,不需要预加载,不怕数据丢失,随时可重启的这种,当然,可以做到读写分离也是极好的。每个关于缓存的请求,都会先去查询二级缓存中的redis(轻量化的那个),当不命中时,会去一级缓存(大容量的那个)中查询,如果命中,则将数据加载至二级缓存,返回结果。当一级缓存也不能命中时,就要去到持久化层获取实际的数据,并同时将数据加载到一级和二级缓存了。

二级缓存的模式下,又涉及到读写分离等问题,但那时,将是千万级别并发访问了,希望将来有一天能够看见。