Управление кешем с помощью redisTemplate в Java

Redis
Управление кешем с помощью redisTemplate в Java

задний план

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

Эта база данных ранее не использоваласьPrestoВ случае использования Hive, используя Hive для простого запроса, скорость может быть в несколько минут. Конечно, несколько минут — это не совсем то время, которое нужно для запуска SQL, которое включает в себя сумму времени на отправку запросов, запрос данных и возврат данных. Но даже при этом такая скорость явно не может удовлетворить потребности интерактивного запроса.

Следующим нашим решением был Presto.После использования Presto скорость запроса упала до секунд. Но для интерактивного запроса интерфейсного интерфейса запросов десять секунд по-прежнему являются неприемлемым временем.

Хотя Presto гораздо быстрее Hive (FaceBook официально заявляет в 10 раз), поддержка пейджинга не очень дружелюбна. Когда я его использую, я сам реализую пагинацию в бэкенде.

В этом случае применение кеша действительно беспомощно. Разумно, оптимизация должна начинаться снизу и работать снизу вверх. Способ и эффективность оптимизации верхнего уровня кажутся ограниченными.

Зачем использовать кеш

В клиентских запросах количество совпадающих данных в одном запросе может достигать сотен или даже тысяч, которые необходимо отображать на страницах во внешнем интерфейсе. Даже если вы каждый раз запрашиваете 10 фрагментов данных, весь запрос займет 6-8 секунд. Представьте, что вы ждете 10 секунд каждый раз, когда переворачиваете страницу.

Итак, используйте кеш Redis в это время. Уменьшите количество запросов к базе данных. Совпадающие данные хранятся в базе данных вместе. Таким образом, для первого запроса требуется немного больше времени.После завершения запроса пользователь может перейти на следующую страницу за миллисекунду.

Использование шаблона redis

Spring инкапсулирует относительно мощный шаблон, то есть redisTemplate, который удобен для работы с кешем Redis во время разработки. String, List, Set, Hash, Zset можно хранить в Redis. Следующее будет представлено отдельно для List и Hash.

List

Список в Redis представляет собой простой список строк, и следующие операции являются общими.

hasKey

Чтобы определить, существует ли ключ, просто вызовитеhasKeyВот и все. Предположим, что этот ключtest, конкретное использование заключается в следующем.

if (redisTemplate.hasKey("test")) {
    System.out.println("存在");
} else {
    System.out.println("不存在");
}

range

Эта функция используется для получения данных указанного интервала из кэша Redis. Конкретное использование заключается в следующем.

if (redisTemplate.hasKey("test")) {
    // 该键的值为 [4, 3, 2, 1]
    System.out.println(redisTemplate.opsForList().range("test", 0, 0)); // [4]
    System.out.println(redisTemplate.opsForList().range("test", 0, 1)); // [4, 3]
    System.out.println(redisTemplate.opsForList().range("test", 0, 2)); // [4, 3, 2]
    System.out.println(redisTemplate.opsForList().range("test", 0, 3)); // [4, 3, 2, 1]
    System.out.println(redisTemplate.opsForList().range("test", 0, 4)); // [4, 3, 2, 1]
    System.out.println(redisTemplate.opsForList().range("test", 0, 5)); // [4, 3, 2, 1]
    
    System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // [4, 3, 2, 1] 如果结束位是-1, 则表示取所有的值
}

delete

Удалить ключ.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");

redisTemplate.opsForList().rightPushAll("test", test);
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // [1, 2, 3, 4]
redisTemplate.delete("test");
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // []

size

Получите длину коллекции для этого ключа.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");

redisTemplate.opsForList().rightPushAll("test", test);
System.out.println(redisTemplate.opsForList().size("test")); // 4

leftPush

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

container

И данные всегда берутся слева, но данные могут храниться слева или справа. слеваleftPush, правильноrightPush. leftPush показан ниже.

left-push

Использование заключается в следующем.

for (int i = 0; i < 4; i++) {
    Integer value = i + 1;
    redisTemplate.opsForList().leftPush("test", value.toString());
    System.out.println(redisTemplate.opsForList().range("test", 0, -1));
}

Результат вывода консоли выглядит следующим образом.

[1]
[2, 1]
[3, 2, 1]
[4, 3, 2, 1]

leftPushAll

В основном то же самое, что и leftPush, но за один раз помещает список в стек.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().leftPushAll("test", test);
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // [4, 3, 2, 1]

Конечно, ты тоже можешь

