JetCache系列 -- Cache 的使用场景和其详细使用

 

序言

本文,我想详细地讲讲缓存的使用。我们都知道,根据现代计算机存储介质的不同,我们引入了Cache 这个概念, Cache 在计算机芯片, 各级内存,硬盘,乃至于各种软件设计中都是非常常见的,Cache 使用的好,能够合理分层,我们能解决百分之八十以上的性能问题,由于目前大部分的

互联网服务应用都是 重io为主的服务,所以 网络服务的质量和速度 跟缓存的使用有着密不可分的关系。这节会结合 各种场景下 缓存的使用,推荐一些最佳的使用场景,结合JetCache 达到一种比较边界的提升接口性能的代码编写方式。

JetCache 的优势:

https://blog.csdn.net/dongjijiaoxiangqu/article/details/109643337

正文

Cache 作为 热点数据的备份, 可以分为进程内缓存,和分布式缓存。 我们常见用的 redis,Memcached 是 分布式缓存, 一般这种缓存的好处是有集中式管理的地方,而且分布式部署缓存能够同步。而进程内缓存,因为每台机器的应用进程是不同的,而一般应用部署会采用多台机器部署,所以进程内缓存 就没有多台机器同步的功能。 

 

而 Cache 的主要关注元素有:

  1. 命中率
  2.  Cache 容量
  3. 达到Cache 最大容量后的数据淘汰策略, 常见淘汰策略有 LRU,FIFO,LFU, Sketch-Min 算法等

 

而常见互联网应用中的Cache 可能是直接用 redis了,通过使用 redis 客户端,异步客户端有 lettuce, 同步客户端较老的有 jedis ,本身来说 redis 有着足够强劲的性能,能够配置主从,和集群的方式,来提高Cache  系统的性能, 那么有没有提高的方式呢,

是有的,在Redis (或其他 远程分布式缓存)的基础上,我们考虑再加上一层进程内级别的缓存。 类似于现代计算机的缓存构成

 

-进程级别Cache

-- 远程分布式级别 Cache

 

读取数据如果能从 进程级别内找到数据,就不用走一次网络开销去查找远程Cache 数据,那么势必能够系统的 接口性能带来一定量的提升。

那么我们介绍多级 Cache的使用, JetCache 也是实现了多级缓存的功能,由于进程级别直接的Cache 数据不能共享,但是应用部署多台服务器,我们希望 进程级别的Cache 数据,当某一 Cache Item 被删除时候,其他服务器的进程级别Cache 相同Item 也能被删除,尽量减少脏缓存数据的产生,尽量进程级别的缓存数据也能够同步删除。

 

 

 

JetCache 的基本依赖

 

  1.  首先是如何引进 依赖,以最常见的 springboot 方式依赖来引入,其他 spring 引入,在此不表。 然后需要声明  @EnableMethodCache(basePackages = "com.company.mypackage")  @EnableCreateCacheAnnotation 表明支持 JetCache 的注解使用

 

 

pom:

    <dependency>
            <groupId>com.weishi</groupId>
            <artifactId>jetcache-starter-redis-lettuce</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>minlog</artifactId>
                    <groupId>com.esotericsoftware</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.weishi</groupId>
            <artifactId>jetcache-anno-api</artifactId>
        </dependency>

        <dependency>
            <groupId>com.weishi</groupId>
            <artifactId>jetcache-core</artifactId>
        </dependency>





App class:

@SpringBootApplication
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class MySpringBootApp {
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApp.class);
    }
}

 

 

如果是 api 接口包,不想引入太多依赖,可以只引入 jetcache-anno-api 包,可以直接使用注解, 但是注解生效还得把 redis 客户端依赖, 和 jetcache-core 依赖引入

 

 

JetCache 常见的配置设置

 

jetcache:
  statIntervalMinutes: 15                 //统一间隔
  areaInCacheName: false                  //设置不把areaName作为Key前缀
  hiddenPackages: com.alibaba   //如果@Cached和@CreateCache的name是自动生成的,会默认的将包 名和方法名作为前缀,为了不让name太长,该设置时将制定的包名截掉
                         
  local:
    default:
      type: caffeine                //缓存类型,caffeine 
      limit: 100                    //
      keyConvertor: fastjson        //Key转换器的全局变量
      expireAfterWriteInMillis: 100000 // ms, 写入多久后 会被清理掉
    otherArea:
      type: linkedhashmap   // lru 算法
      limit: 100
      keyConvertor: none
      expireAfterWriteInMillis: 100000
  remote:
    default:
      type: redis  # 同步的redis 客户端, redis.lettuce 异步的redis 客户端
      keyPrefix: jedisTest_A1  // prefix 设置
      keyConvertor: fastjson
      valueEncoder: java
      valueDecoder: java
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50  
      host: ${redis.host}
      port: ${redis.port}
    otherArea:
      type: redis.lettuce
      keyConvertor: fastjson
      valueEncoder: kryo
      valueDecoder: kryo
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50
     uri: redis://127.0.0.1:6379/   # sentinels 的方式(uri: redis-sentinel://127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381/?sentinelMasterId=mymaster)
	 keyPrefix: lettuceClusterTest3  
	 

    
     

 

 

