百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

缓存管理方案 AutoLoadCache

toyiye 2024-06-21 12:33 10 浏览 0 评论

AutoLoadCache 是使用 Spring AOP 、 Annotation以及Spring EL表达式 来进行管理缓存的解决方案,同时基于AOP实现自动加载机制来达到数据“常驻内存”的目的。

现在使用的缓存技术很多,比如Redis、 Memcache 、 EhCache等,甚至还有使用ConcurrentHashMap 或HashTable 来实现缓存。但在缓存的使用上,每个人都有自己的实现方式,大部分是直接与业务代码绑定,随着业务的变化,要更换缓存方案时,非常麻烦。接下来我们就使用AOP + Annotation 来解决这个问题,同时使用自动加载机制来实现数据“常驻内存”。

Spring AOP这几年非常热门,使用也越来越多,但个人建议AOP只用于处理一些辅助的功能(比如:接下来我们要说的缓存),而不能把业务逻辑使用AOP中实现,尤其是在需要“事务”的环境中。

如下图所示:

AOP拦截到请求后:

  • 根据请求参数生成Key,后面我们会对生成Key的规则,进一步说明;
  • 如果是AutoLoad的,则请求相关参数,封装到AutoLoadTO中,并放到AutoLoadHandler中。
  • 根据Key去缓存服务器中取数据,如果取到数据,则返回数据,如果没有取到数据,则执行DAO中的方法,获取数据,同时将数据放到缓存中。如果是 AutoLoad的,则把最后加载时间,更新到AutoLoadTO中,最后返回数据;如是AutoLoad的请求,每次请求时,都会更新 AutoLoadTO中的 最后请求时间。
  • 为了减少并发,增加等待机制:如果多个用户同时取一个数据,那么先让第一个用户去DAO取数据,其它用户则等待其返回后,去缓存中获取,尝试一定次数后,如果还没获取到,再去DAO中取数据。

AutoLoadHandler(自动加载处理器)主要做的事情:当缓存即将过期时,去执行DAO的方法,获取数据,并将数据放到缓存中。为了防止 自动加载队列过大,设置了容量限制;同时会将超过一定时间没有用户请求的也会从自动加载队列中移除,把服务器资源释放出来,给真正需要的请求。

使用自加载的目的:

  • 避免在请求高峰时,因为缓存失效,而造成数据库压力无法承受;
  • 把一些耗时业务得以实现。
  • 把一些使用非常频繁的数据,使用自动加载,因为这样的数据缓存失效时,最容易造成服务器的压力过大。

分布式自动加载

如果将应用部署在多台服务器上,理论上可以认为自动加载队列是由这几台服务器共同完成自动加载任务。比如应用部署在A,B两台服务器上,A服务器自 动加载了数据D,(因为两台服务器的自动加载队列是独立的,所以加载的顺序也是一样的),接着有用户从B服务器请求数据D,这时会把数据D的最后加载时间 更新给B服务器,这样B服务器就不会重复加载数据D。

使用方法

1. Maven

<dependency>

<groupId>com.github.qiujiayu</groupId>

<artifactId>autoload-cache</artifactId>

<version>2.2</version>

</dependency>

2. Spring AOP配置

从0.4版本开始增加了Redis及Memcache的PointCut 的实现,直接在Spring 中用aop:config就可以使用。

Redis 配置:

<!-- Jedis 连接池配置 -->

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">

<property name="maxTotal" value="2000" />

<property name="maxIdle" value="100" />

<property name="minIdle" value="50" />

<property name="maxWaitMillis" value="2000" />

<property name="testOnBorrow" value="false" />

<property name="testOnReturn" value="false" />

<property name="testWhileIdle" value="false" />

</bean>

<bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool">

<constructor-arg ref="jedisPoolConfig" />

<constructor-arg>

<list>

<bean class="redis.clients.jedis.JedisShardInfo">

<constructor-arg value="${redis1.host}" />

<constructor-arg type="int" value="${redis1.port}" />

<constructor-arg value="instance:01" />

</bean>

<bean class="redis.clients.jedis.JedisShardInfo">

<constructor-arg value="${redis2.host}" />

<constructor-arg type="int" value="${redis2.port}" />

<constructor-arg value="instance:02" />

</bean>

<bean class="redis.clients.jedis.JedisShardInfo">

<constructor-arg value="${redis3.host}" />

<constructor-arg type="int" value="${redis3.port}" />

<constructor-arg value="instance:03" />

</bean>

</list>

</constructor-arg>

</bean>

<bean id="autoLoadConfig" class="com.jarvis.cache.to.AutoLoadConfig">

<property name="threadCnt" value="10" />

<property name="maxElement" value="20000" />

<property name="printSlowLog" value="true" />

<property name="slowLoadTime" value="500" />

<property name="sortType" value="1" />

<property name="checkFromCacheBeforeLoad" value="true" />

</bean>

<bean id="hessianSerializer" class="com.jarvis.cache.serializer.HessianSerializer" />

<bean id="cachePointCut" class="com.jarvis.cache.redis.ShardedCachePointCut" destroy-method="destroy">

<constructor-arg ref="autoLoadConfig" />

<property name="serializer" ref="hessianSerializer" />

<property name="shardedJedisPool" ref="shardedJedisPool" />

<property name="namespace" value="test_hessian" />

</bean>

Memcache 配置:

<bean id="memcachedClient" class="net.spy.memcached.spring.MemcachedClientFactoryBean">

<property name="servers" value="192.138.11.165:11211,192.138.11.166:11211" />

<property name="protocol" value="BINARY" />

<property name="transcoder">

<bean class="net.spy.memcached.transcoders.SerializingTranscoder">

<property name="compressionThreshold" value="1024" />

</bean>

</property>

<property name="opTimeout" value="2000" />

<property name="timeoutExceptionThreshold" value="1998" />

<property name="hashAlg">

<value type="net.spy.memcached.DefaultHashAlgorithm">KETAMA_HASH</value>

</property>

<property name="locatorType" value="CONSISTENT" />

<property name="failureMode" value="Redistribute" />

<property name="useNagleAlgorithm" value="false" />

</bean>

<bean id="hessianSerializer" class="com.jarvis.cache.serializer.HessianSerializer" />

<bean id="cachePointCut" class="com.jarvis.cache.memcache.CachePointCut" destroy-method="destroy">

<constructor-arg ref="autoLoadConfig" />

<property name="serializer" ref="hessianSerializer" />

<property name="memcachedClient", ref="memcachedClient" />

<property name="namespace" value="test" />

</bean>

AOP 配置:

<aop:config>

<aop:aspect ref="cachePointCut">

<aop:pointcut id="daoCachePointcut" expression="execution(public !void com.jarvis.cache_example.common.dao..*.*(..)) && @annotation(cache)" />

<aop:around pointcut-ref="daoCachePointcut" method="proceed" />

</aop:aspect>

<aop:aspect ref="cachePointCut" order="1000"><!-- order 参数控制 aop通知的优先级,值越小,优先级越高 ,在事务提交后删除缓存 -->

<aop:pointcut id="deleteCachePointcut" expression="execution(* com.jarvis.cache_example.common.dao..*.*(..)) && @annotation(cacheDelete)" />

<aop:after-returning pointcut-ref="deleteCachePointcut" method="deleteCache" returning="retVal"/>

</aop:aspect>

</aop:config>

通过Spring配置,能更好地支持,不同的数据使用不同的缓存服务器的情况。

实例代码

3. 将需要使用缓存操作的方法前增加 @Cache和 @CacheDelete注解(Redis为例子)

package com.jarvis.example.dao;

import ... ...

