Страницы

Поиск по вопросам

Показаны сообщения с ярлыком redis. Показать все сообщения
Показаны сообщения с ярлыком redis. Показать все сообщения

понедельник, 30 марта 2020 г.

Как работает Redis при одновременном доступе нескольких клиентов?

#redis


Первый клиент (локальный) пишет в список редис строку 50 байт 20 раз в секунду.
Допустим набрали 200 000 строк = 10 мегабайт
Второй клиент пытается получить первые 100 000 строк.
Передача через инет займет какое-то время.
Что в это время происходит с первым клиентом?
    


Ответы

Ответ 1



Все операции в Redis атомарны. Версионности в Redis, насколько я знаю, нет. Следовательно, будет блокировка: пока один клиент читает данные, второй будет ждать. Цитата: пишет в список Если речь именно о списках - List, то общий размер списка не будет влиять на работу клиентов: один клиент пишет данные в список, другой клиент читает. Как я уже сказал ранее, одному из клиентов приходится ждать, пока завершится операция другого. Но общий размер списка не влияет на это, ведь другой клиент будет читать за раз одну короткую строку размером 50 байт, а не всё содержимое списка разом. Я попытался проиллюстрировть это следующим кодом на C#: using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis; namespace ConAppRedis { class Program { static void Main(string[] args) { ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost,allowAdmin=true"); redis.GetServer("localhost", 6379).FlushAllDatabases(); var are = new AutoResetEvent(false); const int count = 10000; RedisKey key = "key"; var t1 = Task.Run(() => { IDatabase db = redis.GetDatabase(); var list = new List(); are.WaitOne(); for (int i = 0; i < count; i++) { list.Add(db.ListRightPop(key)); } }); var t2 = Task.Run(() => { IDatabase db = redis.GetDatabase(); var s = new string('a', 25); // 50 bytes for (int i = 0; i < count * 2; i++) { db.ListLeftPush(key, s); if (i == count) { are.Set(); } } Console.WriteLine(db.ListLength(key)); }); Task.WaitAll(t1, t2); } } } Запускаются две задачи (клиента): первая сперва ждёт разрешения на старт (его выдаст вторая задача, записав половину данных), потом начинает считывать данные. Вторая задача пишет данные в список. В конце эта задача получает текущий размер списка и выводит его. Значение каждый раз разное, что естественно, и равно где-то 10055. То есть к тому времени когда вторая задача заканчивает записывать 20000 значений, первая уже успела прочитать почти половину. Следовательно, они обращались к серверу по очереди совместно. А вот если бы речь шла о конкатенации одной строки с помощью APPEND, что приводило бы к её увеличению, то вторая задача довольно долго ждала бы, когда первая будет получать её полностью за раз (иначе никак).

понедельник, 2 марта 2020 г.

StackExchange.Redis: несколько вопросов по транзакциям

#c_sharp #net #redis


Возникло несколько вопросов по работе транзакций в StackExchange.Redis:


Можно ли помещать в одну транзакцию команды, предназначенные для разных нод в кластере?
Например, у ключа первой команды хэштэг {1}, а у ключа второй команды -- {2}. (Подозреваю,
что нельзя.)
В каких случаях вызов Execute()/ExecuteAsync() возвращает false? Только когда установленные
условия не были выполнены? Может ли вызов вернуть false, если условия вообще не были
установлены?
Eсли происходит ошибка сети или внутренняя ошибка Redis, то вызов Execute()/ExecuteAsync()
выбросит исключение или вернет false? Нужно ли еще проверять таски всех команд (предполагается,
что все команды корректные и не должны выбрасывать ошибок) или они просто будут в состоянии
cancelled?


К сожалению, документация насчет #2 и #3 не слишком подробная.



Этот вопрос -- дубликат с enSO.
    


Ответы

Ответ 1



Я заглянул в исходники StackExchange.Redis, а также поигрался с драйвером, и вот что обнаружил: при вызове методов команд у ITransaction никакого общения с сервером не происходит драйвер обращается к Redis только после вызова Execute()/ExecuteAsync() Поэтому ответы следующие: Как подсказали в оригинальном вопросе на enSO, в случае кластера все multi-key команды должны соответствовать одному слоту. В контексте кластера транзакция считается multi-key командой. Redis Cluster implements all the single key commands available in the non-distributed version of Redis. Commands performing complex multi-key operations like Set type unions or intersections are implemented as well as long as the keys all belong to the same node. https://redis.io/topics/cluster-spec При попытке выполнить транзакцию с невалидными ключами получим исключение "Multi-key operations must involve a single slot". Execute()/ExecuteAsync() возвращает false в двух случаях: когда условия транзакции не были выполнены и транзакция была отменена, и когда драйвер не смог поставить команду в очередь (например, если на сервере кончилась память). Все таски при этом помечаются как canceled. Также Execute()/ExecuteAsync() не возвращает false если некоторые команды завершились неудачно (например, если команда была адресована ключу с неправильным типом значения). В случае проблем с сетью, Execute()/ExecuteAsync() выбрасывает исключение, а все таски остаются в состоянии waiting for activation. Итого, таски следует проверять только когда Execute()/ExecuteAsync() возвращает true: каждый таск будет содержать или результат команды, или ошибку (см. свойство Exception).

понедельник, 6 января 2020 г.

Грамотное кэширование в NodeJS проекте

#nodejs #кэширование #redis #highload


Планируется highload-проект с использованием NodeJS, MySQL, Redis, NGINX.
За основу взят nodejs фреймворк express, и для работы с сокетами (может это и не
хорошее решение) - socket.io. Проект представляет собой single page application, предполагается,
что сокет-соединение будет открыто повсеместно. Будет большое количество страниц, которые
являются статистично-информативными, и не щадят бд тяжелыми запросами.

Есть несколько вопросов о том, как построить кэширование данных, и где лучше применять
Redis, а где использовать другие способы. 


Например, есть запрос к БД, его результат мы можем сохранить в Redis в виде JSON,
при следующих обращениях уже будем брать его из Redis и, когда необходимо - делать
инвалидацию данного кэша. - Насколько правильно это? И стоит ли именно в JSON хранить
данные?
Нам иногда нужна закэшированная страница не только в JSON, а кусок или целиком HTML.
- Правильно ли хранить эти куски в Redis?
Есть идея реализовать session-storage в Redis, как это сделать правильно? например
ключом будет id сессии, а значением - массив данных о клиенте. - Это правильно или
нет? Как будет лучше?
В каких случаях стоит записывать кэш в файлы на диск?
Стоит ли вообще пихать все что выше описано в redis? 


Я не имею опыта в highload, поэтому мне интересно все, что вы скажете, очень нужны
грамотные советы в построении проекта на перечисленных компонентах. Может, есть какие-то
рекомендации по работе с сокетами в highload, либо еще что-то, что касается основ такого
проекта. 

