Spring Boot/Cloud项目中使用缓存以及各种缓存产品(Cache Provider)的特性介绍

几乎稍微大一点的项目都会用到缓存。

在之前的某个Spring boot项目中,需要用到缓存,于是翻阅了Spring官方的文档,文档讲的比较概要,网上好多博客又比较杂,所以简单总结以下要点,以便快速有个清晰认识 少走坑路。

1. 如何选择cache provider?

这通常是我们首先要面临的第一个问题,到底应该选择哪个缓存提供商呢,这得根据项目的具体需求以及可用的OSS来定。Spring提供了cache相关的接口,并且它也提供了一两个简单的实现,但简单实现可能无法满足更加复杂的场景,所以我先把Spring支持的产品以及他们的特性列举一下,希望对你有所帮助。

Spring 5.0以上的版本的CacheManager支持以下产品:

JCache (JSR-107)

一种即将公布的标准规范(JSR 107),说明了一种对Java对象临时在内存中进行缓存的方法,包括对象的创建、共享访问、假脱机(spooling)、失效、各JVM的一致性等

从Spring 4.1版开始,Spring的缓存抽象完全支持JCache标准注释:@CacheResult,@CachePut,@CacheRemove和@CacheRemoveAll,@CacheDefaults,@CacheKey 以及@CacheValue。即使不将缓存存储库迁移到JSR-107,也可以使用这些注释。 内部实现使用Spring的缓存抽象,并提供符合规范的默认CacheResolver和KeyGenerator实现。 换句话说,如果您已经在使用Spring的缓存抽象,则可以切换到这些标准注释,而无需更改缓存存储(或配置)。

 

EhCache 2.x

Ehcache是一个用Java实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的cache管理方案。同时ehcache作为开放源代码项目,采用限制比较宽松的Apache License V2.0作为授权方式,被广泛地用于Hibernate,  Spring,Cocoon等其他开源系统。

 

Hazelcast

Hazelcast是基于内存的数据网格开源项目,同时也是该公司的名称。Hazelcast提供弹性可扩展的分布式内存计算,Hazelcast被公认是提高应用程序性能和扩展性最好的方案。Hazelcast通过开放源码的方式提供以上服务。更重要的是,Hazelcast通过提供对开发者友好的Map、Queue、ExecutorService、Lock和JCache接口使分布式计算变得更加简单。例如,Map接口提供了内存中的键值存储,这在开发人员友好性和开发人员生产力方面提供了NoSQL的许多优点。除了在内存中存储数据外,Hazelcast还提供了一组方便的api来访问集群中的cpu,以获得最大的处理速度轻量化和简单易用是Hazelcast的设计目标。Hazelcast以Jar包的方式发布,因此除Java语言外Hazelcast没有任何依赖。Hazelcast可以轻松地内嵌已有的项目或应用中,并提供分布式数据结构和分布式计算工具。Hazelcast 具有高可扩展性和高可用性(100%可用,从不失败)。分布式应用程序可以使用Hazelcast进行分布式缓存、同步、集群、处理、发布/订阅消息等。Hazelcast基于Java实现,并提供C/C++,.NET,REST,Python、Go和Node.js客户端。Hazelcast遵守内存缓存协议,可以内嵌到Hibernate框架,并且可以和任何现有的数据库系统一起使用。

 

Infinispan

Infinispan是基于Apache 2.0协议的分布式键值存储系统可以以普通java lib或者独立服务的方式提供服务支持各种协议(Hot Rod, REST, Memcached and WebSockets)
支持的高级特性包括:事务、事件通知、高级查询、分布式处理、off-heap及故障迁移

适用场景:

缓存部署在独立节点上,其耗用CPU、内存等对应用程序自身运行不会带来影响

只要遵守Infinispan支持的协议,客户端可运行在各种环境之下。另外,对于非Java应用只能采用此种方式。

适用于Java应用但自身并不需要长期、稳定运行(如运行一次,或存在频繁重启场景)等场景。

 

Couchbase

Couchbase是CouchDB和MemBase的合并而memBase是基于Memcached的。因此couchbase联合了couchbase的简单可靠和memcached的高性能,以及membase的可扩展性

灵活的数据模型:couchbase中使用json格式存储对象和对象之间的关系。

Nosql数据库的一个特性是不需要定义数据结构,在couchbase中,数据可以存储为key-value对或者json文档,不需要预先定义严格的格式,由于这种特性,couchbase支持以 scale out(水平扩展)方式扩展数据量,提升io性能,只需要在集群中添加更多的服务器就行了。相反,关系数据库管理系统scale up(纵向扩展),通过加更多的CPU,内存和硬盘以扩展容量。

