目录
-
- 环境搭建
-
-
- 建表
- 创建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()设置生存时间。