缓存

Posted by Gong on March 10, 2019

缓存的特征

命中率

命中率=返回正确结果数/请求缓存次数,命中率问题是缓存中的一个非常重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。

最大元素(或最大空间)

缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的时候缓存。

清空策略

如上描述,缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳定服务的同时有效提升命中率?这就由缓存清空策略来处理,设计适合自身数据特征的清空策略能有效提升命中率。常见的一般策略有:

  • FIFO(first in first out)

先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。

  • LFU(less frequently used)

最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。

  • LRU(least recently used)

最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。

缓存的类型

1.缓存存储介质

虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。

  • 内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原。
  • 硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
  • 数据库:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力。现在使用数据库做缓存介质是不是又回到了老问题上了?其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。

2.缓存的分类

根据缓存与应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存):

  • 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
  • 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存

本地缓存

1.编程直接实现缓存

个别场景下,我们只需要简单的缓存数据的功能,而无需关注更多存取、清空策略等深入的特性时,直接编程实现缓存则是最便捷和高效的。

a. 成员变量或局部变量实现

  public void UseLocalCache(){
     //一个本地的缓存变量
     Map<String, Object> localCacheStoreMap = new HashMap<String, Object>();

    List<Object> infosList = this.getInfoList();
    for(Object item:infosList){
        if(localCacheStoreMap.containsKey(item)){ //缓存命中 使用缓存数据
            // todo
        } else { // 缓存未命中  IO获取数据,结果存入缓存
            Object valueObject = this.getInfoFromDB();
            localCacheStoreMap.put(valueObject.toString(), valueObject);

        }
    }
}
//示例
private List<Object> getInfoList(){
    return new ArrayList<Object>();
}
//示例数据库IO获取
private Object getInfoFromDB(){
    return new Object();
}

以局部变量map结构缓存部分业务数据,减少频繁的重复数据库I/O操作。缺点仅限于类的自身作用域内,类间无法共享缓存。

b. 静态变量实现

最常用的单例实现静态资源缓存,代码示例如下:

      public class CityUtils {
      private static final HttpClient httpClient = ServerHolder.createClientWithPool(); 
      private static Map<Integer, String> cityIdNameMap = new HashMap<Integer, String>();
      private static Map<Integer, String> districtIdNameMap = new HashMap<Integer, String>();

  static {
    HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/city/all");
    BaseAuthorizationUtils.generateAuthAndDateHeader(get,
            BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
            BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);
    try {
        String resultStr = httpClient.execute(get, new BasicResponseHandler());
        JSONObject resultJo = new JSONObject(resultStr);
        JSONArray dataJa = resultJo.getJSONArray("data");
        for (int i = 0; i < dataJa.length(); i++) {
            JSONObject itemJo = dataJa.getJSONObject(i);
            cityIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
        }
    } catch (Exception e) {
        throw new RuntimeException("Init City List Error!", e);
    }
}
    static {
    HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/district/all");
    BaseAuthorizationUtils.generateAuthAndDateHeader(get,
            BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
            BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);
    try {
        String resultStr = httpClient.execute(get, new BasicResponseHandler());
        JSONObject resultJo = new JSONObject(resultStr);
        JSONArray dataJa = resultJo.getJSONArray("data");
        for (int i = 0; i < dataJa.length(); i++) {
            JSONObject itemJo = dataJa.getJSONObject(i);
            districtIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
        }
    } catch (Exception e) {
        throw new RuntimeException("Init District List Error!", e);
    }
}

    public static String getCityName(int cityId) {
      String name = cityIdNameMap.get(cityId);
      if (name == null) {
        name = "未知";
      }
       return name;
     }

    public static String getDistrictName(int districtId) {
      String name = districtIdNameMap.get(districtId);
       if (name == null) {
         name = "未知";
        }
       return name;
     }
   }

O2O业务中常用的城市基础基本信息判断,通过静态变量一次获取缓存内存中,减少频繁的I/O读取,静态变量实现类间可共享,进程内可共享,缓存的实时性稍差。

为了解决本地缓存数据的实时性问题,目前大量使用的是结合ZooKeeper的自动发现机制,实时变更本地静态变量缓存:

美团内部的基础配置组件MtConfig,采用的就是类似原理,使用静态变量缓存,结合ZooKeeper的统一管理,做到自动动态更新缓存,如图所示。

