缓存当道,如何通用

文摘   2022-11-16 11:40   江苏  


  1.  前言 

    无论是网站还是APP,在用户登陆成功后都会有一个主页,这个页面渲染时间的快慢直接决定了用户要不要放弃、会不会吐槽、问不问候技术全家。

    而更过分的是上面都发生了,还不停的在刷,卡的问题很有可能就会由点到面到瘫痪。


  2. 调优

    这个问题就比较广泛了有很多方面可以下手,相对来说,正常情况下比较耗时的有http网络开销和磁盘IO开销,多数会首先从这2个下手优化,本人这边放一些经历过的操作:

    前端:合理发送http请求、合理使用localstorage|sessionstorage、合理控制页面按钮、放静态目录前压缩js|css,组件化(不是专业的,先说这么多

    后端:合理应用层代码、合理的传输数据、合理的数据提取范围、合理的应用(数据库)架构、合理的引入缓存


  3. Redis、Memcache Or MongoDB

  4. 这三者都用过,当前正使用着Redis & MongoDB,总的来说Redis多一些,根据自己的业务,选择合适的即可。

    Memcache:在360时,架构师(徐榕)写了一个注解,只要在查询方法上使用,即可自动将查询结果放到缓存中。

    MongoDB:当前正在使用,与第三方(其它部门)协作时,前期可能业务不成熟,导致外部输入经常会增减字段,这个时候如果用Mysql就比较麻烦,需要不停的应对写DDL修改表结构,MongoDB很好的解决了这个问题,查询上与Mysql语法类似

    Redis:在360与小鹏都在使用,自己写的一个工具类,主要实现将查询结果放置到缓存且尽量前置,方便下一次相同条件时从缓存获取,有效节约了微服务间RPC的时间开销与DB的IO开销,需要注意:后台管理系统编辑一些(产品)数据时,与该数据关联的缓存应该无效;至于缓存与DB间的数据一致性,这个问题不在本篇范围,网上也有很多,有兴趣的可以了解一下。

    Redis与其它二者间的对比,资料整理如下:

  5. 我的工具类

    /** * 自定义缓存读取接口 * @param <T> * @param <I> */public interface CacheLoadable<T, I extends Number> {    T load();}/** * 缓存模板方法实现 */@Component@ConditionalOnProperty(value = "xpilot.tools.redis.enable",havingValue = "true")public class CacheTemplateService {
    @Autowired private RedisComponent redisComponent;
    @Resource(name="redisTemplate") private RedisTemplate redisTemplate;
    private ConcurrentHashMap<String,String> keyMap = new ConcurrentHashMap<>();
    private volatile String cacheKey;
    /** * 缓存只是用来在某些场景增加查询效率,如果想直接当DB使用,请考虑具体业务声明 * @param redisTemplate 可以自行配置一个template,比如想配置不一样的redis db,可以重新配置一个即得到一个新的template * @param key 避免在某些时候运维介入,key的定义应该尽量区分服务名; eg:err_map_mgr_key(错误图层前缀_key) * @param expire 过期时间 * @param unit 与expire配合使用 过期单位 * @param clazz 返回类型 * @param callback 当缓存不存在时要执行的 方法(业务逻辑) * @return */ public <T> T loadFromCache(RedisTemplate redisTemplate,String key, Long expire, TimeUnit unit, TypeReference<T> clazz, CacheLoadable<T, Number> callback){ Object obj = redisComponent.get(redisTemplate,key); if(!Objects.isNull(obj)){ return JSON.parseObject(obj+"",clazz); } cacheKey = keyMap.get(key); cacheKey = StringUtils.isEmpty(cacheKey) ? key : cacheKey; synchronized (cacheKey){ obj = redisComponent.get(redisTemplate,key); if(!Objects.isNull(obj)){ return JSON.parseObject(obj+"",clazz); } T result = callback.load(); if(Objects.isNull(result)){ return null; } if(redisComponent.setNxAndTimeWithTransaction(redisTemplate,key,JSON.toJSONString(result) , expire,unit)){ keyMap.put(key,key); } return result; } }

    /** * 缓存只是用来在某些场景增加查询效率,如果想直接当DB使用,请考虑具体业务声明 * @param key 避免在某些时候运维介入,key的定义应该尽量区分服务名; eg:err_map_mgr_key(错误图层前缀_key) * @param expire 过期时间 * @param unit 与expire配合使用 过期单位 * @param clazz 返回类型 * @param callback 当缓存不存在时要执行的 方法(业务逻辑) * @return */ public <T> T loadFromCache(String key, Long expire, TimeUnit unit, TypeReference<T> clazz, CacheLoadable<T, Number> callback){ return this.loadFromCache(redisTemplate, key,expire ,unit , clazz, callback); }
    /** * Simple type of set * @param key */ public Object loadFromCache(String key){ return loadFromCache(redisTemplate, key); } public Object loadFromCache(RedisTemplate redisTemplate,String key){ Object cache = redisComponent.get(redisTemplate,key); if(Objects.isNull(cache)){ return null; } return cache; }
    /** * Simple type of set * @param key * @param object */ public void setCache(String key,Object object){ this.setCacheWithTime(key,object,null,null); }
    public boolean setCacheWithTime(String key,Object object,Long expiredTime,TimeUnit unit){ return redisComponent.setNxAndTimeWithTransaction(key,object,expiredTime,unit); }
    public Long decrement(String key,Long delta){ return redisComponent.decrement(key,delta); }
    public Long increment(String key,Long delta){ return redisComponent.increment(key,delta); }
    public Boolean expire(String key,Long expire,TimeUnit unit){ return redisComponent.expire(key,expire,unit); }
    public Long exists(String key,TimeUnit unit){ return redisComponent.ttl(key,unit); }}/***********************************时间工具类*****************************************/ /** * 避免跨天缓存导致用户看到数据不实时,所以动态计算缓存时间,确保缓存时间不超过当天 * @param def 默认值 * @param unit 单位 * @return */ public static Long dynamicCalcCacheTime(Long def,TimeUnit unit){ long base = 1000; //Default SECOND if(TimeUnit.MINUTES.equals(unit)){ base *= 60; //Cast To MINUTES } else if(TimeUnit.HOURS.equals(unit)){ base *= 60*60; //Cast To HOURS } Date now = Calendar.getInstance().getTime(); Date last = calcLastTimeOfDay(now); long diff = (last.getTime() - now.getTime()) / base; return diff > def ? def : diff; } /** * 获取指定日期对应的最后一刻时间 * @param date * @return */ public static Date calcLastTimeOfDay(Date date){ Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY,23); calendar.set(Calendar.MINUTE,59); calendar.set(Calendar.SECOND,59); calendar.set(Calendar.MILLISECOND, 999);
    return calendar.getTime(); }
    /** * 获取指定日期对应的开始一刻时间 * @param date * @return */ public static Date calcFirstTimeOfDay(Date date){ Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY,0); calendar.set(Calendar.MINUTE,0); calendar.set(Calendar.SECOND,0); calendar.set(Calendar.MILLISECOND, 000);
    return calendar.getTime();    }

    说明:上面的代码可能会因当前实现Redis操作的方式所有不同,有的用RedisTemplate、有的也直接用RedissonClient,但不影响缓存实现的方法,之所以loadFromCache方法还提供一个RedisTemplate是考虑到有的应用同时配置多个redis数据库,这样可以通过构建RedisTemplate来实现同一个应用操作不同的dbindex。


  6. 最后

    缓存多数公司都应该引入得有,看过之后若觉得有用可以试一下。本文同时也是为后续我要写的另一个主题做准备,欢迎订阅。Redis在 >= 3.2.0版本中引入了地理信息相关的操作,方便做地图的同学使用,参考链接:

    http://redisdoc.com/index.html

    有问题欢迎指出,长按可以找到我哦!!!

晚霞程序员
一位需要不断学习的30+程序员……