实现一个高效的本地缓存

不知道经常学习的你有没有这些疑惑。
为什么面试官总是会问hashmap的实现原理?
spring源码看了到底有没有用啊?
面试官总问我线程池干啥?还问得那么深入!

又或者是这些疑惑:
hashmap实现原理看了百八十遍了,spring源码也撸了两三遍,
线程池每天看都能手写了,但我公司小,项目上用不到这些技术啊。

今天就来介绍一个本地缓存的工具让你对这些技术有更深入的理解!
实现一个高效的本地缓存

缓存使用场景如下:

像我是做机票业务的,项目里经常会用到机场,航司,城市,国家,机型等业务信息,这些数据在99%的情况下是不常更新的,如果每次都去请求接口,那就太浪费用户的请求时间了。
这时候缓存的必要性就体现出来了。

那么问题来了,分布式缓存还是本地缓存 如何取舍?

1、缓存性能PK
redis:在国内国际机票查询场景下,一个城市对单程 可能会查询出来200多条航线,航线里面还有舱位,中转等场景,再加上对航司的PK 过滤逻辑,一趟航班可能有20次要往redis里去数据,200条航线 20 = 4000次查询;如果当前接口的qps 是1000,4000 1000 = 1000000 (100w / s) 就这么一个简单的场景每秒需要请求 100w /s redis ,redis 的 iops 性能再高也撑不住啊, 从redis 取出数据后 还需要反序列化,反序列化是一个消耗CPU 的操作,这么大的数据量还会提高对 CPU 的负载。redis 似乎有点堪忧。

本地缓存:将机场,城市这些信息直接放进 JVM 中,由 JVM 统一管理,本地缓存几乎不用考虑性能,也不要序列化。看起来还不错。

2、缓存更新PK
redis:redis 是分布式缓存,那么对应的缓存定时更新的需求交给分布式定时任务框架来做就好了,我们使用的是去哪儿的 qschedule ,开源的可以使用 当当的 elastic-job
本地缓存:本地缓存可以使用Quartz,spring schedule 来实现。

顺便提一下,分布式定时任务框架在选型时应该考虑到以下几点:

1、必须要在指定时间执行(延时必须少或者没有)
2、日志有追溯
3、集群部署不能重复执行
4、集群可以弹性扩容
5、阻塞、异常等处理策略
6、是否与 spring 整合
7、社区力量是否强大,文档是否丰富且及时更新
8、高可用
9、定时任务能否重用、分开执行

所以综合来看使用缓存主要是考虑性能,本地缓存似乎性能更好一些。

本地缓存实现的要点又有哪些呢?

实现一个高效的本地缓存

扫描二维码关注公众号,回复: 12289306 查看本文章

1、定时异步执行:Quartz 和 spring schedule 都可以做到异步和定时

2、初始化:需要在项目服务器启动完成后,缓存数据就得加载完成,似乎所有的定时任务都无法做到在这一点,于是只能将定时跑缓存数据的任务交给延时线程池 ScheduledThreadPoolExecutor 来完成了,基于初始化就更用不得分布式缓存了。

3、重试机制:当数据源的服务发生异常或超时,缓存数据就会更新失败,但接口的发生异常毕竟是少数情况,这时候就需要重新请求接口,我们可以将重试次数配置在注解里。

4、更新:接口请求成功后,新数据将替换旧数据。
使用 con实现一个高效的本地缓存currentHashMap 来实现,做到相同key 直接覆盖。

本地缓存的实现思路:

使用注解作为在方法级别,配置的要求用注解的属性来表示即可:

2、定义springbean对象实现ApplicationListener接口,监听springContextRefreshedEvent事件容器初始化完所有业务bean以后开始初始化缓存组件。

@Component("beanDefineConfig")
public class BeanDefineConfig implements ApplicationListener<ContextRefreshedEvent> {
  @Autowired
  private OnlineCacheManager cacheManager;

