guava cache学习

Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

通常来说,Guava Cache适用于:

  • 你愿意消耗一些内存空间来提升速度。
  • 你预料到某些键会被查询一次以上。
  • 缓存中存放的数据总量不会超出内存容量。

Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。

缓存回收

一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。你你必须决定:什么时候某个缓存项就不值得保留了?Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

基于容量的回收(size-based eviction)

如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。

另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。

01 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
02         .maximumWeight(100000)
03         .weigher(new Weigher<Key, Graph>() {
04             public int weigh(Key k, Graph g) {
05                 return g.vertices().size();
06             }
07         })
08         .build(
09             new CacheLoader<Key, Graph>() {
10                 public Graph load(Key key) { // no checked exception
11                     return createExpensiveGraph(key);
12                 }
13             });

定时回收(Timed Eviction)

CacheBuilder提供两种定时回收的方法:

  • expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
  • expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

如下文所讨论,定时回收周期性地在写操作中执行,偶尔在读操作中执行。

测试定时回收

对定时回收进行测试时,不一定非得花费两秒钟去测试两秒的过期。你可以使用Ticker接口和CacheBuilder.ticker(Ticker)方法在缓存中自定义一个时间源,而不是非得用系统时钟。

基于引用的回收(Reference-based Eviction)

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
  • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。

显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:

移除监听器

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。

请注意,RemovalListener抛出的任何异常都会在记录到日志后被丢弃[swallowed]。

01 CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
02     public DatabaseConnection load(Key key) throws Exception {
03         return openConnection(key);
04     }
05 };
06  
07 RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
08     public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
09         DatabaseConnection conn = removal.getValue();
10         conn.close(); // tear down properly
11     }
12 };
13  
14 return CacheBuilder.newBuilder()
15     .expireAfterWrite(2, TimeUnit.MINUTES)
16     .removalListener(removalListener)
17     .build(loader);

警告:默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。

清理什么时候发生?

使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。

这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。

相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()ScheduledExecutorService可以帮助你很好地实现这样的定时调度。

刷新

刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。

如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。

重载CacheLoader.reload(K, V)可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。

01 //有些键不需要刷新,并且我们希望刷新是异步完成的
02 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
03         .maximumSize(1000)
04         .refreshAfterWrite(1, TimeUnit.MINUTES)
05         .build(
06             new CacheLoader<Key, Graph>() {
07                 public Graph load(Key key) { // no checked exception
08                     return getGraphFromDatabase(key);
09                 }
10  
11                 public ListenableFuture<Key, Graph> reload(final Key key, Graph prevGraph) {
12                     if (neverNeedsRefresh(key)) {
13                         return Futures.immediateFuture(prevGraph);
14                     }else{
15                         // asynchronous!
16                         ListenableFutureTask<Key, Graph> task=ListenableFutureTask.create(new Callable<Key, Graph>() {
17                             public Graph call() {
18                                 return getGraphFromDatabase(key);
19                             }
20                         });
21                         executor.execute(task);
22                         return task;
23                     }
24                 }
25             });

CacheBuilder.refreshAfterWrite(long, TimeUnit)可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)。因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。

其他特性

统计

CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

asMap视图

asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:

  • cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;
  • asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。
  • 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。

中断

缓存加载方法(如Cache.get)不会抛出InterruptedException。我们也可以让这些方法支持InterruptedException,但这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者实际获益。详情请继续阅读。

Cache.get请求到未缓存的值时会遇到两种情况:当前线程加载值;或等待另一个正在加载值的线程。这两种情况下的中断是不一样的。等待另一个正在加载值的线程属于较简单的情况:使用可中断的等待就实现了中断支持;但当前线程加载值的情况就比较复杂了:因为加载值的CacheLoader是由用户提供的,如果它是可中断的,那我们也可以实现支持中断,否则我们也无能为力。

