Углубленный анализ принципа работы скрипта Redis Lua

Redis задняя часть сервер Lua
Углубленный анализ принципа работы скрипта Redis Lua

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

Например, в разделе распределенной блокировки мы упомянули псевдоинструкцию del_if_equals, которая может комбинировать совпадающие ключи и удалять ключи для атомарного выполнения, Redis изначально не предоставляет такую ​​инструкцию, это можно сделать с помощью lua-скриптов.

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Как выполнить приведенный выше скрипт? Использование инструкции EVAL

127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> eval 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end' 1 foo bar
(integer) 1
127.0.0.1:6379> eval 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end' 1 foo bar
(integer) 0

Первым параметром инструкции EVAL является строка содержимого сценария.В приведенном выше примере мы сжимаем сценарий lua в одну строку и заключаем его в одинарные кавычки, чтобы облегчить выполнение командной строки. Затем количество ключей и каждая ключевая строка, и, наконец, ряд дополнительных строк параметров. Количество дополнительных параметров не обязательно должно совпадать с ключом, и дополнительных параметров может вообще не быть.

EVAL SCRIPT KEY_NUM KEY1 KEY2 ... KEYN ARG1 ARG2 ....

В приведенном выше примере есть только 1 ключ, это foo, за которым следует bar в качестве единственного дополнительного параметра. В lua-скриптах индексы массива начинаются с 1, поэтому первый ключ можно получить через KEYS[1], а первый дополнительный параметр можно получить через ARGV[1]. Функция redis.call позволяет нам вызывать нативные инструкции Redis, Приведенный выше код вызывает инструкцию get и инструкцию del соответственно. Результат, возвращенный return, будет возвращен клиенту.

SCRIPT LOAD и инструкции EVALSHA

В приведенном выше примере содержимое скрипта очень короткое. Если содержимое сценария очень длинное и клиент должен выполнять его часто, то он неизбежно будет тратить сетевой трафик каждый раз, когда необходимо передать длинное содержимое сценария. Поэтому Redis также предоставляет инструкции SCRIPT LOAD и EVALSHA для решения этой проблемы.

Инструкция SCRIPT LOAD используется для передачи сценария lua, предоставленного клиентом, на сервер без его выполнения, но он получает уникальный идентификатор сценария.Этот уникальный идентификатор используется для уникальной идентификации этого сценария lua, кэшированного сервером. используется Redis с использованием алгоритма sha1. Очень длинная строка, полученная в результате перемешивания содержимого скрипта. С помощью этого уникального идентификатора клиент может многократно выполнять сценарий с помощью команды EVALSHA. Мы знаем, что в Redis есть инструкция incrby для завершения операции автоматического увеличения целых чисел, но такой инструкции для самоумножения нет.

incrby key value  ==> $key = $key + value
mulby key value ==> $key = $key * value

Ниже мы используем инструкции SCRIPT LOAD и EVALSHA для завершения операции самоумножения.

local curVal = redis.call("get", KEYS[1])
if curVal == false then
  curVal = 0
else
  curVal = tonumber(curVal)
end
curVal = curVal * tonumber(ARGV[1])
redis.call("set", KEYS[1], curVal)
return curVal

Во-первых, сделайте приведенный выше сценарий в одну строку и разделите операторы точкой с запятой.

local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal

загрузить скрипт

127.0.0.1:6379> script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal'
"be4f93d8a5379e5e5b768a74e77c8a4eb0434441"

Командная строка выводит длинную строку, которая является уникальным идентификатором скрипта, далее мы используем этот уникальный идентификатор для выполнения команды

127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 notexistskey 5
(integer) 0
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 notexistskey 5
(integer) 0
127.0.0.1:6379> set foo 1
OK
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 foo 5
(integer) 5
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 foo 5
(integer) 25

обработка ошибок

Приведенный выше параметр скрипта требует, чтобы переданный дополнительный параметр был целым числом. Что делать, если целое число не передается?

127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 foo bar
(error) ERR Error running script (call to f_be4f93d8a5379e5e5b768a74e77c8a4eb0434441): @user_script:1: user_script:1: attempt to perform arithmetic on a nil value

Вы можете видеть, что клиент выводит общее сообщение об ошибке, возвращаемое сервером. Обратите внимание, что это динамическое исключение. Redis защитит основной поток от сбоя сервера из-за ошибок скрипта. Оператор try catch завернут. Произошла ошибка во время выполнения сценария lua. Как и транзакция redis, инструкции, которые были выполнены через функцию redis.call, оказывают необратимое влияние на состояние сервера. Будьте осторожны при написании кода lua, чтобы не игнорировать условие оценки достижения приводит к тому, что скрипт не может быть полностью выполнен.

Если читатель немного знаком с языком lua и знает, что lua изначально не предоставляет оператора try catch, то как реализуется упомянутый выше оператор переноса исключений? Альтернативой lua является вызов встроенной функции pcall(f). pcall означает защищенный вызов, он заставит функцию f работать в защищенном режиме, если f имеет ошибку, вызов pcall вернет false и сообщение об ошибке. Обычный вызов call(f) просто генерирует исключение вверх, когда обнаруживает ошибку. В исходном коде Redis вы можете видеть, что выполнение lua-скрипта заключено в вызове функции pcall.

