Chapter 4. Mybatis Caching Mechanism

Posted by swebajen on Sat, 14 Sep 2019 08:10:51 +0200

Article directory

What is caching?

Caching is an object in memory, which is used to save the query results of database, to reduce the number of interactions with the database, thereby reducing the pressure of the database, and thus improving the response speed.

What is the cache in Mybatis?

The cache in Mybatis means that after Mybatis executes an SQL query or an SQL update, the SQL statement will not disappear, but

Being cached by Mybatis, when the same SQL statement is executed again, it will be extracted directly from the cache instead of executing the SQL command again.

1. Brief introduction of caching mechanism

(1) MyBatis contains a very powerful query caching feature that can be easily configured and customized. Caching can greatly improve query efficiency

(2) Two-level cache is defined by default in MyBatis system

Level 1 cache: SqlSession level cache, also known as local cache.

Second-level caching: namespace-based caching, also known as table-level caching. Global scope

(3) By default, only one level cache (SqlSession level cache, also known as local cache) is opened.

What is Sqlsession?

Sqlsession is a session object created by the Sqlsession Factory, which is used to execute specific SQL statements and return the results of user requests.

What does Sqlsession level caching mean? Statement caching, also known as session caching

Sqlsession level caching means that every time an SQL statement is executed, it defaults to the SQL.

(4) The secondary cache needs to be opened and configured manually. It is based on the namespace level of the cache.

(5) In order to improve scalability. == MyBatis defines the cache interface Cache. == We can customize the secondary cache by implementing the Cache interface

(6) Cache lookup order: secondary cache - > primary cache - > Database

2. Level 1 Cache

2.1. Use of Level 1 Cache

(1) Local Cache, that is, local Cache, whose scope is sqlSession by default. When Session flush or close, all Caches in the Session will be emptied.

(2) The local cache cannot be closed, but clearCache() can be called to empty the local cache or to change the scope of the cache.

(3) After mybatis 3.1, you can configure the scope of the local cache. In mybatis.xml, you can configure the scope of the local cache.

(4) The working mechanism of the first level cache (working principle)

All data queried during the same session will be stored in a Map of the current SqlSession

​ key:

hashCode + Query SqlId + sql Query Statement + Parameters

(5) Benefits of Level 1 Caching

In the running process of the application, it is possible to execute SQL with exactly the same conditions for multiple insertions in a database session.

Solution:

Mybatis provides a first-level cache solution to optimize this part of the scenario. If the same SQL statement is used, the first-level cache will be hit first, avoiding direct queries to the database and improving performance.

2.2. Several Cases of First-level Cache Failure

(1) Different SqlSession s correspond to different first-level caches

Code example:

@Test
	public void testSelectMul() throws IOException {
		SqlSession sqlSession = getSqlSession();//The first sqlSession
		sqlSession.commit(true);
		
		EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee = employeeMapper.getEmployeeById(3);
		System.out.println(employee);
		
		SqlSession sqlSession2 = getSqlSession();//Second sqlSession
		sqlSession2.commit(true);
		EmployeeMapper employeeMapper2 = sqlSession2.getMapper(EmployeeMapper.class);
		Employee employee2 = employeeMapper2.getEmployeeById(3);
		System.out.println(employee2);
		
	}
(2) The same SqlSession but different query conditions

Code example:

@Test
	public void testSelectMul() throws IOException {
		SqlSession sqlSession = getSqlSession();//There is only one Sqlsession
		sqlSession.commit(true);
		
		EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee = employeeMapper.getEmployeeById(3);//Check 3
		System.out.println(employee);
		
		
		EmployeeMapper employeeMapper2 = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee2 = employeeMapper2.getEmployeeById(4);//Check 4
		System.out.println(employee2);
		
	}
(3) During the same SqlSession two queries, any addition, deletion and modification operations were performed.

Code example:

@Test
public void testSelectMul() throws IOException {
    SqlSession sqlSession = getSqlSession();//There is only one Sqlsession
    sqlSession.commit(true);

    EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee = employeeMapper.getEmployeeById(3);
    System.out.println(employee);
	employeeMapper.updateEmployee(new Employee(null,"wanwu",null,null));//Modifications were made

    EmployeeMapper employeeMapper2 = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee2 = employeeMapper2.getEmployeeById(3);
    System.out.println(employee2);

}
(4) Manually empty the cache during two queries of the same SqlSession

