首先来了解下mybatis 缓存,mybatis缓存分为一级缓存和二级缓存。一级缓存是默认开启的,无需其他配置操作,二级缓存则需要手动设置开启。
一级缓存原理:
Mybatis的一级缓存是指同一个SqlSession中的操作。一级缓存的作用域是一个SqlSession。
在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。
二级缓存原理:
Mybatis的二级缓存是指mapper映射文件。二级缓存是多个sqlSession共享的,其作用域是mapper下的同一个namespace。
在不同的sqlSession中,相同的namespace下,相同的查询sql语句并且参数也相同的情况下,会命中二级缓存。如果调用相同namespace下的mapper映射文件中的增删改SQL,并执行了commit操作。此时会清空该namespace下的二级缓存。
了解一些基本原理后,我们开始在springboot集成mybatis的情况下,开启二级缓存。
- 在pom.xml文件中引入mybatis和redis的依赖
<!--mybatis 依赖包--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--redis lettuce--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>
- 在application.yml文件中配置mybatis相关设置时,开启二级缓存
### mybatis相关配置mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启MyBatis的二级缓存 cache-enabled: true mapper-locations: classpath*:mappers/*Mapper.xml### Redis 相关配置redis: host: localhost port: 6379 timeout: 10000 database: 0 lettuce:pool: max-active: 8 max-wait: -1 max-idle: 8 min-idle: 0
- 实体类实现序列化
我们采用的redis序列化方式是默认的jdk序列化。所以数据库的查询对象(比如Student类)需要实现Serializable接口。
public class Student implements Serializable {//采用的redis序列化方式是默认的jdk序列化。所以数据库的查询对象实体需要实现Serializable接口。private static final long serialVersionUID = 1L;private int id;private String name;private int age;private String position;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getPosition() {return position;}public void setPosition(String position) {this.position = position;}@Overridepublic String toString() {return "Student{" +"id=" + id +", name='" + name + '\'' +", age=" + age +", position='" + position + '\'' +'}';}}
- 先看一下Redis的配置类(这里用的是lettuce)
@Configurationpublic class RedisConfig {@Autowiredprivate LettuceConnectionFactory connectionFactory;@Beanpublic RedisTemplate<String,Object> redisTemplate() {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();initDomainRedisTemplate(redisTemplate, connectionFactory);return redisTemplate;}/** * 设置数据存入 redis 的序列化方式 * @param template * @param factory */private void initDomainRedisTemplate(RedisTemplate<String, Object> template,LettuceConnectionFactory factory) {// 定义 key 的序列化方式为 string// 需要注意这里Key使用了 StringRedisSerializer,那么Key只能是String类型的,不能为Long,Integer,否则会报错抛异常。StringRedisSerializer redisSerializer = new StringRedisSerializer();template.setKeySerializer(redisSerializer);// 定义 value 的序列化方式为 json@SuppressWarnings({"rawtypes", "unchecked"})Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);template.setValueSerializer(jackson2JsonRedisSerializer);//hash结构的key和value序列化方式template.setHashKeySerializer(jackson2JsonRedisSerializer);template.setHashValueSerializer(jackson2JsonRedisSerializer);template.setEnableTransactionSupport(true);template.setConnectionFactory(factory);}}
- 缓存配置类
public class MybatisRedisCache implements Cache {private static final Logger log = LoggerFactory.getLogger(MybatisRedisCache.class);private String id;private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间public MybatisRedisCache(String id) {this.id = id;}private RedisTemplate<Object, Object> getRedisTemplate(){return ApplicationContextHolder.getBean("redisTemplate");}@Overridepublic String getId() {return id;}@Overridepublic void putObject(Object key, Object value) {RedisTemplate redisTemplate = getRedisTemplate();redisTemplate.boundHashOps(getId()).put(key, value);log.info("[结果放入到缓存中: " + key + "=" + value+" ]");}@Overridepublic Object getObject(Object key) {RedisTemplate redisTemplate = getRedisTemplate();Object value = redisTemplate.boundHashOps(getId()).get(key);log.info("[从缓存中获取了: " + key + "=" + value+" ]");return value;}@Overridepublic Object removeObject(Object key) {RedisTemplate redisTemplate = getRedisTemplate();Object value = redisTemplate.boundHashOps(getId()).delete(key);log.info("[从缓存删除了: " + key + "=" + value+" ]");return value;}@Overridepublic void clear() {RedisTemplate redisTemplate = getRedisTemplate();redisTemplate.delete(getId());log.info("清空缓存!!!");}@Overridepublic int getSize() {RedisTemplate redisTemplate = getRedisTemplate();Long size = redisTemplate.boundHashOps(getId()).size();return size == null ? 0 : size.intValue();}@Overridepublic ReadWriteLock getReadWriteLock() {return readWriteLock;}}
ps:
重点部分就是重写这个mybatis的cache类,它只会对配置文件类型的映射文件起作用。
该接口共有以下五个方法:
String getId():mybatis缓存操作对象的标识符。一个mapper对应一个mybatis的缓存操作对象。
void putObject(Object key, Object value):将查询结果塞入缓存。
Object getObject(Object key):从缓存中获取被缓存的查询结果。
Object removeObject(Object key):从缓存中删除对应的key、value。只有在回滚时触发。
void clear():发生更新时,清除缓存。
int getSize():可选实现。返回缓存的数量。
ReadWriteLock getReadWriteLock():可选实现。用于实现原子性的缓存操作。
上述重写cache类中有几个关键点:
- 自定义实现的二级缓存,必须要有一个带id的构造函数,否则会报错。
- 此处使用Spring封装的redisTemplate来操作Redis。很多都是直接用jedis库,但是现在springboot2.x 以上对lettuce的兼容更好。RedisTemplate封装了底层的实现,使用redisTemplate会更加方便,无论是使用jedis还是使用lettuce,我们可以直接更换底层的库,无需修改上层代码。
- 这里不能通过@Autowire的方式引用redisTemplate,因为RedisCache并不是Spring容器里的bean。所以我们需要手动地去调用容器的getBean方法来拿到这个bean,那么这样,我们就需要引入ApplicationContextHolder这个类。
- ApplicationContextHolder.java (我们需要通过这个类得到RedisTemplate)
@Componentpublic class ApplicationContextHolder implements ApplicationContextAware{private static ApplicationContext applicationContext;/** * 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量. */public void setApplicationContext(ApplicationContext applicationContext) {ApplicationContextHolder.applicationContext = applicationContext; // NOSONAR}/** * 取得存储在静态变量中的ApplicationContext. */public static ApplicationContext getApplicationContext() {checkApplicationContext();return applicationContext;}/** * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型. */@SuppressWarnings("unchecked")public static <T> T getBean(String name) {checkApplicationContext();return (T) applicationContext.getBean(name);}/** * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型. */@SuppressWarnings("unchecked")public static <T> T getBean(Class<T> clazz) {checkApplicationContext();return (T) applicationContext.getBeansOfType(clazz);}/** * 清除applicationContext静态变量. */public static void cleanApplicationContext() {applicationContext = null;}private static void checkApplicationContext() {if (applicationContext == null) {throw new IllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextHolder");}}}
- 然后再映射文件中开启二级缓存(使用二级缓存)
<mapper namespace="com.example.demo.dao.StudentDao"><!-- 开启基于redis的二级缓存 --><cache type="com.example.demo.redis.cache.MybatisRedisCache"/><cache/><insert id="insert" parameterType="com.example.demo.entity.Student" useGeneratedKeys="true" keyProperty="id">insert into students(name,age,position) values (#{name},#{age},#{position})</insert><insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">insert into students(name,age,position) values<foreach collection="studentList" item="item" index="index" open="" close="" separator=",">( #{item.name}, #{item.age}, #{item.position} )</foreach></insert><delete id="delete" parameterType="java.lang.String">delete from students where name = #{name}</delete><!--并且在update语句中,设置flushCache为true,这样在更新信息时,能够自动失效缓存(本质上调用的是clear方法)--><update id="update" parameterType="com.example.demo.entity.Student" flushCache="true">update students set students.position = #{position} where name = #{name}</update><select id="findByName" resultMap="BaseResultMap">select * from students where name = #{name}</select><select id="findAll" resultMap="BaseResultMap">select * from students</select><resultMap id="BaseResultMap" type="com.example.demo.entity.Student"><result column="name" property="name"/><result column="age" property="age"/><result column="position" property="position"/></resultMap></mapper>
下面是我在实现二级缓存过程中一些报错问题:
- 在我修改了序列化问题后,报错消失。