MyBatis----05----缓存&&配置进阶

MyBatis----05----缓存&&配置进阶

1. MyBatis中的缓存机制

1.1 MyBatis中的一级缓存

1.1.1 什么是一级缓存

      一级缓存用于提高查询效率。
      我们使用MyBatis执行查询时,当我们的多次查询内容完全一致,如果MyBatis不采取一些措施的话,将会导致每次查询都查询一次数据库,如果我们在极短的时间内做了完全相同的查询,并且查询结果也完全相同,将会导致浪费大量的数据库资源。
      为了解决这个问题,MyBatis会在SqlSession对象中创建一个本地缓(local catch)存对象,每次查询都会先从本地缓存中查询,如果本地缓存命中,直接返回本地缓存中的结果,如果缓存没有命中,那么从数据库中搜寻,将数据库的查询结果放入本地缓存并返回。
      一级缓存是默认打开,强制使用,无法关闭。
在这里插入图片描述

1.1.2 一级缓存的组织架构

在这里插入图片描述
查看PerpetualCache源码

public class PerpetualCache implements Cache {
    private final String id;
    //实现一级缓存使用HashMap
    private Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }
	//获得缓存数量
    public int getSize() {
        return this.cache.size();
    }
	//存入缓存
    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }
	//从缓存中取值
    public Object getObject(Object key) {
        return this.cache.get(key);
    }
	//移除某个缓存
    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }
	//清楚所有缓存
    public void clear() {
        this.cache.clear();
    }

    public ReadWriteLock getReadWriteLock() {
        return null;
    }

1.1.3 一级缓存的生命周期

演示代码1:

@Test
    public void testFindById(){
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //第一次查询,缓存为命中 => 查询数据库,并放入缓存
        User user1 = userMapper.findById(1);
        //第二次查询,缓存命中 => 返回缓存中的数据
        User user2 = userMapper.findById(1);
        System.out.println(user1);
        System.out.println(user2);
    }

输出结果(控制台):

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8]
==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, tom, 1234
<==      Total: 1
User{id=1, name='tom', password='1234', accounts=null, roles=null}
User{id=1, name='tom', password='1234', accounts=null, roles=null}

演示代码2:

 @Test
    public void testFindById(){
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //第一次查询,缓存为命中 => 查询数据库,并放入缓存
        User user1 = userMapper.findById(1);

        //关闭sqlsession
        session.close();
        //开启一个新的sqlsession
        session = SqlSessionUtils.openSession();
        userMapper = session.getMapper(UserMapper.class);
        //第二次查询,缓存仍然未命中 => 返回缓存中的数据
        User user2 = userMapper.findById(1);
        System.out.println(user1);
        System.out.println(user2);
    }

输出结果(控制台)

Created connection 1944978632.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8]
==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, tom, 1234
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8]
Returned connection 1944978632 to pool.
Opening JDBC Connection
Checked out connection 1944978632 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8]
==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, tom, 1234
<==      Total: 1
User{id=1, name='tom', password='1234', accounts=null, roles=null}
User{id=1, name='tom', password='1234', accounts=null, roles=null}

演示代码3:

 @Test
    public void testFindById(){
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //第一次查询,缓存为命中 => 查询数据库,并放入缓存
        User user1 = userMapper.findById(1);

        //清空一级缓存
        session.clearCache();

        //第二次查询,缓存仍然为命中 => 返回缓存中的数据
        User user2 = userMapper.findById(1);
        System.out.println(user1);
        System.out.println(user2);
    }

输出结果(控制台)

Created connection 1944978632.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8]
==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, tom, 1234
<==      Total: 1
==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, tom, 1234
<==      Total: 1
User{id=1, name='tom', password='1234', accounts=null, roles=null}
User{id=1, name='tom', password='1234', accounts=null, roles=null}

