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

《Zookeeper 分布式过程协同技术详解》读书笔记-2

目录

  • zk的一些内部原理和应用
    • 请求,事务和标识
      • 读写操作
      • 事务标识(zxid)
    • 群首选举
    • Zab协议(ZooKeeper Atomic Broadcast protocol)
    • 文件系统和监听通知机制
      • 分布式配置中心, 简单Demo
        • java code
      • 集群管理
        • code
      • 分布式锁

zk的一些内部原理和应用

请求,事务和标识

读写操作

  1. 只读请求(exists, getData,getChildren)会在zk服务其本地处理,然后返回给客户端(所以zk处理以只读请求为主要负载时,性能会很高,可以增加更多的服务器到zk集群,这样能处理更多的读请求)

因为ZooKeeper集群中所有的server节点都拥有相同的数据,所以读的时候可以在任意一台server节点上,客户端连接到集群中某一节点,读请求,然后直接返回。当然因为ZooKeeper协议的原因(一半以上的server节点都成功写入了数据,这次写请求便算是成功),读数据的时候可能会读到数据不是最新的server节点,所以比较推荐使用watch机制,在数据改变时,及时感知到

  1. 写操作(create, delete, setData) 将会被转发给群首(群首会执行相应请求,并形成状态更新,我们称为事务(Transaction))

在这里插入图片描述

  1. Client向Zookeeper的server1发送一个写请求,客户端写数据到服务器1上;
  2. 如果server1不是Leader,那么server1会把接收到的写请求转发给Leader;然后Leader会将写请求转发给每个server;
    • server1和server2负责写数据,并且两个Follower的写入数据是一致的,保存相同的数据副本;
    • server1和server2写数据成功后,通知Leader;
  3. 当Leader收到集群半数以上的节点写成功的消息后,说明该写操作执行成功;
    • 这里是3台服务器,只要2台Follower服务器写成功就ok
    • 因为client访问的是server1,所以Leader会告知server1集群中数据写成功;
  4. 被访问的server1进一步通知client数据写成功,这时,客户端就知道整个写操作成功了

事务标识(zxid)

当群首产生了一个事务,就会为该事务分配一个标识符,我们称之 为ZooKeeper会话ID(zxid),通过Zxid对事务进行标识,就可以按照群 首所指定的顺序在各个服务器中按序执行

zxid为一个long型(64位)整数,分为两部分:时间戳(epoch)部 分和计数器(counter)部分。每个部分为32位,在我们讨论zab(Zookeeper Atomic Broadcast )协议 时,我们就会发现时间戳(epoch)和计数器(counter)的具体作用, 我们通过该协议来广播各个服务器的状态变更信息。

eg:

setData 加上事务(版本和新数据值),一个事务为一个单位,以原子方式执行;ZooKeeper集群以事务方式运行,并确保所 有的变更操作以原子方式被执行,同时不会被其他事务所干扰;并不存在传统的关系数据库中所涉及的回滚机制,而是 确保事务的每一步操作都互不干扰

同时一个事务还具有幂等性,也就是说,我们可以对同一个事务执 行两次,我们得到的结果还是一样的,我们甚至还可以对多个事务执行 多次,同样也会得到一样的结果,前提是我们确保多个事务的执行顺序 每次都是一样的。事务的幂等性可以让我们在进行恢复处理时更加简单。

实际上,ZooKeeper的每个节点维护者两个Zxid值,为别为:cZxid、mZxid。

(1)cZxid: 是节点的创建时间所对应的Zxid格式时间戳。

(2)mZxid:是节点的修改时间所对应的Zxid格式时间戳。

高32位是epoch用来标识Leader关系是否改变,每次一个Leader被选出来,它都会有一个新的epoch。低32位是个递增计数。

群首选举

群首为集群中的服务器选择出来的一个服务器,并会一直被集群所 认可。设置群首的目的是为了对客户端所发起的ZooKeeper状态变更请 求进行排序,包括:create、setData和delete操作。群首将每一个请求转 换为一个事务,将这些事务发送给追随者,确保集 群按照群首确定的顺序接受并处理这些事务。

  • 每个服务器启动后进入LOOKING状态,开始选举一个新的群首或 查找已经存在的群首,如果群首已经存在,其他服务器就会通知这个新 启动的服务器,告知哪个服务器是群首,与此同时,新的服务器会与群 首建立连接,以确保自己的状态与群首一致。

  • 如果集群中所有的服务器均处于LOOKING状态,这些服务器之间 就会进行通信来选举一个群首,通过信息交换对群首选举达成共识的选 择。在本次选举过程中胜出的服务器将进入LEADING状态,而集群中 其他服务器将会进入FOLLOWING状态。

