- 浏览: 397746 次
- 性别:
- 来自: 杭州
文章分类
- 全部博客 (760)
- 股票日志 (26)
- Selenium (0)
- selenium 2 环境的搭建 (1)
- 并发 (7)
- 框架开发 (1)
- 动态代理 (2)
- Struts2 (2)
- POI (2)
- jdk (3)
- maven (31)
- spring (35)
- mysql (31)
- 工作机会 (3)
- xtream (1)
- oracle dbms_metadata GET_DDL (0)
- SSI (1)
- DB (61)
- powermock (4)
- java 基础 (25)
- 多线程 (11)
- 高手 (2)
- java 底层 (2)
- 专业网站 (1)
- 开发联想 (1)
- 开发联想 (1)
- bat文件 (2)
- 清queue 语句 (1)
- 清queue 语句 (1)
- jquery (7)
- html5 (1)
- Jenkins (10)
- Linux (17)
- 工作issue (2)
- tomcat log (3)
- jvm (23)
- 项目细节 (0)
- oracle (41)
- 泛型 (3)
- 新知识点 (1)
- 数据库ddl 语句 (0)
- AQ (2)
- jms (0)
- 网络资源 (6)
- github (6)
- Easymock (1)
- Dom 解析XML (1)
- windows命令 (2)
- java (7)
- 正则表达式 (5)
- sequence (1)
- oracle 表meta信息 (1)
- 小工具技巧 (1)
- 辅助工具 (1)
- Junit (1)
- 泛型 generic (2)
- Java程序设计 (1)
- cglib (2)
- 架构师之路 (1)
- 数据库连接池 (5)
- c3p0 (1)
- eclipse使用 (1)
- oracle sql plus (1)
- 码农人生 (3)
- SVN (15)
- sqlplus (2)
- jsoup (1)
- 网络爬虫 (2)
- 新技能 (1)
- zookeeper (4)
- hadoop (1)
- SVNKIT (1)
- 从工具到知识点的整理 (1)
- log4j (13)
- 读文件 (0)
- 转义字符 (1)
- command (1)
- web service (3)
- 锁 (1)
- shell 脚本 (1)
- 遇到的错误 (2)
- tomcat (14)
- 房产 (5)
- bootstrap jquery ui (1)
- easyui (2)
- 个人征信 (1)
- 读写分离 (1)
- 备份 (1)
- rmi (6)
- webservice (1)
- JMX (4)
- 内存管理 (3)
- java设计 (1)
- timer (1)
- lock (2)
- concurrent (2)
- collection (1)
- tns (1)
- java基础 (15)
- File (1)
- 本机资源 (1)
- bat (1)
- windows (4)
- 数据结构 (3)
- 代码安全 (1)
- 作用域 (1)
- 图 (2)
- jvm内存结构 (1)
- 计算机思想 (1)
- quartz (6)
- Mongo DB (2)
- Nosql (4)
- sql (5)
- 第三方Java 工具 jar 项目 (2)
- drools (1)
- java swing (2)
- 调用console (1)
- runtime (1)
- process (1)
- swing (2)
- grouplayout (1)
- dubbo (0)
- bootstrap (0)
- nodejs (2)
- SVN hooks (1)
- jdbc (3)
- jdbc error (1)
- precedure (1)
- partition_key (1)
- active mq (1)
- blob (2)
- Eclipse (6)
- web server (1)
- bootstrapt (2)
- struts (1)
- ajax (1)
- js call back (1)
- 思想境界拓展 (1)
- JIRA (1)
- log (1)
- jaxb (3)
- xml java互相转换 (1)
- 装修 (2)
- 互联网 (2)
- threadlocal (3)
- mybatis (22)
- xstream (1)
- 排序 (1)
- 股票资源 (1)
- RPC (2)
- NIO (3)
- http client (6)
- 他人博客 (1)
- 代理服务器 (1)
- 网络 (2)
- web (1)
- 股票 (5)
- deadlock (1)
- JConsole (2)
- activemq (3)
- oralce (1)
- 游标 (1)
- 12月13日道富内部培训 (0)
- grant (1)
- 速查 (2)
- classloader (4)
- netty (4)
- 设计模式 (2)
- 缓存 (2)
- ehcache (2)
- framework (1)
- 内存分析 (2)
- dump (1)
- memory (2)
- 多高线程,并发 (1)
- hbase (2)
- 分布式系统 (1)
- socket (3)
- socket (1)
- 面试问题 (1)
- jetty (2)
- http (2)
- 源码 (1)
- 日志 (2)
- jni (1)
- 编码约定 (1)
- memorycache (1)
- redis (13)
- 杂谈 (1)
- drool (1)
- blockingqueue (1)
- ScheduledExecutorService (1)
- 网页爬虫 (1)
- httpclient (4)
- httpparser (1)
- map (1)
- 单例 (1)
- synchronized (2)
- thread (1)
- job (1)
- hashcode (1)
- copyonwriteArrayList (2)
- 录制声音 (1)
- java 标准 (2)
- SSL/TLS (1)
- itext (1)
- pdf (1)
- 钻石 (2)
- sonar (1)
- unicode (1)
- 编码 (4)
- html (1)
- SecurityManager (1)
- 坑 (1)
- Restful (2)
- svn hook (1)
- concurrentHashMap (1)
- 垃圾回收 (1)
- vbs (8)
- visual svn (2)
- power shell (1)
- wmi (3)
- mof (2)
- c# (1)
- concurrency (1)
- 劳动法 (1)
- 三国志游戏 (2)
- 三国 (1)
- 洪榕 (2)
- 金融投资知识 (1)
- motan (1)
- tkmybatis mapper (1)
- 工商注册信息查询 (1)
- consul (1)
- 支付业务知识 (2)
- 数据库备份 (1)
- 字段设计 (1)
- 字段 (1)
- dba (1)
- 插件 (2)
- PropEdit插件 (1)
- web工程 (1)
- 银行业知识 (2)
- 国内托管银行 (1)
- 数据库 (1)
- 事务 (2)
- git (18)
- component-scan (1)
- 私人 (0)
- db2 (14)
- alias (1)
- 住房 (1)
- 户口 (1)
- fastjson (1)
- test (6)
- RSA (2)
- 密钥 (1)
- putty (1)
- sftp (1)
- 加密 (1)
- 公钥私钥 (3)
- markdown (1)
- sweet (1)
- sourcetree (1)
- 好工具 (1)
- cmd (1)
- scp (1)
- notepad++ (1)
- ssh免密登录 (1)
- https (1)
- ssl (2)
- js (2)
- h2 (1)
- 内存 (2)
- 浏览器 (1)
- js特效 (1)
- io (1)
- 乱码 (1)
- 小工具 (1)
- 每周技术任务 (1)
- mongodb (7)
- 内存泄漏 (1)
- 码云 (2)
- 如何搭建java 视频服务器 tomcat (1)
- 资源 (1)
- 书 (1)
- 四色建模法 (1)
- 建模 (1)
- 配置 (1)
- 职位 (1)
- nginx (1)
- excel (1)
- log4j2 (2)
- 做菜 (1)
- jmap (1)
- jspwiki (1)
- activiti (1)
- 工作流引擎 (1)
- 安卓 (1)
- acitviti 例子 (1)
- 二维码 (1)
- 工作流 (1)
- powerdesign (2)
- 软件设计 (1)
- 乐观锁 (1)
- 王者荣耀 (1)
- session (2)
- token (5)
- cookie (4)
- springboot (24)
- jwt (2)
- 项目路径 (1)
- magicbook (1)
- requestType (1)
- json (2)
- swagger (1)
- eolinker (1)
- springdata (1)
- springmvc (1)
- controlleradvice (1)
- profile (1)
- 银行四要素 (1)
- 支付人员资源 (1)
- 支付渠道 (1)
- yaml (1)
- 中文编码 (1)
- mongo (2)
- serializable (1)
- 序列化 (1)
- zyd (1)
- unittest (1)
- 工具 (1)
- Something (1)
- 通达信 (1)
- protobuf (1)
- 算法 (1)
- springcloud (2)
- hikari (1)
- rocketmq (7)
- cachecloud (1)
- serfj (1)
- axure (1)
- lombok (1)
- 分布式锁 (1)
- 线程 (2)
- 同步代码块 (1)
- cobar (1)
- mq (1)
- rabbitmq (1)
- 定时执行 (1)
- 支付系统 (3)
- 唱歌 (1)
- elasticjob (1)
- 定时任务 (1)
- 界面 (1)
- flink (2)
- 大数据 (1)
- 接私活 (0)
- 内部培训 (2)
最新评论
-
dannyhz:
做股票从短线 试水,然后 慢慢发现 波段和 中期的故事可挖, ...
搭台唱戏 -
dannyhz:
http://developer.51cto.com/art/ ...
如何自己开发框架 它的注意点是什么
http://blog.csdn.net/liaoyulin0609/article/details/51787020
在上篇《性能优化-缓存篇》中已经从理论上介绍了缓存,其实,缓存简单易用,更多精力关注的是根据实际业务的选择缓存策略。
本文主要介绍为什么要构建ehcache+redis两级缓存?以及在实战中如何实现?思考如何配置缓存策略更合适?这样的方案可能遗留什么问题?JUST DO IT! GO!
问题描述
场景:我们的应用系统是分布式集群的,可横向扩展的。应用中某个接口操作满足以下一个或多个条件:
1. 接口运行复杂代价大,
2. 接口返回数据量大,
3. 接口的数据基本不会更改,
4. 接口数据一致性要求不高(只需满足最终一致)。
此时,我们会考虑将这个接口的返回值做缓存。考虑到上述条件,我们需要一套高可用分布式的缓存集群,并具备持久化功能,备选的有ehcache集群或redis主备(sentinel)。
ehcache集群因为节点之间数据同步通过组播的方式,可能带来的问题:节点间大量的数据复制带来额外的开销,在节点多的情况下此问题越发严重,N个节点会出现N-1次网络传输数据进行同步。(见下图)
redis主备由于作为中心节点提供缓存,其他节点都向redis中心节点取数据,所以,一次网络传输即可。(当然此处的一次网络代价跟组播的代价是不一样的)但是,随着访问量增大,大量的缓存数据访问使得应用服务器和缓存服务器之间的网络I/O消耗越大。(见下图)
提出方案
那么要怎么处理呢?所以两级缓存的思想诞生了,在redis的方案上做一步优化,在缓存到远程redis的同时,缓存一份到本地进程ehcache(此处的ehcache不用做集群,避免组播带来的开销),取缓存的时候会先取本地,没有会向redis请求,这样会减少应用服务器<–>缓存服务器redis之间的网络开销。(见下图)
如果用过j2cache的都应该知道,oschina用j2cache这种两级缓存,实践证明了该方案是可行的。该篇使用spring+ehcache+redis实现更加简洁。
方案实施
1、 spring和ehcache集成
主要获取ehcache作为操作ehcache的对象。
ehcache.xml 代码如下:
<ehcache updateCheck="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.sf.net/ehcache.xsd">
<diskStore path="java.io.tmpdir/ehcache"/>
<!-- 默认的管理策略
maxElementsOnDisk: 在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
diskPersistent: 是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。
diskExpiryThreadIntervalSeconds:对象检测线程运行时间间隔。标识对象状态(过期/持久化)的线程多长时间运行一次。
-->
<defaultCache maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- 对象无过期,一个1000长度的队列,最近最少使用的对象被删除 -->
<cache name="userCache"
maxElementsInMemory="1000"
eternal="true"
overflowToDisk="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LFU">
</cache>
<!-- 组播方式:multicastGroupPort需要保证与其他系统不重复,进行端口注册 -->
<!-- 若因未注册,配置了重复端口,造成权限缓存数据异常,请自行解决 -->
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="peerDiscovery=automatic,
multicastGroupAddress=230.0.0.1,
multicastGroupPort=4546, timeToLive=1"/>
<!-- replicatePuts=true | false – 当一个新元素增加到缓存中的时候是否要复制到其他的peers. 默认是true。 -->
<!-- replicateUpdates=true | false – 当一个已经在缓存中存在的元素被覆盖时是否要进行复制。默认是true。 -->
<!-- replicateRemovals= true | false – 当元素移除的时候是否进行复制。默认是true。 -->
<!-- replicateAsynchronously=true | false – 复制方式是异步的(指定为true时)还是同步的(指定为false时)。默认是true。 -->
<!-- replicatePutsViaCopy=true | false – 当一个新增元素被拷贝到其他的cache中时是否进行复制指定为true时为复制,默认是true。 -->
<!-- replicateUpdatesViaCopy=true | false – 当一个元素被拷贝到其他的cache中时是否进行复制(指定为true时为复制),默认是true。 -->
<cache name="webCache_LT"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
memoryStoreEvictionPolicy="LRU">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateRemovals=true"/>
<bootstrapCacheLoaderFactory
class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
</cache>
<cache name="webCache_ST"
maxElementsInMemory="1000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="300"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateRemovals=true"/>
<bootstrapCacheLoaderFactory
class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
</cache>
</ehcache>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
spring.xml中注入ehcacheManager和ehCache对象,ehcacheManager是需要加载ehcache.xml配置信息,创建ehcache.xml中配置不同策略的cache。
<!-- ehCache 配置管理器 -->
<bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
<!--true:单例,一个cacheManager对象共享;false:多个对象独立 -->
<property name="shared" value="true" />
<property name="cacheManagerName" value="ehcacheManager" />
</bean>
<!-- ehCache 操作对象 -->
<bean id="ehCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheName" value="ehCache"/>
<property name="cacheManager" ref="ehcacheManager"/>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2、 spring和redis集成
主要获取redisTemplate作为操作redis的对象。
redis.properties配置信息
#host 写入redis服务器地址
redis.ip=127.0.0.1
#Port
redis.port=6379
#Passord
#redis.password=123456
#连接超时30000
redis.timeout=30
#最大分配的对象数
redis.pool.maxActive=100
#最大能够保持idel状态的对象数
redis.pool.maxIdle=30
#当池内没有返回对象时,最大等待时间
redis.pool.maxWait=1000
#当调用borrow Object方法时,是否进行有效性检查
redis.pool.testOnBorrow=true
#当调用return Object方法时,是否进行有效性检查
redis.pool.testOnReturn=true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring注入jedisPool、redisConnFactory、redisTemplate对象
<!-- 加载redis.propertis -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:redis.properties"/>
</bean>
<!-- Redis 连接池 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="${redis.pool.maxActive}" />
<property name="maxIdle" value="${redis.pool.maxIdle}" />
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
<property name="testOnReturn" value="${redis.pool.testOnReturn}" />
<property name="maxWaitMillis" value="${redis.pool.maxWait}" />
</bean>
<!-- Redis 连接工厂 -->
<bean id="redisConnFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.ip}" />
<property name="port" value="${redis.port}" />
<!-- property name="password" value="${redis.password}" -->
<property name="timeout" value="${redis.timeout}" />
<property name="poolConfig" ref="jedisPool" />
</bean>
<!-- redis 操作对象 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="redisConnFactory" />
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
3、 spring集成ehcache和redis
通过上面两步注入的ehcache和redisTemplate我们就能自定义一个方法将两者整合起来。详见EhRedisCache类。
EhRedisCache.java
/**
* 两级缓存,一级:ehcache,二级为redisCache
* @author yulin
*
*/
public class EhRedisCache implements Cache{
private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
private String name;
private net.sf.ehcache.Cache ehCache;
private RedisTemplate<String, Object> redisTemplate;
private long liveTime = 1*60*60; //默认1h=1*60*60
@Override
public String getName() {
return this.name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
public ValueWrapper get(Object key) {
Element value = ehCache.get(key);
LOG.info("Cache L1 (ehcache) :{}={}",key,value);
if (value!=null) {
return (value != null ? new SimpleValueWrapper(value.getObjectValue()) : null);
}
//TODO 这样会不会更好?访问10次EhCache 强制访问一次redis 使得数据不失效
final String keyStr = key.toString();
Object objectValue = redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection)
throws DataAccessException {
byte[] key = keyStr.getBytes();
byte[] value = connection.get(key);
if (value == null) {
return null;
}
//每次获得,重置缓存过期时间
if (liveTime > 0) {
connection.expire(key, liveTime);
}
return toObject(value);
}
},true);
ehCache.put(new Element(key, objectValue));//取出来之后缓存到本地
LOG.info("Cache L2 (redis) :{}={}",key,objectValue);
return (objectValue != null ? new SimpleValueWrapper(objectValue) : null);
}
@Override
public void put(Object key, Object value) {
ehCache.put(new Element(key, value));
final String keyStr = key.toString();
final Object valueStr = value;
redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection)
throws DataAccessException {
byte[] keyb = keyStr.getBytes();
byte[] valueb = toByteArray(valueStr);
connection.set(keyb, valueb);
if (liveTime > 0) {
connection.expire(keyb, liveTime);
}
return 1L;
}
},true);
}
@Override
public void evict(Object key) {
ehCache.remove(key);
final String keyStr = key.toString();
redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection)
throws DataAccessException {
return connection.del(keyStr.getBytes());
}
},true);
}
@Override
public void clear() {
ehCache.removeAll();
redisTemplate.execute(new RedisCallback<String>() {
public String doInRedis(RedisConnection connection)
throws DataAccessException {
connection.flushDb();
return "clear done.";
}
},true);
}
public net.sf.ehcache.Cache getEhCache() {
return ehCache;
}
public void setEhCache(net.sf.ehcache.Cache ehCache) {
this.ehCache = ehCache;
}
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public long getLiveTime() {
return liveTime;
}
public void setLiveTime(long liveTime) {
this.liveTime = liveTime;
}
public void setName(String name) {
this.name = name;
}
/**
* 描述 : Object转byte[]. <br>
* @param obj
* @return
*/
private byte[] toByteArray(Object obj) {
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
bytes = bos.toByteArray();
oos.close();
bos.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return bytes;
}
/**
* 描述 : byte[]转Object . <br>
* @param bytes
* @return
*/
private Object toObject(byte[] bytes) {
Object obj = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
obj = ois.readObject();
ois.close();
bis.close();
} catch (IOException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return obj;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
spring注入自定义缓存
<!-- 自定义ehcache+redis-->
<bean id="ehRedisCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean id="ehRedisCache" class="org.musicmaster.yulin.ercache.EhRedisCache">
<property name="redisTemplate" ref="redisTemplate" />
<property name="ehCache" ref="ehCache"/>
<property name="name" value="userCache"/>
<!-- <property name="liveTime" value="3600"/> -->
</bean>
</set>
</property>
</bean>
<!-- 注解声明 -->
<cache:annotation-driven cache-manager="ehRedisCacheManager"
proxy-target-class="true" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4、 模拟问题中提到的接口
此处假设该接口满足上述条件。
UserService.java
public interface UserService {
User findById(long id);
List<User> findByPage(int startIndex, int limit);
List<User> findBySex(Sex sex);
List<User> findByAge(int lessAge);
List<User> findByUsers(List<User> users);
boolean update(User user);
boolean deleteById(long id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService{
private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
@Cacheable("userCache")
@Override
public User findById(long id) {
LOG.info("visit business service findById,id:{}",id);
User user = new User();
user.setId(id);
user.setUserName("tony");
user.setPassWord("******");
user.setSex(Sex.M);
user.setAge(32);
//耗时操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return user;
}
@Override
public List<User> findByPage(int startIndex, int limit) {
return null;
}
@Cacheable("userCache")
@Override
public List<User> findBySex(Sex sex) {
LOG.info("visit business service findBySex,sex:{}",sex);
List<User> users = new ArrayList<User>();
for (int i = 0; i < 5; i++) {
User user = new User();
user.setId(i);
user.setUserName("tony"+i);
user.setPassWord("******");
user.setSex(sex);
user.setAge(32+i);
users.add(user);
}
return users;
}
@Override
public List<User> findByAge(int lessAge) {
// TODO Auto-generated method stub
return null;
}
//FIXME 此处将list参数的地址作为key存储,是否有问题?
@Cacheable("userCache")
@Override
public List<User> findByUsers(List<User> users) {
LOG.info("visit business service findByUsers,users:{}",users);
return users;
}
@CacheEvict("userCache")
@Override
public boolean update(User user) {
return true;
}
@CacheEvict("userCache")
@Override
public boolean deleteById(long id) {
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
User.java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
public enum Sex{
M,FM
}
private long id;
private String userName;
private String passWord;
private int age;
private Sex sex;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Sex getSex() {
return sex;
}
public void setSex(Sex sex) {
this.sex = sex;
}
@Override
public String toString() {
return "User [id=" + id + ", userName=" + userName + ", passWord="
+ passWord + ", age=" + age + ", sex=" + sex + "]";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
实施结果
我们写个测试类来模拟下
TestEhRedisCache.java
public class TestEhRedisCache{
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-ehRedisCache.xml");
UserService userService= (UserService) context.getBean("userServiceImpl");
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
TEST1 输出结果:
Cache L1 (ehcache) :UserServiceImpl/findById/5=null
Cache L2 (redis) :UserServiceImpl/findById/5=null
visit business service findById,id:5
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
上面第一次访问,一级缓存ehcache和二级缓存redis都没有数据,访问接口耗时操作,打印日志:
visit business service findById,id:5
第二次之后的访问,都会访问一级缓存ehcache,此时响应速度很快。
TEST2 在TEST1结束后,我们在liveTime的时间内,也就是redis缓存还未过期再次执行,会出现以下结果
Cache L1 (ehcache) :UserServiceImpl/findById/5=null
Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
由于TEST1执行完结束后,ehcache为进程间的缓存,自然随着运行结束而释放,所以TEST2出现:
Cache L1 (ehcache) :UserServiceImpl/findById/5=null
然而在第二次访问二级缓存redis,还未到缓存过期时间,所以在redis中找到数据(同时数据入一级缓存ehcache):
Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=**, age=32, sex=M]
此处不会visit….没有经过接口的耗时操作,接下来数据都可以在本地缓存ehcache中获取。
总结
经过demo实践结果符合预期效果,还需更大规模的测试。遗留了几个问题,在代码处的TODO和FIXME中,留给大家一起来思考,一起来探讨解决。问题解决和源码下载:《spring + ehcache + redis两级缓存实战篇(2)》
引用
在上篇《性能优化-缓存篇》中已经从理论上介绍了缓存,其实,缓存简单易用,更多精力关注的是根据实际业务的选择缓存策略。
本文主要介绍为什么要构建ehcache+redis两级缓存?以及在实战中如何实现?思考如何配置缓存策略更合适?这样的方案可能遗留什么问题?JUST DO IT! GO!
问题描述
场景:我们的应用系统是分布式集群的,可横向扩展的。应用中某个接口操作满足以下一个或多个条件:
1. 接口运行复杂代价大,
2. 接口返回数据量大,
3. 接口的数据基本不会更改,
4. 接口数据一致性要求不高(只需满足最终一致)。
此时,我们会考虑将这个接口的返回值做缓存。考虑到上述条件,我们需要一套高可用分布式的缓存集群,并具备持久化功能,备选的有ehcache集群或redis主备(sentinel)。
ehcache集群因为节点之间数据同步通过组播的方式,可能带来的问题:节点间大量的数据复制带来额外的开销,在节点多的情况下此问题越发严重,N个节点会出现N-1次网络传输数据进行同步。(见下图)
redis主备由于作为中心节点提供缓存,其他节点都向redis中心节点取数据,所以,一次网络传输即可。(当然此处的一次网络代价跟组播的代价是不一样的)但是,随着访问量增大,大量的缓存数据访问使得应用服务器和缓存服务器之间的网络I/O消耗越大。(见下图)
提出方案
那么要怎么处理呢?所以两级缓存的思想诞生了,在redis的方案上做一步优化,在缓存到远程redis的同时,缓存一份到本地进程ehcache(此处的ehcache不用做集群,避免组播带来的开销),取缓存的时候会先取本地,没有会向redis请求,这样会减少应用服务器<–>缓存服务器redis之间的网络开销。(见下图)
如果用过j2cache的都应该知道,oschina用j2cache这种两级缓存,实践证明了该方案是可行的。该篇使用spring+ehcache+redis实现更加简洁。
方案实施
1、 spring和ehcache集成
主要获取ehcache作为操作ehcache的对象。
ehcache.xml 代码如下:
<ehcache updateCheck="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.sf.net/ehcache.xsd">
<diskStore path="java.io.tmpdir/ehcache"/>
<!-- 默认的管理策略
maxElementsOnDisk: 在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
diskPersistent: 是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。
diskExpiryThreadIntervalSeconds:对象检测线程运行时间间隔。标识对象状态(过期/持久化)的线程多长时间运行一次。
-->
<defaultCache maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- 对象无过期,一个1000长度的队列,最近最少使用的对象被删除 -->
<cache name="userCache"
maxElementsInMemory="1000"
eternal="true"
overflowToDisk="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LFU">
</cache>
<!-- 组播方式:multicastGroupPort需要保证与其他系统不重复,进行端口注册 -->
<!-- 若因未注册,配置了重复端口,造成权限缓存数据异常,请自行解决 -->
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="peerDiscovery=automatic,
multicastGroupAddress=230.0.0.1,
multicastGroupPort=4546, timeToLive=1"/>
<!-- replicatePuts=true | false – 当一个新元素增加到缓存中的时候是否要复制到其他的peers. 默认是true。 -->
<!-- replicateUpdates=true | false – 当一个已经在缓存中存在的元素被覆盖时是否要进行复制。默认是true。 -->
<!-- replicateRemovals= true | false – 当元素移除的时候是否进行复制。默认是true。 -->
<!-- replicateAsynchronously=true | false – 复制方式是异步的(指定为true时)还是同步的(指定为false时)。默认是true。 -->
<!-- replicatePutsViaCopy=true | false – 当一个新增元素被拷贝到其他的cache中时是否进行复制指定为true时为复制,默认是true。 -->
<!-- replicateUpdatesViaCopy=true | false – 当一个元素被拷贝到其他的cache中时是否进行复制(指定为true时为复制),默认是true。 -->
<cache name="webCache_LT"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
memoryStoreEvictionPolicy="LRU">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateRemovals=true"/>
<bootstrapCacheLoaderFactory
class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
</cache>
<cache name="webCache_ST"
maxElementsInMemory="1000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="300"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateRemovals=true"/>
<bootstrapCacheLoaderFactory
class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
</cache>
</ehcache>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
spring.xml中注入ehcacheManager和ehCache对象,ehcacheManager是需要加载ehcache.xml配置信息,创建ehcache.xml中配置不同策略的cache。
<!-- ehCache 配置管理器 -->
<bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
<!--true:单例,一个cacheManager对象共享;false:多个对象独立 -->
<property name="shared" value="true" />
<property name="cacheManagerName" value="ehcacheManager" />
</bean>
<!-- ehCache 操作对象 -->
<bean id="ehCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheName" value="ehCache"/>
<property name="cacheManager" ref="ehcacheManager"/>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2、 spring和redis集成
主要获取redisTemplate作为操作redis的对象。
redis.properties配置信息
#host 写入redis服务器地址
redis.ip=127.0.0.1
#Port
redis.port=6379
#Passord
#redis.password=123456
#连接超时30000
redis.timeout=30
#最大分配的对象数
redis.pool.maxActive=100
#最大能够保持idel状态的对象数
redis.pool.maxIdle=30
#当池内没有返回对象时,最大等待时间
redis.pool.maxWait=1000
#当调用borrow Object方法时,是否进行有效性检查
redis.pool.testOnBorrow=true
#当调用return Object方法时,是否进行有效性检查
redis.pool.testOnReturn=true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring注入jedisPool、redisConnFactory、redisTemplate对象
<!-- 加载redis.propertis -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:redis.properties"/>
</bean>
<!-- Redis 连接池 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="${redis.pool.maxActive}" />
<property name="maxIdle" value="${redis.pool.maxIdle}" />
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
<property name="testOnReturn" value="${redis.pool.testOnReturn}" />
<property name="maxWaitMillis" value="${redis.pool.maxWait}" />
</bean>
<!-- Redis 连接工厂 -->
<bean id="redisConnFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.ip}" />
<property name="port" value="${redis.port}" />
<!-- property name="password" value="${redis.password}" -->
<property name="timeout" value="${redis.timeout}" />
<property name="poolConfig" ref="jedisPool" />
</bean>
<!-- redis 操作对象 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="redisConnFactory" />
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
3、 spring集成ehcache和redis
通过上面两步注入的ehcache和redisTemplate我们就能自定义一个方法将两者整合起来。详见EhRedisCache类。
EhRedisCache.java
/**
* 两级缓存,一级:ehcache,二级为redisCache
* @author yulin
*
*/
public class EhRedisCache implements Cache{
private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
private String name;
private net.sf.ehcache.Cache ehCache;
private RedisTemplate<String, Object> redisTemplate;
private long liveTime = 1*60*60; //默认1h=1*60*60
@Override
public String getName() {
return this.name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
public ValueWrapper get(Object key) {
Element value = ehCache.get(key);
LOG.info("Cache L1 (ehcache) :{}={}",key,value);
if (value!=null) {
return (value != null ? new SimpleValueWrapper(value.getObjectValue()) : null);
}
//TODO 这样会不会更好?访问10次EhCache 强制访问一次redis 使得数据不失效
final String keyStr = key.toString();
Object objectValue = redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection)
throws DataAccessException {
byte[] key = keyStr.getBytes();
byte[] value = connection.get(key);
if (value == null) {
return null;
}
//每次获得,重置缓存过期时间
if (liveTime > 0) {
connection.expire(key, liveTime);
}
return toObject(value);
}
},true);
ehCache.put(new Element(key, objectValue));//取出来之后缓存到本地
LOG.info("Cache L2 (redis) :{}={}",key,objectValue);
return (objectValue != null ? new SimpleValueWrapper(objectValue) : null);
}
@Override
public void put(Object key, Object value) {
ehCache.put(new Element(key, value));
final String keyStr = key.toString();
final Object valueStr = value;
redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection)
throws DataAccessException {
byte[] keyb = keyStr.getBytes();
byte[] valueb = toByteArray(valueStr);
connection.set(keyb, valueb);
if (liveTime > 0) {
connection.expire(keyb, liveTime);
}
return 1L;
}
},true);
}
@Override
public void evict(Object key) {
ehCache.remove(key);
final String keyStr = key.toString();
redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection)
throws DataAccessException {
return connection.del(keyStr.getBytes());
}
},true);
}
@Override
public void clear() {
ehCache.removeAll();
redisTemplate.execute(new RedisCallback<String>() {
public String doInRedis(RedisConnection connection)
throws DataAccessException {
connection.flushDb();
return "clear done.";
}
},true);
}
public net.sf.ehcache.Cache getEhCache() {
return ehCache;
}
public void setEhCache(net.sf.ehcache.Cache ehCache) {
this.ehCache = ehCache;
}
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public long getLiveTime() {
return liveTime;
}
public void setLiveTime(long liveTime) {
this.liveTime = liveTime;
}
public void setName(String name) {
this.name = name;
}
/**
* 描述 : Object转byte[]. <br>
* @param obj
* @return
*/
private byte[] toByteArray(Object obj) {
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
bytes = bos.toByteArray();
oos.close();
bos.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return bytes;
}
/**
* 描述 : byte[]转Object . <br>
* @param bytes
* @return
*/
private Object toObject(byte[] bytes) {
Object obj = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
obj = ois.readObject();
ois.close();
bis.close();
} catch (IOException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return obj;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
spring注入自定义缓存
<!-- 自定义ehcache+redis-->
<bean id="ehRedisCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean id="ehRedisCache" class="org.musicmaster.yulin.ercache.EhRedisCache">
<property name="redisTemplate" ref="redisTemplate" />
<property name="ehCache" ref="ehCache"/>
<property name="name" value="userCache"/>
<!-- <property name="liveTime" value="3600"/> -->
</bean>
</set>
</property>
</bean>
<!-- 注解声明 -->
<cache:annotation-driven cache-manager="ehRedisCacheManager"
proxy-target-class="true" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4、 模拟问题中提到的接口
此处假设该接口满足上述条件。
UserService.java
public interface UserService {
User findById(long id);
List<User> findByPage(int startIndex, int limit);
List<User> findBySex(Sex sex);
List<User> findByAge(int lessAge);
List<User> findByUsers(List<User> users);
boolean update(User user);
boolean deleteById(long id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService{
private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
@Cacheable("userCache")
@Override
public User findById(long id) {
LOG.info("visit business service findById,id:{}",id);
User user = new User();
user.setId(id);
user.setUserName("tony");
user.setPassWord("******");
user.setSex(Sex.M);
user.setAge(32);
//耗时操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return user;
}
@Override
public List<User> findByPage(int startIndex, int limit) {
return null;
}
@Cacheable("userCache")
@Override
public List<User> findBySex(Sex sex) {
LOG.info("visit business service findBySex,sex:{}",sex);
List<User> users = new ArrayList<User>();
for (int i = 0; i < 5; i++) {
User user = new User();
user.setId(i);
user.setUserName("tony"+i);
user.setPassWord("******");
user.setSex(sex);
user.setAge(32+i);
users.add(user);
}
return users;
}
@Override
public List<User> findByAge(int lessAge) {
// TODO Auto-generated method stub
return null;
}
//FIXME 此处将list参数的地址作为key存储,是否有问题?
@Cacheable("userCache")
@Override
public List<User> findByUsers(List<User> users) {
LOG.info("visit business service findByUsers,users:{}",users);
return users;
}
@CacheEvict("userCache")
@Override
public boolean update(User user) {
return true;
}
@CacheEvict("userCache")
@Override
public boolean deleteById(long id) {
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
User.java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
public enum Sex{
M,FM
}
private long id;
private String userName;
private String passWord;
private int age;
private Sex sex;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Sex getSex() {
return sex;
}
public void setSex(Sex sex) {
this.sex = sex;
}
@Override
public String toString() {
return "User [id=" + id + ", userName=" + userName + ", passWord="
+ passWord + ", age=" + age + ", sex=" + sex + "]";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
实施结果
我们写个测试类来模拟下
TestEhRedisCache.java
public class TestEhRedisCache{
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-ehRedisCache.xml");
UserService userService= (UserService) context.getBean("userServiceImpl");
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
System.out.println(userService.findById(5l));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
TEST1 输出结果:
Cache L1 (ehcache) :UserServiceImpl/findById/5=null
Cache L2 (redis) :UserServiceImpl/findById/5=null
visit business service findById,id:5
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
上面第一次访问,一级缓存ehcache和二级缓存redis都没有数据,访问接口耗时操作,打印日志:
visit business service findById,id:5
第二次之后的访问,都会访问一级缓存ehcache,此时响应速度很快。
TEST2 在TEST1结束后,我们在liveTime的时间内,也就是redis缓存还未过期再次执行,会出现以下结果
Cache L1 (ehcache) :UserServiceImpl/findById/5=null
Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
User [id=5, userName=tony, passWord=******, age=32, sex=M]
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
由于TEST1执行完结束后,ehcache为进程间的缓存,自然随着运行结束而释放,所以TEST2出现:
Cache L1 (ehcache) :UserServiceImpl/findById/5=null
然而在第二次访问二级缓存redis,还未到缓存过期时间,所以在redis中找到数据(同时数据入一级缓存ehcache):
Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=**, age=32, sex=M]
此处不会visit….没有经过接口的耗时操作,接下来数据都可以在本地缓存ehcache中获取。
总结
经过demo实践结果符合预期效果,还需更大规模的测试。遗留了几个问题,在代码处的TODO和FIXME中,留给大家一起来思考,一起来探讨解决。问题解决和源码下载:《spring + ehcache + redis两级缓存实战篇(2)》
发表评论
-
实现redis 分布式锁的 改进方式,比-传-化-的优秀 使用redis的setnx()
2018-10-29 15:12 327引用 https://blog.csdn.net/xiaoly ... -
spring mvc的工作原理 有图很清晰
2018-09-29 11:01 384引用 https://www.cnblogs.com/xiao ... -
spring property loader的 placeholder的详细分析
2018-09-29 10:59 401引用 https://blog.csdn.net/liuxin ... -
对于spring controller的详细分析和使用指导
2018-09-29 10:58 405引用 https://www.cnblogs.com/lcs- ... -
简单servlet spring 已经mvc controller
2018-09-28 20:42 325引用 https://blog.csdn.net/liuxin ... -
redis分布式锁的代码实践
2018-09-25 15:55 338引用 http://www.importnew.com/193 ... -
redis 实现分布式锁
2018-09-24 23:50 336引用 https://blog.csdn.net/KingCa ... -
@value 的详解
2018-08-23 19:07 437引用 http://www.cnblogs.com/kings ... -
哨兵模式详解
2018-08-16 19:21 656引用 https://blog.csdn.net/liguan ... -
单机 哨兵模式模拟
2018-08-16 14:41 413引用 https://blog.csdn.net/fightx ... -
redis 的哨兵模式 介绍
2018-08-16 14:12 446引用 https://www.cnblogs.com/Patr ... -
spring data redis 模块
2018-08-16 12:33 346引用 https://blog.csdn.net/adsada ... -
spring mvc ,spring boot里面的关于 global exception handle , controller advice的解释
2018-07-18 12:43 592引用 https://blog.csdn.net/kingin ... -
关于spring 的json的传递 yixiaoping
2018-06-29 22:01 250https://blog.csdn.net/**/articl ... -
redis 来生成全局唯一主键
2018-06-12 13:40 1098引用 https://blog.csdn.net/Unkno ... -
github 下载具体版本的 spring
2018-06-06 22:02 391引用https://blog.csdn.net/bbc2005 ... -
tomcat 到 servlet 到 spring servlet dispatcher的一串
2018-05-30 21:57 614引用 五月 30, 2018 9:31:35 下午 org. ... -
前三句可以打印 mybatis log
2018-03-25 21:46 457引用 @Test public void test( ... -
spring整合 quartz
2018-03-06 22:07 334http://blog.csdn.net/YangRunkan ... -
redis 持久化 的方式 rdm
2018-03-05 12:09 481http://blog.csdn.net/jy69240518 ...
相关推荐
当我们谈论“Spring + Ehcache + Redis”两级缓存时,我们实际上是在讨论如何在Java环境中利用Spring框架来集成Ehcache作为本地缓存,并利用Redis作为分布式二级缓存,构建一个高效且可扩展的缓存解决方案。...
"springMybatis+redis三级缓存框架"是一个高效且灵活的解决方案,它将MyBatis的二级缓存与Redis相结合,形成一个三级缓存体系,以优化数据读取速度并减轻数据库压力。 首先,MyBatis作为一款轻量级的持久层框架,其...
两级缓存在redis的方案上做一步优化,在缓存到远程redis的同时,缓存一份到本地进程ehcache(此处的ehcache不用做集群,避免组播带来的开销),取缓存的时候会先取本地,没有会向redis请求,这样会减少应用服务器<–...
"Spring Boot+Spring Cache实现两级缓存(Redis+Caffeine)" 知识点一:缓存与两级缓存 缓存是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。平时我们会将数据存储到磁盘上,如:数据库。...
Spring的缓存抽象层允许开发者选择不同的缓存提供商,如Redis、EhCache等,无需深入学习每个缓存系统的细节。 Redis则是一个高性能的键值存储系统,常用于数据库、缓存和消息代理。它的特点是速度快、数据结构丰富...
在本文中,我们将深入探讨如何在Spring Boot 2.1.4.RELEASE项目中结合JPA(Java Persistence API)和Hibernate实现Redis作为二级缓存。首先,我们需要理解这些技术的基本概念。 Spring Boot 是一个用于简化Spring...
SpringCache与Redis集成,优雅的缓存解决方案 SpringCache是一种基于Java的缓存解决方案,它可以与Redis集成,提供了一种优雅的缓存解决方案。在本文中,我们将对SpringCache与Redis集成的优雅缓存解决方案进行详细...
在一些要求高一致性(任何数据变化都能及时的被查询到)的系统和应用中,就不能再使用EhCache来解决了,这个时候使用集中式缓存是个不错的选择,因此本文将介绍如何在Spring Boot的缓存支持中使用Redis进行数据缓存...
1、springboot 整合ehcache+redis 通过配置文件application.yml切换缓存类型 2、ehcache 、redis 通过缓存管理器管理 3、可分别设置缓存的过期时间 ehcache :添加依赖 pom.xml 2、添加配置文件ehcache.xml 3、添加...
"浅谈SpringCache与redis集成实现缓存解决方案" SpringCache是Spring Framework中的一种缓存机制,可以与Redis集成实现缓存解决方案。Redis是一种key-value型数据库,具有高性能、低延迟的特点,广泛应用于缓存...
Spring集成Redis缓存能有效提升应用性能,减少数据库压力。通过合理配置和使用,可以在保证数据一致性的前提下,为用户提供快速响应。理解并掌握Spring Cache和Redis的集成,是优化Java Web应用的关键技能。
本文将详细探讨基于Redis和Ehcache的两级缓存实现方案,以及如何与Spring服务相结合,提高系统性能。 首先,我们来看Ehcache。Ehcache是一款广泛使用的Java缓存库,它支持本地内存缓存,具有快速响应和较低延迟的...
Ehcache缓存简介 1、基础简介 EhCache是一个纯Java的进程内缓存框架,具有快速、上手简单等特点,是Hibernate中默认的缓存提供方。 2、Hibernate缓存 ...4、对比Redis缓存 Ehcache:直接在Jvm虚拟机中缓存
在本文中,我们将深入探讨如何在Spring Boot 1.x版本中使用Spring注解来实现J2Cache的两级缓存机制,其中包括一级缓存Ehcache和二级缓存Redis。通过这种方式,我们可以显著提高应用程序的性能,减少对数据库的依赖,...
在IT行业中,Spring AOP(面向切面编程)和EhCache是两个非常重要的概念,它们在提升应用程序性能和管理缓存方面发挥着关键作用。本文将深入探讨如何结合Spring AOP与EhCache实现一个简单的缓存实例,以便优化Java...
标题中的“memcached完全剖析ehcache memcached redis 缓存技术总结”涵盖了三种常见的缓存技术:Memcached、Ehcache和Redis。这三种技术在IT行业中被广泛应用于提高应用程序性能,通过存储和检索频繁访问的数据来...
J2Cache 是一个为 Java 应用程序设计的两级缓存框架,旨在提供高效、可靠的缓存解决方案。它结合了本地内存缓存(如 Ehcache 或 Caffeine)与分布式缓存系统(如 Redis),在确保数据高速访问的同时,减轻了完全依赖...
1. **Ehcache**: Ehcache是Java中广泛使用的内存缓存库,它提供了一个快速且轻量级的解决方案来存储和检索数据。Ehcache支持本地内存缓存、磁盘持久化以及分布式缓存模式。在Spring框架中,可以通过注解配置将...
Spring框架提供了强大的缓存抽象,支持多种缓存机制,如 EhCache、Redis、Hazelcast 和 Infinispan 等。在Spring中实现缓存,通常涉及以下几个步骤: 1. **启用缓存**: 在Spring配置文件中,我们需要启用缓存管理...
Spring 整合 EhCache 是一个常见的缓存管理方案,它允许我们在Spring应用中高效地缓存数据,提高系统性能。EhCache 是一个开源、基于内存的Java缓存库,适用于快速、轻量级的数据存储。现在我们来详细探讨如何在...