MyBatis 的一级缓存
一、MyBatis 的缓存体系
MyBatis 的缓存体系只有两级:
1、一级缓存(Local Cache / SqlSession 级)
- 默认开启、无法关闭,生命周期 = 同一个 SqlSession。
- 只要作用域、statementId、参数相同,第二次查询直接返回第一次查到的对象引用------因此会出现"内存里改了,再查还是新值"的现象。
2、二级缓存(Namespace Cache / Mapper 级)
- 默认关闭,需要三步显式打开:
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 中的值没有变。