Java-11 深入浅出 MyBatis 一级缓存详解:从原理到失效场景 Executor

Java-11 MyBatis 一级缓存详解:SqlSession 本地缓存、CacheKey 与失效场景

TL;DR

  • 场景:在 MyBatis 持久层开发中,同一个查询方法连续执行两次,第二次可能不会再次访问数据库。
  • 核心结论 :MyBatis 一级缓存是 SqlSession 级别的本地缓存,默认开启。相同 SqlSession 内,相同 SQL、相同参数、相同分页条件的查询可能命中缓存。
  • 失效条件 :执行 insertupdatedeletecommitrollbackclose,或者手动调用 clearCache(),都会导致当前 session 的本地缓存被清理。
  • 源码结构 :一级缓存主要由 BaseExecutor 持有,底层使用 PerpetualCache,当前 MyBatis 3.5.x 中 PerpetualCache 内部使用 HashMap<Object, Object> 存储数据。
  • 注意点 :一级缓存不是业务缓存,也不是跨请求缓存。它主要用于同一个 SqlSession 内减少重复查询,并辅助处理嵌套查询、循环引用等问题。

核心关键词:MyBatis、一级缓存、SqlSession、本地缓存、CacheKey、BaseExecutor、PerpetualCache、localCacheScope、缓存失效


背景与问题

在使用 MyBatis 查询数据库时,经常会看到这样的现象:

同一个 SqlSession 中,连续执行两次相同的查询方法,控制台只打印了一次 SQL。

这不是日志丢失,也不是数据库没有执行成功,而是 MyBatis 的一级缓存生效了。

一级缓存是 MyBatis 默认启用的本地缓存。它对开发者基本透明,所以很多时候并不会被显式感知。但如果不了解它的作用范围和失效条件,就容易产生几个误判:

  • 为什么第二次查询没有打印 SQL?
  • 为什么执行一次 update 后,第二次查询又重新访问数据库?
  • 一级缓存是不是全局缓存?
  • 一级缓存能不能用来做业务缓存?
  • 在 Spring 项目中,它和事务、Mapper、SqlSession 又是什么关系?

本文围绕这些问题,结合示例代码和源码结构,梳理 MyBatis 一级缓存的工作方式。


环境与版本

本文以 MyBatis 3.5.x 的常见行为为说明对象,示例代码基于普通 Java main 方法演示。

项目 说明
JDK Java 8+ 均可理解本文示例
MyBatis 以 MyBatis 3.5.x 为主
数据库 示例使用用户表和订单表,数据库类型不影响一级缓存原理
日志 需要开启 MyBatis SQL 日志,便于观察 SQL 是否真正执行
示例方式 通过 SqlSessionFactory 手动创建 SqlSession

说明:MyBatis 的一级缓存属于框架内部行为,核心机制长期稳定。但源码细节、默认配置说明和版本文档仍建议以当前项目实际依赖版本为准。

如果是 Spring Boot + MyBatis 项目,SqlSession 通常由框架托管,观察方式和普通 main 方法略有不同,后文会单独说明。


一级缓存是什么

MyBatis 中有两类缓存:

缓存类型 作用范围 默认状态 主要用途
一级缓存 / 本地缓存 SqlSession 级别 默认开启 减少同一 session 内重复查询
二级缓存 Mapper namespace 级别 需要额外配置 跨 session 复用查询结果

本文只讨论一级缓存。

一级缓存可以简单理解为:MyBatis 在当前 SqlSession 内部维护了一个本地 Map。当执行查询时,MyBatis 会根据当前查询生成一个 CacheKey。如果这个 key 已经存在于本地缓存中,就直接返回缓存中的结果;如果不存在,就访问数据库,并把查询结果放入缓存。

这个缓存不是全局缓存,也不是 Redis、Caffeine 这类业务缓存。它只在当前 SqlSession 生命周期内有效。


一级缓存为什么是 SqlSession 级别

SqlSession 是 MyBatis 执行 SQL、获取 Mapper、管理事务边界的核心入口。

一级缓存绑定在 SqlSession 上,主要有两个原因:

