Java透明化緩存實現 - SimpleCache

jopen 12年前發布 | 38K 次閱讀 Java 緩存組件

SimpleCache 是一個簡單易用的java緩存工具,用來簡化緩存代碼的編寫,讓你擺脫單調乏味的重復工作!

1. 完全透明的緩存支持,對業務代碼零侵入

2. 支持使用Redis和Memcached作為后端緩存。

3. 支持緩存數據分區規則的定義

4. 使用redis作緩存時,支持list類型的高級數據結構,更適合論壇帖子列表這種類型的數據

5. 支持混合使用redis緩存和memcached緩存。可以將列表數據緩存到redis中,其他kv結構數據繼續緩存到memcached

6. 支持redis的主從集群,可以做讀寫分離。緩存讀取自redis的slave節點,寫入到redis的master節點

使用方式如下:


   `
    @Cache(key = @CacheKey(template = KEY_LIKECOUNT, els = {"#p[0]", "#p[1]"}))
    public LikeCount getCount(String appId, String contentId) {}
   `


   #p0表示取方法的第一個參數

整個:

     `@CacheKey(template = "LIKECOUNT/%s/%s", els = {"#p[0]","#p[1]"})`

這一段其實就是 
    `String.format("LIKECOUNT/%s/%s", appId, contentId)`

expire參數用來控制緩存的時間,cacheNull參數用來控制,如果返回的結果為null時,是否緩存改結果。

如果一個方法的緩存key非常簡單,不需要從參數生成,可以簡單的使用:

   ` @SimpleCache(key = "TEST", expire = 3000, cacheNull = true)`
如果該方法返回的是一個List類型的數據,則可以使用:


 `@ListedCache(key = @CacheKey(template = "ACTIVITY/%s", els = {"#p[0]"}), offsetIndex = 1, limitIndex = 2)`

其中offsetIdex和limitIndex對應查詢是的分頁參數。 比如查詢 offset=10, limit=10的數據,將首先嘗試從緩存中加載,如果緩存中僅有18條數據,則首先從緩存中加載 offset=10, limit=8的數據,然后將修改傳遞給方法調用的參數為 offset=18, limit =2 ,將剩余的數據補充上。
還支持另外兩個標注:

        /***
            寫入緩存,writeReutun表示將方法的返回結果寫入緩存。writeParameter表示將parameterIndex 制定的參數寫入緩存
        ***/
       @WriteCache(key = @CacheKey(template = "ACTIVITY/%s", els = {"#p[0]"}), writeReturn=true)
       @WriteCache(key = @CacheKey(template = "ACTIVITY/%s", els = {"#p[0]"}), writeParameter=true, writeReturn = false, parameterIndex = 0)

       /***
           將緩存寫入list類型的緩存
       ***/
       @WriteListCache(key = @CacheKey(template = "ACTIVITY/%s", els = {"#p[0]"}),writeParameter = true, writeReturn = false, parameterIndex = 0)
如果需要刪除緩存,則可以使用:

   @RemoveCache

## redis主從支持 ##

redis proxy增加主從支持,所有寫操作通過主進行,讀操作通過從進行。配置如下:

 <bean id="masterRedisClient" class="com.skymobi.sns.cache.redis.RedisClient">
        <constructor-arg value="${redis.url}"/>
    </bean>

    <bean id="slaveRedisClient" class="com.skymobi.sns.cache.redis.RedisClient">
        <constructor-arg value="${redis-slave.url}"/>
    </bean>

    <bean id="layerCachePactory" class="com.skymobi.sns.cache.layer.LayerInterceptorFactory">
        <constructor-arg ref="masterRedisClient"/>
        <constructor-arg ref="slaveRedisClient"/>
    </bean>
同時cacheproxy配置中增加緩存類型設定,可以為不同的場景指定使用不同類型的緩存,比如在有列表類型數據時使用redis,普通數據直接使用memcached。配置如下:

    <bean id="cacheProxy" class="com.skymobi.sns.cache.EasyCacheProxy">
        <constructor-arg ref="layerCachePactory"/>
        <property name="factories">
            <map>
                <entry key="default" value-ref="layerCachePactory"></entry>
                <entry key="memcached" value-ref="memcachedFactory"></entry>
            </map>
        </property>
    </bean>
此時如果在service類中直接使用@CacheProxy注解,默認將采用 default指定的緩存。
如果使用

   @CacheProxy(type = "memcached")
   public class SpecialUserServiceImpl implements SpecialUserService {}
則在該service中將使用memcached作為緩存

## 數據切分支持 ##

增加對一個根據key的規則,將數據劃分到不同服務器的功能。
同時提供一個工具,在規則重劃分時進行數據遷移。

如下:

        KeyRouter keyRouter = new DefaultKeyRouter("172.16.3.214:6379",
                ImmutableMap.of("SNS/TEST1/.*", "172.16.3.214:6379",
                        "SNS/TEST2/.*", "172.16.3.214:6389" 
                ));
        RedisClient client = new RedisClient(keyRouter);
        client.zadd("SNS/TEST1/1", "t3", 3.0);
        client.zadd("SNS/TEST2/2", "t1", 10.0);
        RedisClient client1 = new RedisClient("172.16.3.214:6379");
        RedisClient client2 = new RedisClient("172.16.3.214:6389");
        assertFalse(client1.exists("SNS/TEST2/2"));  //"SNS/TEST2/2" 只會出現在172.16.3.214:6389上
        assertFalse(client2.exists("SNS/TEST1/1"));   //"SNS/TEST1/1" 只會出現在172.16.3.214:6379上
