目錄
-
- 環境搭建
-
-
- 建表
- 建立springboot項目
- 實體類
- Dao接口
- 注解掃描dao包
- Mapper配置檔案
- Service接口
- Service實作類
- application.properties
-
- MyBatis開啟緩存
- MyBatis+Redis緩存
完整代碼位址:https://gitee.com/Gsomeone/my-batis-redis-cache
環境搭建有點全,可以跳過。
環境搭建
建表
CREATE TABLE teacher(
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255)
)
CREATE TABLE student (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
tid VARCHAR(255) REFERENCES teacher(id)
)
建立springboot項目
我們首先搭建一個spring+mybatis環境
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
</dependencies>
實體類
package com.gu.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
@Data
@Accessors(chain = true)
// 必須實作Serializable,不然無法序列化
public class Teacher implements Serializable {
private String id;
private String name;
private List<Student> students;
}
package com.gu.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class Student implements Serializable {
private String id;
private String name;
private String tid;
}
Dao接口
package com.gu.dao;
import com.gu.entity.Student;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public interface StudentDao {
List<Student> findAll();
Student findOne(String id);
void delete(String id);
void save(Student student);
void update(Student student);
}
注解掃描dao包
package com.gu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.gu.dao")
public class RedisStudyApplication {
public static void main(String[] args) {
SpringApplication.run(RedisStudyApplication.class, args);
}
}
package com.gu.dao;
import com.gu.entity.Teacher;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public interface TeacherDao {
List<Teacher> findAll();
Teacher findOne(String id);
void delete(String id);
void save(Teacher teacher);
void update(Teacher teacher);
}
Mapper配置檔案
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gu.dao.StudentDao">
<select id="findAll" resultType="Student">
select id, name, tid from student
</select>
<select id="findOne" parameterType="String" resultType="Student">
select id, name, tid from student where id = #{id}
</select>
<delete id="delete" parameterType="String">
delete from student where id = #{id}
</delete>
<insert id="save" parameterType="Student">
insert into student (id, name, tid) values (#{id}, #{name}, #{tid});
</insert>
<update id="update" parameterType="Student">
update student
<set>
<if test="name != null">
name = #{name},
</if>
<if test="tid != null">
tid = #{tid},
</if>
</set>
where id = #{id};
</update>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gu.dao.TeacherDao">
<select id="findAll" resultMap="TeacherStudent">
select t.id, t.name, s.id, s.name
from teacher t, student s
where t.id = s.tid
</select>
<select id="findOne" parameterType="String" resultType="Teacher">
select id, name from teacher where id = #{id};
</select>
<delete id="delete" parameterType="String">
delete from teacher where id = #{id}
</delete>
<insert id="save" parameterType="Teacher">
insert into teacher (id, name) values (#{id}, #{name});
</insert>
<update id="update" parameterType="Teacher">
update teacher
<set>
<if test="name != null">
name = #{name},
</if>
</set>
where id = #{id};
</update>
<!--結果集-->
<resultMap id="TeacherStudent" type="Teacher">
<result property="id" column="id"/>
<result property="name" column="name"/>
<collection property="students" ofType="Student">
<result column="id" property="id"/>
<result column="name" property="name"/>
</collection>
</resultMap>
</mapper>
Service接口
package com.gu.service;
import com.gu.entity.Student;
import java.util.List;
public interface StudentService {
List<Student> findAll();
Student findOne(String id);
void delete(String id);
void save(Student student);
void update(Student student);
}
package com.gu.service;
import com.gu.entity.Teacher;
import java.util.List;
public interface TeacherService {
List<Teacher> findAll();
Teacher findOne(String id);
void delete(String id);
void save(Teacher teacher);
void update(Teacher teacher);
}
Service實作類
package com.gu.service;
import com.gu.dao.StudentDao;
import com.gu.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
@Service
@Transactional
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentDao studentDao;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public List<Student> findAll() {
return studentDao.findAll();
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public Student findOne(String id) {
return studentDao.findOne(id);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void delete(String id) {
studentDao.delete(id);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Student student) {
student.setId(UUID.randomUUID().toString());
studentDao.save(student);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void update(Student student) {
studentDao.update(student);
}
}
package com.gu.service;
import com.gu.dao.TeacherDao;
import com.gu.entity.Teacher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
@Service
@Transactional
public class TeacherServiceImpl implements TeacherService {
@Autowired
private TeacherDao teacherDao;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public List<Teacher> findAll() {
return teacherDao.findAll();
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public Teacher findOne(String id) {
return teacherDao.findOne(id);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void delete(String id) {
teacherDao.delete(id);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Teacher teacher) {
teacher.setId(UUID.randomUUID().toString());
teacherDao.save(teacher);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void update(Teacher teacher) {
teacherDao.update(teacher);
}
}
application.properties
# datasource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/study?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
# mybatis
mybatis.type-aliases-package=com.gu.entity
mybatis.mapper-locations=classpath:com/gu/mapper/*.xml
# redis
spring.redis.host=192.168.70.130
spring.redis.port=6379
spring.redis.database=0
#log
logging.level.com.gu.dao=debug
MyBatis開啟緩存
mybatis的一級緩存在SqlSession,預設開啟。二級緩存在對應namespace的mapper檔案中,需要手動開啟。
如何開啟maybatis二級緩存?
答:隻需要在mapper檔案下添加cache标簽即可。例如:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gu.dao.StudentDao">
<cache/>
<select id="findAll" resultType="Student">
select id, name, tid from student
</select>
...
</mapper>
查詢測試
@Test
public void testFindAll() {
studentService.findAll().forEach(student -> System.out.println(student));
System.out.println("==============================================");
studentService.findAll().forEach(student -> System.out.println(student));
}

總結:可以看到第一次查詢沒有在緩存中找到,是通過sql查詢資料庫得到的。而第二次查詢,在緩存中找到了,是以沒有查詢資料庫。
深入探究
cache标簽的type屬性可以選擇不同的緩存,比如預設的type是PerpetualCache,即
/**
* Copyright 2009-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
從31行可以看出,PerpetualCache是基于HashMap的,它的putObject,getObject方法就是對HashMap的put,get。 而HashMap在多線程下是不安全的,是以PerpetualCache也是線程不安全的,但是Redis是單線程是安全的。我們隻需要實作Cache接口就能寫RedisCache。
MyBatis+Redis緩存
package com.gu.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
}
package com.gu.cache;
import com.gu.utils.ApplicationContextUtils;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.DigestUtils;
public class RedisCache implements Cache {
private final String id;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
getRedisTemplate().opsForHash().put(id, getKeyToMD5(key.toString()), value);
}
@Override
public Object getObject(Object key) {
return getRedisTemplate().opsForHash().get(id, getKeyToMD5(key.toString()));
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
System.out.println("清空緩存-----");
getRedisTemplate().delete(id);
}
@Override
public int getSize() {
return getRedisTemplate().opsForHash().size(id).intValue();
}
public RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
public String getKeyToMD5(String key) {
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
增删改的時候,為了保證Redis中的資料和資料庫中具有一緻性,應該同時删除Redis中對于的緩存。通過打斷點我們可以知道,調用的是clear()方法,而不是removeObject()方法。執行增删改時,會執行clear方法,清除對應緩存。
由于student和teacher存在多對一,是以學生類的緩存應該被老師類包含。我們可以直接使用cache-ref将學生類緩存到老師類下。
代碼如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gu.dao.TeacherDao">
<cache type="com.gu.cache.RedisCache"/>
<select id="findAll" resultMap="TeacherStudent">
select t.id, t.name, s.id, s.name
from teacher t, student s
where t.id = s.tid
</select>
......
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gu.dao.StudentDao">
<cache-ref namespace="com.gu.dao.TeacherDao"/>
<select id="findAll" resultType="Student">
select id, name, tid from student
</select>
......
</mapper>
總結:
- 使用散清單來緩存查詢的結果。斷點調試可以發現,這裡的id就是對應的mapper檔案中的namespacecom.gu.dao.StudentDao,而key和value對應就是的查詢語句和查詢結果。
findAll" resultMap=“TeacherStudent”>
select t.id, t.name, s.id, s.name
from teacher t, student s
where t.id = s.tid
......
```
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gu.dao.StudentDao">
<cache-ref namespace="com.gu.dao.TeacherDao"/>
<select id="findAll" resultType="Student">
select id, name, tid from student
</select>
......
</mapper>
總結:
- 使用散清單來緩存查詢的結果。斷點調試可以發現,這裡的id就是對應的mapper檔案中的namespacecom.gu.dao.StudentDao,而key和value對應就是的查詢語句和查詢結果。
- 如果是設定具有時效性的資料,可以在putObject()方法中執行expire()設定生存時間。