Чтобы использовать живой Redis, сценарий Lua — это препятствие, которое нельзя обойти.

Redis
Чтобы использовать живой Redis, сценарий Lua — это препятствие, которое нельзя обойти.

предисловие

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

Опубликовать и подписаться

Теоретически функция публикации и подписки может быть реализована напрямую с помощью двустороннего связанного списка, однако функция публикации и подписки, реализованная с помощью обычного двустороннего связанного списка, имеет два ограничения:

  • Если скорость, с которой производитель создает сообщения, намного выше, чем скорость, с которой потребитель потребляет сообщения, неиспользованные сообщения в связанном списке будут накапливаться в больших количествах, что приведет к большому объему памяти.
  • Очередь сообщений, реализованная на основе связанного списка, не поддерживает рассылку сообщений «один ко многим».

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

реализация на основе каналов

Реализация на основе каналов в основном осуществляется с помощью следующих трех команд:

  • подписаться на канал-1 канал-2: Подпишитесь на один или несколько каналов.
  • отписаться от канала-1: отменить подписку на канал (нельзя отказаться от подписки в командном интерфейсе).
  • опубликовать сообщение канала-1: на каналchannel-1отправлять сообщенияmessage.

Откройте клиентскую, введите команду подпискиsubscribe music movie, указывая, что текущий клиент подписываетсяmusicа такжеmovieСообщения с двух каналов:

Затем откройте другой клиент 2 и выполните следующую команду, чтобы опубликовать сообщение:

publish movie myCountry //发布消息 myCountry 到 movie 频道
publish music love  //发布消息 love 到 music 频道
publish tv myHome  //发布消息 myHome 到 tv 频道

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

В это время, когда мы вернемся к предыдущему клиенту, мы обнаружим, что клиент получил сообщение.myCountryа такжеloveдва сообщения иmyHomeЭто сообщение принадлежит каналуtv, клиент не подписан, поэтому не получит:

Также имеются следующие2Эта команда может просмотреть информацию о канале, на который подписан текущий клиент:

  • каналы punsub [имя_канала] : просмотр каналов, на которые подписан текущий сервер. Возвращает все каналы без параметров, а для последующих параметров можно использовать подстановочные знаки.?или*.
  • pubsub numsub имя_канала [имя_канала]: просмотреть количество подписок на указанный канал (одновременно можно просмотреть несколько).

Анализ принципа реализации

Информация о клиентах и ​​их подписанных каналах хранится вredisServerв объектеpubsub_channelsв свойствах.

struct redisServer {
	dict *pubsub_channels;//保存了客户端及其订阅的频道信息
	//... 省略其他信息
};

pubsub_channelsатрибут представляет собой словарь, чейkeyСохраняемое значение — это имя канала,valueЭто связанный список, и связанный список хранит информацию о каждом клиенте.id, на следующем рисунке схематически представлена ​​структура хранилища на основе подписки на канал:

  • подписка При подписке сначала проверит, существует ли канал в словаре: если его нет, то нужно создать словарь для текущего канала, а связный список создать какvalueи текущий клиентidПоместите его в связанный список; если он существует, прямо поместите текущий клиентidПоместите его в конец списка.
  • отписаться При отписке клиент должен бытьidУдалить из соответствующего связанного списка.Если после удаления связанный список пуст, необходимо одновременно удалить канал из словаря.
  • отправлять сообщения будет идти первым при отправке сообщенияpubsub_channelsПоиск ключей в словаре.Если найден ключ, который может быть сопоставлен, будет найден соответствующий связанный список, и сообщение будет пройдено.

реализация на основе шаблонов

Публикация и подписка на основе схемы реализуются в основном с помощью следующих трех команд:

  • psubscribe pattern-1 pattern-2: Подпишитесь на один или несколько шаблонов, шаблоны могут передаваться подстановочными знаками?а также*Представлять.
  • punsubscribe pattern-1 pattern-1: отменить подписку на шаблон (на основе командных операций, нельзя отменить подписку на интерфейсе)
  • опубликовать сообщение канала-1: на каналchannel-1отправлять сообщенияmessage. Это то же самое, что и приведенная выше команда на основе канала.

Откройте клиентскую, введите команду подпискиpsubscribe m*, указывая, что текущий клиент подписывается на всеmКаналы, которые начинаются с:

Затем откройте другой клиент 2 и выполните команду публикации сообщения:

publish movie myCountry //发布消息 myCountry 到 movie 频道
publish music love  //发布消息 love 到 music 频道
publish tv myHome  //发布消息 myHome 到 tv 频道

Возврат после публикации первых двух каналов1означает, что в настоящее время1Клиент подписался на канал (вышеупомянутый клиент на основе подписки на канал автоматически отпишется после его закрытия), и сообщение было отправлено этому клиенту.

