Технология кэширования SpringBoot в действии

Spring Boot

введение

Два дня назад писал проект по обработке данных в реальном времени.Требование к проекту -обработка 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, и есть более глубокие знания, такие как: как обеспечить согласованность кэшированных данных в распределенной среде, обновление кэшированных данных, настраиваемая стратегия кэширования в многоуровневом кеш и т.п. Их осталось изучить и представить позже!


Использованная литература: