Alibaba 开源通用缓存访问框架:JetCache

Introduction

JetCache 是由阿里巴巴开源的一款通用缓存访问框架。上篇文章介绍过了 Spring Cache 的基本使用,下面我们再来了解下这款更好用的 JetCache。

引用下官方文档说明,JetCache 提供的核心能力包括:

  • 提供统一的,类似jsr-107风格的API访问Cache,并可通过注解创建并配置Cache实例
  • 通过注解实现声明式的方法缓存,支持TTL和两级缓存
  • 分布式缓存自动刷新,分布式锁 (2.2+)
  • 支持异步Cache API
  • Spring Boot支持
  • Key的生成策略和Value的序列化策略是可以定制的
  • 针对所有Cache实例和方法缓存的自动统计

目前支持的缓存系统包括以下4个:

  • Caffeine(基于本地内存)
  • LinkedHashMap(基于本地内存,JetCache自己实现的简易LRU缓存)
  • Alibaba Tair(相关实现未在Github开源,在阿里内部Gitlab上可以找到)
  • Redis

来看个简单的使用示例:

public interface UserService {

    @Cached(name="user", key="#userId", expire = 3600, cacheType = CacheType.REMOTE)
    @CacheRefresh(refresh = 1800, stopRefreshAfterLastAccess = 3600, timeUnit = TimeUnit.SECONDS)
    @CachePenetrationProtect
    User getById(long userId);
    
}

是不是和 Spring Cache 很像,不过这里原生支持了细粒度的 TTL(超时时间,而 Spring Cache Redis 默认只支持配置全局的),CacheType 还有 LOCAL/REMOTE/BOTH 三种选择, 分别代表本地内存/远程 Cache Server(如Redis)/两级缓存。下面,结合 Redis 的使用,来看看更复杂的一些使用场景:

Example

首先,使用 JetCache 的环境需求包括如下:

  • JDK:必须 Java 8+
  • Spring Framework:v4.0.8 以上,如果不使用注解就不需要
  • Spring Boot:v1.1.9 以上(可选)
  1. 依赖部分,示例中使用的是基于 Lettuce Redis 客户端的 Spring Boot 方式的 Starter
    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-redis -->
        <dependency>
            <groupId>com.alicp.jetcache</groupId>
            <artifactId>jetcache-starter-redis-lettuce</artifactId>
            <version>2.6.0.M1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

插件这部分的配置是必需的,参考下方引用:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgument>-parameters</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </build>

@Cached的key、condition等表达式中使用参数名以后缓存没有生效?

  1. 全局配置
spring:
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create
    # 开启 SQL 输出,方便查看结果是否走了缓存
    show-sql: true

# @see com.alicp.jetcache.autoconfigure.JetCacheProperties
jetcache:
  # 统计间隔,默认0:表示不统计
  statIntervalMinutes: 1
  # areaName是否作为缓存key前缀,默认True
  areaInCacheName: false
  local:
    default:
      # 已支持可选:linkedhashmap、caffeine
      type: linkedhashmap
      # key转换器的全局配置,当前只有:fastjson, @see com.alicp.jetcache.support.FastjsonKeyConvertor
      keyConvertor: fastjson
      # 每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定
      limit: 100
      # jetcache2.2以上,以毫秒为单位,指定多长时间没有访问,就让缓存失效,当前只有本地缓存支持。0表示不使用这个功能
      expireAfterAccessInMillis: 30000
  remote:
    default:
      # 已支持可选:redis、tair
      type: redis.lettuce
      # 连接格式@see:https://github.com/lettuce-io/lettuce-core/wiki/Redis-URI-and-connection-details
      uri: redis://localhost:6379/1?timeout=5s
      keyConvertor: fastjson
      # 序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo
      valueEncoder: java
      valueDecoder: java
      # 以毫秒为单位指定超时时间的全局配置
      expireAfterWriteInMillis: 5000
  1. 编写实体
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Coffee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private Float price;

}
public interface CoffeeRepository extends JpaRepository<Coffee, Integer> {
}

JetCache 提供了两种实现缓存的方式,可以通过 @CreateCache 注解创建 Cache 实例,这种灵活性比较高;其次是通过纯注解的方式来实现缓存。

4.1 通过 @CreateCache 注解创建 Cache 实例

@Slf4j
@Service
public class CoffeeCreateCacheService {

    private static final String CACHE_NAME = "CoffeeCreateCache:";

    @Resource
    private CoffeeRepository coffeeRepository;

     /**
     * 使用 @CreateCache 注解创建Cache实例;
     * 未定义默认值的参数,将使用yml中指定的全局配置;
     * 缓存在 Local,也可以配置成 both 开启两级缓存
     */
    @CreateCache(name = CACHE_NAME, expire = 1, localLimit = 10,
            timeUnit = TimeUnit.MINUTES, cacheType = CacheType.LOCAL)
    private Cache<Integer, Coffee> coffeeCache;

    @Transactional
    public void add(Coffee coffee) {
        coffeeRepository.save(coffee);
        coffeeCache.put(coffee.getId(), coffee, 3, TimeUnit.SECONDS);
    }

    public Optional<Coffee> get(int id) {
        Coffee coffee = coffeeCache.get(id);
        log.info("CoffeeCreateCache get {} res {}", id, coffee);
        if (Objects.isNull(coffee)) {
            Optional<Coffee> coffeeOptional = coffeeRepository.findById(id);
            if (coffeeOptional.isPresent()) {
                coffee = coffeeOptional.get();
                boolean res = coffeeCache.putIfAbsent(id, coffee);
                log.info("CoffeeCreateCache putIfAbsent {} res {}", id, res);
            }
        }
        return Optional.ofNullable(coffee);
    }

    @Transactional
    public Coffee update(Coffee coffee) {
        Coffee c = coffeeRepository.save(coffee);
        coffeeCache.put(c.getId(), c, 60, TimeUnit.SECONDS);
        return c;
    }

    @Transactional
    public void delete(int id) {
        coffeeRepository.deleteById(id);
        boolean res = coffeeCache.remove(id);
        log.info("CoffeeCreateCache delete {} res {}", id, res);
    }

}

4.2 通过注解实现方法缓存,主要是:@Cached(缓存新增)、@CacheUpdate(缓存更新)、@CacheInvalidate(缓存删除),还有用于配置自动刷新和加载保护的“大杀器”:@CacheRefresh@CachePenetrationProtect

@Slf4j
@Service
public class CoffeeMethodCacheService {

    private static final String CACHE_NAME = "CoffeeMethodCache:";

    @Resource
    private CoffeeRepository coffeeRepository;

    @Transactional
    public Coffee add(Coffee coffee) {
        return coffeeRepository.save(coffee);
    }
    
    /**
     * 缓存在 Remote 的 Redis,也可以配置成 both 开启两级缓存
     */
    @Cached(name = CACHE_NAME, key = "#id", cacheType = CacheType.REMOTE, serialPolicy = SerialPolicy.KRYO,
            condition = "#id>0", postCondition = "result!=null")
    public Coffee get(int id) {
        return coffeeRepository.findById(id).orElse(null);
    }

    @CacheUpdate(name = CACHE_NAME, key = "#coffee.id", value = "result", condition = "#coffee.id!=null")
    @Transactional
    public Coffee update(Coffee coffee) {
        return coffeeRepository.save(coffee);
    }

    @CacheInvalidate(name = CACHE_NAME, key = "#id")
    @Transactional
    public void delete(int id) {
        coffeeRepository.deleteById(id);
    }

}
  1. 简单测试
/**
 * 这里 @EnableMethodCache,@EnableCreateCacheAnnotation 分别用于激活 @Cached 和 @CreateCache 注解
 */
@Slf4j
@EnableMethodCache(basePackages = "cn.mariojd.jetcache")
@EnableCreateCacheAnnotation
@SpringBootApplication
public class SpirngBootJetcacheApplication implements ApplicationRunner {

    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .sources(SpirngBootJetcacheApplication.class)
                .bannerMode(Banner.Mode.OFF)
                .web(WebApplicationType.NONE)
                .run(args);
    }

    @Resource
    private CoffeeCreateCacheService coffeeCreateCacheService;

    @Resource
    private CoffeeMethodCacheService coffeeMethodCacheService;

    @Override
    public void run(ApplicationArguments args) throws InterruptedException {
        // Test Coffee create cache

        Coffee latte = Coffee.builder().name("Latte").price(20.0f).build();
        coffeeCreateCacheService.add(latte);
        log.info("Reading from cache... {}", coffeeCreateCacheService.get(latte.getId()));
        TimeUnit.SECONDS.sleep(3);
        log.info("Cache expire... ");
        coffeeCreateCacheService.get(latte.getId());
        latte.setPrice(25.0f);
        latte = coffeeCreateCacheService.update(latte);
        coffeeCreateCacheService.delete(latte.getId());

        // Test Coffee method cache

        Coffee cappuccino = Coffee.builder().name("Cappuccino").price(30.0f).build();
        coffeeMethodCacheService.add(cappuccino);
        coffeeMethodCacheService.get(cappuccino.getId());
        log.info("Reading from cache... {}", coffeeMethodCacheService.get(cappuccino.getId()));
        cappuccino.setPrice(25.0f);
        cappuccino = coffeeMethodCacheService.update(cappuccino);
        coffeeMethodCacheService.delete(cappuccino.getId());
    }
    
}

具体的可以自行运行测试,最后来看看 Redis 的 Monitor,图中红框部分说明该查询走了缓存:

Redis Monitor

参考阅读

最后,如果在使用中遇到问题,在下方的参考阅读中可以寻求答案。

JetCache wiki
JetCache introduce

示例源码
欢迎关注我的个人公众号:超级码里奥
如果这对您有帮助,欢迎点赞和分享,转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容