public class UserDAO {

/**

* 添加用户的同时,把数据放到缓存中

* @param userName

* @return

*/

@Cache(expire=600, key="'user'+#retVal.id", opType=CacheOpType.WRITE)

public UserTO addUser(String userName) {

UserTO user=new UserTO();

user.setName(userName);

Random rand=new Random();

// 数据库返回ID

Integer id=rand.nextInt(100000);

user.setId(id);

System.out.println("add User:" + id);

return user;

}

/**

*

* @param id

* @return

*/

@Cache(expire=600, autoload=true, key="'user'+#args[0]", condition="#args[0]>0")

public UserTO getUserById(Integer id) {

UserTO user=new UserTO();

user.setId(id);

user.setName("name" + id);

System.out.println("getUserById from dao");

return user;

}

/**

*

* @param user

*/

@CacheDelete({@CacheDeleteKey(value="'user'+#args[0].id", keyType=CacheKeyType.DEFINED)})

public void updateUserName(UserTO user) {

System.out.println("update user name:" + user.getName());

// save to db

}

// 注意:因为没有用 SpEL表达式,所以不需要用单引号

@CacheDelete({@CacheDeleteKey(value="user*", keyType=CacheKeyType.DEFINED)})

public void clearUserCache() {

System.out.println("clearUserCache");

}

// ------------------------以下是使用默认生成Key的方法--------------------

@Cache(expire=600, autoload=true, condition="#args[0]>0")

public UserTO getUserById2(Integer id) {

UserTO user=new UserTO();

user.setId(id);

user.setName("name" + id);

System.out.println("getUserById from dao");

return user;

}

@CacheDelete({@CacheDeleteKey(cls=UserDAO.class, method="getUserById2", argsEl={"#args[0].id"}, keyType=CacheKeyType.DEFAULT)})

public void updateUserName2(UserTO user) {

System.out.println("update user name:" + user.getName());

// save to db

}

@CacheDelete({@CacheDeleteKey(deleteByPrefixKey=true, cls=UserDAO.class, method="getUserById2", keyType=CacheKeyType.DEFAULT)})

public void clearUserCache2() {

System.out.println("clearUserCache");

// save to db

}

}

缓存Key的生成

  1. 使用Spring EL 表达式自定义缓存Key:CacheUtil.getDefinedCacheKey(String keySpEL, Object[] arguments),我们称之为自定义缓存Key:
  2. 例如:
  3. ?
  4. 1
  5. 2
  6. @Cache(expire=600, key="'goods.getGoodsById'+#args[0]")
  7. public GoodsTO getGoodsById(Long id){...}
  8. 注意:Spring EL表达式支持调整类的static 变量和方法,比如:"T(java.lang.Math).PI"。 所以对于复杂的参数,我们可以在Spring EL 表达式中使用:"T(com.jarvis.cache.CacheUtil).objectToHashStr(#args)",会生成一个比较短的 Hash字符串。
  9. 为了使用方便,在Spring EL表达式,"$hash(...)"会被替换为:"T(com.jarvis.cache.CacheUtil).getUniqueHashStr(...)",例如:
  10. ?
  11. 1
  12. 2
  13. @Cache(expire=720, key="'GOODS.getGoods:'+$hash(#args)")
  14. public List<GoodsTO> getGoods(GoodsCriteriaTO goodsCriteria){...}
  15. 生成的缓存Key为"GOODS.getGoods:xxx",xxx为args,的转在的字符串。
  16. 在拼缓存Key时,各项数据最好都用特殊字符进行分隔,否则缓存的Key有可能会乱的。比如:a,b 两个变量a=1,b=11,如果a=11,b=1,两个变量中间不加特殊字符,拼在一块,值是一样的。
  17. 默认生成缓存Key的方法:CacheUtil.getDefaultCacheKey(String className, String method, Object[] arguments, String subKeySpEL)
  • className 类名称
  • method 方法名称
  • arguments 参数
  • subKeySpEL SpringEL表达式

生成的Key格式为:{类名称}.{方法名称}{.SpringEL表达式运算结果}:{参数值的Hash字符串}。

当@Cache中不设置key值时,使用默认方式生成缓存Key。

根据自己的情况选择不同的缓存Key生成策略,用自定义Key使用比较灵活,但维护成本会高些,而且不能出现笔误。

subKeySpEL 使用说明

根据业务的需要,将缓存Key进行分组。举个例子,商品的评论列表:

package com.jarvis.example.dao;

import ... ...

public class GoodsCommentDAO{

@Cache(expire=600, subKeySpEL="#args[0]", autoload=true, requestTimeout=18000)

public List<CommentTO> getCommentListByGoodsId(Long goodsId, int pageNo, int pageSize) {

... ...

}

}

如果商品Id为:100,那么生成缓存Key格式为:com.jarvis.example.dao.GoodsCommentDAO.getCommentListByGoodsId.100:xxxx 在Redis中,能精确删除商品Id为100的评论列表,执行命令即可: del com.jarvis.example.dao.GoodsCommentDAO.getCommentListByGoodsId.100:*

SpringEL表达式使用起来确实非常方便,如果需要,@Cache中的expire,requestTimeout以及autoload参数都可以用SpringEL表达式来动态设置,但使用起来就变得复杂,所以我们没有这样做。

