一文让你彻底搞懂Mybatis之缓存机制

news2024/9/29 9:22:20

编译软件:IntelliJ IDEA 2019.2.4 x64
操作系统:win10 x64 位 家庭版
Maven版本:apache-maven-3.6.3
Mybatis版本:3.5.6


文章目录

  • 一. 缓存是什么?
  • 二. 为什么要使用缓存?
  • 三. Mybatis中的缓存分哪几种?
    • 3.1 Mybatis缓存机制之一级缓存
    • 3.2 Mybatis缓存机制之二级缓存
    • 3.3 Mybatis中缓存机制之第三方缓存【以EhCache为例】


一. 缓存是什么?

一说到缓存,我们可能都会想到Cashe,这里摘自百度百科对它的解释:它原本是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。它的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。

在这里插入图片描述
以上部分内容可能看不懂,没关系,你只需要知道正是由于这样的读取机制,使得CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在CPU缓存中,只有大约10%需要从内存读取。这样大大节省了CPU直接读取内存的时间,非常快捷!!!

在这里插入图片描述

简而言之,以cpu中的缓存举例,缓存其实就是数据交换的缓冲区【又称Cashe】,是存贮数据(使用频繁的数据)的临时地方。当用户查询数据,首先在缓存中寻找,如果找到了则直接执行。如果找不到,则去数据库中查找


二. 为什么要使用缓存?

举个生活中的例子,当我们在线观看视频时,以哔哩哔哩网站为例,你会发现,底下的进度条会实时显示蓝色,白色等两种颜色。不难发现,蓝色代表的是视频实际播放的进度,而白色代表的是视频实时预先缓存的进度。如下所示。

在这里插入图片描述
这种让用户一边下载一边观看、收听,而不要等整个文件下载到自己的计算机上才可以观看的网络传输技术,就是鼎鼎大名的流媒体技术。该技术的原理是先在使用者端的计算机上创建一个缓冲区,在播放前预先下一段数据作为缓冲,在网路实际连线速度小于播放所耗的速度时,播放程序就会取用一小段缓冲区内的数据,这样可以避免播放的中断,也使得播放品质得以保证。该技术在很多音频影视网站上被大量使用,旨在丰富用户的使用体验并提高音频的播放性能。
在这里插入图片描述
而程序中的缓存【Mybatis缓存】,亦是如此,Mybatis使用缓存优势可以提高查询效率,并降低服务器的压力。它的本质就是用利用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。


三. Mybatis中的缓存分哪几种?

👉分类

  1. 一级缓存
  2. 二级缓存
  3. 第三方缓存

在这里插入图片描述

3.1 Mybatis缓存机制之一级缓存

👉概述

一级缓存【本地缓存(Local Cache)或SqlSessiona级别缓存】

🤔什么是SqlSessiona?

SqlSession是一个会话,相当于JDBC中的一个Connection对象,是整个Mybatis运行的核心,它是MyBatis的关键对象,是执行持久化操作的独享,类似于JDBC中的Connection。它是应用程序与持久层之间执行交互操作的一个单线程对象,也是MyBatis的核心接口之一

👉特点

  • 一级缓存默认开启
  • 不能关闭
  • 可以清空

💡 :有点类似于使用腾讯视频网站去看电影,电影观看进度条前的那一小段灰色的进度条

👉缓存原理

  1. 第一次获取数据时,先从数据库中加载数据,将数据缓存至Mybatis一级缓存中【缓存底层实现原理Map,key:hashCode+查询的Sqlld+编写的sal查询语句+参数】
  2. 以后再次获取数据时,先从一级缓存中获取,如未获取到数据,再从数据库中获取数据

不信?请看如下测试代码的体现

代码示例如下:

@Test
public void test04(){
    try {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //通过SqlSessionFactory对象调用openSession();
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //获取EmployeeMapper的代理对象
        EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);

        //第一次调用selectEmpByOneOpr(1);
        List<Employee> employees = employeeMapper.selectEmpByOneOpr(1);
        System.out.println(employees);

        System.out.println("---------------------------------------------");
        //第二次调用selectEmpByOneOpr(1);
        List<Employee> employees1 = employeeMapper.selectEmpByOneOpr(1);
        System.out.println(employees1);



    } catch (IOException e) {
        e.printStackTrace();
    }
}