Couchbase可用于单机环境,也可以和其他服务器一起提供分布式的数据存储。

 

Redis

Redisd大家应该比较熟悉了,它是一个Key-Value非关系型数据库,存储和读取非常简单,它也可以用来做缓存,既可以缓存数据到内存,也可以基于磁盘持久化到redis数据库

Spring + Redis是用的比较多的,适用大部分场景,所以一般只要条件允许(服务器装有redis,或者有钱购买redis云服务)无脑就选它

Redis的优势:

异常快 - Redis非常快,每秒可执行大约110000次的设置(SET)操作,每秒大约可执行81000次的读取/获取(GET)操作。

 

支持丰富的数据类型 - Redis支持开发人员常用的大多数数据类型,例如列表,集合,排序集和散列等等。

这使得Redis很容易被用来解决各种问题,因为我们知道哪些问题可以更好使用地哪些数据类型来处理解决。

 

操作具有原子性 - 所有Redis操作都是原子操作,这确保如果两个客户端并发访问,Redis服务器能接收更新的值。

多实用工具 - Redis是一个多实用工具,可用于多种用例,如:缓存,消息队列(Redis本地支持发布/订阅),应用程序中的任何短期数据,例如,web应用程序中的会话,网页命中计数等。

 

Caffeine

Caffeine是使用Java8对Guava缓存的重写版本在Spring Boot 2.0中将取代(怪不得我再Spring Boot2.0里面找不到GuavaCacheManager L),基于LRU算法实现,支持多种缓存过期策略

spring5已经放弃guava,拥抱caffeine。

java应用缓存一般分两种,一是进程内缓存,就是使用java应用虚拟机内存的缓存;另一个是进程外缓存,也就是现在我们常用的各种分布式缓存

 

Simple

就是Spring自带的简单阉割版缓存实现机制啦,比如ConcurrentMapCacheManager,NoOpCacheManager等,这种也用的也比较多,为啥? 就是简单,容易上手!而且大部分项目都不是大型分布式系统,不需要考虑那么多,能将缓存功能跑起来就万事大吉了。

当然这种缓存机制不太适用于对性能内存要求严格的场景,因为它是进程内缓存。也就是说消耗的是你虚拟机的内存。

我们可以基于这种简单缓存实现来定制自己的个性化需求,比如下面我将会演示使用ConcurrentMapCacheManager这种方式如何做缓存,这样很方便大家快速理解,并且也会演示如何在这种基本的Cache上面加入自己的操作,比如过期时间验证。

 

2. 如何实现缓存?

选择好Cache Provider之后便是在项目中去应用了,同其他spring boot依赖的开源软件一样,只需要简单的在项目管理工具(maven 或者gradle)里面添加一下依赖,然后用注解配置一下就可以方便使用了,当然也有写缓存产品有自己特定的配置(比如EhCache 需要在项目的resource目录下配置ehcache-config.xml). 配置的话,主要是配置Spring中的缓存管理器,即CacheManager。 用于缓存产品比较多,这里我只列举在spring boot项目中配置Redis缓存和简单Spring 自带的缓存。这两个是用的比较多的。

 

2.1 配置Redis CacheManager

新建一个RedisCacheConfig类,配置一下redisTemplate 和 cacheManager这两个bean:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisSerializer stringSerializer = new StringRedisSerializer();
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(stringSerializer);
        template.setValueSerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setHashValueSerializer(stringSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Primary
    @Bean("cacheManager")
    public CacheManager redisCacheManager(RedisTemplate redisTemplate) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(60));
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }
}

注意在配置redisTemplate的时候,会注入一个redisConnectionFactory对象作为参数,这个对象是不用手动写代码配置的,只需要配置文件里面去指定一下连接信息即可,如下所示。Redis本地默认连接的是6379端口,请确保本地成功安装了redis数据库,并且服务是开启状态。当然也可以指定一个远程的连接信息。

spring:

  redis:

    host: localhost

    password: XXXX

    port: 6379

 

配置redisTemplate还有个RedisSerializer对象需要配置,这里用的是StringRedisSerializer。这个是序列化器,也就是Redis会用什么方式去序列化/反序列化被缓存的对象,有StringRedisSerializer(用Spring去做序列化),也有Jackson2JsonRedisSerializer,用json做序列化。

配置CacheManager的时候需要注入刚刚配置的RedisTemplate对象,这里做了一些简单的超时和连接工厂设置。

可以发现,上面再配置CacheManager的时候,有个@Primary注解,这个有什么用呢?

