Skip to content

Commit 8f968ca

Browse files
committed
Adding environmental setup for Redis phase. Adding Redis to my Docker Compose YAML and application.yml, adding a Redis Config file, a Repository file for Tasks and Redis, and then a Redis-Lua file for implementing a Redis-based Distributed Lock. Have not yet adjusted any of the pre-existing infrastructure.
1 parent 7a6e689 commit 8f968ca

6 files changed

Lines changed: 180 additions & 0 deletions

File tree

‎springqpro-backend/docker-compose.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ services:
1111
- "5432:5432"
1212
volumes:
1313
- pgdata:/var/lib/postgresql
14+
redis:
15+
image: redis:7-alpine # Chosen intentionally - 7 becase stable, alpine because smaller size (better for CI stuff and deployment on AWS eventually).
16+
container_name: springqpro-redis
17+
ports:
18+
- "6379:6379"
19+
volumes:
20+
- redis-data:/data
21+
command: ["redis-server", "--save", "", "--appendonly", "no"]
1422

1523
volumes:
1624
pgdata:
25+
redis-data:
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.springqprobackend.springqpro.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
4+
import com.fasterxml.jackson.annotation.PropertyAccessor;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
9+
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.data.redis.core.RedisTemplate;
12+
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
13+
import org.springframework.data.redis.serializer.StringRedisSerializer;
14+
15+
// 2025-11-21-REDIS-PHASE-NOTE: THIS FILE CREATES CONNECTION FACTORY AND REDIS TEMPLATE W/ JACKSON SERIALIZER:
16+
@Configuration
17+
public class RedisConfig {
18+
@Bean
19+
public LettuceConnectionFactory redisConnectionFactory(
20+
@Value("${spring.redis.host}") String host,
21+
@Value("${spring.redis.port") int port
22+
) {
23+
RedisStandaloneConfiguration cfg = new RedisStandaloneConfiguration(host, port);
24+
return new LettuceConnectionFactory(cfg);
25+
}
26+
27+
@Bean
28+
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
29+
RedisTemplate<String, Object> template = new RedisTemplate<>();
30+
template.setConnectionFactory(connectionFactory);
31+
// Use Jackson JSON serializer for values -- keeps Task metadata readable and avoids Java native serialization pitfalls.
32+
ObjectMapper mapper = new ObjectMapper();
33+
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
34+
GenericJackson2JsonRedisSerializer jackson = new GenericJackson2JsonRedisSerializer(mapper);
35+
template.setKeySerializer(new StringRedisSerializer());
36+
template.setValueSerializer(jackson);
37+
template.setHashKeySerializer(new StringRedisSerializer());
38+
template.setHashValueSerializer(jackson);
39+
template.afterPropertiesSet();
40+
return template;
41+
}
42+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.springqprobackend.springqpro.redis;
2+
3+
import org.springframework.data.redis.core.RedisTemplate;
4+
import org.springframework.data.redis.core.script.DefaultRedisScript;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.time.Duration;
8+
import java.util.Collections;
9+
import java.util.UUID;
10+
11+
/* 2025-11-21-REDIS-PHASE-NOTE:
12+
- Safe lock using SET key value NX PX ms and a Lua script unlock that checks token (prevents deleting another holder’s lock).
13+
- How to use: String token = lock.tryLock("task:lock:"+id, 10000); if(token!=null){ try { ... } finally { lock.unlock(...); } }
14+
SO BASICALLY, safe lock happens using SET NX PX, and we use a Lua script to unlock it. That's how this is going to work.
15+
*/
16+
@Component
17+
public class RedisDistributedLock {
18+
// Field(s):
19+
private final RedisTemplate<String, Object> redis;
20+
private final DefaultRedisScript<Long> releaseScript;
21+
// Constructor(s):
22+
public RedisDistributedLock(RedisTemplate<String, Object> redis) {
23+
this.redis = redis;
24+
// [FOR UNLOCKING] -> Lua script: if redis.get(KEY)==ARGV[1] then del(KEY) return 1 else return 0 end.
25+
String lua =
26+
"if redis.call('get', KEYS[1]) == ARGV[1] " +
27+
"then return redis.call('del', KEYS[1]) " +
28+
"else return 0 end";
29+
releaseScript = new DefaultRedisScript<>();
30+
releaseScript.setScriptText(lua);
31+
releaseScript.setResultType(Long.class);
32+
}
33+
// Methods:
34+
public String tryLock(String key, long ttlMs) {
35+
String token = UUID.randomUUID().toString();
36+
Boolean ok = redis.opsForValue().setIfAbsent(key, token, Duration.ofMillis(ttlMs));
37+
if(Boolean.TRUE.equals(ok)) return token;
38+
return null;
39+
}
40+
public boolean unlock(String key, String token) {
41+
Long res = redis.execute(releaseScript, Collections.singletonList(key), token);
42+
return res != null && res > 0;
43+
}
44+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Redis supports server-side scripting using Lua (a lightweight, high-level, multi-paradigm programming language primarily designed
2+
for embedded use within applications. It's known for its simplicity, efficiency, and ease of integration with other languages,
3+
particularly C and C++), whcih allows for extending Redis' functionality and performing complex operations atomically.
4+
5+
- Lua scripts execute as a single, atomic unit. This means that either the entire script completes successfully, or none of its
6+
operations are applied, ensuring data consistency even with multiple commands. This eliminates the need for explicit transactions
7+
or pipelines for atomicity within the script's execution.
8+
9+
- Lua scripts can implement complex business logic, conditional execution, and custom data manipulations that aren't possible
10+
w/ individual Redis commands. They can access and modify Redis data structures and utilize most of the standard Redis commands
11+
through the redis.call() and redis.pcall() functions within the script.
12+
13+
- Lua scripts are executed using the EVAL command, which takes the script itself, the number of keys the script will access,
14+
and then the keys and arguments as separate parameters. For subsequent executions of the same script, EVALSHA can be used with the
15+
script's SHA1 digest for efficiency, avoiding repeated transmission of the full script.
16+
17+
- Arguments passed to a Lua script are divided into KEYS and ARGV:
18+
- KEYS are used to specify the keys the script will interact with, allowing Redis to properly route commands in a clustered environment.
19+
- ARGV contains additional arguments or data needed by the script. Within the Lua script, these are accessible as tables KEYS[1], KEYS[2], etc.., and ARGV[1], ARGV[2], etc.
20+
- Important **NOTE**: Lua tables are 1-indexed (they start at index 1 instead of 0).
21+
22+
- Error Handling: Lua scripts can manage errors within the script itself, allowing for more robust and self-contained logic.
23+
- Use Cases: Lua scripting is particularly useful for implementing atomic operations like distributed locks, counters with conditional increments, complex data migrations, or custom commands that combine multiple Redis operations.
24+
(**Obviously in my case, I'm going to be using it for Distributed Locks**).
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.springqprobackend.springqpro.redis;
2+
3+
import com.springqprobackend.springqpro.domain.TaskEntity;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.data.redis.core.RedisTemplate;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.time.Duration;
9+
10+
// A thin cache abstraction. Keep logic here so you can change eviction/format later.
11+
// Note: We cache TaskEntity (DB representation) rather than domain Task. This fits DDD: cache the authoritative persisted shape.
12+
13+
/* 2025-11-21-REDIS-PHASE-NOTE:
14+
- This file is a thin cache abstraction.
15+
- We'll be caching TaskEntity (DB representation, not domain Task). This is good DDD (Domain-Driven Design): Cache the authoritative persisted shape.
16+
*/
17+
@Component
18+
public class TaskRedisRepository {
19+
// Field(s):
20+
private static final String TASK_KEY_PREFIX = "task:";
21+
private final RedisTemplate<String, Object> redis;
22+
private final Duration ttl;
23+
// Constructor(s):
24+
public TaskRedisRepository(RedisTemplate<String, Object> redis, @Value("${cache.task.ttl-seconds:600}") long ttlSeconds) {
25+
this.redis = redis;
26+
this.ttl = Duration.ofSeconds(ttlSeconds);
27+
}
28+
// Method(s):
29+
private String key(String id) {
30+
return TASK_KEY_PREFIX + id;
31+
}
32+
public void put(TaskEntity entity) {
33+
if(entity == null || entity.getId() == null) return;
34+
redis.opsForValue().set(key(entity.getId()), entity, ttl);
35+
}
36+
public TaskEntity get(String id) {
37+
Object o = redis.opsForValue().get(key(id));
38+
if(o instanceof TaskEntity) return (TaskEntity) o;
39+
return null;
40+
}
41+
public void delete(String id) {
42+
redis.delete(key(id));
43+
}
44+
// DEBUG:+NOTE:+TO-DO: I can add other methods like exists(), setIfAbsent() and so on...
45+
}

‎springqpro-backend/src/main/resources/application.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ spring:
1313
hibernate:
1414
format_sql: true
1515

16+
redis:
17+
host: ${REDIS_HOST:localhost}
18+
port: ${REDIS_PORT:6379}
19+
timeout: 2000ms
20+
lettuce:
21+
pool:
22+
enabled: true
23+
max-active: 8
24+
max-idle: 8
25+
min-idle: 0
26+
27+
# Redis cache defaults for my Task entities.
28+
cache:
29+
task:
30+
ttl-seconds: 600 # 10 minutes
31+
1632
graphql:
1733
graphiql:
1834
enabled: true # enable in-browser GraphiQL UI

0 commit comments

Comments
 (0)