SpringBoot интегрирует Redis для реализации обработки кэша (технология Spring AOP)

Spring Boot Redis

Глава 1 Анализ спроса

Планируется добавить Redis в проект Team с открытым исходным кодом для достижения обработки кеша, потому что часть бизнес-функций реализована путем написания классов инструментов Redis с последующим обращением, количество изменений велико, и развязка не может быть достигнута, поэтому я подумал об АОП среды Spring (аспектно-ориентированное программирование).
Проект с открытым исходным кодом:GitHub.com/U014427391/…
Приветственная звезда (коллекция)

Глава 2. Введение в SpringBoot

Являясь важным фреймворком с открытым исходным кодом в области фреймворка JavaEE, фреймворк Spring играет важную роль в разработке корпоративных приложений.В то же время существует множество фреймворков Spring и его подфреймворков, поэтому объем знаний очень широк. SpringBoot: подфреймворк Spring Framework, также известный как микрофреймворк, — это фреймворк, запущенный в 2014 году и упрощающий разработку Spring Framework. Изучив все знания фреймворка Spring, фреймворк Spring неизбежно требует настройки большого количества XMl, а если вы используете фреймворк SpringBoot, то можете использовать разработку аннотаций, что значительно упрощает разработку на основе фреймворка Spring. SpringBoot в полной мере использует режим конфигурации JavaConfig и концепцию «конвенция вместо конфигурации», что может значительно упростить разработку веб-приложений и служб REST на основе SpringMVC.

Глава 3. Введение в Redis

3.1 Установка и развертывание Redis (Linux)

Для установки и развертывания Redis обратитесь к моему блогу (Redis написан на основе C, поэтому перед установкой установите компилятор gcc):blog.CSDN.net/U014427391/…

3.2 Введение в Redis

В настоящее время Redis стал одной из самых популярных баз данных в памяти в сообществе веб-разработчиков.С быстрым развитием Web 2.0 и увеличением доли полуструктурированных данных веб-сайты предъявляют все более высокие требования к эффективной производительности. А крупные веб-сайты обычно имеют сотни или более серверов Redis. Redis как мощная система, будь то система хранения, очереди или кэша, имеет свое место.

Чтобы начать работу со средой SpringBoot, вы можете обратиться к моему предыдущему блогу:blog.CSDN.net/U014427391/…

Глава 4 Реализация кэша Redis

4.1 Следующая структурная схема

Схема структуры проекта:
这里写图片描述

4.2 Конфигурация yml-файла SpringBoot

Добавьте конфигурацию application.yml под ресурс, здесь в основном настраивайте mysql, druid, redis

spring:
  datasource:

    # 主数据源
    shop:
      url: jdbc:mysql://127.0.0.1:3306/jeeplatform?autoReconnect=true&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false
      username: root
      password: root

    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    # 连接池设置
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      # 配置获取连接等待超时的时间
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      # Oracle请使用select 1 from dual
      validation-query: SELECT 'x'
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall,slf4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 合并多个DruidDataSource的监控数据
      use-global-data-source-stat: true
  jpa:
    database: mysql
    hibernate:
      show_sql: true
      format_sql: true
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  mvc:
    view:
      prefix: /WEB-INF/jsp/
      suffix: .jsp
  #Jedis配置
  jedis :
    pool :
      host : 127.0.0.1
      port : 6379
      password : password
      timeout : 0
      config :
        maxTotal : 100
        maxIdle : 10
        maxWaitMillis : 100000

Напишите класс конфигурации для запуска конфигурации JedisConfig.java:

package org.muses.jeeplatform.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
//@ConfigurationProperties(prefix = JedisConfig.JEDIS_PREFIX )
public class JedisConfig {

    //public static final String JEDIS_PREFIX = "jedis";

    @Bean(name= "jedisPool")
    @Autowired
    public JedisPool jedisPool(@Qualifier("jedisPoolConfig") JedisPoolConfig config,
                                   @Value("${spring.jedis.pool.host}")String host,
                                   @Value("${spring.jedis.pool.port}")int port,
                                   @Value("${spring.jedis.pool.timeout}")int timeout,
                                   @Value("${spring.jedis.pool.password}")String password) {
            return new JedisPool(config, host, port,timeout,password);
    }

    @Bean(name= "jedisPoolConfig")
    public JedisPoolConfig jedisPoolConfig (@Value("${spring.jedis.pool.config.maxTotal}")int maxTotal,
                                                @Value("${spring.jedis.pool.config.maxIdle}")int maxIdle,
                                                @Value("${spring.jedis.pool.config.maxWaitMillis}")int maxWaitMillis) {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(maxTotal);
            config.setMaxIdle(maxIdle);
            config.setMaxWaitMillis(maxWaitMillis);
            return config;
        }


}

4.3 Написание класса метааннотаций

Напишите класс мета-аннотации RedisCache.java, и классы, определенные аннотациями, автоматически реализуют обработку кэша АОП.

package org.muses.jeeplatform.annotation;

import org.muses.jeeplatform.common.RedisCacheNamespace;

import java.lang.annotation.*;

/**
 * 元注解 用来标识查询数据库的方法
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
//    RedisCacheNamespace nameSpace();
}

В дополнение к Retention JDK 5 предоставляет еще три аннотации: Target, Inherited и Documented. Исходя из этого, мы можем реализовать пользовательские мета-аннотации.Мы настраиваем RedisCache на основе ссылок на уровне методов.

1.RetentionPolicy.SOURCE Этот тип аннотаций зарезервирован только на уровне исходного кода и будет игнорироваться во время компиляции.
2.RetentionPolicy.CLASS Этот тип аннотаций сохраняется во время компиляции и существует в файле класса, но будет игнорироваться JVM.
3.RetentionPolicy.RUNTIME Аннотации этого типа будут сохранены JVM, поэтому они могут быть прочитаны и использованы JVM или другим кодом, который использует механизм отражения во время выполнения.

4.4 Вызов JedisPool для реализации обработки кэша Redis

package org.muses.jeeplatform.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.Resource;
@Component("redisCache")
public class RedisCache {

    @Autowired
    private JedisPool jedisPool;

    private JedisPool getJedisPool(){
        return jedisPool;
    }

    public void setJedisPool(JedisPool jedisPool){
        this.jedisPool = jedisPool;
    }

    /**
     * 从Redis缓存获取数据
     * @param redisKey
     * @return
     */
    public Object getDataFromRedis(String redisKey){
        Jedis jedis = jedisPool.getResource();
        byte[] byteArray = jedis.get(redisKey.getBytes());

        if(byteArray != null){
            return SerializeUtil.unSerialize(byteArray);
        }
        return null;
    }

    /**
     * 保存数据到Redis
     * @param redisKey
     */
    public String saveDataToRedis(String redisKey,Object obj){

        byte[] bytes = SerializeUtil.serialize(obj);

        Jedis jedis = jedisPool.getResource();

        String code = jedis.set(redisKey.getBytes(), bytes);

        return code;
    }


}

Класс инструмента для сериализации объектов:

package org.muses.jeeplatform.cache;

import java.io.*;

public class SerializeUtil {

