当前位置: 首页 > news >正文

MyBatis 的一级、二级缓存机制

目录标题

  • 缓存
    • 什么是缓存
    • 为什么使用缓存
    • 什么样的数据能使用缓存,什么样的数据不能使用
      • 适用于缓存
      • 不适用于缓存
    • MyBatis 一级缓存、二级缓存关系
  • 1. 一级缓存
    • 1.1 什么是一级缓存mybatis
    • 1.2 一级缓存配置
    • 1.3 什么情况下会命中一级缓存
      • mybatis清除一级缓存的几种方法
    • 1.4 内部结构
      • PerpetualCache
    • 1.5 clear() == map.clear()
      • insert/delete/update 方法, 清空 localCache
    • 1.6 Mybatis的一级缓存时序图
    • 1.7 一级缓存实验
      • 一级缓存同一个会话共享数据
      • 同一个会话如果有更新操作则缓存清除
      • 一级缓存在多会话中会导致脏数据
      • 解决方式:在配置一级缓存作用范围的时候将其设置为 STATEMENT,那么缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。
  • 2. 二级缓存
    • 2.2 二级缓存工作流程
    • 2.2 二级缓存配置
      • 二级缓存何时存入
      • 二级缓存如何清空
    • 2.3 二级缓存实验
      • 测试二级缓存与 SqlSession 无关
      • 测试执行 commit(),二级缓存数据清空
      • 多表操作一定不能使用缓存
    • useCache 和 flushCache

缓存

什么是缓存

缓存是存在于内存中的临时数据。

为什么使用缓存

使用缓存减少和数据库的交互次数,提高执行效率。(因为查询数据库是一件很费时很费效率的事,还涉及一些硬盘等io操作,而缓存是存在内存中的,读取都很快,而且效率高)

什么样的数据能使用缓存,什么样的数据不能使用

适用于缓存

经常查询并且不经常改变的;
数据的正确与否对最终结果影响不大的;

不适用于缓存

经常改变的数据;
数据的正确与否对最终结果影响很大的;
例如:商品的库存,银行的汇率,股市的牌价;

MyBatis 一级缓存、二级缓存关系

在这里插入图片描述
一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的是 SqlSession 之间的缓存数据区(HashMap)是互相不影响。
二级缓存是 Mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。

1. 一级缓存

1.1 什么是一级缓存mybatis

默认情况下只会开启一级缓存,也就是局部的 session 会话缓存。
每一个 session 会话都会有各自的缓存,是局部的。

1.2 一级缓存配置

<setting name="localCacheScope" value="SESSION"/>

在 MyBatis 的配置文件中添加上面语句,就可以使用一级缓存。共有两个选项,SESSION 或者 STATEMENT。
默认是 SESSION 级别,即在一个 MyBatis 会话中执行的所有语句,都会共享这一个缓存。
一种是 STATEMENT 级别,可以理解为缓存只对当前执行的这一个 Statement 有效;STATEMENT 级别粒度更细。

1.3 什么情况下会命中一级缓存

必须是在一个会话 Session当中,相同的 namespace(同一个命名空间 -> 同一个mapper文件) , sql 和 参数
不能够在查询之前执行 clearCache
中间不能执行 任何 update ,delete , insert (会将SqlSession中的数据全部清空)

mybatis清除一级缓存的几种方法

  1. 主动调用清理缓存的方法
sqlSession.clearCache()
  1. 提交事务,或者关闭session。
sqlSession.commit();
sqlSession.close();
  1. 执行增删改操作回清理缓存

1.4 内部结构

SqlSession 是一个接口,提供了一些 CRUD 的方法,而 SqlSession 的默认实现类是 DefaultSqlSession,DefaultSqlSession 类持有 Executor 接口对象,而 Executor 的默认实现是 BaseExecutor 对象,每个 BaseExecutor 对象都有一个 PerpetualCache 缓存,也就是上图的 Local Cache。
在这里插入图片描述

PerpetualCache

内部持有 HashMap,对一级缓存的操作实则是对 HashMap 的操作。

public class PerpetualCache implements Cache {private final String id;private Map<Object, Object> cache = new HashMap();...
}

