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

Моя проблема в том, что в Redis у меня много наборов ключей, например. s1 = {1,2,3} s2 = {3,4} и ключи 1=a, 2=b, 3=c, 4=d. Я хочу вернуть все значения всех ключей в объединении заданных наборов, например. f(s1 s2) = a b c d

Я мог SUNION s1 s2, который возвращает 1 2 3 4, затем MGET 1 2 3 4, чтобы затем получить a b c d. Это отчасти расточительно, потому что это 2 обращения к Redis, когда я хочу сделать это за 1.

Введите Lua и EVAL.

Redis позволяет вам отправлять Lua-скрипт, который может делать сразу несколько вещей; Я могу вызвать Redis с помощью:

EVAL "
local indexes = redis.call('SUNION', unpack(KEYS))
return redis.call('MGET', unpack(indexes))
" 2 s1 s2

Теперь это вызовет SUNION на входе KEYS, которые равны s1 s2, а затем вернет результат MGET. unpack здесь в основном просто splat (* в Ruby и Python), взяв Lua table (массив) и разбив его на аргументы для метода call.

Вот и сделали, и все работает.

Ооооооооо. Он просто сломался в производстве!?!

Итак, я обнаружил, что unpack имеет максимальный размер (около 8000). Итак, если количество наборов ИЛИ количество ключей больше 8K, Lua выдает ошибку.

Итак, теперь мне нужно написать больше Lua. Интересно знать, что MGET на самом деле довольно медленный. Не знаю почему, но многие GET быстрее, чем 1 MGET [cite]. Не знаю почему, но это, по крайней мере, немного упрощает задачу.

local indexes = redis.call('SUNION', unpack(KEYS))
local values = {}
for i=1,#indexes do
  local value = redis.call('get', indexes[i])
  table.insert(values, value)
end
return values

Теперь перейдем к более сложной проблеме: число KEYS превышает 8000.

Итак, поскольку мы звоним от клиента, мы могли бы просто разделить его там, например если у клиента 18 000 ключей, мы просто вызываем вышеуказанный скрипт три раза. Но это возвращает нас туда, где мы были изначально, когда мы вызывали Redis несколько раз.

Итак, давайте сделаем что-нибудь вроде:

local splitby = 8000
local indexes = {}
if #KEYS <= splitby then
  indexes = redis.call('sunion', 
    unpack(KEYS, 1, #KEYS)
  )
elseif #KEYS <= (splitby * 2) then
  indexes = redis.call('sunion', 
    unpack(KEYS, 1, splitby), 
    unpack(KEYS, splitby + 1, #KEYS)
  )
...

Ой, подождите, это не работает! Если вы используете unpack более одного раза в функции ОНА ВЫБИРАЕТ ТОЛЬКО ПЕРВЫЙ ЭЛЕМЕНТ СПИСКА. Я повторюсь, он молча прерывается, а затем отправляет только первый элемент своего списка в качестве аргумента.

Это примерно 2 часа моей жизни, включая регистрацию ошибки в Redis, потому что я не понимал этого странного поведения. Луа украл 2 часа моей жизни. Если вы хотите лучше описать эту и другие unpack боли, иди сюда.

Итак, мы снова идем, на этот раз с правильным решением:

local all_indexes = {}
local step = 0
for i=1,1000 do
    if #KEYS == step then
        break
    end
    local next_step = step + 8000
    if next_step > #KEYS then
        next_step = #KEYS
    end
    local indexes = redis.call('sunion', 
      unpack(KEYS, step+1, next_step)
    )
    table.insert(all_indexes, indexes)
    step = next_step
end
local values = {}
local seen = {}
for i=1,#all_indexes do
    local indexes = all_indexes[i]
    for j=1,#indexes do
        local getkey = indexes[j]
        if seen[getkey] ~= true then
            seen[getkey] = true
            local value = redis.call('get', getkey)
            table.insert(values, value)
        end
    end
end
return values

Сначала мы разбиваем поступающие KEYS на части и sunion их по партиям, добавляя результаты в all_indexes таблицу.

Затем мы перебираем эту таблицу таблиц и get каждый ключ, стараясь не получить один и тот же ключ дважды.

Сделанный.

Подводя итог: поскольку unpack имеет ограничение по размеру и странное поведение, мой простой ДВУСТОРОННИЙ СКРИПТ теперь состоит как минимум из 30 сложных строк.

Я не люблю называть Lua плохим языком, он явно занимает свое место в мире. Но сегодня Lua причинил мне боль, и я просто хотел поделиться.