SSM从入门到实战: 2.6 MyBatis缓存机制与性能优化

👋 大家好,我是 阿问学长!专注于分享优质开源项目解析、毕业设计项目指导支持、幼小初高教辅资料推荐等,欢迎关注交流!🚀

13-MyBatis缓存机制与性能优化

📖 本文概述

本文是SSM框架系列MyBatis进阶篇的第三篇,将深入探讨MyBatis的缓存机制和性能优化策略。通过详细的原理分析和实践示例,帮助读者掌握MyBatis缓存的使用技巧和性能调优方法。

🎯 学习目标

  • 深入理解MyBatis的一级缓存和二级缓存
  • 掌握缓存的配置和使用方法
  • 学会分析和解决缓存相关问题
  • 了解MyBatis性能优化的最佳实践
  • 掌握缓存失效和更新策略

1. MyBatis缓存机制概述

1.1 缓存的作用和意义

缓存是提高数据库应用性能的重要手段,MyBatis提供了强大的缓存机制:

java 复制代码
/**
 * 缓存机制的作用演示
 */
public class CacheDemo {
    
    /**
     * 无缓存的情况
     */
    public void withoutCache() {
        // 每次查询都会执行SQL
        User user1 = userMapper.findById(1L); // 执行SQL: SELECT * FROM users WHERE id = 1
        User user2 = userMapper.findById(1L); // 再次执行SQL: SELECT * FROM users WHERE id = 1
        User user3 = userMapper.findById(1L); // 又执行SQL: SELECT * FROM users WHERE id = 1
        
        // 结果:执行了3次相同的SQL查询
    }
    
    /**
     * 有缓存的情况
     */
    public void withCache() {
        // 第一次查询执行SQL,后续从缓存获取
        User user1 = userMapper.findById(1L); // 执行SQL: SELECT * FROM users WHERE id = 1
        User user2 = userMapper.findById(1L); // 从缓存获取,不执行SQL
        User user3 = userMapper.findById(1L); // 从缓存获取,不执行SQL
        
        // 结果:只执行了1次SQL查询,性能提升显著
    }
}

缓存的优势:

  1. 减少数据库访问 - 降低数据库负载
  2. 提高响应速度 - 内存访问比磁盘访问快得多
  3. 节省系统资源 - 减少网络传输和CPU消耗
  4. 提升用户体验 - 更快的页面加载速度

1.2 MyBatis缓存架构

复制代码
MyBatis缓存架构图:

┌─────────────────────────────────────────────────────────────┐
│                    应用程序                                  │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                   SqlSession                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                一级缓存                              │   │
│  │            (Session级别)                           │   │
│  │         默认开启,无法关闭                           │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                SqlSessionFactory                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                二级缓存                              │   │
│  │           (Mapper级别)                             │   │
│  │         需要手动配置开启                             │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    数据库                                   │
└─────────────────────────────────────────────────────────────┘

2. 一级缓存详解

2.1 一级缓存的特点

一级缓存是SqlSession级别的缓存,具有以下特点:

java 复制代码
/**
 * 一级缓存演示
 */
@Test
public void testFirstLevelCache() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    System.out.println("=== 一级缓存测试 ===");
    
    // 第一次查询,执行SQL
    System.out.println("第一次查询:");
    User user1 = userMapper.findById(1L);
    System.out.println("用户信息:" + user1);
    
    // 第二次查询,从缓存获取
    System.out.println("第二次查询:");
    User user2 = userMapper.findById(1L);
    System.out.println("用户信息:" + user2);
    
    // 验证是否为同一对象
    System.out.println("是否为同一对象:" + (user1 == user2)); // true
    
    sqlSession.close();
}

一级缓存的特点:

  1. 默认开启 - 无法关闭,始终有效
  2. Session级别 - 同一个SqlSession内有效
  3. 自动管理 - 无需手动配置
  4. 生命周期短 - SqlSession关闭时缓存清空

2.2 一级缓存的失效情况

java 复制代码
/**
 * 一级缓存失效场景演示
 */
@Test
public void testFirstLevelCacheInvalidation() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    // 场景1:不同SqlSession
    System.out.println("=== 场景1:不同SqlSession ===");
    User user1 = userMapper.findById(1L); // 执行SQL
    
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    User user2 = userMapper2.findById(1L); // 再次执行SQL
    
    System.out.println("不同Session查询结果相等:" + user1.equals(user2)); // true
    System.out.println("不同Session是否同一对象:" + (user1 == user2)); // false
    
    // 场景2:执行更新操作
    System.out.println("=== 场景2:执行更新操作 ===");
    User user3 = userMapper.findById(1L); // 从缓存获取
    
    // 执行更新操作,缓存被清空
    User updateUser = new User();
    updateUser.setId(2L);
    updateUser.setUsername("updated");
    userMapper.update(updateUser);
    
    User user4 = userMapper.findById(1L); // 重新执行SQL
    System.out.println("更新后是否同一对象:" + (user3 == user4)); // false
    
    // 场景3:手动清空缓存
    System.out.println("=== 场景3:手动清空缓存 ===");
    User user5 = userMapper.findById(1L); // 从缓存获取
    
    sqlSession.clearCache(); // 手动清空缓存
    
    User user6 = userMapper.findById(1L); // 重新执行SQL
    System.out.println("清空缓存后是否同一对象:" + (user5 == user6)); // false
    
    sqlSession.close();
    sqlSession2.close();
}

2.3 一级缓存的配置

xml 复制代码
<!-- mybatis-config.xml中的一级缓存配置 -->
<settings>
    <!-- 本地缓存机制,SESSION或STATEMENT -->
    <setting name="localCacheScope" value="SESSION"/>
</settings>

localCacheScope配置说明:

  • SESSION(默认):缓存在整个SqlSession期间有效
  • STATEMENT:缓存仅在语句执行期间有效,执行完立即清空

3. 二级缓存详解

3.1 二级缓存的特点

二级缓存是Mapper级别的缓存,需要手动配置:

xml 复制代码
<!-- 1. 在mybatis-config.xml中开启二级缓存 -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

<!-- 2. 在Mapper.xml中配置缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
    
    <!-- 开启二级缓存 -->
    <cache/>
    
    <!-- 或者自定义缓存配置 -->
    <cache eviction="LRU" 
           flushInterval="60000" 
           size="512" 
           readOnly="false"/>
    
    <select id="findById" parameterType="long" resultType="User" useCache="true">
        SELECT * FROM users WHERE id = #{id}
    </select>
    
</mapper>

3.2 二级缓存配置详解

xml 复制代码
<!-- 详细的二级缓存配置 -->
<cache 
    eviction="LRU"           <!-- 缓存回收策略 -->
    flushInterval="60000"    <!-- 缓存刷新间隔(毫秒) -->
    size="512"               <!-- 缓存对象数量 -->
    readOnly="false"         <!-- 是否只读 -->
    blocking="false"         <!-- 是否阻塞 -->
    type="org.mybatis.caches.ehcache.EhcacheCache"/> <!-- 自定义缓存实现 -->

配置参数说明:

  1. eviction(回收策略)

    • LRU(默认):最近最少使用,移除最长时间不被使用的对象
    • FIFO:先进先出,按对象进入缓存的顺序来移除
    • SOFT:软引用,基于垃圾回收器状态和软引用规则移除对象
    • WEAK:弱引用,更积极地基于垃圾收集器状态和弱引用规则移除对象
  2. flushInterval(刷新间隔)

    • 缓存多长时间清空一次,默认不清空
    • 单位:毫秒
  3. size(缓存大小)

    • 缓存存放多少元素,默认值是1024
  4. readOnly(只读)

    • true:只读缓存,返回缓存对象的相同实例,速度快但不安全
    • false(默认):读写缓存,返回缓存对象的拷贝,速度慢但安全

3.3 二级缓存使用示例

java 复制代码
/**
 * 二级缓存演示
 */
@Test
public void testSecondLevelCache() {
    // 第一个SqlSession
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    
    System.out.println("=== 第一个SqlSession查询 ===");
    User user1 = userMapper1.findById(1L); // 执行SQL,结果放入二级缓存
    System.out.println("用户信息:" + user1);
    sqlSession1.close(); // 关闭session,一级缓存清空,二级缓存保留
    
    // 第二个SqlSession
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    
    System.out.println("=== 第二个SqlSession查询 ===");
    User user2 = userMapper2.findById(1L); // 从二级缓存获取,不执行SQL
    System.out.println("用户信息:" + user2);
    
    System.out.println("对象内容相等:" + user1.equals(user2)); // true
    System.out.println("是否同一对象:" + (user1 == user2)); // false(因为是拷贝)
    
    sqlSession2.close();
}

3.4 自定义缓存实现

java 复制代码
/**
 * 自定义Redis缓存实现
 */
public class RedisCache implements Cache {
    
    private final String id;
    private RedisTemplate<String, Object> redisTemplate;
    
    public RedisCache(String id) {
        this.id = id;
        // 初始化Redis连接
        this.redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
    }
    
    @Override
    public String getId() {
        return this.id;
    }
    
    @Override
    public void putObject(Object key, Object value) {
        String redisKey = generateKey(key);
        redisTemplate.opsForValue().set(redisKey, value, 30, TimeUnit.MINUTES);
    }
    
    @Override
    public Object getObject(Object key) {
        String redisKey = generateKey(key);
        return redisTemplate.opsForValue().get(redisKey);
    }
    
    @Override
    public Object removeObject(Object key) {
        String redisKey = generateKey(key);
        Object value = redisTemplate.opsForValue().get(redisKey);
        redisTemplate.delete(redisKey);
        return value;
    }
    
    @Override
    public void clear() {
        Set<String> keys = redisTemplate.keys(id + ":*");
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }
    
    @Override
    public int getSize() {
        Set<String> keys = redisTemplate.keys(id + ":*");
        return keys != null ? keys.size() : 0;
    }
    
    private String generateKey(Object key) {
        return id + ":" + key.toString();
    }
}
xml 复制代码
<!-- 使用自定义缓存 -->
<cache type="com.example.cache.RedisCache">
    <property name="timeout" value="1800"/>
</cache>

4. 缓存失效和更新策略

4.1 缓存失效场景

java 复制代码
/**
 * 缓存失效场景演示
 */
@Test
public void testCacheInvalidation() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    // 1. 查询数据,放入缓存
    User user1 = userMapper.findById(1L);
    System.out.println("第一次查询:" + user1);
    
    // 2. 执行更新操作,缓存失效
    User updateUser = new User();
    updateUser.setId(1L);
    updateUser.setUsername("updated_name");
    userMapper.update(updateUser);
    sqlSession.commit(); // 提交事务,二级缓存失效
    
    // 3. 再次查询,重新执行SQL
    User user2 = userMapper.findById(1L);
    System.out.println("更新后查询:" + user2);
    
    sqlSession.close();
}

4.2 缓存更新策略

