详解SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)

引言

​前两天在写一个实时数据处理的项目,项目要求是 1s 要处理掉 1k 的数据,这时候显然光靠查数据库是不行的,技术选型的时候老大跟我提了一下使用 Layering-Cache 这个开源项目来做缓存框架。

​之间问了一下身边的小伙伴,似乎对这块了解不多。一般也就用用 Redis 来缓存,应该是很少用多级缓存框架来专门性的管理缓存吧。

​趁着这个机会,我多了解了一些关于 SpringBoot 中缓存的相关技术,于是有了这篇文章!

在项目性能需求比较高时,就不能单单依赖数据库访问来获取数据了,必须引入缓存技术。

常用的有本地缓存、Redis 缓存。

  • 本地缓存:也就是内存,速度快,缺点是不能持久化,一旦项目关闭,数据就会丢失。而且不能满足分布式系统的应用场景(比如数据不一致的问题)。
  • Redis 缓存:也就是利用数据库等,最常见的就是 Redis。Redis 的访问速度同样很快,可以设置过期时间、设置持久化方法。缺点是会受到网络和并发访问的影响。

本节介绍三种缓存技术:Spring Cache、Layering Cache 框架、Alibaba JetCache 框架。示例使用的 SpringBoot 版本是 2.1.3.RELEASE。非 SpringBoot 项目请参考文章中给出的文档地址。

项目源码地址:https://github.com/laolunsi/spring-boot-examples

一、Spring Cache

Spring Cache 是 Spring 自带的缓存方案,使用简单,既可以使用本地缓存,也可以使用 Redis

CacheType 包括:

GENERIC, JCACHE, EHCACHE, HAZELCAST, INFINISPAN, COUCHBASE, REDIS, CAFFEINE, SIMPLE, NONE

Spring Cache 的使用很简单,引入 即可,我这里使用创建的是一个 web 项目,引入的 `spring-boot-starter-web` 包含了 。

这里利用 Redis 做缓存,再引入 spring-boot-starter-data-redis 依赖:

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

<!--Redis-->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在配置类 or Application 类上添加 @EnableCaching 注解以启动缓存功能。

配置文件很简洁(功能也比较少):

server:
 port: 8081
 servlet:
 context-path: /api
spring:
 cache:
 type: redis
 redis:
 host: 127.0.0.1
 port: 6379
 database: 1

下面我们编写一个对 User 进行增删改查的 Controller,实现对 User 的 save/delete/findAll 三个操作。为演示方便,DAO 层不接入数据库,而是使用 HashMap 来直接模拟数据库操作。

我们直接看 service 层的接口实现:

@Service
public class UserServiceImpl implements UserService {

 @Autowired
 private UserDAO userDAO;

 @Override
 @Cacheable(value = "user", key = "#userId")
 public User findById(Integer userId) {
  return userDAO.findById(userId);
 }

 @Override
 @CachePut(value = "user", key = "#user.id", condition = "#user.id != null")
 public User save(User user) {
  user.setUpdateTime(new Date());
  userDAO.save(user);
  return userDAO.findById(user.getId());
 }

 @Override
 @CacheEvict(value = "user", key = "#userId")
 public boolean deleteById(Integer userId) {
  return userDAO.deleteById(userId);
 }

 @Override
 public List<User> findAll() {
  return userDAO.findAll();
 }
}

我们可以看到使用了 @Cacheable、@CachePut、@CacheEvict 注解。

  • Cacheable:启用缓存,首先从缓存中查找数据,如果存在,则从缓存读取数据;如果不存在,则执行方法,并将方法返回值添加到缓存
  • @CachePut:更新缓存,如果 condition 计算结果为 true,则将方法返回值添加到缓存中
  • @CacheEvict:删除缓存,根据 value 与 key 字段计算缓存地址,将缓存数据删除

测试发现默认的对象存到 Redis 后是 binary 类型,我们可以通过修改 RedisCacheConfiguration 中的序列化规则去调整。比如:

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

 @Bean
 public RedisCacheConfiguration redisCacheConfiguration(){
  Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
  RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
  configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofDays(30));
  return configuration;
 }
}

Spring Cache 的功能比较单一,例如不能实现缓存刷新、二级缓存等功能。下面介绍一个开源项目:Layering-Cache,该项目实现了缓存刷新、二级缓存(一级内存、二级 Redis)。同时较容易扩展实现为自己的缓存框架。

二、Layering Cache 框架

文档:https://github.com/xiaolyuh/layering-cache/wiki/文档

