Управление кэшем с использованием паттерна одиночка

Управление кэшем с использованием паттерна одиночка

1. Введение

Управление правами доступа пользователей является одной из очень важных функций в корпоративных услугах. То, какие меню пользователь может просматривать, какие функции использовать и к каким данным получить доступ, зависит от информации о правах.
Текущий сервис также имел структуру, при которой меню, отображаемое пользователю, изменялось в зависимости от прав доступа. Администратор может настроить права меню для пользователей или ролей через отдельную администраторскую страницу. Когда пользователь входит в систему, доступные меню формируются на основе этой информации о правах и отображаются на экране.
Эта функция управления правами доступа необходима для работы сервиса, но в зависимости от способа реализации она может значительно повлиять на производительность системы и её поддержку. Особенно с увеличением числа пользователей и увеличением запросов на вход необходимо задуматься о методах проверки прав.
В этой статье я хочу поделиться проблемами, возникающими в процессе управления информацией о правах доступа к меню, а также тем, как мы внедрили кеш в памяти, используя паттерн Singleton, для решения этих проблем, методами реализации и полученными результатами после внедрения.

2. Существующая структура и проблемы

Изначальная система была разработана с относительно простой структурой.
Когда пользователь входит в систему, он вызывает сервис проверки прав. Сервис проверяет информацию о правах меню этого пользователя в базе данных (DB). Затем на основе полученных данных создается список меню, который возвращается пользователю.
Если выразить структуру просто, она выглядит следующим образом.
1. Вход пользователя в систему
2. Вызов сервиса проверки прав
3. Проверка в базе данных
4. Создание информации о правах меню
5. Возврат экрана пользователя
На начальном этапе разработки, поскольку пользователей было не так много, такая схема была вполне работоспособной. Объем данных также был небольшим, и запросов на вход не было много, поэтому нагрузка на базу данных не заметно увеличивалась.
Однако по мере увеличения срока эксплуатации сервиса и роста числа пользователей начали проявляться некоторые проблемы.

Повторяющийся запрос одних и тех же данных

Первой обнаруженной проблемой было то, что выполнялся повторный запрос одной и той же информации о разрешениях.
Например, предположим, что есть 1,000 пользователей, принадлежащих к группе обычных пользователей. Все они будут использовать одну и ту же информацию о меню прав. Однако в структуре, где база данных запрашивается каждый раз при входе в систему, одни и те же данные считываются 1,000 раз.
Информация о разрешениях в большинстве случаев не изменяется, но при этом каждый раз происходит обращение к базе данных.

Увеличение нагрузки на базу данных

Запрос информации о разрешениях — это операция, которая обязательно выполняется в процессе входа в систему.
Таким образом, по мере увеличения числа запросов на вход в систему количество обращений к базе данных также пропорционально увеличивается. В частности, когда время начала лекции или после проверки системы пользователи одновременно входят в систему, SQL-запрос на получение разрешений выполнялся особенно активно. Это могло стать причиной увеличения нагрузки на базу данных.
Конечно, единичный запрос сам по себе не является высокой стоимостью. Но если сотни или тысячи пользователей входят в систему одновременно, накопление ненужных запросов может повлиять на общую производительность системы.

Увеличение времени отклика

Для того чтобы первый экран был показан после входа пользователя, необходимо завершить запрос информации о разрешениях.
Если скорость отклика базы данных замедляется или возникает временная нагрузка, это напрямую влияет на время отклика при входе в систему. Запрос разрешений является обязательным этапом в логике сервиса, поэтому улучшение производительности на этом этапе также было важным фактором для повышения пользовательского опыта.

Ограничения масштабируемости

Хотя в настоящее время проблемы не велико, по мере увеличения масштаба службы количество запросов на вход также будет увеличиваться.
Структура, в которой все запросы напрямую обращаются к базе данных, в долгосрочной перспективе может иметь ограничения с точки зрения масштабируемости. Поэтому потребовалась новая структура, которая бы обеспечивала улучшение производительности запросов и в то же время поддерживала возможность обслуживания.

3. Анализ характеристик данных

Для рассмотрения способов улучшения производительности мы сначала проанализировали характеристики данных о правах меню. В результате анализа данные о правах меню имели следующие особенности.
Во-первых, частота изменения данных очень низка. Информация о правах может быть изменена только через административную панель. Во время использования сервиса обычными пользователями она почти не изменяется.
Во-вторых, частота запросов очень высока. Каждый раз, когда пользователь входит в систему, необходимо запрашивать информацию о правах. В некоторых функциях также выполняется дополнительная проверка прав.
В-третьих, одни и те же данные делят несколько пользователей. Большинство пользователей, относящихся к одной роли (Role), используют одинаковую информацию о правах.
То есть данные о правах меню имеют характеристику "мало изменений, много запросов (Read-Heavy)". Эти данные являются典型ным примером, когда эффект применения кэширования наиболее силен. Поэтому мы стали рассматривать решение управления информацией о правах не в базе данных, а в памяти.

4. Почему был выбран паттерн Singleton

После решения о применении кэширования у нас возникло еще одно беспокойство. Как же управлять кэш-объектом?
Информация о правах используется не только в сервисе входа, но и в сервисах создания меню, проверки прав и других компонентах. Если для каждого сервиса создавать отдельный кэш-объект, то одни и те же данные могут загружаться в память несколько раз. Кроме того, даже если кэш обновляется в одном сервисе, это не будет отражено в кэше других сервисов, что может привести к проблемам с несоответствием данных.
Чтобы решить эту проблему, мы спроектировали приложение так, чтобы использовать только один кэш-объект по всему приложению. Подходящим паттерном в этом случае является паттерн Singleton. Паттерн Singleton позволяет создать только один экземпляр определенного объекта в приложении, который делят все компоненты. Применение паттерна Singleton позволяет получить следующие преимущества.

