spring+ mybatis 二级缓存使用 redis作为缓存

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35336312/article/details/79916627

springMybatisConfig.xml配置

<?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:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx" 
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="  
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd  
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd  
            http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd  
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"
	default-autowire="byName" default-lazy-init="false">

   <!-- 一、使用druid数据库连接池注册数据源 -->  
  <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">  
     <!-- 基础配置 -->  
     <property name="url" value="${jdbc.url}"></property>  
     <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>  
     <property name="username" value="root"></property>  
     <property name="password" value="password"></property>  
  
     <!-- 关键配置 -->  
     <!-- 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 -->   
     <property name="initialSize" value="3" />   
     <!-- 最小连接池数量 -->   
     <property name="minIdle" value="2" />   
     <!-- 最大连接池数量 -->   
     <property name="maxActive" value="15" />  
     <!-- 配置获取连接等待超时的时间 -->   
     <property name="maxWait" value="10000" />  
  
     <!-- 性能配置 -->  
     <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->   
     <property name="poolPreparedStatements" value="true" />   
     <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />  
  
     <!-- 其他配置 -->  
     <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->   
     <property name="timeBetweenEvictionRunsMillis" value="60000" />  
     <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->   
     <property name="minEvictableIdleTimeMillis" value="300000" />  
     <!--   建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,  
               执行validationQuery检测连接是否有效。 -->  
     <property name="testWhileIdle" value="true" />  
     <!-- 这里建议配置为TRUE,防止取到的连接不可用 ,申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。-->   
     <property name="testOnBorrow" value="true" />   
     <!-- 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 -->  
     <property name="testOnReturn" value="false" />  
  </bean>  

	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<!--dataSource属性指定要用到的连接池-->
		<property name="dataSource" ref="dataSource" />
        <!-- 开启缓存支持 -->
        <property name="configurationProperties">
            <props>
           		 <!--全局映射器启用缓存-->
                <prop key="cacheEnabled">true</prop>
                <!-- 查询时,关闭关联对象即时加载以提高性能 -->
                <prop key="lazyLoadingEnabled">false</prop>
                <!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指定),不会加载关联表的所有字段,以提高性能 -->
                <prop key="aggressiveLazyLoading">true</prop>
                <!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
                <prop key="multipleResultSetsEnabled">true</prop>
                <!-- 允许使用列标签代替列名 -->
                <prop key="useColumnLabel">true</prop>
                <!-- 允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值),数据表的PK生成策略将被覆盖 -->
                <prop key="useGeneratedKeys">true</prop>
                <!-- 给予被嵌套的resultMap以字段-属性的映射支持 -->
                <prop key="autoMappingBehavior">FULL</prop>
                <!-- 对于批量更新操作缓存SQL以提高性能 -->
                <prop key="defaultExecutorType">BATCH</prop>
                <!-- 数据库超过25000秒仍未响应则超时 -->
                <prop key="defaultStatementTimeout">25000</prop>
            </props>
        </property>
		 <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:com/dg/mapping/*.xml"></property>
	</bean>

	<!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
    <bean id="transactionManager"  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
	
</beans> 

springRedisConfig.xml

<?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:context="http://www.springframework.org/schema/context"
	xmlns:cache="http://www.springframework.org/schema/cache"
	xsi:schemaLocation="http://www.springframework.org/schema/beans    
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd     
    http://www.springframework.org/schema/context     
    http://www.springframework.org/schema/context/spring-context-4.3.xsd
    http://www.springframework.org/schema/cache
    http://www.springframework.org/schema/cache/spring-cache-4.3.xsd"
	>

	<!-- redis数据源 -->
	<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<!-- 最大空闲数 -->
		<property name="maxIdle" value="400" />
		<!-- 最大空连接数 -->
		<property name="maxTotal" value="6000" />
		<!-- 最大等待时间 -->
		<property name="maxWaitMillis" value="1000" />
		<!-- 连接超时时是否阻塞,false时报异常,ture阻塞直到超时, 默认true -->
		<property name="blockWhenExhausted" value="true" />
		<!-- 返回连接时,检测连接是否成功 -->
		<property name="testOnBorrow" value="true" />
	</bean>

	<!-- Spring-redis连接池管理工厂 -->
	<bean id="jedisConnectionFactory"
		class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
		<!-- IP地址 -->
		<property name="hostName" value="127.0.0.1" />
		<!-- 端口号 -->
		<property name="port" value="6379" />
		<!-- 超时时间 默认2000 -->
		<property name="timeout" value="30000" />
		<!-- 连接池配置引用 -->
		<property name="poolConfig" ref="poolConfig" />
		<!-- usePool:是否使用连接池 -->
		<property name="usePool" value="true" />
	</bean>

	<!-- redis template definition -->
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
		<property name="connectionFactory" ref="jedisConnectionFactory" />
		<property name="keySerializer">
			<bean
				class="org.springframework.data.redis.serializer.StringRedisSerializer" />
		</property>
		<property name="valueSerializer">
			<bean
				class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
		</property>
		<property name="hashKeySerializer">
			<bean
				class="org.springframework.data.redis.serializer.StringRedisSerializer" />
		</property>
		<property name="hashValueSerializer">
			<bean
				class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
		</property>
		<!--开启事务 -->
		<property name="enableTransactionSupport" value="true"></property>
	</bean>
	
	<!-- 已下不是实现mybatis的缓存接口 实现spring caches接口的缓存配置 -->
    <!--使用实现spring caches的缓存方案 缓存管理器-->
    <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
        <property name="caches">
            <set>
                <!--自定义的redis缓存操作实现-->
                <bean class="com.redisCache.RedisCache">
                    <property name="name" value="myCache"/>
                    <property name="redisTemplate" ref="redisTemplate"/>
                </bean>
            </set>
        </property>
    </bean>
    <!--spring 启用缓存注解-->
 	<cache:annotation-driven cache-manager="cacheManager" />  
 	
</beans>


通大多数ORM层框架一样,Mybatis自然也提供了对一级缓存和二级缓存的支持。一下是一级缓存和二级缓存的作用于和定义。

      1、一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。

二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。 

      2、一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。Mybatis默认开启一级缓存。

      二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递参数也相同即最终执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。Mybatis默认没有开启二级缓存需要在setting全局参数中配置开启二级缓存。

      一般的我们将Mybatis和Spring整合时,mybatis-spring包会自动分装sqlSession,而Spring通过动态代理sqlSessionProxy使用一个模板方法封装了select()等操作,每一次select()查询都会自动先执行openSession(),执行完close()以后调用close()方法,相当于生成了一个新的session实例,所以我们无需手动的去关闭这个session(),当然也无法使用mybatis的一级缓存,也就是说mybatis的一级缓存在spring中是没有作用的。

      因此我们一般在项目中实现Mybatis的二级缓存,虽然Mybatis自带二级缓存功能,但是如果实在集群环境下,使用自带的二级缓存只是针对单个的节点,所以我们采用分布式的二级缓存功能。一般的缓存NoSql数据库如redis,Mancache等,或者EhCache都可以实现,从而更好地服务tomcat集群中ORM的查询。

下面主要通过Redis实现Mybatis的二级缓存功能。

1、配置文件中开启二级缓存

[html]  view plain  copy
  1. <setting name="cacheEnabled" value="true"/>  

2、实现Mybatis的Cache接口

package com.redisCache;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.DigestUtils;

public class RedisCache implements Cache {

	private static Logger logger = LoggerFactory.getLogger(RedisCache1.class);

	/** The ReadWriteLock. */
	private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	public final long liveTime = 86400;
	private final String COMMON_CACHE_KEY = "COM:";

	private String id;
	private RedisTemplate<String, Object> redisTemplate;
	private ApplicationContext context;


	public RedisCache() {

	}

	public RedisCache(final String id) {
		if (id == null) {
			throw new IllegalArgumentException("必须传入ID");
		}
		//springBean 获取配置好的 RedisTemplate
		context = new ClassPathXmlApplicationContext("spring-redis.xml");
		redisTemplate =(RedisTemplate)context.getBean("redisTemplate");
		logger.debug(">>>>>>>>>>>>>>>>>>>>>MybatisRedisCache:id=" + id);
		this.id = id;
	}

	private String getKey(Object key) {
		StringBuilder accum = new StringBuilder();
		accum.append(COMMON_CACHE_KEY);
		accum.append(this.id).append(":");
		accum.append(DigestUtils.md5DigestAsHex(String.valueOf(key).getBytes()));
		return accum.toString();
	}

	/**
	 * redis key规则前缀
	 */
	private String getKeys() {
		return COMMON_CACHE_KEY + this.id + ":*";
	}

	@Override
	public String getId() {
		return id;
	}

	@Override
	public void putObject(Object key, Object value) {
		final String keyf = getKey(key);
		final Object valuef = value;
		redisTemplate.execute(new RedisCallback<Long>() {
			public Long doInRedis(RedisConnection connection) throws DataAccessException {
				byte[] keyb = keyf.getBytes();
				byte[] valueb = SerializeUtil.serialize(valuef);
				connection.set(keyb, valueb);
				logger.debug("添加缓存 key:--------" + keyf + " liveTime seconds: " + liveTime);
				if (liveTime > 0) {
					connection.expire(keyb, liveTime);
				}
				return 1L;
			}
		});

	}

	@Override
	public Object getObject(Object key) {
		final String keyf = getKey(key);
		Object object = null;
		object = redisTemplate.execute(new RedisCallback<Object>() {
			public Object doInRedis(RedisConnection connection) throws DataAccessException {
				byte[] k = keyf.getBytes();
				byte[] value = connection.get(k);
				if (value == null) {
					return null;
				}
				return SerializeUtil.unserialize(value);
			}
		});
		return object;
	}

	@Override
	public Object removeObject(Object key) {
		final String keyf = getKey(key);
		Object object = null;
		object = redisTemplate.execute(new RedisCallback<Long>() {
			public Long doInRedis(RedisConnection connection) throws DataAccessException {
				logger.debug("移除缓存 key:--------" + keyf);
				return connection.del(keyf.getBytes());
			}
		});
		return object;
	}

	@Override
	public void clear() {
		redisTemplate.execute(new RedisCallback<Integer>() {
			@Override
			public Integer doInRedis(RedisConnection connection) throws DataAccessException {
				Set<byte[]> keys = connection.keys(getKeys().getBytes());
				int num = 0;
				if (null != keys && !keys.isEmpty()) {
					num = keys.size();
					for(byte[] k:keys) {
						connection.decr(k);
					}
				}
				logger.debug("删除所有Key前缀为 " + getKeys() + "的数目:" + num);
				return 1;
			}

		});

	}

	@Override
	public int getSize() {
		Object object = null;
		object = redisTemplate.execute(new RedisCallback<Integer>() {
			@Override
			public Integer doInRedis(RedisConnection connection) throws DataAccessException {
				Set<byte[]> keys = connection.keys(getKeys().getBytes());
				int num = 0;
				if (null != keys && !keys.isEmpty()) {
					num = keys.size();
				}
				logger.debug("查询所有Key前缀为 " + getKeys() + "的数目:" + num);
				return num;
			}

		});
		return ((Integer) object).intValue();
	}

	@Override
	public ReadWriteLock getReadWriteLock() {
		// TODO Auto-generated method stub
		return readWriteLock;
	}

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

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

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

	public static class SerializeUtil {
		public static byte[] serialize(Object object) {
			ObjectOutputStream oos = null;
			ByteArrayOutputStream baos = null;
			try {
				// 序列化
				baos = new ByteArrayOutputStream();
				oos = new ObjectOutputStream(baos);
				oos.writeObject(object);
				byte[] bytes = baos.toByteArray();
				return bytes;
			} catch (Exception e) {
				e.printStackTrace();
			}
			return null;
		}

		public static Object unserialize(byte[] bytes) {
			if (bytes == null)
				return null;
			ByteArrayInputStream bais = null;
			try {
				// 反序列化
				bais = new ByteArrayInputStream(bytes);
				ObjectInputStream ois = new ObjectInputStream(bais);
				return ois.readObject();
			} catch (Exception e) {
				e.printStackTrace();
			}
			return null;
		}
	}

}

