Mybatis 两级缓存可能导致的问题

Mybatis 两级缓存可能导致的问题

    • 两级缓存简介
      • 一级缓存 localCache
        • 效果
        • 开关
      • 二级缓存
    • 两级缓存可能导致的问题
      • 分布式环境下查询到过期数据
      • 事务隔离级别失效
        • 读已提交失效
        • 读未提交失效
    • 总结

两级缓存简介

一级缓存 localCache

效果

一级缓存是 session 或者说事务级别的,只在同一事务内有效,在以相同的参数执行多次同一个查询方法时,实际只会在第一次时进行数据库 select 查询,后续会直接从缓存中返回。如下:

@GetMapping("/test1")
@Transactional(rollbackFor = Exception.class)
public String test1() {log.info("---------------------------------------------------------------------------");Teacher teacher1 = teacherMapper.selectByPrimaryKey("01");log.info("teacher1: {}, hashCode: {} \n", teacher1, System.identityHashCode(teacher1));Teacher teacher2 = teacherMapper.selectByPrimaryKey("01");log.info("teacher2: {}, hashCode: {} \n", teacher2, System.identityHashCode(teacher2));Student student1 = studentMapper.selectByPrimaryKey("01");log.info("student1: {}, hashCode: {} \n", student1, System.identityHashCode(student1));Student student2 = studentMapper.selectByPrimaryKey("01");log.info("student2: {}, hashCode: {} \n", student2, System.identityHashCode(student2));return "test1";
}

下图中是调用了两次的输出,从第一次输出中可以看出查询 teacher、student 的 SQL 都只打印了一遍,说明分别只执行了一次数据库查询。且两个 teacher、student 的 hashCode 分别是一样的,说明是同一个对象。第二次调用的输出和第一次的相似,都重新执行了一次数据库查询,说明一级缓存只在同一事务内有效,不能跨事务。

image-20250713102056458

如果事务中有 DML 语句的话,会清空所有的缓存。不管 DML 语句中的表是否与缓存中的表相同,都会无条件的清空所有缓存。

@GetMapping("/test2")
@Transactional(rollbackFor = Exception.class)
public String test2() {log.info("---------------------------------------------------------------------------");Teacher teacher1 = teacherMapper.selectByPrimaryKey("01");log.info("teacher1: {}, hashCode: {} \n", teacher1, System.identityHashCode(teacher1));Teacher teacher2 = teacherMapper.selectByPrimaryKey("01");log.info("teacher2: {}, hashCode: {} \n", teacher2, System.identityHashCode(teacher2));Student student1 = studentMapper.selectByPrimaryKey("01");log.info("student1: {}, hashCode: {} \n", student1, System.identityHashCode(student1));Student student2 = studentMapper.selectByPrimaryKey("01");log.info("student2: {}, hashCode: {} \n", student2, System.identityHashCode(student2));insertScore();log.info("insertScore\n");Teacher teacher3 = teacherMapper.selectByPrimaryKey("01");log.info("teacher3: {}, hashCode: {} \n", teacher3, System.identityHashCode(teacher3));Student student3 = studentMapper.selectByPrimaryKey("01");log.info("student3: {}, hashCode: {} \n", student3, System.identityHashCode(student3));return "test2";
}private void insertScore() {Score score = new Score();score.setSId("08");score.setCId("01");score.setSScore(100);scoreMapper.insert(score);
}

前半部分的输出与 test1 相同,当插入 score 后再次查询 teacher、student 时,打印了 SQL,且与上半部分的 hashCode 不相同,说明执行 insertScore 时缓存被全部清空了。

image-20250713103010097

开关

一级缓存在 mybatis 源码中被称为 localCache,springboot 可使用 mybatis.configuration.local-cache-scope 来控制其行为,默认值是 session,也就是事务级别的缓存。可将其配置为 statement 以关闭 localCache 功能。

下面是将 mybatis.configuration.local-cache-scope 配置为 statement 后再执行 test1 的输出,每次都打印了 SQL,且 hashCode 都不一样,说明缓存没有起作用。

image-20250713104126602

二级缓存

二级缓存是 namespace 级别的(或者说是 Mapper 级别的,如下 xml),与一级缓存类似,在以相同的参数执行多次同一个查询方法时,实际只会在第一次时进行数据库 select 查询,后续会直接从缓存中返回。如果执行同一个 namespace 中的 DML 语句(比如 delete、insert、update)的话,会清空 namespace 相关的所有 select 的缓存。

<?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="org.example.mybatis.mapper.StudentMapper"><select>...</select><delete>...</delete><insert>...</insert>...
</mapper>

二级缓存由 mybatis.configuration.cache-enabled 控制,默认为 true。除此之外还需要在要开启二级缓存的 Mapper.xml 中添加 <cache/> 表情才能开启对应 Mapper 的二级缓存。

下面是在关闭一级缓存,且只开启 StudentMapper.xml 二级缓存的情况下的测试:

application.properties

...
mybatis.configuration.local-cache-scope=statement
mybatis.configuration.cache-enabled=true

StudentMapper.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="org.example.mybatis.mapper.StudentMapper"><resultMap id="BaseResultMap" type="org.example.mybatis.entity.Student"><!--@mbg.generated--><!--@Table student--><id column="s_id" jdbcType="VARCHAR" property="sId" /><result column="s_name" jdbcType="VARCHAR" property="sName" /><result column="s_birth" jdbcType="VARCHAR" property="sBirth" /><result column="s_sex" jdbcType="VARCHAR" property="sSex" /></resultMap><cache readOnly="true"/>...
</mapper>

这是执行了两次 test1 的输出:

由于没有开启 TeacherMapper.xml 的二级缓存,所以每次查询 teacher 都打印了 SQL,且 hashCode 不相同,说明 teacher 的缓存没起作用。

第 ① 次查询 student 打印了 SQL,直接查询了数据库,这是正常的,因为此时缓存中没有数据。但第 ② 次查询 student 也没有走缓存,也直接查询了数据库,这是为啥?是因为二级缓存不是在执行完 select 后立即填充的,是要等到事务提交之后才会填充缓存。

从最后几行的输出能看出最后两次查询 student 确实走了缓存,并且还打印了缓存命中率。这是因为第一次调用 test1 结束后事务提交了,数据被填充到了缓存里。

image-20250713121621685

测试无事务时的效果

test3 是在 test1 的基础上删除了 @Transactional 注解

@GetMapping("/test3")
public String test3() {log.info("---------------------------------------------------------------------------");Teacher teacher1 = teacherMapper.selectByPrimaryKey("01");log.info("teacher1: {}, hashCode: {} \n", teacher1, System.identityHashCode(teacher1));Teacher teacher2 = teacherMapper.selectByPrimaryKey("01");log.info("teacher2: {}, hashCode: {} \n", teacher2, System.identityHashCode(teacher2));Student student1 = studentMapper.selectByPrimaryKey("01");log.info("student1: {}, hashCode: {} \n", student1, System.identityHashCode(student1));Student student2 = studentMapper.selectByPrimaryKey("01");log.info("student2: {}, hashCode: {} \n", student2, System.identityHashCode(student2));return "test3";
}

teacher 的缓存还是没起作用。

只有第一次查询 student 时直接查询了数据库,其他三次都命中了缓存。

image-20250713123335310

两级缓存可能导致的问题

分布式环境下查询到过期数据

假设支付服务 A 有两个实例 A1、A2,负载均衡采用轮训策略,第一次查询余额访问 A1 返回 100000,第二次消费 100 访问 A2 返回余额 99900,第三次查询余额访问 A1 返回的还是 100000。如下的模拟

application.properties

...
mybatis.configuration.local-cache-scope=statement
mybatis.configuration.cache-enabled=true

AccountMapper.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="org.example.mybatis.mapper.AccountMapper">...<cache readOnly="true"/><update id="pay">update accountset balance = balance - #{amount}where id = #{id}</update>
</mapper>
@GetMapping("/balance")
public Long queryBalance() {return accountMapper.selectByPrimaryKey(1).getBalance();
}@GetMapping("/pay")
public Long pay() {accountMapper.pay(1, 100);return accountMapper.selectByPrimaryKey(1).getBalance();
}

分别在 8080、8081 启动两个实例,如下输出:

image-20250713130221849

要解决这个问题很简单,就是不使用缓存,比如 mybatis.configuration.cache-enabled=false 或者将 AccountMapper.xml 中的 <cache/> 标签删除。

事务隔离级别失效

读已提交失效

在开发中经常有这种场景:先判断是否存在,如果不存在再插入。这种判断再插入的操作不是原子的,多线程会有问题,所以需要加锁保证操作的安全性。在读多写少的场景中,会使用 double check 来尽可能的减少用锁的使用,伪代码如下:

def doubleCheck(id) {o = select(id);if (o == null) {lock.lock();try {o = select(id);if (o == null) {o = create(id);}} finally {lock.unlock();}}return o;
}

创建 Account 的测试

application.properties

还原成默认值,且删除 AccountMapper.xml 中的 <cache/> 标签,用以关闭 AccountMapper 的二级缓存。

...
mybatis.configuration.local-cache-scope=session
mybatis.configuration.cache-enabled=true

注意这里使用的隔离级别为读已提交

@PutMapping("/accounts/{id}")
// double check 需要使用读已提交隔离级别才能读到最新数据
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
public Account createAccount(@PathVariable("id") Integer id) throws InterruptedException {Account account = accountMapper.selectByPrimaryKey(id);// 等待多个请求到达TimeUnit.SECONDS.sleep(5);// 如果账户不存在,需要加分布式锁后进行 double check,防止并发问题if (account == null) {RLock lock = redissonClient.getLock("lock:account:create:" + id);boolean locked = lock.tryLock(10, TimeUnit.SECONDS);if (locked) {try {account = accountMapper.selectByPrimaryKey(id);if (account == null) {// 创建账户account = createAccount0(id);}} finally {lock.unlock();}}}return account;
}public Account createAccount0(Integer id) {Account account = new Account();account.setId(id);account.setBalance(0L);accountMapper.insertSelective(account);// 操作其他表return account;
}

同时发起两个 Put 请求 http://localhost:8080/accounts/2。一个正常返回,另一个在 insert 时报错 Duplicate entry ‘2’ for key ‘account.PRIMARY’,说明读已提交的隔离级别没起作用,第二个请求没有读到最新的数据。

一级缓存实际起到了类似可重复读的效果。

image-20250713140828986

两个请求(线程分别为 nio-8080-exec-3、nio-8080-exec-4)执行了 3 次(第一个请求 1 次,第二个请求 2 次) accountMapper.selectByPrimaryKey(id),但每个线程都只打印了 1 次 SQL,说明第二个请求的第 2 次查询走了缓存,导致没有查询到第一个请求插入的最新数据,才导致的后来的报错。

image-20250713140226289

解决办法

  1. 最简单办法就是修改 mybatis.configuration.local-cache-scope=statement,直接关闭一级缓存。

  2. 直接去掉 @Transactional 注解肯定能解决问题,但如果 createAccount0 方法中操作多张表的话,如果部分失败事务将无法回滚。

  3. 不能直接去掉 @Transactional 注解,但可以缩小事务的范围,将两次查询放到事务外,只将 createAccount0 方法放到事务内。

    @Lazy
    @Autowired
    private TestController self;@PutMapping("/accounts/{id}")
    public Account createAccount(@PathVariable("id") Integer id) throws InterruptedException {Account account = accountMapper.selectByPrimaryKey(id);// 等待多个请求到达TimeUnit.SECONDS.sleep(5);// 如果账户不存在,需要加分布式锁后进行 double check,防止并发问题if (account == null) {RLock lock = redissonClient.getLock("lock:account:create:" + id);boolean locked = lock.tryLock(10, TimeUnit.SECONDS);if (locked) {try {account = accountMapper.selectByPrimaryKey(id);if (account == null) {// 创建账户account = self.createAccount0(id);}} finally {lock.unlock();}}}return account;
    }@Transactional(rollbackFor = Exception.class)
    public Account createAccount0(Integer id) {Account account = new Account();account.setId(id);account.setBalance(0L);accountMapper.insertSelective(account);// 操作其他表return account;
    }
    
  4. 如果外层有其他事务的话,由于一级缓存只有在同一个事务中才会生效,所以可以将两个 accountMapper.selectByPrimaryKey(id) 拆分到不同的事务中,propagation 必须是 Propagation.REQUIRES_NEW

    @Lazy
    @Autowired
    private TestController self;@PutMapping("/accounts/{id}")
    public Account createAccount(@PathVariable("id") Integer id) throws InterruptedException {Account account = self.getAccount0(id);// 等待多个请求到达TimeUnit.SECONDS.sleep(5);// 如果账户不存在,需要加分布式锁后进行 double check,防止并发问题if (account == null) {RLock lock = redissonClient.getLock("lock:account:create:" + id);boolean locked = lock.tryLock(10, TimeUnit.SECONDS);if (locked) {try {account = self.getAccount0(id);if (account == null) {// 创建账户//account = self.createAccount0(id);}} finally {lock.unlock();}}}return account;
    }// 读已提交 REQUIRES_NEW
    @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
    public Account getAccount0(Integer id) {return accountMapper.selectByPrimaryKey(id);
    }
    
