文档章节

spring aop自定义redis缓存实现

chworld
 chworld
发布于 2016/02/21 18:13
字数 2108
阅读 379
收藏 3

应用场景

我们希望能够将数据库查询结果缓存到Redis中,这样在第二次做同样的查询时便可以直接从redis取结果,从而减少数据库读写次数。

需要解决的问题

操作缓存的代码写在哪?必须要做到与业务逻辑代码完全分离。 如何避免脏读? 从缓存中读出的数据必须与数据库中的数据一致。 如何为一个数据库查询结果生成一个唯一的标识?即通过该标识(Redis中为Key),能唯一确定一个查询结果,同一个查询结果,一定能映射到同一个key。只有这样才能保证缓存内容的正确性 如何序列化查询结果?查询结果可能是单个实体对象,也可能是一个List。

解决方案

避免脏读

我们缓存了查询结果,那么一旦数据库中的数据发生变化,缓存的结果就不可用了。为了实现这一保证,可以在执行相关表的更新查询(update, delete, insert)查询前,让相关的缓存过期。这样下一次查询时程序就会重新从数据库中读取新数据缓存到redis中。那么问题来了,在执行一条insert前我怎么知道应该让哪些缓存过期呢?对于Redis,我们可以使用Hash Set数据结构,让一张表对应一个Hash Set,所有在这张表上的查询都保存到该Set下。这样当表数据发生变动时,直接让Set过期即可。我们可以自定义一个注解,在数据库查询方法上通过注解的属性注明这个操作与哪些表相关,这样在执行过期操作时,就能直接从注解中得知应该让哪些Set过期了。

为查询生成唯一标识

对于MyBatis,我们可以直接使用SQL字符串做为key。但是这样就必须编写基于MyBatis的拦截器,从而使你的缓存代码与MyBatis紧紧耦合在一起。如果哪天更换了持久层的框架,你的缓存代码就白写了,所以这个方案并不完美。
仔细想一想,其实如果两次查询调用的类名、方法名和参数值相同,我们就可以确定这两次查询结果一定是相同的(在数据没有变动的前提下)。因此,我们可以将这三个元素组合成一个字符串做为key, 就解决了标识问题。

序列化查询结果

最方便的序列化方式就是使用JDK自带的ObjectOutputStream和ObjectInputStream。优点是几乎任何一个对象,只要实现了Serializable接口,都用同一套代码能被序列化和反序列化。但缺点也很致命,那就是序列化的结果容量偏大,在redis中会消耗大量内存(是对应JSON格式的3倍左右)。那么我们只剩下JSON这一个选择了。
JSON的优点是结构紧凑,可读性强,但美中不足的是,反序列化对象时必须提供具体的类型参数(Class对象),如果是List对象,还必须提供List和List中的元素类型两种信息,才能被正确反序列化。这样就增加了代码的复杂度。不过这些困难都是可以克服的,所以我们还是选择JSON作为序列化存储方式。

代码写在哪

毫无疑问,该AOP上场了。在我们的例子中,持久化框架使用的是MyBatis,因此我们的任务就是拦截Mapper接口方法的调用,通过Around(环绕通知)编写以下逻辑:

方法被调用之前,根据类名、方法名和参数值生成Key 通过Key向Redis发起查询 如果缓存命中,则将缓存结果反序列化作为方法调用的返回值 ,并阻止被代理方法的调用。 如果缓存未命中,则执行代理方法,得到查询结果,序列化,用当前的Key将序列化结果放入redis中。

代码实现

因为我们要拦截的是Mapper接口方法,因此必须命令spring使用JDK的动态代理而不是cglib的代理。为此,我们需要做以下配置:

view sourceprint?

1.<code class="hljs xml"><!-- 当proxy-target-classfalse时使用JDK动态代理 -->

2.<!-- 为true时使用cglib -->

3.<!-- cglib无法拦截接口方法 -->

4.<aop:aspectj-autoproxy proxy-target-class='false' /></code>

然后定义两个标注在接口方法上的注解,用于传递类型参数:

view sourceprint?

1.<code class="hljs xml"><code class="hljs java">@Retention(RetentionPolicy.RUNTIME)

2.@Target (ElementType.METHOD)

3.@Documented

4.public @interface  RedisCache {

5.Class type();

6.}</code></code>

view sourceprint?

1.<code class="hljs xml"><code class="hljs java"><code class="hljs java">@Retention(RetentionPolicy.RUNTIME)

2.@Target(ElementType.METHOD)

3.public @interface RedisEvict {

4.Class type();

5.}</code></code></code>

注解的使用方式如下:

view sourceprint?