img

直接编程的缓存实现,优点是能直接在heap区内读写,最快也最方便;缺点同样是受heap区域影响,缓存的数据量非常有限,同时缓存时间受GC影响。主要满足单机场景下的小数据量缓存需求,同时对缓存数据的变更无需太敏感感知,如上一般配置管理、基础静态数据等场景。

2.缓存框架——Ehcache

Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是一个非常轻量级的缓存实现,我们常用的Hibernate里面就集成了相关缓存功能。

图3 Ehcache框架图

从图3中我们可以了解到,Ehcache的核心定义主要包括:

  • cache manager:缓存管理器,以前是只允许单例的,不过现在也可以多实例了。
  • cache:缓存管理器内可以放置若干cache,存放数据的实质,所有cache都实现了Ehcache接口,这是一个真正使用的缓存实例;通过缓存管理器的模式,可以在单个应用中轻松隔离多个缓存实例,独立服务于不同业务场景需求,缓存数据物理隔离,同时需要时又可共享使用。
  • element:单条缓存数据的组成单位。
  • system of record(SOR):可以取到真实数据的组件,可以是真正的业务逻辑、外部接口调用、存放真实数据的数据库等,缓存就是从SOR中读取或者写入到SOR中去的。

在上层可以看到,整个Ehcache提供了对JSR、JMX等的标准支持,能够较好的兼容和移植,同时对各类对象有较完善的监控管理机制。它的缓存介质涵盖堆内存(heap)、堆外内存(BigMemory商用版本支持)和磁盘,各介质可独立设置属性和策略。Ehcache最初是独立的本地缓存框架组件,在后期的发展中,结合Terracotta服务阵列模型,可以支持分布式缓存集群,主要有RMI、JGroups、JMS和Cache Server等传播方式进行节点间通信,如图3的左侧部分描述。

整体数据流转包括这样几类行为:

  • Flush:缓存条目向低层次移动。
  • Fault:从低层拷贝一个对象到高层。在获取缓存的过程中,某一层发现自己的该缓存条目已经失效,就触发了Fault行为。
  • Eviction:把缓存条目除去。
  • Expiration:失效状态。
  • Pinning:强制缓存条目保持在某一层。

图4反映了数据在各个层之间的流转,同时也体现了各层数据的一个生命周期。

图4 缓存数据流转图(L1:本地内存层;L2:Terracotta服务节点层)

主要特性:

  • 快速,针对大型高并发系统场景,Ehcache的多线程机制有相应的优化改善。
  • 简单,很小的jar包,简单配置就可直接使用,单机场景下无需过多的其他服务依赖。
  • 支持多种的缓存策略,灵活。
  • 缓存数据有两级:内存和磁盘,与一般的本地内存缓存相比,有了磁盘的存储空间,将可以支持更大量的数据缓存需求。
  • 具有缓存和缓存管理器的侦听接口,能更简单方便的进行缓存实例的监控管理。
  • 支持多缓存管理器实例,以及一个实例的多个缓存区域

整体上看,Ehcache的使用还是相对简单便捷的,提供了完整的各类API接口。需要注意的是,虽然Ehcache支持磁盘的持久化,但是由于存在两级缓存介质,在一级内存中的缓存,如果没有主动的刷入磁盘持久化的话,在应用异常down机等情形下,依然会出现缓存数据丢失,为此可以根据需要将缓存刷到磁盘,将缓存条目刷到磁盘的操作可以通过cache.flush()方法来执行,需要注意的是,对于对象的磁盘写入,前提是要将对象进行序列化。

Ehcache原理

它的缓存介质涵盖堆内存(heap)、堆外内存;其中堆外内存的实现通过nio的DirectByteBuffer类;

(1)系统IO调用

首先来看一下一般的IO调用。在传统的文件IO操作中,我们都是调用操作系统提供的底层标准IO系统调用函数 read()、write() ,此时调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。如下图所示。

img

注意两点:

OS的read函数会在内核IO缓冲区中预读取数据,减少磁盘IO操作(Step2) Java的BufferedReader或BufferedInputStream的缓冲区的作用是减少系统调用(Step1) (2)内存映射文件

