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

相关推荐
带刺的坐椅11 小时前
Solon AI Skills 会是 Agent 的未来吗?
java·agent·langchain4j·solon-ai
jacGJ11 小时前
记录学习--文件读写
java·前端·学习
花间相见12 小时前
【JAVA开发】—— Nginx服务器
java·开发语言·nginx
難釋懷12 小时前
SpringDataRedis数据序列化器
redis·缓存
扶苏-su12 小时前
Java---Properties 类
java·开发语言
cypking12 小时前
四、CRUD操作指南
java
2301_7806698613 小时前
文件字节流输出、文件复制、关闭流的方法
java
麦克马13 小时前
MyBatis中字符串比较的类型解析问题与解决方案
mybatis
剑锋所指,所向披靡!14 小时前
C++之类模版
java·jvm·c++
Coder_Boy_14 小时前
基于SpringAI的在线考试系统-0到1全流程研发:DDD、TDD与CICD协同实践
java·人工智能·spring boot·架构·ddd·tdd