读未提交失效

同样的由于一级缓存的存在,读未提交也读不到最新的未提交数据。

读未提交 查询 Account 的测试

application.properties

还原成默认值,且删除 AccountMapper.xml 中的 <cache/> 标签,用以关闭 AccountMapper 的二级缓存。

...
mybatis.configuration.local-cache-scope=session
mybatis.configuration.cache-enabled=true
@GetMapping("/accounts/{id}")
// 读未提交
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_UNCOMMITTED)
public Account getAccount(@PathVariable("id") Integer id) throws InterruptedException {Account account = accountMapper.selectByPrimaryKey(id);log.info("account1: {}\n", account);// 若不存在,则等待几秒再查if (account == null) {TimeUnit.SECONDS.sleep(10);}account = accountMapper.selectByPrimaryKey(id);log.info("account2: {}\n", account);return account;
}@PutMapping("/accounts/{id}")
@Transactional(rollbackFor = Exception.class)
public Account createAccount(@PathVariable("id") Integer id) throws InterruptedException {Account account = new Account();account.setId(id);account.setBalance(0L);accountMapper.insertSelective(account);log.info("insert account: {}\n", account);// 延迟提交事务TimeUnit.SECONDS.sleep(15);// 操作其他表return account;
}

先请求 getAccount 再请求 createAccount,从输出中可以看出,在使用读未提交的情况下,account2 依旧为 null,走了缓存,导致读未提交失效。

image-20250713152132113

解决办法

  1. 最简单办法就是修改 mybatis.configuration.local-cache-scope=statement,直接关闭一级缓存。

  2. 由于一级缓存只有在同一个事务中才会生效,所以可以将两个 accountMapper.selectByPrimaryKey(id) 拆分到不同的事务中,propagation 必须是 Propagation.REQUIRES_NEW

    @Lazy
    @Autowired
    private TestController self;@GetMapping("/accounts/{id}")
    public Account getAccount(@PathVariable("id") Integer id) throws InterruptedException {Account account = self.getAccount0(id);log.info("account1: {}\n", account);// 若不存在,则等待几秒再查if (account == null) {TimeUnit.SECONDS.sleep(10);}account = self.getAccount0(id);log.info("account2: {}\n", account);return account;
    }// 读未提交 REQUIRES_NEW
    @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRES_NEW)
    public Account getAccount0(Integer id) {return accountMapper.selectByPrimaryKey(id);
    }
    

总结

一级缓存是事务级别的,实际起到了类似可重复读的效果,而且比可重复读的性能更好,因为多次查询的话不会请求数据库了。在事务隔离级别是可重复读时使用一级缓存能提高性能。但就因为其类似可重复读的效果会导致其他的隔离级别失效。要解决失效的问题,最简单方式就是关闭一级缓存,但这样会损失性能。另一个解决办法是将需要使用其他隔离级别的方法使用 propagation = Propagation.REQUIRES_NEW 拆分到新的事务中。如果是读已提交的话可通过缩小事务范围的方式解决。