Code example:

@Test
	public void testSelectMul() throws IOException {
		SqlSession sqlSession = getSqlSession();//There is only one Sqlsession
		sqlSession.commit(true);
		
		EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee = employeeMapper.getEmployeeById(3);
		System.out.println(employee);
		sqlSession.clearCache();//Manual Clearance of Cache
		
		EmployeeMapper employeeMapper2 = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee2 = employeeMapper2.getEmployeeById(3);
		System.out.println(employee2);
		
	}

2.3. The Execution Process of Level 1 Cache

When it comes to first-level caching, SqlSession can't be bypassed when it comes to first-level caching, so we can directly look at SqlSession to see if there are caching or caching-related attributes or methods.

After a round of research, it seems that only clearCache() is associated with cache contamination in all the methods mentioned above.

Let's start with this method. When we analyze the source code, we need to see who it is and who its parent and child classes are. If we understand the above relationship, you will have a better understanding of this class and make a circle of analysis.

The following flow chart is obtained:

Write a piece of code:

//Method in Interface
//Query individual employee information based on employee id
Employee getEmployeeById(Integer id );
<!--SQL Mapping file-->

<select id="getEmployeeById" resultType="Employee">
  	select id,last_name,gender,descr from tbl_employee where id=#{id}
</select>
private SqlSession getSqlSession() throws IOException {
    String resource = "mybatis-conf.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sessionFactory.openSession();
    return sqlSession;
}
	
	
@Test
public void testSelectMul() throws IOException {
    SqlSession sqlSession = getSqlSession();
    sqlSession.commit(true);

    EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee = employeeMapper.getEmployeeById(3);//You can set breakpoints here for debug debugging
    System.out.println(employee);

}
	

After debug runs:

Step 1: First run to the clear() method in Perpetualcache and call its cache.clear() method

Cache is actually a private Map < Object, Object > cache = new HashMap < Object, Object >(); that is, a Map, so cache.clear() is actually a map.clear(), that is to say, a cache is actually a map object stored locally, and every SqlSession stores a reference to a map object.

The second step:

After running the clear () method in the first step, press F6 to execute the next step and go to the ** clearLocalCache()** method below:

So when was this cache created?

I think it's Executor. Why do you think so? Because Executor is an executor to execute SQL requests, and the method to clear caches is also executed in Executor, it is likely that the creation of caches is also possible in Executor. Look around and find that there is a createCacheKey method in Executor. This method is very similar to the method of creating caches. Follow up and see. Look, you find that the createCacheKey method is executed by BaseExecutor with the following code

 @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
//id of MappedStatement
// id is the package name + class name + SQL name where the Sql statement is located
    cacheKey.update(ms.getId());
// offset is zero
    cacheKey.update(rowBounds.getOffset());
// limit is Integer.MAXVALUE
    cacheKey.update(rowBounds.getLimit());
// Specific SQL statements
    cacheKey.update(boundSql.getSql());
//Later, update takes the parameters in sql
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        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 = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

Creating a cache key passes through a series of update methods, which are executed by an object called CacheKey. The update method is ultimately stored in a list of updateList s. Comparing the above code with the following illustrations, you can understand what these five values are.

Here you need to note the last value, configuration.getEnvironment().getId(), which is actually the label defined in mybatis-config.xml, as shown below.

<!-- Environmental configuration -->
	<environments default="xxj">
		<environment id="xxj">
			<transactionManager type="JDBC"/>
			
			<dataSource type="POOLED">
				<property name="driver" value="${jdbc.driver}"/>
				<property name="url" value="${jdbc.url}"/>
				<property name="username" value="${jdbc.user}"/>
				<property name="password" value="${jdbc.password}"/>
			</dataSource>
		
		</environment>
	
	</environments>