Признателен за ваши полезные советы, благодарю за внимание!
    


Ответы

Ответ 1



Например, есть запрос к БД, его результат мы можем сохранить в Redis в виде JSON, при следующих обращениях уже будем брать его из Redis и, когда необходимо - делать инвалидацию данного кэша. - Насколько правильно это? На сто процентов, но своевременная инвалидация кэша - довольно большой челлендж. И стоит ли именно в JSON хранить данные? Вообще меня несколько коробит сохранять данные строкой, но особого выбора нет, и вообще это не должно влиять ни на что. Нам иногда нужна закэшированная страница не только в JSON, а кусок или целиком HTML. - Правильно ли хранить эти куски в Redis? Нет, у вас же SPA, весь рендеринг должен быть на клиенте. Есть идея реализовать session-storage в Redis, как это сделать правильно? например ключом будет id сессии, а значением - массив данных о клиенте. - Это правильно или нет? Как будет лучше? Если сессия у вас считается за постоянное хранилище, то неправильно, потому что Redis не предназначен для постоянного хранения данных (хоть и умеет время от времени скидывать данные на диск). Вместо этого лучше организовать двухуровневое (или даже трехуровневое) хранилище из связки Redis - БД (In-Memory - Redis - БД в случае трехуровневой связки). В каких случаях стоит записывать кэш в файлы на диск? Как правило, ни в каких, есть база данных. Туда можно скидывать результаты тяжелых запросов, чтобы все ноды их видели, но даже в этом случае я бы организовывал промежуточный Redis-слой. Стоит ли вообще пихать все что выше описано в redis? Чем больше будет висеть в кэше, тем лучше, но время от времени посматривайте на количество съедаемой оперативной памяти. Может, есть какие-то рекомендации по работе с сокетами в highload, либо еще что-то, что касается основ такого проекта. Помните про две вещи Проект обязан масштабироваться горизонтально, т.е. простым добавлением новых серверов. Из этого вытекает, что изменения на одном сервере должны быть видны для всех остальных (это достигается за счет использования одного Redis и БД) Существует такая штука, как dogpile effect, который означает кучу пользователей, одновременно запросивших один и тот же ресурс. В случае с вышеописанным трехуровневым хранилищем сто синхронно пришедших пользователей не обнаружат запись в кэше и ломанутся (все сто) к БД. В случае, если в БД 20 одновременных подключений, и каждый запрос занимает 30мс, то последний пользователь получит данные не раньше, чем через 150мс (это в идеальном случае, на самом деле там будет значительно большее число), что очень нехорошо.

пятница, 27 декабря 2019 г.

Отказоустойчивость серверов

#mysql #сервер #nginx #redis


Суть проблемы в том, что если упадет сервер с приложением, то запросы пойдут на другой,
и такая же проблема с серверами mysql и redis.


Сервер:
Чтобы гарантировать отказоустойчивость сервера, достаточно купить 2 сервера и зарегистрировать
их на один домен ?
БД(Mysql):
Сделать 2 мастера, чтобы они ссылались друг на друга и в случае если один упадет,
то все запросы перейдут на другой ? И как сделать чтобы приложение само знало на какой
сервер mysql/redis посылать запросы, такой же трюк как с сервером сделать  ?


Подскажите как можно решить эту проблему ?

Схема:


    


Ответы

Ответ 1



В вашем случае, надо реализовать балансировщик нагрузки на базе NGINX (модуль ngx_http_upstream_module). Он будет принимать запросы для указанного домена и перенаправлять их на свободный или менее загруженный бакенд (сервер приложений). Для баз данных надо настроить Master и Slave сервер и сделать между ними репликацию.

Ответ 2



Для обеспечения отказоустойчивости можно также создать linux кластер на основе pacemaker из двух или больше нод (серверов), создать сервисный IP с именем, который будет подниматься на одном из узлов кластера. Приложения подключать к базам по этому сервисному имени. Все узлы должны располагаться в идеале в разных ЦОД, иначе отказоустойчивости не будет.

пятница, 20 декабря 2019 г.

Redis, как кэш для SQL запросов в веб-приложении

#проектирование #веб_программирование #кэширование #структуры_данных #redis


Я впервые использую редис в веб-приложении. Изначально, цель была - кэшировать результаты
сложных запросов, которых было не очень много, это не так сложно, и я подумал, почему
бы не кэшировать всё? Ну или почти всё.

И так, я написал конструктор mysql-запросов, который всегда перед тем как выполнить
запрос к бд, принимает необязательный параметр - ключ, по которому хранятся данные
в redis. В итоге если такой ключ был передан, конструктор сначала посмотрит, есть ли
данные по ключу в редисе, и если есть - то он вернет данные из редиса и к мускулу обращаться
не будет, а если нет - то выполнит sql-запрос, вернет данные в контроллер и в соседнем
потоке запишет их в редис (используя тот самый, переданный ключ). С запросами на выборку
понятно, а вот если мы делаем insert/update, то мы удаляем из редиса все данные которые
попали под маску переданного ключа, и тогда при новой выборке кэш обновится.

Все что попадает в редис, в основном хранится в виде JSON. И, вот настал тот ужасный
момент, когда количество хранимых данных стало большим (а на проде, будет еще в over
9999 раз больше), и самое важное - много дублирующихся данных. Приведу простой пример.
Мы заходим в раздел articles, загружаются посты которые находятся на первой странице
(пусть их по умолчанию отображается по 50). В этот момент, контроллер сформировал ключ
articles:list:page_1 по которому будет искать данные в редисе. Дальше походили по страницам,
сформировали кэш для articles:list:page_2 articles:list:page_5 articles:list:page_19
articles:list:page_28 .... После чего, нажали на кнопку "Показать все статьи", тем
обратились по ключу articles:list:all - понятно, что тут хранятся все записи (и я думаю
- это ужасно!).  Потом мы открыли несколько статей articles:post:id1234 articles:post:id567
articles:post:id890. Вы представьте сколько уже есть дублирующих данных в редисе, а
если мы дадим пользователю выбирать кол-во выводимых записей, то тогда появятся такие
ключи: articles:list:page_1_55 articles:list:page_3_55 и тд (_55 записей на странице).
Это приведет к большой беде. Теперь дальше, если  в админке мы отредактируем какой-то
пост, а заголовок этого поста выводится в листинге, тогда мы удалим из кэша старые
данные так articles:post:id890 и очистим весь листинг articles:list:* - что есть тоже
очень плохо.