遷移數據:

        RedisReplicationTool.copy("172.16.3.214:6379", "172.16.3.214:6389", "SNS/TEST1/*");
        RedisReplicationTool.copy("172.16.3.214:6389", "172.16.3.214:6379", "SNS/TEST2/*");


## 代碼重構 ##

代碼做了大的重構,簡單清晰了很多。
移除不需要的factory類
增加一個混合緩存類型,現在可以在Cache注解中為每一個cache指定要采用的緩存類型
移除Cache注解中的layer屬性。
暫時放棄對本地cache的支持
現在最完整的使用方式下,配置文件應該是這樣的:

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

        <property name="servers" value="${cache.server}"/>
        <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="1000"/>
        <property name="timeoutExceptionThreshold" value="1998"/>
        <property name="hashAlg" value="KETAMA_HASH"/>
        <property name="locatorType" value="CONSISTENT"/>
        <property name="failureMode" value="Redistribute"/>
        <property name="useNagleAlgorithm" value="false"/>
    </bean>

    <bean id="masterRedisClient" class="com.skymobi.sns.cache.redis.RedisClient">
        <constructor-arg value="${redis.url}"/>
    </bean>

    <bean id="slaveRedisClient" class="com.skymobi.sns.cache.redis.RedisClient">
        <constructor-arg value="${redis-slave.url}"/>
    </bean>

    <bean id="redisInterceptor" class="com.skymobi.sns.cache.redis.RedisInterceptor">
        <constructor-arg ref="masterRedisClient"/>
        <constructor-arg ref="slaveRedisClient"/>
    </bean>

    <bean id="memcachedInterceptor" class="com.skymobi.sns.cache.memcached.MemcachedInterceptor">
        <constructor-arg ref="memcachedClient"/>
    </bean>

    <bean id="hybridInterceptor" class="com.skymobi.sns.cache.hybrid.HybridInterceptor">
        <property name="defaultInterceptor" ref="redisInterceptor"/>
        <property name="interceptors">
            <map>
                <entry key="default" value-ref="redisInterceptor"></entry>
                <entry key="memcached" value-ref="memcachedInterceptor"></entry>
            </map>
        </property>
    </bean>

    <bean id="cacheProxy" class="com.skymobi.sns.cache.EasyCacheProxy">
        <constructor-arg ref="hybridInterceptor"/>
        <property name="interceptors">
            <map>
                <entry key="default" value-ref="hybridInterceptor"></entry>
                <entry key="memcached" value-ref="memcachedInterceptor"></entry>
            </map>
        </property>
    </bean>
如果使用了HybridInterceptor類型,則可以為每個cache指定不同的緩存類型:

@WriteCache(key = @CacheKey(template = KEY_LIKECOUNT, els = {"#p[0].appId", "#p[0].contentId"}), type = "memcached")

## 切分功能改進 ##

Cache代碼進行了修改,升級到1.1.0.
1. 增加了兩個redis緩存清理和遷移的groovy腳本
2. KeyRoute增加了一個FunctionRoute的實現。現在可以自定義函數來對key進行處理。默認提供了取模的實現。常見的場景是按照skyid取模,將不同用戶的數據存儲到不同的服務器上。
比如 有服務器s1,s2,s3,s4 4臺 ,按skyid 123456 % 4 = 0 ,則skyid=123456的用戶數據應該保存到 s1 這臺server上。
使用方式如下:

        Function function = new ModFunction(4);
        FunctionRouter keyRouter = new FunctionRouter(
                ImmutableMap.of(
                        "SNS/TEST1/(\\d+)", function
        ));
        keyRouter.setHosts(Lists.newArrayList(
                "172.16.3.214:6379", "172.16.3.215:6379" ,"172.16.3.216:6379" ,"172.16.3.217:6379" 
        ));
        keyRouter.setDefaultHost("172.16.3.214:6379"); 
        String host = keyRouter.getHost("SNS/TEST1/123456");
        assertEquals("172.16.3.214:6379", host);
        host = keyRouter.getHost("SNS/TEST1/123457");
        assertEquals("172.16.3.215:6379", host);
        host = keyRouter.getHost("SNS/TEST1/123458");
        assertEquals("172.16.3.216:6379", host);
        host = keyRouter.getHost("SNS/TEST1/123459");
        assertEquals("172.16.3.217:6379", host);
        host = keyRouter.getHost("SNS/TEST1/123460");
        assertEquals("172.16.3.214:6379", host);
        RedisClient client = new RedisClient(keyRouter);
        client.set("SNS/TEST1/123456", 1);
        int v = client.get("SNS/TEST1/123456", Integer.class);
        assertEquals(1, v);
        try{
            client.set("SNS/TEST1/123457", 1);
        }catch (Exception e){
            assertEquals(JedisConnectionException.class,e.getClass());
        }

## key編寫方式改進 ##
1.1.1版本。
參照Spring3.1的實現,改進了CacheKey的定義,現在可以這樣:

@Cache(key=@CacheKey(template="USER/${p0}"))

expire現在支持用字符串表達式來指定,如下:

exireTime = "1h"

可以使用的方式有
1d ---> 1天

1h ---> 1小時

1mn ---> 1分鐘

1s --> 1秒

項目主頁:http://www.baiduhome.net/lib/view/home/1342431046620

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!