计算机缓存(从计算机缓存策略到redis的使用)

前言

        为了解决CPU与主存储器之间性能的极度失衡,同时解决硬件性能与价格之间的矛盾,根据程序访问的时间局部性和空间局部性,计算机通过在CPU和主存储器之间引入高速缓存(cache)的方式来实现对CPU算力的最大化利用,使整个存储系统形成cache--主存--辅存的分层结构。在软件开发中,同样的矛盾也存在于服务器与数据库之间,某种程度上来看甚至可以认为他们是等价的,对计算机cache的学习能帮助我们从熟悉的知识层面来理解目前大火的各种内存数据库,反向来说,也提供一种遇到类似问题时在现有的计算机体系中寻找解决方案的思路,以达到站在巨人肩膀上前进的目的。

一、存储器分层结构

   计算机缓存(从计算机缓存策略到redis的使用)

图1

        存储器分层结构如上图所示,CPU发出请求,按照Cache->主存->辅存的优先顺序(按照性能)依次判断数据是否存在,若不存在则依次 往上取,直至返回数据给CPU。(生产软件中通常也是按照redis->数据库的优先顺序对数据进行获取。)

二、Cache工作原理

计算机缓存(从计算机缓存策略到redis的使用)

图2

        CPU发出携带块号和块内地址的信息对数据进行读取,计算机先对主存和内存地址进行映射来获取数据在Cache中的地址,如果命中直接返回Cache中的内容到CPU,若未命中,判断Cache是否装满,若未装满将数据块装入Cache同时返回数据给CPU,若已装满,根据Cache替换策略通过对Cache进行替换的方式将数据装入,同时返回数据给CPU。

        主存->Cache的映射方式包括直接映射,全相连映射和组相连映射(此处不做详解)。而对应的redis和数据库数据映射相对来说就要简单的多,根据《阿里云redis开发规范》,redis的key值通常为以业务名(或数据库名)为前缀(防止key冲突),用冒号进行分割,比如业务名:表名:id的方式命名key值,同时也完成redis到关系型数据库的主键映射。

三、Cache替换策略

        随着程序的运行,Cache需要不断的从主存中读取数据,因此Cache总有装满的时候,当Cache装满而又有新的数据需要装入,此时就需要用新的数据通过某些替换策略来替换老的数据。好的替换策略能够使Cache中数据的重复使用率增加从而达到提升性能的目的,目前Cache中常用的替换策略有如下几种:

3.1、随机替换

        顾名思义,随机的选择一块替换出去。

3.2、先进先出(FIFO)

        计算机缓存(从计算机缓存策略到redis的使用)

图3

        该方式符合队列特性,用数据结构Queue实现,如上图所示。

3.2、最近最久未使用(LRU)

    计算机缓存(从计算机缓存策略到redis的使用)
图4
        新数据插入链表头部,每当Cache命中,将数据移到链表头部,淘汰链表末尾数据。该方式为Cache实际采用方式也是更为合理的方式。

        上面提到的几种替换算法,redis都支持,区别在于redis的LRU算法颗粒度更细,有相对更多的选择,同时Redis的LRU也并非完整的实现,它会对回收候选池内的数据进行LRU替换,替换的精准度随采样数量增大而增加,同时性能将会降低,采样数量可通过参数maxmemory-samples进行配置,redis替换算法详解将在后续文章中进行阐述。

四、Cache写回策略

        Cache中的数据可能会由CPU不断的更新,而更新的数据也需要写回到主存,如何将Cache和主存内容保持一致有几种写操作方式供选择,统称为写策略。

4.1、写回(write back)

        写回是指只有在一个cache行被选中替换回主存时,如果cache行的数据是修改过的(dirty),才将它写回主存。这种策略,要在Cache中设置一个脏位(dirty bit),用来表示缓存中的cache行是否被修改过。如果 一个内存块在加载到Cache后未被修改过,Cache直接把该cache行设置为无效。不需要把数据写回主存,这样可以有效降低从Cache到主存的写次数。

         Redis使用时,若数据需要持久化,通常不采用该方式,数据未持久化的时间越长,那么数据丢失的风险就越高。

4.2、写通(write through)

        写通是指,每当Cache收到写数据(store)指令时,若写命中,则CPU会同时将数据写到Cache和主存。若命中,用写分配方式,只在数据写不命中产生作用,即,给数据分配一个cache line,先在主存块中更新到主存中,然后分配一个cache行,将数据写到Cache中,这种方式充分利用了空间局部性,但每次写不命中都要从主存读一个块到Cache中,增加了 读主存 的开销。非写分配方式,直接把数据写回主存而不加载数据到缓存,这种方式可以减少读主存的时间,没有利用好空间局部性。

        Redis使用时,当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存则更新缓存,然后再由缓存更新数据库。同时,也可以先更新数据库,再更新缓存,或将缓存移除,后续查询时更新。先将缓存移除,再更新数据库方式是不可取的,当两个线程同时访问,一个更新一个读取,可能会在移除后还没更新时将旧的数据放入缓存,这样产生的脏数据将会一直保留到下次更新操作时才可能一致。

        对缓存原理的理解可以帮助我们更深刻理解计算机存储的内在逻辑,同时亦可在使用redis作为缓存并要求与数据库的一致性时为我们提供一些思路,但这仅是从Redis作为缓存的用途在进行分析,Redis的应用场景远远不止于此,后续将会在文章中进行更多的挖掘与思考。