redis: Lua клиент для Redis для nginx-module-lua, основанный на API cosocket
Установка
Если вы еще не подписались на RPM репозиторий, зарегистрируйтесь. После этого вы можете продолжить с следующими шагами.
CentOS/RHEL 7 или Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-redis
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-redis
Чтобы использовать эту Lua библиотеку с NGINX, убедитесь, что nginx-module-lua установлен.
Этот документ описывает lua-resty-redis v0.33, выпущенную 9 июля 2025 года.
Эта Lua библиотека является клиентом Redis для модуля ngx_lua nginx:
https://github.com/openresty/lua-nginx-module/#readme
Эта Lua библиотека использует API cosocket ngx_lua, который обеспечивает 100% неблокирующее поведение.
Обратите внимание, что требуется как минимум ngx_lua 0.5.14 или OpenResty 1.2.1.14.
Синопсис
# вам не нужна следующая строка, если вы используете
# пакет OpenResty:
server {
location /test {
# необходимо указать резолвер для разрешения имени хоста
resolver 8.8.8.8;
content_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 сек
-- или подключиться к файлу сокета unix,
-- на который слушает сервер redis:
-- local ok, err = red:connect("unix:/path/to/redis.sock")
-- подключение напрямую по IP-адресу
local ok, err = red:connect("127.0.0.1", 6379)
-- или подключение по имени хоста, необходимо указать резолвер, как выше
local ok, err = red:connect("redis.openresty.com", 6379)
if not ok then
ngx.say("не удалось подключиться: ", err)
return
end
ok, err = red:set("dog", "животное")
if not ok then
ngx.say("не удалось установить dog: ", err)
return
end
ngx.say("результат установки: ", ok)
local res, err = red:get("dog")
if not res then
ngx.say("не удалось получить dog: ", err)
return
end
if res == ngx.null then
ngx.say("собака не найдена.")
return
end
ngx.say("собака: ", res)
red:init_pipeline()
red:set("cat", "Marry")
red:set("horse", "Bob")
red:get("cat")
red:get("horse")
local results, err = red:commit_pipeline()
if not results then
ngx.say("не удалось выполнить запросы в конвейере: ", err)
return
end
for i, res in ipairs(results) do
if type(res) == "table" then
if res[1] == false then
ngx.say("не удалось выполнить команду ", i, ": ", res[2])
else
-- обработать значение таблицы
end
else
-- обработать скалярное значение
end
end
-- поместить в пул соединений размером 100,
-- с максимальным временем простоя 10 секунд
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.say("не удалось установить keepalive: ", err)
return
end
-- или просто закрыть соединение сразу:
-- local ok, err = red:close()
-- if not ok then
-- ngx.say("не удалось закрыть: ", err)
-- return
-- end
}
}
}
Методы
Все команды Redis имеют свои собственные методы с тем же именем, но все в нижнем регистре.
Вы можете найти полный список команд Redis здесь:
Вам нужно ознакомиться с этой справкой по командам Redis, чтобы увидеть, какие аргументы принимает каждая команда Redis.
Аргументы команды Redis могут быть непосредственно переданы в соответствующий вызов метода. Например, команда Redis "GET" принимает один аргумент ключа, тогда вы можете просто вызвать метод "get" следующим образом:
local res, err = red:get("key")
Аналогично, команда Redis "LRANGE" принимает три аргумента, тогда вы должны вызвать метод "lrange" следующим образом:
local res, err = red:lrange("nokey", 0, 1)
Например, команды "SET", "GET", "LRANGE" и "BLPOP" соответствуют методам "set", "get", "lrange" и "blpop".
Вот еще несколько примеров:
-- HMGET myhash field1 field2 nofield
local res, err = red:hmget("myhash", "field1", "field2", "nofield")
-- HMSET myhash field1 "Hello" field2 "World"
local res, err = red:hmset("myhash", "field1", "Hello", "field2", "World")
Все эти методы команд возвращают один результат в случае успеха и nil в противном случае. В случае ошибок или сбоев также будет возвращено второе значение, которое является строкой, описывающей ошибку.
Ответ Redis "статус" возвращает значение типа строка с удаленным префиксом "+".
Ответ Redis "целое число" возвращает значение типа Lua number.
Ответ Redis "ошибка" возвращает значение false и строку, описывающую ошибку.
Ответ Redis "объем" не равен nil и возвращает значение типа Lua string. Ответ nil возвращает значение ngx.null.
Ответ Redis "мульти-объем" не равен nil и возвращает таблицу Lua, содержащую все составные значения (если таковые имеются). Если любое из составных значений является допустимым значением ошибки Redis, то оно будет двухэлементной таблицей {false, err}.
Ответ nil мульти-объем возвращает значение ngx.null.
Смотрите http://redis.io/topics/protocol для получения подробной информации о различных типах ответов Redis.
В дополнение ко всем этим методам команд Redis также предоставляются следующие методы:
new
синтаксис: red, err = redis:new()
Создает объект redis. В случае неудачи возвращает nil и строку, описывающую ошибку.
connect
синтаксис: ok, err = red:connect(host, port, options_table?)
синтаксис: ok, err = red:connect("unix:/path/to/unix.sock", options_table?)
Пытается подключиться к удаленному хосту и порту, на которых слушает сервер redis, или к локальному файлу сокета unix, на который слушит сервер redis.
Перед фактическим разрешением имени хоста и подключением к удаленному бэкенду этот метод всегда будет искать в пуле соединений совпадающие неактивные соединения, созданные предыдущими вызовами этого метода.
Необязательный аргумент options_table — это таблица Lua, содержащая следующие ключи:
-
sslЕсли установлено в true, то используется SSL для подключения к redis (по умолчанию false).
-
ssl_verifyЕсли установлено в true, то проверяется действительность SSL-сертификата сервера (по умолчанию false). Обратите внимание, что вам нужно настроить lua_ssl_trusted_certificate, чтобы указать CA (или сервер) сертификат, используемый вашим сервером redis. Вам также может потребоваться настроить lua_ssl_verify_depth соответственно.
-
server_nameУказывает имя сервера для нового TLS-расширения Server Name Indication (SNI) при подключении по SSL.
-
poolУказывает пользовательское имя для используемого пула соединений. Если опущено, имя пула соединений будет сгенерировано из строкового шаблона
<host>:<port>или<unix-socket-path>. -
pool_sizeУказывает размер пула соединений. Если опущено и не был предоставлен параметр
backlog, пул не будет создан. Если опущено, но был предоставленbacklog, пул будет создан с размером по умолчанию, равным значению директивы lua_socket_pool_size. Пул соединений удерживает доpool_sizeактивных соединений, готовых к повторному использованию последующими вызовами к connect, но обратите внимание, что нет верхнего предела для общего числа открытых соединений вне пула. Если вам нужно ограничить общее количество открытых соединений, укажите параметрbacklog. Когда пул соединений превышает свой лимит по размеру, наименее недавно использованное (keep-alive) соединение, уже находящееся в пуле, будет закрыто, чтобы освободить место для текущего соединения. Обратите внимание, что пул соединений cosocket является перпроцессным для рабочих процессов Nginx, а не для экземпляра сервера Nginx, поэтому лимит размера, указанный здесь, также применяется к каждому отдельному рабочему процессу Nginx. Также обратите внимание, что размер пула соединений не может быть изменен после его создания. Обратите внимание, что требуется как минимум ngx_lua 0.10.14 для использования этих параметров. -
backlogЕсли указан, этот модуль ограничит общее количество открытых соединений для этого пула. Не более
pool_sizeсоединений могут быть открыты для этого пула в любое время. Если пул соединений полон, последующие операции подключения будут помещены в очередь, равную значению этого параметра (очередь "backlog"). Если количество ожидающих операций подключения равноbacklog, последующие операции подключения завершатся неудачей и вернут nil плюс строку ошибки"слишком много ожидающих операций подключения". Ожидающие операции подключения будут возобновлены, как только количество соединений в пуле станет меньшеpool_size. Ожидающая операция подключения будет прервана, как только она будет находиться в очереди болееconnect_timeout, управляемого set_timeout, и вернет nil плюс строку ошибки "тайм-аут". Обратите внимание, что требуется как минимум ngx_lua 0.10.14 для использования этих параметров.
set_timeout
синтаксис: red:set_timeout(time)
Устанавливает тайм-аут (в мс) для последующих операций, включая метод connect.
Начиная с версии v0.28 этого модуля, рекомендуется использовать set_timeouts вместо этого метода.
set_timeouts
синтаксис: red:set_timeouts(connect_timeout, send_timeout, read_timeout)
Соответственно устанавливает пороговые значения тайм-аутов подключения, отправки и чтения (в мс) для последующих операций сокета. Установка пороговых значений тайм-аутов с помощью этого метода предлагает большую детализацию, чем set_timeout. Таким образом, предпочтительно использовать set_timeouts вместо set_timeout.
Этот метод был добавлен в релиз v0.28.
set_keepalive
синтаксис: ok, err = red:set_keepalive(max_idle_timeout, pool_size)
Сразу помещает текущее соединение Redis в пул соединений ngx_lua cosocket.
Вы можете указать максимальное время простоя (в мс), когда соединение находится в пуле, и максимальный размер пула для каждого рабочего процесса nginx.
В случае успеха возвращает 1. В случае ошибок возвращает nil с строкой, описывающей ошибку.
Вызывайте этот метод только в том месте, где вы бы вызвали метод close. Вызов этого метода немедленно переведет текущий объект redis в состояние closed. Любые последующие операции, кроме connect() на текущем объекте, вернут ошибку closed.
get_reused_times
синтаксис: times, err = red:get_reused_times()
Этот метод возвращает количество (успешно) повторно использованных раз для текущего соединения. В случае ошибки он возвращает nil и строку, описывающую ошибку.
Если текущее соединение не поступает из встроенного пула соединений, то этот метод всегда возвращает 0, то есть соединение никогда не использовалось повторно (пока). Если соединение поступает из пула соединений, то возвращаемое значение всегда ненулевое. Таким образом, этот метод также можно использовать для определения, поступает ли текущее соединение из пула.
close
синтаксис: ok, err = red:close()
Закрывает текущее соединение redis и возвращает статус.
В случае успеха возвращает 1. В случае ошибок возвращает nil с строкой, описывающей ошибку.
init_pipeline
синтаксис: red:init_pipeline()
синтаксис: red:init_pipeline(n)
Включает режим конвейеризации redis. Все последующие вызовы методов команд Redis будут автоматически кэшироваться и отправляться на сервер за один раз, когда вызывается метод commit_pipeline, или будут отменены вызовом метода cancel_pipeline.
Этот метод всегда выполняется успешно.
Если объект redis уже находится в режиме конвейеризации Redis, то вызов этого метода отменит существующие кэшированные запросы Redis.
Необязательный аргумент n указывает (приблизительное) количество команд, которые будут добавлены в этот конвейер, что может немного ускорить процесс.
commit_pipeline
синтаксис: results, err = red:commit_pipeline()
Выходит из режима конвейеризации, подтверждая все кэшированные запросы Redis на удаленный сервер за один раз. Все ответы на эти запросы будут автоматически собраны и возвращены как если бы это был большой мульти-объемный ответ на самом высоком уровне.
Этот метод возвращает nil и строку Lua, описывающую ошибку в случае неудачи.
cancel_pipeline
синтаксис: red:cancel_pipeline()
Выходит из режима конвейеризации, отменяя все существующие кэшированные команды Redis с момента последнего вызова метода init_pipeline.
Этот метод всегда выполняется успешно.
Если объект redis не находится в режиме конвейеризации Redis, то этот метод не выполняет никаких действий.
hmset
синтаксис: res, err = red:hmset(myhash, field1, value1, field2, value2, ...)
синтаксис: res, err = red:hmset(myhash, { field1 = value1, field2 = value2, ... })
Специальная обертка для команды Redis "hmset".
Когда есть только три аргумента (включая сам объект "red"), то последний аргумент должен быть таблицей Lua, содержащей все пары поле/значение.
array_to_hash
синтаксис: hash = red:array_to_hash(array)
Вспомогательная функция, которая преобразует массивоподобную таблицу Lua в таблицу, подобную хешу.
Этот метод был впервые представлен в релизе v0.11.
read_reply
синтаксис: res, err = red:read_reply()
Читает ответ от сервера redis. Этот метод в основном полезен для Redis Pub/Sub API, например,
local cjson = require "cjson"
local redis = require "resty.redis"
local red = redis:new()
local red2 = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 сек
red2:set_timeouts(1000, 1000, 1000) -- 1 сек
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("1: не удалось подключиться: ", err)
return
end
ok, err = red2:connect("127.0.0.1", 6379)
if not ok then
ngx.say("2: не удалось подключиться: ", err)
return
end
local res, err = red:subscribe("dog")
if not res then
ngx.say("1: не удалось подписаться: ", err)
return
end
ngx.say("1: подписка: ", cjson.encode(res))
res, err = red2:publish("dog", "Hello")
if not res then
ngx.say("2: не удалось опубликовать: ", err)
return
end
ngx.say("2: публикация: ", cjson.encode(res))
res, err = red:read_reply()
if not res then
ngx.say("1: не удалось прочитать ответ: ", err)
return
end
ngx.say("1: получение: ", cjson.encode(res))
red:close()
red2:close()
Запуск этого примера дает следующий вывод:
1: подписка: ["subscribe","dog",1]
2: публикация: 1
1: получение: ["message","dog","Hello"]
Следующие методы класса предоставляются:
add_commands
синтаксис: hash = redis.add_commands(cmd_name1, cmd_name2, ...)
ПРЕДУПРЕЖДЕНИЕ этот метод теперь устарел, поскольку мы уже выполняем автоматическую генерацию методов Lua для любых команд redis, которые пользователь пытается использовать, и, следовательно, нам больше не нужен этот метод.
Добавляет новые команды redis в класс resty.redis. Вот пример:
local redis = require "resty.redis"
redis.add_commands("foo", "bar")
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 сек
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("не удалось подключиться: ", err)
return
end
local res, err = red:foo("a")
if not res then
ngx.say("не удалось выполнить foo: ", err)
end
res, err = red:bar()
if not res then
ngx.say("не удалось выполнить bar: ", err)
end
Аутентификация Redis
Redis использует команду AUTH для аутентификации: http://redis.io/commands/auth
Для этой команды нет ничего особенного по сравнению с другими командами Redis, такими как GET и SET. Поэтому можно просто вызвать метод auth на вашем экземпляре resty.redis. Вот пример:
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 сек
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("не удалось подключиться: ", err)
return
end
local res, err = red:auth("foobared")
if not res then
ngx.say("не удалось аутентифицироваться: ", err)
return
end
где мы предполагаем, что сервер Redis настроен с паролем foobared в файле redis.conf:
requirepass foobared
Если указанный пароль неверен, то приведенный выше пример выведет следующее клиенту HTTP:
не удалось аутентифицироваться: ERR неверный пароль
Транзакции Redis
Эта библиотека поддерживает транзакции Redis. Вот пример:
local cjson = require "cjson"
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 сек
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("не удалось подключиться: ", err)
return
end
local ok, err = red:multi()
if not ok then
ngx.say("не удалось выполнить multi: ", err)
return
end
ngx.say("ответ multi: ", cjson.encode(ok))
local ans, err = red:set("a", "abc")
if not ans then
ngx.say("не удалось выполнить sort: ", err)
return
end
ngx.say("ответ set: ", cjson.encode(ans))
local ans, err = red:lpop("a")
if not ans then
ngx.say("не удалось выполнить sort: ", err)
return
end
ngx.say("ответ set: ", cjson.encode(ans))
ans, err = red:exec()
ngx.say("ответ exec: ", cjson.encode(ans))
red:close()
Тогда вывод будет следующим:
ответ multi: "OK"
ответ set: "QUEUED"
ответ set: "QUEUED"
ответ exec: ["OK",[false,"ERR Операция против ключа с неправильным типом значения"]]
Модуль Redis
Эта библиотека поддерживает модуль Redis. Вот пример с модулем RedisBloom:
local cjson = require "cjson"
local redis = require "resty.redis"
-- регистрируем префикс модуля "bf" для RedisBloom
redis.register_module_prefix("bf")
local red = redis:new()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("не удалось подключиться: ", err)
return
end
-- вызываем команду BF.ADD с префиксом 'bf'
res, err = red:bf():add("dog", 1)
if not res then
ngx.say(err)
return
end
ngx.say("получено: ", cjson.encode(res))
-- вызываем команду BF.EXISTS
res, err = red:bf():exists("dog")
if not res then
ngx.say(err)
return
end
ngx.say("получено: ", cjson.encode(res))
Балансировка нагрузки и отказоустойчивость
Вы можете легко реализовать свою собственную логику балансировки нагрузки Redis на Lua. Просто храните таблицу Lua со всей доступной информацией о бэкенде Redis (например, имя хоста и номера портов) и выбирайте один сервер в соответствии с каким-либо правилом (например, круговой или на основе хеширования ключей) из таблицы Lua при каждом запросе. Вы можете отслеживать текущее состояние правила в данных вашего собственного модуля Lua, см. https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker
Аналогично, вы можете реализовать автоматическую логику отказоустойчивости на Lua с большой гибкостью.
Отладка
Обычно удобно использовать библиотеку lua-cjson для кодирования возвращаемых значений методов команд redis в JSON. Например,
local cjson = require "cjson"
...
local res, err = red:mget("h1234", "h5678")
if res then
print("res: ", cjson.encode(res))
end
Автоматическая регистрация ошибок
По умолчанию основной модуль ngx_lua выполняет регистрацию ошибок, когда происходят ошибки сокета. Если вы уже выполняете правильную обработку ошибок в своем собственном коде Lua, то рекомендуется отключить эту автоматическую регистрацию ошибок, отключив директиву lua_socket_log_errors модуля ngx_lua, то есть,
lua_socket_log_errors off;
Контрольный список для проблем
- Убедитесь, что вы правильно настроили размер пула соединений в set_keepalive. В основном, если ваш Redis может обрабатывать
nодновременных соединений, а ваш NGINX имеетmрабочих процессов, то размер пула соединений должен быть настроен какn/m. Например, если ваш Redis обычно обрабатывает 1000 одновременных запросов, а у вас 10 рабочих процессов NGINX, то размер пула соединений должен составлять 100. Аналогично, если у васpразличных экземпляров NGINX, то размер пула соединений должен составлятьn/m/p. - Убедитесь, что настройка backlog на стороне Redis достаточно велика. Для Redis 2.8+ вы можете напрямую настроить параметр
tcp-backlogв файлеredis.conf(а также настроить параметр ядраSOMAXCONNсоответственно, по крайней мере, на Linux). Вы также можете настроить параметрmaxclientsвredis.conf. - Убедитесь, что вы не используете слишком короткую настройку тайм-аута в методах set_timeout или set_timeouts. Если необходимо, попробуйте повторить операцию после тайм-аута и отключить автоматическую регистрацию ошибок (поскольку вы уже выполняете правильную обработку ошибок в своем собственном коде Lua).
- Если использование CPU ваших рабочих процессов NGINX очень высоко под нагрузкой, то цикл событий NGINX может быть заблокирован вычислениями CPU слишком сильно. Попробуйте сделать выборку C-land on-CPU Flame Graph и Lua-land on-CPU Flame Graph для типичного рабочего процесса NGINX. Вы можете оптимизировать ресурсоемкие операции в соответствии с этими Flame Graphs.
- Если использование CPU ваших рабочих процессов NGINX очень низко под нагрузкой, то цикл событий NGINX может быть заблокирован некоторыми блокирующими системными вызовами (например, системными вызовами ввода-вывода файлов). Вы можете подтвердить проблему, запустив инструмент epoll-loop-blocking-distr против типичного рабочего процесса NGINX. Если это действительно так, то вы можете дополнительно сделать выборку C-land off-CPU Flame Graph для рабочего процесса NGINX, чтобы проанализировать фактические блокировки.
- Если ваш процесс
redis-serverработает на уровне 100% использования CPU, то вам следует рассмотреть возможность масштабирования вашего бэкенда Redis на несколько узлов или использовать инструмент C-land on-CPU Flame Graph для анализа внутренних узких мест в процессе сервера Redis.
Ограничения
- Эта библиотека не может использоваться в контекстах кода, таких как init_by_lua, set_by_lua, log_by_lua, и header_filter_by_lua, где API cosocket ngx_lua недоступен.
- Экземпляр объекта
resty.redisне может храниться в переменной Lua на уровне модуля Lua, потому что он будет разделяться всеми одновременными запросами, обрабатываемыми тем же рабочим процессом nginx (см. https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker) и приведет к плохим условиям гонки, когда одновременные запросы пытаются использовать один и тот же экземплярresty.redis(вы бы увидели ошибку "плохой запрос" или "сокет занят", возвращаемую из вызовов методов). Вы всегда должны инициировать объектыresty.redisв локальных переменных функции или в таблицеngx.ctx. Эти места все имеют свои собственные копии данных для каждого запроса.
Клонировать последнюю версию, предположим v0.29
wget https://github.com/openresty/lua-resty-redis/archive/refs/tags/v0.29.tar.gz
Извлечь
tar -xvzf v0.29.tar.gz
Перейти в каталог
cd lua-resty-redis-0.29
export LUA_LIB_DIR=/usr/local/openresty/site/lualib
Скомпилировать и установить
make install
Теперь будет выведен скомпилированный путь
/usr/local/lib/lua/resty = lua_package_path в конфигурации nginx
```
См. также
- модуль ngx_lua: https://github.com/openresty/lua-nginx-module/#readme
- спецификация протокола Redis: http://redis.io/topics/protocol
- библиотека lua-resty-memcached
- библиотека lua-resty-mysql
GitHub
Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-redis.