3、二级缓存的实用

我们需要将所有的实体类进行序列化,然后在Mapper中添加自定义cache功能。自定义的缓存类

	<cache eviction="LRU" type="com.RedisCache.RedisCache" />


Mapper XML文件配置支持cache后,文件中所有的Mapper statement就支持了。此时要个别对待某条,需要:
<select id="inetAton" parameterType="string" resultType="integer" useCache=“false”>  
select inet_aton(#{name})
</select>

看如下例子,即一个常用的cache标签属性:

<cache 
eviction="FIFO"  <!--回收策略为先进先出-->
flushInterval="60000" <!--自动刷新时间60s-->
size="512" <!--最多缓存512个引用对象-->
readOnly="true"/> <!--只读-->
  • 1
  • 2
  • 3
  • 4
  • 5

eviction(回收策略)

  1. LRU – 最近最少使用的:移除最长时间不被使用的对象。(默认的属性)

  2. FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

  3. SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。

  4. WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

flushInterval(刷新间隔)

可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size(引用数目)

可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。

readOnly(只读)

可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。



二、注意的几个细节
1、如果readOnly为false,此时要结果集对象是可序列化的。
<cache readOnly="false"/>

2、在SqlSession未关闭之前,如果对于同样条件进行重复查询,此时采用的是local session cache,而不是上面说的这些cache。

3、MyBatis缓存查询到的结果集对象,而非结果集数据,是将映射的PO对象集合缓存起来。


缓存使用的注释方法:

  1. //配置缓存  
  2. @CacheNamespace(size=100,eviction=LruCache.class,implementation=org.mybatis.caches.ehcache.EhcacheCache.class)  
  3.   
  4. public interface IUsersMapper {  
  5.     //打开缓存  
  6.     @Options(useCache=true)  
  7.     @Select("select * from users")  
  8.     public List<Map> findAll();  
  9.       
  10.     @Select("select count(1) from users where userName=#{userName}")  
  11.     public int findById(@Param("userName") String userName);  

  12.     //存储过程  
  13.     @Select("call pp11()")  
  14.     public List<Map> findAll_a();  
  15. }  










当时使用 实现 org.springframework.cache.Cache;

在上面吧这个缓存管理交给 org.springframework.cache.support.SimpleCacheManager 来管理 启用注释即可
package com.redisCache;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.concurrent.Callable;

import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;

public class RedisSpringCache 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() {
		// TODO Auto-generated method stub
		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
		System.out.println("get key");
		final String keyf = key.toString();
		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 void put(Object key, Object value) {
		// TODO Auto-generated method stub
		System.out.println("put key");
		final String keyf = key.toString();
		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;
			}
		});
	}

	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;
	}

	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
		System.out.println("del key");
		final String keyf = key.toString();
		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
		System.out.println("clear key");
		redisTemplate.execute(new RedisCallback<String>() {
			public String doInRedis(RedisConnection connection) throws DataAccessException {
				connection.flushDb();
				return "ok";
			}
		});
	}

	@Override
	public <T> T get(Object key, Class<T> type) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public ValueWrapper putIfAbsent(Object key, Object value) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public <T> T get(Object key, Callable<T> valueLoader) {
		// TODO Auto-generated method stub
		return null;
	}

}

附上:转载的sprigcache的使用: https://blog.csdn.net/u011202334/article/details/61923172


记录下自己项目在用的Spring Cache的使用方式。
Spring的抽象已经做得够好了,适合于大多数场景,非常复杂的就需要自己AOP实现了。
Spring官网的文档挺不错的,但是对Cache这块的介绍不是很详细,结合网上大牛的博文,汇总下文。

缓存概念

缓存简介

缓存,我的理解是:让数据更接近于使用者;工作机制是:先从缓存中读取数据,如果没有再从慢速设备上读取实际数据(数据也会存入缓存);缓存什么:那些经常读取且不经常修改的数据/那些昂贵(CPU/IO)的且对于相同的请求有相同的计算结果的数据。如CPU—L1/L2—内存—磁盘就是一个典型的例子,CPU需要数据时先从 L1/L2中读取,如果没有到内存中找,如果还没有会到磁盘上找。还有如用过Maven的朋友都应该知道,我们找依赖的时候,先从本机仓库找,再从本地服务器仓库找,最后到远程仓库服务器找;还有如京东的物流为什么那么快?他们在各个地都有分仓库,如果该仓库有货物那么送货的速度是非常快的。

缓存命中率

即从缓存中读取数据的次数 与 总读取次数的比率,命中率越高越好:
命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])
Miss率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])

这是一个非常重要的监控指标,如果做缓存一定要健康这个指标来看缓存是否工作良好;

缓存策略

Eviction policy

移除策略,即如果缓存满了,从缓存中移除数据的策略;常见的有LFU、LRU、FIFO:

  • FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除;
  • LRU(Least Recently Used):最久未使用算法,使用时间距离现在最久的那个被移除;
  • LFU(Least Frequently Used):最近最少使用算法,一定时间段内使用次数(频率)最少的那个被移除;

TTL(Time To Live )

存活期,即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)

TTI(Time To Idle)

空闲期,即一个数据多久没被访问将从缓存中移除的时间。

到此,基本了解了缓存的知识,在Java中,我们一般对调用方法进行缓存控制,比如我调用”findUserById(Long id)”,那么我应该在调用这个方法之前先从缓存中查找有没有,如果没有再掉该方法如从数据库加载用户,然后添加到缓存中,下次调用时将会从缓存中获取到数据。

自Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache支持,且提供了Cache抽象;在此之前一般通过AOP实现;使用Spring Cache的好处:

  • 提供基本的Cache抽象,方便切换各种底层Cache;
  • 通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;
  • 提供事务回滚时也自动回滚缓存;
  • 支持比较复杂的缓存逻辑;

对于Spring Cache抽象,主要从以下几个方面学习:

  • Cache API及默认提供的实现
  • Cache注解
  • 实现复杂的Cache逻辑
缓存简介 开涛的博客

Spring Cache简介

Spring3.1开始引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如EHCache、 memcached集成。

其特点总结如下:

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
Spring Cache 介绍 Spring Cache 介绍 - Rollen Holt - 博客园

API介绍

Cache接口

理解这个接口有助于我们实现自己的缓存管理器

[java]  view plain  copy
  1. package org.springframework.cache;  
  2.   
  3. public interface Cache {  
  4.   
  5.     /** 
  6.      * 缓存的名字 
  7.      */  
  8.     String getName();  
  9.   
  10.     /** 
  11.      * 得到底层使用的缓存 
  12.      */  
  13.     Object getNativeCache();  
  14.   
  15.     /** 
  16.      * 根据key得到一个ValueWrapper,然后调用其get方法获取值  
  17.      */  
  18.     ValueWrapper get(Object key);  
  19.   
  20.     /** 
  21.      * 根据key,和value的类型直接获取value   
  22.      */  
  23.     <T> T get(Object key, Class<T> type);  
  24.   
  25.     /** 
  26.      * 存数据 
  27.      */  
  28.     void put(Object key, Object value);  
  29.   
  30.     /** 
  31.      * 如果值不存在,则添加,用来替代如下代码 
  32.      * Object existingValue = cache.get(key); 
  33.      * if (existingValue == null) { 
  34.      *     cache.put(key, value); 
  35.      *     return null; 
  36.      * } else { 
  37.      *     return existingValue; 
  38.      * } 
  39.      */  
  40.     ValueWrapper putIfAbsent(Object key, Object value);  
  41.   
  42.     /** 
  43.      * 根据key删数据 
  44.      */  
  45.     void evict(Object key);  
  46.   
  47.     /** 
  48.      * 清空数据 
  49.      */  
  50.     void clear();  
  51.   
  52.     /** 
  53.      * 缓存值的Wrapper   
  54.      */  
  55.     interface ValueWrapper {  
  56.         /** 
  57.          * 得到value 
  58.          */  
  59.         Object get();  
  60.     }  
  61. }  

默认实现

默认已经实现了几个常用的cache
位于spring-context-x.RELEASE.jar和spring-context-support-x.RELEASE.jar的cache目录下

  • ConcurrentMapCache:基于java.util.concurrent.ConcurrentHashMap
  • GuavaCache:基于Google的Guava工具
  • EhCacheCache:基于Ehcache
  • JCacheCache:基于javax.cache.Cache(不常用)

CacheManager

用来管理多个cache

[java]  view plain  copy
  1. package org.springframework.cache;  
  2.   
  3. import java.util.Collection;  
  4.   
  5. public interface CacheManager {  
  6.   
  7.     /** 
  8.      * 根据cache名获取cache 
  9.      */  
  10.     Cache getCache(String name);  
  11.   
  12.     /** 
  13.      * 得到所有cache的名字 
  14.      */  
  15.     Collection<String> getCacheNames();  
  16.   
  17. }  

默认实现

对应Cache接口的默认实现

  • ConcurrentMapCacheManager / ConcurrentMapCacheFactoryBean
  • GuavaCacheManager
  • EhCacheCacheManager / EhCacheManagerFactoryBean
  • JCacheCacheManager / JCacheManagerFactoryBean

CompositeCacheManager

用于组合CacheManager,可以从多个CacheManager中轮询得到相应的Cache

[xml]  view plain  copy
  1. <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">  
  2.     <property name="cacheManagers">  
  3.         <list>  
  4.             <ref bean="concurrentMapCacheManager"/>  
  5.             <ref bean="guavaCacheManager"/>  
  6.         </list>  
  7.     </property>  
  8.     <!-- 都找不到时,不返回null,而是返回NOP的Cache -->  
  9.     <property name="fallbackToNoOpCache" value="true"/>  
  10. </bean>  

事务

除GuavaCacheManager外,其他Cache都支持Spring事务,如果注解方法出现事务回滚,对应缓存操作也会回滚

缓存策略

都是Cache自行维护,Spring只提供对外抽象API

Cache注解

每个注解都有多个参数,这里不一一列出,建议进入源码查看注释

启用注解

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

@CachePut

写数据

[java]  view plain  copy
  1. @CachePut(value = "addPotentialNoticeCache", key = "targetClass + '.' + #userCode")  
  2. public List<PublicAutoAddPotentialJob.AutoAddPotentialNotice> put(int userCode, List<PublicAutoAddPotentialJob.AutoAddPotentialNotice> noticeList) {  
  3.     LOGGER.info("缓存({})的公客自动添加潜在客的通知", userCode);  
  4.     return noticeList;  
  5. }  

@CacheEvict

失效数据

[java]  view plain  copy
  1. @CacheEvict(value = "addPotentialNoticeCache", key = "targetClass + '.' + #userCode")  
  2. public void remove(int userCode) {  
  3.     LOGGER.info("清除({})的公客自动添加潜在客的通知", userCode);  
  4. }  

@Cacheable

这个用的比较多
用在查询方法上,先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中

[java]  view plain  copy
  1. @Cacheable(value = "kyAreaCache", key="targetClass + '.' + methodName + '.' + #areaId")  
  2. public KyArea findById(String areaId) {  
  3.     // 业务代码省略  
  4. }  

运行流程

  1. 首先执行@CacheEvict(如果beforeInvocation=true且condition 通过),如果allEntries=true,则清空所有
  2. 接着收集@Cacheable(如果condition 通过,且key对应的数据不在缓存),放入cachePutRequests(也就是说如果cachePutRequests为空,则数据在缓存中)
  3. 如果cachePutRequests为空且没有@CachePut操作,那么将查找@Cacheable的缓存,否则result=缓存数据(也就是说只要当没有cache put请求时才会查找缓存)
  4. 如果没有找到缓存,那么调用实际的API,把结果放入result
  5. 如果有@CachePut操作(如果condition 通过),那么放入cachePutRequests
  6. 执行cachePutRequests,将数据写入缓存(unless为空或者unless解析结果为false);
  7. 执行@CacheEvict(如果beforeInvocation=false 且 condition 通过),如果allEntries=true,则清空所有

SpEL上下文数据

在使用时,#root.methodName 等同于 methodName

名称 位置 描述 示例
methodName root对象 当前被调用的方法名 #root.methodName
method root对象 当前被调用的方法 #root.method.name
target root对象 当前被调用的目标对象 #root.target
targetClass root对象 当前被调用的目标对象类 #root.targetClass
args root对象 当前被调用的方法的参数列表 #root.args[0]
caches root对象 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache #root.caches[0].name
argument name 执行上下文 当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数 #user.id
result 执行上下文 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache evict’的beforeInvocation=false) #result

条件缓存

主要是在注解内用condition和unless的表达式分别对参数和返回结果进行筛选后缓存

@Caching

多个缓存注解组合使用

[java]  view plain  copy
  1. @Caching(  
  2.         put = {  
  3.                 @CachePut(value = "user", key = "#user.id"),  
  4.                 @CachePut(value = "user", key = "#user.username"),  
  5.                 @CachePut(value = "user", key = "#user.email")  
  6.         }  
  7. )  
  8. public User save(User user) {  
  9.   
  10. }  

自定义缓存注解

把一些特殊场景的注解包装到一个独立的注解中,比如@Caching组合使用的注解

[java]  view plain  copy
  1. @Caching(  
  2.         put = {  
  3.                 @CachePut(value = "user", key = "#user.id"),  
  4.                 @CachePut(value = "user", key = "#user.username"),  
  5.                 @CachePut(value = "user", key = "#user.email")  
  6.         }  
  7. )  
  8. @Target({ElementType.METHOD, ElementType.TYPE})  
  9. @Retention(RetentionPolicy.RUNTIME)  
  10. @Inherited  
  11. public @interface UserSaveCache {  
  12.   
  13. }  
  14.   
  15. @UserSaveCache  
  16. public User save(User user) {  
  17.   
  18. }  

示例

基于ConcurrentMapCache

自定义CacheManager

我需要使用有容量限制和缓存失效时间策略的Cache,默认的ConcurrentMapCacheManager没法满足
通过实现CacheManager接口定制出自己的CacheManager。
还是拷贝ConcurrentMapCacheManager,使用Guava的Cache做底层容器,因为Guava的Cache容器可以设置缓存策略

新增了exp、maximumSize两个策略变量
修改底层Cache容器的创建

下面只列出自定义的代码,其他的都是Spring的ConcurrentMapCacheManager的代码

[java]  view plain  copy
  1. import com.google.common.cache.CacheBuilder;  
  2. import org.springframework.cache.Cache;  
  3. import org.springframework.cache.CacheManager;  
  4. import org.springframework.cache.concurrent.ConcurrentMapCache;  
  5.   
  6. import java.util.Arrays;  
  7. import java.util.Collection;  
  8. import java.util.Collections;  
  9. import java.util.Map;  
  10. import java.util.concurrent.ConcurrentHashMap;  
  11. import java.util.concurrent.TimeUnit;  
  12.   
  13. /** 
  14.  * 功能说明:自定义的ConcurrentMapCacheManager,新增超时时间和最大存储限制 
  15.  * 作者:liuxing(2015-04-13 18:44) 
  16.  */  
  17. public class ConcurrentMapCacheManager implements CacheManager {  
  18.   
  19.     /** 
  20.      * 过期时间,秒(自定义) 
  21.      */  
  22.     private long exp = 1800;  
  23.     /** 
  24.      * 最大存储数量 (自定义) 
  25.      */  
  26.     private long maximumSize = 1000;  
  27.   
  28.     public void setExp(long exp) {  
  29.         this.exp = exp;  
  30.     }  
  31.   
  32.     public void setMaximumSize(long maximumSize) {  
  33.         this.maximumSize = maximumSize;  
  34.     }  
  35.   
  36.     /** 
  37.      * 创建一个缓存容器,这个方法改写为使用Guava的Cache 
  38.      * @param name 
  39.      * @return 
  40.      */  
  41.     protected Cache createConcurrentMapCache(String name) {  
  42.         return new ConcurrentMapCache(name, CacheBuilder.newBuilder().expireAfterWrite(this.exp, TimeUnit.SECONDS)  
  43.                                                                      .maximumSize(this.maximumSize)  
  44.                                                                      .build()  
  45.                                                                      .asMap(), isAllowNullValues());  
  46.     }  
  47. }  

初始化

xml风格

