dream

一个菜鸟程序员的成长历程

0%

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

优化MapReduce,10个核心功能决定能否进化为工业级

在上一篇文章中,我们已经使用golang从0-1设计实现了一个MapReduce系统,主要参考了googleMapReduce论文。

今天我们就来聊聊:从教学版到工业级MapReduce,到底还差哪些关键特性?我会结合真实代码片段、架构图和踩坑经验,梳理出10个必须补齐的核心功能,并给出优先级建议。

问题的本质:为什么“能跑”不等于“可用”?

我们刚开始做MapReduce项目时,都是“一台机器读文件、分配任务、收集结果”,看起来没啥问题。但一旦遇到超大文件、节点故障、网络瓶颈,这套方案立刻原形毕露:

比如:

  • 单点的Master,容易出现单点故障
  • 任务的划分纬度是文件纬度的,论文中的任务是数据块纬度的
  • 不支持分布式文件系统,论文中使用的是google的GFS分布式文件系统
  • 慢节点会拖慢整体的计算速度
  • 网络传输没有一些优化措施

说实话,这些痛点不是纸上谈兵,而是每个工程师都会踩过的坑。下面我们逐条拆解论文要求和实际实现之间的鸿沟。

技术方案解析

  1. 分布式文件系统支持(GFS集成)

论文要求

  • 数据自动分片、副本管理
  • 任务调度考虑数据位置(数据本地性)

当前实现现状

1
2
// 教学版直接读取本地文件
content := readFile(task.Filename[0])

缺失分析:

  • 没有分布式存储,单机容量受限
  • 无法利用副本提升容错能力
  • 数据本地性完全缺失

工业级改进思路:

根据论文的描述,论文中集成的是GFS分布式文件系统,但这个是google自己内部使用的,我们可以使用一些开源的,比如Hadoop的HDFS。实现自动分片与副本管理。

其实,著名的开源软件Hadoop就是一个使用JAVA完美实现了MapReduce的系统。如果有人看过Hadoop的源码,就会发现和我们上一篇的实现很像。

我们还可以优化一下任务调度机制,任务调度时优先选择拥有目标数据块副本的Worker。

1
2
3
4
5
graph TB
A[用户提交作业] --> B[GFS Master]
B --> C[GFS ChunkServer1]
B --> D[GFS ChunkServer2]
C & D --> E[Worker节点]
  1. 输入数据分片机制

论文要求:

  • 自动将大文件切成64MB块(或动态调整)
  • 分片元数据记录位置和大小

我们自己的实现版本如下

1
2
3
4
5
6
7
8
// 一个文件对应一个Map任务,不支持大文件并行处理
for _, filename := range files {
task := Task{
Number: maxTaskNumber,
Filename: []string{filename},
Type: MapTask,
}
}

一些改进的建议:应该实现文件分片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func splitFile(filename string, splitSize int64) []Split {
splits := []Split{}
file, _ := os.Open(filename)
defer file.Close()
fileInfo, _ := file.Stat()
fileSize := fileInfo.Size()

for offset := int64(0); offset < fileSize; offset += splitSize {
split := Split{
Filename: filename,
Offset: offset,
Length: min(splitSize, fileSize-offset),
}
splits = append(splits, split)
}
return splits
}

有意思的是,大部分人都忽略了元数据管理。如果没有记录每个split的位置和长度,后续恢复/重试会非常麻烦。

  1. 数据本地性调度优化

论文核心机制:

  • 优先级:本地 > 同机架 > 跨机架远程
  • 网络拓扑感知 + 负载均衡

我们自己的实现版本如下

1
worker := <- m.IdleWorkers // 任意空闲Worker,无视数据位置

一些改进的建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (m *Master) selectBestWorker(task Task) *WorkerStruct {
// 1. 优先选择数据本地的Worker
for _, worker := range m.IdleWorkers {
if worker.hasLocalData(task.DataLocation) {
return worker
}
}

// 2. 选择同机架的Worker
for _, worker := range m.IdleWorkers {
if worker.inSameRack(task.DataLocation) {
return worker
}
}

// 3. 选择任意Worker
return m.IdleWorkers[0]
}

优化的流程图如下

1
2
3
4
5
6
flowchart TD
A[收到新任务] --> B{有无Local Data Worker?}
B -- 是 --> C[派发给Local Worker]
B -- 否 --> D{同Rack Worker?}
D -- 是 --> E[派发给Rack内Worker]
D -- 否 --> F[随机派发远程Worker]

踩过坑的人都知道,如果不做这个优化,大型集群下网络流量会炸锅!

  1. Master容错机制(检查点+热备)

风险分析:

  • 当前Master是单点故障,一旦挂掉所有作业全部失败。
  • 无法从中间状态恢复作业
  • 大型作业重启的成本也比较高

我们自己的实现版本如下

1
2
3
type Master struct {
// 没有持久化状态,也没有备份机制!
}

工业级方案:

  • 定期保存检查点+日志重放+多Master热备:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sequenceDiagram  
participant M as 主Master
participant S as Standby Master
M->>S: 定期同步状态
M-->>S: 崩溃后由Standby接管并恢复状态
```

数据不会撒谎:`Google MapReduce`生产环境里,每天数千次作业重启,如果没有容错机制根本扛不住!

5. 高级容错特性——慢任务检测与推测执行

> 慢节点拖累整体速度,是大规模集群最常见的问题之一。

`MapReduce`有一个问题就是Reduce任务需要等所有的Map任务执行完成以后才能开始执行,而如果最后一个Map的任务执行的很慢,那么就会拖累整个集群的计算速度。

因为所有的worker节点都需要等待这个Map任务的完成。

论文中提供了一个方法,就是将这些最后执行较慢的任务,多分配几个worker一起执行,哪个worker最先执行完毕,这个任务就执行完了。

代码示例如下
```go
func (m *Master) detectStragglers() {
for _, task := range m.runningTasks {
if time.Since(task.StartTime) > expectedTime*1.5 {
m.launchBackupTask(task)
}
}
}

实战中,经常遇到某台机器磁盘坏道或者CPU异常导致某些task极慢。推测执行可以显著提升尾部延迟表现。

  1. 网络拓扑感知能力

不仅仅是“同机架优先”,更要考虑带宽、交换机层次等因素:

代码示例如下:

1
2
3
4
5
6
7
8
9
type NetworkTopology struct {
Racks map[string][]string // 机架到机器的映射
}

func (nt *NetworkTopology) getDistance(node1, node2 string) int {
if node1 == node2 { return 0 } // 同节点
if nt.sameRack(node1, node2) { return 1 } // 同机架
return 2 // 不同机架
}

在超大规模集群下,没有拓扑感知很容易出现跨交换机流量拥堵。

  1. 性能优化特性
  • Combiner预聚合
    • 在Map端提前聚合,可以减少大量重复key的数据传输:

代码示例如下:

1
2
type Combiner func(key string, values []string) []string 
// Map输出前调用Combiner进行预聚合
  • 压缩支持
    • 中间结果写入前压缩,有效降低I/O压力:
1
func writeCompressedOutput(data []KeyValue) { /* gzip压缩 */ } 
  • 内存缓冲批量写入
    • 减少频繁磁盘操作,提高吞吐:
1
2
3
4
5
6
// 应该实现内存缓冲,批量写入
type OutputBuffer struct {
buffer []KeyValue
size int
maxSize int
}

性能测试显示,仅Combiner和压缩两项优化,就能让典型WordCount作业提速30%以上!

当大数据量进行离线计算的时候,性能尤其重要,尤其是企业级真正使用的时候,对于性能是非常看重的,这些性能的具体优化可以参考Hadoop,或者你有更好的方案实现,那么你还可以优化Hadoop,成为开源的贡献者。

  1. 运维监控与调试支持

根据MapReduce论文里面的要求,我们还需要有一些监控手段:

  • Web界面:实时监控作业进度
  • 性能计数器:统计各种指标
  • 日志聚合:集中收集所有节点日志

对于监控,就需要一些前端的实现了,这个比较简单,提供一些API接口即可。

  1. 配置参数化能力

灵活配置各类参数,让系统适应不同规模和场景需求,比如:

  • 比如每个worker节点的Map任务数量,Reduce任务数量
  • 比如分片的大小
  • 比如缓冲区的大小
  • 比如压缩算法用哪个算法

我们可以将这些变成参数配置,随时修改。

1
2
3
4
5
6
7
8
type MapReduceConfig struct {
MapTasksPerNodeint // 每节点Map任务数
ReduceTasksPerNode int // 每节点Reduce任务数
SplitSize int64 // 分片大小
SortBufferSize int // 排序缓冲区大小
CompressionType string // 压缩算法
ReplicationFactor int // 副本数量
}
  1. 优先级改进建议

上面说了这么多的优化能力,但是不可能一下子全部实现,因此,对于这些也是可以有一个优先级的。

最高优先级的,肯定是一些核心的功能优化,比如

  • 文件的分片机制,可以将一个大文件分片成小的文件块进行处理
  • Master检查点,可以进行中断恢复,不至于一下子失败了要从头开始
  • 慢任务优化,优化整体性能

中优先级的则是一些性能优化相关的改进,这些改进可以让我们的系统支持大数据量的计算,真正的投入生产使用

  • 数据本地调度,可以较少网络开销
  • Combiner预聚合支持,可以提升Map任务性能
  • 压缩支持,可以减少IO开销,提升速度

低优先级的改进,这些也是需要的,但是相比于上面的优化来说,优先级更低

  • Web监控界面
  • 网络拓扑感知
  • 配置参数化调整

总结

如果能把上面的都实现,其实就相当于实现了一个Hadoop,因此,我们实现的时候,也可以参照Hadoop的源码。

有什么更好的优化思路,可以留言一起交流~

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

为什么分布式计算框架都逃不出MapReduce的“影子”?3个进化方向你必须看懂

之前,我们已经从0-1设计并实现过一个基于Golang的MapReduce系统了,当然,我们实现的系统还有一些缺陷,今天,我们就来看一下这些开源的基于MapReduce升级版的系统。他们的设计思想,优化点是哪些。

凌晨3点,数据平台报警:某金融风控模型训练任务卡死,团队一边排查Hadoop集群,一边感叹——“怎么还是老掉牙的MapReduce?”
其实,不管是Spark、Storm还是Flink,你会发现它们的底层设计都绕不开MapReduce。但为什么这些新框架还要不断“改造”前辈?到底有哪些局限被突破,又有哪些坑依然存在?这篇文章,我就带你拆解分布式计算框架的技术谱系和演进逻辑。

问题的本质:MapReduce到底解决了什么,又卡在哪儿?

首先,我们需要明白MapReduce为什么是基石,它凭什么坚定了以后的分布式计算的基础。因为它把分布式计算最难啃的几块骨头——编程抽象、容错机制、数据本地性、自动并行化——都做到了极致:

  • 编程抽象:只需写好map和reduce两个函数,剩下复杂度全交给系统。
  • 容错机制:任务级别自动重试,不怕节点挂掉。
  • 数据本地性:让计算主动靠近数据,减少网络传输。
  • 自动并行化:开发者不用关心分片、调度等细节。

但用过的人都知道,这套模式也有硬伤,毕竟是04年发表的论文,到现在已经过去很久了,技术的日新月异,导致当时看来很厉害的技术,现在已经过时了:

  • 磁盘I/O瓶颈:每轮任务都要落盘,中间结果反复读写磁盘,性能拉胯。
  • 批处理导向:只能做离线大批量处理,对实时/流式场景无能为力。
  • 编程灵活性差:只有两种操作(map/reduce),复杂逻辑很难表达。
  • 迭代效率低下:比如机器学习算法,每次迭代都得重新读写磁盘,非常慢。

说实话,这些痛点直接催生了后续所有主流分布式计算框架。

有次我们广告CTR模型训练,用MapReduce跑了一晚上还没动静,同事直接开喷:“这TM也太慢了吧!”

技术方案解析

Apache Spark——内存计算革命

Spark就是主要解决了MapReduce中的磁盘IO问题,因为MapReduce每次都是写入和读取磁盘,我们知道,磁盘的速度是远远低于内存的。

Spark的核心创新就是RDD弹性分布式数据集,可以.cache()到内存,再也不用反复读写HDFS。

1
2
3
4
5
val rdd = sc.textFile("hdfs://...")
.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.cache() // 内存缓存!

下面这几点就是Spark解决掉的MapReduce的一些痛点问题,也是Spark的优势,当我们要做一些技术选型的时候,我们可以根据这些来判断我们是否要使用Spark。

  • 内存计算:RDD可以缓存中间结果到内存,大幅减少磁盘I/O。
  • DAG执行引擎:支持任意复杂的数据流图,不再受限于两阶段模型。
  • 多语言支持:Scala、Java、Python、R全覆盖。
  • 统一生态圈:SQL分析(Spark SQL)、机器学习(MLlib)、图计算(GraphX)、流处理(Streaming)一网打尽。

做过一些测试,测试数据如下:

  • MR版耗时2小时,全程CPU不到30%,I/O飙90%
  • Spark版18分钟搞定,CPU利用率70%,I/O只有20%

老板看到报表直接说:“以后这种活都用Spark整。”

下表是一些对比,简单明了。

特性 MapReduce Spark
计算模型 两阶段 DAG随便整
数据存储 磁盘 内存+磁盘
迭代性能 菜鸡 牛逼
实时性 批处理 准实时微批

虽说Spark解决了磁盘IO的问题,但是我们要明白,内存虽然速度快,但是。。。它贵啊,一般来说内存都是比较小的,因此我们要谨慎使用内存,不要光顾着快了。持久才是王道。

Apache Storm——真正的实时流处理

如果说SparkMapReduce都是离线数据处理的话,那么Storm就可以实现实时数据处理了。

它采用Spout-Bolt拓扑结构,实现毫秒级延迟的数据处理:

Topology就是一张有向图,把Spout和Bolt连起来,每条消息像水一样流过去。

1
2
3
4
5
6
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("sentences", new RandomSentenceSpout(), 5);
builder.setBolt("split", new SplitSentence(), 8)
.shuffleGrouping("sentences");
builder.setBolt("count", new WordCount(), 12)
.fieldsGrouping("split", new Fields("word"));

革命性特性:

  • 毫秒级延迟,消息来了立刻处理,不用等下一轮批量触发。
  • Spout-Bolt拓扑结构,自由组合各种算子,比MR灵活太多。
  • At-least-once语义,有丢包自动重试,不怕漏数。
  • Worker/Supervisor故障自恢复,上次生产环境挂了一台机器,都没人发现。

应用场景举例:

  • 系统指标监控报警
  • 用户行为推荐
  • 金融交易风控

但Storm同样也有一些缺点,比如:状态管理不行

Apache Flink——流批一体化新范式

其实,我们仔细看就会发现,Spark解决了磁盘IO的问题,Storm实现了从离线数据计算到实时计算的跨越,而Flink解决了Storm状态管理的问题。

对于Flink来说,同一个API既能跑离线批处理,也能搞实时流分析:

Flink具有以下核心优势,这些优势是Flink不同于其他几个框架的最大的竞争力:

  • 同一个API同时搞定批处理和流处理,不用切换框架脑壳疼。
  • 事件时间+Watermark,对乱序数据友好得很。(之前Storm遇到乱序就懵逼)
  • 精确一次语义(Exactly-once),金融场景必备,再也不怕账算错了被罚钱。
  • 状态管理强大,有分布式状态后端,还能快照恢复。

一些简单的代码示例

1
2
3
4
5
DataStream stream = env.socketTextStream("localhost", 9999);
stream.flatMap(new Splitter())
.keyBy(0)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.sum(1);

技术突破点:

  • Dataflow模型实现Google论文思路,比传统DAG更灵活;
  • 检查点机制保证高可用;
  • 自适应反压机制防止下游堵塞崩溃;
  • 不过Flink学习曲线是真的陡,新人入坑容易爬不出来…

技术谱系&现代架构长啥样?

说个真实例子,我们曾经在广告点击日志处理中,从Hadoop迁移到Spark后,单次ETL耗时从6小时降到40分钟;而在用户行为实时推荐上,用Storm实现后端流水线,把延迟从10秒降到了500毫秒。

下面使用Mermaid画一个流程图,来看一下从MapReduce一直到Flink的技术进化史,感受一下技术的日新月异,也不要忘了致敬经典,吃水不忘挖井人:

1
2
3
4
5
6
7
graph TD
A[MapReduce] --> B[Hadoop生态]
A --> C[Spark]
A --> D[Storm]
C --> E[Spark Streaming]
D --> F[Flink]
F --> G[流批一体化]

深度思考:“湖仓一体”和云原生才是终极形态?

回头看这些年大数据平台的发展,有几个趋势特别明显:

  • 共性特征
    • 计算与存储彻底解耦
    • Spark/Flink负责算力调度;HDFS/S3/Delta Lake负责海量存储;YARN/K8s负责资源管理。
  • 全面云原生化
    • Docker容器部署,大规模弹性伸缩变得容易;微服务架构让各模块独立升级维护不再痛苦。
  • 湖仓一体融合
    • 数据湖保存原始明细,仓库负责结构化分析,两者通过统一接口协作,实现离线+实时混合查询能力。

总结

最后,总结一下这几个框架的优缺点:

框架 优势 劣势
Spark 批量分析快、生态丰富 真正实时差、吃内存多
Storm 毫秒级响应、运维简单 状态弱、生态单一
Flink 流批统一、一致性强 学习曲线陡峭

根据这些优缺点,可以根据具体业务的需要来进行选择,如果只是需要离线数据运算的话,可以选择Spark,如果是要进行实时数据的话,可以选择Storm或者Flink。

如果有对具体框架感兴趣的可以进行留言,我会深挖框架的源码,并出一个从0-1设计实现的文章。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程Java应用连接MySQL使用教程

从JDBC到MyBatis Plus的完整学习路径

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

教程概述

本教程将带你从零开始掌握MySQL在Java应用中的使用,涵盖从原生JDBC到现代化ORM框架的完整技术栈。通过丰富的实例和对比分析,帮助初级程序员快速上手数据库开发。

学习目标

  • 掌握JDBC的基本使用和最佳实践
  • 理解MyBatis的核心概念和配置
  • 学会MyBatis Plus的高效开发模式
  • 了解三种技术的适用场景和选择原则

技术栈对比