xml 复制代码
<!-- 配置缓存刷新策略 -->
<mapper namespace="com.example.mapper.UserMapper">
    
    <cache flushInterval="60000"/> <!-- 60秒自动刷新 -->
    
    <!-- 查询语句使用缓存 -->
    <select id="findById" parameterType="long" resultType="User" useCache="true">
        SELECT * FROM users WHERE id = #{id}
    </select>
    
    <!-- 更新语句刷新缓存 -->
    <update id="update" parameterType="User" flushCache="true">
        UPDATE users 
        SET username = #{username}, email = #{email}
        WHERE id = #{id}
    </update>
    
    <!-- 插入语句刷新缓存 -->
    <insert id="insert" parameterType="User" flushCache="true">
        INSERT INTO users (username, email, password)
        VALUES (#{username}, #{email}, #{password})
    </insert>
    
    <!-- 删除语句刷新缓存 -->
    <delete id="deleteById" parameterType="long" flushCache="true">
        DELETE FROM users WHERE id = #{id}
    </delete>
    
</mapper>

4.3 跨Mapper缓存管理

xml 复制代码
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <cache-ref namespace="com.example.mapper.CommonCache"/>
    
    <select id="findById" parameterType="long" resultType="User">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>

<!-- RoleMapper.xml -->
<mapper namespace="com.example.mapper.RoleMapper">
    <cache-ref namespace="com.example.mapper.CommonCache"/>
    
    <select id="findByUserId" parameterType="long" resultType="Role">
        SELECT * FROM roles r 
        INNER JOIN user_roles ur ON r.id = ur.role_id
        WHERE ur.user_id = #{userId}
    </select>
</mapper>

<!-- CommonCache.xml -->
<mapper namespace="com.example.mapper.CommonCache">
    <cache eviction="LRU" flushInterval="300000" size="1024" readOnly="false"/>
</mapper>

5. 性能优化最佳实践

5.1 SQL优化

xml 复制代码
<!-- 优化前:N+1查询问题 -->
<select id="findAllUsers" resultMap="UserResultMap">
    SELECT * FROM users
</select>

<select id="findRolesByUserId" parameterType="long" resultType="Role">
    SELECT * FROM roles r 
    INNER JOIN user_roles ur ON r.id = ur.role_id
    WHERE ur.user_id = #{userId}
</select>

<!-- 优化后:一次查询获取所有数据 -->
<resultMap id="UserWithRolesResultMap" type="User">
    <id column="user_id" property="id"/>
    <result column="username" property="username"/>
    <result column="email" property="email"/>
    <collection property="roles" ofType="Role">
        <id column="role_id" property="id"/>
        <result column="role_name" property="roleName"/>
        <result column="role_description" property="description"/>
    </collection>
</resultMap>

<select id="findAllUsersWithRoles" resultMap="UserWithRolesResultMap">
    SELECT 
        u.id as user_id, u.username, u.email,
        r.id as role_id, r.role_name, r.description as role_description
    FROM users u
    LEFT JOIN user_roles ur ON u.id = ur.user_id
    LEFT JOIN roles r ON ur.role_id = r.id
</select>

5.2 分页查询优化

xml 复制代码
<!-- 物理分页 -->
<select id="findUsersByPage" parameterType="map" resultType="User">
    SELECT * FROM users
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="email != null and email != ''">
            AND email LIKE CONCAT('%', #{email}, '%')
        </if>
    </where>
    ORDER BY create_time DESC
    LIMIT #{offset}, #{limit}
</select>

<!-- 统计总数(使用缓存) -->
<select id="countUsers" parameterType="map" resultType="long" useCache="true">
    SELECT COUNT(*) FROM users
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="email != null and email != ''">
            AND email LIKE CONCAT('%', #{email}, '%')
        </if>
    </where>
</select>

5.3 批量操作优化

xml 复制代码
<!-- 批量插入优化 -->
<insert id="batchInsertUsers" parameterType="list">
    INSERT INTO users (username, email, password, age)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.email}, #{user.password}, #{user.age})
    </foreach>
</insert>

<!-- 批量更新优化 -->
<update id="batchUpdateUsers" parameterType="list">
    <foreach collection="list" item="user" separator=";">
        UPDATE users
        SET username = #{user.username}, email = #{user.email}
        WHERE id = #{user.id}
    </foreach>
</update>

5.4 连接池优化

xml 复制代码
<!-- Druid连接池优化配置 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <!-- 基本配置 -->
    <property name="driverClassName" value="${database.driver}"/>
    <property name="url" value="${database.url}"/>
    <property name="username" value="${database.username}"/>
    <property name="password" value="${database.password}"/>
    
    <!-- 连接池配置 -->
    <property name="initialSize" value="10"/>        <!-- 初始连接数 -->
    <property name="minIdle" value="10"/>            <!-- 最小空闲连接数 -->
    <property name="maxActive" value="50"/>          <!-- 最大活跃连接数 -->
    <property name="maxWait" value="60000"/>         <!-- 获取连接最大等待时间 -->
    
    <!-- 性能优化 -->
    <property name="poolPreparedStatements" value="true"/>
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
    
    <!-- 连接检测 -->
    <property name="validationQuery" value="SELECT 1"/>
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>
    <property name="testWhileIdle" value="true"/>
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>
    <property name="minEvictableIdleTimeMillis" value="300000"/>
    
    <!-- 监控配置 -->
    <property name="filters" value="stat,wall,slf4j"/>
</bean>

6. 缓存监控和调试

6.1 缓存统计信息

java 复制代码
/**
 * 缓存统计工具类
 */
@Component
public class CacheStatistics {
    
    @Autowired
    private SqlSessionFactory sqlSessionFactory;
    
    /**
     * 获取缓存统计信息
     */
    public void printCacheStatistics() {
        Configuration configuration = sqlSessionFactory.getConfiguration();
        
        // 获取所有Mapper的缓存信息
        Collection<Cache> caches = configuration.getCaches();
        
        System.out.println("=== MyBatis缓存统计信息 ===");
        for (Cache cache : caches) {
            System.out.println("缓存ID: " + cache.getId());
            System.out.println("缓存大小: " + cache.getSize());
            System.out.println("缓存类型: " + cache.getClass().getSimpleName());
            System.out.println("---");
        }
    }
    
    /**
     * 清空所有缓存
     */
    public void clearAllCaches() {
        Configuration configuration = sqlSessionFactory.getConfiguration();
        Collection<Cache> caches = configuration.getCaches();
        
        for (Cache cache : caches) {
            cache.clear();
        }
        
        System.out.println("所有缓存已清空");
    }
}

6.2 缓存调试配置

xml 复制代码
<!-- logback.xml中配置MyBatis缓存日志 -->
<configuration>
    <!-- MyBatis缓存相关日志 -->
    <logger name="org.apache.ibatis.cache" level="DEBUG"/>
    <logger name="com.example.mapper" level="DEBUG"/>
    
    <!-- SQL执行日志 -->
    <logger name="org.apache.ibatis.logging.jdbc" level="DEBUG"/>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

7. 小结

本文深入介绍了MyBatis的缓存机制和性能优化:

  1. 一级缓存:SqlSession级别,默认开启,自动管理
  2. 二级缓存:Mapper级别,需要配置,支持自定义实现
  3. 缓存配置:回收策略、刷新间隔、大小限制等
  4. 性能优化:SQL优化、分页优化、批量操作、连接池调优
  5. 监控调试:缓存统计、日志配置、问题排查

掌握MyBatis缓存机制的关键点:

  • 理解一级缓存和二级缓存的区别和适用场景
  • 合理配置缓存参数,避免内存溢出
  • 注意缓存失效时机,保证数据一致性
  • 结合业务场景选择合适的缓存策略
  • 定期监控缓存效果,及时调优

🔗 下一篇预告

下一篇文章将介绍MyBatis与Spring集成,学习如何在Spring环境中使用MyBatis,以及事务管理的最佳实践。


相关文章: