Redis (Remote Dictionary Server) - заслужено считается старичком в мире NoSql решений. Этот пост про то, как Spring Data с ним работает. Идея написания данного поста возникла потому, что Redis не совсем похож на привычную базу, он поддерживает типы данных, которые не удобно использовать для хранения объектов(Кеш не в счет) и выполнять поиск по определенным полям. Здесь на примерах я постараюсь описать как с ним работает Spring Data посредством привычного CrudRepository и QueryDSL. Это не пример HowTo, которых множество. Кому интересны внутренности идем дальше.
Примеры будут основаны на простом проекте. Redis поднимается в
докер контейнере, spring-boot приложение, которое тоже в контейнере
с ним общается. Приложение содержит простую модель, репозиторий,
сервис и контроллер. Потрогать все это можно через swagger на
localhost:8080.
Кроме команд, которые сервис выполняет к базе я буду приводить еще
небольшой псевдо-код, который более понятно описывает
происходящее.
Работать мы будем с сущностью Student:
@Data@AllArgsConstructor@NoArgsConstructor@RedisHash("Student")public class Student { @Id private String id; private String name; private int age;}
Здесь, необходимо уточнить что аннотация
@RedisHash("Student")
говорит о том, под каким ключом
будут агрегироваться все сущности.
Попробуем сохранить первого студента:
curl -X POST "http://localhost:8080/save" -H "accept: */*" -H "Content-Type: application/json" -d "{\"id\":\"1\",\"name\":\"Stephen\",\"age\":12}"
Выполнились 3 команды:
"DEL" "Student:1""HMSET" "Student:1" "_class" "com.odis.redisserviceweb.model.Student" "id" "1" "name" "Stephen" "age" "12""SADD" "Student" "1"
Мы видим, что первая команда - это "DEL"
"Student:1"
, говорит о том что удали запись с ключом
"Student:1"
. Этот ключ был сформирован с помощью
значения аннотации @RedisHash
+ значение поля
помеченного аннотацией @Id
.
Далее следует "HMSET" "Student:1" "_class"
"com.odis.redisserviceweb.model.Student" "id" "1" "name" "Stephen"
"age" "12".
Эта команда добавляет значения в хеш с именем
"Student:1"
. На псевдо-коде это будет
выглядеть как
Map "Student:1";"Student:1".put("_class", "com.odis.redisserviceweb.model.Student");"Student:1".put("id", "1");"Student:1".put("name", "Stephen");"Student:1".put("age", "12");
Ну и завершающая команда - это "SADD" "Student" "1"
- добавляет в сет с именем "Student"
значение
"1"
.
В итоге что мы получили? Было создано два объекта в Redis. Первый -
хеш с именем "Student:1"
, второй - сет с именем
"Student"
.
Выполнив команду keys * - дай все ключи (Не выполнять на проде под страхом экзекуции) получим:
127.0.0.1:6379> keys *1) "Student"2) "Student:1"
Типы у этих объектов, как и было ранее описано:
127.0.0.1:6379> type "Student"set127.0.0.1:6379> type "Student:1"hash
Собственно - зачем два объекта? Сейчас все станет на свои места.
В поиске по @Id
нет ничего необычного:
curl -X GET "http://localhost:8080/get/1" -H "accept: */*"
Сформировался ключ "Student:1" и выполнилась команда, которая и вернула искомый объект:
"HGETALL" "Student:1"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "1"5) "name"6) "Stephen"7) "age"8) "12"
А теперь попробуем выполнить поиск всего, что мы сохранили,
добавив перед этим еще одного студента:
curl -X POST "http://localhost:8080/save" -H "accept: */*" -H "Content-Type: application/json" -d "{\"id\":\"2\",\"name\":\"Macaulay\",\"age\":40}"curl -X GET "http://localhost:8080/get" -H "accept: */*"
Выполнилось 3 команды:
"SMEMBERS" "Student"1) "1"2) "2""HGETALL" "Student:1"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "1"5) "name"6) "Stephen"7) "age"8) "12"127.0.0.1"HGETALL" "Student:2"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "2"5) "name"6) "Macaulay"7) "age"8) "40"
Сначала мы получили список всех ключей и после этого сделали
запрос по каждому на получение значения. Именно поэтому было
создано несколько объектов - "Student"
- хранит все
ключи, и по одному объекту на каждого студента с ключом
"Student:@Id"
. Получается, что получение всех
студентов имеет сложность O (N) где N - количество объектов в
базе.
Удалим студунта:
curl -X DELETE "http://localhost:8080/delete/1" -H "accept: */*"
Получаем:
"HGETALL" "Student:1"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "1"5) "name"6) "Stephen"7) "age"8) "12""DEL" "Student:1"(integer) 1"SREM" "Student" "1"(integer) 1"SMEMBERS" "Student:1:idx"(empty array)"DEL" "Student:1:idx"(integer) 0
Смотрим, есть ли студент с нужным нам Id
Удаляем
этот хеш. Удаляем из сета "Student"
ключ
"1"
.
А дальше фигурирует объект Student:1:idx
. О нем речи
не шло ранее. Давайте посмотрим, зачем он необходим. Но для начала
попробуем добавить метод в наш репозиторий для поиска студента по
имени:
List<Student> findAllByName(String name);
Приложение поднялось сохраняем студента и делаем поиск по имени:
curl -X POST "http://localhost:8080/save" -H "accept: */*" -H "Content-Type: application/json" -d "{\"id\":\"1\",\"name\":\"Stephen\",\"age\":12}"curl -X GET "http://localhost:8080/get/filter/Stephen" -H "accept: */*"
В ответе у нас пустой массив, а в логах запросов к Redis видим:
"SINTER" "Student:name:Stephen"(empty array)
Команда "SINTER"
- Возвращает элементы сета,
полученные в результате пересечения всех данных сетов, в нашем
случае передан только один сет -
"Student:name:Stephen"
но мы о нем ничего не знаем и
он не создавался.
Дело в том, что если мы хотим искать по полю, которое не помечено
аннотацией @Id
, это поле должно быть помечено
аннотацией @Indexed
и тогда Spring Data сделает
дополнительные манипуляции при сохранении студента, т. к. понятие
индекс в Redis отсутствует. Пометим поле name этой аннотацией:
@Data@AllArgsConstructor@NoArgsConstructor@RedisHash("Student")public class Student { @Id private String id; @Indexed private String name; private int age;}
Теперь сохраним студента в чистую базу:
curl -X POST "http://localhost:8080/save" -H "accept: */*" -H "Content-Type: application/json" -d "{\"id\":\"1\",\"name\":\"Stephen\",\"age\":12}"
И в логах команд видим:
"DEL" "Student:1""HMSET" "Student:1" "_class" "com.odis.redisserviceweb.model.Student" "id" "1" "name" "Stephen" "age" "12""SADD" "Student" "1""SADD" "Student:name:Stephen" "1""SADD" "Student:1:idx" "Student:name:Stephen"
Первые три команды нам знакомы, но добавились еще две: создали
сет "Student:name:Stephen"
имя которого состоит из
ключа, названия поля, помеченного аннотацией @Indexed
и значением этого поля. В этот сет был добавлен Id
этого студента. Если у нас появится студент с другим
Id
и именем Stephen
его Id
так же будет добавлен в этот сет. И был создан сет, который хранит
все ключи индексов которые были созданы для этого объекта.
Получилось что-то по типу:
Map "Student:1";"Student:1".put("_class", "com.odis.redisserviceweb.model.Student");"Student:1".put("id", "1");"Student:1".put("name", "Stephen");"Student:1".put("age", "12");Set "Student";"Student".add("1");Set "Student:name:Stephen";"Student:name:Stephen".add("1");Set "Student:1:idx";"Student:1:idx".add("Student:name:Stephen");
Теперь должно работать, Выполним поиск по имени и получим наше значение, в логах команд Redis будет:
"SINTER" "Student:name:Stephen""HGETALL" "Student:1"
Получаем Id
студентов, и вытаскиваем их из хеша.
Опять же количество операций прямо пропорционально количеству
данных.
Так же мы можем искать по нескольким полям для этого в качестве
аргумента команды SINTER
будут передаваться два
объекта. Команда вернет пересекающееся id
и выполнит
по ним поиск.
В примере есть метод поиска по имени и возрасту.
Как видим, Spring Data достаточно неплохо интерпретирует работу
с объектами и индексами в Redis. Данный подход можно перенять и не
используя решение Spring.
Недостаток - это то, что поля по которым будет проводится поиск
должны быть промаркированы аннотацией @Indexed
с
самого начала. В противном случае, "индексы" будут созданы
только для объектов, которые сохраняются после добавления этой
аннотации. И да, я понимаю, что Redis это не лучшее решения для
таких нужд, но если в силу определенной ситуации его необходимо
будет использовать, то SpringData сумеет это сделать достаточно
неплохо.