雲客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
祝大家元旦愉快,新年更上一層樓,通往理想的路走的更遠一些,內心的風景更豐富一些。