深入剖析Mybatis实现的细节设计模式

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

2019-01-23  本文已影响0人  Xcdf

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

  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;

五、 实现非阻塞的缓存

待续...

上一篇下一篇

猜你喜欢

热点阅读