1.<code class="hljs xml"><code class="hljs java"><code class="hljs java"><code class="hljs fsharp">// 表示该方法需要执行 (缓存是否命中 ? 返回缓存并阻止方法调用 : 执行方法并缓存结果)的缓存逻辑

2.@RedisCache(type = JobPostModel.class)

3.JobPostModel selectByPrimaryKey(Integer id);</code></code></code></code>

view sourceprint?

1.<code class="hljs xml"><code class="hljs java"><code class="hljs java"><code class="hljs fsharp"><code class="hljs rust">// 表示该方法需要执行清除缓存逻辑

2.@RedisEvict(type = JobPostModel.class)

3.int deleteByPrimaryKey(Integer id);</code></code></code></code></code>

AOP的代码如下:

view sourceprint?

001.<code class="hljs xml"><code class="hljs java"><code class="hljs java"><code class="hljs fsharp"><code class="hljs rust"><code class="hljs java">@Aspect

002.@Component

003.public class RedisCacheAspect {

004.public static final Logger infoLog = LogUtils.getInfoLogger();

005. 

006.@Qualifier('redisTemplateForString')

007.@Autowired

008.StringRedisTemplate rt;

009. 

010. 

011./**

012.* 方法调用前,先查询缓存。如果存在缓存,则返回缓存数据,阻止方法调用;

013.* 如果没有缓存,则调用业务方法,然后将结果放到缓存中

014.* @param jp

015.* @return

016.* @throws Throwable

017.*/

018.@Around('execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.select*(..))' +

019.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.get*(..))' +

020.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.find*(..))' +

021.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.search*(..))')

022.public Object cache(ProceedingJoinPoint jp) throws Throwable {

023.// 得到类名、方法名和参数

024.String clazzName = jp.getTarget().getClass().getName();

025.String methodName = jp.getSignature().getName();

026.Object[] args = jp.getArgs();

027. 

028.// 根据类名,方法名和参数生成key

029.String key = genKey(clazzName, methodName, args);

030.if (infoLog.isDebugEnabled()) {

031.infoLog.debug('生成key:{}', key);

032.}

033. 

034.// 得到被代理的方法

035.Method me = ((MethodSignature) jp.getSignature()).getMethod();

036.// 得到被代理的方法上的注解

037.Class modelType = me.getAnnotation(RedisCache.class).type();

038. 

039.// 检查redis中是否有缓存

040.String value = (String)rt.opsForHash().get(modelType.getName(), key);

041. 

042.// result是方法的最终返回结果

043.Object result = null;

044.if (null == value) {

045.// 缓存未命中

046.if (infoLog.isDebugEnabled()) {

047.infoLog.debug('缓存未命中');

048.}

049. 

050.// 调用数据库查询方法

051.result = jp.proceed(args);

052. 

053.// 序列化查询结果

054.String json = serialize(result);

055. 

056.// 序列化结果放入缓存

057.rt.opsForHash().put(modelType.getName(), key, json);

058.} else {

059.// 缓存命中

060.if (infoLog.isDebugEnabled()) {

061.infoLog.debug('缓存命中, value = {}', value);

062.}

063. 

064.// 得到被代理方法的返回值类型

065.Class returnType = ((MethodSignature) jp.getSignature()).getReturnType();

066. 

067.// 反序列化从缓存中拿到的json

068.result = deserialize(value, returnType, modelType);

069. 

070.if (infoLog.isDebugEnabled()) {

071.infoLog.debug('反序列化结果 = {}', result);

072.}

073.}

074. 

075.return result;

076.}

077. 

078./**

079.* 在方法调用前清除缓存,然后调用业务方法

080.* @param jp

081.* @return

082.* @throws Throwable

083.*/

084.@Around('execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.insert*(..))' +

085.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.update*(..))' +

086.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.delete*(..))' +

087.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.increase*(..))' +

088.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.decrease*(..))' +

089.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.complaint(..))' +

090.'|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.set*(..))')

091.public Object evictCache(ProceedingJoinPoint jp) throws Throwable {

092. 

093.// 得到被代理的方法

094.Method me = ((MethodSignature) jp.getSignature()).getMethod();

095.// 得到被代理的方法上的注解

096.Class modelType = me.getAnnotation(RedisEvict.class).type();

097. 

098.if (infoLog.isDebugEnabled()) {

099.infoLog.debug('清空缓存:{}', modelType.getName());

100.}

101. 

102.// 清除对应缓存

103.rt.delete(modelType.getName());

104. 

105.return jp.proceed(jp.getArgs());

106.}

107. 

108. 

109. 

