本文记录了在 Halo 2.x 插件中集成 Redis 的完整过程,包括遇到的各种问题和最终解决方案。
背景
在开发 Halo 插件时,可能需要使用 Redis 来实现各种功能,例如:
支付订单状态缓存
支付回调幂等性校验
订单超时自动取消(延迟队列)
分布式锁防止重复支付
最终方案
核心设计
插件复用 Halo 主程序的 Redis 配置,不需要在插件设置里单独配置 Redis 连接信息。
为什么用 Jedis?
Halo 插件使用 PF4J 框架,每个插件有独立的类加载器。尝试过的方案:
Jedis 是纯 Java 实现的 Redis 客户端,不依赖 Spring 或 Reactor,避免了类加载器冲突问题。
配置方式
本地开发
创建
halo-dev.yaml(已在.gitignore中):
spring:
data:
redis:
host: localhost
port: 6379
database: 0
password: your_password
halo:
redis:
enabled: true
session:
store-type: redis
在
build.gradle中引用:
halo {
version = '2.22.2'
additionalConfigFile = file("${projectDir}/halo-dev.yaml")
}
启动开发服务器:
./gradlew haloServer
生产环境(Docker)
通过环境变量配置:
docker run -d \
--name halo \
-p 8090:8090 \
-v ~/.halo2:/root/.halo2 \
-e SPRING_DATA_REDIS_HOST=redis \
-e SPRING_DATA_REDIS_PORT=6379 \
-e SPRING_DATA_REDIS_DATABASE=0 \
-e SPRING_DATA_REDIS_PASSWORD=your_password \
-e HALO_SESSION_STORE_TYPE=redis \
-e HALO_REDIS_ENABLED=true \
halohub/halo:2.22
或使用 Docker Compose:
services:
halo:
image: halohub/halo:2.22
environment:
- SPRING_DATA_REDIS_HOST=redis
- SPRING_DATA_REDIS_PORT=6379
- SPRING_DATA_REDIS_DATABASE=0
- SPRING_DATA_REDIS_PASSWORD=your_password
- HALO_SESSION_STORE_TYPE=redis
- HALO_REDIS_ENABLED=true
ports:
- "8090:8090"
depends_on:
- redis
redis:
image: redis:8-alpine
command: redis-server --requirepass your_password
代码实现
RedisConfig.java
从 Spring Environment 读取 Halo 的 Redis 配置:
@Slf4j
@Component
public class RedisConfig {
private final Environment environment;
@Nullable
private volatile JedisPool jedisPool;
@Getter
private volatile boolean redisAvailable = false;
public RedisConfig(Environment environment) {
this.environment = environment;
}
public void ensureInitialized() {
// 检查 Halo 是否启用了 Redis
String haloRedisEnabled = environment.getProperty("halo.redis.enabled", "false");
if (!"true".equalsIgnoreCase(haloRedisEnabled)) {
log.info("Halo Redis not enabled");
return;
}
// 读取 Redis 配置
String host = environment.getProperty("spring.data.redis.host", "localhost");
int port = Integer.parseInt(environment.getProperty("spring.data.redis.port", "6379"));
String password = environment.getProperty("spring.data.redis.password", "");
int database = Integer.parseInt(environment.getProperty("spring.data.redis.database", "0"));
initRedis(host, port, password, database);
}
private void initRedis(String host, int port, String password, int database) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(10);
poolConfig.setTestOnBorrow(true);
HostAndPort hostAndPort = new HostAndPort(host, port);
DefaultJedisClientConfig.Builder configBuilder = DefaultJedisClientConfig.builder()
.connectionTimeoutMillis(10000)
.database(database);
if (password != null && !password.isEmpty()) {
// Redis 6+ ACL 需要用户名
configBuilder.user("default");
configBuilder.password(password);
}
jedisPool = new JedisPool(poolConfig, hostAndPort, configBuilder.build());
// 测试连接
try (var jedis = jedisPool.getResource()) {
jedis.ping();
redisAvailable = true;
}
}
}
RedisCacheService.java
同步操作包装成 Mono,在 boundedElastic 调度器上执行:
@Service
@RequiredArgsConstructor
public class RedisCacheService {
private final RedisConfig redisConfig;
public boolean isAvailable() {
return redisConfig.getJedisPool() != null;
}
public Mono<Long> incrementLikeCount(String postName) {
return executeAsync(() -> {
JedisPool pool = redisConfig.getJedisPool();
if (pool == null) return -1L;
try (Jedis jedis = pool.getResource()) {
return jedis.incr(getKey("like:count:" + postName));
}
}, -1L);
}
public Mono<List<String>> getHotPosts(String circleName, int limit) {
return executeAsync(() -> {
JedisPool pool = redisConfig.getJedisPool();
if (pool == null) return Collections.emptyList();
try (Jedis jedis = pool.getResource()) {
return jedis.zrevrange(getKey("hot:posts:" + circleName), 0, limit - 1);
}
}, Collections.emptyList());
}
private <T> Mono<T> executeAsync(Callable<T> callable, T defaultValue) {
return Mono.fromCallable(callable)
.subscribeOn(Schedulers.boundedElastic())
.onErrorResume(e -> Mono.just(defaultValue));
}
}
测试接口
访问 /apis/api.public.circle.xhhao.com/v1alpha1/redis/test 查看 Redis 状态:
{
"redisAvailable": true,
"haloRedisEnabled": "true",
"springRedisHost": "localhsot",
"springRedisPort": "6379",
"springRedisDatabase": "0",
"writeSuccess": true,
"readValue": "Hello from Circle Plugin!",
"message": "Redis is working!"
}
注意事项
Redis 6+ ACL
Redis 6+ 引入了 ACL,需要同时提供用户名和密码。默认用户名是 default:
if (password != null) {
configBuilder.user("default"); // 关键!
configBuilder.password(password);
}
优雅降级
当 Redis 不可用时,服务应该能正常工作:
public Mono<Long> getLikeCount(String postName) {
if (!isAvailable()) {
return Mono.just(-1L); // 返回默认值,由调用方从数据库查询
}
// ... Redis 操作
}
依赖配置
dependencies {
implementation platform('run.halo.tools.platform:plugin:2.22.0')
compileOnly 'run.halo.app:api'
// Redis - Jedis 纯 Java 客户端
implementation 'redis.clients:jedis:5.1.0'
}
总结
复用 Halo 配置:插件从 Spring Environment 读取 Halo 主程序的 Redis 配置
使用 Jedis:避免类加载器冲突
本地开发:通过
halo-dev.yaml+additionalConfigFile配置生产环境:通过 Docker 环境变量配置
优雅降级:Redis 不可用时不影响核心功能
默认评论
Halo系统提供的评论