Halo 2.22.x 插件集成 Redis 完整指南

Handsome
2025-12-26
点 赞
2
热 度
41
评 论
0
  1. 首页
  2. 林间·栈笔
  3. Halo 2.22.x 插件集成 Redis 完整指南

本文记录了在 Halo 2.x 插件中集成 Redis 的完整过程,包括遇到的各种问题和最终解决方案。

背景

在开发 Halo 插件时,可能需要使用 Redis 来实现各种功能,例如:

  • 支付订单状态缓存

  • 支付回调幂等性校验

  • 订单超时自动取消(延迟队列)

  • 分布式锁防止重复支付

最终方案

核心设计

插件复用 Halo 主程序的 Redis 配置,不需要在插件设置里单独配置 Redis 连接信息。

环境

配置来源

说明

本地开发

halo-dev.yaml

通过 additionalConfigFile 传递给 haloServer

生产环境

Docker 环境变量

-e SPRING_DATA_REDIS_HOST=...

为什么用 Jedis?

Halo 插件使用 PF4J 框架,每个插件有独立的类加载器。尝试过的方案:

方案

结果

原因

Spring Data Redis

类加载器冲突

Lettuce

Reactor 依赖冲突

Jedis

纯 Java,无冲突

Jedis 是纯 Java 实现的 Redis 客户端,不依赖 Spring 或 Reactor,避免了类加载器冲突问题。

配置方式

本地开发

  1. 创建 halo-dev.yaml(已在 .gitignore 中):

spring:
  data:
    redis:
      host: localhost
      port: 6379
      database: 0
      password: your_password

halo:
  redis:
    enabled: true
  session:
    store-type: redis
  1. build.gradle 中引用:

halo {
    version = '2.22.2'
    additionalConfigFile = file("${projectDir}/halo-dev.yaml")
}
  1. 启动开发服务器:

./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'
}

总结

  1. 复用 Halo 配置:插件从 Spring Environment 读取 Halo 主程序的 Redis 配置

  2. 使用 Jedis:避免类加载器冲突

  3. 本地开发:通过 halo-dev.yaml + additionalConfigFile 配置

  4. 生产环境:通过 Docker 环境变量配置

  5. 优雅降级:Redis 不可用时不影响核心功能


心若有所向往,何惧道阻且长

Handsome

infp 调停者

站长

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性
切换评论

目录

欢迎来到Handsome的站点,为您导航全站动态

45 文章数
4 分类数
189 评论数
47标签数