1.5 clear() == map.clear()

在这里插入图片描述
也就是说一级缓存的底层数据结构就是 HashMap。所以说 cache.clear() 其实就是 map.clear(),也就是说,缓存其实是本地存放的一个 map 对象,每一个 SqlSession 都会存放一个 map 对象的引用。

public class PerpetualCache implements Cache {...private Map<Object, Object> cache = new HashMap();public void clear() {this.cache.clear();}...
}

insert/delete/update 方法, 清空 localCache

而为了保证缓存里面的数据肯定是准确数据避免脏读,每次我们进行数据修改后(增、删、改操作)就会执行commit操作,清空缓存区域。

1.6 Mybatis的一级缓存时序图

在这里插入图片描述

1.7 一级缓存实验

一级缓存同一个会话共享数据

@Test
public void firstLevelCacheFindUserById() {// 第一次查询id为1的用户User user1 = userMapper.findUserById(1);// 第二次查询id为1的用户User user2 = userMapper.findUserById(1);System.out.println(user1);System.out.println(user2);System.out.println(user1 == user2);
}

在这里插入图片描述
我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。

同一个会话如果有更新操作则缓存清除

增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。

@Test
public void firstLevelCacheOfUpdate() {// 第一次查询id为1的用户User user1 = userMapper.findUserById(1);System.out.println(user1);// 更新用户User user = new User();user.setId(2);user.setUsername("tom");System.out.println("更新了" + userMapper.updateUser(user) + "个用户");// 第二次查询id为1的用户User user2 = userMapper.findUserById(1);System.out.println(user2);System.out.println(user1 == user2);
}

在这里插入图片描述
我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效。

一级缓存在多会话中会导致脏数据

开启两个 SqlSession,在 sqlSession1 中查询数据,使一级缓存生效,在 sqlSession2 中更新数据库,验证一级缓存只在数据库会话内部共享。

@Test
public void firstLevelCacheOfScope() {SqlSession sqlSession2 = sqlSessionFactory.openSession(true);UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);System.out.println("userMapper读取数据: " + userMapper.findUserById(1));System.out.println("userMapper读取数据: " + userMapper.findUserById(1));// 更新用户User user = new User();user.setId(1);user.setUsername("andy");System.out.println("userMapper2更新了" + userMapper2.updateUser(user) + "个用户");System.out.println("userMapper读取数据: " + userMapper.findUserById(1));System.out.println("userMapper2读取数据: " + userMapper2.findUserById(1));
}

在这里插入图片描述
sqlSession2 更新了 id 为 1 的用户的姓名,从 riemann 改为了 andy,但 session1 之后的查询中,id 为 1 的学生的名字还是 riemann,出现了脏数据,也证明了之前的设想,一级缓存只在数据库会话内部共享。

解决方式:在配置一级缓存作用范围的时候将其设置为 STATEMENT,那么缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

<settings><setting name="localCacheScope" value="STATEMENT"/>
</settings>

2. 二级缓存

2.2 二级缓存工作流程

在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
在这里插入图片描述
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

MyBatis 是默认关闭二级缓存的,因为对于增删改操作频繁的话,那么二级缓存形同虚设,每次都会被清空缓存。

2.2 二级缓存配置

和一级缓存默认开启不一样,二级缓存需要我们手动开启。

  1. 全局配置文件 SqlMapConfig.xml
<!--开启二级缓存-->
<settings><setting name="cacheEnabled" value="true"/>
</settings>
  1. 在 UserMapper.xml 文件中开启二级缓存

mapper 代理模式

<!--开启二级缓存-->
<cache />

注解开发模式

@CacheNamespace(implementation = PerpetualCache.class) // 开启二级缓存
public interface UserMapper {
}

开启二级缓存后,还需要将要缓存的实体类去实现 Serializable 序列化接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们再取出这个缓存的话,就需要反序列化。所以 MyBatis 的所有 pojo 类都要去实现 Serializable 序列化接口。

二级缓存何时存入

在关闭sqlsession后(close),才会把该sqlsession一级缓存中的数据添加到namespace的二级缓存中。

二级缓存如何清空