В это время, когда мы вернемся к предыдущему клиенту, мы обнаружим, что клиент получилmyCountryа такжеloveдва сообщения, так как оба канала начинаются сmначало, иmyHomeЭто сообщение принадлежит каналуtv, не соmВ начале клиент не подписан, поэтому не получит:

Аналогично, подписки на основе схемы также предоставляют команду запроса:

  • pubsub numpat: запрос количества подписанных схем на текущем сервере.

Анализ принципа реализации

Информация о схеме для клиентов и их подписок хранится вredisServerв объектеpubsub_patternsв свойствах.

struct redisServer {
	list pubsub_patterns;//保存了客户端及其订阅的模式信息
	//...省略其他信息
};

pubsub_patternsСвойство — это список, структура которого внутри списка (исходныйserer.hвнутри) определяется следующим образом:

typedef struct pubsubPattern {
    client *client;//订阅模式的客户端
    robj *pattern;//被订阅的模式
} pubsubPattern;

  • подписка создать новыйpubsubPatternструктура данных добавлена ​​в связанный списокpubsub_patternsконец.
  • отписаться Отписаться от списка отписавшихся на данный момент клиентовpubsubPattern из связанного спискаpubsub_patternsудалено в
  • отправлять сообщения На этом этапе необходимо пройти весь связанный список, чтобы найти соответствующий шаблон. Причина, по которой связанный список используется на основе сценария шаблона, заключается в том, что шаблон поддерживает подстановочные знаки, поэтому его невозможно реализовать напрямую со словарем.

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

Lua-скрипт

Redisот2.6Версия начинает поддерживатьLuaсценарий, для поддержкиLuaсценарий,Redisвстроенный в серверLuaокружающая обстановка.

использоватьLuaСамым большим преимуществом сценариев является то, чтоRedisВесь скрипт будет выполняться как единое целое и не будет прерываться другими запросами, сохраняя атомарность и снижая нагрузку на сеть.

Вызов Lua-скрипта

LuaСинтаксис выполнения скрипта следующий:

eval lua-script numkeys key [key ...] arg [arg ...]
  • оценка: выполнитьLuaкоманда скрипта.
  • луа-скрипт:Luaсодержание скрипта.
  • numkeys: указывает, чтоLuaСколько нужно в сценарииkey, если не используется, напишите0.
  • клавиша [клавиша...]: будетkeyпередается в качестве аргументов, чтобыLuaсценарий,numkeysда0можно опустить.
  • аргумент:LuaПараметры, используемые в скрипте, можно опустить, если их нет.

Далее мы выполняем простой без каких-либо аргументовLuaКоманда скрипта:

eval "return 'Hello Redis'" 0

Выполнение команд Redis в сценариях Lua

существуетLuaвыполнить в скриптеRedisДля команды требуется следующий синтаксис:

redis.call(command, key [key ...] argv [argv…])
  • команда:Redisв команде, напримерset,getЖдать.
  • ключ: операцияRedisсерединаkeyзначение, эквивалентное формальному параметру при вызове метода.
  • param: представляет параметр, эквивалентный фактическому параметру при вызове метода.

Предположим, мы хотим выполнить командуset name lonely_wolf, затем используйтеLuaСкрипт должен выполняться так:

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name lonely_wolf

нужно знать, это:KEYSа такжеARGVДолжен быть в верхнем регистре, нижний индекс параметра начинается с1Начинать. в приведенной выше команде1Указывает, что текущий необходимо пройти1индивидуальныйkey

Сводка Lua-скрипта

Иногда, если мы выполняемLuaЕсли скрипт очень длинный, вызовите его прямо такLuaСкрипт очень неудобный, поэтомуRedisкоторый дает командуscript loadвручную дать каждомуLuaСкрипт генерирует сводку,Причина, по которой мы говорим здесь manual, заключается в том, что даже если мы не используем эту команду, каждый раз, когда мы вызываемLuaПри написании сценарияRedisтакже для каждогоLuaСкрипт формирует сводку.

Другие связанные команды:

  • script exists 摘要: определяет, существует ли дайджест.0значит не существует,1Указывает на существование.
  • script flush: очистить всеLuaКэширование скрипта.

Далее, давайте проверим это, последовательно выполнив следующие команды:

script load "return redis.call('set',KEYS[1],ARGV[1])"  //给当前 Lua脚本生成摘要,这时候会返回一个摘要
evalsha "c686f316aaf1eb01d5a4de1b0b63cd233010e63d" 1 address china  //相当于执行命令 set address china
get address //获取 adress,确认上面的脚本是否执行成功
script exists "c686f316aaf1eb01d5a4de1b0b63cd233010e63d"  //判断当前摘要的 Lua脚本是否存在
script flush //清除所有 Lua脚本缓存
script exists "c686f316aaf1eb01d5a4de1b0b63cd233010e63d"  //清除之后这里就不存在了

