MIT 6.5840 (Spring, 2024) – Lab 1: MapReduce

👨‍💻 Charles

🔗 实验手册: 6.5840 Lab 1: MapReduce

📃 MapReduce 论文原文: mapreduce-osdi04.pdf

✍️ 本系列前文: MIT 6.5840 (Spring, 2024) 通关指南——入门篇

文章目录

  • MIT 6.5840 (Spring, 2024) -- Lab 1: MapReduce
    • 代码理解
      • baseline:串行实现
      • todo:并行实现
        • `coordinator.go`
        • `worker.go`
    • 代码实现
      • `coordinator.go`
        • 初始化
        • 处理 Map 任务
        • 处理 Reduce 任务
        • 监控任务情况(防超时)
      • `rpc.go`
        • worker 获取 Map 任务
        • worker 提交 Map 结果
        • worker 获取 Reduce 任务
        • worker 提交 Reduce 结果
      • `worker.go`
        • 初始化
        • 处理 Map 任务
        • 处理 Reduce 任务
    • 实验结果
    • 踩坑记录/建议

代码理解

baseline:串行实现

首先,看看 Lab 中已给出的一个串行版 MapReduce —— src/main/mrsequential.go ,这是我们后续自己实现并行版本的重要参考。在 mrsequential.go 中,有 mapfreducef 两个组件,分别对应 Map 任务和 Reduce 任务:

mapf, reducef := loadPlugin(os.Args[1])

可以看到,它们是通过插件的形式导入的, loadPlugin 的实现在 mrsequential.go 中,利用了golang的 plugin 库,所以我们可以看到实验手册运行 mrsequential.go 之前先运行了:

go build -buildmode=plugin ../mrapps/wc.go

即将 wc.go 编译为 wc.so (动态加载共享库),之后运行 mrsequential.go 的时候就可以这样使用 wc.go 中的各种方法:

go run mrsequential.go wc.so pg*.txt

Anyway,其实就是说,Map 和 Reduce 的实现要到 src/mrapps/wc.go 中去找。源代码也挺简单的,实现方法为:

  • Split:以非字母符号为分隔符,将输入文件拆分为若干个单词,存到切片 words

  • Map:顺序处理 words 的单词,对于每个单词 w ,构建一个键值对 {w, 1} ,将这个键值对存到一个切片 kva

  • Reduce:统计 kva 中,每个单词的个数(在 kva 排序后,相同的单词挨在一起,把它们放到新切片 values := []string{} 中,Reduce其实就是返回 len(values)

todo:并行实现

本实验主要需要在已提供的代码基础上,完善 mr/coordinator.gomr/worker.gomr/rpc.go 。为了实现单 coordinator、多 worker 的并行架构,coordinator 需要负责给各 worker 分配 Map 任务和 Reduce 任务,并监控 worker 的工作情况、在发生超时的时候将其任务重新分配给其他 worker;同时,每个 worker 需要通过 RPC 调用 coordinator 的 Map 方法和 Reduce 方法,并保存相关结果、告知 coordinator 完成情况。

coordinator.go

我们需要实现的并行版 MapReduce 的主程序在 src/main/mrcoordinator.go 中,它负责调用 MakeCoordinator 构建 coordinator (任务分发者,相当于server)——这是在 src/mr/coordinator.go 中实现的,这个文件中已经声明/提示了我们 需要补全 的若干方法(见注释)。

coordinator 启动后,会通过 server() 方法创建一个 goroutine 来监听 src/mr/worker.goRPC 调用请求:

//
// start a thread that listens for RPCs from worker.go
//
func (c *Coordinator) server() {rpc.Register(c)rpc.HandleHTTP()//l, e := net.Listen("tcp", ":1234")sockname := coordinatorSock()os.Remove(sockname)l, e := net.Listen("unix", sockname)if e != nil {log.Fatal("listen error:", e)}go http.Serve(l, nil)
}

关于RPC的使用方法, worker.gocoordinator.go 中都有示例函数,所用的相关参数/方法定义在 src/mr/rpc.go 中。

建议先在现有代码上尝试 RPC 调用示例函数( worker.go 里面有个 CallExample() 基本可以直接用),从而熟悉代码框架。

关于 RPC 以及 golang 中如何使用 RPC,建议逢山开路,遇到不懂的就问 AI 🤖

worker.go

worker.go 即 map 和 reduce 任务的执行者,需要补全 Worker 方法:

// main/mrworker.go calls this function.
func Worker(mapf func(string, string) []KeyValue,reducef func(string, []string) string) {...}

关键在于处理和 coordinator 的通信(需要通过 RPC 调用,获取任务、执行任务)。

代码实现

完整代码: MIT-6.5840/src/mr at lab1 · Charles-T-T/MIT-6.5840

coordinator.go

Coordinator 结构体定义如下:

type Coordinator struct {mu            sync.RWMutexnMap          intnReduce       inttoMapTasks    chan MapTasktoReduceTasks chan ReduceTaskremainMapTask    map[string]string // filename -> workerIDremainReduceTask map[string]string // reduceID -> workerIDworkerRegistry   map[string]string // workerID -> workerAddrallMapDone       boolallReduceDone    bool
}
  • mu :读写锁,用于防止多个worker访问同一个coordinator成员出现冲突

  • workerRegistry :记录已经注册了的worker——只有已注册的worker提交的Map或Reduce结果才会被接受(防止收到超时worker的任务结果——已被重新分配了)

    原本设计的是workerID ➡️ workerAddr(worker 的 sock 地址)的一个哈希表,但是后续实现中发现维护 coordinator 和 worker 的双向通信似乎没必要,故这里仅当作一个集合使用。

  • nMapnReduce :需要执行的Map和Reduce任务总数

  • 其余成员变量作用易从其名称得出

初始化
// create a Coordinator.
// main/mrcoordinator.go calls this function.
// nReduce is the number of reduce tasks to use.
func MakeCoordinator(files []string, nReduce int) *Coordinator {c := Coordinator{nMap:             len(files),nReduce:          nReduce,toMapTasks:       make(chan MapTask, len(files)),toReduceTasks:    make(chan ReduceTask, nReduce),remainMapTask:    make(map[string]string),remainReduceTask: make(map[string]string),workerRegistry:   make(map[string]string),allMapDone:       false,allReduceDone:    false,}...
处理 Map 任务

初始化后,启动一个 goroutine 来处理 Map 任务:

// Manage Map tasks
go func() {// Init todo Map tasksfor i, file := range files {mapTask := MapTask{Filename: file,MapID:    strconv.Itoa(i),NMap:     c.nMap,NReduce:  c.nReduce,}c.toMapTasks <- mapTaskDPrintf("Get todo-file: %s\n", file)c.remainMapTask[file] = "init"}// Wait all Map tasks to be donefor len(c.remainMapTask) > 0 {time.Sleep(time.Second)}close(c.toMapTasks)c.allMapDone = trueDPrintf("All map tasks done.\n")
}()
处理 Reduce 任务

启动另一个 goroutine 来处理 Reduce 任务:

// Manage Reduce tasks
go func() {// output files for reduce resultsfor i := 0; i < nReduce; i++ {c.toReduceTasks <- ReduceTask{ReduceID: strconv.Itoa(i)}c.remainReduceTask[strconv.Itoa(i)] = "init"}// Wait all Map tasks to be donefor !c.allMapDone {time.Sleep(time.Second)}// Wait all Reduce tasks to be donefor len(c.remainReduceTask) > 0 {time.Sleep(time.Second)}close(c.toReduceTasks)c.allReduceDone = trueDPrintf("All reduce tasks done.\n")
}()
监控任务情况(防超时)

每次 worker 开始一个任务后,coordinator 就会启动一个 goroutine ——如果 10s(实验手册建议的超时时间)后任务仍未完成则视为超时,需要将该任务放回 todo-channel 中,等待其他 worker 认领:

// Monitor a Map task, reassign it if time out.
func (c *Coordinator) monitorMapTask(file string, mapID string) {time.Sleep(time.Second * 10) // wait for 10sworkerID, exist := c.remainMapTask[file]if exist {c.mu.Lock()delete(c.workerRegistry, workerID)DPrintf("Map job by %s time out!\n", workerID)c.mu.Unlock()c.toMapTasks <- MapTask{Filename: file, MapID: mapID, NMap: c.nMap, NReduce: c.nReduce}}
}// Monitor a Reduce task, reassign it if time out.
func (c *Coordinator) monitorReduceTask(reduceID string) {time.Sleep(time.Second * 10) // wait for 10sworkerID, exist := c.remainReduceTask[reduceID]if exist {c.mu.Lock()delete(c.workerRegistry, workerID)DPrintf("Reduce job by %s time out!\n", workerID)c.mu.Unlock()c.toReduceTasks <- ReduceTask{ReduceID: reduceID}}
}

rpc.go

worker 需要 RPC 调用 coordinator 的各方法均写在 rpc.go 中。

worker 获取 Map 任务

每个worker启动后,会首先尝试从coordinator的 toMapTasks channel 中获取一个Map任务,如果所有Map任务已完成、channel已关闭,则返回任务的 AllMapDone 字段为 true ;如果获取任务成功,则worker在 workerRegistry 注册,同时coordinator启动监视( c.monitorMapTask ),以在任务超时后重新分配任务。

func (c *Coordinator) WorkerGetMapTask(workerID string, mapTask *MapTask) error {toMapTask, ok := <-c.toMapTasksif ok {mapTask.Filename = toMapTask.FilenamemapTask.MapID = toMapTask.MapIDmapTask.NReduce = toMapTask.NReduce} else {mapTask.AllMapDone = true // all Map tasks already done.mapTask.AllReduceDone = c.allReduceDonereturn nil}// worker registersc.mu.Lock()c.workerRegistry[workerID] = workerSock(workerID)c.remainMapTask[toMapTask.Filename] = workerIDgo c.monitorMapTask(toMapTask.Filename, toMapTask.MapID)c.mu.Unlock()return nil
}
worker 提交 Map 结果

worker 完成其 Map 任务后,需要告知 coordinator,随后 coordinator 会从 remainMapTask 中移除该任务,视为任务完成。coordinator 只接受注册了的 worker 的结果。

worker 具体处理 Map 任务的过程在 worker.go 中,此处只是“通知任务完成”。

func (c *Coordinator) WorkerGiveMapRes(mapTask MapTask, reply *string) error {// Coordinator only accepts results from worker IN workerRegistryworkerID := mapTask.WorkerIDfilename := mapTask.Filename_, exist := c.workerRegistry[workerID]if !exist {DPrintf("Illegal map result: get from unknown worker: %s\n", workerID)return nil}c.mu.Lock()DPrintf("Successfully get map result from: %s\n", workerID)delete(c.remainMapTask, filename)c.mu.Unlock()return nil
}
worker 获取 Reduce 任务

实现思路和获取 Map 任务的一致:

func (c *Coordinator) WorkerGetReduceTask(workerID string, reduceTask *ReduceTask) error {toReduceTask, ok := <-c.toReduceTasksif ok {*reduceTask = toReduceTaskreduceTask.WorkerID = workerIDreduceTask.TempResFile = fmt.Sprintf("mr-tmp-%s", workerID)} else {reduceTask.AllReduceDone = true // all Reduce tasks already done.return nil}// worker registersc.mu.Lock()c.workerRegistry[workerID] = workerSock(workerID)c.remainReduceTask[toReduceTask.ReduceID] = workerIDgo c.monitorReduceTask(toReduceTask.ReduceID)c.mu.Unlock()return nil
}
worker 提交 Reduce 结果

实现思路和提交 Map 结果的一致:

func (c *Coordinator) WorkerGiveReduceRes(reduceTask ReduceTask, reply *string) error {// Coordinator only accepts results from worker in workerRegistryworkerID := reduceTask.WorkerID_, exist := c.workerRegistry[workerID]if !exist {DPrintf("Illegal reduce result: get from unknown worker: %s\n", workerID)return nil}newname := fmt.Sprintf("mr-out-%s", reduceTask.ReduceID)*reply = newnameerr := os.Rename(reduceTask.TempResFile, newname)if err != nil {DPrintf("Error when rename temp file: %v\n", err)}c.mu.Lock()DPrintf("Successfully get reduce result from: %s\n", workerID)delete(c.remainReduceTask, reduceTask.ReduceID)c.mu.Unlock()return nil
}

worker.go

worker 采用的 Map 和 Reduce 方法是通过不同插件载入的,我们不需要关心其实现,直接用就行了。

初始化
workerID := strconv.Itoa(os.Getpid())
mapDone := false    // flag whether all Map tasks have been finished
reduceDone := false // flag whether all Reduce tasks have been finished
处理 Map 任务

worker 启动后,周期性尝试从 coordinator 那里获取一个 Map 任务,获取任务后处理、向 coordinator 提交结果,直到收到所有 Map 任务已完成的通知,则将 mapDone 置为 true

// Do the map task
for !mapDone {mapTask := MapTask{WorkerID: workerID}DPrintf("<%s> ask for a map task...\n", workerID)call("Coordinator.WorkerGetMapTask", workerID, &mapTask)DPrintf("<%s> get task: %s\n", workerID, mapTask.Filename)if !mapTask.AllMapDone {file, err := os.Open(mapTask.Filename)if err != nil {DPrintf("cannot open %v\n", mapTask.Filename)return}content, err := io.ReadAll(file)if err != nil {DPrintf("cannot read %v\n", mapTask.Filename)return}file.Close()kva := mapf(mapTask.Filename, string(content))saveMapRes(kva, mapTask.MapID, mapTask.NReduce)mapTask.Result = kvavar reply stringcall("Coordinator.WorkerGiveMapRes", mapTask, &reply)} else {mapDone = truereduceDone = mapTask.AllReduceDoneDPrintf("All map tasks done.\n")}time.Sleep(500 * time.Millisecond)
}

其中,Map 任务产生的中间结果需要保存到文件中,参考实验手册的 hint:

hint1

实现如下:

func saveMapRes(kva []KeyValue, mapID string, nReduce int) {reduceChunks := make(map[string][]KeyValue) // reduceID -> kvsfor _, kv := range kva {reduceID := strconv.Itoa(ihash(kv.Key) % nReduce)reduceChunks[reduceID] = append(reduceChunks[reduceID], kv)}for reduceID, kvs := range reduceChunks {oname := fmt.Sprintf("mr-%s-%s.json", mapID, reduceID)ofile, _ := os.Create(oname)defer ofile.Close()enc := json.NewEncoder(ofile)err := enc.Encode(&kvs)if err != nil {DPrintf("Error when encoding kv: %v\n", err)}}DPrintf("Finish saving map result.\n")
}
处理 Reduce 任务

和处理 Map 任务的思路一致——周期性尝试获取一个 Reduce 任务 ➡️ 处理 Reduce 任务 ➡️ 保存 Reduce 结果 ➡️ 向 coordinator 提交结果:

// Do the Reduce task
for !reduceDone {reduceTask := ReduceTask{WorkerID: workerID}DPrintf("<%s> ask for a reduce task...\n", workerID)call("Coordinator.WorkerGetReduceTask", workerID, &reduceTask)DPrintf("<%s> get reduceID: %s\n", workerID, reduceTask.ReduceID)if !reduceTask.AllReduceDone {// Get Map result files to be Reducedpattern := fmt.Sprintf(`^mr-.*-%s.json$`, regexp.QuoteMeta(reduceTask.ReduceID))re := regexp.MustCompile(pattern)files, err := os.ReadDir(".")if err != nil {fmt.Println("Error reading directory:", err)return}var toReduceFiles []stringfor _, file := range files {if !file.IsDir() && re.MatchString(file.Name()) {toReduceFiles = append(toReduceFiles, file.Name())}}// Do the reduce jobdoReduce(toReduceFiles, reducef, reduceTask.TempResFile)DPrintf("<%s> finish reduce job, res to %s.\n", workerID, reduceTask.TempResFile)var reply stringcall("Coordinator.WorkerGiveReduceRes", reduceTask, &reply)DPrintf("<%s> reduce res save to %s.\n", workerID, reply)} else {reduceDone = trueDPrintf("All reduce done.\n")}time.Sleep(100 * time.Millisecond)
}

其中负责执行 Reduce 的方法 doReduce 主要参考 mrsequential.go 实现:

func doReduce(toReduceFiles []string, reducef func(string, []string) string, oname string) {ofile, _ := os.Create(oname)defer ofile.Close()intermediate := []KeyValue{}for _, toReduceFile := range toReduceFiles {file, _ := os.Open(toReduceFile)dec := json.NewDecoder(file)kva := []KeyValue{}if err := dec.Decode(&kva); err != nil {DPrintf("Error when json decode: %v\n", err)return}intermediate = append(intermediate, kva...)file.Close()}sort.Sort(ByKey(intermediate))i := 0for i < len(intermediate) {j := i + 1for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {j++}values := []string{}for k := i; k < j; k++ {values = append(values, intermediate[k].Value)}output := reducef(intermediate[i].Key, values)fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)i = j}
}

实验结果

手动测试并打印中间过程(在 worker.go 中将 Debug 设置为 true ):
lab1-res1

运行测试脚本 mr-test.sh
res2

测试通过。

踩坑记录/建议

  • DPrintf 打印日志能发现大部分 bug,但是可能有些细节需要用打断点调试,如果是 vscode 的话需要配置一下:

    • 例如,对于 worker,要断点调试 wc 任务,需要在 .vscode/launch.json 中添加配置:

      {"name": "mrworker-wc","type": "go","request": "launch","mode": "exec","program": "${workspaceFolder}/6.5840/src/main/mrworker","args": ["wc.so"],"cwd": "${workspaceFolder}/6.5840/src/main"
      },
      
    • 对于 coordinator,可以配置:

      {"name": "debug mrcoordinator","type": "go","request": "launch","mode": "auto","program": "${workspaceFolder}/6.5840/src/main/mrcoordinator.go","args": ["pg-being_ernest.txt","pg-dorian_gray.txt","pg-frankenstein.txt","pg-grimm.txt","pg-huckleberry_finn.txt","pg-metamorphosis.txt","pg-sherlock_holmes.txt","pg-tom_sawyer.txt"]
      }
      

      具体参数可以根据任务调整,不懂的多问 AI。

  • 本 lab 实现的是一个 MapReduce 框架 ,也就是说具体的 Map 任务和 Reduce 任务 不是一定的 ——我一开始以为只有单词计数( src/mrapps/wc.go ),所以傻了吧唧地搬运 mrsequential.go 的代码,但实际上最后测试的任务有很多,都在 src/mrapps/ 下。最后跑 test-mr.sh 的时候,也可以根据出错任务到 src/mrapps/ 中看看对应任务代码,可能有所启发。

  • RPC 函数,不仅 函数名首字母大写 ,如果参数是结构体,则该结构体中的 成员变量也要首字母大写

    否则你可能会像我一样,发现 reply 中有些成员被更新了、有些没有,非常诡异 🤷‍♂ ​

  • 仔细阅读官方实验手册的 Hints ,很有用。

    比如前一条,其实 Hints 中就有提到:

    “Go RPC sends only struct fields whose names start with capital letters. Sub-structures must also have capitalized field names.”

  • 注意采用合理方法保存 Map 任务的中间结果,便于之后 Reduce 任务读取。 Hints 中的建议是:

    “A reasonable naming convention for intermediate files is mr-X-Y, where X is the Map task number, and Y is the reduce task number.”

  • 注意给 coordinator 上锁,防止多 worker 的读写冲突。


如果你觉得有帮助,欢迎去 我的代码仓库 点个 star ⭐️ : )

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

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

相关文章

吴恩达机器学习作业五:神经网络正向传播

数据集在作业一正向传播正向传播&#xff08;Forward Propagation&#xff09;是神经网络计算过程中的核心步骤&#xff0c;指的是将输入数据通过神经网络的各层依次传递&#xff0c;最终得到输出结果的过程。核心原理在神经网络中&#xff0c;信息从输入层流入&#xff0c;经过…

网络编程(4)

【0】复习 sockfdsocket(); //指定网络信息 bind(); listen(); //创建表 fd_set rfds,tempfds; FD_ZERO(); FD_SET(sockfd); max sockfd while(1) {tempfdsrfds;select(max1,&tempfds)if(FD_ISSET(scokfd,&tempfds)){acceptfdaccept();FD_SET(acceptfd,&rfds);if(m…

Windows系统提示“找不到文件‘javaw‘”

1. Java 未安装或安装不完整javaw.exe 是 Java 运行环境&#xff08;JRE&#xff09;的核心文件&#xff0c;用于运行 Java 程序&#xff08;如.jar 文件&#xff09;。如果你的电脑没有安装 Java&#xff0c;或安装过程中 javaw.exe 被误删&#xff0c;系统就会找不到它。2. J…

【PCIE系列】1---PCIE系统拓扑结构分析

架构由点对点链路&#xff08;Links&#xff09;组成&#xff0c;用于互连组成系统的一系列组件。下图展示了一个示例拓扑结构。该图描述了一个有层次的体系架构实例&#xff0c;其包含根复合体&#xff08;Root Complex, RC&#xff09;、多个端点&#xff08;I/O设备&#xf…

SpringBoot防止重复提交(2)

例如&#xff1a;多次点击提现按钮问题描述&#xff1a;在提现操作中&#xff0c;用户可能会多次点击提现按钮&#xff0c;导致多个相同的请求发送到服务器&#xff0c;从而引发重复提现的问题。为了解决这一问题&#xff0c;必须保证每个提现请求只能执行一次&#xff0c;防止…

mysql zip包安装步骤

下载地址 windows MSI Install 安装包程序。 这里下载zip包&#xff0c;执行安装过程 确认my.ini 配置的路径&#xff0c;创建mysql数据服务的data目录管理员身份cmd 进入bin目录&#xff0c;开始初始化服务 mysqld --initialize-insecure --usermysql mysqld -install#启动…

Python 的 argparse 模块中,add_argument 方法的 nargs 参数

在 Python 的 argparse 模块中&#xff0c;add_argument 方法的 nargs 参数用于指定命令行参数可以接受的参数数量。你提到的 nargs* 和 nargs 是两种常见设置&#xff0c;它们分别表示不同的参数数量要求。以下是两者的详细区别和含义&#xff1a;1. nargs*: 接受零个或多个参…

嵌入式Linux LED驱动开发

嵌入式Linux LED驱动开发 一、LED驱动概述 本笔记基于IMX6ULL处理器的LED驱动开发&#xff0c;详细介绍了字符设备驱动开发的基本流程。该驱动实现了对LED的基本控制功能&#xff0c;通过字符设备接口供用户空间程序调用。 二、LED驱动核心概念 1. 寄存器地址定义 本驱动涉…

Excel Word Pdf 格式转换

引入aspose包手动更新本地mvn仓库mvn install:install-file -DfileC:\aspose-cells-22.9.jar -DgroupIdaspose -DartifactIdaspose-cells -Dversion22.9 -Dpackagingjar mvn install:install-file -DfileC:\aspose-pdf-22.9.jar -DgroupIdaspose -DartifactIdaspose-pdf -Dvers…

变频器实习DAY40 调整测试零伺服PI LDO

目录变频器实习DAY40一、工作内容1.1 调整测试零伺服PI二、学习内容2.1 LDOLDO的核心工作原理——“采样-比较-调整”闭环控制LDO的关键参数——选型核心依据LDO与其他稳压器的选型对比附学习参考网址欢迎大家有问题评论交流 (* ^ ω ^)变频器实习DAY40 一、工作内容 1.1 调整…

【半导体制造流程概述】

半导体制造流程概述 半导体制造是一个高度复杂且精密的过程&#xff0c;涉及多个关键步骤&#xff0c;通常分为以下几个主要阶段&#xff1a;设计、晶圆制备、光刻、刻蚀、掺杂、薄膜沉积、互连和封装测试。 文章目录半导体制造流程概述晶圆制备光刻刻蚀掺杂薄膜沉积互连封装测…

为什么大模型需要文档预处理:从数据到智能的关键一步

在人工智能&#xff0c;尤其是大语言模型&#xff08;LLM, Large Language Models&#xff09;的应用落地过程中&#xff0c;数据质量与处理流程的重要性正逐渐被各行各业所认识。无论是企业内部构建知识库、自动化文档审核&#xff0c;还是面向用户提供智能问答服务&#xff0…

50.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--二期功能规划

啰嗦了这么多文章&#xff0c;我们终于进入到了二期功能的开发。这篇文章我们先来规划一下二期要做的功能&#xff0c;在一期功能中&#xff0c;我们完成了基础的记账功能&#xff0c;但是作为一个记账软件&#xff0c;仅有这些功能是远远不够的。我们需要更多的功能来满足用户…

Oracle下载安装(学习版)

1. 下载&#xff08;学习版&#xff09; 网址&#xff1a;软件下载 | Oracle 中国 2. 安装 解压缩 双击可执行文件 下一步 选同意&#xff0c;下一步 下一步 设置密码&#xff08;自己记住&#xff09; 开始安装 测试安装是否成功

`basic_filebuf`、`basic_ifstream`、`basic_ofstream`和 `basic_fstream`。

C 文件 I/O 模板类深度解析 文章目录C 文件 I/O 模板类深度解析1. basic_filebuf 深度解析1.1 类模板定义详解1.2 关键成员变量1.3 核心成员函数实现原理1.3.1 open() 函数实现1.3.2 overflow() 函数实现1.4 完整示例&#xff1a;自定义缓冲策略2. basic_ifstream 深度解析2.1 …

计算机毕设 java 阿歹果园养鸡场管理系统 基于 SSM 框架的果园养鸡场全流程管理系统设计与实现 Java+MySQL 的养殖生产与进销存一体化平台开发

计算机毕设 java 阿歹果园养鸡场管理系统ky7dc9 &#xff08;配套有源码 程序 mysql数据库 论文&#xff09;本套源码可以先看具体功能演示视频领取&#xff0c;文末有联xi 可分享 随着农业养殖规模化发展&#xff0c;传统果园养鸡场依赖人工记录、纸质台账的管理模式&#xf…

生成式BI工具(WrenAI)

生成式 BI 工具支持自然语言查询数据库&#xff0c;自动生成 SQL 与可视化图表&#xff0c;被金融分析师和数据科学家广泛采用。 WrenAI是由Canner团队开发的开源生成式BI&#xff08;GenBI&#xff09;智能体&#xff0c;致力于通过自然语言交互实现数据库查询、可视化生成和洞…

论文Review 3DGS PGSR | TVCG2024 ZJU-3DV | 几何约束的3DGS表面重建

基本信息 题目&#xff1a;PGSR: Planar-based Gaussian Splatting for Efficient and High-Fidelity Surface Reconstruction 来源&#xff1a;TVCG2024 学校&#xff1a;ZJU-3DV 是否开源&#xff1a;https://github.com/zju3dv/PGSR 摘要&#xff1a;3DGS表面重建 最近…

最新After Effects2025下载安装(含安装包)AE 2025 保姆级下载一键安装图文教程

文章目录一、After Effects 2025下载二、After Effects 2025安装教程三、核心功能升级详解四、系统配置与兼容性说明一、After Effects 2025下载 ①夸克网盘下载链接&#xff1a;https://pan.quark.cn/s/a06e6200e64c 二、After Effects 2025安装教程 1.解压安装包:找到下载…

【网络安全领域】边界安全是什么?目前的发展及应用场景

在网络安全领域&#xff0c;边界安全&#xff08;Perimeter Security&#xff09; 是指围绕企业或组织网络的 “物理与逻辑边界” 构建的防护体系&#xff0c;核心目标是阻止未授权访问从外部网络&#xff08;如互联网、合作方网络&#xff09;侵入内部可信网络&#xff0c;同时…