一级缓存是事务级别的,缓存的生命周期较短,但二级缓存是 namespace (Mapper)级别的,生命周期可能很长,在分布式、多实例环境中很容易查询到过期的数据,导致其他问题。我个人建议在分布式、多实例环境中应该设置 mybatis.configuration.cache-enabled=false 来关闭二级缓存,从根源上杜绝这种问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/88933.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/88933.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue3+uniapp 使用vue-plugin-hiprint中实现打印效果

前言&#xff1a; vue3uniapp 使用vue-plugin-hiprint中实现打印效果 官网地址&#xff1a;gitee https://gitee.com/ccsimple/vue-plugin-hiprinthttps://gitee.com/ccsimple/vue-plugin-hiprint 实现效果&#xff1a; 预览打印内容&#xff1a; 实现步骤&#xff1a; 1、安…

【elementUI踩坑记录】解决 el-table 固定列 el-table__fixed 导致部分滚动条无法拖动的问题

目录一、问题背景二、 问题现象三、核心原因四、解决办法增强方案&#x1f680;写在最后一、问题背景 在使用 Element UI 的 el-table 组件时&#xff0c;固定列功能虽然实用&#xff0c;但会引发滚动条交互问题&#xff1a; 固定列区域悬浮显示滚动条但无法正常拖动滚动条 …

【机器人编程基础】python文件的打开和关闭

文件的打开和关闭 在Python中,文件操作是一项基本而重要的任务,涉及到打开、读取、写入、关闭文件等操作。正确地管理文件对于数据持久化、输入输出处理等至关重要。下面将详细解释如何在Python中打开和关闭文件,并提供相应的代码示例。 文件打开 在Python中,可以使用内…

ShenYu实战、问题记录

概述 一款高性能的国产的Apache开源API网关&#xff0c;官方文档。 在ShenYu v2.6.1, ShenYu注册中心只支持http类型&#xff0c;中间件注册类型已经被移除。 所以&#xff0c;请使用http注册类型来注册你的服务。不是微服务注册中心&#xff0c;它只是将元数据、选择器数据、…

走近科学IT版:EasyTire设置了ip,但是一闪之后就变回到原来的dhcp获得的地址

EasyTier 是一款简单、安全、去中心化的内网穿透和异地组网工具&#xff0c;适合远程办公、异地访问、游戏加速等多种场景。无需公网 IP&#xff0c;无需复杂配置&#xff0c;轻松实现不同地点设备间的安全互联。 上次实践的记录&#xff1a;适合远程办公、异地访问的EasyTier…

rk3588平台USB 3.0 -OAK深度相机适配方法

目录 文件更改记录表 1、usb规则添加 2、拉取相关依赖 3、安装python3、安装pip 4、安装依赖 5、安装ffmeg 6、摄像头功能测试 7、将视频拷贝到U盘查看 1、usb规则添加 由于OAK是USB设备,因此为了在使用 udev 工具的系统上与之通信, 您需要添加udev规则以使…

工厂模式总结

工厂模式1. 简单工厂模式&#xff08;Simple Factory&#xff09; 核心思想 定义一个工厂类&#xff0c;根据输入参数创建不同的具体对象。客户端不直接调用具体类的构造函数&#xff0c;而是通过工厂类获取对象。 示例代码 #include <iostream> #include <memory>…

MySQL的三种安装方式(mis、zip、yum)

目录 2.0数据库安装 2.1windows上.mis格式 环境准备 MySQL的安装 环境配置&#xff08;非必要&#xff09; 2.2windows上.zip格式安装 环境准备 配置文件的内容 MySQL的安装 附录可能出现问题 图形工具远程连接数据库 2.3Linux上安装yum包 环境准备 过程命令 My…

串口学习和蓝牙通信HC05(第八天)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-削好皮的Pineapple! &#x1f468;‍&#x1f4bb; hello 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 削好皮的Pineapple! 原创 &#x1f468;‍&#x1f4b…

设计总监的“轻量化”新武器:用Adobe Express,音频一键驱动动画

在快节奏的创意项目中&#xff0c;如何将复杂的设计理念或冗长的研究报告&#xff0c;快速转化为易于理解、富有吸引力的动态内容&#xff0c;是衡量一个团队沟通效率的关键。作为一名在海外设计界工作了十余年的设计师&#xff0c;我发现&#xff0c;最高效的团队&#xff0c;…

