修改分类信息的时候将分类异步写入redis

修改分类信息的时候将分类异步写入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 直接抛异常丢数据

📊 生产部署建议

  1. 监控埋点 :建议在 asyncSyncToRedis 入口/出口增加 Micrometer 指标:

    复制代码
    复制代码
    MeterRegistry registry; // 注入
    Timer.builder("category.redis.sync.duration").register(registry).record(() -> {
        // 执行 put/expire
    });
  2. 失败重试机制 :若 Redis 写入失败,可写入本地数据库 cache_sync_failed_log 表,配合 @Scheduled 定时任务扫描重试(最多 3 次)。

  3. 序列化配置 :确保 Redisson 使用 JSON 序列化(默认行为),若使用自定义编解码器,需保证 CategoryDTO 字段与 Redis 中结构一致。

  4. 压测验证 :使用 JMeterwrk 模拟高并发更新,观察线程池监控(ThreadPoolTaskExecutor 提供 getActiveCount(), getQueueSize() 等方法)。


✅ 直接替换 CategoryMapper 为你的实际 DAO 接口即可运行。

相关推荐
STAT abil2 小时前
MySQL 的mysql_secure_installation安全脚本执行过程介绍
数据库·mysql·安全
SelectDB2 小时前
从 T+1 到分钟级:金城银行基于 Apache Doris 构建高可靠、强一致的实时数据平台
大数据·数据库·数据分析
abc123456sdggfd2 小时前
bootstrap如何修改输入框获取焦点时的光晕
jvm·数据库·python
woniu_buhui_fei2 小时前
Redis知识整理二
数据库·redis·缓存
qq_330037992 小时前
如何配置ASM元数据备份_md_backup与md_restore重建磁盘组结构
jvm·数据库·python
untE EADO2 小时前
redis的下载和安装详解
数据库·redis·缓存
a9511416422 小时前
SQL触发器实现自动生成流水号_配合序列对象实现递增逻辑
jvm·数据库·python
BduL OWED2 小时前
SQL进阶——JOIN操作详解
数据库·sql·oracle
解救女汉子3 小时前
mysql如何配置元数据锁超时_mysql lock_wait_timeout设置
jvm·数据库·python