Spring Boot 3.4 + Redisson 构建高并发AI工具索引:从“内存溢出”到“毫秒级检索”的架构演进
上周处理一个日均PV突破50万的免费AI工具导航站时,我们遇到了典型的“读多写少”但热点集中的场景。传统的 MySQL 分页查询在热门工具(如 Midjourney、Copilot)被大量并发访问时,CPU 飙升且响应延迟超过 2s。既然近期社区讨论多集中于 AI 模型集成或大模型编排,本文将完全剥离 AI 业务逻辑,纯粹聚焦于后端基础设施层:如何利用 Redisson 的 RMapCache 和 Lua 脚本,在 Spring Boot 3.4 环境下构建一个具备自动过期、防穿透、支持缓存同步的高性能缓存层。
这不仅是缓存问题,更是数据一致性与高可用性的工程权衡。
环境准备
本次实战基于以下技术栈,所有版本均为 2026 年主流稳定版:
- JDK: 17.0.12 (LTS)
- Spring Boot: 3.4.0 (基于 Spring Framework 6.2.0)
- Redis: 7.2.5 (Cluster 模式)
- Redisson: 3.35.0 (客户端)
- MySQL: 8.0.36
需引入的 Maven 依赖如下,注意排除旧版 Netty 冲突:
```xml
org.springframework.boot
spring-boot-starter-data-redis
3.4.0
org.redisson
redisson-spring-boot-starter
3.35.0
org.springframework.boot
spring-boot-starter-actuator
org.projectlombok
lombok
1.18.34
provided
```
核心步骤
1. 配置 Redisson 与集群拓扑
在application.yml中,我们不仅配置连接池,还需开启 Redisson 的异步事件循环线程,以应对高并发下的 IO 阻塞。
```yaml
spring:
redis:
host: 192.168.1.100
port: 6379
password: your_secure_password
redisson:
config: |
singleServerConfig:
address: "redis://192.168.1.100:6379"
connectionPoolSize: 64
connectionMinimumIdleSize: 24
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
codec: org.redisson.codec.JsonJacksonCodec
useDefaultCodec: false
```
这里使用JsonJacksonCodec而非默认的MarshallingCodec,主要是为了减少序列化开销并避免类加载器冲突,尤其在微服务环境下更为关键。
2. 实现带“逻辑删除”标记的缓存结构
AI 工具平台的数据存在软删除场景(下架工具)。如果直接缓存对象,删除后 Redis 中仍残留脏数据。我们采用RMapCache存储,并引入一个独立的 Set 来维护“已下架/逻辑删除”的工具 ID,利用 Lua 脚本保证原子性。
```java
@Data
@Accessors(chain = true)
public class ToolInfo {
private String id;
private String name;
private String category; // 写作、绘画、编程等
private Long viewCount;
private boolean deleted; // 逻辑删除标记
private LocalDateTime updateAt;
}
```
3. 核心缓存同步服务(Lua 原子操作)
防止缓存击穿和脏读的最佳实践是“缓存旁路+原子更新”。当更新或删除工具时,必须同时修改 Map 中的值和 Set 中的删除标记。
```java
@Service
@Slf4j
public class ToolCacheService {
@Autowired
private RedissonClient redissonClient;
private static final String TOOLS_MAP_KEY = "ai_tools:v1";
private static final String DELETED_IDS_KEY = "ai_tools_deleted_ids:v1";
// Lua 脚本:原子性地更新缓存并标记删除
private static final String UPDATE_AND_DELETE_LUA =
"local mapKey = KEYS[1] " +
"local delSetKey = KEYS[2] " +
"local toolId = ARGV[1] " +
"local toolDataJson = ARGV[2] " +
"local isDelete = tonumber(ARGV[3]) " +
" " +
"-- 获取当前缓存值 " +
"local currentVal = redis.call('HGET', mapKey, toolId) " +
" " +
"-- 如果存在且未删除,先移除删除标记集合中的该ID " +
"if currentVal and isDelete == 0 then " +
" redis.call('SREM', delSetKey, toolId) " +
"end " +
" " +
"-- 写入或更新 Map " +
"if isDelete == 1 then " +
" -- 如果是删除操作,可以在Map中标记为deleted=true,或者直接不删,靠Set判断 " +
" -- 这里选择保持Map完整,仅靠Set判断可见性,避免频繁IO " +
" local data = cjson.decode(currentVal or '{}') " +
" data['deleted'] = true " +
" redis.call('HSET', mapKey, toolId, cjson.encode(data)) " +
"else " +
" redis.call('HSET', mapKey, toolId, toolDataJson) " +
"end " +
" " +
"-- 设置过期时间,防止内存无限增长 " +
"redis.call('EXPIRE', mapKey, 86400) " +
"return 1";
/**
- 同步工具信息到缓存
- @param toolId 工具ID
- @param toolJson JSON字符串
- @param isDeleted 是否逻辑删除
*/
public void syncToolToCache(String toolId, String toolJson, boolean isDeleted) {
RMapCache toolsMap = redissonClient.getMapCache(TOOLS_MAP_KEY);
RSet