本文,我想详细地讲讲缓存的使用。我们都知道,根据现代计算机存储介质的不同,我们引入了Cache 这个概念, Cache 在计算机芯片, 各级内存,硬盘,乃至于各种软件设计中都是非常常见的,Cache 使用的好,能够合理分层,我们能解决百分之八十以上的性能问题,由于目前大部分的
互联网服务应用都是 重io为主的服务,所以 网络服务的质量和速度 跟缓存的使用有着密不可分的关系。这节会结合 各种场景下 缓存的使用,推荐一些最佳的使用场景,结合JetCache 达到一种比较边界的提升接口性能的代码编写方式。
JetCache 的优势:
https://blog.csdn.net/dongjijiaoxiangqu/article/details/109643337
Cache 作为 热点数据的备份, 可以分为进程内缓存,和分布式缓存。 我们常见用的 redis,Memcached 是 分布式缓存, 一般这种缓存的好处是有集中式管理的地方,而且分布式部署缓存能够同步。而进程内缓存,因为每台机器的应用进程是不同的,而一般应用部署会采用多台机器部署,所以进程内缓存 就没有多台机器同步的功能。
而 Cache 的主要关注元素有:
而常见互联网应用中的Cache 可能是直接用 redis了,通过使用 redis 客户端,异步客户端有 lettuce, 同步客户端较老的有 jedis ,本身来说 redis 有着足够强劲的性能,能够配置主从,和集群的方式,来提高Cache 系统的性能, 那么有没有提高的方式呢,
是有的,在Redis (或其他 远程分布式缓存)的基础上,我们考虑再加上一层进程内级别的缓存。 类似于现代计算机的缓存构成
-进程级别Cache
-- 远程分布式级别 Cache
读取数据如果能从 进程级别内找到数据,就不用走一次网络开销去查找远程Cache 数据,那么势必能够系统的 接口性能带来一定量的提升。
那么我们介绍多级 Cache的使用, JetCache 也是实现了多级缓存的功能,由于进程级别直接的Cache 数据不能共享,但是应用部署多台服务器,我们希望 进程级别的Cache 数据,当某一 Cache Item 被删除时候,其他服务器的进程级别Cache 相同Item 也能被删除,尽量减少脏缓存数据的产生,尽量进程级别的缓存数据也能够同步删除。
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.statIntervalMinutes | 0 | 间隔时间,0表示不统计 |
jetcache.areaInCacheName | true | jetcache-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}.valueEncoder | java | 序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.valueDecoder | java | 序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.limit | 100 | 每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定。注意是每个缓存实例的限制,而不是全部 |
jetcache.[local|remote].${area}.expireAfterWriteInMillis | 无穷大 | 以毫秒为单位指定超时时间的全局配置,以前为defaultExpireInMillis |
jetcache.local.${area}.expireAfterAccessInMillis | 0 | 需要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 性能