属性默认值说明
jetcache.statIntervalMinutes0间隔时间,0表示不统计
jetcache.areaInCacheNametruejetcache-anno把cacheName作为远程缓存key前缀,2.4.3以前的版本总是把areaName加在cacheName中,因此areaName也出现在key前缀中。2.4.4以后可以配置,为了保持远程key兼容默认值为true,但是新项目的话false更合理些。
jetcache.hiddenPackages@Cached和@CreateCache自动生成name的时候,为了不让name太长,hiddenPackages指定的包名前缀被截掉
jetcache.[local|remote].${area}.type缓存类型,tair、redis为当前支持的远程缓存,linkedhashmap、caffeine为当前支持的本地缓存类型
jetcache.[local|remote].${area}.keyConvertor

key转换器的全局配置,当前只有一个已经实现的keyConvertor:fastjson。仅当使用@CreateCache且缓存类型为LOCAL是可以指定为none,此时通过equals方法来识别key。方法缓存必须指定keyConvertor

jetcache.[local|remote].${area}.valueEncoderjava序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local|remote].${area}.valueDecoderjava序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local|remote].${area}.limit100每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定。注意是每个缓存实例的限制,而不是全部
jetcache.[local|remote].${area}.expireAfterWriteInMillis无穷大以毫秒为单位指定超时时间的全局配置,以前为defaultExpireInMillis
jetcache.local.${area}.expireAfterAccessInMillis0需要jetcache2.2以上,以毫秒为单位,指定多长时间没有访问,就让缓存失效,当前只有本地缓存支持。0表示不使用这个功能。

jetcache.local.${area}.expireAfterAccessInMillis

  里面的area 可以设置不同的redis 链接 和 不同的redis 数据库, @CreateCache 里面没有声明 area 的,默认使用default  area

 

 

这里值得一说的是, Cache 的type ,其中 redis 和 redis.lettuce的差距,这里推荐使用 redis.lettuce的方式来配置, lettuce 使用基于nio的 redis客户端,支持异步和reactive 方式来调用redis命令。 

 

Cache 的声明式的使用

 

生成Cache ,手动调用cache 方法,配置完成以后, 推荐使用方式,用注解的方式来 声明Cache ,因为通过注解声明出来的 Cache 会走默认的生成Cache 策略,会把全局配置给加上, 并且有数据监控,

采用多级缓存情况下,一级缓存(local)删除会有同步通知,并且删除其他机器的一级缓存进行同步删除动作,这是利用 Monitor 监听 删除事件,然后 从 Redis 发布远程命令,订阅者接受命令,删除进程缓存value来实现的。(也就是 redis PubSub 发布订阅模式,来做到多台服务器 进程缓存同步更新的策略)。

 

例子:

 @CreateCache(name = "ws_user", expire = 3, timeUnit = TimeUnit.MINUTES,
            localExpire = 2, cacheType = CacheType.BOTH, localLimit = 2000, serialPolicy = SerialPolicy.JAVA,
            keyConvertor = KeyConvertor.FASTJSON)
 Cache<Serializable, UserEntity> multiLevelCache;


 @CreateCache(name ="ws_gift",cacheType = CacheType.BOTH)
 Cache<Serializable,GiftEntity> giftEntityCache;


@PostConstruct
public void init(){
// 手动设置 xxx
    orderSumCache.config().setXXX();

}

 

可以通过 CreateCache 的方式创建 cache,如果需要手动设置一些没有提到的 Config, 可以通过上文方式, 通过调用 Cache.config() 来设置一些属性。

手动设置 config 可以设置, Loader,  监听 缓存数据操作的事件(包括放置,删除等)Monitors,RefreshPolicy(刷新策略),cachePenetrationProtect(缓存穿透保护等)

 

创建 Cache 的声明 @CreateCache ,我们经常配置了全局配置参数(也就是前面提到的 properties 或者 yml 方式来配置),这时候,我们使用 @CreateCache(name=xxx, cacheType=CacheType.BOTH), 通常来说这两个参数就够了,其他没设置的注解参数会默认继承全局配置。