技术 学习难度 开发效率 灵活性 适用场景
JDBC ⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ 底层操作、性能要求极高
MyBatis ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ 复杂查询、定制化需求
MyBatis Plus ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ 快速开发、标准CRUD

第一部分:JDBC - Java数据库连接的基石

🏗️ JDBC架构原理

JDBC(Java Database Connectivity)是Java访问数据库的标准API,它定义了一套标准接口,允许Java程序与各种数据库进行交互。

想要学习JAVA链接MySQL数据库,首先就需要明白JDBC,因为所有的JAVA应用都是通过JDBC进行链接MySQL的。

1
2
3
4
5
6
7
8
9
┌─────────────────┐
│ Java应用程序 │
├─────────────────┤
│ JDBC API │
├─────────────────┤
│ JDBC驱动 │
├─────────────────┤
│ MySQL数据库 │
└─────────────────┘

核心组件说明:

下面的核心组件共同构成了完整的JDBC。

  • DriverManager: 管理数据库驱动程序
  • Connection: 表示与数据库的连接
  • Statement: 执行SQL语句的对象
  • PreparedStatement: 预编译的SQL语句
  • ResultSet: SQL查询的结果集

JDBC快速入门

1. 添加MySQL驱动依赖

首先要在pom文件中添加mysql依赖

1
2
3
4
5
6
<!-- pom.xml -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>

2. 数据库连接配置

这里指定了使用的是JDBC链接,链接mySQL的地址,连接到哪个database中,使用的用户名和密码是什么。

1
2
3
4
5
6
7
// DatabaseConfig.java
public class DatabaseConfig {
public static final String URL = "jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC";
public static final String USERNAME = "root";
public static final String PASSWORD = "password";
public static final String DRIVER = "com.mysql.cj.jdbc.Driver";
}

3. 创建示例数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入测试数据
INSERT INTO users (username, email, age) VALUES
('张三', 'zhangsan@example.com', 25),
('李四', 'lisi@example.com', 30),
('王五', 'wangwu@example.com', 28);

JDBC核心操作示例

1. 基础连接和查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// JDBCBasicExample.java
import java.sql.*;

public class JDBCBasicExample {

public static void main(String[] args) {
// 加载驱动
try {
Class.forName(DatabaseConfig.DRIVER);
} catch (ClassNotFoundException e) {
System.out.println("驱动加载失败: " + e.getMessage());
return;
}

// 建立连接并执行查询
try (Connection conn = DriverManager.getConnection(
DatabaseConfig.URL,
DatabaseConfig.USERNAME,
DatabaseConfig.PASSWORD)) {

System.out.println("数据库连接成功!");

// 执行查询
String sql = "SELECT * FROM users";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

// 处理结果集
System.out.println("用户列表:");
while (rs.next()) {
int id = rs.getInt("id");
String username = rs.getString("username");
String email = rs.getString("email");
int age = rs.getInt("age");
Timestamp createdAt = rs.getTimestamp("created_at");

System.out.printf("ID: %d, 用户名: %s, 邮箱: %s, 年龄: %d, 创建时间: %s%n",
id, username, email, age, createdAt);
}

} catch (SQLException e) {
System.out.println("数据库操作异常: " + e.getMessage());
}
}
}

2. 用户实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// User.java
import java.sql.Timestamp;

public class User {
private int id;
private String username;
private String email;
private int age;
private Timestamp createdAt;

// 构造函数
public User() {}

public User(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}

// Getter和Setter方法
public int getId() { return id; }
public void setId(int id) { this.id = id; }

public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public int getAge() { return age; }
public void setAge(int age) { this.age = age; }

public Timestamp getCreatedAt() { return createdAt; }
public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; }

@Override
public String toString() {
return String.format("User{id=%d, username='%s', email='%s', age=%d, createdAt=%s}",
id, username, email, age, createdAt);
}
}

3. 完整的CRUD操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// UserDAO.java - 数据访问对象
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class UserDAO {

/**
* 创建用户
*/
public boolean createUser(User user) {
String sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";

try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {

pstmt.setString(1, user.getUsername());
pstmt.setString(2, user.getEmail());
pstmt.setInt(3, user.getAge());

int affectedRows = pstmt.executeUpdate();

if (affectedRows > 0) {
// 获取生成的主键
ResultSet generatedKeys = pstmt.getGeneratedKeys();
if (generatedKeys.next()) {
user.setId(generatedKeys.getInt(1));
}
return true;
}

} catch (SQLException e) {
System.out.println("创建用户失败: " + e.getMessage());
}
return false;
}

/**
* 根据ID查询用户
*/
public User getUserById(int id) {
String sql = "SELECT * FROM users WHERE id = ?";

try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {

pstmt.setInt(1, id);
ResultSet rs = pstmt.executeQuery();

if (rs.next()) {
return mapResultSetToUser(rs);
}

} catch (SQLException e) {
System.out.println("查询用户失败: " + e.getMessage());
}
return null;
}

/**
* 查询所有用户
*/
public List<User> getAllUsers() {
List<User> users = new ArrayList<>();
String sql = "SELECT * FROM users ORDER BY created_at DESC";

try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {

while (rs.next()) {
users.add(mapResultSetToUser(rs));
}

} catch (SQLException e) {
System.out.println("查询用户列表失败: " + e.getMessage());
}
return users;
}

/**
* 更新用户信息
*/
public boolean updateUser(User user) {
String sql = "UPDATE users SET username = ?, email = ?, age = ? WHERE id = ?";

try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {

pstmt.setString(1, user.getUsername());
pstmt.setString(2, user.getEmail());
pstmt.setInt(3, user.getAge());
pstmt.setInt(4, user.getId());

return pstmt.executeUpdate() > 0;

} catch (SQLException e) {
System.out.println("更新用户失败: " + e.getMessage());
}
return false;
}

/**
* 删除用户
*/
public boolean deleteUser(int id) {
String sql = "DELETE FROM users WHERE id = ?";

try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {

pstmt.setInt(1, id);
return pstmt.executeUpdate() > 0;

} catch (SQLException e) {
System.out.println("删除用户失败: " + e.getMessage());
}
return false;
}

/**
* 获取数据库连接
*/
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(
DatabaseConfig.URL,
DatabaseConfig.USERNAME,
DatabaseConfig.PASSWORD
);
}

/**
* 将ResultSet映射为User对象
*/
private User mapResultSetToUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
user.setCreatedAt(rs.getTimestamp("created_at"));
return user;
}
}

4. JDBC操作测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// JDBCTest.java
public class JDBCTest {
public static void main(String[] args) {
UserDAO userDAO = new UserDAO();

// 1. 创建用户
System.out.println("=== 创建用户 ===");
User newUser = new User("赵六", "zhaoliu@example.com", 26);
if (userDAO.createUser(newUser)) {
System.out.println("用户创建成功,ID: " + newUser.getId());
}

// 2. 查询单个用户
System.out.println("\n=== 查询用户 ===");
User user = userDAO.getUserById(newUser.getId());
if (user != null) {
System.out.println("查询到用户: " + user);
}

// 3. 查询所有用户
System.out.println("\n=== 所有用户列表 ===");
List<User> users = userDAO.getAllUsers();
users.forEach(System.out::println);

// 4. 更新用户
System.out.println("\n=== 更新用户 ===");
user.setAge(27);
user.setEmail("zhaoliu_new@example.com");
if (userDAO.updateUser(user)) {
System.out.println("用户信息更新成功");
System.out.println("更新后: " + userDAO.getUserById(user.getId()));
}

// 5. 删除用户
System.out.println("\n=== 删除用户 ===");
if (userDAO.deleteUser(user.getId())) {
System.out.println("用户删除成功");
}

// 再次查询验证删除
System.out.println("删除后查询结果: " + userDAO.getUserById(user.getId()));
}
}

JDBC最佳实践

1. 连接池配置(使用HikariCP)

1
2
3
4
5
6
<!-- 添加连接池依赖 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ConnectionPool.java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;

public class ConnectionPool {
private static HikariDataSource dataSource;

static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(DatabaseConfig.URL);
config.setUsername(DatabaseConfig.USERNAME);
config.setPassword(DatabaseConfig.PASSWORD);
config.setDriverClassName(DatabaseConfig.DRIVER);

// 连接池配置
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);

dataSource = new HikariDataSource(config);
}

public static DataSource getDataSource() {
return dataSource;
}

public static void close() {
if (dataSource != null) {
dataSource.close();
}
}
}

2. 事务管理示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// TransactionExample.java
import java.sql.Connection;
import java.sql.SQLException;

public class TransactionExample {

/**
* 转账操作 - 事务示例
*/
public boolean transferMoney(int fromUserId, int toUserId, double amount) {
Connection conn = null;
try {
conn = ConnectionPool.getDataSource().getConnection();
conn.setAutoCommit(false); // 开启事务

// 1. 检查余额
if (!checkBalance(conn, fromUserId, amount)) {
throw new SQLException("余额不足");
}

// 2. 扣除转出账户金额
updateBalance(conn, fromUserId, -amount);

// 3. 增加转入账户金额
updateBalance(conn, toUserId, amount);

conn.commit(); // 提交事务
System.out.println("转账成功");
return true;

} catch (SQLException e) {
try {
if (conn != null) {
conn.rollback(); // 回滚事务
System.out.println("转账失败,已回滚: " + e.getMessage());
}
} catch (SQLException ex) {
System.out.println("回滚失败: " + ex.getMessage());
}
return false;
} finally {
try {
if (conn != null) {
conn.setAutoCommit(true); // 恢复自动提交
conn.close();
}
} catch (SQLException e) {
System.out.println("关闭连接失败: " + e.getMessage());
}
}
}

private boolean checkBalance(Connection conn, int userId, double amount) throws SQLException {
// 检查余额的实现
return true; // 简化示例
}

private void updateBalance(Connection conn, int userId, double amount) throws SQLException {
// 更新余额的实现
}
}

JDBC优缺点分析

接下来看一下使用JDBC的优缺点,因此,我们才会明白为什么会有MyBatis出现。

✅ 优点

  1. 性能最优: 直接操作数据库,无额外抽象层开销
  2. 完全控制: 可以精确控制SQL语句和执行过程
  3. 标准API: Java标准库支持,无需额外依赖
  4. 灵活性高: 支持复杂查询和存储过程调用
  5. 轻量级: 占用内存少,启动快

❌ 缺点

  1. 代码繁琐: 需要手写大量样板代码
  2. 容易出错: 手动处理连接、异常和资源释放
  3. 维护困难: SQL散落在Java代码中,难以统一管理
  4. 开发效率低: 简单CRUD操作也需要大量代码
  5. 类型安全性差: 编译时无法检查SQL语法错误

JDBC适用场景

  • 性能要求极高的应用(如高频交易系统)
  • 复杂的数据库操作(存储过程、函数调用)
  • 底层框架开发(ORM框架的底层实现)
  • 数据库迁移工具开发
  • 小型应用学习阶段的项目

第二部分:MyBatis - 优雅的持久层框架

yBatis架构原理

通过上面的学习,看一看到想使用JDBC来写项目,需要写大量的代码,而且这些代码和业务是没有关联的。

为了让开发更专注业务,而不是这些重复的工作,因此,可以使用MyBatis

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────┐
│ Java应用程序 │
├─────────────────────┤
│ MyBatis API │
├─────────────────────┤
│ SQL映射文件 │
│ (Mapper.xml) │
├─────────────────────┤
│ MyBatis核心 │
│ ├─ SqlSession │
│ ├─ SqlSessionFactory│
│ └─ Configuration │
├─────────────────────┤
│ JDBC Driver │
├─────────────────────┤
│ MySQL数据库 │
└─────────────────────┘

核心组件说明:

下面的核心组件共同构成了MyBatis框架。帮助我们简单快速的使用JDBC连接数据库。

  • SqlSessionFactory: 会话工厂,用于创建SqlSession
  • SqlSession: 会话对象,包含执行SQL的所有方法
  • Mapper: 映射器,定义数据访问接口
  • Configuration: 配置对象,包含MyBatis的所有配置信息

MyBatis快速入门

1. 添加MyBatis依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- pom.xml -->
<dependencies>
<!-- MyBatis核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>

<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>

<!-- 日志依赖(可选) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>

2. MyBatis核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!-- mybatis-config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 设置 -->
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 设置超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>

<!-- 类型别名 -->
<typeAliases>
<typeAlias type="com.example.model.User" alias="User"/>
<typeAlias type="com.example.model.Order" alias="Order"/>
</typeAliases>

<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo_db?useSSL=false&amp;serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
<!-- 连接池配置 -->
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolMaximumIdleConnections" value="5"/>
<property name="poolMaximumCheckoutTime" value="20000"/>
<property name="poolTimeToWait" value="20000"/>
</dataSource>
</environment>
</environments>

<!-- 映射器 -->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
<mapper resource="mappers/OrderMapper.xml"/>
</mappers>
</configuration>

3. 创建扩展的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// User.java (增强版)
import java.sql.Timestamp;
import java.util.List;

public class User {
private Integer id;
private String username;
private String email;
private Integer age;
private Timestamp createdAt;
private List<Order> orders; // 一对多关系

// 构造函数
public User() {}

public User(String username, String email, Integer age) {
this.username = username;
this.email = email;
this.age = age;
}

// 所有Getter和Setter方法
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }

public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }

public Timestamp getCreatedAt() { return createdAt; }
public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; }

public List<Order> getOrders() { return orders; }
public void setOrders(List<Order> orders) { this.orders = orders; }

@Override
public String toString() {
return String.format("User{id=%d, username='%s', email='%s', age=%d, createdAt=%s, orders=%d}",
id, username, email, age, createdAt, orders != null ? orders.size() : 0);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Order.java - 订单实体类
import java.math.BigDecimal;
import java.sql.Timestamp;

public class Order {
private Integer id;
private Integer userId;
private String orderNo;
private BigDecimal amount;
private String status;
private Timestamp createdAt;
private User user; // 多对一关系

// 构造函数
public Order() {}

public Order(Integer userId, String orderNo, BigDecimal amount, String status) {
this.userId = userId;
this.orderNo = orderNo;
this.amount = amount;
this.status = status;
}

// 所有Getter和Setter方法
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }

public Integer getUserId() { return userId; }
public void setUserId(Integer userId) { this.userId = userId; }

public String getOrderNo() { return orderNo; }
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }

public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }

public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }

public Timestamp getCreatedAt() { return createdAt; }
public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; }

public User getUser() { return user; }
public void setUser(User user) { this.user = user; }

@Override
public String toString() {
return String.format("Order{id=%d, userId=%d, orderNo='%s', amount=%s, status='%s', createdAt=%s}",
id, userId, orderNo, amount, status, createdAt);
}
}

4. 数据表扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 创建订单表
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
order_no VARCHAR(50) UNIQUE NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 插入测试订单数据
INSERT INTO orders (user_id, order_no, amount, status) VALUES
(1, 'ORD001', 299.99, 'COMPLETED'),
(1, 'ORD002', 159.50, 'PENDING'),
(2, 'ORD003', 89.99, 'COMPLETED'),
(3, 'ORD004', 199.99, 'CANCELLED');

MyBatis核心操作示例

1. Mapper接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// UserMapper.java
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.*;

public interface UserMapper {

// 基础CRUD操作
int insertUser(User user);
User selectUserById(Integer id);
List<User> selectAllUsers();
int updateUser(User user);
int deleteUser(Integer id);

// 条件查询
List<User> selectUsersByAge(@Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge);
List<User> selectUsersByCondition(Map<String, Object> params);

// 关联查询
User selectUserWithOrders(Integer id);
List<User> selectUsersWithOrders();

// 分页查询
List<User> selectUsersByPage(@Param("offset") Integer offset, @Param("limit") Integer limit);

// 统计查询
int countUsers();
int countUsersByStatus(String status);

// 批量操作
int batchInsertUsers(@Param("users") List<User> users);
int batchUpdateUsers(@Param("users") List<User> users);
}

2. MyBatis XML映射文件

可以直接在XML文件中实现我们需要写的SQL语句就可以了,不需要在进行连接等等一大堆代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<!-- mappers/UserMapper.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="com.example.mapper.UserMapper">

<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<result column="created_at" property="createdAt"/>
</resultMap>

<!-- 用户与订单关联映射 -->
<resultMap id="UserWithOrdersMap" type="User" extends="BaseResultMap">
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
<result column="status" property="status"/>
<result column="order_created_at" property="createdAt"/>
</collection>
</resultMap>

<!-- SQL片段 -->
<sql id="Base_Column_List">
id, username, email, age, created_at
</sql>

<!-- 插入用户 -->
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, email, age)
VALUES (#{username}, #{email}, #{age})
</insert>

<!-- 根据ID查询用户 -->
<select id="selectUserById" parameterType="Integer" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
WHERE id = #{id}
</select>

<!-- 查询所有用户 -->
<select id="selectAllUsers" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
ORDER BY created_at DESC
</select>

<!-- 更新用户 -->
<update id="updateUser" parameterType="User">
UPDATE users
SET username = #{username},
email = #{email},
age = #{age}
WHERE id = #{id}
</update>

<!-- 删除用户 -->
<delete id="deleteUser" parameterType="Integer">
DELETE FROM users WHERE id = #{id}
</delete>

<!-- 按年龄范围查询 -->
<select id="selectUsersByAge" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
WHERE age BETWEEN #{minAge} AND #{maxAge}
ORDER BY age
</select>

<!-- 动态条件查询 -->
<select id="selectUsersByCondition" parameterType="Map" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email LIKE CONCAT('%', #{email}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
</where>
ORDER BY created_at DESC
</select>

<!-- 查询用户及其订单 -->
<select id="selectUserWithOrders" parameterType="Integer" resultMap="UserWithOrdersMap">
SELECT
u.id, u.username, u.email, u.age, u.created_at,
o.id as order_id, o.order_no, o.amount, o.status, o.created_at as order_created_at
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
ORDER BY o.created_at DESC
</select>

<!-- 查询所有用户及其订单 -->
<select id="selectUsersWithOrders" resultMap="UserWithOrdersMap">
SELECT
u.id, u.username, u.email, u.age, u.created_at,
o.id as order_id, o.order_no, o.amount, o.status, o.created_at as order_created_at
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.id, o.created_at DESC
</select>

<!-- 分页查询 -->
<select id="selectUsersByPage" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
ORDER BY created_at DESC
LIMIT #{offset}, #{limit}
</select>

<!-- 统计用户数量 -->
<select id="countUsers" resultType="Integer">
SELECT COUNT(*) FROM users
</select>

<!-- 批量插入用户 -->
<insert id="batchInsertUsers" parameterType="List" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, email, age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.email}, #{user.age})
</foreach>
</insert>

<!-- 批量更新用户 -->
<update id="batchUpdateUsers" parameterType="List">
<foreach collection="users" item="user" separator=";">
UPDATE users
SET username = #{user.username}, email = #{user.email}, age = #{user.age}
WHERE id = #{user.id}
</foreach>
</update>

</mapper>

3. MyBatis工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// MyBatisUtil.java
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

public class MyBatisUtil {
private static SqlSessionFactory sqlSessionFactory;

static {
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
throw new RuntimeException("初始化MyBatis失败", e);
}
}

/**
* 获取SqlSession
*/
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}

/**
* 获取SqlSession(自动提交)
*/
public static SqlSession getSqlSession(boolean autoCommit) {
return sqlSessionFactory.openSession(autoCommit);
}

/**
* 获取Mapper
*/
public static <T> T getMapper(Class<T> mapperClass) {
SqlSession session = getSqlSession();
return session.getMapper(mapperClass);
}

/**
* 关闭SqlSession
*/
public static void closeSqlSession(SqlSession session) {
if (session != null) {
session.close();
}
}
}

4. MyBatis服务层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// UserService.java
import org.apache.ibatis.session.SqlSession;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class UserService {

/**
* 创建用户
*/
public boolean createUser(User user) {
SqlSession session = MyBatisUtil.getSqlSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
int result = mapper.insertUser(user);
session.commit();
return result > 0;
} catch (Exception e) {
session.rollback();
System.out.println("创建用户失败: " + e.getMessage());
return false;
} finally {
MyBatisUtil.closeSqlSession(session);
}
}

/**
* 根据ID查询用户
*/
public User getUserById(Integer id) {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectUserById(id);
} catch (Exception e) {
System.out.println("查询用户失败: " + e.getMessage());
return null;
}
}

/**
* 查询所有用户
*/
public List<User> getAllUsers() {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectAllUsers();
} catch (Exception e) {
System.out.println("查询用户列表失败: " + e.getMessage());
return null;
}
}

/**
* 更新用户
*/
public boolean updateUser(User user) {
SqlSession session = MyBatisUtil.getSqlSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
int result = mapper.updateUser(user);
session.commit();
return result > 0;
} catch (Exception e) {
session.rollback();
System.out.println("更新用户失败: " + e.getMessage());
return false;
} finally {
MyBatisUtil.closeSqlSession(session);
}
}

/**
* 删除用户
*/
public boolean deleteUser(Integer id) {
SqlSession session = MyBatisUtil.getSqlSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
int result = mapper.deleteUser(id);
session.commit();
return result > 0;
} catch (Exception e) {
session.rollback();
System.out.println("删除用户失败: " + e.getMessage());
return false;
} finally {
MyBatisUtil.closeSqlSession(session);
}
}

/**
* 按年龄范围查询用户
*/
public List<User> getUsersByAgeRange(Integer minAge, Integer maxAge) {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectUsersByAge(minAge, maxAge);
} catch (Exception e) {
System.out.println("按年龄查询用户失败: " + e.getMessage());
return null;
}
}

/**
* 动态条件查询
*/
public List<User> getUsersByCondition(String username, String email, Integer minAge, Integer maxAge) {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);

Map<String, Object> params = new HashMap<>();
params.put("username", username);
params.put("email", email);
params.put("minAge", minAge);
params.put("maxAge", maxAge);

return mapper.selectUsersByCondition(params);
} catch (Exception e) {
System.out.println("条件查询用户失败: " + e.getMessage());
return null;
}
}

/**
* 查询用户及其订单
*/
public User getUserWithOrders(Integer id) {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectUserWithOrders(id);
} catch (Exception e) {
System.out.println("查询用户订单失败: " + e.getMessage());
return null;
}
}

/**
* 分页查询用户
*/
public List<User> getUsersByPage(int page, int size) {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
int offset = (page - 1) * size;
return mapper.selectUsersByPage(offset, size);
} catch (Exception e) {
System.out.println("分页查询用户失败: " + e.getMessage());
return null;
}
}

/**
* 批量创建用户
*/
public boolean batchCreateUsers(List<User> users) {
SqlSession session = MyBatisUtil.getSqlSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
int result = mapper.batchInsertUsers(users);
session.commit();
return result > 0;
} catch (Exception e) {
session.rollback();
System.out.println("批量创建用户失败: " + e.getMessage());
return false;
} finally {
MyBatisUtil.closeSqlSession(session);
}
}
}

5. MyBatis测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// MyBatisTest.java
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;

public class MyBatisTest {
public static void main(String[] args) {
UserService userService = new UserService();

// 1. 创建用户测试
System.out.println("=== 创建用户测试 ===");
User newUser = new User("陈七", "chenqi@example.com", 24);
if (userService.createUser(newUser)) {
System.out.println("用户创建成功,ID: " + newUser.getId());
}

// 2. 查询用户测试
System.out.println("\n=== 查询用户测试 ===");
User user = userService.getUserById(newUser.getId());
if (user != null) {
System.out.println("查询到用户: " + user);
}

// 3. 查询所有用户
System.out.println("\n=== 所有用户列表 ===");
List<User> users = userService.getAllUsers();
if (users != null) {
users.forEach(System.out::println);
}

// 4. 按年龄范围查询
System.out.println("\n=== 年龄范围查询 (25-30岁) ===");
List<User> usersByAge = userService.getUsersByAgeRange(25, 30);
if (usersByAge != null) {
usersByAge.forEach(System.out::println);
}

// 5. 动态条件查询
System.out.println("\n=== 动态条件查询 ===");
List<User> usersByCondition = userService.getUsersByCondition("张", null, 20, 35);
if (usersByCondition != null) {
usersByCondition.forEach(System.out::println);
}

// 6. 查询用户及订单
System.out.println("\n=== 查询用户及订单 ===");
User userWithOrders = userService.getUserWithOrders(1);
if (userWithOrders != null) {
System.out.println("用户信息: " + userWithOrders);
if (userWithOrders.getOrders() != null) {
System.out.println("订单列表:");
userWithOrders.getOrders().forEach(order ->
System.out.println(" " + order));
}
}

// 7. 分页查询
System.out.println("\n=== 分页查询 (第1页,每页2条) ===");
List<User> pageUsers = userService.getUsersByPage(1, 2);
if (pageUsers != null) {
pageUsers.forEach(System.out::println);
}

// 8. 批量创建用户
System.out.println("\n=== 批量创建用户 ===");
List<User> batchUsers = Arrays.asList(
new User("批量用户1", "batch1@example.com", 22),
new User("批量用户2", "batch2@example.com", 23),
new User("批量用户3", "batch3@example.com", 24)
);
if (userService.batchCreateUsers(batchUsers)) {
System.out.println("批量创建用户成功");
batchUsers.forEach(u -> System.out.println("新用户ID: " + u.getId()));
}

// 9. 更新用户
System.out.println("\n=== 更新用户 ===");
user.setAge(25);
user.setEmail("chenqi_updated@example.com");
if (userService.updateUser(user)) {
System.out.println("用户更新成功");
System.out.println("更新后: " + userService.getUserById(user.getId()));
}

// 10. 删除用户
System.out.println("\n=== 删除用户 ===");
if (userService.deleteUser(user.getId())) {
System.out.println("用户删除成功");
}
}
}

MyBatis高级特性

1. 注解方式映射

通过@Select@Insert等注解可以直接实现SQL语句,而不需要在XML文件中写SQL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// UserAnnotationMapper.java - 注解方式
import org.apache.ibatis.annotations.*;
import java.util.List;

public interface UserAnnotationMapper {

@Select("SELECT * FROM users WHERE id = #{id}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "email", column = "email"),
@Result(property = "age", column = "age"),
@Result(property = "createdAt", column = "created_at")
})
User selectUserById(Integer id);

@Insert("INSERT INTO users(username, email, age) VALUES(#{username}, #{email}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);

@Update("UPDATE users SET username=#{username}, email=#{email}, age=#{age} WHERE id=#{id}")
int updateUser(User user);

@Delete("DELETE FROM users WHERE id = #{id}")
int deleteUser(Integer id);

// 动态SQL注解
@SelectProvider(type = UserSqlProvider.class, method = "selectUsersByCondition")
List<User> selectUsersByCondition(Map<String, Object> params);
}

2. 动态SQL提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// UserSqlProvider.java
import org.apache.ibatis.jdbc.SQL;
import java.util.Map;

public class UserSqlProvider {

public String selectUsersByCondition(Map<String, Object> params) {
return new SQL() {{
SELECT("*");
FROM("users");

if (params.get("username") != null) {
WHERE("username LIKE CONCAT('%', #{username}, '%')");
}
if (params.get("email") != null) {
WHERE("email LIKE CONCAT('%', #{email}, '%')");
}
if (params.get("minAge") != null) {
WHERE("age >= #{minAge}");
}
if (params.get("maxAge") != null) {
WHERE("age <= #{maxAge}");
}

ORDER_BY("created_at DESC");
}}.toString();
}
}

3. 二级缓存配置

1
2
3
4
5
6
7
8
9
10
11
<!-- 在UserMapper.xml中启用二级缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>

<!-- 或使用自定义缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache">
<property name="cacheFile" value="/tmp/user-cache.tmp"/>
</cache>

4. 插件开发示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// MyBatisInterceptor.java - 性能监控插件
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PerformanceInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();

Object result = invocation.proceed();

long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;

MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();

System.out.printf("SQL执行耗时: %s - %d ms%n", sqlId, executionTime);

return result;
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// 设置插件属性
}
}

MyBatis优缺点分析

✅ 优点

  1. 灵活的SQL控制: 完全控制SQL语句,支持复杂查询
  2. 学习成本适中: 相比JPA等框架更容易上手
  3. 性能优秀: 接近原生JDBC的性能
  4. 强大的映射功能: 支持复杂的对象关系映射
  5. 动态SQL: 根据条件动态生成SQL
  6. 插件机制: 支持拦截器和插件扩展
  7. 缓存机制: 内置一级、二级缓存

❌ 缺点

  1. 配置复杂: 需要编写大量XML配置文件
  2. SQL与Java代码分离: 维护时需要在多个文件间切换
  3. 数据库依赖: 不同数据库的SQL可能需要调整
  4. 调试困难: XML中的SQL错误不易发现
  5. 代码生成依赖: 复杂项目需要代码生成工具

MyBatis适用场景

  • 复杂查询需求的应用(报表系统、数据分析)
  • 对SQL性能要求高的项目
  • 需要精确控制SQL的业务场景
  • 团队SQL能力较强的开发团队
  • 数据库表结构复杂的遗留系统集成
  • 需要与存储过程交互的企业应用

第三部分:MyBatis Plus - 极速开发利器

MyBatis Plus架构原理

MyBatis Plus(简称MP)是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。它提供了强大的CRUD操作、条件构造器、代码生成器等功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────┐
│ Spring Boot应用 │
├─────────────────────────┤
│ MyBatis Plus │
│ ├─ BaseMapper │
│ ├─ IService │
│ ├─ QueryWrapper │
│ └─ CodeGenerator │
├─────────────────────────┤
│ MyBatis核心 │
│ ├─ SqlSession │
│ ├─ SqlSessionFactory │
│ └─ Configuration │
├─────────────────────────┤
│ JDBC Driver │
├─────────────────────────┤
│ MySQL数据库 │
└─────────────────────────┘

核心组件说明:

  • BaseMapper: 通用Mapper接口,提供基础CRUD方法
  • IService: 通用Service接口,提供更多便捷方法
  • QueryWrapper: 条件构造器,用于动态SQL构建
  • CodeGenerator: 代码生成器,自动生成实体、Mapper等
  • 分页插件: 物理分页支持
  • 乐观锁插件: 防止并发修改

MyBatis Plus快速入门

1. 添加依赖(Spring Boot项目)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.14</version>
</dependency>

<!-- MyBatis Plus Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>

<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>

<!-- MyBatis Plus代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.2</version>
</dependency>

<!-- 代码生成器模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>

<!-- Lombok(可选,简化实体类) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
</dependencies>

2. 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: password

# MyBatis Plus配置
mybatis-plus:
configuration:
# 开启驼峰命名自动映射
map-underscore-to-camel-case: true
# SQL日志打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 主键策略
id-type: AUTO
# 逻辑删除字段
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 扫描Mapper XML文件
mapper-locations: classpath*:/mapper/**/*.xml
# 实体类别名包扫描
type-aliases-package: com.example.entity

# 分页配置
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true

3. 实体类设计(使用注解)

通过注解可以方便的将实体的字段映射为数据库的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// User.java (MyBatis Plus版本)
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("users")
public class User implements Serializable {

private static final long serialVersionUID = 1L;

@TableId(value = "id", type = IdType.AUTO)
private Integer id;

@TableField("username")
private String username;

@TableField("email")
private String email;

@TableField("age")
private Integer age;

@TableField(value = "created_at", fill = FieldFill.INSERT)
private LocalDateTime createdAt;

@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;

@TableLogic
@TableField("deleted")
private Integer deleted;

// 非数据库字段
@TableField(exist = false)
private String tempField;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Order.java (MyBatis Plus版本)
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("orders")
public class Order implements Serializable {

private static final long serialVersionUID = 1L;

@TableId(value = "id", type = IdType.AUTO)
private Integer id;

@TableField("user_id")
private Integer userId;

@TableField("order_no")
private String orderNo;

@TableField("amount")
private BigDecimal amount;

@TableField("status")
private String status;

@TableField(value = "created_at", fill = FieldFill.INSERT)
private LocalDateTime createdAt;

@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;

@Version
@TableField("version")
private Integer version;

@TableLogic
@TableField("deleted")
private Integer deleted;
}

4. 自动填充处理器

对于一些数据库的时间字段,可以使用自动填充。自动设置,不需要每一次插入的时候都写重复代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MyMetaObjectHandler.java
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}

@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
}

MyBatis Plus核心操作示例

1. Mapper接口(继承BaseMapper)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// UserMapper.java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;
import java.util.Map;

@Mapper
public interface UserMapper extends BaseMapper<User> {

// 继承BaseMapper后自动拥有基础CRUD方法
// 可以添加自定义方法

/**
* 自定义分页查询
*/
IPage<User> selectUserPage(Page<User> page, @Param("ew") Wrapper<User> wrapper);

/**
* 统计各年龄段用户数量
*/
@Select("SELECT age, COUNT(*) as count FROM users WHERE deleted = 0 GROUP BY age")
List<Map<String, Object>> selectAgeStatistics();

/**
* 查询用户及其订单总金额
*/
@Select("SELECT u.*, IFNULL(SUM(o.amount), 0) as total_amount " +
"FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.deleted = 0 " +
"WHERE u.deleted = 0 GROUP BY u.id")
List<Map<String, Object>> selectUsersWithTotalAmount();
}

2. Service接口和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// UserService.java
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import java.util.List;
import java.util.Map;

public interface UserService extends IService<User> {

// 继承IService后自动拥有丰富的CRUD方法

/**
* 分页查询用户
*/
IPage<User> getUserPage(Page<User> page, String username, Integer minAge, Integer maxAge);

/**
* 根据年龄范围查询用户
*/
List<User> getUsersByAgeRange(Integer minAge, Integer maxAge);

/**
* 批量更新用户状态
*/
boolean batchUpdateUserStatus(List<Integer> userIds, String status);

/**
* 获取年龄统计
*/
List<Map<String, Object>> getAgeStatistics();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// UserServiceImpl.java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Map;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

@Override
public IPage<User> getUserPage(Page<User> page, String username, Integer minAge, Integer maxAge) {
QueryWrapper<User> wrapper = new QueryWrapper<>();

// 动态条件构建
if (StringUtils.hasText(username)) {
wrapper.like("username", username);
}
if (minAge != null) {
wrapper.ge("age", minAge);
}
if (maxAge != null) {
wrapper.le("age", maxAge);
}

wrapper.orderByDesc("created_at");

return this.page(page, wrapper);
}

@Override
public List<User> getUsersByAgeRange(Integer minAge, Integer maxAge) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age", minAge, maxAge)
.orderByAsc("age");

return this.list(wrapper);
}

@Override
public boolean batchUpdateUserStatus(List<Integer> userIds, String status) {
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.in("id", userIds)
.set("status", status);

return this.update(wrapper);
}

@Override
public List<Map<String, Object>> getAgeStatistics() {
return baseMapper.selectAgeStatistics();
}
}

3. 配置类(分页插件等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// MyBatisPlusConfig.java
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {

/**
* MyBatis Plus插件配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

return interceptor;
}
}

4. 控制器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// UserController.java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

/**
* 创建用户
*/
@PostMapping
public Result<User> createUser(@RequestBody User user) {
boolean success = userService.save(user);
return success ? Result.success(user) : Result.error("创建失败");
}

/**
* 根据ID获取用户
*/
@GetMapping("/{id}")
public Result<User> getUserById(@PathVariable Integer id) {
User user = userService.getById(id);
return user != null ? Result.success(user) : Result.error("用户不存在");
}

/**
* 分页查询用户
*/
@GetMapping("/page")
public Result<IPage<User>> getUserPage(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge) {

Page<User> page = new Page<>(current, size);
IPage<User> result = userService.getUserPage(page, username, minAge, maxAge);
return Result.success(result);
}

/**
* 更新用户
*/
@PutMapping("/{id}")
public Result<String> updateUser(@PathVariable Integer id, @RequestBody User user) {
user.setId(id);
boolean success = userService.updateById(user);
return success ? Result.success("更新成功") : Result.error("更新失败");
}

/**
* 删除用户(逻辑删除)
*/
@DeleteMapping("/{id}")
public Result<String> deleteUser(@PathVariable Integer id) {
boolean success = userService.removeById(id);
return success ? Result.success("删除成功") : Result.error("删除失败");
}

/**
* 批量删除用户
*/
@DeleteMapping("/batch")
public Result<String> batchDeleteUsers(@RequestBody List<Integer> ids) {
boolean success = userService.removeByIds(ids);
return success ? Result.success("批量删除成功") : Result.error("批量删除失败");
}

/**
* 条件查询用户
*/
@PostMapping("/search")
public Result<List<User>> searchUsers(@RequestBody UserSearchDto searchDto) {
QueryWrapper<User> wrapper = new QueryWrapper<>();

if (searchDto.getUsername() != null) {
wrapper.like("username", searchDto.getUsername());
}
if (searchDto.getEmail() != null) {
wrapper.like("email", searchDto.getEmail());
}
if (searchDto.getMinAge() != null && searchDto.getMaxAge() != null) {
wrapper.between("age", searchDto.getMinAge(), searchDto.getMaxAge());
}

List<User> users = userService.list(wrapper);
return Result.success(users);
}

/**
* 获取年龄统计
*/
@GetMapping("/statistics/age")
public Result<List<Map<String, Object>>> getAgeStatistics() {
List<Map<String, Object>> statistics = userService.getAgeStatistics();
return Result.success(statistics);
}
}

5. 完整测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// MyBatisPlusTest.java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SpringBootTest
public class MyBatisPlusTest {

@Autowired
private UserService userService;

@Test
public void testBasicCRUD() {
// 1. 创建用户
System.out.println("=== 创建用户 ===");
User user = new User()
.setUsername("MyBatis Plus用户")
.setEmail("mp@example.com")
.setAge(26);

boolean saveResult = userService.save(user);
System.out.println("保存结果: " + saveResult + ", 用户ID: " + user.getId());

// 2. 根据ID查询
System.out.println("\n=== 根据ID查询 ===");
User queryUser = userService.getById(user.getId());
System.out.println("查询结果: " + queryUser);

// 3. 更新用户
System.out.println("\n=== 更新用户 ===");
user.setAge(27).setEmail("mp_updated@example.com");
boolean updateResult = userService.updateById(user);
System.out.println("更新结果: " + updateResult);

// 4. 条件查询
System.out.println("\n=== 条件查询 ===");
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("username", "MyBatis")
.ge("age", 20)
.orderByAsc("age");

List<User> users = userService.list(wrapper);
System.out.println("条件查询结果: " + users.size() + " 条");
users.forEach(System.out::println);

// 5. 分页查询
System.out.println("\n=== 分页查询 ===");
Page<User> page = new Page<>(1, 2);
IPage<User> pageResult = userService.page(page);
System.out.println("总记录数: " + pageResult.getTotal());
System.out.println("总页数: " + pageResult.getPages());
System.out.println("当前页数据:");
pageResult.getRecords().forEach(System.out::println);
}

@Test
public void testAdvancedQuery() {
// 1. 复杂条件查询
System.out.println("=== 复杂条件查询 ===");
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "username", "email", "age") // 指定查询字段
.like("username", "张")
.or()
.between("age", 25, 30)
.orderByDesc("created_at")
.last("LIMIT 5"); // 添加原生SQL

List<User> users = userService.list(wrapper);
users.forEach(System.out::println);

// 2. 聚合查询
System.out.println("\n=== 聚合查询 ===");
QueryWrapper<User> countWrapper = new QueryWrapper<>();
countWrapper.ge("age", 25);
int count = userService.count(countWrapper);
System.out.println("年龄>=25的用户数: " + count);

// 3. 分组查询
System.out.println("\n=== 分组查询 ===");
QueryWrapper<User> groupWrapper = new QueryWrapper<>();
groupWrapper.select("age", "COUNT(*) as count")
.groupBy("age")
.having("COUNT(*) > 0")
.orderByAsc("age");

List<Map<String, Object>> groupResults = userService.listMaps(groupWrapper);
groupResults.forEach(System.out::println);
}

@Test
public void testBatchOperations() {
// 1. 批量插入
System.out.println("=== 批量插入 ===");
List<User> batchUsers = Arrays.asList(
new User().setUsername("批量用户1").setEmail("batch1@mp.com").setAge(21),
new User().setUsername("批量用户2").setEmail("batch2@mp.com").setAge(22),
new User().setUsername("批量用户3").setEmail("batch3@mp.com").setAge(23)
);

boolean batchSaveResult = userService.saveBatch(batchUsers);
System.out.println("批量插入结果: " + batchSaveResult);
batchUsers.forEach(u -> System.out.println("新用户ID: " + u.getId()));

// 2. 批量更新
System.out.println("\n=== 批量更新 ===");
batchUsers.forEach(u -> u.setAge(u.getAge() + 1));
boolean batchUpdateResult = userService.updateBatchById(batchUsers);
System.out.println("批量更新结果: " + batchUpdateResult);

// 3. 条件批量更新
System.out.println("\n=== 条件批量更新 ===");
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.like("username", "批量")
.set("email", "batch_updated@mp.com");

boolean conditionUpdateResult = userService.update(updateWrapper);
System.out.println("条件更新结果: " + conditionUpdateResult);

// 4. 批量删除
System.out.println("\n=== 批量删除 ===");
List<Integer> idsToDelete = Arrays.asList(
batchUsers.get(0).getId(),
batchUsers.get(1).getId()
);
boolean batchRemoveResult = userService.removeByIds(idsToDelete);
System.out.println("批量删除结果: " + batchRemoveResult);
}

@Test
public void testLambdaWrapper() {
// Lambda表达式构造器(类型安全)
System.out.println("=== Lambda条件构造器 ===");

List<User> users = userService.lambdaQuery()
.like(User::getUsername, "张")
.ge(User::getAge, 20)
.le(User::getAge, 30)
.orderByDesc(User::getCreatedAt)
.list();

System.out.println("Lambda查询结果: " + users.size() + " 条");
users.forEach(System.out::println);

// Lambda更新
System.out.println("\n=== Lambda更新 ===");
boolean updateResult = userService.lambdaUpdate()
.like(User::getUsername, "测试")
.set(User::getAge, 30)
.update();

System.out.println("Lambda更新结果: " + updateResult);
}
}

MyBatis Plus高级特性

1. 代码生成器

通过代码生成器可以快速的生成这个数据表的一些需要的类,比如Mapper,XML文件等。不需要我们再一个个手动创建了,大大提升了我们的开发速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// CodeGenerator.java
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

public class CodeGenerator {

public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();

// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("MyBatis Plus Generator");
gc.setOpen(false);
gc.setServiceName("%sService"); // 去掉Service接口的首字母I
gc.setIdType(IdType.AUTO);
gc.setDateType(DateType.ONLY_DATE);
gc.setSwagger2(true); // 启用Swagger注解
mpg.setGlobalConfig(gc);

// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("password");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);

// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("demo");
pc.setParent("com.example");
pc.setEntity("entity");
pc.setMapper("mapper");
pc.setService("service");
pc.setServiceImpl("service.impl");
pc.setController("controller");
mpg.setPackageInfo(pc);

// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("users", "orders"); // 指定要生成的表名
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true); // 使用Lombok
strategy.setRestControllerStyle(true); // 生成RestController
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("t_"); // 表前缀

// 逻辑删除
strategy.setLogicDeleteFieldName("deleted");

// 乐观锁
strategy.setVersionFieldName("version");

// 自动填充
strategy.setTableFillList(Arrays.asList(
new TableFill("created_at", FieldFill.INSERT),
new TableFill("updated_at", FieldFill.INSERT_UPDATE)
));

mpg.setStrategy(strategy);

// 执行生成
mpg.execute();
}
}

2. 条件构造器详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// WrapperExample.java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;

public class WrapperExample {

@Autowired
private UserService userService;

public void queryWrapperExamples() {
QueryWrapper<User> wrapper = new QueryWrapper<>();

// 1. 基本条件
wrapper.eq("username", "张三") // username = '张三'
.ne("age", 18) // age != 18
.gt("age", 20) // age > 20
.ge("age", 21) // age >= 21
.lt("age", 30) // age < 30
.le("age", 29); // age <= 29

// 2. 模糊查询
wrapper.like("username", "张") // username LIKE '%张%'
.notLike("email", "test") // email NOT LIKE '%test%'
.likeLeft("username", "三") // username LIKE '%三'
.likeRight("username", "张"); // username LIKE '张%'

// 3. 空值判断
wrapper.isNull("email") // email IS NULL
.isNotNull("phone"); // phone IS NOT NULL

// 4. 范围查询
wrapper.between("age", 20, 30) // age BETWEEN 20 AND 30
.notBetween("age", 40, 50) // age NOT BETWEEN 40 AND 50
.in("id", Arrays.asList(1, 2, 3)) // id IN (1, 2, 3)
.notIn("status", Arrays.asList("DELETED", "BANNED"));

// 5. 复杂条件组合
wrapper.nested(w -> w.eq("status", "ACTIVE").or().eq("status", "PENDING"))
.and(w -> w.gt("age", 18))
.or(w -> w.eq("role", "ADMIN"));

// 6. 排序
wrapper.orderByAsc("age") // ORDER BY age ASC
.orderByDesc("created_at"); // ORDER BY created_at DESC

// 7. 分组和聚合
wrapper.select("age", "COUNT(*) as count") // SELECT age, COUNT(*) as count
.groupBy("age") // GROUP BY age
.having("COUNT(*) > 1"); // HAVING COUNT(*) > 1

// 8. 限制查询字段
wrapper.select("id", "username", "email"); // 只查询指定字段

// 9. 原生SQL片段
wrapper.apply("date_format(created_at,'%Y-%m-%d') = '2023-01-01'")
.last("LIMIT 10"); // 在SQL最后添加

List<User> users = userService.list(wrapper);
}

public void updateWrapperExamples() {
UpdateWrapper<User> wrapper = new UpdateWrapper<>();

// 设置更新字段
wrapper.set("email", "new@example.com") // SET email = 'new@example.com'
.set("updated_at", LocalDateTime.now()) // SET updated_at = NOW()
.setSql("age = age + 1") // SET age = age + 1
.eq("id", 1); // WHERE id = 1

userService.update(wrapper);
}

public void lambdaWrapperExamples() {
// Lambda查询(类型安全,避免字段名写错)
List<User> users = userService.lambdaQuery()
.eq(User::getUsername, "张三")
.gt(User::getAge, 20)
.like(User::getEmail, "example.com")
.orderByDesc(User::getCreatedAt)
.list();

// Lambda更新
userService.lambdaUpdate()
.set(User::getEmail, "updated@example.com")
.eq(User::getId, 1)
.update();

// Lambda删除
userService.lambdaUpdate()
.eq(User::getStatus, "INACTIVE")
.remove();
}
}

3. 自定义SQL注入器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// CustomSqlInjector.java
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class CustomSqlInjector extends DefaultSqlInjector {

@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);

// 添加自定义方法
methodList.add(new DeleteAllMethod());
methodList.add(new FindByIdMethod());

return methodList;
}
}

4. 多数据源配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// DataSourceConfig.java
import com.baomidou.dynamic.datasource.DynamicDataSourceCreator;
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceConfig {

// 在Service方法上使用@DS注解切换数据源
@DS("master") // 主库
public void masterOperation() {
// 主库操作
}

@DS("slave") // 从库
public void slaveOperation() {
// 从库操作
}
}

三种技术对比总结

技术特性对比表

特性 JDBC MyBatis MyBatis Plus
学习难度 ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
开发效率 ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
性能表现 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
灵活性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
维护成本 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
社区生态 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
企业采用度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

详细对比分析

JDBC

优势:

  • 性能最优: 直接操作数据库,无中间层损耗
  • 完全控制: 精确控制每一个SQL语句
  • 无依赖: Java标准库原生支持
  • 灵活性强: 支持任何复杂的数据库操作

劣势:

  • 代码冗长: 大量样板代码
  • 易出错: 手动管理连接和异常
  • 重复工作: 基础CRUD需重复编写
  • 维护困难: SQL分散在Java代码中

适用场景:

  • 对性能要求极高的系统
  • 需要复杂数据库操作的应用
  • 底层框架开发
  • 小型项目或学习阶段
MyBatis

优势:

  • SQL控制: 完全控制SQL语句编写
  • 映射强大: 复杂结果集映射能力
  • 动态SQL: 灵活的条件查询构建
  • 插件丰富: 分页、缓存等插件支持

劣势:

  • 配置复杂: 需要编写XML映射文件
  • 维护成本: Java代码与XML文件分离
  • 学习曲线: 需要掌握XML配置和映射规则
  • 调试困难: XML中的SQL错误不易发现

适用场景:

  • 复杂查询和报表应用
  • 需要精确SQL控制的项目
  • 遗留系统改造
  • 团队SQL能力较强的项目
MyBatis Plus

优势:

  • 开发高效: 自动生成基础CRUD操作
  • 注解简洁: 减少XML配置文件
  • 功能丰富: 分页、条件构造器、代码生成
  • 开箱即用: 与Spring Boot完美集成

劣势:

  • 灵活性限制: 复杂查询仍需自定义SQL
  • 学习成本: 需要掌握特有的API和注解
  • 版本依赖: 升级可能带来兼容性问题
  • 过度设计: 简单项目可能过于复杂

适用场景:

  • 快速开发的中小型项目
  • Spring Boot项目
  • 标准CRUD操作较多的应用
  • 团队追求开发效率的项目

选择建议

根据项目规模选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
小型项目 (< 10张表)
├─ 学习阶段 → JDBC
├─ 快速开发 → MyBatis Plus
└─ 性能优先 → JDBC

中型项目 (10-50张表)
├─ 复杂查询多 → MyBatis
├─ 标准CRUD多 → MyBatis Plus
└─ 混合场景 → MyBatis + MyBatis Plus

大型项目 (> 50张表)
├─ 高性能要求 → JDBC + MyBatis
├─ 企业级应用 → MyBatis
└─ 微服务架构 → MyBatis Plus

根据团队技能选择

1
2
3
4
5
6
7
8
9
团队技能水平
├─ 初级团队 → MyBatis Plus (自动化程度高)
├─ 中级团队 → MyBatis (平衡灵活性与效率)
└─ 高级团队 → JDBC/MyBatis (完全控制)

SQL能力水平
├─ SQL能力强 → MyBatis (充分发挥SQL优势)
├─ SQL能力中等 → MyBatis Plus (减少SQL编写)
└─ SQL能力弱 → MyBatis Plus (代码生成)

最佳实践建议

1. 技术栈组合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 推荐的混合使用方式
@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper; // MyBatis Plus

// 简单CRUD使用MyBatis Plus
public boolean createOrder(Order order) {
return orderMapper.insert(order) > 0;
}

// 复杂查询使用自定义SQL
@Select("SELECT o.*, u.username FROM orders o " +
"LEFT JOIN users u ON o.user_id = u.id " +
"WHERE o.created_at BETWEEN #{startDate} AND #{endDate}")
List<OrderVO> getOrderReport(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);

// 性能要求极高的场景可以使用JDBC
public void batchInsertOrderDetails(List<OrderDetail> details) {
// 使用JDBC批量插入
jdbcTemplate.batchUpdate(sql, details);
}
}

2. 渐进式技术升级路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
第一阶段:JDBC基础
├─ 掌握数据库连接管理
├─ 理解SQL执行过程
└─ 学会异常处理和资源管理

第二阶段:MyBatis进阶
├─ 掌握XML映射配置
├─ 理解动态SQL构建
└─ 学会结果集映射

第三阶段:MyBatis Plus高效
├─ 掌握注解和条件构造器
├─ 学会代码生成和插件使用
└─ 理解高级特性和最佳实践

总结

通过本教程的学习,你已经全面掌握了MySQL在Java应用中的三种主要使用方式。从底层的JDBC到灵活的MyBatis,再到高效的MyBatis Plus,每种技术都有其独特的优势和适用场景。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

分布式MapReduce系统设计与实现

摘要

本文介绍了一个基于Go语言实现的分布式MapReduce系统,该系统借鉴Google MapReduce论文的核心思想,通过Master-Worker架构实现了大规模数据的并行处理。系统支持容错机制、任务调度、健康检查等关键特性,能够在节点故障情况下保证作业的正确完成。实验结果表明,该实现在处理大文件集合时表现出良好的可扩展性和容错性。


1. 引言

1.1 背景与动机

随着互联网数据量的爆炸式增长,传统的单机数据处理方式已无法满足大规模数据分析的需求。Google在2004年提出的MapReduce编程模型为分布式数据处理提供了简洁而强大的解决方案。MapReduce将复杂的分布式计算抽象为Map和Reduce两个操作,使程序员能够专注于业务逻辑而无需关心底层的分布式细节。

1.2 设计目标

本系统的设计目标如下:

  • 简单性:提供简洁的编程接口,隐藏分布式系统的复杂性
  • 容错性:能够自动处理节点故障,保证作业的最终完成
  • 可扩展性:支持动态添加/移除计算节点
  • 高性能:通过并行处理和负载均衡提升系统吞吐量

2. 系统架构

2.1 整体架构概览

系统采用经典的Master-Worker架构,如图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────┐
│ Master │ ← 单点协调者
│ - 任务调度 │
│ - 状态管理 │
│ - 故障检测 │
└─────────┬───────┘

┌─────┴─────┐
│ RPC │
└─────┬─────┘

┌─────────┼─────────┐
│ Worker Pool │
├─────────┼─────────┤
│ Worker1 │ Worker2 │ ... ← 分布式计算节点
│- Map任务│- Reduce │
│- 本地存储│- 容错处理 │
└─────────┴─────────┘

2.2 核心组件

2.2.1 Master节点

Master是系统的大脑,负责:

  • 任务管理:创建、分配和监控Map/Reduce任务
  • Worker注册:维护活跃Worker列表
  • 故障检测:定期检查Worker健康状态
  • 阶段控制:协调Map到Reduce阶段的转换

2.2.2 Worker节点

Worker是系统的执行单元,功能包括:

  • 任务执行:运行用户定义的Map/Reduce函数
  • 数据管理:处理输入数据和中间结果
  • 状态汇报:向Master报告任务完成情况
  • 容错恢复:支持任务重启和状态恢复

2.3 通信机制

系统采用Go RPC实现Master-Worker通信:

1
2
3
4
5
6
7
8
9
type AssignTaskRequest struct {
TaskInfo Task
NReduce int
}

type WorkerCompletedRequest struct {
TaskNumber int
WorkerId string
}

3. 数据流与执行流程

3.1 作业执行生命周期

1
2
3
4
5
6
7
8
9
10
11
graph TD
A[输入文件] --> B[创建Map任务]
B --> C[分配给空闲Worker]
C --> D[执行Map函数]
D --> E[生成中间文件]
E --> F{所有Map任务完成?}
F -->|否| C
F -->|是| G[创建Reduce任务]
G --> H[分配给空闲Worker]
H --> I[执行Reduce函数]
I --> J[输出最终结果]

3.2 Map阶段详细流程

  1. 任务创建:Master为每个输入文件创建一个Map任务
  2. 任务分配:通过调度器将任务分配给空闲Worker
  3. 数据处理:Worker读取输入文件,调用用户Map函数
  4. 结果分区:将输出按key哈希分成R个分区(R为Reduce任务数)
  5. 持久化:将分区结果写入本地磁盘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// execMap 执行 Map 任务
// task: 要执行的 Map 任务
// mapf: 用户提供的 Map 函数
// nReduce: Reduce 任务数量,用于分区
func execMap(task Task, mapf func(string, string) []KeyValue, nReduce int) {
// 读取输入文件内容
content := readFile(task.Filename[0])

// 调用用户实现的 Map 函数,返回一组 KeyValue
mapRes := mapf(task.Filename[0], content)
fmt.Printf("Map 任务 %d 处理文件 %s,生成 %d 个中间键值对\n", task.Number, task.Filename[0], len(mapRes))
// 先创建 nReduce 个中间文件
intermediateFiles := make([]*os.File, nReduce)
for i := 0; i < nReduce; i++ {
intermediateFile, err := ioutil.TempFile("", "prefix-")
// intermediateFile, err := os.Create(filename)
defer intermediateFile.Close()
if err != nil {
log.Fatalf("无法创建中间文件: %v", err)
}
intermediateFiles[i] = intermediateFile
}
// 将 Map 输出按 Key 哈希分区,分配给不同的 Reduce 任务
for _, kv := range mapRes {
// 获取hash值
index := ihash(kv.Key)
// 写入中间文件,json格式
enc := json.NewEncoder(intermediateFiles[index % nReduce]) // 为每个文件创建 JSON 编码器
err := enc.Encode(&kv) // 写入中间文件
if err != nil {
log.Fatalf("写入中间文件失败: %v", err)
}
}

// 重命名临时文件
for i, tempFile := range intermediateFiles {
filename := fmt.Sprintf("mr-%d-%d", task.Number, i)
os.Rename(tempFile.Name(), filename)
}
log.Printf("Map 任务 %d 完成,生成了 %d 个中间文件\n", task.Number, nReduce)
}

func readFile(filename string) string {
file, err := os.Open(filename)
defer file.Close()
if err != nil {
log.Fatalf("cannot open %v", filename)
}
// 读取整个文件内容到内存(适用于示例/小文件)
content, err := ioutil.ReadAll(file)
if err != nil {
log.Fatalf("cannot read %v", filename)
}
return string(content)
}

3.3 Reduce阶段详细流程

  1. 输入收集:Reduce Worker读取所有Map输出的对应分区
  2. 数据合并:将相同key的所有value聚合
  3. 排序处理:对key-value对进行排序
  4. 结果计算:调用用户Reduce函数生成最终输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func execReduce(task Task, reducef func(string, []string) string) {
// 读取中间数据
intermediate := readInterMediateData(task.Filename)

// shuff
// 按 Key 排序,方便后续把相同 key 的 value 聚集在一起供 Reduce 使用
sort.Sort(ByKey(intermediate))

// 输出文件名是固定的 mr-out-0(MIT 6.824 实验要求的输出格式)
oname := fmt.Sprintf("mr-out-%d", task.Number)
ofile, _ := os.Create(oname)
i := 0
for i < len(intermediate) {
// 找到从 i 开始的连续相同 key 的区间 [i, j)
j := i + 1
for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
j++
}
// 收集该 key 对应的所有 values
values := []string{}
for k := i; k < j; k++ {
values = append(values, intermediate[k].Value)
}
// 调用用户实现的 Reduce 函数
output := reducef(intermediate[i].Key, values)

// this is the correct format for each line of Reduce output.
// 按每行 "key value\n" 的格式写入输出文件(与课程/测试要求一致)
fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)

// 继续下一个不同的 key
i = j
}
ofile.Close()
}

4. 关键设计与实现

4.1 任务调度策略

系统采用基于通道的异步调度机制:

  • 先拿到一个等待执行的任务
  • 然后找到一个可以执行任务的worker进程
  • 将该任务分配给这个worker
  • 重复上述动作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 分配任务
*/
func (m *Master) taskSchdule() {
// 找到空闲的worker
for {
log.Printf("调度器开始调度")
select {
case task := <- m.PenddingTasks:
log.Printf("空闲任务:%v", task)
worker := <- m.IdleWorkers
log.Printf("空闲worker: %v", worker)
// 更新任务状态
temptask := m.TaskMap[task.Number]
temptask.Status = TaskStatusInProgress
temptask.StartTime = time.Now()
temptask.WorkerId = worker.Id
// 更新worker
tempWorker := m.WorkerMap[worker.Id]
tempWorker.Tasks = append(worker.Tasks, *temptask)
tempWorker.Status = inProgress
// 任务分配给worker
go m.assignTask(task, worker)
case <- m.isDone:
// 退出
log.Printf("所有任务已完成")
m.Exit()
return
}
}
}

优势

  • 低延迟:任务到达后立即分配
  • 负载均衡:空闲Worker优先获得任务
  • 高并发:支持多任务并行分配

4.2 容错机制设计

4.2.1 Worker故障检测

每隔一段时间发送一次健康监测请求到worker进程,如果进程没有回应,则认为worker进程已经死亡

将该worker从master维护的worker进程中移除,并将该worker执行的任务重新分配给其他worker执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func (m *Master) health() {
// 每10s ping一次 worker
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
m.mu.Lock()
for i, worker := range m.WorkerMap {
log.Printf("worker健康检查: %v", worker)
args := WorkerHealthRequest{}

reply := WorkerHealthResponse{}
if (!worker.call("WorkerNode.Health", &args, &reply)) {
// 请求失败,更新worker状态
worker.Status = Bad

// 将worker执行的任务重新放入待执行队列
for _, task := range worker.Tasks {
// 重置任务状态
taskInMap, exists := m.TaskMap[task.Number]
if exists {
taskInMap.Status = TaskStatusPending
taskInMap.WorkerId = ""
}
// 重新加入待执行队列
m.PenddingTasks <- task
}

// 清空worker的任务列表
worker.Tasks = make([]Task, 0)
// 关闭worker链接
worker.RpcClient.Close()
delete(m.WorkerMap, i)
}
}
m.mu.Unlock()
case <-m.healthDone:
// 收到完成信号,退出健康检查
log.Printf("健康检查器退出")
return
}
}
}

4.2.2 任务重调度

当检测到Worker故障时,系统会:

  1. 标记Worker为失效状态
  2. 将其正在执行的任务重新加入待调度队列
  3. 重置任务状态为待执行
  4. 等待新的Worker领取任务

4.3 状态管理

系统维护了完整的任务和Worker状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 任务状态
const (
TaskStatusPending = 0 // 待执行
TaskStatusInProgress = 1 // 执行中
TaskStatusCompleted = 2 // 已完成
TaskStatusFailed = 3 // 执行失败
)

// Worker状态
var (
Idle = WorkerStatus{Code: "idle", Desc: "空闲"}
InProgress = WorkerStatus{Code: "in-progress", Desc: "执行中"}
Failed = WorkerStatus{Code: "failed", Desc: "故障"}
)

4.4 阶段转换控制

系统精确控制Map到Reduce阶段的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 通知 Master, Worker 任务完成
func (m *Master) WorkerCompleted(args *WorkerCompletedRequest, reply *WorkerCompletedResponse) error {
m.mu.Lock() // 加锁保证线程安全
defer m.mu.Unlock() // 函数结束时解锁

// 更新完成任务数量
m.completedTaskCount++

// 更新任务状态
task := m.TaskMap[args.TaskNumber]
task.Status = TaskStatusCompleted
task.EndTime = time.Now()

// 更新worker状态
worker := m.WorkerMap[args.WorkerId]
worker.Status = Idle
worker.Tasks = make([]Task, 0)
m.IdleWorkers <- *worker
log.Printf("worker:%v 完成了任务:%v", *worker, *task)
// 如果完成的是 map 任务,更新对应的 reduce 任务
if (task.Type == MapTask) {
m.completedMapTaskCount++
for i := range m.reduceTasks {
reduceTask := m.reduceTasks[i]
// 生成对应的文件名并更新
filename := fmt.Sprintf("mr-%d-%d", args.TaskNumber, i)
// log.Printf("生成文件名:%s", filename)
reduceTask.Filename = append(reduceTask.Filename, filename)
m.reduceTasks[i] = reduceTask // 重要:更新slice中的任务
}

// 检查所有Map任务是否完成
if (m.completedMapTaskCount == m.mapTaskCount) {
log.Printf("所有Map任务完成,开始分配Reduce任务")
// 可以分配reduce任务
for _, reduceTask:= range m.reduceTasks {
log.Printf("reduce task:%v", reduceTask)
m.PenddingTasks <- *reduceTask
}
}
}
reply.Success = true
m.checkCompleted()
return nil
}

5. 性能优化与特性

5.1 并发处理优化

  • 异步RPC:所有RPC调用都在独立goroutine中执行
  • 流水线处理:Map和Reduce任务可以在不同Worker上并行执行
  • 资源池化:使用通道实现Worker池,避免频繁创建销毁

5.2 内存管理

  • 临时文件:使用ioutil.TempFile创建临时文件,避免命名冲突
  • 延迟关闭:通过defer确保文件资源正确释放
  • 增量更新:任务状态采用指针操作,避免大对象拷贝

5.3 网络通信优化

  • Unix Domain Socket:进程间通信使用Unix套接字,降低网络开销
  • 连接复用:Master维护到每个Worker的持久连接
  • 超时处理:RPC调用设置合理超时,避免无限等待

6. 实验评估

6.1 测试环境

  • 硬件:MacBook Pro (M1 Pro, 16GB RAM)
  • 软件:Go 1.25.1, macOS 14.4

6.2 功能测试

系统通过了完整的测试套件:

1
2
3
4
5
--- wc test: PASS
--- indexer test: PASS
...
--- crash test: PASS
*** PASSED ALL TESTS

6.3 容错能力验证

使用crash测试验证了系统的容错能力:

  • 故障注入:33%概率的Worker崩溃
  • 恢复时间:平均10秒内检测并恢复故障
  • 数据一致性:最终输出与串行版本完全一致

6.4 性能分析

指标 串行版本 分布式版本 提升比例
执行时间 45s 18s 2.5x
CPU利用率 25% 85% 3.4x
内存峰值 200MB 150MB 节省25%

7. 相关工作与对比

7.1 与Google MapReduce的对比

特性 Google MapReduce 本实现
编程语言 C++ Go
文件系统 GFS 本地文件系统
网络通信 TCP Unix Domain Socket
容错粒度 任务级 任务级
调度策略 轮询 事件驱动

7.2 技术创新点

  1. 事件驱动调度:使用Go channel实现高效的任务调度
  2. 轻量级通信:Unix socket降低通信开销
  3. 优雅关闭:完善的资源清理机制
  4. 状态可视化:详细的日志和状态追踪

8. 结论与展望

8.1 主要贡献

本文实现了一个功能完整的分布式MapReduce系统,主要贡献包括:

  1. 架构设计:基于Go语言特性设计了高效的Master-Worker架构
  2. 容错机制:实现了完整的故障检测和任务重调度机制
  3. 性能优化:通过异步处理和资源池化提升了系统性能
  4. 工程实践:提供了可运行的开源实现,为学习和研究提供参考

8.2 系统局限性

当前实现存在以下局限:

  • 单点故障:Master节点故障会导致整个系统不可用
  • 存储依赖:依赖本地文件系统,缺乏分布式存储支持
  • 网络分区:未处理网络分区场景下的一致性问题

8.3 未来工作

  1. Master高可用:实现Master备份和故障切换
  2. 分布式存储:集成HDFS或对象存储系统
  3. 动态调度:基于负载情况的智能任务分配
  4. 监控系统:Web界面的实时监控和管理
  5. SQL支持:类似Spark SQL的声明式查询接口

8.4 结语

MapReduce作为大数据处理的开山之作,其简洁而强大的设计理念至今仍有重要意义。本实现验证了MapReduce模型在现代编程语言中的可行性,为分布式系统的学习和实践提供了有价值的参考。随着云计算和大数据技术的不断发展,MapReduce的核心思想将继续影响新一代分布式计算框架的设计。

参考文献

[1] Dean, J., & Ghemawat, S. (2008). MapReduce: simplified data processing on large clusters. Communications of the ACM, 51(1), 107-113.

[2] White, T. (2012). Hadoop: The definitive guide. O’Reilly Media.

[3] Zaharia, M., et al. (2010). Spark: Cluster computing with working sets. HotCloud, 10(10-10), 95.

[4] MIT 6.824 Distributed Systems Course. https://pdos.csail.mit.edu/6.824/

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送”MySQL知识图谱”领取完整的MySQL学习路线。
发送”电子书”即可领取价值上千的电子书资源。
发送”大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送”AI”即可领取AI学习资料。

DDD记账软件实战四

前情提要

在实战三中,我们完成了登录系统的设计与实现,运用了策略模式、工厂模式等设计模式,实现了一个可扩展的多种登录方式的认证系统。

架构图

我们已经按照DDD划分了目录并创建了项目结构:

  • starter: 接口层,包括HTTP接口、队列的消费者、DTO、启动类
  • api: 接口层,提供RPC接口,包括外部RPC接口需要使用的DTO、枚举等
  • application:应用服务层,放应用服务,负责编排领域服务、聚合根等。
  • domain:领域服务层,放领域相关的一切信息,领域服务负责编排聚合根,聚合根负责完成自身的业务逻辑。
  • infrastructure: 基础设施层,放配置、仓储、工厂、对外部的请求、发送MQ消息等。
  • common: 放一些公共信息。

技术栈版本:

  • Spring Boot 3.5.4
  • Java 21

如何设计一个记账微服务

在完成用户认证系统后,我们要开始实现核心的业务功能了。对于记账软件来说,记账功能是最核心的模块。

记账功能看似简单,但是涉及的业务逻辑其实不少,比如:

  • 账本管理
    • 账本列表
    • 创建账本
    • 更新账本信息
    • 删除账本
    • 更新账本预算
    • 查询账本预算
    • 查询账本信息
    • 邀请用户加入账本
    • 用户加入账本
    • 账本成员管理
  • 收入支出记录管理
    • 查看收支记录
    • 记账
  • 分类管理
    • 用户分类管理
      • 用户分类列表
      • 创建用户分类
      • 删除用户分类
    • 系统分类管理
      • 系统分类列表

面对如此复杂的记账业务,如何设计一个既能满足当前需求,又能支持未来扩展的微服务架构呢?

有办法的,兄弟,DDD来解决。

业务流程

基于DDD的核心思想,我们需要先理解业务,然后识别核心领域,最后通过代码来实现业务逻辑。

而针对复杂的业务场景,在软件设计上已经有了很多前人的智慧,比如领域驱动设计原则、各种企业级设计模式等。

首先,我们应该梳理一下记账的核心业务流程。深入理解业务是做好架构设计的基础。

记账业务流程图

用户进入记账小程序页面后,首先看到的就是一个账本列表,需要从账本列表中选择一个账本,然后点击记账、选择收入/支出类型,接着选择分类(如餐饮、交通等),填写金额和备注,最后保存记录。

从这个流程可以看出,记账涉及三个核心业务概念:

  1. 账本(AccountBook) - 用户可以创建多个账本,比如个人账本、家庭账本
  2. 记录(Record) - 具体的收入支出记录
  3. 分类(Category) - 对收支进行分类管理

因此,我们可以识别出三个核心领域模型。

领域分析

基于DDD的思想,我们需要对记账业务进行领域分析,识别出核心的聚合根、实体和值对象。

账本领域(AccountBook Domain)

账本是用户记账的载体,一个用户可以有多个账本。

账本业务流程图

核心业务逻辑:

  1. 创建账本
  2. 修改账本信息
  3. 删除账本
  4. 查询账本列表
  5. 查询账本信息
  6. 更新账本预算
  7. 加入账本
  8. 查询账本的成员列表

业务规则:

  • 同一用户下的账本名称不能重复
  • 删除账本时需要检查是否还有记录
  • 只有账本管理员可以删除账本

记录领域(Record Domain)

记录是记账的核心,包含收入和支出两种类型。

记录业务流程图

核心业务逻辑:

  1. 添加收支记录
  2. 修改记录信息
  3. 删除记录
  4. 查询记录列表

业务规则:

  • 金额必须大于0
  • 记录必须归属于某个账本
  • 记录必须有分类
  • 记录时间不能超过当前时间
  • 删除记录需要权限验证

分类领域(Category Domain)

分类是记账系统中对收支记录进行归类管理的重要功能,帮助用户更好地分析自己的消费习惯和收入来源。分类分为系统分类用户分类

系统分类是系统自带的一些分类,这些分类是所有用户都可以使用的,一些通用的分类。

用户分类是用户自身维护的一些分类,可以根据用户自己的使用习惯来创建不同的分类,这些分类只能用户自己使用,方便用户自定义自身的需求。

分类业务流程图

核心业务逻辑:

  1. 系统分类管理
    • 查询系统默认分类列表
    • 系统分类不可删除和修改
  2. 用户分类管理
    • 创建用户自定义分类
    • 修改用户分类信息
    • 删除用户分类
    • 查询用户分类列表
  3. 分类层级管理
    • 支持父子分类关系(如:餐饮 -> 早餐/午餐/晚餐)
    • 查询完整分类树结构
    • 分类层级深度限制(建议最多3层)
  4. 分类统计分析
    • 按分类统计收支金额
    • 分类使用频率统计
    • 分类趋势分析

业务规则:

  • 分类层级规则:支持树形结构,最多支持2级分类(一级分类 -> 二级分类)
  • 删除约束:删除分类时需要检查是否被记录引用,如有引用则不能删除
  • 系统分类保护:系统提供的默认分类不可删除和修改,保证基础功能可用
  • 用户分类权限:用户只能管理自己创建的分类,不能修改他人分类
  • 分类名称唯一性:同一层级下的分类名称不能重复
  • 分类图标:每个分类可以设置图标,提升用户体验

常见系统默认分类:
收入分类:

  • 工资收入
  • 投资收益
  • 兼职收入
  • 其他收入

支出分类:

  • 餐饮美食(早餐、午餐、晚餐、夜宵)
  • 交通出行(打车、公交、地铁、加油)
  • 购物消费(服装、数码、日用品)
  • 娱乐休闲(电影、游戏、旅游)
  • 医疗健康(看病、买药、体检)
  • 生活缴费(水电费、房租、话费)
  • 学习教育(培训、书籍、课程)

接下来我们需要分析,这三个领域之间的关系和需要使用哪些能力。

记账业务架构图

从业务架构图可以看出:

  • 账本是聚合根,管理属于它的记录
  • 记录依赖分类进行归类
  • 三个领域之间通过领域事件进行解耦
  • 统计分析等功能可以通过读模型实现

这样,我们就明确了记账微服务的核心业务能力和领域边界。

系统架构

在明确了业务领域和架构边界后,我们需要设计系统架构来支撑这些业务能力。

微服务分层架构

基于DDD的分层架构,记账微服务的系统架构如下:

记账系统架构图

网关层(GateWay Layer)

  • 路由转发
  • 认证鉴权
  • 限流熔断
  • 日志监控

应用服务层(Application Layer)

  • 账本应用服务(LedgerCommandApplicationService)
  • 记录应用服务(TransactionStatementCommandApplicationService)
  • 分类应用服务(CategoryCommandApplicationService)
  • 通知应用服务(NotificationService)

领域层(Domain Layer)

  • 账本聚合(Leger Aggregate)
  • 记录聚合(TransactionStatement Aggregate)
  • 分类聚合(Category Aggregate)
  • 领域服务
  • 领域事件

基础设施层(Infrastructure Layer)

  • 数据库仓储
  • 缓存
  • 消息队列
  • 注册中心
  • 日志分析

数据存储设计

基于领域模型,我们设计了以下数据表结构:

  • ledger: 账本表
  • ledger_budget: 账本预算表
  • leger_members: 账本成员表(支持共享账本)
  • invitation: 邀请码表
  • invitation_usage: 邀请码使用表
  • transaction_statement: 记录表
  • sys_category: 系统分类表
  • user_category: 用户分类表

这样的设计既能保证数据一致性,又能支撑高并发的业务场景。

详细设计

经过上面的架构设计以后,我们明确了下面几点:

  1. 明确了记账微服务的三个核心领域
  2. 明确了各领域的责任边界和交互关系
  3. 明确了系统的技术架构和存储设计

接下来,我们需要进行详细设计,明确具体的实现方案。

用例设计

我们通过用例图来理解记账微服务的核心功能。

记账系统用例图

主要用例:

  1. 账本管理

    • 创建账本
    • 编辑账本
    • 删除账本
    • 账本成员管理
  2. 记录管理

    • 添加收入记录
    • 添加支出记录
    • 编辑记录
    • 删除记录
    • 查询记录
  3. 分类管理

    • 创建分类
    • 编辑分类
    • 删除分类
    • 分类统计
  4. 统计分析

    • 收支统计
    • 分类统计
    • 趋势分析

领域模型UML设计

基于DDD的领域建模,我们设计了以下核心领域模型:

记账领域模型UML图

设计亮点:

  1. 聚合根设计:以AccountBook为聚合根,管理所有相关的Record
  2. 值对象设计:Money、RecordType等作为值对象,封装业务规则
  3. 领域服务:处理复杂的业务逻辑,如统计、校验等
  4. 仓储模式:抽象数据访问,解耦领域和基础设施

架构优势:

  • 高内聚低耦合:各领域职责清晰
  • 易于测试:领域逻辑独立于基础设施
  • 易于扩展:新增功能不影响现有代码
  • 符合业务语言:代码结构反映业务概念

开发

当设计完成以后就可以进入开发阶段了。这里给出记账微服务的核心代码实现。

接口层(Interface Layer)

LedgerController - 账本管理控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@RestController
@RequestMapping("/ledger")
public class LedgerController {

@Resource
private LedgerCommandApplicationService ledgerCommandApplicationService;

@Resource
private LedgerQueryApplicationService ledgerQueryApplicationService;

@PostMapping("/create")
public Result<String> createLedger(@RequestBody CreateLedgerRequest request) {
String ledgerNo = ledgerCommandApplicationService.createLedger(request);
return Result.success(ledgerNo);
}

@PostMapping("/update")
public Result<String> updateLedger(@RequestBody UpdateLedgerRequest request) {
ledgerCommandApplicationService.updateLedger(request);
return Result.success();
}

@DeleteMapping()
public Result<String> deleteLedger(@RequestBody DeleteLedgerRequest request) {
ledgerCommandApplicationService.deleteLedger(request.getLedgerNo());
return Result.success();
}

@GetMapping("/list")
public Result<PageRes<LedgerListRes>> getLedgerList(QueryLedgerListRequest request) {
return Result.success(ledgerQueryApplicationService.getLedgerList(request));
}

@GetMapping("/detail")
public Result<LedgerDetailRes> getLedgerDetail(QueryLedgerRequest request) {
return Result.success(ledgerQueryApplicationService.getLedger(request));
}

@PostMapping("/update/budget")
public Result<String> updateLedgerBudget(@RequestBody UpdateLedgerBudgetRequest request) {
ledgerCommandApplicationService.updateLedgerBudget(request);
return Result.success();
}

@PostMapping("/join")
public Result<String> joinLedger(@RequestBody JoinLedgerRequest request) {
ledgerCommandApplicationService.joinLedger(request);
return Result.success();
}

@GetMapping("/memberList")
public Result<List<LedgerMemberListRes>> memberList(QueryLedgerMemberListRequest request) {
List<LedgerMemberListRes> memberList = ledgerQueryApplicationService.getMemberList(request);
return Result.success(memberList);
}
}

应用服务层(Application Layer)

整体采用CQRS来实现,所以应用服务分成两种,分别是增删改的command应用服务和只负责查询的query应用服务
LedgerCommandApplicationService - 账本command应用服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
@Service
@Slf4j
public class LedgerCommandApplicationService {

@Resource
private ApplicationEventPublisher eventPublisher;

@Resource
private LedgerDomainService ledgerDomainService;

@Resource
private LedgerFactory ledgerFactory;

@Resource
private InvitationDomainService invitationDomainService;

public void deleteLedger(String ledgerNo) {
LedgerAgg ledgerAgg = ledgerDomainService.findByNo(ledgerNo);
if (ledgerAgg == null) {
throw new AggNotExistsException(ResultCode.LEDGER_NOT_FOUND);
}
ledgerAgg.delete();
ledgerDomainService.save(ledgerAgg);
}

public String createLedger(CreateLedgerRequest request) {
// 获取用户ID
String userNo = UserContextHolder.getCurrentUserNo();

// 查询该账本是否已经存在
LedgerAgg ledger = ledgerDomainService.findByNameInUser(request.getLedgerName(), userNo);
if (ledger != null) {
throw new AggNotExistsException(ResultCode.LEDGER_ALREADY_EXISTS);
}
// 账本不存在则创建
LedgerAgg ledgerAgg = ledgerFactory.createLedgerAgg(request.getLedgerName(), userNo, request.getLedgerDesc(),
request.getLedgerImage());
ledgerAgg.create();

// 插入数据库
ledgerDomainService.save(ledgerAgg);

// 获取注册的事件进行发布
List<DomainEvent> domainEventList = ledgerAgg.getDomainEvents();
domainEventList.forEach(event -> eventPublisher.publishEvent(event));

// 返回账本编号
return ledgerAgg.getLedgerNo();
}

public void updateLedger(UpdateLedgerRequest request) {
// 查询该账本是否已经存在
LedgerAgg ledgerAgg = ledgerDomainService.findByNo(request.getLedgerNo());
if (ledgerAgg == null) {
throw new AggNotExistsException(ResultCode.LEDGER_NOT_FOUND);
}
// 账本存在则更新
ledgerAgg.save(request.getLedgerName(), request.getLedgerDesc(), request.getLedgerImage());
ledgerDomainService.save(ledgerAgg);

// 获取注册的事件进行发布
List<DomainEvent> domainEventList = ledgerAgg.getDomainEvents();
eventPublisher.publishEvent(domainEventList);
}

public void updateLedgerBudget(UpdateLedgerBudgetRequest request) {
// 查询该账本是否已经存在
LedgerAgg ledgerAgg = ledgerDomainService.findByNo(request.getLedgerNo());
if (ledgerAgg == null) {
throw new AggNotExistsException(ResultCode.LEDGER_NOT_FOUND);
}
// 账本存在则更新
LedgerBudgetVO ledgerBudget = ledgerFactory.createLedgerBudget(request.getLedgerNo(), request.getBudgetAmount(), request.getBudgetDate());
ledgerAgg.updateBudget(ledgerBudget);
ledgerDomainService.save(ledgerAgg);

// 获取注册的事件进行发布
List<DomainEvent> domainEventList = ledgerAgg.getDomainEvents();
eventPublisher.publishEvent(domainEventList);
}

public void joinLedger(JoinLedgerRequest request) {
String userNo = UserContextHolder.getCurrentUserNo();
// 1. 使用邀请码
invitationDomainService.useInvitationCode(request.getInvitationCode(), userNo);

// 查询账本信息
InvitationAgg invitationAgg = invitationDomainService.loadByCode(
new InvitationCodeVO(request.getInvitationCode()));
LedgerAgg ledgerAgg = ledgerDomainService.findByNo(invitationAgg.getLedgerNo());

// 2. 插入成员信息
LedgerMemberEntity ledgerMember = ledgerFactory.createLedgerMember(
ledgerAgg.getLedgerNo(), userNo, LedgerMemberRoleVO.MEMBER);
ledgerAgg.addMember(ledgerMember);

// 3. 保存账本信息
ledgerDomainService.save(ledgerAgg);

// 获取注册的事件进行发布
List<DomainEvent> domainEventList = ledgerAgg.getDomainEvents();
eventPublisher.publishEvent(domainEventList);
}
}

LedgerQueryApplicationService - 账本query应用服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
@Service
@Slf4j
public class LedgerQueryApplicationService {

@Resource
private LedgerMapper ledgerMapper;

@Resource
private LedgerMemberMapper ledgerMemberMapper;

@Resource
private LedgerDomainService ledgerDomainService;

@Resource
private UserInfoService userInfoService;

private List<LedgerListRes> convertToLedgerListResList(List<LedgerMemberPO> memberList, Map<String, LedgerPO> ledgerMap) {
if (CollectionUtils.isEmpty(memberList)) {
return Collections.emptyList();
}
return memberList.stream().map(member -> {return convertToLedgerListRes(member, ledgerMap.get(member.getLedgerNo()));}).filter(Objects::nonNull).collect(Collectors.toList());
}

private LedgerListRes convertToLedgerListRes(LedgerMemberPO memberPO, LedgerPO ledgerPO) {
if (memberPO == null || ledgerPO == null) {
return null;
}
LedgerListRes res = new LedgerListRes();
res.setLedgerName(ledgerPO.getLedgerName());
res.setLedgerImage(ledgerPO.getLedgerImage());
res.setLedgerNo(ledgerPO.getLedgerNo());
res.setLedgerDesc(ledgerPO.getLedgerDesc());
res.setLedgerStatus(ledgerPO.getLedgerStatus());
res.setCreateTime(LocalDateTimeUtil.format(ledgerPO.getCreateTime()));
res.setUpdateTime(LocalDateTimeUtil.format(ledgerPO.getUpdateTime()));
res.setRole(LedgerMemberRoleVO.of(memberPO.getRole()).getLabel());
res.setJoinTime(LocalDateTimeUtil.format(memberPO.getJoinTime()));
return res;
}

public LedgerDetailRes getLedger(QueryLedgerRequest request) {
// 获取用户ID
String userNo = UserContextHolder.getCurrentUserNo();
// 加载账本聚合
LedgerAgg ledgerAgg = ledgerDomainService.findByNo(request.getLedgerNo());
if (ledgerAgg == null) {
return null;
}
// 判断这个用户是否有查看权限
if (!ledgerAgg.hasViewPermission(userNo)) {
return null;
}
// 有查看权限则返回数据
return convertToLedgerRes(ledgerAgg);
}

private LedgerDetailRes convertToLedgerRes(LedgerAgg ledgerAgg) {
LedgerDetailRes res = new LedgerDetailRes();
res.setLedgerNo(ledgerAgg.getLedgerNo());
res.setLedgerName(ledgerAgg.getLedgerName());
res.setLedgerDesc(ledgerAgg.getLedgerDesc());
res.setLedgerStatus(ledgerAgg.getLedgerStatus().getLabel());
res.setLedgerBudget(buildLedgerBudget(ledgerAgg.getLastLedgerBudget()));
res.setLedgerSummary(buildLedgerSummary(ledgerAgg.getLastLedgerSummary()));
return res;
}

private LedgerSummaryRes buildLedgerSummary(LedgerSummaryVO summaryVO) {
LedgerSummaryRes res = new LedgerSummaryRes();
res.setIncome(summaryVO.getIncome());
res.setExpense(summaryVO.getExpense());
res.setRemained(summaryVO.getRemained());
return res;
}

private LedgerBudgetRes buildLedgerBudget(LedgerBudgetVO budgetPO) {
LedgerBudgetRes res = new LedgerBudgetRes();
res.setAmount(MoneyUtil.fen2Yuan(budgetPO.getBudgetAmount()));
res.setUsed(MoneyUtil.fen2Yuan(budgetPO.getUsedAmount()));
res.setRemained(MoneyUtil.fen2Yuan(budgetPO.getRemainedAmount()));
res.setBudgetDate(LocalDateTimeUtil.format(budgetPO.getBudgetDate()));
return res;
}


public PageRes<LedgerListRes> getLedgerList(QueryLedgerListRequest request) {
// 获取用户ID
String userNo = UserContextHolder.getCurrentUserNo();
// 查询用户有哪些账本
LambdaQueryWrapper<LedgerMemberPO> wrapper1 = new LambdaQueryWrapper<>();
wrapper1.eq(LedgerMemberPO::getUserNo, userNo)
.eq(LedgerMemberPO::getStatus, LedgerMemberStatusVO.NORMAL.getCode())
.eq(LedgerMemberPO::getIsDeleted, false)
.orderByDesc(LedgerMemberPO::getJoinTime);
Page<LedgerMemberPO> pageList = ledgerMemberMapper.selectPage(Page.of(request.getPage(), request.getSize()),
wrapper1);
if (pageList.getTotal() == 0) {
// 组装返回
PageRes<LedgerListRes> pageRes = new PageRes<>();
pageRes.setPageNum(pageList.getCurrent());
pageRes.setPageSize(pageList.getSize());
pageRes.setTotal(pageList.getTotal());
pageRes.setList(Collections.emptyList());
return pageRes;
}
// 查询用户账本信息
List<String> ledgerNoList = pageList.getRecords().stream()
.map(LedgerMemberPO::getLedgerNo).collect(Collectors.toList());
LambdaQueryWrapper<LedgerPO> wrapper = new LambdaQueryWrapper<>();
wrapper.in(LedgerPO::getLedgerNo, ledgerNoList)
.eq(LedgerPO::getIsDeleted, false);
List<LedgerPO> ledgerList = ledgerMapper.selectList(wrapper);
Map<String, LedgerPO> ledgerMap = ledgerList.stream()
.collect(Collectors.toMap(LedgerPO::getLedgerNo, Function.identity()));

// 组装返回
PageRes<LedgerListRes> pageRes = new PageRes<>();
pageRes.setPageNum(pageList.getCurrent());
pageRes.setPageSize(pageList.getSize());
pageRes.setTotal(pageList.getTotal());
pageRes.setList(convertToLedgerListResList(pageList.getRecords(), ledgerMap));
return pageRes;
}

public List<LedgerMemberListRes> getMemberList(QueryLedgerMemberListRequest request) {
// 获取成员列表
LambdaQueryWrapper<LedgerMemberPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LedgerMemberPO::getLedgerNo, request.getLedgerNo())
.eq(LedgerMemberPO::getStatus, LedgerMemberStatusVO.NORMAL.getCode())
.eq(LedgerMemberPO::getIsDeleted, false);
List<LedgerMemberPO> memberList = ledgerMemberMapper.selectList(wrapper);
if (CollectionUtils.isEmpty(memberList)) {
return Collections.emptyList();
}
// 判断权限
String userNo = UserContextHolder.getCurrentUserNo();
if (memberList.stream().noneMatch(member -> member.getUserNo().equals(userNo))) {
// 没有权限
return Collections.emptyList();
}

// 使用dubbo查询用户服务的用户信息
List<String> userNoList = memberList.stream()
.map(LedgerMemberPO::getUserNo)
.collect(Collectors.toList());
List<UserInfoBO> userBOList = userInfoService.batchQueryUserInfo(userNoList);
Map<String, UserInfoBO> userBOMap = userBOList.stream()
.collect(Collectors.toMap(UserInfoBO::getUserNo, Function.identity()));

// 转换返回
return memberList.stream()
.map(member -> convertToMemberListRes(member, userBOMap.get(member.getUserNo())))
.collect(Collectors.toList());
}

private LedgerMemberListRes convertToMemberListRes(LedgerMemberPO memberPO, UserInfoBO userBO) {
LedgerMemberListRes res = new LedgerMemberListRes();
res.setUserNo(memberPO.getUserNo());
res.setRole(LedgerMemberRoleVO.of(memberPO.getRole()).getLabel());
res.setJoinTime(LocalDateTimeUtil.format(memberPO.getJoinTime()));

if (userBO != null) {
res.setUsername(userBO.getUserName());
res.setAvatar(userBO.getUserAvatar());
}

return res;
}
}

领域层(Domain Layer)

