feat(redis): add standalone and cluster mode support with backward compatibility

This commit is contained in:
yuchangfu 2026-06-11 11:52:25 +08:00
parent 7e23508a32
commit 92e7bd3def
9 changed files with 468 additions and 1 deletions

View file

@ -1,4 +1,4 @@
.PHONY: build build-backend build-backend-app build-cli build-frontend build-web check clean cli-install db-reset dev dev-all dev-all-down dev-all-reset dev-down dev-logs dev-server dev-server-restart dev-status dev-web docs-build docs-dev docs-preview generate-api help lint-cli lint-web namespace-smoke parallel-down parallel-init parallel-sync parallel-up pr publish-cli publish-cli-major publish-cli-minor staging staging-down staging-logs test test-backend test-backend-app test-cli test-e2e-frontend test-e2e-smoke-frontend test-frontend test-web typecheck-cli typecheck-web validate-release-config web-deps web-install web-install-ci
.PHONY: build build-backend build-backend-app build-cli build-frontend build-web check clean cli-install db-reset dev dev-all dev-all-down dev-all-reset dev-down dev-logs dev-server dev-server-restart dev-status dev-web docs-build docs-dev docs-preview generate-api help lint-cli lint-web namespace-smoke parallel-down parallel-init parallel-sync parallel-up pr publish-cli publish-cli-major publish-cli-minor staging staging-down staging-logs test test-backend test-backend-app test-cli test-e2e-frontend test-e2e-smoke-frontend test-frontend test-web typecheck-cli typecheck-web validate-release-config verify-redis web-deps web-install web-install-ci
DEV_DIR := .dev
DEV_SERVER_PID := $(DEV_DIR)/server.pid
@ -393,3 +393,6 @@ docs-build: ## 构建文档站点
docs-preview: ## 预览构建后的文档站点
cd docs/skillhub && npm run preview
verify-redis: ## 验证 Redis 配置(支持 standalone 和 cluster 模式)
bash scripts/verify-redis-config.sh

116
REDIS-CLUSTER-SUPPORT.md Normal file
View file

@ -0,0 +1,116 @@
# Redis Cluster Support Implementation Summary
## Overview
Added support for both standalone and cluster Redis deployment modes to SkillHub.
## Changes Made
### 1. New Configuration Class
**File:** `server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedisConfig.java`
- Created a new configuration class that supports both standalone and cluster modes
- Uses `@ConditionalOnProperty` to select the appropriate connection factory based on `spring.data.redis.mode`
- Default mode is `standalone` for backward compatibility
- Both modes use Lettuce as the Redis client
### 2. Updated Configuration Files
#### application.yml
Added new configuration properties:
```yaml
spring:
data:
redis:
mode: ${SPRING_DATA_REDIS_MODE:standalone}
database: ${SPRING_DATA_REDIS_DATABASE:0}
cluster:
nodes: ${SPRING_DATA_REDIS_CLUSTER_NODES:}
max-redirects: ${SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS:3}
```
#### application-local.yml
Updated to explicitly specify standalone mode for local development.
### 3. Example Configuration
**File:** `server/skillhub-app/src/main/resources/application-cluster-example.yml`
- Provides a complete example of cluster configuration
- Shows how to configure multiple cluster nodes
- Includes environment variable examples
### 4. Documentation
**File:** `server/skillhub-app/src/main/resources/REDIS-CONFIG-GUIDE.md`
- Comprehensive guide on using both modes
- Configuration examples for YAML and environment variables
- Instructions for switching between modes
### 5. Test Coverage
**File:** `server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedisConfigTest.java`
- Basic test to verify Redis template is configured correctly
- Tests basic Redis operations
## Usage
### Standalone Mode (Default)
```bash
# No changes needed - works as before
make dev-all
```
Or explicitly:
```bash
SPRING_DATA_REDIS_MODE=standalone make dev-all
```
### Cluster Mode
```bash
SPRING_DATA_REDIS_MODE=cluster \
SPRING_DATA_REDIS_CLUSTER_NODES=redis-node1:6379,redis-node2:6379,redis-node3:6379 \
make dev-all
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `SPRING_DATA_REDIS_MODE` | Redis mode: standalone or cluster | standalone |
| `SPRING_DATA_REDIS_HOST` | Redis host (standalone mode) | localhost |
| `SPRING_DATA_REDIS_PORT` | Redis port (standalone mode) | 6379 |
| `SPRING_DATA_REDIS_PASSWORD` | Redis password | (empty) |
| `SPRING_DATA_REDIS_DATABASE` | Redis database number (standalone) | 0 |
| `SPRING_DATA_REDIS_CLUSTER_NODES` | Cluster nodes (comma-separated) | (empty) |
| `SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS` | Max redirects for cluster | 3 |
## Backward Compatibility
- Existing deployments continue to work without any changes
- Default behavior remains standalone mode
- All existing environment variables are still supported
- No breaking changes to the API or configuration structure
## Testing
To test the configuration:
1. **Standalone mode:**
```bash
make dev-all
# Verify Redis connectivity in logs
```
2. **Cluster mode:**
```bash
# Set up a Redis cluster first
SPRING_DATA_REDIS_MODE=cluster \
SPRING_DATA_REDIS_CLUSTER_NODES=node1:6379,node2:6379,node3:6379 \
make dev-all
```
## Notes
- The implementation uses Spring Boot's Lettuce connection factory
- Session storage and all Redis-dependent features work with both modes
- Cluster mode requires proper Redis cluster setup before use
- For production cluster deployments, ensure proper network configuration and security

View file

@ -0,0 +1,76 @@
#!/bin/bash
# Redis Configuration Verification Script
# This script helps verify Redis configuration is working correctly
set -e
echo "=== Redis Configuration Verification ==="
echo ""
# Check if running in standalone or cluster mode
REDIS_MODE=${SPRING_DATA_REDIS_MODE:-standalone}
echo "Redis Mode: $REDIS_MODE"
if [ "$REDIS_MODE" = "standalone" ]; then
REDIS_HOST=${SPRING_DATA_REDIS_HOST:-${REDIS_HOST:-localhost}}
REDIS_PORT=${SPRING_DATA_REDIS_PORT:-${REDIS_PORT:-6379}}
echo "Host: $REDIS_HOST"
echo "Port: $REDIS_PORT"
# Test connection
echo "Testing Redis connection..."
if command -v redis-cli &> /dev/null; then
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ping | grep -q PONG; then
echo "✓ Redis connection successful"
else
echo "✗ Redis connection failed"
exit 1
fi
else
echo "⚠ redis-cli not found, skipping connection test"
fi
elif [ "$REDIS_MODE" = "cluster" ]; then
REDIS_NODES=${SPRING_DATA_REDIS_CLUSTER_NODES:-}
if [ -z "$REDIS_NODES" ]; then
echo "✗ Cluster nodes not configured"
exit 1
fi
echo "Cluster Nodes: $REDIS_NODES"
# Test each node
IFS=',' read -ra NODES <<< "$REDIS_NODES"
for node in "${NODES[@]}"; do
HOST=$(echo "$node" | cut -d: -f1)
PORT=$(echo "$node" | cut -d: -f2)
echo "Testing node: $HOST:$PORT"
if command -v redis-cli &> /dev/null; then
if redis-cli -h "$HOST" -p "$PORT" ping | grep -q PONG; then
echo "✓ Node $HOST:$PORT is reachable"
else
echo "✗ Node $HOST:$PORT is not reachable"
exit 1
fi
else
echo "⚠ redis-cli not found, skipping connection test for $HOST:$PORT"
fi
done
else
echo "✗ Invalid Redis mode: $REDIS_MODE"
echo "Valid modes: standalone, cluster"
exit 1
fi
echo ""
echo "=== Configuration Summary ==="
echo "Mode: $REDIS_MODE"
if [ "$REDIS_MODE" = "standalone" ]; then
echo "Database: ${SPRING_DATA_REDIS_DATABASE:-0}"
elif [ "$REDIS_MODE" = "cluster" ]; then
echo "Max Redirects: ${SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS:-3}"
fi
echo ""
echo "✓ Redis configuration verification completed successfully"

View file

@ -0,0 +1,119 @@
package com.iflytek.skillhub.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.List;
/**
* Redis configuration supporting both standalone and cluster modes.
* Mode selection is controlled via spring.data.redis.mode property:
* - standalone (default): Single Redis instance
* - cluster: Redis Cluster with multiple nodes
*/
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host:localhost}")
private String host;
@Value("${spring.data.redis.port:6379}")
private int port;
@Value("${spring.data.redis.password:}")
private String password;
@Value("${spring.data.redis.database:0}")
private int database;
@Value("${spring.data.redis.cluster.nodes:}")
private List<String> clusterNodes;
@Value("${spring.data.redis.cluster.max-redirects:3}")
private int maxRedirects;
/**
* Creates Redis connection factory for standalone mode.
*/
@Bean
@ConditionalOnProperty(name = "spring.data.redis.mode", havingValue = "standalone", matchIfMissing = true)
@ConditionalOnMissingBean(name = "redisConnectionFactory")
public LettuceConnectionFactory redisStandaloneConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
if (password != null && !password.isEmpty()) {
config.setPassword(password);
}
config.setDatabase(database);
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder().build();
return new LettuceConnectionFactory(config, clientConfig);
}
/**
* Creates Redis connection factory for cluster mode.
*/
@Bean
@ConditionalOnProperty(name = "spring.data.redis.mode", havingValue = "cluster")
@ConditionalOnMissingBean(name = "redisConnectionFactory")
public LettuceConnectionFactory redisClusterConnectionFactory() {
if (clusterNodes == null || clusterNodes.isEmpty()) {
throw new IllegalStateException("Redis cluster nodes must be configured when using cluster mode");
}
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
clusterConfig.setClusterNodes(clusterNodes.stream()
.map(node -> {
String[] parts = node.split(":");
if (parts.length == 2) {
return new org.springframework.data.redis.connection.RedisNode(
parts[0], Integer.parseInt(parts[1]));
} else {
throw new IllegalArgumentException("Invalid cluster node format: " + node);
}
})
.toList());
if (password != null && !password.isEmpty()) {
clusterConfig.setPassword(password);
}
clusterConfig.setMaxRedirects(maxRedirects);
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder().build();
return new LettuceConnectionFactory(clusterConfig, clientConfig);
}
/**
* Creates RedisTemplate bean for both modes.
*/
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer keySerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
return template;
}
}

View file

@ -0,0 +1,82 @@
# Redis Configuration Guide
SkillHub supports both **standalone** and **cluster** Redis deployment modes.
## Configuration Modes
### Standalone Mode (Default)
This is the default mode for local development and simple deployments.
**Configuration in `application.yml`:**
```yaml
spring:
data:
redis:
mode: standalone # or omit this line (default)
host: localhost
port: 6379
password: ""
database: 0
```
**Environment Variables:**
```bash
SPRING_DATA_REDIS_MODE=standalone
SPRING_DATA_REDIS_HOST=localhost
SPRING_DATA_REDIS_PORT=6379
SPRING_DATA_REDIS_PASSWORD=""
SPRING_DATA_REDIS_DATABASE=0
```
### Cluster Mode
For production environments requiring high availability and scalability.
**Configuration in `application.yml`:**
```yaml
spring:
data:
redis:
mode: cluster
password: "your-password" # optional
cluster:
nodes:
- redis-node1:6379
- redis-node2:6379
- redis-node3:6379
max-redirects: 3
```
**Environment Variables:**
```bash
SPRING_DATA_REDIS_MODE=cluster
SPRING_DATA_REDIS_CLUSTER_NODES=redis-node1:6379,redis-node2:6379,redis-node3:6379
SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=3
SPRING_DATA_REDIS_PASSWORD=your-password
```
## Switching Between Modes
To switch from standalone to cluster mode:
1. Set `SPRING_DATA_REDIS_MODE=cluster`
2. Configure cluster nodes via `SPRING_DATA_REDIS_CLUSTER_NODES`
3. Restart the application
To switch back to standalone mode:
1. Set `SPRING_DATA_REDIS_MODE=standalone` (or unset it)
2. Configure single node via `SPRING_DATA_REDIS_HOST` and `SPRING_DATA_REDIS_PORT`
3. Restart the application
## Example Configurations
See `application-cluster-example.yml` for a complete cluster configuration example.
## Notes
- The default mode is `standalone` if `spring.data.redis.mode` is not specified
- Cluster mode requires at least one node to be configured
- Both modes use Lettuce as the Redis client
- Session storage and all Redis-dependent features work with both modes

View file

@ -0,0 +1,23 @@
# Redis Cluster Configuration Example
# Copy this file to application-cluster.yml and modify the cluster nodes as needed
spring:
data:
redis:
mode: cluster
password: ${REDIS_PASSWORD:}
cluster:
# List of cluster nodes in host:port format
nodes:
- redis-node1:6379
- redis-node2:6379
- redis-node3:6379
- redis-node4:6379
- redis-node5:6379
- redis-node6:6379
max-redirects: 3
# For environment variables, set:
# SPRING_DATA_REDIS_MODE=cluster
# SPRING_DATA_REDIS_CLUSTER_NODES=redis-node1:6379,redis-node2:6379,redis-node3:6379
# SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=3

View file

@ -11,8 +11,11 @@ spring:
password: skillhub_dev
data:
redis:
mode: standalone
host: localhost
port: 6379
password:
database: 0
session:
store-type: redis
security:

View file

@ -39,9 +39,14 @@ spring:
maximum-pool-size: ${DB_POOL_MAX_SIZE:10}
data:
redis:
mode: ${SPRING_DATA_REDIS_MODE:standalone} # standalone or cluster
host: ${SPRING_DATA_REDIS_HOST:${REDIS_HOST:localhost}}
port: ${SPRING_DATA_REDIS_PORT:${REDIS_PORT:6379}}
password: ${SPRING_DATA_REDIS_PASSWORD:${REDIS_PASSWORD:}}
database: ${SPRING_DATA_REDIS_DATABASE:0}
cluster:
nodes: ${SPRING_DATA_REDIS_CLUSTER_NODES:}
max-redirects: ${SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS:3}
session:
store-type: redis
redis:

View file

@ -0,0 +1,40 @@
package com.iflytek.skillhub.config;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test to verify Redis configuration works correctly.
*/
@SpringBootTest
@ActiveProfiles("test")
class RedisConfigTest {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
void testRedisTemplateIsConfigured() {
assertThat(redisTemplate).isNotNull();
}
@Test
void testRedisConnection() {
// Test basic Redis operations
String key = "test:key";
String value = "test:value";
redisTemplate.opsForValue().set(key, value);
Object retrieved = redisTemplate.opsForValue().get(key);
assertThat(retrieved).isEqualTo(value);
// Cleanup
redisTemplate.delete(key);
}
}