Я использую 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 причинил мне боль, и я просто хотел поделиться.