一般情况下,如果项目只有一个缓存管理器的时候,是不需要care这些的,Spring会默认就使用那个缓存管理器,但是如果配有多个的话,就需要指定哪个是首要的,作为默认项,不然启动会报错的。由于一会我还需要再配一个缓存管理器,所以这里指定RedisCacheManagerPrimary(也就是默认的,首要的)。

 

OKRedis的缓存管理器就配置完了,已经可以开始使用了,简单到难以置信吧,这就是spring boot的强大之处,简单操作即可实现强大功能。那么我们怎么在项目中用它呢?请看下面:

建议大家最好写一个工具类去操作redis缓存,方便管理。

 

引入RedisTemplate 并使用它进行存取对象:

//引入RedisTemplate:
@Autowired
private RedisTemplate redisTemplate;

//存对象(设置一下key, value 以及超时时间,并设置时间单位为秒):
public void setMyObject(final String key, final Object obj, final long expireTime) {
    ValueOperations<String, Object> valueOper = redisTemplate.opsForValue();
    valueOper.set(key, obj, expireTime, TimeUnit.SECONDS);
}

//取对象(传入key就行):
public Object getMyObject(final String key) {
    ValueOperations<String, Object> valueOper = redisTemplate.opsForValue();
    return valueOper.get(key);
}

//也可以用字节去设置缓存对象和获取缓存对象(取的话同样逻辑,把connection.set换成connection.get):
//RedisCallBack里面有个doInRedis接口,需要自己实现,你可以在里面做任何偷鸡摸狗的事情。
public void set(final byte[] keyBytes, final byte[] valueBytes, final long expireTime) {
    redisTemplate.execute(new RedisCallback() {
       public Long doInRedis(RedisConnection connection) throws DataAccessException {
            connection.set(keyBytes, valueBytes);
            if (expireTime > 0) {
                connection.expire(keyBytes, expireTime);
            }
            return 1L;
        }
    });
}

请进一步去优化和复杂化缓存业务逻辑。

 

2.2 配置Spring自带的CacheManager实现:

新建一个SimpleCacheConfig类,做如下配置:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean("simpleCacheManager")
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }

}

这个Bean的名称就不能叫cacheManager了,得换个名字,比如simpleCacheManager, 并且它也不是默认的缓存管理器,所以在使用的时候,需要显示指定使用这个。

这个缓存管理器比较简单,你甚至可以简单理解成就是一个支持并发的Map去做存取,它是线程安全的。尽管简单,但我们依然可以加入自定义的元素进去让它变得强大。

比如,我们自定义一个MyTimeSupportedCacheData 类来支持超时设置,在这个类里面有缓存对象和超时时间两个成员变量,在存的时候,我们通过存MyTimeSupportedCacheData 来代替直接存对象,这样的话 在取的时候也是取的MyTimeSupportedCacheData对象,然后通过MyTimeSupportedCacheData对象里面的超时时间来判断要不要返回缓存对象。

 

示例:

public class MyTimeSupportedCacheData {

    private Object cachedValue;
    private Date expireTime;

    public Object getCachedValue() {
        return cachedValue;
    }

    public void setCachedValue(Object cachedValue) {
        this.cachedValue = cachedValue;
    }

    public Date getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(Date expireTime) {
        this.expireTime = expireTime;
    }
}

新建一个工具类,在里面加入存和取的方法(支持超时时间设置):

@Autowired
@Qualifier("simpleCacheManager")
private CacheManager simpleCacheManager;

public void setObject(String key, Object value, int expireTime) {
   Date expireDate = DateUtils.addSeconds(new Date(), expireTime);
   MyTimeSupportedCacheData myTimeCacheData = new MyTimeSupportedCacheData();
   myTimeCacheData.setCachedValue(value);
   myTimeCacheData.setExpireTime(expireDate);

   Cache cache = simpleCacheManager.getCache("TestCache");
   cache.put(key, myTimeCacheData);
}

public Object getObject(String key) {
   Cache cache = simpleCacheManager.getCache("TestCache");
   Cache.ValueWrapper valueWrapper = cache.get(key);
   if (valueWrapper != null) {
      MyTimeSupportedCacheData myCacheData = (MyTimeSupportedCacheData) valueWrapper.get();
      if (myCacheData != null) {
         boolean expired = (new Date()).after(myCacheData.getExpireTime());
         if (expired) {
            cache.evict(key);
         }else {
            return myCacheData.getCachedValue();
         }
      }
   }
   return null;
}
原文地址:https://www.cnblogs.com/cnsec/p/13407125.html