正确读取与设置缓存方法

 

     如果你感觉到缓存的重要性,那么,恭喜你,你的技术水平已经从初级上升了一个层次,意识到性能的重要性。你不再仅限于完成用户提出的功能,而是更注重提高系统的性能和软件的质量了。但是,仅仅在软件中随便加一个memcache或者osCache包就以为能够解决性能问题的话,那你就大错特错了。缓存只是提高性能一小步,提高性能更多是从设计层次来提高,但必有的编程技巧也是解决性能问题的一个主要因素。

本文章主要从如何查询及构建缓存开始,主要参考了Java Concurrency In Practice的一些章节,及网上的一些资料,结合实际的项目,做了一些应用。

首先,你一般设置缓存是否是这样写的呢?

Java代码  
  1. //计算缓存的key  
  2. String cacheKey = getCacheKey(param1,param2);  
  3. //查询memcached  
  4. List list = (List)memcached.get(cacheKey);  
  5. if(list == null){  
  6.     //memcache 已经失效或者不存在,去查询数据库  
  7.     list = getFromDB(param1,param2);  
  8.     memcached.set(list,5*60);  
  9.     return list;  
  10. }  

 这个方法在并发小的时候,应该不存在问题,但是当是一个高并发的系统时,那么这样的写法可能会导致缓存失效时,向数据库发起多个查询,然后查询完之后,还要向memcache Set多次。为什么,因为在如果同时过来10个请求,都发现缓存中没有数据(list == null),那么就都会去查询数据库,然后直到其中一个最先获得结果的线程,将结果设置到memcache,之后到来的线程,才会走缓存,但已经进来的线程,则还会继续查数据库,然后再将结果设置到memcache,这显然是我们不想看到的。那么如何处理呢,在方法上面加synchronized锁?开销太大。

这时,我们可以看看专家的意见,在设计高效的线程安全的缓存--JCIP5.6读书笔记 中讲了一种方法,可以既可以不使用锁,又保证多个线程同时请求时只有一个线程会访问数据库执行查询,其他线程都只读取计算结果的方法。如果你对里面讲的内容一头雾水的话,那么,你可以看看我写的这个示例,你只需要构建一个自己的Callable类,就能正确的设置与读取缓存。

 

假设concurrentService是一个先读缓存,没有缓存则读取数据库的方法,其代码如下:

Java代码  
  1. public List concurrentService(int para1,int param2){  
  2.     long beginTime = System.nanoTime();  
  3.     final String cacheKey = "IamKey";  
  4.     List list = (List)memCachedClient.get(cacheKey);  
  5.     if(list == null){  
  6.         Callable caller = new Callable() {  
  7.             public Object call() throws InterruptedException {  
  8.                 System.out.println(" go to dao or rmi");  
  9.                 List list = new ArrayList();  
  10.                 list.add(1l);list.add(2l);  
  11.                 //将计算结果缓存  
  12.                 System.out.println("结果计算完毕,存入分布式缓存中");  
  13.                 memCachedClient.set(cacheKey, 5*60, list);  
  14. /                   Thread.sleep(500);  
  15.                 //计算结果,通常是访问数据库或者远程服务  
  16.                 return list;  
  17.             }  
  18.         };  
  19.         List result = (List)TaskUtils.getInTask(cacheKey,caller);  
  20.         long end = System.nanoTime();  
  21.         useTimes.add(end-beginTime);  
  22.         return result;  
  23.     }  
  24.     else {  
  25.         System.out.println("1.缓存命中,直接返回");  
  26.         long end = System.nanoTime();  
  27.         useTimes.add(end-beginTime);  
  28.         return list;  
  29.     }  
  30. }  
  31.  

     其中的TaskUtils.getInTask定义如下:

    Java代码  
    1. import java.util.concurrent.Callable;  
    2. import java.util.concurrent.ConcurrentHashMap;  
    3. import java.util.concurrent.ConcurrentMap;  
    4. import java.util.concurrent.FutureTask;  
    5.   
    6. public class TaskUtils {  
    7.     private static final ConcurrentMap> cache = new ConcurrentHashMap>();  
    8.     public static Object getInTask(String cacheKey, Callable caller) {  
    9.         System.out.println("1.缓存未命中,将查询数据库或者调用远程服务");  
    10.         //未命中缓存,开始计算  
    11.         FutureTask f = cache.get(cacheKey);  
    12.         if (f == null) {  
    13.             FutureTask ft = new FutureTask(caller);  
    14.             f = cache.putIfAbsent(cacheKey, ft);  
    15.             if (f == null) {  
    16.                 System.out.println("2.任务未命中,将查询数据库或者调用远程服务");  
    17.                 f = ft;  
    18.                 ft.run();  
    19.             }  
    20.         }  
    21.         else {  
    22.             System.out.println("2.任务命中,直接从缓存取结果");  
    23.         }  
    24.         try {  
    25.             Object result = f.get();  
    26.             System.out.println("取回的结果result:"+result);  
    27.             return result;  
    28.         } catch (Exception e) {  
    29.             e.printStackTrace();  
    30.         }  
    31.         finally{  
    32.             //最后将计算任务去掉,虽然已经移除任务对象,但其他线程  
    33.             //仍然能够获取到计算的结果,直到所有引用都失效,被垃圾回收掉  
    34.             boolean success = cache.remove(cacheKey,f);  
    35.             System.out.println(success);  
    36.         }  
    37.         return null;  
    38.     }  
    39. }  
    40.        经过测试,使用这种方法读取与设置缓存,比使用synchronized方法和锁定键值的方法要快3-10倍,不信大家可以试试。

              有人可能有疑问,如果并发的线程很多,同时都没有命中缓存,那么不就会产生很多Callable对象吗?这样岂不会浪费很大内存吗?其实,我们仔细分析一下代码,可以看到Callable对象不管创建了多少,但最终经过putIfAbsent方法之后,就留下了一个有效的对象,其他的对象都成为失效对象,随时可以被GC掉。因此,使用这种方法,并不会造成JVM的内存溢出。

           另外,Callable就是一个普通的对象,跟线程一点关系都没有,里面虽然包括了一个runnable方法,但是并不是说这个会启动一个线程。里面的runnable方法在本代码中是在调用者线程中执行,但执行结果共享给了其他没有命中缓存的线程。

              赶紧回去review你们项目的代码吧,你们设置缓存的方式对吗?

              实际上这个TaskUtil的方法只是使用了两个重要的并发工具类,一个是ConcurrentMap,主要支持并发中经常使用的putIfAbsent方法,和一个FutureTask对象,这个对象的get方法能够阻塞调用者线程,直到结果可用。

       

       

      对这类话题感兴趣?欢迎发送邮件至[email protected]

      关于我:邯郸人


      你可能感兴趣的:(并发编程)