Springboot(四十九)SpringBoot3整合jetcache缓存

1,305 阅读7分钟

上文中我们学习了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等。

 

放一下官方文档:

github.com/alibaba/jet…

官方文档中有部分配置是有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。官方文档如下图所示:

1.png.jpg

 

这里得配置写法要参考一下官方文档中内容。

 

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是哨兵集群。官方文档给出的哨兵集群链接写法是:

#uriredis-sentinel://127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381/?sentinelMasterId=mymaster
      #readFromslavePreferred

但是呢,是不好用的,连不上,下边是我尝试好用的链接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.statIntervalMinutes0统计间隔,0表示不统计
jetcache.areaInCacheNametrue(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}.keyConvertorfastjson2key转换器的全局配置,2.6.5+已经支持的keyConvertor:fastjson2/jackson;   2.6.5-只有一个已经实现的keyConvertor:fastjson。仅当使用@CreateCache且缓存类型为LOCAL时可以指定为none,此时通过equals方法来识别key。方法缓存必须指定keyConvertor
jetcache.[local/remote].${area}.valueEncoderjava序列化器的全局配置。仅remote类型的缓存需要指定,2.7+可选java/kryo/kryo5;2.6-可选java/kryo
jetcache.[local/remote].${area}.valueDecoderjava序列化器的全局配置。仅remote类型的缓存需要指定,2.7+可选java/kryo/kryo5;2.6-可选java/kryo
jetcache.[local/remote].${area}.limit100每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定。注意是每个缓存实例的限制,而不是全部,比如这里指定100,然后用@CreateCache创建了两个缓存实例(并且注解上没有设置localLimit属性),那么每个缓存实例的限制都是100
jetcache.[local/remote].${area}.expireAfterWriteInMillis无穷大以毫秒为单位指定超时时间的全局配置(以前为defaultExpireInMillis)
jetcache.remote.${area}.broadcastChanneljetcahe2.7的两级缓存支持更新以后失效其他JVM中的local cache,但多个服务共用redis同一个channel可能会造成广播风暴,需要在这里指定channel,你可以决定多个不同的服务是否共用同一个channel。如果没有指定则不开启。
jetcache.local.${area}.expireAfterAccessInMillis0需要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 属性说明

2.png.jpg

如果使用二级缓存(本地+远程),需要设置localExpire(本地缓存过期时间)这个时间通常小于远程缓存过期时间。


@CacheRefresh 属性说明

3.jpg

@CacheInvalidate 属性说明

4.png.jpg

@CacheUpdate 属性说明

5.png.jpg

@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缓存框架的基本使用。

 

有好的建议,请在下方输入你的评论。