第二章 缓存模块与装饰器模式

作者: Xcdf | 来源:发表于2019-01-23 00:07 被阅读0次

    简书 许乐
    转载请注明原创出处,谢谢!

      MyBatis作为一个强大的持久层框架,缓存是其必不可少的功能之一,MyBatis中的缓存是两层结构的,分为一级缓存,二级缓存,但本质上是相同的,它们使用的都是Cache接口的实现。

    一、 Cache 接口及其实现

    public interface Cache {
       
       String getId();//获取缓存块的id
       
       void putObject(Object key, Object value);//向缓存中添加数据
       
       Object getObject(Object key);//从缓存中获取数据
       
       Object removeObject(Object key);//从缓存中移除数据
       
       void clear();//清空缓存
       
       int getSize();//获取缓存中存储对象个数,该方法不会被Mybatis 核心代码使用,所以可以提供空实现
       
       ReadWriteLock getReadWriteLock();//已经废弃
    }
    

      Cache接口有多个实现,这些实现类中大部分都是装饰器,只有PrepetualCache提供了Cache接口的基本实现。PrepetualCache在缓存中扮演着具体组件实现类的角色,其底层实现比较简单,使用HashMap 存储缓存项。代码如下:

    public class PerpetualCache implements Cache {
      //缓存块的唯一标识
      private final String id; //为什么不加static?id是每个缓存对象的私有属性,是对象级别的属性 
    
      private final Map cache = new HashMap();//缓存的集合
    
      //构造器
      public PerpetualCache(String id) {
        this.id = id;
      }
      //get,remove,clear等其他方法已经省略
      @Override
      public void putObject(Object key, Object value) {
        cache.put(key, value);
      }
      //equals 和hashcode 只关心id字段
      @Override
      public boolean equals(Object o) {
        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());//比较cache的id是否相等
      }
    }
    

    二、 装饰器(BlockingCache)

    下面介绍一下cache.decorators包下的装饰器,他们都直接实现了Cache接口,扮演者具体装饰实现类的角色。这些装饰器会在PerpetualCache的基础上提供一下额外的功能,通过组合满足一定的需求。
    BlockingCache中各个字段的含义如下:

    private long timeout;//阻塞超时时长
    private final Cache delegate;//被修饰的底层Cache对象,通过构造器传入
    private final ConcurrentHashMap locks;//每个key对应的ReentrantLock对象
    //构造器
    public BlockingCache(Cache delegate) {
      this.delegate = delegate;
      this.locks = new ConcurrentHashMap();
    }
    

    1 、 BlockingCache 的get
      BlockingCache 是阻塞版本的缓存装饰器,它可以实现【Mybatis并未实现,需要配合具体业务场景实现】只有一个线程到数据库中查找指定key对应的数据。
      现在模拟多个线程向缓存取数据的场景:如果线程A在BlockingCache中未查找到keyA对应的缓存项时,线程A会持续获取keyA对应的锁,这样后续其他线程在查找keyA时会发生阻塞,如下图所示:

    getObject
    @Override
    public Object getObject(Object key) {
      acquireLock(key);//创建锁对象,线程持有锁
      Object value = delegate.getObject(key);
      if (value != null) {  
        releaseLock(key);
      }else{
        //do nothing
        //value为null时继续持有锁 ,其他线程未获取锁
      }  
      return value;
    }
    

    acquireLock() 方法会尝试获取指定key对应的锁。如果该key没有对应的锁对象则为其创建新的ReentrantLock对象,再加锁;如果获取失败,则阻塞一段时间。

    //1.获取锁对象
    //2.当前线程尝试获取锁
    private void acquireLock(Object key) {
      Lock lock = getLockForKey(key);//获取ReentrantLock对象
      //设置超时时间
      if (timeout > 0) {
        try {
          boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
          if (!acquired) {
            throw new Exception("...");  
          }
        } catch (InterruptedException e) {
          throw new Exception("...");
        }
      } else {//获取锁,不带超时时长
        //如果未获取锁,则当前线程将被禁用以进行线程调度,并且在获取锁定之前处于休眠状态。
        lock.lock();
      }
    }
    //获取key对应的锁对象
    private ReentrantLock getLockForKey(Object key) {
      ReentrantLock lock = new ReentrantLock();
      ReentrantLock previous = locks.putIfAbsent(key, lock);
      return previous == null ? lock : previous;
    }
    

    2、 BlockingCache 的put
    假如线程A从数据库中查找到keyA对应的结果对象后,将结果放入到BlockingCache 中,此时线程A会释放keyA对应的锁,唤醒阻塞在该锁上的线程,其它线程可以从缓存中获取数据,而不是再次访问数据库。

    putObject
    @Override
    public void putObject(Object key, Object value) {
      try {
        delegate.putObject(key, value);
      } finally {
        releaseLock(key);
      }
    }
    private void releaseLock(Object key) {
      ReentrantLock lock = locks.get(key);
      if (lock.isHeldByCurrentThread()) {
        lock.unlock();
      }
    }
    

    三、 问题

    1.业务上怎么保证只有一个线程查询数据库?

    //业务代码如下
    Object obj=getObject(key);// 从缓存中获取指定key的值
    if(null==obj){//缓存中没有
      Object result=queryForObject(key); //查询数据库
      putObject(result);// 将数据写入缓存
    }else{//缓存中有,则返回
      return obj;
    }
    

    2.缓存中有数据为什么getObject()还要阻塞?应该如何解决?
      从源码来看,多个线程在调用getObject(Object key)时,只有一个线程拿到了key对应的锁,如果获取锁失败,则阻塞一段时间。先要获取key对应的锁,然后再次查询缓存(如果缓存没有,则程序数据库;有则释放锁并且返回)。
      这种阻塞式的缓存实际上是使用非常少的。

    四、 BlockingCache 多线程测试

      启动1000个线程同时来获取指定的key对应的value

    public class BlockingCacheTest {
    
        CyclicBarrier cyclicBarrier = new CyclicBarrier(1000);
        //建立一个BlockingCache
        BlockingCache cache=new BlockingCache(new PerpetualCache("test"));
       
        ExecutorService executorService ;
    
        private void runThread() {
            executorService = Executors.newFixedThreadPool(1000);
            for (int i = 0; i < 1000; i++) {
                executorService.submit(createThread(i));
            }
        }
    
        private Thread createThread(int i) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        cyclicBarrier.await();
                        // 获取key对应的value
                        String key="key";
                        Object object = cache.getObject(key);
                        if (object == null) {
                            Object obj = new String("value");//模拟数据库查询操作
                            cache.putObject(key, obj); //put操作结束后释放锁
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.setName("name" + i);
            return thread;
        }
    
        public static void main(String[] args) {
            BlockingCacheTest test = new BlockingCacheTest();
            test.runThread();
        }
    }
    

    测试结果:


    测试结果

    只有一个线程从数据库获取了数据,其他线程从缓存中获取数;另外需要注意的是:不管key对应的value是否存在,getObject都需要获取key锁,才能拿到value;

    五、 实现非阻塞的缓存

    待续...

    相关文章

      网友评论

        本文标题:第二章 缓存模块与装饰器模式

        本文链接:https://www.haomeiwen.com/subject/jnydjqtx.html