简书 许乐
转载请注明原创出处,谢谢!
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时会发生阻塞,如下图所示:
@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对应的锁,唤醒阻塞在该锁上的线程,其它线程可以从缓存中获取数据,而不是再次访问数据库。
@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;
五、 实现非阻塞的缓存
待续...
网友评论