So let's get back to the point, where should we use the cache after it's created? You don't create a cache out of nothing, do you? Absolutely not. After exploring the first-level cache, we find that the first-level cache is more used for query operations. After all, the first-level cache is also called query cache. Why is it called query cache? Let's first look at where this cache is actually used, and we trace the query method as follows:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  // Create cache
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ...
  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  if (list != null) {
      // This is mainly for processing stored procedures.
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }
  ...
}

// QueyFromDatabase method
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key);
  }
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

If not, check from the database and write to the local cache in queryFromDatabase. The put method of the localcache object is finally handed over to Map for storage.

private Map<Object, Object> cache = new HashMap<Object, Object>();

@Override
public void putObject(Object key, Object value) {
  cache.put(key, value);
}

2.4. Why is the first level cache called query cache?

Let's first look at the update method, and first look at the source code of update.

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  clearLocalCache();
  return doUpdate(ms, parameter);
}

By BaseExecutor, clearLocalCache() is the first step in every update method execution, so the update method does not have a cache, which is why the first level cache is also called query cache, which is why we have not explored the impact of multiple update methods on the first level cache.

2.5. Why does the first level cache fail?

  1. Explore the impact of updates on first-level cache failure: From the above analysis, we can see that every time we implement the update method, we refresh the first-level cache first, because the same SqlSession is stored by the same Map, so the first-level cache will fail at this time.

  2. Explore the impact of different SqlSessions on first-level caching: This is understandable, because different SqlSessions have different Map storage level caches, but SqlSessions do not share with each other, so there is no same first-level caching at this time.

  3. The same SqlSession uses different query operations: This argument needs to be addressed from the perspective of cache composition. We know from cacheKey that the necessary condition for a first-level cache hit is that two cacheKeys are the same. To make the cacheKey the same, we need to make the values in the cacheKey the same, that is, to make the cacheKey the same.

Do you see the difference? The first SQL we query is the value of department number 1, while the second SQL we query is the value of number 5. The two cache objects are different, so there is no cache.

  1. The impact of manual cleaning of caches on first-level caches: programmers themselves call the clearCache method, which is the way to clear caches, so there is no cache.

3. Level 2 Cache

3.1. Use of Level 2 Cache

(1) Second level cache, global scope cache
Based on namespace level caching, multiple SqlSession objects based on the same namespace share the same secondary cache
(2) The secondary cache is not opened by default and needs to be configured manually.
(3) MyBatis provides the interface and implementation of secondary caching, which requires POJO to implement Serializable interface.
(4) The steps of using the secondary cache:
(1) Open secondary cache in global configuration file
(2) Configure caching using cache at mapping files requiring secondary caching
(3) Note: POJO needs to implement Serializable interface
(5) The secondary cache will not take effect until the SqlSession is closed or submitted.
Based on multiple SqlSession objects of the same namespace, the first query is first searched from the cache, if not found, then from the database, the data found from the database is stored in the first level cache of the current SqlSession object, only when the SqlSession object is closed or submitted. Store the data in the first level cache in the second level cache

(6) Second-level cache-related attributes
eviction= "FIFO": Cache Recycling Strategy:
LRU - The least recently used: Remove objects that have not been used for the longest time.
FIFO - First in, first out: Remove objects in the order they enter the cache.
SOFT - Soft Reference: Remove objects based on garbage collector status and soft reference rules.
WEAK - Weak Reference: More aggressive removal of objects based on garbage collector state and weak reference rules.
The default is LRU.
Flush Interval: refresh (empty cache) interval, in milliseconds
By default, there is no refresh interval, and the cache refreshes only when the statement is invoked.
(3) size: number of references, positive integer
How many objects can be stored on behalf of the cache, which is too large to cause memory overflow
readOnly: read-only, true/false
true: read-only caching; the same instance (reference) of the cached object is returned to all callers. Therefore, these objects cannot be modified. This provides important performance advantages.
False: read-write cache; returns a copy of the cached object (by serialization). This is slower, but safe, so the default is false.
_type: A specified cache class, usually set when customizing or integrating third-party caches

3.2. Workflow of Level 2 Cache

The largest share scope of MyBatis first-level cache is within one SqlSession. If multiple SqlSessions need to share the cache, they need to open the second-level cache. After opening the second-level cache, they decorate the Executor with Caching Executor. Before entering the query process of the first-level cache, they first slow down the second-level cache in Caching Executor. The specific workflow of the stored query is as follows

When the secondary cache is opened, all operation statements in the same namespace affect a common cache, that is, the secondary cache is shared by multiple SqlSession s, and is a global variable. When the cache is turned on, the process of query execution of data is secondary cache - > primary cache - > database.

3.3. Conditions for Second-level Cache Failure

(1) The first SqlSession was not submitted

When SqlSession is not submitted, the query result generated by the SQL statement has not been put into the secondary cache. At this time, SqlSession 2 can not feel the existence of the secondary cache when querying. Modify the corresponding test class. The results are as follows:

@Test
public void testSqlSessionUnCommit(){
  //Send the request for the first time during the session and get the result from the database
  //After the result is obtained, mybatis automatically places the query result into the current user's first-level cache
  DeptDao dao =  sqlSession.getMapper(DeptDao.class);
  Dept dept = dao.findByDeptNo(1);
  System.out.println("The first query gets the Department object = "+dept);
  //Triggering the MyBatis framework to save Dept objects from the current first-level cache to the second-level cache

  SqlSession session2 = factory.openSession();
  DeptDao dao2 = session2.getMapper(DeptDao.class);
  Dept dept2 = dao2.findByDeptNo(1);
  System.out.println("Second query to get department objects = "+dept2);
}

The corresponding output is as follows

(2) The impact of updates on secondary caching

Like first-level cache, update operation is likely to have an impact on second-level cache. Here, three SqlSessions are used to simulate. The first SqlSession is a simple submission, the second SqlSession is used to verify the impact of second-level cache, and the third SqlSession is used to perform update operation. The tests are as follows:

@Test
public void testSqlSessionUpdate(){
  SqlSession sqlSession = factory.openSession();
  SqlSession sqlSession2 = factory.openSession();
  SqlSession sqlSession3 = factory.openSession();

  // The first SqlSession performs the update operation
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println("dept = " + dept);
  sqlSession.commit();

  // Determine whether the second SqlSession is read from the cache
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(1);
  System.out.println("dept2 = " + dept2);

  // The third SqlSession performs the update operation
  DeptDao deptDao3 = sqlSession3.getMapper(DeptDao.class);
  deptDao3.updateDept(new Dept(1,"ali","hz"));
  sqlSession3.commit();

  // Determine whether the second SqlSession is read from the cache
  dept2 = deptDao2.findByDeptNo(1);
  System.out.println("dept2 = " + dept2);
}

The corresponding output is as follows

3.4. The influence of multi-table operation on secondary cache

In this scenario, there are two tables, Dept (dept No, dname, loc) and
deptNum (id,name,num), where the name of the Department table is the same as the name of the Department table. By looking up the names of the two tables, we can know the coordinates (loc) and the number (num). Now I want to list the number of departments.
Num is updated, and then I query dept and deptNum again. You think this SQL statement can query num.
What's the quantity? Take a look at the code and explore it.

public class DeptNum {

    private int id;
    private String name;
    private int num;

    get and set...
}
public class DeptVo {

    private Integer deptNo;
    private String  dname;
    private String  loc;
    private Integer num;

    public DeptVo(Integer deptNo, String dname, String loc, Integer num) {
        this.deptNo = deptNo;
        this.dname = dname;
        this.loc = loc;
        this.num = num;
    }

    public DeptVo(String dname, Integer num) {
        this.dname = dname;
        this.num = num;
    }

    get and set...
    toString()...
}


public interface DeptDao {

     // ... Other ways
    DeptVo selectByDeptVo(String name);

    DeptVo selectByDeptVoName(String name);

    int updateDeptVoNum(DeptVo deptVo);
}
<select id="selectByDeptVo" resultType="com.mybatis.beans.DeptVo">
  select d.deptno,d.dname,d.loc,dn.num from dept d,deptNum dn where dn.name = d.dname
  and d.dname = #{name}
</select>

<select id="selectByDeptVoName" resultType="com.mybatis.beans.DeptVo">
  select * from deptNum where name = #{name}
</select>

<update id="updateDeptVoNum" parameterType="com.mybatis.beans.DeptVo">
  update deptNum set num = #{num} where name = #{dname}
</update>

DeptNum database initial value:

/**
     * Explore the influence of multi-table operation on secondary cache
     */
@Test
public void testOtherMapper(){

  // The first mapper performs the joint search operation first
  SqlSession sqlSession = factory.openSession();
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  DeptVo deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);
  // The second mapper performs the update operation and submits it
  SqlSession sqlSession2 = factory.openSession();
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  deptDao2.updateDeptVoNum(new DeptVo("ali",1000));
  sqlSession2.commit();
  sqlSession2.close();
  // The first mapper queries again and observes the results of the query
  deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);
}

The test results are as follows:

After performing an update to the DeptNum table, a joint search was conducted again and it was found that the query was still in the database.
num is the value of 1050, that is to say, actually 1050 - > 1000. The last joint query actually queries the cache of the first query result, not the value from the database, so that dirty data can be read.

Solution

If you have two mapper namespaces, you can use them to point one namespace to another, thereby eliminating the above effects. If you execute again, you can query the correct data.

3.5 Level 2 Cache Source Parsing

Creation of Level 2 Cache in 3.5.1

(1) The creation of the secondary cache starts with reading the XML configuration file using Resource

InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
factory = builder.build(is);

(2) After reading the configuration file, you need to create a Configuration for XML and initialize it.

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());

(3) Call parser.parse() to parse the labels under the root directory / configuration, and parse them in turn.

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

(4) The resolution of one of the secondary caches is

mapperElement(root.evalNode("mappers"));

(5) Then go into the mapperElement method

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();

(6) Continue to follow the mapperParser.parse() method

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

(7) One of these is the configurationElement method, which creates a secondary cache, as follows

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

(8) There are two key points of secondary caching

cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));

(9) That is to say, mybatis parses cache-ref tags first, and then cache tags.

According to the solution mentioned in our section on the impact of multi-table operations on secondary caching, using cache-ref to dependence on namespace can avoid secondary caching, but it can't always be used to write an XML configuration every time. The most effective way is to avoid using secondary slowdown for multi-table operations. Deposit

Then let's look at the cacheElement(context.evalNode("cache") method.

private void cacheElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

Look carefully at the analysis of the attributes. Do you feel familiar with it? This is not an analysis of cache tag attributes?!!

The last sentence of the above code

builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

This code uses the builder pattern, builds all the attributes of the Cache tag step by step, and finally returns the cache.

Use of Level 2 Cache in 3.5.2

In mybatis, where Cache is used is in CachingExecutor. Let's look at what caching does in CachingExecutor. Let's take queries as an example.

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
  // Get cache
  Cache cache = ms.getCache();
  if (cache != null) {
    // Refresh the cache if necessary
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, parameterObject, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // Delegation mode, to SimpleExecutor and other implementation classes to implement methods.
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

First, the cache is extracted from MapperStatement. Only through, or @CacheNamespace, @CacheNamespace Ref tags using cached Mapper.xml or Mapper interfaces (the same namespace, can not be used at the same time) will there be a secondary cache.

If the cache is not empty, there is a cache. If a cache exists, it will be determined whether to empty the cache based on the flushCache attribute of the sql configuration (,,).

flushCacheIfRequired(ms);

Then use Cache, the attribute of xml configuration, to determine whether to use caching (the default value commonly used by resultHandler, rarely null).

if (ms.isUseCache() && resultHandler == null)

Make sure that the method does not have parameters of type Out, and mybatis does not support the caching of stored procedures, so if it is a stored procedure, there will be an error.

private void ensureNoOutParams(MappedStatement ms, Object parameter, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
      if (parameterMapping.getMode() != ParameterMode.IN) {
        throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
      }
    }
  }
}

Then, according to the key in the Transactional Cache Manager, the cache is fetched. If there is no cache, the query will be executed, and the query results will be put into the cache and returned. Otherwise, the real query method will be executed.

List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
  list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;

Should Level 2 Cache be used in 3.6?

