Рубрики
Без рубрики

Как добиться сокращения использования памяти Redis на 50 %

Да, вы все правильно поняли. Чтобы дать вам некоторый контекст, некоторое время назад наша (моя организация) Использование Redis было запрещено… С тегом redis, java.

Да, вы все правильно поняли.

Чтобы дать вам некоторый контекст, некоторое время назад наша (моя организация) Использование Redis не отслеживалось – это означало, что мы не знали, почему наша память Redis была занята так сильно, как это было. Наши 2,5 ГБ Redis ElastiCache были почти заполнены, и если бы они каким-то образом достигли своего предела, наша система начала бы выходить из строя. Хотя существовали резервные варианты, Redis мог оказаться узким местом.

В этом посте я бы попытался объяснить, как мы сократили объем хранилища, занимаемого данными, более чем на 50%. Это также было бы своего рода пошаговым руководством по основам, поэтому, если вам просто интересно, как используется Redis, просто пропустите и перейдите в раздел Оптимизации.

Я бы использовал последнюю версию Spring Boot из https://start.spring.io . Во-первых, выберите две из наших основных зависимостей – Spring Boot Web и Весенние данные Реактивного красного цвета - это .

Вы найдете их в pom.xml файл при загрузке начального проекта.

Веб-сайт Spring Boot предназначен для создания базовых веб-приложений с помощью Spring Boot, в то время как Spring Data Reactive Redis будет использоваться для подключения и использования Redis внутри приложения. По своей сути, зависимость Redis по умолчанию использует Салат-латук Клиент Redis, и поддерживается последними версиями Spring Boot.

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

После загрузки базового приложения вам нужно будет извлечь и открыть его в вашей любимой IDE (моя любимая – IntelliJ IDEA).

В моем случае имя проекта redisutil , и вы найдете мои “базовые пакеты” с именем com.darshitpp.redis.redisutil . Этот базовый пакет будет иметь класс с именем RedisUtil Application , который в моем случае имеет следующую конфигурацию.

@SpringBootApplication
@ComponentScan(basePackages = {"com.darshitpp.redis.redisutil"})
public class RedisUtilApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisUtilApplication.class, args);
    }

}

Я вручную добавил аннотацию @ComponentScan , чтобы указать имя пакета верхнего уровня в разделе, где Spring должен искать определенные компоненты/конфигурации.

Чтобы подключиться к Redis, я создаю класс конфигурации с именем Салат Красный - это конфигурация , в новом пакете с именем конфигурация ((обратите внимание, что это должно быть под базовыми пакетами путь, определенный выше.

Вы можете определить конфигурацию в самом классе RedisUtil Application , но я хочу, чтобы это было как можно более “готовым к производству”. Таким образом, хорошей практикой является разделение различных частей приложения.

Мой класс конфигурации – это

@Configuration
public class LettuceRedisConfiguration {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379));
    }

}

Это очень простой класс, который имеет конфигурацию, к какому URL-адресу подключаться для Redis. В моем случае это localhost , но в большинстве производственных приложений это будет внешний сервер Redis. Порт 6379 – порт по умолчанию, на котором запускается сервер Redis. Это Боб вернул бы нам “фабрику” соединений Redis. Думайте об этом как о чем-то, что позволит вам подключаться к Redis при необходимости.

На данный момент моя структура пакета выглядит следующим образом:

->src
  ->main
    ->java
      ->com.darshitpp.redis.redisutil
        ->configuration

Теперь, когда мы знаем, как подключиться к серверу Redis, нам нужно выяснить, какие данные нам нужно хранить в Redis. В нашем случае мы бы хранили Пользователь данные. Это “модель домена” нашего приложения (модель домена может быть переведена в таблицу в базе данных, но в нашем сценарии таблицы нет). Это Пользователь хранится в пакете с именем домен .

Пользователь будет иметь три поля, а именно: Имя , фамилия , и день рождения

Перед сохранением объектов в Redis рекомендуется определить как вы будете хранить данные, чтобы было эффективно их возвращать. Это означает, что Reddit является простым хранилищем значений ключей, вам нужно будет определить ключ, с помощью которого вы будете хранить значение. В нашем случае я выбираю имя как ключ. Данные будут храниться в хэше, так что в хэш-ключ , который мы выбираем, будет Фамилия и значение, сопоставленное с хэш-ключ является объектом User .

Это связано с тем, что хэши в Redis имеют следующую структуру:

key1 --- hashKey1 === value1
     --- hashKey2 === value2
     --- hashKey3 === value3
key2 --- hashKey4 === value4
     --- hashKey5 === value5
.
.
.

Вы также можете представить его в виде дерева, в котором узлы верхнего уровня являются ключами, ближайший следующий уровень – хэш-ключами, а конечные узлы – значениями. Чтобы получить доступ к значению 2 , вам потребуется ключ 1 и хэш-ключ 2 .

Наш пример немного неверен, так как Пользователь может иметь тот же ключ= Имя и Хэш-ключ= фамилия как другого пользователя, и Redis перезапишет значение . Однако для краткости мы предположим, что существуют уникальные Пользователь ы, использующие наше приложение.

Теперь мы будем создавать класс контроллера с именем Обычный контроллер , который будет выступать в качестве точки входа для нашего API. Мы назвали его Обычный контроллер по причинам, которые будут понятны далее в этой статье.

@RestController
@RequestMapping("/normal")
public class NormalController {

    private final NormalService normalService;

    @Autowired
    public NormalController(NormalService normalService) {
        this.normalService = normalService;
    }

    @GetMapping("/get")
    public User get(@RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName) {
        return normalService.get(firstName, lastName);
    }

    @PostMapping("/insert")
    public void insert(@RequestBody User user) {
        normalService.put(user);
    }

    @PostMapping("/delete")
    public void delete(@RequestParam("firstName") String firstName) {
        normalService.delete(firstName);
    }
}

Обычный контроллер также имеет службу с именем Обычная служба которая Подключена автоматически . Класс должен быть определен в новом упакованном файле с именем контроллер , после чего структура пакета будет выглядеть следующим образом

->src
  ->main
    ->java
      ->com.darshitpp.redis.redisutil
        ->configuration
        ->domain
        ->controller

Наши основные операции были бы простыми, похожими на операции, которые Обычная служба реализует использование пользовательского интерфейса операций .

public interface Operations {
    User get(String firstName, String lastName);
    void put(User user);
    void delete(String firstName);
}

Однако, чтобы использовать салат-латук в нашем приложении, нам нужно сделать еще пару вещей. Точно так же, как для доступа к JDBC, существует условие для JdbcTemplate , вы должны аналогичным образом использовать RedisTemplate для работы с Redis. Мы также должны определить, в каком формате Redis будет хранить данные внутри него. По умолчанию он хранит данные в виде строки. Однако знайте, что вы будете хранить Пользователь в Redis, и для облегчения хранения и извлечения из Redis вам понадобится способ, с помощью которого Redis сможет идентифицировать и преобразовать его обратно в нужный вам тип данных.

Думайте об этом как о разговоре с кем-то, кто не знает того же языка, что и вы. Если вы хотите общаться с кем-то, кто говорит только по-испански, вам нужно будет найти переводчика, который переведет английский язык на испанский для вас. Этот процесс преобразования и восстановления известен как Сериализация и десериализация.

С английского на Испанский, чтобы

Таким образом, в нашем случае нам тоже нужен переводчик или сериализатор. Мы бы использовали Джексон для этого процесса. Jackson – это отличная библиотека, которая Spring Boot поддерживает “из коробки” для обработки Json.

Нам нужно было бы создать сериализатор, который реализует Повторный выпуск для наших целей. В нашем случае я создал класс Jsondeserializer внутри нового пакета под названием сериализатор .

class JsonRedisSerializer implements RedisSerializer {
    public static final Charset DEFAULT_CHARSET;
    private final JavaType javaType;
    private ObjectMapper objectMapper = new ObjectMapper()
            .registerModules(new Jdk8Module(), new JavaTimeModule(), new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
            .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);

    public JsonRedisSerializer(Class type) {
        this.javaType = JavaTypeHandler.getJavaType(type);
    }