引入依赖:

 <dependency>
   <groupId>com.github.xiaolyuh</groupId>
   <artifactId>layering-cache-starter</artifactId>
   <version>2.0.7</version>
 </dependency>

配置文件不需要做什么修改。启动类依然加上 @EnableCaching 注解。

然后需要配置一下 RedisTemplate:

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

 @Bean
 public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  return createRedisTemplate(redisConnectionFactory);
 }

 public RedisTemplate createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
  RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  redisTemplate.setConnectionFactory(redisConnectionFactory);

  // 使用Jackson2JsonRedisSerialize 替换默认序列化
  Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

  jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

  // 设置value的序列化规则和 key的序列化规则
  redisTemplate.setKeySerializer(new StringRedisSerializer());
  redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  //Map
  redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
  redisTemplate.afterPropertiesSet();
  return redisTemplate;
 }

}

下面我们使用 layering 包中的 @Cacheable @CachePut @CatchEvict 三个注解来替换 Spring Cache 的默认注解。

@Service
public class UserServiceImpl implements UserService {

 @Autowired
 private UserDAO userDAO;

 @Override
 //@Cacheable(value = "user", key = "#userId")
 @Cacheable(value = "user", key = "#userId",
  firstCache = @FirstCache(expireTime = 5, timeUnit = TimeUnit.MINUTES),
  secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, isAllowNullValue = true, timeUnit = TimeUnit.MINUTES))
 public User findById(Integer userId) {
  return userDAO.findById(userId);
 }

 @Override
 //@CachePut(value = "user", key = "#user.id", condition = "#user.id != null")
 @CachePut(value = "user", key = "#user.id",
   firstCache = @FirstCache(expireTime = 5, timeUnit = TimeUnit.MINUTES),
   secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, isAllowNullValue = true, timeUnit = TimeUnit.MINUTES))
 public User save(User user) {
  user.setUpdateTime(new Date());
  userDAO.save(user);
  return userDAO.findById(user.getId());
 }

 @Override
 //@CacheEvict(value = "user", key = "#userId")
 @CacheEvict(value = "user", key = "#userId")
 public boolean deleteById(Integer userId) {
  return userDAO.deleteById(userId);
 }

 @Override
 public List<User> findAll() {
  return userDAO.findAll();
 }
}

三、Alibaba JetCache 框架

文档:https://github.com/alibaba/jetcache/wiki/Home_CN

JetCache是一个基于Java的缓存系统封装,提供统一的API和注解来简化缓存的使用。 JetCache提供了比SpringCache更加强大的注解,可以原生的支持TTL、两级缓存、分布式自动刷新,还提供了Cache接口用于手工缓存操作。 当前有四个实现,RedisCache、TairCache(此部分未在github开源)、CaffeineCache(in memory)和一个简易的LinkedHashMapCache(in memory),要添加新的实现也是非常简单的。

全部特性:

  • 通过统一的API访问Cache系统
  • 通过注解实现声明式的方法缓存,支持TTL和两级缓存
  • 通过注解创建并配置Cache实例
  • 针对所有Cache实例和方法缓存的自动统计
  • Key的生成策略和Value的序列化策略是可以配置的
  • 分布式缓存自动刷新,分布式锁 (2.2+)
  • 异步Cache API (2.2+,使用Redis的lettuce客户端时)
  • Spring Boot支持

SpringBoot 项目中,引入如下依赖:

<dependency>
 <groupId>com.alicp.jetcache</groupId>
 <artifactId>jetcache-starter-redis</artifactId>
 <version>2.5.14</version>
</dependency>

配置:

server:
 port: 8083
 servlet:
 context-path: /api

jetcache:
 statIntervalMinutes: 15
 areaInCacheName: false
 local:
 default:
  type: caffeine
  keyConvertor: fastjson
 remote:
 default:
  expireAfterWriteInMillis: 86400000 # 全局,默认超时时间,单位毫秒,这里设置了 24 小时
  type: redis
  keyConvertor: fastjson
  valueEncoder: java #jsonValueEncoder #java
  valueDecoder: java #jsonValueDecoder
  poolConfig:
  minIdle: 5
  maxIdle: 20
  maxTotal: 50
  host: ${redis.host}
  port: ${redis.port}
  database: 1

redis:
 host: 127.0.0.1
 port: 6379

Application.class

@EnableMethodCache(basePackages = "com.example.springcachealibaba")
@EnableCreateCacheAnnotation
@SpringBootApplication
public class SpringCacheAlibabaApplication {