Should secondary caching be used? First, let's look at the cautions of the secondary cache:

  1. Caching is based on namespace, and operations under different namespaces do not affect each other.

  2. The insert,update,delete operations empty all caches under the namespace.

  3. Usually, the code generated by MyBatis Generator is independent of tables, each table has its own namespace.

  4. Multi-table operations must not use secondary caching, because multi-table operations update operations, will produce dirty data.

    If you follow the precautions of Level 2 Caching, you can use Level 2 Caching.

    However, if multi-table operations cannot be used, can the secondary cache be replaced by the primary cache? Secondary cache is table-level cache, which is expensive. It is more efficient to use HashMap directly without first-level cache, so=== second-level cache is not recommended. = =

4. Cache-related property settings

(1) cacheEnable for global setting s:

Configure the switch for the second level cache, and the first level cache is always open.

(2) The useCache attribute of the select tag:

Configure whether this select uses a secondary cache. First-level caching has always been used

(3) flushCache attribute of sql tag:

Add or delete the default flushCache=true. After sql is executed, both primary and secondary caches are cleared.

Query default flushCache=false.

(4) sqlSession.clearCache(): Used only to clear the first level cache.

5. Integrating Third Party Cache

(1) In order to improve scalability. MyBatis defines the cache interface Cache. We can customize the secondary cache by implementing the Cache interface

(2) EhCache is a pure Java intra-process caching framework, which is fast and compact. It is the default CacheProvider in Hibernate.

(3) The steps of integrating EhCache cache:

Import ehcache packages, as well as integration packages, log packages

​ ehcache-core-2.6.8.jar,mybatis-ehcache-1.0.3.jar

​ slf4j-api-1.6.1.jar,slf4j-log4j12-1.6.2.jar

(2) Write ehcache.xml configuration file

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
 <!-- Disk save path -->
 <diskStore path="D:\atguigu\ehcache" />
 
 <defaultCache 
   maxElementsInMemory="1000" 
   maxElementsOnDisk="10000000"
   eternal="false" 
   overflowToDisk="true" 
   timeToIdleSeconds="120"
   timeToLiveSeconds="120" 
   diskExpiryThreadIntervalSeconds="120"
   memoryStoreEvictionPolicy="LRU">
 </defaultCache>
</ehcache>
 
<!-- 
//Attribute description:
1,diskStore: Specifies where data is stored on disk.
2,defaultCache: With the help of CacheManager.add("demoCache")Establish Cache At that time, EhCache Will adopt<defalutCache/>Designated management policies
 
//The following attributes are necessary:
2.1,maxElementsInMemory - Cached in memory element Maximum number 
2.2,maxElementsOnDisk - Cached on disk element The maximum number, if 0, is infinite.
2.3,eternal - Cached elements Whether it will never expire. If so true,The cached data is always valid if it is false So there's a basis for that. timeToIdleSeconds,timeToLiveSeconds judge
2.4,overflowToDisk - Sets whether the memory cache will expire when it overflows element Cached to disk
 
//The following attributes are optional:
2.5,timeToIdleSeconds - When slow exists EhCache The time of two visits to the data in the timeToIdleSeconds The data is deleted when the value of the attribute is taken, and the default value is 0.,That is to say, the free time is infinite.
2.6,timeToLiveSeconds - cache element Valid lifetime, default is 0.,that is element Living time is infinite
 diskSpoolBufferSizeMB This parameter setting DiskStore(Disk cache)Buffer size of.The default is 30. MB.each Cache You should all have your own buffer.
2.7,diskPersistent - stay VM Whether disk save is enabled when restarting EhCache The default is false. 
l diskExpiryThreadIntervalSeconds - The clearing thread run interval of the disk cache is 120 seconds by default. Each 120 s,The corresponding thread will run once EhCache Cleaning up Data in Chinese
2.8,memoryStoreEvictionPolicy - When the memory cache reaches its maximum, there is a new one element When joining, remove the cache element Strategy. The default is LRU(Minimum recently used), optional LFU(Most infrequently used) and FIFO(First in first out)
 -->

Configuration of cache tags

org.mybatis.caches.ehcache.EhcacheCache

Topics: Mybatis SQL Ehcache Database