在这里插入图片描述

👉一级缓存的五种失效情况

  1. 不同的SqlSession对应不同的一级缓存

  2. 同一个SqlSession但是查间条件不同

  3. 同一个SqlSession两次查询期间执行了任何一次增删改操作

    执行任何一次的增删改操作会默认清空一级缓存

  4. 同一个SqlSession两次查询期间手动清空了缓存

    如何手动清空一级缓存?

    sqlSession.clearCache()
    
  5. 同一个SqlSessioni两次查询期间提交了事务

    sqlSession.commit()
    

3.2 Mybatis缓存机制之二级缓存

👉概述

二级缓存【全局作用域缓存】是SqlSessionFactory级别的缓存

👉特点

  • 二级缓存默认关闭,需要开启才能使用

  • 二级缓存需要提交sqlSession或关闭sqlSessionl时,才会缓存。

👉二级缓存使用的步骤

①全局配置文件中开启二级缓存“<setting name="cacheEnab1ed"value=“true”/>”

②需要使用二级缓存的映射文件处使用cache配置缓存

③注意:POJO(Java Bean【java的实体类】)需要实现Serializable接口

④关闭sqlSession或提交sqlSessionl时,将数据缓存到二级缓存

👉用法案例

演示二级缓存的效果

代码示例如下:

①全局配置文件中开启二级缓存

在这里插入图片描述

②这里假设映射文件EmployeeMapper.xml使用cache配置缓存

在这里插入图片描述

③在映射文件EmployeeMapper.xml对应的pojo类实现Serializable接口

在这里插入图片描述

④测试

@Test
public void test04(){
    try {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //通过SqlSessionFactory对象调用openSession()获取sqlSession对象;
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象
        EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);

        //第一次调用selectEmpByOneOpr(1);
        List<Employee> employees = employeeMapper.selectEmpByOneOpr(1);
        System.out.println(employees);

        //关闭sqlsession,目的是为了将数据加载到二级缓存中
        sqlSession.close();

        System.out.println("---------------------------------------------");
        //重新获取sqlsession对象
        SqlSession sqlSession1 =sqlSessionFactory.openSession();

        //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper
        EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class);

        //调用selectEmpByOneOpr(1);
        List<Employee> employees1 = mapper.selectEmpByOneOpr(1);
        System.out.println(employees1);



    } catch (IOException e) {
        e.printStackTrace();
    }
}

在这里插入图片描述

👉底层原理

  1. 第一次获取数据时,先从数据库中获取数据,将数据缓存至一级缓存;当提交或关闭SqlSessionl时,将数据缓存至二级缓存
  2. 以后再次获取数据时,先从一级缓存中获取数据,如果一级缓存没有指定数据,再去二级缓存中获取数据。如果二级缓存也没有指定数据时,需要去数据库中获取数据

👉二级缓存相关属性(在设置了cash的映射文件中设置以下属性)

  • eviction=“FIFO"缓存清除【回收】策略

    • LU-最近最少使用的移除最长时间不被使用的对象
    • FFO-先进先出按对象进入缓存的顺序来移除它们
  • flushlnterval刷新间隔,单位毫秒,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新

  • size引用数目,正整数,代表缓存最多可以存储多少个对象,太大容易导致内存益出

  • readOnly只读,true/false

    • true只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势
    • false读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false

👉二级缓存的失效情况

在两次查询之间,执行增删改操作,会同时清空一级缓存和二级缓存

sqlSession.clearCache()只是用来清除一级缓存

🤔思考

执行两次查询操作(查询条件相同且查询语句相同),中间使用sqlsession.clearCache(),然后关闭第一个sqlsession对象,又新建一个sqlsession对象,执行第二次查询,是否会导致二级缓存失效?

我的推测: 会失效

测试代码如下:

