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 中的值没有变。

相关推荐
两个蝴蝶飞2 小时前
Java量化系列(四):实现自选股票维护功能
java·经验分享
短剑重铸之日3 小时前
7天读懂MySQL|Day 5:执行引擎与SQL优化
java·数据库·sql·mysql·架构
酒九鸠玖4 小时前
Java--多线程
java
Dreamboat-L4 小时前
云服务器上部署nginx
java·服务器·nginx
长安er4 小时前
LeetCode215/347/295 堆相关理论与题目
java·数据结构·算法·leetcode·
cici158745 小时前
C#实现三菱PLC通信
java·网络·c#
k***92166 小时前
【C++】继承和多态扩展学习
java·c++·学习
weixin_440730506 小时前
java结构语句学习
java·开发语言·学习
JIngJaneIL6 小时前
基于java+ vue医院管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
Coder_Boy_6 小时前
Spring AI 源码大白话解析
java·人工智能·spring