首先來了解下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;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", position='" + position + '\'' +
'}';
}
}
- 先看一下Redis的配置類(這裏用的是lettuce)
@Configuration
public class RedisConfig {
@Autowired
private LettuceConnectionFactory connectionFactory;
@Bean
public 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");
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.boundHashOps(getId()).put(key, value);
log.info("[結果放入到緩存中: " + key + "=" + value+" ]");
}
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
Object value = redisTemplate.boundHashOps(getId()).get(key);
log.info("[從緩存中獲取了: " + key + "=" + value+" ]");
return value;
}
@Override
public Object removeObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
Object value = redisTemplate.boundHashOps(getId()).delete(key);
log.info("[從緩存刪除了: " + key + "=" + value+" ]");
return value;
}
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(getId());
log.info("清空緩存!!!");
}
@Override
public int getSize() {
RedisTemplate redisTemplate = getRedisTemplate();
Long size = redisTemplate.boundHashOps(getId()).size();
return size == null ? 0 : size.intValue();
}
@Override
public 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)
@Component
public 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>
下面是我在實現二級緩存過程中一些報錯問題:
- 在我修改了序列化問題後,報錯消失。