diff --git a/README.md b/README.md
index c9b29be..e2f6461 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,8 @@
How To Run And Test Application
How To Run And Test Application with Dockerfile (OPTIONAL)
How To Run And Test Application with docker-compose.yml (OPTIONAL)
+ Redis Commands
+ References
@@ -45,7 +47,7 @@
- `Micrometer` dependencies were added to track the logs easily
- `Testcontainers` dependencies were added for integration tests
- `docker-compose.yml` contains `Grafana`, `Prometheus` and `Zipkin` to track metrics, `Kafka` for event-driven
- architecture
+ architecture, `Redis` for caching
- `Actuator`: http://localhost:8080/actuator
- `Kafka UI`: http://localhost:9091/
- `Grafana`
@@ -117,8 +119,31 @@
-------
+### Redis
+
+* The following command returns all matched data by `'keyPattern:*'` pattern
+ * `redis-cli --scan --pattern 'keyPattern:*'`
+
+* The following command deletes all matched data by `'keyPattern:*'` pattern
+ * `redis-cli KEYS 'keyPattern:*' | xargs redis-cli DEL`
+
+* The following command finds `TYPE` in redis with `KEY`
+ * `TYPE key` -> `TYPE xxx:hashedIdOrSomethingElse`
+
+* The following commands search by `TYPE`
+
+ * for `"string" TYPE`: `get key`
+ * for `"hash" TYPE`: `hgetall key`
+ * for `"list" TYPE`: `lrange key 0 -1`
+ * for `"set" TYPE`: `smembers key`
+ * for `"zset" TYPE`: `zrange key 0 -1 withScores`
+
+* RedisInsight:
+
### References
- [Metrics Made Easy Via Spring Actuator, Docker, Prometheus, and Grafana](https://www.youtube.com/watch?v=Utv7MWgNTvI)
- https://prometheus.io/docs/prometheus/latest/installation/#volumes-bind-mount
-- [Spring Boot Rest Controller Unit Test with @WebMvcTest](https://www.bezkoder.com/spring-boot-webmvctest/)
\ No newline at end of file
+- [Spring Boot Rest Controller Unit Test with @WebMvcTest](https://www.bezkoder.com/spring-boot-webmvctest/)
+- [Redis Commands](https://auth0.com/blog/introduction-to-redis-install-cli-commands-and-data-types/)
+- [Running RedisInsight using Docker Compose](https://collabnix.com/running-redisinsight-using-docker-compose/)
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 31db0ea..23ab8e2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -71,6 +71,20 @@ services:
ports:
- "9411:9411"
+ redis:
+ image: redis
+ restart: always
+ container_name: redis
+ ports:
+ - "6378:6379" # Default port is 6379
+
+ redisinsight:
+ image: redislabs/redisinsight:latest
+ restart: always
+ container_name: redisinsight
+ ports:
+ - '8001:8001'
+
volumes:
zookeeper_data:
driver: local
diff --git a/pom.xml b/pom.xml
index db24a3f..d9795f9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,6 +33,22 @@
spring-boot-starter-data-jpa
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+ org.springframework.session
+ spring-session-data-redis
+
+
+
+ org.redisson
+ redisson-spring-boot-starter
+ 3.26.0
+
+
com.h2database
h2
@@ -142,6 +158,12 @@
test
+
+ org.testcontainers
+ kafka
+ test
+
+
diff --git a/src/main/java/com/mb/livedataservice/api/controller/RedisController.java b/src/main/java/com/mb/livedataservice/api/controller/RedisController.java
new file mode 100644
index 0000000..d155a27
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/api/controller/RedisController.java
@@ -0,0 +1,25 @@
+package com.mb.livedataservice.api.controller;
+
+import com.mb.livedataservice.data.entity.RedisHashData;
+import com.mb.livedataservice.service.RedisHashService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+public class RedisController {
+
+ private final RedisHashService redisHashService;
+
+ /**
+ * Create RedisHashData
+ */
+ @PostMapping("/redis-hash")
+ public RedisHashData createRedisHashData() {
+ log.info("Received a request to create RedisHashData. createRedisHashData.");
+ return redisHashService.save(RedisHashData.builder().destination("hello_world").build());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mb/livedataservice/config/CacheConfig.java b/src/main/java/com/mb/livedataservice/config/CacheConfig.java
new file mode 100644
index 0000000..db8cd83
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/config/CacheConfig.java
@@ -0,0 +1,61 @@
+package com.mb.livedataservice.config;
+
+import com.mb.livedataservice.utils.RedisConstants;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisKeyValueAdapter;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.session.data.redis.config.ConfigureRedisAction;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@EnableCaching
+@AutoConfigureAfter(RedisAutoConfiguration.class)
+@ConditionalOnClass({RedisOperations.class, RedisConnectionFactory.class, RedisCacheConfiguration.class})
+@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
+public class CacheConfig {
+
+ @Bean(name = "cacheManager")
+ @ConditionalOnMissingBean(name = "cacheManager")
+ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+ RedisCacheConfiguration expireIn1Day = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1));
+
+ Map cacheConfigurations = new HashMap<>();
+
+ cacheConfigurations.put(RedisConstants.CACHE_KEY, expireIn1Day);
+
+ return RedisCacheManager.RedisCacheManagerBuilder
+ .fromConnectionFactory(connectionFactory)
+ .withInitialCacheConfigurations(cacheConfigurations)
+ .build();
+ }
+
+ /*
+ * If the Redis client is protected, add this config bean. Otherwise, this bean can be removed.
+ *
+ * However, if you can run any commands in redis, please run the following command to enable org.springframework.data.redis.core.RedisKeyExpiredEvent
+ * command -> redis-cli config set notify-keyspace-events xE
+ *
+ * notify-keyspace-events should be xE.
+ * To get the value of notify-keyspace-events run this -> config get "notify-keyspace-events"
+ *
+ * This means that Spring Session cannot configure Redis Keyspace events for you.
+ * To disable the automatic configuration add ConfigureRedisAction.NO_OP as a bean.
+ * */
+ @Bean
+ public static ConfigureRedisAction configureRedisAction() {
+ return ConfigureRedisAction.NO_OP;
+ }
+}
diff --git a/src/main/java/com/mb/livedataservice/config/RedissonConfig.java b/src/main/java/com/mb/livedataservice/config/RedissonConfig.java
new file mode 100644
index 0000000..9cfad78
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/config/RedissonConfig.java
@@ -0,0 +1,22 @@
+package com.mb.livedataservice.config;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RedissonConfig {
+
+ @Bean
+ @ConditionalOnProperty(value = "redisson.enabled", havingValue = "true")
+ public RedissonClient redissonClient(@Value("${redisson.url}") String address) {
+ Config config = new Config();
+ config.useSingleServer().setAddress(address);
+
+ return Redisson.create(config);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/mb/livedataservice/data/entity/RedisHashData.java b/src/main/java/com/mb/livedataservice/data/entity/RedisHashData.java
new file mode 100644
index 0000000..8bfe830
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/data/entity/RedisHashData.java
@@ -0,0 +1,43 @@
+package com.mb.livedataservice.data.entity;
+
+import jakarta.persistence.Id;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.TimeToLive;
+import org.springframework.data.redis.core.index.Indexed;
+
+import java.util.UUID;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@RedisHash(value = "RedisHashData")
+public class RedisHashData {
+
+ @Id
+ @Builder.Default
+ private String id = UUID.randomUUID().toString();
+
+ @Indexed
+ private String redisHashCode;
+
+ @Indexed
+ private String reference;
+
+ private String destination;
+
+ private int count;
+
+ @TimeToLive
+ @Builder.Default
+ private long expiration = 60L;
+
+ public RedisHashData(String redisHashCode, String reference) {
+ this.redisHashCode = redisHashCode;
+ this.reference = reference;
+ }
+}
diff --git a/src/main/java/com/mb/livedataservice/data/repository/RedisHashDataRepository.java b/src/main/java/com/mb/livedataservice/data/repository/RedisHashDataRepository.java
new file mode 100644
index 0000000..e2907ae
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/data/repository/RedisHashDataRepository.java
@@ -0,0 +1,8 @@
+package com.mb.livedataservice.data.repository;
+
+import com.mb.livedataservice.data.entity.RedisHashData;
+import org.springframework.data.repository.CrudRepository;
+
+public interface RedisHashDataRepository extends CrudRepository {
+
+}
diff --git a/src/main/java/com/mb/livedataservice/queue/RedisKeyExpiredEventListenerImpl.java b/src/main/java/com/mb/livedataservice/queue/RedisKeyExpiredEventListenerImpl.java
new file mode 100644
index 0000000..355e25b
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/queue/RedisKeyExpiredEventListenerImpl.java
@@ -0,0 +1,23 @@
+package com.mb.livedataservice.queue;
+
+import com.mb.livedataservice.data.entity.RedisHashData;
+import com.mb.livedataservice.service.RedisHashService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.data.redis.core.RedisKeyExpiredEvent;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@AllArgsConstructor
+public class RedisKeyExpiredEventListenerImpl {
+
+ private final RedisHashService redisHashService;
+
+ @EventListener(condition = "#event.keyspace == 'RedisHashData'")
+ public void redisExpiredKeyEventForRedisHashData(RedisKeyExpiredEvent> event) {
+ log.info("Redis key expired event log. RedisHashData - event:{}", event.toString());
+ redisHashService.delete(RedisHashData.builder().id(new String(event.getId())).build());
+ }
+}
diff --git a/src/main/java/com/mb/livedataservice/service/RedisHashService.java b/src/main/java/com/mb/livedataservice/service/RedisHashService.java
new file mode 100644
index 0000000..a1000fa
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/service/RedisHashService.java
@@ -0,0 +1,16 @@
+package com.mb.livedataservice.service;
+
+import com.mb.livedataservice.data.entity.RedisHashData;
+
+import java.util.Optional;
+
+public interface RedisHashService {
+
+ RedisHashData save(RedisHashData redisHashData);
+
+ Optional findById(String id);
+
+ void delete(RedisHashData redisHashData);
+
+ void deleteRedisHashDataById(String redisHashDataId);
+}
diff --git a/src/main/java/com/mb/livedataservice/service/RedisTokenStoreService.java b/src/main/java/com/mb/livedataservice/service/RedisTokenStoreService.java
new file mode 100644
index 0000000..8255e97
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/service/RedisTokenStoreService.java
@@ -0,0 +1,10 @@
+package com.mb.livedataservice.service;
+
+public interface RedisTokenStoreService {
+
+ String getToken(String tokenId, String key);
+
+ void storeToken(String tokenId, String key);
+
+ void deleteToken(String tokenId, String key);
+}
diff --git a/src/main/java/com/mb/livedataservice/service/impl/RedisHashServiceImpl.java b/src/main/java/com/mb/livedataservice/service/impl/RedisHashServiceImpl.java
new file mode 100644
index 0000000..3417910
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/service/impl/RedisHashServiceImpl.java
@@ -0,0 +1,39 @@
+package com.mb.livedataservice.service.impl;
+
+import com.mb.livedataservice.data.entity.RedisHashData;
+import com.mb.livedataservice.data.repository.RedisHashDataRepository;
+import com.mb.livedataservice.service.RedisHashService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class RedisHashServiceImpl implements RedisHashService {
+
+ private final RedisHashDataRepository redisHashDataRepository;
+
+ @Override
+ public RedisHashData save(RedisHashData redisHashData) {
+ return redisHashDataRepository.save(redisHashData);
+ }
+
+ @Override
+ public Optional findById(String id) {
+ return redisHashDataRepository.findById(id);
+ }
+
+ @Override
+ public void delete(RedisHashData redisHashData) {
+ redisHashDataRepository.delete(redisHashData);
+ }
+
+ @Override
+ public void deleteRedisHashDataById(String redisHashDataId) {
+ log.info("Deleting RedisHashData by ID: '{}'.", redisHashDataId);
+ redisHashDataRepository.deleteById(redisHashDataId);
+ }
+}
diff --git a/src/main/java/com/mb/livedataservice/service/impl/RedisTokenStoreServiceImpl.java b/src/main/java/com/mb/livedataservice/service/impl/RedisTokenStoreServiceImpl.java
new file mode 100644
index 0000000..41a74df
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/service/impl/RedisTokenStoreServiceImpl.java
@@ -0,0 +1,82 @@
+package com.mb.livedataservice.service.impl;
+
+import com.mb.livedataservice.service.RedisTokenStoreService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "services.token.store", havingValue = "redis")
+public class RedisTokenStoreServiceImpl implements RedisTokenStoreService {
+
+ private final RedissonClient redissonClient;
+ private final StringRedisTemplate stringRedisTemplate;
+
+ // tokenId should be unique for every integration.
+ @Override
+ public String getToken(String tokenId, String key) {
+ String token = stringRedisTemplate.opsForValue().get(tokenId);
+ if (!StringUtils.hasLength(token)) {
+ RLock lock = redissonClient.getLock(key);
+
+ boolean control = false;
+ while (lock.isLocked()) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(1000);
+ control = true;
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (control) {
+ return stringRedisTemplate.opsForValue().get(tokenId);
+ }
+ try {
+ lock.lock(3, TimeUnit.SECONDS);
+ // Call 3rd party to get token
+ storeToken(tokenId, token);
+ } catch (Exception ex) {
+ log.info("Error occurred while getting and saving 3rd party token exception. Exception: {}", ExceptionUtils.getStackTrace(ex));
+ } finally {
+ if (lock.isLocked()) {
+ lock.unlock();
+ }
+ }
+ }
+ return token;
+ }
+
+ @Override
+ public void storeToken(String tokenId, String key) {
+ stringRedisTemplate.opsForValue().set(tokenId, key, Duration.ofMinutes(30));
+ }
+
+ @Override
+ public void deleteToken(String tokenId, String key) {
+ RLock lock = redissonClient.getLock(key);
+
+ boolean control = true;
+ while (lock.isLocked()) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(1000);
+ control = false;
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (control) {
+ stringRedisTemplate.delete(tokenId);
+ }
+ }
+}
diff --git a/src/main/java/com/mb/livedataservice/utils/RedisConstants.java b/src/main/java/com/mb/livedataservice/utils/RedisConstants.java
new file mode 100644
index 0000000..8a5d40b
--- /dev/null
+++ b/src/main/java/com/mb/livedataservice/utils/RedisConstants.java
@@ -0,0 +1,13 @@
+package com.mb.livedataservice.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+/**
+ * Collected constants of general utility. All members of this class are immutable.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class RedisConstants {
+
+ public static final String CACHE_KEY = "cacheKey";
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 24082a3..fbfa9fc 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,3 +1,6 @@
+REDIS_HOST: ${REDIS_HOST_ENV:localhost}
+REDIS_PORT: ${REDIS_PORT_ENV:6378}
+
server:
port: 8080
@@ -15,6 +18,17 @@ spring:
driverClassName: org.h2.Driver
url: jdbc:h2:mem:${DB_NAME:MB_TEST};DB_CLOSE_DELAY=-1
+ data:
+ redis:
+ host: ${REDIS_HOST}
+ port: ${REDIS_PORT}
+ jedis:
+ pool:
+ max-active: 7
+ max-idle: 7
+ min-idle: 2
+ max-wait: -1ms
+
jpa:
database-platform: org.hibernate.dialect.H2Dialect
show-sql: ${SHOW_SQL_ENABLED:true}
@@ -75,4 +89,12 @@ swagger:
version: 2.0
- name: live-data-service-2
url: /v2/api-docs
- version: 2.0
\ No newline at end of file
+ version: 2.0
+
+redisson:
+ enabled: true
+ url: redis://${REDIS_HOST}:${REDIS_PORT}
+
+services:
+ token:
+ store: redis
\ No newline at end of file
diff --git a/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java b/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java
index 8f9de07..2eee14a 100644
--- a/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java
+++ b/src/test/java/com/mb/livedataservice/integration_tests/api/controller/TutorialControllerTest.java
@@ -18,9 +18,12 @@
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
import java.util.Objects;
@@ -35,6 +38,14 @@ class TutorialControllerTest extends BaseUnitTest {
@ServiceConnection
private static final PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:16.1");
+ @Container
+ @ServiceConnection
+ public static final GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:7.2.4")).withExposedPorts(6379);
+
+ @Container
+ @ServiceConnection
+ public static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.3"));
+
@Autowired
private TestRestTemplate restTemplate;
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
index c3462d1..e0e6164 100644
--- a/src/test/resources/application.yml
+++ b/src/test/resources/application.yml
@@ -1,3 +1,6 @@
+REDIS_HOST: ${REDIS_HOST_ENV:localhost}
+REDIS_PORT: ${REDIS_PORT_ENV:6378}
+
spring:
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
@@ -5,6 +8,17 @@ spring:
hibernate:
ddl-auto: update
+ data:
+ redis:
+ host: ${REDIS_HOST}
+ port: ${REDIS_PORT}
+ jedis:
+ pool:
+ max-active: 7
+ max-idle: 7
+ min-idle: 2
+ max-wait: -1ms
+
kafka:
consumer:
bootstrap-servers: localhost:9092
@@ -27,4 +41,12 @@ swagger:
version: 2.0
- name: live-data-service-2
url: /v2/api-docs
- version: 2.0
\ No newline at end of file
+ version: 2.0
+
+redisson:
+ enabled: false
+ url: redis://${REDIS_HOST}:${REDIS_PORT}
+
+services:
+ token:
+ store: redis
\ No newline at end of file