如果用户提供的CacheLoader是可中断的,为什么不让Cache.get也支持中断?从某种意义上说,其实是支持的:如果CacheLoader抛出InterruptedException,Cache.get将立刻返回(就和其他异常情况一样);此外,在加载缓存值的线程中,Cache.get捕捉到InterruptedException后将恢复中断,而其他线程中InterruptedException则被包装成了ExecutionException。

原则上,我们可以拆除包装,把ExecutionException变为InterruptedException,但这会让所有的LoadingCache使用者都要处理中断异常,即使他们提供的CacheLoader不是可中断的。如果你考虑到所有非加载线程的等待仍可以被中断,这种做法也许是值得的。但许多缓存只在单线程中使用,它们的用户仍然必须捕捉不可能抛出的InterruptedException异常。即使是那些跨线程共享缓存的用户,也只是有时候能中断他们的get调用,取决于那个线程先发出请求。

对于这个决定,我们的指导原则是让缓存始终表现得好像是在当前线程加载值。这个原则让使用缓存或每次都计算值可以简单地相互切换。如果老代码(加载值的代码)是不可中断的,那么新代码(使用缓存加载值的代码)多半也应该是不可中断的。

如上所述,Guava Cache在某种意义上支持中断。另一个意义上说,Guava Cache不支持中断,这使得LoadingCache成了一个有漏洞的抽象:当加载过程被中断了,就当作其他异常一样处理,这在大多数情况下是可以的;但如果多个线程在等待加载同一个缓存项,即使加载线程被中断了,它也不应该让其他线程都失败(捕获到包装在ExecutionException里的InterruptedException),正确的行为是让剩余的某个线程重试加载。为此,我们记录了一个bug。然而,与其冒着风险修复这个bug,我们可能会花更多的精力去实现另一个建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象。

Guava Cache的get方法先在本地缓存中取,如果不存在,则会触发load方法。但load方法不能返回null。

Guava Cache的get方法先在本地缓存中取,如果不存在,则会触发load方法。但load方法不能返回null。

Guava Cache的get方法先在本地缓存中取,如果不存在,则会触发load方法。但load方法不能返回null。

保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义

保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义

保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义

保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义

保留"获取缓存-如果没有-则计算"[get-if-absent-compute]的原子语义

设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。

设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。

设想这样一个场景:进行某些热点数据查询时,如果缓存中没有,则去数据库中查询,并把查询到的结果保存到缓存中。 
但假如说数据库中也没有呢? 
这个时候load方法就会抛异常,例如:

  1. public enum TestGuavaCache {
  2. INSTANCE;
  3. private LoadingCache<String, Person> infos;
  4. TestGuavaCache() {
  5. infos = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build(new CacheLoader<String, Person>() {
  6. public Person load(String key) throws Exception {
  7. //load from database
  8. Person p = loadFromDatabase();
  9. return p;
  10. }
  11. });
  12. }
  13. //假设数据库中不存在
  14. private Person loadFromDatabase() {
  15. return null;
  16. }
  17. public Person get(String key) {
  18. try {
  19. return infos.get(key);
  20. } catch (Exception e) {
  21. //log exception
  22. }
  23. return null;
  24. }
  25. }

这是因为Guava Cache认为cache null是无意义的,因此Guava Cache的javadoc里加粗说明:must not be null。

现实世界没那么理想,肯定会有null的情况,那怎么处理呢?我的处理一般是对Guava Cache的get方法做try-catch。

有时候cache null也是有意义的,例如对于一个key,假如数据库中也没有对应的value,那就把这个情况记录下来, 
避免频繁的查询数据库(例如一些攻击性行为),直接在缓存中就把这个key挡住了。 
怎么做呢?举例:

  1. @Test
  2. public void whenNullValue_thenOptional() {
  3. CacheLoader<String, Optional<String>> loader;
  4. loader = new CacheLoader<String, Optional<String>>() {
  5. @Override
  6. public Optional<String> load(String key) {
  7. return Optional.fromNullable(getSuffix(key));
  8. }
  9. };
  10. LoadingCache<String, Optional<String>> cache;
  11. cache = CacheBuilder.newBuilder().build(loader);
  12. assertEquals("txt", cache.getUnchecked("text.txt").get());
  13. assertFalse(cache.getUnchecked("hello").isPresent());
  14. }
  15. private String getSuffix(final String str) {
  16. int lastIndex = str.lastIndexOf('.');
  17. if (lastIndex == -1) {
  18. return null;
  19. }
  20. return str.substring(lastIndex + 1);
  21. }