数据实时性

上面商品评论的例子中,如果用户发表了评论,要立即显示该如何来处理?

比较简单的方法就是,在发表评论成功后,立即把缓存中的数据也清除,这样就可以了。

package com.jarvis.example.dao;

import ... ...

public class GoodsCommentDAO{

@Cache(expire=600, subKeySpEL="#args[0]", autoload=true, requestTimeout=18000)

public List<CommentTO> getCommentListByGoodsId(Long goodsId, int pageNo, int pageSize) {

... ...

}

@CacheDelete({@CacheDeleteKey(cls=GoodsCommentDAO.class, method="getCommentListByGoodsId", deleteByPrefixKey=true, subKeySpEL=subKeySpEL="#args[0].goodsId" , keyType=CacheKeyType.DEFAULT)})

public void addComment(Comment comment) {

... ...// 省略添加评论代码

}

}

}

使用自定义缓存Key的方案:

package com.jarvis.example.dao;

import ... ...

public class GoodsCommentDAO{

@Cache(expire=600, key="'goods_comment_'+#args[0]+'.list__'+#args[1]+'_'+#args[2]", autoload=true, requestTimeout=18000)

public List<CommentTO> getCommentListByGoodsId(Long goodsId, int pageNo, int pageSize) {

... ...

}

@CacheDelete({@CacheDeleteKey(value="'goods_comment_'+#args[0].goodsId+'*'", keyType=CacheKeyType.DEFINED)}) // 删除当前所属商品的所有评论,不删除其它商品评论

public void addComment(Comment comment) {

... ...// 省略添加评论代码

}

}

删除缓存AOP 配置:

<aop:aspect ref="cachePointCut" order="1000">

<aop:pointcut id="deleteCachePointcut"

expression="execution(* com.jarvis.cache_example.common.dao..*.*(..)) && @annotation(cacheDelete)" />

<aop:after-returning pointcut-ref="deleteCachePointcut" method="deleteCache" returning="retVal"/>

</aop:aspect>

@Cache

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Cache {

/**

* 缓存的过期时间,单位:秒

*/

int expire();

/**

* 自定义缓存Key,如果不设置使用系统默认生成缓存Key的方法

* @return

*/

String key() default "";

/**

* 是否启用自动加载缓存

* @return

*/

boolean autoload() default false;

/**

* 自动缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,优化级高级autoload,例如:null != #args[0].keyword,当第一个参数的keyword属性为null时设置为自动加载。

* @return

*/

String autoloadCondition() default "";

/**

* 当autoload为true时,缓存数据在 requestTimeout 秒之内没有使用了,就不进行自动加载数据,如果requestTimeout为0时,会一直自动加载

* @return

*/

long requestTimeout() default 36000L;

/**

* 使用SpEL,将缓存key,根据业务需要进行二次分组

* @return

*/

String subKeySpEL() default "";

/**

* 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存,例如:"#args[0]==1",当第一个参数值为1时,才进缓存。

* @return

*/

String condition() default "";

/**

* 缓存的操作类型:默认是READ_WRITE,先缓存取数据,如果没有数据则从DAO中获取并写入缓存;如果是WRITE则从DAO取完数据后,写入缓存

* @return CacheOpType

*/

CacheOpType opType() default CacheOpType.READ_WRITE;

}

AutoLoadConfig 配置说明

  • threadCnt 处理自动加载队列的线程数量,默认值为:10;
  • maxElement 自动加载队列中允许存放的最大容量, 默认值为:20000
  • printSlowLog 是否打印比较耗时的请求,默认值为:true
  • slowLoadTime 当请求耗时超过此值时,记录目录(printSlowLog=true 时才有效),单位:毫秒,默认值:500;
  • sortType 自动加载队列排序算法, 0:按在Map中存储的顺序(即无序);1 :越接近过期时间,越耗时的排在最前;2:根据请求次数,倒序排序,请求次数越多,说明使用频率越高,造成并发的可能越大。更详细的说明,请查看代码com.jarvis.cache.type.AutoLoadQueueSortType
  • checkFromCacheBeforeLoad 加载数据之前去缓存服务器中检查,数据是否快过期,如果应用程序部署的服务器数量比较少,设置为false, 如果部署的服务器比较多,可以考虑设置为true

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码