cacheType 有 local, remote, both(两级缓存),意思也是很容易理解的,对应CacheType 为  CacheType.LOCAL,  CacheType.REMOTE, cacheType = CacheType.BOTH, 如果不写CacheType ,默认走的是 REMOTE 方式。

 

 

 

这种方式适用于手工创建cache 的过程,当然也可以通过 CacheBuilder 方式来创建,但是推荐上述注解方式来创建Cache,这样可以加入统一aop创建,加入数据监控 log和多级缓存的一级缓存同步更新。

通过接口可以看出,其支持同步和异步的方式取值

 

CacheGetResult r = cache.GET(userId);
CompletionStage<ResultData> future = r.future();
future.thenRun(() -> {
    if(r.isSuccess()){
        System.out.println(r.getValue());
    }
});

// 直接返回结果, 注意 lettuce 客户端的话,(内部实现其实是异步调用,也有其默认超时时间 1s)也需要注意判空
V value=cache.getValue(userId);


 

由于JetCache 基于 lettuce 可以实现异步取值,异步放置值的调用,这有时候对于我们不关心放置缓存 的同步时候,可以采用异步调用的方式来减少同步调用带来时间开销,当我们不关注返回值或者是想要用异步的方式来放置缓存时候,这类方法都是 大写开头的方法,返回了CacheGetResult 结果,然后采用异步方式可以获取结果,或者不关注结果

例子:

 

cache.PUT(userId);

cache.REMOVE(userId);

//还有一些大写开头的方法,都是可以异步调用

 

 

CacheGetResult 取值方式可以以更清楚方式获取值的状态,不仅仅是一个值

CacheGetResult<OrderDO> r = cache.GET(orderId);
if( r.isSuccess() ){
    OrderDO order = r.getValue();
} else if (r.getResultCode() == CacheResultCode.NOT_EXISTS) {
    System.out.println("cache miss:" + orderId);
} else if(r.getResultCode() == CacheResultCode.EXPIRED) {
    System.out.println("cache expired:" + orderId));
} else {
    System.out.println("cache get error:" + orderId);
}

 

 

也可以支持 Spring Annotation 方式进行 方法的声明式缓存 , 也支持 spring EL 表达式

例子:

 

public interface UserService {
    @Cached(name="userCache-", key="#userId", expire = 3600)
    @CachePenetrationProtect
    User getUserById(long userId);

	@CacheInvalidate(name="userCache-", key="#user.userId")
    void updateUser(User user);

    @CacheInvalidate(name="userCache-", key="#userId")
    void deleteUser(long userId);
}

 

上文代码中的意图比较明显,这里值得一提的是,更新操作,可以直接把 Cache的值 Invaildate ,然后让其取数据库加载。如要防止缓存穿透,可以在取值过程中加上 @CachePenetrationProtect 注解

 

其中 @CreateCache和 @Cached 注解都可以 声明是否是多级 Cache, 通过 cacheType = CacheType.BOTH ( CacheType.REMOTE, CacheType.LOCAL)来声明,value过期时间等其他配置如果不设置都会设置为

配置文件里面的默认值。

 

如果需要定期更新 Cache值(当然这种情况,我觉得是很少会有这种场景的,也可以用 refresh loader注解)  @CacheRefresh ,会定期去取 loader 中的数据

例子

 

public interface SummaryService{
    @Cached(expire = 3600, cacheType = CacheType.REMOTE)
    @CacheRefresh(refresh = 1800, stopRefreshAfterLastAccess = 3600, timeUnit = TimeUnit.SECONDS)
    @CachePenetrationProtect
    BigDecimal summaryOfToday(long catagoryId);
}

 

 

分布式锁的简单支持

通过 实现一个较为简单的分布式锁,指的是 key not exist 并加上特有的 key 和其超时时间,不会解锁其他线程的key ,和自己目前实现的不复杂分布式锁是基本一致的。如果需要更高级严谨的分布式锁,可以考虑 redission 的 redlock  。 MutliCache 会默认使用分布式锁。

 

 // 使用try-with-resource方式,可以自动释放锁
  try(AutoReleaseLock lock = cache.tryLock("MyKey",100, TimeUnit.SECONDS)){
     if(lock != null){
        // do something
     }
  }finally{
   lock.close();
}



  boolean hasRun = cache.tryLockAndRun("MyKey",100, TimeUnit.SECONDS, () -> {
    // do something
  });

 

上文的意图也很明显了,在此不表。

 

在此 JetCache 的简单使用 基本是介绍完了。

 

结尾

本文意在介绍 JetCache 的 常规和推荐用法, 通过声明式的注解,就能为接口赋予不俗的 Cache 性能

 

 

 

 

 

 


版权声明:本文为dongjijiaoxiangqu原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。