美文网首页程序员JAVA
「微服务」缓存本质全分析、对比与实践

「微服务」缓存本质全分析、对比与实践

作者: 厦门张明爽 | 来源:发表于2019-12-31 08:34 被阅读0次

    使用缓存的目的和问题

    缓存的目的是加快数据的读取数据,有效减少核心应用、数据库的压力。但是缓存的使用也同时牺牲了其他方面的优势,比如数据的强一致性。

    因此,我们在使用缓存提高读取性能的同时,一定会失去一定的一致性。在某些场景下对读取缓存的一致性要求并不很高,因此可以牺牲一定的一致性来换取高性能。

    下面,我们以秒杀场景中的库存查询为例来说明缓存对性能的提升和一致性的权衡。假设系统的需要满足10W/s的请求查询。并且查询的数据允许与实际数据允许5秒以内的时间窗口差异。

    image

    从上图流程与服务的吞吐量假设中,我们可以很容易的对系统需求进行设计。

    Note:上图所示中为假设数据,在真实环境中所有的数据都需要经过多轮系统的压测得到较为准确的数据。

    1. 服务(Service)可以支撑5K/s的吞吐,所以10W/s的需求需要20台服务进行支撑(这里不考虑服务不可用问题,假设100%可用,正常真实系统需要有余量)。

    2. 缓存(Cache)可以支撑5W/s的吞吐,所以10W/s的需求只需要2台即可以支撑。

    3. 数据库(Mysql)可以支撑1K/s的吞吐,所以不使用缓存,则需要100台来进行支撑。在使用缓存后,假设系统商品数据为3000种,缓存过期时间为5s,则数据库请求量为3000/5s=600/s,只需要1台。

    Note:关于第3点数据计算的说明

    首先,缓存的设计上必须防止击穿(热点Key过期时,所有请求直接打到数据库)与雪崩(大量Key同时过期,导致请求大量打到数据库)。

    所以,在商品设定为3000种的情况下,最坏的情况所有商品数据同时过期(雪崩),最高请求量3000(防击穿设计,可以保证一个Key过期只有一个请求打到数据库)。

    这里,因为只是一个示例,所以以理想的情况来进行评估。

    从上面的图示中,可以很好的理解缓存与数据库由于是分离的,不可能达到强一致性。也就是说,分布式缓存会产生不一致问题,因此我们在分布式缓存在使用时需要判断非强一致性对用户的影响。

    使用缓存的最终目的在于提升系统的性价比,而不是单纯地提高系统的性能。

    Redis与Memcache缓存介绍与对比

    1. 数据结构

    Redis支持多种数据结构,并且查询性能极高,可以达到log2^N的查询速度。

    Memcache只支持key/value存储,不支持其他数据结构。

    2. 线程模型

    Redis使用单线程,Memcache使用多线程。所以Redis只支持单线程请求,一个Redis进程只使用单核,所有命令串行执行,并发情况下不需要考虑数据一致性。但是可以通过多个Redis进程使用多核。而Memcache可以充分发挥多核优势,单实例吞吐量极高,可以达到几十万QPS。

    3. 持久化

    Redis提供了二种持久化方式:

    1. RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。
    2. AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

    Memcache并不提供持久化机制,缓存的数据都是临时的。但是可以通过第三方来提供持久化。

    4. 高可用

    Redis支持主从节点复制,还支持Sentinel、Cluster等高可用集群方案。

    Memcache不支持高可用模型,但是可使用第三方,如megagent代理。当一个实例宕机时,可以连接另外一个实例。

    5. 对队列的支持

    Redis本身支持lpush/rpop/brpop、lpushrprop(安全队列)、publish/subscribe/psubscribe等队列与发布订阅模式。

    Memcache不支持队列,但是可以通过如MemcacheQ来实现。

    6. 数据淘汰机制

    Redis数据淘汰策略:

    • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
    • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
    • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
    • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
    • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
    • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
    • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
    • allkeys-lfu:从所有键中驱逐使用频率最少的键

    如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。

    Memcache在容量达到指定值后,就基于LRU算法自动删除不使用的缓存。

    7. 内存分配

    Redis的内存管理主要通过源码中zmalloc.h和zmalloc.c两个文件来实现的。Redis为了方便内存的管理,在分配一块内存之 后,会将这块内存的大小存入内存块的头部。如图 5所示,real_ptr是redis调用malloc后返回的指针。redis将内存块的大小size存入头部,size所占据的内存大小是已知的,为 size_t类型的长度,然后返回ret_ptr。当需要释放内存的时候,ret_ptr被传给内存管理程序。通过ret_ptr,程序可以很容易的算出 real_ptr的值,然后将real_ptr传给free释放内存。

    image

    Redis通过定义一个数组来记录所有的内存分配情况,这个数组的长度为ZMALLOC_MAX_ALLOC_STAT。数组的每一个元素代表当前 程序所分配的内存块的个数,且内存块的大小为该元素的下标。在源码中,这个数组为zmalloc_allocations。 zmalloc_allocations[16]代表已经分配的长度为16bytes的内存块的个数。zmalloc.c中有一个静态变量 used_memory用来记录当前分配的内存总大小。所以,总的来看,Redis采用的是包装的mallc/free,相较于Memcached的内存 管理方法来说,要简单很多。

    Memcache采用slab table的方式分配内存,首先将可得内存按照不同大小分类,在使用时根据需求查找接近于需求大小的块分配的机制减少内存碎片,但是这依赖于合理的配置。

    综上:

    Redis和Memcache本质上都是基于k/v实现的缓存,但是Memcache正如其名,依赖于内存,不支持数据的持久化,服务器关闭后数据丢失。而Redis在很多方面具备数据库的特征,或者说就是一个数据库系统,可以通过RDB快照或者AOF日志将数据持久化到磁盘,支持master-slave机制的数据备份;

    在存储小于100k的数据,Redis具有性能上的优势,而数据量大于100k,Memcache性能更好;

    Redis只支持单线程请求,一个Redis进程只使用单核,所有命令串行执行,并发情况下不需要考虑数据一致性。但是可以通过多个Redis进程使用多核。而Memcache可以充分发挥多核优势,单实例吞吐量极高,可以达到几十万QPS;

    在内存利用率上Memcache更高,但如果Redis使用hash结构做k/v存储,由于其组合式的压缩,内存利用率会高于Memcache;

    支持数据类型上,Memcache只支持k/v类型的数据,而Redis还支持list,set,zset,hash等数据结构,并且Redis支持服务器端的数据操作,而Memcache需要将数据拿到客户端进行修改再set回去,大大增加了网络IO和数据的体积,如果需要缓存能进行更复杂的数据结构和操作,那么Redis更适合

    应用层缓存经典使用姿势

    image

    这是最常用的缓存服务架构模式,对于读操作,先读缓存,如果缓存不存在数据,则再读数据库,读取数据之后再回写缓存;对于写操作,先写数据,然后删除缓存(Cache-Aside pattern)。

    缓存在写时的会涉及到双写一致性问题,在这里暂时不展开。后续会专门章节进行介绍。

    缓存穿透、击穿与雪崩

    缓存穿透、击穿与雪崩是常见的由于并发量大而导致的缓存问题。

    #缓存穿透

    缓存穿透指的是使用不存在的Key请求,导致缓存无法命中,每次请求都穿透到数据库,使数据库压力过大,甚至宕机。

    通常解决方案是将空值缓存起来,再次接到同样的请求时,就可以命中缓存空值,直接返回而不再请求数据库。当然,如果每次的请求Key都不一样,导致空值缓存方案不起作用,这正常依赖于参数的合法性校验与数据摘要的校验,尽最大可能使用户无法修改请求参数。

    更进一步的做法是针对高并发接口的数据使用布隆过滤器来判断请求的数据是否存在,如果不存在直接返回,避免数据库的查询操作。

    什么是布隆过滤器

    本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

    (祥细原理在后续文章中介绍)

    #缓存击穿

    缓存击穿是指在高并发场景下,当某个热点Key过期时,大量的并发请求同时打到数据库,造成数据库压力巨大甚至打死的情况。

    缓存击穿的处理方案有:

    • 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。* 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。* 若缓存的数据更新频繁或者缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

    #缓存雪崩

    缓存雪崩是指缓存重启或者大量的缓存数据集中在某一时间内失效,导致所有请求完全打到数据库,造成数据库负载巨大、压垮。

    缓存使用实践

    缓存系统在使用过程中,从配置、使用、监控等手段都是必须的。下面对在使用过程中实践进行总结。

    1. 应用数据缓存大小评估。包括数据结构、缓存大小、缓存数量、缓存失效时间,然后根据业务情况推算在未来一段时间内的容量。
    2. 根据需要的内存大小划分缓存实例数量。
    3. 缓存数据超时。缓存数据在正常情况下必须设置合理过期时间,并且不可以设置为集中的某一时刻过期,防止缓存雪崩,导致服务拖垮、雪崩。
    4. 建议业务隔离。核心业务与非核心业务使用不同的缓存集群,减少相互影响。由于成本问题无法隔离,规范各个应用的Key有唯一的前缀,避免缓存覆盖问题。
    5. 实例监控。对慢查询、大对像、内存使用情况、集群健康情况做可靠的监控。
    6. 缓存热点数据。缓存的数据应该为热点数据,低频数据不建议放置在缓存中。
    7. 缓存的数据大小。缓存的数据大小不能过大,必须控制在合理的范围内。
    8. 集合操作。对大集合不能直接GETALL,会导致阻塞。如Redis应该使用SCAN。
    9. 写时数据必须完整。缓存的数据更新必须是全量、全整更新。
    10. 缓存降级。缓存必须有降级处理,避免缓存全完崩溃时系统直接不可用。

    相关文章

      网友评论

        本文标题:「微服务」缓存本质全分析、对比与实践

        本文链接:https://www.haomeiwen.com/subject/pmzvoctx.html