两级缓存框架J2Cache的使用

说起cache总是想要唠两句buffer,至于cache和buffer的区别在此Cache 和 Buffer 都是缓存,主要区别是什么?

缓存在整个计算机生活中处处可见,比如网页静态资源缓存,DNS解析缓存, CPU缓存,maven仓库缓存等等

说回缓存,缓存最常见的几个特征如下:

  1. 命中率
  2. 最大容量
  3. 淘汰策略
  • FIFO
  • LRU
  • LFU

对于缓存简单的分类方法:

  1. 本地cache
  2. 分布式cache

Java常用的本地Cache框架包括EhCache GuavaCache等

常用分布式Cache包括MemCached Redis等

通俗来说使用缓存的目的就是保存计算好的数据方便下次取用,降低下次取用的时间属于典型的用空间换时间的例子,符合计算机的局部性原理。最根本的说明是让数据贴近使用者。

对于本系统中采用Ehcache作为缓存框架。Ehcache作为常见的本地缓存框架也提供了对于分布式的支持,可以参考红薯的深入探讨在集群环境中使用 EhCache 缓存系统

系统中采用配置较为简单的rmi组播方式作为配置

<cacheManagerPeerProviderFactory
        class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
        properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1,
multicastGroupPort=4446, timeToLive=32"/>

但是在生产环境中总是偶尔出现缓存同步延迟和失败的问题。对于组播来说ehcache的策略比较简单,同时多网卡或者复杂网络环境下的表现也并不好。分布式EHCACHE系统在缓存同步上存在着不小的缺陷

通常此种情况下会使用Redis作为分布式缓存是较好的选择。

但是分布式缓存随之带来的问题就是网络开销的加大。

有没有可以兼得的方案呢?

自然是有的O(∩_∩)O

两级缓存

对于应用同时使用本地缓存和分布式缓存。

  • 一级缓存 我们称之为本地缓存,进程内缓存(L1)或者LocalCache。常用的方案仍然是采用Ehcache或者是Guava Cache。本次方案仍然采用Ehcache。
  • 二级缓存 我们称之为分布式缓存,集中式缓存(L2)或者RemoteCache。常用的方案仍然是采用Redis或者是MemCached。本次方案将采用Redis。 由于大量的缓存读取会导致 L2 的网络带宽成为整个系统的瓶颈,因此 L1 的目标是降低对 L2 的读取次数
    1. 读取顺序 -> L1 -> L2 -> DB
    2. 数据更新 1 从数据库中读取最新数据,依次更新 L1_1 -> L2 ,广播清除某个缓存信息 2 接收到广播(手工清除缓存 & 一级缓存自动失效),从 L1_2 中清除指定的缓存信息

    如上所述,在做数据更新的时候需要收到通知,基本方案可以使用jms方案(目前采用redis订阅消息)

取数据流程

更新数据流程

数据过期监听

实现EhCache的notifyElementExpired方法,发送清除消息(存在一些问题)

目前使用开源中国红薯的作品J2Cache作为支撑。

目前系统中采用了spring缓存抽象(小提示一下,当cacheService内部调用方法时无法走到cache切面,即缓存失效 )

设置Spring cache的方法

<cache:annotation-driven cache-manager="cacheManager"/>
<!-- 声明cacheManager -->
<bean id="cacheManager" class="org.nutz.j2cache.spring.SpringJ2CacheManager" depends-on="ehCacheManager"/>
<bean id="cacheManagerFactory"
      class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
      p:configLocation="classpath:ehcache.xml"
      p:cacheManagerName="${ehcache.ehcache.name}"
      p:shared="true" />
 
<!-- 声明cacheManager -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"
      p:cacheManager-ref="cacheManagerFactory" />

默认来说开启了Spring cache注解将会走到此切面

public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
 
   private static class ThrowableWrapper extends RuntimeException {
      private final Throwable original;
 
      ThrowableWrapper(Throwable original) {
         this.original = original;
      }
   }
 
   public Object invoke(final MethodInvocation invocation) throws Throwable {
      Method method = invocation.getMethod();
 
      Invoker aopAllianceInvoker = new Invoker() {
         public Object invoke() {
            try {
               return invocation.proceed();
            } catch (Throwable ex) {
               throw new ThrowableWrapper(ex);
            }
         }
      };
 
      try {
         return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
      } catch (ThrowableWrapper th) {
         throw th.original;
      }
   }
}

在业务中添加对应注解到实现上,比如

@Override
@Cacheable(value = "remind", key = "#idOwnOrg+'CustomerLoseRemind'")
public int getCustomerLoseNumber(String idOwnOrg)

该方法当外部调用时会优先Check缓存中是否有值,如果没有值则执行业务逻辑否则返回缓存中的值(假如此结果返回null会怎么样?/(ㄒoㄒ)/~~)

当业务中执行某个逻辑时会对缓存中的值造成影响,可以自动清除Cache,比如

@CacheEvict(value = "remind", allEntries = true)
@Override
public void handlerKHSRRemind(String idCustomer, String idOwnOrg)

当然此处不妥会造成缓存集体失效,其实可以改造缓存失效的key,如果多个通过数组即可。

下一步优化可以精细化失效缓存key。

关于使用两级缓存Redis存储的实现为Hash可以自定义nameSpace此处为j2cache_test

