MyBatis入门-一级和二级缓存

MyBatis缓存配置

使用缓存的好处是1.更快地获取数据2.避免频繁的数据库交互。

一般提到MyBatis缓存的时候,都是指二级缓存。一级缓存(也叫本地缓存)默认会启用,并且不能控制。

MyBatis全局配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>
    <settings>
        <setting name="logImpl" value="LOG4J"/>
<!--        开启数据库列驼峰转化-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
        <setting name="aggressiveLazyLoading" value="false"/>
        <!--        开启二级缓存;学习一级缓存时请置为false-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC">
                <property name="" value=""/>
            </transactionManager>
            <dataSource type="UNPOOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://192.168.15.79:3306/mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="tk.mybatis.simple.mapper"/>
    </mappers>
</configuration>

一个获取sqlSession的类BaseMapperTest代码:

public class BaseMapperTest {
    public static final Logger LOGGER = LoggerFactory.getLogger(BaseMapperTest.class);

    private static SqlSessionFactory sqlSessionFactory;


    @BeforeClass
    public static void init(){
        try {
            // 1.通过Resource工具类将mybatis-config.xml配置文件读取Reader
            // 2.通过SqlSessionFactoryBuilder建造类使用Reader创建SqlSessionFactory工厂对象
            // 在创建SqlSessionFactory对象的过程中,首先解析mybatis-config.xml配置文件,读取
            // 配置文件中的mappers配置后会读取全部的Mapper.xml进行具体方法的解析,在这些解析完成后
            // ,SqlSessionFactory就包含了所有的属性配置和执行SQL的信息
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public SqlSession getSqlSession(){
        return sqlSessionFactory.openSession();
    }
}

介绍-一级缓存

实践-使用一级缓存

SysUserMapper接口

    /**
     * 通过id查用户
     *
     * @param id 用户id
     * @return 用户
     */
    SysUser queryById(Long id);

SysUserMapper.xml,其中有一个属性为flushCache,默认为false

    <!--查询单个-->
    <select id="queryById" flushCache="false" resultMap="BaseResultMap">
        select
          id, user_name, user_password, user_email, user_info, head_img, create_time
        from mybatis.sys_user
        where id = #{id}
    </select>

测试类

public void myTestL1Cache(){
        //获取sqlSession
        SqlSession sqlSession = getSqlSession();
        // 存储第一个sqlSession中查询出的用户数据,用来和第二个sqlSession查询结果做比较
        SysUser user1 = null;
        try {
            SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
            // 查询id为1的用户
            user1 = userMapper.queryById(1L);
            // 重置用户名
            user1.setUserName("xxxxx");
            // 重新查询,本次查询会调用mybatis的一级缓存
            SysUser user2 = userMapper.queryById(1L);
            // 观察两个user对象的信息
            LOGGER.info("==================第1个sqlSession========================");
            LOGGER.info("user1的信息:{}\nuser2的信息:{}",user1,user2);
        } finally {
            sqlSession.close();
        }

        LOGGER.info("============开启新的sqlSession===================");

        //开始新的session
        sqlSession = getSqlSession();

        try {
            SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
            // 查询id为1的用户
            SysUser user3 = userMapper.queryById(1L);
            // 观察两个user对象的信息
            LOGGER.info("==================第2个sqlSession========================");
            LOGGER.info("user1的信息:{}\nuser3的信息:{}",user1,user3);

            // 执行删除操作
            SysUser condition = new SysUser() {{
                setId(2L);
            }};
            userMapper.deleteById(condition);
            // 获取user4
            SysUser user4 = userMapper.queryById(1L);
            // 观察两个user对象的信息
            LOGGER.info("user3的信息:{}\nuser4的信息:{}",user3,user4);
        } finally {
            sqlSession.close();
        }
    }

测试结果:

1.第一段try finally代码解析:

发现开启新的sqlSession之前只进行了一次查询的sql,代码中调用了两次查询方法。获取user1后更新了userName的值,再获取user2对象后,发现user1和user2仍然是同一个对象,这就是因为mybatis的一级缓存,在同一个sqlSession中调用同样的方法并且参数一致时,只会执行一次sql。

MyBatis的一级缓存存在于sqlSession的生命周期中,同一个sqlSession中查询时,MyBatis会把执行的方法和参数转化生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个sqlSession中执行的方法和参数完全一致,那么通过算法生成的键值也会相同,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。

所以,查询出user1时,MyBatis会将querById和1L生成一个键值,把user1作为值缓存起来;当我们更新user1的userName时,user1这个对象本身的引用没有变;当我们继续调用查询方法querById和参数1L时,生成的键值和上一次查询相同,就去缓存中取对应的值;取出来的值就是之前缓存的user1;所以user1和user2指向的是同一个对象。

2.第二段try finally解析

在关闭第一个sqlSession后,又重新获取了一个sqlSession,因此又重新查询了user2,这时输出了查询的sql,user2是一个新的实例。这是因为一级缓存和sqlSession绑定,只存在于sqlSession的生命周期中。

接下来执行了一个deleteById操作,使用上面同样的查询方法和参数获取了user3实例,发现user3和user2是完全不同的两个对象。这是因为任何的INSERT UPDATE DELETE操作都会清空一级缓存

==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
==================1个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@673be18f
user2的信息:tk.mybatis.simple.model.SysUser@673be18f
============开启新的sqlSession===================
==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
==================2个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@673be18f
user3的信息:tk.mybatis.simple.model.SysUser@38234a38
==>  Preparing: delete from mybatis.sys_user where id = ? 
==> Parameters: 2(Long)
<==    Updates: 0
==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
user3的信息:tk.mybatis.simple.model.SysUser@38234a38
user4的信息:tk.mybatis.simple.model.SysUser@c94fd30

实践-清空一级缓存

如果把select标签中的flushCache属性值改为true时,上面测试方法执行结果如下:发现调用了两次查询的sql,当flushCache改为true时,会在查询数据前清空当前的一级缓存,因此该select方法每次都会从数据库中查询数据,此时的user2和user1就会成为两个不同的实例。

==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
==================1个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@39de3d36
user2的信息:tk.mybatis.simple.model.SysUser@6ce86ce1
============开启新的sqlSession===================
==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
==================2个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@39de3d36
user3的信息:tk.mybatis.simple.model.SysUser@32c726ee
==>  Preparing: delete from mybatis.sys_user where id = ? 
==> Parameters: 2(Long)
<==    Updates: 0
==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
user3的信息:tk.mybatis.simple.model.SysUser@32c726ee
user4的信息:tk.mybatis.simple.model.SysUser@76a2ddf3

总结:

1.一级缓存出现在SqlSession的生命周期中,缓存是以键值对方式存在的,键是执行的方法和参数生成的值,值是sql返回的结果。

2.一级缓存会和SqlSession绑定

3.UPDATE INSERT DELETE方法会清空当前SqlSession中的一级缓存

介绍-二级缓存

二级缓存存在于SqlSessionFactory的生命周期中。当存在多个SqlSessionFactory时,它们的缓存都是绑定在各自对象上的,缓存数据一般情况下不相通。

实践-配置二级缓存

配置在mybatis的全局配置settings中,设置项为<setting name="cacheEnabled" value="true"/>;cacheEnabled默认MyBatis是启用状态。

除了全局配置外,二级缓存需要关联mapper,关联方式可以是关联xml或接口。

1.xml中配置二级缓存

cache标签是和resultMap平级的。

<mapper namespace="tk.mybatis.simple.mapper.SysUserMapper">
    <cache/>
</mapper>

默认的二级缓存有如下效果:

  • 映射语句文件中的所有SELECT语句被缓存
  • 映射语句文件中的所有INSERT UPDATE DELETE语句会清除缓存
  • 缓存使用Least Recently Used(LRU,最近最少使用)算法回收
  • 根据时间表,缓存不会以任何时间顺序来刷新
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用
  • 缓存被视为read/write(可读/写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

这些属性都可以修改,如下:该配置创建了先进先出缓存,每个60秒刷新一次,存储集合或对象的512个引用,而且返回的对象只读。因此在不同线程中的调用者之间修改它们会导致冲突。

    <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>
  • eviction回收策略
    • LRU:移除最长时间不被使用的对象,默认
    • FIFO:按对象进入缓存的顺序来移除,先进先出
    • SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
    • WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象
  • flushInterval刷新间隔:支持任意的正整数,代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新
  • size引用数目:支持任意正整数,默认1024
  • readOnly只读:支持true或false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,性能好。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式慢一些,但是安全,默认为false

2.Mapper接口中配置二级缓存

@CacheNamespace
public interface SysUserMapper {}

@CacheNamespace注解内容如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CacheNamespace {
    Class<? extends Cache> implementation() default PerpetualCache.class;
	//回收策略
    Class<? extends Cache> eviction() default LruCache.class;
	//刷新周期
    long flushInterval() default 0L;
	//默认存储对象或集合引用个数
    int size() default 1024;
	//可读可写
    boolean readWrite() default true;
	
    boolean blocking() default false;
}

@CacheNamespace注解不能与<cache/>同时使用。

可以是@CacheNamespaceRef注解与<cache/>同时使用或@CacheNamespace与与<cache-ref/>同时使用。

例如:

@CacheNamespaceRef(SysUserMapper.class)
public interface SysUserMapper {}

<mapper namespace="tk.mybatis.simple.mapper.SysUserMapper">
    <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>
</mapper>

实践-使用二级缓存

配置可读写的缓存时,MyBatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。

配置为只读缓存,MyBatis就会使用Map来存储缓存值,从缓存中获取的对象是同一个实例。

开启一级和二级缓存,并使用可读写缓存配置

使用可读写缓存,可以使用SerializedCache序列化缓存。这个缓存类要求所有被序列化的对象必须实现Serializable接口。

SysUser

public class SysUser implements Serializable {
    private static final long serialVersionUID = 854674092994608168L;
    ....
}

SysUserMapper

@CacheNamespaceRef(SysUserMapper.class)
public interface SysUserMapper {}

SysUserMapper.xml,注意其中readOnly为false并且select标签中flushCache值为false;它们分别表示可读写缓存和开启一级缓存

<mapper namespace="tk.mybatis.simple.mapper.SysUserMapper">
    <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="false"/>  
<!--查询单个-->
    <select id="queryById" flushCache="false" resultMap="BaseResultMap">
        select
          id, user_name, user_password, user_email, user_info, head_img, create_time
        from mybatis.sys_user
        where id = #{id}
    </select>
</mapper>

测试方法

public void testL2Cache(){
        SqlSession sqlSession = getSqlSession();

        SysUser user1 = null;
        try {
            SysUserMapper mapper = sqlSession.getMapper(SysUserMapper.class);

            user1 =  mapper.queryById(1L);

            user1.setUserName("xxxx");

            SysUser user2 = mapper.queryById(1L);

            LOGGER.info("==================第1个sqlSession========================");
            LOGGER.info("user1的信息:{}\nuser2的信息:{}",user1,user2);
        } finally {
            sqlSession.close();
        }

        sqlSession = getSqlSession();

        try {
            SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
            // 查询id为1的用户
            SysUser user3 = userMapper.queryById(1L);
            // 观察两个user对象的信息,应该会直接从缓存中获取,但是如果是可读写缓存,则不是同一个对象
            LOGGER.info("==================第2个sqlSession========================");
            LOGGER.info("比较第1个sqlSession中查询结果user1与第2个sqlSession查询结果user3");
            LOGGER.info("user1的信息:{}\nuser3的信息:{}",user1,user3);

            // 获取user4
            SysUser user4 = userMapper.queryById(1L);
            // 观察两个user对象的信息,应该会直接从缓存中获取,但是如果是可读写缓存,则不是同一个对象
            LOGGER.info("比较第2个sqlSession中查询结果user3与第2个sqlSession查询结果user4");
            LOGGER.info("user3的信息:{}\nuser4的信息:{}",user3,user4);
        } finally {
            sqlSession.close();
        }
    }

测试结果,我们整个测试方法中有4次调用userMapper.queryById(1L)方法,但是实际执行的sql只有第一次;第一个sqlSession中user1与user2是同一个对象,这是因为开启了一级缓存,获取user2时直接从一级缓存中获取;当第一个sqlSession关闭时,会保存查询的数据到二级缓存中,在这之后二级缓存才有了数据,这也是为什么前两次出现的Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.0中的值都是0.0。第二个sqlSession中user1和user3不是同一个对象,获取user3没有查询sql而是从二级缓存中获取,由于是可读写缓存,通过反序列化出user3,就是一个新的对象了,user3和user4也不是同一个对象,user4也是从二级缓存中获取的,也是一个新的对象。

Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.0
==>  Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<==        Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<==      Total: 1
Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.0
==================1个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@764faa6
user2的信息:tk.mybatis.simple.model.SysUser@764faa6
Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.3333333333333333
==================2个sqlSession========================
比较第1个sqlSession中查询结果user1与第2个sqlSession查询结果user3
user1的信息:tk.mybatis.simple.model.SysUser@764faa6
user3的信息:tk.mybatis.simple.model.SysUser@19e4fcac
Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.5
比较第2个sqlSession中查询结果user3与第2个sqlSession查询结果user4
user3的信息:tk.mybatis.simple.model.SysUser@19e4fcac
user4的信息:tk.mybatis.simple.model.SysUser@52c3cb31

总结:开启二级缓存后,每一次查询都会从二级缓存获取值,如果是第一次调用查询,控制台会输出Cache Hit Ratio [xx]: 0.0,即命中率为0,当本次的sqlSession还未关闭时,继续调用相同的查询,返回的结果通过一级缓存获取数据,控制台也会输出Cache Hit Ratio [xx]: 0.0,即二级缓存中还未有数据。当本次sqlSession结束后,就会把所有查询的数据缓存到二级缓存中;后续的sqlSession中查询时,如果是开启的可读写,则会从缓存中反序列化出查询结果对象,这是一个新指向地址的对象;如果开启的是只读,则获取的对象指向的都是同一个地址。

MyBatis默认提供的缓存实现是基于Map实现的内存缓存,已经可以满足基本的应用。当需要缓存大量数据时,不能只靠MyBatis的二级缓存,可以选择EhCache和Redis来保存二级缓存数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值