当对SqlSession执行更新操作(update、delete、insert)后并执行commit时,不仅清空其自身的一级缓存(执行更新操作的效果),也清空二级缓存(执行commit()的效果)。

2.3 二级缓存实验

测试二级缓存与 SqlSession 无关

@Test
public void secondLevelCache() {SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);// 第一次查询id为1的用户User user1 = userMapper1.findUserById(1);sqlSession1.close(); // 清空一级缓存System.out.println(user1);// 第二次查询id为1的用户User user2 = userMapper2.findUserById(1);System.out.println(user2);System.out.println(user1 == user2);
}

在这里插入图片描述
第一次查询时,将查询结果放入缓存中,第二次查询,即使 sqlSession1.close(); 清空了一级缓存,第二次查询依然不发出 sql 语句。
这里的你可能有个疑问,这里不是二级缓存了吗?怎么 user1 与 user2 不相等?

这是因为二级缓存的是数据,并不是对象。而 user1 与 user2 是两个对象,所以地址值当然也不想等。

测试执行 commit(),二级缓存数据清空

@Test
public void secondLevelCacheOfUpdate() {SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();SqlSession sqlSession3 = sqlSessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);// 第一次查询id为1的用户User user1 = userMapper1.findUserById(1);sqlSession1.close(); // 清空一级缓存User user = new User();user.setId(3);user.setUsername("edgar");userMapper3.updateUser(user);sqlSession3.commit(); //清空二级缓存// 第二次查询id为1的用户User user2 = userMapper2.findUserById(1);sqlSession2.close();System.out.println(user1 == user2);
}

在这里插入图片描述
在 sqlSession3 更新数据库,并提交事务后,sqlsession2 的 UserMapper namespace 下的查询走了数据库,没有走 Cache。

多表操作一定不能使用缓存

首先不管多表操作写到那个namespace下,都会存在某个表不在这个namespace下的情况。
例如两个表:role和user_role,如果我想查询出某个用户的全部角色role,就一定会涉及到多表的操作。

<select id="selectUserRoles" resultType="UserRoleVO">select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>

不管是写到RoleMapper.xml还是UserRoleMapper.xml,或者是一个独立的XxxMapper.xml中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。
如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。
这点应该很容易理解。

useCache 和 flushCache

<select id="findAll" resultMap="userMap" useCache="false" flushCache="true">select * from user u left join orders o on u.id = o.uid
</select>

设置 statement 配置中的 flushCache=“true” 属性,默认情况下为 true,即刷新缓存,一般执行完 commit 操作都需要刷新缓存,flushCache=“true” 表示刷新缓存,这样可以避免增删改操作而导致的脏读问题。默认不要配置。

<select id="findAll" resultMap="userMap" useCache="false">select * from user u left join orders o on u.id = o.uid
</select>

useCache 是用来设置是否禁用二级缓存的,在 statement 中设置 useCache=“false”,可以禁用当前 select 语句的二级缓存,即每次都会去数据库查询。

参考文章:
深入浅出 MyBatis 的一级、二级缓存机制
Mybatis 一级缓存
mybatis的一级缓存和二级缓存

相关文章:

MyBatis 的一级、二级缓存机制

目录标题缓存什么是缓存为什么使用缓存什么样的数据能使用缓存&#xff0c;什么样的数据不能使用适用于缓存不适用于缓存MyBatis 一级缓存、二级缓存关系1. 一级缓存1.1 什么是一级缓存mybatis1.2 一级缓存配置1.3 什么情况下会命中一级缓存mybatis清除一级缓存的几种方法1.4 内…...

剑指 Offer 65. 不用加减乘除做加法

摘要 剑指 Offer 65. 不用加减乘除做加法 一、位运算 有符号整数通常用补码来表示和存储&#xff0c;补码具有如下特征&#xff1a; 正整数的补码与原码相同&#xff1b;负整数的补码为其原码除符号位外的所有位取反后加 11。可以将减法运算转化为补码的加法运算来实现。符…...

5年软件测试年薪30w+,我的坎坷之路谁又知道