零知开源——STM32F407VET6驱动SHT41温湿度传感器完整教程

✔零知开源是一个真正属于国人自己的开源软硬件平台&#xff0c;在开发效率上超越了Arduino平台并且更加容易上手&#xff0c;大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码&#xff0c;让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品&…

Linux流量分析:tcpdump wireshark

前言 最近因为工作需要&#xff0c;研究了下如何使用tcpdump和wireshark分析业务流量。如果要使用tcpdump分析具体的HTTP请求耗时&#xff0c;需捕获网络数据包并分析时间戳信息&#xff0c;重点关注TCP连接的建立、HTTP请求发送到响应接收的全过程。 以下是具体步骤和技巧&…

深度学习图像分类数据集—角膜溃疡识别分类

该数据集为图像分类数据集&#xff0c;适用于ResNet、VGG等卷积神经网络&#xff0c;SENet、CBAM等注意力机制相关算法&#xff0c;Vision Transformer等Transformer相关算法。 数据集信息介绍&#xff1a;角膜溃疡识别分类&#xff1a;[dot, mix, slice] 训练数据集总共有270张…

功能强、超好用【PDF转换工具】的介绍下载与安装教程

Windows 电脑上一款简单好用的PDF转换工具&#xff0c;可以轻松地将其他文档转换为 PDF 格式&#xff0c;也可以将 PDF 文件转换为其他格式&#xff0c;如常见的 Word、Excel、PPT 等。 此外软件还支持 Office 文档合并分割、旋转页面、拼接页面、删除文字、删除页面、添加水印…

c# 钉钉应用实现监听审批事件以及获取审批结果的流程

oa的操作已经测试了一遍 image.png如果是自建oa则代表发起的审批是跳转网页&#xff0c;否则钉钉打开后是一个表单界面&#xff0c;不需要调整自己搞得oa。 所以我感觉目前公司的需求更适合官方oa 表单来填写,更灵活&#xff0c;还支持用户配置。 但是用户点了审批&#xff0c;…

Typecho架构深度剖析:轻量级博客系统的设计哲学与实现原理

文章目录 深度解析Typecho:轻量级博客系统的架构设计与实现1. Typecho概述与技术背景1.1 发展历程1.2 核心特性2. 系统架构设计分析2.1 核心架构图2.2 核心组件3. 核心模块实现分析3.1 路由系统实现3.2 数据库抽象层4. 插件系统深度解析4.1 Hook机制实现4.2 插件开发示例5. 性…

LangChain 内存(Memory)

1. 为什么需要内存&#xff1f; 大型语言模型&#xff08;LLM&#xff09;本身是无状态的。这意味着每次你向 LLM 发送一个请求&#xff08;Prompt&#xff09;&#xff0c;它都会独立处理这个请求&#xff0c;完全不记得之前任何的交互。这在构建一次性问答应用时没问题&#…

基于定制开发开源AI智能名片S2B2C商城小程序的社群游戏定制策略研究

摘要&#xff1a;本文聚焦社群游戏定制领域&#xff0c;深入探讨以社群文化和用户偏好为导向的定制策略。通过分析互动游戏活动、社群文化塑造等关键要素&#xff0c;结合定制开发开源AI智能名片S2B2C商城小程序的技术特性&#xff0c;提出针对性游戏定制方案。研究旨在提升社群…

自动驾驶决策与规划

目录 自动驾驶决策与规划概述 决策与规划体系结构 分层递阶式决策规划 反应式体系结构 混合式体系结构 决策与规划系统的关键环节 路径规划 轨迹规划 行为决策 异常处理 自动驾驶的路径规划技术 维诺图法 栅格法 Dijkstra算法 A*算法 自动驾驶的行为决策方法 …

C++编译期计算:常量表达式(constexpr)全解析

在C性能优化领域&#xff0c;"将计算尽可能转移到编译期"是一条黄金法则。编译期计算&#xff08;Compile-Time Computation&#xff09;能显著减少程序运行时的开销&#xff0c;提升执行效率&#xff0c;同时还能在编译阶段暴露潜在错误。C11引入的constexpr关键字及…