Mybatis提供了对缓存的支持,分为一级缓存和二级缓存,其查询顺序为:二级缓存>一级缓存->数据库,最原始是直接查询数据库,为了提高效率和节省资源,引入了一级缓存,为了进一步提高效率,引入了二级缓存。
Mybatis一级缓存是sqlSession级别的缓存,缓存的数据只在sqlSession内有效。
Mybatis二级缓存是mapper级别的缓存,同一个namespace公用这一个缓存,对sqlSession是共享的。
Mybatis默认开启一级缓存,默认没有开启二级缓存,需要在setting全局参数中配置开启二级缓存。
1、二级缓存
Mybatis二级缓存是多个sqlSession共享的,其作用域是mapper的同一个namespace,即同一个namespace下的所有操作语句都影响着同一个cache,即二级缓存被多个sqlSession共享,是一个全局的变量。
不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递参数也相同时,即最终执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。
二级缓存默认是关闭二级缓存的,因为对应增删改操作频繁的话,那么二级缓存形同虚设,每次都会被清空缓存。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatis的Cache 接口实现,有一定的开发成本,直接使用Redis、分布式缓存可能成本更低,安全性也更高。
2、实例
2.1、新建springboot工程
在idea中新建springboot工程,版本为2.7.+,引入相关依赖,如下
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
在application.properties文件中配置mybatis、redis等
# 端口
server.port=8083
# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
#mybatis
#entity扫描的包名
mybatis.type-aliases-package=com.*.*.model
#Mapper.xml所在的位置
mybatis.mapper-locations=classpath*:/mapper/*Mapper.xml
#开启MyBatis的二级缓存
mybatis.configuration.cache-enabled=true
#日志配置
logging.level.com.*=debug
logging.level.org.springframework.web=debug
logging.level.org.springframework.transaction=debug
logging.level.org.mybatis=debug
#redis
#database name
spring.redis.database=0
#server host
spring.redis.host=127.0.0.1
#server password
spring.redis.password=
#connection port
spring.redis.port=6379
debug=false
2.2、相关配置类
Redis配置类替换序列化实现方式
@Configuration
public class RedisConfig {
/**
* 重写Redis序列化方式,使用Json方式:
* 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。
* RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。
* Spring Data JPA为我们提供了下面的Serializer:
* GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
* 在此我们将自己配置RedisTemplate并定义Serializer。
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 设置值(value)的序列化采用Jackson2JsonRedisSerializer。
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 设置键(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
自定义缓存需要实现Mybatis的Cache接口,这里将使用Redis来作为缓存的容器
public class MybatisRedisCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private RedisTemplate<String, Object> redisTemplate = SpringContextHolder.getBean("redisTemplate");
private String id;
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
logger.info("Redis Cache id " + id);
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
@Override
public void putObject(Object key, Object value) {
if (value != null) {
// 向Redis中添加数据,有效时间是2天
redisTemplate.opsForValue().set(key.toString(), value, 2, TimeUnit.DAYS);
}
}
@Override
public Object getObject(Object key) {
try {
if (key != null) {
Object obj = redisTemplate.opsForValue().get(key.toString());
return obj;
}
} catch (Exception e) {
logger.error("redis ");
}
return null;
}
@Override
public Object removeObject(Object key) {
try {
if (key != null) {
redisTemplate.delete(key.toString());
}
} catch (Exception e) {
}
return null;
}
@Override
public void clear() {
logger.debug("清空缓存");
try {
Set<String> keys = redisTemplate.keys("*:" + this.id + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
} catch (Exception e) {
}
}
@Override
public int getSize() {
Long size = (Long) redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
return connection.dbSize();
}
});
return size.intValue();
}
}
通过Spring
Aware(容器感知)来获取到ApplicationContext,然后根据ApplicationContext获取容器中的Bean
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
/**
* 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
// NOSONAR
SpringContextHolder.applicationContext = applicationContext;
}
/**
* 取得存储在静态变量中的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("applicationContext未注入,请在applicationContext.xml中定义SpringContextHolder");
}
}
}
2.3、mapper
实体类
@Data
@ToString
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
private Long id;
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 地址
*/
private String address;
}
Mapper接口
@Mapper
public interface PersonMapper {
/**
* 查询
* @param id
* @return
*/
Person selectByPrimaryKey(Long id);
/**
* 删除
* @param id
* @return
*/
int deleteByPrimaryKey(Long id);
/**
* 更新
* @param record
* @return
*/
int updateByPrimaryKey(Person record);
/**
* 插入
* @param record
* @return
*/
int insert(Person record);
/**
* 插入
* @param record
* @return
*/
int insertSelective(Person record);
/**
* 更新
* @param record
* @return
*/
int updateByPrimaryKeySelective(Person record);
/**
* 获取所有数据
* @return
*/
List<Person> findAll();
}
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.*.springboot_mybatis_cache.mapper.PersonMapper">
<cache type="com.*.springboot_mybatis_cache.common.MybatisRedisCache">
<property name="eviction" value="LRU" />
<property name="flushInterval" value="6000000" />
<property name="size" value="1024" />
<property name="readOnly" value="false" />
</cache>
<resultMap id="BaseResultMap" type="com.*.springboot_mybatis_cache.model.Person">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="age" property="age" jdbcType="INTEGER"/>
<result column="address" property="address" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id, name, age, address
</sql>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long">
select
<include refid="Base_Column_List"/>
from person
where id = #{id,jdbcType=BIGINT}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from person
where id = #{id,jdbcType=BIGINT}
</delete>
<update id="updateByPrimaryKey" parameterType="com.*.springboot_mybatis_cache.model.Person">
update person
set name = #{name,jdbcType=VARCHAR},
age = #{age,jdbcType=INTEGER},
address = #{address,jdbcType=VARCHAR}
where id = #{id,jdbcType=BIGINT}
</update>
<insert id="insert" parameterType="com.*.springboot_mybatis_cache.model.Person">
<selectKey resultType="java.lang.Long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into person (name, age, address)
values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{address,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" parameterType="com.*.springboot_mybatis_cache.model.Person">
<selectKey resultType="java.lang.Long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into person
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name != null">
name,
</if>
<if test="age != null">
age,
</if>
<if test="address != null">
address,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
<if test="age != null">
#{age,jdbcType=INTEGER},
</if>
<if test="address != null">
#{address,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.*.springboot_mybatis_cache.model.Person">
update person
<set>
<if test="name != null">
name = #{name,jdbcType=VARCHAR},
</if>
<if test="age != null">
age = #{age,jdbcType=INTEGER},
</if>
<if test="address != null">
address = #{address,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
<!-- 对这个语句useCache="true"默认是true,可以不写 -->
<select id="findAll" resultMap="BaseResultMap" useCache="true">
select
<include refid="Base_Column_List"/>
from person
</select>
</mapper>
2.4、service
Service接口
public interface PersonService {
/**
* 查询
* @return
*/
List<Person> findAll();
/**
* 插入
* @param person
*/
void insert(Person person);
}
Service实现类
@Service
@Transactional(readOnly = true)
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonMapper personMapper;
@Override
public List<Person> findAll() {
return personMapper.findAll();
}
@Override
@Transactional
public void insert(Person person) {
personMapper.insert(person);
}
}
2.5、测试
private Logger logger = LoggerFactory.getLogger(SpringbootMybatisCacheApplicationTests.class);
@Autowired
private PersonService personService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
public void testInsert() {
Person person = new Person();
person.setName("测试");
person.setAddress("address");
person.setAge(10);
personService.insert(person);
logger.debug(JSON.toJSONString(person));
}
@Test
public void testFindAll() {
List<Person> persons = personService.findAll();
logger.debug(JSON.toJSONString(persons));
}
// 测试mybatis缓存
@Test
public void testCache() {
long begin = System.currentTimeMillis();
List<Person> persons = personService.findAll();
long ing = System.currentTimeMillis();
personService.findAll();
long end = System.currentTimeMillis();
logger.debug("第一次请求时间:" + (ing - begin) + "ms");
logger.debug("第二次请求时间:" + (end - ing) + "ms");
logger.debug(JSON.toJSONString(persons));
}
// 测试Redis存储和获取一个List
@Test
public void testRedisCacheSetList() {
List<Person> persons = new ArrayList<>();
Person person = new Person();
person.setName("测试");
person.setAddress("address");
person.setAge(10);
persons.add(person);
persons.add(person);
persons.add(person);
redisTemplate.opsForValue().set("111", persons, 2, TimeUnit.MINUTES);
persons = (List<Person>) redisTemplate.opsForValue().get("111");
System.out.println(JSON.toJSONString(persons));
}
// 测试Redis存储和获取一个Object
@Test
public void testRedisCacheSetObject() {
Person person = new Person();
person.setName("测试");
person.setAddress("address");
person.setAge(10);
redisTemplate.opsForValue().set("222", person, 2, TimeUnit.MINUTES);
Object p = redisTemplate.opsForValue().get("222");
if (p instanceof Person) {
Person person1 = (Person) p;
System.out.println(JSON.toJSONString(person1));
}
}
// 测试通过SpringAware获取Spring容器中的额Bean
@Test
public void testApplicationContextAware() {
RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
System.out.println(redisTemplate);
}
测试mybatis缓存结果类似如下
3、小结
实际开发中,MyBatis 通常和 Spring 进行整合开发。Spring 将事务放到 Service 中管理,对于每一个 service 中的 sqlSession 是不同的,这是通过 mybatis-spring 中的 org.mybatis.spring.mapper.MapperScannerConfigurer 创建 sqlsession 自动注入到 service 中的。每次查询之后都要进行关闭 sqlSession ,关闭之后数据被清空。所以 spring 整合之后,如果没有事务,一级缓存是没有意义的。那么如果开启二级缓存,关闭 sqlsession 后,会把该 sqlsession 一级缓存中的数据添加到namespace 的二级缓存中。这样,缓存在sqlsession关闭之后依然存在。
二级缓存的应用场景多用于查询多于修改时的场景,因为任何增删改操作都将刷新二级缓存,对二级缓存的频繁刷新将降低系统性能。
感兴趣的小伙伴可以尝试一下~