在深圳做了五年软件测试工作&#xff0c;从之前的一脸懵的点点点&#xff0c;到现在会自动化测试&#xff0c;说一点点非计算机专业人员从事软件测试的心得体会&#xff0c;仅供参考交流。 大部分测试在公司没啥地位&#xff0c;当然如果你懂技术就还行&#xff0c;单纯点点点…...

【Opencv--自适应图像二值化】cv2.adaptiveThreshold()

【Opencv–adaptiveThreshold】自适应阈值图像二值化 文章目录【Opencv--adaptiveThreshold】自适应阈值图像二值化1. 介绍2. adaptiveThreshold函数2.1 函数调用2.2 补充说明3. 代码示例4. 效果4.1 原图&#xff08;ori.img&#xff09;4.2 处理后5. 参考1. 介绍 在这里 cv2.…...

洛谷P8601[蓝桥杯][2013年第四届真题]剪格子

题目描述如图 11 所示&#xff0c;33 的格子中填写了一些整数。我们沿着图中的红色线剪开&#xff0c;得到两个部分&#xff0c;每个部分的数字和都是 60。本题的要求就是请你编程判定&#xff1a;对给定的 mn 的格子中的整数&#xff0c;是否可以分割为两个部分&#xff0c;使…...

配置alias实现快速生成.gitignore文件

git工具&#xff1a;版本控制开发工具。 cscope工具&#xff1a;用于浏览C源码的工具&#xff0c;类似于ctags。在代码根目录下执行cscope -Rbq&#xff0c;然后产生三个索引文件&#xff08;cscope.out、cscope.in.out和cscope.po.out三个文件&#xff09;。 在Linux下使用vi…...

MySQL数据库调优————GROUP BY及DISTINCT优化

GROUP BY 三种处理GROUP BY的方式 松散索引扫描&#xff08;Loose Index Scan&#xff09;紧凑索引扫描&#xff08;Tight Index Scan&#xff09;临时表&#xff08;Temporary table&#xff09; 三种方式的性能一次递减 松散索引扫描 无需扫描满足条件的所有索引键即可返…...

LRU缓存算法

双向链表哈希表&#xff08;非线程安全&#xff09; https://leetcode.cn/problems/lru-cache/solutions/259678/lruhuan-cun-ji-zhi-by-leetcode-solution/ /*** LRU算法: 哈希表双向链表实现* 1. 双向链表按照被使用的顺序来存储, 靠近头部的节点是最近使用的, 靠近尾部的节…...

@Configuration注解

Configuration注解介绍 Configuration注解&#xff0c;用于标注一个类是一个spring的配置类&#xff08;同时&#xff0c;也是一个bean&#xff09;&#xff0c;配置类中可以使用ComponentScan、Import、ImportResource 和 Bean等注解的方式定义beanDefinition。 Target(Elem…...

基于springboot+vue的食疗系统

基于springbootvue的食疗系统 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&…...

sklearn学习-朴素贝叶斯

文章目录一、概述1、真正的概率分类器2、sklearn中的朴素贝叶斯二、不同分布下的贝叶斯1、高斯朴素贝叶斯GaussianNB2、探索贝叶斯&#xff1a;高斯朴素贝叶斯擅长的数据集3、探索贝叶斯&#xff1a;高斯朴素贝叶斯的拟合效果与运算速度总结一、概述 1、真正的概率分类器 算法…...

分享112个HTML艺术时尚模板,总有一款适合您

分享112个HTML艺术时尚模板&#xff0c;总有一款适合您 112个HTML艺术时尚模板下载链接&#xff1a;https://pan.baidu.com/s/1D3-mfPOud-f3vy9yLl-bmw?pwdfph2 提取码&#xff1a;fph2 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 时尚平面模特网站模板 潮…...

用GDB远程调试运行于QEMU的程序

1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. 测试环境 本文使用 Ubuntu 16.04.4 LTS QEMU 环境进行调试。 3. 用 GDB 调试 QEMU 内程序 3.1 编写用来调试的程序 我们用 ARM32 来进行调试…...

20 堆排序

文章目录1 堆排序的概念2 堆排序基本思想3 堆排序步骤图解说明4 堆排序的代码实现1 堆排序的概念 1) 堆排序是利用堆这种数据结构而设计的一种排序算法&#xff0c;堆排序是一种选择排序&#xff0c;它的最坏&#xff0c;最好&#xff0c;平均时间复杂度均为 O(nlogn)&#xf…...

2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享

内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享 很多时候&#xff0c;我们都想将一些文件或文本传送给别人&#xff0c;或者跨端传递一些信息&#xff0c;但是我们又不…...

分片策略(二)

分片策略 基本概念 分片键 用于分片的字段&#xff0c;是将数据库或表拆分的字段&#xff0c;比如&#xff0c;我可以使用user_id作为分片键将用户数据分到不同的表中&#xff0c;这里的user_id就是分片键&#xff0c;除了这种单字段分片&#xff0c;ShardingSphere还支持多…...

Qt之调色板类QPalette的使用

文章目录QPalette调色板类前言代码知识点讲解QPalette调色板类 前言 Qt提供的调色板类QPalette专门用于管理部件的外观显示&#xff0c;相当于部件或对话框的调色板&#xff0c;管理他们所有的颜色信息。每个部件都包含一个QPalette对象&#xff0c;在显示时&#xff0c;按照…...

Kotlin 32. Kotlin 多语言支持

Kotlin 多语言支持 对于 Kotlin 来说&#xff0c;当我们新建一个项目时&#xff0c;会默认在 values/ 文件夹下&#xff0c;生成一个 strings.xml 文件。比如说&#xff0c; <resources><string name"app_name">exampleNewProject</string> <…...

【Flutter入门到进阶】Dart进阶篇---DartVM单线程设计原理

1 虚拟机的指令执行设计 1.1 虚拟机的分类 基于栈的虚拟机&#xff0c;比如JVM虚拟机 基于寄存器的虚拟机&#xff0c;比如Dalvik虚拟机 1.2 虚拟机的概念 首先问一个基本的问题&#xff0c;作为一个虚拟机&#xff0c;它最基本的要实现哪些功能&#xff1f; 他应该能够模拟…...

Dem和NvM(NVRAM Manager)的交集

NVRAM&#xff08;NvM&#xff09;提供了在NVRAM中存储数据Block的机制。 NVRAM Block&#xff08;最大大小取决于配置&#xff09;被分配给Dem&#xff0c;并由Dem实现事件状态信息和相关数据的永久存储&#xff08;例如通电复位&#xff09;。 ECU 状态管理器&#xff08;Ec…...

Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件

今天呢&#xff0c;博主的学习进度也是步入了Java Mybatis 框架&#xff0c;目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学&#xff0c;希望能对大家有所帮助&#xff0c;也特别欢迎大家指点不足之处&#xff0c;小生很乐意接受正确的建议&…...

汽车生产虚拟实训中的技能提升与生产优化​

在制造业蓬勃发展的大背景下&#xff0c;虚拟教学实训宛如一颗璀璨的新星&#xff0c;正发挥着不可或缺且日益凸显的关键作用&#xff0c;源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例&#xff0c;汽车生产线上各类…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

12.找到字符串中所有字母异位词

&#x1f9e0; 题目解析 题目描述&#xff1a; 给定两个字符串 s 和 p&#xff0c;找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义&#xff1a; 若两个字符串包含的字符种类和出现次数完全相同&#xff0c;顺序无所谓&#xff0c;则互为…...

css3笔记 (1) 自用

outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size&#xff1a;0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格&#xff…...

算法岗面试经验分享-大模型篇

文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer &#xff08;1&#xff09;资源 论文&a…...

基于TurtleBot3在Gazebo地图实现机器人远程控制

1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...

JavaScript基础-API 和 Web API

在学习JavaScript的过程中&#xff0c;理解API&#xff08;应用程序接口&#xff09;和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能&#xff0c;使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...

C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...

Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成

一个面向 Java 开发者的 Sring-Ai 示例工程项目&#xff0c;该项目是一个 Spring AI 快速入门的样例工程项目&#xff0c;旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计&#xff0c;每个模块都专注于特定的功能领域&#xff0c;便于学习和…...