SpringBoot面试题 - SpringBoot工程启动以后,我希望将数据库中已有的固定内容,打入到Redis缓存中,请问如何处理?


一、需求背景

在SpringBoot应用中,我们经常需要将数据库中一些不常变化但频繁访问的数据(如系统配置、字典数据、城市列表等)缓存到Redis中,以减少数据库访问压力,提高系统响应速度。本文将详细介绍如何在SpringBoot工程启动时,自动将数据库中的固定内容加载到Redis缓存中。

二、实现方案

1. 整体流程图

SpringBoot应用启动
是否启用缓存预热
从数据库查询固定数据
正常启动流程
将数据序列化
存入Redis
启动完成

2. 具体实现步骤

2.1 添加依赖

首先确保项目中已添加Spring Data Redis和数据库访问相关依赖:

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 数据库访问 (根据实际使用选择) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 或 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
2.2 配置Redis

在application.properties或application.yml中配置Redis连接:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
2.3 实现缓存预热逻辑

有几种方式可以实现启动时加载数据到Redis:

方案一:使用CommandLineRunner或ApplicationRunner接口
@Component
public class RedisCachePreloader implements CommandLineRunner {

    private final SomeRepository someRepository;
    private final RedisTemplate<String, Object> redisTemplate;
    
    public RedisCachePreloader(SomeRepository someRepository, RedisTemplate<String, Object> redisTemplate) {
        this.someRepository = someRepository;
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public void run(String... args) throws Exception {
        // 1. 从数据库查询需要缓存的数据
        List<SomeEntity> fixedData = someRepository.findFixedData();
        
        // 2. 将数据存入Redis
        if (!fixedData.isEmpty()) {
            String cacheKey = "fixed:data:key";
            redisTemplate.opsForValue().set(cacheKey, fixedData);
            
            // 可以设置过期时间,如果不设置则永久有效
            // redisTemplate.expire(cacheKey, 24, TimeUnit.HOURS);
        }
    }
}
方案二:使用@PostConstruct注解
@Service
public class CacheService {

    @Autowired
    private SomeRepository someRepository;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @PostConstruct
    public void init() {
        loadFixedDataToRedis();
    }
    
    public void loadFixedDataToRedis() {
        // 实现数据加载逻辑
    }
}
方案三:使用ApplicationListener监听ContextRefreshedEvent事件
@Component
public class RedisCacheLoader implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 确保只执行一次
        if (event.getApplicationContext().getParent() == null) {
            loadDataToRedis();
        }
    }
    
    private void loadDataToRedis() {
        // 实现数据加载逻辑
    }
}

3. 更完整的实现示例

下面是一个更完整的实现示例,包含异常处理和日志记录:

@Component
@Slf4j
public class RedisCachePreloader implements CommandLineRunner {

    private final SystemConfigRepository configRepository;
    private final RedisTemplate<String, Object> redisTemplate;
    
    public RedisCachePreloader(SystemConfigRepository configRepository, 
                             RedisTemplate<String, Object> redisTemplate) {
        this.configRepository = configRepository;
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public void run(String... args) {
        try {
            log.info("开始加载固定配置数据到Redis缓存...");
            
            // 1. 加载系统配置
            loadSystemConfigs();
            
            // 2. 加载字典数据
            loadDictionaryData();
            
            log.info("固定配置数据加载到Redis缓存完成");
        } catch (Exception e) {
            log.error("加载固定配置数据到Redis缓存失败", e);
        }
    }
    
    private void loadSystemConfigs() {
        List<SystemConfig> configs = configRepository.findAll();
        if (!configs.isEmpty()) {
            Map<String, String> configMap = configs.stream()
                .collect(Collectors.toMap(
                    SystemConfig::getConfigKey,
                    SystemConfig::getConfigValue
                ));
            
            redisTemplate.opsForHash().putAll("system:configs", configMap);
            log.info("已加载 {} 条系统配置到Redis", configs.size());
        }
    }
    
    private void loadDictionaryData() {
        List<Dictionary> dictionaries = dictionaryRepository.findByFixed(true);
        if (!dictionaries.isEmpty()) {
            Map<String, List<Dictionary>> grouped = dictionaries.stream()
                .collect(Collectors.groupingBy(Dictionary::getType));
            
            grouped.forEach((type, items) -> {
                String key = "dictionary:" + type;
                redisTemplate.opsForValue().set(key, items);
            });
            
            log.info("已加载 {} 类字典数据到Redis,共计 {} 条", 
                    grouped.size(), dictionaries.size());
        }
    }
}

三、进阶优化

1. 分布式环境下的处理

在分布式环境中,多个实例同时启动可能会导致重复加载数据的问题。可以通过Redis的分布式锁来解决:

private void loadDataWithDistributedLock() {
    String lockKey = "lock:data:preload";
    String clientId = UUID.randomUUID().toString();
    try {
        // 尝试获取锁,设置10秒过期时间
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
        
        if (Boolean.TRUE.equals(locked)) {
            // 获取锁成功,执行数据加载
            loadActualData();
        } else {
            log.info("其他实例正在加载数据,本实例跳过");
        }
    } finally {
        // 释放锁,确保是自己的锁才释放
        String currentValue = (String) redisTemplate.opsForValue().get(lockKey);
        if (clientId.equals(currentValue)) {
            redisTemplate.delete(lockKey);
        }
    }
}

2. 数据变更时的同步更新

除了启动时加载,还需要考虑数据变更时的同步更新:

@Service
@Transactional
public class SystemConfigService {

    @Autowired
    private SystemConfigRepository configRepository;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public SystemConfig updateConfig(SystemConfig config) {
        SystemConfig updated = configRepository.save(config);
        
        // 更新Redis缓存
        redisTemplate.opsForHash().put(
            "system:configs", 
            updated.getConfigKey(), 
            updated.getConfigValue()
        );
        
        return updated;
    }
}

3. 缓存键设计规范

良好的缓存键设计可以提高可维护性:

public class CacheKeyConstants {
    public static final String SYSTEM_CONFIGS = "system:configs";
    public static final String DICTIONARY_PREFIX = "dictionary:";
    // 其他缓存键...
}

// 使用示例
String dictionaryKey = CacheKeyConstants.DICTIONARY_PREFIX + type;

四、性能优化建议

  1. 批量操作:对于大量数据,使用Redis的批量操作命令减少网络开销

    redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        // 在pipeline中执行多个操作
        return null;
    });
    
  2. 异步加载:对于非关键路径数据,可以使用异步方式加载

    @Async
    public void asyncLoadDataToRedis() {
        // 异步加载逻辑
    }
    
  3. 数据压缩:对于大对象,可以考虑序列化前进行压缩

  4. 分页加载:对于大量数据,可以分页查询并分批存入Redis

五、总结

本文介绍了在SpringBoot应用启动时将数据库固定内容加载到Redis缓存的多种实现方案,包括:

  1. 使用CommandLineRunner/ApplicationRunner接口
  2. 使用@PostConstruct注解
  3. 使用ApplicationListener监听ContextRefreshedEvent事件

并提供了分布式环境下的优化方案和数据变更时的同步策略。通过合理的缓存预热策略,可以显著提高系统性能,减少数据库压力。

SpringBoot启动
执行缓存预热
数据库查询
获取分布式锁
数据序列化
存入Redis
释放锁
启动完成

在实际项目中,应根据数据量大小、访问频率和业务需求选择合适的实现方式,并注意异常处理和性能优化。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