redisTemplate.opsForList().leftPushAll("test", "1", "2", "3", "4");
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // [4, 3, 2, 1]

leftPushIfPresent

иleftPush— это та же операция, с той лишь разницей, что значение ключа обновляется тогда и только тогда, когда ключ существует. Если ключ не существует, никакие операции с данными выполняться не будут.

redisTemplate.delete("test");

redisTemplate.opsForList().leftPushIfPresent("test", "1");
redisTemplate.opsForList().leftPushIfPresent("test", "2");
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // []

leftPop

Эта функция используется для удаления самого левого элемента в нашем абстрактном контейнере выше.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);

redisTemplate.opsForList().leftPop("test"); // [2, 3, 4]
redisTemplate.opsForList().leftPop("test"); // [3, 4]
redisTemplate.opsForList().leftPop("test"); // [4]
redisTemplate.opsForList().leftPop("test"); // []
redisTemplate.opsForList().leftPop("test"); // []

Стоит отметить, что когда return пуст, ключ больше не существует в Redis. Если вы снова позвоните в это времяleftPushIfPresent, дополнительные данные добавить нельзя. Есть код и правда.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);

redisTemplate.opsForList().leftPop("test"); // [2, 3, 4]
redisTemplate.opsForList().leftPop("test"); // [3, 4]
redisTemplate.opsForList().leftPop("test"); // [4]
redisTemplate.opsForList().leftPop("test"); // []
redisTemplate.opsForList().leftPop("test"); // []

redisTemplate.opsForList().leftPushIfPresent("test", "1"); // []
redisTemplate.opsForList().leftPushIfPresent("test", "1"); // []

rightPush

rightPush показан на рисунке ниже.

right-push

Использование заключается в следующем.

for (int i = 0; i < 4; i++) {
    Integer value = i + 1;
    redisTemplate.opsForList().leftPush("test", value.toString());
    System.out.println(redisTemplate.opsForList().range("test", 0, -1));
}

Результат вывода консоли выглядит следующим образом.

[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]

rightPushAll

То же, что и rightPush, сохранить список за один раз.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // [1, 2, 3, 4]

Конечно, вы тоже можете.

redisTemplate.opsForList().rightPushAll("test", "1", "2", "3", "4");
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // [1, 2, 3, 4]

rightPushIfPresent

иrightPush— это та же операция, с той лишь разницей, что значение ключа обновляется тогда и только тогда, когда ключ существует. Если ключ не существует, никакие операции с данными выполняться не будут.

redisTemplate.delete("test");

redisTemplate.opsForList().rightPushIfPresent("test", "1");
redisTemplate.opsForList().rightPushIfPresent("test", "2");
System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // []

rightPop

Эта функция используется для удаления самого правого элемента в нашем абстрактном контейнере выше.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);

redisTemplate.opsForList().rightPop("test"); // [1, 2, 3]
redisTemplate.opsForList().rightPop("test"); // [1, 2]
redisTemplate.opsForList().rightPop("test"); // [1]
redisTemplate.opsForList().rightPop("test"); // []
redisTemplate.opsForList().rightPop("test"); // []

иleftPopТо же самое, вернувшись пустым, снова звонитьrightPushIfPresent, дополнительные данные добавить нельзя.

index

Получить элемент в указанной позиции в списке.

if (redisTemplate.hasKey("test")) {
    // 该键的值为 [1, 2, 3, 4]
    System.out.println(redisTemplate.opsForList().index("test", -1)); // 4
    System.out.println(redisTemplate.opsForList().index("test", 0)); // 1
    System.out.println(redisTemplate.opsForList().index("test", 1)); // 2
    System.out.println(redisTemplate.opsForList().index("test", 2)); // 3
    System.out.println(redisTemplate.opsForList().index("test", 3)); // 4
    System.out.println(redisTemplate.opsForList().index("test", 4)); // null
    System.out.println(redisTemplate.opsForList().index("test", 5)); // null
}

Стоит отметить два момента. Один из них, если индекс-1Если это так, он вернет последний элемент списка, а если нижний индекс массива выходит за пределы, он вернетnull.

trim

Используется для перехвата элементов указанного диапазона, вы можете понимать это какrangeтот же эффект. После прочтения приведенного ниже кода его следует сразу понять.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test); // [1, 2, 3, 4]

redisTemplate.opsForList().trim("test", 0, 2); // [1, 2, 3]

