Mybatis缓存机制
2.1 缓存介绍
像大多数的持久化框架一样, Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。Mybatis 中缓存分为一级缓存,二级缓存。
1、默认情况下,只有一级缓存(session级别的缓存,也称为本地缓存)开启。
2、二级缓存需要手动开启和配置(默认支持状态),他是基于Mapper级别的缓存。
3、为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
从图中我们可以看出:一级缓存是基于SqlSessoion的缓存,一级缓存的内容不能跨sqlsession。由MyBatis自动维护。二级缓存是基于映射文件的缓存,缓存范围比一级缓存更大。不同的sqlsession可以访问二级缓存的内容。哪些数据放入二级缓存需要自己指定。
2.2 一级缓存
一级缓存,也叫本地缓存,作用域默认为当前的SqlSession,也就是说不同的SqlSession的缓存是不同的。当 Session flush 或 close 后, 该Session中的一级缓存将被清空。一级缓存是MyBatis维护的,并且默认开启,且不能被关闭,但可以调用clearCache()来清空缓存,或者改变缓存的作用域;
2.2.1 一级缓存相关参数
localCacheScope :
SESSION :会话级别缓存,默认值STATEMENT :语句级别缓存,缓存只对当前执行的语句生效(相当于关闭一级缓存)
2.2.2 一级缓存测试
package com.dfbz.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Emp implements Serializable {
private Integer id;
private String name;
private Integer age;
private String addr;
private Double salary;
private Double deptId;
}
package com.dfbz.dao;
import com.dfbz.entity.Emp;
public interface EmpDao {
Emp findById(Integer id);
void save(Emp emp);
}
<?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.dfbz.dao.EmpDao">
<select id="findById" resultType="emp">
select * from emp where id=#{id}
</select>
<insert id="save" flushCache="false">
insert into emp(id,name) values(null,#{name});
</insert>
</mapper>
@Test
public void test1() {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
Emp emp2 = mapper.findById(1);
System.out.println(emp2);
System.out.println(emp == emp2);
session.close();
}
观察日志:
2.2.3 一级缓存失效清空
一级缓存在如下情况下,会情况:
- 1)将一级缓存的作用域设置为语句级别(
localCacheScope 设置为STATEMENT ) - 2)清空缓存(clearCache)
- 3)执行任何增删改操作都会导致整个一级缓存(并不是清除影响的那行缓存,而是整个一级缓存)
- 4)刷新缓存(flushCache)
- 5)session关闭,一级缓存清空
2.2.3.1 设置localCacheScope
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
@Test
public void test1() {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
Emp emp2 = mapper.findById(1);
System.out.println(emp2);
System.out.println(emp == emp2);
session.close();
}
执行程序,观察控制台:
2.2.3.2 clearCache
一级缓存清空测试:
@Test
public void test2() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
session.clearCache();
Emp emp2 = mapper.findById(1);
System.out.println(emp2);
System.out.println(emp == emp2);
session.close();
}
执行结果,查看控制台:
2.2.3.3 关闭session清空缓存
一级缓存是基于session级别的,如果session关闭,那么一级缓存将会失效!
@Test
public void test3() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession();
EmpDao mapper2 = session2.getMapper(EmpDao.class);
Emp emp2 = mapper2.findById(1);
System.out.println(emp2);
System.out.println(emp == emp2);
session2.close();
}
执行程序,查看控制台:
2.2.3.4 执行任何的增删改清空缓存
在MyBatis中,执行任何的增删改都会导致一级缓存清空!
@Test
public void test3() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession();
EmpDao mapper2 = session2.getMapper(EmpDao.class);
Emp emp2 = mapper2.findById(1);
System.out.println(emp2);
System.out.println(emp == emp2);
session2.close();
}
2.2.3.5 flushCache清空缓存
flushCache属性用于控制执行完操作后是否要刷新缓存,对于增删改语句,flushCache的值默认是true,对于查询语句,flushCache的值默认是false,但是MyBatis不支持将任何的增删改语句设置为false;
<?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.dfbz.dao.EmpDao">
<select id="findById" resultType="emp" flushCache="true">
select * from emp where id=#{id}
</select>
<insert id="save" flushCache="false">
insert into emp values(null,#{name},#{age},#{addr},#{salary},null)
</insert>
</mapper>
Tips:在MyBatis中,不支持将任何的增删改语句的flushCache属性设置为false
@Test
public void test5() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
Emp emp2 = mapper.findById(1);
System.out.println(emp2);
System.out.println(emp == emp2);
session.close();
}
执行程序,观察控制台:
需要注意的是MyBatis并不支持将任何的增删改语句的flushCache设置为false
- 测试代码(此时save语句的flushCache为false):
@Test
public void test6() {
SqlSession session = factory.openSession();
EmpDao empDao = session.getMapper(EmpDao.class);
Emp emp = empDao.findById(1);
System.out.println(emp);
empDao.save(new Emp(null, "test", 20, null, null, null));
Emp emp2 = empDao.findById(1);
System.out.println(emp2);
session.close();
}
2.3 二级缓存
我们刚刚学习完了一级缓存,一级缓存是session级别的缓存,不同的session一级缓存是不能共享的;
二级缓存是mapper级别的缓存,多个session去操作同一个Mapper映射的SQL语句时,多个session可以共用二级缓存,二级缓存是跨SqlSession 的。
当二级缓存和一级缓存同时存在时,先查询二级缓存,再查询一级缓存;
2.3.1 二级缓存相关配置
MyBatis默认是开启二级缓存的,我可以通过cacheEnabled 参数来控制二级缓存的开关;此配置默认开启
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="cacheEnabled" value="true"/>
</settings>
在SqlMapConfig.xml开启二级缓存后(此配置默认开启状态),还需要在对应的mapper.xml文件中激活缓存;否则二级缓存并不会生效;
<?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.dfbz.dao.EmpDao">
<cache />
<select id="findById" resultType="emp">
select * from emp where id=#{id}
</select>
<insert id="save" >
insert into emp(id,name) values(null,#{name});
</insert>
</mapper>
存入二级缓存中的对象必须实现序列化接口:
2.3.2 二级缓存测试
2.3.2.1 二级缓存代码测试
当二级缓存和一级缓存同时存在时,先查询二级缓存,再查询一级缓存;
当session关闭时,将一级缓存中的数据写入二级缓存;
@Test
public void test1() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession();
EmpDao mapper2 = session2.getMapper(EmpDao.class);
Emp emp2 = mapper2.findById(1);
System.out.println(emp2);
System.out.println(emp == emp2);
session2.close();
}
观察日志:
2.3.2.2 useCache
useCache可以控制当前SQL语句的结果集是否要存入二级缓存;默认清空下为true
<select id="findById" resultType="emp" useCache="true">
select * from emp where id=#{id}
</select>
Tips:
- 1)useCache只能控制二级缓存,并不会影响一级缓存;
- 2)useCache需要当前的mapper.xml开启二级缓存功能后才能使用;
2.3.3 二级缓存失效情况
二级缓存在如下情况下,会情况:
- 1)执行任何增删改操作
- 2)刷新缓存(flushCache)
2.3.3.1 执行增删改
执行任何的增删改操作,不仅会导致一级缓存清空,也会导致二级缓存清空!
@Test
public void test2() throws Exception {
SqlSession session = factory.openSession(true);
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession(true);
EmpDao mapper2 = session2.getMapper(EmpDao.class);
Emp saveEmp = new Emp();
saveEmp.setName("aaa");
mapper2.save(saveEmp);
Emp emp2 = mapper2.findById(1);
System.out.println(emp2);
session2.close();
}
执行程序,日志如下:
Created connection 368342628.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@15f47664]
==> Preparing: select * from dept where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, location
<== Row: 1, 研发部, 中国台湾
<== Total: 1
研发部
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@15f47664]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@15f47664]
Returned connection 368342628 to pool.
Opening JDBC Connection
Checked out connection 368342628 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@15f47664]
==> Preparing: insert into dept values(null,?,?)
==> Parameters: test(String), test(String)
<== Updates: 1
Cache Hit Ratio [com.dfbz.dao.DeptDao]: 0.5
==> Preparing: select * from dept where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, location
<== Row: 1, 研发部, 中国台湾
<== Total: 1
false
Rolling back JDBC Connection [com.mysql.jdbc.JDBC4Connection@15f47664]
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@15f47664]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@15f47664]
Returned connection 368342628 to pool.
Disconnected from the target VM, address: '127.0.0.1:58814', transport: 'socket'
Process finished with exit code 0
2.3.3.2 flushCache
flushCache不仅会清空一级缓存,而且还会清空二级缓存
Tips:flushCache控制二级缓存时可以设置任何的增删改查是否清空二级缓存
@Test
public void test4() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession();
EmpDao mapper2 = session2.getMapper(EmpDao.class);
Emp emp2 = mapper2.findById(1);
System.out.println(emp2);
session2.close();
}
执行程序,日志如下:
Created connection 527829831.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
==> Preparing: select * from emp where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, age, addr, salary, dept_id
<== Row: 1, 张三, 20, 广西来宾, 7600.00, 1
<== Total: 1
Emp(id=1, name=张三, age=20, addr=广西来宾, salary=7600.0, deptId=null)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Returned connection 527829831 to pool.
Cache Hit Ratio [com.dfbz.dao.EmpDao]: 0.5
Opening JDBC Connection
Checked out connection 527829831 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
==> Preparing: select * from emp where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, age, addr, salary, dept_id
<== Row: 1, 张三, 20, 广西来宾, 7600.00, 1
<== Total: 1
Emp(id=1, name=张三, age=20, addr=广西来宾, salary=7600.0, deptId=null)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Returned connection 527829831 to pool.
Disconnected from the target VM, address: '127.0.0.1:52082', transport: 'socket'
Process finished with exit code 0
flushCache不仅会清空一级缓存,而且也会清空二级缓存
Emp findByName(String name);
<select id="findByName" resultType="emp" >
select * from emp where name=#{name}
</select>
@Test
public void test5() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findByName("小明");
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession();
EmpDao mapper2 = session2.getMapper(EmpDao.class);
Emp emp2 = mapper2.findById(1);
System.out.println(emp2);
Emp emp3 = mapper2.findByName("小明");
System.out.println(emp3);
session2.close();
}
执行程序,日志如下:
Created connection 527829831.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
==> Preparing: select * from emp where name=?
==> Parameters: 小明(String)
<== Columns: id, name, age, addr, salary, dept_id
<== Row: 3, 小明, 25, 广东云浮, 6600.00, 2
<== Total: 1
Emp(id=3, name=小明, age=25, addr=广东云浮, salary=6600.0, deptId=null)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Returned connection 527829831 to pool.
Cache Hit Ratio [com.dfbz.dao.EmpDao]: 0.0
Opening JDBC Connection
Checked out connection 527829831 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
==> Preparing: select * from emp where id=?
==> Parameters: 1(Integer)
<== Columns: id, name, age, addr, salary, dept_id
<== Row: 1, 张三, 20, 广西来宾, 7600.00, 1
<== Total: 1
Emp(id=1, name=张三, age=20, addr=广西来宾, salary=7600.0, deptId=null)
Cache Hit Ratio [com.dfbz.dao.EmpDao]: 0.3333333333333333
==> Preparing: select * from emp where name=?
==> Parameters: 小明(String)
<== Columns: id, name, age, addr, salary, dept_id
<== Row: 3, 小明, 25, 广东云浮, 6600.00, 2
<== Total: 1
Emp(id=3, name=小明, age=25, addr=广东云浮, salary=6600.0, deptId=null)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Returned connection 527829831 to pool.
Disconnected from the target VM, address: '127.0.0.1:52207', transport: 'socket'
Process finished with exit code 0
前面我们学习一级缓存的时候,flushCache对于增删改语句是无法设置为false(设置了不生效),即执行任何增删改的时候一定会清空一级缓存,但flushCache却可以控制二级缓存的增删改;
将insert语句的flushCache设置为false(不清空缓存):
@Test
public void test6() throws Exception {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findByName("小明");
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession();
EmpDao mapper2 = session2.getMapper(EmpDao.class);
mapper2.save(new Emp(null,"xxx",null,null,null,null));
Emp emp2 = mapper2.findByName("小明");
System.out.println(emp2);
session2.close();
}
日志如下:
Created connection 527829831.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
==> Preparing: select * from emp where name=?
==> Parameters: 小明(String)
<== Columns: id, name, age, addr, salary, dept_id
<== Row: 3, 小明, 25, 广东云浮, 6600.00, 2
<== Total: 1
Emp(id=3, name=小明, age=25, addr=广东云浮, salary=6600.0, deptId=null)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Returned connection 527829831 to pool.
Opening JDBC Connection
Checked out connection 527829831 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
==> Preparing: insert into emp(id,name) values(null,?);
==> Parameters: xxx(String)
<== Updates: 1
Cache Hit Ratio [com.dfbz.dao.EmpDao]: 0.5
Emp(id=3, name=小明, age=25, addr=广东云浮, salary=6600.0, deptId=null)
Rolling back JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f760b47]
Returned connection 527829831 to pool.
Disconnected from the target VM, address: '127.0.0.1:52373', transport: 'socket'
Process finished with exit code 0
2.4 Redis替换二级缓存
2.4.1 PerpetualCache
MyBatis 跟缓存相关的类都在cache 包里面,其中有一个Cache 接口,只有一个默认的实现类 PerpetualCache,它是用HashMap 实现的。
查看PerpetualCache实现类:
查看Cache接口:
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
public interface Cache {
String getId();
void putObject(Object var1, Object var2);
Object getObject(Object var1);
Object removeObject(Object var1);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() {
return null;
}
}
2.4.2 自定义缓存
我们可以自定义一个类,重写Cache接口,让MyBatis存入二级缓存的数据交给我们来处理!
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
package com.dfbz.cache;
import com.alibaba.fastjson.JSON;
import com.dfbz.entity.Emp;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import java.util.List;
public class MyCache implements Cache {
private Jedis jedis = new Jedis();
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object val) {
jedis.set(key.toString(), JSON.toJSONString(val));
System.out.println("putObject..............");
}
@Override
public Object getObject(Object key) {
System.out.println("getObject................");
String jsonStr = jedis.get(key.toString());
List<Emp> list = JSON.parseArray(jsonStr, Emp.class);
return list;
}
@Override
public Object removeObject(Object key) {
System.out.println("removeObject................");
return jedis.del(key.toString());
}
@Override
public void clear() {
System.out.println("clear................");
jedis.flushDB();
}
@Override
public int getSize() {
System.out.println("getSize................");
return Integer.parseInt(jedis.dbSize() + "");
}
}
<?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.dfbz.dao.EmpDao">
<cache type="com.dfbz.cache.MyCache"></cache>
<select id="findById" resultType="Emp">
select * from emp where id=#{id}
</select>
</mapper>
2.4.3 测试Redis替代二级缓存
package com.dfbz.test;
import com.dfbz.dao.EmpDao;
import com.dfbz.entity.Emp;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
public class Demo03_自定义缓存 {
private SqlSessionFactoryBuilder builder;
private SqlSessionFactory factory;
@Before
public void before() throws IOException {
builder = new SqlSessionFactoryBuilder();
factory = builder.build(Resources.getResourceAsStream("SqlMapConfig.xml"));
}
@Test
public void test1() {
SqlSession session = factory.openSession();
EmpDao mapper = session.getMapper(EmpDao.class);
Emp emp = mapper.findById(1);
System.out.println(emp);
session.close();
SqlSession session2 = factory.openSession();
EmpDao mapper2 = session2.getMapper(EmpDao.class);
Emp emp2 = mapper2.findById(1);
System.out.println(emp2);
session.close();
}
}
记得点赞~
|