// 编译期
int luaCreateFunction(client *c, lua_State *lua, char *funcname, robj *body) {
  ...
  if (lua_pcall(lua,0,0,0)) {
    addReplyErrorFormat(c,"Error running script (new function): %s\n",
            lua_tostring(lua,-1));
    lua_pop(lua,1);
    return C_ERR;
  }
  ...
}

// 运行期
void evalGenericCommand(client *c, int evalsha) {
  ...
  err = lua_pcall(lua,0,1,-2);
  ...
}

В дополнение к функции redis.call Redis также предоставляет функцию redis.pcall в сценариях lua. Первый генерирует исключение при обнаружении ошибки, а второй возвращает сообщение об ошибке. При его использовании обязательно обратите внимание, что функция вызова будет прерывать выполнение скрипта при возникновении ошибки.Чтобы обеспечить атомарность скрипта, ее следует использовать с осторожностью.

ошибка доставки

Вызов функции redis.call выдаст ошибку Какую информацию вернет скрипт при обнаружении такой ошибки? Давайте посмотрим на другой пример

127.0.0.1:6379> hset foo x 1 y 2
(integer) 2
127.0.0.1:6379> eval 'return redis.call("incr", "foo")' 0
(error) ERR Error running script (call to f_8727c9c34a61783916ca488b366c475cb3a446cc): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value

Клиент по-прежнему выводит общее сообщение об ошибке вместо сообщения об ошибке WRONGTYPE, которое должен был вернуть вызов incr. Redis внутренне генерирует исключение, когда обнаруживает ошибку при обработке redis.call.Когда вызов pcall, невидимый для периферийного пользователя, перехватывает исключение скрипта, он отвечает клиенту общим сообщением об ошибке. Результат был бы другим, если бы мы изменили приведенный выше вызов на pcall, который может пропустить конкретную ошибку, возвращаемую внутренней инструкцией.

127.0.0.1:6379> eval 'return redis.pcall("incr", "foo")' 0
(error) WRONGTYPE Operation against a key holding the wrong kind of value

Что делать, если скрипт находится в бесконечном цикле?

Выполнение инструкций Redis — это один поток, и этот единственный поток также выполняет скрипт lua от клиента. Если в lua-скрипте есть бесконечный цикл, будет ли завершен Redis? Чтобы решить эту проблему, Redis предоставляет команду script kill для динамического уничтожения lua-скрипта, время выполнения которого превышено. Однако выполнение уничтожения сценария имеет важную предпосылку, то есть выполняемый в данный момент сценарий не изменяет внутреннее состояние данных Redis, поскольку Redis не позволяет уничтожению сценария разрушать атомарность выполнения сценария. Например, если внутри скрипта используется redis.call("set", key, value) для изменения внутренних данных, сервер вернет ошибку при выполнении скрипта kill. Давайте попробуем следующую команду kill скрипта.

127.0.0.1:6379> eval 'while(true) do print("hello") end' 0

После выполнения команды eval ясно видно, что Redis зависает и ответа нет.Если вы посмотрите на журнал сервера Redis, вы увидите, что журнал лихорадочно выводит строку приветствия. В это время вы должны перезапустить Redis-Cli, чтобы выполнить команду kill скрипта.

127.0.0.1:6379> script kill
OK
(2.58s)

Посмотрите на вывод инструкции eval.

127.0.0.1:6379> eval 'while(true) do print("hello") end' 0
(error) ERR Error running script (call to f_d395649372f578b1a0d3a1dc1b2389717cadf403): @user_script:1: Script killed by user with SCRIPT KILL...
(6.99s)

Внимательные студенты заметят здесь два сомнения: во-первых, почему выполнение команды script kill заняло 2,58 секунды, а во-вторых, что скрипт завис, где Redis берет время, чтобы принять команду kill script. Если вы попытаетесь выполнить redis-cli во втором окне для самостоятельного подключения к серверу, вы также найдете третье сомнение, установка соединения redis-cli немного медленная, около 1 секунды.

Принцип Script Kill

Теперь я начну раскрывать принцип kill.Скриптовый движок lua ​​слишком мощный.Он предоставляет множество функций ловушек, которые позволяют запускать код ловушек, когда внутренняя виртуальная машина выполняет инструкции. Например, каждый раз, когда выполняются N инструкций, выполняется функция ловушки, и Redis использует эту функцию ловушки.

void evalGenericCommand(client *c, int evalsha) {
  ...
  // lua引擎每执行10w条指令,执行一次钩子函数 luaMaskCountHook
  lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000);
  ...
}

Redis возьмет время из функции ловушки для обработки запроса клиента и обработает запрос только после того, как будет найдено время ожидания выполнения сценария lua.Тайм-аут по умолчанию составляет 5 секунд. Таким образом, три сомнения, поднятые выше, исчезли.

мыслительные вопросы

В разделе очереди задержки мы используем две инструкции, zrangebyscore и zdel, чтобы конкурировать за задачи в очереди задержки.Возвращаемое значение zdel используется для определения того, какой клиент захватил задачу, что означает, что те клиенты, которые не захватили задачу .Будет такое ощущение - дошедшее до рта мясо (задание) окончательно отнимают другие, что будет очень неудобно. Если вы можете использовать lua-скрипты для реализации логики состязания, атомарное выполнение инструкций zrangebyscore и zdel не будет иметь этой проблемы, читатель может попробовать.

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

Прочтите более подробные технические статьи, отсканируйте приведенный выше QR-код и подпишитесь на общедоступную учетную запись WeChat «Code Cave».