На самом деле эффект совсем другой.rangeсостоит в том, чтобы получить данные в указанном интервале, иtrimЭто оставить данные в указанном интервале и удалить все данные, которые не находятся в интервале.trimдаvoid, не возвращает данных.

remove

Используется для удаления элемента, указанного в ключе. Принимает 3 параметра: имя ключа кеша, количество событий и значение, которое нужно удалить. Есть три значения, которые можно передать в событие count:-1,0,1.

-1Обозначает удаление данных, соответствующих удаляемому значению, начиная с правого края контейнера хранения;0Представляет удаление всех данных, соответствующих входящему значению;1Представляет, начиная с крайнего левого края контейнера хранилища, и удаляет данные, соответствующие удаляемому значению.

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
test.add("4");
test.add("3");
test.add("2");
test.add("1");

redisTemplate.opsForList().rightPushAll("test", test); // [1, 2, 3, 4, 4, 3, 2, 1]

// 当计数事件是-1、传入值是1时
redisTemplate.opsForList().remove("test", -1, "1"); // [1, 2, 3, 4, 4, 3, 2]

// 当计数事件是1,传入值是1时
redisTemplate.opsForList().remove("test", 1, "1"); // [2, 3, 4, 4, 3, 2]

// 当计数事件是0,传入值是4时
redisTemplate.opsForList().remove("test", 0, "4"); // [2, 3, 3, 2]

rightPopAndLeftPush

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

List<String> test = new ArrayList<>();
test.add("1");
test.add("2");
test.add("3");
test.add("4");

List<String> test2 = new ArrayList<>();
test2.add("1");
test2.add("2");
test2.add("3");

redisTemplate.opsForList().rightPushAll("test", test); // [1, 2, 3, 4]
redisTemplate.opsForList().rightPushAll("test2", test2); // [1, 2, 3]

redisTemplate.opsForList().rightPopAndLeftPush("test", "test2");

System.out.println(redisTemplate.opsForList().range("test", 0, -1)); // [1, 2, 3]
System.out.println(redisTemplate.opsForList().range("test2", 0, -1)); // [4, 1, 2, 3]

Hash

Тип хранения — хэш, на самом деле очень легко понять. вышеList, ключ redis можно понимать как список, а вHash, ключ Redis можно понимать как HashMap.

put

для записи данных.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

redisTemplate.opsForHash().put("test", "map", list.toString()); // [1, 2, 3, 4]
redisTemplate.opsForHash().put("test", "isAdmin", true); // true

putAll

Используется для одновременного добавления нескольких ключей к хеш-ключу.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<String> list2 = new ArrayList<>();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");
Map<String, String> valueMap = new HashMap<>();
valueMap.put("map1", list.toString());
valueMap.put("map2", list2.toString());

redisTemplate.opsForHash().putAll("test", valueMap); // {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]}

putIfAbsent

Используется для записи данных в хэш-ключ. Когда ключ уже существует в хеш-ключе, никакие данные не будут записаны, и только если ключ не существует в хеш-ключе, данные будут записаны.

В то же время, если ключ Hash даже не существует, redisTemplate создаст новый ключ Hash, а затем запишет ключ.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().putIfAbsent("test", "map", list.toString());
System.out.println(redisTemplate.opsForHash().entries("test")); // {map=[1, 2, 3, 4]}

get

для получения данных.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);

System.out.println(redisTemplate.opsForHash().get("test", "map")); // [1, 2, 3, 4]
System.out.println(redisTemplate.opsForHash().get("test", "isAdmin")); // true

Boolean bool = (Boolean) redisTemplate.opsForHash().get("test", "isAdmin");
System.out.println(bool); // true

String str = redisTemplate.opsForHash().get("test", "map").toString();
List<String> array = JSONArray.parseArray(str, String.class);
System.out.println(array.size()); // 4

Стоит отметить, что использованиеgetДанные, полученные функцией, имеют тип Object.

Поэтому, если вам нужно использовать тип и логический тип в приведенном выше примере, вам нужно привести его один раз.Listможно использовать типfastjsonэтот инструмент для преобразования. Примеры преобразований перечислены в коде выше.

delete

Используется для удаления ключа в хеш-ключе. Это можно понимать как удаление ключа на карте.

 List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<String> list2 = new ArrayList<>();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");
Map<String, String> valueMap = new HashMap<>();
valueMap.put("map1", list.toString());
valueMap.put("map2", list2.toString());