    /**
     * 序列化对象
     * @param obj
     * @return
     */
    public static byte[] serialize(Object obj){
        ObjectOutputStream oos = null;
        ByteArrayOutputStream baos = null;
        try{
            baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);

            oos.writeObject(obj);
            byte[] byteArray = baos.toByteArray();
            return byteArray;

        }catch(IOException e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 反序列化对象
     * @param byteArray
     * @return
     */
    public static Object unSerialize(byte[] byteArray){
        ByteArrayInputStream bais = null;
        try {
            //反序列化为对象
            bais = new ByteArrayInputStream(byteArray);
            ObjectInputStream ois = new ObjectInputStream(bais);
            return ois.readObject();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

Помните, что класс Vo должен реализовывать Serializable, например класс VO с информацией о меню, который является классом сущностей, отображаемым JPA.

package org.muses.jeeplatform.core.entity.admin;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

/**
 * @description 菜单信息实体
 * @author Nicky
 * @date 2017年3月17日
 */
@Table(name="sys_menu")
@Entity
public class Menu implements Serializable {

    /** 菜单Id**/
    private int menuId;

    /** 上级Id**/
    private int parentId;

    /** 菜单名称**/
    private String menuName;

    /** 菜单图标**/
    private String menuIcon;

    /** 菜单URL**/
    private String menuUrl;

    /** 菜单类型**/
    private String menuType;

    /** 菜单排序**/
    private String menuOrder;

    /**菜单状态**/
    private String menuStatus;

    private List<Menu> subMenu;

    private String target;

    private boolean hasSubMenu = false;

    public Menu() {
        super();
    }   

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    public int getMenuId() {
        return this.menuId;
    }

    public void setMenuId(int menuId) {
        this.menuId = menuId;
    }

    @Column(length=100)
    public int getParentId() {
        return parentId;
    }

    public void setParentId(int parentId) {
        this.parentId = parentId;
    }

    @Column(length=100)
    public String getMenuName() {
        return this.menuName;
    }

    public void setMenuName(String menuName) {
        this.menuName = menuName;
    }   

    @Column(length=30)
    public String getMenuIcon() {
        return this.menuIcon;
    }

    public void setMenuIcon(String menuIcon) {
        this.menuIcon = menuIcon;
    }   

    @Column(length=100)
    public String getMenuUrl() {
        return this.menuUrl;
    }

    public void setMenuUrl(String menuUrl) {
        this.menuUrl = menuUrl;
    }   

    @Column(length=100)
    public String getMenuType() {
        return this.menuType;
    }

    public void setMenuType(String menuType) {
        this.menuType = menuType;
    }

    @Column(length=10)
    public String getMenuOrder() {
        return menuOrder;
    }

    public void setMenuOrder(String menuOrder) {
        this.menuOrder = menuOrder;
    }

    @Column(length=10)
    public String getMenuStatus(){
        return menuStatus;
    }

    public void setMenuStatus(String menuStatus){
        this.menuStatus = menuStatus;
    }

    @Transient
    public List<Menu> getSubMenu() {
        return subMenu;
    }

    public void setSubMenu(List<Menu> subMenu) {
        this.subMenu = subMenu;
    }

    public void setTarget(String target){
        this.target = target;
    }

    @Transient
    public String getTarget(){
        return target;
    }

    public void setHasSubMenu(boolean hasSubMenu){
        this.hasSubMenu = hasSubMenu;
    }

    @Transient
    public boolean getHasSubMenu(){
        return hasSubMenu;
    }

}

4.5 Реализация Spring AOP отслеживает все кэши методов, аннотированные @RedisCache

Сначала получите кеш от Redis, если запрос не выполнен, запросите базу данных MySQL, а затем сохраните ее в кеше Redis и вызовите кеш Redis напрямую для следующего запроса.

package org.muses.jeeplatform.cache;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
 * AOP实现Redis缓存处理
 */
@Component
@Aspect
public class RedisAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisAspect.class);

    @Autowired
    @Qualifier("redisCache")
    private RedisCache redisCache;

    /**
     * 拦截所有元注解RedisCache注解的方法
     */
    @Pointcut("@annotation(org.muses.jeeplatform.annotation.RedisCache)")
    public void pointcutMethod(){

    }

    /**
     * 环绕处理,先从Redis里获取缓存,查询不到,就查询MySQL数据库,
     * 然后再保存到Redis缓存里
     * @param joinPoint
     * @return
     */
    @Around("pointcutMethod()")
    public Object around(ProceedingJoinPoint joinPoint){
        //前置:从Redis里获取缓存
        //先获取目标方法参数
        long startTime = System.currentTimeMillis();
        String applId = null;
        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0) {
            applId = String.valueOf(args[0]);
        }

        //获取目标方法所在类
        String target = joinPoint.getTarget().toString();
        String className = target.split("@")[0];

        //获取目标方法的方法名称
        String methodName = joinPoint.getSignature().getName();

        //redis中key格式:    applId:方法名称
        String redisKey = applId + ":" + className + "." + methodName;

        Object obj = redisCache.getDataFromRedis(redisKey);

        if(obj!=null){
            LOGGER.info("**********从Redis中查到了数据**********");
            LOGGER.info("Redis的KEY值:"+redisKey);
            LOGGER.info("REDIS的VALUE值:"+obj.toString());
            return obj;
        }
        long endTime = System.currentTimeMillis();
        LOGGER.info("Redis缓存AOP处理所用时间:"+(endTime-startTime));
        LOGGER.info("**********没有从Redis查到数据**********");
        try{
            obj = joinPoint.proceed();
        }catch(Throwable e){
            e.printStackTrace();
        }
        LOGGER.info("**********开始从MySQL查询数据**********");
        //后置:将数据库查到的数据保存到Redis
        String code = redisCache.saveDataToRedis(redisKey,obj);
        if(code.equals("OK")){
            LOGGER.info("**********数据成功保存到Redis缓存!!!**********");
            LOGGER.info("Redis的KEY值:"+redisKey);
            LOGGER.info("REDIS的VALUE值:"+obj.toString());
        }
        return obj;
    }


}

Затем вызовите @RedisCache для реализации кэширования.

/**
     * 通过菜单Id获取菜单信息
     * @param id
     * @return
     */
    @Transactional
    @RedisCache
    public Menu findMenuById(@RedisCacheKey int id){
        return menuRepository.findMenuByMenuId(id);
    }

Войдите в систему, а затем добавьте метод аннотации @RedisCache для обработки кэша Redis.
这里写图片描述

这里写图片描述

Вы можете видеть, что Redis сохраняется в кеше.

这里写图片描述

Код проекта:GitHub.com/U014427391/…, добро пожаловать в звезду (коллекцию) на github