面向切面缓存设计
一、问题背景
在互联网行业,缓存作为一种家喻户晓的技术,在各个系统中起着提效降压的作用。但是缓存的引入,也使得处理逻辑变得复杂,尤其在当下微服务大行其道,一个大型系统动辄十几个模块,多人共同开发、维护的情况下,不同开发人员的缓存设计都不尽相同,并且多与业务代码紧密耦合。在这个背景下,本文着重思考一种统一的方案,借鉴面向切面的编程思想(AOP),实现面向切面的缓存设计,将系统中的缓存设计与主业务逻辑剥离开来。
二、常见的缓存设计分析
这是一个带有缓存的查询接口的时序图,相信这样的缓存设计大多数读者都实现过:
其流程图如下,这个带有缓存的查询接口涉及到了Redis的EXISTS、GET、SET等动作:
尽管这只是一个简单的带有缓存的查询接口,但是可以看到Redis的EXISTS、GET、SET这些动作将主业务逻辑(本例中是查询数据库)包围了起来,紧密的耦合在了一起。
到这里就应该抛出我们的问题了:
1、如果接口内的业务逻辑更加复杂,比如一个支持重入的下单接口,该如何设计缓存才能使耦合降低,使逻辑更清晰,代码的可读性更高?
2、如果有10个乃至100个这样复杂的接口,如何去简化开发缓存的工作量,复用之前的逻辑,避免大量重复代码?
答案就在面向切面编程的思想里。
三、面向切面缓存设计思路
何为切面?
可以理解为动态代理的实现,代理提供了在访问代理对象方法前后进行控制的支持,在这个过程中,代理类就是一个切面。
从面向切面编程的思想出发,一个与业务完全解耦、高复用性的缓存设计应该是一个缓存切面。
这里要
缓存切面的设计目的应该是:
1、将缓存设计和业务逻辑分离——将缓存设计放在切面中,将业务类加入切入点
2、切面内的缓存设计适用绝大多数的场景——从业务类中抽出高通用性的缓存设计,设计统一的缓存切面
3、功能可扩展性高——统一切面结合Java中的自定义注解实现功能增强
基于这些考虑因素,介绍设计缓存切面的主要思路:
首先是介绍缓存切面用到的注解技术(Annotation)。
相信熟悉Java的同学都会留意到,大多数的切面设计都与注解有关,对于缓存切面亦是如此。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface CacheKey {
String fields() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GetCache {
/**
* 失效时间
* 默认300秒
*
* @return 过期时间
*/
int expire() default 300;
/**
* 缓存开关
*
* @return cache switch
*/
boolean isCache() default true;
/**
* key的前缀
*
* @return cache前缀
*/
String keyPrefix() default "";
/**
* 是否需要对key做hash
*
* @return isHash
*/
boolean isHash() default false;
}
CacheKey和GetCache是我们声明的两个用于标示组成缓存key的参数和开启缓存的方法的注解,GetCache还有许多的属性,用于功能的增强。
他们都有一个Target注解,说明了CacheKey和GetCache作用的地方,CacheKey作用于PARAMETER参数,而GetCache作用于METHOD方法。
下面是一个简单的使用例子,描述了一个带有缓存的订单查询接口:
public interface OrderService {
@GetCache
Order queryOrder(@CacheKey Long orderId);
}
在这个例子中,GetCache标示了queryOrder是一个带有缓存的接口,CacheKey标示了用于组成缓存key的参数orderId。
那么这两个注解是如何起作用的呢?这就要提到缓存切面里的具体设计了。
简化后的流程图如下:
从图上可以看出,GetCache在缓存切面中用于判断是否进入通用的缓存逻辑,而CacheKey则作为识别组成key的参数的依据。
在满足了基本的缓存需求后,我们可以通过注解上的属性设计开关,对一些业务的缓存设计进行增强。
上述的缓存设计主要面向的是被动缓存,由于缓存设计的统一和注解配置的灵活性,缓存切面可以往主动缓存、多级缓存等进行扩展,能得到很好的支持。
篇幅有限不一一展开,架构如下图:
最后来了解一下如何接入缓存切面。
SpringAOP的配置方式多样,在这里仅举一例。
首先需要依赖lib,在项目pom里进行依赖的配置:
<dependency>
<groupId>com.baidu.trade</groupId>
<artifactId>btc-library</artifactId>
<version>1.0.1-SNAPSHOT</version>
</dependency>
然后是切面bean的注入,以及切入点的配置:(缓存切面使用MethodInterceptor进行实现)
<bean id="cacheMethodInterceptor" class="com.baidu.trade.library.cache.CacheMethodInterceptor"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>orderService</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>cacheMethodInterceptor</value>
</list>
</property>
</bean>
<bean id="btcLibraryCache" class="com.baidu.driver4j.bdrp.client.BdrpClientFactory">
<property name="nodes" value="${btc.library.cache.redis.nodes}"/>
</bean>
最后就是在需要开启缓存的接口、接口参数上加上GetCache和CacheKey注解,可以参考上文的Annotation Demo
四、总结
本文从开发人员常用的缓存设计出发,展示了向面向切面编程技术的思路演变,提出了面向切面的缓存设计方案。作为一个纯技术的设计优化方案,缓存切面是高复用、高扩展、高避错、易使用的。
1、高复用主要体现在缓存切面里的缓存设计高度通用,使用缓存切面可以大大减少项目系统代码里重复的缓存设计,在开发和维护上节约人力成本。
2、高扩展则得益于自定义注解的支持,可以通过注解上的属性控制扩展内容,对于缓存的扩展性升级无须代码改动(只需要改动注解)。
3、高避错,指的是缓存切面可以有效避免由于不同开发人员,尤其是对缓存技术了解不够深入的初级程序员的编码问题导致的缓存设计缺陷,以及缓存设计缺陷带来的业务影响等。
4、易使用表现在接入缓存切面时的无代码侵入,接入方无须任何代码开发即可进行接入(只需要增加注解,配置)。
- 1131: [POI2008]Sta
- 3172: [Tjoi2013]单词
- WebApiThrottle限流框架使用手册
- webpack学习(六)打包压缩js和css
- 1051: [HAOI2006]受欢迎的牛
- 1572: [Usaco2009 Open]工作安排Job
- 深海中的STL—mt19937
- 探索ASP.NET MVC5系列之~~~4.模型篇---包含模型常用特性和过度提交防御
- POJ1201 Intervals(差分约束)
- 【NLP】十分钟快览自然语言处理学习总结
- MVC5 网站开发之九 网站设置
- Redis安全小结
- webpack学习(七)打包压缩图片
- POJ1275 Cashier Employment(差分约束)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 如何白嫖3个月的JetBrains全家桶(包括Java神器IDEA)
- 潘石屹用Python解决100个问题 | 打印菱形
- 八、适配器模式与桥接模式详解
- 九、委派模式与模板模式详解
- 白嫖JetBrains全家桶第二波与第三波
- 28.MyBatis应用分析与最佳实践
- 30.MyBatis插件原理与Spring集成
- 10X bam文件按barcode分割
- 格拉姆-施密特正交化说明
- flow.ci - 简单强大的开源 CI/CD 工具
- golang切片
- CSRF跨站请求伪造——原理及复现
- 【Spark】常见的编译错误
- 聊聊dubbo-go的randomLoadBalance
- CentOS 6 安装 Chrome最简单的方法