Spring Boot错误消息存储优化:同内容限流与资源占用问询
问题解答
一、按内容维度限制单位时间内的错误消息写入次数
当前实现仅按用户维度限流,未区分错误内容,会导致同一重复错误被多次写入数据库,既冗余又增加DB压力。可以通过用户标识+错误内容+请求URL作为唯一键,结合本地缓存实现限流+UUID复用,完全匹配你“同一会话重复操作返回相同UUID错误消息”的需求,具体方案如下:
1. 核心逻辑
- 用缓存存储复合键:
{jwtToken}:{messageContent}:{url},对应值包含两个信息:当前时间窗口内的请求计数、已生成的错误UUID - 每次请求先检查缓存:
- 缓存存在且计数未达上限:计数自增,直接返回已有的UUID对应的DTO,跳过DB写入
- 缓存存在但计数达上限:返回null触发限流
- 缓存不存在:写入DB生成新UUID,再将计数(初始1)和UUID存入缓存并设置过期时间
2. 代码实现示例(基于Caffeine本地缓存)
首先添加Caffeine依赖(Spring Boot可直接用starter):
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
修改后的Service类:
@Slf4j @Service @RequiredArgsConstructor public class FrontendEventsService { private final ErrorMessageRepository messageRepository; private final JwtService jwtService; private final DateTimeProvider dateTimeProvider; @Value("${error.message.limit:10}") private Integer messagesLimit; @Value("${error.message.expire.minutes:1}") private Integer expireMinutes; // 本地缓存:键为用户+内容+URL,值为计数+UUID的封装对象 private final LoadingCache<String, ErrorCacheEntry> errorCache = Caffeine.newBuilder() .expireAfterWrite(expireMinutes, TimeUnit.MINUTES) .maximumSize(10000) // 限制缓存条目数,防止内存溢出 .build(key -> null); // 手动控制缓存写入,无需自动加载逻辑 public ErrorMessageDto handleMessage(ErrorMessageDto message) { String jwtToken = StringJwtParser.getTokenShortcut(jwtService.getRawToken()); String cacheKey = String.format("%s:%s:%s", jwtToken, message.getMessage(), message.getUrl()); ErrorCacheEntry cacheEntry = errorCache.getIfPresent(cacheKey); if (cacheEntry != null) { if (cacheEntry.count >= messagesLimit) { log.warn("Error limiter triggered: key={}, message={}", cacheKey, message.getMessage()); return null; } // 更新缓存计数,返回已有UUID的DTO errorCache.put(cacheKey, new ErrorCacheEntry(cacheEntry.count + 1, cacheEntry.uuid)); ErrorMessageDto dto = new ErrorMessageDto(); dto.setUuid(cacheEntry.uuid); dto.setMessage(message.getMessage()); dto.setUrl(message.getUrl()); return dto; } // 缓存不存在,写入数据库 ErrorMessageEntity entity = ErrorMessageEntity.builder() .createdAt(dateTimeProvider.getCurrentDateTime()) .uuid(UUID.randomUUID().toString()) .tokenHash(jwtToken) .userLogin(jwtService.getUserLogin()) .message(message.getMessage()) .url(message.getUrl()) .build(); ErrorMessageEntity saved = messageRepository.save(entity); ErrorMessageDto dto = ErrorMessageMapper.toDto(saved); // 写入缓存,初始化计数为1 errorCache.put(cacheKey, new ErrorCacheEntry(1, saved.getUuid())); return dto; } // 缓存值封装类 private static class ErrorCacheEntry { int count; String uuid; public ErrorCacheEntry(int count, String uuid) { this.count = count; this.uuid = uuid; } } }
3. 核心优势
- 彻底避免同一用户的重复错误写入DB,大幅降低数据库压力
- 自动复用UUID,完全满足重复操作返回相同错误消息的需求
- 本地缓存性能远高于DB查询,降低接口响应延迟
二、本地缓存Map的生产环境资源消耗问题
只要合理配置参数,本地缓存(如Caffeine)的资源消耗完全可控:
- 容量限制:通过
maximumSize设置最大条目数(比如10000),即使有大量用户,缓存也不会无限膨胀 - 自动过期:
expireAfterWrite会自动清理过期缓存,释放内存 - 内存占用极低:每个缓存条目仅包含字符串键和一个小对象(整数+UUID字符串),10000条缓存仅占用几十KB到几百KB内存
- 若需分布式全局限流,可替换为Redis缓存,但本地缓存已能覆盖绝大多数单节点或低并发场景的需求
可通过监控缓存命中率、条目数等指标,动态调整maximumSize和过期时间参数,进一步优化资源占用。
内容的提问来源于stack exchange,提问作者haoz
相关产品推荐
相关产品推荐

