Java - 内存框架设计与实现

作者: 右耳菌 | 来源:发表于2022-06-14 16:08 被阅读0次

    一、使用缓存

    • 对缓存的常用操作描述
    • 查询时,先读取缓存,如果缓存中没有数据,则触发真正的数据获取,如果缓存中有数据,直接返回缓存中的数据;
    • 新增数据时,将数据写入缓存;
    • 删除数据时,删除对应的缓存数据。并且可以自定义每个KEY的缓存有效期。

    二、Spring Cache 提供缓存注解

    • @Cacheable
      主要针对方法配置,能够根据方法的请求参数对其进行缓存
    • @CacheEvict
      清空缓存
    • @CachePut
      保证方法被调用,又希望结果被缓存与@Cacheable区别在于是否每次都调用方法,常用于更新

    二、一个简单的缓存架构的实现(例子)

    pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.7.0</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>cn.lazyfennec</groupId>
        <artifactId>custom-cache</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>custom-cache</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.2.2</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </dependency>
            <dependency>
                <groupId>com.github.ben-manes.caffeine</groupId>
                <artifactId>caffeine</artifactId>
                <version>2.6.2</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    1. Cacheable 注解
    package cn.lazyfennec.customcache.aop.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @ClassName Cacheable
     * @Description 用于缓存读取
     * @Author Neco
     * @Version 1.0
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Cacheable {
    
        String cacheName() default ""; // 缓存名称
    
        String cacheKey(); // 缓存key
    
        int expire() default 3600; // 有效时间(单位,秒),默认1个小时
    
        int reflash() default -1; // 缓存主动刷新时间(单位,秒)
    }
    
    2.
    package cn.lazyfennec.customcache.aop.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @ClassName CacheEvict
     * @Description 缓存清除
     * @Author Neco
     * @Version 1.0
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface CacheEvict {
        String cacheName() default ""; //缓存名称
    
        String cacheKey(); //缓存key
    
        boolean allEntries() default false; //是否清空cacheName的全部数据
    }
    
    3. CachePut 注解
    package cn.lazyfennec.customcache.aop.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @ClassName CachePut
     * @Description 缓存写入
     * @Author Neco
     * @Version 1.0
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface CachePut {
        String cacheName() default ""; //缓存名称
    
        String cacheKey(); //缓存key
    
        int expire() default 3600; //有效期时间(单位:秒),默认1个小时
    }
    

    4. 缓存配置文件
    package cn.lazyfennec.customcache.aop.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @ClassName CachePut
     * @Description 缓存写入
     * @Author Neco
     * @Version 1.0
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface CachePut {
        String cacheName() default ""; //缓存名称
    
        String cacheKey(); //缓存key
    
        int expire() default 3600; //有效期时间(单位:秒),默认1个小时
    }
    
    5. 默认缓存键生成器
    package cn.lazyfennec.customcache.aop;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.DefaultParameterNameDiscoverer;
    import org.springframework.expression.EvaluationContext;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    import org.springframework.expression.spel.support.StandardEvaluationContext;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.lang.reflect.Method;
    
    /**
     * @Author: Neco
     * @Description: key生成策略,缓存名+缓存KEY(支持Spring EL表达式)
     * @Date: create in 2022/6/13 16:04
     */
    @Component
    public class DefaultKeyGenerator {
        private static Logger logger = LoggerFactory.getLogger(DefaultKeyGenerator.class);
    
        // 用于SpEL表达式解析
        private SpelExpressionParser parser = new SpelExpressionParser();
    
        // 用于获取方法参数定义名字
        private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
    
        /**
         * @Description cache key生成
         * @Param cacheKey:key值必传,cacheNames:缓存名称,不传取方法路径
         **/
        public String generateKey(ProceedingJoinPoint pjp, String cacheName, String cacheKey) throws NoSuchMethodException {
            if (StringUtils.isEmpty(cacheKey)) {
                throw new NullPointerException("CacheKey can not be null...");
            }
            Signature signature = pjp.getSignature();
            if (cacheName == null) {
                cacheName = new String(signature.getDeclaringTypeName() + "." + signature.getName());
            }
            EvaluationContext evaluationContext = new StandardEvaluationContext();
            if (!(signature instanceof MethodSignature)) {
                throw new IllegalArgumentException("This annotation can only be used for methods...");
            }
            MethodSignature methodSignature = (MethodSignature) signature; //method参数列表
            Method method = pjp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
            String[] parameterNames = nameDiscoverer.getParameterNames(method);
            Object[] args = pjp.getArgs();
            for (int i = 0; i < parameterNames.length; i++) {
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
            //解析cacheKey
            String result = "CacheName_" + cacheName + "_CacheKey_" + parser.parseExpression(cacheKey).getValue(evaluationContext, String.class); //暂时只使用String类型
            logger.info("=============>>> generateKeys : {}", result);
            return result;
        }
    }
    
    6. AOP
    package cn.lazyfennec.customcache.aop;
    
    import cn.lazyfennec.customcache.aop.annotation.CacheEvict;
    import cn.lazyfennec.customcache.aop.annotation.CachePut;
    import cn.lazyfennec.customcache.aop.annotation.Cacheable;
    import com.github.benmanes.caffeine.cache.Cache;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.util.SerializationUtils;
    
    /**
     * @Author: Neco
     * @Description: 自定义缓存注解AOP实现
     * @Date: create in 2022/6/13 15:47
     */
    @Aspect
    @Component
    public class CacheableAspect {
        private static Logger logger = LoggerFactory.getLogger(CacheableAspect.class);
    
        @Autowired
        private Cache caffeineCache;
    
        @Autowired
        private DefaultKeyGenerator defaultKeyGenerator;
    
        /**
         * 读取缓存数据
         * 定义增强,pointcut连接点使用@annotaion(xxx)进行定义
         * @param pjp
         * @param cache
         * @return
         * @throws Throwable
         */
        @Around(value = "@annotation(cache)") // cache 与 下面参数名around对应
        public Object cache(final ProceedingJoinPoint pjp, Cacheable cache) throws Throwable {
            try {
                String key = defaultKeyGenerator.generateKey(pjp, cache.cacheName(), cache.cacheKey());
                Object valueData = null;
                // 获取缓存中的值
                Object value = caffeineCache.getIfPresent(key);
                if (value != null) {
                    //如果缓存有值,需要判断刷新缓存设置和当前缓存的失效时间
                    if (cache.reflash() > 0) {
                        //查询当前缓存失效时间是否在主动刷新规则范围内
                        // caffeine 中刷新的话可以使用同步加载的方式调用reflash()
                    }
                    return value;
                }
                //缓存中没有值,执行实际数据查询方法
                if (valueData == null) {
                    valueData = pjp.proceed(); //写入缓存
                }
                if (cache.expire() > 0) {
                    caffeineCache.put(key, valueData);
                } else { //否则设置缓存时间 ,序列化存储
                    caffeineCache.put(key, valueData);
                }
                return valueData;
            } catch (Exception e) {
                logger.error("读取caffeine缓存失败,异常信息:" + e.getMessage());
                return pjp.proceed();
            }
        }
    
        /**
         * 新增缓存
         */
        @Around(value = "@annotation(cache)")
        public Object cachePut(final ProceedingJoinPoint pjp, CachePut cache) throws Throwable {
            try {
                String key = defaultKeyGenerator.generateKey(pjp, cache.cacheName(), cache.cacheKey());
                Object valueData = pjp.proceed();
                // 写入缓存
                // 由于 caffeine 是内存缓存,对每个可以设置超时支持并不够好。除非每个 key 都构造一个 cache 对象;可以使用 redis 代替
                if (cache.expire() > 0) {
                    caffeineCache.put(key, SerializationUtils.serialize(pjp.getArgs()[0]));
                } else {
                    caffeineCache.put(key, SerializationUtils.serialize(pjp.getArgs()[0]));
                }
                return valueData;
            } catch (Exception e) {
                logger.error("写入caffeine缓存失败,异常信息:" + e.getMessage());
                return pjp.proceed();
            }
        }
    
        /**
         * 删除缓存
         */
        @Around(value = "@annotation(cache)")
        public Object cacheEvict(final ProceedingJoinPoint pjp, CacheEvict cache) throws Throwable {
            try {
                String cacheName = cache.cacheName();
                boolean allEntries = cache.allEntries();
                if (allEntries) {
                    if (cacheName == null) {
                        Signature signature = pjp.getSignature();
                        cacheName = new String(signature.getDeclaringTypeName() + "." + signature.getName());
                    }
                    caffeineCache.invalidate("CacheName_" + cacheName);
                } else {
                    String key = defaultKeyGenerator.generateKey(pjp, cache.cacheName(), cache.cacheKey());
                    caffeineCache.invalidate(key);
                }
            } catch (Exception e) {
                logger.error("删除caffeine缓存失败,异常信息:" + e.getMessage());
            }
            return pjp.proceed();
        }
    }
    
    7.模拟实现类
    package cn.lazyfennec.customcache.entity;
    
    import lombok.Data;
    
    import java.io.Serializable;
    
    @Data
    public class Country implements Serializable {
        /**
         * 主键
         */
        private Integer id;
    
        /**
         * 名称
         */
        private String countryname;
    
        /**
         * 代码
         */
        private String countrycode;
    
    }
    
    8. service相关
    package cn.lazyfennec.customcache.service;
    
    import cn.lazyfennec.customcache.entity.Country;
    
    import java.util.List;
    
    /**
     * @Author: Neco
     * @Description:
     * @Date: create in 2022/6/14 13:04
     */
    public interface ICountryService {
    
        List<Country> listAll();
    
        int save(Country country);
    
        int update(Country country);
    
        int deleteById(int id);
    
        Country getById(int id);
    
    }
    
    package cn.lazyfennec.customcache.service.impl;
    
    import cn.lazyfennec.customcache.aop.annotation.CacheEvict;
    import cn.lazyfennec.customcache.aop.annotation.CachePut;
    import cn.lazyfennec.customcache.aop.annotation.Cacheable;
    import cn.lazyfennec.customcache.entity.Country;
    import cn.lazyfennec.customcache.mapper.CountryMapper;
    import cn.lazyfennec.customcache.service.ICountryService;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    /**
     * @Author: Neco
     * @Description:
     * @Date: create in 2022/6/14 13:07
     */
    @Service
    public class CountryServiceImpl implements ICountryService {
    
        @Resource
        private CountryMapper countryMapper;
    
        @Override
        public List<Country> listAll() {
            return countryMapper.listAll();
        }
    
        @Override
        @CachePut(cacheName = "country_", cacheKey = "#country.id", expire = 3600)
        public int save(Country country) {
            return countryMapper.save(country);
        }
    
        @Override
        @CacheEvict(cacheName = "country_", cacheKey = "#id")
        public int update(Country country) {
            return countryMapper.update(country);
        }
    
        @Override
        @CacheEvict(cacheName = "country_", cacheKey = "#id")
        public int deleteById(int id) {
            return countryMapper.deleteById(id);
        }
    
        @Override
        @Cacheable(cacheName = "country_", cacheKey = "#id", expire = 3600)
        public Country getById(int id) {
            return countryMapper.getById(id);
        }
    }
    
    9. Mapper
    package cn.lazyfennec.customcache.mapper;
    
    import cn.lazyfennec.customcache.entity.Country;
    import org.apache.ibatis.annotations.*;
    
    import java.util.List;
    
    /**
     * @Author: Neco
     * @Description:
     * @Date: create in 2022/6/14 13:07
     */
    @Mapper
    public interface CountryMapper {
    
        @Select("select * from country")
        List<Country> listAll();
    
        @Insert("insert into country (id, countryname, countrycode) values(#{country.id}, #{country.countryname}, #{country.countrycode})")
        int save(@Param("country") Country country);
    
        @Update("update country set countryname = #{country.countryname} and countrycode = #{country.countrycode} where id=#{country.id}")
        int update(@Param("country") Country country);
        
        @Delete("delete from country where id=#{country.id}")
        int deleteById(int id);
    
        @Select("select * from country where id = #{id}")
        Country getById(int id);
    
    }
    
    10. Controller
    package cn.lazyfennec.customcache.controller;
    
    import cn.lazyfennec.customcache.entity.Country;
    import cn.lazyfennec.customcache.entity.vo.Result;
    import cn.lazyfennec.customcache.service.ICountryService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    /**
     * @Author: Neco
     * @Description:
     * @Date: create in 2022/6/14 11:07
     */
    @RestController
    @RequestMapping("/cache")
    public class CacheController {
    
        @Autowired
        private ICountryService countryService;
    
        @GetMapping("/country/list")
        public Result list() {
            List<Country> list = countryService.listAll();
            return Result.OK(list);
        }
    
        @GetMapping("/country/{id}")
        public Result getById(@PathVariable Integer id) {
            Country country = countryService.getById(id);
            return Result.OK(country);
        }
    
        @DeleteMapping("/country/{id}")
        public Result deleteById(@PathVariable Integer id) {
            countryService.deleteById(id);
            return Result.OK("删除成功!");
        }
    
        @PostMapping("/country/save")
        public Result save(@RequestBody Country country) {
            countryService.save(country);
            return Result.OK("保存成功!");
        }
    
        @PutMapping("/country/update")
        public Result update(@RequestBody Country country) {
            countryService.update(country);
            return Result.OK("更新成功!");
        }
    }
    

    测试

    可以得知,一定时间内,多次访问 http://localhost:8080/cache/country/1 这样的地址,后边的内容会从缓存中取相关的数据。


    如果觉得有收获就点个赞吧,更多知识,请点击关注查看我的主页信息哦~

    相关文章

      网友评论

        本文标题:Java - 内存框架设计与实现

        本文链接:https://www.haomeiwen.com/subject/qnlnmrtx.html