  @Override
  public void onApplicationEvent(ContextRefreshedEvent event) {
    cacheManager.initCache();
  }
}

3、初始化延时线程池, 拿到所有的实现了CacheBean接口的对象 获取缓存配置

4、将注解的配置赋值给OnlineCache这个对象
聚合所有的缓存配置给cacheMap

public class OnlineCacheManager {

  private static final ILog LOGGER = LogManager.getLogger(OnlineCacheManager.class);
  private Map<String, OnlineCache> cacheMap = new ConcurrentHashMap<>();
  private ApplicationContext applicationContext;
  private ScheduledThreadPoolExecutor executor;
  private Object lock = new Object();
  private List<Runnable> executeRunnableList = new ArrayList<>();
  private boolean isInit = true;

  public synchronized void initCache() {
    try {
      if (executor == null) {
        executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(5);
      }
      cacheMap.clear();
      executeRunnableList.clear();
      Map<String, CacheBean> map = applicationContext.getBeansOfType(CacheBean.class);
      Collection<CacheBean> beans = map.values();
      for (CacheBean bean : beans) {
        List<Method> methods = OnlineCollectionUtil.findAll(
            Arrays.asList(bean.getClass().getMethods()),
            x -> x.getAnnotation(CacheAnnotation.class) != null);
        for (Method method : methods) {
          OnlineCache cache = new OnlineCache();
          CacheAnnotation cacheAnnotation = method.getAnnotation(CacheAnnotation.class);
          Parameter[] parameters = method.getParameters();
          cache.setContainParam(parameters != null && parameters.length > 0);
          cache.setAutoRefresh(cacheAnnotation.autoFlash());
          cache.setCacheBean(bean);
          cache.setCacheName(cacheAnnotation.name());
          cache.setTimeOut(getTimeOut(cacheAnnotation.timeOut(), cacheAnnotation.timeType()));
          cache.setData(new ConcurrentHashMap<>());
          cache.setParams(new ConcurrentHashMap<>());
          cache.setDescription(cacheAnnotation.description());
          cache.setHandler(convertHandler(method, bean));
          cache.setDependentReference(cacheAnnotation.dependentReference() != null
              && cacheAnnotation.dependentReference().length > 0 ? cacheAnnotation
              .dependentReference() : null);
          cache.setEssential(cacheAnnotation.essential());
          cache.setRetryTimes(cacheAnnotation.retryTimes());
          cacheMap.put(cacheAnnotation.name(), cache);
        }
      }
      // 为了解决缓存之间的依赖问题 不做深究
      List<String> keyList = sortKey();
      for (String key : keyList) {
        OnlineCache cache = cacheMap.get(key);
        executeSaveCache(cache);
        if (cache.isAutoRefresh()) {
          Runnable runnable = () -> executeSaveCache(cache);
          executor.scheduleAtFixedRate(runnable, cache.getTimeOut(),
              cache.getTimeOut(), TimeUnit.MILLISECONDS);
          executeRunnableList.add(runnable);
        }
      }
    } catch (Throwable e) {
      LOGGER.error(e.getMessage(), e);
    }
  }
  }

5、开始请求接口,把结果放进缓存
key 是注解的name value 是 对应的所有结果集Map<String,cacheEntity>

@Component
public class AirportCacheHelper implements CacheBean {
    private static final ILog LOGGER = LogManager.getLogger(AirportCacheHelper.class);

    @Autowired
    OnlineCacheManager onlineCacheManager;
    /**
     * 获取所有的机场信息
     *
     * @return the all airports
     */
    public Map<String, AirportEntity> getAllAirports() {
        return onlineCacheManager.getCache(LocalCacheConstant.CODE_TO_AIRPORT_MAP);
    }

