Говорить о технологиях в отрыве от бизнеса — это хулиганство. - Кевин Ван
Зачем нужно кэширование?
Начнем со старомодного вопроса: как работают наши программы?
- программа хранится в
disk
середина - программа работает на
RAM
в, что мы называемmain memory
- Логика расчета программы находится в
CPU
выполнить в
Давайте рассмотрим самый простой пример:a = a + 1
load x:
x0 = x0 + 1
load x0 -> RAM
Есть 3 носителя, упомянутые выше. Все мы знаем, что скорость чтения и записи трех типов обратно пропорциональна стоимости, поэтому нам нужно ввестисредний слой. Этот средний уровень требует высокой скорости доступа, но его стоимость приемлема. Так,Cache
быть представленным
В компьютерных системах есть два кэша по умолчанию:
- Кэш последнего уровня в ЦП, т.е.
LLC
. Кэшировать данные в памяти - Кэш страниц в памяти, т.е.
page cache
. Кэшировать данные на диск
Стратегия чтения и записи кэша
представлятьCache
После этого переходим к просмотру того, что происходит с операционным кешем. Поскольку есть разница в скорости доступа «и разница очень большая», при работе с данными задержки или программные сбои приведут к несоответствию между кешем и реальными данными уровня хранения.
мы используем стандартCache+DB
Давайте рассмотрим классические стратегии чтения и письма и сценарии их применения.
Cache Aside
Давайте сначала рассмотрим один из простейших бизнес-сценариев, например таблицу пользователей:userId:用户id, phone:用户电话token,avtoar:用户头像url
, в кеше используемphone
Сохранить аватар пользователя как ключ. Что делать, если пользователь изменяет URL-адрес аватара?
- возобновить
DB
данные, обновлениеCache
данные - возобновить
DB
данные, а затем удалитьCache
данные
первыйизменить базу данныха такжеизменить кеш— это две независимые операции, и мы не осуществляем параллельный контроль над операциями. Затем, когда два потока обновляют их одновременно, данные будут несогласованными из-за разного порядка записи.
Так что лучшим решением является2
:
- Не обновлять кеш при обновлении данных, а удалять кеш напрямую
- Последующие запросы обнаруживают, что кэш отсутствует, и возвращаются к запросу.
DB
, и поставить результатload cache
Эта стратегия является наиболее распространенной стратегией, которую мы используем для кэширования:Cache Aside
. Эти данные политики основаны на данных в базе данных.Данные в кэше загружаются по требованию и делятся на политики чтения и политики записи.
Но возникает и видимая проблема: частые операции чтения и записи приведут кCache
При повторной замене частота попаданий в кэш снижается. Конечно, если в бизнесе есть сигнал тревоги для отслеживания частоты попаданий, можно рассмотреть следующие решения:
- Обновляйте кеш одновременно с обновлением данных, но добавляйте его перед обновлением кешаРаспределенная блокировка. Таким образом, с кэшем одновременно работает только один поток, что решает проблему параллелизма. При этом последний кэш считывается в последующих запросах на чтение, что решает проблему несогласованности.
- Обновляйте кеш одновременно с обновлением данных, но давайте кешу короче
TTL
.
Конечно, помимо этой стратегии, в компьютерной системе существует еще несколько классических стратегий кэширования, и у них тоже есть свои применимые сценарии использования.
Write Through
Сначала запросите, попадает ли ключ записи данных в кеш, если в -> обновить кеш, и компонент кеша синхронизирует данные с БД; если он не существует, он будет запущенWrite Miss
.
и вообщеWrite Miss
Есть два способа:
-
Write Allocate
: прямое выделение при записиCache line
-
No-write allocate
: Не писать в кеш при записи, писать напрямую в БД, возвращать
существуетWrite Through
средний, как правилоNo-write allocate
. Потому что на самом деле, несмотря ни на что, окончательные данные будут сохранены в БД, что экономит один шаг записи в кеш и повышает производительность записи. пока кэшRead Through
записать кеш.
Основные принципы этой стратегии:Пользователь имеет дело только с кешем, а компонент кеша взаимодействует с БД для записи или чтения данных.. Эту стратегию можно рассматривать в некоторых компонентах локального кэша процессов.
Write Back
Я полагаю, вы также можете увидеть недостатки вышеописанной схемы: кеш и база данных синхронизируются при записи данных, но мы знаем, что скорость двух носителей отличается на несколько порядков, что сильно влияет на запись. представление. Так мы обновляем базу данных асинхронно?
Write back
То есть при записи данных обновлять толькоCache Line
соответствующие данные и пометить строку какDirty
. При чтении данных или когда «стратегия замены кеша» заменяется, когда кеш заполнен,Dirty
Запись в хранилище.
Следует отметить, что: вWrite Miss
случай, взятиеWrite Allocate
, то есть запись в хранилище одновременно с записью в кеш, так что нам нужно только обновлять кеш для последующих запросов на запись.
async purge
Такие концепции действительно существуют в компьютерной системе.Mysql
Суть очистки грязных страниц состоит в том, чтобы максимально предотвратить случайную запись и унифицировать время записи на диск.
Redis
Redis
Это независимое системное программное обеспечение, а бизнес-программы, которые мы пишем, — это два программного обеспечения. когда мы развертываемRedis
После создания экземпляра он просто пассивно ожидает, пока клиент отправит запрос, прежде чем обработать его. Таким образом, если приложение хочет использоватьRedis
Кэш, нам нужно добавить в программу соответствующий код операции с кешем. Так мы тоже ставимRedis
называетсяобход кеша, то есть: операции чтения кеша, чтения базы данных и обновления кеша должны выполняться в приложении.
как в кэшеRedis
, также необходимо столкнуться с общими проблемами:
- Емкость кэша все-таки ограничена
- Шок параллельных запросов вверх по течению
- Согласованность данных кэша и внутреннего хранилища
стратегия замены
Вообще говоря, кеш выберет прямое удаление или запись обратно в базу данных для удаления выбранных данных в зависимости от того, являются ли они чистыми данными или грязными данными. Однако в Redis устаревшие данные будут удалены независимо от того, чистые они или нет, поэтому при использовании кеша Redis мы должны обратить особое внимание: когда данные изменяются, чтобы стать грязными данными, данные необходимо изменить в базу данных.
Таким образом, независимо от стратегии замены, грязные данные могут быть потеряны во время загрузки и выгрузки. Затем мы должны удалить кеш при генерации грязных данных, вместо обновления кеша все данные должны быть основаны на базе данных. Также хорошо известно, что запись в кэш должна выполняться запросами на чтение; запросы на запись обеспечивают максимально возможную согласованность данных.
Что касается стратегий замещения, то в Интернете уже есть много статей, в которых суммируются преимущества и недостатки, поэтому я не буду их здесь повторять.
ShardCalls
В параллельном сценарии может быть несколько потоков (сопрограмм), одновременно запрашивающих один и тот же ресурс.Если каждый запрос должен проходить через процесс запроса ресурсов, это не только неэффективно, но и вызовет параллельную нагрузку на службы ресурсов.
go-zero
серединаShardCalls
Он может выполнять несколько запросов одновременно, нужно только инициировать вызов, чтобы получить результат, а другие запросы «откиньтесь на спинку кресла и наслаждайтесь». Такой дизайн эффективно снижает давление параллелизма служб ресурсов и может эффективно предотвращать сбой кеша.
Чтобы предотвратить внезапную высокую нагрузку на нижестоящие сервисы, вызванную внезапным всплеском интерфейсных запросов, вы можете обернуть это в свою функцию:
fn = func() (interface{}, error) {
// 业务查询
}
data, err = g.Do(apiKey, fn)
// 就获得到data,之后的方法或者逻辑就可以使用这个data
На самом деле принцип очень прост:
func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
// done: false,才会去执行下面的业务逻辑;为 true,直接返回之前获取的data
c, done := g.createCall(key)
if done {
return c.val, c.err
}
// 执行调用者传入的业务逻辑
g.makeCall(c, key, fn)
return c.val, c.err
}
func (g *sharedGroup) createCall(key string) (c *call, done bool) {
// 只让一个请求进来进行操作
g.lock.Lock()
// 如果携带标示一系列请求的key在 calls 这个map中已经存在,
// 则解锁并同时等待之前请求获取数据,返回
if c, ok := g.calls[key]; ok {
g.lock.Unlock()
c.wg.Wait()
return c, true
}
// 说明本次请求是首次请求
c = new(call)
c.wg.Add(1)
// 标注请求,因为持有锁,不用担心并发问题
g.calls[key] = c
g.lock.Unlock()
return c, false
}
этоmap+lock
хранить и ограничивать операции запросов, а такжеgroupcacheсерединаsingleflight
Аналогично, все они являются острыми инструментами для предотвращения поломки кеша.
Адрес источника:sharedcalls.go
Кэшировать и хранить порядок обновления
Это обычная запутанная проблема в разработке:Должен ли я сначала удалить кеш или сначала обновить хранилище?
Случай 1: сначала удалите кэш, а затем обновите хранилище;
A
Удалить кеш, сетевую задержку при обновлении хранилищаB
Запрос на чтение, поиск отсутствующего кеша, чтение хранилища -> чтение старых данных в это время
Это создает две проблемы:
-
B
прочитать старое значение -
B
В то же время запрос на чтение запишет старое значение в кеш, в результате чего последующие запросы на чтение будут считывать старое значение.
Поскольку кеш может быть старым значением, его удаление не имеет значения. Есть неэлегантное решение:После того, как запрос на запись обновил сохраненное значение,sleep()
Через короткий промежуток времени выполните еще одну операцию удаления кеша..
sleep
Это делается для того, чтобы запрос на чтение был завершен, а запрос на запись мог удалить кэшированные грязные данные, вызванные запросом на чтение.Конечно, необходимо также учитывать время, отнимающее время на синхронизацию master-slave redis. Тем не менее, это все еще зависит от фактического бизнеса.
Эта схема задержит удаление на некоторое время после того, как кэшированное значение будет удалено в первый раз, и называется:отложенное двойное удаление.
Вариант 2: сначала обновите значение базы данных, а затем удалите кэшированное значение:
A
Удалите сохраненное значение, но удалите сетевую задержку кешаB
Когда делается запрос на чтение, кеш попадает, и старое значение возвращается напрямую.
Эта ситуация мало влияет на бизнес, и большинство компонентов кэша используют этот порядок обновления, чтобы соответствовать возможным требованиям согласованности.
Случай 3: Когда новый пользователь регистрируется, он напрямую записывается в базу данных, и в кеше ничего не должно быть. Если в это время программа считывает ведомую библиотеку, пользовательские данные не могут быть прочитаны из-за задержки ведущий-ведомый.
Эта ситуация требуетInsert
Это своего рода операция: вставка новых данных в базу и запись в кеш одновременно. Это позволяет последующим запросам на чтение напрямую читать кэш, а поскольку это только что вставленные новые данные, маловероятно, что они будут изменены в течение определенного периода времени.
Вышеупомянутые решения имеют более или менее потенциальные проблемы в сложных ситуациях и должны быть изменены в зависимости от бизнеса..
Как разработать полезный уровень операций с кэшем?
Сказав так много выше, возвращаясь к нашей перспективе развития, если нам нужно рассмотреть так много вопросов, это, очевидно, слишком хлопотно. Итак, как инкапсулировать эти стратегии кэширования и замены, чтобы упростить процесс разработки?
Несколько моментов для ясности:
- Отделите бизнес-логику от операций с кэшем и оставьте ее для разработки этой логики записи.
- Операции кэширования должны учитывать такие проблемы, как скачки трафика и политики кэширования. . .
Поговорим о чтении и письмеgo-zero
как инкапсулировать.
QueryRow
// res: query result
// cacheKey: redis key
err := m.QueryRow(&res, cacheKey, func(conn sqlx.SqlConn, v interface{}) error {
querySQL := `select * from your_table where campus_id = ? and student_id = ?`
return conn.QueryRow(v, querySQL, campusId, studentId)
})
Мы разработаем бизнес-логику запроса с помощьюfunc(conn sqlx.SqlConn, v interface{})
упаковка. Пользователям не нужно учитывать записи в кэш, им нужно передать только те, которые необходимо записать.cacheKey
. Заодно поместите результаты запросаres
вернуть.
Как операция кэширования инкапсулируется внутри? Заглянем внутрь функции:
func (c cacheNode) QueryRow(v interface{}, key string, query func(conn sqlx.SqlConn, v interface{}) error) error {
cacheVal := func(v interface{}) error {
return c.SetCache(key, v)
}
// 1. cache hit -> return
// 2. cache miss -> err
if err := c.doGetCache(key, v); err != nil {
// 2.1 err defalut val {*}
if err == errPlaceholder {
return c.errNotFound
} else if err != c.errNotFound {
return err
}
// 2.2 cache miss -> query db
// 2.2.1 query db return err {NotFound} -> return err defalut val「see 2.1」
if err = query(c.db, v); err == c.errNotFound {
if err = c.setCacheWithNotFound(key); err != nil {
logx.Error(err)
}
return c.errNotFound
} else if err != nil {
c.stat.IncrementDbFails()
return err
}
// 2.3 query db success -> set val to cache
if err = cacheVal(v); err != nil {
logx.Error(err)
return err
}
}
// 1.1 cache hit -> IncrementHit
c.stat.IncrementHit()
return nil
}
Из процесса это как раз соответствует стратегии кэширования:Read Through
.
Адрес источника:cachedsql.go
Exec
Запрос на запись использует предыдущую стратегию кэширования.Cache Aside
-> Сначала запишите базу данных, затем удалите кеш.
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
execSQL := fmt.Sprintf("update your_table set %s where 1=1", m.table, AuthRows)
return conn.Exec(execSQL, data.RangeId, data.AuthContentId)
}, keys...)
func (cc CachedConn) Exec(exec ExecFn, keys ...string) (sql.Result, error) {
res, err := exec(cc.db)
if err != nil {
return nil, err
}
if err := cc.DelCache(keys...); err != nil {
return nil, err
}
return res, nil
}
а такжеQueryRow
Точно так же вызывающей стороне нужно отвечать только за бизнес-логику, а запись и удаление кеша прозрачны для вызова.
Адрес источника:cachedsql.go
онлайн кэш
Первое предложение вступительного абзаца: Отключение технологий от бизнеса — хулиганство. Все вышесказанное посвящено анализу режима кэширования, но играет ли кэш должную роль в ускорении реального бизнеса? Наиболее интуитивно понятным является частота попаданий в кеш, а как наблюдать за попаданием в кеш службы? Это предполагает мониторинг.
На следующем рисунке показана запись кэша службы в нашей онлайн-среде:
помните вышеQueryRow
Средний: попадание в кеш запросов, вызовc.stat.IncrementHit()
. один из нихstat
Он используется в качестве индикатора мониторинга для непрерывного расчета частоты попаданий и отказов.
Адрес источника:cachestat.go
В других бизнес-сценариях, например при просмотре информации на домашней странице, неизбежно большое количество запросов. Поэтому кэширование информации домашней страницы особенно важно для взаимодействия с пользователем. Но в отличие от некоторых отдельных ключей, упомянутых ранее, может быть задействовано большое количество сообщений, и в это время необходимо добавить другие типы кеша:
- Разделенный кеш: можно разделить
消息id
-> по消息id
Сообщения запросов и кэш-вставки消息list
середина. - Срок действия сообщения: установите срок действия сообщения, чтобы его кэширование не занимало слишком много времени.
Вот лучшие практики, когда дело доходит до кэширования:
- Не разрешать кэширование с неистекшим сроком действия «особенно важно»
- Распределенный кеш, легко масштабируемый
- Генерируется автоматически, со встроенной статистикой
Суммировать
Эта статья начинается с введения в кеш, общих стратегий чтения и записи кеша, того, как обеспечить возможную согласованность данных, как инкапсулировать полезный уровень операций с кешем, а также показывает ситуацию и мониторинг онлайн-кэша. Все эти детали кэширования, упомянутые выше, могут быть отнесены кgo-zero
реализация исходного кода, см.go-zero
исходный кодcore/stores
.
адрес проекта
Добро пожаловать в Go-Zero иstarПоощряйте нас! 👏🏻
🏆 Технический спецвыпуск 8 | Говоря о волшебном использовании и проблемах кеша