Redis整合Spring結合使用快取例項

林炳文的專欄發表於2015-12-27

摘要:本文介紹瞭如何在Spring中配置redis,並通過Spring中AOP的思想,將快取的方法切入到有需要進入快取的類或方法前面。

一、Redis介紹

什麼是Redis?

redis是一個key-value儲存系統。和Memcached類似,它支援儲存的value型別相對更多,包括string(字串)、list(連結串列)、set(集合)、zset(sorted set –有序集合)和hash(雜湊型別)。這些資料型別都支援push/pop、add/remove及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,redis支援各種不同方式的排序。與memcached一樣,為了保證效率,資料都是快取在記憶體中。區別的是redis會週期性的把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案,並且在此基礎上實現了master-slave(主從)同步。

它有什麼特點?

(1)Redis資料庫完全在記憶體中,使用磁碟僅用於永續性。
(2)相比許多鍵值資料儲存,Redis擁有一套較為豐富的資料型別。
(3)Redis可以將資料複製到任意數量的從伺服器。

Redis 優勢?

(1)異常快速:Redis的速度非常快,每秒能執行約11萬集合,每秒約81000+條記錄。
(2)支援豐富的資料型別:Redis支援最大多數開發人員已經知道像列表,集合,有序集合,雜湊資料型別。這使得它非常容易解決各種各樣的問題,因為我們知道哪些問題是可以處理通過它的資料型別更好。
(3)操作都是原子性:所有Redis操作是原子的,這保證瞭如果兩個客戶端同時訪問的Redis伺服器將獲得更新後的值。
(4)多功能實用工具:Redis是一個多實用的工具,可以在多個用例如快取,訊息,佇列使用(Redis原生支援釋出/訂閱),任何短暫的資料,應用程式,如Web應用程式會話,網頁命中計數等。

Redis 缺點?

(1)單執行緒

(2)耗記憶體

二、使用例項

本文使用maven+eclipse+sping

1、引入jar包

      <!--Redis start -->
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
			<version>1.6.1.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.7.3</version>
		</dependency>
     <!--Redis end -->

2、配置bean

在application.xml加入如下配置

 <!-- jedis 配置 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig" >
          <property name="maxIdle" value="${redis.maxIdle}" />
          <property name="maxWaitMillis" value="${redis.maxWait}" />
          <property name="testOnBorrow" value="${redis.testOnBorrow}" />
    </bean >
   <!-- redis伺服器中心 -->
    <bean id="connectionFactory"  class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
          <property name="poolConfig" ref="poolConfig" />
          <property name="port" value="${redis.port}" />
          <property name="hostName" value="${redis.host}" />
          <property name="password" value="${redis.password}" />
          <property name="timeout" value="${redis.timeout}" ></property>
    </bean >
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" >
          <property name="connectionFactory" ref="connectionFactory" />
          <property name="keySerializer" >
              <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
          </property>
          <property name="valueSerializer" >
              <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
          </property>
    </bean >

     <!-- cache配置 -->
    <bean id="methodCacheInterceptor" class="com.mucfc.msm.common.MethodCacheInterceptor" >
          <property name="redisUtil" ref="redisUtil" />
    </bean >
    <bean id="redisUtil" class="com.mucfc.msm.common.RedisUtil" >
          <property name="redisTemplate" ref="redisTemplate" />
    </bean >

其中配置檔案redis一些配置資料redis.properties如下:

#redis中心
redis.host=10.75.202.11
redis.port=6379
redis.password=123456
redis.maxIdle=100
redis.maxActive=300
redis.maxWait=1000
redis.testOnBorrow=true
redis.timeout=100000

# 不需要加入快取的類
targetNames=xxxRecordManager,xxxSetRecordManager,xxxStatisticsIdentificationManager
# 不需要快取的方法
methodNames=

#設定快取失效時間
com.service.impl.xxxRecordManager= 60
com.service.impl.xxxSetRecordManager= 60
defaultCacheExpireTime=3600

fep.local.cache.capacity =10000

要掃這些properties檔案,在application.xml加入如下配置

     <!-- 引入properties配置檔案 -->  
     <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
               <value>classpath:properties/*.properties</value>
                <!--要是有多個配置檔案,只需在這裡繼續新增即可 -->
            </list>
        </property>
    </bean>

3、一些工具類

(1)RedisUtil

上面的bean中,RedisUtil是用來快取和去除資料的例項

package com.mucfc.msm.common;

import java.io.Serializable;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

/**
 * redis cache 工具類
 * 
 */
public final class RedisUtil {
	private Logger logger = Logger.getLogger(RedisUtil.class);
	private RedisTemplate<Serializable, Object> redisTemplate;

