Статьи

Spring Data и Redis

Эта статья является частью нашего курса Academy под названием Redis — хранилище ключей NoSQL .

Это ускоренный курс по Redis. Вы узнаете, как установить Redis и запустить сервер. Кроме того, вы будете возиться с командной строкой Redis. Далее следуют более сложные темы, такие как репликация, сегментирование и кластеризация, а также объясняется интеграция Redis с Spring Data. Проверьте это здесь !

1. Введение

Redis был создан для решения реальных проблем реальных программных систем. До сих пор мы исследовали очень богатый набор функций Redis, но на самом деле мы не использовали ни одну из них в реальных приложениях. Чтобы заполнить этот пробел, последняя часть урока посвящена представлению этой темы. Мы собираемся создать простое Java-приложение, которое использует Redis и отличный проект Spring Data Redis ( http://projects.spring.io/spring-data-redis/ ) вместе со Spring Framework ( http://projects.spring.io). / spring-framework / ) из портфолио проектов Spring ( http://spring.io/ ). Последними выпущенными версиями Spring Data Redis и Spring Framework на момент написания являются 1.2.0 и 4.0.2 соответственно.

Прежде чем углубляться в детали, стоит упомянуть, что Redis поддерживает широкий спектр сред приложений и языков программирования. Полный список клиентов доступен здесь: http://redis.io/clients .

В остальной части этого руководства предполагается, что читатель умеет программировать на Java и имеет базовые знания Spring Framework ( http://projects.spring.io/spring-framework/ ).

2. Предпосылки

Обычный набор инструментов разработчика Java включает JDK (Java Development Kit) и IDE, например Eclipse или Intellij IDEA, для облегчения процесса разработки приложений. Последней версией JDK, которую мы будем использовать, является 1.7_51, которую можно загрузить с http://www.oracle.com/technetwork/java/javase/downloads/index.html .

Выбором IDE будет Eclipse , последняя версия — 4.3.2, которую можно загрузить по адресу https://www.eclipse.org/downloads/ (любой из Eclipse IDE для разработчиков Java , Eclipse IDE для разработчиков Java EE или Версии Spring Tool Suite хороши).

3. Комплектация Redis Java клиент

Для доступа к Redis из приложений Java доступно несколько клиентов Java (все перечисленные ниже также поддерживаются Spring Data Redis ):

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

4. Зависимости Java-приложений и Eclipse

Проект, который мы собираемся построить, представит все важные функции Redis, которые мы обсуждали до сих пор, но с точки зрения разработчика приложений. Мы начнем с базового примера, предполагая, что есть отдельный автономный экземпляр Redis, который где-то работает (будем называть его redis-host ). Большинство сценариев будут представлены в виде небольших тестовых фрагментов JUnit ( http://junit.org/ ).

Де-факто инструментом построения и управления зависимостями в мире Java по-прежнему является Apache Maven ( http://maven.apache.org/ ), и версия, которую мы собираемся использовать, — 3.1.1, которую можно загрузить с http: //maven.apache. org / docs / 3.1.1 / release-notes.html . На самом деле мы не будем использовать Apache Maven слишком сильно, позволяя Eclipse выполнять эту работу от нашего имени, но мы рассмотрим базовый файл описания зависимостей, который по соглашению называется pom.xml .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<project
    xsi:schemaLocation="
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.javacodegeeks</groupId>
    <artifactId>redis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>4.0.2.RELEASE</spring.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>1.2.0.RELEASE</version>
        </dependency>
 
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.4.1</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
         
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-all</artifactId>
            <version>1.3</version>
            <scope>test</scope>
        </dependency>
 
        <dependency>
              <groupId>com.jayway.awaitility</groupId>
              <artifactId>awaitility</artifactId>
              <version>1.5.0</version>
              <scope>test</scope>
        </dependency>
 
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Если вы опытный Java-разработчик, файлы pom.xml должны быть вам очень знакомы. Но для новичков несколько комментариев могут быть полезны. По сути, мы заявляем, что наш проект с именем com.javacodegeeks.redis зависит от:

  • redis.clients.jedis ( redis.clients.jedis )
  • org.springframework.data.spring-data-redis Data Redis ( org.springframework.data.spring-data-redis )
  • Spring Framework ( org.springframework.spring-core , org.springframework.spring-context , org.springframework.spring-tx , org.springframework.spring-test )
  • JUnit и сопутствующие тестовые леса ( junit.junit , org.hamcrest.hamcrest-all , com.jayway.awaitility.awaitility )

На данный момент мы можем импортировать этот pom.xml в Eclipse, используя Импорт (меню Файл -> Импорт…) из существующего проекта Maven Project .

Рисунок 1: Импорт существующих проектов Maven в Eclipse.

Рисунок 1: Импорт существующих проектов Maven в Eclipse

Когда импорт завершен, проект с именем com.javacodegeeks.redis (или просто redis , в зависимости от настроек импорта) должен появиться в представлении Eclipse Project Explorer .

Рисунок 2. Наш com.javacodegeeks.redis в представлении проекта Eclipse.

Рисунок 2. Наш com.javacodegeeks.redis в представлении Eclipse Project

5. Настройка автономного Redis поверх Spring Redis

JedisConnectionFactory Redis с Jedis начинается с определения JedisConnectionFactory . По умолчанию Jedis использует пул соединений ( http://en.wikipedia.org/wiki/Connection_pool ), чтобы не создавать подключения к серверу Redis каждый раз, а заимствовать их из пула доступных соединений. В целом это считается хорошей практикой, поскольку процесс создания сетевых подключений является относительно дорогой операцией.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.javacodegeeks.redis;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
 
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
 
@Configuration
public class ConnectionConfiguration  {
    @Bean
    public JedisPoolConfig poolConfig() {
        final JedisPoolConfig jedisPoolConfig =  new JedisPoolConfig();        
        jedisPoolConfig.setTestOnBorrow( true );
        jedisPoolConfig.setMaxTotal( 10 );
        return jedisPoolConfig;
    }
     
    @Bean
    public JedisConnectionFactory connectionFactory() {
        final JedisConnectionFactory connectionFactory =
            new JedisConnectionFactory( poolConfig() );    
        connectionFactory.setHostName( "redis-host" );
        connectionFactory.setDatabase( Protocol.DEFAULT_DATABASE );
        connectionFactory.setPort( Protocol.DEFAULT_PORT );       
        return connectionFactory;
    }
}

В этом фрагменте кода мы настраиваем фабрику соединений для экземпляра Redis, работающего на redis-host с пулом максимум 10 соединений. Параметр « test on borrow фактически гарантирует, что заимствованное из пула соединение все еще действует и может быть использовано (в противном случае соединение будет создано заново).

6. Конфигурирование соединения с зашифрованным (разделенным) Redis

В четвертой части , Redis Sharding , мы говорили о разделении на стороне клиента. На самом деле Jedis предоставляет такую ​​возможность, но, к сожалению, Spring Data Redis пока не поддерживает эту функцию.

7. Настройка подключения к Redis Cluster

В части 5 , Redis Clustering , мы обнаружили возможность кластеризации Redis и отметили, что клиенты должны поддерживать и распознавать изменения в протоколе Redis, чтобы выдавать команды нужным узлам. Jedis уже предоставляет поддержку Redis Cluster, но, к сожалению, Spring Data Redis пока не поддерживает эту функцию.

8. Доступ к Redis с помощью Spring Data Redis

Spring Data Redis предоставляет согласованную и лаконичную абстракцию программирования для разных клиентов Redis (см. Раздел «Выбор Java-клиента Redis»). Суть этой абстракции — концепция шаблона: самый простой способ обеспечить доступ к необходимым функциям, не тратя много времени на написание стандартного кода. В случае с Redis это RedisTemplate .

Из предыдущих частей этого руководства мы знаем, что Redis поддерживает несколько примитивных типов данных: строки и числа. Но система типов Java гораздо богаче этой, и поэтому RedisTemplate требует, чтобы тип ключа и тип значения работали (и, соответственно, сериализаторы для этих типов). Мы начнем с простого примера, где ключи и значения являются просто строками (фактически Spring Data Redis уже включает в себя такой класс, называемый StringRedisTemplate но тем не менее давайте рассмотрим общую концепцию).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.javacodegeeks.redis;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
@Configuration
@Import( value = ConnectionConfiguration.class )
public class ApplicationConfiguration {
    @Bean @Autowired
    public RedisTemplate< String, String > redisTemplate(
            final JedisConnectionFactory connectionFactory ) {       
        final RedisTemplate< String, String > template =
            new RedisTemplate< String, String >();       
        template.setConnectionFactory( connectionFactory );
        template.setKeySerializer( new StringRedisSerializer() );
        template.setHashValueSerializer( new StringRedisSerializer() );
        template.setHashKeySerializer( new StringRedisSerializer() );
        template.setValueSerializer( new StringRedisSerializer() );
        template.setStringSerializer( new StringRedisSerializer() );
        return template;
    }
}

С этим мы готовы написать наши первые тесты. Контейнером для всех тестовых случаев, которые мы собираемся создать, является класс RedisStringsTestCase который активно использует возможности тестирования, предоставляемые скаффолдингом Spring Test .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.javacodegeeks.redis;
 
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
 
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith( SpringJUnit4ClassRunner.class )
@ContextConfiguration( classes = ApplicationConfiguration.class )
public class RedisStringsTestCase {
    @Autowired private RedisTemplate< String, String > template;
 
    // Out tests are going to be there   
}

В основном для каждой категории команд Redis (см., Пожалуйста, часть 2 , Команды Redis — Использование командной строки Redis ), RedisTemplate имеет метод façade, который начинается с « ops » (opsForValue, opsForList, opsForHash,…), который буквально возвращает один к одно отображение специфичных для категории команд вызовам методов Java. Первый тест использует команду SET для хранения некоторого ключа / значения и сразу же использует команду GET чтобы убедиться, что он сохранен правильно.

1
2
3
4
5
@Test
public void testSetAndGet() {
    template.opsForValue().set( "mykey", "myvalue" );
    assertThat( template.opsForValue().get( "mykey"), equalTo( "myvalue" ) );
}

Чтобы убедиться, что тест не лжет, мы можем проверить, что mykey имеет значение « myvalue », используя инструмент redis-host и подключаясь к redis-host .

Рисунок 3. Проверка того, что значение для mykey действительно сохранено в экземпляре redis-host.

Рисунок 3. Проверка того, что значение для mykey действительно хранится в redis-host

Прежде чем переходить к следующим тестовым случаям, необходимо принять во внимание одну проблему: после завершения предыдущего теста mykey останется в Redis, пока кто-нибудь не удалит его. Такое поведение может привести к сбою в других тестовых случаях и обычно считается плохой практикой. Было бы неплохо начинать каждый тест с чистой базы данных, и на самом деле мы собираемся заставить его работать так. Команда, которая очищает текущую базу данных Redis — FLUSHDB . Он не предоставляется RedisTemplate напрямую, но может быть легко доступен и запущен с помощью метода execute() и вызова flushDb() в базовом RedisConnection .

01
02
03
04
05
06
07
08
09
10
@Before
public void setUp() {
    template.execute( new RedisCallback< Void >() {
        @Override
        public Void doInRedis( RedisConnection connection ) throws DataAccessException {
            connection.flushDb();
            return null;
        }
    } );
}

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

1
2
3
4
5
6
@Test
public void testSetAndIncrementAndGet() {
    template.opsForValue().set( "mykey", "10" );
    template.opsForValue().increment( "mykey", 5 );
    assertThat( template.opsForValue().get( "mykey"), equalTo( "15" ) );
}

Достаточно просто, давайте перейдем к более сложным структурам данных: спискам, наборам и отсортированным наборам. Первый тестовый пример создает список значений « a », « b », « c », « d » (с RPUSH команды RPUSH ). Затем он проверяет размер списка (команда LLEN ), запрашивает последний элемент списка по индексу (команда LINDEX ) и, наконец, извлекает первый элемент из списка (команда LPOP ).

1
2
3
4
5
6
7
8
@Test
public void testPushToListAndGetElementByIndexAndPopFirstElement() {
    template.opsForList().rightPushAll( "mykey", "a", "b", "c", "d" );
    assertThat( template.opsForList().size( "mykey" ), equalTo( 4L ) );
    assertThat( template.opsForList().index( "mykey", 3 ), equalTo( "d" ) );
    assertThat( template.opsForList().leftPop( "mykey" ), equalTo( "a" ) );
    assertThat( template.opsForList().size( "mykey"), equalTo( 3L ) );
}

Код выглядит очень компактным и читабельным. Давайте перейдем от списков к наборам, и следующий тестовый набор создаст набор значений « a », « b », « c », « d » (с SADD команды SADD ). Затем он проверяет размер набора (используя команду SCARD ) и спрашивает, являются ли « c » и « e » членами этого набора или нет (используя команду SISMEMBER ).

1
2
3
4
5
6
7
@Test
public void testAddToSetAndCheckElementExists() {
    template.opsForSet().add( "mykey", "a", "b", "c", "d" );
    assertThat( template.opsForSet().size( "mykey" ), equalTo( 4L ) );
    assertThat( template.opsForSet().isMember( "mykey", "c" ), equalTo( true ) );
    assertThat( template.opsForSet().isMember( "mykey", "e" ), equalTo( false ) );
}

Полная мощность наборов проявляется в операциях с множествами: пересечение (команда SINTER ), объединение (команда SUNION ) и разность (команда SDIFF ). Следующий тестовый пример демонстрирует это в действии, применяя эти операции к двум наборам.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Test
public void testIntersetOperations() {
    template.opsForSet().add( "mykey1", "a", "b", "c", "d" );
    template.opsForSet().add( "mykey2", "c", "d", "e", "f" );
        
    assertThat( template.opsForSet().intersect( "mykey1", "mykey2" ),
         equalTo( set( "c", "d" ) ) );
         
    assertThat( template.opsForSet().union( "mykey1", "mykey2" ),
        equalTo( set( "a", "b", "c", "d""e", "f" ) ) );
 
    assertThat( template.opsForSet().difference( "mykey1", "mykey2" ),
        equalTo( set( "a", "b" ) ) );
}

Чтобы закончить с типами сбора данных, мы рассмотрим отсортированные наборы, которые Spring Data Redis называет ZSets. Приведенный ниже тестовый пример создает отсортированный набор (команда ZADD ), а затем просит Redis вернуть все элементы, отсортированные по оценкам от высокого до низкого (команда WITHSCORE опцией WITHSCORE ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Test
public void testAddToSortedSetAndCheckElementsAreSortedByScore() {
    template.opsForZSet().add( "mykey", "a", 6.15d );
    template.opsForZSet().add( "mykey", "b", 9.95d );
    template.opsForZSet().add( "mykey", "c", 8.45d );
         
    assertThat( template.opsForZSet().reverseRangeByScoreWithScores( "mykey", 0d, 10d ),
        equalTo(
           set(
                ( TypedTuple< String > )new DefaultTypedTuple< String >( "b", 9.95d ),
                ( TypedTuple< String > )new DefaultTypedTuple< String >( "a", 6.15d ),
                ( TypedTuple< String > )new DefaultTypedTuple< String >( "c", 8.45d )
           )
        )
     );
}

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

Наконец, мы собираемся переключить наше внимание на хэши. Хэши Redis можно рассматривать как объекты данных в Java: контейнер свойств (или полей) и их значений. В следующем тестовом примере создается хеш (с HSET команды HSET ) с двумя свойствами (или полями): « prop1 » и « prop2 ». Затем он проверяет, что все свойства и их значения хранятся правильно (с HGETALL команды HGETALL ), удаляет все свойства (поля) из хеша (с HDEL команды HDEL ) и проверяет, действительно ли они удалены (с HGET команды HGET ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testHashOperations() {
    template.opsForHash().put( "mykey", "prop1", "value1"  );
    template.opsForHash().put( "mykey", "prop2", "value2" );       
         
    assertThat( template.opsForHash().entries( "mykey" ),
        equalTo( map( "prop1", "value1", "prop2", "value2" ) ) );
 
    assertThat( template.opsForHash().get( "mykey", "prop1" ),
        equalTo( ( Object )"value1" ) );
         
    template.opsForHash().delete( "mykey", "prop1", "prop2" );
    assertThat( template.opsForHash().get( "mykey", "prop1" ),
        equalTo( null ) );
}

В этом разделе мы рассмотрели некоторые основы Spring Data Redis и получили хорошее представление о том, как команды Redis отображаются на его API. Количество разработанных нами тестовых примеров просто взглянуло на богатый набор функций Spring Data Redis . В следующих трех разделах мы рассмотрим расширенные шаблоны API: транзакции, конвейерная обработка и публикация / подписка.

9. Транзакции с использованием Spring Data Redis

Хотя поддержка транзакций Redis несколько ограничена, она по-прежнему очень полезна, когда вам это нужно. Чтобы показать, как Spring Data Redis поддерживает семантику транзакций Redis, мы собираемся создать тестовые случаи, которые:

  • присваивает некоторые значения двум клавишам: mykey1 (команда SET ) и mykey2 (команда SADD )
  • проверяет, что члена « a » нет в наборе SISMEMBER (команда SISMEMBER )
  • начинает наблюдать за ключом mykey1 (команда WATCH )
  • инициирует транзакцию (команда MULTI )
  • увеличивает mykey1 (команда mykey1 )
  • добавляет нового члена « b » в набор mykey2 (команда SADD )
  • завершает транзакцию с помощью команды EXEC
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testTransaction() {
    template.opsForValue().set( "mykey1", "10" );               
         
    template.opsForSet().add( "mykey2", "a" );       
    assertThat( template.opsForSet().isMember( "mykey2", "b"), equalTo( false ) );
         
    template.execute( new SessionCallback< List< Object > >() {
        @SuppressWarnings("unchecked")
        @Override
        public< K, V > List<Object> execute( final RedisOperations< K, V > operations )
                throws DataAccessException {
                 
            operations.watch( ( K )"mykey1" );
            operations.multi();                         
                 
            operations.opsForValue().increment( ( K )"mykey1", 5 );
            operations.opsForSet().add( ( K )"mykey2", ( V )"b" );
                 
            return operations.exec();
        }
    } );
         
    assertThat( template.opsForValue().get( "mykey1"), equalTo( "15" ) );
    assertThat( template.opsForSet().isMember( "mykey2", "b"), equalTo( true ) );
}

Однако мы проигнорировали возвращаемое значение вызова метода template.execute (), он возвращает результат каждой отдельной команды. В нашем тестовом случае это будет 15 в результате INCREMENT и 1 в результате SADD .

10. Конвейерная обработка с использованием Redis Data Redis

То, как мы обращаемся к Redis в основном все время, — это одна команда / ответная последовательность: чтобы отправить новую команду, клиент должен дождаться возвращения предыдущей команды сервером Redis. Но есть способ отправить несколько команд на сервер, не дожидаясь ответа, и, наконец, прочитать все ответы за один шаг. Эта техника называется pipelining .

Redis поддерживает конвейеризацию с самых ранних выпусков, поэтому независимо от того, какую версию вы используете, можно использовать конвейеризацию с Redis (более подробную информацию смотрите на http://redis.io/topics/pipelining ).

Конвейерная обработка может значительно повысить производительность вашего приложения за счет уменьшения задержки в сети. Но здесь есть одна загвоздка: в то время как любой клиент отправляет команды, используя конвейерную технику, сервер будет вынужден помещать ответы в очередь. Если необходимо направить огромное количество команд, лучше отправить эти команды до определенного разумного числа (чтобы разделить их на несколько конвейеров). Производительность будет почти такой же, но объем используемой дополнительной памяти будет ограничен.

В следующем тестовом примере демонстрируется конвейеризация с использованием Spring Data Redis . Мы собираемся отправить 100 команд с использованием конвейерной обработки и убедиться, что все команды были отправлены и обработаны, сравнивая значение счетчика с ожидаемым.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Test
public void testPipelining() {
    template.opsForValue().set( "mykey1", "10" );               
                  
    template.executePipelined( new RedisCallback< Object >() {
        @Override
        public Object doInRedis(RedisConnection connection) throws DataAccessException {
            for( int i = 0; i < 100; ++i ) {
                template.opsForValue().increment( "mykey1", 1 );
            }
            return null;
        }
    } );
         
    assertThat( template.opsForValue().get( "mykey1"), equalTo( "110" ) );
}

Как и в случае теста транзакции Redis, мы проигнорировали возвращаемое значение вызова метода template.executePipelined() но он возвращает результат каждой отдельной команды (всего 100 результатов). Кроме того, если вам интересно, почему мы возвращаем null из RedisCallback , есть причина: это возвращаемое значение будет переопределено фактическим значением из ответа (при получении), и в качестве обратного вызова не разрешено возвращать ненулевое значение (см. http://docs.spring.io/spring-data/data-redis/docs/1.2.0.RELEASE/reference/html/redis.html#pipeline для получения дополнительной информации).

11. Публикация / подписка с использованием Red Data Redis

Redis поддерживает парадигму обмена сообщениями публикации / подписки, а Spring Data Redis также обеспечивает полную поддержку этой функции.

По своей природе обмен сообщениями публикации / подписки включает как минимум двух участников: издателя, который публикует сообщения, и подписчика, который прослушивает сообщения от издателя (обычно это отношение «многие ко многим», но мы упростили его до модели «один издатель / подписчик»). ).

Разработка надежного тестового примера для этого может показаться немного сложной. Публикация — это простая часть, но прослушивание сообщения на определенном канале (или шаблоне) требует некоторой работы. Мы начнем с определения класса подписчика RedisMessageListener . Он не будет ничего делать, но посчитает все сообщения, которые он получил до сих пор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package com.javacodegeeks.redis;
 
import java.util.concurrent.atomic.AtomicInteger;
 
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
 
public class RedisMessageListener implements MessageListener {
    private AtomicInteger count = new AtomicInteger( 0 );
     
    @Override
    public void onMessage(Message message, byte[] pattern) {
        count.incrementAndGet();
    }
     
    public int getCount() {
        return count.get();
    }
}

Далее мы должны расширить нашу конфигурацию дополнительными компонентами для RedisMessageListener и RedisMessageListenerContainer . Роль последнего очень важна: он склеивает слушателей и каналы, которые они слушают. Вот минимальная конфигурация ( PubsubConfiguration ), необходимая для начала работы (обратите внимание, что мы импортируем ранее созданную ApplicationConfiguration ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.javacodegeeks.redis;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
 
@Configuration
@Import( value = ApplicationConfiguration.class )
public class PubsubConfiguration  {
    @Bean @Autowired
    public RedisMessageListenerContainer container(
            final JedisConnectionFactory connectionFactory ) {       
 
        final RedisMessageListenerContainer container =
            new RedisMessageListenerContainer();
         
        container.setConnectionFactory( connectionFactory );
        container.addMessageListener( listener(), new ChannelTopic( "test-channel" ) );
         
        return container;
    }
     
    @Bean
    public MessageListener listener() {
        return new RedisMessageListener();
    }
}

Чтобы дать немного контекста, мы подключаем нашего слушателя к каналу, называемому « test-channel », и именно к этому каналу мы собираемся публиковать сообщения.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.javacodegeeks.redis;
 
import static com.jayway.awaitility.Awaitility.await;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
 
 
import java.util.concurrent.Callable;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith( SpringJUnit4ClassRunner.class )
@ContextConfiguration( classes = PubsubConfiguration.class )
public class RedisPublishSubscriberTestCase {
    @Autowired private RedisTemplate< String, String > template;
    @Autowired private RedisMessageListener listener;
     
    @Test
    public void testPublishSubscribe() {
        assertThat( listener.getCount(), equalTo( 0 ) );
         
        template.convertAndSend( "test-channel", "Test Message 1!" );
        template.convertAndSend( "test-channel", "Test Message 2!" );
        template.convertAndSend( "test-channel", "Test Message 3!" );
         
        await().atMost( 1, SECONDS ).until(
            new Callable< Integer >() {           
                @Override
                public Integer call() throws Exception {
                    return listener.getCount();
                }
            },
            equalTo( 3 )
        );
    }
}

Этот новый контрольный PubsubConfiguration использует наш класс PubsubConfiguration для создания экземпляра тестового контекста Spring . В самом тестовом примере мы публикуем три сообщения в канал « test-channel » на сервере Redis. Затем мы даем слушателю сообщений некоторое время (но не более одной секунды), чтобы использовать все сообщения, опубликованные на канале. После этого мы ожидаем, что слушатель получит все 3 сообщения, и это то, что мы проверяем в конце.

12. Выводы

Redis развивается в отличном темпе. Очень сложно идти в ногу со всеми его новыми функциями и командами. Таким образом, вы можете обнаружить, что Spring Data Redis еще не поддерживает некоторые из последних функциональных возможностей, доступных в последних выпусках Redis (даже Java-клиентам требуется некоторое время для добавления поддержки). Хорошим примером является Redis Cluster, который еще не поддерживается Spring Data Redis .

Это последняя часть учебника Redis, но это только начало путешествия в мир Redis. Если вы хотите следить за последними разработками, происходящими в Redis, есть несколько ресурсов, которые вы можете проверить: