参考代码demo4

MyBatis 缓存

MyBatis\text {MyBatis} 提供了两级缓存:一级缓存和二级缓存。

一级缓存

作用范围:一级缓存是会话级别的缓存,它仅在会话(SqlSession)内有效。默认情况下,一级缓存总是被启用的。

工作机制

  1. 查询操作:当执行查询操作时,MyBatis\text {MyBatis} 首先检查一级缓存。
    • 如果请求的数据在缓存中,MyBatis\text {MyBatis} 将从缓存中返回数据,不会执行数据库查询。
    • 如果请求的数据不在缓存中,MyBatis\text {MyBatis} 将执行数据库查询。
  2. 缓存数据:执行数据库查询后,查询结果会被缓存在一级缓存中。
  3. 缓存失效
  • 当当前会话结束时,一级缓存中的所有数据都会被清空。
  • 当在会话中执行任何写操作(insertupdatedelete)时,MyBatis\text {MyBatis} 会自动清空一级缓存,以避免数据不一致的问题。

生命周期:一级缓存的生命周期与SqlSession的生命周期相同。当SqlSession被关闭时,其一级缓存也会被清空。

1
2
3
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>
1
2
3
4
Customer selectCustomerById(@Param("CustomerId") Integer customerId);
int insertCustomer(Customer customer);
int deleteCustomerById(@Param("CustomerId") Integer customerId);
int updateCustomer(Customer customer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<resultMap id="CustomerMap" type="com.liangjiajia.cs.pojo.Customer">
<result property="customerId" column="CustomerID"/>
<result property="customerName" column="CustomerName"/>
<result property="contactName" column="ContactName"/>
<result property="address" column="Address"/>
<result property="city" column="City"/>
<result property="postalCode" column="PostalCode"/>
<result property="country" column="Country"/>
</resultMap>
<select id="selectCustomerById" resultMap="CustomerMap">
SELECT *
FROM customers
WHERE CustomerID = #{CustomerId}
</select>
<insert id="insertCustomer" useGeneratedKeys="true" keyProperty="customerId">
INSERT INTO customers (CustomerName, ContactName, Address, City, PostalCode, Country)
VALUES (#{customerName}, #{contactName}, #{address}, #{city}, #{postalCode}, #{country})
</insert>
<delete id="deleteCustomerById">
DELETE FROM customers WHERE CustomerID = #{customerId}
</delete>
<update id="updateCustomer">
UPDATE customers
SET CustomerName = #{customerName},
ContactName = #{contactName},
Address = #{address},
City = #{city},
PostalCode = #{postalCode},
Country = #{country}
WHERE CustomerID = #{customerId}
</update>

实验一:

1
2
3
4
5
6
7
8
@Test
public void testOneLevelCache1() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
Customer customer1 = customerMapper.selectCustomerById(1);
Customer customer2 = customerMapper.selectCustomerById(1);
System.out.println("Is same instance: " + (customer1 == customer2));
}
实验二:
1
2
3
4
5
6
7
8
9
10
@Test
public void testOneLevelCache2() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
OneLevelCacheMapper customerMapper = sqlSession.getMapper(OneLevelCacheMapper.class);
Customer customer1 = customerMapper.selectCustomerById(1);
Customer customer = new Customer("Cardinal", "Tom B. Erichsen", "Skagen 21", "Stavanger", "4006", "Norway");
System.out.println("Add " + customerMapper.insertCustomer(customer) + " customer(s)");
Customer customer2 = customerMapper.selectCustomerById(1);
System.out.println("Is same instance: " + (customer1 == customer2));
}
实验三:
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testOneLevelCache3() {
SqlSession sqlSession1 = SqlSessionUtils.getSqlSession();
SqlSession sqlSession2 = SqlSessionUtils.getSqlSession();
OneLevelCacheMapper customerMapper1 = sqlSession1.getMapper(OneLevelCacheMapper.class);
OneLevelCacheMapper customerMapper2 = sqlSession2.getMapper(OneLevelCacheMapper.class);
Customer customer1 = customerMapper1.selectCustomerById(1);
Customer customer = new Customer(92, "Cardinal", "Alfred Schmidt", "Skagen 21", "Frankfurt", "4006", "Norway");
System.out.println("Update " + customerMapper2.updateCustomer(customer) + " customer(s)");
Customer customer2 = customerMapper1.selectCustomerById(1);
System.out.println("Is same instance: " + (customer1 == customer2));
}
- `SESSION`(默认值):当 `localCacheScope` 设置为`SESSION`时,一级缓存的作用范围为整个`SqlSession`的生命周期。这意味着,只要 `SqlSession` 没有关闭,它的查询结果会被缓存并在同一个 `SqlSession` 内被重用,以提高性能。 - `STATEMENT`:当`localCacheScope`设置为`STATEMENT`时,一级缓存仅在执行语句的过程中有效。这意味着缓存仅对当前执行的 SQL 语句有效,一旦 SQL 语句执行完毕,对应的缓存就会被清空。这个设置对于那些想要最小化一级缓存带来的潜在内存占用或是想要避免脏读问题的应用来说是有用的。

重新设置:

1
2
3
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>

会避免出现数据脏读现象。

一级缓存实现的时序图

二级缓存

作用范围:二级缓存是映射器级别的缓存,跨SqlSession共享。开启二级缓存后,多个会话可以共享缓存数据。

默认情况下,二级缓存是禁用的。需要在 MyBatis\text {MyBatis} 配置文件中配置,以及在 Mapper\text {Mapper} 映射文件中添加相应的设置。

工作机制

  1. 启用二级缓存:首先需要在 MyBatis\text {MyBatis} 配置文件和 Mapper\text {Mapper} 文件中明确启用二级缓存。
  2. 查询操作:当执行查询操作时,MyBatis\text {MyBatis} 按照以下顺序查找数据:
    • 查找二级缓存:首先检查是否有符合条件的数据存储在二级缓存中。
    • 查找一级缓存:如果在二级缓存中没有找到,会检查当前会话的一级缓存。
    • 执行数据库查询:如果两级缓存都没有找到数据,最后才会执行数据库查询。
  3. 缓存数据:当数据通过数据库查询得到后,这些数据会被缓存在当前会话的一级缓存以及相应的二级缓存中。
  4. 缓存失效
    • 当当前会话结束时,它可能会将一级缓存中的数据转移到二级缓存。只有当会话提交时,更改才会影响到二级缓存;关闭会话时,不涉及数据变更,一般不会直接影响二级缓存的内容。
    • 当执行写操作(insertupdatedelete)时,MyBatis\text {MyBatis} 会自动清空二级缓存中所有相关的缓存内容,以避免数据不一致的问题。这意味着,一旦有写操作发生,依赖于相同 Mapper\text {Mapper} 的二级缓存都会被清空,确保后续的读操作能获取到最新的数据。

生命周期:二级缓存的生命周期与SqlSessionFactory的生命周期相同。当SqlSessionFactory被关闭时,其二级缓存也会被清空。

开启二级缓存步骤:

mybatis-config.xml\text {mybatis-config.xml}

1
2
3
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

CustomerMapper.xml\text {CustomerMapper.xml}