内存文件映射适用于对大文件的读写。虚拟地址空间有一块区域: “Memory mapped region for shared libraries” ,这段区域就是在内存映射文件的时候将某一段的虚拟地址和文件对象的某一部分建立起映射关系,此时并没有拷贝数据到内存中去,而是当进程代码第一次引用这段代码内的虚拟地址时,触发了缺页异常,这时候OS根据映射关系直接将文件的相关部分数据拷贝到进程的用户私有空间中去,如下图所示。

img

内存映射文件的效率比标准IO高的重要原因就是因为少了把数据拷贝到OS内核缓冲区这一步。

MappedByteBuffer:映射是通过FileChannel提供的map方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。

DirectByteBuffer:继承了MappedByteBuffer,主要是实现了byte获得函数get等

Ehcache使用

1.Ehcache基本操作

ElementCacheCacheManager是Ehcache最重要的API。

  • Element:缓存的元素,它维护着一个键值对。
  • Cache:它是Ehcache的核心类,它有多个Element,并被CacheManager管理。它实现了对缓存的逻辑行为。
  • CacheManager:Cache的容器对象,并管理着Cache的生命周期。
创建CacheManager

下面的代码列举了创建CacheManager的五种方式。 使用静态方法create()会以默认配置来创建单例的CacheManager实例。newInstance()方法是一个工厂方法,以默认配置创建一个新的CacheManager实例。 此外,newInstance()还有几个重载函数,分别可以通过传入StringURLInputStream参数来加载配置文件,然后创建CacheManager实例。

// 使用Ehcache默认配置获取单例的CacheManager实例
CacheManager.create();
String[] cacheNames = CacheManager.getInstance().getCacheNames();

// 使用Ehcache默认配置新建一个CacheManager实例
CacheManager.newInstance();
String[] cacheNames = manager.getCacheNames();

// 使用不同的配置文件分别创建一个CacheManager实例
CacheManager manager1 = CacheManager.newInstance("src/config/ehcache1.xml");
CacheManager manager2 = CacheManager.newInstance("src/config/ehcache2.xml");
String[] cacheNamesForManager1 = manager1.getCacheNames();
String[] cacheNamesForManager2 = manager2.getCacheNames();

// 基于classpath下的配置文件创建CacheManager实例
URL url = getClass().getResource("/anotherconfigurationname.xml");
CacheManager manager = CacheManager.newInstance(url);

// 基于文件流得到配置文件,并创建CacheManager实例
InputStream fis = new FileInputStream(new File
("src/config/ehcache.xml").getAbsolutePath());
try {
 CacheManager manager = CacheManager.newInstance(fis);
} finally {
 fis.close();
}

2.添加缓存

需要强调一点,Cache对象在用addCache方法添加到CacheManager之前,是无效的。 使用CacheManager的addCache方法可以根据缓存名将ehcache.xml中声明的cache添加到容器中;它也可以直接将Cache对象添加到缓存容器中。 Cache有多个构造函数,提供了不同方式去加载缓存的配置参数。 有时候,你可能需要使用API来动态的添加缓存,下面的例子就提供了这样的范例。

// 除了可以使用xml文件中配置的缓存,你也可以使用API动态增删缓存
// 添加缓存
manager.addCache(cacheName);

// 使用默认配置添加缓存
CacheManager singletonManager = CacheManager.create();
singletonManager.addCache("testCache");
Cache test = singletonManager.getCache("testCache");

// 使用自定义配置添加缓存,注意缓存未添加进CacheManager之前并不可用
CacheManager singletonManager = CacheManager.create();
Cache memoryOnlyCache = new Cache("testCache", 5000, false, false, 5, 2);
singletonManager.addCache(memoryOnlyCache);
Cache test = singletonManager.getCache("testCache");

// 使用特定的配置添加缓存
CacheManager manager = CacheManager.create();
Cache testCache = new Cache(
 new CacheConfiguration("testCache", maxEntriesLocalHeap)
 .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
 .eternal(false)
 .timeToLiveSeconds(60)
 .timeToIdleSeconds(30)
 .diskExpiryThreadIntervalSeconds(0)
 .persistence(new PersistenceConfiguration().strategy(Strategy.LOCALTEMPSWAP)));
 manager.addCache(testCache);

3.删除缓存

删除缓存比较简单,你只需要将指定的缓存名传入removeCache方法即可。

CacheManager singletonManager = CacheManager.create();
singletonManager.removeCache("sampleCache1");

4.基本缓存操作

