二、mybatis核心应用配置与原理剖析

709 阅读3分钟

一、mybatis一级缓存


知识点

  1. 一级缓存使用场景
  2. 一级缓存的使用条件
  3. 一级缓存源码解析

1、一级缓存使用场景

订单表与会员表是存在一对多的关系,为了尽可能减少join查询,进行了分阶段查询,即先查询出订单表,在根据member_id字段查询出会员表,最后进行数据整合。如果订单表中存在重复的member_id,就会出现很多没必要的重复查询。

针对这种情况myBatis通过一缓存来实现,在同一次查询会话中如果出现相同的语句及参数,就会从缓存中取出不在走数据库查询。一级缓存只能作用于查询会话中,所以也叫做会话缓存。

1)数据库设计,新建数据表:t_label

CREATE TABLE `t_label` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO t_label(name)values('青子');
INSERT INTO t_label(name)values('灵棋');
INSERT INTO t_label(name)values('糖糖');

2)新建与数据表对应的pojo:Label

public class Label implements Serializable {

    private static final long serialVersionUID = 5965813391610443954L;

    private Integer id;
    private String name;
    
    //省略setter、getter方法
}

3)创建与之对应的mapper接口

public interface LabelMapper1 {

    @Select("select * from t_label where id=#{id}")
    Label selectLabelById(Integer id);

    @Select("select * from t_label where id=#{id}")
    Label getLabelById(Integer id);

    @Update("update t_label set name=#{name} where id=#{id}")
    Integer updateLabelById(Label label);
}

public interface LabelMapper2 {
    @Select("select * from t_label where id=#{id}")
    Label selectLabelById(Integer id);
}

4)一级缓存测试类

public class LabelTest {

    private SqlSessionFactory sessionFactory1;
    private SqlSession session1;

    @Before
    public void init()throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sessionFactory1 = new SqlSessionFactoryBuilder().build(inputStream);
        session1 = sessionFactory1.openSession(true);
    }

    @Test//同一session同一mapper同一方法相同的sql语句以及参数
    public void test01() throws IOException {
        LabelMapper1 $proxy = session1.getMapper(LabelMapper1.class);
        Label label1 = $proxy.selectLabelById(1);
        Label label2 = $proxy.selectLabelById(1);
        System.out.println(label1 == label2);
    }

    @Test//不同session同一mapper同一方法相同的sql语句以及参数
    public void test02() throws IOException {
        LabelMapper1 $proxy = session1.getMapper(LabelMapper1.class);
        Label label1 = $proxy.selectLabelById(1);
        SqlSession session2 = sessionFactory1.openSession(true);
        Label label2 = session2.getMapper(LabelMapper1.class).selectLabelById(1);
        System.out.println(label1 == label2);
    }

    @Test//同一session不同mapper同一方法相同的sql语句以及相同的参数
    public void test03() throws IOException {
        LabelMapper1 $proxy = session1.getMapper(LabelMapper1.class);
        Label label1 = $proxy.selectLabelById(1);
        Label label2 = session1.getMapper(LabelMapper2.class).selectLabelById(1);
        System.out.println(label1 == label2);
    }

    @Test//同一session同一mapper不同方法相同的sql语句以及相同的参数
    public void test04() throws IOException {
        LabelMapper1 $proxy = session1.getMapper(LabelMapper1.class);
        Label label1 = $proxy.selectLabelById(1);
        Label label2 = $proxy.getLabelById(1);
        System.out.println(label1 == label2);
    }

    @Test
    public void test05() throws IOException {
        LabelMapper1 $proxy = session1.getMapper(LabelMapper1.class);
        Label label1 = $proxy.selectLabelById(1);
        session1.clearCache();//清空以及缓存
        Label label2 = $proxy.selectLabelById(1);
        System.out.println(label1 == label2);
    }

    @Test
    public void test06() throws IOException {
        LabelMapper1 $proxy = session1.getMapper(LabelMapper1.class);
        Label label1 = $proxy.selectLabelById(1);
        Label label = new Label();
        label.setId(20);
        label.setName("灵棋");
        Integer row = $proxy.updateLabelById(label);
        Label label2 = $proxy.selectLabelById(1);
        System.out.println(label1 == label2);
    }
}

2、一级缓存的使用条件

1)必须是相同的SQL语句和相同的参数
2)必须是相同的会话
3)必须是相同的namespace,即同一个mapper
4)必须是相同的statement,即同一个mapper接口中的同一个方法
5)查询语句中间没有执行session.clearCache()方法
6)查询语句中间没有执行insert、update、delete方法(无论变动记录是否与缓存数据有关系)

3、一级缓存源码解析

一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当session clearCache或close之后,该session中的所有Cache也将清空

1)缓存的获取

> org.apache.ibatis.session.defaults.DefaultSqlSession#selectList()
> org.apache.ibatis.executor.CachingExecutor#query()
> org.apache.ibatis.executor.BaseExecutor#query()
> org.apache.ibatis.cache.impl.PerpetualCache#getObject()

2)缓存的存储

> org.apache.ibatis.session.defaults.DefaultSqlSession#selectList()
> org.apache.ibatis.executor.CachingExecutor#query()
> org.apache.ibatis.executor.BaseExecutor#query()
> org.apache.ibatis.executor.BaseExecutor#queryFromDatabase()
> org.apache.ibatis.cache.impl.PerpetualCache#putObject()

3)通过对clearCache作为入口追踪到一级缓存的实现PerpetualCache

> org.apache.ibatis.session.defaults.DefaultSqlSession#clearCache()
> org.apache.ibatis.executor.CachingExecutor#clearLocalCache()
> org.apache.ibatis.executor.BaseExecutor#clearLocalCache()
> org.apache.ibatis.cache.impl.PerpetualCache#clear()

二、mybatis二级缓存


知识点:

  1. 二级缓存使用场景
  2. CacheNamespace注解或cache标签属性说明
  3. 二级缓存使用条件
  4. 二级缓存清除条件
  5. 二级缓存源码解析

1、二级缓存使用场景

业务系统中存在很多的静态数据,如字典表、菜单表、权限表等,这些数据的特性是不会轻易修改但又是查询的热点数据。一级缓存针对的是同一个会话当中相同SQL,并不适合这情热点数据的缓存场景。为了解决这个问题引入了二级缓存,它脱离于会话之外。

1)数据库设计,新建数据表:t_user

CREATE TABLE `t_user` (
  `id` tinyint(4) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2)新建与数据表对应的pojo:User

public class User implements Serializable {

    private static final long serialVersionUID = 6924477354525285670L;

    private Integer id;
    private String name;
    private Date createTime;
    private Date updateTime;
    
    //省略setter、getter方法
}

3)创建与之对应的mapper接口

@CacheNamespace(readWrite = true)//深拷贝
public interface UserMapper1 {

    @Select("select * from t_user where id = #{id}")
    User selectUserById(Integer id);

    @Select("select * from t_user where id = #{id}")
    User getUserById(Integer id);

    @Update("update t_user set name = #{name},update_time = #{updateTime} where id = #{id}")
    Integer updateUserById(User user);

    Integer modifyUserById(User user);

}

@CacheNamespace()
public interface UserMapper2 {

    @Select("select * from t_user where id = #{id}")
    User selectUserById(Integer id);

}

4)创建与之对应的mapper.xml文件

<update id="modifyUserById" parameterType="com.cyan.pojo.User">
    update t_user set name = #{name},update_time = #{updateTime} where id = #{id}
</update>

5)二级缓存测试类

public class UserTest {

    private SqlSessionFactory sessionFactory1;
    private SqlSession session1;

    @Before
    public void init()throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sessionFactory1 = new SqlSessionFactoryBuilder().build(inputStream);
        session1 = sessionFactory1.openSession(true);
    }

    @Test//不同session同一mapper同一方法相同的sql语句以及参数,pojo必须序列化
    public void test01() throws IOException {
        UserMapper1 $proxy = session1.getMapper(UserMapper1.class);
        User user1 = $proxy.selectUserById(1);
        session1.close();//会话关闭之后才放入二级缓存中
        SqlSession session2 = sessionFactory1.openSession(true);
        User user2 = session2.getMapper(UserMapper1.class).selectUserById(1);
        System.out.println(user1 == user2);//false深拷贝
    }

    @Test//不同session不同mapper同一方法相同的sql语句以及参数,pojo必须序列化
    public void test02() throws IOException {
        UserMapper1 $proxy = session1.getMapper(UserMapper1.class);
        User user1 = $proxy.selectUserById(1);
        session1.close();
        SqlSession session2 = sessionFactory1.openSession(true);
        User user2 = session2.getMapper(UserMapper2.class).selectUserById(1);
        System.out.println(user1 == user2);
    }

    @Test//不同session同一mapper不同方法相同的sql语句以及参数,pojo必须序列化
    public void test03() throws IOException {
        UserMapper1 $proxy = session1.getMapper(UserMapper1.class);
        User user1 = $proxy.selectUserById(1);
        session1.close();
        SqlSession session2 = sessionFactory1.openSession(true);
        User user2 = session2.getMapper(UserMapper1.class).getUserById(1);
        System.out.println(user1 == user2);
    }
    
    @Test
    public void test04() throws IOException {
        UserMapper1 $proxy = session1.getMapper(UserMapper1.class);
        User user1 = $proxy.selectUserById(1);
        session1.close();

        User user = new User();
        user.setId(2);
        user.setName("灵棋");
        user.setUpdateTime(new Date());
        SqlSession session2 = sessionFactory1.openSession(true);
        session2.getMapper(UserMapper1.class).updateUserById(user);
        session2.close();

        SqlSession session3 = sessionFactory1.openSession(true);
        User user2 = session3.getMapper(UserMapper1.class).selectUserById(1);
        System.out.println(user1 == user2);
    }

    @Test
    public void test05() throws IOException {
        UserMapper1 $proxy = session1.getMapper(UserMapper1.class);
        User user1 = $proxy.selectUserById(1);
        session1.close();

        User user = new User();
        user.setId(2);
        user.setName("灵棋");
        user.setUpdateTime(new Date());
        SqlSession session2 = sessionFactory1.openSession(true);
        session2.getMapper(UserMapper1.class).modifyUserById(user);
        session2.close();

        SqlSession session3 = sessionFactory1.openSession(true);
        User user2 = session3.getMapper(UserMapper1.class).selectUserById(1);
        System.out.println(user1 == user2);
    }
}

