[БЕЗ_ЗВУКА] В этом видео мы рассмотрим, каким образом можно работать с memcache из гошки. memcache — это очень простое key-value хранилище. У него не очень много возможностей, однако это компенсируется его простой и надежностью. На самом деле, рассматривать просто сохранение значений memcache и получение оттуда данных — это примерно такой же драйв, как и рассматривать присвоение значений в переменные. Поэтому пройдемся мы очень быстро и перейдем к более практическому примеру. Итак, команда Set сохраняет запись, независимо от того была она там или нет. Если она там была, она ее перезаписывает. Для этого вам нужно передать полностью всю запись memcache Item, ключ — строка, Value — слайс-байт, и Expiration — время в секундах, через которое вы через которое запись будет уже неактуальна. Increment увеличивает запись на 1 или на −1, в зависимости от того что вы передадите. Get получает запись по ключу. Обратите внимание, что если запись не найдена, признак об этом вернется вам в специальном коде ошибки. Удаление — ничего сложного и интересного. Поэтому не будем на этом останавливаться и пойдем дальше. memcache очень простой, но кеширование все-таки хочется делать какое-то посложнее. Рассмотрим следующую схему. Представьте, что у нас контент-проект и есть много различных закешированных данных, например RSS, главная страница, какой-то информер для партнеров. И там их больше, больше, больше, больше. При этом если вы будете пытаться каждую из этих запись — RSS, mainPage, partnersInformer — сохранять в админке, для того чтобы сбросить, когда у вас что-то новое добавляется, вы быстро утомитесь и наверняка что-то пропустите. Поэтому есть подход, который называется «тегированный кеш», когда вы храните не сразу свои данные, а еще какую-то небольшую метаинформацию в нем. Например, это теги в данном случае. Теги — это такая мапа, string, int в данном случае, где ключ указывает на какую-то другую запись в memcache, а значение — это какой-то числовой код, например timestamp, время. Вот. И если вы хотите, если вы увеличите время, инкрементируете счетчик в этом теге, то есть в этой записи memcache, используя команду Increment, либо просто поставите туда текущее значение Unix timestamp, то все записи, у которых есть такой тег, когда вы их достанете из кеша, распакуете, то вы можете понять, что эта запись не валидна и кеш нужно перестроить. Вот, например, смотрите: partnersInformer я выделил красным. Почему? Потому что тег video у него равен 3, то есть он был сохранен, когда тег video был равен 3. Однако текущее значение у тега video — 4. Это значит, что вот этот кеш, он уже не валиден, и его нужно перестроить. Вот это один из вариантов такой инвалидации кеша, когда у вас неизвестно количество самих закешированных данных. Например, вы в админке просто инкрементните video, поставите там 5, и уже mainPage, кеш для mainPage будет невалиден. В следующий раз, когда он выберется, там сравнится значение тегов и будет перестроение кеша. При этом вы сразу же не вызываете каскадное перестроение всего, просто увеличиваете значение в админке, а перестраиваться кеш будет по мере запроса. Теперь давайте посмотрим, как реализовать такую схему, используя Go. Итак, теперь рассмотрим уже код тегированного кеша. Этот пример будет довольно большой, если у вас закипят мозги, так и задумано. Хорошо. Для начала сделаем структуру нашу. Я ее взял тут в комментарии, потому что, на самом деле, она у меня определена в соседнем файле. Назовем это Cache, и она включает в себя клиент memcache, то есть я могу пользоваться всеми методами, которые предоставляет мне memcache.Client, вызывая их у экземпляра этой структуры. Вот так она инициализируется, инициируется. И начну с того, что я грохну старые данные, которые были закешированы, чтобы пример у нас был чистым. Вот. tc — так я буду называть свой кеш. Я вызываю Delete. Delete — это метод memcache.Client, то есть в данном случае я демонстрирую вам композицию. В этом примере я вообще буду демонстрировать половину возможностей языка Go. Хорошо, удалили. Теперь как выглядит интрефейс по тегированному кешу? Вот вызов, вот вызов кеша. Я вызываю mkey — не важно, какое там значение, то есть значение ключа с кешом. 30 — это время жизни этого ключа, 30 секунд. И я говорю, что запиши мне, пожалуйста, вот в этот RSS, вот в это значение. И передаю в функцию к rebuild. Ее нужно посмотреть внимательней — что она делает и когда она будет вызвана. Эта функция, она получает значение уже, например, из холодного хранилища, из базы данных. В данном примере она идет на сайт «Хабра» в RSS и получает оттуда посты. Возвращает она interface, слайс стрингов и ошибку. interface нужен, для того чтобы я мог возвращать, чтобы этот механизм был универсальный, то есть я делаю функцию, которая мне возвращает что угодно. Это что угодно я потом сохраню и в случае чего достану. В данном случае у меня там RSS внутри. Дальше слайс стрингов — это те теги, которые я хочу сохранить. для данной структуры, для данного кеша. Если вернуться к нашей схеме, то в data будет уже значение постов с «Хабра», а в tags в tags будут сохранены те данные, которые я вернул в слайсе стрингов. Хорошо. То есть так мы посмотрели, каким образом я получу, собственно, настоящие данные уже честно. Да, это анонимная функция. И я передаю ее в TGet. Потом я вызываю еще раз, теперь полезли внутрь, в TGet. Так, mkey, ttl, in interface, то есть я передаю сюда — напоминаю, это у меня универсальная функция, она должна работать с любыми данными. Сюда я передал RSS. Теперь я хочу себя немножко обезопасить, я проверяю через reflect тип данных, которые мне прислали. В данном случае я хочу проверить, что мне прислали действительно указатель на какую-то структуру, то есть если я туда запишу результат, он наверху там где-то уже появится. Отлично. Дальше идет checklock. Что такое checklock, зачем он? Он проверяет, есть ли уже в данном случае lock на этот кеш, то есть вдруг его кто-то в данный момент перестраивает. Что я в нем делаю? Я в нем пытаюсь несколько раз сходить в в memcache по ключу lock + mkey, и если я вдруг вижу, что я получил эту запись, то есть если ее там нет, это хорошо, значит, никто не перестраивает, я могу идти дальше, В противном случае я просто сплю, то есть жду, пока кто-то перестроит. Обращу внимание, что это несколько учебный пример. У вас может быть другая логика, вы можете спать дольше или в зависимости от кэша, или захотите передавать эти параметры извне. То есть я подождал. Допустим, я дождался. Идем дальше. Идем дальше. Теперь я получаю уже сами записи из моего кэша, то есть уже по mkey. Допустим, я столкнулся с ситуацией, когда записи в кэше еще нет, и мне нужно теперь перестроить кэш. Я вызываю функцию memcache rebuilt, куда передаю фактически все те же данные — mkey, время жизни, переменную, куда нужно записать, и callback, который мне вернет настоящие данные. Сейчас в rebuild мы не будем углубляться, а потом дальше. Предположим, что значение в кэше у нас нашлось. Окей. То есть я хочу его распаковать. Там json, я напомню. Покажу еще раз на схеме. Это json. Фигурные скобочки, все хорошо. Я хочу её распаковать. Причем давайте посмотрим, как выглядит структура CacheItem. Там полноценный кэш. В Data я использую параметр json.RawMessage. Это значит, что, даже если там будут дальше фигурные скобки, оно не будет их распаковывать дальше во что-то другое, а сохранит прямо как есть, как слайс байт. Это понадобится чуть дальше. Хорошо, я распаковал. Теперь у меня есть какие-то мои данные. Я не знаю, какова их структура сейчас, на этом моменте, поэтому я не могу их распаковать сразу. И распаковал теги в map[string]int. Теперь мне нужно пойти и проверить, валидны ли мои теги. Вернемся к схеме. То есть для выделенного красным кэша я должен пройтись по news и узнать, действительно ли в new лежит значение 2, и video, и действительно ли в video лежит значение 3. В данном случае там лежит 4. Значит, isTagsValid вернет мне false, что внутри isTagsValid. Для начала я конструирую слайс строк. Потом я использую метод GetMulti из memcache-клиента. Он мне получает сразу много записей. И по ним я начинаю уже строить мапу текущих записей. Я конвертирую, мне вернулось много memcache item'ов. Мне вернулось много структур memcache item. Я прохожусь, вот curr. Я прохожусь по ним. Key — это те ключи которые я передал, и Item. Я то, что там есть, я преобразую слайс байт в int и кладу в мапу. Теперь у меня есть две мапы — одна мапа, которая лежала у меня в кэше, и вторая мапа с текущими значениями, актуальными. Используя функцию DeepEqual из пакета reflect я сравниваю, действительно ли обе эти мапы полностью равны. И, как я уже говорил, если они равны, значит кэш валиден. Там все хорошо. Если не равны, значит кэш не валиден, его нужно перестроить. Допустим, кэш валиден. Кэш валиден. Хорошо. Теперь я должен распаковать данные этого кэша уже в реальные — в ту структуру, которую я хочу видеть на выходе. Я не мог её распаковать раньше, потому что я не знал, что там будет и надо ли мне это распаковывать. Поэтому я её просто сохранил в Data. Теперь я беру эту Data и передаю туда параметр, то есть тот, который мне пришел извне, в который я хочу записать результат. В данном случае тут err ss и json.Unmarshal за счет того, что он внутри работает через reflect динамически, он пройдется по этим данным, структуры раньше которых я не знал, и сможет их уже теперь записать туда, куда мне нужно. То есть я могу работать в данном случае, вообще, абсолютно с любыми данными, которые я храню в кэше, используя всего лишь несколько этапов просто распаковки. Первый раз я распаковываю их в json.RawMessage. Там слайс байт будет. А второй раз я их распаковываю уже в реальную переменную. Допустим, да, и сейчас все хорошо, то есть кэш валиден и мне вернулся результат, я могу им дальше пользоваться. Теперь кэш не валиден и мне нужно его перестроить. Как раз этим занимается функция rebuild. Ранее мы в нее не заходили, а теперь зайдем. Да, я напоминаю, что я туда передаю ключ, время жизни, переменную, куда я хочу записать результат, и callback, который мне вернет реальные данные. Идем внутрь. Для начала я залочу кэш на перестроение — lockRebuild. Я буду в цикле несколько раз пробовать делать add на запись. Почему add? Почему не set? Add добавляет запись, только если её там нет. Это значит, что, если я в данном случае делаю add и запись я не смог добавить — ErrNotStored, то кто-то другой перестраивает эту запись. И мне нужно здесь немножко подождать. Я ставлю timeout — tt Expiration на 3 секунды. Напомню, это учебный пример. Все цифры тут учебные, поэтому, возможно, у вас вы захотите настраивать эти параметры или перестроить возможность для залоченного кэша что-то делать. Хорошо. Допустим, я подождал, я дождался lock. То есть если я смог взять lock, то, значит, что я смог успешно добавить запись в memcache. Я дождался lock. Все, теперь я верну true, что все хорошо, я взял lock на перестроение. Теперь я могу работать. И там сразу же я делаю в defer unlockRebuild. Он просто удаляет запись из memcache. Кстати, обратите внимание, я везде обращаюсь к методам memcache-клиента как к методам TCache, потому что memcache у меня встроен как анонимная структура. Этот как раз композиция — то, каким образом вы можете расширять свою структуру в Go. Rebuild, ладно. Мы залочились. Все хорошо. Мы взяли lock на перестроение. На самом деле, если бы у вас, например, только один сервис мог перестраивать этот кэш, у вас могла бы быть локальная мапка. А если у вас много серверов, которые могут перестраивать этот кэш, тогда мы сохраняем этот lock в том же самом memcache, как в этом примере. Мы залочились. Теперь я вызываю callback. Теперь я вызываю callback, который я тащил с самого начала. Он мне возвращает результат. Напомню, там пустой интерфейс. RebuildFunc. RebuildFunc я определил как отдельный тип, то есть он мне возвращает интерфейс, слайс стрингов и ошибку. Теги, которые мне нужно будет сохранить, и ошибку. Теперь немножко проверок. Я хочу убедиться, что то, что я запрашиваю извне, куда я хочу записать результат, и тот результат, который мне вернул callback, они имеют одинаковые типы. Согласитесь, будет очень странно, если вы будете присваивать в разные типы данных. Я проверил через reflect.TypeOf. Он возвращает мне тип данных, которые там находятся. В обоих из этих случаев будет *err ss. Дальше, напомню, что rebuildCb мне возвращает теги в виде слайса строк. Я хочу их получить теперь, теперь их значения из memcache. Опять-таки все просто. У меня пришел слайс тегов. Я сделал GetMulti. Потом я получил мапу. И дальше я смотрю, если у меня такая запись в memcache есть, я беру её значение, то, которое реально в memcache. Если же записи у меня нет, То есть нет такой записи в memcache еще, то я создаю, используя текущее время. И я получил map уже из текущих значений, которые лежат в memcache по тегам. То есть у меня это я либо взял из memcache их реальные, либо их добавил, и они тоже стали реальными. Пойдем дальше. Хорошо, я получил текущие теги в виде мапки, теперь я хочу сохранить. Как выглядит структура, которую я буду сериализовать в JSON? В качестве data там уже интерфейс, это значит, что я могу туда присвоить любые данные, и когда я буду их сериализовать, уже оно через reflect, или если вы там сгенерили маршалеры, оно пройдется, и все сериализует. То есть я туда могу присваивать, в эту структуру, в data, любые данные, и они сериализуются нормальными, ну и теги. Отлично, замаршалили, установили через set. Теперь, каким образом мне нужно присвоить result? Конечно, изначально была идея, что in = result, ну вот как-то так. На самом деле не сработает. Вот так тоже не сработает, потому что там внутри интерфейс, и ему присвоится немножко не так, как вы ожидаете. Поэтому тут дальше хитрое очень такое присваивание будет. Поскольку это интерфейсы, то я хочу достучаться до того значения, которое там лежит в глубине. Это можно было бы сделать, например, вот так in вот так, однако я не знаю, какое там значение в реальности будет, поэтому вот следующей хитрой конструкцией я, собственно, проверну примерно такую, ну да, val = result, вот типа такого могло бы быть. Вот то, что я напишу снизу, — это аналогичная конструкция. Так, я сначала получаю ValueOf, той переменной, это уже reflect той переменной, куда я хочу записать, я получаю Value той переменной, что я хочу туда записать. Теперь, используя indirect, то есть direct получает Value, куда указывает предыдущая Value. Это уже прямо магия. Я получаю оба значения, и теперь уже наконец я могу сделать set у этого значения. Теперь наконец я достучался до самых-самых-самых переменных, которые лежат за этим пустым интерфейсом, и я наконец могу присвоить их значение, то есть это примерно, если бы я сделал вот так, если бы я знал их типы, если бы я мог туда присвоить, а поскольку это пустой интерфейс, то вот такая хитрая конструкция. Ну и в данном случае уже вот здесь, вот в этой строчке, я записываю результат финальный, который прокинется на самый верх и будет наружу. Давайте теперь это запустим. [БЕЗ_ЗВУКА] Отлично. В первый раз я запросил записи из memcache, ее там не было вообще, я поэтому зафетчил сразу данные с Хабра. Второй раз я запросил, да, возвращается мне тут количество записей в rss и ошибка. Второй раз я запросил, мне данные в memcache были, теперь я заинкременчу тег, чтобы сбросить кэш. Обратите внимание, я сразу же запустил асинхронное перестроение, тут будет немножко непонятно, что к чему относится, ну ладно. Я запустил асинхронное перестроение и, обратите внимание — тут нет записи, что record not found, значит, она найдена, кэш не тот, и я делаю фетч. В то же время асинхронная запись, асинхронное перестроение, оно пытается взять lock, и только после этого, после того как оно дождется, когда кто-то отпустит запись, оно пытается перестроить эту запись. Это тегированный кэш. Пример, конечно, сложный, но зато тут демонстрируется довольно много возможностей самого языка. Сразу оговорюсь, именно этот код в продакшене я никогда не использовал, это учебный пример, подход рабочий, поэтому, возможно, вам стоит посмотреть на инвалидацию кэша, немножко подобрать ее под себя. Например, если кэш не валиден, то возвращать старые данные. Или, например, вы захотите перестраивать кэш, просто запуская его, запуская функцию в отдельной горутине, а те данные, которые есть, возвращать уже наверх, даже несмотря на то, что они немножко устарели, вы будете просто знать, что они там сейчас перестроятся. Вот пример использования memcache и еще куча возможностей Go.