Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -394,3 +394,6 @@ docs-build: ## 构建文档站点

docs-preview: ## 预览构建后的文档站点
cd docs/skillhub && npm run preview

verify-redis: ## 验证 Redis 配置(支持 standalone 和 cluster 模式)
bash scripts/verify-redis-config.sh
116 changes: 116 additions & 0 deletions REDIS-CLUSTER-SUPPORT.md
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions scripts/verify-redis-config.sh
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading