Alibaba 開(kāi)源通用緩存訪問(wèn)框架:JetCache

Introduction

JetCache 是由阿里巴巴開(kāi)源的一款通用緩存訪問(wèn)框架。上篇文章介紹過(guò)了 Spring Cache 的基本使用,下面我們?cè)賮?lái)了解下這款更好用的 JetCache。

引用下官方文檔說(shuō)明,JetCache 提供的核心能力包括:

  • 提供統(tǒng)一的,類似jsr-107風(fēng)格的API訪問(wèn)Cache,并可通過(guò)注解創(chuàng)建并配置Cache實(shí)例
  • 通過(guò)注解實(shí)現(xiàn)聲明式的方法緩存,支持TTL和兩級(jí)緩存
  • 分布式緩存自動(dòng)刷新,分布式鎖 (2.2+)
  • 支持異步Cache API
  • Spring Boot支持
  • Key的生成策略和Value的序列化策略是可以定制的
  • 針對(duì)所有Cache實(shí)例和方法緩存的自動(dòng)統(tǒng)計(jì)

目前支持的緩存系統(tǒng)包括以下4個(gè):

  • Caffeine(基于本地內(nèi)存)
  • LinkedHashMap(基于本地內(nèi)存,JetCache自己實(shí)現(xiàn)的簡(jiǎn)易LRU緩存)
  • Alibaba Tair(相關(guān)實(shí)現(xiàn)未在Github開(kāi)源,在阿里內(nèi)部Gitlab上可以找到)
  • Redis

來(lái)看個(gè)簡(jiǎn)單的使用示例:

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 很像,不過(guò)這里原生支持了細(xì)粒度的 TTL(超時(shí)時(shí)間,而 Spring Cache Redis 默認(rèn)只支持配置全局的),CacheType 還有 LOCAL/REMOTE/BOTH 三種選擇, 分別代表本地內(nèi)存/遠(yuǎn)程 Cache Server(如Redis)/兩級(jí)緩存。下面,結(jié)合 Redis 的使用,來(lái)看看更復(fù)雜的一些使用場(chǎng)景:

Example

首先,使用 JetCache 的環(huán)境需求包括如下:

  • 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等表達(dá)式中使用參數(shù)名以后緩存沒(méi)有生效?

  1. 全局配置
spring:
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create
    # 開(kāi)啟 SQL 輸出,方便查看結(jié)果是否走了緩存
    show-sql: true

# @see com.alicp.jetcache.autoconfigure.JetCacheProperties
jetcache:
  # 統(tǒng)計(jì)間隔,默認(rèn)0:表示不統(tǒng)計(jì)
  statIntervalMinutes: 1
  # areaName是否作為緩存key前綴,默認(rèn)True
  areaInCacheName: false
  local:
    default:
      # 已支持可選:linkedhashmap、caffeine
      type: linkedhashmap
      # key轉(zhuǎn)換器的全局配置,當(dāng)前只有:fastjson, @see com.alicp.jetcache.support.FastjsonKeyConvertor
      keyConvertor: fastjson
      # 每個(gè)緩存實(shí)例的最大元素的全局配置,僅local類型的緩存需要指定
      limit: 100
      # jetcache2.2以上,以毫秒為單位,指定多長(zhǎng)時(shí)間沒(méi)有訪問(wèn),就讓緩存失效,當(dāng)前只有本地緩存支持。0表示不使用這個(gè)功能
      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
      # 以毫秒為單位指定超時(shí)時(shí)間的全局配置
      expireAfterWriteInMillis: 5000
  1. 編寫實(shí)體
@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 提供了兩種實(shí)現(xiàn)緩存的方式,可以通過(guò) @CreateCache 注解創(chuàng)建 Cache 實(shí)例,這種靈活性比較高;其次是通過(guò)純注解的方式來(lái)實(shí)現(xiàn)緩存。

4.1 通過(guò) @CreateCache 注解創(chuàng)建 Cache 實(shí)例

@Slf4j
@Service
public class CoffeeCreateCacheService {

    private static final String CACHE_NAME = "CoffeeCreateCache:";

    @Resource
    private CoffeeRepository coffeeRepository;

     /**
     * 使用 @CreateCache 注解創(chuàng)建Cache實(shí)例;
     * 未定義默認(rèn)值的參數(shù),將使用yml中指定的全局配置;
     * 緩存在 Local,也可以配置成 both 開(kāi)啟兩級(jí)緩存
     */
    @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 通過(guò)注解實(shí)現(xiàn)方法緩存,主要是:@Cached(緩存新增)、@CacheUpdate(緩存更新)、@CacheInvalidate(緩存刪除),還有用于配置自動(dòng)刷新和加載保護(hù)的“大殺器”:@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 開(kāi)啟兩級(jí)緩存
     */
    @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. 簡(jiǎn)單測(cè)試
/**
 * 這里 @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());
    }
    
}

具體的可以自行運(yùn)行測(cè)試,最后來(lái)看看 Redis 的 Monitor,圖中紅框部分說(shuō)明該查詢走了緩存:

Redis Monitor

參考閱讀

最后,如果在使用中遇到問(wèn)題,在下方的參考閱讀中可以尋求答案。

JetCache wiki
JetCache introduce

示例源碼
歡迎關(guān)注我的個(gè)人公眾號(hào):超級(jí)碼里奧
如果這對(duì)您有幫助,歡迎點(diǎn)贊和分享,轉(zhuǎn)載請(qǐng)注明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容