精华内容
下载资源
问答
  • 目前市面上已经有很多开源的缓存框架,比如Redis、Memcached、Ehcache等,那为什么还要自己动手写缓存?本章将带领大家从0到1写一个简单的缓存框架,目的是让大家对缓存的类型、缓存的标准、缓存的实现以及原理方面...

    目前市面上已经有很多开源的缓存框架,比如Redis、Memcached、Ehcache等,那为什么还要自己动手写缓存?本章将带领大家从0到1写一个简单的缓存框架,目的是让大家对缓存的类型、缓存的标准、缓存的实现以及原理方面有一个系统的了解,做到知其然,知其所以然。

    3.1 缓存定义的规范

        JSRJavaSpecification Requests写,意思是Java规范提案,已成Java界的一个重要准。在20121026JSR规范委员会发布了JSR 107JCache API)的首个早期范,该规范定义了一种对Java对象临时在内存中进行缓存的方法,包括象的建、访问、失效、一致性等。

    3.1.1 新规范的主要内容及特性

    新规范的主要内容如下:

    l    为应用程序提供缓存Java对象的功能。

    l    定义了一套通用的缓存概念和工具。

    l    最小化开发人员使用缓存的学习成本。

    l    最大化应用程序在使用不同缓存实现之间的可移植性。

    l    支持进程内和分布式的缓存实现。

    l    支持by-value和by-reference的缓存对象。

    l    定义遵照JSR-175的缓存注解;定义一套Java编程语言的元数据。

    从规范的设计角度来看,在javax.cache包中有一个CacheManager接口负责保存和控制一系列的缓存,主要特性包括:

    l    能从缓存中读取数据

    l    能将数据写入到缓存中

    l    缓存具有原子性操作

    l    具有缓存事件监听器

    l    具有缓存注解

    l    保存缓存的KEY和VALUE类型应该为泛型

    3.1.2 新规范的API介绍

    在JSR107规范中将Cache API(javax.cache)作为实现的类库,通过如下的Maven进行引入。

     

    <dependency>

               <groupId>javax.cache</groupId>

               <artifactId>cache-api</artifactId>

               <version>1.0.0</version>

    </dependency>

    1. 核心概念

    Cache API定义了4个核心概念:

    l    CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。

    l    CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。

    l    Cache:是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。

    l    Entry:是一个存储在Cache中的key-value对。

     

    每一个存储在Cache中的条目有一个定义的有效期,即Expiry Duration。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

    2. Store-By-Value和Store-By-Reference

    Store-By-Value和Store-By-Reference是两种不同的缓存实现。

    l    Store-By-Value:指在key/value存入缓存时,将其值拷贝一份存入缓存。避免在其他程序修改key或value的值时,污染缓存内存储的内容。

    l    Store-By-Reference:指在key/value存入缓存时,直接将其引用存入缓存。

    java常见的堆内缓存,一般使用Store-By-Reference方式,提升缓存性能。常见的堆外缓存和进程外缓存,一般由于使用引用在技术上比较复杂,通常使用Store-By-Value方式。

    3. 缓存过期策略

    如果缓存中的数据已经过期,那它将不能从缓存返回。如果缓存没有配置过期政策,默认是永久有效的策略(Eternal)。

    过期策略可以在配置时提供一个ExpiryPolicy实现的设置,见下面的定义

    publicinterface ExpiryPolicy<K, V> {

     Duration getExpiryForCreatedEntry(Entry<?extends K, ? extends V>entry);

       DurationgetExpiryForAccessedEntry(Entry<? extends K, ? extends V>entry);

       Duration getExpiryForModifiedEntry(Entry<?extends K, ? extends V>entry);

    }

    其中:

    l    getExpiryForCreatedEntry():当数据创建后的到期持续时间

    l    getExpiryForAccessedEntry(): 当数据访问后的到期持续时间

    l    getExpiryForModifiedEntry():当数据修改后的到期持续时间

    当这些方法被调用时,ExpiryPolicy将返回下列值之一:

    l    持续时间等于缓存配置的过期时间

    l    Duration.ZERO表明数据目前已经是过期的

    在特定的缓存操作执行后的一段时间后数据需要进行回收,该时间由Duration类定义。Duration是由一个由java.util.concurrent.TimeUnit和时长durationAmount组成,TimeUnit的最小值为TimeUnit.MILLISECONDS。

    3.2 缓存框架的实现

    基于3.1节缓存定义的规范,我们可以自己动手写一个简单的缓存框架,我们先对缓存框架做一个初步的规划,实现一个具有如表3-1所描述的特性的简单缓存。

                         表3-1缓存框架特性

    特性点

    特性描述

    类型

    进程内缓存

    实现语言

    JAVA

    内存使用

    JAVA堆内存

    内存管理

    使用LRU淘汰算法

    支持Weak key

    缓存标准

    JCache(JSR 107)

     

    下面,我们将遵循我们的规划,由简入繁逐步迭代我们的缓存组件,我们给组件取名叫做CsCache(Cache Study)。

    3.2.1 前期准备

    参考开源缓存组件EhCache和Guava,提取它们的公共方法,可以得到最核心的,也是我们最关心的一些方法,见表3-2。

                       表3-2简单缓存的常用方法

    接口

    说明

    EhCache

    Guava

    CsCache

    clear

    清空缓存

    get

    根据Key获取

    getAll(keys)

    根据Key列表获取,如果未命中可能触发加载动作

     

    getAllPresent(keys)

    根据Key列表获取,如果未命中不会触发加载动作

     

     

    keySet()

    获取所有key列表

     

     

     

    put(K,V)

    写入一个k/v

    putAll(entries)

    将entries写入缓存

     

    putIfAbsent

    如果缓存中没有则写入

     

    remove

    删除一个key

    remove(K,V)

    匹配k/v删除

     

    removeAll(keys)

    根据key列表删除

     

    replace(K,V)

    替换一个key

     

    replace (K,V,V)

    匹配k/v替换

     

        我们的缓存框架选取了最基本的get(获取缓存)、put(放入缓存)、remove(根据key值删除缓存)、clear(清空缓辈子)方法,这些方法是实际工作中当中最常用的功能。

     

    3.2.2 缓存的架构介绍

    通过上一小节的前期准备,我们确定了缓存框架的几个基本的使用方法,那么从这一小节,我们就由浅入深的介绍CsCache缓存框架。

    通过JSR107规范,我们将框架定义为客户端层、缓存提供层、缓存管理层、缓存存储层。其中缓存存储层又分为基本存储层、LRU存储层和Weak存储层,如图3-1所示。

                        图3-1 缓存分层图

    其中:

    l    客户端层:使用者直接通过该层与数据进行交互。

    l    缓存提供层:主要对缓存管理层的生命周期进行维护,负责缓存管理层的创建,保存、获取以及销毁。

    l    缓存管理层:主要对缓存客户端的生命周期进行维护,负责缓存客户端的创建,保存、获取以及销毁

    l    缓存存储层:负责数据以什么样的形式进行存储。

    l   基本存储层:是以普通的ConcurrentHashMap为存储核心,数据不淘汰。

    l   LRU存储层:是以最近最少用为原则进行的数据存储和缓存淘汰机制。

    l   Weak存储层:是以弱引用为原则的数据存储和缓存淘汰机制。

    3.2.3 设计思路以及知识点详解

    本节开始深入介绍缓存框架的类图以及相关知识点。图3-2所示列出了缓存框架的工程结构。

                  图3-2 框架工程结构图

    整个工程结构的包结构分为JSR107包和store包,JSR107是与规范相关的一些类的封装,store包是与数据存储相关类的封装。

    1. 设计类图

    通过分析3.2.2节的缓存架构介绍和图3-2工程结构图,我们能够对框架的整体情况有一个概览,本小节将以类图的方式展现框架的设计理念,如图3-3所示。

                               图3-3 类图

    根据规范,CacheProvider、CacheManager、Cache是抽象出来的最基础的缓存接口。其中Cache是提供最终缓存实现的基础接口,其实现类是CsCache107,初始化时即持有一个BasicDataStore对象。完整的类列表见表3-3所示。

    表3-3 框架核心类列表

    类名

    类型

    说明

    CsCache

    直接使用CsCache的时候,接口类

    DataStore

    接口

    存储数据的规范定义

    BasicDataStore

    使用ConcurrentHashMap实现的简单数据存储

    WeakValueDataStore

    使用ConcurrentHashMap实现的弱引用数据存储

    LRUDataStore

    使用ConcurrentHashMap实现LRU算法的数据存储

    ValueHolder

    接口

    具体存储值的规范定义

    BasicValueHolder

    简单的强引用值存储类

    WeakValueHolder

    简单的弱引用值存储类

    LRUEntry

    实现了简单LRU的数据存储类

    *CsCache107

    用于适配JSR107标准

    *CsCache107Manager

    用于实现JSR107标准中的CacheManager,管理多个cache实例

    *CsCaching107Provider

    实现了JSR107标准中的CacheProvider,用于提供SPI服务

    2. 缓存框架的SPI机制

    在工程结构中的META-INF/services/下面有一个javax.cache.spi.CachingProvider配置文件,里面有一个org.cachestudy.writeitbyself.jsr107.CsCaching107Provider实现类,这个配置文件实际上是利用的JAVA SPI机制进行组件的发现与加载。

    (1)什么是SPI

    SPI的全名为Service Provider Interface,JDK内置的一种服提供发现机制,在Java.util.ServiceLoader的文档里有比较详细的介绍。

    JAVA SPI机制的思想简单来说是:在面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。JAVA SPI就是提供了这样的一个机制,为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

    (2)SPI的使用约定

    当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。而在jdk里面提供服查找工具类:java.util.ServiceLoader,如图3-4所示。

     

                                                                              图3-4 SPI约定结构图

    3. 解读缓存数据层

    缓存数据层实际承担的责任主要是缓存数据的存储和缓存的淘汰机制,在图3-2中可以看到数据的存储和淘汰是基于DataStore这个接口来实现的,而这一实现也正是图3-1提到的数据存储层。目前框架一共实现了三个实现类分别是:LRUDataStore、WeakDataStore和BaseDataStore。

    我们先来分析一下LRUDataStore的设计原理:

    (1)基于引用的淘汰算法

    基于引用的淘汰算法,是一种简单有效的算法,由JVM的GC进行回收。Java的引用主要分为强引用、软引用、弱引用、虚引用。

    l    强引用(StrongReference):强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

    l    软引用(SoftReference):如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

     

    l    弱引用(WeakReference):弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    l    虚引用(PhantomReference):“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

    我们的引用淘汰算法是基于弱引用来实现的,在图3-5中展示了store包的类列表

            图3-5弱引用淘汰算法

    其中WeakValueDataStore和WeakValueHoler是弱引用实现所需要的实现类。WeakValueDataStore实现了DataStore接口,提供基于弱引用的数据存储,WeakValueHolder实现ValueHolder接口,提供基于弱引用的实际值存储逻辑。

     

    WeakValueDataStore类的代码及实现原理如下:

    //定义了使用简单弱引用的数据存储器,代码经过剪裁,完整代码请参考github

    public classWeakValueDataStore<K, V> implements DataStore<K, V> {

    ConcurrentHashMap<K,ValueHolder<V>> map = new ConcurrentHashMap<K,ValueHolder<V>>();

     

              @Override

          public ValueHolder<V> get(K key)throws StoreAccessException {

                   return map.get(key);

          }

          @Override

          public PutStatus put(K key, V value)throws StoreAccessException {

                   ValueHolder<V> v = newWeakValueHolder<V>(value);

                   map.put(key, v);

                   return PutStatus.PUT;

          }

     

          @Override

          public ValueHolder<V> remove(K key)throws StoreAccessException {

                   return map.remove(key);

          }

     

              @Override

          public void clear() throwsStoreAccessException {

                   map.clear();

          }

    }

    WeakValueHolder的代码及实现原理如下:

    //简单的弱引用实现

    public classWeakValueHolder<V> implements ValueHolder<V> {

          public WeakValueHolder(V value) {

    /* 使用JDK提供的WeakReference,建立对象的弱引用

    * 在没有强引用时,JVM GC将回收对象,调用WeakReference.get时

    * 返回null

       */

                   this.v = new WeakReference<V>(value);

          }

          privateWeakReference<V> v;

          @Override

          public V value() {

                   return this.v.get();

          }

    }

    测试用例验证方法如下:

    @Test

    public voidTestWeakValue() throws InterruptedException {

          CsCache<String, User> cache = newCsCache<String, User>(new WeakValueDataStore<String, User>());

          String key = "leo";

          User user = new User();

          user.setName("leo");

          cache.put(key, user);

    /* 释放对象的强引用,等待JVM GC */

          user = null;

          System.out.println("Hello " + cache.get(key).getName());

          System.gc();

          Thread.sleep(10000);

    /* JVM调度显式GC后,回收了name是leo的user

    * get返回null

    */

          System.out.println("Hello " + cache.get(key));

    }

     

     

    (2)基于LRU的淘汰算法

    LRULeast recently used,最近最少使用)算法根据数据的访问记录行淘汰数据,其核心思想是如果数据最近被访问过,那么将来被访问的几率也更高

    CsCache的LRU简单实现逻辑如下:我们通过维护entry的列表,在get、put时维护entry列表实现,使最少访问的键值对维持在entry列表的最尾部。在数据量超过缓存容量需要做LRU淘汰时,我们通过删除链表尾部的数据,来实现简单的LRU数据淘汰机制,如图3-6所示。

    图3-6 LRU淘汰算法

    其中LRUDataStore和LRUEntry是弱引用实现所需要的实现类。LRUDataStore实现了DataStore接口。LRUEntry对象则是LRU的数据存储类

    LRUDataStore类的关键代码及实现原理如下:

              @Override

          public ValueHolder<V> get(K key)throws StoreAccessException {

          LRUEntry<K, ValueHolder<?>>entry = (LRUEntry<K, ValueHolder<?>>) getEntry(key);

                   if (entry == null) {

                            return null;

                   }

                        /**

                   在获取数据的时候,将该entity节点数据移动到列表头。

                   moveToFirst(entry);

                   return (ValueHolder<V>)entry.getValue();

          }

          @Override

          public PutStatus put(K key, V value)throws StoreAccessException {

                   LRUEntry<K,ValueHolder<?>> entry = (LRUEntry<K, ValueHolder<?>>)getEntry(key);

                   PutStatus status =PutStatus.NOOP;

                   if (entry == null) {

                                  /**

                            数据缓存列表中的数据已经超过预定值,则删除列表中

                                  尾的节点数据,以实现LRU算法

                            **/

                            if (map.size() >=maxSize) {

                                     map.remove(last.getKey());

                                            removeLast();

                            }

                            entry = newLRUEntry<K, ValueHolder<?>>(key, newBasicValueHolder<V>(value));

                            status = PutStatus.PUT;

                   } else {

                            entry.setValue(newBasicValueHolder<V>(value));

                            status =PutStatus.UPDATE;

                   }

                   /**

                   新添加的数据要加到列表的头部

                   **/

                   moveToFirst(entry);

                   map.put(key, entry);

                   return status;

    }

    这段关键代码的核心意思是,在LRUDataStore类中维护了一个LRUEntity的数据链表,当执行put操作的时,则将数据封装成LRUEntity数据节点,加入到链表的头部以表示数据是最新的,如果数据超出链表的设定的大小范围,则从链表的尾部删除最不活跃的数据节点。当执行get操作的时,首先将LRUEntity数据节点移到到链表的头部,以表示该数据被最新请求访问,然后将数据返回。

    4. 解读缓存管理层(CacheManager)

    在上面图3-1中我们介绍了框架的分层结构,其中接口类CacheManager所对应的正是缓存管理层,在CsCache框架中CacheManager的实现类是CsCache107Manager,它主要负责管理多个Cache客户端实例,以及负责缓存客户端实例的创建、销毁、获取等。

    下面具体介绍CsCache107Manager类的关键代码及实现原理。

    (1)缓存实例的创建

    缓存实例创建的实现代码如下:

    //缓存客户端实例的创建

    //缓存池是用ConcurrentMap来实现的,用以缓存已经创建好的缓存实例

    synchronizedpublic <K, V, C extends Configuration<K, V>> Cache<K, V>createCache(String cacheName, C configuration)

                            throwsIllegalArgumentException {

                   if (isClosed()) {

                            throw newIllegalStateException();

                   }

                     //检查缓存实例名称是否为空

                   checkNotNull(cacheName,"cacheName");

                     //检查配置信息是否为空

                   checkNotNull(configuration,"configuration");

                     //根据cacheName获取缓存客户端实例

                   CsCache107<?, ?> cache =caches.get(cacheName);

     

                   if (cache == null) {

                            //如果无法从事先创建好的缓存池中获取,则创建一个新的实例

                            cache = newCsCache107<K, V>(this, cacheName, configuration);

                            //将新创建的缓存实例加到缓存池中

                                  caches.put(cache.getName(),cache);

                            return (Cache<K,V>) cache;

                   } else {

                            throw newCacheException("A cache named " + cacheName + " alreadyexists.");

                   }

    }

    上面的代码只是针对CsCache107Manager类的createCache方法的代码进行了解读,完整的缓存实例的创建流程,如图3-7所示。

    图3-7 缓存实例创建

    (2)缓存实例的获取

    缓存实例获取的实现代码如下:

    public<K, V> Cache<K, V> getCache(String cacheName, Class<K>keyClazz, Class<V> valueClazz) {

                   if (isClosed()) {

                            throw newIllegalStateException();

                   }

                   //判断key类型是否为空

                   checkNotNull(keyClazz, "keyType");

                   //判断值类型是否为空

                   checkNotNull(valueClazz,"valueType");

                   //从缓存池中获取缓存实例

                  CsCache107<K,V> cache = (CsCache107<K, V>) caches.get(cacheName);

                   //如果获取为空则返回null

                   if (cache == null) {

                            return null;

                   } else {

                            //判断传入的对象和值类型是否与设定的类型一致

                            Configuration<?,?> configuration = cache.getConfiguration(Configuration.class);

     

                            if(configuration.getKeyType() != null &&configuration.getKeyType().equals(keyClazz)) {

                                     //如果一致则返回实例

                                     return cache;

                            } else {

                                     //如果不一致则抛出类型不一致异常

                                     throw new ClassCastException("Incompatiblecache key types specified, expected "

                                                        +configuration.getKeyType() + " but " + valueClazz + " wasspecified");

                            }

                   }

    }

    完整的缓存实例获取流程图,如图3-8所示。

    图3-8 缓存实例的获取

    缓存实例的创建和获取实际上主要是基于一个缓存池来实现的,在代码中使用的是一个ConcurrentHashMap类,可以根据多个不同的缓存名称创建多个缓存实例,从而可以并发的读取。

    5. 解读数据客户端层

    缓存客户端层主要是针对实际使用者的,在工程结构中主要涉及二个类,分别是:CsCache和CsCache107,而CsCache107使用的代理模式对CsCache进行的包装,如图3-8所示。用户在使用的时候,通过缓存管理层的CacheManager对象就可以获得CsCache107客户端对象,从而可以实现对缓存的直接操作。

     

    图3-8 数据客户端层

    CsCache关键代码和实现原理如下:

    privatefinal DataStore<K, V> store;

          private static Logger logger =LoggerFactory.getLogger(CsCache.class);

          //构造方法,参数是传入数据存储和淘汰策略对象

          public CsCache(final DataStore<K, V>dataStore) {

                   store = dataStore;

          }

          //根据key值获取缓存数据

          public V get(final K key) {

                   try {

                            //从数据存储和淘汰策略对象中获取缓存数据

                            ValueHolder<V>value = store.get(key);

                            if (null == value) {

                                     return null;

                            }

                            //返回缓存数据

                            return value.value();

                   } catch (StoreAccessException e){

                            logger.error("storeaccess error : ", e.getMessage());

                            logger.error(e.getStackTrace().toString());

                            return null;

                   }

          }

          //缓存数据的存储

          public void put(final K key, final Vvalue) {

                   try {

                            将数据直接存放到数据和淘汰策略对象中

                            store.put(key, value);

                   } catch (StoreAccessException e){

                            logger.error("storeaccess error : ", e.getMessage());

                            logger.error(e.getStackTrace().toString());

                   }

    }

    整个过程其实较为简单对象的构造方法中有一个DataStore对象,这个对象正是缓数据存储与淘汰策略对象,这个机制已经在解读缓存数据层小节中进行了详解,get方法则是从DataStore中获取缓存数据,put方法则是往DataStore对象中存入数据。

    CscCache107对象实际上是对CsCache对象根据JSR107规范,使用了代理模式进行包装,下面将展示几个示例方法,原理与上面CsCache是一样的,本节就不在说明。CsCache107关键代码和实现原理如下:

          //获取缓存数据

          @Override

          public V get(K key) {

                   return csCache.get(key);

          }

          //存放缓存数据

          @Override

          public void put(K key, V value) {

                   this.csCache.put(key, value);

          }

          //删除缓存数据

          @Override

          public boolean remove(K key) {

                   csCache.remove(key);

                   return true;

          }

    通过上面代码可以看到put、get、remove方法都是调用的CsCache对象的相关方法进行的操作,其目的主要是在有特殊需求的时候可以对这几个方法进行功能的扩展和增强。

    3.2.3.5 缓存框架的使用示例

    缓存框架的实现以及原理到这里就基本介绍完了,下面我们将以一个使用示例结束本章的讲解。

         //获取缓存提供层对象

    CachingProvidercachingProvider = Caching.getCachingProvider();

    //获取缓存管理层对象

    CacheManagermanager = cachingProvider.getCacheManager();

    //创建缓存实例对象

         Cache<String, User> cache =(Cache<String, User>) manager.<String, User,     Configuration<String, User>>createCache("Test", new  MutableConfiguration<String,User>());

         Stringkey = "leo";

         User user = new User();

    user.setName("leo");

    //将User数据对象存放到缓存中

      cache.put(key, user);

      System.out.println("Hello " +cache.get(key).getName());

    为方便读者能够完整的学习CsCache框架,本章实例的完整代码放入在 https://github.com/mfcliu/demo_cache,读者可以自行下载学习。

    欲了解更多有关分布式缓存方面的内容,请阅读《深入分布式缓存:从原理到实践》一书。

    京东购书,扫描二维码:

     
    展开全文
  • 先写数据库还是先写缓存

    千次阅读 2020-02-05 08:49:23
    关于维护一份数据是先写数据库,还是先写缓存的问题,很多朋友发表了自己的看法,本文来谈谈我的看法。我的结论非常清晰明确:先写数据库再写缓存。核心思想是数据库和缓存之间追求最终一致性,不追求强一致性。 (1)...

    关于维护一份数据是先写数据库,还是先写缓存的问题,很多朋友发表了自己的看法,本文来谈谈我的看法。我的结论非常清晰明确:先写数据库再写缓存。核心思想是数据库和缓存之间追求最终一致性,不追求强一致性。

    (1) 在缓存作为提升系统性能手段的背景下,不需要保证数据库和缓存的强一致性。如果非要保证二者的强一致性,会增大系统的复杂度,完全没有必要

    (2) 如果更新数据库成功,再更新缓存。此时存在两种情况:更新缓存成功则万事大吉。更新缓存失败,没有关系,等待缓存失效,此处要合理设置失效时间

    (3) 如果更新数据库失败,则操作失败,重试或者等待用户重新发起

    (4) 数据库是持久化数据,是操作成功还是失败的判断依据。缓存是提升性能的手段,允许短时间和数据库的不一致

    (5) 在互联网架构中,很少追求强一致性,一般都是追求最终一致性

    如果非要保证缓存和数据库的一致性,本质上是在解决分布式一致性问题。

    分布式一致性问题解决方案有很多,可以选择比如两阶段提交,TCC,本地消息表,MQ事务性消息等方案。

    展开全文
  • innodb的写缓存

    万次阅读 2020-08-16 18:42:46
    innodb的写缓存,其设计思想同样是为了减少磁盘的io来提升性能。 对于数据库的写来说,有如下两种情况 1. 修改的内容所在页在缓冲池内 会有如下两步操作 直接修改缓冲池中的页,一次内存操作 写入redo log,一次...

    innodb的写缓存,其设计思想同样是为了减少磁盘的io来提升性能。

    对于数据库的写来说,有如下两种情况

    1. 修改的内容所在页在缓冲池内

    会有如下两步操作

    1. 直接修改缓冲池中的页,一次内存操作

    2. 写入redo log,一次磁盘顺序写操作

    那什么时候数据会把修改的数据落盘呢,他会进行定期刷盘,如果没有等到刷盘时间,数据就被从缓存淘汰,就会把脏页刷回磁盘。这样能减少io次数,提升性能。把随机写变成顺序写和批量写,这是优化程序性能的有效方式。

    这样数据在读取怎么保持一致性,数据库异常的时候是怎么处理的呢

    1. 读取:会命中缓冲池的页,而不是从磁盘获取
    2. 缓冲池数据淘汰,会将“脏页”刷回磁盘
    3. 数据库异常崩溃,能够从redo log中恢复

    2.修改的内容所在页不在缓冲池内

    这种情况略为复杂一些,常见的操作是先把数据从磁盘中读取到缓存中,一般操作系统的缓存都是这样处理的。步骤如下:

    1. 先把需要写的页,从磁盘加载到缓冲池,一次磁盘随机读操作
    2. 修改缓冲池中的页,一次内存操作
    3. 写入redo log,一次磁盘顺序写操作

    这期间可能触发LRU淘汰我们就不分析了。

    但是这个时候,使用缓存和不使用缓存,都是需要一次磁盘随机读的,这样就会导致写的效率比较低。

    innodb的解决方案

    innodb提升数据库写性能是通过写缓冲(change buffer)来实现的,他的主要作用就是当要写入的页不在缓存中的时候,并不把这个页读取到缓存中来,而只是记录这次更改,在读取这页数据的时候或者写磁盘的时候,再把这次更改和磁盘数据合并,放入缓存中。

    在MySQL5.5之前,叫插入缓冲(insert buffer),只针对insert做了优化;现在对delete和update也有效,叫做写缓冲(change buffer)。

    写缓存优点

    对于情况1中,其实没有什么影响,但是对于情况二,从原来的操作变成了如下的操作,省去了一次磁盘io

    1. 在写缓冲中记录这个操作,一次内存操作
    2. 写入redo log,一次磁盘顺序写操作

    但是这里要注意,写缓冲中记录的是操作而不是完整的数据。

    那么这种操作是否会影响读操作或者带来数据一致性问题呢

    1. 数据读取时,将数据合并到缓冲池中(所以写后一定会读取的数据,不适合用写缓存)
    2. 数据库异常奔溃,能够从redo log中恢复数据
    3. 写缓冲不只是一个内存结构,它也会被定期刷盘到写缓冲系统表空间

    2和3我们不做详解,我们说一下读取的时候发生了什么

    因为这一页不在缓存池中,如果发生了读取,则必定会触发磁盘读,这个时候会从写缓存中读出相关的操作,把操作和数据进行合并处理,放入缓存池中。

    写缓存的限制

    写缓冲优化,仅适用于非唯一普通索引,这是因为如果索引设置了唯一(unique)属性,在进行修改操作时,InnoDB必须进行唯一性检查。这样就必须读取所有的索引数据到缓冲池进行判断是否唯一,磁盘io不可避免,所以写缓存减少磁盘io的意义就不存在了。这时候就采用常规流程,直接把页放到内存中,然后再修改。

    写缓冲的刷盘

    1. 是缓冲都会有缓冲池大小,数据库缓冲池不够用时就会触发刷盘
    2. 有一个后台线程,会认为数据库空闲时;
    3. 数据库正常关闭时;

    使用写缓冲的场景和参数设置

    • 数据库大部分是非唯一索引
    • 业务是写多读少,或者不是写后立刻读取(立即读取就会触发磁盘io,相当于给你省了,你马上又浪费了。)

    mysql参数

    参数:innodb_change_buffer_max_size

    说明:配置写缓冲的大小,占整个缓冲池的比例,默认值是25%,最大值是50%。

    参数:innodb_change_buffering

    说明:配置哪些写操作启用写缓冲,可以设置成all/none/inserts/deletes等。

    可使用mysql查询参数的命令进行查询

    show variables like '%【上面的参数】%';
    
    展开全文
  • 自己动手写缓存系统 - tmcache 作者:heiyeluren时间:2008-10-24博客:http://blog.csdn.net/heiyeshuwu 【 原理介绍 】tmcache 大致就是一个类似于Memcache的缓存服务器,用过的应该都大致了解它的执行过程,...

     

    自己动手写缓存系统 - tmcache

     

    作者:heiyeluren
    时间:2008-10-24
    博客:
    http://blog.csdn.net/heiyeshuwu

     

     

     

    【 原理介绍 】

    tmcache 大致就是一个类似于Memcache的缓存服务器,用过的应该都大致了解它的执行过程,为了便于理解,我简单描述一下。

    发送请求过程:
    客户端(PHP/Java/C++) --> 缓存服务器 --> 内存(共享内存)

    接收数据过程:
    内存(共享内存) --> 缓存服务器 --> 客户端

    大致描述就是:客户端(任何能够访问Socket的客户端语言或工具) 访问缓存服务器的指定端口,进行 存储/读取/删除 数据的操作,缓存服务器接收到指令后进行内存操作,操作结束后回写结果给客户端。所以缓存服务器端包含这些模块:Socket通信、协议解析、数据存储、数据有效期控制

    以下代码就是按照这些模块来进行描述的,下面的代码取自于 tmcache - TieMa(Tiny&Mini) Memory Cache,tmcache 目前支持的功能包括:

      *  Based memory data storage
      *  Compatible memcached communication protocol
      *  Few operation interface, The use of simple
      *  Support custom port,max_clients,memory use control


    tmcache下载(Windows版可直接运行):

    Windows版本:http://heiyeluren.googlecode.com/files/tmcache-1.0.0_alpha-win32.zip
    Unix/Linux版: http://heiyeluren.googlecode.com/files/tmcache-1.0.0_alpha.tar.gz

     

     

    【 系统实现 】

     

    一、通信协议处理模块

     

    这个主要是包含一方面是监听处理Socket,tmcache里主要是依靠 init_server_listen() 函数进行监听操作,同时并发接受连接是程序里很重要的一块,可以选择方式有 select/poll 多路IO的方式,epoll/kqueue 的事件方式,另外还可以使用线程(thread)的方式,tmcache为了兼容性和简单起见,使用了线程的方式。


    线程相关核心处理代码:

    1. void tm_thread( int serversock, unsigned int max_client ){
    2.     int clientsock, *arg;
    3.     struct sockaddr_in client_addr;
    4.     char currtime[32];
    5.     unsigned clientlen;
    6.     pthread_attr_t thread_attr;
    7.     void *thread_result;
    8.     
    9.     /* Setting pthread attribute */
    10.     pthread_attr_init(&thread_attr);
    11.     pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
    12.     /* Run until cancelled */
    13.     while (1){
    14.         pthread_t thread;
    15.         unsigned int clientlen = sizeof(client_addr);
    16.         memset(currtime, 0, sizeof(currtime));
    17.         getdate(currtime);
    18.         /* Wait for client connection */
    19.         if ((clientsock = accept(serversock, (struct sockaddr *) &client_addr, &clientlen)) < 0){
    20.             die("Failed to accept client connection");
    21.         }
    22.         /* Use thread process new connection */
    23.         arg = &clientsock;
    24.         if (pthread_create(thread, &thread_attr, tm_thread_callback, (void *)arg) != 0){
    25.             die("Create new thread failed");
    26.         }
    27.     }
    28.     /* Destory pthread attribute */
    29.     (void)pthread_attr_destroy(&thread_attr);
    30. }


    协议处理是很核心的,主要是包括存储数据的 set/add/replace/append,还有提取数据的 get/gets,删除数据的 delete/remove,获取状态 stats/stat 等指令的各种操作,主要操作处理函数是 proc_request(),它负责协议的分析很调用相关的接口来进行处理。

     


     

    二、数据处理模块

     

    这是数据存储处理的核心,主要是通过使用哈希表来存储数据,使用队列来记录数据的存储顺序并且为内存不够用时的处理数据结构,还有使用概率处理算法来不定期清除过期数据等等。

     

    1. 哈希表数据存储

    数据是采用哈希表的存储方式,存储速度简单快速,算法效率是 O(1),非常适合这种 Key => Value 的存储场合,核心的哈希算法是经典的Times33算法:


     

    1. unsigned tm_hash( const char *str, unsigned table_size ){
    2.  unsigned long hash = 5381; 
    3.  int c;
    4.  while (c = *str++) hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
    5.  hash = table_size > 0 ? hash % table_size : hash;
    6.  return hash;
    7. }

    同时如果存在一个数据节点冲突的情况,则采用开拉链法来解决,一个哈希存储节点的数据结构,next程序用于存储下一个相同哈希映射结果的值:

     

    1. /* Hash data item struct */
    2. struct tm_hash_entry_t {
    3.  char *key;   /* data key string */
    4.  char *data;   /* data value string */
    5.  size_t length;   /* data length */
    6.  unsigned created;  /* data create time (Unix Timestamp) */
    7.  unsigned expired;  /* data expire time (Unix Timestamp) */
    8.  struct tm_hash_entry_t *next; /* key conflict link next data node pointer */
    9. };


    2. 数据失效处理

     

    目前主要是两种方法处理时效,一种是当访问某个数据节点的时候,如果发现该数据的 expired 字段已经超过当前时间,那么将remove该节点。另外一种方法是在进行数据操作的时候,按照概率计算算法,不定期的清除掉已经过期的算法,看看概率算法实现:

    1. status get_gc_probability(unsigned probaility, unsigned divisor){
    2.     int n;
    3.     struct timeval tv; 
    4.     gettimeofday(&tv , (struct timezone *)NULL);
    5.     srand((int)(tv.tv_usec + tv.tv_sec));
    6.     n = 1 + (int)( (float)divisor * rand() / (RAND_MAX+1.0) );
    7.     return (n <= probaility ? TRUE : FALSE); 
    8. }

    概率的几率百分比是通过 probaility 和 divisor 来确定的,缺省是 1/100 的几率,就是一百次操作里,有一次是可能执行清除过期数据操作的,这样做便于减轻程序操作的压力。


    3. 内存使用完了的操作

    如果tmcache启动的时候,设定了16MB的内存使用空间,但是最后内存不够用了,那么就只有通过清除前面插入的缓存数据来空出空间来进行存储新数据,这里主要是使用了队列,因为队列是使用先进先出(First in first out) 的原则的,代码:

     

    1. /* current memory use size exceed MAX_MEM_SIZE, remove last node from queue, remove key from hash table */
    2. if ( (get_mem_used() + length) > g_max_mem_size ){
    3.  struct tm_queue_node_t *qnode;
    4.  while ( (get_mem_used() + length) > g_max_mem_size ){
    5.   qnode = tm_qremove( g_qlist );
    6.   remove_data( qnode->key );
    7.  }
    8. }

    这样做的缺点很明显,就是明明数据没有失效期,确被删除了,所以,缓存工具并不能作为持久化数据一样的对待方式,必须确保每次查询缓存的时候都进行了相应的存储操作,因为无法保证数据是还在内存中的。

     

     

    【 结束语 】

     

    基本可以确定 tmcache 是一个非常简单的缓存系统,比Memcache差距很远,更多来说 tmcache 只是一个学习的作品,同时也是做了一些简单的引导思路,希望对真正要做一个成型复杂稳定的缓存系统做一个抛砖引玉的简单参考,所以,tmcache 并不是一个稳定可靠的缓存系统,也不适合用于生产环境,更适合作为一个学习参考的小东西。 :-)


    关于其他上面没有描述的内容,建议阅读tmcache的代码来获得更多相关知识。

     

    下载地址:http://code.google.com/p/heiyeluren/downloads

     

     

     

     

    展开全文
  • 写缓存属性查询

    千次阅读 2010-06-10 23:35:00
    如果写缓存没有电压备份,电源关闭可能导致缓冲数据丢失。 一个弥补数据丢失问题的方法是刷新写缓存(在SCSI设备上使用SCSI SYNCHRONIZE CACHE命令)。然而,刷新写缓存是昂贵的操作,如果频繁操作,它可能会...
  • linux系统读写缓存

    千次阅读 2014-10-14 18:50:07
    1. 操作系统缓存 在linux世界里,一切可读写设备都可看作是文件。文件cache设计的好坏直接影响着文件系统和磁盘的性能。最直观的是使用free命令看到的cached列。 这里面的cached列就是操作系统缓存,操作...
  • 写缓存预读及磁盘调整

    千次阅读 2016-12-30 21:34:17
    因为cpu,内存操作速度要比磁盘的速度快,所以系统在设计的时候,用了回写缓存。 回写缓存怎么理解呢?就是应用提交了写的请求,数据被放在了缓存中,应用就认为是持久化完毕了,去干别的事情了,而实际上系统可能...
  • 打开或关闭硬盘写缓存(Write Cache)

    千次阅读 2020-03-15 12:22:23
    查看当前硬盘Cache状态 # hdparm -W /dev/sda 关闭硬盘的Cache # hdparm -W 0 /dev/sda 打开硬盘的Cache # hdparm -W 1 /dev/sda 建议下载最新hdparm版本,网址为: http://sourceforge.net/projects/hd...
  • 缓存穿透、缓存击穿、缓存雪崩区别和解决方案

    万次阅读 多人点赞 2018-09-19 14:35:57
    一、缓存处理流程  前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。     二、缓存穿透  描述:  缓存...
  • Cache Aside(旁路缓存)策略以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和策略。 读策略 从缓存中读取数据;如果缓存命中,则直接返回数据;如果缓存不命中,则从数据库中查询数据;查询...
  • 》,今天给大家整理一篇关于Redis经常被问到的问题:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等概念的入门及简单解决方案。 一、缓存雪崩 缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未...
  • 缓存由于其高并发和高性能的特性,在项目中被广泛使用。读缓存流程如下图: 双一致性有以下三个要求: 缓存不能读到脏数据 缓存可能会读到过期数据,但要在可容忍时间内实现最终一致 这个可容忍时间尽可能的小 ...
  • 我的程序使用的是memcache,需要频繁的读写缓存,但是发现memache删除缓存的时候,有时候命中率 太低,使用redis会不会好一点?
  • Linux文件读写与缓存

    千次阅读 2017-05-06 00:18:00
    题外话:每日七点,QQ群大家分享技术相关文章,睡什么睡起来嗨!...缓存是用来减少高速设备访问低速设备所需平均时间的组件,文件读写涉及到计算机内存和磁盘,内存操作速度远远大于磁盘,如果每次调用read,wri
  • 一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中 数据变更的程序完成了数据库的修改 。 完了,数据库和缓存中的数据不一样了。。。。   只有在对一个数据在并发的进行...
  • 缓存算法是指令的一个明细表,用于提示计算设备的缓存信息中哪些条目应该被删去。常见类型包括LFU、LRU、ARC、FIFO、MRU。 最近最少使用算法(LRU):这个缓存算法将最近使用的条目存放到靠近缓存顶部的位置。当一个...
  • 缓存穿透,缓存击穿,缓存雪崩解决方案分析

    万次阅读 多人点赞 2017-01-06 11:12:50
    缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,...
  • Redis缓存与数据库双一致性

    千次阅读 2019-03-02 21:18:13
    保证缓存和数据库的一致性主要有两个方面的考虑: 1、读数据时,先读取缓存,如果换成不存在,则读取数据库,再将结果保存在缓存中 2、更新数据时,主要有以下几种策略: (1)先更新数据库,再更新缓存 (2)先删除...
  • 缓存的三种读写方式

    千次阅读 2020-02-10 17:07:28
    结合业务场景,缓存的读写方式可以分为以下三种模式: Cache Aside(旁路缓存) Read/Write Through(读写穿透) Write Behind Caching(异步缓存写入) Cache Aside 查询:应用程序先去cache中读取...
  • Caffeine 缓存

    万次阅读 2017-12-13 17:07:05
    缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。 回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 — 缓存库的一个重要特征。 Caffeine 因使用 Window TinyLfu 回收策略,提供了一个...
  • 一、为什么这篇文章? 首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作: 但是在更新缓存方面,对于更新完数据库,是更新缓存...
  • 文件写缓存机制是指,当从内存向磁盘文件写入数据时,实际是先将数据写入到缓存区(直到写满缓存区),再从缓存区写入磁盘文件。 文件读缓存机制类似,当需要从磁盘文件读入数据到内存时,是先将数据读入到缓存区...
  • 我需要从头先读这个文件f1,同时往这个f1 又需要向末尾添加,同时还要保证,内容不能重复,当需要执行很多次这种操作时,这个时候如果不考虑,文件底层的缓存问题,极有可能出现重复内容,即使你已经做了重复性...
  • 磁盘缓存、Hibernate缓存、Mybatis缓存

    千次阅读 2017-03-12 21:47:10
    一、磁盘缓存:(disk cache)磁盘缓存分为读缓存和写缓存。 (1)读缓存指的是把从磁盘中读取的数据存储到内存空间中的方式。这样一来,当接下来需要读取同一数据时,就不用查询实际的磁盘,而是从磁盘缓存(内存)...
  • 1.缓存过期 缓存过期:在使用缓存时,可以通过TTL(Time To Live)设置失效时间,当TTL为0时,缓存失效。 为什么要设置缓存的过期时间呢? 一、为了节省内存 例如,在缓存中存放了近3年的10亿条博文数据,但是经常被...
  • Linux页高速缓存和页回

    千次阅读 2018-03-19 10:11:26
    页高速缓存(cache)是Linux内核实现磁盘缓存。它主要用来减少对磁盘I/O操作。是通过把磁盘中的数据缓存到 物理内存中,把对磁盘的访问变为对物理内存的访问。 磁盘高速缓存有两个重要因素:第一,访问磁盘的速度要...
  • 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。...
  • 缓存穿透、缓存击穿、缓存雪崩解决方案

    千次阅读 多人点赞 2021-03-21 23:23:34
    缓存穿透、缓存击穿、缓存雪崩解决方案

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,106,880
精华内容 442,752
关键字:

自己写缓存