 public static void main(String[] args) {
  SpringApplication.run(SpringCacheAlibabaApplication.class, args);
 }

}

字如其意,@EnableMethodCache 用于注解开启方法上的缓存功能,@EnableCreateCacheAnnotation 用于注解开启 @CreateCache 来引入 Cache Bean 的功能。两套可以同时启用。

这里以上面对 User 的增删改查功能为例:

3.1 通过 @CreateCache 创建 Cache 实例

@Service
public class UserServiceImpl implements UserService {

 // 下面的示例为使用 @CreateCache 注解创建 Cache 对象来缓存数据的示例

 @CreateCache(name = "user:", expire = 5, timeUnit = TimeUnit.MINUTES)
 private Cache<Integer, User> userCache;

 @Autowired
 private UserDAO userDAO;

 @Override
 public User findById(Integer userId) {
  User user = userCache.get(userId);
  if (user == null || user.getId() == null) {
   user = userDAO.findById(userId);
  }
  return user;
 }

 @Override
 public User save(User user) {
  user.setUpdateTime(new Date());
  userDAO.save(user);
  user = userDAO.findById(user.getId());

  // cache
  userCache.put(user.getId(), user);
  return user;
 }

 @Override
 public boolean deleteById(Integer userId) {
  userCache.remove(userId);
  return userDAO.deleteById(userId);
 }

 @Override
 public List<User> findAll() {
  return userDAO.findAll();
 }
}

3.2 通过注解实现方法缓存

@Service
public class UserServiceImpl implements UserService {

 // 下面为使用 AOP 来缓存数据的示例

 @Autowired
 private UserDAO userDAO;

 @Autowired
 private UserService userService;

 @Override
 @Cached(name = "user:", key = "#userId", expire = 1000)
 //@Cached( name = "user:", key = "#userId", serialPolicy = "bean:jsonPolicy")
 public User findById(Integer userId) {
  System.out.println("userId: " + userId);
  return userDAO.findById(userId);
 }

 @Override
 @CacheUpdate(name = "user:", key = "#user.id", value = "#user")
 public User save(User user) {
  user.setUpdateTime(new Date());
  boolean res = userDAO.save(user);
  if (res) {
   return userService.findById(user.getId());
  }
  return null;
 }

 @Override
 @CacheInvalidate(name = "user:", key = "#userId")
 public boolean deleteById(Integer userId) {
  return userDAO.deleteById(userId);
 }

 @Override
 public List<User> findAll() {
  return userDAO.findAll();
 }
}

这里用到了三个注解:@Cached/@CacheUpdate/@CacheInvalidate,分别对应着 Spring Cache 中的 @Cacheable/@CachePut/@CacheEvict

具体含义可以参考:https://github.com/alibaba/jetcache/wiki/MethodCache_CN

3.3 自定义序列化器

默认的 value 存储格式是 binary 的,JetCache 提供的 Redis key 和 value 的序列化器仅有 java 和 kryo 两种。可以通过自定义序列化器来实现自己想要的序列化方式,比如 json。

JetCache 开发者提出:

jetcache老版本中是有三个序列化器的:java、kryo、fastjson。 但是fastjson做序列化兼容性不是特别好,并且某次升级以后单元测试就无法通过了,怕大家用了以后觉得有坑,就把它废弃了。 现在默认的序列化器是性能最差,但是兼容性最好,大家也最熟悉的java序列化器。

参考原仓库中 FAQ 中的建议,可以通过两种方式来定义自己的序列化器。

3.3.1 实现 SerialPolicy 接口

第一种方式是定义一个 SerialPolicy 的实现类,然后将其注册成一个 bean,然后在 @Cached 中的 serialPolicy 属性中指明 bean:name

比如:

import com.alibaba.fastjson.JSONObject;
import com.alicp.jetcache.CacheValueHolder;
import com.alicp.jetcache.anno.SerialPolicy;

import java.util.function.Function;

public class JsonSerialPolicy implements SerialPolicy {

 @Override
 public Function<Object, byte[]> encoder() {
  return o -> {
   if (o != null) {
    CacheValueHolder cacheValueHolder = (CacheValueHolder) o;
    Object realObj = cacheValueHolder.getValue();
    String objClassName = realObj.getClass().getName();
    // 为防止出现 Value 无法强转成指定类型对象的异常,这里生成一个 JsonCacheObject 对象,保存目标对象的类型(比如 User)
    JsonCacheObject jsonCacheObject = new JsonCacheObject(objClassName, realObj);
    cacheValueHolder.setValue(jsonCacheObject);
    return JSONObject.toJSONString(cacheValueHolder).getBytes();
   }
   return new byte[0];
  };
 }