1
<cache/>
  • eviction:缓存回收策略,决定了对象如何从缓存中移除。
    • LRU(Least Recently Used):最近最少使用的,移除最长时间不被使用的对象。
    • FIFO(First In First Out):先进先出,按对象进入缓存的顺序移除对象。
    • SOFT(Soft References):软引用,基于垃圾回收器状态和软引用规则移除对象。
    • WEAK(Weak References):弱引用,更积极地基于垃圾回收器状态移除对象。
  • flushInterval:缓存刷新间隔,定期清空缓存。
    • 以毫秒为单位的时间间隔。例如,60000 表示每 6060 秒刷新缓存。
  • size:引用的对象数,缓存中可以存储的最大对象数量。
    • 整数,表示缓存可以存储的对象数量。
  • readOnly:缓存数据的只读状态。
    • true:表示缓存的对象不会被修改,因此可以安全地由多个调用者共享相同的实例,不需要复制对象,性能较高。
    • false:表示缓存的对象可以被检索出来并修改,每次查询都会返回缓存对象的一个副本,安全性较高,但性能相对较低。
  • type:指定自定义缓存的实现。
    • 缓存实现的全限定类名。MyBatis\text {MyBatis} 允许使用第三方缓存实现,如 Ehcache\text {Ehcache}Redis\text {Redis}等。
  • blocking:请求的缓存元素不在缓存中时,是否阻塞缓存调用,直到缓存元素被加载。
    • true:开启阻塞。
    • false:非阻塞。

Customer.java\text {Customer.java}:确保实体类是可序列化的

1
2
3
4
public class Customer implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}

1⃣实验一:

1
2
3
4
5
6
7
8
9
10
@Test
public void testSecondLevelCache1() {
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
TwoLevelCacheMapper customerMapper1 = sqlSession1.getMapper(TwoLevelCacheMapper.class);
TwoLevelCacheMapper customerMapper2 = sqlSession2.getMapper(TwoLevelCacheMapper.class);
System.out.println(customerMapper1.selectCustomerById(1));
System.out.println(customerMapper2.selectCustomerById(1));
}

2⃣实验二:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testSecondLevelCache2() {
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
TwoLevelCacheMapper customerMapper1 = sqlSession1.getMapper(TwoLevelCacheMapper.class);
TwoLevelCacheMapper customerMapper2 = sqlSession2.getMapper(TwoLevelCacheMapper.class);
System.out.println(customerMapper1.selectCustomerById(1));
sqlSession1.commit();
System.out.println(customerMapper2.selectCustomerById(1));
}
3⃣实验三:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testSecondLevelCache3() {
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
TwoLevelCacheMapper customerMapper1 = sqlSession1.getMapper(TwoLevelCacheMapper.class);
TwoLevelCacheMapper customerMapper2 = sqlSession2.getMapper(TwoLevelCacheMapper.class);
TwoLevelCacheMapper customerMapper3 = sqlSession3.getMapper(TwoLevelCacheMapper.class);
System.out.println(customerMapper1.selectCustomerById(1));
sqlSession1.commit();
System.out.println(customerMapper2.selectCustomerById(1));
CustomerSerializable customer = new CustomerSerializable(92, "Cardinal", "Alfred Schmidt", "Skagen 21", "Frankfurt", "00000", "Mexico");
System.out.println("Update " + customerMapper3.updateCustomer(customer) + " customer(s)");
sqlSession3.commit();
System.out.println(customerMapper2.selectCustomerById(1));
}

流程分析:

  1. 初始化三个 SqlSession\text {SqlSession}sqlSession1\text {sqlSession1}sqlSession2\text {sqlSession2}sqlSession3\text {sqlSession3},它们都来源于同一个 SqlSessionFactory\text {SqlSessionFactory},可以共享二级缓存。
  2. 第一次查询:通过 customerMapper1\text {customerMapper1} 执行查询 ID\text {ID}11 的客户信息,并提交 sqlSession1\text {sqlSession1},查询结果被存储在二级缓存中。
  3. 第二次查询:通过 customerMapper2\text {customerMapper2} 执行相同的查询。由于 sqlSession1\text {sqlSession1} 的提交使得查询结果已经被放入二级缓存,能够直接从二级缓存中获取结果,而不需要再次访问数据库。
  4. 更新操作:通过 customerMapper3\text {customerMapper3} 更新客户的信息,并提交 sqlSession3\text {sqlSession3},触发了二级缓存的清空。
  5. 第三次查询:再次通过 customerMapper2\text {customerMapper2} 执行相同的查询。由于 sqlSession3\text {sqlSession3} 的提交使得二级缓存被清空,需要重新访问数据库来获取最新数据。

