From 92e7bd3def725cb89e23b73ba8d8acda911579a9 Mon Sep 17 00:00:00 2001 From: yuchangfu Date: Thu, 11 Jun 2026 11:52:25 +0800 Subject: [PATCH] feat(redis): add standalone and cluster mode support with backward compatibility --- Makefile | 5 +- REDIS-CLUSTER-SUPPORT.md | 116 +++++++++++++++++ scripts/verify-redis-config.sh | 76 +++++++++++ .../iflytek/skillhub/config/RedisConfig.java | 119 ++++++++++++++++++ .../src/main/resources/REDIS-CONFIG-GUIDE.md | 82 ++++++++++++ .../resources/application-cluster-example.yml | 23 ++++ .../src/main/resources/application-local.yml | 3 + .../src/main/resources/application.yml | 5 + .../skillhub/config/RedisConfigTest.java | 40 ++++++ 9 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 REDIS-CLUSTER-SUPPORT.md create mode 100644 scripts/verify-redis-config.sh create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedisConfig.java create mode 100644 server/skillhub-app/src/main/resources/REDIS-CONFIG-GUIDE.md create mode 100644 server/skillhub-app/src/main/resources/application-cluster-example.yml create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedisConfigTest.java diff --git a/Makefile b/Makefile index 039f213d2..05637d633 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/REDIS-CLUSTER-SUPPORT.md b/REDIS-CLUSTER-SUPPORT.md new file mode 100644 index 000000000..f30ba7cf5 --- /dev/null +++ b/REDIS-CLUSTER-SUPPORT.md @@ -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 diff --git a/scripts/verify-redis-config.sh b/scripts/verify-redis-config.sh new file mode 100644 index 000000000..3480e5510 --- /dev/null +++ b/scripts/verify-redis-config.sh @@ -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" diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedisConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedisConfig.java new file mode 100644 index 000000000..caf827501 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedisConfig.java @@ -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 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 redisTemplate(LettuceConnectionFactory connectionFactory) { + RedisTemplate 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; + } +} diff --git a/server/skillhub-app/src/main/resources/REDIS-CONFIG-GUIDE.md b/server/skillhub-app/src/main/resources/REDIS-CONFIG-GUIDE.md new file mode 100644 index 000000000..0bf08e663 --- /dev/null +++ b/server/skillhub-app/src/main/resources/REDIS-CONFIG-GUIDE.md @@ -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 diff --git a/server/skillhub-app/src/main/resources/application-cluster-example.yml b/server/skillhub-app/src/main/resources/application-cluster-example.yml new file mode 100644 index 000000000..ba8a0befe --- /dev/null +++ b/server/skillhub-app/src/main/resources/application-cluster-example.yml @@ -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 diff --git a/server/skillhub-app/src/main/resources/application-local.yml b/server/skillhub-app/src/main/resources/application-local.yml index 87dd24194..ba5bcd763 100644 --- a/server/skillhub-app/src/main/resources/application-local.yml +++ b/server/skillhub-app/src/main/resources/application-local.yml @@ -11,8 +11,11 @@ spring: password: skillhub_dev data: redis: + mode: standalone host: localhost port: 6379 + password: + database: 0 session: store-type: redis security: diff --git a/server/skillhub-app/src/main/resources/application.yml b/server/skillhub-app/src/main/resources/application.yml index a592b0359..3d7c7efe8 100644 --- a/server/skillhub-app/src/main/resources/application.yml +++ b/server/skillhub-app/src/main/resources/application.yml @@ -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: diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedisConfigTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedisConfigTest.java new file mode 100644 index 000000000..2ce8709d3 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedisConfigTest.java @@ -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 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); + } +}