Cache最重要的两个方法就是put和get,分别用来添加Element和获取Element。 Cache还提供了一系列的get、set方法来设置或获取缓存参数,这里不一一列举,更多API操作可参考官方API开发手册

/**
 * 测试:使用默认配置或使用指定配置来创建CacheManager
 *
 * @author Zhang Peng
 */
public class CacheOperationTest {
    private final Logger log = LoggerFactory.getLogger(CacheOperationTest.class);

    /**
     * 使用Ehcache默认配置(classpath下的ehcache.xml)获取单例的CacheManager实例
     */
    @Test
    public void operation() {
        CacheManager manager = CacheManager.newInstance("src/test/resources/ehcache/ehcache.xml");

        // 获得Cache的引用
        Cache cache = manager.getCache("userCache");

        // 将一个Element添加到Cache
        cache.put(new Element("key1", "value1"));

        // 获取Element,Element类支持序列化,所以下面两种方法都可以用
        Element element1 = cache.get("key1");
        // 获取非序列化的值
        log.debug("key:{}, value:{}", element1.getObjectKey(), element1.getObjectValue());
        // 获取序列化的值
        log.debug("key:{}, value:{}", element1.getKey(), element1.getValue());

        // 更新Cache中的Element
        cache.put(new Element("key1", "value2"));
        Element element2 = cache.get("key1");
        log.debug("key:{}, value:{}", element2.getObjectKey(), element2.getObjectValue());

        // 获取Cache的元素数
        log.debug("cache size:{}", cache.getSize());

        // 获取MemoryStore的元素数
        log.debug("MemoryStoreSize:{}", cache.getMemoryStoreSize());

        // 获取DiskStore的元素数
        log.debug("DiskStoreSize:{}", cache.getDiskStoreSize());

        // 移除Element
        cache.remove("key1");
        log.debug("cache size:{}", cache.getSize());

        // 关闭当前CacheManager对象
        manager.shutdown();

        // 关闭CacheManager单例实例
        CacheManager.getInstance().shutdown();
    }
}

5. Spring整合Ehcache

Spring3.1开始添加了对缓存的支持。和事务功能的支持方式类似,缓存抽象允许底层使用不同的缓存解决方案来进行整合。

原理:Spring对缓存的支持类似于对事务的支持。 首先使用注解标记方法,相当于定义了切点,然后使用Aop技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。而Spring Cache利用了Spring AOP的动态代理技术,即当客户端尝试调用pojo的foo()方法的时候,给它的不是pojo自身的引用,而是一个动态生成的代理类。

img

  1. 一般首先要在spring中加入对ehcache的管理:

    <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache/ehcache.xml"/>
    </bean>
       
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager" ref="ehcache"/>
    </bean>
    
  2. 注解开启spring对ehcache的支持;

    (1) 在xml中声明 像上一节spring-ehcache.xml中的做法一样,使用<cache:annotation-driven/>

    <cache:annotation-driven cache-manager="cacheManager"/>
    

    (2) 使用标记注解 你也可以通过对一个类进行注解修饰的方式在这个类中使用缓存注解。 范例如下:

    @Configuration
    @EnableCaching
    public class AppConfig {
    }
    
  3. 通过配置,生成cacheManager/cache(见上面1-3)

  4. 使用注解操作缓存: 下面前三个注解都是方法级别:

    • @Cacheable

      表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。 这个注解可以用condition属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。 可以使用key属性来指定key的生成规则。

    • @CachePut

    @Cacheable不同,@CachePut不仅会缓存方法的结果,还会执行方法的代码段。 它支持的属性和用法都与@Cacheable一致。

    • @CacheEvict

      @Cacheable功能相反,@CacheEvict表明所修饰的方法是用来删除失效或无用的缓存数据。

    • @Caching

      如果需要使用同一个缓存注解(@Cacheable@CacheEvict@CachePut)多次修饰一个方法,就需要用到@Caching

    @Caching(evict = { @CacheEvict("primary"),  	     @CacheEvict(cacheNames="secondary", key="#p0") })
    public Book importBooks(String deposit, Date date)
    
    • @CacheConfig

      与前面的缓存注解不同,这是一个类级别的注解。 如果类的所有操作都是缓存操作,你可以使用@CacheConfig来指定类,省去一些配置。

    @CacheConfig("books")
    public class BookRepositoryImpl implements BookRepository {
    	@Cacheable
    	public Book findBook(ISBN isbn) {...}
    }
    
标签类型 作用 主要配置参数说明
@Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 value:缓存的名称,在 Spring 配置文件中定义,必须指定至少一个; key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合; condition:缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 value:缓存的名称,在 spring 配置文件中定义,必须指定至少一个; key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合; condition:缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@CacheEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空 value:缓存的名称,在 Spring 配置文件中定义,必须指定至少一个; key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合; condition:缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存; allEntries:是否清空所有缓存内容,默认为 false,如果指定为 true,则方法调用后将立即清空所有缓存; beforeInvocation:是否在方法执行前就清空,默认为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,默认情况下,如果方法执行抛出异常,则不会清空缓存

Redis缓存

Redis存储原理

1.RedisObject

Redis内部使用一个redisObject对象来标识所有的key和value数据,redisObject最主要的信息如图所示:

  • type代表一个value对象具体是何种数据类型,
  • encoding是不同数据类型在Redis内部的存储方式,比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或是int,如果是int则代表世界Redis内部是按数值类型存储和表示这个字符串。

img

图9左边的raw列为对象的编码方式:

  • 字符串可以被编码为raw(一般字符串)或Rint(为了节约内存,Redis会将字符串表示的64位有符号整数编码为整数来进行储存);
  • 列表可以被编码为ziplist或linkedlist,ziplist是为节约大小较小的列表空间而作的特殊表示;
  • 集合可以被编码为intset或者hashtable,intset是只储存数字的小集合的特殊表示;
  • hash表可以编码为zipmap或者hashtable,zipmap是小hash表的特殊表示;
  • 有序集合可以被编码为ziplist或者skiplist格式,ziplist用于表示小的有序集合,而skiplist则用于表示任何大小的有序集合;

IO模型:从网络I/O模型上看,Redis使用单线程的I/O复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select。

内存申请:相较于memcached的预分配内存管理,Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片。Redis跟据存储命令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临时数据(但会尝试剔除部分临时数据)。

2.持久化方式

我们描述Redis为内存数据库,作为缓存服务,大量使用内存间的数据快速读写,支持高并发大吞吐;而作为数据库,则是指Redis对缓存的持久化支持。Redis由于支持了非常丰富的内存数据库结构类型,如何把这些复杂的内存组织方式持久化到磁盘上?Redis的持久化与传统数据库的方式差异较大,Redis一共支持四种持久化方式,主要使用的两种:

  1. 定时快照方式(snapshot):该持久化方式实际是在Redis内部一个定时器事件,每隔固定时间去检查当前数据发生的改变次数与时间是否满足配置的持久化触发的条件,如果满足则通过操作系统fork调用来创建出一个子进程,这个子进程默认会与父进程共享相同的地址空间,这时就可以通过子进程来遍历整个内存来进行存储操作,而主进程则仍然可以提供服务,当有写入时由操作系统按照内存页(page)为单位来进行copy-on-write保证父子进程之间不会互相影响。它的缺点是快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据。

  2. 基于语句追加文件的方式(aof):aof方式实际类似MySQl的基于语句的binlog方式,即每条会使Redis内存数据发生改变的命令都会追加到一个log文件中,也就是说这个log文件就是Redis的持久化数据。

    aof的方式的主要缺点是追加log文件可能导致体积过大,当系统重启恢复数据时如果是aof的方式则加载数据会非常慢,几十G的数据可能需要几小时才能加载完,当然这个耗时并不是因为磁盘文件读取速度慢,而是由于读取的所有命令都要在内存中执行一遍。另外由于每条命令都要写log,所以使用aof的方式,Redis的读写性能也会有所下降。

Redis的持久化使用了Buffer I/O,所谓Buffer I/O是指Redis对持久化文件的写入和读取操作都会使用物理内存的Page Cache,而大多数数据库系统会使用Direct I/O来绕过这层Page Cache并自行维护一个数据的Cache。而当Redis的持久化文件过大(尤其是快照文件),并对其进行读写时,磁盘文件中的数据都会被加载到物理内存中作为操作系统对该文件的一层Cache,而这层Cache的数据与Redis内存中管理的数据实际是重复存储的。虽然内核在物理内存紧张时会做Page Cache的剔除工作,但内核很可能认为某块Page Cache更重要,而让你的进程开始Swap,这时你的系统就会开始出现不稳定或者崩溃了,因此在持久化配置后,针对内存使用需要实时监控观察。

