mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
feat(redis): add standalone and cluster mode support with backward compatibility
This commit is contained in:
parent
7e23508a32
commit
92e7bd3def
9 changed files with 468 additions and 1 deletions
5
Makefile
5
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
|
||||
|
|
|
|||
116
REDIS-CLUSTER-SUPPORT.md
Normal file
116
REDIS-CLUSTER-SUPPORT.md
Normal 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
|
||||
76
scripts/verify-redis-config.sh
Normal file
76
scripts/verify-redis-config.sh
Normal 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"
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
82
server/skillhub-app/src/main/resources/REDIS-CONFIG-GUIDE.md
Normal file
82
server/skillhub-app/src/main/resources/REDIS-CONFIG-GUIDE.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -11,8 +11,11 @@ spring:
|
|||
password: skillhub_dev
|
||||
data:
|
||||
redis:
|
||||
mode: standalone
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
session:
|
||||
store-type: redis
|
||||
security:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue