Mybatis深入源码分析之基于【装饰设计模式】纯手写实现多级缓存框架

前言:设计模式源于生活

什么是装饰模式

在不改变原有对象的基础上附加功能,相比生成子类更灵活。

装饰者模式应用场景

Mybatis缓存,过滤器,网关控制,P2P分控审批

装饰者模式定义

(1)抽象组件:定义一个抽象接口,来规范准备附加功能的类
(2)具体组件:将要被附加功能的类,实现抽象构件角色接口
(3)抽象装饰者:持有对具体构件角色的引用并定义与抽象构件角色一致的接口
(4)具体装饰:实现抽象装饰者角色,负责对具体构件添加额外功能。

装饰模式和代理模式区别?

代理模式:在方法之前和之后实现处理,在方法上实现增强,隐藏真实方法的真实性,保证安全。
装饰模式:不改变原有的功能,实现增强,不断新增很多装饰。

多级缓存框架的设计

一般场景如下:
1.在很早之前框架只有一级缓存,数据缓存在jvm内存中,首先去查询内存是否有数据,如果没有数据则查询db,然后再将数据添加到jvm内存
2.第二次查询数据,查询jvm,jvm有数据,则直接返回view,否则又进行查询db,但是这样也会有个缺点,就是数据过多,可能会造成jvm内存溢出
3.后来有了redis或其他的缓存框架,就方便实现了二级缓存,或者三级甚至更高的缓存,但是同样也会缺点,就是会造成缓存穿透

首先在实现多级缓存框架之前,我先大概讲解一下我的实现的思路原理

1.首先我会定义装饰抽象骨架,用于定义一个基础的动作组件
2.定义一个具体组件,用于实现一级缓存
3.定义一个装饰类,用于后期扩展骨架
4.定义一个装饰具体类,用于实现二级缓存
5.定义一个注解,通过AOP来控制缓存和业务代码,以便代码的简洁度和可读性

多级缓存框架实现开始

首先展示一波,我的整体项目结构

先将工具类贴到前面,怕代码大家阅读代码看混乱
jvm工具类
public class JvmMapCacheUtils {

    private static Map<String, String> caches = new ConcurrentHashMap<>();

    /**
     * 从内存中获取缓存
     *
     * @param key
     * @param t
     * @param <T>
     * @return
     */
    public static <T> T getEntity(String key, Class<T> t) {
        String json = caches.get(key);
        T t1 = JSONObject.parseObject(json, t);
        return t1;
    }

    /**
     * 添加缓存至内存中
     *
     * @param key
     * @param value
     */
    public static void putCache(String key, Object value) {
        String jsonString = JSONObject.toJSONString(value);
        caches.put(key, jsonString);
    }
}
redis工具类
@Component
public class RedisUtils {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 如果key存在的话返回fasle 不存在的话返回true
    public Boolean setNx(String key, String value, Long timeout) {
        Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if (timeout != null) {
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
        return setIfAbsent;
    }

    /**
     * 存放string类型
     *
     * @param key     key
     * @param data    数据
     * @param timeout 超时间
     */
    public void setString(String key, String data, Long timeout) {
        stringRedisTemplate.opsForValue().set(key, data);
        if (timeout != null) {
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    /**
     * 存放string类型
     *
     * @param key  key
     * @param data 数据
     */
    public void setString(String key, String data) {
        setString(key, data, null);
    }

    /**
     * 根据key查询string类型
     *
     * @param key
     * @return
     */
    public String getString(String key) {
        String value = stringRedisTemplate.opsForValue().get(key);
        return value;
    }

    public <T> T getEntity(String key, Class<T> t) {
        String json = getString(key);
        return JSONObject.parseObject(json, t);
    }

    public void putEntity(String key, Object object) {
        String json = JSONObject.toJSONString(object);
        setString(key, json);
    }

    /**
     * 根据对应的key删除key
     *
     * @param key
     */
    public boolean delKey(String key) {
        return stringRedisTemplate.delete(key);
    }


    public void setList(String key, List<String> listToken) {
        stringRedisTemplate.opsForList().leftPushAll(key, listToken);
    }

    public StringRedisTemplate getStringRedisTemplate() {
        return stringRedisTemplate;
    }
}
定义一个抽象组件,用于构建我们的骨架
public interface ComponentCache {

    /**
     * 根据key查询缓存数据
     *
     * @param key       key查询缓存
     * @param clz       动态返回对象
     * @param joinPoint 目标对象方法
     * @param <T>       返回对象
     * @return
     */
    <T> T getCacheEntity(String key, Class<T> clz, ProceedingJoinPoint joinPoint);
}
定义一个具体组件,实现一级缓存

这里的思路是,通过key查询jvm内存是否有数据,有数据直接返回结果,没有数据,则通过AOP执行目标对象方法,查询数据库,将结果再插入到jvm内存中

@Slf4j
@Component
public class JvmComponentCache implements ComponentCache {

    /**
     * 查询jvm一级缓存
     *
     * @param key       key查询缓存
     * @param clz       动态返回对象
     * @param joinPoint 目标对象方法
     * @param <T>
     * @return
     */
    @Override
    public <T> T getCacheEntity(String key, Class<T> clz, ProceedingJoinPoint joinPoint) {
        //一级缓存
        T t = JvmMapCacheUtils.getEntity(key, clz);
        if (t != null) {
            log.info("查询一级缓存,{}", t);
            return t;
        }

        try {
            log.info("查询db");
            //这个地方执行目标方法
            Object dbResult = joinPoint.proceed();
            JvmMapCacheUtils.putCache(key, dbResult);
            return (T) dbResult;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        
        return null;
    }
}
接下来定义装饰类,用于后期扩展二级缓存,或三级缓存
public interface AbstractDecoration extends ComponentCache {

}
定义二级缓存

这里的思路是:先通过key去查询redis中是否存在数据,如果存在则直接返回结果,不存在的话,再查询jvm内存中是否有结果,没有结果再继续往下查询db
但是这里我是通过super关键字去调用查询一级缓存,因为二级缓存是基于一级缓存进行扩展的

@Component
@Slf4j
public class RedisDecoration extends JvmComponentCache implements AbstractDecoration {

    @Autowired
    private RedisUtils redisUtils;

    /**
     * redis 二级缓存
     *
     * @param key       key查询缓存
     * @param clz       动态返回对象
     * @param joinPoint 目标对象方法
     * @param <T>       返回对象
     * @return
     */
    @Override
    public <T> T getCacheEntity(String key, Class<T> clz, ProceedingJoinPoint joinPoint) {
        log.info("二级缓存查询开始");
        //二级缓存
        T t1 = redisUtils.getEntity(key, clz);
        if (t1 != null) {
            log.info("查询二级缓存,{}", t1);
            log.info("二级缓存查询结束");
            return t1;
        }

        //一级缓存
        T t2 = super.getCacheEntity(key, clz, joinPoint);
        if (t2 != null) {
            log.info("查询一级缓存,{}", t2);
            redisUtils.setString(key, JSON.toJSONString(t2));
            log.info("查询一级缓存结束");
            return t2;
        }
        return null;
    }

}
定义SunnyCache这个类,主要用于是方便外部调用的时候,其次如果有多个装饰类的话,可以将调用放在一个类中进行管理,方便后期维护
@Component
public class SunnyCache {

    @Autowired
    private RedisDecoration redisDecoration;

    /**
     * 根据key获取缓存数据
     *
     * @param key       获取缓存的key
     * @param clz       动态返回对象
     * @param joinPoint 通过代理获取目标对象
     * @param <T>       返回类型
     * @return
     */
    public <T> T getCache(String key, Class<T> clz, ProceedingJoinPoint joinPoint) {
        return redisDecoration.getCacheEntity(key, clz, joinPoint);
    }
}
定义注解,通过注解来实现多级缓存控制
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) //允许通过java反射获取注解
@Documented
public @interface SunnyCacheAop {
}
实现注解,这里的思路是,拦截方法上加入注解方法,并获取目标对象,通过方法名加参数类型加参数的值拼装成key,存放入内存中
@Aspect
@Component
@Slf4j
public class SunnyCacheAopImpl {

    @Autowired
    private SunnyCache sunnyCache;

    /**
     * 拦截使用缓存注解
     *
     * @param joinPoint
     */
    @Around(value = "@annotation(com.dream.sunny.aop.SunnyCacheAop)")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("AOP拦截->查询缓存数据开始");
        //获取目标对象
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;

        //获取目标方法
        Method method = methodSignature.getMethod();
        //key = 方法名+方法参数类型+方法的参数的值
        String key = method.getName() + Arrays.toString(method.getParameterTypes()) + Arrays.toString(joinPoint.getArgs());
        Object cache = sunnyCache.getCache(key, method.getReturnType(), joinPoint);
        log.info("AOP拦截->查询缓存数据结束");
        return cache;
    }
}
多级缓存框架以上就基本实现好了,接下来我们开始实现这个功能哈~
entity类
@Data
public class Student {

    private Integer id;

    private String name;

    private Integer age;
}
mapper类
@Repository
public interface StudentMapper {

    /**
     * 查询用户
     *
     * @param userId
     * @return
     */
    @Select("select id,name,age from student s where s.id = #{userId}")
    Student getStudent(@Param("userId") Integer userId);
}
service类
public interface StudentService {

    /**
     * 获取学生信息
     *
     * @param userId
     * @return
     */
    Student getStudent(Integer userId);
}
service实现类
@Service
@Slf4j
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentMapper studentMapper;

    /**
     * 获取学生
     *
     * @param userId
     * @return
     */
    @Override
    @SunnyCacheAop //加上注解,拦截这个查询方法
    public Student getStudent(Integer userId) {
        log.info("业务执行开始");
        Student mapperStudent = studentMapper.getStudent(userId);
        System.out.println("业务执行结束");
        return mapperStudent;
    }
}
controller类
@RestController
public class StudentController {

    @Autowired
    private StudentService studentService;

    @GetMapping("/getStudent")
    public String getStudent(@RequestParam("userId") Integer userId) {
        Student student = studentService.getStudent(userId);
        return JSON.toJSONString(student);
    }
}

演示开始:

数据库数据


这里我本地redis数据,都被我清空了哈


当二级缓存和一级缓存都没有数据的执行效果:


当二级缓存有数据的效果图


看一下redis数据


到此手写多级缓存框架基本就到此结束啦

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352