13

SpringBoot 缓存实战

 3 years ago
source link: http://www.eknown.cn/index.php/spring-boot/cache.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

引言

​ 前两天在写一个实时数据处理的项目,项目要求是 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 依赖:

<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 接口用于手工缓存操作。 当前有四个实现, RedisCacheTairCache (此部分未在github开源)、 CaffeineCache (in memory)和一个简易的 LinkedHashMapCache (in memory),要添加新的实现也是非常简单的。

全部特性:

Cache
Cache

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.valueEncoderjetcache.xxx.valueDecoder 这两个配置项上设置值 myJson/java/kryo 三者之一了。

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

参考资料:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK