Java 接口与抽象类实战对比:缓存热启动设计模式演进

330 阅读5分钟

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();
    }
}

这种实现存在三个明显问题:

  1. 代码重复:线程池管理、Redis操作等重复逻辑
  2. 扩展困难:新增缓存类型需修改启动类
  3. 维护成本:同类功能散落在不同类中

二、接口方案:标准化契约

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> {
    // 仅保留数据构建等业务逻辑
    // 公共方法已由抽象类实现
}

抽象类方案突破

  1. 代码复用率提升60%以上
  2. 统一资源管理(线程池/Redis模板)
  3. 标准化生命周期管理
  4. 异常处理统一化

四、架构演进对比分析

维度基础实现接口方案抽象类方案
代码复用方法级组件级
扩展性修改启动类新增实现类即可新增实现类即可
维护成本
规范程度无标准接口契约接口+实现规范
资源管理各自为政各自为政统一管理
适合场景简单临时方案多实现场景复杂公共逻辑

五、设计模式深度思考

  1. 接口与抽象类不是二选一:最佳实践是接口定义契约,抽象类实现公共逻辑
  2. 模板方法模式:抽象类的reload()方法正是模板方法的典型应用
  3. 开闭原则实践:对扩展开放(新增缓存类型),对修改关闭(不改动已有代码)
  4. 资源隔离:虽然使用公共线程池,但通过固定线程数控制资源竞争

思考题:为什么抽象类中要定义protected的RedisTemplate,而不是通过构造函数注入?

六、性能优化启示录

  1. 并行加载:使用CompletableFuture实现省级数据的并行加载
  2. 超时控制:统一15分钟超时机制,防止线程阻塞
  3. 优雅停机:通过@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项目地址

欢迎大家在评论区分享你的心得,对于文中提到的思考题,你有怎样的见解?在实际项目中你是如何平衡接口与抽象类的使用?期待你的真知灼见!