 @Override
 public Function<byte[], Object> decoder() {
  return bytes -> {
   if (bytes != null) {
    String str = new String(bytes);
    CacheValueHolder cacheValueHolder = JSONObject.parseObject(str, CacheValueHolder.class);
    JSONObject jsonObject = JSONObject.parseObject(str);
    // 首先要解析出 JsonCacheObject,然后获取到其中的 realObj 及其类型
    JSONObject jsonOfMy = jsonObject.getJSONObject("value");
    if (jsonOfMy != null) {
     JSONObject realObjOfJson = jsonOfMy.getJSONObject("realObj");
     String className = jsonOfMy.getString("className");
     try {
      Object realObj = realObjOfJson.toJavaObject(Class.forName(className));
      cacheValueHolder.setValue(realObj);
     } catch (ClassNotFoundException e) {
      e.printStackTrace();
     }

    }
    return cacheValueHolder;
   }
   return null;
  };
 }
}

注意,在 JetCache 的源码中,我们看到实际被缓存的对象的 CacheValueHolder,这个对象包括了一个泛型字段 V,这个 V 就是实际被缓存的数据。为了将 JSON 字符串和 CacheValueHolder(包括了泛型字段 V )进行互相转换,我在转换过程中使用 CacheValueHolder 和一个自定义的 JsonCacheObject 类,其代码如下:

public class JsonCacheObject<V> {

 private String className;
 private V realObj;

 public JsonCacheObject() {
 }

 public JsonCacheObject(String className, V realObj) {
  this.className = className;
  this.realObj = realObj;
 }

 // ignore get and set methods
}

然后定义一个配置类:

@Configuration
public class JetCacheConfig {
 @Bean(name = "jsonPolicy")
 public JsonSerializerPolicy jsonSerializerPolicy() {
  return new JsonSerializerPolicy();
 }
}

使用很简单,比如:

@Cached( name = "user:", key = "#userId", serialPolicy = "bean:jsonPolicy")

这种序列化方法是局部的,只能对单个缓存生效。

下面介绍如何全局序列化方法。

3.3.2 全局配置 SpringConfigProvider

JetCache 默认提供了两种序列化规则:KRYO 和 JAVA (不区分大小写)。

这里在上面的 JSONSerialPolicy 的基础上,定义一个新的 SpringConfigProvider:

@Configuration
public class JetCacheConfig {

 @Bean
 public SpringConfigProvider springConfigProvider() {
  return new SpringConfigProvider() {
   @Override
   public Function<byte[], Object> parseValueDecoder(String valueDecoder) {
    if (valueDecoder.equalsIgnoreCase("myJson")) {
     return new JsonSerialPolicy().decoder();
    }
    return super.parseValueDecoder(valueDecoder);
   }

   @Override
   public Function<Object, byte[]> parseValueEncoder(String valueEncoder) {
    if (valueEncoder.equalsIgnoreCase("myJson")) {
     return new JsonSerialPolicy().encoder();
    }
    return super.parseValueEncoder(valueEncoder);
   }
  };
 }
}

这里使用了类型 myJson 作为新序列化类型的名称,这样我们就可以在配置文件的 jetcache.xxx.valueEncoder jetcache.xxx.valueDecoder 这两个配置项上设置值 myJson/java/kryo 三者之一了。

关于 Java 中缓存框架的知识就介绍到这里了,还有一些更加深入的知识,比如:如何保证分布式环境中缓存数据的一致性、缓存数据的刷新、多级缓存时定制化缓存策略等等。这些都留待以后再学习和介绍吧!

参考资料:

Spring Cache: https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
Caffeine 缓存: https://www.jianshu.com/p/9a80c662dac4
Layering-Cache:https://github.com/xiaolyuh/layering-cache
Alibaba JetCache: https://github.com/alibaba/jetcache
JetCache FAQ: https://github.com/alibaba/jetcache/wiki/FAQ_CN

以上就是详解SpringBoot的三种缓存技术的详细内容,更多关于SpringBoot 缓存技术的资料请关注我们其它相关文章!

(0)