3.什么时候用get,什么时候用getUnchecked

官网文档说:

  1. . If you have defined a CacheLoader that does not declare any checked exceptions then you can perform cache lookups using getUnchecked(K);
  2. however care must be taken not to call getUnchecked on caches whose CacheLoaders declare checked exceptions.

字面意思是,如果你的CacheLoader没有定义任何checked Exception,那你可以使用getUnchecked。 
这一段话我也不是很理解。。官网上给了一个例子是,load方法没有声明throws Exception,那就可以使用getUnchecked:

  1. LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
  2. .expireAfterAccess(10, TimeUnit.MINUTES)
  3. .build(
  4. new CacheLoader<Key, Graph>() {
  5. public Graph load(Key key) { // no checked exception
  6. return createExpensiveGraph(key);
  7. }
  8. });
  9. ...
  10. return graphs.getUnchecked(key);

4.如何定义一个普通的Guava Cache,不需要用到load方法

假如只是简单的把Guava Cache当作HashMap或ConcurrentHashMap的替代品,不需要用到load方法,而是手动地插入,可以这样:

  1. com.google.common.cache.Cache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();

注意不能用LoadingCache了。 
查找: 
cache.getIfPresent("xx"); 
插入: 
cache.put("xx", "xxx");

5.Guava Cache的超时机制不是精确的。

我曾经依赖Guava Cache的超时机制和RemovalListener,以实现类似定时任务的功能;后来发现Guava Cache的超时机制是不精确的,例如你设置cache的缓存时间是30秒, 
那它存活31秒、32秒,都是有可能的。 
官网说:

  1. Timed expiration is performed with periodic maintenance during writes and occasionally during reads, as discussed below.
  2. Caches built with CacheBuilder do not perform cleanup and evict values "automatically," or instantly after a value expires, or anything of the sort.
  3. Instead, it performs small amounts of maintenance during write operations, or during occasional read operations if writes are rare.

另一篇

google guava中有cache包,此包提供内存缓存功能。内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 当然这些东西guava都考虑到了。

guava中使用缓存需要先声明一个CacheBuilder对象,并设置缓存的相关参数,然后调用其build方法获得一个Cache接口的实例。请看下面的代码和注释,注意在注释中指定了Cache的各个参数。

    public static void main(String[] args) throws ExecutionException, InterruptedException{
        //缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
        LoadingCache<Integer,Student> studentCache
                //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                //设置并发级别为8,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(8)
                //设置写缓存后8秒钟过期
                .expireAfterWrite(8, TimeUnit.SECONDS)
                //设置缓存容器的初始容量为10
                .initialCapacity(10)
                //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
                .maximumSize(100)
                //设置要统计缓存的命中率
                .recordStats()
                //设置缓存的移除通知
                .removalListener(new RemovalListener<Object, Object>() {
                    @Override
                    public void onRemoval(RemovalNotification<Object, Object> notification) {
                        System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
                    }
                })
                //build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
                .build(
                        new CacheLoader<Integer, Student>() {
                            @Override
                            public Student load(Integer key) throws Exception {
                                System.out.println("load student " + key);
                                Student student = new Student();
                                student.setId(key);
                                student.setName("name " + key);
                                return student;
                            }
                        }
                );         for (int i=0;i<20;i++) {
            //从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据
            Student student = studentCache.get(1);
            System.out.println(student);
            //休眠1秒
            TimeUnit.SECONDS.sleep(1);
        }         System.out.println("cache stats:");
        //最后打印缓存的命中率等 情况
        System.out.println(studentCache.stats().toString());
    }

以上程序的输出如下:

load student 1
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
1 was removed, cause is EXPIRED
load student 1 ...... Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
cache stats:
CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1348802, evictionCount=2}

