1. Обзор
Коллекции являются важным строительным блоком, который обычно используется почти во всех современных приложениях. Поэтому неудивительно, что Redis предлагает нам множество популярных структур данных , таких как списки, наборы, хэши и сортированные наборы.
В этом уроке мы узнаем, как эффективно считывать все доступные ключи Redis, соответствующие определенному шаблону.
2. Исследуйте Коллекции
Давайте представим, что наше приложение использует Redis для хранения информации о мячах , используемых в различных видах спорта. Мы должны иметь возможность видеть информацию о каждом шаре, доступную из коллекции Redis. Для простоты мы ограничим наш набор данных только тремя шарами:
- Мяч для крикета весом 160 г
- Футбольный мяч весом 450 г
- Волейбол весом 270 г
Как обычно, давайте сначала проясним наши основы, работая над наивным подходом к изучению коллекций Redis.
3. Наивный Подход С использованием redis-cli
Прежде чем мы начнем писать Java-код для изучения коллекций, у нас должно быть четкое представление о том, как мы будем это делать, используя интерфейс redis-cli . Предположим, что наш экземпляр Redis доступен по адресу 127.0.0.1 по порту 6379 , чтобы мы могли изучить каждый тип коллекции с помощью интерфейса командной строки.
3.1. Связанный список
Во-первых, давайте сохраним наш набор данных в связанном списке Redis с именем balls в формате sports-name _ ball-weight с помощью команды rpush :
% redis-cli -h 127.0.0.1 -p 6379 127.0.0.1:6379> RPUSH balls "cricket_160" (integer) 1 127.0.0.1:6379> RPUSH balls "football_450" (integer) 2 127.0.0.1:6379> RPUSH balls "volleyball_270" (integer) 3
Мы можем заметить, что успешная вставка в список выводит новую длину списка . Однако в большинстве случаев мы будем слепы к деятельности по вставке данных. В результате мы можем узнать длину связанного списка с помощью команды len :
127.0.0.1:6379> llen balls (integer) 3
Когда мы уже знаем длину списка, удобно использовать команду lrange | для легкого извлечения всего набора данных:
127.0.0.1:6379> lrange balls 0 2 1) "cricket_160" 2) "football_450" 3) "volleyball_270"
3.2. Набор
Далее давайте посмотрим, как мы можем исследовать набор данных, когда решим сохранить его в наборе Redis. Для этого нам сначала нужно заполнить наш набор данных в наборе Redis с именем balls, используя команду sadd :
127.0.0.1:6379> sadd balls "cricket_160" "football_450" "volleyball_270" "cricket_160" (integer) 3
Упс! У нас в команде было дублирующее значение. Но, поскольку мы добавляли значения в набор, нам не нужно беспокоиться о дубликатах. Конечно, мы можем видеть количество элементов, добавленных из выходного значения ответа.
Теперь мы можем использовать команду members , чтобы увидеть все элементы набора :
127.0.0.1:6379> smembers balls 1) "volleyball_270" 2) "cricket_160" 3) "football_450"
3.3. Хэш
Теперь давайте используем структуру хэш-данных Redis для хранения нашего набора данных в хэш-ключе с именем balls таким образом, чтобы поле хэша было спортивным именем, а значение поля-весом мяча. Мы можем сделать это с помощью команды hmset :
127.0.0.1:6379> hmset balls cricket 160 football 450 volleyball 270 OK
Чтобы просмотреть информацию, хранящуюся в нашем хэше, мы можем использовать команду hgetall :
127.0.0.1:6379> hgetall balls 1) "cricket" 2) "160" 3) "football" 4) "450" 5) "volleyball" 6) "270"
3.4. Сортированный набор
В дополнение к уникальному значению члена, сортированные наборы позволяют нам вести счет рядом с ними. Ну, в нашем случае мы можем сохранить название вида спорта в качестве значения участника, а вес мяча-в качестве счета. Давайте используем команду zadd для хранения нашего набора данных:
127.0.0.1:6379> zadd balls 160 cricket 450 football 270 volleyball (integer) 3
Теперь мы можем сначала использовать команду z card , чтобы найти длину отсортированного набора, а затем команду zrange , чтобы исследовать полный набор :
127.0.0.1:6379> zcard balls (integer) 3 127.0.0.1:6379> zrange balls 0 2 1) "cricket" 2) "volleyball" 3) "football"
3.5. Строки
Мы также можем видеть обычные строки ключа-значения как поверхностную коллекцию элементов . Давайте сначала заполним наш набор данных с помощью команды mset :
127.0.0.1:6379> mset balls:cricket 160 balls:football 450 balls:volleyball 270 OK
Мы должны отметить, что мы добавили префикс “шары: “ , чтобы мы могли идентифицировать эти ключи из остальных ключей, которые могут находиться в нашей базе данных Redis. Кроме того, эта стратегия именования позволяет нам использовать команду keys для изучения нашего набора данных с помощью сопоставления шаблонов префиксов:
127.0.0.1:6379> keys balls* 1) "balls:cricket" 2) "balls:volleyball" 3) "balls:football"
4. Собственная Реализация Java
Теперь, когда мы разработали базовую идею соответствующих команд Redis, которые мы можем использовать для изучения коллекций различных типов, пришло время нам испачкать руки кодом.
4.1. Зависимость Maven
В этом разделе мы будем использовать клиентскую библиотеку Jedis для Redis в нашей реализации:
redis.clients jedis 3.2.0
4.2. Клиент Redis
Библиотека Джедаев поставляется с методами, похожими на имена Redis-CLI. Тем не менее, рекомендуется создать клиент Redis-оболочки, который будет внутренне вызывать вызовы функций Jedis .
Всякий раз, когда мы работаем с библиотекой Jedis, мы должны иметь в виду, что один экземпляр Jedis не является потокобезопасным . Поэтому, чтобы получить ресурс Jedis в нашем приложении, мы можем использовать JedisPool , который является потокобезопасным пулом сетевых подключений.
И, поскольку мы не хотим, чтобы несколько экземпляров клиентов Redis плавали в любой момент времени в течение жизненного цикла нашего приложения, мы должны создать наш класс redisClient по принципу одноэлементного шаблона проектирования .
Во-первых, давайте создадим частный конструктор для нашего клиента, который будет внутренне инициализировать JedisPool при создании экземпляра класса redisClient :
private static JedisPool jedisPool; private RedisClient(String ip, int port) { try { if (jedisPool == null) { jedisPool = new JedisPool(new URI("http://" + ip + ":" + port)); } } catch (URISyntaxException e) { log.error("Malformed server address", e); } }
Далее, нам нужна точка доступа к нашему одноэлементному клиенту. Итак, давайте создадим статический метод getInstance() для этой цели:
private static volatile RedisClient instance = null; public static RedisClient getInstance(String ip, final int port) { if (instance == null) { synchronized (RedisClient.class) { if (instance == null) { instance = new RedisClient(ip, port); } } } return instance; }
Наконец, давайте посмотрим, как мы можем создать метод обертки поверх метода Jedis lrange :
public List lrange(final String key, final long start, final long stop) { try (Jedis jedis = jedisPool.getResource()) { return jedis.lrange(key, start, stop); } catch (Exception ex) { log.error("Exception caught in lrange", ex); } return new LinkedList(); }
Конечно, мы можем следовать той же стратегии для создания остальных методов оболочки, таких как push , hmset , hgetall , sadd , smembers , keys , zadd и zrange .
4.3. Анализ
Все команды Redis, которые мы можем использовать для изучения коллекции за один раз, естественно, будут иметь O(n) временную сложность в лучшем случае .
Мы, возможно, немного либеральны, называя этот подход наивным. В реальном производственном экземпляре Redis довольно часто в одной коллекции находятся тысячи или миллионы ключей. Кроме того, однопоточная природа Redis приносит больше страданий, и наш подход может катастрофически блокировать другие операции с более высоким приоритетом.
Таким образом, мы должны подчеркнуть, что мы ограничиваем наш наивный подход, который будет использоваться только для целей отладки.
5. Основы итератора
Главный недостаток нашей наивной реализации заключается в том, что мы запрашиваем Redis, чтобы он выдал нам все результаты для нашего единственного запроса выборки за один раз. Чтобы преодолеть эту проблему, мы можем разбить наш исходный запрос выборки на несколько последовательных запросов выборки, которые работают с меньшими фрагментами всего набора данных.
Давайте предположим, что у нас есть книга на 1000 страниц, которую мы должны прочитать. Если мы будем следовать нашему наивному подходу, нам придется читать эту большую книгу за один присест без перерывов. Это будет фатально для нашего благополучия, поскольку это истощит нашу энергию и помешает нам заниматься любой другой более приоритетной деятельностью.
Конечно, правильный способ-закончить книгу за несколько сеансов чтения. В каждом сеансе мы возобновляем с того места, на котором остановились в предыдущем сеансе — мы можем отслеживать наш прогресс с помощью закладки страницы .
Хотя общее время чтения в обоих случаях будет сопоставимо, тем не менее, второй подход лучше, поскольку он дает нам пространство для дыхания.
Давайте посмотрим, как мы можем использовать итераторный подход для изучения коллекций Redis.
6. Сканирование Redis
Redis предлагает несколько стратегий сканирования для чтения ключей из коллекций с использованием подхода, основанного на курсоре, который в принципе аналогичен закладке страницы.
6.1. Стратегии сканирования
Мы можем сканировать все хранилище коллекций ключей и значений с помощью команды Scan . Однако, если мы хотим ограничить наш набор данных типами коллекций, мы можем использовать один из вариантов:
- Scan может использоваться для перебора наборов
- Hscan помогает нам перебирать пары значений полей в хэше
- Z scan позволяет выполнять итерацию по элементам, хранящимся в отсортированном наборе
Мы должны отметить, что нам на самом деле не нужна стратегия сканирования на стороне сервера, специально разработанная для связанных списков . Это потому, что мы можем получить доступ к членам связанного списка через индексы с помощью команды lindex или lrange . Кроме того, мы можем узнать количество элементов и использовать range в простом цикле для итерации всего списка небольшими фрагментами.
Давайте используем команду SCAN для сканирования ключей строкового типа. Чтобы начать сканирование, нам нужно использовать значение курсора как “0” , соответствующую строку шаблона как “шар*”:
127.0.0.1:6379> mset balls:cricket 160 balls:football 450 balls:volleyball 270 OK 127.0.0.1:6379> SCAN 0 MATCH ball* COUNT 1 1) "2" 2) 1) "balls:cricket" 127.0.0.1:6379> SCAN 2 MATCH ball* COUNT 1 1) "3" 2) 1) "balls:volleyball" 127.0.0.1:6379> SCAN 3 MATCH ball* COUNT 1 1) "0" 2) 1) "balls:football"
С каждым завершенным сканированием мы получаем следующее значение курсора, которое будет использоваться в последующей итерации. В конце концов, мы знаем, что просмотрели всю коллекцию, когда следующее значение курсора равно “0”.
7. Сканирование С Помощью Java
К настоящему времени мы достаточно хорошо понимаем наш подход, чтобы начать его реализацию на Java.
7.1. Стратегии сканирования
Если мы заглянем в основные функции сканирования, предлагаемые классом Jedis , мы найдем стратегии сканирования различных типов коллекций:
public ScanResultscan(final String cursor, final ScanParams params); public ScanResult sscan(final String key, final String cursor, final ScanParams params); public ScanResult > hscan(final String key, final String cursor, final ScanParams params); public ScanResult zscan(final String key, final String cursor, final ScanParams params);
Jedis требует двух дополнительных параметров, шаблон поиска и размер результата, для эффективного управления сканированием – ScanParams делает это . Для этой цели он полагается на методы match() и count () , которые слабо основаны на шаблоне проектирования builder :
public ScanParams match(final String pattern); public ScanParams count(final Integer count);
Теперь, когда мы впитали базовые знания о подходе Jedi к сканированию, давайте смоделируем эти стратегии с помощью интерфейса ScanStrategy :
public interface ScanStrategy{ ScanResult scan(Jedis jedis, String cursor, ScanParams scanParams); }
Во-первых, давайте поработаем над простейшей стратегией scan , которая не зависит от типа коллекции и считывает ключи, но не значение ключей:
public class Scan implements ScanStrategy{ public ScanResult scan(Jedis jedis, String cursor, ScanParams scanParams) { return jedis.scan(cursor, scanParams); } }
Далее давайте рассмотрим стратегию scan , которая предназначена для считывания всех ключей полей и значений полей конкретного хэш-ключа:
public class Hscan implements ScanStrategy> { private String key; @Override public ScanResult > scan(Jedis jedis, String cursor, ScanParams scanParams) { return jedis.hscan(key, cursor, scanParams); } }
Наконец, давайте построим стратегии для наборов и отсортированных наборов. Стратегия scan может считывать все элементы набора, в то время как стратегия scan может считывать элементы вместе с их оценками в виде кортежа s:
public class Sscan implements ScanStrategy{ private String key; public ScanResult scan(Jedis jedis, String cursor, ScanParams scanParams) { return jedis.sscan(key, cursor, scanParams); } } public class Zscan implements ScanStrategy { private String key; @Override public ScanResult scan(Jedis jedis, String cursor, ScanParams scanParams) { return jedis.zscan(key, cursor, scanParams); } }
7.2. Итератор Redis
Далее давайте набросаем строительные блоки, необходимые для создания нашего Redis итератора класса:
- Строковый курсор
- Стратегия сканирования, такая как сканирование , сканирование, сканирование , сканирование
- Заполнитель для параметров сканирования
- Доступ к JedisPool для получения Jedis ресурса
Теперь мы можем продолжить и определить эти члены в нашем Redis итераторе классе:
private final JedisPool jedisPool; private ScanParams scanParams; private String cursor; private ScanStrategystrategy;
На нашем этапе все готово для определения специфичной для итератора функциональности для нашего итератора. Для этого наш класс Redis Iterator должен реализовать интерфейс Iterator :
public class RedisIteratorimplements Iterator > { }
Естественно, мы должны переопределить методы hasNext() и next () , унаследованные от интерфейса Iterator .
Во-первых, давайте выберем низко висящий плод – метод hasNext () , поскольку основная логика проста. Как только значение курсора становится “0”, мы знаем, что мы закончили со сканированием. Итак, давайте посмотрим, как мы можем реализовать это всего за одну строку:
@Override public boolean hasNext() { return !"0".equals(cursor); }
Далее давайте поработаем над методом next () , который выполняет тяжелую работу по сканированию:
@Override public List next() { if (cursor == null) { cursor = "0"; } try (Jedis jedis = jedisPool.getResource()) { ScanResult scanResult = strategy.scan(jedis, cursor, scanParams); cursor = scanResult.getCursor(); return scanResult.getResult(); } catch (Exception ex) { log.error("Exception caught in next()", ex); } return new LinkedList(); }
Следует отметить, что Результат сканирования дает не только результаты сканирования, но и следующее значение курсора , необходимое для последующего сканирования.
Наконец, мы можем включить функциональность для создания нашего RedisIterator в классе redisClient :
public RedisIterator iterator(int initialScanCount, String pattern, ScanStrategy strategy) { return new RedisIterator(jedisPool, initialScanCount, pattern, strategy); }
7.3. Чтение С Помощью Итератора Redis
Поскольку мы разработали наш итератор Redis с помощью интерфейса Iterator , довольно интуитивно понятно считывать значения коллекции с помощью метода next () , пока hasNext() возвращает true .
Для полноты и простоты мы сначала сохраним набор данных, связанный со спортивными мячами, в хэше Redis. После этого мы будем использовать наш redisClient для создания итератора с использованием стратегии Scan scanning. Давайте проверим нашу реализацию, увидев это в действии:
@Test public void testHscanStrategy() { HashMaphash = new HashMap (); hash.put("cricket", "160"); hash.put("football", "450"); hash.put("volleyball", "270"); redisClient.hmset("balls", hash); Hscan scanStrategy = new Hscan("balls"); int iterationCount = 2; RedisIterator iterator = redisClient.iterator(iterationCount, "*", scanStrategy); List > results = new LinkedList >(); while (iterator.hasNext()) { results.addAll(iterator.next()); } Assert.assertEquals(hash.size(), results.size()); }
Мы можем следовать тому же мыслительному процессу с небольшими изменениями, чтобы протестировать и реализовать оставшиеся стратегии сканирования и чтения ключей, доступных в различных типах коллекций.
8. Заключение
Мы начали этот урок с намерением узнать о том, как мы можем прочитать все соответствующие ключи в Redis.
Мы выяснили, что Redis предлагает простой способ считывания ключей за один раз. Хотя это и просто, мы обсудили, как это создает нагрузку на ресурсы и, следовательно, не подходит для производственных систем. Копнув глубже, мы узнали, что существует подход, основанный на итераторе для сканирования через соответствующие ключи Redis для нашего запроса на чтение.
Как всегда, полный исходный код реализации Java, используемой в этой статье, доступен на GitHub .