惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
N
Netflix TechBlog - Medium
Google Online Security Blog
Google Online Security Blog
月光博客
月光博客
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
腾讯CDC
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
M
MIT News - Artificial intelligence
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
博客园 - 三生石上(FineUI控件)
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
The Cloudflare Blog
Blog — PlanetScale
Blog — PlanetScale
F
Full Disclosure
G
Google Developers Blog
罗磊的独立博客
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
A
About on SuperTechFans
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
有赞技术团队
有赞技术团队
GbyAI
GbyAI
雷峰网
雷峰网
T
The Blog of Author Tim Ferriss
The Register - Security
The Register - Security
U
Unit 42
D
Docker
Martin Fowler
Martin Fowler
L
LINUX DO - 热门话题
NISL@THU
NISL@THU
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
博客园_首页
Google DeepMind News
Google DeepMind News

林间拾语

搜个微信客服被骗1900块,315曝光的“AI投毒”套路防不胜防 🌿 林间第5页拾语:OpenClaw 龙虾真的是刚需吗? 🌿 林间第4页拾语:再见 2025 告别等待!Halo在线客服插件,让网站沟通秒回时代 Halo 2.21.x 登录弹窗:从后端机制到前端实现 🌿 林间第3页拾语:把事情做好,把自己照顾好 从 0 到熟练:Mermaid 流程图的进阶之路 智阅AI助手第四次重构上线:这次把“摘要”两个字删掉了 禅导航 v2 升级:彻底重构,只为更好用 谈谈SEO:什么是SEO,如何做好SEO,及需要注意的事项 🌿 林间第2页拾语:糟心事很少,懂你的人刚好够 智阅 GPT V3 版本:全面升级,让内容精髓触手可及 全面了解腾讯 EdgeOne 边缘加速:加速网站并提高用户体验 数字隐私与数据安全:推销电话背后的隐私泄露风险 两个小插件偷偷上线了:SEO 时间因子 & 公告弹窗 MacOS 和 Linux 使用 SDKMAN 管理 Java 工具链 🌿 林间第1页拾语:光没来前,咱先煮点饭吧 Halo插件|一个面向创作者的多功能AI媒体处理工具集 林间拾语|一次命名的回归,也是一种自我表达
Halo 2.22.x 插件集成 Redis 完整指南
Handsome · 2025-12-26 · via 林间拾语

本文记录了在 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 不可用时不影响核心功能