	/**
	 * 批量刪除對應的value
	 * 
	 * @param keys
	 */
	public void remove(final String... keys) {
		for (String key : keys) {
			remove(key);
		}
	}

	/**
	 * 批量刪除key
	 * 
	 * @param pattern
	 */
	public void removePattern(final String pattern) {
		Set<Serializable> keys = redisTemplate.keys(pattern);
		if (keys.size() > 0)
			redisTemplate.delete(keys);
	}

	/**
	 * 刪除對應的value
	 * 
	 * @param key
	 */
	public void remove(final String key) {
		if (exists(key)) {
			redisTemplate.delete(key);
		}
	}

	/**
	 * 判斷快取中是否有對應的value
	 * 
	 * @param key
	 * @return
	 */
	public boolean exists(final String key) {
		return redisTemplate.hasKey(key);
	}

	/**
	 * 讀取快取
	 * 
	 * @param key
	 * @return
	 */
	public Object get(final String key) {
		Object result = null;
		ValueOperations<Serializable, Object> operations = redisTemplate
				.opsForValue();
		result = operations.get(key);
		return result;
	}

	/**
	 * 寫入快取
	 * 
	 * @param key
	 * @param value
	 * @return
	 */
	public boolean set(final String key, Object value) {
		boolean result = false;
		try {
			ValueOperations<Serializable, Object> operations = redisTemplate
					.opsForValue();
			operations.set(key, value);
			result = true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * 寫入快取
	 * 
	 * @param key
	 * @param value
	 * @return
	 */
	public boolean set(final String key, Object value, Long expireTime) {
		boolean result = false;
		try {
			ValueOperations<Serializable, Object> operations = redisTemplate
					.opsForValue();
			operations.set(key, value);
			redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
			result = true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	public void setRedisTemplate(
			RedisTemplate<Serializable, Object> redisTemplate) {
		this.redisTemplate = redisTemplate;
	}
}

(2)MethodCacheInterceptor

切面MethodCacheInterceptor,這是用來給不同的方法來加入判斷如果快取存在資料,從快取取資料。否則第一次從資料庫取,並將結果儲存到快取 中去。

package com.mucfc.msm.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.log4j.Logger;

public class MethodCacheInterceptor implements MethodInterceptor {
	private Logger logger = Logger.getLogger(MethodCacheInterceptor.class);
	private RedisUtil redisUtil;
	private List<String> targetNamesList; // 不加入快取的service名稱
	private List<String> methodNamesList; // 不加入快取的方法名稱
	private Long defaultCacheExpireTime; // 快取預設的過期時間
	private Long xxxRecordManagerTime; //
	private Long xxxSetRecordManagerTime; //

	/**
	 * 初始化讀取不需要加入快取的類名和方法名稱
	 */
	public MethodCacheInterceptor() {
		try {
			 File f = new File("D:\\lunaJee-workspace\\msm\\msm_core\\src\\main\\java\\com\\mucfc\\msm\\common\\cacheConf.properties"); 
			 //配置檔案位置直接被寫死,有需要自己修改下
		     InputStream in = new FileInputStream(f); 
//			InputStream in = getClass().getClassLoader().getResourceAsStream(
//					"D:\\lunaJee-workspace\\msm\\msm_core\\src\\main\\java\\com\\mucfc\\msm\\common\\cacheConf.properties");
			Properties p = new Properties();
			p.load(in);
			// 分割字串
			String[] targetNames = p.getProperty("targetNames").split(",");
			String[] methodNames = p.getProperty("methodNames").split(",");

			// 載入過期時間設定
			defaultCacheExpireTime = Long.valueOf(p.getProperty("defaultCacheExpireTime"));
			xxxRecordManagerTime = Long.valueOf(p.getProperty("com.service.impl.xxxRecordManager"));
			xxxSetRecordManagerTime = Long.valueOf(p.getProperty("com.service.impl.xxxSetRecordManager"));
			// 建立list
			targetNamesList = new ArrayList<String>(targetNames.length);
			methodNamesList = new ArrayList<String>(methodNames.length);
			Integer maxLen = targetNames.length > methodNames.length ? targetNames.length
					: methodNames.length;
			// 將不需要快取的類名和方法名新增到list中
			for (int i = 0; i < maxLen; i++) {
				if (i < targetNames.length) {
					targetNamesList.add(targetNames[i]);
				}
				if (i < methodNames.length) {
					methodNamesList.add(methodNames[i]);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		Object value = null;

		String targetName = invocation.getThis().getClass().getName();
		String methodName = invocation.getMethod().getName();
		// 不需要快取的內容
		//if (!isAddCache(StringUtil.subStrForLastDot(targetName), methodName)) {
		if (!isAddCache(targetName, methodName)) {
			// 執行方法返回結果
			return invocation.proceed();
		}
		Object[] arguments = invocation.getArguments();
		String key = getCacheKey(targetName, methodName, arguments);
		System.out.println(key);

		try {
			// 判斷是否有快取
			if (redisUtil.exists(key)) {
				return redisUtil.get(key);
			}
			// 寫入快取
			value = invocation.proceed();
			if (value != null) {
				final String tkey = key;
				final Object tvalue = value;
				new Thread(new Runnable() {
					@Override
					public void run() {
						if (tkey.startsWith("com.service.impl.xxxRecordManager")) {
							redisUtil.set(tkey, tvalue, xxxRecordManagerTime);
						} else if (tkey.startsWith("com.service.impl.xxxSetRecordManager")) {
							redisUtil.set(tkey, tvalue, xxxSetRecordManagerTime);
						} else {
							redisUtil.set(tkey, tvalue, defaultCacheExpireTime);
						}
					}
				}).start();
			}
		} catch (Exception e) {
			e.printStackTrace();
			if (value == null) {
				return invocation.proceed();
			}
		}
		return value;
	}

	/**
	 * 是否加入快取
	 * 
	 * @return
	 */
	private boolean isAddCache(String targetName, String methodName) {
		boolean flag = true;
		if (targetNamesList.contains(targetName)
				|| methodNamesList.contains(methodName)) {
			flag = false;
		}
		return flag;
	}

	/**
	 * 建立快取key
	 *
	 * @param targetName
	 * @param methodName
	 * @param arguments
	 */
	private String getCacheKey(String targetName, String methodName,
			Object[] arguments) {
		StringBuffer sbu = new StringBuffer();
		sbu.append(targetName).append("_").append(methodName);
		if ((arguments != null) && (arguments.length != 0)) {
			for (int i = 0; i < arguments.length; i++) {
				sbu.append("_").append(arguments[i]);
			}
		}
		return sbu.toString();
	}

	public void setRedisUtil(RedisUtil redisUtil) {
		this.redisUtil = redisUtil;
	}
}

4、配置需要快取的類或方法

在application.xml加入如下配置,有多個類或方法可以配置多個

    <!-- 需要加入快取的類或方法 -->
    <bean id="methodCachePointCut"  class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" >
          <property name="advice" >
              <ref local="methodCacheInterceptor" />
          </property>
          <property name="patterns" >
              <list>
               <!-- 確定正規表示式列表 -->
                 <value>com\.mucfc\.msm\.service\.impl\...*ServiceImpl.*</value >
              </list>
          </property>
    </bean >

5、執行結果:

寫了一個簡單的單元測試如下:

    @Test
    public void getSettUnitBySettUnitIdTest() {
        String systemId = "CES";
        String merchantId = "133";
        SettUnit configSettUnit = settUnitService.getSettUnitBySettUnitId(systemId, merchantId, "ESP");
        SettUnit configSettUnit1 = settUnitService.getSettUnitBySettUnitId(systemId, merchantId, "ESP");
        boolean flag= (configSettUnit == configSettUnit1);
        System.out.println(configSettUnit);
        logger.info("查詢結果" + configSettUnit.getBusinessType());

      //  localSecondFIFOCache.put("configSettUnit", configSettUnit.getBusinessType());
     //  String string = localSecondFIFOCache.get("configSettUnit");
//        logger.info("查詢結果" + string);
    }

這是第一次執行單元測試的過程:

MethodCacheInterceptor這個類中打了斷點,然後每次查詢前都會先進入這個方法

依次執行,發現沒有快取,所以會直接去查資料庫

列印了出來的SQL語句:

第二次執行:

因為第一次執行時,已經寫入快取了。所以第二次直接從快取中取資料

3、取兩次的結果進行地址的對比:

發現兩個不是同一個物件,沒錯,是對的。如果是使用Ehcache的話,那麼二者的記憶體地址會是一樣的。那是因為redis和ehcache使用的快取機制是不一樣的。ehcache是基於本地電腦的記憶體使用快取,所以使用快取取資料時直接在本地電腦上取。轉換成java物件就會是同一個記憶體地址,而redis它是在裝有redis服務的電腦上(一般是另一臺電腦),所以取資料時經過傳輸到本地,會對應到不同的記憶體地址,所以用==來比較會返回false。但是它確實是從快取中去取的,這點我們從上面的斷點可以看到。

相關文章