110./**

111.* 根据类名、方法名和参数生成key

112.* @param clazzName

113.* @param methodName

114.* @param args 方法参数

115.* @return

116.*/

117.protected String genKey(String clazzName, String methodName, Object[] args) {

118.StringBuilder sb = new StringBuilder(clazzName);

119.sb.append(Constants.DELIMITER);

120.sb.append(methodName);

121.sb.append(Constants.DELIMITER);

122. 

123.for (Object obj : args) {

124.sb.append(obj.toString());

125.sb.append(Constants.DELIMITER);

126.}

127. 

128.return sb.toString();

129.}

130. 

131.protected String serialize(Object target) {

132.return JSON.toJSONString(target);

133.}

134. 

135.protected Object deserialize(String jsonString, Class clazz, Class modelType) {

136.// 序列化结果应该是List对象

137.if (clazz.isAssignableFrom(List.class)) {

138.return JSON.parseArray(jsonString, modelType);

139.}

140. 

141.// 序列化结果是普通对象

142.return JSON.parseObject(jsonString, clazz);

143.}

144.}</code></code></code></code></code></code>

这样我们就完成了数据库查询缓存的实现。

 

本文转载自:http://www.it165.net/database/html/201507/13069.html

共有 人打赏支持
下一篇: redis查漏补缺
chworld

chworld

粉丝 18
博文 131
码字总数 24711
作品 0
昌平
程序员
私信 提问
Spring AOP整合redis 实现缓存统一管理

项目使用redis作为缓存数据,但面临着问题,比如,项目A,项目B都用到redis,而且用的redis都是一套集群,这样会带来一些问题。 问题:比如项目A的开发人员,要缓存一些热门数据,想到了red...

豆芽菜橙
2018/08/01
0
0
Spring Boot(11)——使用Spring Cache

使用Spring Cache Spring提供了Cache抽象,它允许我们声明哪些bean的哪些方法的外部调用需要使用Cache。方法调用使用了Cache后,在调用真实方法前会先从缓存中获取结果,缓存中如果没有则会调...

elim168
2018/12/22
0
0
AutoLoadCache 2.8 发布,缓存管理方案

AutoLoadCache 2.8 发布,此版本主要优化通过反射生成缓存Key的方法,将反射结果放入内存,提升反射效率。 在2.7版本中解决了, 当参数类型为 Class,自动生成的缓存Key会出问题。 AutoLoad...

qiujiayu
2016/01/19
2.6K
4
SpringBoot集成Redis实现缓存处理(Spring AOP实现)

第一章 需求分析 计划在Team的开源项目里加入Redis实现缓存处理,因为业务功能已经实现了一部分,通过写Redis工具类,然后引用,改动量较大,而且不可以实现解耦合,所以想到了Spring框架的A...

Javahih
2017/12/14
0
2
搞懂分布式技术14:Spring Boot使用注解集成Redis缓存

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a724888/article/details/80785403 为了提高性能,减少数据库的压力,使用缓存是非常好的手段之一。本文,讲解...

你的猫大哥
2018/06/23
0
0

没有更多内容

加载失败,请刷新页面

加载更多

mybatis缓存的装饰器模式

一般在开发生产中,对于新需求的实现,我们一般会有两种方式来处理,一种是直接修改已有组件的代码,另一种是使用继承方式。第一种显然会破坏已有组件的稳定性。第二种,会导致大量子类的出现...

算法之名
昨天
11
0
单元测试

右键方法 Go To --> Test,简便快速生成测试方法。 相关注解 @RunWith(SpringRunner.class) 表示要在测试环境中跑,底层实现是 jUnit测试工具。 @SpringBootTest 表示启动整个 Spring工程 @A...

imbiao
昨天
2
0
欧拉公式

欧拉公式表达式 欧拉公式的几何意 cosθ + j sinθ 是个复数,实数部分也就是实部为 cosθ ,虚数部分也就是虚部为 j sinθ ,对应复平面单位圆上的一个点。 根据欧拉公式和这个点可以用 复指...

sharelocked
昨天
4
0
burpsuite无法抓取https数据包

1.将浏览器和burpsuite的代理都设置好 2.在浏览器地址栏输入: http://burp 3.下载下面的证书,并将证书导入浏览器 cacert.der

Frost729
昨天
2
0
JeeSite4.x 消息管理、消息推送、消息提醒

实现统一的消息推送接口,包含PC消息、短信消息、邮件消息、微信消息等,无需让所有开发者了解消息是怎么发送出去的,只需了解消息发送接口即可。 所有推送消息均通过 MsgPushUtils 工具类发...

ThinkGem
昨天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部