1. 程式人生 > >雲客Drupal8原始碼分析之快取上下文CacheContext

雲客Drupal8原始碼分析之快取上下文CacheContext

“上下文Context”這個詞是什麼意思呢?平常生活中它常見於語言、文字交流裡面,意思是當前交流處於一個特定的環境下,依託前面的內容交流才有意義

比如這句話:“他正在學習drupal”,如果單獨說是沒有意義的,因為你不知道“他”指代誰,在交流中前面一定定義清楚了“他”是誰,這個“他”就是上下文,這個誰就是上下文的值

在軟體工程中,上下文是一種屬性的有序序列,它們為駐留在環境內的物件定義環境。不過你無需去理會這樣晦澀的定義,只需要知道“上下文”相當於“環境”就行了,它們是等價的。

假設將來能製造出真正的類人智慧機器人,那麼把它投放到社會中,啟用那一刻,他第一件事情就是偵查環境,換句話說就是搞清楚自己所在的上下文,然後他才能有所行動

可見上下文概念是如此重要,在腦子裡面建立一個印象:有目的的行為是建立在環境之上的,萬事萬物皆是如此

在drupal中上下文就是指當請求到來時,系統所處的工作環境,這個環境由請求和系統設定共同構成,系統首先要搞清楚環境(上下文)才知道自己該怎麼行動(正應前文所講)。

那麼快取上下文CacheContext呢,就是指相對於快取系統的環境(快取環境是系統環境的子集),快取系統依據此環境才能正確行動,具體實現就是快取依據這個上下文來存放或取回正確的資料。

在快取系統中,相對於快取標籤代表快取有效性而言,快取上下文代表資料的變體,同一份資料不同環境有不同變體,可以說快取上下文主要目的就是產生快取id

快取上下文決定了快取id,也就是Cid,它唯一標識一條快取,用它取回和設定一條快取,關於快取系統的介紹請看本系列前面的主題

下面我們來看一看快取上下文怎麼使用,以及系統是如何實現的:

快取上下文的用法:

快取上下文的值是一個字串陣列,字串是特定的,代表一種上下文,這個上下文用一個上下文物件實現,以容器的服務形式存在,加上“cache_context.”字首就是它對應的容器服務id

這個字串經常被叫做“token”、上下文id、快取上下文佔位符等

本質上講它是某一環境引數的識別符號,在計算Cid(快取id)時將通過對應的上下文服務物件得到具體的環境值

一個簡單的例子,比如在控制器返回的渲染陣列可以這樣指定快取屬性:

$build = [
  '#markup' => t('Hi, %name, welcome back!', ['%name' => $current_user->getUsername()]),
  '#cache' => [
    'keys' => [...],
    'contexts' => ['user'],
    'tags' => [...],
    'max-age' => -1
     ]
];

這裡的'user'就是快取上下文id,可以有多個上下文id,它們組成一個上下文陣列,系統預設提供了以下上下文id:

Array
 (
     [0] => cookies
     [1] => headers
     [2] => ip
     [3] => languages
     [4] => request_format
     [5] => route
     [6] => route.menu_active_trails
     [7] => route.name
     [8] => session
     [9] => session.exists
     [10] => theme
     [11] => timezone
     [12] => url
     [13] => url.path
     [14] => url.path.parent
     [15] => url.query_args
     [16] => url.query_args.pagers
     [17] => url.site
     [18] => user
     [19] => user.is_super_user
     [20] => user.node_grants
     [21] => user.permissions
     [22] => user.roles
 )

這些上下文id都有對應的上下文物件,加上“cache_context.”字首就是這些物件的容器服務id,下面我們來看一下快取上下文的具體實現:

通過理解具體實現能夠掌握更多高階用法

處理快取上下文的程式碼位於:\core\lib\Drupal\Core\Cache\Context

每種型別的快取上下文系統都定義了一個快取上下文物件來處理,這些物件具備共同特性,其被歸納在快取上下文介面中:Drupal\Core\Cache\Context\CacheContextInterface

這個介面很簡單,定義了以下三方法:

public static function getLabel(); 得到快取上下文標籤,這個標籤用於描述快取上下文,顯示給人類看

public function getContext(); 將快取上下文id轉換為快取上下文值,比如指定了快取上下文id為“ip”,那麼這個方法將返回具體的客戶端ip值

public function getCacheableMetadata(); 返回可快取元資料,定義這個方法是為了配合快取上下文優化(見下文)

這裡有一個問題,請思考:我們知道一個快取上下文id指代一種環境引數,體現了這種環境引數的改變帶來的資料變體改變,然而環境引數是非常非常多的,比如請求頭、cookie他們都包含很多子條目

每個子條目都可以是一個上下文id,那麼我們豈不是要定義非常多的快取上下文物件?但我們發現這樣的上下文有一個共同特點:這些細碎繁多的上下文它們都是同性質的一個大類的子類

具體說就是所有的請求頭條目都是請求頭,所有的cookie變數都是cookie

為了解決這個問題,引入了計算型快取上下文介面Drupal\Core\Cache\Context\CalculatedCacheContextInterface

和CacheContextInterface相比,不同之處僅是他們的方法多了一個引數,用引數指定一個大類的具體小類,這樣大大減少了快取上下文物件的數量

因為實現這個介面的快取上下文物件有許多小類,所以約定這樣的快取上下文以複數形式命名上下文id

使用這樣的快取上下文時可以傳遞引數,上下文id和引數間用冒號分割,這些引數代表這一類上下文的某一具體快取上下文,不指定引數代表全部子類,相當於指定了全部引數

筆者注:在寫這篇帖子時(版本8.2.3),快取上下文id:session 類:Drupal\Core\Cache\Context\SessionCacheContext沒有實現介面,已向官網報告bug

如何自定義一個快取上下文呢?

定義一個服務,它實現了以上兩種介面之一,服務id為:cache_context.context_id,這裡“cache_context.”是系統要求的強制字首,“context_id”就是要使用的上下文id了

定義好服務後給出“cache.context”標籤,下面是一個列子:

cache_context.ip:
    class: Drupal\Core\Cache\Context\IpCacheContext
    arguments: ['@request_stack']
    tags:
      - { name: cache.context }

然後重新編譯容器即可(清除快取),編譯器:\core\lib\Drupal\Core\Cache\Context\CacheContextsPass將在容器編譯階段收集處理這些快取上下文,並將它們儲存到容器引數cache_contexts中

系統提供了一個叫做快取上下文管理器的物件以方便使用,服務id為“cache_contexts_manager”,類:Drupal\Core\Cache\Context\CacheContextsManager

它的主要作用是接受快取上下文定義引數然後把它轉化為快取id:convertTokensToKeys(array $context_tokens)

這個方法返回快取id的時候,並不是一個字串,而是Drupal\Core\Cache\Context\ContextCacheKeys物件,為什麼要這樣處理?因為快取上下文優化,見下。

快取上下文管理器同時也提供了一些其他輔助方法,比較簡單,不多講,重點是快取上下文優化

快取上下文優化:

你可能已經注意到了有些快取id包含句點,這可能代表什麼嗎?其實快取上下文是分層級的,句點就是層級間的分割,從左到右就是從父到子

為什麼要分層級?這是為了起到快取上下文優化作用,怎麼優化呢?背後的原理是什麼?優化的目的不是避免產生大量變體,而是避免在產生Cid時進行過多的計算

實際上層級間分級的原則就是父更基礎,子更具象

舉個例子:如果一個上下文同時指定了user和user.permissions,其實使用者的改變也就暗含了許可權的改變,父層更加基礎,所以這裡許可權上下文是多餘的,它可以被優化掉(也就是去掉了)

然而這樣的優化會產生一個問題,當用戶許可權變化時,會取回之前的快取,這可能是有問題的,怎麼辦?這就需要將許可權的變化反應到快取的資料中

因此解決辦法是給快取上下文加上快取屬性,並把這些被優化掉的快取上下文的快取資料冒泡到被快取的資料中,這也就是快取上下文介面為什麼需要有getCacheableMetadata方法的原因。

如上列中user.permissions雖然被優化掉了,但它的快取標籤卻不會丟掉,它的快取屬性會傳遞給被快取的資料,此時許可權變化將引起快取失效,這樣就保證了資料的正確性

這種辦法還帶來一個額外的好處,就是避免殭屍快取(永不過期、也不失效,但永不會用到的快取),想想上面這個例子如果不進行優化,當權限變化是將產生新的Cid,原Cid可能就成為殭屍快取了

更一般的抽象是:如果一個快取上下文依賴於某配置,而配置可能改變,導致快取上下文具體值改變,那麼當優化掉該上下文時,必須將它的快取屬性合併(也叫做冒泡)到被快取的資料

這其實是一種隱式的指定上下文,效果相當於快取了getContext()的結果,避免去再次計算getContext(),就達到了優化的目的:效能提升!對它的計算已經體現在快取標籤上面了

如果該快取上下文依賴的配置改變的太快,就可以設定max-age = 0來禁止上下文優化

回過頭來,應該明白為什麼快取管理器在計算返回cid時返回的是ContextCacheKeys物件了吧,它承擔快取資料冒泡的工作,這個物件包含了快取屬性,快取系統將合併它到被快取資料的快取屬性