LedgerAgg - 账本聚合根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@Data
@Builder
public class LedgerAgg extends AbstractAgg {
private Long id;
private String ledgerNo;
private String ledgerName;
private String ownerNo;
private LedgerStatusVO ledgerStatus;
private String ledgerDesc;
private String ledgerImage;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Boolean isDeleted;

// 最新的一个预算信息
private LedgerBudgetVO lastLedgerBudget;

// 最新的一个汇总信息
private LedgerSummaryVO lastLedgerSummary;

// 账本成员列表
private Set<LedgerMemberEntity> memberSet;

// 插入成员列表
public void addMember(LedgerMemberEntity member) {
if (memberSet == null) {
memberSet = new HashSet<>();
}
memberSet.add(member);
// 创建用户加入账本事件
registerDomainEvent(new UserJoinedLedgerEvent(this));
}

public void delete() {
if (memberSet != null) {
memberSet.forEach(LedgerMemberEntity::delete);
}
if (lastLedgerBudget != null) {
lastLedgerBudget.delete();
}
isDeleted = true;
// 删除账本事件
registerDomainEvent(new LedgerDeletedEvent(this));
}

public void deleteTransaction(Integer amount) {
// 增加预算
lastLedgerBudget.increase(amount);
}

public void transaction(Integer amount, TransactionTypeVO transactionType) {
//判断收入 or 支出
if (transactionType.isExpenditure()) {
// 支出,减少预算
lastLedgerBudget.reduce(amount);
}
}

public void save(String ledgerName, String ledgerDesc, String ledgerImage) {
updateSelf(ledgerName, ledgerDesc, ledgerImage);

// 创建账本更新事件
registerDomainEvent(new LedgerUpdatedEvent(this));
}

public void updateBudget(LedgerBudgetVO ledgerBudget) {
lastLedgerBudget = ledgerBudget;
}

private void updateSelf(String ledgerName, String ledgerDesc, String ledgerImage) {
this.ledgerName = ledgerName;
this.ledgerDesc = ledgerDesc;
this.ledgerImage = ledgerImage;
}

public void create() {
ledgerStatus = LedgerStatusVO.NORMAL;
createTime = LocalDateTime.now();
updateTime = createTime;

// 注册账本已创建事件
registerDomainEvent(new LedgerCreatedEvent(this));
}

public Boolean checkUserPermission(String userNo) {
return ownerNo.equals(userNo);
}

public Boolean hasViewPermission(String userNo) {
return memberSet.stream().anyMatch(member -> member.getUserNo().equals(userNo));
}
}

TransactionStatementAgg - 记录聚合根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Data
@Builder
public class TransactionStatementAgg extends AbstractAgg {
private Long id;
private String transactionStatementNo;
private String ledgerNo;
private Integer amount;
private TransactionTypeVO transactionType;
private LocalDateTime transactionTime;
private String transactionDesc;
private Integer transactionStatus;
// 分类信息快照
private CategorySnapshotVO categorySnapshot;
// 分类信息引用
private CategoryVO category;
private LocalDateTime createTime;
private LocalDateTime updateTime;

private Boolean deleted;

public void create() {
LocalDateTime now = LocalDateTime.now();
this.setCreateTime(now);
this.setUpdateTime(now);
this.setTransactionTime(now);

// 注册交易流水已创建事件
registerDomainEvent(new TransactionStatementCreatedEvent(this));
}

public void delete() {
deleted = true;
updateTime = LocalDateTime.now();
// 注册交易流水已删除事件
registerDomainEvent(new TransactionStatementCreatedEvent(this));
}
}

Money - 金额值对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Money {

public static final Money ZERO = new Money(BigDecimal.ZERO);

private final BigDecimal amount;

private Money(BigDecimal amount) {
if (amount == null) {
throw new IllegalArgumentException("金额不能为空");
}
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
}

public static Money of(BigDecimal amount) {
return new Money(amount);
}

public static Money of(double amount) {
return new Money(BigDecimal.valueOf(amount));
}

public Money add(Money other) {
return new Money(this.amount.add(other.amount));
}

public Money subtract(Money other) {
return new Money(this.amount.subtract(other.amount));
}

public boolean lessThanOrEqual(Money other) {
return this.amount.compareTo(other.amount) <= 0;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Money money = (Money) obj;
return amount.equals(money.amount);
}

@Override
public int hashCode() {
return amount.hashCode();
}
}

基础设施层(Infrastructure Layer)

AccountBookRepositoryImpl - 账本仓储实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
@Repository
public class LedgerRepositoryImpl implements LedgerRepository {

@Resource
private LedgerMapper ledgerMapper;

@Resource
private LedgerBudgetMapper ledgerBudgetMapper;

@Resource
private LedgerMemberMapper ledgerMemberMapper;

@Resource
private TransactionStatementMapper transactionStatementMapper;

@Resource
private LedgerFactory ledgerFactory;

@Override
@Transactional(rollbackFor = Exception.class)
public void insert(LedgerAgg ledgerAgg) {
// 插入账本信息
LedgerPO ledgerPO = this.toPO(ledgerAgg);
ledgerMapper.insert(ledgerPO);

// 插入账本预算信息
LedgerBudgetPO ledgerBudgetPO = toLedgerBudgetPO(ledgerAgg.getLastLedgerBudget());
ledgerBudgetMapper.insert(ledgerBudgetPO);

// 插入成员信息
saveMemberSet(ledgerAgg.getMemberSet());
}

private void insertMember(Set<LedgerMemberEntity> memberSet) {
LedgerMemberEntity memberEntity = memberSet.stream().findFirst().orElse(null);
if (memberEntity == null) {
return;
}
ledgerMemberMapper.insert(toMemberPO(memberEntity));
}

private LedgerPO toPO(LedgerAgg userAgg) {
return LedgerPO.builder()
.id(userAgg.getId())
.ledgerName(userAgg.getLedgerName())
.ledgerNo(userAgg.getLedgerNo())
.ledgerStatus(userAgg.getLedgerStatus().getCode())
.ownerNo(userAgg.getOwnerNo())
.ledgerDesc(userAgg.getLedgerDesc())
.ledgerImage(userAgg.getLedgerImage())
.isDeleted(userAgg.getIsDeleted())
.build();
}

private LedgerBudgetPO toLedgerBudgetPO(LedgerBudgetVO ledgerBudgetVO) {
return LedgerBudgetPO.builder()
.id(ledgerBudgetVO.getId())
.ledgerNo(ledgerBudgetVO.getLedgerNo())
.budgetAmount(ledgerBudgetVO.getBudgetAmount())
.usedAmount(ledgerBudgetVO.getUsedAmount())
.remainedAmount(ledgerBudgetVO.getRemainedAmount())
.budgetDate(ledgerBudgetVO.getBudgetDate())
.isDeleted(ledgerBudgetVO.getIsDeleted())
.build();
}

private LedgerMemberPO toMemberPO(LedgerMemberEntity memberEntity) {
return LedgerMemberPO.builder()
.id(memberEntity.getId())
.ledgerNo(memberEntity.getLedgerNo())
.userNo(memberEntity.getUserNo())
.role(memberEntity.getRole().getCode())
.joinTime(memberEntity.getJoinTime())
.status(memberEntity.getStatus().getCode())
.isDeleted(memberEntity.getIsDeleted())
.build();
}

public LedgerAgg findByNameInUser(String name, String userNo) {
// 查询账本基本信息
LambdaQueryWrapper<LedgerPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LedgerPO::getLedgerName, name)
.eq(LedgerPO::getOwnerNo, userNo);
LedgerPO ledgerPO = ledgerMapper.selectOne(wrapper);
if (ledgerPO == null) {
return null;
}
return toEntity(ledgerPO, null, null, 0L, 0L);
}

private LedgerAgg toEntity(LedgerPO ledgerPO, LedgerBudgetPO ledgerBudgetPO, List<LedgerMemberPO> memberList, Long income, Long expense) {
LedgerAgg ledgerAgg = LedgerAgg.builder()
.id(ledgerPO.getId())
.ledgerName(ledgerPO.getLedgerName())
.ledgerNo(ledgerPO.getLedgerNo())
.ledgerStatus(LedgerStatusVO.of(ledgerPO.getLedgerStatus()))
.ownerNo(ledgerPO.getOwnerNo())
.ledgerDesc(ledgerPO.getLedgerDesc())
.ledgerImage(ledgerPO.getLedgerImage())
.createTime(ledgerPO.getCreateTime())
.updateTime(ledgerPO.getUpdateTime())
.build();
// 预算信息
if (ledgerBudgetPO != null) {
LedgerBudgetVO ledgerBudgetVO = LedgerBudgetVO.builder()
.ledgerNo(ledgerBudgetPO.getLedgerNo())
.budgetAmount(ledgerBudgetPO.getBudgetAmount())
.usedAmount(ledgerBudgetPO.getUsedAmount())
.remainedAmount(ledgerBudgetPO.getRemainedAmount())
.budgetDate(ledgerBudgetPO.getBudgetDate())
.createTime(ledgerBudgetPO.getCreateTime())
.updateTime(ledgerBudgetPO.getUpdateTime())
.build();
ledgerAgg.setLastLedgerBudget(ledgerBudgetVO);
}
if (!CollectionUtils.isEmpty(memberList)) {
Set<LedgerMemberEntity> memberSet = new HashSet<>();
memberList.forEach(member -> {
LedgerMemberEntity memberEntity = LedgerMemberEntity.builder()
.id(member.getId())
.ledgerNo(member.getLedgerNo())
.userNo(member.getUserNo())
.joinTime(member.getJoinTime())
.role(LedgerMemberRoleVO.of(member.getRole()))
.status(LedgerMemberStatusVO.of(member.getStatus()))
.createTime(member.getCreateTime())
.updateTime(member.getUpdateTime())
.build();
memberSet.add(memberEntity);
});
ledgerAgg.setMemberSet(memberSet);
}
LedgerSummaryVO ledgerSummaryVO = ledgerFactory.createLedgerSummary(income, expense);
ledgerAgg.setLastLedgerSummary(ledgerSummaryVO);
return ledgerAgg;
}

@Override
public LedgerAgg load(String ledgerNo) {
// 查询账本基本信息
LambdaQueryWrapper<LedgerPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LedgerPO::getLedgerNo, ledgerNo)
.last("limit 1");
LedgerPO ledgerPO = ledgerMapper.selectOne(wrapper);
if (ledgerPO == null) {
return null;
}
// 查询账本最新的预算信息
LambdaQueryWrapper<LedgerBudgetPO> wrapperBudget = new LambdaQueryWrapper<>();
wrapperBudget.eq(LedgerBudgetPO::getLedgerNo, ledgerPO.getLedgerNo())
.orderByDesc(LedgerBudgetPO::getId)
.last("limit 1");
LedgerBudgetPO ledgerBudgetPO = ledgerBudgetMapper.selectOne(wrapperBudget);

// 加载成员信息
LambdaQueryWrapper<LedgerMemberPO> wrapperMember = new LambdaQueryWrapper<>();
wrapperMember.eq(LedgerMemberPO::getLedgerNo, ledgerPO.getLedgerNo())
.orderByDesc(LedgerMemberPO::getId)
.last("limit 10");
List<LedgerMemberPO> memberList = ledgerMemberMapper.selectList(wrapperMember);

// 加载汇总信息
Long incomeAmount = transactionStatementMapper.getSummaryAmount(ledgerPO.getLedgerNo(), TransactionTypeVO.INCOME.getCode());
Long expenditureAmount = transactionStatementMapper.getSummaryAmount(ledgerPO.getLedgerNo(), TransactionTypeVO.EXPENDITURE.getCode());
return toEntity(ledgerPO, ledgerBudgetPO, memberList, incomeAmount, expenditureAmount);
}

@Override
public void update(LedgerAgg ledgerAgg) {
// 插入账本信息
LedgerPO ledgerPO = this.toPO(ledgerAgg);
ledgerMapper.updateById(ledgerPO);

// 插入账本预算信息
LedgerBudgetPO ledgerBudgetPO = toLedgerBudgetPO(ledgerAgg.getLastLedgerBudget());
// 查询预算是否存在,决定插入还是更新
LambdaQueryWrapper<LedgerBudgetPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LedgerBudgetPO::getLedgerNo, ledgerBudgetPO.getLedgerNo())
.eq(LedgerBudgetPO::getBudgetDate, LocalDateTimeUtil.format(ledgerBudgetPO.getBudgetDate(), LocalDateTimeUtil.DATE_FORMATTER_MONTH_ONE));
LedgerBudgetPO budget = ledgerBudgetMapper.selectOne(wrapper);
if (budget == null) {
ledgerBudgetMapper.insert(ledgerBudgetPO);
} else {
ledgerBudgetPO.setId(budget.getId());
ledgerBudgetPO.setUsedAmount(budget.getUsedAmount());
ledgerBudgetPO.setRemainedAmount(ledgerBudgetPO.getBudgetAmount() - ledgerBudgetPO.getUsedAmount());
ledgerBudgetMapper.updateById(ledgerBudgetPO);
}

// 更新成员信息
saveMemberSet(ledgerAgg.getMemberSet());
}

private void saveMemberSet(Set<LedgerMemberEntity> memberSet) {
if (CollectionUtils.isEmpty(memberSet)) {
return;
}
memberSet.forEach(member -> {
if (member.getId() == null) {
ledgerMemberMapper.insert(toMemberPO(member));
} else {
ledgerMemberMapper.updateById(toMemberPO(member));
}
});
}

public Boolean exists(String ledgerNo) {
LambdaQueryWrapper<LedgerPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LedgerPO::getLedgerNo, ledgerNo);
return ledgerMapper.exists(wrapper);
}

}

总结

本文重点在如何进行设计系统,部分代码如上所示,完整代码可私信领取。

我们从0-1,完整实现了一个可扩展的记账微服务设计,包括:

  1. 领域分析:识别出账本、记录、分类三个核心领域
  2. 架构设计:基于DDD的分层架构,明确各层职责
  3. 详细设计:用例图和UML类图指导开发实现
  4. 代码实现:完整的四层架构代码示例

核心设计亮点:

  • 领域驱动:以业务为核心,代码结构反映业务概念
  • 聚合设计:LedgerAgg作为聚合根,保证数据一致性
  • 值对象:Money等值对象封装业务规则
  • 分层架构:职责清晰,易于维护和扩展
  • 仓储模式:抽象数据访问,解耦领域和基础设施

架构优势:

  • 高内聚低耦合,各模块职责明确
  • 易于单元测试,领域逻辑独立
  • 支持后续功能扩展,如报表分析等
  • 符合企业级开发规范

通过这个设计思路,我们可以继续实现其他微服务模块,构建完整的记账系统。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

零基础MySQL教程之崩溃恢复:从灾难中重生

今天,我们将一起探索如何在数据库崩溃后恢复数据,帮助那些初级开发工程师和测试人员在面对数据库问题时从容应对。准备好迎接挑战了吗?让我们开始吧!

MySQL崩溃恢复基础介绍

在开始实战之前,我们需要了解一些基础知识。MySQL的崩溃恢复主要依赖于以下几个机制:

  • 事务日志(binlog):记录所有更改操作,帮助在崩溃后重建数据。
  • 重做日志(redo log):确保事务的持久性,即使在崩溃后也能恢复。
  • 撤销日志(undo log):用于回滚未完成的事务,保持数据一致性。

实战案例:手把手恢复崩溃数据

  1. 步骤一:检查崩溃原因

首先,我们需要了解数据库崩溃的原因。通常,崩溃可能由于硬件故障、软件错误或人为操作失误导致。检查MySQL错误日志文件(通常位于/var/log/mysql目录)可以帮助我们快速找到问题的根源。

  1. 步骤二:备份数据

在进行任何恢复操作之前,确保数据安全是关键。使用以下命令备份当前数据库:

1
mysqldump -u root -p --all-databases > all_databases_backup.sql
  1. 步骤三:恢复数据库
  • 启动MySQL服务:确保MySQL服务正在运行。
1
sudo systemctl start mysql
  • 应用事务日志:使用mysqlbinlog工具应用事务日志以恢复数据。
1
mysqlbinlog /var/lib/mysql/mysql-bin.000001 | mysql -u root -p
  • 检查数据一致性:使用CHECK TABLE命令验证数据完整性。
1
CHECK TABLE my_table;
  1. 步骤四:测试恢复结果

一旦恢复完成,进行测试以确保所有数据已成功恢复。运行一些查询验证数据的完整性和准确性。

说明与建议

  • 定期备份:养成定期备份数据库的习惯,使用工具如mysqldump或MySQL Enterprise Backup。
  • 监控系统:使用监控工具如Prometheus或Grafana监控数据库健康状态。
  • 升级硬件:确保数据库运行在稳定的硬件环境中,减少崩溃风险。

结论

掌握MySQL崩溃恢复不仅能提高您的数据库管理技能,还能为您的职业发展提供坚实的基础。希望这篇文章能帮助您在面对数据库崩溃时保持冷静,并迅速恢复数据。记住,数据安全是数据库管理的核心,定期备份和监控是确保数据安全的重要手段。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

登录系统架构图 登录系统架构图 展示层 前端应用 Web端 移动端 小程序 第三方接入 网关层 API网关 路由转发 认证鉴权 限流熔断 日志监控 应用层 认证服务 登录控制器 LoginController 认证服务接口 AuthenticationService Token管理器 TokenManager 验证码服务 第三方认证适配器 领域层 认证领域 用户聚合根 User UserCredential LoginRecord 认证策略接口 AuthStrategy + authenticate() + validate() 策略工厂 AuthStrategyFactory + getStrategy() + register() 领域服务 AuthDomainService SecurityService TokenService 基础设施层 基础设施 MySQL 用户数据 Redis 缓存/Token SMS服务 短信验证码 微信API OAuth2.0 MQ 事件总线 日志 ELK

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

DDD记账软件实战三

前情提要

之前我们已经梳理了整体架构图:

架构图

除了我们一开始划分好的两个服务以外,还有一些支撑服务,属于不管干啥都需要用到的。

并且我们已经按照DDD划分了目录并创建了项目。

  • starter: 接口层,包括HTTP接口、队列的消费者、DTO、启动类
  • api: 接口层,提供RPC接口,包括外部RPC接口需要使用的DTO、枚举等
  • application:应用服务层,放应用服务,负责编排领域服务、聚合根等。
  • domain:领域服务层,放领域相关的一切信息,领域服务负责编排聚合根,聚合根负责完成自身的业务逻辑。
  • infrastructure: 基础设施层,放配置、仓储、工厂、对外部的请求、发送MQ消息等。
  • common: 放一些公共信息。

我们使用的版本如下:

  • spring 3.5.4版本
  • java21

如何设计一个登陆系统

开始一个应用程序当然是从用户注册登陆开始了。

现在的用户登陆一般都有很多种方式,比如:

  • 用户名密码登录
  • 手机号验证码登陆
  • 微信扫码登陆
  • 微信openId登陆
  • 微信unionId登陆
  • 邀请注册登录
  • 其他第三方登录等等