    /**
     * 缓存初始化
     */
    @CacheAnnotation(name = LocalCacheConstant.CODE_TO_AIRPORT_MAP, timeOut = 120, essential = true, retryTimes = 3)
    public Map<String, AirportEntity> initCodeToAirportMap() {
        Map<String, AirportEntity> map = null;
        // 从数据源接口获取需要缓存的数据  故不展示纯业务代码
        List<AirportEntityWs> airports = getAirportsFromSoa();
        if (!CollectionUtils.isEmpty(airports)) {
            map = new HashMap<>(airports.size());
            for (AirportEntityWs soaEntity : airports) {
                if (map.containsKey(soaEntity.getCode())) {
                    continue;
                }
                AirportEntity cacheEntity = new AirportEntity();
                cacheEntity.setCode(soaEntity.getCode());
                cacheEntity.setAddress(soaEntity.getAddress());
                cacheEntity.setAirportPy(soaEntity.getAirportPY());
                cacheEntity.setCityId(soaEntity.getCityID());
                cacheEntity.setCityCode(soaEntity.getCityCode());
                cacheEntity.setDistance(soaEntity.getDistance());
                cacheEntity.setName(soaEntity.getName());
                cacheEntity.setNameEn(soaEntity.getName_En());
                cacheEntity.setShortName(soaEntity.getShortName());
                cacheEntity.setTelphone(soaEntity.getTelphone());
                cacheEntity.setSuperShortName(soaEntity.getSuperShortName());
                cacheEntity.setShortNameEn(soaEntity.getShortName_En());
                cacheEntity.setLocType(soaEntity.getLocType());
                cacheEntity.setLatitude(soaEntity.getLatitude());
                cacheEntity.setLongitude(soaEntity.getLongitude());

                map.put(cacheEntity.getCode(), cacheEntity);
            }
        }
        return map;
    }

    // 在 OnlineCacheManager 中获取缓存
    public <T> T getCache(String cacheName, Object... params) {
    T t = null;
    try {
      if (cacheMap.containsKey(cacheName)) {
        OnlineCache cache = cacheMap.get(cacheName);
        long nowTime = System.currentTimeMillis();
        if (!cache.isContainParam()) {
          if (cache.isAutoRefresh()) {
            t = (T) cache.getData().get(cacheName);
          } else {
            t = getStaticCache(cacheName, cache, nowTime);
          }
        } else {
          if (params != null && params.length > 0) {
            StringBuilder cacheKey = new StringBuilder(cacheName);
            for (Object o : params) {
              cacheKey.append(o.hashCode());
            }
            cache.getParams().put(cacheKey.toString(), params);
            t = getStaticCache(cacheKey.toString(), cache, nowTime);
          }
        }
      }
    } catch (Throwable e) {
      LOGGER.error(e);

    }
    return t;
  }

6、通过 API CacheManageHelper.getXXXEntity(Stringcode) 调用即可


/**
     * 根据机场三字码获取机场信息
     *
     * @param code the code
     * @return the airport entity
     */
    public static AirportEntity getAirportEntity(String code) {
        AirportEntity entity = null;
        if (StringUtils.isEmpty(code)) {
            return entity;
        }
        Map<String, AirportEntity> dataMap = airportCacheHelper.getAllAirports();
        if (null != dataMap && dataMap.containsKey(code)) {
            entity = dataMap.get(code);
        }
        return entity;
    }

总结:

这个本地缓存实现起来并不难,关键是需要找到业务上的痛点,并灵活的运用所学会的技术去加以实现。

看过springIOC的源码 或是 springMVC 的源码 会spring运用反射有很深入的理解,知道待会儿要反射,就能想到 我是不是可以封装一个 handler呢、
看 过hashmap 的源码就知道根据缓存数据的大小去 初始化 hashmap,避免过大的hashmap扩容占用CPU资源、看过线程池才知道缓存初始化的时候定时任务做不到这点。刚好spring 的schedule 也是用的限时线程池,这都是长期技术积累沉淀出来的结果。

看完这个缓存组件是不是对这些技术的理解及运用更深了呢?
喜欢的朋友 顺手来个关注呀实现一个高效的本地缓存

猜你喜欢

转载自blog.51cto.com/15075523/2606417