[xml]  view plain  copy
  1. <!-- 启用缓存注解功能,这个是必须的,否则注解不会生效,指定一个默认的Manager,否则需要在注解使用时指定Manager -->  
  2. <cache:annotation-driven cache-manager="memoryCacheManager"/>  
  3.   
  4. <!-- 本地内存缓存 -->  
  5. <bean id="memoryCacheManager" class="com.dooioo.ky.cache.ConcurrentMapCacheManager" p:maximumSize="2000" p:exp="1800">  
  6.     <property name="cacheNames">  
  7.         <list>  
  8.             <value>kyMemoryCache</value>  
  9.         </list>  
  10.     </property>  
  11. </bean>  

使用

[java]  view plain  copy
  1. @Cacheable(value = "kyMemoryCache", key="targetClass + '.' + methodName")  
  2. public Map<String, String> queryMobiles(){  
  3.     // 业务代码省略  
  4. }  


使用Memcached

一般常用的缓存当属memcached了,这个就需要自己实现CacheManager和Cache
注意我实现的Cache里面有做一些定制化操作,比如对key的处理

创建MemcachedCache

[java]  view plain  copy
  1. import com.dooioo.common.jstl.DyFunctions;  
  2. import com.dooioo.commons.Strings;  
  3. import com.google.common.base.Joiner;  
  4. import net.rubyeye.xmemcached.MemcachedClient;  
  5. import net.rubyeye.xmemcached.exception.MemcachedException;  
  6. import org.slf4j.Logger;  
  7. import org.slf4j.LoggerFactory;  
  8. import org.springframework.cache.Cache;  
  9. import org.springframework.cache.support.SimpleValueWrapper;  
  10.   
  11. import java.util.concurrent.TimeoutException;  
  12.   
  13. /** 
  14.  * 功能说明:自定义spring的cache的实现,参考cache包实现 
  15.  * 作者:liuxing(2015-04-12 13:57) 
  16.  */  
  17. public class MemcachedCache implements Cache {  
  18.   
  19.     private static final Logger LOGGER = LoggerFactory.getLogger(MemcachedCache.class);  
  20.   
  21.     /** 
  22.      * 缓存的别名 
  23.      */  
  24.     private String name;  
  25.     /** 
  26.      * memcached客户端 
  27.      */  
  28.     private MemcachedClient client;  
  29.     /** 
  30.      * 缓存过期时间,默认是1小时 
  31.      * 自定义的属性 
  32.      */  
  33.     private int exp = 3600;  
  34.     /** 
  35.      * 是否对key进行base64加密 
  36.      */  
  37.     private boolean base64Key = false;  
  38.     /** 
  39.      * 前缀名 
  40.      */  
  41.     private String prefix;  
  42.   
  43.     @Override  
  44.     public String getName() {  
  45.         return name;  
  46.     }  
  47.   
  48.     @Override  
  49.     public Object getNativeCache() {  
  50.         return this.client;  
  51.     }  
  52.   
  53.     @Override  
  54.     public ValueWrapper get(Object key) {  
  55.         Object object = null;  
  56.         try {  
  57.             object = this.client.get(handleKey(objectToString(key)));  
  58.         } catch (TimeoutException e) {  
  59.             LOGGER.error(e.getMessage(), e);  
  60.         } catch (InterruptedException e) {  
  61.             LOGGER.error(e.getMessage(), e);  
  62.         } catch (MemcachedException e) {  
  63.             LOGGER.error(e.getMessage(), e);  
  64.         }  
  65.   
  66.         return (object != null ? new SimpleValueWrapper(object) : null);  
  67.     }  
  68.   
  69.     @Override  
  70.     public <T> T get(Object key, Class<T> type) {  
  71.         try {  
  72.             Object object = this.client.get(handleKey(objectToString(key)));  
  73.             return (T) object;  
  74.         } catch (TimeoutException e) {  
  75.             LOGGER.error(e.getMessage(), e);  
  76.         } catch (InterruptedException e) {  
  77.             LOGGER.error(e.getMessage(), e);  
  78.         } catch (MemcachedException e) {  
  79.             LOGGER.error(e.getMessage(), e);  
  80.         }  
  81.   
  82.         return null;  
  83.     }  
  84.   
  85.     @Override  
  86.     public void put(Object key, Object value) {  
  87.         if (value == null) {  
  88. //            this.evict(key);  
  89.             return;  
  90.         }  
  91.   
  92.         try {  
  93.             this.client.set(handleKey(objectToString(key)), exp, value);  
  94.         } catch (TimeoutException e) {  
  95.             LOGGER.error(e.getMessage(), e);  
  96.         } catch (InterruptedException e) {  
  97.             LOGGER.error(e.getMessage(), e);  
  98.         } catch (MemcachedException e) {  
  99.             LOGGER.error(e.getMessage(), e);  
  100.         }  
  101.     }  
  102.   
  103.     @Override  
  104.     public ValueWrapper putIfAbsent(Object key, Object value) {  
  105.         this.put(key, value);  
  106.         return this.get(key);  
  107.     }  
  108.   
  109.     @Override  
  110.     public void evict(Object key) {  
  111.         try {  
  112.             this.client.delete(handleKey(objectToString(key)));  
  113.         } catch (TimeoutException e) {  
  114.             LOGGER.error(e.getMessage(), e);  
  115.         } catch (InterruptedException e) {  
  116.             LOGGER.error(e.getMessage(), e);  
  117.         } catch (MemcachedException e) {  
  118.             LOGGER.error(e.getMessage(), e);  
  119.         }  
  120.     }  
  121.   
  122.     @Override  
  123.     public void clear() {  
  124.         try {  
  125.             this.client.flushAll();  
  126.         } catch (TimeoutException e) {  
  127.             LOGGER.error(e.getMessage(), e);  
  128.         } catch (InterruptedException e) {  
  129.             LOGGER.error(e.getMessage(), e);  
  130.         } catch (MemcachedException e) {  
  131.             LOGGER.error(e.getMessage(), e);  
  132.         }  
  133.     }  
  134.   
  135.     public void setName(String name) {  
  136.         this.name = name;  
  137.     }  
  138.   
  139.     public MemcachedClient getClient() {  
  140.         return client;  
  141.     }  
  142.   
  143.     public void setClient(MemcachedClient client) {  
  144.         this.client = client;  
  145.     }  
  146.   
  147.     public void setExp(int exp) {  
  148.         this.exp = exp;  
  149.     }  
  150.   
  151.     public void setBase64Key(boolean base64Key) {  
  152.         this.base64Key = base64Key;  
  153.     }  
  154.   
  155.     public void setPrefix(String prefix) {  
  156.         this.prefix = prefix;  
  157.     }  
  158.   
  159.     /** 
  160.      * 处理key 
  161.      * @param key 
  162.      * @return 
  163.      */  
  164.     private String handleKey(final String key) {  
  165.         if (base64Key) {  
  166.             return Joiner.on(EMPTY_SEPARATOR).skipNulls().join(this.prefix, DyFunctions.base64Encode(key));  
  167.         }  
  168.   
  169.         return Joiner.on(EMPTY_SEPARATOR).skipNulls().join(this.prefix, key);  
  170.     }  
  171.   
  172.     /** 
  173.      * 转换key,去掉空格 
  174.      * @param object 
  175.      * @return 
  176.      */  
  177.     private String objectToString(Object object) {  
  178.         if (object == null) {  
  179.             return null;  
  180.         } else if (object instanceof String) {  
  181.             return Strings.replace((String) object, " ""_");  
  182.         } else {  
  183.             return object.toString();  
  184.         }  
  185.     }  
  186.   
  187.     private static final String EMPTY_SEPARATOR = "";  
  188.   
  189. }  

创建MemcachedCacheManager

继承AbstractCacheManager

[java]  view plain  copy
  1. import org.springframework.cache.Cache;  
  2. import org.springframework.cache.support.AbstractCacheManager;  
  3.   
  4. import java.util.Collection;  
  5.   
  6. /** 
  7.  * 功能说明:memcachedCacheManager 
  8.  * 作者:liuxing(2015-04-12 15:13) 
  9.  */  
  10. public class MemcachedCacheManager extends AbstractCacheManager {  
  11.   
  12.     private Collection<Cache> caches;  
  13.   
  14.     @Override  
  15.     protected Collection<? extends Cache> loadCaches() {  
  16.         return this.caches;  
  17.     }  
  18.   
  19.     public void setCaches(Collection<Cache> caches) {  
  20.         this.caches = caches;  
  21.     }  
  22.   
  23.     public Cache getCache(String name) {  
  24.         return super.getCache(name);  
  25.     }  
  26.   
  27. }  

初始化

[xml]  view plain  copy
  1. <!-- 启用缓存注解功能,这个是必须的,否则注解不会生效,指定一个默认的Manager,否则需要在注解使用时指定Manager -->  
  2. <cache:annotation-driven cache-manager="cacheManager"/>  
  3.   
  4. <!-- memcached缓存管理器 -->  
  5. <bean id="cacheManager" class="com.dooioo.ky.cache.MemcachedCacheManager">  
  6.     <property name="caches">  
  7.         <set>  
  8.             <bean class="com.dooioo.ky.cache.MemcachedCache" p:client-ref="ky.memcachedClient" p:name="kyAreaCache" p:exp="86400"/>  
  9.             <bean class="com.dooioo.ky.cache.MemcachedCache" p:client-ref="ky.memcachedClient" p:name="kyOrganizationCache" p:exp="3600"/>  
  10.         </set>  
  11.     </property>  
  12. </bean>  

使用

[java]  view plain  copy
  1. @Cacheable(value = "kyAreaCache", key="targetClass + '.' + methodName + '.' + #areaId")  
  2. public KyArea findById(String areaId) {  
  3.     // 业务代码省略  
  4. }  


猜你喜欢

转载自blog.csdn.net/qq_35336312/article/details/79916627