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语句的实际参数值