А что касается сложных запросов к бд, есть еще один случай, когда сам пост хранится
в одной таблице, комменты к нему в другой, лайки в третьей и еще 5 таких зависимостей.
Сейчас (без редиса) это вытягивается с помощью сложного запроса с JOIN'ами (допустим
их 7 шт.). Это все можно сохранить в редис, но тогда если кто-то лайкнет пост, нужно
инвалидировать кэш, и заново придется выполнять сложный запрос. Есть вариант разбить
это на 7 простых запросов и сохранить каждый в редисе, и тогда если кто-то оставит
коммент или лайкнет, то нужно будет только один простой запрос выполнить. Вот, как
в такой ситуации было бы правильно поступить? (не будем говорить о тестах, будем рассуждать
примерно, берем какие-то средние значения и среднестатичные случаи). 

Что в этом случае вы посоветуете делать? Как правильно реализовать хранение данных
в кэше? Возможно стоит пересмотреть способ хранения, может не хранить коллекции :list:
целиком, а извлекать всегда какие-то части (элементы)? Я вот не могу в этом разобраться.

p.s. буду благодарен за ссылки на интересные статьи по теме (желат. на русском),
и по настройке редис на сервере, да и вообще как можно больше про редис хочется узнать
(не просто из постов на хабре, а что-то более подробное и доходчивое для человека который
впервые с ним работает)
    


Ответы

Ответ 1