看看到在20此循环中命中次数是17次,未命中3次,这是因为我们设定缓存的过期时间是写入后的8秒,所以20秒内会失效两次,另外第一次获取时缓存中也是没有值的,所以才会未命中3次,其他则命中。

guava的内存缓存非常强大,可以设置各种选项,而且很轻量,使用方便。另外还提供了下面一些方法,来方便各种需要:

  1. ImmutableMap<K, V> getAllPresent(Iterable<?> keys) 一次获得多个键的缓存值
  2. putputAll方法向缓存中添加一个或者多个缓存项
  3. invalidate 和 invalidateAll方法从缓存中移除缓存项
  4. asMap()方法获得缓存数据的ConcurrentMap<K, V>快照
  5. cleanUp()清空缓存
  6. refresh(Key) 刷新缓存,即重新取缓存数据,更新缓存

另一篇

google guava中有cache包,此包提供内存缓存功能。内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 当然这些东西guava都考虑到了。

guava中使用缓存需要先声明一个CacheBuilder对象,并设置缓存的相关参数,然后调用其build方法获得一个Cache接口的实例。请看下面的代码和注释,注意在注释中指定了Cache的各个参数。

    public static void main(String[] args) throws ExecutionException, InterruptedException{
        //缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
        LoadingCache<Integer,Student> studentCache
                //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                //设置并发级别为8,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(8)
                //设置写缓存后8秒钟过期
                .expireAfterWrite(8, TimeUnit.SECONDS)
                //设置缓存容器的初始容量为10
                .initialCapacity(10)
                //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
                .maximumSize(100)
                //设置要统计缓存的命中率
                .recordStats()
                //设置缓存的移除通知
                .removalListener(new RemovalListener<Object, Object>() {
                    @Override
                    public void onRemoval(RemovalNotification<Object, Object> notification) {
                        System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
                    }
                })
                //build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
                .build(
                        new CacheLoader<Integer, Student>() {
                            @Override
                            public Student load(Integer key) throws Exception {
                                System.out.println("load student " + key);
                                Student student = new Student();
                                student.setId(key);
                                student.setName("name " + key);
                                return student;
                            }
                        }
                );         for (int i=0;i<20;i++) {
            //从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据
            Student student = studentCache.get(1);
            System.out.println(student);
            //休眠1秒
            TimeUnit.SECONDS.sleep(1);
        }         System.out.println("cache stats:");
        //最后打印缓存的命中率等 情况
        System.out.println(studentCache.stats().toString());
    }

以上程序的输出如下:

load student 1
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
1 was removed, cause is EXPIRED
load student 1 ...... Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
cache stats:
CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1348802, evictionCount=2}

看看到在20此循环中命中次数是17次,未命中3次,这是因为我们设定缓存的过期时间是写入后的8秒,所以20秒内会失效两次,另外第一次获取时缓存中也是没有值的,所以才会未命中3次,其他则命中。

guava的内存缓存非常强大,可以设置各种选项,而且很轻量,使用方便。另外还提供了下面一些方法,来方便各种需要:

  1. ImmutableMap<K, V> getAllPresent(Iterable<?> keys) 一次获得多个键的缓存值
  2. putputAll方法向缓存中添加一个或者多个缓存项
  3. invalidate 和 invalidateAll方法从缓存中移除缓存项
  4. asMap()方法获得缓存数据的ConcurrentMap<K, V>快照
  5. cleanUp()清空缓存
  6. refresh(Key) 刷新缓存,即重新取缓存数据,更新缓存

一.背景

缓存是我们在开发中为了提高系统的性能,把经常的访问业务的数据第一次把处理结果先放到缓存中,第二次就不用在对相同的业务数据在重新处理一遍,这样就提高了系统的性能。缓存分好几种:

(1)本地缓存。

(2)数据库缓存。

(3)分布式缓存。

分布式缓存比较常用的有memcached等,memcached是高性能的分布式内存缓存服务器,缓存业务处理结果,减少数据库访问次数和相同复杂逻辑处理的时间,以提高动态Web应用的速度、 提高可扩展性。

