mybatis的二级缓存

真实场景:商城【商品分类】

电商网站 / APP:
首页永远要加载:手机、电脑、服饰、零食…分类列表

  • 数据几个月都不改一次
  • 几百万用户,每个人打开首页都要查这张表
  • 不加二级缓存:每次请求都查数据库,数据库压力爆炸

加 MyBatis 二级缓存后

  1. 第一个用户访问:查数据库,把分类数据存到全局共享缓存
  2. 后面所有用户、所有接口请求:不查数据库,直接读缓存
  3. 只有运营后台修改/新增/删除分类时:自动清空缓存,自动刷新新数据

二级缓存介绍

1. 定义

MyBatis二级缓存是Mapper命名空间(namespace)级别的全局缓存,跨 SqlSession 共享。
多个数据库会话、多个请求,访问同一个 Mapper 下的相同查询,可命中缓存,减少数据库查询。

2. 范围与特性

  1. 层级:介于一级缓存(SqlSession 本地) 与 第三方分布式缓存中间;
  2. 作用域:绑定 Mapper.xml 的 namespace,不同namespace缓存相互隔离;
  3. 生效时机:
    • 查询数据先存一级缓存;
    • 当前 SqlSession 关闭/提交后,数据才会刷新到二级缓存;
  4. 触发清空:当前 namespace 执行 insert/update/delete,自动清空该命名空间全部二级缓存;
  5. 强制要求:缓存实体类必须实现 Serializable 序列化接口;
  6. 开关:全局总开关 + Mapper 单独开启,默认整体关闭。

3. 查询执行顺序

二级缓存 → 一级缓存 → 数据库
优先走缓存,极大降低DB压力。

4. 和一级缓存关键区别

  • 一级缓存:单会话私有,会话关闭即销毁,无法跨请求;
  • 二级缓存:全局共享,跨会话、跨请求,生命周期更长。

应用场景

✅ 适合使用

  1. 读多写少业务
    基础数据、静态数据、变动极少的数据,频繁查询、极少修改。
    例:字典表、地区数据、商品分类、系统配置、角色权限、常量参数。

  2. 数据实时性要求低
    允许短时间缓存延迟(秒/分钟级不一致),不影响业务逻辑。

  3. 单应用单体项目
    非集群、非分布式部署,无多实例缓存不同步问题,使用原生二级缓存无脏数据风险。

  4. 高频重复查询
    首页展示、列表页、基础信息下拉选项,大量重复SQL查询,需要减轻数据库负载。


不适合使用(禁用场景)

  1. 强数据一致性业务
    订单、库存、支付、金融、账务、秒杀等,不能容忍缓存延迟、脏数据。

  2. 分布式/集群部署
    多台服务实例,原生二级缓存是单机内存缓存,各节点缓存独立:
    A服务修改数据,B服务缓存不刷新,必然出现脏数据。

  3. 高频更新表
    频繁增删改的业务表,缓存会频繁失效、清空,缓存命中率极低,反而增加性能开销。

  4. 多表关联跨namespace查询
    多Mapper联查,单表更新只清空自身缓存,关联数据缓存残留,引发数据错乱。

使用

指 SqlSessionFactory对象中的缓存

由同一个 SqlSessionFactory 对象创建的 SqlSession,共享缓存,即:都可以使用

特点

  • 默认不开启。需要额外配置才能使用

  • 存放的是序列化后的数据,而不是对象(所以从缓存中提取的对象,并不是原对象)

  • 实体类必须实现 Serializable 接口

开启二级缓存

修改 mybatis-config.xml

增加:

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

完整配置:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD SQL Map Config 3.0//EN"  
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <!-- 给实体类起别名 -->
    <typeAliases>
        <package name="org.malaoshi.entity" />
    </typeAliases>
    <!-- 配置环境 。default名字随意起-->
    <environments default="mysql">
    <!--    id名字必须同上面default名    -->
        <environment id="mysql">
            <!-- 配置事务管理器类型,JDBC 是 JdbcTransactionFactory.class 的别名 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置数据源。POOLED 是 PooledDataSourceFactory.class 的别名 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3308/mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>
<mappers>
    <mapper resource="mapper/userMapper.xml"></mapper>
    <mapper resource="mapper/postMapper.xml"></mapper>
</mappers>

</configuration>

修改 userMapper.xml

加入以下配置:

<!-- 该mapper开启二级缓存 -->
<cache />

详细配置在下面

修改实体类

实体类必须实现 Serializable 接口

package org.malaoshi.entity;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    private String id;
    private String username;
    private String password;
    private Integer age;
    private String sex;
    private String status;

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

测试

注意: 要 关闭1个 sqlsession,另一个sqlsession才会用二级缓存

@Test
public void testSelectById() throws IOException {

    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);

    User u=mapper.selectById("10001");
    String info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword());
    System.out.println(info);
    sqlSession.close();

    //重新创建SqlSession
    sqlSession = sqlSessionFactory.openSession();
    mapper = sqlSession.getMapper(IUserMapper.class);

    u=mapper.selectById("10001");
    info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword());
    System.out.println(info);


    sqlSession.close();
    inputStream.close();
}

在控制台中打印1条查询sql,说明重新创建 SqlSession 后,也没有查询数据库

<cache />配置说明

<cache 
        eviction="LRU"
        flushInterval="60000"
        size="1024"
        readOnly="true"
    />

缓存淘汰算法

eviction 是缓存的淘汰算法,可选值有:

缓存过期时间

flashInterval指缓存过期时间,单位为毫秒,60000即为60秒,缺省值为空,即只要容量足够,永不过期

缓存数量

size指缓存多少个对象,默认值为1024

是否只读

readOnly是否只读,如果为true,则所有相同的sql语句返回的是同一个对象(有助于提高性能,但并发操作同一条数据时,可能不安全),如果设置为false,则相同的sql,后面访问的是cache的clone副本。

注意

需要注意的是global caching的作用域是针对Mapper的Namespace而言的,也就是说只在有在这个Namespace内的查询才能共享这个cache

<select useCache="true"> 属性说明

增加 useCache="false" 属性

<select  ...   useCache="true">

useCache属性,默认为true,即开启二级缓存。通过该属性可以局部控制,比如设置userCache=”false”禁用二级缓存

useCache="false"表示该select语句不使用缓存(即使xml最开头的全局cache启用)

<select flushCache="true"> 属性说明

默认情况下,如果开启二级缓存,insert、update、delete 成功后,会自动刷新相关的缓存

  • 当为select语句时:
    flushCache默认为false,表示任何时候语句被调用,都不会去清空本地缓存和二级缓存。

  • 当为insert、update、delete语句时:
    flushCache默认为true,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空。

一般执行inser操作时,需要刷新缓存,否则会新插入的数据查询不出来。

测试

@Test
public void test() throws IOException {

    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);

    User u=mapper.selectById("10001");
    String info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword());
    System.out.println(info);

    User user=new User();
    user.setId("1000");
    user.setUsername("admin2");
    user.setPassword("12345678");
    int num=mapper.updateById(user);
    System.out.println("更新 "+num+" 条记录");
    sqlSession.commit();

    u=mapper.selectById("10001");
    info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword());
    System.out.println(info);


    sqlSession.close();
    inputStream.close();
}

由于执行了 update 语句,所以再次查询相同sql时,会查询数据库:

二级缓存的危害

参看以下文章:

http://blog.csdn.net/isea533/article/details/44566257

参考文章,非常感谢:

http://www.cnblogs.com/QQParadise/articles/5109633.html
http://blog.csdn.net/lycemail/article/details/50923245
http://www.360doc.com/content/15/1205/07/29475794_518018352.shtml


原文出处:/show_1IXPEuxjXC.html