redisTemplate.opsForHash().putAll("test", valueMap); // {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]}
redisTemplate.opsForHash().delete("test", "map1"); // {map2=[5, 6, 7, 8]}

values

Используется для получения всех значений ключа типа Hash.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);

System.out.println(redisTemplate.opsForHash().values("test")); // [[1, 2, 3, 4], true]

entries

Используется для получения всех значений ключа Hash в формате Map.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);

Map<String, String> map = redisTemplate.opsForHash().entries("test");
System.out.println(map.get("map")); // [1, 2, 3, 4]
System.out.println(map.get("map") instanceof String); // true
System.out.println(redisTemplate.opsForHash().entries("test")); // {a=[1, 2, 3, 4], isAdmin=true}

hasKey

Используется, чтобы узнать, содержит ли хэш-ключ определенный ключ.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);

System.out.println(redisTemplate.opsForHash().hasKey("test", "map")); // true
System.out.println(redisTemplate.opsForHash().hasKey("test", "b")); // false
System.out.println(redisTemplate.opsForHash().hasKey("test", "isAdmin")); // true

keys

Используется для получения всех ключей в хеш-ключе.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);

System.out.println(redisTemplate.opsForHash().keys("test")); // [a, isAdmin]

size

Используется для получения количества ключей, содержащихся в ключе Hash.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);

System.out.println(redisTemplate.opsForHash().size("test")); // 2

increment

Используется для накопления ключа в хеш-ключе в соответствии с входящим значением. Передаваемое значение может быть толькоdoubleилиlong, не принимает числа с плавающей запятой

redisTemplate.opsForHash().increment("test", "a", 3);
redisTemplate.opsForHash().increment("test", "a", -3);
redisTemplate.opsForHash().increment("test", "a", 1);
redisTemplate.opsForHash().increment("test", "a", 0);

System.out.println(redisTemplate.opsForHash().entries("test")); // {a=1}

multiGet

Он используется для получения значений нескольких ключей в хеш-ключе в пакетном режиме.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<String> list2 = new ArrayList<>();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");

redisTemplate.opsForHash().put("test", "map1", list.toString()); // [1, 2, 3, 4]
redisTemplate.opsForHash().put("test", "map2", list2.toString()); // [5, 6, 7, 8]

List<String> keys = new ArrayList<>();
keys.add("map1");
keys.add("map2");

System.out.println(redisTemplate.opsForHash().multiGet("test", keys)); // [[1, 2, 3, 4], [5, 6, 7, 8]]
System.out.println(redisTemplate.opsForHash().multiGet("test", keys) instanceof List); // true

scan

Получить значение ключа в хеш-ключе всех совпадающих условий. Я проверил некоторую информацию, и в большинстве из них написано, что нечеткое сопоставление невозможно, я пробовал сам, и это действительно возможно. Как следует, используйтеscanВ ключе нечеткого совпадения хеш-ключа, сSCANключ.

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<String> list2 = new ArrayList<>();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");
List<String> list3 = new ArrayList<>();
list3.add("9");
list3.add("10");
list3.add("11");
list3.add("12");
Map<String, String> valueMap = new HashMap<>();
valueMap.put("map1", list.toString());
valueMap.put("SCAN_map2", list2.toString());
valueMap.put("map3", list3.toString());

redisTemplate.opsForHash().putAll("test", valueMap); // {SCAN_map2=[5, 6, 7, 8], map3=[9, 10, 11, 12], map1=[1, 2, 3, 4]}

Cursor<Map.Entry<String, String>> cursor = redisTemplate.opsForHash().scan("test", ScanOptions.scanOptions().match("*SCAN*").build());
if (cursor.hasNext()) {
    while (cursor.hasNext()) {
        Map.Entry<String, String> entry = cursor.next();
        System.out.println(entry.getValue()); // [5, 6, 7, 8]
    }
}

Представляем redisTemplate

Если вы понимаете, как им пользоваться, то можете внедрить в проект redisTemplate.

Внедрение pom-зависимостей

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.0.5.RELEASE</version>
</dependency>

Новый файл конфигурации

Затем нужно создать новыйRedisConfigконфигурационный файл.

package com.detectivehlh;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

/**
 * RedisConfig
 *
 * @author Lunhao Hu
 * @date 2019-01-17 15:12
 **/
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //redis序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisTemplate template = new StringRedisTemplate(factory);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

инъекция

Вставьте redisTemplate там, где его нужно использовать.

@Autowired
private RedisTemplate redisTemplate;

написать на обороте

Github