上下文優化的原則是指定的快取上下文中,如果同時存在具備共同父級的上下文,將只保留共同父級上下文,冒號視為句點,具體優化轉變列子如下:

['a', 'a.b'] -> ['a']
['a', 'a.b.c'] -> ['a']
['a.b', 'a.b.c'] -> ['a.b']
['a', 'a.b', 'a.b.c'] -> ['a']
['x', 'x:foo'] -> ['x']
['a', 'a.b.c:bar'] -> ['a']

優化實現的程式碼見:Drupal\Core\Cache\Context\CacheContextsManager::optimizeTokens(array$context_tokens)

額外的筆者提醒思考以下問題:

快取ID碰撞:

有沒有可能兩份資料出現同一個Cid呢?

其實是有可能的,但系統採取的措施很難遇到這樣的情況,首先不同用途的資料已經分別被放在不同快取Bin裡面,即使碰撞也沒有關係,再次資料會使用多個快取上下文的組合,這降低了碰撞概率

每一個快取上下文對應一個特定環境,同一個特定環境出現兩份資料的概率相當低

此外為了進一步防範Cid碰撞,還可以額外指定快取鍵,比如渲染陣列就採用了這樣的措施

快取上下文和標籤的區別:

在本文開頭大量敘述了上下文,就是為了強調上下文代表的是相對於資料而言外在的環境,而標籤代表的是資料自身,有兩個關鍵詞“外在”、“自身”特別重要

上文的user.permissions上下文,它被優化掉後其標籤雖然和資料標籤合併,但它的標籤帶來的資料失效是在這種環境下資料失效了,而資料自己的標籤帶來的失效是所有情況下它都失效了

所以這兩種標籤依然體現了外在環境和自己本身之間的區別,合併只是一種技巧而已。

快取上下文官網文件:

https://www.drupal.org/docs/8/api/cache-api/cache-contexts

題外話:

一直堅持每週出一篇原創來介紹drupal,這篇恰逢是2016年的最後一篇,時間過的好快,drupal8一歲了,我的兩個寶貝,小的也一歲多了,在drupal中和生活裡都有好多感慨

為什麼要堅持寫作呢?

一方面源於希望對中國開源社群有所貢獻。現階段國內人們都很忙,生活壓力比較大,少有精力去開源社群免費工作,不像發達國家即使什麼也不做至少政府會提供基本生活保障,這導致很難發展出一個好的開源社群,很現實的說國內難有這樣的條件,但開源軟體有非常多的好處,我們需要足夠優秀且開源的軟體,預測未來drupal會成為國內網站系統的主流,就像linux一樣

一個好的軟體不只是技術那麼簡單,更重要的是生態,生態裡面有數量眾多願意貢獻的人,不止有開發者還有使用者,這才是關鍵!萬眾雕琢才能創造不凡,drupal就是這樣的產品,足夠優秀,生態足夠大。

既然國內難有培養開源的土壤,drupal在全球又做的那麼出色,那就參與吧,推廣它讓大家都受益,但是國內缺乏中文資料,成了學習障礙,與其枯燥的翻譯不如原創,寫一些能讓國人看的懂的文字。

另一方面我有一個願景:當今我國日新月異,創業創新生氣勃勃,是一個精彩的世界。人,生來是需要做點事情的,短短一生希望能留下點什麼,我有一種情懷,特別嚮往那些去世後能在墓碑上面留下一行公式的科學家,

人生的目的應該不是為了得到,就算得到離開時也必將放手,留下才有意義,才是真實的,留下的代表自己存在過,並在自己死去後代表自己一直活著,所以我追求付出,讓付出的在自己離開這個世界時得以留下。

能使用drupal開發的工程師大多技術水平都比較高,希望在這個過程中結識一批優秀的人才,一起去做點事,推動社會的發展,得以留下一些東西,讓生命有所價值,可以通過留下的聯絡方式讓我們走到一起。

最後奉上非常喜歡的一段話:

人生中出現的一切,都無法佔有,只能經歷。我們只是時間的過客,總有一天,我們會和所有的一切永別。

深知這一點的人,就會懂得:無所謂失去,而只是經過而已;亦無所謂得到,那只是體驗罷了。

經過的,即使再美好,終究只能是一種記憶;得到的,就該好好珍惜,然後在失去時坦然地告別。

我是雲客,【雲遊天下,做客四方】,微信號:php-world,歡迎轉載,但須註明出處,討論請加qq群203286137

祝大家元旦愉快,新年更上一層樓,通往理想的路走的更遠一些,內心的風景更豐富一些。