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

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
GbyAI
GbyAI
SecWiki News
SecWiki News
Project Zero
Project Zero
C
Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy International News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Scott Helme
Scott Helme
A
Arctic Wolf
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy & Cybersecurity Law Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
The Hacker News
The Hacker News
T
Tenable Blog
雷峰网
雷峰网
有赞技术团队
有赞技术团队
V
V2EX
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threat Research - Cisco Blogs
T
Threatpost
AWS News Blog
AWS News Blog
L
LINUX DO - 热门话题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
SegmentFault 最新的问题
月光博客
月光博客
Spread Privacy
Spread Privacy
S
Secure Thoughts
宝玉的分享
宝玉的分享
博客园 - 三生石上(FineUI控件)
Forbes - Security
Forbes - Security
T
The Exploit Database - CXSecurity.com
G
GRAHAM CLULEY
The Last Watchdog
The Last Watchdog
Y
Y Combinator Blog
I
Intezer
博客园 - 【当耐特】
B
Blog RSS Feed
Attack and Defense Labs
Attack and Defense Labs
I
InfoQ
博客园 - 叶小钗
Cyberwarzone
Cyberwarzone
V2EX - 技术
V2EX - 技术
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hugging Face - Blog
Hugging Face - Blog
H
Help Net Security
C
CERT Recently Published Vulnerability Notes

林间拾语

智阅 V4:从文章助手,到站点 AI 助手 评论组件 Next:给 Halo 评论区换一个更顺手的选择 搜个微信客服被骗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 不可用时不影响核心功能