二.本地缓存在高并发下的问题以及解决

今天我们介绍的是本地缓存缓存,我们这边采用Java.util.concurrent.ConcurrentHashMap来保存,ConcurrentHashMap是一个线程安全的HashTable,并提供了一组和HashTable功能相同但是线程安全的方法,ConcurrentHashMap可以做到读取数据不加锁,提高了并发能力。我们先不考虑内存元素回收或者在保存数据会出现内存溢出的情况,我们用ConcurrentHashMap模拟本地缓存,当在高并发环境一下,会出现一些什么问题?

我们这边采用实现多个线程来模拟高并发场景。

第一种:我们先来看一下代码:

  1. public class TestConcurrentHashMapCache<K,V> {
  2. private final ConcurrentHashMap<K, V>  cacheMap=new ConcurrentHashMap<K,V> ();
  3. public  Object getCache(K keyValue,String ThreadName){
  4. System.out.println("ThreadName getCache=============="+ThreadName);
  5. Object value=null;
  6. //从缓存获取数据
  7. value=cacheMap.get(keyValue);
  8. //如果没有的话,把数据放到缓存
  9. if(value==null){
  10. return putCache(keyValue,ThreadName);
  11. }
  12. return value;
  13. }
  14. public Object putCache(K keyValue,String ThreadName){
  15. System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
  16. //可以根据业务从数据库获取等取得数据,这边就模拟已经获取数据了
  17. @SuppressWarnings("unchecked")
  18. V value=(V) "dataValue";
  19. //把数据放到缓存
  20. cacheMap.put(keyValue, value);
  21. return value;
  22. }
  23. public static void main(String[] args) {
  24. final TestConcurrentHashMapCache<String,String> TestGuaVA=new TestConcurrentHashMapCache<String,String>();
  25. Thread t1=new Thread(new Runnable() {
  26. @Override
  27. public void run() {
  28. System.out.println("T1======start========");
  29. Object value=TestGuaVA.getCache("key","T1");
  30. System.out.println("T1 value=============="+value);
  31. System.out.println("T1======end========");
  32. }
  33. });
  34. Thread t2=new Thread(new Runnable() {
  35. @Override
  36. public void run() {
  37. System.out.println("T2======start========");
  38. Object value=TestGuaVA.getCache("key","T2");
  39. System.out.println("T2 value=============="+value);
  40. System.out.println("T2======end========");
  41. }
  42. });
  43. Thread t3=new Thread(new Runnable() {
  44. @Override
  45. public void run() {
  46. System.out.println("T3======start========");
  47. Object value=TestGuaVA.getCache("key","T3");
  48. System.out.println("T3 value=============="+value);
  49. System.out.println("T3======end========");
  50. }
  51. });
  52. t1.start();
  53. t2.start();
  54. t3.start();
  55. }
  56. }

我们看一下执行结果,如图所示:

guava cache学习

我们实现了本地缓存代码,我们执行一下结果,发现在多线程时,出现了在缓存里没有缓存时,会执行一样执行多次的业务数据并返回处理的数据,我们分析一下出现这种情况的:

(1)当线程T1访问cacheMap里面有没有,这时根据业务到后台处理业务数据并返回处理数据,并放入缓存。

(2)当线程T2访问cacheMap里面同样也没有,也把根据业务到后台处理业务数据并返回处理数据,并放入缓存。

   第二种:

