Java 接口与抽象类实战对比:缓存热启动设计模式演进
宝子们好!在 Java 编程中,接口和抽象类这对"黄金搭档"总是让人又爱又惑。今天我们将通过一个缓存热启动的实战案例,深入剖析两者的应用场景与设计哲学。建议结合代码实践理解(代码链接在文末)。
一、业务背景与挑战
1.1 缓存热启动的核心价值
在我们的系统中,缓存热启动承担着系统性能优化的关键角色。通过预加载高频访问数据(如省市区数据、系统字典),实现:
- 降低冷启动延迟:避免首请求穿透数据库
- 流量削峰:应对突发流量冲击
- 服务稳定性:保障核心业务数据高可用
1.2 初始方案痛点分析
基础实现中我们采用直接编码方式:
// 省市区缓存实现
@Slf4j
@Component
public class AreaCache {
@Resource
private AreaProvinceService areaProvinceService;
@Resource
private AreaCityService areaCityService;
@Resource
private AreaCountyService areaCountyService;
// redis模板
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 线程池
private final static ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10);
// 缓存KEY
private final static String AREA_CACHE_KEY = "area:cache:key";
@SneakyThrows
public void init() {
long startTime = System.currentTimeMillis();
List<AreaProvince> provinces = areaProvinceService.list();
if (provinces.isEmpty()) {
return;
}
List<CompletableFuture<Void>> futures = provinces.stream().map(
province -> CompletableFuture.runAsync(() -> {
AreaProvinceVO areaProvinceVO = buildProvinceTree(province);
redisTemplate.opsForValue().set(AREA_CACHE_KEY + province.getProvinceId(), JSON.toJSONString(areaProvinceVO));
})
).toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(15, TimeUnit.MINUTES);
long endTime = System.currentTimeMillis();
log.info("共加载省份: {} 个, 用时: {} ms", provinces.size(), endTime - startTime);
}
private AreaProvinceVO buildProvinceTree(AreaProvince areaProvince) {
List<AreaProvinceVO.AreaCityVO> cities = areaCityService.lambdaQuery()
.eq(AreaCity::getProvinceId, areaProvince.getProvinceId())
.list().stream().map(this::buildCityTree).toList();
return AreaProvinceVO.builder()
.provinceId(areaProvince.getProvinceId())
.provinceName(areaProvince.getName())
.cities(cities)
.build();
}
private AreaProvinceVO.AreaCityVO buildCityTree(AreaCity areaCity) {
List<AreaProvinceVO.AreaCountyVO> counties = areaCountyService.lambdaQuery()
.eq(AreaCounty::getCityId, areaCity.getCityId())
.list().stream().map(county ->
AreaProvinceVO.AreaCountyVO.builder()
.countyId(county.getCountyId())
.countyName(county.getName())
.build()).toList();
return AreaProvinceVO.AreaCityVO.builder()
.cityId(areaCity.getCityId())
.cityName(areaCity.getName())
.counties(counties)
.build();
}
public AreaProvinceVO get(String provinceId) {
if (!redisTemplate.hasKey(AREA_CACHE_KEY + provinceId)) {
return null;
}
return JSONObject.parseObject(redisTemplate.opsForValue().get(AREA_CACHE_KEY + provinceId).toString(), AreaProvinceVO.class);
}
public void clear() {
redisTemplate.delete(redisTemplate.keys(AREA_CACHE_KEY + "*"));
}
public void reload() {
clear();
init();
}
@PreDestroy
private void shutdownThreadPool() {
try {
EXECUTOR_SERVICE.shutdown();
if (EXECUTOR_SERVICE.awaitTermination(15, TimeUnit.MINUTES)) {
log.info("{} 线程池自动销毁", this.getClass().getName());
EXECUTOR_SERVICE.shutdownNow();
}
} catch (Exception e) {
log.error("{} 线程池自动销毁失败", this.getClass().getName());
EXECUTOR_SERVICE.shutdownNow();
}
}
}
// 字典缓存实现
@Slf4j
@Component
public class DictCache {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private SysDictTypeService dictTypeService;
@Resource
private SysDictDataService dictDataService;
private final static ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10);
private final static String DICT_CACHE_KEY = "dict:cache:key";
@SneakyThrows
public void init() {
long startTime = System.currentTimeMillis();
List<SysDictType> dictTypes = dictTypeService.list();
if (dictTypes.isEmpty()) {
return;
}
List<CompletableFuture<Void>> futures = dictTypes.stream().map(
dictType -> CompletableFuture.runAsync(() -> {
DictVO dictVO = buildDictTree(dictType);
redisTemplate.opsForValue().set(DICT_CACHE_KEY + dictType.getDictType(), JSON.toJSONString(dictVO));
})
).toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(15, TimeUnit.MINUTES);
long endTime = System.currentTimeMillis();
log.info("共加载字典: {} 个, 用时: {} ms", dictTypes.size(), endTime - startTime);
}
private DictVO buildDictTree(SysDictType dictType) {
List<DictVO.DictDataVO> dictDataList = dictDataService.lambdaQuery()
.eq(SysDictData::getDictType, dictType.getDictType())
.list().stream().map(dictData -> DictVO.DictDataVO.builder()
.dictValue(dictData.getDictValue())
.dictLabel(dictData.getDictLabel())
.build()).toList();
return DictVO.builder()
.dictType(dictType.getDictType())
.dictName(dictType.getDictName())
.dictDataList(dictDataList)
.build();
}
public DictVO get(String dictType) {
if (!redisTemplate.hasKey(DICT_CACHE_KEY + dictType)) {
return null;
}
return JSONObject.parseObject(redisTemplate.opsForValue().get(DICT_CACHE_KEY + dictType).toString(), DictVO.class);
}
public void clear() {
redisTemplate.delete(redisTemplate.keys(DICT_CACHE_KEY + "*"));
}
public void reload() {
clear();
init();
}
@PreDestroy
private void shutdownThreadPool() {
try {
EXECUTOR_SERVICE.shutdown();
if (EXECUTOR_SERVICE.awaitTermination(15, TimeUnit.MINUTES)) {
log.info("{} 线程池自动销毁", this.getClass().getName());
EXECUTOR_SERVICE.shutdownNow();
}
} catch (Exception e) {
log.error("{} 线程池自动销毁失败", this.getClass().getName());
EXECUTOR_SERVICE.shutdownNow();
}
}
}
// 统一初始化
@Component
public class CacheRunner implements InitializingBean {
@Resource
private AreaCache areaCache;
@Resource
private DictCache dictCache;
@Override
public void afterPropertiesSet() throws Exception {
areaCache.init();
dictCache.init();
}
}
这种实现存在三个明显问题:
- 代码重复:线程池管理、Redis操作等重复逻辑
- 扩展困难:新增缓存类型需修改启动类
- 维护成本:同类功能散落在不同类中
二、接口方案:标准化契约
2.1 接口设计哲学
通过定义IHotCache
接口,我们确立了缓存组件的标准契约:
public interface IHotCache<T, R> {
void init();
R get(T requestParams);
void clear();
void reload();
}
2.2 实现类改造
具体缓存组件实现接口契约:
@Component
public class AreaCache implements IHotCache<String, AreaProvinceVO> {
// 保持原有核心逻辑
// 实现接口定义的方法
}
@Component
public class DictCache implements IHotCache<String, DictVO> {
// 保持原有核心逻辑
// 实现接口定义的方法
}
2.3 统一初始化机制
利用Spring的依赖注入特性,实现自动化装配:
@Component
public class CacheRunner implements InitializingBean {
@Resource
private List<IHotCache<?, ?>> hotCacheList;
@Override
public void afterPropertiesSet() {
hotCacheList.forEach(IHotCache::init);
}
}
接口方案优势:
- 开闭原则:新增缓存类型无需修改已有代码
- 统一契约:规范缓存组件的行为标准
- 依赖倒置:高层模块不依赖具体实现
三、抽象类方案:代码复用艺术
3.1 抽象类设计理念
在接口基础上,通过抽象类实现公共逻辑下沉:
public abstract class AbstractCache<T, R> implements IHotCache<T, R> {
protected final ExecutorService executor = Executors.newFixedThreadPool(10);
@Resource
protected RedisTemplate<String, Object> redisTemplate;
@Override
public void reload() {
clear();
init();
}
@PreDestroy
protected void shutdownExecutor() {
// 统一线程池关闭逻辑
}
}
3.2 具体实现简化
子类只需关注业务差异部分:
@Component
public class AreaCache extends AbstractCache<String, AreaProvinceVO> {
// 仅保留数据构建等业务逻辑
// 公共方法已由抽象类实现
}
@Component
public class DictCache extends AbstractCache<String, DictVO> {
// 仅保留数据构建等业务逻辑
// 公共方法已由抽象类实现
}
抽象类方案突破:
- 代码复用率提升60%以上
- 统一资源管理(线程池/Redis模板)
- 标准化生命周期管理
- 异常处理统一化
四、架构演进对比分析
维度 | 基础实现 | 接口方案 | 抽象类方案 |
---|---|---|---|
代码复用 | 无 | 方法级 | 组件级 |
扩展性 | 修改启动类 | 新增实现类即可 | 新增实现类即可 |
维护成本 | 高 | 中 | 低 |
规范程度 | 无标准 | 接口契约 | 接口+实现规范 |
资源管理 | 各自为政 | 各自为政 | 统一管理 |
适合场景 | 简单临时方案 | 多实现场景 | 复杂公共逻辑 |
五、设计模式深度思考
- 接口与抽象类不是二选一:最佳实践是接口定义契约,抽象类实现公共逻辑
- 模板方法模式:抽象类的reload()方法正是模板方法的典型应用
- 开闭原则实践:对扩展开放(新增缓存类型),对修改关闭(不改动已有代码)
- 资源隔离:虽然使用公共线程池,但通过固定线程数控制资源竞争
思考题:为什么抽象类中要定义protected的RedisTemplate,而不是通过构造函数注入?
六、性能优化启示录
- 并行加载:使用CompletableFuture实现省级数据的并行加载
- 超时控制:统一15分钟超时机制,防止线程阻塞
- 优雅停机:通过@PreDestroy实现线程池的有序关闭
// 典型的并行加载实现
List<CompletableFuture<Void>> futures = provinces.stream()
.map(province -> CompletableFuture.runAsync(() -> {
// 数据处理逻辑
}, executor))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(15, TimeUnit.MINUTES);
源码获取:GitHub项目地址
欢迎大家在评论区分享你的心得,对于文中提到的思考题,你有怎样的见解?在实际项目中你是如何平衡接口与抽象类的使用?期待你的真知灼见!