После выполнения получаются следующие результаты:

Файл Lua-скрипта

когда нашLuaКогда сценарий очень длинный, писать сценарий непосредственно в командном окне не интуитивно понятно, и сложно найти проблемы с синтаксисом, поэтомуRedisЭто также позволяет нам сначала напрямую записать скрипт в файл, а затем напрямую вызвать файл. Например, мы создаем новыйtest.luaсценарий:

redis.call('set',KEYS[1],ARGV[1])
return redis.call('get',KEYS[1])

После загрузки файла в указанный каталог выполните следующую команду:

redis-cli --eval test.lua 1 age , 18 //注意 key 和 arg 参数之间要以逗号隔开,且逗号两边的空格不能省略

Затем вы можете вернуться в обычном режиме18:

Исключение скрипта

мы знаем,Redisинструкции являются однопоточными, и теперь используйтеLuaсценарий, мы можем пройтиLuaскрипт для реализации некоторой бизнес-логики, то еслиLuaВремя выполнения скрипта истекает или попадает в бесконечный цикл, в это время другие инструкции будут заблокированы, что приведет кRedisнельзя использовать нормально. Как мы должны относиться к этому сейчас?

Время ожидания сценария истекло

чтобы решитьLuaПроблема таймаута скрипта,RedisПредоставляет параметр тайм-аутаlua-time-limitконтролироватьLuaВремя ожидания выполнения скрипта в миллисекундах, по умолчанию5000(который5секунд), по истечении периода тайм-аутаLuaавтоматически сломает сценарий.

Скрипт застрял в бесконечном цикле

Если скрипт попадет в бесконечный цикл, таймаут в это время не сработает, смоделируем это: Сначала откройте клиент 1 и выполните бесконечный циклluaсценарий:

eval 'while(true) do end' 0

Затем откройте еще один клиент 2 и выполните любую команду:

get name

вернусьbusy, указывающее, что команда не может быть выполнена в данный момент:

намекатьbusyПосле этого в то же времяRedisРешение также дано, мы можем только использоватьscript killилиshutdown nosaveКоманда, для чего эти две команды?

  • Убить скрипт: когда скрипт находится в бесконечном цикле, выполнение этой команды может принудительноLuaВыполнение скрипта прерывается. Ограничение этого скрипта в том, что он застрял в бесконечном цикле.LuaСценарий не должен успешно выполнить команду.
  • выключение nosave: принудительный выходLuaскрипт, может решитьscript killОграничения команд.

Далее давайте выполним команду на втором клиентеscript kill, а затем посмотрите на эффект зависания клиента 1 в бесконечном цикле:

Видно, что клиентLuaСкрипт завершился.По следующим подсказкам можно узнать, что это из-за выполнения.script killвызвано командойLuaПрерывание сценария.

Теперь мы повторно используем клиент для выполнения следующегоLuaСкрипт, разница между этим скриптом и вышеприведенным скриптом в том, что выполнение здесь проходит успешно.RedisБесконечный цикл запускается только после команды:

eval "redis.call('set','age','28') while true do end" 0

В это время переходим ко второму клиенту для выполненияscript killкоманда, найденная неспособной прерватьLuaВышел сценарий:

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

В этом случае только путем выполненияshutdown nosaveкоманда принудительно прерватьLuaСкрипт, вот потому что добавилnosaveне сработает послеRedisпостоянство, поэтому при перезапускеRedisПосле обслуживания можно гарантировать согласованность данных.На следующем рисунке показано выполнениеshutdown nosaveОтрисовка клиента 1 после команды:

Почему команда скрипта kill может быть выполнена

RedisВыполнение команды однопоточное, так почемуLuaПосле того, как сценарий находится в бесконечном цикле, другие клиенты все еще могут его выполнять.script killА команды?

Это потому чтоLuaМеханизм сценариев предоставляет функцию ловушки, которая позволяет запускать код ловушки, когда внутренняя виртуальная машина выполняет инструкции, поэтомуRedisИменно этот принцип используется для реализацииLuaХук устанавливается перед скриптом, т.е.script killКоманды выполняются через хуки-функции.

Суммировать

Эта статья в основном знакомитRedisФункциональность публикации-подписки в иLuaиспользование сценария, использованиеLuaСкрипты могут позволить выполнять несколько команд атомарно, уменьшая нагрузку на сеть, но в то же время уделяя вниманиеLuaПроблема с бесконечным циклом, вызванная скриптом.