面试重灾区:请说说mybatis的一级缓存和二级缓存

2,107 阅读3分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战 在面试过程中经常会问请说说mybatis的一级缓存和二级缓存,下文将带你实现如何使用。如果不敢兴趣请直接看结论。

1.理解mybatis缓存

Mybatis的一级缓存和二级缓存是Mybatis自带的。

目的:将sql的查询结果存放于缓存内,在接下来再次查询时,会直接从缓存中取出而不是从数据库里面获取。这样会提升查询速度,但是会产生脏读。

注意:一级缓存是自动开启的,二级缓存是需要手动开启的。所以开启二级缓存需要慎重考虑。(使用spring boot环境)

2.一级缓存

一级缓存是自动开启的。是sqlSession级别的缓存。

个人理解就是同一个事务之内,调取两次相同sql。会读取一级缓存。

我们只要满足以下条件就可以使用一级缓存:

  • 1.sqlSession级别的缓存。
  • 2.使用@Transactional。(必须有此注解)

此处只贴上service的代码,其余的就是普通的mvc查询代码很简单。

       @Transactional
	public City selectCity(int id) {
     	System.out.println("第一次查询");
    	redisDao.selectAnno(id);         	//调用dao层查询方法
    	System.out.println("第二次查询");
		return redisDao.selectAnno(id);  //调用dao层查询方法

	}

这样我们就可以看到查询sql。经过测试发现两次相同的查询只会打印出一条sql。

spring boot中使用开启查看sql的功能,需要指定dao层的包的位置。

logging.level.com.example.redis.dao=debug

3.二级缓存

1.什么是二级缓存

二级缓存是mapper级别的,只要是对一个mapper调用就可以使用二级缓存。

2.查询顺序

在Mybatis中查询的顺序是:二级缓存----一级缓存-------数据库。

3.使用redis作二级缓存

首先连接redis,实现Cache 接口,实现接口中的方法,从redis取值和存值。这里使用了RedisTemplate 。

public class MybatisRedisCache implements Cache {

	private static Logger log = LoggerFactory.getLogger(MybatisRedisCache.class);
	private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
	private RedisTemplate redisTemplate;
	private String id;
	private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间

	public MybatisRedisCache(String id) {
		if (id == null) {
			throw new IllegalArgumentException("Cache instances require an ID");
		}
		this.id = id;
	}

	@Override
	public void clear() {
		RedisTemplate redisTemplate = getRedisTemplate();
		redisTemplate.execute((RedisCallback) connection -> {
			connection.flushDb();
			return null;
		});
		log.debug("Clear all the cached query result from redis");

	}

	@Override
	public String getId() {
		// TODO Auto-generated method stub
		return this.id;
	}

	@Override
	public Object getObject(Object key) {
		RedisTemplate redisTemplate = getRedisTemplate();
		//ValueOperations opsForValue = redisTemplate.opsForValue();
		log.debug("Get cached query result from redis");
		//return opsForValue.get(key);
		Object obj = redisTemplate.opsForValue().get(key.toString());
        return obj;

	}

	@Override
	public ReadWriteLock getReadWriteLock() {
		return readWriteLock;
	}

	@Override
	public int getSize() {
		return 0;
	}

	@Override
	@SuppressWarnings("uncheck")
	public void putObject(Object key, Object value) {
		//RedisTemplate redisTemplate = getRedisTemplate();
		//ValueOperations opsForValue = redisTemplate.opsForValue();
		//opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
		
        redisTemplate.opsForValue().set(key.toString(), value, 2, TimeUnit.DAYS);

		log.debug("Put query result to redis");

	}

	@Override
	public Object removeObject(Object key) {
		RedisTemplate redisTemplate = getRedisTemplate();
		redisTemplate.delete(key);
		log.debug("Remove cached query result from redis");
		return null;
	}

	private RedisTemplate getRedisTemplate() {
		if (redisTemplate == null) {
			redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
		}
		return redisTemplate;
	}

}

但是RedisTemplate 不是spring提供的,不能用spring的ioc进行依赖注入,所以需要我们手写来注册RedisTemplate 。

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
	private static ApplicationContext applicationContext;

	@Override
	public void setApplicationContext(ApplicationContext ctx) throws BeansException {
		applicationContext = ctx;
	}

	/**
	 * Get application context from everywhere
	 *
	 * @return
	 */
	public static ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	/**
	 * Get bean by class
	 *
	 * @param clazz
	 * @param <T>
	 * @return
	 */
	public static <T> T getBean(Class<T> clazz) {
		return applicationContext.getBean(clazz);
	}

	/**
	 * Get bean by class name
	 *
	 * @param name
	 * @param <T>
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T> T getBean(String name) {
		return (T) applicationContext.getBean(name);
	}              
}

然后在mapper文件中开启二级缓存。

<mapper namespace="com.example.redis.dao.RedisDao">
<!-- 开启二级缓存 -->
<cache type="com.example.redis.cache.MybatisRedisCache"></cache> 
	<select id="selectAnno" resultType="com.example.redis.entity.City">
		select * from city 
		<if test="id != null">where id = #{id}</if>
	</select>
	<insert id="insertIn">
		INSERT INTO `mysql`.`city` (`id`,
		`provinceId`, `cityName`, `description`) VALUES ('1', '1', 'aa', 'aa')
	</insert>
</mapper>

最后需要序列化,要不会在redis存放乱码。

@Configuration
public class RedisConfig {

	@Autowired
	private RedisTemplate redisTemplate;
	
	@Bean
	public RedisTemplate redisTemplateInit() {
		Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
		// 设置序列化Key的实例化对象
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		// 设置序列化Value的实例化对象
		redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
		redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
		redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
		return redisTemplate;
	}
}

然后我们测试,在不同的sqlsession中执行同一条sql时,只有第一次会打印出sql。我们在redis中会发现,执行一次sql后会在redis存储本次的查询结果。

4.结论

一级缓存:同一个事务之内,调取两次相同sql。会读取一级缓存。

二级缓存:mapper级别的,只要是对一个mapper调用就可以使用二级缓存。