这样相同的业务并处理两遍,如果在高并发的情况下相同的业务不止执行两遍,这样这样跟我们当初做缓存不相符合,这时我们想到了Java多线程时,在执行获取缓存上加上Synchronized,代码如下:

  1. public class TestConcurrentHashMapCache<K,V> {
  2. private final ConcurrentHashMap<K, V>  cacheMap=new ConcurrentHashMap<K,V> ();
  3. public <span style="color:#ff0000;">synchronized </span>Object getCache(K keyValue,String ThreadName){
  4. System.out.println("ThreadName getCache=============="+ThreadName);
  5. Object value=null;
  6. //从缓存获取数据
  7. value=cacheMap.get(keyValue);
  8. //如果没有的话,把数据放到缓存
  9. if(value==null){
  10. return putCache(keyValue,ThreadName);
  11. }
  12. return value;
  13. }
  14. public Object putCache(K keyValue,String ThreadName){
  15. System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
  16. //可以根据业务从数据库获取等取得数据,这边就模拟已经获取数据了
  17. @SuppressWarnings("unchecked")
  18. V value=(V) "dataValue";
  19. //把数据放到缓存
  20. cacheMap.put(keyValue, value);
  21. return value;
  22. }
  23. public static void main(String[] args) {
  24. final TestConcurrentHashMapCache<String,String> TestGuaVA=new TestConcurrentHashMapCache<String,String>();
  25. Thread t1=new Thread(new Runnable() {
  26. @Override
  27. public void run() {
  28. System.out.println("T1======start========");
  29. Object value=TestGuaVA.getCache("key","T1");
  30. System.out.println("T1 value=============="+value);
  31. System.out.println("T1======end========");
  32. }
  33. });
  34. Thread t2=new Thread(new Runnable() {
  35. @Override
  36. public void run() {
  37. System.out.println("T2======start========");
  38. Object value=TestGuaVA.getCache("key","T2");
  39. System.out.println("T2 value=============="+value);
  40. System.out.println("T2======end========");
  41. }
  42. });
  43. Thread t3=new Thread(new Runnable() {
  44. @Override
  45. public void run() {
  46. System.out.println("T3======start========");
  47. Object value=TestGuaVA.getCache("key","T3");
  48. System.out.println("T3 value=============="+value);
  49. System.out.println("T3======end========");
  50. }
  51. });
  52. t1.start();
  53. t2.start();
  54. t3.start();
  55. }
  56. }

执行结果,如图所示:

guava cache学习

这样就实现了串行,在高并发行时,就不会出现了第二个访问相同业务,肯定是从缓存获取,但是加上Synchronized变成串行,这样在高并发行时性能也下降了。

第三种:

我们为了实现性能和缓存的结果,我们采用Future,因为Future在计算完成时获取,否则会一直阻塞直到任务转入完成状态和ConcurrentHashMap.putIfAbsent方法,代码如下:

  1. public class TestFutureCahe<K,V> {
  2. private final ConcurrentHashMap<K, Future<V>>  cacheMap=new ConcurrentHashMap<K, Future<V>> ();
  3. public   Object getCache(K keyValue,String ThreadName){
  4. Future<V> value=null;
  5. try{
  6. System.out.println("ThreadName getCache=============="+ThreadName);
  7. //从缓存获取数据
  8. value=cacheMap.get(keyValue);
  9. //如果没有的话,把数据放到缓存
  10. if(value==null){
  11. value= putCache(keyValue,ThreadName);
  12. return value.get();
  13. }
  14. return value.get();
  15. }catch (Exception e) {
  16. }
  17. return null;
  18. }
  19. public Future<V> putCache(K keyValue,final String ThreadName){
  20. //      //把数据放到缓存
  21. Future<V> value=null;
  22. Callable<V> callable=new Callable<V>() {
  23. @SuppressWarnings("unchecked")
  24. @Override
  25. public V call() throws Exception {
  26. //可以根据业务从数据库获取等取得数据,这边就模拟已经获取数据了
  27. System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
  28. return (V) "dataValue";
  29. }
  30. };
  31. FutureTask<V> futureTask=new FutureTask<V>(callable);
  32. value=cacheMap.putIfAbsent(keyValue, futureTask);
  33. if(value==null){
  34. value=futureTask;
  35. futureTask.run();
  36. }
  37. return value;
  38. }
  39. public static void main(String[] args) {
  40. final TestFutureCahe<String,String> TestGuaVA=new TestFutureCahe<String,String>();
  41. Thread t1=new Thread(new Runnable() {
  42. @Override
  43. public void run() {
  44. System.out.println("T1======start========");
  45. Object value=TestGuaVA.getCache("key","T1");
  46. System.out.println("T1 value=============="+value);
  47. System.out.println("T1======end========");
  48. }
  49. });
  50. Thread t2=new Thread(new Runnable() {
  51. @Override
  52. public void run() {
  53. System.out.println("T2======start========");
  54. Object value=TestGuaVA.getCache("key","T2");
  55. System.out.println("T2 value=============="+value);
  56. System.out.println("T2======end========");
  57. }
  58. });
  59. Thread t3=new Thread(new Runnable() {
  60. @Override
  61. public void run() {
  62. System.out.println("T3======start========");
  63. Object value=TestGuaVA.getCache("key","T3");
  64. System.out.println("T3 value=============="+value);
  65. System.out.println("T3======end========");
  66. }
  67. });
  68. t1.start();
  69. t2.start();
  70. t3.start();
  71. }
  72. }

