🏆 Принцип кэширования и автоматическое управление кэшем микросервисов |

Go

Говорить о технологиях в отрыве от бизнеса — это хулиганство. - Кевин Ван

Зачем нужно кэширование?

Начнем со старомодного вопроса: как работают наши программы?

  1. программа хранится вdiskсередина
  2. программа работает наRAMв, что мы называемmain memory
  3. Логика расчета программы находится вCPUвыполнить в

Давайте рассмотрим самый простой пример:a = a + 1

  1. load x:
  2. x0 = x0 + 1
  3. load x0 -> RAM

Есть 3 носителя, упомянутые выше. Все мы знаем, что скорость чтения и записи трех типов обратно пропорциональна стоимости, поэтому нам нужно ввестисредний слой. Этот средний уровень требует высокой скорости доступа, но его стоимость приемлема. Так,Cacheбыть представленным

В компьютерных системах есть два кэша по умолчанию:

  • Кэш последнего уровня в ЦП, т.е.LLC. Кэшировать данные в памяти
  • Кэш страниц в памяти, т.е.page cache. Кэшировать данные на диск

Стратегия чтения и записи кэша

представлятьCacheПосле этого переходим к просмотру того, что происходит с операционным кешем. Поскольку есть разница в скорости доступа «и разница очень большая», при работе с данными задержки или программные сбои приведут к несоответствию между кешем и реальными данными уровня хранения.

мы используем стандартCache+DBДавайте рассмотрим классические стратегии чтения и письма и сценарии их применения.

Cache Aside

Давайте сначала рассмотрим один из простейших бизнес-сценариев, например таблицу пользователей:userId:用户id, phone:用户电话token,avtoar:用户头像url, в кеше используемphoneСохранить аватар пользователя как ключ. Что делать, если пользователь изменяет URL-адрес аватара?

  1. возобновитьDBданные, обновлениеCacheданные
  2. возобновитьDBданные, а затем удалитьCacheданные

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

Так что лучшим решением является2:

  • Не обновлять кеш при обновлении данных, а удалять кеш напрямую
  • Последующие запросы обнаруживают, что кэш отсутствует, и возвращаются к запросу.DB, и поставить результатload cache

Эта стратегия является наиболее распространенной стратегией, которую мы используем для кэширования:Cache Aside. Эти данные политики основаны на данных в базе данных.Данные в кэше загружаются по требованию и делятся на политики чтения и политики записи.

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

  1. Обновляйте кеш одновременно с обновлением данных, но добавляйте его перед обновлением кешаРаспределенная блокировка. Таким образом, с кэшем одновременно работает только один поток, что решает проблему параллелизма. При этом последний кэш считывается в последующих запросах на чтение, что решает проблему несогласованности.
  2. Обновляйте кеш одновременно с обновлением данных, но давайте кешу короче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

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

  1. Разделенный кеш: можно разделить消息id-> по消息idСообщения запросов и кэш-вставки消息listсередина.
  2. Срок действия сообщения: установите срок действия сообщения, чтобы его кэширование не занимало слишком много времени.

Вот лучшие практики, когда дело доходит до кэширования:

  • Не разрешать кэширование с неистекшим сроком действия «особенно важно»
  • Распределенный кеш, легко масштабируемый
  • Генерируется автоматически, со встроенной статистикой

Суммировать

Эта статья начинается с введения в кеш, общих стратегий чтения и записи кеша, того, как обеспечить возможную согласованность данных, как инкапсулировать полезный уровень операций с кешем, а также показывает ситуацию и мониторинг онлайн-кэша. Все эти детали кэширования, упомянутые выше, могут быть отнесены кgo-zeroреализация исходного кода, см.go-zeroисходный кодcore/stores.

адрес проекта

GitHub.com/them-specialty/go…

Добро пожаловать в Go-Zero иstarПоощряйте нас! 👏🏻

🏆 Технический спецвыпуск 8 | Говоря о волшебном использовании и проблемах кеша