演示代码4:

 @Test
    public void testFindById(){
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //第一次查询,缓存为命中 => 查询数据库,并放入缓存
        User user1 = userMapper.findById(1);
        //修改操作(update|delete|insert) => 导致缓存被清空
        userMapper.update(1,"汤姆");
        //第二次查询,缓存仍然为命中 => 返回缓存中的数据
        User user2 = userMapper.findById(1);
        System.out.println(user1);
        System.out.println(user2);
    }

输出结果(控制台):

Created connection 1944978632.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73ee04c8]
==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, tom, 1234
<==      Total: 1
==>  Preparing: update t_user set name = ? where id =? 
==> Parameters: 汤姆(String), 1(Integer)
<==    Updates: 1
==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, 汤姆, 1234
<==      Total: 1
User{id=1, name='tom', password='1234', accounts=null, roles=null}
User{id=1, name='汤姆', password='1234', accounts=null, roles=null}

结论:

  • 如果SqlSession调用了Close方法,会释放掉一级缓存,并且整个SqlSession以及一级缓存不再可用;
  • 如果SqlSession调用了cleraCache方法,会清空一级缓存,缓存仍然可用;
  • 如果SqlSession执行了任何一个更新操作(update | delete | insert)都会导致清空以及缓存,缓存仍然可用。

1.1.4 一级缓存中的CacheKey设计

     一级缓存本质就是一个Map,第一次执行查询时会使用本次查询的特征值作为key,查询结果作为value存入一级缓存Map中。
     我们接下来要研究,查询的特征值,也就是key时如何定义的?

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
        	//创建CacheKer对象 -> 存入HashMap中
            CacheKey cacheKey = new CacheKey();
            //1.StatementId
            cacheKey.update(ms.getId());
            //2.rowBounds -> getOffset
            cacheKey.update(rowBounds.getOffset());
            //3.rowBounds -> getLimit
            cacheKey.update(rowBounds.getLimit());
            //4.sql语句
            cacheKey.update(boundSql.getSql());
            //5.sql语句的参数
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
            Iterator var8 = parameterMappings.iterator();

            while(var8.hasNext()) {
                ParameterMapping parameterMapping = (ParameterMapping)var8.next();
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }

                    cacheKey.update(value);
                }
            }

            if (this.configuration.getEnvironment() != null) {
                cacheKey.update(this.configuration.getEnvironment().getId());
            }

            return cacheKey;
        }
    }

CacheKey设计和如下4点相关:

  • 传入的StatementId;
  • 查询时要求的结果集范围(RowBounds对象);
  • Sql语句;
  • Sql参数。

解释上述4点:

  • StatementId:就是我们为查询指定的唯一id,如下"findById"就是StatementId:
		<select id="findById" resultType="User">
        		select * from t_user where id = #{id}
    		</select>
  • RowBounds:MyBatis中的软分页对象,实际开发中并不推荐使用该对象;
  • Sql语句:MyBatis底层实现就是JDBC,传给PrepareStatement对象的Sql语句;
  • Sql参数:MyBatis底层实现就是JDBC,传给PrepareStatement对象的参数;

结论:
调用方法时,是同一个方法,参数也相同的情况下,会使用一级缓存。

1.2 MyBatis中的二级缓存

     二级缓存是进程级别的缓存,属于可选类型缓存,从性质上来说可用可不用,而且不建议使用

1.2.1 什么是二级缓存

在这里插入图片描述

1.2.2 如何让打开二级缓存

  • 在主配置文件中打开二级缓存;
  		<!-- 开启二级缓存 -->
        <setting name="CacheEnable" value="true"/>
  • 在具体要使用二级缓存的Mapper中开启;
<mapper namespace="com.leo.mapper.UserMapper">
    <!-- 表示再UserMapper中使用二级缓存 -->
    <cache/>
  • 要使用二级缓存的实体需要实现Serializable接口。
public class User implements Serializable

1.2.3 演示二级缓存