线程T1或者线程T2访问cacheMap,如果都没有时,这时执行了FutureTask来完成异步任务,假如线程T1执行了FutureTask,并把保存到ConcurrentHashMap中,通过PutIfAbsent方法,因为putIfAbsent方法如果不存在key对应的值,则将value以key加入Map,否则返回key对应的旧值。这时线程T2进来时可以获取Future对象,如果没值没关系,这时是对象的引用,等FutureTask执行完,在通过get返回。

我们问题解决了高并发访问缓存的问题,可以回收元素这些,都没有,容易造成内存溢出,Google  Guava Cache在这些问题方面都做得挺好的,接下来我们介绍一下。

三.Google  Guava Cache的介绍和应用

http://www.java2s.com/Code/Jar/g/Downloadguava1401jar.htm  下载对应的jar包

Guava Cache与ConcurrentMap很相似,Guava Cache能设置回收,能解决在大数据内存溢出的问题,源代码如下:

public class TestGuaVA<K,V> {
private   Cache<K, V> cache=  CacheBuilder.newBuilder() .maximumSize(2).expireAfterWrite(10, TimeUnit.MINUTES).build(); 
public   Object getCache(K keyValue,final String ThreadName){
Object value=null;
try {
System.out.println("ThreadName getCache=============="+ThreadName);
//从缓存获取数据
value = cache.get(keyValue, new Callable<V>() {  
           @SuppressWarnings("unchecked")
public V call() {  
             System.out.println("ThreadName 执行业务数据并返回处理结果的数据(访问数据库等)=============="+ThreadName);
return (V) "dataValue";
           }  
       });  
} catch (ExecutionException e) {
e.printStackTrace();
}
return value;
}

public static void main(String[] args) {
final TestGuaVA<String,String> TestGuaVA=new TestGuaVA<String,String>();

Thread t1=new Thread(new Runnable() {
@Override
public void run() {

System.out.println("T1======start========");
Object value=TestGuaVA.getCache("key","T1");
System.out.println("T1 value=============="+value);
System.out.println("T1======end========");

}
});

Thread t2=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("T2======start========");
Object value=TestGuaVA.getCache("key","T2");
System.out.println("T2 value=============="+value);
System.out.println("T2======end========");

}
});

Thread t3=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("T3======start========");
Object value=TestGuaVA.getCache("key","T3");
System.out.println("T3 value=============="+value);
System.out.println("T3======end========");

}
});

t1.start();
t2.start();
t3.start();

}

}

说明:

CacheBuilder.newBuilder()后面能带一些设置回收的方法:

(1)maximumSize(long):设置容量大小,超过就开始回收。

(2)expireAfterAccess(long, TimeUnit):在这个时间段内没有被读/写访问,就会被回收。

(3)expireAfterWrite(long, TimeUnit):在这个时间段内没有被写访问,就会被回收 。

(4)removalListener(RemovalListener):监听事件,在元素被删除时,进行监听。

执行结果,如图所示:

guava cache学习

上一篇:JS获取时间并格式化


下一篇:卸载了PL/SQL Developer,说一下与Toad for Oracle的对照