Spring Cache集成redis

Posted zhenghuasheng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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"/>

以上是关于Spring Cache集成redis的主要内容,如果未能解决你的问题,请参考以下文章

spring boot redis 缓存(cache)集成

Spring Cache集成redis

Spring Boot (24) 使用Spring Cache集成Redis

企业级 SpringBoot 教程 (十三)springboot集成spring cache

企业级 SpringBoot 教程 (十三)springboot集成spring cache

SpringBoot系列:Spring Boot集成Spring Cache,使用RedisCache