4⃣实验四:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testSecondLevelCache4() {
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
OrderMapper orderMapper1 = sqlSession1.getMapper(OrderMapper.class);
OrderMapper orderMapper2 = sqlSession2.getMapper(OrderMapper.class);
TwoLevelCacheMapper customerMapper = sqlSession3.getMapper(TwoLevelCacheMapper.class);
System.out.println(orderMapper1.selectOrderByIdWithCustomer(10365));
sqlSession1.commit();
System.out.println(orderMapper2.selectOrderByIdWithCustomer(10365));
CustomerSerializable customer = new CustomerSerializable(3, "Cardinal", "Alfred Schmidt", "Skagen 21", "Frankfurt", "00000", "Mexico");
System.out.println("Update " + customerMapper.updateCustomer(customer) + " customer(s)");
sqlSession3.commit();
System.out.println(orderMapper2.selectOrderByIdWithCustomer(10365));
}
  1. 初始化三个 SqlSessionsqlSession1\text {sqlSession1}sqlSession2\text {sqlSession2}sqlSession3\text {sqlSession3},它们都来源于同一个 SqlSessionFactory,可以共享二级缓存。
  2. 第一次查询:通过 orderMapper1\text {orderMapper1} 执行查询 ID\text {ID}1036510365 的订单信息,包括关联的客户信息,并提交 sqlSession1\text {sqlSession1},查询结果(订单及其客户信息)被存储在二级缓存中。
  3. 第二次查询:通过 orderMapper2\text {orderMapper2} 执行相同的查询。由于的 sqlSession1\text {sqlSession1} 的提交使得查询结果已经被放入二级缓存,能够直接从二级缓存中获取结果,而不需要再次访问数据库。
  4. 更新操作:通过 customerMapper\text {customerMapper} 更新客户的信息,并提交 sqlSession3\text {sqlSession3},触发了二级缓存的清空。

customerMapper\text {customerMapper}updateCustomer\text {updateCustomer} 不属于 orderMapper\text {orderMapper}namespace\text {namespace},所以 orderMapper\text {orderMapper} 下的 Cache\text {Cache} 没有感知到变化

  1. 第三次查询:再次通过 orderMapper2\text {orderMapper2} 执行相同的查询,会从缓存中读到脏数据!

5⃣实验五:

还原原本的数据库

OrderMapper.xml\text {OrderMapper.xml} 中使用<cache-ref>标签引用 TwoLevelCacheMapper.xml\text {TwoLevelCacheMapper.xml} 缓存空间。

1
<cache-ref namespace="com.liangjiajia.cs.mapper.TwoLevelCacheMapper"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testSecondLevelCache4() {
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
OrderMapper orderMapper1 = sqlSession1.getMapper(OrderMapper.class);
OrderMapper orderMapper2 = sqlSession2.getMapper(OrderMapper.class);
TwoLevelCacheMapper customerMapper = sqlSession3.getMapper(TwoLevelCacheMapper.class);
System.out.println(orderMapper1.selectOrderByIdWithCustomer(10365));
sqlSession1.commit();
System.out.println(orderMapper2.selectOrderByIdWithCustomer(10365));
CustomerSerializable customer = new CustomerSerializable(3, "Cardinal", "Alfred Schmidt", "Skagen 21", "Frankfurt", "00000", "Mexico");
System.out.println("Update " + customerMapper.updateCustomer(customer) + " customer(s)");
sqlSession3.commit();
System.out.println(orderMapper2.selectOrderByIdWithCustomer(10365));
}
二级缓存实现的时序图

整合 EHCache

1⃣添加依赖

1
2
3
4
5
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>${mybatis-ehcache.version}</version>
</dependency>

2⃣配置 EHCache\text {EHCache}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<diskStore path="xxx"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>

3⃣配置 MyBatis\text {MyBatis} 使用 EHCache\text {EHCache}

1
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>