当一个服务器进入LOOKING状态,就会发送向集群中每个服 务器发送一个通知消息,如下

投票<服务器标识,最近执行的事务的zxid信息>
(vote<sid, zxid>

在这里插入图片描述

在这里插入图片描述

快速群首选举的快速指的是什么?

Zab协议(ZooKeeper Atomic Broadcast protocol)

在接收到一个写请求操作后,追随者会将请求转发给群首,群首将探索性地执行该请求,并将执行结果以事务的方式对状态更新进行广播。一个事务中包含服务器需要执行变更的确切操作,当事务提交时, 服务器就会将这些变更反馈到数据树上,其中数据树为ZooKeeper用于 保存状态信息的数据结构(请参考DataTree类)。

之后我们需要面对的问题便是服务器如何确认一个事务是否已经提 交,由此引入了我们所采用的协议:Zab:ZooKeeper原子广播协议 (ZooKeeper Atomic Broadcast protocol)。

  • propose: leader 广播给 followers
  • accept: followers收到广播信息给leader发送确认消息
  • commit: leader收到确认仲裁数量后发送消息给follower进行提交操作
    在这里插入图片描述
    具体如下:
    在这里插入图片描述

两个保障(依靠了zxid)

  1. 一个被选举的群首确保在提交完所有之前的时间戳内需要提交的 事务,之后才开始广播新的事务。
  2. 在任何时间点,都不会出现两个被仲裁支持的群首。

为了实现第一个需求,群首并不会马上处于活动状态,直到确保仲 裁数量的服务器认可这个群首新的时间戳值。一个时间戳的最初状态必 须包含所有的之前已经提交的事务,或者某些已经被其他服务器接受, 但尚未提交完成的事务。这一点非常重要,在群首进行时间戳e的任何 新的提案前,必须保证自时间戳开始值到时间戳e-1内的所有提案被提 交。如果一个提案消息处于时间戳e’<e,在群首处理时间戳e的第一个提 案消息前没有提交之前的这个提案,那么旧的提案将永远不会被提交。

文件系统和监听通知机制

分布式配置中心, 简单Demo

  1. 在zookeeper里增加一个目录节点,并把配置信息存储在里面(作为配置中心存储)

在这里插入图片描述

  1. 多个客户端能够读取到
    在这里插入图片描述
  2. 服务端配置改变,客户端(所有注册监听的客户端)都能监听到事件
    在这里插入图片描述
    在这里插入图片描述
java code
package demo;import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;import java.util.concurrent.CountDownLatch;/*** @Author mubi* @Date 2020/3/27 22:36*/
public class ZooKeeperProSync implements Watcher {private static CountDownLatch connectedSemaphore = new CountDownLatch(1);private static ZooKeeper zk = null;private static Stat stat = new Stat();public static void main(String[] args) throws Exception {// zookeeper配置数据的存放路径String path = "/myconfig";// 连接zookeeper并且注册一个监听器zk = new ZooKeeper("127.0.0.1:2281", 5000, new ZooKeeperProSync());// 等待zk连接成功的通知(等待connectedSemaphore.countDown()减少为0)connectedSemaphore.await();// 获取path目录节点的配置数据,并注册对节点的监听System.out.println(new String(zk.getData(path, true, stat)));Thread.sleep(Integer.MAX_VALUE);}@Overridepublic void process(WatchedEvent event) {// zk连接成功通知事件if (KeeperState.SyncConnected == event.getState()) {if (EventType.None == event.getType() && null == event.getPath()) {connectedSemaphore.countDown();}else if (event.getType() == EventType.NodeDataChanged) {// zk目录节点数据变化通知事件try {System.out.println("配置已修改,新值为:" + new String(zk.getData(event.getPath(), true, stat)));// TODO 具体业务} catch (Exception e) {}}}}
}

集群管理

集群管理原理:机器的加入/退出,选举leader节点

在这里插入图片描述

  • 持久节点 / 临时节点
  • 有序节点 / 无序节点

可以临时+有序构成server,然后最小编号节点作为leader节点

  1. 创建集群节点目录(持久节点)
    在这里插入图片描述
  2. 启动服务,并加入和减少机器观察
    在这里插入图片描述
    在这里插入图片描述
code
  • AppMaster
package demo;import java.util.ArrayList;
import java.util.List;import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooKeeper;/*** @Author mubi* @Date 2020/3/27 23:43*/
public class AppMaster {private String clusterNode = "mycluster";private ZooKeeper zk;private volatile List<String> serverList;public void connectZookeeper() throws Exception{// 注册全局默认watcherzk = new ZooKeeper("127.0.0.1:2281", 5000, new Watcher(){@Overridepublic void process(WatchedEvent event){if (event.getType() == EventType.NodeChildrenChanged&& ("/" + clusterNode).equals(event.getPath())){try{updateServerList();}catch (Exception e){e.printStackTrace();}}}});updateServerList();}private void updateServerList() throws Exception{List<String> newServerList = new ArrayList<String>();// watcher注册后,只能监听事件一次,参数true表示继续使用默认watcher监听事件List<String> subList = zk.getChildren("/" + clusterNode, true);for (String subNode : subList){// 获取节点数据byte[] data = zk.getData("/" + clusterNode + "/" + subNode, false, null);newServerList.add(new String(data, "utf-8"));}serverList = newServerList;System.out.println("server list updated: " + serverList);}public static void main(String[] args) throws Exception{AppMaster ac = new AppMaster();ac.connectZookeeper();Thread.sleep(Long.MAX_VALUE);}
}
  • AppServer
package demo;import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;/*** @Author mubi* @Date 2020/3/27 23:44*/
public class AppServer extends Thread
{private String clusterNode = "mycluster";private String serverNode = "server_address";private String serverName;@Overridepublic void run(){try {connectZookeeper(serverName);} catch (Exception e) {e.printStackTrace();}}public void connectZookeeper(String address) throws Exception{ZooKeeper zk = new ZooKeeper("127.0.0.1:2281", 5000, new Watcher() {@Overridepublic void process(WatchedEvent event) {}});// 关键方法,创建包含自增长id名称的目录,这个方法支持了分布式锁的实现// 四个参数:// 1、目录名称 2、目录文本信息// 3、文件夹权限,Ids.OPEN_ACL_UNSAFE表示所有权限// 4、目录类型,CreateMode.EPHEMERAL_SEQUENTIAL表示创建临时目录,session断开连接则目录自动删除String createdPath = zk.create("/" + clusterNode + "/" + serverNode,address.getBytes("utf-8"),Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);System.out.println("create: " + createdPath);Thread.sleep(Integer.MAX_VALUE);}public AppServer(String serverName){this.serverName = serverName;}
}
  • Server1
package demo;/*** @Author mubi* @Date 2020/3/27 23:46*/public class Server1
{public static void main(String[] args) throws Exception{AppServer server1 = new AppServer("Server1");server1.start();}
}

分布式锁

一个zookeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程都会在这个节点下创建个临时顺序节点,由于序号的递增性,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。

避免羊群效应:所谓羊群效应就是每个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来巨大压力。所以有了临时顺序节点,当一个节点挂掉,只有它后面的那一个节点才做出反应。

在这里插入图片描述

相关文章:

《Zookeeper 分布式过程协同技术详解》读书笔记-2

目录 zk的一些内部原理和应用请求&#xff0c;事务和标识读写操作事务标识&#xff08;zxid&#xff09; 群首选举Zab协议&#xff08;ZooKeeper Atomic Broadcast protocol&#xff09;文件系统和监听通知机制分布式配置中心, 简单Demojava code 集群管理code 分布式锁 zk的一…...

缺陷检测之图片标注工具--labme

一、labelme简介 Labelme是开源的图像标注工具&#xff0c;常用做检测&#xff0c;分割和分类任务的图像标注。 它的功能很多&#xff0c;包括&#xff1a; 对图像进行多边形&#xff0c;矩形&#xff0c;圆形&#xff0c;多段线&#xff0c;线段&#xff0c;点形式的标注&a…...

机器学习_13 决策树知识总结

决策树是一种直观且强大的机器学习算法&#xff0c;广泛应用于分类和回归任务。它通过树状结构的决策规则来建模数据&#xff0c;易于理解和解释。今天&#xff0c;我们就来深入探讨决策树的原理、实现和应用。 一、决策树的基本概念 1.1 决策树的工作原理 决策树是一种基于…...

请解释一下Standford Alpaca格式、sharegpt数据格式-------deepseek问答记录

1 Standford Alpaca格式 json格式数据。Stanford Alpaca 格式是一种用于训练和评估自然语言处理&#xff08;NLP&#xff09;模型的数据格式&#xff0c;特别是在指令跟随任务中。它由斯坦福大学的研究团队开发&#xff0c;旨在帮助模型理解和执行自然语言指令。以下是该格式的…...

ubuntu 安装管理多版本python3 相关问题解决

背景&#xff1a;使用ubuntu 22.04 默认python 未3.10.编译一些模块的时候发现需要降级到python3.9.于是下载安装 下载&#xff1a; wget https://www.python.org/ftp/python/3.9.16/Python-3.9.16.tgz解压与编译 tar -xf Python-3.9.16.tgz cd Python-3.9.16 ./configure -…...

滑动窗口算法篇:连续子区间与子串问题

1.滑动窗口原理 那么一谈到子区间的问题&#xff0c;我们可能会想到我们可以用我们的前缀和来应用子区间问题&#xff0c;但是这里对于子区间乃至子串问题&#xff0c;我们也可以尝试往滑动窗口的思路方向去进行一个尝试&#xff0c;那么说那么半天&#xff0c;滑动窗口是什么…...

Python爬虫实战:股票分时数据抓取与存储 (1)

在金融数据分析中&#xff0c;股票分时数据是投资者和分析师的重要资源。它能够帮助我们了解股票在交易日内的价格波动情况&#xff0c;从而为交易决策提供依据。然而&#xff0c;获取这些数据往往需要借助专业的金融数据平台&#xff0c;其成本较高。幸运的是&#xff0c;通过…...

【设计模式】【行为型模式】访问者模式(Visitor)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f3b5; 当你的天空突…...

基于实例详解pytest钩子pytest_generate_tests动态生成测试的全过程

关注开源优测不迷路 大数据测试过程、策略及挑战 测试框架原理&#xff0c;构建成功的基石 在自动化测试工作之前&#xff0c;你应该知道的10条建议 在自动化测试中&#xff0c;重要的不是工具 作为一名软件开发人员&#xff0c;你一定深知有效测试策略的重要性&#xff0c;尤其…...

Copilot基于企业PPT模板生成演示文稿

关于copilot创建PPT&#xff0c;咱们写过较多文章了&#xff1a; Copilot for PowerPoint通过文件创建PPT Copilot如何将word文稿一键转为PPT Copilot一键将PDF转为PPT&#xff0c;治好了我的精神内耗 测评Copilot和ChatGPT-4o从PDF创建PPT功能 Copilot for PPT全新功能&a…...

2025百度快排技术分析:模拟点击与发包算法的背后原理

一晃做SEO已经15年了&#xff0c;2025年还有人问我如何做百度快速排名&#xff0c;我能给出的答案就是&#xff1a;做好内容的前提下&#xff0c;多刷刷吧&#xff01;百度的SEO排名算法一直是众多SEO从业者研究的重点&#xff0c;模拟算法、点击算法和发包算法是百度快速排名的…...

七星棋牌全开源修复版源码解析:6端兼容,200种玩法全面支持

本篇文章将详细讲解 七星棋牌修复版源码 的 技术架构、功能实现、二次开发思路、搭建教程 等内容&#xff0c;助您快速掌握该棋牌系统的开发技巧。 1. 七星棋牌源码概述 七星棋牌修复版源码是一款高度自由的 开源棋牌项目&#xff0c;该版本修复了原版中的多个 系统漏洞&#…...

解锁原型模式:Java 中的高效对象创建之道

系列文章目录 后续补充~~~ 文章目录 一、引言1.1 软件开发中的对象创建困境1.2 原型模式的登场 二、原型模式的核心概念2.1 定义与概念2.2 工作原理剖析2.3 与其他创建型模式的差异 三、原型模式的结构与角色3.1 抽象原型角色3.2 具体原型角色3.3 客户端角色3.4 原型管理器角色…...

DeepSeek从入门到精通:揭秘 AI 提示语设计误区与 AI 幻觉(新手避坑指南)

文章目录 引言常见陷阱与应对策略&#xff1a;新手必知的提示词设计误区缺乏迭代陷阱&#xff1a;期待一次性完美结果过度指令与模糊指令陷阱&#xff1a;当细节缺乏重点或意图不明确假设偏见陷阱&#xff1a;当前 AI 只听你想听的幻觉生成陷阱&#xff1a;当AI自信地胡说八道忽…...

Jenkins同一个项目不同分支指定不同JAVA环境

背景 一些系统应用,会为了适配不同的平台,导致不同的分支下用的是不同的gradle,导致需要不同的JAVA环境来编译,比如a分支需要使用JAVA11, b分支使用JAVA17。 但是jenkins上,一般都是Global Tool Configuration 全局所有环境公用一个JAVA_HOME。 尝试过用 Build 的Execut…...

从入门到精通:Postman 实用指南

Postman 是一款超棒的 API 开发工具&#xff0c;能用来测试、调试和管理 API&#xff0c;大大提升开发效率。下面就给大家详细讲讲它的安装、使用方法&#xff0c;再分享些实用技巧。 一、安装 Postman 你能在 Postman 官网&#xff08;https://www.postman.com &#xff09;下…...

win32汇编环境,对话框中使用月历控件示例二

;运行效果 ;win32汇编环境,对话框中使用月历控件示例二 ;以下示例有2个操作,即将每周的开始日进行改变,将默认的周日开始改为周一开始,同时实现点击哪个日期,则设定为哪个日期 ;直接抄进RadAsm可编译运行。重要部分加备注。 ;下面为asm文件 ;>>>>>>>&…...

gsoap实现webservice服务

gsoap实现webservice服务 在实现Web服务时&#xff0c;使用gSOAP是一个很好的选择&#xff0c;因为它提供了强大的工具和库来创建SOAP和RESTful服务。gSOAP是一个C和C语言开发的库&#xff0c;它支持SOAP协议的各种版本&#xff0c;包括SOAP 1.1和SOAP 1.2。下面是如何使用gSO…...

容联云联络中心AICC:深度整合DeepSeek,业务验证结果公开

容联云重磅推出AICC3.2版本&#xff0c;实现了智能化的升级与外呼效率的突破——深度整合DeepSeek-R1大模型、预测式外呼在数据分析侧的增强、全渠道路由能力、一键多呼效率的强化。 同时&#xff0c;全面接入DeepSeek-R1的容联云 AICC3.2 &#xff0c;目前已与某知名汽车金融企…...

腿足机器人之七- 逆运动学

腿足机器人之七- 逆运动学 基本概念腿部运动的数学表示坐标系定义以及自由度说明正运动学模型 逆运动学求解几何解法数值迭代法雅可比矩阵法基础双足机器人步态规划中的雅可比法应用 工程挑战与解决方案实际应用中的工具和算法多解问题高自由度机器人&#xff08;如Atlas的28自…...

PHP和Node.js哪个更爽?

先说结论&#xff0c;rust完胜。 php&#xff1a;laravel&#xff0c;swoole&#xff0c;webman&#xff0c;最开始在苏宁的时候写了几年php&#xff0c;当时觉得php真的是世界上最好的语言&#xff0c;因为当初活在舒适圈里&#xff0c;不愿意跳出来&#xff0c;就好比当初活在…...

java 实现excel文件转pdf | 无水印 | 无限制

文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

【位运算】消失的两个数字(hard)

消失的两个数字&#xff08;hard&#xff09; 题⽬描述&#xff1a;解法&#xff08;位运算&#xff09;&#xff1a;Java 算法代码&#xff1a;更简便代码 题⽬链接&#xff1a;⾯试题 17.19. 消失的两个数字 题⽬描述&#xff1a; 给定⼀个数组&#xff0c;包含从 1 到 N 所有…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

Caliper 负载(Workload)详细解析

Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...

小木的算法日记-多叉树的递归/层序遍历

&#x1f332; 从二叉树到森林&#xff1a;一文彻底搞懂多叉树遍历的艺术 &#x1f680; 引言 你好&#xff0c;未来的算法大神&#xff01; 在数据结构的世界里&#xff0c;“树”无疑是最核心、最迷人的概念之一。我们中的大多数人都是从 二叉树 开始入门的&#xff0c;它…...

前端高频面试题2:浏览器/计算机网络

本专栏相关链接 前端高频面试题1&#xff1a;HTML/CSS 前端高频面试题2&#xff1a;浏览器/计算机网络 前端高频面试题3&#xff1a;JavaScript 1.什么是强缓存、协商缓存&#xff1f; 强缓存&#xff1a; 当浏览器请求资源时&#xff0c;首先检查本地缓存是否命中。如果命…...

如何在Windows本机安装Python并确保与Python.NET兼容

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...