修改分类信息的时候将分类异步写入redis,怎么实现异步
在 Spring Boot + Redis 场景下,实现"修改分类后异步写入 Redis"有 3 种主流方案。按 生产可靠性与复杂度 推荐如下:
🏆 方案一:Spring @Async(最推荐,适合单体/标准微服务)
利用 Spring 内置异步线程池,代码最简洁,易监控、易扩展。
1. 开启异步支持
@SpringBootApplication
@EnableAsync // ✅ 必须加
public class Application {}
2. 配置安全线程池(⚠️ 生产必配,防 OOM)
@Configuration
public class AsyncConfig {
@Bean("categoryExecutor")
public ThreadPoolTaskExecutor categoryExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("cat-redis-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
3. 业务调用 + 异步写入
@Service
@RequiredArgsConstructor
public class CategoryService {
private final CategoryMapper categoryMapper;
private final CategoryCacheService cacheService;
@Transactional
public void updateCategory(CategoryDTO dto) {
// 1. 更新数据库
categoryMapper.updateById(dto);
// 2. 异步同步到 Redis(立即返回,不阻塞主线程)
cacheService.asyncSyncToRedis(dto);
}
}
@Service
@RequiredArgsConstructor
public class CategoryCacheService {
private final RedissonClient redissonClient;
@Async("categoryExecutor") // ✅ 指定线程池
public void asyncSyncToRedis(CategoryDTO dto) {
RMap<String, CategoryDTO> map = redissonClient.getMap("category:info");
map.put(String.valueOf(dto.getId()), dto);
map.expire(24, TimeUnit.HOURS);
}
}
🛠️ 方案二:CompletableFuture(轻量级,无需 Spring 配置)
适合不想引入 Spring 异步上下文,或需要链式处理结果的场景。
@Transactional
public void updateCategory(CategoryDTO dto) {
categoryMapper.updateById(dto);
// 异步执行 + 异常捕获(不阻塞主线程)
CompletableFuture.runAsync(() -> {
RMap<String, CategoryDTO> map = redissonClient.getMap("category:info");
map.put(String.valueOf(dto.getId()), dto);
}, customExecutor) // 传入自定义线程池
.exceptionally(ex -> {
log.error("分类缓存异步写入失败, id: {}", dto.getId(), ex);
return null;
});
}
⚡ 方案三:Redisson 原生异步 I/O(仅非阻塞网络,不脱离主线程)
如果你只想避免 网络阻塞,但仍在当前线程执行逻辑:
@Transactional
public void updateCategory(CategoryDTO dto) {
categoryMapper.updateById(dto);
// putAsync 返回 RFuture,底层基于 Netty 异步 NIO,不阻塞当前线程
RMap<String, CategoryDTO> map = redissonClient.getMap("category:info");
map.putAsync(String.valueOf(dto.getId()), dto)
.thenAcceptAsync(success -> {
log.info("Redis 写入完成: {}", success);
map.expireAsync(24, TimeUnit.HOURS);
})
.exceptionally(ex -> {
log.error("Redis 异步写入异常", ex);
return null;
});
// 方法直接返回,不等待 Redis 响应
}
⚠️ 生产环境 4 大致命陷阱(必看)
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
| 1. 事务未提交就写 Redis | @Async 方法可能比 DB commit 先执行,导致缓存读到旧数据或写入失败 |
使用事务同步回调:<br>TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { public void afterCommit() { asyncSync(dto); } }); |
| 2. 默认线程池无限创建 | Spring 默认 SimpleAsyncTaskExecutor 每次新建线程 → 内存泄漏/线程数爆炸 |
必须配置 ThreadPoolTaskExecutor,设置 core/max/queue/rejected |
| 3. 异步异常被静默吞掉 | @Async 方法抛异常不会中断主流程,但 Redis 未更新,缓存长期不一致 |
加 @Async 方法内 try-catch 记录日志 + 失败重试/告警;或使用 AsyncUncaughtExceptionHandler |
| 4. 缓存与 DB 最终不一致 | 异步写入失败、Redis 宕机、节点重启导致数据丢失 | Redis 仅作缓存,DB 为主数据源。可配合定时任务补偿:@Scheduled 扫描 updated_at > cache_sync_time 的记录重新同步 |
📦 完整生产级实现(含事务同步 + 异常处理)
@Service
@RequiredArgsConstructor
@Slf4j
public class CategoryService {
private final CategoryMapper categoryMapper;
private final CategoryCacheService cacheService;
@Transactional
public void updateCategory(CategoryDTO dto) {
categoryMapper.updateById(dto);
// ✅ 确保 DB 提交后再执行异步写入
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
cacheService.asyncSyncToRedis(dto);
}
}
);
}
}
@Service
@RequiredArgsConstructor
public class CategoryCacheService {
private final RedissonClient redissonClient;
@Async("categoryExecutor")
public void asyncSyncToRedis(CategoryDTO dto) {
try {
RMap<String, CategoryDTO> map = redissonClient.getMap("category:info");
map.put(String.valueOf(dto.getId()), dto);
map.expire(24, TimeUnit.HOURS);
} catch (Exception e) {
log.error("异步同步分类到Redis失败, id: {}", dto.getId(), e);
// 可接入 MQ 重试或告警
}
}
}
📊 选型建议
| 你的需求 | 推荐方案 |
|---|---|
| 标准 Spring Boot 项目,追求开发效率与可维护性 | ✅ @Async + 事务同步回调 |
| 轻量工具类/非 Spring 环境 | ✅ CompletableFuture.runAsync() |
| 仅需网络 I/O 不阻塞,不关心线程切换 | ✅ Redisson putAsync() |
| 高可靠、需重试/死信/跨服务解耦 | 🔄 改用消息队列(RabbitMQ/Kafka) |
💡 核心原则 :Redis 是缓存,DB 是真相源。异步写入仅用于 性能优化 ,不能替代 DB。务必做好 异常降级、监控告警、定期补偿。
完整可运行代码
以下是基于 CompletableFuture 方案的完整、可生产运行的代码实现。已包含事务同步保障、独立线程池配置、异常兜底与监控埋点,直接复制即可集成到 Spring Boot 项目中。
📦 1. 线程池配置类(防资源耗尽)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncExecutorConfig {
/**
* 分类缓存异步写入专用线程池
* 隔离业务线程,避免 Redis 慢查询/抖动拖垮主业务
*/
@Bean("categoryAsyncExecutor")
public Executor categoryAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4); // 核心线程数(根据 QPS 调整)
executor.setMaxPoolSize(8); // 最大线程数
executor.setQueueCapacity(200); // 阻塞队列容量
executor.setThreadNamePrefix("cat-redis-"); // 线程名前缀,方便排查
executor.setRejectedExecutionHandler(
new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy() // 队列满时调用者线程执行,防任务丢失
);
executor.setWaitForTasksToCompleteOnShutdown(true); // 应用关闭时等待任务完成
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor; // ThreadPoolTaskExecutor 实现了 java.util.concurrent.Executor 接口
}
}
🧱 2. 数据传输对象(示例)
import lombok.Data;
import java.io.Serializable;
@Data
public class CategoryDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String code;
private Integer status;
// Redis 序列化要求:必须实现 Serializable 或配置 JSON 序列化器
}
🛠️ 3. 核心 Service 实现(含事务同步 + 异步写入)
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Slf4j
public class CategoryService {
private final CategoryMapper categoryMapper; // 假设已注入(MyBatis/JPA 等)
private final RedissonClient redissonClient;
private final Executor categoryAsyncExecutor; // 注入上方配置的线程池
@Transactional(rollbackFor = Exception.class)
public void updateCategory(CategoryDTO dto) {
// 1️⃣ 更新数据库
int rows = categoryMapper.updateById(dto);
if (rows == 0) {
throw new RuntimeException("分类更新失败,ID: " + dto.getId());
}
// 2️⃣ 注册事务同步回调:确保 DB commit 成功后,再触发异步写 Redis
// ⚠️ 关键:防止"DB 回滚但缓存已更新"或"未提交事务被其他线程读到脏数据"
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
asyncSyncToRedis(dto);
}
});
} else {
// 非事务环境(如单元测试/定时任务)直接异步执行
asyncSyncToRedis(dto);
}
}
/**
* 异步同步到 Redis
*/
private void asyncSyncToRedis(CategoryDTO dto) {
CompletableFuture.runAsync(() -> {
try {
RMap<String, CategoryDTO> map = redissonClient.getMap("category:info");
// 写入 Hash(field = id, value = 对象)
map.put(String.valueOf(dto.getId()), dto);
// 设置过期时间(24小时)
map.expire(24, TimeUnit.HOURS);
log.info("✅ 分类缓存异步写入成功, id: {}, name: {}", dto.getId(), dto.getName());
} catch (Exception e) {
log.error("❌ 分类缓存异步写入失败, id: {}, data: {}", dto.getId(), dto, e);
// 🚨 生产建议:记录失败日志表 / 发送 MQ 重试 / 触发钉钉/企微告警
}
}, categoryAsyncExecutor)
.exceptionally(ex -> {
// 兜底捕获 CompletableFuture 框架级异常(如线程池拒绝策略未拦截的异常)
log.error("⚠️ CompletableFuture 执行异常, id: {}", dto.getId(), ex);
return null;
});
// 📌 此处方法立即返回,不阻塞主流程
}
}
🔍 核心设计亮点(为什么这样写?)
| 设计点 | 作用 | 原方案缺陷 |
|---|---|---|
TransactionSynchronization.afterCommit() |
保证最终一致性:DB 提交成功才写缓存 | 直接异步写可能导致 DB回滚但缓存已更新 |
独立线程池 categoryAsyncExecutor |
资源隔离:Redis 慢查询/网络抖动不影响主业务线程 | 共用 Tomcat/ForkJoinPool 易导致线程耗尽 |
try-catch + .exceptionally() |
双重异常拦截:业务异常与框架异常全覆盖 | 异常被静默吞掉,缓存长期不一致且无日志 |
CallerRunsPolicy 拒绝策略 |
防任务丢失:队列满时由主线程执行写入 | 默认 AbortPolicy 直接抛异常丢数据 |
📊 生产部署建议
-
监控埋点 :建议在
asyncSyncToRedis入口/出口增加 Micrometer 指标:MeterRegistry registry; // 注入 Timer.builder("category.redis.sync.duration").register(registry).record(() -> { // 执行 put/expire }); -
失败重试机制 :若 Redis 写入失败,可写入本地数据库
cache_sync_failed_log表,配合@Scheduled定时任务扫描重试(最多 3 次)。 -
序列化配置 :确保 Redisson 使用 JSON 序列化(默认行为),若使用自定义编解码器,需保证
CategoryDTO字段与 Redis 中结构一致。 -
压测验证 :使用
JMeter或wrk模拟高并发更新,观察线程池监控(ThreadPoolTaskExecutor提供getActiveCount(),getQueueSize()等方法)。
✅ 直接替换 CategoryMapper 为你的实际 DAO 接口即可运行。