对应hash的name为缓存的名称。hash的field为对应缓存的key。为了便于区分key的类型value为对象的fst序列化结果Java快速序列化框架FST

protected byte[] getKeyName(Object key) {
    if (key instanceof Number)
        return ("I:" + key).getBytes();
    else if (key instanceof String || key instanceof StringBuilder || key instanceof StringBuffer)
        return ("S:" + key).getBytes();
    return ("O:" + key).getBytes();
}

应用app会订阅redis的消息收到如下格式的结果

qixiaobo@qixiaobo.mac.pro:/Users/qixiaobo> redis-cli -h 192.168.1.116
                                       17-03-23 13:07
192.168.1.116:6379> SUBSCRIBE j2cache_channel_test
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "j2cache_channel_test"
3) (integer) 1
1) "message"
2) "j2cache_channel_test"
3) "\xd2\xe6F\x00\x01\x18\x00shiro-activeSessionCache&\x00\x00\x00\xfc$eaa302d1-ec27-434a-b0d1-0131a8095575"
1) "message"
2) "j2cache_channel_test"
3) "\xd2\xe6F\x00\x01\x18\x00shiro-activeSessionCache&\x00\x00\x00\xfc$eaa302d1-ec27-434a-b0d1-0131a8095575"
1) "message"
2) "j2cache_channel_test"
3) "\xd2\xe6F\x00\x01\x18\x00shiro-activeSessionCache&\x00\x00\x00\xfc$eaa302d1-ec27-434a-b0d1-0131a8095575"
1) "message"
2) "j2cache_channel_test"
3) "\xd2\xe6F\x00\x01\x18\x00shiro-activeSessionCache&\x00\x00\x00\xfc$eaa302d1-ec27-434a-b0d1-0131a8095575"
1) "message"
2) "j2cache_channel_test"
3) "\xd2\xe6F\x00\x01\x18\x00shiro-activeSessionCache&\x00\x00\x00\xfc$eaa302d1-ec27-434a-b0d1-0131a8095575"
1) "message"
2) "j2cache_channel_test"
3) "\xd2\xe6F\x00\x01\x18\x00shiro-activeSessionCache&\x00\x00\x00\xfc$eaa302d1-ec27-434a-b0d1-0131a8095575"

应用启动时开启线程订阅redis消息,并完成消息的处理

/**
 * 初始化缓存通道并连接
 *
 * @param name : 缓存实例名称
 */
private RedisCacheChannel(String name) throws CacheException {
    this.name = name;
    try {
        long ct = System.currentTimeMillis();
        CacheManager.initCacheProvider(this);
        redisCacheProxy = new RedisCacheProvider().getResource();
        thread_subscribe = new Thread(new Runnable() {
            @Override
            public void run() {
                redisCacheProxy.subscribe(RedisCacheChannel.this, SafeEncoder.encode(channel));
            }
        });
 
        thread_subscribe.start();
 
        log.info("Connected to channel:" + this.name + ", time " + (System.currentTimeMillis() - ct) + " ms.");
 
    } catch (Exception e) {
        throw new CacheException(e);
    }
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏帘卷西风的专栏

使用Cmake生成跨平台项目编译解决方案

    项目最近有需求在windows下面运行,我花了几周时间将linux的服务器移植到windows下面,目前已经能够正常运行服务器,目前又有了新需求,两边的...

3672
来自专栏程序猿DD

Logback中使用TurboFilter实现日志级别等内容的动态修改

可能看到这个标题,读者会问:要修改日志的级别,不是直接修改log.xxx就好了吗?为何要搞那么复杂呢?

1002
来自专栏猿天地

Netty-整合Protobuf高性能数据传输

前言 本篇文章是Netty专题的第四篇,前面三篇文章如下: 高性能NIO框架Netty入门篇 高性能NIO框架Netty-对象传输 高性能NIO框架Netty-...

30311
来自专栏乐沙弥的世界

Linux 内核参数优化(for oracle)

    Oracle 不同平台的数据库安装指导为我们部署Oracle提供了一些系统参数设置的建议值,然而建议值是在通用的情况下得出的结论,并非能完全满足不同的需...

1482
来自专栏JavaEdge

JVM源码分析之synchronized1 字节码实现2 偏向锁

javap命令生成的字节码中包含 ** monitorenter ** 和 ** monitorexit **指令

763
来自专栏web编程技术分享

数据库备忘(MySQL)

3398
来自专栏FreeBuf

如何从内存提取LastPass中的账号密码

简介 首先必须要说,这并不是LastPass的exp或者漏洞,这仅仅是通过取证方法提取仍旧保留在内存中数据的方法。之前我阅读《内存取证的艺术》(The Art ...

2478
来自专栏老码农专栏

ActFramework r1.3.0 - 激动人心的特性一览

1132
来自专栏java一日一条

Java Spring中同时访问多种不同数据库

开发企业应用时我们常常遇到要同时访问多种不同数据库的问题,有时是必须把数据归档到某种数据仓库中,有时是要把数据变更推送到第三方数据库中。使用Spring框架时,...

1751
来自专栏Java架构解析

MyBatis源码窥探:MyBatis整体架构解析

http://www.mybatis.org/mybatis-3/zh/index.html

870

扫码关注云+社区