предисловие
Оптимизация производительности интерфейсаДля студентов, занимающихся бэкенд-разработкой, это должно быть знакомо, потому что это общедоступная тема, не имеющая никакого отношения к языку разработки.
Проблема простая и простая, сложная и сложная.
Иногда простое добавление индекса решает проблему.
Иногда требуется рефакторинг кода.
Иногда необходимо увеличить кеш.
Иногда необходимо ввести какое-то промежуточное ПО, например mq.
Иногда необходимо создать подбиблиотеку и подтаблицу.
Иногда необходимо разделить услуги.
и Т. Д. . .
Причины проблем с производительностью интерфейса разные, в разных проектах разные интерфейсы, и причины могут быть разные.
В этой статье я обобщил несколько эффективных способов оптимизации производительности интерфейса и дал ссылку нуждающимся друзьям.
1. Индекс
Первое, что вы можете подумать об оптимизации производительности интерфейса, это:优化索引
.
Правильно, затраты на оптимизацию индекса минимальны.
При просмотре онлайн-журналов или отчетов мониторинга требуется много времени, чтобы найти инструкцию SQL, используемую интерфейсом.
На этом этапе у вас могут возникнуть следующие вопросы:
- Индексируется ли оператор sql?
- Добавленный индекс вступил в силу?
- MySQL выбрал неправильный индекс?
Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.
Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение
1.1 Не индексируется
в sql-оператореwhere
ключевое поле условия илиorder by
Для последнего поля сортировки я забыл добавить индекс, эта проблема очень распространена в проектах.
В начале проекта из-за небольшого объема данных в таблице разница в производительности SQL-запросов с индексами и без них незначительна.
Позже, с развитием бизнеса, объем данных в таблице увеличился, и ее пришлось индексировать.
Вы можете использовать команду:
show index from `order`;
Вы можете просмотреть статус индекса таблицы отдельно.
Вы также можете передать команду:
show create table `order`;
Просмотрите инструкцию создания таблицы для всей таблицы, которая также будет отображать статус индекса.
пройти черезALTER TABLE
Команда может добавить индекс:
ALTER TABLE `order` ADD INDEX idx_name (name);
также черезCREATE INDEX
Команда для добавления индекса:
CREATE INDEX idx_name ON `order` (name);
Однако здесь есть одно замечание: невозможно изменить индекс с помощью команд.
В настоящее время, если вы хотите изменить индекс в mysql, вы можете сначала удалить индекс, а затем добавить новый.
Чтобы удалить индекс, вы можете использоватьDROP INDEX
Заказ:
ALTER TABLE `order` DROP INDEX idx_name;
использоватьDROP INDEX
Также работает команда:
DROP INDEX idx_name ON `order`;
1.2 Индекс не действует
С помощью приведенной выше команды мы смогли подтвердить, что индекс существует, но вступает ли он в силу? В этот момент у вас может возникнуть вопрос.
Итак, как проверить, действует ли индекс?
Ответ: можно использоватьexplain
команда для просмотра плана выполнения mysql, она отобразит использование файла index.
Например:
explain select * from `order` where code='002';
результат:По этим столбцам можно судить об использовании индекса.Значение столбцов, включенных в план выполнения, показано на следующем рисунке:Если вы хотите узнать больше о подробном использовании объяснения, вы можете прочитать другую мою статью "объясните | Этот несравненный меч оптимизации индексов, вы действительно знаете, как им пользоваться?》
Честно говоря, оператор sql не использует индекс, за исключением того, что индекс не создается, самая большая вероятность того, что индекс недействителен.
Вот несколько распространенных причин сбоя индекса:Если это не вышеуказанные причины, необходимо дополнительно исследовать другие причины.
1.3 Неправильный выбор индекса
Кроме того, вы когда-нибудь сталкивались с такой ситуацией: это явно один и тот же sql, только входные параметры другие. Иногда идет индекс a, а иногда индекс b?
Да, иногда mysql выбирает неправильный индекс.
можно использовать при необходимостиforce index
Чтобы заставить запрос sql перейти к определенному индексу.
Что касается того, почему mysql выбирает неправильный индекс, об этом будет рассказано позже в специальной статье, так что давайте оставим здесь некоторую интригу.
2. оптимизация sql
Если индекс оптимизирован, это не имеет никакого эффекта.
Затем попытайтесь оптимизировать оператор sql, потому что стоимость его преобразования намного меньше, чем у java-кода.
Вот 15 советов по оптимизации SQL:Поскольку эти методы были подробно описаны в моих предыдущих статьях, я не буду вдаваться в подробности здесь.
Подробнее читайте в другой моей статье »Разговор о 15 советах по оптимизации sql", я верю, что вы много почерпнете после прочтения.
3. Удаленный вызов
Много раз нам нужно вызывать интерфейс других сервисов в определенном интерфейсе.
Например, есть такой бизнес-сценарий:
В интерфейсе запроса информации о пользователе нужно вернуть: имя пользователя, пол, уровень, аватар, очки, значение роста и другую информацию.
Имя пользователя, пол, уровень и аватар находятся в службе пользователей, баллы — в службе баллов, а значение роста — в службе значений роста. Чтобы агрегировать эти данные и возвращать их единообразно, необходимо предоставить другую службу внешнего интерфейса.
Следовательно, интерфейс запроса пользовательской информации должен вызывать интерфейс пользовательского запроса, интегральный интерфейс запроса и интерфейс запроса значения роста, а затем единообразно возвращать агрегированные данные.
Процесс вызова показан на следующем рисунке:Общее время вызова удаленного интерфейса составляет 530 мс = 200 мс + 150 мс + 180 мс.
Очевидно, что производительность последовательного вызова удаленного интерфейса очень низкая, а общее время, затрачиваемое на вызов удаленного интерфейса, равно сумме времени, затрачиваемого всеми удаленными интерфейсами.
Так как же оптимизировать производительность удаленного интерфейса?
3.1 Параллельные вызовы
Как упоминалось выше, поскольку производительность последовательного вызова нескольких удаленных интерфейсов очень низкая, почему бы не изменить его на параллельный?
Как показано ниже:Общее время, затраченное на вызов удаленного интерфейса, составляет 200 мс = 200 мс (то есть вызов удаленного интерфейса занял больше всего времени).
Прежде чем java8 может быть достигнутоCallable
Интерфейс для получения результата возврата потока.
После того, как java8 прошелCompleteFuture
класс реализует эту функциональность. Здесь мы берем CompleteFuture в качестве примера:
public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {
final UserInfo userInfo = new UserInfo();
CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
getRemoteUserAndFill(id, userInfo);
return Boolean.TRUE;
}, executor);
CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {
getRemoteBonusAndFill(id, userInfo);
return Boolean.TRUE;
}, executor);
CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {
getRemoteGrowthAndFill(id, userInfo);
return Boolean.TRUE;
}, executor);
CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();
userFuture.get();
bonusFuture.get();
growthFuture.get();
return userInfo;
}
Напоминаем, что не забывайте использовать пул потоков двумя способами. В примере я использовал executor, представляющий пользовательский пул потоков, чтобы предотвратить проблему слишком большого количества потоков в сценариях с высоким параллелизмом.
3.2 Неоднородность данных
Упомянутый выше интерфейс запроса пользовательской информации должен вызывать интерфейс пользовательского запроса, интерфейс запроса точек и интерфейс запроса значения роста, а затем единообразно возвращать агрегированные данные.
Затем мы можем сделать данные избыточными и хранить данные о пользовательской информации, баллах и значении роста в одном месте, например: Redis, сохраненная структура данных — это контент, требуемый интерфейсом запроса пользовательской информации. Затем через идентификатор пользователя запросите данные непосредственно из Redis, все в порядке?
В сценарии с высокой степенью параллелизма для повышения производительности интерфейса вызов удаленного интерфейса будет удален с высокой вероятностью и изменен на решение с гетерогенными данными, которое сохраняет избыточные данные.
Однако следует отметить, что если используется решение с гетерогенными данными, могут возникнуть проблемы с согласованностью данных.
Если информация о пользователе, баллы и значения роста обновляются, то в большинстве случаев они будут сначала обновляться в базе данных, а затем синхронизироваться с Redis. Однако эта операция с несколькими базами данных может привести к несогласованности данных с обеих сторон.
4. Повторные звонки
重复调用
Его можно увидеть повсюду в нашем ежедневном рабочем коде, но если его плохо контролировать, это сильно повлияет на производительность интерфейса.
Если вы мне не верите, давайте посмотрим.
4.1 База данных циклических запросов
Иногда нам нужно запросить, какие из них уже существуют в базе данных из указанного набора пользователей.
Код реализации можно записать так:
public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}
List<User> result = Lists.newArrayList();
searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
return result;
}
Если здесь 50 пользователей, вам нужно выполнить 50 циклов, чтобы запросить базу данных. Все мы знаем, что каждый раз, когда запрашивается база данных, это удаленный вызов.
Если вы запросите базу данных 50 раз, будет 50 удаленных вызовов, что является очень трудоемкой операцией.
Итак, как мы оптимизируем?
Конкретный код выглядит следующим образом:
public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}
List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
return userMapper.getUserByIds(ids);
}
Предоставляет интерфейс для пакетного запроса пользователей в соответствии с установленным идентификатором пользователя, и только один удаленный вызов может запросить все данные.
Здесь следует отметить, что размер коллекции идентификаторов должен быть ограничен, и лучше не запрашивать слишком много данных за один раз. В зависимости от реальной ситуации рекомендуется контролировать количество записей на запрос в пределах 500.
4.2 Бесконечный цикл
Некоторые друзья могут немного удивиться, увидев это название. Учитывается ли бесконечный цикл?
Не следует ли избегать бесконечных циклов в коде? Почему до сих пор существует бесконечный цикл?
Иногда бесконечный цикл пишется нами самими, например следующий код:
while(true) {
if(condition) {
break;
}
System.out.println("do samething");
}
Здесь используется вызов цикла while(true), который написан наCAS自旋锁
использовал больше.
Когда выполненное условие равно true, цикл автоматически завершается.
Если условия очень сложные, если суждение неверное или некоторые логические суждения написаны меньше, в некоторых сценариях может возникнуть проблема с бесконечным циклом.
Бесконечный цикл, скорее всего, вызван человеческими ошибками разработчиков, но эту ситуацию легко обнаружить.
Существует также скрытый и глубокий бесконечный цикл, вызванный менее строгим написанием кода. Если используются нормальные данные, проблема может быть не обнаружена, но как только появятся ненормальные данные, сразу же появится бесконечный цикл.
4.3 Бесконечная рекурсия
Если вы хотите напечатать все родительские категории категории, вы можете сделать это рекурсивно следующим образом:
public void printCategory(Category category) {
if(category == null
|| category.getParentId() == null) {
return;
}
System.out.println("父分类名称:"+ category.getName());
Category parent = categoryMapper.getCategoryById(category.getParentId());
printCategory(parent);
}
Обычно этот код подходит.
Но если кто-то сделает ошибку и укажет parentId категории на себя, будет бесконечная рекурсия. В результате интерфейс не может вернуть данные, и в конечном итоге происходит переполнение стека.
При написании рекурсивного метода рекомендуется задавать глубину рекурсии, например, если максимальный уровень классификации равен 4, глубину можно установить равной 4. Затем сделайте вывод в рекурсивном методе.Если глубина больше 4, он автоматически вернется, чтобы избежать ситуации бесконечного цикла.
5. Асинхронная обработка
Иногда, когда мы оптимизируем производительность интерфейса, нам нужно реорганизовать бизнес-логику, чтобы увидеть, нет ли неразумного дизайна.
Например, существует интерфейс запроса пользователя, который должен выполнять бизнес-операции, отправлять уведомления на месте и записывать журналы операций. Чтобы было удобнее реализовать, обычно мы будем помещать эту логику в интерфейс для синхронного выполнения, что неизбежно окажет определенное влияние на производительность интерфейса.
Внутренняя блок-схема интерфейса выглядит следующим образом:На первый взгляд этот интерфейс выглядит хорошо, но если вы тщательно разберетесь с бизнес-логикой, то обнаружите, что только бизнес-операции核心逻辑
, остальные функции есть非核心逻辑
.
Здесь есть принцип: логика ядра может выполняться синхронно, и библиотека может быть написана синхронно. Неосновная логика может выполняться асинхронно, и библиотека может быть написана асинхронно.
В приведенном выше примере функция отправки внутренних уведомлений и журналов операций пользователей не требует высокой производительности в реальном времени, даже если база данных будет записана позже, пользователь получит внутренние уведомления позже или операция будет завершена. откладывать просмотр журнала операций пользователя, что мало влияет на бизнес, поэтому его можно обрабатывать асинхронно.
Обычно различают два основных вида асинхронности:多线程
иmq
.
5.1 Пул потоков
использовать线程池
После преобразования логика интерфейса выглядит следующим образом:Функции внутреннего уведомления и журнала операций пользователя передаются в два отдельных пула потоков.
Таким образом, интерфейс фокусируется на бизнес-операциях, а остальная логика передается потоку для асинхронного выполнения.После преобразования производительность интерфейса мгновенно повышается.
Но есть небольшая проблема с использованием пула потоков: если сервер будет перезапущен, или функция, которую нужно выполнить, будет ненормальной, повторная попытка невозможна, и данные будут потеряны.
Так что же делать с этой проблемой?
5.2 mq
использоватьmq
После преобразования логика интерфейса выглядит следующим образом:Что касается функции отправки внутрисайтовых уведомлений и журналов операций пользователей, то она в интерфейсе толком не реализована, она только отправляет mq-сообщения на mq-сервер. Затем, когда сообщение потребляется потребителем mq, эти две функции фактически выполняются.
После этого преобразования производительность интерфейса также улучшается, потому что скорость отправки mq-сообщений очень быстрая, нам нужно сосредоточиться только на коде бизнес-операций.
6. Избегайте крупных сделок
Когда многие мелкие партнеры используют фреймворк spring для разработки проектов, для удобства им нравится использовать@Transactional
Аннотации обеспечивают функциональность транзакций.
Правильно, использование аннотации @Transactional для предоставления функций транзакций в этом декларативном методе транзакций действительно может сэкономить много кода и повысить эффективность разработки.
Но также легко вызвать большие дела и вызвать другие проблемы.
Давайте воспользуемся картинкой, чтобы увидеть проблемы, вызванные крупными транзакциями.Как видно из рисунка, проблема с большой транзакцией может привести к тайм-ауту интерфейса, что напрямую влияет на производительность интерфейса.
Как мы можем оптимизировать большие транзакции?
- Используйте меньше аннотаций @Transactional
- Поместите метод запроса (выбора) вне транзакции
- Избегайте удаленных вызовов в транзакциях
- Избегайте одновременной обработки слишком большого количества данных в транзакции
- Некоторые функции могут выполняться без транзакций.
- Некоторые функции могут обрабатываться асинхронно
Еще одна моя статья о проблемах большого бизнесаКак решить большую проблему, которая вызывает у людей головную боль?", который имеет очень подробное введение, если вам интересно, вы можете взглянуть.
7. Блокировка гранулярности
В некоторых бизнес-сценариях, чтобы предотвратить одновременное изменение общих данных несколькими потоками, что приводит к исключениям данных.
Чтобы решить проблему несогласованности данных, вызванную одновременным изменением данных несколькими потоками в параллельном сценарии. Как правило, мы будем:加锁
.
Однако, если блокировка не добавлена должным образом, степень детализации блокировки будет слишком грубой, что также сильно повлияет на производительность интерфейса.
7.1 synchronized
предоставляется в javasynchronized
Ключевые слова блокируют наш код.
Обычно существует два способа написания:在方法上加锁
и在代码块上加锁
.
Сначала посмотрим, как заблокировать метод:
public synchronized doSave(String fileUrl) {
mkdir();
uploadFile(fileUrl);
sendMessage(fileUrl);
}
Целью блокировки здесь является предотвращение создания одного и того же каталога в случае параллелизма, и второе создание не удастся, что повлияет на бизнес-функции.
Но этот метод напрямую заблокирован для метода, и степень детализации блокировки немного грубая. Потому что файл загрузки и метод сообщения в методе doSave не нужно блокировать. Блокировка требуется только для метода создания каталога.
Все мы знаем, что операция загрузки файла занимает очень много времени, и если весь метод заблокирован, блокировку необходимо снять после выполнения всего метода. Очевидно, что это приведет к плохой работе метода, который не стоит затрат.
В настоящее время мы можем изменить, чтобы добавить блокировку в блок кода, конкретный код выглядит следующим образом:
public void doSave(String path,String fileUrl) {
synchronized(this) {
if(!exists(path)) {
mkdir(path);
}
}
uploadFile(fileUrl);
sendMessage(fileUrl);
}
После этого преобразования степень детализации блокировки внезапно уменьшается, и блокировка добавляется только к функции параллельного создания каталога. Создание каталога — очень быстрая операция, и даже блокировка мало влияет на производительность интерфейса.
Самое главное, что другие функции загрузки файлов и отправки сообщений могут выполняться одновременно.
Конечно, в автономной версии сервиса это сделать не проблема. Однако в производственной среде, развернутой сейчас, для обеспечения стабильности службы, как правило, одна и та же служба будет развернута на нескольких узлах. Если узел зависает однажды, другие службы узла по-прежнему доступны.
Развертывание с несколькими узлами позволяет избежать ситуации, когда службы недоступны из-за отказа узла. В то же время он также может разделять поток всей системы, чтобы избежать чрезмерного давления в системе.
В то же время это также приносит новые проблемы: синхронизация может гарантировать только эффективность блокировки одного узла, но как заблокировать, если узлов несколько?
Ответ: Для этого необходимо использовать:分布式锁
. Текущие основные распределенные блокировки включают: распределенные блокировки Redis, распределенные блокировки zookeeper и распределенные блокировки базы данных.
Поскольку производительность распределенной блокировки zookeeper не очень высока, она мало используется в реальных бизнес-сценариях, поэтому я не буду говорить об этом здесь.
Давайте поговорим о распределенных блокировках redis.
7.2 Распределенная блокировка Redis
В распределенных системах, поскольку распределенные блокировки Redis проще и эффективнее, они стали первыми из распределенных блокировок и использовались во многих практических бизнес-сценариях.
Псевдокод для использования распределенной блокировки Redis выглядит следующим образом:
public void doSave(String path,String fileUrl) {
try {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
if(!exists(path)) {
mkdir(path);
uploadFile(fileUrl);
sendMessage(fileUrl);
}
return true;
}
} finally{
unlock(lockKey,requestId);
}
return false;
}
использовать доsynchronized
Как и в случае с блокировкой по ключевым словам, область действия блокировки слишком велика, другими словами, степень детализации блокировки слишком грубая, что приведет к очень низкой эффективности выполнения всего метода.
По сути, только при создании каталога нужно добавить распределенную блокировку, а остальной код вообще не нужно блокировать.
Итак, нам нужно оптимизировать код:
public void doSave(String path,String fileUrl) {
if(this.tryLock()) {
mkdir(path);
}
uploadFile(fileUrl);
sendMessage(fileUrl);
}
private boolean tryLock() {
try {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
} finally{
unlock(lockKey,requestId);
}
return false;
}
Приведенный выше код сужает область блокировки и добавляет блокировку только при создании каталога. После такой, казалось бы, простой оптимизации производительность интерфейса можно значительно улучшить. Возможно, будут неожиданные сюрпризы. Хахаха.
Несмотря на то, что распределенная блокировка redis проста в использовании, она имеет много деталей при использовании и скрывает много ям, на которую легко наступить, если не быть осторожным. Подробнее читайте в другой моей статье »Разговор о 8 ямах распределенной блокировки Redis》
7.3 Распределенная блокировка базы данных
В базе данных MySQL есть три основных типа блокировок:
- Блокировка таблицы: быстрая блокировка, без взаимоблокировок. Однако степень детализации блокировки велика, вероятность конфликта блокировок самая высокая, а параллелизм самый низкий.
- Блокировка строки: блокировка выполняется медленно и возникает тупиковая ситуация. Однако степень детализации блокировки является наименьшей, вероятность конфликта блокировок наименьшей, а параллелизм — наибольшей.
- Гэп-блокировка: накладные расходы и время блокировки между блокировками таблиц и строк. Это приведет к взаимоблокировке, степень детализации блокировки находится между блокировками таблиц и блокировками строк, а степень параллелизма является средней.
Более высокий параллелизм означает лучшую производительность интерфейса.
Таким образом, направление оптимизации блокировок базы данных:
Приоритетное использование行锁
, а затем используйте间隙锁
, а затем используйте表锁
.
Проверьте, правильно ли вы его используете?
8. Пейджинговая обработка
Иногда я буду вызывать интерфейс для запроса данных в пакетах, например: запрашивать информацию о пользователях в пакетах через идентификатор пользователя, а затем отправлять баллы этим пользователям.
Но если вы запрашиваете слишком много пользователей одновременно, например, запрашивайте данные 2000 пользователей одновременно. Идентификаторы 2000 пользователей передаются в параметрах, и при удаленном вызове интерфейса будет обнаружено, что время ожидания интерфейса пользовательского запроса часто истекает.
Код вызова следующий:
List<User> users = remoteCallUser(ids);
Как мы все знаем, вызов интерфейса для получения данных из базы данных должен передаваться по сети. Если объем данных слишком велик, будь то скорость сбора данных или передача по сети ограничена пропускной способностью, это займет много времени.
Итак, как оптимизировать эту ситуацию?
отвечать:分页处理
.
Запрос на получение всех данных за один раз превращается в несколько сборов, каждый раз собирается только часть данных пользователя, и, наконец, они объединяются и суммируются.
На самом деле есть два сценария решения этой проблемы:同步调用
и异步调用
.
8.1 Синхронные вызовы
если вjob
Требуется получить информацию о пользователях 2000. Требуется, чтобы, если данные могут быть получены правильно, общее время, необходимое для получения данных, не было слишком большим.
Однако существуют требования к длительности каждого вызова удаленного интерфейса, которая не может превышать 500 мс, в противном случае будет отправлено предупреждение по электронной почте.
В это время мы можем синхронно разбивать на страницы и вызывать информационный интерфейс пользователя пакетного запроса.
Конкретный пример кода выглядит следующим образом:
List<List<Long>> allIds = Lists.partition(ids,200);
for(List<Long> batchIds:allIds) {
List<User> users = remoteCallUser(batchIds);
}
В коде, который я используюgoogle
изguava
в инструментеLists.partition
метод, очень полезно использовать его для подкачки, иначе придется писать много кода подкачки.
8.2 Асинхронные вызовы
если в某个接口
Нужно получить информацию о 2000 пользователей, нужно учитывать больше.
Помимо затрат времени на удаленный вызов интерфейса, также необходимо учитывать общее время работы самого интерфейса, а тайм-аут не может быть 500 мс.
В настоящее время определенно невозможно использовать описанный выше синхронный пейджинг для запроса удаленного интерфейса.
Затем используйте только异步调用
.
код показывает, как показано ниже:
List<List<Long>> allIds = Lists.partition(ids,200);
final List<User> result = Lists.newArrayList();
allIds.stream().forEach((batchIds) -> {
CompletableFuture.supplyAsync(() -> {
result.addAll(remoteCallUser(batchIds));
return Boolean.TRUE;
}, executor);
})
Используя класс CompletableFuture, несколько потоков асинхронно вызывают удаленный интерфейс, и, наконец, агрегированные результаты возвращаются единообразно.
9. Добавить кеш
Решить проблемы с производительностью интерфейса,加缓存
это очень действенный метод.
Однако его нельзя кэшировать ради кэширования, и это все еще зависит от конкретного бизнес-сценария. Ведь добавление кеша увеличит сложность интерфейса, что приведет к несогласованности данных.
В некоторых сценариях с низким параллелизмом, например, когда пользователь размещает заказ, нет необходимости добавлять кэширование.
Также есть некоторые сценарии, например, когда классификация продуктов отображается на главной странице торгового центра, предполагается, что классификация здесь — это данные, полученные при вызове интерфейса, но страница пока не статична.
Если интерфейс запроса дерева классификации не использует кеш, а данные запрашиваются напрямую из базы данных, производительность будет очень низкой.
Итак, как использовать кеш?
9.1 Redis-кеш
Как правило, кэши, которые мы используем чаще всего, вероятно:redis
иmemcached
.
Но для Java-приложений большинство из них используют Redis, поэтому возьмем Redis в качестве примера.
Потому что в реляционной базе данных, такой как: mysql, меню имеет подчиненную связь. Категория четвертого уровня является подкатегорией категории третьего уровня, эта категория третьего уровня является подкатегорией категории второго уровня, а эта категория второго уровня является подкатегорией категории первого уровня.
Такая структура хранения определяет, что найти это дерево классификации за один раз не очень просто. Это требует рекурсивного запроса программы.Если категорий много, эта рекурсия занимает много времени.
Поэтому, если данные дерева классификации каждый раз напрямую запрашиваются из базы данных, это очень трудоемкая операция.
В это время мы можем использовать кеш, в большинстве случаев интерфейс получает данные напрямую из кеша. Для работы с Redis можно использовать зрелые фреймворки, такие как jedis и redisson.
Псевдокод с jedis выглядит следующим образом:
String json = jedis.get(key);
if(StringUtils.isNotEmpty(json)) {
CategoryTree categoryTree = JsonUtil.toObject(json);
return categoryTree;
}
return queryCategoryTreeFromDb();
Во-первых, запросите, есть ли данные меню в Redis по определенному ключу, если они есть, преобразуйте их в объект и верните напрямую. Если данные меню не найдены в Redis, данные меню будут запрошены из базы данных и возвращены.
Кроме того, нам также нужно задание для запроса данных меню из базы данных через регулярные промежутки времени и обновления их до Redis, чтобы мы могли каждый раз получать данные меню непосредственно из Redis без доступа к базе данных.После этой модификации производительность может быть быстро улучшена.
Но это улучшение производительности не самое лучшее, есть и другие решения, давайте взглянем на следующий контент.
9.2 Кэш второго уровня
Приведенная выше схема основана на кеше Redis, хотя скорость доступа Redis очень высока. Но ведь это удаленный звонок, и в дереве меню много данных, что немного отнимает время в процессе передачи по сети.
Есть ли способ получить данные напрямую, не запрашивая пульт?
Ответ: использовать二级缓存
, который представляет собой кеш в памяти.
В дополнение к моему собственному рукописному кешу памяти, в настоящее время используются фреймворки кеша памяти: guava, Ehcache, caffine и т. д.
мы здесь сcaffeine
Например, официально рекомендуется к весне.
Первый шаг - ввести соответствующую банку с кофеином.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.0</version>
</dependency>
Второй шаг, настроить CacheManager, включить EnableCaching
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(){
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
//Caffeine配置
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
//最后一次写入后经过固定时间过期
.expireAfterWrite(10, TimeUnit.SECONDS)
//缓存的最大条数
.maximumSize(1000);
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
Третий шаг: используйте аннотацию Cacheable для получения данных.
@Service
public class CategoryService {
@Cacheable(value = "category", key = "#categoryKey")
public CategoryModel getCategory(String categoryKey) {
String json = jedis.get(categoryKey);
if(StringUtils.isNotEmpty(json)) {
CategoryTree categoryTree = JsonUtil.toObject(json);
return categoryTree;
}
return queryCategoryTreeFromDb();
}
}
При вызове метода categoryService.getCategory() данные сначала берутся из caffine cache, если данные можно получить, то данные будут возвращены напрямую, без входа в тело метода.
Если данные не могут быть получены, снова проверьте данные из redis. Если запрос найден, данные возвращаются и помещаются в caffine.
Если данные по-прежнему не найдены, данные берутся напрямую из базы данных и помещаются в кэш кофеина.
Конкретная блок-схема выглядит следующим образом:Производительность этой схемы лучше, но есть недостаток в том, что если данные обновляются, кеш не может быть вовремя обновлен. Кроме того, при наличии нескольких серверных узлов данные на каждом узле могут отличаться.
Видно, что, хотя кеш L2 обеспечивает повышение производительности, он также вызывает проблему несогласованности данных. Использование кэша L2 должно сочетаться с реальными бизнес-сценариями, не все бизнес-сценарии применимы.
А вот сценарии классификации, которые я перечислил выше, подходят для использования кеша L2. Поскольку это относится к нечувствительным к пользователю данным, даже если есть небольшое несоответствие данных, это не имеет значения, пользователь может этого не заметить.
10. Подбиблиотека и подтаблица
Иногда производительность интерфейса ограничивается только базой данных.
Когда система развивается до определенной стадии, количество одновременных пользователей велико, и будет большое количество запросов к базе данных, которые должны занимать большое количество соединений с базой данных, а также приведет к узким местам производительности дискового ввода-вывода. .
Кроме того, по мере увеличения числа пользователей генерируется все больше и больше данных, и таблица может не сохраняться. Из-за большого объема данных, когда оператор sql запрашивает данные, это займет очень много времени, даже если используется индекс.
Что нам делать тогда?
Ответ: нужно сделать分库分表
.
Как показано ниже:На рисунке пользовательская библиотека разделена на три библиотеки, и каждая библиотека содержит четыре пользовательские таблицы.
Если есть запрос пользователя, он сначала направится в одну из пользовательских библиотек в соответствии с идентификатором пользователя, а затем найдет определенную таблицу.
Существует множество алгоритмов маршрутизации:
-
根据id取模
, например: id=7, есть 4 таблицы, затем 7%4=3, по модулю 3, маршрут к пользовательской таблице 3. -
给id指定一个区间范围
Например, если значение id равно 0–100 000, данные существуют в пользовательской таблице 0, а значение id равно 10–200 000, то данные существуют в пользовательской таблице 1. 一致性hash算法
Существует два основных направления для подбиблиотеки и подтаблицы:垂直
и水平
.
Честно говоря, вертикальное направление (то есть бизнес-направление) проще.
В горизонтальном направлении (то есть в направлении данных) функции подбиблиотек и подтаблиц на самом деле разные и их нельзя путать.
-
分库
: это решение проблемы нехватки ресурсов для подключения к базе данных и узкого места производительности дискового ввода-вывода. -
分表
: это решение проблемы, заключающейся в том, что объем данных в одной таблице слишком велик, и это занимает очень много времени, даже если индекс используется, когда оператор sql запрашивает данные. Кроме того, это также может решить проблему потребления ресурсов процессора. -
分库分表
: Это может решить такие проблемы, как нехватка ресурсов для подключения к базе данных, узкие места производительности дискового ввода-вывода, затраты времени на получение данных и потребление ресурсов процессора.
Если в некоторых бизнес-сценариях количество одновременно работающих пользователей велико, а объем данных, которые необходимо сохранить, мал, то разделить можно только базу данных, а не таблицу.
Если в некоторых бизнес-сценариях количество одновременных пользователей невелико, а количество сохраняемых пользователей велико, то разделить можно только таблицу, а не базу данных.
Если в некоторых бизнес-сценариях существует большое количество одновременных пользователей и большой объем данных, которые необходимо сохранить, базу данных и таблицы можно разделить.
Для более подробного содержания подбазы данных и подтаблицы вы можете прочитать мою другую статью, в которой более подробно рассказывается "Али Эрмиан: Почему подбаза данных и подтаблица?》
11. Доступность
Для оптимизации производительности интерфейса, помимо общих методов, упомянутых выше, некоторые вспомогательные функции необходимо использовать вместе, потому что они действительно могут помочь нам повысить эффективность поиска проблем.
11.1 Включить журнал медленных запросов
Обычно, чтобы найти узкое место производительности sql, нам нужно открыть журнал медленных запросов mysql. Запишите операторы SQL, которые превышают указанное время, отдельно, а затем проанализируйте и найдите проблему.
Чтобы включить журнал медленных запросов, вам нужно сосредоточиться на трех параметрах:
-
slow_query_log
медленный переключатель запросов -
slow_query_log_file
Путь, по которому хранятся журналы медленных запросов -
long_query_time
Сколько секунд записывать перед записью
через mysqlset
Команда может установить:
set global slow_query_log='ON';
set global slow_query_log_file='/usr/local/mysql/data/slow.log';
set global long_query_time=2;
После настройки, если время выполнения sql превысит 2 секунды, оно будет автоматически записано в файл slow.log.
Конечно, вы также можете напрямую изменить файл конфигурации.my.cnf
[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
long_query_time = 2
Но для этого необходимо перезапустить службу mysql.
Многие компании каждое утро отправляют электронное письмо с журналом медленных запросов, и разработчики оптимизируют SQL на основе этой информации.
11.2 Добавить мониторинг
Чтобы мы могли вовремя узнать, когда возникают проблемы с SQL, нам нужно сделать监控
.
В настоящее время системы мониторинга с открытым исходным кодом, которые широко используются в отрасли:Prometheus
.
Это обеспечивает监控
и预警
функция.
Схема архитектуры выглядит следующим образом:
Мы можем использовать его для мониторинга следующей информации:
- время отклика интерфейса
- Затраты времени на вызов сторонних сервисов
- Медленный запрос sql отнимает много времени
- использование процессора
- использование памяти
- использование диска
- Использование базы данных
и Т. Д. . .
Его интерфейс выглядит так:Вы можете увидеть текущий qps mysql, количество активных потоков, количество подключений, размер пула кеша и другую информацию.
Если обнаружится, что пул соединений тома данных занимает слишком много места, это обязательно повлияет на производительность интерфейса.
В это время соединение может быть открыто в коде и забылось закрыть, или параллелизм слишком велик, и требуется дальнейшее исследование и оптимизация системы.
Скриншоты — это лишь малая часть его функций, если вы хотите узнать больше о функциях, вы можете посетить официальный сайт Prometheus:prometheus.io/
11.3 Отслеживание ссылок
Иногда интерфейс включает в себя много логики, такой как: проверка баз данных, проверка Redis, удаленный вызов интерфейсов, отправка сообщений mq, выполнение бизнес-кодов и так далее.
Ссылка, запрошенная этим интерфейсом, очень длинная. Если проверять ее по одной, это займет много времени. В настоящее время мы больше не можем использовать традиционные методы для обнаружения проблемы.
Есть ли способ решить эту проблему?
Отслеживание системы с распределенными ссылками:skywalking
.
Схема архитектуры выглядит следующим образом:Найдите проблемы с производительностью при ходьбе по небу:В небе можно пройтиtraceId
(глобальный уникальный идентификатор), который объединяет полную ссылку, запрошенную интерфейсом. Вы можете увидеть трудоемкость всего интерфейса, трудоемкость вызова удаленных служб, трудоемкость доступа к базе данных или Redis и т. д. Функция очень мощная.
Когда раньше эта функция была недоступна, чтобы найти проблему с производительностью онлайн-интерфейса, нам также нужно было добавить лог в код, вручную распечатать трудоемкий статус каждой ссылки в ссылке, а затем проверить одну одним.
Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.
Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение
Если вы использовали Skywalking для решения проблем с производительностью интерфейса, вы бессознательно влюбитесь в него. Если вы хотите узнать больше о функциях, вы можете посетить официальный сайт Skywalking:skywalking.apache.org/
Последнее слово (пожалуйста, обратите внимание, не портите меня зря)
Если эта статья оказалась для вас полезной или познавательной, отсканируйте QR-код и обратите внимание, ваша поддержка — самая большая мотивация для меня продолжать писать.
Попросите в один клик три ссылки: лайк, вперед и смотреть.
Следите за официальной учетной записью: [Су Сан сказал о технологии], ответьте в официальной учетной записи: интервью, артефакты кода, руководства по разработке, управление временем имеют большие преимущества для поклонников, и ответьте: присоединяйтесь к группе, вы можете общаться и учиться со многими старшими из BAT. производители .