我设计了一个牛逼的本地缓存!_v get(k key, callable<? extends v> loader)-程序员宅基地

技术标签: 缓存设计  

最近在看Mybatis的源码,刚好看到缓存这一块,Mybatis提供了一级缓存和二级缓存;一级缓存相对来说比较简单,功能比较齐全的是二级缓存,基本上满足了一个缓存该有的功能;当然如果拿来和专门的缓存框架如ehcache来对比可能稍有差距;本文我们将来整理一下实现一个本地缓存都应该需要考虑哪些东西。

考虑点
考虑点主要在数据用何种方式存储,能存储多少数据,多余的数据如何处理等几个点,下面我们来详细的介绍每个考虑点,以及该如何去实现;

1.数据结构
首要考虑的就是数据该如何存储,用什么数据结构存储,最简单的就直接用Map来存储数据;或者复杂的如redis一样提供了多种数据类型哈希,列表,集合,有序集合等,底层使用了双端链表,压缩列表,集合,跳跃表等数据结构;

2.对象上限
因为是本地缓存,内存有上限,所以一般都会指定缓存对象的数量比如1024,当达到某个上限后需要有某种策略去删除多余的数据;

3.清除策略
上面说到当达到对象上限之后需要有清除策略,常见的比如有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等策略;

4.过期时间
除了使用清除策略,一般本地缓存也会有一个过期时间设置,比如redis可以给每个key设置一个过期时间,这样当达到过期时间之后直接删除,采用清除策略+过期时间双重保证;

5.线程安全
像redis是直接使用单线程处理,所以就不存在线程安全问题;而我们现在提供的本地缓存往往是可以多个线程同时访问的,所以线程安全是不容忽视的问题;并且线程安全问题是不应该抛给使用者去保证;

6.简明的接口
提供一个傻瓜式的对外接口是很有必要的,对使用者来说使用此缓存不是一种负担而是一种享受;提供常用的get,put,remove,clear,getSize方法即可;

7.是否持久化
这个其实不是必须的,是否需要将缓存数据持久化看需求;本地缓存如ehcache是支持持久化的,而guava是没有持久化功能的;分布式缓存如redis是有持久化功能的,memcached是没有持久化功能的;

8.阻塞机制
在看Mybatis源码的时候,二级缓存提供了一个blocking标识,表示当在缓存中找不到元素时,它设置对缓存键的锁定;这样其他线程将等待此元素被填充,而不是命中数据库;其实我们使用缓存的目的就是因为被缓存的数据生成比较费时,比如调用对外的接口,查询数据库,计算量很大的结果等等;这时候如果多个线程同时调用get方法获取的结果都为null,每个线程都去执行一遍费时的计算,其实也是对资源的浪费;最好的办法是只有一个线程去执行,其他线程等待,计算一次就够了;但是此功能基本上都交给使用者来处理,很少有本地缓存有这种功能;

如何实现
以上大致介绍了实现一个本地缓存我们都有哪些需要考虑的地方,当然可能还有其他没有考虑到的点;下面继续看看关于每个点都应该如何去实现,重点介绍一下思路;

1.数据结构
本地缓存最常见的是直接使用Map来存储,比如guava使用ConcurrentHashMap,ehcache也是用了ConcurrentHashMap,Mybatis二级缓存使用HashMap来存储:

Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>()

Mybatis使用HashMap本身是非线程安全的,所以可以看到起内部使用了一个SynchronizedCache用来包装,保证线程的安全性;
当然除了使用Map来存储,可能还使用其他数据结构来存储,比如redis使用了双端链表,压缩列表,整数集合,跳跃表和字典;当然这主要是因为redis对外提供的接口很丰富除了哈希还有列表,集合,有序集合等功能;

2.对象上限
本地缓存常见的一个属性,一般缓存都会有一个默认值比如1024,在用户没有指定的情况下默认指定;当缓存的数据达到指定最大值时,需要有相关策略从缓存中清除多余的数据这就涉及到下面要介绍的清除策略;

3.清除策略
配合对象上限之后使用,场景的清除策略如:LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用);
LRU :Least Recently
Used的缩写最近最少使用,移除最长时间不被使用的对象;常见的使用LinkedHashMap来实现,也是很多本地缓存默认使用的策略;
FIFO :先进先出,按对象进入缓存的顺序来移除它们;常见使用队列Queue来实现;
LFU :Least Frequently
Used的缩写大概也是最近最少使用的意思,和LRU有点像;区别点在LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的;可以通过HashMap并且记录访问次数来实现;
SOFT :软引用基于垃圾回收器状态和软引用规则移除对象;常见使用SoftReference来实现;
WEAK :弱引用更积极地基于垃圾收集器状态和弱引用规则移除对象;常见使用WeakReference来实现;

4.过期时间
设置过期时间,让缓存数据在指定时间过后自动删除;常见的过期数据删除策略有两种方式:被动删除和主动删除;
被动删除 :每次进行get/put操作的时候都会检查一下当前key是否已经过期,如果过期则删除,类似如下代码:

    if (System.currentTimeMillis() - lastClear > clearInterval) {
          clear();
    }

主动删除 :专门有一个job在后台定期去检查数据是否过期,如果过期则删除,这其实可以有效的处理冷数据;

5.线程安全
尽量用线程安全的类去存储数据,比如使用ConcurrentHashMap代替HashMap;或者提供相应的同步处理类,比如Mybatis提供了SynchronizedCache:

     public synchronized void putObject(Object key, Object object) {
        ...省略...
      }

      @Override
      public synchronized Object getObject(Object key) {
        ...省略...
      }

6.简明的接口
提供常用的get,put,remove,clear,getSize方法即可,比如Mybatis的Cache接口:

    public interface Cache {
      String getId();
      void putObject(Object key, Object value);
      Object getObject(Object key);
      Object removeObject(Object key);
      void clear();
      int getSize();
      ReadWriteLock getReadWriteLock();
    }

再来看看guava提供的Cache接口,相对来说也是比较简洁的:

    public interface Cache<K, V> {
      V getIfPresent(@CompatibleWith("K") Object key);
      V get(K key, Callable<? extends V> loader) throws ExecutionException;
      ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
      void put(K key, V value);
      void putAll(Map<? extends K, ? extends V> m);
      void invalidate(@CompatibleWith("K") Object key);
      void invalidateAll(Iterable<?> keys);
      void invalidateAll();
      long size();
      CacheStats stats();
      ConcurrentMap<K, V> asMap();
      void cleanUp();
    }

7.是否持久化
持久化的好处是重启之后可以再次加载文件中的数据,这样就起到类似热加载的功效;比如ehcache提供了是否持久化磁盘缓存的功能,将缓存数据存放在一个.data文件中;

    diskPersistent="false" //是否持久化磁盘缓存

redis更是将持久化功能发挥到极致,慢慢的有点像数据库了;提供了AOF和RDB两种持久化方式;当然很多情况下可以配合使用两种方式;

8.阻塞机制
除了在Mybatis中看到了BlockingCache来实现此功能,之前在看<>的时候其中有实现一个很完美的缓存,大致代码如下:

    public class Memoizerl<A, V> implements Computable<A, V> {
        private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
        private final Computable<A, V> c;

        public Memoizerl(Computable<A, V> c) {
            this.c = c;
        }

        @Override
        public V compute(A arg) throws InterruptedException, ExecutionException {
            while (true) {
                Future<V> f = cache.get(arg);
                if (f == null) {
                    Callable<V> eval = new Callable<V>() {
                        @Override
                        public V call() throws Exception {
                            return c.compute(arg);
                        }
                    };
                    FutureTask<V> ft = new FutureTask<V>(eval);
                    f = cache.putIfAbsent(arg, ft);
                    if (f == null) {
                        f = ft;
                        ft.run();
                    }
                    try {
                        return f.get();
                    } catch (CancellationException e) {
                        cache.remove(arg, f);
                    }
                }
            }
        }
    }

compute是一个计算很费时的方法,所以这里把计算的结果缓存起来,但是有个问题就是如果两个线程同时进入此方法中怎么保证只计算一次,这里最核心的地方在于使用了ConcurrentHashMap的putIfAbsent方法,同时只会写入一个FutureTask;

总结
本文大致介绍了要设计一个本地缓存都需要考虑哪些点:数据结构,对象上限,清除策略,过期时间,线程安全,阻塞机制,实用的接口,是否持久化;当然肯定有其他考虑点,欢迎补充。

其他文章

0. 免费帮忙下载csdn和百度文库资料福利

1. 学习笔记和学习资料汇总:前端 + 后端 + java + 大数据 + python + 100多实战项目 + C++

2. 我的秋招经历总结:一站式秋招规划

3. 零基础学爬虫

4. 零基础C++学习总结

欢迎关注个人公众号【菜鸟名企梦】,公众号专注:互联网求职面经javapython爬虫大数据等技术分享:

公众号菜鸟名企梦后台发送“csdn”即可免费领取【csdn】和【百度文库】下载服务;

公众号菜鸟名企梦后台发送“资料”:即可领取5T精品学习资料java面试考点java面经总结,以及几十个java、大数据项目,资料很全,你想找的几乎都有
扫码关注,及时获取更多精彩内容。(博主985、A+学科硕士,今日头条大数据工程师)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/liewen_/article/details/110597639

智能推荐

将一个线程不安全的集合转换成线程安全的_list 变成线程安全的-程序员宅基地

将一个线程不安全的集合转换成线程安全的_list 变成线程安全的

oracle设置环境变量语言,Oracle环境变量设置脚本-程序员宅基地

每次都傻乎乎的往bashrc里面写环境变量,感觉不任性。于是,看了本书了解了/etc/oratab这个东东后,参考着书也写了一个设置Oracle环境变量的脚本。在/etc/下创建oraset,权限设置为chown oracle:oinstall /etc/orasetoraset内容:#!/bin/bash# Sets Oracle environment variables.# Setup: ...._[info] [setp 4/9] set oracle environment variables

asp.net夜话之八:数据绑定控件-程序员宅基地

通过前面的例子我们看到每次我们要显示数据的时候都要通过一个循环来显示满足条件的数据,这是一个比较麻烦的过程,为此微软定义了一系列的控件专门用于显示数据的格式,通过这些控件可以以可视化的方式查看绑定数据之后的效果。这些控件称之为数据绑定控件。在asp.net中所有的数据库绑定控件都是从BaseDataBoundControl这个抽象类派生的,这个抽象类定义了几个重要属性和一个重要方法:DataS...

android camera获取yuv数据、旋转_imagereader 转yuv_原总破局的博客-程序员宅基地

https://blog.csdn.net/qq_15255121/article/details/119041652?spm=1001.2014.3001.5501这篇文章我们已经讲了如何才能从摄像头中获取到yuv数据。那么这篇文章我们讲一下如何生成yuv的文件。yuv是原始数据,我们只要把一帧一帧图片的yuv数据保存起来就可以。这里有个小插曲 mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailabl_imagereader 转yuv

从菜鸟到巨星:程序员职业生涯的11个阶段-程序员宅基地

程序员的职业生涯是一段充满起伏的有趣经历。考虑到其陡峭的学习曲线,完全可以预见你将经历挫折、启蒙、骄傲自大这几个时期,以及穿插其间的各种心路历程。在这篇文章中让我们轻松一下。阶段1-菜鸟初入职场的日子,我们都充满激情。在此期间,你毫无头绪,根本不知道该做什么。就像鱼儿离开了水,每一行代码对你来说都是个迷。Doctype?哈?见鬼,这个到底是干什么用的?第一个阶段令人生畏、让人提心吊胆,却又激动...

12月更新!EasyOps全平台产品能力再升级,新增22+功能亮点解读~-程序员宅基地

为此,Hub商店支持制作运维作业资源,即一个作业资源可封装多个作业,同时封装了作业的菜单项、所使用的工具/流程信息,开箱即用,同时运维作业小产品支持Hub作业资源的离线安装。当我们的业务具备一定规模时,经常会伴随多脚本,多数据库实例的变更,但由于数据库变更采用的是一种串行执行的策略,一次大规模的变更需要花费长达数个小时的变更时间。ITSM 目前的工单体系里有大量的父子工单和关联工单,但在工单的列表中没有相关的展示,不能直观找到某个工单的父子工单和关联工单,只能点进详情里看,效率较低。

随便推点

对称加密和非对称加密的区别-程序员宅基地

对称加密: 加密和解密的秘钥使用的是同一个. 非对称加密:与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。对称加密算法:密钥较短,破译困难,除了数据加密标准(DES),另一个对称密钥加密系统是国际数据加密算法(IDEA),它比DES的加密性好,且对计算机性能要求也没有那么高.优点: 算法公开、计算量小、加密速度快、加密效率高缺点: 在数据传送前,发送方和接收方必须商定好秘钥,然后 使双方都能保存好秘钥。其次..._对称加密和非对称加密的区别

微信小程序授权登录取消授权重新授权处理方法 附可用代码_微信小程序取消登录授权怎么实现代码运行-程序员宅基地

微信小程序授权登录基本是小程序的标配了,但是官方的demo,取消授权后,就不能再重新点击登录,除非重新加载小程序才可以,这下怎么办?我们可以先在首页引导用户点击,然后跳转到一个新的页面,在新的页面进行授权,然后新的页面授权成功,立马跳回首页,显示用户信息。话不多说,直接上代码代码结构:index是首页,login是授权页首页代码index.wxml<..._微信小程序取消登录授权怎么实现代码运行

Flex 布局教程:语法篇 ---作者: 阮一峰_flex布局阮一峰语法篇-程序员宅基地

Flex 布局教程:语法篇网页布局(layout)是 CSS 的一个重点应用。布局的传统解决方案,基于[盒状模型],依赖 [display]属性 + [position]属性 + [float]属性。它对于那些特殊布局非常不方便,比如,[垂直居中]就不容易实现。2009年,W3C 提出了一种新的方案----Flex 布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏..._flex布局阮一峰语法篇

使用mp4v2将aac音频h264视频数据封装成mp4开发心得-程序员宅基地

这阵子在捣鼓一个将游戏视频打包成本地可播放文件的模块。开始使用avi作为容器,弄了半天无奈avi对aac的支持实在有限,在播放时音视频时无法完美同步。关于这点avi文档中有提到:For AAC, one RAW AAC frame usually spans over 1024 samples. However, depending onthe source container_mp4v2

手机应用图标的展现扩展方式-程序员宅基地

手机里的应用多了,应用分类就显得很有必要了,可以使用windows系统里的开始--所有程序,这样的经典查找方法也可以使用一个酷炫的扇形扩展,点击一个分类图标,在整个屏幕上弹出里面的多个应用,如同打开的扇子,和传统的二级分类相比,效果更加酷,显示的图标也更加的多,索引效果更好