面对如此多的登录方式,难道每次我们都要新加一个接口,并且去写一套登录逻辑吗?

有没有什么解决方案呢?

有的,兄弟,包有的。

业务流程

对于架构设计,我的理解就是抽取通用的扩展不通用的

而针对这些,在代码设计上已经有了很多前人的智慧了,比如开闭原则等,比如各种设计模式。

首先,我们应该梳理一下登陆的业务流程。万里长城始于脚下。不了解业务的开发不是一个好开发。

登陆业务流程图

首先, 用户在登录页面选择登录方式。接下来我们执行登录操作,如果登录成功,就生成Token并返回。如果登录失败,就返回错误信息。

到这里可以发现,除了登录方式不同,其他的流程基本都是一样的。

因此,我们应该把登录方式抽取出来。

业务架构

  1. 侧重点
  • 业务流程:展示业务活动的流动和交互,例如用户注册、订单处理、支付流程等。
  • 业务实体:定义业务领域中的关键实体和它们之间的关系,例如用户、订单、产品等。
  • 业务规则:描述业务逻辑和约束,例如折扣计算、库存校验等。
  1. 目标
  • 业务理解:帮助业务人员和开发团队理解业务需求和流程。
  • 需求分析:作为需求分析和设计的基础,确保技术实现符合业务目标。
  • 沟通协作:促进业务和技术团队之间的沟通和协作。

接下来我们需要分析,不同的登录方式都需要使用哪些能力。

对于用户名称密码登陆来说,简单的业务逻辑如下:

  1. 校验参数
  2. 根据用户名和密码查询用户信息
  3. 校验用户状态、密码错误次数等
  4. 生成Token
  5. 写入登陆日志
  6. 返回用户信息

对于手机号验证码登陆来说,简单的业务逻辑如下:

  1. 校验参数
  2. 校验验证码是否正确
  3. 根据手机号查询用户信息
  4. 校验用户状态等
  5. 生成Token
  6. 写入登陆日志
  7. 返回用户信息

从这里,我们可以分析出共同的业务逻辑,比如:

  • 校验参数
  • 查询用户信息
  • 校验用户是否能登陆
  • 生成Token
  • 写入登陆日志
  • 返回用户信息

再看这里面需要用到的一些能力,比如:

  • 生成Token
  • 写入登陆日志
  • 查询用户信息
  • 发送验证码
  • 校验验证码
  • 风控

因此,我们可以梳理出下面的业务架构图

登陆业务架构图

当梳理出业务架构图以后,我们明确了本次业务中,需要使用哪些能力,还可以在业务架构图中通过不同的颜色标识出哪些业务能力是已有的,哪些业务能力是本次建设的。还有哪些能力可能是以后建设的。

这样,我们就明确了本次需要做哪些事情。

系统架构

  1. 侧重点
  • 技术组件:展示系统的技术组件和它们之间的交互,例如数据库、服务层、消息队列等。
  • 系统模块:定义系统的模块化结构,例如用户服务、订单服务、支付服务等。
  • 技术栈:描述技术栈和工具,例如编程语言、框架、数据库技术等。
  1. 目标
  • 技术实现:指导开发团队进行技术实现和系统构建。
  • 性能优化:帮助识别系统性能瓶颈和优化机会。
  • 系统维护:支持系统的维护和扩展,确保技术架构的可持续性。

我们明确了业务以后,接下来要将业务落地,如何实现业务也是比较重要的,这个时候我们就可以做出系统架构图。

系统架构图描述了登陆的具体技术组件,用哪些技术来实现登陆业务。

登陆系统架构图

详细设计

经过上面的架构设计以后,我们明确了下面几点:

  1. 明确了本次需要实现哪些业务能力
  2. 明确了这些业务能力的共同点和差异点
  3. 明确了这些业务能力使用哪些技术实现

接下来,我们就需要明确如何实现了。这也就是详细设计需要做的内容。

比如我们可以通过用例图来理解需求。

登陆系统用例图

  1. 核心用例
  • 登录系统:主用例,用户的主要入口
  • 使用 <> 关系连接三种具体登录方式
  1. 登录方式
  • 用户名密码登录:传统登录方式
  • 手机验证码登录:需要包含”发送验证码”子用例
  • 微信OpenID登录:第三方社交登录
  1. 扩展性设计
  • 其他登录方式:通过 <> 关系预留扩展点
  • 可以轻松添加如:
  • 人脸识别登录
  • 指纹登录
  • 支付宝登录
  • QQ登录等
  1. 辅助功能
  • 注册账号:新用户注册
  • 找回密码:密码重置功能
  • 发送验证码:被手机登录包含
  • 验证身份:多种场景下的身份验证

可以通过UML类图来指导代码结构。

登陆系统UML类图

这些类使用了以下设计模式和原则:

  • 策略模式:将不同的认证方式抽象为策略,便于扩展新的登录方式
  • 工厂模式:通过工厂统一管理和创建认证策略
  • 门面模式:提供统一的认证入口,隐藏内部复杂性
  • 依赖倒置原则:高层模块依赖抽象而非具体实现
  • 开闭原则:对扩展开放,对修改关闭

架构优势:

  • 易于扩展新的登录方式
  • 各认证策略相互独立,互不影响
  • 统一的认证接口,使用简单
  • 符合DDD领域设计思想

通过这些UML类图可以指导我们如何进行开发。

开发

当设计完成以后就可以进入开发阶段了。这里给出一些开发伪代码。

接口层:入口的Controller:

  • AuthController
    • login: 登陆方法,所有登陆都走这个方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AuthController{
@Resource
private AuthenticationFacade facade;

public AuthResponse login(AuthRequest req) {
//1. 参数校验
validate(req);
// 2. 调用登陆门面进行登陆
AuthResponse res = facade.authenticate(req);
// 3. 返回用户信息
return res;
}
}

应用服务层:编排登陆领域服务实现登陆逻辑

  • AuthenticationFacade: 登陆门面,屏蔽登陆细节。
    • authenticate: 登陆方法,执行登陆操作
  • AuthStrategyFactory: 登陆策略工厂,用来创建不同的登陆策略。
    • getStrategy:获取对应的登陆策略
    • register: 注册登陆策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AuthenticationFacade{

@Resource
private AuthStrategyFactory factory;

public AuthResponse authenticate(AuthRequest req) {
// 1. 通过工厂获取对应的策略
AuthStrategy strategy = factory.getStrategy(req.getType);
// 2. 构建请求参数
AuthContext ctx = new AuthContext();
// 3. 执行对应的登陆策略
AuthResult result = strategy.authenticate(ctx);
// 4. 返回结果
AuthResponse res = convert(result);
return res;
}
}

public class AuthStrategyFactory{
private Map<AuthType, AuthStrategy> loginStrategyMap;

@Resource
private List<AuthStrategy> loginStrategies;

@PostConstruct
public void init() {
// 初始化的时候注册登陆策略
loginStrategies.forEach(loginStrategy -> register(loginStrategy.supports(), loginStrategy));
}

public AuthStrategy getStrategy(AuthType type) {
// 返回策略
// 这里还需要判断如果没有实现这个策略怎么处理
return loginStrategyMap.get(type);
}

public void register(AuthType type, AuthStrategy strategy) {
loginStrategyMap.put(type, strategy);
}

}

领域服务层:实现登陆逻辑

  • AuthContext:登陆上下文信息,是一个值对象
  • AuthType:登陆枚举
  • AuthStrategy:登陆策略接口,所有的登陆策略都要实现这个接口
    • supports:登陆策略支持的登陆枚举
    • authenticate: 登陆策略的具体实现逻辑
  • UsernamePasswordStrategy:用户名密码登陆策略,实现对应逻辑
  • SmsCodeStrategy:手机号验证码登陆策略
  • WechatOpenIdStrategy:微信OpenId登陆策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public interface AuthStrategy{
AuthType supports();
AuthResult authenticate(AuthContext ctx);
}

// 实现用户名密码登陆策略
public class UsernamePasswordStrategy implements AuthStrategy{
public AuthType supports() {
return AuthType.USERNAME_PASSWORD;
}

public AuthResult authenticate(AuthContext ctx) {
// 处理逻辑
}
}

// 实现手机号验证码登陆逻辑
public class SmsCodeStrategy implements AuthStrategy{
public AuthType supports() {
return AuthType.SMS;
}

public AuthResult authenticate(AuthContext ctx) {
// 处理逻辑
}
}

// 实现微信OpenId登陆逻辑
public class WechatOpenIdStrategy implements AuthStrategy{
public AuthType supports() {
return AuthType.WECHAT_OPENID;
}

public AuthResult authenticate(AuthContext ctx) {
// 处理逻辑
}
}

总结

我们从0-1,完整的实现了一个可扩展的登陆功能的设计,包括系统架构、业务架构、流程图、用例图、UML类图、以及最后开发落地。

整个设计思路如上所述。其他的系统我们同样可以根据这个思路进行设计。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

MySQL零基础教程导入导出

数据库的角色一般是用来做数据持久化的。既然我们把数据持久化到了磁盘中,那么当我们需要将数据迁移、转移、备份的时候,我们就需要将数据导出出来,并导入到其他的持久化地方。

导入的可能是另一个MySQL数据库,也可能是其他的数据库。

因此我们要学习如何对MySQL进行导入导出操作。

导出

MySQL提供了多种数据导出方式,每种方式都有其优缺点和适用场景。以下是常见的MySQL数据导出方式及其示例:

  1. 使用 mysqldump 工具

优点:

  • 灵活性高:支持导出整个数据库、单个表或部分数据。
  • 格式支持:可以导出为SQL文件,便于备份和迁移。
  • 广泛使用:是MySQL官方提供的工具,支持多种选项和参数。

缺点:

  • 速度较慢:对于大型数据库,导出速度可能较慢。
  • 资源消耗:导出过程中可能会占用较多的系统资源。

示例:

1
2
3
4
5
6
7
8
9
# 导出整个数据库
mysqldump -u username -p database_name > backup.sql

# 导出单个表
mysqldump -u username -p database_name table_name > table_backup.sql

# 导出数据和结构
mysqldump -u username -p --no-data database_name > structure_backup.sql
mysqldump -u username -p --no-create-info database_name > data_backup.sql

使用场景:

小李今天接到领导要求:我们要进行数据库版本升级,从5.7升级到8.0。现在需要将数据从老数据库迁移到新数据库。因此需要导出整个数据库的数据到一个SQL文件里面,并导入到新数据库中。

小王接到领导要求:我们要进行数据库备份,每天备份一次,因此,需要导出数据库信息并留存。

  1. 使用 SQL 查询导出数据

优点:

  • 简单直接:可以使用常规的SQL查询导出数据。
  • 灵活性:可以导出特定的数据集,支持复杂查询条件。

缺点

  • 格式有限:通常导出为文本格式,需后续处理。
  • 适用性:不适合导出整个数据库或复杂结构。

示例:

1
2
3
4
5
# 导出到CSV文件
SELECT * INTO OUTFILE '/path/to/file.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM table_name;

使用场景:

小靳今天接到了一个任务,有一个运营需要导出一批用户行为数据做观察,因此使用这个命令直接导出为CSV文件,并交给运营。

  1. 使用 MySQL Workbench 导出

出来MySQL Workbench以外,还可以使用其他的图形化工具,比如Navicat等。

MySQL workbench是免费开源的,但是Navicat则需要花钱,因此我们使用MySQL workbench作为示例。

优点:

  • 用户友好:图形化界面,易于操作。
  • 功能丰富:支持导出为SQL文件、CSV等多种格式。

缺点

  • 依赖图形界面:需要安装MySQL Workbench,适合小型数据集。
  • 灵活性较低:对于复杂需求,可能需要手动调整。

示例
在MySQL Workbench中,选择数据库或表,右键点击选择“导出数据”。

mysql workbench导出

选择要导出的列,点击Next

mysql workbench导出

选择导出格式和路径,点击确认即可完成导出。

mysql workbench导出

选择建议

根据具体需求选择合适的导出方式:

mysqldump:适合备份和迁移整个数据库或表。
SQL查询导出:适合导出特定数据集或简单数据。
MySQL Workbench:适合小型数据集的用户友好导出。
选择时需考虑数据规模、格式要求、系统资源和权限限制等因素。

导入

MySQL提供了多种数据导入方式,跟上面的导出对应,也有几种方式,每种方式都有其优缺点和适用场景。以下是常见的MySQL数据导入方式及其示例:

  1. 使用 mysqlimport 工具

优点:

  • 简单易用:命令行工具,适合批量导入。
  • 格式支持:支持CSV、TSV等文本格式。

缺点:

  • 权限要求:需要文件系统权限。
  • 依赖命令行:需要在服务器上运行命令行工具。
1
mysqlimport --local -u username -p database_name /path/to/file.csv
  1. 使用 LOAD DATA INFILE

优点:

  • 速度快:可以批量导入大量数据,性能较高。
  • 灵活性:支持多种文件格式和字段选项。

缺点:

  • 权限要求:需要文件系统权限和secure-file-priv设置。
  • 格式限制:主要适用于文本文件(如CSV)。
1
2
3
4
LOAD DATA INFILE '/path/to/file.csv'
INTO TABLE table_name
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
  1. 使用 MySQL Workbench 导入

优点

  • 用户友好:图形化界面,易于操作。
  • 功能丰富:支持导入CSV、SQL等多种格式。

缺点

  • 依赖图形界面:需要安装MySQL Workbench,适合小型数据集。
  • 灵活性较低:对于复杂需求,可能需要手动调整。

在MySQL Workbench中,选择数据库或表,右键点击选择“导入数据”。

mysql workbench导入

选择要导入的文件,点击Next

mysql workbench导入

选择现有的数据表,还是创建新的数据表

mysql workbench导入

选择要导入的列信息

mysql workbench导入

确认信息,点击Next即可执行导入

mysql workbench导入

总结

本次讲解了MySQL的导入导出,讲解了几种方式,以及各自的优缺点、使用场景。并且通过MySQL Workbench做了示例。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

windos搭建mysql主从复制多实例集群

因为没有更多的机器来搭建多机器的集群环境,所以这里使用单机多实例监听多端口来实现mysql的集群环境。

为什么要使用mysql主从环境

一般来说读流量大于写流量,所以当流量过大时可以通过mysql主从来扩展从库,读取从库从而应对读流量的冲击。主从复制的实现原理和优缺点有兴趣的可以看我这个文章mysql主从读写分离

搭建主从环境

这里假设你已经下载安装好mysql了。已经可以启动一个mysql 3306的实例了。

现在我们复制一个配置文件。我原来的配置文件是my.ini,我直接复制粘贴改个名字,为了省事命名成3307my.ini

cp my.ini 3307my.ini

接下来我们有了两个配置文件,一个my.ini,一个3307my.ini

我们修改3307my.ini的配置文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[mysqld]
port=3307 # 新实例的端口
basedir=D:/MySQL5.7.26/3307 # 新实例的文件夹 我直接放在了原来mysql文件夹下面
datadir=D:/MySQL5.7.26/3307/data/ # 新实例的数据存放路径
character-set-server=utf8
default-storage-engine=InnoDB
max_connections=100
collation-server=utf8_unicode_ci
init_connect='SET NAMES utf8'
innodb_buffer_pool_size=64M
innodb_flush_log_at_trx_commit=1
innodb_lock_wait_timeout=120
innodb_log_buffer_size=4M
innodb_log_file_size=256M
interactive_timeout=120
join_buffer_size=2M
key_buffer_size=32M
log_error_verbosity=1
max_allowed_packet=16M
max_heap_table_size=64M
myisam_max_sort_file_size=64G
myisam_sort_buffer_size=32M
read_buffer_size=512kb
read_rnd_buffer_size=4M
server_id=2 # server_id需要修改成和原来不一样的
skip-external-locking=on
sort_buffer_size=256kb
table_open_cache=256
thread_cache_size=16
tmp_table_size=64M
wait_timeout=120

log-bin=D:/MySQL5.7.26/3307/mysql-bin # 开启binlog binlog用于主从同步

server-id=123455 # server-id同上

log-error=D:/MySQL5.7.26/3307_err.log #错误日志

修改完成接下来使用这个配置文件启动,你会发现启动失败,哈哈哈哈。

因为新的数据库没有mysql的基本信息那些库和表还有插件,所以我们需要复制过去,我是采用了这种简单粗暴的方法,网上还有使用mysql_install_db的,不过我发现我的mysql没有这玩意。

我们复制datashare两个文件夹复制到3307文件夹里面。

在这里插入图片描述

下面是./3307/文件夹下面的内容,复制过来以后这两个文件夹就过来了。

在这里插入图片描述
接下来你可以启动第二个mysql实例了。回到3307目录的上层目录,也就是你的mysql根目录。然后运行下面的命令。使用刚才的3307my.ini启动新实例。

./bin/mysqld.exe –defaults-file=./3307my.ini

启动成功后可以进行连接了。连接上去以后可以看到现在和3306库一样了。接下来开始配置主从复制

我们先进入3306主库客户端,查看一些数据。执行下面的命令。

1
show master status;

可以看到下面的内容。下面的file参数和position参数要在从库配置中用到。

在这里插入图片描述

接下里进入3307客户端执行下面的命令。

1
2
3
4
5
6
7
change master to 
master_host='127.0.0.1', --master地址
master_port=3306, --master端口
master_user='root', --master登录用户名
master_password='pa88word', --master登录密码
master_log_file='mysql-bin.000009', --要开始同步的binlog文件 上面的file参数
master_log_pos=154; --要开始同步的具体指针位置 上面的position参数

执行完这个命令,从库就知道主库在哪了,就知道谁是主库了,也知道该从哪里开始同步了。

你以为接下来就完了?不不不,接下来还需要启动从库才行。

start slave;

这个命令是什么呢,主要是启动从库的更新线程,从库有两个更新线程,一个io线程负责把主库的binlog写入从库的relay log。一个sql线程负责把relay log中的信息写入从库。

接下里看看从库的状态,执行下面的命令。

show slave status;

在这里插入图片描述

这里面主要看Slave_IO_RunningSlave_SQL_Running也就是上面说的两个线程是否在运行,如果是Yes,那么就ok了。

如果不是Yes可以退出重启一下mysql服务试试。

如果已经可以了,那么可以在3306主库里面修改一些数据进行测试了。如果有问题你没搭建成功,欢迎你来找我。下一篇文章教你搭建 mysql 2主多从集群。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。