@Test
public void test05() {
    try {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //通过SqlSessionFactory对象调用openSession()获取sqlSession对象;
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象
        EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);

        //第一次调用selectEmpByOneOpr(1);
        List<Employee> employees = employeeMapper.selectEmpByOneOpr(1);
        System.out.println(employees);

        //清空一级缓存
        sqlSession.clearCache();

        //需要先关闭sqlsession对象,以缓存数据到二级缓存
        sqlSession.close();


        System.out.println("---------------------------------------------");

        //重新获取sqlsession对象
        SqlSession sqlSession1 = sqlSessionFactory.openSession();

        //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper
        EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class);

       /* //在两个查询之间执行一次更新操作,目的是为了清空二级缓存
        Employee e = new Employee();
        e.setId(1);
        e.setSalary(25000.0);
        mapper.updateEmp(e);*/

        //调用selectEmpByOneOpr(1);
        List<Employee> employees1 = mapper.selectEmpByOneOpr(1);
        System.out.println(employees1);


    } catch (IOException e) {
        e.printStackTrace();
    }

}

在这里插入图片描述

推测错了,二级缓存仍然可以命中该查询结果

⭐原因分析

当第一次执行某个 SQL 语句时,该 SQL 语句的缓存条目会被添加到二级缓存中;而在清空当前 SqlSession 对象的一级缓存时,并没有清空对应的 SQL 语句在二级缓存中的缓存条目,因此该 SQL 语句的缓存条目仍然存在于二级缓存中,即使这个二级缓存还没有和数据库同步。

当新建另一个 SqlSession 对象,执行相同的查询操作且查询条件和前一次查询操作相同时,MyBatis 将会先从一级缓存中尝试获取数据。由于已经执行了 sqlsession.clearCache() 清空了当前 SqlSession 的一级缓存,因此一级缓存会命中失败,但是 MyBatis 可以从二级缓存中获取到之前查询过的结果集,返回给我当前的查询操作结果。

3.3 Mybatis中缓存机制之第三方缓存【以EhCache为例】

👉概述

EhCache【第三方缓存】是一个纯ava的进程内缓存框架

👉使用步骤

①导入jar包

代码示例如下:

<!-- 导入ehcache的jar包 -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.0.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<!-- 导入一个日志的jar包 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.6.2</version>
</dependency>

②编写ehcache的配置文件(resources目录下)【ehcache.xml】

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">

    <!-- 磁盘保存路径 -->
    <diskStore path="E:\mybatis\ehcache" />

    <defaultCache
            maxElementsInMemory="512"
            maxElementsOnDisk="10000000"
            eternal="false"
            overflowToDisk="true"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

③加载第三方缓存【映射文件】

<!--  此处加载第三方缓存ehcache  -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>

④开始使用

@Test
public void test04(){
    try {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //通过SqlSessionFactory对象调用openSession()获取sqlSession对象;
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象
        EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);

        //第一次调用selectEmpByOneOpr(1);
        List<Employee> employees = employeeMapper.selectEmpByOneOpr(1);
        System.out.println(employees);

        //关闭sqlsession,目的是为了将数据加载到二级缓存中
        sqlSession.close();

        System.out.println("---------------------------------------------");
        //重新获取sqlsession对象
        SqlSession sqlSession1 =sqlSessionFactory.openSession();

        //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper
        EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class);

        //调用selectEmpByOneOpr(1);
        List<Employee> employees1 = mapper.selectEmpByOneOpr(1);
        System.out.println(employees1);



    } catch (IOException e) {
        e.printStackTrace();
    }
}

在这里插入图片描述

在这里插入图片描述

❗注意

①第三方缓存,需要建立在二级缓存基础上【需要开启二级缓存,第三方缓存才能生效】

②如何让第三方缓存失效?

将二级缓存设置失效即可【在两次查询之间,进行一次增删改操作以清除二级缓存】

代码示例如下:

