Перейти к содержанию

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 здесь:

http://redis.io/commands

Вам нужно ознакомиться с этой справкой по командам 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;

Контрольный список для проблем

  1. Убедитесь, что вы правильно настроили размер пула соединений в set_keepalive. В основном, если ваш Redis может обрабатывать n одновременных соединений, а ваш NGINX имеет m рабочих процессов, то размер пула соединений должен быть настроен как n/m. Например, если ваш Redis обычно обрабатывает 1000 одновременных запросов, а у вас 10 рабочих процессов NGINX, то размер пула соединений должен составлять 100. Аналогично, если у вас p различных экземпляров NGINX, то размер пула соединений должен составлять n/m/p.
  2. Убедитесь, что настройка backlog на стороне Redis достаточно велика. Для Redis 2.8+ вы можете напрямую настроить параметр tcp-backlog в файле redis.conf (а также настроить параметр ядра SOMAXCONN соответственно, по крайней мере, на Linux). Вы также можете настроить параметр maxclients в redis.conf.
  3. Убедитесь, что вы не используете слишком короткую настройку тайм-аута в методах set_timeout или set_timeouts. Если необходимо, попробуйте повторить операцию после тайм-аута и отключить автоматическую регистрацию ошибок (поскольку вы уже выполняете правильную обработку ошибок в своем собственном коде Lua).
  4. Если использование CPU ваших рабочих процессов NGINX очень высоко под нагрузкой, то цикл событий NGINX может быть заблокирован вычислениями CPU слишком сильно. Попробуйте сделать выборку C-land on-CPU Flame Graph и Lua-land on-CPU Flame Graph для типичного рабочего процесса NGINX. Вы можете оптимизировать ресурсоемкие операции в соответствии с этими Flame Graphs.
  5. Если использование CPU ваших рабочих процессов NGINX очень низко под нагрузкой, то цикл событий NGINX может быть заблокирован некоторыми блокирующими системными вызовами (например, системными вызовами ввода-вывода файлов). Вы можете подтвердить проблему, запустив инструмент epoll-loop-blocking-distr против типичного рабочего процесса NGINX. Если это действительно так, то вы можете дополнительно сделать выборку C-land off-CPU Flame Graph для рабочего процесса NGINX, чтобы проанализировать фактические блокировки.
  6. Если ваш процесс 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

```

См. также

GitHub

Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-redis.