Вы столкнулись с некоторыми вещами, которые не очень хорошо помещаются в тот мир, в котором вы работали раньше. В какой-то степени можно это назвать просто NoSQL, но это не совсем так, в любом случае могу поздравить с важным шагом в карьере. Дублирование данных Первое, о чем хотелось бы сказать: самое важное - много дублирующихся данных Дублирующиеся данные сами по себе нормальны. Нормализация базы данных SQL невольно учит нас тому, чтобы данные были в единичном экземпляре, но это, на самом деле, не аксиома. Дублирующиеся данные сложнее поддерживать, но само их наличие в целях улучшения работы приложения - это абсолютно нормально. Кроме этого, стоит вставить мою любимую ремарку про модель работы приложения: если сейчас у вас это де-факто pull-on-change - сформировать данные по запросу, то есть гораздо более интересная модель push-on-change - когда данные для запросов подготавливаются при добавлении, и в этом дублирование данных подразумевается само по себе. Представьте себе некую социальную сеть с лентой новостей. При стандартном подходе (и SQL-бэкенде) придется формировать гигантский SQL-запрос, который будет проверять необходимость показа каждой записи, наличие доступа к ней (мы же не хотим показывать приватные записи, верно?) и прочие атрибуты. Push-on-change же предлагает в этом случае в момент обновления записи в ленте вычислять, кому она должна быть показана, и формировать таблицу вида 'id пользователя | id записи | datetime', чтобы просто доставать из нее N первых записей, по которым уже формировать запрос по новостям. Данные в этом случае де-факто будут продублированы, но это необходимо, чтобы ускорить многократные read-операции за счет однократной write-операции. С переходом в хайлоад это становится особенно актуально, потому что один сервер не в состоянии хранить все данные и/или выдержать нагрузку, а поэтому необходимо разделять ответственность между серверами (что может поставить запрет на джойны, например). Однако, Отказ от подготовки ответов на все запросы геометрическая прогрессия, конечно, не позволяет подготовить заранее готовые ответы на все возможные запросы: Вы представьте сколько уже есть дублирующих данных в редисе, а если мы дадим пользователю выбирать кол-во выводимых записей, то тогда появятся такие ключи: articles:list:page_1_55 поэтому от вышеприведенного примера действительно стоит отказаться - у Redis нет столько оперативки (как и от :all - тут проблема не в оперативке, а в количестве данных, которые надо будет передать по сети и затем разобрать). Конкретно в вашем случае есть две вещи, которые я считаю необходимым отметить Текущий подход работает, де-факто, на кэширование конкретных запросов Redis используется как KeyValue-кэш Первое подразумевает тот самый рост данных в геометрической прогрессии со степенью, зависящей от количества параметров, по которым может проводиться выборка. Вам не обязательно получать из Redis уже готовый ответ - вы можете получать его либо по частям, либо бОльшим куском. В случае со страницами по 55 записей вы можете просто хранить "верхушку" таблицы в редисе: # пусть верхушка состоит из 500 записей from = 63 to = 63 + 55 if to < 500: top_entries = redis.get 'articles:top' # в редисе может не оказаться ничего или оказаться устаревший и слишком короткий список if top_entries && top_entries.length > to: return top_entries.slice from, to return article_repository.get_slice from, to В этом случае приложение знает, что у него (возможно) есть кусман на 500 записей в редисе, и запрос, скорее всего, проще обслужить оттуда, поэтому пытается сделать именно это. В случае со сложной иерархической структурой проще наоборот, разбить сущность на составляющие: article = redis.get 'article:' + id if not article: article = article_repository.get id if not article: throw new ResourceNotFoundException comments = redis.get 'comments:by-article:' + id if not comments: comments = comment_repository.get_by_article id likes = redis.get ' В этом случае вы делаете из сущностей некоторое подобие строительного материала, который не слишком сильно дублируется и позволяет собирать различные результаты на основе одних и тех же источников (например, поиск по статьям в любом случае пойдет через БД, но лайки для них можно вытащить из Redis без особых затрат по времени). Это не самый богатый арсенал, но он поможет избежать избыточного дублирования, которое может возникнуть, если иерархические сущности кэшируются целиком, и, заодно, уменьшит развесистость операции, необходимой для обновления одной атомарной сущности (т.е. не придется сбрасывать половину кэша из-за одного проставленного лайка). Закешировать всё Кроме всего вышеописанного, я бы относился немного более практично к идее закешировать всё. Сейчас вы пытаетесь уменьшить нагрузку на сервис за счет кэширования, но переносите в Redis буквально вообще все, но это вам не нужно. До десятой страницы новостей доберется один читатель из сотни - вам действительно нужно оптимизировать ее быстродействие? В том случае, если она срендерится за 100 мс, а не за 10, пользователь это не почувствует, и сервер тоже не почувствует, потому что основная нагрузка у него идет на получение первых записей. LazyLoad Все вышеописанное только вскользь говорило о том, как данные попадают в Redis - что в случае добавления записи необходимо пересчитать специально подготовленную выборку. Однако мы знаем, что записи в любом in-memory кэш-сервисе (будь то Redis, Memcache, Aerospike) вечны максимум вплоть до первой перезагрузки машины, и даже подготовленные выборки в этом случае умрут. В этом случае поможет механизм ленивой загрузки - если грубо, то он пытается получить данные из некоего источника данных А, и, если не находит, загружает их из источника Б, кладет в А, и возвращает. В программировании этот подход часто применяется для инициализации тяжелых объектов и вызовов из БД, которые могут быть не нужны: private heavy_object private settings get_heavy_object(): if not heavy_object: heavy_object = new HeavyObject return heavy_object get_settings(): if not settings: settings = read_settings_from_database() return settings В случае с кэшем хранилищем А является не переменная, а кэш, а хранилищем Б - БД: get_article(id): redis_id = 'article:' + id article = redis.get redis_id if not article: # не обнаружен в Redis - ищем в БД article = repository.get id if not article: # значит, он вообще не существует return null redis.put id, article # кладем в Redis для последующих вызовов return article Если вы будете применять эту парадигму везде, то приложению можно будет хоть живьем подменить Redis, оно все равно будет работать. Кроме того - для меня это самый важный пункт - в этом случае можно спокойно инвалидировать существующий кэш, не боясь за само приложение (но, конечно, стоит иметь в виду резко возрастающую нагрузку на БД). Инвалидация (и немного про CAP) Следующий вопрос, который у вас так же возникает - это инвалидация данных, т.е., когда они должны исчезнуть из кэша или обновиться. Самый напроломный вариант - это обновление данных in-place, т.е., как только обновилась сущность, обновились и все связи в кэше. Однако, это плохой вариант - точнее, он отличный, но его невероятно сложно реализовать, не забыв про что-то и не раздув код до невероятных размеров. В случае, если кэш условно-бесконечен, и какой-то тип записей не обновляется, то все пользователи будут видеть устаревшие данных и спрашивать у вас, почему на разных страницах у одной и той же статьи разные комментарии. Тут на помощь приходят две стратегии инвалидации, которые есть в Redis: LRU (Least Recently Used). При переполнении указанного в параметре max-memory размера Redis начнет удалять записи, которые не запрашивались дольше всех. Это гарантирует некоторую ротацию записей (при добавлении новых записей некоторые из старых будут удаляться, чтобы, при повторном запросе, быть обновленными из БД) TTL (Time To Live). Запись с указанным TTL может быть удалена по истечении этого TTL (может быть - в связи с проблемами в выполнении задачи не могу сказать, насколько вовремя это сделает Redis, но вряд ли задержка составит больше трех секунд). Именно это я и хочу предложить в качестве серебряной пули - делайте всем записям TTL в пределах 1-60 секунд, и устаревшие данные у вас гарантированно не продержатся дольше минуты, а благодаря lazy load воскреснут вновь. Резюмируя, проще всего задавать небольшое время жизни записям, и сильно не беспокоиться об инвалидации, пока продукт не вышел на поддержку. В этом случае у вас страдает целостность данных (consistency), но в большинстве случаев она на самом деле не является критичной, более того, существует применимая к распределенным системам теорема CAP (NB: обсуждаемое в этом ответе приложение - это не распределенная система), которая (если очень грубо) говорит о том, что невозможно одновременно поддерживать доступность и согласованность данных - это просто некоторое свойство нашей вселенной, и небольшая задержка в обновлении данных чаще всего не только остается незамеченной, но и не всегда может быть замеченной (если у вас есть задержка в обновлении выдачи статей, то пользователь не ожидает получить новую в тот же момент, когда она была написана - он не знает момента написания). Dogpile effect В связи с параграфом про TTL и lazyload нельзя пропустить так называемый эффект собачьей стаи. С момента запроса обнаружения пустоты в кэше первым клиентом до момента его обновления проходит некоторое время X, за которое могут прийти еще N клиентов. Приложение в этом случае добросовестно попытается реконструировать кэш еще N раз. Единого решения у этой проблемы нет, но вы можете либо ставить лок на время обновления кэша (но если у вас приложение стоит более, чем на одном сервере, то придется использовать сервис, поддерживающий распределенные блокировки - например, consul, etcd или даже реализация с помощью самого Redis - см. Redlock), либо просто считать, что в SLA приложения добавлена возможность работать без кэша вообще (если за кэшем у вас скрывается один запрос для одной записи в БД, то почему бы и нет). Поиск И, наконец, самое веселое. Как объединить сам поиск и подготовку данных для него? В общем случае - никак, ищите по базе, вы не сможете запихнуть все необходимое для запроса в KV-хранилище. Но если вас интересует, как это решается в серьезных случаях (имеется в виду поиск по базе данных, а не уровня яндекса/гугла, конечно), то есть специализированные решения, например, Lucene (и построенный на его основе ElasticSearch), Solr, Sphinx, YoctoDB. В общем случае поиск сводится к построению индексов из полей документа, поиску по этим индексам и агрегированию результата. Микросервисная архитектура Предложенные мной решения по разбиению на building blocks так или иначе приведут к появлению в приложении менеджеров каждой сущности, каждый из которых заведует отдельной сущностью. Хочется сказать, что это разбиение может продолжиться и вне приложения - приложение можно разбить на отдельные приложения, каждое из которых будет заниматься своим доменом. Это так же убьет всякую возможность джойнов, но, на самом деле, она и не нужна. Если пользователи у вас лежат в одном приложении на одном кластере, а статьи, в которых автор указан в виде идентификатор - в другом приложении на другом кластере, то вряд ли вам потребуется искать все статьи, в имени автора которого стоит "Андрей". Как обо всем позаботиться? Скорее всего, через весь ответ так и сквозят вопросы вроде "какой TTL ставить", "нужно ли кэшировать Х" и "какйо прирост производительности я получу". Ответ простой - это вообще никак не узнать, это выясняется только на практике. Деплойте приложение, анализируйте статистику, и через некоторое время вы просто поймете, где оно подтормаживает. Если оно не подтормаживает в месте, где вы забыли поставить кэш - возможно, там и нет смертельной необходимости в нем, и к этому месту нужно будет вернуться, если вообще не останется других задач? Просто пара слов про Redis Во-первых просто хотелось сказать, что кроме Redis есть еще сервисы, выполняющие те же функции (мой любимый - Aerospike). Redis довольно прост в использовании, но не умеет, например, шардиться, у него были проблемы с фрагментацией памяти (порог в 100 мб теоретически имел право забрать до 200 мб оперативки), он однопоточный и в мире NoSQL довольно похож на MySQL в мире баз данных. Тем не менее, с небольшим приложением по меркам интернет-гигантов он наверняка справится без особых проблем.

Ответ 2



MySQL предлагает собственный похожий механизм кэширования: query cache. Только тестирование объективно покажет, что эффективнее именно для вашего веб-приложения: работа без кэша, query cache или ваш велосипед с Redis.

Ответ 3



Может быть, перед тем как пытаться совместить SQL c не-SQL стоит сначала хорошо разобраться хотя бы в чём-то одном? Ваш случай не уникален и в SQL уже есть всё, что может понадобиться для кэширования сложных запросов. В MySQL для этого есть Materialized Views. В других базах данных это называется чуть по другому, но тоже ничего изобретать не приходится.

суббота, 7 декабря 2019 г.

Уведомления пользователю в реальном времени. Laravel 5.3

#php #nodejs #laravel #websocket #redis


Возникла необходимость уведомлять юзера о событиях в системе в реальном времени. 

