After a day of research, I finally figured out the first-level cache and second-level cache of MyBatis

 introduce

MyBatis is now a must-have question in interviews. The main one is the cache problem, except for some startup processes and basic syntax.

Everyone knows that MyBatis has a second-level cache. The bottom layer is implemented with HashMap. The key is the CacheKey object (the reason will be explained later), and the value is the value found from the database , among which:

  • The first level cache is enabled by default;
  • L2 cache is to be manually configured to open.

However, due to some disadvantages of the second-level cache, it is not recommended to use it in actual production (there is a dirty read problem), but to implement your own cache externally, such as using a more mature and reliable Redis.

There have been many blog introductions about this part. Standing on the shoulders of the predecessors and adding my own understanding in simple language, I would like to share my experience with you:


1. Cache classification

In order to verify the effect of MyBatis first-level cache, we create an instance table - studunt, and create the corresponding POJO class and the method of adding and changing, which can be viewed in the entity package and mapper package (omitted here).

Note, please restore the modified data after each unit test below.

CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `age` tinyint(3) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

1. Level 1 cache

The life cycle of the first-level cache is the same as that of SqlSession. If you are not familiar with SqlSession, you can compare it to Connection in JDBC programming, that is, a session of the database.

1.1 Source code analysis:

Let's start directly with the SqlSession interface to see if there is any cache or cache-related properties or methods. After analyzing it, you may get such a method call flow:

insert image description here

To understand the cache, you must understand the Executor. What does this Executor do? You can understand that the SQL to be executed will go through the methods of this class, and StatementHandler is called in the methods of this class to finally execute the SQL

The implementation of Executor is a typical decorator pattern. The UML diagram is as follows:

insert image description here

 I believe you have seen that SimpleExecutor and BatchExecutor are specific component implementation classes, while CachingExecutor is a specific decorator. It can be seen that the specific component implementation class has a parent class BaseExecutor, and this parent class is a typical application of the template pattern. The operations of operating the first-level cache are all in this class, and the specific functions of operating the database are implemented by subclasses. .

At this point, I finally figured out that all the operations of the first-level cache are in the BaseExecutor class. Let's take a look at the specific operations:

1.1 .1 The bottom layer uses HashMap

After the process goes to the clear() method in Perpetualcache, it will call its cache.clear() method, click in and find that the cache is actually private Map cache = new HashMap(); that is, a Map. This Map stores the first-level cache data, the key is the CacheKey object (the reason will be explained later), and the value is the value found from the database.

 1.1.2 query()

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

When the select operation is performed, a CacheKey will be generated first, and if the value can be obtained from the HashMap according to the CacheKey, it will be put back.

1.1.3 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);
    }

When the update operation is performed, you can see that the clearLocalCache() method will be called, and this method will clear the first-level cache, that is, clear the HashMap.

1.2 Experimental verification:

Next, we use a few small experiments to verify the conjecture we have drawn above. In the experiment, the name of the student whose id is 1 is [Karen].

1.2.1 Experiment 1: Open the first level cache, the scope is session level, and call getStudentById() twice

    @Test
    public void test_level_1_cache() throws Exception {
        // 1.读取数据库配置文件,加载入内存
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        // 2.对加载入内存的配置内容进行构建、解析
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 3.根据 sqlSessionFactory 产生 session
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 4.验证
         StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 4.1 从第一次查询,发出sql语句,并将查询出来的结果放进缓存中
        System.out.println(studentMapper.getStudentById(1));
        // 4.2 第二次、三次查询,由于是同一个sqlSession,会在缓存中查询结果
        //     如果有,则直接从缓存中取出来,不和数据库进行交互
        System.out.println(studentMapper.getStudentById(1));
        sqlSession.close();
    }

Results of the:

insert image description here

 We can see that only the first time the database is actually queried, and subsequent queries do not have direct interaction with the database, but use the first-level cache.

1.2.2 Experiment 2: The studunt table is also queried twice, but a modification operation to the database is added between the two queries

    @Test
    public void test_level_1_cache() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 4.验证
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 4.1 第一次查询
        System.out.println(studentMapper.getStudentById(1));
        // 4.2 新增一条数据
        System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
        // 4.3 第二次查询
        System.out.println(studentMapper.getStudentById(1));
        sqlSession.close();
    }

Results of the:

insert image description here

 We can see that the same query executed after the modification operation all queries the database, and the first-level cache is invalid.

1.2.3 Experiment 3: Open two SqlSessions to verify that the first-level cache is only shared within its sessions

    @Test
    public void testLocalCacheScope() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);

        StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        // 4. 验证
	    System.out.println("studentMapper1读取数据: " + studentMapper1.getStudentById(1));
	    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
	    System.out.println("studentMapper2更新数据" + studentMapper2.updateStudentName("小明",1));
	    System.out.println("studentMapper1读取数据: " + studentMapper1.getStudentById(1));
	    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }

2. L2 cache

As mentioned earlier, the implementation of the first-level cache is in BaseExecutor, and the implementation of the second-level cache is in CachingExecutor.

The following is a detailed introduction:

insert image description here

 The principle of the second-level cache is the same as that of the first-level cache: the first query will put the data in the cache, and then the second query will go directly to the cache to get it.

However, the first-level cache is based on sqlSession, and the second-level cache is based on the namespace of the mapper file, which means that multiple sqlSessions can share the second-level cache area in a mapper, and if the namespaces of the two mappers are the same, even if the two mappers have the same namespace. If there are two mappers, the data obtained by executing SQL queries in these two mappers will also be stored in the same second-level cache area.

2.1 Enable L2 cache

Step 1: mybatis-config.xml

<settings>
	<setting name="cacheEnabled" value="true"/>
</settings>

This is the master switch of the second-level cache. Only when this configuration item is set to true, the configuration of the latter two items will take effect.

As can be seen from the newExecutor method of the Configuration class, when cacheEnabled is true, use the cache decorator to decorate the specific component implementation class, so that the second-level cache takes effect.

Step 2: mapperr.xml mapping file

If any of the <cache> and <cache-ref> tags are configured in the mapper.xml mapping file, it means that the second-level cache function is enabled. If it is not, it means that it is not enabled.

<cache type="" eviction="FIFO" size="512"></cache>

Part of the configuration of the second-level cache is as above. The PerpetualCache class is the class that mybatis implements the cache function by default. type is to fill in a full class name. We can use the default cache of mybatis without writing it, or we can implement the Cache interface to customize the cache.

The second-level cache is represented by Cache, and the first-level cache is represented by HashMap. This means that you can provide the implementation class of the second-level cache yourself, and you do not have to use the default HashMap (the second-level cache is implemented with HashMap by default). This is why Mybatis can be integrated with Redis and Memcache.

Attribute description, eviction represents the cache clearing strategy, the options are as follows:

 It can be seen that changing the cache clearing strategy in Mybatis is to change the decorator. Also, if the interviewer asks you to write a FIFO algorithm or an LRU algorithm, isn't this a ready-made implementation?

2.2 Experimental verification

2.2.1 Experiment 1: When submitting a transaction, after sqlSession1 queries the data, the same query in sqlSession2 will obtain data from the cache

    @Test
    public void testCacheWithCommitOrClose() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
        
        StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper1读取数据: " + studentMappe1r.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }

Results of the:

insert image description here

We can see that the query of sqlsession2 uses the cache, and the cache hit rate is 0.5.

2.2.2 Experiment 2: The update operation will refresh the second-level cache under the namespace

    @Test
    public void testCacheWithUpdate() throws Exception {
        // 1 ~ 2 见上
        // 3.根据 sqlSessionFactory 产生 session,自动提交事务
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true); 
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true); 
        SqlSession sqlSession3 = sqlSessionFactory.openSession(true); 
        
        StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);
        
        System.out.println("studentMapper1读取数据: " + studentMapper1.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
        
        studentMapper3.updateStudentName("方方",1);
        sqlSession3.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }

Results of the:

insert image description here

We can see that after sqlSession3 updates the database and submits the transaction, the queries under the StudentMapper namespace of sqlsession2 go to the database but not to the Cache. 


Summarize

  • Compared with the first-level cache, the second-level cache of MyBatis realizes the sharing of cached data between SqlSessions;
  • When MyBatis queries multiple tables, it is very likely that dirty data will appear, there are design flaws, and the conditions for safely using the second-level cache are harsh;
  • In a distributed environment, since the default MyBatis Cache implementation is based on local, dirty data will inevitably be read in a distributed environment. It is necessary to use a centralized cache to implement the MyBatis Cache interface, which has a certain development cost. Using distributed caches such as Redis, Memcached, etc. may be less expensive and more secure.

1. About the first level cache

  • MyBatis "turns on" the first level cache by default;
  • The bottom layer of the first-level cache is actually a local memory cache based on HashMap;
  • The scope of the first-level cache is session (session), and the cache is emptied when the session is closed or refreshed;
  • The caches between different sqlsessions are independent of each other and do not affect each other.

2. About the second level cache

  • MyBatis "doesn't open" the second level cache by default, we need to manually open it;
  • The second-level cache is a mapper-level cache;
  • All operation statements under the same namespace affect the same Cache, that is, the second-level cache is shared by multiple SqlSessions and is a global variable.

3. Recommendations

  • In production, it is recommended to use the default configuration of MyBatis, that is, the first level cache is enabled by default, and the second level cache is disabled by default, so that MyBatis can only complete the operations within the ORM framework (such as database object mapping, CRUD, etc.);
  • Hand over the global cache to third-party plugins, such as redis, mamcache, etc.

Guess you like

Origin blog.csdn.net/weixin_44259720/article/details/122058116