MyBatis 的一级缓存

MyBatis 的一级缓存

一、MyBatis 的缓存体系

MyBatis 的缓存体系只有两级:

1、一级缓存(Local Cache / SqlSession 级)

  1. 默认开启、无法关闭,生命周期 = 同一个 SqlSession。
  2. 只要作用域、statementId、参数相同,第二次查询直接返回第一次查到的对象引用------因此会出现"内存里改了,再查还是新值"的现象。

2、二级缓存(Namespace Cache / Mapper 级)

  1. 默认关闭,需要三步显式打开:
    a. 全局开关 (默认就是 true)
    b. 在对应 Mapper.xml 加 (或 )
    c. 查询语句加 useCache=true(默认就是 true)
    生命周期跨 SqlSession,以 namespace 为维度;粒度可以细到 statement 级别(@Options(flushCache = ...))。
    默认实现是 PerpetualCache(纯内存 HashMap),可替换为 EhCache、Redis、Caffeine 等。
    失效策略:
    同一 namespace 下出现写操作(insert/update/delete)时默认 flush
    也可以手动 flushCache="true"

二、一级缓存

这里详细说一下一级缓存。

表结构:

sql 复制代码
CREATE TABLE `test` (
  `id` int NOT NULL AUTO_INCREMENT,
  `status` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `operator_status` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

java 代码准备:

实体

java 复制代码
// bean 实体类
public class Test {

    private static final long serialVersionUID = 1L;

    private Integer id;            // 主键
    private String status;         // 状态
    private String operatorStatus; // 操作状态

    public Integer getId() {
        return id;
    }

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

    public String getStatus() {
        return status;
    }

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

    public String getOperatorStatus() {
        return operatorStatus;
    }

    public void setOperatorStatus(String operatorStatus) {
        this.operatorStatus = operatorStatus;
    }

	@Override
    public String toString() {
        return "Test{" +
                "id=" + id +
                ", status='" + status + '\'' +
                ", operatorStatus='" + operatorStatus + '\'' +
                '}';
    }
}





// dao 层
@Mapper
public interface TestMapper {

    /* 增 */
    int insert(Test record);
    int insertSelective(Test record);

    /* 删 */
    int deleteByPrimaryKey(Integer id);

    /* 改 */
    int updateByPrimaryKey(Test record);
    int updateByPrimaryKeySelective(Test record);

    /* 查 */
    Test selectByPrimaryKey(Integer id);
    List<Test> selectAll();
    List<Test> selectByStatus(@Param("status") String status);
}


// xml 层
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.TestMapper">

    <resultMap id="BaseResultMap" type="com.ruoyi.system.domain.Test">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="status" jdbcType="VARCHAR" property="status"/>
        <result column="operator_status" jdbcType="VARCHAR" property="operatorStatus"/>
    </resultMap>

    <sql id="Base_Column_List">
        id, status, operator_status
    </sql>

    <!-- 新增 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO test (status, operator_status)
        VALUES (#{status,jdbcType=VARCHAR}, #{operatorStatus,jdbcType=VARCHAR})
    </insert>

    <insert id="insertSelective" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO test
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="status != null">status,</if>
            <if test="operatorStatus != null">operator_status,</if>
        </trim>
        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
            <if test="status != null">#{status,jdbcType=VARCHAR},</if>
            <if test="operatorStatus != null">#{operatorStatus,jdbcType=VARCHAR},</if>
        </trim>
    </insert>

    <!-- 删除 -->
    <delete id="deleteByPrimaryKey">
        DELETE FROM test WHERE id = #{id,jdbcType=INTEGER}
    </delete>

    <!-- 修改 -->
    <update id="updateByPrimaryKey">
        UPDATE test
        SET status        = #{status,jdbcType=VARCHAR},
            operator_status = #{operatorStatus,jdbcType=VARCHAR}
        WHERE id = #{id,jdbcType=INTEGER}
    </update>

    <update id="updateByPrimaryKeySelective">
        UPDATE test
        <set>
            <if test="status != null">status = #{status,jdbcType=VARCHAR},</if>
            <if test="operatorStatus != null">operator_status = #{operatorStatus,jdbcType=VARCHAR},</if>
        </set>
        WHERE id = #{id,jdbcType=INTEGER}
    </update>

    <!-- 查询 -->
    <select id="selectByPrimaryKey" resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List"/>
        FROM test
        WHERE id = #{id,jdbcType=INTEGER}
    </select>

    <select id="selectAll" resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List"/>
        FROM test
    </select>

    <select id="selectByStatus" resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List"/>
        FROM test
        WHERE status = #{status,jdbcType=VARCHAR}
    </select>

</mapper>

数据库中插入一条数据:

测试 service:

java 复制代码
@Service
public class TestService {

    private static final Logger logger = LoggerFactory.getLogger(TestService.class);
    
    @Autowired
    private TestMapper testMapper;

    @Transactional
    public void testUpdate(String code){

        Test test1 = testMapper.selectByPrimaryKey(1);
        logger.info("查询test1结果:{}",test1.toString());

        if (Objects.equals("200",code)){
            test1.setOperatorStatus("1");
        }
        Test test2 = testMapper.selectByPrimaryKey(1);
        logger.info("查询test2结果:{}",test2.toString());

    }
}

打印结果:

如上图所示,并没有更新数据库,但是查询的 test2 的 operatorStatus 是1。这是因为:
Spring 在事务里并不是把"对象"重新查一遍,而是把"同一个对象"缓存起来反复用

代码开启了事务,Spring 把当前事务绑定到同一个 SqlSession 上,MyBatis 在这个 SqlSession 里维护了一级缓存(也叫 local cache)。只要是在同一个 SqlSession、同一条 SQL、同一个参数范围内,第二次查询不会发 SQL,而是直接把第一次查出来的对象原样返回。因此:

  • 第一次查出来的 test 对象地址为 @1234
  • 把 @1234.operatorStatus 改成 "1"
  • 第二次查询时,MyBatis 发现缓存命中,直接把 @1234 又拿给你,所以看到的 operatorStatus 已经是 "1"

此时数据库里那行数据其实依旧是旧值,只是你拿到的是内存里被你自己改过的同一个对象。

一句话:不是数据库被更新了,而是 MyBatis 的一级缓存把"同一个对象"又给了你

如何把一级缓存清空?

一级缓存(LocalCache)绑定在 SqlSession 上,因此只要让当前 SqlSession 执行"清空"操作即可。方法有很多,这里只介绍最直观的一种:

① 手动调 API 清空缓存(最直观,推荐使用这个)

java 复制代码
@Service
public class TestService {

    private static final Logger logger = LoggerFactory.getLogger(TestService.class);

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;   // 已经线程安全

    @Autowired
    private TestMapper testMapper;

    @Transactional
    public void testUpdate(String code){

        Test test1 = testMapper.selectByPrimaryKey(1);
        logger.info("查询test1结果:{}",test1.toString());

        if (Objects.equals("200",code)){
            test1.setOperatorStatus("1");
        }

        sqlSessionTemplate.clearCache();   // 立刻清空当前 SqlSession 的一级缓存
        Test test2 = testMapper.selectByPrimaryKey(1);
        logger.info("查询test2结果:{}",test2.toString());

    }
}

打印结果:

可以看出,test2 中的值没有变。

相关推荐
ZouZou老师2 小时前
C++设计模式之适配器模式:以家具生产为例
java·设计模式·适配器模式
曼巴UE53 小时前
UE5 C++ 动态多播
java·开发语言
VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
程序员鱼皮3 小时前
刚刚,IDEA 免费版发布!终于不用破解了
java·程序员·jetbrains
热心市民蟹不肉3 小时前
黑盒漏洞扫描(三)
数据库·redis·安全·缓存
Hui Baby3 小时前
Nacos容灾俩种方案对比
java
曲莫终3 小时前
Java单元测试框架Junit5用法一览
java
成富4 小时前
Chat Agent UI,类似 ChatGPT 的聊天界面,Spring AI 应用的测试工具
java·人工智能·spring·ui·chatgpt
凌波粒4 小时前
Springboot基础教程(9)--Swagger2
java·spring boot·后端