相关推荐

  • Springboot如何设置静态资源缓存一年

    这篇文章主要介绍了Springboot如何设置静态资源缓存一年,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 由于本人所在项目组,所用的项目是前后端分离的,前端是React 的SPA,每次打包都会新版本的静态文件. 然而,在有些时候,这些静态资源是不变的,故可以将资源缓存至用户本地,提升性能. 写法如下,需要继承WebMvcConfigurerAdapter类,并重写addResourceHandlers方法.就可以告诉浏览器强制缓存 pack

  • SpringBoot2.3整合redis缓存自定义序列化的实现

    1.引言 我们使用redis作为缓存中间件时,当我们第一次查询数据的时候,是去数据库查询,然后查到的数据封装到实体类中,实体类会被序列化存入缓存中,当第二次查数据时,会直接去缓存中查找被序列化的数据,然后反序列化被我们获取.我们在缓存中看到的序列化数据不直观,如果想看到类似json的数据格式,就需要自定义序列化规则. 2.整合redis pom.xml: <!--引入redis--> <dependency> <groupId>org.springframework.d

  • SpringBoot下Mybatis的缓存的实现步骤

    说起 mybatis,作为 Java 程序员应该是无人不知,它是常用的数据库访问框架.与 Spring 和 Struts 组成了 Java Web 开发的三剑客--- SSM.当然随着 Spring Boot 的发展,现在越来越多的企业采用的是 SpringBoot + mybatis 的模式开发,我们公司也不例外.而 mybatis 对于我也仅仅停留在会用而已,没想过怎么去了解它,更不知道它的缓存机制了,直到那个生死难忘的 BUG.故事的背景比较长,但并不是啰嗦,只是让读者知道这个 BUG 触

  • SpringBoot中Shiro缓存使用Redis、Ehcache的方法

    SpringBoot 中配置redis作为session 缓存器. 让shiro引用 本文是建立在你是使用这shiro基础之上的补充内容 第一种:Redis缓存,将数据存储到redis 并且开启session存入redis中. 引入pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifac

  • springboot的缓存技术的实现

    引子 我门知道一个程序的瓶颈在于数据库,我门也知道内存的速度是大大快于硬盘的速度的.当我门需要重复的获取相同的数据的时候,我门一次又一次的请求数据库或者远程服务,导致大量的时间耗费在数据库查询或者远程方法的调用上,导致程序性能的恶化,这更是数据缓存要解决的问题. spring 缓存支持 spring定义了 org.springframework.cache.CacheManager和org.springframework.cache.Cache接口来统一不同的缓存技术.其中,CacheManag

  • 详解SpringBoot集成Redis来实现缓存技术方案

    概述 在我们的日常项目开发过程中缓存是无处不在的,因为它可以极大的提高系统的访问速度,关于缓存的框架也种类繁多,今天主要介绍的是使用现在非常流行的NoSQL数据库(Redis)来实现我们的缓存需求. Redis简介 Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件,Redis 的优势包括它的速度.支持丰富的数据类型.操作原子性,以及它的通用性. 案例整合 本案例是在之前一篇SpringBoot + Mybatis + RESTful的基础上来集

  • SpringBoot加入Guava Cache实现本地缓存代码实例

    这篇文章主要介绍了SpringBoot加入Guava Cache实现本地缓存代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在pom.xml中加入guava依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version>

  • SpringBoot中默认缓存实现方案的示例代码

    在上一节中,我带大家学习了在Spring Boot中对缓存的实现方案,尤其是结合Spring Cache的注解的实现方案,接下来在本章节中,我带大家通过代码来实现. 一. Spring Boot实现默认缓存 1. 创建web项目 我们按照之前的经验,创建一个web程序,并将之改造成Spring Boot项目,具体过程略. 2. 添加依赖包 <dependency> <groupId>org.springframework.boot</groupId> <artif

  • SpringBoot redis分布式缓存实现过程解析

    前言 应用系统需要通过Cache来缓存不经常改变得数据来提高系统性能和增加系统吞吐量,避免直接访问数据库等低速存储系统.缓存的数据通常存放在访问速度更快的内存里或者是低延迟存取的存储器,服务器上.应用系统缓存,通常有如下作用:缓存web系统的输出,如伪静态页面.缓存系统的不经常改变的业务数据,如用户权限,字典数据.配置信息等 大家都知道springBoot项目都是微服务部署,A服务和B服务分开部署,那么它们如何更新或者获取共有模块的缓存数据,或者给A服务做分布式集群负载,如何确保A服务的所有集群

  • SpringBoot2整合Redis缓存三步骤代码详解

    遵循SpringBoot三板斧 第一步加依赖 <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- redis依赖commons-pool 这个依赖一定要添加 --> <

随机推荐