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語句的實際參數值