GUAVA本地缓存01_概述优缺点创建方式回收机制监听器统计异步锁定
Posted 所得皆惊喜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GUAVA本地缓存01_概述优缺点创建方式回收机制监听器统计异步锁定相关的知识,希望对你有一定的参考价值。
文章目录
- ①. 本地缓存 - 背景
- ②. 本地缓存 - 优缺点
- ③. Guava Cache介绍
- ④. Guava - 三种创建方式
- ⑤. Guava - 如何回收缓存
- ⑥. Guava - 移除监听器
- ⑦. Guava - 统计功能
- ⑧. Guava - asMap视图
- ⑨. 异步锁定 - refreshAfterWrites
- ⑩. 核心原理之数据结构
①. 本地缓存 - 背景
-
①. 在高性能的服务架构设计中,缓存是一个不可或缺的环节。在实际的项目中,我们通常会将一些热点数据存储到Redis或Memcached 这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时,也能降低数据库的压力
-
②. 随着不断的发展,这一架构也产生了改进,在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能。于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构
-
③. 在先不考虑并发等复杂问题的情况下,两级缓存的访问流程可以用下面这张图来表示:
@Service
public class PositionServiceImpl implements PositionService
@Autowired
PositionMapper positionMapper;
@Autowired
RedisTemplate redisTemplate;
private static Cache<Object, Object> cache = CacheBuilder.newBuilder().expireAfterWrite(5,TimeUnit.SECONDS).build();
@Override
public List<Position> getHotPosition() throws ExecutionException
//从guava本地缓存中获取数据,如果没有则从redis中回源
Object value = cache.get("position", new Callable()
@Override
public Object call() throws Exception
return getHotPositionListFromRedis();
);
if(value != null)
return (List<Position>)value;
return null;
@Override
public List<Position> getHotPositionListFromRedis()
Object position = redisTemplate.opsForValue().get("position");
System.out.println("从redis中获取数据");
if(position == null)
//从mysql中获取数据
List<Position> positionList = positionMapper.select(null);
System.out.println("从mysql中获取数据");
//同步至redis
redisTemplate.opsForValue().set("position",positionList);
System.out.println("同步至redis");
redisTemplate.expire("position",5, TimeUnit.SECONDS);
//getHotPositionListFromRedis在guava不存在的时候调用,这里会将数据写入到guava本地缓存中
return positionList;
return (List<Position>)position;
②. 本地缓存 - 优缺点
- ①. 优点:
- 查询频率高的场景(重复查,并且耗时的情况)
- 数量少,不会超过内存总量(缓存中存放的数据不会超过内存空间)
- 以空间换取时间,就是你愿意用内存的消耗来换取读取性能的提升
- ②. 不足
- 数据存放在本机的内存,未持久化到硬盘,机器重启会丢失
- 单机缓存,受机器容量限制
- 多个应用实例出现缓存数据不一致的问题
③. Guava Cache介绍
- ①. JVM缓存,是堆缓存。其实就是创建一些全局容器,比如List、Set、Map等。这些容器用来做数据存储,这样做的问题:
- 不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO
- 清除数据时的回调通知
- 并发处理能力差,针对并发可以使用CurrentHashMap,但缓存的其他功能需要自行实现缓存过期处理,缓存数据加载刷新等都需要手工实现
-
②. Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制(JVM缓存)
Guava cache的设计来源于CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。 -
③. Guava Cache的优势:
- 缓存过期和淘汰机制
在GuavaCache中可以设置Key的过期时间,包括访问过期和创建过期
GuavaCache在缓存容量达到指定大小时,采用LRU的方式,将不常使用的键值从Cache中删除 - 并发处理能力
GuavaCache类似CurrentHashMap,是线程安全的。
提供了设置并发级别的api,使得缓存支持并发的写入和读取
采用分离锁机制,分离锁能够减小锁力度,提升并发能力分离锁是分拆锁定,把一个集合看分成若干partition, 每个partiton一把锁。ConcurrentHashMap就是分了16个区域,这16个区域之间是可以并发的。GuavaCache采用Segment做分区 - 更新锁定
一般情况下,在缓存中查询某个key,如果不存在,则查源数据,并回填缓存(Cache AsidePattern)
在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降
GuavaCache可以在CacheLoader的load方法中加以控制,对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待
④. Guava - 三种创建方式
- ①. 导入依赖
<!--引入guava缓存-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.0-jre</version>
</dependency>
- ②. 方式一:使用CacheLoader插件
- 使用场景:首先问自己一个问题:有没有合理的默认方法来加载或计算与键关联的值?如果有的话,你应当使用CacheLoader。首选使用,因为它可以更容易地推断所有缓存内容的一致性
- LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法
private static void method1() throws ExecutionException, InterruptedException
LoadingCache<String, String> cacheLoadInit = CacheBuilder
.newBuilder()
.initialCapacity(5)
.maximumSize(100)
.expireAfterWrite(5000, TimeUnit.MILLISECONDS) //
.removalListener(new RemovalListener<String, String>()
@Override
public void onRemoval(RemovalNotification<String, String> removalNotification)
System.out.println(removalNotification.getKey() + ":remove==> :" + removalNotification.getValue());
)
.build(
new CacheLoader<String, String>()
@Override
public String load(String key) throws Exception
System.out.println("first init.....");
return key;
);
//由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常
String key = cacheLoadInit.get("first-name");
String key2 = cacheLoadInit.get("first-name");
System.out.println(key);
TimeUnit.MILLISECONDS.sleep(5000);
System.out.println("==========");
//设置了5s的过期时间,5s后会重新加载缓存
String keySecond = cacheLoadInit.get("first-name");
System.out.println("5s后重新加载缓存数据" + keySecond);
/**
* first init.....
* first-name
* ==========
* first init.....
* 5s后重新加载缓存数据first-name
*/
- ③. 方式二:Cache.put方法直接插入
private static void method2()
Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.initialCapacity(1)
.maximumSize(2)
.removalListener(new RemovalListener<String, String>()
@Override
public void onRemoval(RemovalNotification<String, String> notification)
System.out.println(notification.getKey()+"移除了,value:"+notification.getValue());
)
.build();
// getIfPresent(key):从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null
String key1 = cache.getIfPresent("java金融1");
System.out.println(key1);//null
if(StringUtils.isEmpty(key1))
/**
* null
* value - java金融2
* value - java金融3
*/
cache.put("java金融1","value - java金融1");
cache.put("java金融2","value - java金融2");
cache.put("java金融3","value - java金融3");
System.out.println(cache.getIfPresent("java金融1"));
System.out.println(cache.getIfPresent("java金融2"));
System.out.println(cache.getIfPresent("java金融3"));
- ④. 方式三:调用get时传入一个Callable实例
/**
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable<V>)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
Cache<Key, Graph> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
...
try
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable<Key, Graph>()
@Override
public Value call() throws AnyException
return doThingsTheHardWay(key);
);
catch (ExecutionException e)
throw new OtherException(e.getCause());
* @throws ExecutionException
*/
private static void method3() throws ExecutionException
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.initialCapacity(1)
.expireAfterWrite(1,TimeUnit.MINUTES)
.build();
cache.get("key1",new Callable<String>()
@Override
public String call() throws Exception
return "value1";
);
/**
* value1
* null
*/
System.out.println(cache.getIfPresent("key1"));
System.out.println("手动清除缓存");
cache.invalidateAll();
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
- ⑤. cache.get方法的两个参数:key,Callable对象
- Cache的get方法有两个参数,第一个参数是要从Cache中获取记录的key,第二个记录是一个Callable对象
- 当缓存中已经存在key对应的记录时,get方法直接返回key对应的记录。如果缓存中不包含key对应的记录,Guava会启动一个线程执行Callable对象中的call方法,call方法的返回值会作为key对应的值被存储到缓存中,并且被get方法返回
- Guava可以保证当有多个线程同时访问Cache中的一个key时,如果key对应的记录不存在,Guava只会启动一个线程执行get方法中Callable参数对应的任务加载数据存到缓存。当加载完数据后,任何线程中的get方法都会获取到key对应的值
⑤. Guava - 如何回收缓存
- ①. 基于容量的回收size-based eviction(maximumSize)
private static void maximumSizeMethod() throws ExecutionException
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.initialCapacity(1)
.maximumSize(3)
.build(new CacheLoader<String, String>()
@Override
public String load(String key) throws Exception
return key + " - value";
);
cache.get("1");
cache.get("2");
cache.get("3");
cache.get("4");
// 最大maximumSize=3 这里添加了四个元素进来了
// 这里是采用的URL 和 FIFO的方式去进行移除元素
/**
* null
* 2 - value
* 3 - value
* 4 - value
*/
System.out.println(cache.getIfPresent("1"));
System.out.println(cache.getIfPresent("2"));
System.out.println(cache.getIfPresent("3"));
System.out.println(cache.getIfPresent("4"));
- ②. 定时回收Timed Eviction:CacheBuilder提供两种定时回收的方法:
- expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样
- expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问创建或覆盖,则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的
private static void expireAfterAccessMethod() throws InterruptedException
// 隔多长时间后没有被访问过的key被删除
Cache<String, String> build = CacheBuilder.newBuilder()
//缓存中的数据 如果3秒内没有访问则删除
.expireAfterAccess(3000, TimeUnit.MILLISECONDS)
.build();
build.put("1","1 - value");
build.put("2","2 - value");
build.put("3","3 - value");
Thread.sleep(1000);
build.getIfPresent("1");
Thread.sleep(2100);
// 这里在停顿了1s后,重新获取了,这个时候再次停顿2s,2,3没有发访问,过期了
System.out.println(build.getIfPresent("1"));
// 停顿3s后,这里也过期了
Thread.sleep(3000);
System.out.println("这里已经过了3s了");
System.out.println(build.getIfPresent("1"));
private static void expireAfterWriteDemo() throws ExecutionException, InterruptedException
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
// 等同于expire ttl 缓存中对象的生命周期就是3秒
.expireAfterWrite(3, TimeUnit.SECONDS)
.initialCapacity(1)
.maximumSize(5)
.removalListener(new RemovalListener<String, String>()
@Override
public void onRemoval(RemovalNotification<String, String> removalNotification)
System.out.println("onRemoval ==> key:" + removalNotification.getKey() + "value:" + removalNotification.getValue());
)
.build(new CacheLoader<String, String>()
@Override
public String load(String s) throws Exception
return s + " - value";
);
cache.get("1");
cache.get("2");
Thread.sleep(1000);
cache.getIfPresent("1");
Thread.sleep(2100);
// 虽然前面有访问,expireAfterWrite相当于ttl,这里返回的数据是null
System.out.println(cache.getIfPresent("1"));
- ③. 基于引用的回收Reference-based Eviction(通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收。关于软引用个弱引用的概念可以参考强引用、弱引用、软引用、虚引用)
- CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它强或软引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式,使用弱引用键的缓存用而不是equals比较键。
- CacheBuilder.weakValues():使用弱引用存储值。当值没有其它强或软引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式,使用弱引用值的缓存用而不是equals比较值。
- CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定见上文,基于容量回收。使用软引用值的缓存同样用==而不是equals比较值。
public class GuavaExpireDemo
public static void main(String[] args) throws InterruptedException, ExecutionException
Cache<String, Object> cache REDIS6_分布式存储极致性能目录