第一,避免同一个会话中重复执行完全相同的查询。

例如同一个业务流程里多次查询同一个用户信息,如果 SQL 和参数完全一致,第二次可以直接复用前一次查询结果。

第二,辅助处理嵌套查询和对象引用关系。

MyBatis 在处理复杂结果映射、嵌套查询时,需要避免重复加载和循环引用问题。本地缓存不仅是性能优化点,也是框架内部执行流程的一部分。

因此,MyBatis 一级缓存默认无法完全关闭,但可以通过 localCacheScope=STATEMENT 把缓存范围缩小到单条语句执行期间。


示例准备:Mapper 与 SQL

假设项目中有一个 UserMapper,提供两个方法:

java 复制代码
public interface UserMapper {

    List<WzkUser> findAll();

    int updateById(WzkUser user);
}

对应的查询 SQL 类似如下:

xml 复制代码
<select id="findAll" resultMap="userOrderMap">
    select
        *,
        o.id oid
    from wzk_user u
    left join wzk_orders o on u.id = o.uid
</select>

更新 SQL 类似如下:

xml 复制代码
<update id="updateById" parameterType="com.example.WzkUser">
    update wzk_user
    set username = #{username},
        password = #{password}
    where id = #{id}
</update>

为了观察 SQL 是否执行,需要开启 MyBatis 日志。例如可以在 MyBatis 配置中使用:

xml 复制代码
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

如果项目使用 Logback、Log4j2 或 Spring Boot,也可以通过日志框架把 Mapper 包或 MyBatis 相关包调整到 DEBUG 级别。


实验一:同一个 SqlSession 内重复查询

下面的代码在同一个 SqlSession 中连续执行两次 findAll()

java 复制代码
public class WzkicuCache01 {

    public static void main(String[] args) throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        try {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            List<WzkUser> wzkUser = userMapper.findAll();
            System.out.println(wzkUser);

            List<WzkUser> wzkUser2 = userMapper.findAll();
            System.out.println(wzkUser2);
        } finally {
            sqlSession.close();
        }
    }
}

原文中的控制台截图如下:

从现象上看,两次 findAll() 之间没有第二次 SQL 打印。

原因是:

第一次查询时,本地缓存中还没有对应的 CacheKey,MyBatis 会访问数据库,并把查询结果放入当前 SqlSession 的一级缓存。

第二次查询时,SqlSession 没有关闭,SQL、参数、分页条件等信息也没有变化,因此生成的 CacheKey 相同,MyBatis 可以直接从本地缓存中返回结果。

这里要注意一个细节:命中一级缓存的前提不是"查询了同一张表",而是"生成的 CacheKey 一致"。SQL 语句、参数、分页条件、MappedStatement id 等因素都会影响缓存 key。


实验二:查询后执行 UPDATE,缓存为什么失效

下面的示例在两次查询中间加入一次 updateById(),然后执行 commit()

java 复制代码
public class WzkicuCache02 {

    public static void main(String[] args) throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        try {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            // 第一次查询
            List<WzkUser> wzkUser = userMapper.findAll();
            System.out.println(wzkUser);

            // 执行一次 UPDATE
            WzkUser wzkUserUpdate = WzkUser
                    .builder()
                    .id(1)
                    .username("wzk-update")
                    .password("123-update")
                    .build();

            userMapper.updateById(wzkUserUpdate);

            // 提交事务
            sqlSession.commit();

            // 再次查询
            List<WzkUser> wzkUser2 = userMapper.findAll();
            System.out.println(wzkUser2);
        } finally {
            sqlSession.close();
        }
    }
}

原文中的控制台截图如下:

这一次可以观察到执行顺序变成了:

text 复制代码
第一次查询 SQL
UPDATE SQL
第二次查询 SQL

这说明第二次查询没有复用第一次查询的缓存。

这里需要把原因说准确:不是只有 commit() 会清空缓存,update() 本身也会清空当前 SqlSession 的本地缓存。

在 MyBatis 的执行流程中,只要发生数据修改操作,本地缓存就不应该继续保留旧查询结果。否则,同一个 session 后续查询可能读到修改前的数据。

所以这个实验验证的是:

在同一个 SqlSession 中,查询之后如果执行了写操作,原来的一级缓存会失效,后续相同查询需要重新访问数据库。


一级缓存的基本工作流程

可以把一级缓存的流程简化为下面几步:

第一次查询:

  1. MyBatis 根据当前查询生成 CacheKey
  2. 到当前 SqlSession 的本地缓存中查找。
  3. 如果没有命中,执行数据库查询。
  4. 查询结果写入本地缓存。
  5. 返回查询结果。

第二次相同查询:

  1. 再次生成 CacheKey
  2. 到当前 SqlSession 的本地缓存中查找。
  3. 如果命中,直接返回缓存结果。
  4. 不再执行 SQL。

中间发生写操作或事务操作:

  1. 执行 insertupdatedelete 时清空本地缓存。
  2. 执行 commitrollback 时清空本地缓存。
  3. 调用 clearCache() 时清空本地缓存。
  4. SqlSession 关闭后,本地缓存随 session 生命周期结束。

关键配置解释:localCacheScope

MyBatis 提供了一个和一级缓存相关的重要配置:

xml 复制代码
<settings>
    <setting name="localCacheScope" value="SESSION"/>
</settings>

默认值是 SESSION

配置值 含义
SESSION 默认值。同一个 SqlSession 内,查询结果会在整个 session 生命周期内复用。
STATEMENT 本地缓存只在语句执行期间使用,不会在同一个 SqlSession 的多次调用之间共享数据。

如果配置为 STATEMENT,前面"同一个 session 连续查询两次只打印一次 SQL"的现象通常就不会出现。因为每条语句执行结束后,本地缓存就会被清空。

示例配置:

xml 复制代码
<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

STATEMENT 并不等于完全关闭一级缓存。MyBatis 仍然会在语句执行过程中使用本地缓存,只是不会把缓存数据保留到下一次查询调用。


源码链路:DefaultSqlSession → Executor → BaseExecutor → PerpetualCache

从调用链看,一次 Mapper 查询大致会经过以下层级:

text 复制代码
Mapper 接口方法
    ↓
DefaultSqlSession
    ↓
Executor
    ↓
BaseExecutor
    ↓
PerpetualCache

原文中的源码截图如下:

一级缓存的核心字段在 BaseExecutor 中:

java 复制代码
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;

创建 BaseExecutor 时,会初始化本地缓存:

java 复制代码
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");

PerpetualCache 是 MyBatis 的一个缓存实现。当前 3.5.x 源码中,它内部维护的是一个 Map<Object, Object>,具体实现是 HashMap

可以简化理解为:

java 复制代码
private final Map<Object, Object> cache = new HashMap<>();

当调用 clear() 时,本质上就是清空这个 Map:

java 复制代码
@Override
public void clear() {
    cache.clear();
}

原文中的源码截图如下:


CacheKey 如何生成

一级缓存能否命中,关键在于 CacheKey 是否一致。

BaseExecutor 中,查询前会创建 CacheKey

CacheKey 不是简单地只用 SQL 字符串作为 key。它通常会综合以下信息:

组成部分 作用
MappedStatement id 区分不同 Mapper 方法
RowBounds offset 区分页偏移
RowBounds limit 区分页大小
BoundSql sql 区分最终 SQL 文本
参数值 区分不同查询条件
Environment id 区分不同环境配置

因此,下面这些情况都可能导致一级缓存不命中:

  • Mapper 方法不同;
  • SQL 文本不同;
  • 参数不同;
  • 分页条件不同;
  • 动态 SQL 最终生成结果不同;
  • 不在同一个 SqlSession 中。

原文中提到"SQL 和参数作为键"是一个便于理解的简化说法。更准确的说法是:MyBatis 会基于查询上下文生成 CacheKey,SQL 和参数只是其中最重要的组成部分。


查询时如何使用一级缓存

BaseExecutor.query() 中,核心逻辑可以简化为:

java 复制代码
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

也就是:

  1. 先通过 CacheKeylocalCache 查询。
  2. 如果命中,直接返回。
  3. 如果未命中,调用 queryFromDatabase() 查询数据库。
  4. 数据库查询结果会写入本地缓存。

原文中的源码截图如下:


一级缓存的命中条件

一级缓存命中通常需要同时满足以下条件:

条件 说明
同一个 SqlSession 不同 session 之间一级缓存隔离
相同 Mapper 语句 MappedStatement id 需要一致
相同 SQL 动态 SQL 最终生成结果需要一致
相同参数 查询参数值需要一致
相同分页条件 RowBounds 信息会影响 CacheKey
中间没有清空缓存 没有执行写操作、事务操作或 clearCache()
localCacheScope=SESSION 如果是 STATEMENT,跨语句复用会失效

示例:

java 复制代码
List<WzkUser> list1 = userMapper.findAll();
List<WzkUser> list2 = userMapper.findAll();

如果这两次调用发生在同一个 SqlSession 中,并且中间没有写操作或清理缓存,就可能命中一级缓存。


一级缓存的失效场景

一级缓存失效主要有以下几类。

1. 使用了不同的 SqlSession

java 复制代码
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();

sqlSession1sqlSession2 各自有独立的本地缓存。

即使两次查询 SQL 完全相同,只要不在同一个 session 中,就不能共享一级缓存。


2. 执行了 insert、update、delete

java 复制代码
userMapper.findAll();

userMapper.updateById(user);

userMapper.findAll();

执行写操作后,当前 session 的本地缓存会被清空。

原因是写操作可能改变数据库状态。为了避免后续查询继续读取旧缓存,MyBatis 会主动清理本地缓存。


3. 执行了 commit 或 rollback

java 复制代码
userMapper.findAll();

sqlSession.commit();

userMapper.findAll();

commit()rollback() 都属于事务边界操作。事务状态发生变化后,本地缓存继续保留可能造成数据一致性问题,因此会被清空。


4. 手动调用 clearCache()

java 复制代码
userMapper.findAll();

sqlSession.clearCache();

userMapper.findAll();

clearCache() 用于主动清空当前 SqlSession 的本地缓存。

如果怀疑缓存影响后续查询,可以手动调用它。但正常业务代码中,不建议到处调用 clearCache() 掩盖事务边界设计问题。


5. SQL 或参数发生变化

下面两次查询不会共用同一个 CacheKey

java 复制代码
userMapper.findById(1L);
userMapper.findById(2L);

即使查询的是同一张表,只要参数不同,就不会命中同一个缓存项。

动态 SQL 也需要注意:

xml 复制代码
<select id="findByCondition" resultType="WzkUser">
    select * from wzk_user
    <where>
        <if test="username != null">
            username = #{username}
        </if>
    </where>
</select>

如果传入条件不同,最终生成的 SQL 可能不同,CacheKey 也会不同。


6. localCacheScope 设置为 STATEMENT

如果配置为:

xml 复制代码
<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

那么本地缓存不会在同一个 SqlSession 的多次查询调用之间共享。

这种配置更保守,适合不希望查询结果在 session 内被复用的场景。


验证方式

原文已经通过控制台日志截图观察到一级缓存现象。为了让验证更清晰,可以按下面方式补充验证。

验证一:同一个 SqlSession 连续查询

执行代码:

java 复制代码
List<WzkUser> list1 = userMapper.findAll();
List<WzkUser> list2 = userMapper.findAll();

观察点:

  • 控制台是否只打印一次 Preparing: select ...
  • 第二次查询是否没有再次输出 SQL
  • 两次查询是否在同一个 SqlSession

预期现象:

text 复制代码
第一次查询:执行 SQL
第二次查询:不执行 SQL,命中一级缓存

不要直接伪造日志。不同日志框架、不同 MyBatis 配置下,输出格式可能不同。


验证二:查询后执行 UPDATE

执行代码:

java 复制代码
List<WzkUser> list1 = userMapper.findAll();

userMapper.updateById(user);
sqlSession.commit();

List<WzkUser> list2 = userMapper.findAll();

观察点:

  • 第一次查询是否打印 SQL;
  • updateById() 是否打印 UPDATE SQL;
  • 第二次 findAll() 是否重新打印 SELECT SQL。

预期现象:

text 复制代码
第一次查询:执行 SQL
UPDATE:执行 SQL,并清空本地缓存
commit:提交事务,并清空本地缓存
第二次查询:重新执行 SQL

验证三:手动 clearCache

可以增加一个单独实验:

java 复制代码
List<WzkUser> list1 = userMapper.findAll();

sqlSession.clearCache();

List<WzkUser> list2 = userMapper.findAll();

预期现象:

text 复制代码
第一次查询:执行 SQL
clearCache:清空当前 SqlSession 本地缓存
第二次查询:重新执行 SQL

验证四:设置 localCacheScope=STATEMENT

修改配置:

xml 复制代码
<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

然后执行:

java 复制代码
List<WzkUser> list1 = userMapper.findAll();
List<WzkUser> list2 = userMapper.findAll();

预期现象:

text 复制代码
第一次查询:执行 SQL
第二次查询:重新执行 SQL

这个实验可以帮助理解 SESSIONSTATEMENT 的区别。


常见问题

1. 一级缓存是不是默认开启?

是。MyBatis 默认会使用本地缓存。

但默认开启不等于可以跨 session 使用。一级缓存只在当前 SqlSession 内有效。


2. 一级缓存能不能关闭?

严格来说,MyBatis 的本地缓存不能完全关闭,因为它还承担了处理循环引用和嵌套查询的内部职责。

如果不希望同一个 SqlSession 内跨语句复用查询结果,可以把 localCacheScope 设置为 STATEMENT

xml 复制代码
<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

3. 一级缓存和二级缓存有什么区别?

对比项 一级缓存 二级缓存
作用范围 SqlSession Mapper namespace
默认状态 默认开启 需要配置
生命周期 随 session 结束而结束 可以跨 session
使用复杂度 低,开发者通常无感 更高,需要关注序列化、刷新策略、一致性
适用场景 同一 session 内重复查询 特定读多写少场景

一级缓存是基础机制,二级缓存是更显式的缓存能力。二者不能混为一谈。


4. Spring 项目中一级缓存还存在吗?

存在,但观察方式不同。

在 Spring + MyBatis 项目中,通常不会手动创建 SqlSession,而是通过 SqlSessionTemplate 和事务管理器托管。

如果方法运行在同一个事务中,底层可能复用同一个 session,从而出现一级缓存命中的情况。

如果没有事务边界,或者每次 Mapper 调用对应不同 session,就不一定能观察到连续查询命中一级缓存的现象。

因此,在 Spring 项目中讨论一级缓存时,要结合事务边界一起看。


5. 查询结果对象可以修改吗?

不建议直接修改 MyBatis 返回的对象或集合后,又在同一个 SqlSession 中依赖相同查询结果。

原因是当 localCacheScope=SESSION 时,MyBatis 可能返回本地缓存中的同一个对象引用。你对返回对象的修改,可能影响当前 session 后续从缓存中取出的结果。

如果确实需要修改对象,建议明确区分"查询结果对象"和"用于更新的对象",不要把本地缓存当成对象状态管理工具。


6. 一级缓存能解决性能问题吗?

不能把一级缓存当成主要性能优化手段。

一级缓存只解决同一个 SqlSession 内重复查询的问题。对于跨请求、跨线程、跨服务的重复查询,它没有作用。

真正的性能优化通常应该优先考虑:

  • SQL 是否合理;
  • 索引是否命中;
  • 是否存在 N+1 查询;
  • 是否需要分页;
  • 是否需要业务缓存;
  • 是否需要读写分离;
  • 是否需要改造数据模型。

一级缓存只是 MyBatis 的局部优化机制。


错误速查卡

症状 可能原因 定位方式 处理方式
两次相同查询都执行了 SQL 两次查询不在同一个 SqlSession 检查 session 创建位置和事务边界 确认是否需要在同一事务或同一 session 内执行
第二次查询没有执行 SQL 一级缓存命中 查看是否同一 session、相同 SQL、相同参数 正常现象,不需要处理
UPDATE 后第二次查询重新执行 SQL 写操作清空了本地缓存 查看两次查询之间是否执行了 insert/update/delete 正常现象,符合一致性设计
手动 clearCache() 后缓存失效 主动清空了本地缓存 搜索代码中的 clearCache() 删除不必要的清理,或保留明确注释
设置 localCacheScope=STATEMENT 后无法复用缓存 缓存范围被缩小到语句级别 检查 MyBatis settings 如果需要 session 级复用,改回 SESSION
查询相同表但没有命中缓存 SQL、参数、分页或 Mapper statement 不同 对比最终 SQL、参数和 Mapper 方法 确保生成的 CacheKey 一致
Spring 项目中现象和 main 方法不同 SqlSession 由 Spring 托管 检查事务传播和 Mapper 调用边界 结合事务分析,不要直接套用 main 方法结论

适用边界

一级缓存适合用来理解 MyBatis 的执行机制,但不适合直接当业务缓存使用。

适用场景:

  • 学习 MyBatis 查询执行流程;
  • 分析为什么重复查询没有打印 SQL;
  • 理解 SqlSession 生命周期;
  • 排查同一事务内查询结果复用问题;
  • 阅读 BaseExecutorCacheKeyPerpetualCache 源码。

不适用场景:

  • 跨请求缓存;
  • 跨线程缓存;
  • 跨服务缓存;
  • 高并发业务缓存;
  • 数据强一致性要求很高但事务边界不清晰的场景;
  • 希望通过一级缓存替代 Redis、本地缓存组件或数据库优化的场景。

生产环境中,不建议为了"提高缓存命中率"而刻意延长 SqlSession 生命周期。SqlSession 应该短生命周期使用,并且不能跨线程共享。


总结

MyBatis 一级缓存是默认开启的本地缓存,作用范围是当前 SqlSession

它的核心逻辑可以概括为:

  1. 查询时,MyBatis 根据当前查询上下文生成 CacheKey
  2. 如果当前 SqlSession 的本地缓存中存在该 key,就直接返回缓存结果。
  3. 如果没有命中,就查询数据库,并把结果写入本地缓存。
  4. 当执行写操作、事务提交、事务回滚、关闭 session 或手动清理缓存时,本地缓存会失效。
  5. 如果配置 localCacheScope=STATEMENT,缓存只在语句执行期间有效,不会在多次查询调用之间复用。

理解一级缓存的重点不是"记住它底层是 HashMap",而是理解它和 SqlSessionCacheKey、事务边界之间的关系。

在日常开发中,一级缓存通常不需要手动配置。但当你看到"同样的查询为什么没有再次打印 SQL"或者"为什么更新后查询重新执行 SQL"时,就需要知道:这背后就是 MyBatis 一级缓存和缓存失效机制在起作用。


作者:武子康的个人博客

相关推荐
寻道码路1 小时前
LangChain4j Java AI 应用开发实战(十):Embedding 模型与文本分类 - 语义向量化
java·人工智能·ai·embedding
折哥的程序人生 · 物流技术专研1 小时前
Java 23 种设计模式:从踩坑到精通 | 抽象工厂 —— 支付/收款如何成套创建?跨平台 UI 如何一键换肤?
java·开发语言·后端·设计模式
方也_arkling1 小时前
【Java-Day11】抽象类和抽象方法
java·开发语言
XS0301062 小时前
并发编程 七
java
YikNjy2 小时前
string(c++)
java·服务器·c++
小江的记录本2 小时前
【Spring AI】Spring AI中RAG误触发与系统提示词泄露问题解决方案(完整版+代码方案)
java·人工智能·spring boot·后端·python·spring·面试
勇往直前plus2 小时前
Python 属性访问与操作全解析:内置函数、魔法方法与描述符深度指南
java·网络·python
Arenaschi3 小时前
关于GPT的版特点
java·网络·人工智能·windows·python·gpt
人道领域3 小时前
【LeetCode刷题日记】108.将有序数组转换为二叉搜索树
java·算法·leetcode