SpringBoot 缓存框架

在高性能要求下,必须引入缓存框架。本文介绍 Spring Boot 项目中的三种缓存技术方案,包括 Spring Cache、阿里 JetCache、Layering-Cache 多级缓存框架。希望在本文的基础上,开发者可以对缓存技术有一定的了解,对项目开发有有一些帮助。

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

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

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

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

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

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

本节介绍三种缓存技术:Spring CacheLayering 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 依赖:

    org.springframework.boot    spring-boot-starter-web    org.springframework.boot    spring-boot-starter-data-redis

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

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

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

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

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

@Servicepublic 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 findAll() {        return userDAO.findAll();    }}

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

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

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

@Configurationpublic class RedisConfig extends CachingConfigurerSupport {    @Bean    public RedisCacheConfiguration redisCacheConfiguration(){        Jackson2JsonRedisSerializer 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/文档

引入依赖:

          com.github.xiaolyuh         layering-cache-starter         2.0.7 

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

然后需要配置一下 RedisTemplate:

@EnableCaching@Configurationpublic class RedisConfig extends CachingConfigurerSupport {    @Bean    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {        return createRedisTemplate(redisConnectionFactory);    }    public RedisTemplate createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {        RedisTemplate 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 的默认注解。

@Servicepublic 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 findAll() {        return userDAO.findAll();    }}

三、Alibaba JetCache 框架

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

JetCache 是一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。 JetCache 提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了Cache接口用于手工缓存操作。 当前有四个实现,RedisCacheTairCache(此部分未在 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 项目中,引入如下依赖:

    com.alicp.jetcache    jetcache-starter-redis    2.5.14

配置:

server:  port: 8083  servlet:    context-path: /apijetcache:  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: 1redis:  host: 127.0.0.1  port: 6379

Application.class

@EnableMethodCache(basePackages = "com.example.springcachealibaba")@EnableCreateCacheAnnotation@SpringBootApplicationpublic class SpringCacheAlibabaApplication {    public static void main(String[] args) {        SpringApplication.run(SpringCacheAlibabaApplication.class, args);    }}

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

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


3.1 通过 @CreateCache 创建 Cache 实例

@Servicepublic class UserServiceImpl implements UserService {    // 下面的示例为使用 @CreateCache 注解创建 Cache 对象来缓存数据的示例    @CreateCache(name = "user:", expire = 5, timeUnit = TimeUnit.MINUTES)    private Cache 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 findAll() {        return userDAO.findAll();    }}

3.2 通过注解实现方法缓存

@Servicepublic 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 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 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 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 {    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}

然后定义一个配置类:

@Configurationpublic 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:

@Configurationpublic class JetCacheConfig {    @Bean    public SpringConfigProvider springConfigProvider() {        return new SpringConfigProvider() {            @Override            public Function parseValueDecoder(String valueDecoder) {                if (valueDecoder.equalsIgnoreCase("myJson")) {                    return new JsonSerialPolicy().decoder();                }                return super.parseValueDecoder(valueDecoder);            }            @Override            public Function parseValueEncoder(String valueEncoder) {                if (valueEncoder.equalsIgnoreCase("myJson")) {                    return new JsonSerialPolicy().encoder();                }                return super.parseValueEncoder(valueEncoder);            }        };    }}

这里使用了类型 myJson 作为新序列化类型的名称,这样我们就可以在配置文件的 jetcache.xxx.valueEncoderjetcache.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

阅读全文: http://gitbook.cn/gitchat/activity/5eb631ebe1b5494e0553df3c

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

你可能感兴趣的:(SpringBoot 缓存框架)