    public T deserialize(@Nullable byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length == 0) {
            return null;
        } else {
            try {
                return this.objectMapper.readValue(bytes, 0, bytes.length, this.javaType);
            } catch (Exception ex) {
                throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
            }
        }
    }

    public byte[] serialize(@Nullable Object value) throws SerializationException {
        if (value == null) {
            return new byte[0];
        } else {
            try {
                return this.objectMapper.writeValueAsBytes(value);
            } catch (Exception ex) {
                throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex);
            }
        }
    }

    static {
        DEFAULT_CHARSET = StandardCharsets.UTF_8;
    }

}

Как вы можете видеть, у него есть два метода, называемых сериализация и десериализовать . Каждый из этих методов использует ObjectMapper Джексона для преобразования.

Существует также класс с именем Обработчик типов Java , который помогает вам получить тип объекта, который вы пытаетесь сериализовать.

final class JavaTypeHandler {

    static  JavaType getJavaType(Class clazz) {
        return TypeFactory.defaultInstance().constructType(clazz);
    }

}

Следовательно, нам также понадобится класс, который возвращает нам RedisTemplate , использующий этот сериализатор. Я бы назвал этот класс Redis Построитель сериализации .

public final class RedisSerializationBuilder {

    public static  RedisTemplate getNormalRedisTemplate(final LettuceConnectionFactory factory, final Class clazz) {
        JsonRedisSerializer jsonRedisSerializer = new JsonRedisSerializer<>(clazz);

        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setDefaultSerializer(RedisSerializer.json());
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

Обратите внимание, что описанный выше метод вернет вам шаблон, специфичный для конкретной модели домена (в нашем случае Пользователь ), использующий универсальные шаблоны. Он также определяет, какая фабрика соединений должна использоваться, что должно быть по умолчанию ключ /значение / Хэш-ключ /|/Хэш-значение сериализаторы.

Следовательно, Нормальная служба выглядит так

@Service
public class NormalService implements Operations{

    private final RedisTemplate redisTemplate;
    private final HashOperations hashOperations;

    public NormalService(LettuceConnectionFactory redisConnectionFactory) {
        this.redisTemplate = RedisSerializationBuilder.getNormalRedisTemplate(redisConnectionFactory, User.class);
        this.hashOperations = this.redisTemplate.opsForHash();
    }

    @Override
    public User get(String firstName, String lastName) {
        return hashOperations.get(firstName, lastName);
    }

    @Override
    public void put(User user) {
        hashOperations.put(user.getFirstName(), user.getLastName(), user);
    }

    @Override
    public void delete(String firstName) {
        hashOperations.delete(firstName);
    }
}

Затем я вставил Пользователя , используя метод POST , и URL: локальный хост: 8080/обычная служба/вставить Тело запроса:

{
  "firstName": "Priscilla",
  "lastName": "Haymes",
  "birthday": "2020-04-12T11:15:00Z"
}

Если я затем запущу это приложение для 100 пользователей, я найду следующую статистику использования памяти в Redis (я использовал команду статистика памяти с помощью redis-cli )

21) "keys.count"
22) (integer) 100
23) "keys.bytes-per-key"
24) (integer) 1044
25) "dataset.bytes"
26) (integer) 32840

Использование команды hgetall для ключа дает мне

127.0.0.1:6379>hgetall "Priscilla"
1) "Haymes"
2) "{\"firstName\":\"Priscilla\",\"lastName\":\"Haymes\",\"birthday\":1586690100000}"

Обратите внимание, что 2) дает нам фактический тип данных, хранящихся в Redis ->Json!

Наша базовая структура для дальнейшей оптимизации уже создана! Ура!

Пакет сообщений пришел на помощь! Как я уже сказал, вам понадобится механизм “перевода”. Что делать, если переводчик является экспертом и переводит ваш английский на испанский как можно меньше слов? Пакет сообщений тот же самый!

Вам нужно будет добавить еще две зависимости в свой pom.xml файл.

    
        org.msgpack
        msgpack-core
        0.8.20
    
    
        org.msgpack
        jackson-dataformat-msgpack
        0.8.20
    

Мы создаем контроллер под названием Контроллер MsgPack и службу под названием Служба MsgPack , почти аналогичную Обычный Контроллер и Нормальное обслуживание . Мы бы создали сериализатор msgpack для сериализации с помощью MessagePack.

class MsgPackRedisSerializer implements RedisSerializer {
    public static final Charset DEFAULT_CHARSET;
    private final JavaType javaType;
    private ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory())
            .registerModules(new Jdk8Module(), new JavaTimeModule())
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
            .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);

    public MsgPackRedisSerializer(Class type) {
        this.javaType = JavaTypeHandler.getJavaType(type);
    }

    public T deserialize(@Nullable byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length == 0) {
            return null;
        } else {
            try {
                return this.objectMapper.readValue(bytes, 0, bytes.length, this.javaType);
            } catch (Exception ex) {
                throw new SerializationException("Could not read MsgPack JSON: " + ex.getMessage(), ex);
            }
        }
    }

    public byte[] serialize(@Nullable Object value) throws SerializationException {
        if (value == null) {
            return new byte[0];
        } else {
            try {
                return this.objectMapper.writeValueAsBytes(value);
            } catch (Exception ex) {
                throw new SerializationException("Could not write MsgPack JSON: " + ex.getMessage(), ex);
            }
        }
    }

    static {
        DEFAULT_CHARSET = StandardCharsets.UTF_8;
    }

}

Единственное существенное заметное изменение – это экземпляр MessagePack Factory , передаваемый в ObjectMapper . Это послужило бы мостом между двоичным и строковым форматами данных между Redis и нашим приложением Spring Boot.

Тестирование наших изменений (после очистки ранее использованного хранилища от redis дает нам следующее:

127.0.0.1:6379> hgetall "Priscilla"
1) "Haymes"
2) "\x83\xa9firstName\xa9Priscilla\xa8lastName\xa6Haymes\xa8birthday\xcf\x00\x00\x01qn\x19\x8b "

127.0.0.1:6379> memory stats
.
.
.
21) "keys.count"
22) (integer) 100
23) "keys.bytes-per-key"
24) (integer) 876
25) "dataset.bytes"
26) (integer) 15976

Сравните dataset.bytes из текущей памяти с ранее записанной. 15976 байт против 32840 байт, сокращение уже почти на 50 %!

Но подождите, мы можем уменьшить его еще больше. Как, спросите вы. Сжатие! Что, если мы сожмем данные, а затем сохраним их? В нашем случае это сработало бы! На этот раз, Шустрый спешит на помощь!

Ваш первый вопрос после этого будет таким: сжатие и декомпрессия требуют времени. Не повредит ли это производству? У Снаппи тоже есть ответ на этот вопрос.

Он не нацелен на максимальное сжатие или совместимость с любой другой библиотекой сжатия; вместо этого он нацелен на очень высокие скорости и разумное сжатие.

Использование Snappy также так же просто, как добавление зависимости в pom.xml , и несколько строк изменений кода. Просто добавьте Snappy.сжимать во время сериализации и Быстро.распаковать во время десериализации.

    
        org.xerial.snappy
        snappy-java
        1.1.7.3
    

Повторное тестирование с теми же входными данными возвращает следующее

127.0.0.1:6379> hgetall "Priscilla"
1) "Haymes"
2) "7\\\x83\xa9firstName\xa9Priscilla\xa8la\t\x13`\xa6Haymes\xa8birthday\xcf\x00\x00\x01qn\x19\x8b "

127.0.0.1:6379> memory stats
.
.
.
21) "keys.count"
22) (integer) 100
23) "keys.bytes-per-key"
24) (integer) 873
25) "dataset.bytes"
26) (integer) 15720

Вы можете видеть, что размер набора данных меньше, 15720 байт против 15976 байт, незначительная разница, но при больших объемах данных эта разница увеличивается.

В моем случае, очистив и реструктурировав данные, а также используя описанные выше методы, мы сократили использование памяти с 2 ГБ до менее чем 500 МБ.

Полный код можно найти на моем Github для redis-util .

Отдельное спасибо Рахулу Чопре ( @_rahul Chopra ) за его руководство! Вы были лучшим наставником, о котором только можно было мечтать!

Оригинал: “https://dev.to/darshitpp/how-to-achieve-a-50-reduction-in-redis-memory-usage-482b”