Обеспечение согласованности данных

Поскольку все сервисы используют один и тот же кэш-объект, можно поддерживать согласованность данных о правах.

Минимизация использования памяти

Поскольку информация о правах загружается в память только один раз, это предотвращает дублирование хранения.

Быстрый доступ к данным

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

Увеличение поддерживаемости

Логика, связанная с кэшом, сосредоточена в одном объекте, что облегчает управление. В среде Spring Framework по умолчанию область видимости бинов — Singleton, что позволяет естественно реализовать такую структуру.

5. Способ реализации

Реализация была произведена с относительно простой структурой.
Когда приложение запускается, информация о разрешениях меню извлекается из базы данных и сохраняется в кэше. После этого, когда происходит запрос входа, данные возвращаются из кэша, не запрашивая базу данных. Если администратор изменяет информацию о разрешениях, кэш обновляется для поддержания актуального состояния.
Процесс реализации следующий.
1. Запуск приложения
2. Извлечение информации о разрешениях меню
3. Сохранение в кэше
4. Происходит запрос на вход
5. Возврат данных из кэша
6. Обновление кэша при изменении администратором
Ниже приведен упрощенный пример кода.

image1.png

В приведенном коде кэш-объект, который хранит информацию о разрешениях, создаётся только один.
Во время входа в систему информация о кэшированных меню запрашивается с помощью метода getMenus(), а при изменении информации о разрешениях на странице администратора вызывается метод refresh() для обновления данных.
В реальной операционной среде были добавлены такие функции, как разделение сервисного уровня, обработка исключений, реагирование на сбои при начальной загрузке и мониторинг.

6. Вопросы, которые необходимо учитывать во время эксплуатации

Наиболее важным аспектом, который я учитывал при применении кэша, было недействие кэша (Cache Invalidation). Наибольшим риском при использовании кэша является ситуация, когда реальные данные и данные кэша расходятся. Информация о разрешениях не меняется часто, но возможность изменений полностью не исключена. Если администратор изменил разрешения на конкретное меню, но кэш не обновился, пользователи продолжат использовать разрешения, действовавшие до изменений. Чтобы этого избежать, я реализовал мгновенную перезагрузку кэша после завершения изменения разрешений на странице администратора.

Кроме того, необходимо было учесть проблемы с параллельностью. Запросы на получение разрешений могут одновременно выполнять несколько пользователей, поэтому вместо общего HashMap был использован ConcurrentHashMap для обеспечения безопасности потоков.

Текущая структура была спроектирована с учетом односерверной среды. В однокомпонентной системе достаточно управлять только кэшем, хранящимся в памяти приложения, поэтому структура относительно проста. Однако в будущем, если объем услуг увеличится и произойдет расширение в многокомпонентную среду, может возникнуть проблема с тем, что каждый сервер может иметь разные данные кэша.

Например, если администратор изменил разрешение на определенное меню, и только кэш одного сервера был обновлен, а кэш другого сервера остался без изменений, у пользователей могут быть разные информации о разрешениях. Чтобы избежать этой проблемы, можно рассмотреть возможность внедрения распределенного кэша, такого как Redis, или структуры, передающей события обновления кэша на каждый сервер. В настоящее время это не применялось, поскольку реализуется односерверная среда, но было отмечено как область для рассмотрения при расширении.

Также была учтена ситуация с перезапуском приложения. Когда сервер перезапускается, все данные кэша, хранящиеся в памяти, исчезают. Поэтому была реализована логика инициализации для автоматической загрузки информации о разрешениях при запуске приложения, чтобы поддерживать доступность сервиса.

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

7. Результаты внедрения

После применения кэша в оперативной памяти, основанного на паттерне синглтона, удалось достичь нескольких положительных результатов.
Во-первых, количество выполнений SQL-запросов на получение разрешений, возникающих в процессе входа в систему, значительно сократилось. Ранее данные запрашивались из базы данных при каждом входе, но после применения кэша большинство запросов выполнялось из памяти.
Во-вторых, нагрузка на базу данных снизилась. Потребление соединений, использовавшихся для получения разрешений, уменьшилось, что позволило более эффективно использовать ресурсы базы данных.
В-третьих, время отклика улучшилось. Запросы к памяти значительно быстрее запросов к базе данных, поэтому производительность работы с входом в систему возросла.
В-четвертых, повысилась поддерживаемость. Поскольку информация о разрешениях управляется в одном объекте, структура стала легче для понимания. При анализе ошибок или улучшении функций также стало быстрее понимать связанную логику.
Пятый, мы подготовили основу для будущего расширения. В настоящее время используется кэш в памяти, но при увеличении масштаба системы была обеспечена структурная основа для перехода на распределенные кэш-решения, такие как Redis.

8. Заключение

Данные с низкой частотой изменения и высокой частотой запросов, такие как информация о правах доступа к меню, представляют собой области с очень большим эффектом от применения кэширования. Управляя необходимой информацией о правах доступа к меню на основе кэша в памяти с использованием паттерна одиночки после входа пользователя, мы смогли снизить нагрузку на базу данных и улучшить производительность ответа.
Особенно в результате этого применения мы еще раз убедились, что важно не просто использовать кэш, а анализировать характеристики данных и выбирать подходящие проектные паттерны. Паттерн одиночки имеет относительно простую структуру, но может стать очень эффективным выбором в ситуациях, когда необходимо управлять данными, которые должны быть разделены глобально.
В будущем мы планируем продолжать пересматривать стратегии кэширования и улучшения архитектуры, соответствующие характеристикам сервиса, для создания более стабильной и эффективной системы.

dwmoon

Site footer