演示代码:

 @Test
    public void testFindById(){
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //第一次查询,二级缓存为命中 => 查询数据库,并放入二级缓存,或发送sql
        User user1 = userMapper.findById(1);

        //执行commit的操作才会进入二级缓存
        session.commit();
        //关闭sqlsession,对应的一级缓存失效,但是二级缓存仍然有效
        session.close();
        //开启一个新的sqlsession
        session = SqlSessionUtils.openSession();
        userMapper = session.getMapper(UserMapper.class);
        //第二次查询,二级缓存仍存在查询结果 => 返回二级缓存中的数据,不会发送sql
        User user2 = userMapper.findById(1);
        System.out.println(user1);
        System.out.println(user2);
    }

输出结果(控制台):

==>  Preparing: select * from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name, password
<==        Row: 1, 汤姆, 1234
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@272ed83b]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@272ed83b]
Returned connection 657381435 to pool.
Cache Hit Ratio [com.leo.mapper.UserMapper]: 0.5
User{id=1, name='汤姆', password='1234', accounts=null, roles=null}
User{id=1, name='汤姆', password='1234', accounts=null, roles=null}

1.2.4 结论

     二级缓存当涉及多表操作时,如果其他Mapper中的操作影响到了使用二级缓存的Mapper中的数据,将会导致缓存中的数据与数据库中的数据不一致。
在这里插入图片描述

结论:

  • 缓存是以namespace为单位,不同的namespace下的操作不互相影响;
  • 出现update操作(insert | update | delete)会清空namespace下的二级缓存;
  • 多表操作一般不建议使用二级缓存,因为会产生脏数据,可以使用cache-ref缓解;
  • 不建议使用二级缓存,更推荐其他的专业缓存产品,例如:Redis。

2. 配置进阶

2.1 使用Properties引入外部文件

     例如:在项目中,我们希望使用单独的配置文件配置(db.properties)我们的数据库连接信息.
     准备resource/db.properties:

driverClass = com.mysql.cj.jdbc.Driver
jdbcUrl = jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
user = root
password = hanjuechen

     在mybatis-config.xml配置中引入db.properties:

 	<!-- 引入 db.properties -->
    <properties resource="db.properties" />

     在mybatis-config.xml配置中使用db.properties配置的键值对:

 		<dataSource type="POOLED">
                <!-- 四大连接信息配置-->
                <property name="driver" value="${driverClass}"/>
                <property name="url" value="${jdbcUrl}"/>
                <property name="username" value="${user}"/>
                <property name="password" value="${password}"/>
            </dataSource>

2.2 注册Mapper配置文件的三种方式

     方式1:

<mappers>
        <!-- 方式1:分别指定XXXMapper.xml配置文件路径 -->
        <mapper resource="com/leo/mapper/UserMapper.xml"/>
    </mappers>

     方式2:

<!-- 方式2:通过Mapper类指定Mapper.xml配置文件 
                该方式MyBatis会自动从指定类所在包下查询与接口相同的XML
        -->
        <mapper class="com.leo.mapper.UserMapper" />

     方式3:

<!-- 方式3:直接指定Mapper所在的包名,自动扫描包中的所有Mapper配置文件
                该方式MyBatis会先找包中的类,再找类对应的同名XML
         -->
        <package name="com.leo.mapper"/>

2.3 事务管理

     事务管理方式配置:

<!-- 指定事务管理方式 -->
            <transactionManager type="JDBC"/>

     我们使用的是JDBC方式进行管理:
     JDBC事务操作与MyBatis事务操作对应关系:
     开启事务:

//JDBC,关闭自动提交事务
conn.setAutoCommit(false);
//MyBatis
//参数为true,开启自动提交事务
SqlSessionFactory.openSession(true);
//不传参数,关闭自动提交事务
SqlSessionFactory.openSession();

     提交 | 回滚事务:

//JDBC
//提交事务
conn.commit();
//回滚事务
conn.rollBack();
//MyBatis
//提交事务
SqlSession.commit();
//回滚事务
SqlSession.rollBck();

猜你喜欢

转载自blog.csdn.net/weixin_45245455/article/details/108009697