@Test
public void test04(){
    try {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //通过SqlSessionFactory对象调用openSession()获取sqlSession对象;
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象
        EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);

        //第一次调用selectEmpByOneOpr(1);
        List<Employee> employees = employeeMapper.selectEmpByOneOpr(1);
        System.out.println(employees);

        //关闭sqlsession,目的是为了将数据加载到二级缓存中
        sqlSession.close();


        System.out.println("---------------------------------------------");

        //重新获取sqlsession对象
        SqlSession sqlSession1 =sqlSessionFactory.openSession();

        //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper
        EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class);

        //在两个查询操作之间执行一次更新操作,目的是为了清空二级缓存
        Employee e=new Employee();
        e.setId(1);
        e.setSalary(25000.0);
        mapper.updateEmp(e);

        //调用selectEmpByOneOpr(1);
        List<Employee> employees1 = mapper.selectEmpByOneOpr(1);
        System.out.println(employees1);



    } catch (IOException e) {
        e.printStackTrace();
    }
}

在这里插入图片描述


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

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

相关文章

自己实现MyBatis 底层机制--抽丝剥茧(上)

&#x1f600;前言 本篇博文是学习过程中的笔记和对于MyBatis底层机制的分析思路&#xff0c;希望能够给您带来帮助&#x1f60a; &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到…

Base64之间的相互转化

使用org.apache.ommons.codec.binary.Base64实现字符串和Base64之间的相互转化 字符串转化为Base64之间的相互转化一 //转化为Base64字符串 String strOld "Welcome to the new world"; base64EncodeStr Base64.encodeBase64String(strOld.getBytes()); System.o…

黑马点评项目学习笔记(15w字详解,堪称史上最详细,欢迎收藏)

黑马点评项目学习笔记 文章目录 黑马点评项目学习笔记前言项目搭建导入数据库初始化项目启动项目启动前端项目启动后端项目 基于Session实现短信验证码登录短信验证码登录配置登录拦截器数据脱敏 Session集群共享问题基于Redis实现短信验证码登录短信验证登录配置登录拦截器 店…

漏洞分析|Metabase 代码执行漏洞(CVE-2023-38646):H2 JDBC 深入利用

0x01 概述 最近 Metabase 出了一个远程代码执行漏洞&#xff08;CVE-2023-38646&#xff09;&#xff0c;我们通过研究分析发现该漏洞是通过 JDBC 来利用的。在 Metabase 中兼容了多种数据库&#xff0c;本次漏洞中主要通过 H2 JDBC 连接信息触发漏洞。目前公开针对 H2 数据库…

国产内存强势崛起,光威神条有神价,无套路闭眼可入

今年的DIY电脑市场终于摆脱了前两年的阴霾&#xff0c;从CPU到内存都有着充足的货源&#xff0c;而且价格靠谱&#xff0c;特别是国产存储品牌超级厚道&#xff0c;内存、硬盘等配件的价格基本都是大跳水&#xff0c;相比于去年&#xff0c;同样的价格能够买到容量和性能翻倍的…

ERROR 1064 - You have an error in your SQL syntax;

ERROR 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near (/, 少个逗号吧&#xff0c;以前开始写SQL&#xff0c;特别是修改SQL的时候容易出现这样错误。 而且自己也知道在附近…

SAP财务系统中的“复式记账法”

1. 前言 “复式记账法”是财务的基础知识&#xff0c;对于财务出身的小伙伴是so easy&#xff0c;但对于技术出身的同学&#xff0c;通常会被“借贷”关系弄的晕头转向。 本文会简明扼要的总结“复式记账法”的基本原理&#xff0c;并以采购和销售流程为例来介绍如何进行复式…

Java - 注解开发

注解开发定义bean Component的衍生注解 Service&#xff1a; 服务层的注解 Repository&#xff1a; 数据层的注解 Controller&#xff1a; 控制层的注解 纯注解开发 bean管理 bean作用范围 在类上面添加Scope(“singleton”) // prototype: 非单例 bean生命周期 PostCon…

PyTorch BatchNorm2d详解

通常和卷积层&#xff0c;激活函数一起使用

基于51单片机和proteus的加热洗手器系统设计

此系统是基于51单片机和proteus的仿真设计&#xff0c;功能如下&#xff1a; 1. 检测到人手后开启出水及加热。 2. LED指示加热出水及系统运行状态。 功能框图如下&#xff1a; Proteus仿真界面如下&#xff1a; 下面就各个模块逐一介绍&#xff0c; 模拟人手检测模块 通过…

redis 第三章

目录 1.主从复制 2.哨兵 3.集群 4.总结 1.主从复制 结果&#xff1a; 2.哨兵 3.集群 4.总结 通过集群&#xff0c;redis 解决了写操作无法负载均衡&#xff0c;以及存储能力受到单机限制的问题&#xff0c;实现了较为完善的高可用方案。

【设计模式】详解单例设计模式(包含并发、JVM)

文章目录 1、背景2、单例模式3、代码实现1、第一种实现&#xff08;饿汉式&#xff09;为什么属性都是static的&#xff1f;2、第二种实现&#xff08;懒汉式&#xff0c;线程不安全&#xff09;3、第三种实现&#xff08;懒汉式&#xff0c;线程安全&#xff09;4、第四种实现…

Android kotlin系列讲解之最佳的UI体验 - Material Design 实战

目录 一、什么是Material Design二、Toolbar三、滑动菜单1、DrawerLayout2、NavigationView 四、悬浮按钮和可交互提示1、FloatingActionButton2、Snackbar3、CoordinatorLayout 五、卡片式布局1、MaterialCardView2、AppBarLayout 六、可折叠式标题栏1、CollapsingToolbarLayo…

Python MySQL

pymysql 除了使用图形化工具以外&#xff0c;我们也可以使用编程语言来执行SQL从而操作数据库。 在Python中&#xff0c;使用第三方库&#xff1a;pymysql 来完成对MySQL数据库的操作。 安装&#xff1a; pip install pymysql 或在pycharm中搜索pymysql插件安装 创建到MySQ…

蓝桥杯单片机第十届国赛 真题+代码

iic.c /* # I2C代码片段说明1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。2. 参赛选手可以自行编写相关代码或以该代码为基础&#xff0c;根据所选单片机类型、运行速度和试题中对单片机时钟频率的要求&#xff0c;进行代码调试和修改。 */ #include <STC1…

如何快速用Go获取短信验证码

要用Go获取短信验证码&#xff0c;通常需要连接到一个短信服务提供商的API&#xff0c;并通过该API发送请求来获取验证码。由于不同的短信服务提供商可能具有不同的API和授权方式&#xff0c;我将以一个简单的示例介绍如何使用Go语言来获取短信验证码。 在这个示例中&#xff0…

【Ansible】Ansible自动化运维工具之playbook剧本

playbook 一、playbook 的概述1. playbook 的概念2. playbook 的构成 二、playbook 的应用1. 安装 httpd 并启动2. 定义、引用变量3. 指定远程主机 sudo 切换用户4. when条件判断5. 迭代6. Templates 模块6.1 添加模板文件6.2 修改主机清单文件6.3 编写 playbook 7. tags 模块 …

338. 比特位计数

题目 题解一 动态规划——最低设置位 public static int[] countBits(int n) {int [] nums new int[n1];//存放1的个数nums[0]0;for (int i 1; i <n ; i) {nums[i] nums[i & (i-1)]1;}return nums;}题解二 分奇数和偶数&#xff1a; public static int[] countBits…

【MySQL主从复制】

目录 一、MySQL Replication 1.概述 2.优点 二、MySQL复制类型 1.异步复制&#xff08;Asynchronous repication&#xff09; 2.全同步复制&#xff08;Fully synchronous replication&#xff09; 3.半同步复制&#xff08;Semisynchronous replication&#xff09; 三…

利用VBA制作一个转盘游戏之五:最终的游戏过程

【分享成果&#xff0c;随喜正能量】真正厉害的人&#xff0c;从来不说难听的话&#xff0c;因为人心不需要听真话&#xff0c;只需要听好听的话&#xff0c;所以学着做一个有温度且睿智的人。不相为谋&#xff0c;但我照样能心平气和&#xff0c;冷眼相待&#xff0c;我依旧可…