24. 渲染缓存RenderCache

渲染数组被渲染的时候,为了提高性能,会将渲染结果保存到缓存中,这样就避免了重复渲染
并且每个子元素的渲染结果都可以被独立缓存,如此一来子元素也可以避免重复渲染

不过缓存中保存的渲染数组是经过简化的,渲染缓存默认只缓存以下内容,这样经简化的数组叫做“CacheableRenderArray”:(见辅助内容区)

$elements = [  
      '#markup' => $elements['#markup'],  
      '#attached' => $elements['#attached'],  
      '#cache' => [  
        'contexts' => $elements['#cache']['contexts'],  
        'tags' => $elements['#cache']['tags'],  
        'max-age' => $elements['#cache']['max-age'],  
      ],  
    ];  

 

注意:它并不包括$elements['#cache']['keys']
默认如上,但是当我们也想缓存其他内容的时候怎么处理?可以这样:
指定$elements['#cache_properties'],它是一个数组,键值为想要缓存的元素的键名即可
比如在Drupal\Core\Render\MainContent\HtmlRenderer::prepare中就有这样的用法:
$main_content['#cache_properties'][] = '#title';
这将使得渲染数组的'#title'属性也被缓存
但这里需要注意的是如果$main_content['#cache_properties'][]指定的不是属性而是一个子元素的话
将清空当前元素的'#markup'内容,子元素也只保留'#markup'属性和它的值
具体实现参见:Drupal\Core\Render::getCacheableRenderArray方法

我们可以使用$elements['#cache']['bin']自定义渲染结果数组的缓存位置,也可以不指定
不指定相当于:$elements['#cache']['bin']='render',所以缓存默认被保存到数据库的'render'缓存表里

要进行缓存有两个必要条件,同时满足就是充分条件:
$elements['#cache']['max-age']不等于0
$elements['#cache']['keys']必须要被设置
此两条件只要有一个不成立就不会被缓存,如果都成立就一定会被缓存。

渲染数组的渲染过程中$elements['#cache']['keys']是不允许变化的
它和渲染数组的$elements['#cache']['contexts']共同决定了缓存id(Cid)
那么这里就出现了一个问题:渲染前根据当时的条件可计算出的Cid和渲染后计算出的Cid可能是不同的
为了叙述方便,这里将渲染前可计算出的Cid叫做“渲染前Cid",将渲染后可计算出的Cid叫做“渲染后Cid"
渲染初始时子元素的缓存上下文尚未冒泡上来,或者由于#lazy_builder尚未构建,也可能被一些回调等修改
所以在渲染初期并不知道渲染后的缓存上下文,也就不知道渲染后Cid,如此一来就没有办法取回缓存的数据

那怎么解决呢?drupal使用了双重缓存(Two-tier caching),又叫做缓存重定向策略,具体如下:
渲染完成后保存缓存数据时,判断缓存上下文是否有变化,如果没有变化,那么渲染前后的Cid是一样的,没有什么问题,直接将数据保存到这个cid下即可
如果有变化,则计算出渲染后的缓存元数据数组,其中包含'keys'和'bin'
此外添加一个'#cache_redirect'属性,其值不为NULL,默认设置为true
然后将这个渲染后的缓存元数据数组(注意不是渲染后的数组)保存到渲染前Cid下,形成一个重定向缓存
这个重定向缓存本身的tags、max-age和渲染后的缓存元数据一致
在获取缓存数据时,如果发现设置了'#cache_redirect'则说明是一个重定向缓存,那么通过得到的数据继续get缓存,经过两层缓存最终得到渲染后内容

思考:

能否将渲染后的结果直接保存在渲染前Cid下呢?为什么?
如果这样做,那么在系统其它地方要对这份缓存进行操作时,那么就需要知道它渲染前的Cid,这可能是个问题
缓存API的一般原则是:被缓存的数据对象自带缓存属性数据,可以从渲染后的渲染数组自身计算出对应的缓存id

缓存重定向的“乒乓球问题”:
思考有这样一个渲染数组:

- A (#cache['keys'] = ['foo'])   
-- B (#cache['contexts'] = ['b'])  
--- C (#cache['contexts'] = ['c'])  
--- D (#cache['contexts'] = ['d'])  

在A层级有没有指定上下文对这个问题没有影响,这里假定没有指定上下文
B是A的子元素,C、D是B的子元素,但C、D只能根据B的上下文值,二选一的存在一个
具体讲就是:B的上下文'b' = 'b1'时仅有子元素C,'b' = 'b2'时仅有子元素D
此时整个渲染数组的结果取决于B层级指定的上下文b的值,上下文b和元素的对应关系成为一个配置项,该配置项成为标签影响到整个A的有效性
在这样的情况下看看缓存重定向会发生什么:
当'b' = 'b1'时:A的渲染前Cid为“foo” 内容指向bc
当'b' = 'b2'时:A的渲染前Cid为“foo” 内容指向bd
两个请求的不同内容渲染数组的渲染前Cid碰撞了,如果在渲染数组中不附加额外的标签来体现它们的区别变化,这里将从缓存取回错误的数据
如果附件标签数据,比如这里如果设置上下文b和配置项成为标签,他们会正确反映缓存有效性,所以取回来的数据不会有错
但是在这样的情况下,虽然没有错,但缓存非但没有节省计算,还成为了拖累,在b的不同上下文请求中会不停设置缓存
就像是在水中的瓢按住这头,那头翘起,按住那头时这头又翘起,或像乒乓球两边推,这就是该问题名字的由来
为了解决这个问题,系统实现了一种方法,避免频繁的对渲染前cid的内容进行修改,只修改渲染后Cid的内容
就是让渲染前Cid在跨请求间逐步收集所有的上下文,最终保存所有的上下文,形成一个固定指向
指向的内容就是渲染后Cid的内容,这个内容是会频繁设置的,通过标签来保障准确性

许多工程师调侃在计算机科学中有两件事情比较困难:缓存和命名
如果你不能理解上面的内容,可以先放下,在后面有具体渲染数组的列子后再回过头来看,慢慢体会缓存上下文和标签之间的精妙关系
在自定义的渲染数组中要特别注意上述乒乓球问题引起的潜在错误

占位渲染缓存:

在drupal实际的使用中,渲染缓存采用的是占位渲染缓存PlaceholderingRenderCache它扩充了RenderCache的功能
我们知道渲染占位符的目的就是将系统不适合缓存的内容独立出来,单独渲染,结果并不会缓存
那么在一个页面中这样的内容出现多次,由于没有缓存,所以需要多次渲染,这是没有必要的
所以为了解决这个问题,创建了“占位渲染缓存PlaceholderingRenderCache”,在这个渲染缓存中
将不适合缓存的内容保存在类的属性中,多次需要时,直接返回,将不会再次渲染,这就提高了性能
思考:为什么不把所有从缓存取回的数据都进行这样的处理呢?这样避免多次调用缓存,减少数据库查询不是更高效吗?
要理解为什么系统没有这样做需要明白占位符元素的渲染是在渲染最后进行的,而其他不是
如果这样处理,当系统流程引起标签变化,缓存失效,这样就会产生错误
而占位渲染元素为什么不担心数据变化的问题,因为在系统最后执行占位替换时,他们是连在一起执行的,没有其他流程穿插其中

本书共91小节:

评论 (写第一个评论)