2、CacheNamespace注解或cache标签属性说明

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CacheNamespace {
    //缓存Cache的实现类
    Class<? extends Cache> implementation() default PerpetualCache.class;

    //缓存算法
    Class<? extends Cache> eviction() default LruCache.class;

    //刷新间隔时间,单位毫秒
    long flushInterval() default 3600000L;

    //最大缓存引用对象
    int size() default 1000;

    //是否可写
    boolean readWrite() default true;
}

3、二级缓存使用条件

1.当会话提交或关闭之后才会填充二级缓存
2.必须是在同一个命名空间之下
3.必须是相同的namespace即同一个mapper
4.必须是相同的statement即同一个mapper接口中的同一个方法
5.必须是相同的SQL语句和参数
6.如果readWrite=true,实体对像必须实现Serializable接口

4、二级缓存清除条件

1.xml中配置的update不能清空@CacheNamespace 中的缓存数据(非本cacheNamespace的update操作不能清空本cacheNamespace中的缓存数据)
2.只有修改会话提交之后才会执行清空操作
3.任何一种增删改操作都会清空整个namespace 中的缓存

5、二级缓存源码解析

二级缓存与一级缓存机制相同,默认也是采用PerpetualCache,本地HashMap存储,不同在于其存储作用域为Mapper(Namespace)

1)清除缓存

xml中配置的update不能清空@CacheNamespace 中的缓存数据
只有修改会话提交之后,才会执行清空操作
任何一种增删改操作,都会清空整个namespace中的缓存

2)缓存源码解析

清除缓存

> org.apache.ibatis.session.defaults.DefaultSqlSession#selectList()
> org.apache.ibatis.executor.CachingExecutor#query()
> org.apache.ibatis.executor.CachingExecutor#query()
> org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired()

获取缓存关键源码(装饰者设计模式:职责分离原则)

> org.apache.ibatis.cache.TransactionalCacheManager#getObject()
> org.apache.ibatis.cache.decorators.TransactionalCache#getObject()
> org.apache.ibatis.cache.decorators.SynchronizedCache#getObject()
> org.apache.ibatis.cache.decorators.LoggingCache#getObject()
> org.apache.ibatis.cache.decorators.SerializedCache#getObject()
> org.apache.ibatis.cache.decorators.ScheduledCache#getObject()
> org.apache.ibatis.cache.decorators.LruCache#getObject()
> org.apache.ibatis.cache.impl.PerpetualCache#getObject()

保存二级缓存

> org.apache.ibatis.executor.CachingExecutor#close()
> org.apache.ibatis.cache.TransactionalCacheManager#commit()
> org.apache.ibatis.cache.decorators.TransactionalCache#flushPendingEntries()
> org.apache.ibatis.cache.decorators.SynchronizedCache#putObject()
> org.apache.ibatis.cache.decorators.LoggingCache#putObject()
> org.apache.ibatis.cache.decorators.SerializedCache#putObject()
> org.apache.ibatis.cache.decorators.ScheduledCache#putObject()
> org.apache.ibatis.cache.decorators.LruCache#putObject()
> org.apache.ibatis.cache.impl.PerpetualCache#putObject()