上文中我们学习了springboot中缓存的基本使用。缓存分为本地caffeine缓存和远程redis缓存。现在有一个小小的问题,我想使用本地caffeine缓存和远程redis缓存组成二级缓存。还想保证他们的一致性,这个事情该怎么办呢?
Jetcache框架为我们解决了这个问题。
JetCache是一个由阿里巴巴开发的基于Java的缓存系统封装,旨在通过统一的API和注解简化缓存的使用。JetCache提供了比SpringCache更强大的功能,包括支持TTL(Time To Live,生存时间)、两级缓存、分布式自动刷新等特性。它支持多种缓存实现,如RedisCache、CaffeineCache(内存缓存)和LinkedHashMapCache(内存缓存),并且可以轻松添加新的缓存实现。
简单讲:jetcache不是缓存,他是保证缓存的一致性的框架。
JetCache支持的本地缓存类型包括LinkedHashMapCache和CaffeineCache。
JetCache支持的远程缓存类型包括redis、 Tair等。
放一下官方文档:
官方文档中有部分配置是有bug的,都是坑。后边我们会说到。
下面我们在Springboot项目中集成jatcache。
一:添加pom依赖:
这里需要注意一下,jetcache支持不同的springboot-redis客户端。分别是:jedis和lettuce
Jedis和Lettuce的区别是什么呢?
Jedis 和 Lettuce 都是用于 Java 语言连接 Redis 的客户端,Jedis主要是同步方法,Lettuce主要是异步高性能,基于Netty。
具体请移步《 【SpringBoot 】Jedis 和Lettuce 的区别》
扯远了,回到主题:
如果你使用的是Lettuce客户端,那就引入:
<!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-starter-redis-lettuce -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis-lettuce</artifactId>
<version>2.7.5</version>
</dependency>
如果你使用的是Jedis客户端,那就引入:
<!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-starter-redis -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis </artifactId>
<version>2.7.5</version>
</dependency>
我这里使用的是lettuce
二:配置yml文件
这部分也是一样的,是根据你是用的redis客户端来选择的。这部分上边已经说过了,这里不再赘述,我这里是用的是lettuce。官方文档如下图所示:
这里得配置写法要参考一下官方文档中内容。
Application.yml内容如下:
jetcache:
## 统计间隔,0表示不统计,开启后定期在控制台输出缓存信息
statIntervalMinutes: 15
## 是否把cacheName作为远程缓存key前缀
areaInCacheName: false
## 本地缓存配置
local:
default: ## default表示全部生效,也可以指定某个cacheName
## 本地缓存类型,其他可选:caffeine/linkedhashmap
type: caffeine
keyConvertor: jackson
#expireAfterWrite: 1800s # 缓存写入后10分钟过期
# lettuce远程缓存配置
remote:
default:
type: redis.lettuce
keyConvertor: jackson # fastjson/jackson
broadcastChannel: ${spring.application.name}
valueEncoder: java #other choose:kryo/kryo5
valueDecoder: java #other choose:kryo/kryo5
mode: masterslave # 设置为主从模式 集群模式:cluster
#expireAfterWrite: 1800s # 缓存写入后10分钟过期 10m
# uri格式:redis://密码@ip:端口/redis库名?timeout=5s
# 配置哨兵集群
uri:
- redis-sentinel://xxx@39.99.144.212:26379/?sentinelMasterId=mymaster
- redis-sentinel://xxx@39.99.144.212:26380/?sentinelMasterId=mymaster
- redis-sentinel://xxx@39.99.144.212:26381/?sentinelMasterId=mymaster
# 哨兵模式配置三个哨兵就报错
#uri: redis-sentinel://39.99.144.212:26379,39.99.144.212:26380,39.99.144.212:26381/?sentinelMasterId=mymaster
readFrom: slavePreferred # 优先从Slave节点中读取
官方给出的链接redis代码示例是:
uri: redis://127.0.0.1:6379/
但是,如果我的redis有密码该怎么写呢?官方没给。没事,我给:
uri格式:redis://密码@ip:端口/redis库名?timeout=5s
如上方代码所示,我得redis是哨兵集群。官方文档给出的哨兵集群链接写法是:
#uri: redis-sentinel://127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381/?sentinelMasterId=mymaster
#readFrom: slavePreferred
但是呢,是不好用的,连不上,下边是我尝试好用的链接redis哨兵的写法:
# 配置哨兵集群
uri:
- redis-sentinel://camellia@39.99.144.212:26379/?sentinelMasterId=mymaster
- redis-sentinel://camellia@39.99.144.212:26380/?sentinelMasterId=mymaster
- redis-sentinel://camellia@39.99.144.212:26381/?sentinelMasterId=mymaster
官方文档,真坑。
配置通用说明如下
| 属性 | 默认值 | 说明 |
|---|---|---|
| jetcache.statIntervalMinutes | 0 | 统计间隔,0表示不统计 |
| jetcache.areaInCacheName | true(2.6-) false(2.7+) | jetcache-anno把cacheName作为远程缓存key前缀,2.4.3以前的版本总是把areaName加在cacheName中,因此areaName也出现在key前缀中。2.4.4以后可以配置,为了保持远程key兼容默认值为true,但是新项目的话false更合理些,2.7默认值已改为false。 |
| jetcache.hiddenPackages | 无 | @Cached和@CreateCache自动生成name的时候,为了不让name太长,hiddenPackages指定的包名前缀被截掉 |
| jetcache.[local/remote].${area}.type | 无 | 缓存类型。tair、redis为当前支持的远程缓存;linkedhashmap、caffeine为当前支持的本地缓存类型 |
| jetcache.[local/remote].${area}.keyConvertor | fastjson2 | key转换器的全局配置,2.6.5+已经支持的keyConvertor:fastjson2/jackson; 2.6.5-只有一个已经实现的keyConvertor:fastjson。仅当使用@CreateCache且缓存类型为LOCAL时可以指定为none,此时通过equals方法来识别key。方法缓存必须指定keyConvertor |
| jetcache.[local/remote].${area}.valueEncoder | java | 序列化器的全局配置。仅remote类型的缓存需要指定,2.7+可选java/kryo/kryo5;2.6-可选java/kryo |
| jetcache.[local/remote].${area}.valueDecoder | java | 序列化器的全局配置。仅remote类型的缓存需要指定,2.7+可选java/kryo/kryo5;2.6-可选java/kryo |
| jetcache.[local/remote].${area}.limit | 100 | 每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定。注意是每个缓存实例的限制,而不是全部,比如这里指定100,然后用@CreateCache创建了两个缓存实例(并且注解上没有设置localLimit属性),那么每个缓存实例的限制都是100 |
| jetcache.[local/remote].${area}.expireAfterWriteInMillis | 无穷大 | 以毫秒为单位指定超时时间的全局配置(以前为defaultExpireInMillis) |
| jetcache.remote.${area}.broadcastChannel | 无 | jetcahe2.7的两级缓存支持更新以后失效其他JVM中的local cache,但多个服务共用redis同一个channel可能会造成广播风暴,需要在这里指定channel,你可以决定多个不同的服务是否共用同一个channel。如果没有指定则不开启。 |
| jetcache.local.${area}.expireAfterAccessInMillis | 0 | 需要jetcache2.2以上,以毫秒为单位,指定多长时间没有访问,就让缓存失效,当前只有本地缓存支持。0表示不使用这个功能。 |
上表中${area}对应@Cached和@CreateCache的area属性。注意如果注解上没有指定area默认值是"default"。
三:开启jetcache缓存
开启jetcache缓存的方式很简单,在启动方法上加上注解@EnableMethodCache
代码如下所示:
@SpringBootApplication
//jetcache启用缓存的主开关
//@EnableCreateCacheAnnotation // 开启可通过@CreateCache注解创建Cache实例功能。不使用@CreateCache注解就不用添加这个注解
@EnableMethodCache(basePackages = "com.modules") // 开启可通过@Cached注解创建Cache实例功能,初始化spring aop
public class BlogBusinessArticleApplication
{
public static void main(String[] args)
{
SpringApplication.run(BlogBusinessArticleApplication.class, args);
}
}
四:使用jetcache
使用缓存一共有三种方式:
1:(推荐)AOP模式:通过@Cached,@CacheUpdate,@CacheInvalidate注解
@Cache 属性说明:
如果使用二级缓存(本地+远程),需要设置localExpire(本地缓存过期时间)这个时间通常小于远程缓存过期时间。
@CacheRefresh 属性说明:
@CacheInvalidate 属性说明:
@CacheUpdate 属性说明:
@CachePenetrationProtect 注解:
当缓存访问未命中的情况下对并发进行的加载行为进行保护。
当前版本实现的是单JVM内的保护,即同一个JVM中同一个key只有一个线程去加载,其它线程等待结果。
对于以上未定义默认值的参数,如果没有指定,将使用yml中指定的全局配置
在代码中测试一下:
// ===========================================================================
// jetcache
/**
* 使用远程缓存
* @param id
* @return
*/
@GetMapping("/article/getRemote")
@Cached(name="userCache:", key = "#id", expire = 3600, cacheType = CacheType.REMOTE)
public User getRemote(Long id){
// 直接新建用户,模拟从数据库获取数据
User user = new User();
user.setId(id);
user.setName("用户remote"+id);
user.setAge(23);
user.setSex(1);
System.out.println("远程第一次获取数据,未走缓存:"+id);
return user;
}
/**
* 使用本地缓存
* @param id
* @return
*/
@GetMapping("/article/getLocal")
@Cached(name="userCache:", key = "#id", expire = 3600, cacheType = CacheType.LOCAL)
public User getLocal(Long id){
// 直接新建用户,模拟从数据库获取数据
User user = new User();
user.setId(id);
user.setName("用户local"+id);
user.setAge(23);
user.setSex(1);
System.out.println("本地第一次获取数据,未走缓存:"+id);
return user;
}
/**
* 使用本地和远程缓存
* @param id
* @return
*/
@GetMapping("/article/getBoth")
@Cached(name="userCache:", key = "#id", expire = 3600, localExpire = 3500, cacheType = CacheType.BOTH)
public User getBoth(Long id){
// 直接新建用户,模拟从数据库获取数据
User user = new User();
user.setId(id);
user.setName("用户both"+id);
user.setAge(23);
user.setSex(1);
System.out.println("第一次获取数据,未走缓存:"+id);
return user;
}
/**
* 更新缓存
* @param user
* @return
*/
@GetMapping("/article/updateUser")
@CacheUpdate(name = "userCache:", key = "#user.id", value = "#user")
public Boolean updateUser(@RequestBody User user)
{
// TODO 更新数据库
return true;
}
/**
* 删除缓存
* @param id
* @return
*/
@GetMapping("/article/deleteUser")
@CacheInvalidate(name = "userCache:", key = "#id")
public Boolean deleteUser(Long id)
{
System.out.println("删除缓存:"+id);
// TODO 从数据库删除
return true;
}
获取远程数据访问:http://localhost:8001/java/article/getRemote?id=32111
获取本地数据访问:http://localhost:8001/java/article/getLocal?id=32111
获取二级缓存数据访问:http://localhost:8001/java/article/getBoth?id=32111
删除数据访问:http://localhost:8001/java/article /deleteUser?id=32111
我这里的修改缓存方法是不可用的。在正式开发中,我觉得其实也不需要使用修改缓存的方法,在更新数据库数据结束之后,直接删除缓存即可。再有请求过来,直接重新获取就可以了。
2: 高级API模式:通过CacheManager,2.7 版本才可使用(我刚好用的是2.7.5版本)
(1):创建配置类JetCacheConfig.java
package com.modules.cache.config;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.CacheManager;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.template.QuickConfig;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class JetCacheConfig
{
@Autowired
private CacheManager cacheManager;
private Cache<Long, Object> userCache;
@PostConstruct
public void init(){
QuickConfig qc = QuickConfig.newBuilder("userCache:")
.expire(Duration.ofSeconds(3600))
.cacheType(CacheType.BOTH)
// 本地缓存更新后,将在所有的节点中删除缓存,以保持强一致性
.syncLocal(false)
.build();
userCache = cacheManager.getOrCreateCache(qc);
}
@Bean
public Cache<Long, Object> getUserCache(){
return userCache;
}
}
(2):在代码中使用
// ==========================================================
// jetcache 高级API用法
@Autowired
JetCacheConfig jetcacheConfig;
@Resource
private Cache<Long, Object> userCache;
@GetMapping("getG")
public User getG(Long id)
{
if(userCache.get(id) != null)
{
return (User) userCache.get(id);
}
User user = new User();
user.setId(id);
user.setName("用户both"+id);
user.setAge(23);
user.setSex(1);
userCache.put(id, user);
System.out.println("第一次获取数据,未走缓存:"+id);
return user;
}
@GetMapping("updateUserG")
public Boolean updateUserG(@RequestBody User user){
// TODO 更新数据库
userCache.put(user.getId(), user);
return true;
}
@GetMapping("deleteUserG")
public Boolean deleteUserG(Long id){
// TODO 从数据库删除
userCache.remove(id);
return true;
}//*/
获取数据访问:http://localhost:8001/java/getG?id=32111
删除数据访问:http://localhost:8001/java/deleteUserG?id=32111
这里也一样,我在开发中通常不使用修改缓存的方法。更新完成数据数据之后,直接删除缓存即可。
还有一种用法是通过@CreateCache,注:在jetcache 2.7 版本CreateCache注解已废弃,不推荐使用,我这里就不做测试了。
以上大概就是Springboot集成jetcache缓存框架的基本使用。
有好的建议,请在下方输入你的评论。