mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
feat(redis): complete cluster connection support
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
dad3c15f92
commit
3db3c9685f
32 changed files with 961 additions and 477 deletions
|
|
@ -32,6 +32,25 @@ POSTGRES_PASSWORD=TODO_change_to_a_strong_database_password
|
|||
REDIS_BIND_ADDRESS=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Optional external Redis Cluster. Leave commented to use bundled standalone Redis.
|
||||
# All advertised node addresses must be reachable from the server container.
|
||||
# SPRING_DATA_REDIS_CLUSTER_NODES=redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379
|
||||
# SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=5
|
||||
# SPRING_DATA_REDIS_USERNAME=
|
||||
# SPRING_DATA_REDIS_PASSWORD=
|
||||
# SPRING_DATA_REDIS_SSL_ENABLED=true
|
||||
# SPRING_DATA_REDIS_CONNECT_TIMEOUT=5s
|
||||
# SPRING_DATA_REDIS_TIMEOUT=3s
|
||||
# SPRING_DATA_REDIS_CLIENT_NAME=skillhub
|
||||
|
||||
# Optional external Redis Sentinel. Sentinel takes precedence if both Sentinel
|
||||
# and Cluster settings are present.
|
||||
# SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
|
||||
# SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-0.example.com:26379,sentinel-1.example.com:26379,sentinel-2.example.com:26379
|
||||
# SPRING_DATA_REDIS_SENTINEL_USERNAME=
|
||||
# SPRING_DATA_REDIS_SENTINEL_PASSWORD=
|
||||
# SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=true
|
||||
|
||||
# Host ports exposed by the app containers.
|
||||
API_PORT=8080
|
||||
WEB_PORT=80
|
||||
|
|
|
|||
|
|
@ -27,6 +27,28 @@ POSTGRES_PASSWORD=change-this-postgres-password
|
|||
|
||||
REDIS_BIND_ADDRESS=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Optional external Redis connection. Leave these commented to use the bundled
|
||||
# standalone Redis service. For Redis Cluster, every advertised node address
|
||||
# must be reachable from the server container.
|
||||
# SPRING_DATA_REDIS_CLUSTER_NODES=redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379
|
||||
# SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=5
|
||||
# SPRING_DATA_REDIS_USERNAME=
|
||||
# SPRING_DATA_REDIS_PASSWORD=
|
||||
# SPRING_DATA_REDIS_SSL_ENABLED=false
|
||||
# SPRING_DATA_REDIS_CONNECT_TIMEOUT=5s
|
||||
# SPRING_DATA_REDIS_TIMEOUT=3s
|
||||
# SPRING_DATA_REDIS_CLIENT_NAME=skillhub
|
||||
|
||||
# Optional external Redis Sentinel. Sentinel takes precedence if both Sentinel
|
||||
# and Cluster settings are present. Use separate credentials when Sentinel ACL
|
||||
# differs from the Redis data nodes.
|
||||
# SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
|
||||
# SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-0.example.com:26379,sentinel-1.example.com:26379,sentinel-2.example.com:26379
|
||||
# SPRING_DATA_REDIS_SENTINEL_USERNAME=
|
||||
# SPRING_DATA_REDIS_SENTINEL_PASSWORD=
|
||||
# SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=true
|
||||
|
||||
API_PORT=8080
|
||||
WEB_PORT=80
|
||||
SESSION_COOKIE_SECURE=false
|
||||
|
|
|
|||
6
.github/workflows/pr-helm-chart.yml
vendored
6
.github/workflows/pr-helm-chart.yml
vendored
|
|
@ -94,6 +94,12 @@ jobs:
|
|||
args: >-
|
||||
--set redis.architecture=replication
|
||||
--set redis.sentinel.enabled=true
|
||||
- name: external-redis-cluster
|
||||
description: 外部 Redis Cluster
|
||||
args: >-
|
||||
--set redis.enabled=false
|
||||
--set externalRedis.cluster.enabled=true
|
||||
--set-json 'externalRedis.cluster.nodes=["redis-0.example.com:6379","redis-1.example.com:6379","redis-2.example.com:6379"]'
|
||||
- name: ingress-tls-certmanager
|
||||
description: Ingress + TLS + cert-manager
|
||||
args: >-
|
||||
|
|
|
|||
6
Makefile
6
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 verify-redis 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-redis-cluster test-web typecheck-cli typecheck-web validate-release-config web-deps web-install web-install-ci
|
||||
|
||||
DEV_DIR := .dev
|
||||
DEV_SERVER_PID := $(DEV_DIR)/server.pid
|
||||
|
|
@ -395,5 +395,5 @@ docs-build: ## 构建文档站点
|
|||
docs-preview: ## 预览构建后的文档站点
|
||||
cd docs/skillhub && npm run preview
|
||||
|
||||
verify-redis: ## 验证 Redis 配置(支持 standalone 和 cluster 模式)
|
||||
bash scripts/verify-redis-config.sh
|
||||
test-redis-cluster: ## 使用真实 Redis Cluster 验证 Spring Data、Session 和 Redisson Stream
|
||||
./scripts/redis-cluster-integration-test.sh
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -294,6 +294,10 @@ Sentinel 地址。Chart **不负责数据库架构切换时的数据迁移**,
|
|||
standalone → replication、Redis standalone/replication → Sentinel 等切换,必须由
|
||||
运维人员在 Chart 之外完成备份、恢复、PVC 复用或其他迁移方案。
|
||||
|
||||
外部 Redis Cluster 由云服务或运维系统提供,Chart 只负责注入连接配置,不创建
|
||||
Cluster,也不将其计入上述内置架构运行时验证范围。应用侧应另行验证 Spring
|
||||
Data、Spring Session 与 Redisson Stream 链路。
|
||||
|
||||
### Redis Sentinel
|
||||
|
||||
内置 Sentinel 使用 Bitnami Redis 的同一份密码同时保护 Redis 数据节点和
|
||||
|
|
@ -333,6 +337,48 @@ server:
|
|||
value: "false"
|
||||
```
|
||||
|
||||
### 外部 Redis Cluster
|
||||
|
||||
Chart 不创建内置 Redis Cluster。生产环境的 Cluster 由云 Redis 或独立运维系统
|
||||
提供,SkillHub 通过标准 Spring Boot 配置连接。至少配置一个 seed 节点,且
|
||||
Cluster 通告的所有节点地址都必须能从 Server Pod 访问:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
enabled: false
|
||||
|
||||
externalRedis:
|
||||
username: skillhub
|
||||
tls:
|
||||
enabled: true
|
||||
connectTimeout: 5s
|
||||
timeout: 3s
|
||||
clientName: skillhub-server
|
||||
cluster:
|
||||
enabled: true
|
||||
nodes:
|
||||
- redis-0.example.com:6379
|
||||
- redis-1.example.com:6379
|
||||
- redis-2.example.com:6379
|
||||
maxRedirects: 5
|
||||
```
|
||||
|
||||
Redis Cluster 只支持数据库 `0`。`maxRedirects` 交给 Spring Data/Lettuce 处理;
|
||||
Redisson 使用同一节点、ACL、TLS 与超时配置并自行处理 Cluster 路由。密码建议
|
||||
通过 `existingSecret` 的 `redis-password` 提供:
|
||||
|
||||
```bash
|
||||
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|
||||
-f values-production.yaml \
|
||||
--set redis.enabled=false \
|
||||
--set existingSecret=skillhub-production-secret \
|
||||
--set externalRedis.cluster.enabled=true \
|
||||
--set-json 'externalRedis.cluster.nodes=["redis-0.example.com:6379","redis-1.example.com:6379","redis-2.example.com:6379"]'
|
||||
```
|
||||
|
||||
`externalRedis.sentinel.enabled` 与 `externalRedis.cluster.enabled` 互斥;内置
|
||||
`redis.enabled=true` 时也不能启用外部 Cluster。
|
||||
|
||||
### 存储配置
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|
|
|
|||
|
|
@ -190,8 +190,13 @@ app.kubernetes.io/component: scanner
|
|||
{{- printf "%s-master" (include "skillhub.redis.fullname" .) -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if .Values.externalRedis.cluster.enabled -}}
|
||||
{{- $node := first .Values.externalRedis.cluster.nodes -}}
|
||||
{{- first (splitList ":" $node) -}}
|
||||
{{- else -}}
|
||||
{{- .Values.externalRedis.host -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Redis Port */}}
|
||||
|
|
@ -203,7 +208,10 @@ app.kubernetes.io/component: scanner
|
|||
{{- print "6379" -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if .Values.externalRedis.sentinel.enabled -}}
|
||||
{{- if .Values.externalRedis.cluster.enabled -}}
|
||||
{{- $node := first .Values.externalRedis.cluster.nodes -}}
|
||||
{{- last (splitList ":" $node) -}}
|
||||
{{- else if .Values.externalRedis.sentinel.enabled -}}
|
||||
{{- $node := first .Values.externalRedis.sentinel.nodes -}}
|
||||
{{- last (splitList ":" $node) -}}
|
||||
{{- else -}}
|
||||
|
|
@ -245,7 +253,10 @@ app.kubernetes.io/component: scanner
|
|||
{{- if .Values.redis.enabled -}}
|
||||
{{- include "skillhub.redis.host" . -}}
|
||||
{{- else -}}
|
||||
{{- if .Values.externalRedis.sentinel.enabled -}}
|
||||
{{- if .Values.externalRedis.cluster.enabled -}}
|
||||
{{- $node := first .Values.externalRedis.cluster.nodes -}}
|
||||
{{- first (splitList ":" $node) -}}
|
||||
{{- else if .Values.externalRedis.sentinel.enabled -}}
|
||||
{{- $node := first .Values.externalRedis.sentinel.nodes -}}
|
||||
{{- first (splitList ":" $node) -}}
|
||||
{{- else -}}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,11 @@ spec:
|
|||
value: {{ .Values.externalRedis.sentinel.masterSet | default "mymaster" | quote }}
|
||||
- name: SPRING_DATA_REDIS_SENTINEL_NODES
|
||||
value: {{ join "," .Values.externalRedis.sentinel.nodes | quote }}
|
||||
{{- else if and (not .Values.redis.enabled) .Values.externalRedis.cluster.enabled }}
|
||||
- name: SPRING_DATA_REDIS_CLUSTER_NODES
|
||||
value: {{ join "," .Values.externalRedis.cluster.nodes | quote }}
|
||||
- name: SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS
|
||||
value: {{ .Values.externalRedis.cluster.maxRedirects | quote }}
|
||||
{{- else }}
|
||||
- name: SPRING_DATA_REDIS_HOST
|
||||
valueFrom:
|
||||
|
|
@ -122,6 +127,31 @@ spec:
|
|||
key: redis-port
|
||||
{{- end }}
|
||||
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.username }}
|
||||
- name: SPRING_DATA_REDIS_USERNAME
|
||||
value: {{ .Values.externalRedis.username | quote }}
|
||||
{{- end }}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.tls.enabled }}
|
||||
- name: SPRING_DATA_REDIS_SSL_ENABLED
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.connectTimeout }}
|
||||
- name: SPRING_DATA_REDIS_CONNECT_TIMEOUT
|
||||
value: {{ .Values.externalRedis.connectTimeout | quote }}
|
||||
{{- end }}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.timeout }}
|
||||
- name: SPRING_DATA_REDIS_TIMEOUT
|
||||
value: {{ .Values.externalRedis.timeout | quote }}
|
||||
{{- end }}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.clientName }}
|
||||
- name: SPRING_DATA_REDIS_CLIENT_NAME
|
||||
value: {{ .Values.externalRedis.clientName | quote }}
|
||||
{{- end }}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled .Values.externalRedis.sentinel.username }}
|
||||
- name: SPRING_DATA_REDIS_SENTINEL_USERNAME
|
||||
value: {{ .Values.externalRedis.sentinel.username | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- if or (and .Values.redis.enabled .Values.redis.sentinel.enabled) (and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled) }}
|
||||
- name: SPRING_DATA_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
|
|
@ -145,7 +175,7 @@ spec:
|
|||
key: redis-sentinel-password
|
||||
{{- end }}
|
||||
optional: true
|
||||
{{- else if or .Values.redis.enabled .Values.externalRedis.password }}
|
||||
{{- else if or .Values.redis.enabled .Values.externalRedis.password .Values.existingSecret }}
|
||||
- name: SPRING_DATA_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
|
|
|||
|
|
@ -56,12 +56,33 @@
|
|||
{{- if and (not .Values.postgresql.enabled) (not .Values.externalDatabase.host) -}}
|
||||
{{- fail "postgresql.enabled=false requires externalDatabase.host for dependency checks" -}}
|
||||
{{- end -}}
|
||||
{{- if and (not .Values.redis.enabled) (not .Values.externalRedis.sentinel.enabled) (not .Values.externalRedis.host) -}}
|
||||
{{- if and .Values.externalRedis.sentinel.enabled .Values.externalRedis.cluster.enabled -}}
|
||||
{{- fail "externalRedis.sentinel.enabled and externalRedis.cluster.enabled are mutually exclusive" -}}
|
||||
{{- end -}}
|
||||
{{- if and .Values.redis.enabled .Values.externalRedis.cluster.enabled -}}
|
||||
{{- fail "externalRedis.cluster.enabled=true requires redis.enabled=false" -}}
|
||||
{{- end -}}
|
||||
{{- if and (not .Values.redis.enabled) (not .Values.externalRedis.sentinel.enabled) (not .Values.externalRedis.cluster.enabled) (not .Values.externalRedis.host) -}}
|
||||
{{- fail "redis.enabled=false requires externalRedis.host" -}}
|
||||
{{- end -}}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled (not .Values.externalRedis.sentinel.nodes) -}}
|
||||
{{- fail "external Redis Sentinel requires at least one externalRedis.sentinel.nodes entry" -}}
|
||||
{{- end -}}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.cluster.enabled (not .Values.externalRedis.cluster.nodes) -}}
|
||||
{{- fail "external Redis Cluster requires at least one externalRedis.cluster.nodes entry" -}}
|
||||
{{- end -}}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.cluster.enabled -}}
|
||||
{{- range $node := .Values.externalRedis.cluster.nodes -}}
|
||||
{{- if not (regexMatch "^[A-Za-z0-9._-]+:[0-9]{1,5}$" $node) -}}
|
||||
{{- fail (printf "externalRedis.cluster.nodes entry must use host:port: %s" $node) -}}
|
||||
{{- end -}}
|
||||
{{- $parts := splitList ":" $node -}}
|
||||
{{- $port := last $parts | int -}}
|
||||
{{- if or (lt $port 1) (gt $port 65535) -}}
|
||||
{{- fail (printf "externalRedis.cluster.nodes port must be between 1 and 65535: %s" $node) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if and .Values.s3.endpoint (not (regexMatch $absoluteHttpUrlPattern .Values.s3.endpoint)) -}}
|
||||
{{- fail "s3.endpoint must be an absolute HTTP(S) URL" -}}
|
||||
{{- end -}}
|
||||
|
|
|
|||
|
|
@ -123,18 +123,65 @@ render external-sentinel "$CHART_DIR" \
|
|||
--set postgresql.enabled=false \
|
||||
--set externalDatabase.host=db.example.com \
|
||||
--set redis.enabled=false \
|
||||
--set externalRedis.username=redis-user \
|
||||
--set externalRedis.password=redis-password \
|
||||
--set externalRedis.sentinel.enabled=true \
|
||||
--set externalRedis.sentinel.username=sentinel-user \
|
||||
--set externalRedis.sentinel.password=sentinel-password \
|
||||
--set-json 'externalRedis.sentinel.nodes=["sentinel-a:26379","sentinel-b:26379"]' \
|
||||
--show-only templates/server-deployment.yaml >"$TMP_DIR/external-sentinel.yaml"
|
||||
grep -Fq 'value: "sentinel-a"' "$TMP_DIR/external-sentinel.yaml"
|
||||
grep -Fq 'name: SPRING_DATA_REDIS_PASSWORD' "$TMP_DIR/external-sentinel.yaml"
|
||||
grep -Fq 'name: SPRING_DATA_REDIS_SENTINEL_PASSWORD' "$TMP_DIR/external-sentinel.yaml"
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_USERNAME' "$TMP_DIR/external-sentinel.yaml" \
|
||||
| grep -Fq 'value: "redis-user"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_SENTINEL_USERNAME' "$TMP_DIR/external-sentinel.yaml" \
|
||||
| grep -Fq 'value: "sentinel-user"'
|
||||
if grep -Fq 'name: SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST' "$TMP_DIR/external-sentinel.yaml"; then
|
||||
fail "external Sentinel must preserve Redisson address consistency checks by default"
|
||||
fi
|
||||
|
||||
render external-cluster "$CHART_DIR" \
|
||||
--set postgresql.enabled=false \
|
||||
--set externalDatabase.host=db.example.com \
|
||||
--set redis.enabled=false \
|
||||
--set existingSecret=skillhub-production-secret \
|
||||
--set externalRedis.username=skillhub \
|
||||
--set externalRedis.tls.enabled=true \
|
||||
--set externalRedis.connectTimeout=5s \
|
||||
--set externalRedis.timeout=3s \
|
||||
--set externalRedis.clientName=skillhub-server \
|
||||
--set externalRedis.cluster.enabled=true \
|
||||
--set externalRedis.cluster.maxRedirects=7 \
|
||||
--set-json 'externalRedis.cluster.nodes=["redis-a.example.com:6379","redis-b.example.com:6380"]' \
|
||||
--show-only templates/server-deployment.yaml >"$TMP_DIR/external-cluster.yaml"
|
||||
grep -A1 -F 'name: REDIS_HOST' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "redis-a.example.com"'
|
||||
grep -A1 -F 'name: REDIS_PORT' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "6379"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_CLUSTER_NODES' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "redis-a.example.com:6379,redis-b.example.com:6380"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "7"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_USERNAME' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "skillhub"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_SSL_ENABLED' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "true"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_CONNECT_TIMEOUT' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "5s"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_TIMEOUT' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "3s"'
|
||||
grep -A1 -F 'name: SPRING_DATA_REDIS_CLIENT_NAME' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'value: "skillhub-server"'
|
||||
grep -A4 -F 'name: SPRING_DATA_REDIS_PASSWORD' "$TMP_DIR/external-cluster.yaml" \
|
||||
| grep -Fq 'name: skillhub-production-secret'
|
||||
if grep -Fq 'name: SPRING_DATA_REDIS_HOST' "$TMP_DIR/external-cluster.yaml"; then
|
||||
fail "external Redis Cluster must not render standalone host configuration"
|
||||
fi
|
||||
if grep -Fq 'name: SPRING_DATA_REDIS_SENTINEL_NODES' "$TMP_DIR/external-cluster.yaml"; then
|
||||
fail "external Redis Cluster must not render Sentinel configuration"
|
||||
fi
|
||||
|
||||
render special "$CHART_DIR" \
|
||||
--set-string 'bootstrapAdmin.displayName=Ops: Admin' \
|
||||
--show-only templates/configmap.yaml >"$TMP_DIR/special.yaml"
|
||||
|
|
@ -229,6 +276,26 @@ assert_rejected old-postgres-env --set-json 'postgresql.primary.extraEnv=[{"name
|
|||
assert_rejected old-sentinel-password --set redis.auth.sentinelPassword=unused
|
||||
assert_rejected old-sentinel-nodes --set redis.sentinel.nodes=unused
|
||||
assert_rejected old-sentinel-service-switch --set redis.sentinel.service.enabled=false
|
||||
assert_rejected internal-and-external-cluster \
|
||||
--set externalRedis.cluster.enabled=true \
|
||||
--set-json 'externalRedis.cluster.nodes=["redis-a.example.com:6379"]'
|
||||
assert_rejected sentinel-and-cluster \
|
||||
--set redis.enabled=false \
|
||||
--set externalRedis.sentinel.enabled=true \
|
||||
--set externalRedis.cluster.enabled=true \
|
||||
--set-json 'externalRedis.sentinel.nodes=["sentinel-a.example.com:26379"]' \
|
||||
--set-json 'externalRedis.cluster.nodes=["redis-a.example.com:6379"]'
|
||||
assert_rejected cluster-without-nodes \
|
||||
--set redis.enabled=false \
|
||||
--set externalRedis.cluster.enabled=true
|
||||
assert_rejected cluster-invalid-node \
|
||||
--set redis.enabled=false \
|
||||
--set externalRedis.cluster.enabled=true \
|
||||
--set-json 'externalRedis.cluster.nodes=["redis-a.example.com"]'
|
||||
assert_rejected cluster-invalid-port \
|
||||
--set redis.enabled=false \
|
||||
--set externalRedis.cluster.enabled=true \
|
||||
--set-json 'externalRedis.cluster.nodes=["redis-a.example.com:65536"]'
|
||||
assert_rejected invalid-fullname --set fullnameOverride=INVALID_NAME
|
||||
assert_rejected old-ingress-host --set ingress.host=old.example.com
|
||||
assert_rejected old-ingress-tls-object --set ingress.tls.enabled=true
|
||||
|
|
|
|||
|
|
@ -210,15 +210,27 @@
|
|||
"externalRedis": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["host", "port", "password", "sentinel"],
|
||||
"required": ["host", "port", "username", "password", "tls", "connectTimeout", "timeout", "clientName", "sentinel", "cluster"],
|
||||
"properties": {
|
||||
"host": { "type": "string", "pattern": "^$|^[A-Za-z0-9._-]+$" },
|
||||
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||
"username": { "type": "string" },
|
||||
"password": { "type": "string" },
|
||||
"tls": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled"],
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"connectTimeout": { "type": "string" },
|
||||
"timeout": { "type": "string" },
|
||||
"clientName": { "type": "string" },
|
||||
"sentinel": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "masterSet", "nodes", "password"],
|
||||
"required": ["enabled", "masterSet", "nodes", "username", "password"],
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"masterSet": { "type": "string", "minLength": 1 },
|
||||
|
|
@ -226,8 +238,22 @@
|
|||
"type": "array",
|
||||
"items": { "type": "string", "pattern": "^[^:]+:[0-9]+$" }
|
||||
},
|
||||
"username": { "type": "string" },
|
||||
"password": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"cluster": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "nodes", "maxRedirects"],
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"nodes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "pattern": "^[^:]+:[0-9]+$" }
|
||||
},
|
||||
"maxRedirects": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -257,12 +257,23 @@ redis:
|
|||
externalRedis:
|
||||
host: ""
|
||||
port: 6379
|
||||
username: ""
|
||||
password: ""
|
||||
tls:
|
||||
enabled: false
|
||||
connectTimeout: ""
|
||||
timeout: ""
|
||||
clientName: ""
|
||||
sentinel:
|
||||
enabled: false
|
||||
masterSet: mymaster
|
||||
nodes: []
|
||||
username: ""
|
||||
password: ""
|
||||
cluster:
|
||||
enabled: false
|
||||
nodes: []
|
||||
maxRedirects: 5
|
||||
|
||||
# ============================================================================
|
||||
# Server 配置
|
||||
|
|
|
|||
|
|
@ -53,8 +53,22 @@ services:
|
|||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-skillhub}
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-skillhub}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-skillhub_demo}
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
SPRING_DATA_REDIS_HOST: ${SPRING_DATA_REDIS_HOST:-redis}
|
||||
SPRING_DATA_REDIS_PORT: ${SPRING_DATA_REDIS_PORT:-6379}
|
||||
SPRING_DATA_REDIS_USERNAME:
|
||||
SPRING_DATA_REDIS_PASSWORD:
|
||||
SPRING_DATA_REDIS_DATABASE:
|
||||
SPRING_DATA_REDIS_SENTINEL_MASTER:
|
||||
SPRING_DATA_REDIS_SENTINEL_NODES:
|
||||
SPRING_DATA_REDIS_SENTINEL_USERNAME:
|
||||
SPRING_DATA_REDIS_SENTINEL_PASSWORD:
|
||||
SPRING_DATA_REDIS_CLUSTER_NODES:
|
||||
SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS:
|
||||
SPRING_DATA_REDIS_SSL_ENABLED:
|
||||
SPRING_DATA_REDIS_CONNECT_TIMEOUT:
|
||||
SPRING_DATA_REDIS_TIMEOUT:
|
||||
SPRING_DATA_REDIS_CLIENT_NAME:
|
||||
SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST:
|
||||
SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-false}
|
||||
SKILLHUB_PUBLIC_BASE_URL: ${SKILLHUB_PUBLIC_BASE_URL:-}
|
||||
DEVICE_AUTH_VERIFICATION_URI: ${DEVICE_AUTH_VERIFICATION_URI:-}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ cp secret.yaml.example secret.yaml
|
|||
| spring-datasource-url | PostgreSQL 连接 URL | 是 |
|
||||
| spring-datasource-username | 数据库用户名 | 是 |
|
||||
| spring-datasource-password | 数据库密码 | 是 |
|
||||
| redis-password | Redis 数据节点密码 | 否 |
|
||||
| redis-sentinel-password | Redis Sentinel 独立密码 | 否 |
|
||||
| bootstrap-admin-password | 管理员密码 | 是 |
|
||||
| oauth2-github-client-id | GitHub OAuth ID | 否 |
|
||||
| oauth2-github-client-secret | GitHub OAuth 密钥 | 否 |
|
||||
|
|
@ -86,9 +88,38 @@ redis-host: your-redis-host
|
|||
redis-port: "6379"
|
||||
```
|
||||
|
||||
2. 修改 `base/secret.yaml` 中的数据库连接:
|
||||
连接外部 Redis Cluster 时改为配置节点列表。保留 `redis-host`/`redis-port`
|
||||
不会影响 Cluster 选择:
|
||||
|
||||
```yaml
|
||||
redis-cluster-nodes: "redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379"
|
||||
redis-cluster-max-redirects: "5"
|
||||
redis-username: "skillhub"
|
||||
redis-ssl-enabled: "true"
|
||||
redis-connect-timeout: "5s"
|
||||
redis-timeout: "3s"
|
||||
redis-client-name: "skillhub"
|
||||
```
|
||||
|
||||
所有 Cluster 节点通告的地址都必须能从 Server Pod 访问。Redis Cluster 只支持
|
||||
数据库 `0`。
|
||||
|
||||
连接外部 Redis Sentinel 时配置 master、节点和独立 ACL;Sentinel 配置优先于
|
||||
Cluster 和单机 host/port:
|
||||
|
||||
```yaml
|
||||
redis-username: "skillhub"
|
||||
redis-sentinel-master: "mymaster"
|
||||
redis-sentinel-nodes: "sentinel-0.example.com:26379,sentinel-1.example.com:26379,sentinel-2.example.com:26379"
|
||||
redis-sentinel-username: "sentinel-user"
|
||||
redis-sentinel-check-list: "true"
|
||||
```
|
||||
|
||||
2. 修改 `base/secret.yaml` 中的数据库和 Redis 凭据:
|
||||
```yaml
|
||||
spring-datasource-url: jdbc:postgresql://your-postgres-host:5432/skillhub
|
||||
redis-password: your-redis-password
|
||||
redis-sentinel-password: your-sentinel-password
|
||||
```
|
||||
|
||||
3. 部署:
|
||||
|
|
@ -171,6 +202,17 @@ kubectl apply -k overlays/with-infra/ # 或 overlays/external/
|
|||
|---|---|---|
|
||||
| redis-host | redis | Redis 主机地址 |
|
||||
| redis-port | 6379 | Redis 端口 |
|
||||
| redis-cluster-nodes | 未设置 | 外部 Redis Cluster 节点,逗号分隔的 `host:port` |
|
||||
| redis-cluster-max-redirects | 未设置 | Cluster MOVED/ASK 最大重定向次数 |
|
||||
| redis-sentinel-master | 未设置 | Sentinel master set 名称 |
|
||||
| redis-sentinel-nodes | 未设置 | Sentinel 节点,逗号分隔的 `host:port` |
|
||||
| redis-sentinel-username | 未设置 | Sentinel ACL 用户名 |
|
||||
| redis-sentinel-check-list | true | 是否校验 Sentinel 返回的节点列表 |
|
||||
| redis-username | 未设置 | Redis ACL 用户名 |
|
||||
| redis-ssl-enabled | 未设置 | 是否使用 TLS |
|
||||
| redis-connect-timeout | 未设置 | Redis 建连超时 |
|
||||
| redis-timeout | 未设置 | Redis 命令超时 |
|
||||
| redis-client-name | 未设置 | Redis 客户端名称 |
|
||||
| storage-base-path | /var/lib/skillhub/storage | 技能存储路径 |
|
||||
| skillhub-storage-provider | local | 存储类型(local/s3) |
|
||||
| skill-scanner-enabled | true | 是否启用扫描器 |
|
||||
|
|
@ -190,6 +232,8 @@ kubectl apply -k overlays/with-infra/ # 或 overlays/external/
|
|||
| spring-datasource-url | PostgreSQL 连接 URL | 是 |
|
||||
| spring-datasource-username | 数据库用户名 | 是 |
|
||||
| spring-datasource-password | 数据库密码 | 是 |
|
||||
| redis-password | Redis 数据节点密码 | 否 |
|
||||
| redis-sentinel-password | Redis Sentinel 独立密码 | 否 |
|
||||
| bootstrap-admin-password | 管理员密码 | 是 |
|
||||
| oauth2-github-client-id | GitHub OAuth ID | 否 |
|
||||
| oauth2-github-client-secret | GitHub OAuth 密钥 | 否 |
|
||||
|
|
|
|||
|
|
@ -53,6 +53,84 @@ spec:
|
|||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-port
|
||||
- name: SPRING_DATA_REDIS_CLUSTER_NODES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-cluster-nodes
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-cluster-max-redirects
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_SENTINEL_MASTER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-sentinel-master
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_SENTINEL_NODES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-sentinel-nodes
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_SENTINEL_USERNAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-sentinel-username
|
||||
optional: true
|
||||
- name: SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-sentinel-check-list
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_USERNAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-username
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_SSL_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-ssl-enabled
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_CONNECT_TIMEOUT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-connect-timeout
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_TIMEOUT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-timeout
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_CLIENT_NAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: skillhub-config
|
||||
key: redis-client-name
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: skillhub-secret
|
||||
key: redis-password
|
||||
optional: true
|
||||
- name: SPRING_DATA_REDIS_SENTINEL_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: skillhub-secret
|
||||
key: redis-sentinel-password
|
||||
optional: true
|
||||
|
||||
# Storage
|
||||
- name: STORAGE_BASE_PATH
|
||||
|
|
|
|||
|
|
@ -8,6 +8,21 @@ data:
|
|||
# 使用内置 Redis(overlays/with-infra):保持 redis
|
||||
redis-host: redis
|
||||
redis-port: "6379"
|
||||
# 连接外部 Redis Cluster 时取消下面几行注释。Cluster 节点存在时,
|
||||
# Spring Boot 会自动忽略上面的单机 host/port。
|
||||
# redis-cluster-nodes: "redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379"
|
||||
# redis-cluster-max-redirects: "5"
|
||||
# redis-username: "skillhub"
|
||||
# redis-ssl-enabled: "true"
|
||||
# redis-connect-timeout: "5s"
|
||||
# redis-timeout: "3s"
|
||||
# redis-client-name: "skillhub"
|
||||
# 连接外部 Redis Sentinel 时取消下面几行注释。Sentinel 配置优先于
|
||||
# Cluster 和单机 host/port。
|
||||
# redis-sentinel-master: "mymaster"
|
||||
# redis-sentinel-nodes: "sentinel-0.example.com:26379,sentinel-1.example.com:26379,sentinel-2.example.com:26379"
|
||||
# redis-sentinel-username: "sentinel-user"
|
||||
# redis-sentinel-check-list: "true"
|
||||
|
||||
# 技能存储路径
|
||||
storage-base-path: /var/lib/skillhub/storage
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ stringData:
|
|||
spring-datasource-username: skillhub
|
||||
spring-datasource-password: change-me
|
||||
|
||||
# Redis 数据节点密码(单机、Sentinel 与 Cluster 共用;无密码时留空)
|
||||
redis-password: ""
|
||||
# Redis Sentinel 独立密码;与数据节点相同时也可填写同一个值
|
||||
redis-sentinel-password: ""
|
||||
|
||||
# Bootstrap 管理员密码(敏感)
|
||||
bootstrap-admin-password: ChangeMe!2026
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,56 @@ docker compose --env-file .env.release -f compose.release.yml up -d
|
|||
- Web UI: `SKILLHUB_PUBLIC_BASE_URL`
|
||||
- Backend API: `http://localhost:8080`
|
||||
|
||||
### 5.2 关键文件
|
||||
### 5.2 连接外部 Redis Cluster
|
||||
|
||||
发布 Compose 默认仍使用内置单机 Redis。连接外部 Redis Cluster 时,在
|
||||
`.env.release` 中设置标准 Spring Boot 配置,不需要额外的模式开关:
|
||||
|
||||
```dotenv
|
||||
SPRING_DATA_REDIS_CLUSTER_NODES=redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379
|
||||
SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=5
|
||||
SPRING_DATA_REDIS_USERNAME=skillhub
|
||||
SPRING_DATA_REDIS_PASSWORD=replace-with-secret
|
||||
SPRING_DATA_REDIS_SSL_ENABLED=true
|
||||
SPRING_DATA_REDIS_CONNECT_TIMEOUT=5s
|
||||
SPRING_DATA_REDIS_TIMEOUT=3s
|
||||
```
|
||||
|
||||
Cluster 节点返回给客户端的所有地址必须能从 `server` 容器访问。Redis Cluster
|
||||
只支持数据库 `0`;不要为 Cluster 设置非零的
|
||||
`SPRING_DATA_REDIS_DATABASE`。配置 Cluster 节点后,Spring Boot 自动忽略单机
|
||||
`host`/`port`,Compose 中的内置 Redis 容器仍会启动,但不会被 Server 使用。
|
||||
|
||||
对真实 Cluster 运行功能检查:
|
||||
|
||||
```bash
|
||||
REDIS_CLUSTER_TEST_NODES=redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379 \
|
||||
REDIS_CLUSTER_TEST_USERNAME=skillhub \
|
||||
REDIS_CLUSTER_TEST_PASSWORD=replace-with-secret \
|
||||
make test-redis-cluster
|
||||
```
|
||||
|
||||
该检查覆盖 Spring Data 读写、Spring Session 保存/读取/删除和 Redisson Stream。
|
||||
|
||||
### 5.3 连接外部 Redis Sentinel
|
||||
|
||||
Sentinel 使用标准 Spring Boot 配置。数据节点和 Sentinel 可以使用不同 ACL:
|
||||
|
||||
```dotenv
|
||||
SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
|
||||
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-0.example.com:26379,sentinel-1.example.com:26379,sentinel-2.example.com:26379
|
||||
SPRING_DATA_REDIS_USERNAME=skillhub
|
||||
SPRING_DATA_REDIS_PASSWORD=replace-with-data-node-secret
|
||||
SPRING_DATA_REDIS_SENTINEL_USERNAME=sentinel-user
|
||||
SPRING_DATA_REDIS_SENTINEL_PASSWORD=replace-with-sentinel-secret
|
||||
SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=true
|
||||
```
|
||||
|
||||
Sentinel 配置优先于 Cluster 和单机 `host`/`port`。在 Kubernetes 等 Sentinel
|
||||
返回地址与客户端入口不一致的环境中,可以将
|
||||
`SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST` 设为 `false`。
|
||||
|
||||
### 5.4 关键文件
|
||||
|
||||
- `compose.release.yml`
|
||||
- 使用发布镜像,不在用户机器上执行本地构建
|
||||
|
|
@ -121,7 +170,7 @@ docker compose --env-file .env.release -f compose.release.yml up -d
|
|||
- 在启动前校验 `.env.release`
|
||||
- 可提前拦截占位值、URL 格式错误、缺失的 OSS 凭据、危险的明文默认值
|
||||
|
||||
### 5.3 镜像标签约定
|
||||
### 5.5 镜像标签约定
|
||||
|
||||
- `edge`
|
||||
- `main` 分支最新构建
|
||||
|
|
|
|||
21
scripts/redis-cluster-integration-test.sh
Executable file
21
scripts/redis-cluster-integration-test.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
NODES=${REDIS_CLUSTER_TEST_NODES:-${SPRING_DATA_REDIS_CLUSTER_NODES:-}}
|
||||
|
||||
if [[ -z "$NODES" ]]; then
|
||||
echo "ERROR: REDIS_CLUSTER_TEST_NODES or SPRING_DATA_REDIS_CLUSTER_NODES is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
export REDIS_CLUSTER_TEST_NODES="$NODES"
|
||||
export REDIS_CLUSTER_TEST_USERNAME="${REDIS_CLUSTER_TEST_USERNAME:-${SPRING_DATA_REDIS_USERNAME:-}}"
|
||||
export REDIS_CLUSTER_TEST_PASSWORD="${REDIS_CLUSTER_TEST_PASSWORD:-${SPRING_DATA_REDIS_PASSWORD:-}}"
|
||||
export REDIS_CLUSTER_TEST_SSL_ENABLED="${REDIS_CLUSTER_TEST_SSL_ENABLED:-${SPRING_DATA_REDIS_SSL_ENABLED:-false}}"
|
||||
|
||||
cd "$REPO_ROOT/server"
|
||||
JDK_JAVA_OPTIONS="${JDK_JAVA_OPTIONS:-} -XX:+EnableDynamicAgentLoading" \
|
||||
./mvnw -pl skillhub-app -am test \
|
||||
-Dtest=RedisClusterIntegrationTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false
|
||||
|
|
@ -86,6 +86,70 @@ write_env "$invalid_forwarded_proto_env" "release-download-secret-32-bytes-minim
|
|||
printf '%s\n' "SKILLHUB_TRUST_FORWARDED_PROTO=yes" >>"$invalid_forwarded_proto_env"
|
||||
expect_fail "$invalid_forwarded_proto_env" "SKILLHUB_TRUST_FORWARDED_PROTO must be true or false"
|
||||
|
||||
valid_redis_cluster_env="$tmp/valid-redis-cluster.env"
|
||||
write_env "$valid_redis_cluster_env" "release-download-secret-32-bytes-minimum"
|
||||
cat >>"$valid_redis_cluster_env" <<'EOF'
|
||||
SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:6379,redis-b.example.com:6380
|
||||
SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=5
|
||||
SPRING_DATA_REDIS_SSL_ENABLED=true
|
||||
EOF
|
||||
"$SCRIPT" "$valid_redis_cluster_env" >/dev/null
|
||||
|
||||
invalid_redis_cluster_node_env="$tmp/invalid-redis-cluster-node.env"
|
||||
write_env "$invalid_redis_cluster_node_env" "release-download-secret-32-bytes-minimum"
|
||||
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com" >>"$invalid_redis_cluster_node_env"
|
||||
expect_fail "$invalid_redis_cluster_node_env" "SPRING_DATA_REDIS_CLUSTER_NODES entries must use host:port"
|
||||
|
||||
invalid_redis_cluster_port_env="$tmp/invalid-redis-cluster-port.env"
|
||||
write_env "$invalid_redis_cluster_port_env" "release-download-secret-32-bytes-minimum"
|
||||
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:65536" >>"$invalid_redis_cluster_port_env"
|
||||
expect_fail "$invalid_redis_cluster_port_env" "SPRING_DATA_REDIS_CLUSTER_NODES port must be between 1 and 65535"
|
||||
|
||||
invalid_redis_redirects_env="$tmp/invalid-redis-redirects.env"
|
||||
write_env "$invalid_redis_redirects_env" "release-download-secret-32-bytes-minimum"
|
||||
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=-1" >>"$invalid_redis_redirects_env"
|
||||
expect_fail "$invalid_redis_redirects_env" "SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS must be a non-negative integer"
|
||||
|
||||
invalid_redis_database_env="$tmp/invalid-redis-database.env"
|
||||
write_env "$invalid_redis_database_env" "release-download-secret-32-bytes-minimum"
|
||||
cat >>"$invalid_redis_database_env" <<'EOF'
|
||||
SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:6379
|
||||
SPRING_DATA_REDIS_DATABASE=1
|
||||
EOF
|
||||
expect_fail "$invalid_redis_database_env" "SPRING_DATA_REDIS_DATABASE must be 0 when SPRING_DATA_REDIS_CLUSTER_NODES is set"
|
||||
|
||||
valid_redis_sentinel_env="$tmp/valid-redis-sentinel.env"
|
||||
write_env "$valid_redis_sentinel_env" "release-download-secret-32-bytes-minimum"
|
||||
cat >>"$valid_redis_sentinel_env" <<'EOF'
|
||||
SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
|
||||
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:26379,sentinel-b.example.com:26379
|
||||
SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=false
|
||||
EOF
|
||||
"$SCRIPT" "$valid_redis_sentinel_env" >/dev/null
|
||||
|
||||
missing_redis_sentinel_nodes_env="$tmp/missing-redis-sentinel-nodes.env"
|
||||
write_env "$missing_redis_sentinel_nodes_env" "release-download-secret-32-bytes-minimum"
|
||||
printf '%s\n' "SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster" >>"$missing_redis_sentinel_nodes_env"
|
||||
expect_fail "$missing_redis_sentinel_nodes_env" "SPRING_DATA_REDIS_SENTINEL_NODES is required when SPRING_DATA_REDIS_SENTINEL_MASTER is set"
|
||||
|
||||
missing_redis_sentinel_master_env="$tmp/missing-redis-sentinel-master.env"
|
||||
write_env "$missing_redis_sentinel_master_env" "release-download-secret-32-bytes-minimum"
|
||||
printf '%s\n' "SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:26379" >>"$missing_redis_sentinel_master_env"
|
||||
expect_fail "$missing_redis_sentinel_master_env" "SPRING_DATA_REDIS_SENTINEL_MASTER is required when SPRING_DATA_REDIS_SENTINEL_NODES is set"
|
||||
|
||||
invalid_redis_sentinel_port_env="$tmp/invalid-redis-sentinel-port.env"
|
||||
write_env "$invalid_redis_sentinel_port_env" "release-download-secret-32-bytes-minimum"
|
||||
cat >>"$invalid_redis_sentinel_port_env" <<'EOF'
|
||||
SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
|
||||
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:70000
|
||||
EOF
|
||||
expect_fail "$invalid_redis_sentinel_port_env" "SPRING_DATA_REDIS_SENTINEL_NODES port must be between 1 and 65535"
|
||||
|
||||
invalid_redis_sentinel_check_env="$tmp/invalid-redis-sentinel-check.env"
|
||||
write_env "$invalid_redis_sentinel_check_env" "release-download-secret-32-bytes-minimum"
|
||||
printf '%s\n' "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=yes" >>"$invalid_redis_sentinel_check_env"
|
||||
expect_fail "$invalid_redis_sentinel_check_env" "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST must be true or false"
|
||||
|
||||
draft_env="$tmp/draft.env"
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
case "$line" in
|
||||
|
|
|
|||
|
|
@ -126,6 +126,74 @@ validate_min_length() {
|
|||
fi
|
||||
}
|
||||
|
||||
validate_non_negative_integer() {
|
||||
var_name="$1"
|
||||
eval "var_value=\${$var_name:-}"
|
||||
case "$var_value" in
|
||||
"") ;;
|
||||
*[!0-9]*) error "$var_name must be a non-negative integer" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
validate_redis_nodes() {
|
||||
var_name="$1"
|
||||
eval "nodes=\${$var_name:-}"
|
||||
if [ -z "$nodes" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
old_ifs="$IFS"
|
||||
IFS=","
|
||||
for node in $nodes; do
|
||||
host=${node%:*}
|
||||
port=${node##*:}
|
||||
if [ -z "$host" ] || [ "$host" = "$node" ]; then
|
||||
error "$var_name entries must use host:port"
|
||||
continue
|
||||
fi
|
||||
case "$host" in
|
||||
*[!A-Za-z0-9._-]*)
|
||||
error "$var_name contains an invalid host: $host"
|
||||
;;
|
||||
esac
|
||||
case "$port" in
|
||||
*[!0-9]*|"")
|
||||
error "$var_name contains an invalid port: $node"
|
||||
;;
|
||||
*)
|
||||
if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
||||
error "$var_name port must be between 1 and 65535: $node"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
IFS="$old_ifs"
|
||||
}
|
||||
|
||||
validate_redis_sentinel_configuration() {
|
||||
master="${SPRING_DATA_REDIS_SENTINEL_MASTER:-}"
|
||||
nodes="${SPRING_DATA_REDIS_SENTINEL_NODES:-}"
|
||||
|
||||
if [ -n "$master" ] && [ -z "$nodes" ]; then
|
||||
error "SPRING_DATA_REDIS_SENTINEL_NODES is required when SPRING_DATA_REDIS_SENTINEL_MASTER is set"
|
||||
fi
|
||||
if [ -n "$nodes" ] && [ -z "$master" ]; then
|
||||
error "SPRING_DATA_REDIS_SENTINEL_MASTER is required when SPRING_DATA_REDIS_SENTINEL_NODES is set"
|
||||
fi
|
||||
validate_redis_nodes SPRING_DATA_REDIS_SENTINEL_NODES
|
||||
}
|
||||
|
||||
validate_redis_cluster_database() {
|
||||
if [ -z "${SPRING_DATA_REDIS_CLUSTER_NODES:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${SPRING_DATA_REDIS_DATABASE:-0}" in
|
||||
0) ;;
|
||||
*) error "SPRING_DATA_REDIS_DATABASE must be 0 when SPRING_DATA_REDIS_CLUSTER_NODES is set" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_non_empty SKILLHUB_PUBLIC_BASE_URL
|
||||
validate_url SKILLHUB_PUBLIC_BASE_URL
|
||||
validate_no_trailing_slash SKILLHUB_PUBLIC_BASE_URL
|
||||
|
|
@ -154,11 +222,17 @@ validate_boolean BOOTSTRAP_ADMIN_ENABLED
|
|||
validate_boolean SKILLHUB_TRUST_FORWARDED_PROTO
|
||||
validate_boolean SKILLHUB_STORAGE_S3_FORCE_PATH_STYLE
|
||||
validate_boolean SKILLHUB_STORAGE_S3_AUTO_CREATE_BUCKET
|
||||
validate_boolean SPRING_DATA_REDIS_SSL_ENABLED
|
||||
validate_boolean SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST
|
||||
|
||||
validate_port POSTGRES_PORT
|
||||
validate_port REDIS_PORT
|
||||
validate_port API_PORT
|
||||
validate_port WEB_PORT
|
||||
validate_non_negative_integer SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS
|
||||
validate_redis_nodes SPRING_DATA_REDIS_CLUSTER_NODES
|
||||
validate_redis_cluster_database
|
||||
validate_redis_sentinel_configuration
|
||||
|
||||
require_non_empty POSTGRES_DB
|
||||
require_non_empty POSTGRES_USER
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
#!/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"
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@ package com.iflytek.skillhub.config;
|
|||
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.ClusterServersConfig;
|
||||
import org.redisson.config.Config;
|
||||
import org.redisson.config.SentinelServersConfig;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
|
@ -19,6 +21,7 @@ import java.util.List;
|
|||
public class RedissonConfig {
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
@ConditionalOnMissingBean(RedissonClient.class)
|
||||
public RedissonClient redissonClient(
|
||||
RedisProperties redisProperties,
|
||||
@Value("${skillhub.redis.sentinel.check-sentinels-list:true}") boolean checkSentinelsList) {
|
||||
|
|
@ -35,6 +38,10 @@ public class RedissonConfig {
|
|||
configureSentinelServers(config, redisProperties, checkSentinelsList);
|
||||
return config;
|
||||
}
|
||||
if (hasClusterConfiguration(redisProperties)) {
|
||||
configureClusterServers(config, redisProperties);
|
||||
return config;
|
||||
}
|
||||
|
||||
SingleServerConfig singleServerConfig = config.useSingleServer()
|
||||
.setAddress(resolveAddress(redisProperties))
|
||||
|
|
@ -64,6 +71,20 @@ public class RedissonConfig {
|
|||
if (StringUtils.hasText(redisProperties.getSentinel().getPassword())) {
|
||||
sentinelServersConfig.setSentinelPassword(redisProperties.getSentinel().getPassword());
|
||||
}
|
||||
if (StringUtils.hasText(redisProperties.getSentinel().getUsername())) {
|
||||
sentinelServersConfig.setSentinelUsername(redisProperties.getSentinel().getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureClusterServers(Config config, RedisProperties redisProperties) {
|
||||
ClusterServersConfig clusterServersConfig = config.useClusterServers();
|
||||
redisProperties.getCluster().getNodes().stream()
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::hasText)
|
||||
.map(node -> withRedisScheme(node, redisProperties))
|
||||
.forEach(clusterServersConfig::addNodeAddress);
|
||||
|
||||
applySharedSettings(clusterServersConfig, redisProperties);
|
||||
}
|
||||
|
||||
private static boolean hasSentinelConfiguration(RedisProperties redisProperties) {
|
||||
|
|
@ -73,6 +94,12 @@ public class RedissonConfig {
|
|||
&& !redisProperties.getSentinel().getNodes().isEmpty();
|
||||
}
|
||||
|
||||
private static boolean hasClusterConfiguration(RedisProperties redisProperties) {
|
||||
return redisProperties.getCluster() != null
|
||||
&& redisProperties.getCluster().getNodes() != null
|
||||
&& redisProperties.getCluster().getNodes().stream().anyMatch(StringUtils::hasText);
|
||||
}
|
||||
|
||||
private static void applySharedSettings(org.redisson.config.BaseConfig<?> serverConfig,
|
||||
RedisProperties redisProperties) {
|
||||
if (StringUtils.hasText(redisProperties.getUsername())) {
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# 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,11 +11,8 @@ spring:
|
|||
password: skillhub_dev
|
||||
data:
|
||||
redis:
|
||||
mode: standalone
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
session:
|
||||
store-type: redis
|
||||
security:
|
||||
|
|
|
|||
|
|
@ -39,14 +39,10 @@ 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,136 @@
|
|||
package com.iflytek.skillhub.config;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RStream;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.client.codec.StringCodec;
|
||||
import org.redisson.config.Config;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.data.redis.connection.RedisClusterConfiguration;
|
||||
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.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.redisson.api.stream.StreamAddArgs.entry;
|
||||
|
||||
@Tag("redis-cluster")
|
||||
@EnabledIfEnvironmentVariable(named = "REDIS_CLUSTER_TEST_NODES", matches = ".+")
|
||||
class RedisClusterIntegrationTest {
|
||||
|
||||
@Test
|
||||
void clusterSupportsSpringDataSessionsAndRedissonStreams() {
|
||||
List<String> nodes = clusterNodes();
|
||||
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(nodes);
|
||||
String username = System.getenv("REDIS_CLUSTER_TEST_USERNAME");
|
||||
String password = System.getenv("REDIS_CLUSTER_TEST_PASSWORD");
|
||||
if (username != null && !username.isBlank()) {
|
||||
clusterConfiguration.setUsername(username);
|
||||
}
|
||||
if (password != null && !password.isBlank()) {
|
||||
clusterConfiguration.setPassword(password);
|
||||
}
|
||||
|
||||
boolean sslEnabled = Boolean.parseBoolean(System.getenv("REDIS_CLUSTER_TEST_SSL_ENABLED"));
|
||||
LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfiguration =
|
||||
LettuceClientConfiguration.builder();
|
||||
if (sslEnabled) {
|
||||
clientConfiguration.useSsl();
|
||||
}
|
||||
|
||||
LettuceConnectionFactory connectionFactory =
|
||||
new LettuceConnectionFactory(clusterConfiguration, clientConfiguration.build());
|
||||
connectionFactory.afterPropertiesSet();
|
||||
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Cluster cluster = new RedisProperties.Cluster();
|
||||
cluster.setNodes(nodes);
|
||||
properties.setCluster(cluster);
|
||||
properties.setUsername(username);
|
||||
properties.setPassword(password);
|
||||
properties.getSsl().setEnabled(sslEnabled);
|
||||
|
||||
Config redissonConfig = RedissonConfig.createConfig(properties);
|
||||
String keyPrefix = "skillhub:redis-cluster-smoke:" + UUID.randomUUID();
|
||||
RedissonClient redissonClient = Redisson.create(redissonConfig);
|
||||
try {
|
||||
verifySpringData(connectionFactory, keyPrefix);
|
||||
verifySpringSession(connectionFactory, keyPrefix);
|
||||
verifyRedissonStream(redissonClient, keyPrefix);
|
||||
} finally {
|
||||
redissonClient.shutdown();
|
||||
connectionFactory.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private void verifySpringData(LettuceConnectionFactory connectionFactory, String keyPrefix) {
|
||||
StringRedisTemplate template = new StringRedisTemplate(connectionFactory);
|
||||
String key = keyPrefix + ":value";
|
||||
|
||||
template.opsForValue().set(key, "ok");
|
||||
|
||||
assertThat(template.opsForValue().get(key)).isEqualTo("ok");
|
||||
template.delete(key);
|
||||
}
|
||||
|
||||
private void verifySpringSession(LettuceConnectionFactory connectionFactory, String keyPrefix) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.afterPropertiesSet();
|
||||
|
||||
RedisIndexedSessionRepository repository = new RedisIndexedSessionRepository(template);
|
||||
repository.setRedisKeyNamespace(keyPrefix + ":session");
|
||||
repository.afterPropertiesSet();
|
||||
try {
|
||||
SessionRepository<Session> sessionRepository = sessionRepository(repository);
|
||||
Session session = sessionRepository.createSession();
|
||||
session.setAttribute("userId", "cluster-user");
|
||||
sessionRepository.save(session);
|
||||
|
||||
Session loaded = sessionRepository.findById(session.getId());
|
||||
assertThat(loaded).isNotNull();
|
||||
assertThat(loaded.<String>getAttribute("userId")).isEqualTo("cluster-user");
|
||||
|
||||
sessionRepository.deleteById(session.getId());
|
||||
assertThat(sessionRepository.findById(session.getId())).isNull();
|
||||
} finally {
|
||||
repository.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyRedissonStream(RedissonClient redissonClient, String keyPrefix) {
|
||||
RStream<String, String> stream = redissonClient.getStream(keyPrefix + ":stream", StringCodec.INSTANCE);
|
||||
try {
|
||||
stream.add(entry("status", "ok"));
|
||||
assertThat(stream.size()).isEqualTo(1);
|
||||
} finally {
|
||||
stream.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> clusterNodes() {
|
||||
return Arrays.stream(System.getenv("REDIS_CLUSTER_TEST_NODES").split(","))
|
||||
.map(String::trim)
|
||||
.filter(node -> !node.isEmpty())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private SessionRepository<Session> sessionRepository(RedisIndexedSessionRepository repository) {
|
||||
return (SessionRepository) repository;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.iflytek.skillhub.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.data.redis.connection.RedisClusterConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RedisConnectionConfigurationTest {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void autoConfiguration_keepsStandaloneAsTheDefault() {
|
||||
contextRunner
|
||||
.withPropertyValues(
|
||||
"spring.data.redis.host=redis.internal",
|
||||
"spring.data.redis.port=6380",
|
||||
"spring.data.redis.database=4")
|
||||
.run(context -> {
|
||||
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
|
||||
RedisStandaloneConfiguration standalone = factory.getStandaloneConfiguration();
|
||||
|
||||
assertThat(factory.getClusterConfiguration()).isNull();
|
||||
assertThat(factory.getSentinelConfiguration()).isNull();
|
||||
assertThat(standalone).isNotNull();
|
||||
assertThat(standalone.getHostName()).isEqualTo("redis.internal");
|
||||
assertThat(standalone.getPort()).isEqualTo(6380);
|
||||
assertThat(standalone.getDatabase()).isEqualTo(4);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoConfiguration_selectsClusterFromStandardSpringProperties() {
|
||||
contextRunner
|
||||
.withPropertyValues(
|
||||
"spring.data.redis.cluster.nodes=redis-1:6379,redis-2:6380",
|
||||
"spring.data.redis.cluster.max-redirects=5",
|
||||
"spring.data.redis.username=skillhub",
|
||||
"spring.data.redis.password=secret")
|
||||
.run(context -> {
|
||||
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
|
||||
RedisClusterConfiguration cluster = factory.getClusterConfiguration();
|
||||
|
||||
assertThat(factory.isClusterAware()).isTrue();
|
||||
assertThat(factory.getSentinelConfiguration()).isNull();
|
||||
assertThat(cluster).isNotNull();
|
||||
assertThat(cluster.getClusterNodes())
|
||||
.extracting(node -> node.getHost() + ":" + node.getPort())
|
||||
.containsExactly("redis-1:6379", "redis-2:6380");
|
||||
assertThat(cluster.getMaxRedirects()).isEqualTo(5);
|
||||
assertThat(cluster.getUsername()).isEqualTo("skillhub");
|
||||
assertThat(cluster.getPassword().map(String::new).orElse(null)).isEqualTo("secret");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoConfiguration_prefersSentinelWhenSentinelAndClusterAreBothConfigured() {
|
||||
contextRunner
|
||||
.withPropertyValues(
|
||||
"spring.data.redis.sentinel.master=mymaster",
|
||||
"spring.data.redis.sentinel.nodes=sentinel-1:26379,sentinel-2:26379",
|
||||
"spring.data.redis.cluster.nodes=redis-1:6379,redis-2:6380")
|
||||
.run(context -> {
|
||||
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
|
||||
RedisSentinelConfiguration sentinel = factory.getSentinelConfiguration();
|
||||
|
||||
assertThat(factory.isRedisSentinelAware()).isTrue();
|
||||
assertThat(factory.getClusterConfiguration()).isNull();
|
||||
assertThat(sentinel).isNotNull();
|
||||
assertThat(sentinel.getMaster().getName()).isEqualTo("mymaster");
|
||||
assertThat(sentinel.getSentinels())
|
||||
.extracting(node -> node.getHost() + ":" + node.getPort())
|
||||
.containsExactlyInAnyOrder("sentinel-1:26379", "sentinel-2:26379");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.iflytek.skillhub.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.redisson.config.ClusterServersConfig;
|
||||
import org.redisson.config.Config;
|
||||
import org.redisson.config.SentinelServersConfig;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
|
|
@ -122,6 +123,77 @@ class RedissonConfigTest {
|
|||
assertThat(sentinelConfig.getPassword()).isEqualTo("master-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_appliesSentinelUsernameWhenSet() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
|
||||
sentinel.setMaster("mymaster");
|
||||
sentinel.setNodes(List.of("redis-sentinel-1:26379"));
|
||||
sentinel.setUsername("sentinel-user");
|
||||
properties.setSentinel(sentinel);
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties);
|
||||
SentinelServersConfig sentinelConfig = sentinelConfig(config);
|
||||
|
||||
assertThat(sentinelConfig.getSentinelUsername()).isEqualTo("sentinel-user");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_usesClusterServersWhenClusterNodesArePresent() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Cluster cluster = new RedisProperties.Cluster();
|
||||
cluster.setNodes(List.of("redis-cluster-1:6379", " redis-cluster-2:6380 "));
|
||||
properties.setCluster(cluster);
|
||||
properties.setUsername("skillhub");
|
||||
properties.setPassword("secret");
|
||||
properties.setClientName("skillhub-stream");
|
||||
properties.setTimeout(Duration.ofSeconds(3));
|
||||
properties.setConnectTimeout(Duration.ofSeconds(5));
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties);
|
||||
ClusterServersConfig clusterConfig = clusterConfig(config);
|
||||
|
||||
assertThat(config.isClusterConfig()).isTrue();
|
||||
assertThat(clusterConfig.getNodeAddresses())
|
||||
.containsExactly("redis://redis-cluster-1:6379", "redis://redis-cluster-2:6380");
|
||||
assertThat(clusterConfig.getUsername()).isEqualTo("skillhub");
|
||||
assertThat(clusterConfig.getPassword()).isEqualTo("secret");
|
||||
assertThat(clusterConfig.getClientName()).isEqualTo("skillhub-stream");
|
||||
assertThat(clusterConfig.getTimeout()).isEqualTo(3000);
|
||||
assertThat(clusterConfig.getConnectTimeout()).isEqualTo(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_usesSecureSchemeForClusterAddressesWhenSslEnabled() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Cluster cluster = new RedisProperties.Cluster();
|
||||
cluster.setNodes(List.of("redis-cluster-1:6379"));
|
||||
properties.setCluster(cluster);
|
||||
properties.getSsl().setEnabled(true);
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties);
|
||||
ClusterServersConfig clusterConfig = clusterConfig(config);
|
||||
|
||||
assertThat(clusterConfig.getNodeAddresses()).containsExactly("rediss://redis-cluster-1:6379");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_prefersSentinelWhenSentinelAndClusterAreBothConfigured() {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
|
||||
sentinel.setMaster("mymaster");
|
||||
sentinel.setNodes(List.of("redis-sentinel-1:26379"));
|
||||
properties.setSentinel(sentinel);
|
||||
RedisProperties.Cluster cluster = new RedisProperties.Cluster();
|
||||
cluster.setNodes(List.of("redis-cluster-1:6379"));
|
||||
properties.setCluster(cluster);
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties);
|
||||
|
||||
assertThat(config.isSentinelConfig()).isTrue();
|
||||
assertThat(config.isClusterConfig()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_keepsSentinelMembershipCheckEnabledByDefault() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
|
|
@ -171,4 +243,10 @@ class RedissonConfigTest {
|
|||
method.setAccessible(true);
|
||||
return (SentinelServersConfig) method.invoke(config);
|
||||
}
|
||||
|
||||
private ClusterServersConfig clusterConfig(Config config) throws Exception {
|
||||
Method method = Config.class.getDeclaredMethod("getClusterServersConfig");
|
||||
method.setAccessible(true);
|
||||
return (ClusterServersConfig) method.invoke(config);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue