Redis应用—8.相关的缓存框架
大纲
1.Ehcache缓存框架
(1)Ehcache的核心对象
(2)单独使用Ehcache
(3)Spring整合Ehcache
(4)Spring Boot整合Ehcache
(5)实际工作中如何使用Ehcache
2.Guava Cache缓存框架
(1)Guava Cache具有如下功能
(2)Guava Cache的主要设计思想
(3)Cuava Cache的优势
(4)Cuava Cache核心原理
(6)Guava Cache的单独使用和与Spring集成使用
(7)Guava Cache的几个问题
3.自定义缓存
(1)缓存应该具备的功能
(2)基于LinkedHashMap来实现LRU淘汰策略
(3)基于LinkedList来实现LRU淘汰策略
(4)基于SoftReference实现缓存的内存敏感能力
1.Ehcache缓存框架
(1)Ehcache的核心对象
(2)单独使用Ehcache
(3)Spring整合Ehcache
(4)Spring Boot整合Ehcache
(5)实际工作中如何使用Ehcache
(1)Ehcache的核心对象
一.CacheManager
Cache的容器对象,并管理着Cache的生命周期。
二.Cache
一个Cache可以包含多个Element,并被CacheManager管理。
三.Element
需要缓存的元素,它维护着一个键值对,元素也可以设置有效期。
(2)单独使用Ehcache
一.配置好ehcache.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="false"
dynamicConfig="false">
<!-- ehcache.xsd的内容可以从网址"https://www.ehcache.org/ehcache.xsd"获取 -->
<!-- diskStore: 持久化到磁盘上时的存储位置 -->
<diskStore path="java.io.tmpdir/Tmp_Ehcache"/>
<!-- defaultCache: 默认的缓存策略,如果指定的缓存策略没有找到,那么就用这个默认的缓存策略 -->
<!-- eternal: 缓存对象是否一直存在,如果设置为true,那么timeout就没有效果,缓存就会一直存在,一般默认就是false -->
<!-- maxElementsInMemory: 缓存对象的最大个数 -->
<!-- overflowToDisk: 当内存中缓存对象数量达到maxElementsInMemory时,Ehcache将会把对象写到磁盘中;注意如果缓存的对象要写入到硬盘中,则该对象必须要实现Serializable接口 -->
<!-- diskPersistent: 在JVM崩溃时和重启之间,是否启用持久化的机制 -->
<!-- timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位秒),仅当eternal=false对象不是永久有效时使用,可选属性;默认值是0,也就是可闲置时间无穷大 -->
<!-- timeToLiveSeconds: 设置对象在失效前允许存活时间(单位秒),最大时间介于创建时间和失效时间之间,仅当eternal=false对象不是永久有效时使用;默认值是0,也就是对象存活时间无穷大 -->
<!-- memoryStoreEvictionPolicy: 当缓存对象数量达到maxElementsInMemory时,Ehcache将会根据指定的策略去清理内存;默认策略是LRU(最近最少使用),可以设置为FIFO(先进先出)或是LFU(较少使用) -->
<!-- maxElementsOnDisk: 硬盘最大缓存个数 -->
<!-- name: 缓存名称 -->
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="300"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LRU"
maxElementsOnDisk="100000"
/>
<!-- 手动指定的缓存策略 -->
<!-- 对不同的数据,缓存策略可以在这里配置多种 -->
<cache
name="cacheSpace"
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="300"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
二.在pom.xml文件引入Ehcache的依赖
<!-- 引入Ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
三.创建EhcacheDemo类
public class EhcacheDemo {
public static void main(String[] args) {
//1.获取CacheManager
CacheManager cacheManager = CacheManager.create("./src/main/resources/ehcache.xml");
//2.获取Cache实例,下面的demoCache在ehcache.xml中指定了
Cache demoCache = cacheManager.getCache("demoCache");
//3.存入元素
Element element = new Element("key1", "value1");
demoCache.put(element);
//4.取出元素
Element value = demoCache.get("key1");
System.out.println("value: " + value);
}
}
整体上看,Ehcache的使用是相对简单便捷的,提供了完整的各类API接口。需要注意,虽然Ehcache支持磁盘的持久化,但是由于存在两级缓存介质,在一级内存中的缓存如果没有主动刷入磁盘持久化,则在应用异常宕机时,依然会出现缓存数据丢失,为此可以根据需要将缓存刷到磁盘。将缓存条目刷到磁盘的操作可以通过cache.flush()方法来执行。需要注意:将对象写入磁盘前,要先将对象进行序列化。
(3)Spring整合Ehcache
Spring对缓存的支持类似于对事务的支持。首先使用注解标记方法,相当于定义了切点。然后使用AOP技术在这个方法的调用前、调用后获取方法的入参和返回值,从而实现缓存的逻辑。
一.@Cacheable注解
@Cacheable注解,表明所修饰的方法是可以缓存的。当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。
@Cacheable注解注解可以用condition属性来设置条件。如果不满足条件,就不使用缓存能力,直接执行方法。
可以使用key属性来指定key的生成规则,@Cacheable支持如下几个参数。
参数一:value
缓存位置名称,不能为空。如果使用Ehcache,就是ehcache.xml中声明的cache的name,指明将值缓存到哪个Cache中。
参数二:key
默认情况下,缓存的key就是方法的参数,缓存的value就是方法的返回值。支持SpEL,如果要引用参数值使用井号加参数名,如:#userId。
一般来说,我们的更新操作只需刷新缓存中某一个值。所以定义缓存的key值的方式就很重要,最好是能够唯一。因为这样可以准确清除掉特定的缓存,而不会影响其他缓存值。
下面例子就使用了实体加冒号再加ID组合成键的名称,如"user:1000"。当有多个参数时,默认就使用多个参数来做key。如果只需要其中一个参数做key,则可以在@Cacheable注解中,通过key属性来指定key,如下代码就表示只使用ID作为缓存的key。如果对key有复杂的要求,可自定义keyGenerator,即实现keyGenerator。然后在使用的地方,利用注解中的keyGenerator来指定key生成策略。
参数三:condition
触发条件,只有满足条件的情况才会加入缓存。默认为空,即表示全部都加入缓存,支持SpEL。
二.@CachePut注解
@CachePut不仅会缓存方法的结果,还会执行方法的代码段。@CachePut支持的属性和用法都与@Cacheable一致。
三.@CacheEvict注解
与@Cacheable功能相反,@CacheEvict表明所修饰的方法是用来删除失效或无用的缓存数据。
@CacheEvict支持如下几个参数:
参数一:value,缓存位置名称,不能为空,同上
参数二:key,缓存的key,默认为空,同上
参数三:condition,触发条件,只有满足条件才清除缓存,支持SpEL
参数四:allEntries,true表示清除value中的全部缓存,默认为false
步骤一:在pom.xml引入相关依赖
<properties>
<maven.complier.source>8</maven.complier.source>
<maven.complier.target>8</maven.complier.target>
<junit.version>4.10</junit.version>
<spring.version>4.2.3.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- springframework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 引入Ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
</dependencies>
步骤二:在ehcache.xml添加一个缓存
<cache
name="userCache"
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="1800"
memoryStoreEvictionPolicy="LRU"
/>
步骤三:添加Spring配置文件applicationContext.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.ehcache.*"/>
</beans>
步骤四:新建整合Ehcache配置文件applicationContext-ehcache.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:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-3.2.xsd">
<description>Ehcache缓存配置管理文件</description>
<!-- 开启缓存注解开关 -->
<cache:annotation-driven cache-manager="cacheManager" />
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcache"/>
</bean>
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml"/>
</bean>
</beans>
步骤五:创建EhcacheService、EhcacheServiceImpl
public interface EhcacheService {
User findById(String userId);
}
@Service
public class EhcacheServiceImpl implements EhcacheService {
//@Cacheable注解会将方法返回的结果缓存到key为"'user:' + #userId"的Ehcache缓存中
@Cacheable(value="userCache", key="'user:' + #userId")
@Override
public User findById(String userId) {
System.out.println("execute findById...");
return new User("1000", "test");
}
}
步骤六:创建Spring的测试类TestSpringEhcache
@ContextConfiguration(locations = {"classpath:spring/applicationContext.xml", "classpath:spring/applicationContext-ehcache.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSpringEhcache {
@Autowired
private EhcacheService ehcacheService;
@Autowired
private CacheManager cacheManager;
@Test
public void testFindById() {
User user1 = ehcacheService.findById("1000");
System.out.println(user1);
System.out.println("................");
User user2 = ehcacheService.findById("1000");
System.out.println(user2);
System.out.println("................");
Cache userCache = cacheManager.getCache("userCache");
Element element = userCache.get("user:1000");
System.out.println(element);
}
}
步骤七:自定义缓存key的生成策略
@Component("selfKeyGenerate")
public class SelfKeyGenerate implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "#" + method.getName();
}
}
@Service
public class EhcacheServiceImpl implements EhcacheService {
@Override
//@Cacheable(value="userCache", key="'user:' + #userId")
@Cacheable(value="userCache", keyGenerator="selfKeyGenerate")
public User findById(String userId) {
System.out.println("execute findById...");
return new User("1000", "test");
}
}
步骤八:测试触发条件才加入缓存
public interface EhcacheService {
User findById(String userId);
public boolean isReserved(String userId);
}
@Service
public class EhcacheServiceImpl implements EhcacheService {
...
//将缓存保存进userCache中,并当参数userId的长度小于12时才保存进缓存
@Cacheable(value="userCache", condition="#userId.length() < 12")
public boolean isReserved(String userId) {
System.out.println("userCache: " + userId);
return false;
}
}
@ContextConfiguration(locations = {"classpath:spring/applicationContext.xml", "classpath:spring/applicationContext-ehcache.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSpringEhcache {
@Autowired
private EhcacheService ehcacheService;
@Autowired
private CacheManager cacheManager;
...
@Test
public void testIsReserved() {
ehcacheService.isReserved("123456789101112");
System.out.println("...................");
ehcacheService.isReserved("123456789101112");
System.out.println("...................");
Cache userCache = cacheManager.getCache("userCache");
Element element = userCache.get("123456789101112");
System.out.println(element);
}
}
步骤九:测试@CachePut注解
public interface EhcacheService {
User findById(String userId);
public boolean isReserved(String userId);
public String refreshData(String key);
}
@Service
public class EhcacheServiceImpl implements EhcacheService {
...
@CachePut(value="userCache", key="#key")
public String refreshData(String key) {
System.out.println("模拟从数据库中加载数据");
return key + "::" + String.valueOf(Math.round(Math.random()*1000000));
}
}
@ContextConfiguration(locations = {"classpath:spring/applicationContext.xml", "classpath:spring/applicationContext-ehcache.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSpringEhcache {
@Autowired
private EhcacheService ehcacheService;
@Autowired
private CacheManager cacheManager;
...
@Test
public void testCachePut() {
String key1 = ehcacheService.refreshData("1000");
System.out.println(key1 + ".................");
String key2 = ehcacheService.refreshData("1000");
System.out.println(key2 + ".................");
Cache userCache = cacheManager.getCache("userCache");
Element element = userCache.get("1000");
System.out.println(element);
}
}
步骤十:测试@CacheEvict注解
public interface EhcacheService {
User findById(String userId);
public boolean isReserved(String userId);
public String refreshData(String key);
public void removeUser(String userId);
}
@Service
public class EhcacheServiceImpl implements EhcacheService {
...
//清除userCache中某个指定key的缓存
//@CacheEvict(value="userCache", key="'user' + #userId")
@CacheEvict(value="userCache", allEntries=true)
public void removeUser(String userId) {
System.out.println("userCache" + userId);
}
}
@ContextConfiguration(locations = {"classpath:spring/applicationContext.xml", "classpath:spring/applicationContext-ehcache.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSpringEhcache {
@Autowired
private EhcacheService ehcacheService;
@Autowired
private CacheManager cacheManager;
...
@Test
public void testCacheEvict() {
User user1 = ehcacheService.findById("1000");
System.out.println(user1);
System.out.println("..................");
//删除缓存
ehcacheService.removeUser("1000");
System.out.println("..................");
Cache userCache = cacheManager.getCache("userCache");
Element element = userCache.get("user:1000");
System.out.println(element);
}
}
(4)Spring Boot整合Ehcache
步骤一:创建Spring Boot项目
步骤二:添加Spring Boot相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
步骤三:在resources目录下,添加ehcache的配置文件ehcache.xml
默认情况下,这个文件名是固定的,必须叫ehcache.xml。如果要换一个名字,那么要在application.properties中指定配置文件名。
在application.properties中的配置方式为:
spring.cache.ehcache.config=classpath:xxx.xml
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="false"
dynamicConfig="false">
<!-- ehcache.xsd的内容可以从网址"https://www.ehcache.org/ehcache.xsd"获取 -->
<!-- diskStore: 持久化到磁盘上时的存储位置 -->
<diskStore path="java.io.tmpdir/ehcache"/>
<!-- defaultCache: 默认的缓存策略,如果指定的缓存策略没有找到,那么就用这个默认的缓存策略 -->
<!-- eternal: 缓存对象是否一直存在,如果设置为true,那么timeout就没有效果,缓存就会一直存在,一般默认就是false -->
<!-- maxElementsInMemory: 缓存对象的最大个数 -->
<!-- overflowToDisk: 当内存中缓存对象数量达到maxElementsInMemory时,Ehcache将会把对象写到磁盘中;注意如果缓存的对象要写入到硬盘中,则该对象必须要实现Serializable接口 -->
<!-- diskPersistent: 在JVM崩溃时和重启之间,是否启用持久化的机制 -->
<!-- timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位秒),仅当eternal=false对象不是永久有效时使用,可选属性;默认值是0,也就是可闲置时间无穷大 -->
<!-- timeToLiveSeconds: 设置对象在失效前允许存活时间(单位秒),最大时间介于创建时间和失效时间之间,仅当eternal=false对象不是永久有效时使用;默认值是0,也就是对象存活时间无穷大 -->
<!-- memoryStoreEvictionPolicy: 当缓存对象数量达到maxElementsInMemory时,Ehcache将会根据指定的策略去清理内存;默认策略是LRU(最近最少使用),可以设置为FIFO(先进先出)或是LFU(较少使用) -->
<!-- maxElementsOnDisk: 硬盘最大缓存个数 -->
<!-- name: 缓存名称 -->
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="300"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LRU"
maxElementsOnDisk="100000"
/>
<!-- 手动指定的缓存策略 -->
<!-- 对不同的数据,缓存策略可以在这里配置多种 -->
<cache
name="demoCache"
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
memoryStoreEvictionPolicy="LRU"
/>
<cache
name="userCache"
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="1800"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
步骤四:开启缓存
开启缓存的方式,也和Redis中一样,如下所示,在SpringBoot的启动类中添加@EnableCaching注解即可。
@SpringBootApplication
@EnableCaching
public class SpringBootEhcacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootEhcacheApplication.class, args);
}
}
步骤五:使用缓存,和Sping中使用Ehcache一样
public interface EhcacheService {
User findById(String userId);
public boolean isReserved(String userId);
public String refreshData(String key);
public void removeUser(String userId);
}
@Service
public class EhcacheServiceImpl implements EhcacheService {
@Cacheable(value="userCache", key="'user:' + #userId")
//@Cacheable(value="userCache", keyGenerator="selfKeyGenerate")
public User findById(String userId) {
System.out.println("execute findById...");
return new User("1000", "test");
}
//将缓存保存进userCache中,并当参数userId的长度小于12时才保存进缓存
@Cacheable(value="userCache", condition="#userId.length() < 12")
public boolean isReserved(String userId) {
System.out.println("userCache: " + userId);
return false;
}
@CachePut(value="userCache", key="#key")
public String refreshData(String key) {
System.out.println("模拟从数据库中加载数据");
return key + "::" + String.valueOf(Math.round(Math.random()*1000000));
}
//清除userCache中某个指定key的缓存
//@CacheEvict(value="userCache", key="'user' + #userId")
@CacheEvict(value="userCache", allEntries=true)
public void removeUser(String userId) {
System.out.println("删除userCache" + userId);
}
}
@SpringBootTest
public class SpringBootEhcacheApplicationTests {
@Autowired
private EhcacheService ehcacheService;
@Autowired
private CacheManager cacheManager;
@Test
public void testFindById() {
User user1 = ehcacheService.findById("1000");
System.out.println(user1);
System.out.println("................");
User user2 = ehcacheService.findById("1000");
System.out.println(user2);
System.out.println("................");
Cache userCache = cacheManager.getCache("userCache");
System.out.println(userCache.getKeys());
Element element = userCache.get("user:1000");
System.out.println(element);
}
}
(5)实际工作中如何使用Ehcache
在实际中,更多的是将Ehcache作为与Redis配合的二级缓存(本地缓存)。
第一种方式:
Ehcache所在的应用服务器,通过定时轮询Redis缓存,来更新Ehcache。这种方式的缺点是:每台服务器定时更新Ehcache的时间可能不一样。那么不同服务器刷新最新缓存的时间也不一样,会产生数据不一致问题。对一致性要求不高的场景可以使用。
第二种方式:
通过引入MQ队列,使每台应用服务器的Ehcache同步监听MQ消息。这样通过MQ推送或者拉取的方式,在一定程度上可以达到准同步更新数据。但因为不同服务器之间的网络速度的原因,所以也不能完全达到强一致性。基于此原理使用ZooKeeper等分布式协调通知组件也是如此。
总结:
一.使用二级缓存的好处是减少缓存数据的网络传输开销。当集中缓存出现故障时,Ehcache等本地缓存依然能支撑程序正常使用,这样增加了程序的健壮性。另外使用二级缓存策略可以在一定程度上阻止缓存穿透问题。
二.根据CAP原理可知,如果要使用强一致性缓存,集中式缓存是最佳选择。
2.Guava Cache缓存框架
(1)Guava Cache具有如下功能
(2)Guava Cache的主要设计思想
(3)Cuava Cache的优势
(4)Cuava Cache核心原理
(6)Guava Cache的单独使用和与Spring集成使用
(7)Guava Cache的几个问题
(1)Guava Cache具有如下功能
一.自动将Entry节点加载进缓存中
二.当缓存的数据超过设置的最大值时,使用LRU算法进行缓存清理
三.能够根据Entry节点上次被访问或者写入时间计算它的过期机制
四.缓存的key被封装在WeakReference引用内
五.缓存的value被封装在WeakReference或SoftReference引用内
六.能够统计在使用缓存的过程中命中率、异常率、未命中率等数据
(2)Guava Cache的主要设计思想
Guava Cache基于ConcurrentHashMap的设计思想,其内部大量使用了Segments细粒度锁,既保证线程安全,又提升了并发。
Guava Cache使用Reference引用,保证了GC可回收,有效节省了空间。
Guava Cache分别针对write操作和access操作去设计队列,这样的队列设计能更加灵活高效地实现多种数据类型的缓存清理策略。这些清理策略可以基于容量、可以基于时间、可以基于引用等来实现。
(3)Cuava Cache的优势
一.拥有缓存过期和淘汰机制
采用LRU将不常使用的键值从Cache中删除,淘汰策略还可以基于容量、时间、引用来实现。
二.拥有并发处理能力
GuavaCache类似CurrentHashMap,是线程安全的。它提供了设置并发级别的API,使得缓存支持并发的写入和读取。分段锁是分段锁定,把一个集合看分成若干Partition,每个Partiton一把锁。ConcurrentHashMap就是分了16个区域,这16个区域之间是可以并发的。GuavaCache采用Segment做分区。
三.缓存统计
可以统计缓存的加载、命中情况。
四.更新锁定
一般情况下,在缓存中查询某个key,如果不存在则查源数据,并回填缓存。高并发下可能会出现多次查源并回填缓存,造成数据源宕机,性能下降。GuavaCache可以在CacheLoader的load()方法中加以控制:对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。
(4)Cuava Cache核心原理
Guava Cache的数据结构和CurrentHashMap相似,核心区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。而Guava Cache为了限制内存占用,通常都设定为自动回收元素。
一.LocalCache为Guava Cache的核心类,有一个Segment数组
与ConcurrentHashMap类似,Guava Cache的并发也是通过分段锁来实现的。LoadingCache将映射表分为多个Segment,Segment元素之间可以并发访问。这样就能大大提高并发的效率,降低并发冲突的可能性。
二.Segement数组的长度决定了Cache的并发数
GuavaCache通过设置concurrencyLevel使得缓存支持并发的写入和读取,Segment数组的长度 = concurrencyLevel。
三.每一个Segment使用了单独的锁
每个Segment都继承ReentrantLock,对Segment的写操作需要先拿到锁,每个Segment由一个table和5个队列组成。
四.Segment的5个队列
队列一:键引用队列
已经被GC,需要内部清理的键引用队列。
ReferenceQueue<K> keyReferenceQueue,键引用队列
队列二:值引用队列
已经被GC,需要内部清理的值引用队列。
ReferenceQueue<V> valueReferenceQueue,值引用队列
队列三:LRU队列
当segment达到临界值发生写操作时该队列会移除数据。
Queue<ReferenceEntry<K, V>> recencyQueue,LRU队列
队列四:写队列
按写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。
Queue<ReferenceEntry<K, V>> writeQueue,写队列
队列五:访问队列
按访问时间进行排序的元素队列,访问一个元素时会把它加入到队列尾部。
Queue<ReferenceEntry<K, V>> accessQueue,访问队列
五.Segment的一个table
AtomicReferenceArray<ReferenceEntry<K, V>> table;
AtomicReferenceArray可以用原子方式更新其元素的对象引用数组,ReferenceEntry是Guava Cache中对一个键值对节点的抽象。每个ReferenceEntry数组项都是一 条ReferenceEntry链,且一个ReferenceEntry包含key、hash、valueReference、next字段(单链)。Guava Cache使用ReferenceEntry接口来封装一个键值对,使用ValueReference来封装Value值。
六.GuavaCache的回收机制
回收机制一:基于容量回收
在缓存项的数目达到限定值前,采用LRU回收方式。
回收机制二:定时回收
expireAfterAccess:缓存项在给定时间内没有被读写访问,则回收。回收顺序和基于大小回收一样(LRU)。
回收机制三:基于引用回收
通过使用弱引用的键、或弱引用的值、或软引用的值,在GC时回收。GuavaCache构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制,GuavaCache是在每次进行缓存操作时进行惰性删除:如get()或者put()的时候,判断缓存是否过期。
(6)Guava Cache的单独使用和与Spring集成使用
Guava Cache提供CacheBuilder生成器来创建缓存,可方便设置各种参数。
一.单独使用Guava Cache
通过LoadingCache + CacheLoader使用builder()方法创建缓存。
<!-- 引入Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
public class LoadingCacheTest {
@Test
public void test1() throws InterruptedException {
LoadingCache<Long, User> loadingCache = CacheBuilder.newBuilder()
//指定并发级别
.concurrencyLevel(8)
//初始化大小,配合concurrentLevel做分段锁
.initialCapacity(60)
//缓存中最多可放多少个元素
.maximumSize(10)
//从写入开始计算,10s过期
.expireAfterWrite(10, TimeUnit.SECONDS)
//统计命中率
.recordStats()
//缓存中的元素被驱逐出去后会自动回调到这里
.removalListener(new RemovalListener<Long, User>() {
@Override
public void onRemoval(RemovalNotification<Long, User> notification) {
Long key = notification.getKey();
RemovalCause cause = notification.getCause();
System.out.println("key: " + key + "被移除缓存,原因是: " + cause);
}
})
//缓存中获取不到值,会回调到这里
.build(new CacheLoader<Long, User>() {
@Override
public User load(Long key) throws Exception {
//可以在这里进行数据的加载
System.out.println("去存储中加载数据" + key);
return new User(key.toString(), "abc");
}
});
//10秒后缓存前面的缓存会陆续被移除
for (long i = 0; i < 20; i++) {
User user = loadingCache.getUnchecked(i);
System.out.println(user);
TimeUnit.SECONDS.sleep(1);
}
//统计缓存信息
System.out.println(loadingCache.stats().toString());
}
}
二.Spring整合Guava Cache
由于Spring5并没有提供Guava的CacheManager,所以需要自定义CacheManager才能实现创建缓存,可以参考EhCacheCacheManager。
步骤一:编写com.guava.cache.GuavaCacheCacheManager
package com.guava.cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.cache.Cache;
import org.springframework.cache.support.AbstractCacheManager;
import java.util.Collection;
import java.util.LinkedHashSet;
//因为Spring没有自带的Guava Cache的实现,所以这里参考EhCacheCacheManager的实现来进行自定义
public class GuavaCacheCacheManager extends AbstractCacheManager {
//用来加载当前CacheManager要管理哪些Cache
@Override
protected Collection<Cache> loadCaches() {
//获取所有的缓存Cache
com.google.common.cache.Cache<Object, Object> userCache = CacheBuilder.newBuilder().maximumSize(100).build();
GuavaCache guavaUserCache = new GuavaCache("userCache", userCache);
Collection<Cache> caches = new LinkedHashSet<Cache>();
caches.add(guavaUserCache);
return caches;
}
}
步骤二:编写com.guava.cache.GuavaCache
注意:实现多级缓存时,一种实现是分别使用SpringCacheManager来管理本地缓存、使用Redisson来管理Redis分布式缓存,此时需要同时维护两份数据。另一种优雅的实现是自定义一个缓存。
在这个自定义的缓存里去持有本地缓存和分布式缓存,然后在该缓存的get()、put()等方法里完成本地缓存和分布式缓存的同步逻辑。比如在evict()方法里,通过MQ等中间件完成本地缓存和分布式缓存的同步逻辑。
package com.guava.cache;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
//自定义Cache的场景:
//1.集成Guava Cache时,仿照EhCacheCache来自定义一个GuavaCache
//2.实现多级缓存时:
//一种实现是使用SpringCacheManager来管理本地缓存,使用Redisson来管理Redis分布式缓存,此时需要同时维护两份数据;
//另一种优雅的实现,就是自定义一个缓存,在这个缓存里去持有本地缓存和分布式缓存,然后在该缓存的get、put、evict方法里完成本地缓存和分布式缓存的同步逻辑
//比如在这里的evict()方法里,就需要通过MQ等方式完成本地缓存和分布式缓存的同步逻辑;
public class GuavaCache implements Cache {
private String cacheName;//Cache的名字
//使用组合模式持有真正的Cache对象
private com.google.common.cache.Cache<Object, Object> internalCache;
public GuavaCache(String cacheName, com.google.common.cache.Cache<Object, Object> internalCache) {
this.cacheName = cacheName;
this.internalCache = internalCache;
}
public String getName() {
return cacheName;
}
public Object getNativeCache() {
return internalCache;
}
public ValueWrapper get(Object key) {
Object object = internalCache.getIfPresent(key);
if (object != null) {
//返回SimpleValueWrapper的默认实现
return new SimpleValueWrapper(object);
}
return null;
}
public <T> T get(Object key, Class<T> type) {
throw new RuntimeException("参考get实现");
}
public void put(Object key, Object value) {
internalCache.put(key, value);
}
public ValueWrapper putIfAbsent(Object key, Object value) {
internalCache.put(key, value);
Object object = internalCache.getIfPresent(key);
if (object != null) {
//返回SimpleValueWrapper的默认实现
return new SimpleValueWrapper(object);
}
return null;
}
//逐出
public void evict(Object key) {
internalCache.invalidate(key);
}
public void clear() {
}
}
步骤三:配置spring/applicationContext-guava-cache.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:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-4.0.xsd">
<description>Guava Cache缓存配置管理文件</description>
<!-- 对原生的CacheManager进行包装,org.springframework.cache.CacheManager有多个实现 -->
<bean id="guavaCacheCacheManager" class="com.guava.cache.GuavaCacheCacheManager" />
<!-- 跟org.springframework.cache.annotation.EnableCaching一样 -->
<cache:annotation-driven proxy-target-class="true" cache-manager="guavaCacheCacheManager" />
<bean class="com.guava.cache.impl.UserServiceImpl" />
</beans>
步骤四:最后进行测试
import com.guava.cache.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.cache.CacheManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
//注意:
//Spring集成任何第三方框架的方式:通过xml注入或在@Configuration配置类中的@bean来注入
//SpringBoot集成任何第三方框架的方式:通过starter或@Configuration配置类中的@bean来注入
@ContextConfiguration(locations = {"classpath:spring/applicationContext-guava-cache.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSpringGuavaCache {
@Resource
private CacheManager cacheManager;
@Resource
UserService userService;
//测试自定义CacheManager
//步骤一:编写com.guava.cache.GuavaCacheCacheManager
//步骤二:com.guava.cache.GuavaCache
//步骤三:配置spring/applicationContext-guava-cache.xml
@Test
public void test1() {
System.out.println(cacheManager.getClass());
User user = userService.getById(1L);
System.out.println("接口返回的结果: " + user);
String cache = String.valueOf(cacheManager.getCache("userCache").get(1L).get());
System.out.println("缓存读取的结果: " + cache);
}
}
用于测试的接口:
package com.guava.cache.impl;
import com.guava.cache.UserService;
import com.guava.cache.entity.User;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
@CacheConfig(cacheNames = {"userCache"})
public class UserServiceImpl implements UserService {
@Cacheable(key = "#id")
public User getById(Long id) {
System.out.println("模拟查询DB");
User user = new User();
user.setId(id);
user.setName("demo");
return user;
}
}
(7)Guava Cache的几个问题
一.Guava Cache会OOM吗
会,当设置缓存永不过期或者很长,缓存的对象不限个数或者很大时,不断向GuavaCache加入大字符串,最终就会OOM。解决方案:缓存时间设置相对小些,使用弱引用方式存储对象。
二.Guava Cache缓存到期就会立即清除吗
不会,Guava Cache在每次进行缓存操作时,会判断缓存是否过期。如果一个对象放入缓存后,不再有任何缓存操作,则该缓存不会主动过期。
三.Guava Cache如何找出最久未使用的数据
accessQueue是按照LRU的顺序存放缓存对象(ReferenceEntry)的,accessQueue会把访问过的对象放到队列的最后。accessQueue可以很方便更新和删除链表中的节点,因为每次访问时都可能需要更新该链表,放入到链表的尾部。这样每次从access中拿出的头节点就是最久未使用的数据,writeQueue会用来保存最久未更新的缓存队列,和accessQueue一样。
3.自定义缓存
(1)缓存应该具备的功能
(2)基于LinkedHashMap来实现LRU淘汰策略
(3)基于LinkedList来实现LRU淘汰策略
(4)基于SoftReference实现缓存的内存敏感能力
(1)缓存应该具备的功能
一.最大能放多少个,超出数量时的溢出淘汰机制:LRU、FIFO、LFU
二.过期需要清除
三.内存要敏感,不能因缓存原因导致OOM
四.需要保证并发下的线程安全
五.统计命中率
六.可以序列化扩展
(2)基于LinkedHashMap来实现LRU淘汰策略
一.LinkedHashMap构造参数的accessOrder能够让访问过的元素排前面
public class SelfCacheTest {
//LRU: Least Recently Used
//要实现基于LRU算法的溢出驱逐;
//1.按访问时间来排序,访问时间最早的排前面;
//2.移除排在最前面;
@Test
public void test1() {
//正常普通使用LinkedHashMap,访问其元素不会影响排序
LinkedHashMap<String, String> linkedHashMap1 = new LinkedHashMap<String, String>();
linkedHashMap1.put("a", "a_value");
linkedHashMap1.put("b", "b_value");
linkedHashMap1.put("c", "c_value");
System.out.println(linkedHashMap1);
String bValue1 = linkedHashMap1.get("b");
System.out.println("b的值: " + bValue1);
System.out.println(linkedHashMap1);
System.out.println("...................");
//创建LinkedHashMap时将accessOrder参数设置为true,则后续访问过的元素会排前面
LinkedHashMap<String, String> linkedHashMap2 = new LinkedHashMap<String, String>(16, 0.75f, true);
linkedHashMap2.put("a", "a_value");
linkedHashMap2.put("b", "b_value");
linkedHashMap2.put("c", "c_value");
System.out.println(linkedHashMap2);
String bValue2 = linkedHashMap2.get("b");
System.out.println("b的值: " + bValue2);
System.out.println(linkedHashMap2);
}
}
二.自定义一个缓存类来基于LinkedHashMap来实现LRU驱逐算法
使用LinkedHashMap来存放缓存对象,并设置accessOrder为true。
//基本的缓存功能接口
public interface Cache {
void put(Object key, Object value);
void remove(Object key);
void clear();
Object get(Object key);
int size();
}
//基于LinkedHashMap实现LRU驱逐算法
public class CacheVersion1 implements Cache {
//缓存容量
private int capacity;
//通过组合关系持有一个内部的真正缓存对象
private InternalCache internalCache;
private static class InternalCache extends LinkedHashMap<Object, Object> {
private int capacity;
public InternalCache(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
return size() > capacity;
}
}
public CacheVersion1(int capacity) {
this.capacity = capacity;
internalCache = new InternalCache(capacity);
}
public void put(Object key, Object value) {
internalCache.put(key, value);
}
public void remove(Object key) {
internalCache.remove(key);
}
public void clear() {
internalCache.clear();
}
public Object get(Object key) {
return internalCache.get(key);
}
public int size() {
return internalCache.size();
}
@Override
public String toString() {
return "CacheVersion1{" +
"capacity=" + capacity +
", internalCache=" + internalCache +
'}';
}
}
public class SelfCacheTest {
@Test
public void testCacheVersion1() {
Cache cache = new CacheVersion1(3);
cache.put("a", "a_value");
cache.put("b", "b_value");
cache.put("c", "c_value");
System.out.println(cache);
//访问测试,看看是否改变排序
String bValue = (String)cache.get("b");
System.out.println("b的值: " + bValue);
System.out.println(cache);
//测试是否满了移除
cache.put("d", "d_value");
System.out.println(cache);
}
}
(3)基于LinkedList来实现LRU淘汰策略
使用LinkedList用来存放缓存key的顺序,使用Map来存放缓存对象。
//基于LinkedList实现LRU驱逐算法
public class CacheVersion2 implements Cache {
//缓存容量
private int capacity;
//用来维护缓存key的顺序
private LinkedList<Object> keyList;
//通过组合关系持有一个内部的真正缓存对象
private Map<Object, Object> internalCache;
public CacheVersion2(int capacity) {
this.capacity = capacity;
keyList = new LinkedList<Object>();
internalCache = new HashMap<Object, Object>();
}
public void put(Object key, Object value) {
//调用此方法时,已存在的元素等于容量大小了
if (size() == capacity) {
//移除第一个,因为在get()方法中设置了最新访问的放在最后
Object firstKey = keyList.removeFirst();
internalCache.remove(firstKey);
}
keyList.addLast(key);
internalCache.put(key, value);
}
public void remove(Object key) {
keyList.remove(key);
internalCache.remove(key);
}
public void clear() {
keyList.clear();
internalCache.clear();
}
public Object get(Object key) {
//如果key存在在true,如果key不存在则false
boolean removeResult = keyList.remove(key);
if (removeResult) {
Object value = internalCache.get(key);
//把现在访问的key排序,最新访问的放在最后
keyList.addLast(key);
return value;
}
return null;
}
public int size() {
return internalCache.size();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Object key : keyList) {
sb.append("key:").append(key).append(",").append("value:").append(internalCache.get(key)).append("-----");
}
return sb.toString();
}
}
(4)基于SoftReference实现缓存的内存敏感能力
强引用:只要有引用就不会被回收,即便OOM
软引用:在GC且堆内存快OOM时才会被回收
弱引用:每次GC都会被回收
虚引用:随时都可能被回收
首先对put进去的value值进行SoftReference包装,然后put的是软引用包装后的vale,接着在get的时候先取出软引用包装的值,再返回具体值。
//基于LinkedHashMap实现LRU驱逐算法 + 内存敏感
public class CacheVersionFinal implements Cache {
//缓存容量
private int capacity;
//通过组合关系持有一个内部的真正缓存对象
private InternalCache internalCache;
private static class InternalCache extends LinkedHashMap<Object, Object> {
private int capacity;
public InternalCache(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
return size() > capacity;
}
}
public CacheVersionFinal(int capacity) {
this.capacity = capacity;
internalCache = new InternalCache(capacity);
}
public void put(Object key, Object value) {
SoftReference<Object> softReference = new SoftReference<Object>(value);
internalCache.put(key, softReference);
}
public void remove(Object key) {
internalCache.remove(key);
}
public void clear() {
internalCache.clear();
}
public Object get(Object key) {
Object o = internalCache.get(key);
if (o == null) {
return null;
}
SoftReference<Object> softReference = (SoftReference<Object>) o;
//返回引用的真实对象
return softReference.get();
}
public int size() {
return internalCache.size();
}
@Override
public String toString() {
return "CacheVersion1{" +
"capacity=" + capacity +
", internalCache=" + internalCache +
'}';
}
}
public class Dept {
private Long id;
private byte[] bytes = new byte[1024 * 1024];
public Dept(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public byte[] getBytes() {
return bytes;
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
//JVM要回收这个对象时,会回调这个方法
//可以在该方法里完成资源的清理,或者完成自救
@Override
protected void finalize() throws Throwable {
System.out.println(id + "将要被回收,需要自救.............");
}
}
public class SelfCacheTest {
//测试软引用的内存敏感
//-Xms10M -Xmx10M -XX:+PrintGCDetails
@Test
public void test3() throws InterruptedException {
Cache cache = new CacheVersionFinal(5000);
for (int i = 1; i < Integer.MAX_VALUE; i++) {
System.out.println("放入第" + i + "个");
Dept dept = new Dept((long) i);
cache.put(dept.getId(), dept);
TimeUnit.SECONDS.sleep(1);
}
}
}