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
涉及技术:spring
Spring 缓存注解,即Spring Cache,做用在方法上。当咱们在调用一个缓存方法时会把该方法参数和返回结果做为一个键值对存放在缓存中,等到下次利用一样的参数来调用该方法时将再也不执行该方法,而是直接从缓存中获取结果进行返回。因此在使用Spring Cache的时候咱们要保证咱们缓存的方法对于相同的方法参数要有相同的返回结果。sql
要使用Spring Cache,咱们须要作两件事:数据库
对于第一个问题,这里我就不作介绍了,网上已经有十分红熟的文章供你们参考,主要是@Cacheable、@CacheEvict、@CachePut以及自定义键的SpEL(Spring 表达式语言,Spring Expression Language)的使用,相信部分人有从Spring Boot中了解过这些东西,详细可参考如下文章:apache
对于第二个问题,简单的说下,知道的能够跳过,这里有三种配置方法:编程
不管哪一种配置方法都是在Spring的配置文件进行配置的(不要问我Spring的配置文件是什么)。首先,因为咱们使用的是注解的方式,对Spring不陌生的话,都知道应该要配置个注解驱动,代码以下:segmentfault
<!-- 配置Spring缓存注解驱动 --> <cache:annotation-driven cache-manager="cacheManager"/>
三种方法的不一样体如今CacheManager类以及Cache类的实现上:api
<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>
<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>
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.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
注解用得不少,说明下:
最后,该类要被Spring扫描识别。
关于Redis的介绍,直接去看个人笔记,里面有一些简单又不失全面的介绍,好比Unix Domain Socket相关、一些Redis的基本配置和可视化界面等等。
必要的代码都给出来了,就不贴源码了,Lettuce的TCP、UDS二选一链接方式也能够单独拿出来用。
欢迎你们的指点!
Copyright © 2018, GDUT CSCW back-end Kanarien, All Rights Reserved