Используется как основа laravel 5.3. Но как правильно это организовать и что для
этого необходимо не знаю. Подскажите как правильно построить подобную систему. Возможно
я смотрю не в ту сторону но все что я пока что нашел это Events в laravel

2 типа уведомлений:


Письма
Сообщение на фронте в виде окна с текстом.

    


Ответы

Ответ 1



В двух словах абстрагировано, добавляете методы которые проверяют информацию при появлении которой нужно выдать уведомление. К примеру public function newMessage() { // проверяем... //если есть новое setcookie("newMessage", $count, time() + 86400); return 'yes'; //если нет return 'no'; } Добавляем маршрут к этому методу. На фронте c помощью ajax слушайте эти куки. Как то так...

Ответ 2



Делал себе что-то подобное, реализовал следующим образом: Уведомления Email, SMS и т.п. лучше всего сделать через новую возможность, которая появилась в версии 5.3: Laravel Notifications Если в будущем захотите не только Email уведомление ознакомьтесь с сайтом - http://laravel-notification-channels.com/ там есть много каналов для уведомлений. Уведомления в браузере лучше всего тоже реализовать через новую фичу версии 5.3: Event Broadcasting Начнем с того что выберем драйвер для Event Broadcasting, я покажу на примере Pusher. Для начала внесем изменения в файл .env: BROADCAST_DRIVER=pusher PUSHER_APP_ID=#данные с панели Pusher PUSHER_KEY=#данные с панели Pusher PUSHER_SECRET=#данные с панели Pusher Загрузим пакет через Composer: composer require pusher/pusher-php-server Пока с Pusher всё, в конце вернемся к нему ещё раз. Создаем событие, (примеры и описание есть тут) к примеру мы будем отправлять уведомление когда кто-то пишет пользователю. Объект уведомления будет выглядеть примерно так: user = $user; $this->data = $data; } /** * Get the channels the event should broadcast on. * * @return Channel|array */ public function broadcastOn() { return ['chat-channel']; } public function broadcastAs() { return 'message-received'; } } Методы broadcastOn и broadcastAs нужны для Event Broadcasting, так же надо создать Listener для этого события чтобы в нём отправлять Email сообщение и прочие. Теперь необходимо создать Mail Notification в котором укажем шаблон письма, пример доступен по ссылке, назовем его к примеру MessageSend. Далее нужно создать Listener который будет вызывать это уведомление: user->notify(new MessageSend($event->data)); } } Теперь файле EventServiceProvider привяжем событие и слушатель: protected $listen = [ 'App\Events\ChatMessageSend' => [ 'App\Listeners\SendChatMessageNotification', ], ]; Теперь всё готово для отправки уведомлений. Чтобы отправить уведомление нужно вызвать событие: event(new ChatMessageSend($user, $data)); Письмо на почту ушло, и ушло на сервис Pusher, но теперь надо его вывести в браузере, на сайте Pusher есть примеры для разных языков, я выведу для JS: Pusher Test Вместо alert пишите свой JS для уведомлений в браузере. P.S. Код должен быть рабочий, но мог где то что-то пропустить

понедельник, 8 июля 2019 г.

Хранение массивов в Redis

Доброго времени! Хочу спросить совета по поводу хранения данных в redis как это сделать быстрее и оптимальней в плане кода и быстродействия php и самого Редиса. Дано - база с пользователями в таблице пользователей пордка 3х тысяч строк и количество может быть расширенно до 10. у каждого пользователя есть 7-10 параметров которые надо записать в редис и работать с ними, обращения к хранимым данным будут происходить довольно часто. изменение, перезапись запись.
первый способ который мне видится это хранить массив преобразованный в формат json по принципу 1 пользователь = 1 ключ примерно так:
допустим массив данных
$superparms['1'] = "13"; $superparms['2'] = "22"; $superparms['3'] = "55"; $superparms['4'] = "66";
ключ для хранения составляем строка+id пользователя
$key = 'user'.$id; //допустим user922
и запись
$superparms = json_encode($superparms);
$redis->set($key, $superparms);
$redis->expire($key, '600');
в таком случае в редис под каждым ключом хранится примерно такая строка {"1":"13","2":"22","3":"55","4":"66"}
и естественно при работе с ним придется каждый раз выгружать все данные, даже если надо изменить какой либо один параметр (чаще всего параметры меняются именно по отдельности за раз) делаю примерно так:
$curent = $redis->get($key);
$curent = json_decode ($curent);
foreach ($curent as $key => $value) {
//обрабатываем все и перезаписываем та же все }
смущает в этом способе то что хоть и используется минимум ключей и все хранится компактно - все равно каждый раз приходится вереберать весь массив данных - можно ли как то без обратного преобразования json decode менять на прямую данные в строке json?
и втрой вариант это для каждого параметра создавать свой ключ по конструкции: строка+id+_+номер параметра примерно будет так
$redis->set('user921_1', $superparms[0]); $redis->expire('user921_1', '600');
$redis->set('user921_2', $superparms[1]); $redis->expire('user921_2', '600');
$redis->set('user921_3', $superparms[2]); $redis->expire('user921_3', '600');
и т.д таким образом количество ключей возрастает раз в 10 но работать с ними становится проще т.к. избавляемся от json encod\decode и циклов foreach для перебора, но мне кажется из-за большого количества ключей в системе может создаваться загруженность? так ли это? какой способ наиболее быстрый в плане обработки php и хранения в redis? есть ли еще варианты решения такой задачи?


Ответ

Redis это не только ключ-значение, но и пачка других типов данных и соответствующих методов обработки набора значений, отсортированного списка значений, географических данных, хэш-таблиц
Под вашу задачу вполне подойдёт хэш-таблица. Посмотрите на используемую у вас библиотеку доступа к редису, для php redis ваша задача сводится к методам hMSet, куда сразу PHP-массив передать, hGetAll для получения массива обратно и hSet для редактирования полей по отдельности. И ещё кучка методов для возможной работы с этими данными непосредственно в redis.

пятница, 7 июня 2019 г.

Как работает Redis при одновременном доступе нескольких клиентов?

Первый клиент (локальный) пишет в список редис строку 50 байт 20 раз в секунду. Допустим набрали 200 000 строк = 10 мегабайт Второй клиент пытается получить первые 100 000 строк. Передача через инет займет какое-то время. Что в это время происходит с первым клиентом?


Ответ

Все операции в Redis атомарны.
Версионности в Redis, насколько я знаю, нет.
Следовательно, будет блокировка: пока один клиент читает данные, второй будет ждать.

Цитата:
пишет в список
Если речь именно о списках - List, то общий размер списка не будет влиять на работу клиентов: один клиент пишет данные в список, другой клиент читает. Как я уже сказал ранее, одному из клиентов приходится ждать, пока завершится операция другого. Но общий размер списка не влияет на это, ведь другой клиент будет читать за раз одну короткую строку размером 50 байт, а не всё содержимое списка разом.
Я попытался проиллюстрировть это следующим кодом на C#:
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis;
namespace ConAppRedis { class Program { static void Main(string[] args) { ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost,allowAdmin=true"); redis.GetServer("localhost", 6379).FlushAllDatabases();
var are = new AutoResetEvent(false); const int count = 10000; RedisKey key = "key";
var t1 = Task.Run(() => { IDatabase db = redis.GetDatabase(); var list = new List(); are.WaitOne();
for (int i = 0; i < count; i++) { list.Add(db.ListRightPop(key)); } });
var t2 = Task.Run(() => { IDatabase db = redis.GetDatabase(); var s = new string('a', 25); // 50 bytes
for (int i = 0; i < count * 2; i++) { db.ListLeftPush(key, s); if (i == count) { are.Set(); } } Console.WriteLine(db.ListLength(key)); });
Task.WaitAll(t1, t2); } } }
Запускаются две задачи (клиента): первая сперва ждёт разрешения на старт (его выдаст вторая задача, записав половину данных), потом начинает считывать данные.
Вторая задача пишет данные в список. В конце эта задача получает текущий размер списка и выводит его. Значение каждый раз разное, что естественно, и равно где-то 10055.
То есть к тому времени когда вторая задача заканчивает записывать 20000 значений, первая уже успела прочитать почти половину. Следовательно, они обращались к серверу по очереди совместно.

А вот если бы речь шла о конкатенации одной строки с помощью APPEND, что приводило бы к её увеличению, то вторая задача довольно долго ждала бы, когда первая будет получать её полностью за раз (иначе никак).

понедельник, 20 мая 2019 г.

StackExchange.Redis: несколько вопросов по транзакциям

Возникло несколько вопросов по работе транзакций в StackExchange.Redis:
Можно ли помещать в одну транзакцию команды, предназначенные для разных нод в кластере? Например, у ключа первой команды хэштэг {1}, а у ключа второй команды -- {2}. (Подозреваю, что нельзя.) В каких случаях вызов Execute()/ExecuteAsync() возвращает false? Только когда установленные условия не были выполнены? Может ли вызов вернуть false, если условия вообще не были установлены? Eсли происходит ошибка сети или внутренняя ошибка Redis, то вызов Execute()/ExecuteAsync() выбросит исключение или вернет false? Нужно ли еще проверять таски всех команд (предполагается, что все команды корректные и не должны выбрасывать ошибок) или они просто будут в состоянии cancelled?
К сожалению, документация насчет #2 и #3 не слишком подробная.

Этот вопрос -- дубликат с enSO


Ответ

Я заглянул в исходники StackExchange.Redis, а также поигрался с драйвером, и вот что обнаружил:
при вызове методов команд у ITransaction никакого общения с сервером не происходит драйвер обращается к Redis только после вызова Execute()/ExecuteAsync()
Поэтому ответы следующие:
Как подсказали в оригинальном вопросе на enSO, в случае кластера все multi-key команды должны соответствовать одному слоту. В контексте кластера транзакция считается multi-key командой.
Redis Cluster implements all the single key commands available in the non-distributed version of Redis. Commands performing complex multi-key operations like Set type unions or intersections are implemented as well as long as the keys all belong to the same node.
https://redis.io/topics/cluster-spec
При попытке выполнить транзакцию с невалидными ключами получим исключение "Multi-key operations must involve a single slot".
Execute()/ExecuteAsync() возвращает false в двух случаях: когда условия транзакции не были выполнены и транзакция была отменена, и когда драйвер не смог поставить команду в очередь (например, если на сервере кончилась память). Все таски при этом помечаются как canceled. Также Execute()/ExecuteAsync() не возвращает false если некоторые команды завершились неудачно (например, если команда была адресована ключу с неправильным типом значения). В случае проблем с сетью, Execute()/ExecuteAsync() выбрасывает исключение, а все таски остаются в состоянии waiting for activation.
Итого, таски следует проверять только когда Execute()/ExecuteAsync() возвращает true: каждый таск будет содержать или результат команды, или ошибку (см. свойство Exception).

четверг, 1 ноября 2018 г.

Redis, как кэш для SQL запросов в веб-приложении

Я впервые использую редис в веб-приложении. Изначально, цель была - кэшировать результаты сложных запросов, которых было не очень много, это не так сложно, и я подумал, почему бы не кэшировать всё? Ну или почти всё.
И так, я написал конструктор mysql-запросов, который всегда перед тем как выполнить запрос к бд, принимает необязательный параметр - ключ, по которому хранятся данные в redis. В итоге если такой ключ был передан, конструктор сначала посмотрит, есть ли данные по ключу в редисе, и если есть - то он вернет данные из редиса и к мускулу обращаться не будет, а если нет - то выполнит sql-запрос, вернет данные в контроллер и в соседнем потоке запишет их в редис (используя тот самый, переданный ключ). С запросами на выборку понятно, а вот если мы делаем insert/update, то мы удаляем из редиса все данные которые попали под маску переданного ключа, и тогда при новой выборке кэш обновится.
Все что попадает в редис, в основном хранится в виде JSON. И, вот настал тот ужасный момент, когда количество хранимых данных стало большим (а на проде, будет еще в over 9999 раз больше), и самое важное - много дублирующихся данных. Приведу простой пример. Мы заходим в раздел articles, загружаются посты которые находятся на первой странице (пусть их по умолчанию отображается по 50). В этот момент, контроллер сформировал ключ articles:list:page_1 по которому будет искать данные в редисе. Дальше походили по страницам, сформировали кэш для articles:list:page_2 articles:list:page_5 articles:list:page_19 articles:list:page_28 .... После чего, нажали на кнопку "Показать все статьи", тем обратились по ключу articles:list:all - понятно, что тут хранятся все записи (и я думаю - это ужасно!). Потом мы открыли несколько статей articles:post:id1234 articles:post:id567 articles:post:id890. Вы представьте сколько уже есть дублирующих данных в редисе, а если мы дадим пользователю выбирать кол-во выводимых записей, то тогда появятся такие ключи: articles:list:page_1_55 articles:list:page_3_55 и тд (_55 записей на странице). Это приведет к большой беде. Теперь дальше, если в админке мы отредактируем какой-то пост, а заголовок этого поста выводится в листинге, тогда мы удалим из кэша старые данные так articles:post:id890 и очистим весь листинг articles:list:* - что есть тоже очень плохо.
А что касается сложных запросов к бд, есть еще один случай, когда сам пост хранится в одной таблице, комменты к нему в другой, лайки в третьей и еще 5 таких зависимостей. Сейчас (без редиса) это вытягивается с помощью сложного запроса с JOIN'ами (допустим их 7 шт.). Это все можно сохранить в редис, но тогда если кто-то лайкнет пост, нужно инвалидировать кэш, и заново придется выполнять сложный запрос. Есть вариант разбить это на 7 простых запросов и сохранить каждый в редисе, и тогда если кто-то оставит коммент или лайкнет, то нужно будет только один простой запрос выполнить. Вот, как в такой ситуации было бы правильно поступить? (не будем говорить о тестах, будем рассуждать примерно, берем какие-то средние значения и среднестатичные случаи).
Что в этом случае вы посоветуете делать? Как правильно реализовать хранение данных в кэше? Возможно стоит пересмотреть способ хранения, может не хранить коллекции :list: целиком, а извлекать всегда какие-то части (элементы)? Я вот не могу в этом разобраться.
p.s. буду благодарен за ссылки на интересные статьи по теме (желат. на русском), и по настройке редис на сервере, да и вообще как можно больше про редис хочется узнать (не просто из постов на хабре, а что-то более подробное и доходчивое для человека который впервые с ним работает)


Ответ

Вы столкнулись с некоторыми вещами, которые не очень хорошо помещаются в тот мир, в котором вы работали раньше. В какой-то степени можно это назвать просто NoSQL, но это не совсем так, в любом случае могу поздравить с важным шагом в карьере.
Дублирование данных
Первое, о чем хотелось бы сказать:
самое важное - много дублирующихся данных
Дублирующиеся данные сами по себе нормальны. Нормализация базы данных SQL невольно учит нас тому, чтобы данные были в единичном экземпляре, но это, на самом деле, не аксиома. Дублирующиеся данные сложнее поддерживать, но само их наличие в целях улучшения работы приложения - это абсолютно нормально. Кроме этого, стоит вставить мою любимую ремарку про модель работы приложения: если сейчас у вас это де-факто pull-on-change - сформировать данные по запросу, то есть гораздо более интересная модель push-on-change - когда данные для запросов подготавливаются при добавлении, и в этом дублирование данных подразумевается само по себе. Представьте себе некую социальную сеть с лентой новостей. При стандартном подходе (и SQL-бэкенде) придется формировать гигантский SQL-запрос, который будет проверять необходимость показа каждой записи, наличие доступа к ней (мы же не хотим показывать приватные записи, верно?) и прочие атрибуты. Push-on-change же предлагает в этом случае в момент обновления записи в ленте вычислять, кому она должна быть показана, и формировать таблицу вида 'id пользователя | id записи | datetime', чтобы просто доставать из нее N первых записей, по которым уже формировать запрос по новостям. Данные в этом случае де-факто будут продублированы, но это необходимо, чтобы ускорить многократные read-операции за счет однократной write-операции. С переходом в хайлоад это становится особенно актуально, потому что один сервер не в состоянии хранить все данные и/или выдержать нагрузку, а поэтому необходимо разделять ответственность между серверами (что может поставить запрет на джойны, например). Однако,
Отказ от подготовки ответов на все запросы
геометрическая прогрессия, конечно, не позволяет подготовить заранее готовые ответы на все возможные запросы:
Вы представьте сколько уже есть дублирующих данных в редисе, а если мы дадим пользователю выбирать кол-во выводимых записей, то тогда появятся такие ключи: articles:list:page_1_55
поэтому от вышеприведенного примера действительно стоит отказаться - у Redis нет столько оперативки (как и от :all - тут проблема не в оперативке, а в количестве данных, которые надо будет передать по сети и затем разобрать). Конкретно в вашем случае есть две вещи, которые я считаю необходимым отметить
Текущий подход работает, де-факто, на кэширование конкретных запросов Redis используется как KeyValue-кэш
Первое подразумевает тот самый рост данных в геометрической прогрессии со степенью, зависящей от количества параметров, по которым может проводиться выборка. Вам не обязательно получать из Redis уже готовый ответ - вы можете получать его либо по частям, либо бОльшим куском. В случае со страницами по 55 записей вы можете просто хранить "верхушку" таблицы в редисе:
# пусть верхушка состоит из 500 записей from = 63 to = 63 + 55 if to < 500: top_entries = redis.get 'articles:top' # в редисе может не оказаться ничего или оказаться устаревший и слишком короткий список if top_entries && top_entries.length > to: return top_entries.slice from, to return article_repository.get_slice from, to
В этом случае приложение знает, что у него (возможно) есть кусман на 500 записей в редисе, и запрос, скорее всего, проще обслужить оттуда, поэтому пытается сделать именно это.
В случае со сложной иерархической структурой проще наоборот, разбить сущность на составляющие:
article = redis.get 'article:' + id if not article: article = article_repository.get id if not article: throw new ResourceNotFoundException comments = redis.get 'comments:by-article:' + id if not comments: comments = comment_repository.get_by_article id likes = redis.get '
В этом случае вы делаете из сущностей некоторое подобие строительного материала, который не слишком сильно дублируется и позволяет собирать различные результаты на основе одних и тех же источников (например, поиск по статьям в любом случае пойдет через БД, но лайки для них можно вытащить из Redis без особых затрат по времени). Это не самый богатый арсенал, но он поможет избежать избыточного дублирования, которое может возникнуть, если иерархические сущности кэшируются целиком, и, заодно, уменьшит развесистость операции, необходимой для обновления одной атомарной сущности (т.е. не придется сбрасывать половину кэша из-за одного проставленного лайка).
Закешировать всё
Кроме всего вышеописанного, я бы относился немного более практично к идее закешировать всё. Сейчас вы пытаетесь уменьшить нагрузку на сервис за счет кэширования, но переносите в Redis буквально вообще все, но это вам не нужно. До десятой страницы новостей доберется один читатель из сотни - вам действительно нужно оптимизировать ее быстродействие? В том случае, если она срендерится за 100 мс, а не за 10, пользователь это не почувствует, и сервер тоже не почувствует, потому что основная нагрузка у него идет на получение первых записей.
LazyLoad
Все вышеописанное только вскользь говорило о том, как данные попадают в Redis - что в случае добавления записи необходимо пересчитать специально подготовленную выборку. Однако мы знаем, что записи в любом in-memory кэш-сервисе (будь то Redis, Memcache, Aerospike) вечны максимум вплоть до первой перезагрузки машины, и даже подготовленные выборки в этом случае умрут. В этом случае поможет механизм ленивой загрузки - если грубо, то он пытается получить данные из некоего источника данных А, и, если не находит, загружает их из источника Б, кладет в А, и возвращает. В программировании этот подход часто применяется для инициализации тяжелых объектов и вызовов из БД, которые могут быть не нужны:
private heavy_object private settings
get_heavy_object(): if not heavy_object: heavy_object = new HeavyObject return heavy_object
get_settings(): if not settings: settings = read_settings_from_database() return settings
В случае с кэшем хранилищем А является не переменная, а кэш, а хранилищем Б - БД:
get_article(id): redis_id = 'article:' + id article = redis.get redis_id if not article: # не обнаружен в Redis - ищем в БД article = repository.get id if not article: # значит, он вообще не существует return null redis.put id, article # кладем в Redis для последующих вызовов return article
Если вы будете применять эту парадигму везде, то приложению можно будет хоть живьем подменить Redis, оно все равно будет работать. Кроме того - для меня это самый важный пункт - в этом случае можно спокойно инвалидировать существующий кэш, не боясь за само приложение (но, конечно, стоит иметь в виду резко возрастающую нагрузку на БД).
Инвалидация (и немного про CAP)
Следующий вопрос, который у вас так же возникает - это инвалидация данных, т.е., когда они должны исчезнуть из кэша или обновиться. Самый напроломный вариант - это обновление данных in-place, т.е., как только обновилась сущность, обновились и все связи в кэше. Однако, это плохой вариант - точнее, он отличный, но его невероятно сложно реализовать, не забыв про что-то и не раздув код до невероятных размеров. В случае, если кэш условно-бесконечен, и какой-то тип записей не обновляется, то все пользователи будут видеть устаревшие данных и спрашивать у вас, почему на разных страницах у одной и той же статьи разные комментарии. Тут на помощь приходят две стратегии инвалидации, которые есть в Redis:
LRU (Least Recently Used). При переполнении указанного в параметре max-memory размера Redis начнет удалять записи, которые не запрашивались дольше всех. Это гарантирует некоторую ротацию записей (при добавлении новых записей некоторые из старых будут удаляться, чтобы, при повторном запросе, быть обновленными из БД) TTL (Time To Live). Запись с указанным TTL может быть удалена по истечении этого TTL (может быть - в связи с проблемами в выполнении задачи не могу сказать, насколько вовремя это сделает Redis, но вряд ли задержка составит больше трех секунд). Именно это я и хочу предложить в качестве серебряной пули - делайте всем записям TTL в пределах 1-60 секунд, и устаревшие данные у вас гарантированно не продержатся дольше минуты, а благодаря lazy load воскреснут вновь.
Резюмируя, проще всего задавать небольшое время жизни записям, и сильно не беспокоиться об инвалидации, пока продукт не вышел на поддержку. В этом случае у вас страдает целостность данных (consistency), но в большинстве случаев она на самом деле не является критичной, более того, существует применимая к распределенным системам теорема CAP (NB: обсуждаемое в этом ответе приложение - это не распределенная система), которая (если очень грубо) говорит о том, что невозможно одновременно поддерживать доступность и согласованность данных - это просто некоторое свойство нашей вселенной, и небольшая задержка в обновлении данных чаще всего не только остается незамеченной, но и не всегда может быть замеченной (если у вас есть задержка в обновлении выдачи статей, то пользователь не ожидает получить новую в тот же момент, когда она была написана - он не знает момента написания).
Dogpile effect
В связи с параграфом про TTL и lazyload нельзя пропустить так называемый эффект собачьей стаи. С момента запроса обнаружения пустоты в кэше первым клиентом до момента его обновления проходит некоторое время X, за которое могут прийти еще N клиентов. Приложение в этом случае добросовестно попытается реконструировать кэш еще N раз. Единого решения у этой проблемы нет, но вы можете либо ставить лок на время обновления кэша (но если у вас приложение стоит более, чем на одном сервере, то придется использовать сервис, поддерживающий распределенные блокировки - например, consul, etcd или даже реализация с помощью самого Redis - см. Redlock), либо просто считать, что в SLA приложения добавлена возможность работать без кэша вообще (если за кэшем у вас скрывается один запрос для одной записи в БД, то почему бы и нет).
Поиск
И, наконец, самое веселое. Как объединить сам поиск и подготовку данных для него? В общем случае - никак, ищите по базе, вы не сможете запихнуть все необходимое для запроса в KV-хранилище. Но если вас интересует, как это решается в серьезных случаях (имеется в виду поиск по базе данных, а не уровня яндекса/гугла, конечно), то есть специализированные решения, например, Lucene (и построенный на его основе ElasticSearch), Solr, Sphinx, YoctoDB. В общем случае поиск сводится к построению индексов из полей документа, поиску по этим индексам и агрегированию результата.
Микросервисная архитектура
Предложенные мной решения по разбиению на building blocks так или иначе приведут к появлению в приложении менеджеров каждой сущности, каждый из которых заведует отдельной сущностью. Хочется сказать, что это разбиение может продолжиться и вне приложения - приложение можно разбить на отдельные приложения, каждое из которых будет заниматься своим доменом. Это так же убьет всякую возможность джойнов, но, на самом деле, она и не нужна. Если пользователи у вас лежат в одном приложении на одном кластере, а статьи, в которых автор указан в виде идентификатор - в другом приложении на другом кластере, то вряд ли вам потребуется искать все статьи, в имени автора которого стоит "Андрей".
Как обо всем позаботиться?
Скорее всего, через весь ответ так и сквозят вопросы вроде "какой TTL ставить", "нужно ли кэшировать Х" и "какйо прирост производительности я получу". Ответ простой - это вообще никак не узнать, это выясняется только на практике. Деплойте приложение, анализируйте статистику, и через некоторое время вы просто поймете, где оно подтормаживает. Если оно не подтормаживает в месте, где вы забыли поставить кэш - возможно, там и нет смертельной необходимости в нем, и к этому месту нужно будет вернуться, если вообще не останется других задач?
Просто пара слов про Redis
Во-первых просто хотелось сказать, что кроме Redis есть еще сервисы, выполняющие те же функции (мой любимый - Aerospike). Redis довольно прост в использовании, но не умеет, например, шардиться, у него были проблемы с фрагментацией памяти (порог в 100 мб теоретически имел право забрать до 200 мб оперативки), он однопоточный и в мире NoSQL довольно похож на MySQL в мире баз данных. Тем не менее, с небольшим приложением по меркам интернет-гигантов он наверняка справится без особых проблем.

понедельник, 15 октября 2018 г.

Уведомления пользователю в реальном времени. Laravel 5.3

Возникла необходимость уведомлять юзера о событиях в системе в реальном времени.
Используется как основа laravel 5.3. Но как правильно это организовать и что для этого необходимо не знаю. Подскажите как правильно построить подобную систему. Возможно я смотрю не в ту сторону но все что я пока что нашел это Events в laravel
2 типа уведомлений:
Письма Сообщение на фронте в виде окна с текстом.


Ответ

В двух словах абстрагировано, добавляете методы которые проверяют информацию при появлении которой нужно выдать уведомление. К примеру
public function newMessage() { // проверяем... //если есть новое setcookie("newMessage", $count, time() + 86400); return 'yes'; //если нет return 'no'; }
Добавляем маршрут к этому методу. На фронте c помощью ajax слушайте эти куки.

Как то так...