введение
Базы данных и кэши или использование вместо них Mysql и Redis должны быть знакомы каждому CRUD-ребенку. То, о чем пойдет речь в этой статье, — тоже классическая проблема, то есть как оперировать БД и кэш более разумно.
Прежде чем официально начать эту статью, я думаю, нам необходимо достичь консенсуса по следующим двум пунктам:
- Кэш должен иметь срок действия
- Чтобы обеспечить окончательную согласованность базы данных и кеша, не обязательно стремиться к строгой согласованности.
Почему должен быть срок годности? Прежде всего, для кеша, когда его частота попаданий выше, производительность нашей системы выше. Если элемент кеша не имеет срока действия и вероятность его попадания очень мала, это пустая трата места в кеше. И если есть время истечения, и когда элемент кеша часто попадает, мы можем обновлять его время истечения каждый раз, когда он попадает, что гарантирует, что горячие данные всегда будут существовать в кеше. Таким образом, частота попаданий в кеш гарантировано, а производительность системы улучшена.
Еще одним преимуществом установки времени истечения срока действия является то, что при несогласованности данных между базой данных и кешем это можно использовать в качестве крайней меры. То есть, когда данные действительно кажутся несогласованными, время истечения может гарантировать, что база данных несовместима с кэшированными данными только между временем возникновения несогласованности и истечением срока действия кэша, поэтому окончательная согласованность данных также гарантируется. секс.
Так почему бы нам не стремиться к строгой согласованности данных? В основном это вопрос баланса. База данных и кеш, возьмем в качестве примера Mysql и Redis, ведь это две системы.Если вы хотите обеспечить сильную согласованность, необходимо вводить протоколы распределенной согласованности, такие как 2PC или Paxos, или распределенные блокировки и т. д. Это Это сложно, и это определенно повлияет на производительность. И если требования к консистентности данных такие высокие, так ли уж необходимо внедрять кеш? Не было бы проще читать и писать базу данных напрямую? Так как же добиться строгой согласованности между базой данных и кэшированными данными? Это более сложный вопрос, и в этой статье он будет подробно рассмотрен в конце.
В этой статье в основном обсуждается схема в предпосылке обеспечения согласованности в конечном итоге.
Порядок чтения и записи баз данных и кешей
Когда дело доходит до порядка чтения и записи баз данных и кэшей, наиболее классическим решением является так называемыйCache Aside Pattern. На самом деле эта схема вообще не высокоуровневая.В принципе мы её используем постоянно,но название можем не знать.Ниже краткое введение в идею этой схемы:
- Инвалидация: программа сначала считывает данные из кеша, если нет попадания, читает из базы данных, а затем после успеха помещает данные в кеш
- Попадание: программа сначала считывает данные из кеша, а если попадает, то возвращает напрямую
- Обновление: программа сначала обновляет базу данных, а затем удаляет кеш.
Первые два шага связаны с порядком чтения данных, я думаю, у всех не должно быть возражений против такой конструкции. При чтении данных, естественно, сначала их нужно прочитать из кеша, если их нельзя прочитать, то их надо прочитать из базы данных, а потом поместить в кеш, иначе при следующем поступлении запроса их надо прочитать из базы данных. Ключевой вопрос заключается в третьем пункте, а именно в процессе обновления данных: почему сначала нужно обновить базу данных? Зачем потом удалять кеш вместо обновления? Это основной вопрос, который будет обсуждаться в данной статье.
Всего есть около четырех возможных вариантов (базу удалить нельзя...):
- Сначала обновите кэш, затем обновите базу данных.
- Сначала обновите базу данных, затем обновите кеш.
- Сначала удалите кэш, затем обновите базу данных.
- Сначала обновите базу данных, затем удалите кеш
Далее мы обсудим каждый случай один за другим:
Сначала обновите кэш, затем обновите базу данных.
Все мы знаем, что независимо от того, работает ли это с базой данных или работает с кешем, существует вероятность сбоя. Если мы сначала обновим кеш, а затем обновим базу данных, предполагая, что обновление базы данных не удастся, старые данные будут сохранены в базе данных. Конечно, вы можете повторить попытку обновления базы данных, тогда в крайнем случае машина, отвечающая за обновление базы данных, также выйдет из строя, тогда данные в базе данных никогда не будут обновлены, а когда кеш недействителен, другие машины будут прочитать из базы данных снова.Данные старые данные, а затем поместить их в кеш, что приводит к потере предыдущей операции обновления, поэтому скрытая опасность этого велика.
С точки зрения сохраняемости данных база данных, конечно, лучше, чем кеш, и мы также должны сосредоточиться на данных в базе данных, поэтому, когда нам нужно обновить данные, мы должны сначала обновить базу данных, а не кеш.
Сначала обновите базу данных, затем обновите кеш.
Здесь есть две основные проблемы, первая — проблема параллелизма: если предположить, что поток A (или машина A, причина одна и та же) и поток B должны обновить одни и те же данные, A предшествует B, но временной интервал очень короткий. , то может быть Появление:
- Поток A обновляет базу данных
- Поток B обновляет базу данных
- Поток B обновляет кеш
- Поток A обновляет кеш
Само собой разумеется, что поток B должен обновлять кеш последним, но, возможно, из-за сети и других причин поток B обновляет кеш перед потоком A, что приводит к тому, что данные в кеше не обновляются.
Вторая проблема заключается в том, что мы не уверены, будет ли часто считываться обновляемый элемент кеша. был прочитан. Привел к пустой трате места в кэше. Кроме того, значения в кеше могут вычисляться с помощью серии вычислений, а не напрямую соответствовать данным в базе данных.Частое обновление кеша приведет к большому количеству неверных вычислений, что приведет к пустой трате машинного времени. представление.
Подводя итог, решение обновления кеша не рекомендуется, мы должны рассмотреть возможность удаления кеша.
Сначала удалите кэш, затем обновите базу данных.
Проблема с этой схемой также очевидна.Предполагая, что сейчас есть два запроса, один на запись A, а другой на чтение B, может произойти следующая последовательность выполнения:
- Запрос A на удаление кеша
- Запросите B прочитать кеш, найти, что он не существует, и прочитать старое значение из базы данных
- Запрос A на запись нового значения в базу данных
- Запрос B на запись старого значения в кеш
Это заставит еще старое значение в кэше, и вы не можете прочитать новое значение до истечения кэша. Эта проблема будет более очевидна в случае разделения данных с чтением базы данных, поскольку время первичных потребностей, запрос B приобретена, вероятно, будет старым, то кэш записи также будет старому.
Сначала обновите базу данных, затем удалите кеш
Мы наконец-то пришли к нашему наиболее часто используемому решению, но самое часто используемое не означает, что проблем не будет.Мы по-прежнему предполагаем, что есть два запроса, запрос A — запрос запроса, а запрос B — запрос обновления, тогда может возникнуть следующая ситуация:
- Срок действия предыдущего кеша истек
- Запрос A на проверку базы данных и получение старого значения
- Запрос B на обновление базы данных
- Запрос B на удаление кеша
- Запрос A на запись старого значения в кеш
Вышеупомянутая ситуация действительно может иметь место, но вероятность возникновения может быть невысокой, потому что условием установления вышеуказанной ситуации является то, что при чтении данных кэш просто дает сбой, и в это время есть еще один параллельный запрос на запись . Учитывая, что операция записи в БД обычно медленнее, чем операция чтения, (это означает, что при записи в БД БД вообще блокируется, а обычные операторы запросов не блокируются. Конечно, за исключением сложных операторов запросов, но доля таких операторов будет не слишком высока) и, учитывая распространенную архитектуру разделения чтения-записи БД, разумно думать, что в реальной жизни доля запросов на чтение намного выше, чем на запись, поэтому мы можем нарисовать заключение. В этом случае вероятность грязных данных в кеше невысока.
Что, если это сценарий разделения чтения-записи? Та же проблема возникает, если вы следуете последовательности выполнения, как описано ниже:
- Запрос A на обновление основной библиотеки
- Запрос A на удаление кеша
- Запрос B на запрос кеша, совпадений нет, запрос получает старое значение из библиотеки
- Синхронизация из библиотеки завершена
- Запрос B на запись старого значения в кеш
Если синхронизация master-slave базы данных относительно медленная, также возникнет проблема несогласованности данных. На самом деле это так, ведь мы работаем на двух системах, и в сценарии с высоким уровнем параллелизма нам сложно гарантировать порядок выполнения между несколькими запросами, а даже если и удастся, то это может дорого стоить в плане производительности стоимость. Так почему же мы все еще должны придерживаться стратегии сначала обновить базу данных, а затем удалить кеш? Прежде всего, почему вы хотите удалить, а не обновить кеш, это было проанализировано ранее, поэтому я не буду здесь вдаваться в подробности. Так почему мы должны сначала обновить базу данных? Поскольку кеш часто не так хорош, как база данных с точки зрения сохраняемости данных, а данные в базе данных не имеют концепции срока действия, мы должны сосредоточиться на данных в базе данных. кеш в конечном итоге будет соответствовать базе данных.
Итак, если я просто хочу решить две вышеупомянутые проблемы, что я могу сделать, не требуя строгой согласованности?
Есть ли идея получше?
На самом деле, при обсуждении последнего решения мы не рассматривали ситуацию, что может произойти сбой работы базы данных или работы кеша, и такая ситуация существует объективно. Итак, здесь мы кратко обсудим, прежде всего, если обновление базы данных не удается, это не имеет большого значения, потому что и база данных, и кеш в это время все еще являются старыми данными, и несоответствия нет. Предположим, что удаление кеша не удалось? На этом этапе действительно будут несоответствия данных. В дополнение к базовому решению по установке времени истечения срока действия кеша, если мы хотим гарантировать, что кеш можно удалить вовремя, насколько это возможно, мы должны рассмотреть возможность повторной попытки операции удаления.
Конечно, вы можете повторить операцию удаления непосредственно в коде, но знайте, что если сбой вызван сетевыми причинами, повторная попытка немедленного выполнения операции, скорее всего, не удастся, поэтому вам может потребоваться некоторое время ожидания между каждой повторной попыткой, например, сотни миллисекунд или даже секунд. Чтобы не влиять на нормальную работу основного процесса, вы можете передать это дело асинхронному потоку или пулу потоков для выполнения, но если машина в это время также выйдет из строя, операция удаления будет потеряна.
Итак, как решить эту проблему? Прежде всего, вы можете рассмотреть вопрос о введении очереди сообщений.Хорошо, я знаю, что запись в очередь сообщений может завершиться ошибкой, но это основано на том, что ни кеш, ни очередь сообщений недоступны.Следует сказать, что вероятность от этого не высок. После введения очереди сообщений потребитель отвечает за удаление кеша и повторную попытку, что может быть медленнее, но может гарантировать, что операция не будет потеряна.
Возвращаясь к двум вышеупомянутым проблемам, суть двух вышеупомянутых проблем заключается в том, что старое значение записывается в кеш, поэтому решение этой проблемы состоит в том, чтобы удалить кеш, учитывая сбой выполнения, вызванный сетевыми проблемами или проблемой порядок выполнения, выполняемая здесь операция удаления должна быть асинхронной отложенной операцией. Конкретно что надо сделать? Просто обратитесь к вышеизложенному, введите очередь сообщений, в случае сбоя удаления кеша запишите кеш удаления как сообщение в очередь сообщений, а затем медленно используйте и повторите попытку потребителем.
Что, если это сценарий разделения чтения-записи? Мы знаем, что синхронизация данных между ведущей и подчиненной базой данных (в качестве примера возьмем Mysql) достигается за счет синхронизации binlog, поэтому здесь мы можем рассмотреть возможность подписки на binlog (которую можно реализовать с помощью промежуточного программного обеспечения, такого как canal), и извлечь элементы кэша, подлежащие удалению.Затем оно записывается в очередь сообщений как сообщение, а затем медленно потребляется и повторяется потребителем. В этом случае программа не может активно удалять кеш, но если вы хотите как можно быстрее прочитать последнее значение из кеша, то можете также рассмотреть возможность удаления кеша, тогда может случиться так, что старое значение будет записано в снова кеш, и кеш в случае дедупликации. Но в целом это не будет проблемой.Во-первых, старое значение перезаписывается в кеш.Ситуация не более чем ситуация когда программа активно не удаляет кеш.Кроме того,дедупликация кеша гарантирует,что не будет между базой данных и кешем не должно быть долговременных данных.Данные о времени несовместимы. (Почему после удаления кеша все еще можно записать в кеш старые значения? См. приведенную выше схему сначала обновления базы данных, а затем удаления кеша, и последовательность выполнения в сценарии разделения чтения-записи) Конечно, мое личное предложение: если вы можете это вынести, если данные несовместимы в течение определенного периода времени, нет необходимости самостоятельно удалять кеш.
Суть решения вышеуказанных проблем заключается в реализации стратегии асинхронного отложенного удаления, поэтому здесь нам необходимо ввести очереди сообщений. Если база данных использует архитектуру разделения чтения-записи, вам необходимо рассмотреть возможность подписки на binlog, иначе она может быть сначала удалена, а затем синхронизирована.
разбивка кеша
Некоторые учащиеся могут заметить, что если будет принята схема удаления кеша, это может привести к поломке кеша в сценариях с высоким параллелизмом (это несколько отличается от проникновения в кеш), то есть большое количество запросов запрашивают один и тот же кеш одновременно, но это Если кеш просто истекает или удаляется, все запросы будут попадать в базу данных, вызывая серьезные проблемы с производительностью. Для этой проблемы, в том числе для решения проблемы проникновения в кеш, я могу рассмотреть возможность написания отдельной статьи, чтобы объяснить ее позже Здесь я кратко расскажу о решении, которое на самом деле является блокировкой.
Когда потоку необходимо получить доступ к кешу, если кеш оказывается пустым, он должен сначала конкурировать за блокировку, в случае успеха выполнить обычные операции чтения и записи базы данных, а затем снять блокировку, в противном случае подождать. время, попробуйте снова прочитать кеш и продолжить конкурировать за блокировки, если данных нет. Это сценарий с одним компьютером. Что делать, если несколько компьютеров одновременно обращаются к одному и тому же элементу кэша? Если машин не много, то такая ситуация вообще не будет проблемой, но здесь есть пункт оптимизации, то есть после чтения данных из базы сделать суждение о кеше, есть ли уже данные в кеше кеш, нет необходимости заново писать кеш. Но если машин много, то приходится рассматривать распределенные блокировки. Проблема с этим решением очевидна. Добавление блокировок, особенно распределенных, окажет значительное влияние на производительность системы, а реализация распределенных блокировок станет проверкой опыта и силы разработчиков. Это особенно важно в сценариях с высокой степенью параллелизма. Я предлагаю вам не применять распределенные блокировки вслепую, если в этом нет необходимости.
Как добиться сильной консистенции?
Могут быть некоторые студенты, которые захотят поднять планку. Существующие решения все еще не идеальны. Если я просто хочу добиться строгой согласованности, что я могу сделать?
Конечно, можно реализовать протокол консенсуса, хотя стоимость также очень объективна. 2PC и даже сам 3PC имеют определенные недостатки, поэтому, если будет принято это решение, в архитектурный проект следует ввести множество мер по обеспечению отказоустойчивости, отказоустойчивости и восходящего подхода. А Паксос и Рафт? Тогда вы должны сначала прочитать, по крайней мере, соответствующие документы этих двух, и изучить, какие решения с открытым исходным кодом в настоящее время представлены на рынке, и хорошо провести проверку, и быть в состоянии решить проблему самостоятельно... Кстати, Не говоря уже о проблемах с производительностью.
Есть ли какие-либо другие идеи, кроме протокола консенсуса?
Вернемся к самому решению «сначала обновить базу, а потом удалить кеш». Буквально здесь два шага. Поэтому перед обновлением базы и удалением кеша запрос на чтение считывает грязные данные. Если вы хотите добиться строгой согласованности между ними, все запросы на чтение должны быть заблокированы до тех пор, пока кэш не будет окончательно удален до обновления базы данных. Если это сценарий разделения чтения и записи, запрос на чтение должен быть заблокирован до обновления основной библиотеки и не может быть выпущен до тех пор, пока не будет завершена синхронизация master-slave и кэш не будет удален.
Эта идея на самом деле является идеей сериализации.Запрос на запись должен быть выполнен до запроса на чтение, чтобы гарантировать, что последние данные видны всем запросам на чтение. Вам это ничего не напоминает? Например, volatile, барьер памяти, ReadWriteLock или совместная блокировка базы данных, монопольная блокировка... Текущий сценарий может отличаться, но проблемы, с которыми придется столкнуться, аналогичны.
Теперь вернемся к самому вопросу, как нам добиться этой блокировки? Некоторые студенты, возможно, обнаружили, что то, что нам нужно, на самом деле является своего родаРаспределенная блокировка чтения-записи. Для запросов на запись блокировка записи должна применяться перед обновлением базы данных, в то время как другие потоки или машины должны применять блокировку чтения перед чтением данных. Блокировки чтения являются общими, а блокировки записи являются исключительными, то есть, если блокировка чтения существует, вы можете продолжать применять блокировку чтения, но не можете применять блокировку записи. замок может применяться для. Только путем реализации этой распределенной блокировки чтения-записи можно гарантировать, что запрос на чтение не будет считывать грязные данные до того, как запрос на запись завершит операции с базой данных и кэшем.
Обратите внимание, что используемая здесь распределенная блокировка чтения-записи не решает проблему поломки кеша, потому что с точки зрения запроса на чтение, если происходит обновление базы данных, запрос на чтение либо блокируется, либо кэш пуст. БД и запись в кеш. Чтобы предотвратить попадание большого количества запросов непосредственно в базу данных из-за аннулирования или удаления кеша, что приводит к сбою базы данных, вы можете рассмотреть только добавление блокировок или даже добавление распределенных блокировок.Подробности см. в главе о разбивке кеша.
Поэтому, когда дело доходит до распределенных блокировок чтения-записи, их реализация не менее сложна. Если вы уверены, что хотите использовать его, я рекомендую использовать тот, который предоставлен куратором.InterProcessReadWriteLockили предоставлено RedissonRReadWriteLock. Обсуждение распределенных блокировок чтения-записи выходит за рамки этой статьи и здесь обсуждаться не будет.
Здесь я только излагаю свои личные мысли, у других студентов могут быть свои решения, но я считаю, что независимо от того, какое из них, для достижения сильной согласованности производительность системы должна платить свою цену и может даже превосходить производительность. повышение, которое вы получите, внедрив кэширование.
Суммировать
На мой взгляд, так называемый архитектурный дизайн часто заключается в выборе наиболее подходящего для текущей сцены среди множества компромиссов. На самом деле, когда в схеме используется кеш, это часто означает, что мы отказываемся от строгой согласованности данных, но это также означает, что наша система может получить некоторые улучшения в производительности. Большое внимание уделяется тому, как использовать кеш, например, разумной установке времени истечения срока действия, как решить или избежать проблемы проникновения в кеш, поломки или даже лавины. Если будет возможность в будущем, я постепенно объясню всю подноготную этих проблем и как их решать более адекватно.
об авторе
Лу Ядун, технический эксперт интернет-компании в области управления рисками, в основном фокусируется на высокой производительности, высоком уровне параллелизма, а также на базовых принципах и настройке промежуточного программного обеспечения.