Spring整合Lettuce自定義緩存簡單實現

0. 前言

Spring框架提供了一系列豐富的接口幫助咱們更快捷的開發應用程序,不少功能僅須要在配置文件聲明一下或者在代碼寫幾行就可以實現了,可以使咱們更注重於應用的開發上,某種意義上滋長了咱們的「偷懶」行爲。關於緩存,不少時候咱們使用Hibernate或Mybatis框架的二級緩存結合Ehcache緩存框架來提升執行效率,配置使用起來也很簡單;又或者使用Redis內存型數據庫,利用Jedis鏈接操做數據在內存中的讀寫,一樣用起來也很簡單。html

然而上述兩種方式的緩存,前者的範圍太廣(如Mybatis是mapper級別的緩存),後者又太細(字符串型的鍵值對)。因而,在這裏,我稍微往回走一點,研究一下Spring從3.1版本出現的自定義緩存實現機制,並使用效率更高的Lettuce鏈接Redis,實現方法級自定義緩存。即用Lettuce作Redis的客戶端鏈接,使用Redis做爲底層的緩存實現技術,在應用層或數據層的方法使用Spring緩存標籤進行數據緩存,結合Redis的可視化工具還能夠看到緩存的數據信息。java

1.1部分可能至關一部分人都認識,那就重點看下1.2部分的,歡迎指點。redis

1. 技術準備

涉及技術:spring

  • Spring 3.x 緩存註解
  • Lettuce 4.x Redis鏈接客戶端
  • Redis 3.x +
  • Spring 3.x +
  • 序列化和反序列化

1.1 Spring 3.x 緩存註解

Spring 緩存註解,即Spring Cache做用在方法上。當咱們在調用一個緩存方法時會把該方法參數返回結果做爲一個鍵值對存放在緩存中,等到下次利用一樣的參數來調用該方法時將再也不執行該方法,而是直接從緩存中獲取結果進行返回。因此在使用Spring Cache的時候咱們要保證咱們緩存的方法對於相同的方法參數要有相同的返回結果。sql

要使用Spring Cache,咱們須要作兩件事:數據庫

  1. 在須要緩存的方法上使用緩存註解;
  2. 在配置文件聲明底層使用什麼作緩存。

對於第一個問題,這裏我就不作介紹了,網上已經有十分紅熟的文章供你們參考,主要是@Cacheable、@CacheEvict、@CachePut以及自定義鍵的SpEL(Spring 表達式語言,Spring Expression Language)的使用,相信部分人有從Spring Boot中瞭解過這些東西,詳細可參考如下文章:apache

對於第二個問題,簡單的說下,知道的能夠跳過,這裏有三種配置方法:編程

  1. 使用Spring自帶的緩存實現
  2. 使用第三方的緩存實現
  3. 使用自定緩存實現

不管哪一種配置方法都是在Spring的配置文件進行配置的(不要問我Spring的配置文件是什麼)。首先,因爲咱們使用的是註解的方式,對Spring不陌生的話,都知道應該要配置個註解驅動,代碼以下:segmentfault

<!-- 配置Spring緩存註解驅動 -->
    <cache:annotation-driven cache-manager="cacheManager"/>
  • cache-manager屬性的默認值是cacheManager,因此能夠顯示寫出,在後續的CacheManage實現類的配置中使用是cacheManager做爲id便可。
  • 還有個屬性proxy-target-class,默認爲false,由於咱們編程常用接口,而註解也可能用到接口上,當使用缺省配置時,註解用到接口仍是類上都沒有問題,但若是proxy-target-class聲明爲true,就只能用到類上了。

三種方法的不一樣體如今CacheManager類以及Cache類的實現上:api

  1. Spring自帶一個SimpleCacheManager的實現,配合ConcurrentMap的配置方法以下:
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
      <property name="caches">
         <set>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
                <property name="name" value="myCache"/> <!-- cache的名字name自行定義 -->
            </bean>
         </set>
      </property>
    </bean>
  1. 使用第三方的緩存實現,如經常使用的EhCache:
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
          <property name="cache-manager-ref">
              <bean id="ehcacheManager"/>
          </property>
      </bean>
      <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
          <property name="config-location" value="ehcache.xml"/>    <!-- 指定EhCache配置文件位置 -->
      </bean>
  1. 自定義緩存實現,這裏着重講,配製方法與第一種相似,只不過實際的使用的CacheManager類以及Cache類由咱們本身定義。實現CacheManager有兩種方式,一是直接實現org.springframework.cache.CacheManager請輸入代碼`接口,該接口有兩個方法:
public interface CacheManager {
    /**
     * Return the cache associated with the given name.
     * @param name the cache identifier (must not be {@code null})
     * @return the associated cache, or {@code null} if none found
     */
    Cache getCache(String name);

    /**
     * Return a collection of the cache names known by this manager.
     * @return the names of all caches known by the cache manager
     */
    Collection<String> getCacheNames();

}

很直白易懂的兩個方法,根據Cache的名字獲取CaChe以及獲取全部Cache的名字,恰當利用好在配置文件中配置Cache時,對相應的name及實現的Cache類進行注入,在CacheManager的實現中使用成員變量,如簡單的HashMap<String, Cache>對實現的Cache進行保存便可,對Spring比較熟悉的話,其實很是的簡單,固然,能夠根據業務需求實現本身的邏輯,這裏只是簡單舉例。另外一種方式是繼承抽象類org.springframework.cache.support.AbstractCacheManager,觀看源碼可發現,這是提供了一些模板方法、實現了CacheManager接口的模板類,,只須要實現抽象方法protected abstract Collection<? extends Cache> loadCaches();便可,下面給出我本身的一個簡單實現(觀看源碼後驚奇的發現與SimpleCacheManager的實現如出一轍):

import java.util.Collection;

import org.springframework.cache.Cache;
import org.springframework.cache.support.AbstractCacheManager;

public class RedisCacheManager extends AbstractCacheManager {

    private Collection<? extends Cache> caches;

    public void setCaches(Collection<? extends Cache> caches) {
        this.caches = caches;
    }

    @Override
    protected Collection<? extends Cache> loadCaches() {
        return this.caches;
    }
}

說完CacheManager,天然到了Cache的實現,方法就是直接實現Spring的接口org.springframework.cache.Cache,接口的方法有點多,網上也有很多相關文章,這裏我只說下本身的見解,代碼以下:

// 簡單直白,就是獲取Cache的名字
    String getName();

    // 獲取底層的緩存實現對象
    Object getNativeCache();

    // 根據鍵獲取值,把值包裝在ValueWrapper裏面,若是有必要能夠附加額外信息
    ValueWrapper get(Object key);

    // 和get(Object key)相似,但返回值會被強制轉換爲參數type的類型,但我查了不少文章,
    // 看了源碼也搞不懂是怎麼會觸發這個方法的,取值默認會觸發get(Object key)。
    <T> T get(Object key, Class<T> type);

    // 從緩存中獲取 key 對應的值,若是緩存沒有命中,則添加緩存,
    // 此時可異步地從 valueLoader 中獲取對應的值(4.3版本新增)
    // 與緩存標籤中的sync屬性有關
    <T> T get(Object key, Callable<T> valueLoader);

    // 存放鍵值對
    void put(Object key, Object value);

    // 若是鍵對應的值不存在,則添加鍵值對
    ValueWrapper putIfAbsent(Object key, Object value);

    // 移除鍵對應鍵值對
    void evict(Object key);

    // 清空緩存
    void clear();

下面給出的實現不須要用到<T> T get(Object key, Class<T> type);<T> T get(Object key, Callable<T> valueLoader);,只是簡單的輸出一句話(事實上也沒見有輸出過)。另外存取的時候使用了序列化技術,序列化是把對象轉換爲字節序列的過程,對其實是字符串存取的Redis來講,能夠把字節當成字符串存儲,這裏不詳述了,固然也可使用GSON、Jackson等Json序列化類庫轉換成可讀性高的Json字符串,不過極可能須要緩存的每一個類都要有對應的一個Cache,可能會有十分多的CaChe實現類,但轉換效率比JDK原生的序列化效率高得多,另外也可使用簡單的HashMap,方法不少,能夠本身嘗試。

說多一句,因爲使用Lettuce鏈接,redis鏈接對象的操做和jedis或redisTemplate不一樣,但理解起來不難。

import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.concurrent.Callable;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;

import com.lambdaworks.redis.api.StatefulRedisConnection;
import com.lambdaworks.redis.api.sync.RedisCommands;

public class RedisCache implements Cache {


    private String name;
    private static JdkSerializationRedisSerializer redisSerializer;

    @Autowired
    private StatefulRedisConnection<String, String> redisConnection;

    public RedisCache() {
        redisSerializer = new JdkSerializationRedisSerializer();
        name = RedisCacheConst.REDIS_CACHE_NAME;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Object getNativeCache() {
        // 返回redis鏈接看似奇葩,但redis鏈接就是操做底層實現緩存的對象
        return getRedisConnection();
    }

    @Override
    public ValueWrapper get(Object key) {
        RedisCommands<String, String> redis = redisConnection.sync();
        String redisKey = (String) key;

        String serializable = redis.get(redisKey);
        if (serializable == null) {
            System.out.println("-------緩存不存在------");
            return null;
        }
        System.out.println("---獲取緩存中的對象---");
        Object value = null;
        // 序列化轉化成字節時,聲明編碼RedisCacheConst.SERIALIZE_ENCODE(ISO-8859-1),
        // 不然轉換很容易出錯(編碼爲UTF-8也會轉換錯誤)
        try {
            value = redisSerializer
                    .deserialize(serializable.getBytes(RedisCacheConst.SERIALIZE_ENCODE));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new SimpleValueWrapper(value);

    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        System.out.println("------未實現get(Object key, Class<T> type)------");
        return null;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        System.out.println("---未實現get(Object key, Callable<T> valueLoader)---");
        return null;
    }

    @Override
    public void put(Object key, Object value) {
        System.out.println("-------加入緩存------");
        RedisCommands<String, String> redis = redisConnection.sync();
        String redisKey = (String) key;
        byte[] serialize = redisSerializer.serialize(value);
        try {
            redis.set(redisKey, new String(serialize, RedisCacheConst.SERIALIZE_ENCODE));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        System.out.println("---未實現putIfAbsent(Object key, Object value)---");
        return null;
    }

    @Override
    public void evict(Object key) {
        System.out.println("-------刪除緩存 key=" + key.toString() + " ------");
        RedisCommands<String, String> redis = redisConnection.sync();
        String redisKey = key.toString();
        // RedisCacheConst.WILDCARD是Redis中鍵的通配符「*」,用在這裏使鍵值刪除也能使用通配方式
        if (redisKey.contains(RedisCacheConst.WILDCARD)) {
            List<String> caches = redis.keys(redisKey);
            if (!caches.isEmpty()) {
                redis.del(caches.toArray(new String[caches.size()]));
            }
        } else {
            redis.del(redisKey);
        }
    }

    @Override
    public void clear() {
        System.out.println("-------清空緩存------");
        RedisCommands<String, String> redis = redisConnection.sync();
        redis.flushdb();
    }

    public void setName(String name) {
        this.name = name;
    }

    public StatefulRedisConnection<String, String> getRedisConnection() {
        return redisConnection;
    }

    public void setRedisConnection(StatefulRedisConnection<String, String> redisConnection) {
        this.redisConnection = redisConnection;
    }
}

RedisCacheConst常量類

public class RedisCacheConst {
    public final static String REDIS_CACHE_NAME = "Redis Cache";
    public final static String SERIALIZE_ENCODE = "ISO-8859-1";
    public final static String WILDCARD = "*";
    public final static String SPRING_KEY_TAG = "'";
    // SpEL中普通的字符串要加上單引號,如一個鍵設爲kanarien,應爲key="'kanarien'"
}

Spring配置文件

<!-- 配置Spring緩存註解驅動 -->
    <cache:annotation-driven cache-manager="cacheManager"/>

    <!-- 自定義的CacheManager -->
    <bean id="cacheManager" class="cn.nanhang.daojia.util.cache.RedisCacheManager">
        <property name="caches">
            <set>   <!-- 自定義的Cache -->
                <bean class="cn.nanhang.daojia.util.cache.RedisCache"/>
            </set>
        </property>
    </bean>

1.2 Lettuce 4.x Redis鏈接客戶端

1.1部分講的有點多了,我真正想講的也就是自定義那部分,但其餘部分也不能不說,咳咳。

Lettuce,在Spring Boot 2.0以前幾乎沒怎麼據說過的詞語,自Spring Boot 2.0漸漸進入國人的視野(Lettuce 5.x),由於Spring Boot 2.0默認採用Lettuce 5.x + Redis 方式實現方法級緩存,不少文章都有這麼強調過。Lettuce爲何會受到Spring Boot開發人員的青睞呢?簡單說來,Lettuce底層使用Netty框架,利用NIO技術,達到線程安全的併發訪問,同時有着比Jedis更高的執行效率與鏈接速度

Lettuce還支持使用Unix Domain Sockets,這對程序和Redis在同一機器上的狀況來講,是一大福音。平時咱們鏈接應用和數據庫如Mysql,都是基於TCP/IP套接字的方式,如127.0.0.1:3306,達到進程與進程之間的通訊,Redis也不例外。但使用UDS傳輸不須要通過網絡協議棧,不須要打包拆包等操做,只是數據的拷貝過程,也不會出現丟包的狀況,更不須要三次握手,所以有比TCP/IP更快的鏈接與執行速度。固然,僅限Redis進程和程序進程在同一主機上,並且僅適用於Unix及其衍生系統

事實上,標題中所說的簡單實現,適用於中小項目,由於中小項目不會花太多資源在硬件上,極可能Redis進程和程序進程就在同一主機上,而咱們所寫的程序只須要簡單的實現就足夠了,本篇文章介紹的東西都適用於中小項目的,並且正由於簡單才易於去剖析源碼,邊寫邊學。

另外,爲何這裏說的是Lettuce 4.x而不是Lettuce 5.x呢?
由於我寫項目那時還沒Lettuce 5.x啊,只是寫這篇文章有點晚了,技術突飛猛進啊。4和5之間的差異仍是挺大的,代碼中對Redis鏈接方式就變了(好像?),以後再去研究下。詳細可見官方文檔,這裏再也不班門弄斧了。

下面是Lettuce 4.x的客戶端鏈接代碼(兼用TCP/IP與UDS鏈接方式,後者不行自動轉前者),因爲涉及了邏輯判斷,使用了Java類進行配置而不是在xml中配置:

import java.nio.file.Files;
import java.nio.file.Paths;

import javax.annotation.PostConstruct;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.lambdaworks.redis.RedisClient;
import com.lambdaworks.redis.RedisURI;
import com.lambdaworks.redis.RedisURI.Builder;
import com.lambdaworks.redis.api.StatefulRedisConnection;
import com.lambdaworks.redis.resource.ClientResources;
import com.lambdaworks.redis.resource.DefaultClientResources;

@Primary
@Configuration
public class LettuceConfig {

    private static RedisURI redisUri;

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Value("${redis.host:127.0.0.1}")
    private String hostName;

    @Value("${redis.domainsocket:}")
    private String socket;

    @Value("${redis.port:6379}")
    private int port;

    private int dbIndex = 2;

    @Value(value = "${redis.pass:}")
    private String password;

    @Bean(destroyMethod = "shutdown")
    ClientResources clientResources() {
        return DefaultClientResources.create();
    }

    @Bean(destroyMethod = "close")
    StatefulRedisConnection<String, String> redisConnection(RedisClient redisClient) {
        return redisClient.connect();
    }

    private RedisURI createRedisURI() {
        Builder builder = null;
    // 判斷是否有配置UDS信息,以及判斷Redis是否有支持UDS鏈接方式,是則用UDS,不然用TCP
        if (StringUtils.isNotBlank(socket) && Files.exists(Paths.get(socket))) {
            builder = Builder.socket(socket);
            System.out.println("connect with Redis by Unix domain Socket");
            log.info("connect with Redis by Unix domain Socket");
        } else {
            builder = Builder.redis(hostName, port);
            System.out.println("connect with Redis by TCP Socket");
            log.info("connect with Redis by TCP Socket");
        }
        builder.withDatabase(dbIndex);
        if (StringUtils.isNotBlank(password)) {
            builder.withPassword(password);
        }
        return builder.build();
    }

    @PostConstruct
    void init() {
        redisUri = createRedisURI();
        log.info("鏈接Redis成功!\n host:" + hostName + ":" + port + " pass:" + password + " dbIndex:" + dbIndex);
    }

    @Bean(destroyMethod = "shutdown")
    RedisClient redisClient(ClientResources clientResources) {
        return RedisClient.create(clientResources, redisUri);
    }

    public void setDbIndex(int dbIndex) {
        this.dbIndex = dbIndex;
    }

    public void setHostName(String hostName) {
        this.hostName = hostName;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public void setSocket(String socket) {
        this.socket = socket;
    }

}

Java屬性文件:redis.properties(僅供參考)

redis.pool.maxIdle=100
redis.pool.maxTotal=10
redis.pool.testOnBorrow=true
redis.pool.testOnReturn=true
redis.host=127.0.0.1
#redis.pass=yourpassword
redis.port=6379
redis.expire=6000
redis.domainsocket=/tmp/redis.sock

註解用得不少,說明下:

  • @Primary,表示優先級關係,因爲源程序中涉及數據到Redis的加載,因此要設定,視狀況能夠不加;
  • @Configuration,表名這是個配置的Bean,能被Spring掃描器識別;
  • @Value,與Java屬性文件有關,自動讀取屬性文件的值,括號中的內容就是鍵,冒號後面的是默認值;
  • @PostConstruct,在類加載完、依賴注入完以後才執行所修飾的方法,注意要在Spring配置文件中;
  • @Bean,不解釋。

最後,該類要被Spring掃描識別。

1.3 Redis 3.x +

關於Redis的介紹,直接去看個人筆記,裏面有一些簡單又不失全面的介紹,好比Unix Domain Socket相關、一些Redis的基本配置和可視化界面等等。

2. 補充

必要的代碼都給出來了,就不貼源碼了,Lettuce的TCP、UDS二選一鏈接方式也能夠單獨拿出來用。

歡迎你們的指點!


Copyright © 2018, GDUT CSCW back-end Kanarien, All Rights Reserved
相關文章
相關標籤/搜索