引言
前两天在写一个实时数据处理的项目,项目要求是 1s 要处理掉 1k 的数据,这时候显然光靠查数据库是不行的,技术选型的时候老大跟我提了一下使用 Layering-Cache 这个开源项目来做缓存框架。
之间问了一下身边的小伙伴,似乎对这块了解不多。一般也就用用 Redis 来缓存,应该是很少用多级缓存框架来专门性的管理缓存吧。
趁着这个机会,我多了解了一些关于 SpringBoot 中缓存的相关技术,于是有了这篇文章!
在项目性能需求比较高时,就不能单单依赖数据库访问来获取数据了,必须引入缓存技术。
常用的有本地缓存、Redis 缓存。
本节介绍三种缓存技术: Spring Cache 、 Layering Cache 框架、 Alibaba JetCache 框架。示例使用的 SpringBoot 版本是 2.1.3.RELEASE。非 SpringBoot 项目请参考文章中给出的文档地址。
项目源码地址: https://github.com/laolunsi/spring-boot-examples
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 注解。
测试发现默认的对象存到 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)。同时较容易扩展实现为自己的缓存框架。
文档: 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(); } }
文档: https://github.com/alibaba/jetcache/wiki/Home_CN
JetCache是一个基于Java的缓存系统封装,提供统一的API和注解来简化缓存的使用。 JetCache提供了比SpringCache更加强大的注解,可以原生的支持TTL、两级缓存、分布式自动刷新,还提供了 Cache
接口用于手工缓存操作。 当前有四个实现, RedisCache
、 TairCache
(此部分未在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 的增删改查功能为例:
@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(); } }
@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
默认的 value 存储格式是 binary 的,JetCache 提供的 Redis key 和 value 的序列化器仅有 java 和 kryo 两种。可以通过自定义序列化器来实现自己想要的序列化方式,比如 json。
JetCache 开发者提出:
jetcache老版本中是有三个序列化器的:java、kryo、fastjson。 但是fastjson做序列化兼容性不是特别好,并且某次升级以后单元测试就无法通过了,怕大家用了以后觉得有坑,就把它废弃了。 现在默认的序列化器是性能最差,但是兼容性最好,大家也最熟悉的java序列化器。
参考原仓库中 FAQ 中的建议,可以通过两种方式来定义自己的序列化器。
第一种方式是定义一个 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")
这种序列化方法是局部的,只能对单个缓存生效。
下面介绍如何全局序列化方法。
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 中缓存框架的知识就介绍到这里了,还有一些更加深入的知识,比如:如何保证分布式环境中缓存数据的一致性、缓存数据的刷新、多级缓存时定制化缓存策略等等。这些都留待以后再学习和介绍吧!
参考资料: