Java面试必备:SpringBoot工程启动后加载数据库固定内容到Redis缓存的实现方案
在SpringBoot应用中,我们经常需要将数据库中一些不常变化但频繁访问的数据(如系统配置、字典数据、城市列表等)缓存到Redis中,以减少数据库访问压力,提高系统响应速度。本文将详细介绍如何在SpringBoot工程启动时,自动将数据库中的固定内容加载到Redis缓存中。
SpringBoot面试题 - SpringBoot工程启动以后,我希望将数据库中已有的固定内容,打入到Redis缓存中,请问如何处理?
一、需求背景
在SpringBoot应用中,我们经常需要将数据库中一些不常变化但频繁访问的数据(如系统配置、字典数据、城市列表等)缓存到Redis中,以减少数据库访问压力,提高系统响应速度。本文将详细介绍如何在SpringBoot工程启动时,自动将数据库中的固定内容加载到Redis缓存中。
二、实现方案
1. 整体流程图
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;
四、性能优化建议
-
批量操作:对于大量数据,使用Redis的批量操作命令减少网络开销
redisTemplate.executePipelined((RedisCallback<Object>) connection -> { // 在pipeline中执行多个操作 return null; }); -
异步加载:对于非关键路径数据,可以使用异步方式加载
@Async public void asyncLoadDataToRedis() { // 异步加载逻辑 } -
数据压缩:对于大对象,可以考虑序列化前进行压缩
-
分页加载:对于大量数据,可以分页查询并分批存入Redis
五、总结
本文介绍了在SpringBoot应用启动时将数据库固定内容加载到Redis缓存的多种实现方案,包括:
- 使用CommandLineRunner/ApplicationRunner接口
- 使用@PostConstruct注解
- 使用ApplicationListener监听ContextRefreshedEvent事件
并提供了分布式环境下的优化方案和数据变更时的同步策略。通过合理的缓存预热策略,可以显著提高系统性能,减少数据库压力。
在实际项目中,应根据数据量大小、访问频率和业务需求选择合适的实现方式,并注意异常处理和性能优化。
更多推荐
所有评论(0)