分布式Redis缓存

Redis更倾向于在服务端构建分布式存储

img

img

通过集群+主从结合的设计,Redis在扩展和稳定高可用性能方面都是比较成熟的。

但是,在数据一致性问题上,Redis没有提供CAS操作命令来保障高并发场景下的数据一致性问题,不过它却提供了事务的功能,Redis的Transactions提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行)。但是这个Transactions还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间有会有其它客户端命令插进来执行)

以下多种Web应用场景,在这些场景下可以充分的利用Redis的特性,大大提高效率。

  • 在主页中显示最新的项目列表:Redis使用的是常驻内存的缓存,速度非常快。LPUSH用来插入一个内容ID,作为关键字存储在列表头部。LTRIM用来限制列表中的项目数最多为5000。如果用户需要的检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库。
  • 删除和过滤:如果一篇文章被删除,可以使用LREM从缓存中彻底清除掉。
  • 排行榜及相关问题:排行榜(leader board)按照得分进行排序。ZADD命令可以直接实现这个功能,而ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
  • 按照用户投票和时间排序:排行榜,得分会随着时间变化。LPUSH和LTRIM命令结合运用,把文章添加到一个列表中。一项后台任务用来获取列表,并重新计算列表的排序,ZADD命令用来按照新的顺序填充生成列表。列表可以实现非常快速的检索,即使是负载很重的站点。
  • 过期项目处理:使用Unix时间作为关键字,用来保持列表能够按时间排序。对current_time和time_to_live进行检索,完成查找过期项目的艰巨任务。另一项后台任务使用ZRANGE…WITHSCORES进行查询,删除过期的条目。
  • 计数:进行各种数据统计的用途是非常广泛的,比如想知道什么时候封锁一个IP地址。INCRBY命令让这些变得很容易,通过原子递增保持计数;GETSET用来重置计数器;过期属性用来确认一个关键字什么时候应该删除。
  • 特定时间内的特定项目:这是特定访问者的问题,可以通过给每次页面浏览使用SADD命令来解决。SADD不会将已经存在的成员添加到一个集合。
  • Pub/Sub:在更新中保持用户对数据的映射是系统中的一个普遍任务。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,让这个变得更加容易。
  • 队列:在当前的编程中队列随处可见。除了push和pop类型的命令之外,Redis还有阻塞队列的命令,能够让一个程序在执行时被另一个程序添加到队列。

阿里分布式缓存服务 Tair

一个 Tair 集群主要包括 3 个必选模块:configserver、dataserver和client,以及一个可选模块:invalidserver。通常情况下,一个集群中包含 2 台 configserver 及多台 dataServer 。两台 configserver 互为主备并通过维护和 dataserver 之间的心跳获知集群中存活可用的 dataserver ,构建数据在集群中的分布信息(对照表)。dataserver 负责数据的存储,并按照 configserver 的指示完成数据的复制和迁移工作。client 在启动的时候,从 configserver 获取数据分布信息,根据数据分布信息和相应的 dataserver 交互完成用户的请求。invalidserver 主要负责对等集群的删除和隐藏操作,保证对等集群的数据一致。

img

ConfigServer

  1. 通过维护和 dataserver 心跳来获知集群中存活节点的信息
  2. 根据存活节点的信息来构建数据在集群中的分布表
  3. 提供数据分布表的查询服务
  4. 调度 dataserver 之间的数据迁移、复制

DataServer

  1. 提供存储引擎
  2. 接受 client 的 put/get/remove 等操作
  3. 执行数据迁移,复制等
  4. 插件:在接受请求的时候处理一些自定义功能
  5. 访问统计

client

  1. 在应用端提供访问 Tair 集群的接口
  2. 更新并缓存数据分布表和 invalidserver 地址等
  3. LocalCache,避免过热数据访问影响 Tair 集群服务
  4. 流控

InvalidServer

  1. 接收来自 client 的 invalid/hide 等请求后,对属于同一组的集群(双机房独立集群部署方式)做delete/hide操作,保证同一组集群的一致

  2. 集群断网之后的,脏数据清理

  3. 访问统计

    img

参考

美团-缓存

spring-notes

https://raychase.iteye.com/blog/1545906