введение
Два дня назад писал проект по обработке данных в реальном времени.Требование к проекту -обработка 1к данных за 1с.На данный момент очевидно что недостаточно проверить базу.При выборе технологии Босс упомянул мне, чтобы использовать Layering-Cache, Этот проект с открытым исходным кодом используется в качестве основы для кэширования.
Я расспросила друзей вокруг меня, и кажется, что они мало что знают об этом. Как правило, Redis используется для кэширования, и использование многоуровневой инфраструктуры кэширования для специального управления кэшами должно быть редкостью.
Пользуясь случаем, я узнал больше о сопутствующих технологиях кэширования в SpringBoot, поэтому у меня есть эта статья!
Когда требования к производительности проекта относительно высоки, невозможно полагаться только на доступ к базе данных для получения данных, и необходимо внедрять технологию кэширования.
Обычно используются локальный кеш, кеш Redis.
- Локальный кеш: то есть оперативная память. Недостаток в том, что ее нельзя сохранить. После закрытия проекта данные будут потеряны. И он не может соответствовать сценариям применения распределенных систем (например, несогласованность данных).
- Кэш Redis: то есть использование баз данных и т. д., наиболее распространенным является Redis. Скорость доступа к Redis также высока, и вы можете установить время истечения срока действия и установить метод сохранения. Недостатком является то, что на него будет влиять сеть и одновременный доступ.
В этом разделе описываются три метода кэширования:Spring Cache,Layering CacheРамка,Alibaba JetCacheРамка. В примере используется версия SpringBoot 2.1.3.RELEASE. Для проектов, отличных от SpringBoot, см. адрес документа, указанный в статье.
Адрес исходного кода проекта:GitHub.com/lawrence/tickets…
1. Весенний кэш
Spring Cache — это собственное решение Spring для кэширования. Оно простое в использовании. Вы можете использовать как локальный кеш, так и Redis.
Тип кэша включает:
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>
Добавьте аннотацию @EnableCaching в класс конфигурации или класс приложения, чтобы включить кэширование.
Файл конфигурации очень прост (и менее функционален):
server:
port: 8081
servlet:
context-path: /api
spring:
cache:
type: redis
redis:
host: 127.0.0.1
port: 6379
database: 1
Затем мы пишем контроллер, который добавляет, удаляет, изменяет и проверяет пользователя, а также реализует три операции сохранения/удаления/findAll для пользователя. Для удобства демонстрации уровень DAO не обращается к базе данных, а использует HashMap для прямого моделирования операций с базой данных.
Давайте посмотрим непосредственно на реализацию интерфейса сервисного уровня:
@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: обновите кеш и добавьте возвращаемое значение метода в кеш, если условие оценивается как истинное.
- @CacheEvict: удалить кеш, вычислить адрес кеша в соответствии со значением и ключевыми полями и удалить данные кеша.
Тест показал, что объект по умолчанию, хранящийся в Redis, имеет двоичный тип, мы можем настроить его, изменив правила сериализации в 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 второго уровня). В то же время проще расширить и внедрить собственный фреймворк кэширования.
2. Структура многоуровневого кэша
Документация:GitHub.com/обходные пути/pull…
Импорт зависимостей:
<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;
}
}
Ниже мы используем три аннотации @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();
}
}
3. Фреймворк Alibaba JetCache
Документация:GitHub.com/alibaba/jet…
JetCache — это пакет системы кэширования на основе Java, который предоставляет унифицированный API и аннотации для упрощения использования кэшей. JetCache предоставляет более мощные аннотации, чем SpringCache, который может изначально поддерживать TTL, двухуровневое кэширование, распределенное автоматическое обновление, а также обеспечивает
Cache
Интерфейс используется для ручных операций кэширования. В настоящее время существует четыре реализации,RedisCache
,TairCache
(Эта часть не является открытым исходным кодом на github),CaffeineCache
(в памяти) и простойLinkedHashMapCache
(в памяти), также очень просто добавлять новые реализации.Все функции:
- Доступ к системе Cache через единый API
- Реализовать декларативное кэширование методов через аннотации, поддерживать TTL и двухуровневое кэширование
- Создание и настройка с помощью аннотаций
Cache
пример- для всех
Cache
Автоматическая статистика для кэшей экземпляров и методов- Стратегия генерации ключей и стратегия сериализации значений настраиваются.
- Автоматическое обновление распределенного кеша, распределенная блокировка (2.2+)
- API асинхронного кэша (2.2+, при использовании клиента салата Redis)
- Поддержка весенней загрузки
В проекте 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
Используется для аннотации функции кеша в методе open,@EnableCreateCacheAnnotation
Открыт для аннотации@CreateCache
Познакомить с функцией Cache Bean. Оба набора можно активировать одновременно.
Вот пример добавления, удаления, изменения и проверки функций для пользователя:
3.1 Создайте экземпляр Cache через @CreateCache
@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, которые соответствуют @Cacheable/@CachePut/@CacheEvict в Spring Cache соответственно.
Конкретное значение может относиться к:GitHub.com/alibaba/jet…
3.3 Пользовательские сериализаторы
Формат хранения значений по умолчанию — двоичный, а сериализаторы ключей и значений Redis, предоставляемые JetCache, — это только java и kryo. Вы можете реализовать нужный метод сериализации с помощью специального сериализатора, такого как json.
Разработчики JetCache предлагают:
В старой версии jetcache есть три сериализатора: java, kryo, fastjson. Однако совместимость сериализации у fastjson не особо хорошая, и юнит-тест не пройдет после определенного апгрейда, боюсь, что после его использования все почувствуют, что есть ямки, поэтому от них откажутся. Теперь сериализатор по умолчанию — это сериализатор java с худшей производительностью, но лучшей совместимостью и наиболее знакомым.
Ссылаясь на предложения в FAQ в исходном репозитории, есть два способа определить свой собственный сериализатор.
3.3.1 Реализация интерфейса SerialPolicy
Первый способ — определить класс реализации SerialPolicy, затем зарегистрировать его как bean-компонент, а затем указать его в атрибуте serialPolicy в @Cached.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, представляющее собой фактические кэшированные данные. Чтобы преобразовать строку 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: docs.spring.IO/весна/документы…
-
Кэш кофеина:у-у-у. Краткое описание.com/afraid/9 ах 80 из 662-х…
-
Многоуровневый кэш:GitHub.com/обходные пути/pull…
-
Alibaba JetCache: GitHub.com/alibaba/jet…
-
JetCache FAQ: GitHub.com/alibaba/jet…