1. 程式人生 > >Spring Cache整合redis

Spring Cache整合redis

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

1,redis安裝
Redis安裝指導以及下載地址:http://www.redis.cn/download.html
這裡採用windows版本redis-64.2.8.2101安裝,方便開發

下載解壓安裝包後,在當前目錄下cmd執行:redis-server.exe redis.windows.conf, 具體安裝指導:http://os.51cto.com/art/201403/431103.htm

redis.windows.conf中

# Accept connections on the specified port, default is 6379.
# If port
0 is specified Redis will not listen on a TCP socket. port 6379

為埠設定,預設埠為6379
如果要設定訪問密碼 在redis.windows.conf中,其他配置詳見官方說明

# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
# requirepass 123456

2,spring整合redis快取
如果專案使用了maven,除加入spring的依賴包外,額外將pom.xml中新增以下依賴:

<dependency>  
    <groupId>org.springframework.data</groupId>  
    <artifactId>spring-data-redis</artifactId>  
    <version>1.6.2.RELEASE</version>  
</dependency>  
<dependency>  
    <groupId>redis.clients</groupId>  
    <artifactId>jedis</artifactId>  
    <version>2.8.0</version>  
</dependency>  

如果未使用maven,需新增jar包:

commons-pool2-2.4.2.jar;jedis-2.8.0.jar;spring-data-redis-1.6.2.RELEASE.jar

*2.1,編寫redisCache實現類

/**
 * Created by Administrator on 2016/1/4.
 */
public class RedisCache implements Cache {
    private RedisTemplate<String, Object> redisTemplate;
    private String name;

    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }

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

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

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

    @Override
    public Object getNativeCache() {
        // TODO Auto-generated method stub
        return this.redisTemplate;
    }

    @Override
    public ValueWrapper get(Object key) {
        // TODO Auto-generated method stub
        final String keyf = (String) key;
        Object object = null;
        object = redisTemplate.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection)
                    throws DataAccessException {

                byte[] key = keyf.getBytes();
                byte[] value = connection.get(key);
                if (value == null) {
                    return null;
                }
                return toObject(value);

            }
        });
        return (object != null ? new SimpleValueWrapper(object) : null);
    }

    @Override
    public <T> T get(Object o, Class<T> aClass) {
        return null;
    }

    @Override
    public void put(Object key, Object value) {
        // TODO Auto-generated method stub
        final String keyf = (String) key;
        final Object valuef = value;
        final long liveTime = 86400;

        redisTemplate.execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection)
                    throws DataAccessException {
                byte[] keyb = keyf.getBytes();
                byte[] valueb = toByteArray(valuef);
                connection.set(keyb, valueb);
                if (liveTime > 0) {
                    connection.expire(keyb, liveTime);
                }
                return 1L;
            }
        });
    }

    @Override
    public ValueWrapper putIfAbsent(Object o, Object o1) {
        return null;
    }

    /**
     * ???? : <Object?byte[]>. <br>
     * <p>
     * <??÷??????>
     * </p>
     *
     * @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>
     * <p>
     * <??÷??????>
     * </p>
     *
     * @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;
    }

    @Override
    public void evict(Object key) {
        // TODO Auto-generated method stub
        final String keyf = (String) key;
        redisTemplate.execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection)
                    throws DataAccessException {
                return connection.del(keyf.getBytes());
            }
        });
    }

    @Override
    public void clear() {
        // TODO Auto-generated method stub
        redisTemplate.execute(new RedisCallback<String>() {
            public String doInRedis(RedisConnection connection)
                    throws DataAccessException {
                connection.flushDb();
                return "ok";
            }
        });
    }
}

2.2,開啟spring cache註解功能

<!-- 啟用快取註解功能,這個是必須的,否則註解不會生效,另外,該註解一定要宣告在spring主配置檔案中才會生效 -->
       <cache:annotation-driven cache-manager="cacheManager"/>

2.3,spring cache詳細配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<context:component-scan base-package="org.cpframework.cache.redis"/>
<context:property-placeholder location="config/redis.properties" />
       <!-- spring自己的換管理器,這裡定義了兩個快取位置名稱 ,既註解中的value -->
       <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
              <property name="caches">
                     <set>
                            <bean class="org.cpframework.cache.redis.RedisCache">
                                   <property name="redisTemplate" ref="redisTemplate" />
                                   <property name="name" value="default"/>
                            </bean>
                            <bean class="org.cpframework.cache.redis.RedisCache">
                                   <property name="redisTemplate" ref="redisTemplate" />
                                   <property name="name" value="commonCache"/>
                            </bean>
                     </set>
              </property>
       </bean>

       <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
              <property name="maxIdle" value="${redis.maxIdle}" />
              <property name="maxTotal" value="${redis.maxActive}" />
              <property name="maxWaitMillis" value="${redis.maxWait}" />
              <property name="testOnBorrow" value="${redis.testOnBorrow}" />
       </bean>

       <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
            p:hostName="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:poolConfig-ref="poolConfig"/>

       <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
              <property name="connectionFactory"   ref="connectionFactory" />
       </bean>

</beans>

2.4 redis.properties檔案

# Redis settings
redis.host=localhost
redis.port=6379
redis.pass=123456

redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true

2.5 測試使用例項類model

package com.ac.pt.model;

import com.ac.pt.service.AccountService;

import java.io.Serializable;

/**
 * Created by Administrator on 2016/1/4.
 */
public class Account implements Serializable {

    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public Account() {
    }

    public Account(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public String  getCacheKey(){
        String key = AccountService.class.getSimpleName()+ "-" +this.getName();
        return key;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

2.6,spring cache詳細講解
快取註解有以下三個:
@Cacheable @CacheEvict @CachePut

@Cacheable(value=”accountCache”),這個註釋的意思是,當呼叫這個方法的時候,會從一個名叫 accountCache 的快取中查詢,如果沒有,則執行實際的方法(即查詢資料庫),並將執行的結果存入快取中,否則返回快取中的物件。這裡的快取中的 key 就是引數 userName,value 就是 Account 物件。“accountCache”快取是在 spring*.xml 中定義的名稱。

  @Cacheable(value="commonCache")// 使用了一個快取名叫 accountCache
    public Account getAccountByName(String userName) {
        // 方法內部實現不考慮快取邏輯,直接實現業務
        System.out.println("real query account."+userName);
        return getFromDB(userName);
    }

    private Account getFromDB(String acctName) {
        System.out.println("real querying db..."+acctName);
        account.setName(acctName);
        account.setId(1);
        return account;
    }

測試程式碼:

/**
 * Created by zhenghuasheng on 2016/5/9.
 */
public class Main {
    public static final String LOG4J_CONFIG_PATH = "/config/log4j.properties";
    private static Logger logger = LoggerFactory.getLogger(Main.class);
    private static String path = null;
    static {
        path= System.getProperty("user.dir");
        PropertyConfigurator.configure(path + LOG4J_CONFIG_PATH);
    }
    public static void main(String[] args) {
        ApplicationContext context = new FileSystemXmlApplicationContext(path+"/config/spring-config.xml");
        AccountService service = context.getBean(AccountService.class);

        logger.info("第一次查詢");
        service.getAccountByName("zhenghuasheng");

        logger.info("第二次查詢");
        service.getAccountByName("zhenghuasheng");
    }
}

測試結果:

[QC] INFO [main] com.ac.pt.service.Main.main(32) | 第一次查詢
real query account.zhenghuasheng
real querying db...zhenghuasheng
[QC] INFO [main] com.ac.pt.service.Main.main(35) | 第二次查詢
Disconnected from the target VM, address: '127.0.0.1:61620', transport: 'socket'

結論:第一次從資料庫查詢,第二次未經過程式碼邏輯直接從快取中獲取

@CacheEvict 註釋來標記要清空快取的方法,當這個方法被呼叫後,即會清空快取。注意其中一個 @CacheEvict(value=”accountCache”,key=”#account.getName()”),其中的 Key 是用來指定快取的 key 的,這裡因為我們儲存的時候用的是 account 物件的 name 欄位,所以這裡還需要從引數 account 物件中獲取 name 的值來作為 key,前面的 # 號代表這是一個 SpEL 表示式,此表示式可以遍歷方法的引數物件,具體語法可以參考 Spring 的相關文件手冊。

 @CacheEvict(value="commonCache",key="#account.getName()")// 清空accountCache 快取
 public void updateAccount1(Account account) {
    updateDB(account);
 }

測試程式碼:

package com.ac.pt.service;


import com.ac.pt.model.Account;
import org.apache.log4j.PropertyConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

/**
 * Created by zhenghuasheng on 2016/5/9.
 */
public class Main {
    public static final String LOG4J_CONFIG_PATH = "/config/log4j.properties";
    private static Logger logger = LoggerFactory.getLogger(Launcher.class);
    private static String path = null;
    static {
        path= System.getProperty("user.dir");
        PropertyConfigurator.configure(path + LOG4J_CONFIG_PATH);
    }
    public static void main(String[] args) {
        ApplicationContext context = new FileSystemXmlApplicationContext(path+"/config/spring-config.xml");
        AccountService service = context.getBean(AccountService.class);

        Account account = new Account(1,"zhenghuasheng");
        service.updateAccount1(account);

        service.getAccountByName("zhenghuasheng");
    }
}

測試結果:

Connected to the target VM, address: '127.0.0.1:62076', transport: 'socket'
update accout .....
real query account.zhenghuasheng
real querying db...zhenghuasheng
Disconnected from the target VM, address: '127.0.0.1:62076', transport: 'socket'

結論:@CacheEvict 註釋來標記要清空快取的方法,當這個方法被呼叫後,即會清空快取。再次查詢時將從從程式碼邏輯從資料庫獲取資料

@CachePut 註釋,這個註釋可以確保方法被執行,同時方法的返回值也被記錄到快取中,實現快取與資料庫的同步更新。

  @CachePut(value="commonCache",key="#account.getName()")// 更新accountCache 快取 ,key="#account.getCacheKey()"
    public Account updateAccount(Account account) {
        return updateDB(account);
    }

測試程式碼:

/**
 * Created by zhenghuasheng on 2016/5/9.
 */
public class Main {
    public static final String LOG4J_CONFIG_PATH = "/config/log4j.properties";
    private static Logger logger = LoggerFactory.getLogger(Launcher.class);
    private static String path = null;
    static {
        path= System.getProperty("user.dir");
        PropertyConfigurator.configure(path + LOG4J_CONFIG_PATH);
    }
    public static void main(String[] args) {
        ApplicationContext context = new FileSystemXmlApplicationContext(path+"/config/spring-config.xml");
        AccountService service = context.getBean(AccountService.class);

        Account account = service.getAccountByName("zhenghuasheng");
        System.out.println(account);

        account = new Account(222,"zhenghuasheng");
        service.updateAccount(account);

        account = service.getAccountByName("zhenghuasheng");
        System.out.println(account);
    }
}

測試結果:

Account{id=1100, name='zhenghuasheng'}
update accout .....
Account{id=222, name='zhenghuasheng'}

Process finished with exit code 0

結論:@CachePut 註釋,這個註釋可以確保方法被執行,同時方法的返回值也被記錄到快取中,實現快取與資料庫的同步更新。

3,@Cacheable、@CachePut、@CacheEvict 註釋介紹

cacheable

put+CacheEvict

4,一些存在的問題

對於使用 @Cacheable 註解的方法,每個快取的 key 生成策略預設使用的是引數名+引數值,比如以下方法:

    @Cacheable(value="commonCache")// 使用了一個快取名叫 accountCache
    public Account getAccountByName(String userName) {
        // 方法內部實現不考慮快取邏輯,直接實現業務
        System.out.println("real query account."+userName);
        return getFromDB(userName);
    }

username取值為zhenghuasheng時,key為userName-zhenghuasheng,一般情況下沒啥問題,二般情況如方法 key 取值相等然後引數名也一樣的時候就出問題了,如:

@Cacheable(value="commonCache")
public LoginData getLoginData(String userName){

}

這個方法的快取也將保存於 key 為 users~keys 的快取下。對於 userName 取值為 “zheghuasheng” 的快取,key 也為 “userName-zhenghuasheng”,將另外一個方法的快取覆蓋掉。
解決辦法是使用自定義快取策略,對於同一業務(同一業務邏輯處理的方法,哪怕是叢集/分散式系統),生成的 key 始終一致,對於不同業務則不一致:


/**
 * Created by zhenghuasheng on 2016/5/9.
 */

@Component
public class LocalGenerator implements KeyGenerator {

    @Override
    public Object generate(Object o, Method method, Object... objects) {

        StringBuilder sb = new StringBuilder();
        String className = o.getClass().getSimpleName();

        sb.append(className + "-");
        for (Object obj : objects) {
            sb.append(obj.toString());
        }
        return sb.toString();
    }
}

配置檔案修改:

<cache:annotation-driven cache-manager="cacheManager" key-generator="localGenerator"/>