Mybatis系列5-缓存源码分析

1.装饰器模式

mybatis缓存模块用了装饰器模式,装饰器模式就是用来在类的原有功能基础上添加新功能。
装饰器模式中又四个角色:

  • 组件接口:Component,定义了组件的行为
  • 组件实现类:ConcreteComponent,也就是被装饰的对象
  • 装饰器抽象类:Decorator,也实现了组件接口,并且持有一个被装饰的对象
  • 装饰器实现类:ConcreteDecorator,实现了装饰器
    在这里插入图片描述
    是不是感觉和适配器优点类似,都持有了原对象引用,不过两个设计模式用途不一样,装饰器是给原有类增加新功能,有点像代理的增强。适配器是把一个对象封装适配成另外一个对象。

2.mybatis的缓存

我们知道缓存简单来说就是个map对象,里面放着key,value的数据。当然redis等专用缓存复杂的多,要考虑持久化。mybatis的缓存比较简单,就是个map对象。我们平时使用缓存的时候会先查询缓存中是否有,如果有,则返回。没有则查询数据库。但是这样会有缓存雪崩、缓存击穿、缓存穿透的问题。这些我在redis系列中讲过。
我们这里以mybatis的BlockingCache为例。这个类在原有缓存基础上增加了防止缓存击穿的功能。
(1)BlockingCache
BlockingCache实现类Cache接口,内部持有一个Cache引用。

public class BlockingCache implements Cache {

  //阻塞的超时时长
  private long timeout;
  //被装饰的底层对象,一般是PerpetualCache
  private final Cache delegate;
  //锁对象集,粒度到key值
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    acquireLock(key);//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
    Object value = delegate.getObject(key);
    if (value != null) {//获取数据成功的,要释放锁
      releaseLock(key);
    }        
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // despite of its name, this method is called only to release locks
    releaseLock(key);
    return null;
  }

  @Override
  public void clear() {
    delegate.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }
  
  private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();//创建锁
    ReentrantLock previous = locks.putIfAbsent(key, lock);//把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁
    return previous == null ? lock : previous;
  }
  
//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
  private void acquireLock(Object key) {
	//获得锁对象
    Lock lock = getLockForKey(key);
    if (timeout > 0) {//使用带超时时间的锁
      try {
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {//如果超时抛出异常
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());  
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {//使用不带超时时间的锁
      lock.lock();
    }
  }
  
  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

  public long getTimeout() {
    return timeout;
  }

  public void setTimeout(long timeout) {
    this.timeout = timeout;
  }  
}

(2)get方法
get方法会首先通过acquireLock获得锁
(3)acquireLock

 private void acquireLock(Object key) {
	//获得锁对象
    Lock lock = getLockForKey(key);
    if (timeout > 0) {//使用带超时时间的锁
      try {
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {//如果超时抛出异常
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());  
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {//使用不带超时时间的锁
      lock.lock();
    }
  }

(4)getLockForKey方法

 private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();//创建锁
    ReentrantLock previous = locks.putIfAbsent(key, lock);//把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁
    return previous == null ? lock : previous;
  }

这里的locks是个map对象,key就是传入的参数key,value就是锁。也就是说一个key,一把锁。
这里只有获取锁的线程才可以拿数据。如果缓存中没有就可以取数据库,放回缓存,然后释放锁。这样后续的线程就可以直接从缓存中获取值,防止缓存击穿。

3.缓存key

Mybatis中涉及到动态SQL的原因,缓存项的key不能仅仅通过一个String来表示,所以通过CacheKey来封装缓存的Key值,CacheKey可以封装多个影响缓存项的因素。
构成CacheKey的对象:

  • mappedStatment的id
  • 分页信息
  • 查询所使用的SQL语句
  • 用户传递给SQL语句的实际参数值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章