在Mybatis @CacheNamespace注解中有一个 blocking属性对于该属性的官方解释为
Returns whether block the cache at request time or not.
其默认值为false,在该属性为true的情况下Mybatis采用的缓存装饰器为BlockingCache
Simple blocking decorator
Simple and inefficient version of EhCache's BlockingCache decorator.
It sets a lock over a cache key when the element is not found in cache.
This way, other threads will wait until this element is filled instead of hitting the database.
大意是说在执行同一SQL查询时当前线程会先去获取锁,其他执行该查询的SQL线程只能等待当前线程查询完成后才能继续查询而不是直接命中数据库;
我们来看一下怎么使用
框架:
SpringBoot2.1.7+MyBatis 3.5.8-SNAPSHOT
源码:
Dao层:
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
@CacheNamespace(blocking = true)
public interface DemoDao {
@Select("select * from eg_admin limit 1")
Object query();
}
Service层:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class MyService {
@Resource
private DemoDao demoDao;
public void test(){
log.info("开始>>>>>>>>>>>>");
demoDao.query();
log.info("<<<<<<<<<<<<<<<结束");
}
}
Test:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.TimeUnit;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MyTest {
@Autowired
private MyService myService;
@Test
public void testB() throws InterruptedException {
new Thread(() -> myService.test()).start();
new Thread(() -> myService.test()).start();
TimeUnit.MINUTES.sleep(10);
}
}
为了便于查看
运行结果:
可以看到1,2两个线程同时发起查询,但是实际执行查询的只有1,而且当1线程执行完成后,2对应的线程才执行查询,并且命中了缓存。看起来没有什么问题,但是考虑一下接下来的情况;
修改MyService
import com.starsky.mapper.DemoDao;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class MyService {
@Resource
private DemoDao demoDao;
public void test(){
log.info("开始>>>>>>>>>>>>");
demoDao.query();
demoDao.query();
log.info("<<<<<<<<<<<<<<<结束");
}
}
查询结果:
依然没有什么问题,我们再来修改一下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
@Slf4j
public class MyService {
@Resource
private DemoDao demoDao;
@Transactional(rollbackFor = Exception.class)
public void test(){
log.info("开始>>>>>>>>>>>>");
demoDao.query();
demoDao.query();
log.info("<<<<<<<<<<<<<<<结束");
}
}
执行结果:
可以发现Thread-4 无法执行查询
打开jconsole查询发现线程进入了锁等待;
可以确认的是加上事务控制后这里出现了问题,跟踪源码发现,在执行查询时TransactionalCacheManager会尝试从BlockingCache中获取查询结果,获取时会先尝试获取锁
@Override
public Object getObject(Object key) {
acquireLock(key);
Object value = delegate.getObject(key);
if (value != null) {
releaseLock(key);
}
return value;
}
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 ReentrantLock getLockForKey(Object key) {
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
由于在事务控制内,第一次SQL执行完成后结果并没有缓存,而且ReetrantLock是可重入锁,这就意味着在第二次执行SQL时依旧可以成功获取到锁并且由于结果没有缓存所以并不会释放锁,我们继续跟进源码:
再次跟进:
继续跟进:
可以看到,当事务提交后有一个释放锁的操作,但是这里的锁只释放了一次,而获取的时候是两次,这样当再次调用test方法时就会出现无法获取到锁的问题,不知道这是否是Mybatis的BUG;