news 2026/10/21 19:03:42

彻底搞懂Netty:bossGroup与workerGroup的主从Reactor模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
彻底搞懂Netty:bossGroup与workerGroup的主从Reactor模型

你有机会问过自己这个问题吗?Netty的ServerBootstrap里,为什么要写两行group(),一个叫bossGroup,一个叫workerGroup?如果你只是照着示例代码抄,把两行换成同一个Group,是不是也能跑?我在刚接触Netty的时候就被这个问题卡过好几次,网上答案不少,但大多停留在“一个负责连接、一个负责读写”这种表面解释。直到我花了一个周末去追源码、做压测,才算真正搞明白这背后是一套主从Reactor的线程模型在支撑。这篇内容我想从设计思路讲到源码实现,再结合我实际压测踩过的坑,把bossGroup和workerGroup之间的区别和联系一次讲透。不管是刚入门想搞懂Netty,还是已经在项目里用Netty但遇到性能问题想定位瓶颈,这篇文章应该都能帮你把脉络理清。

1. 先从线程模型说起:Netty为什么要分两组线程

1.1 早期BIO模型的痛点:一连接一线程

在Netty之前,很多人写网络服务用的是BIO模式,也就是阻塞式IO。每次有客户端连上来,服务端就创建一个独立线程去处理这个连接,线程里循环读写数据。这种模式写起来简单,但问题非常致命:线程是稀有资源,一个连接占一个线程,1000个连接就得开1000个线程,5000个连接就得开5000个线程。线程一多,CPU光在切换线程上下文上就忙不过来了,而且大量线程都阻塞在read()上,真正干活的没几个。我印象特别深的是,早年用BIO写过一个简单的文件传输服务,压测到两三百并发的时候CPU就飙到90%以上,但其实大部分线程都在sleep,纯属浪费。

1.2 NIO与Reactor模型的基本思想

NIO引入了一个关键组件——Selector,也就是多路复用器。一个线程可以注册成千上万个Channel到同一个Selector上,然后通过一次select()调用,批量获取所有“就绪”的事件。这就把原来的“一连接一线程”压缩成了“少量线程处理海量连接”。但仅仅有Selector还不够,怎么组织这些线程去处理不同类型的事件,才是架构设计的核心。

Reactor模型应运而生。它的核心思想是:把“等待事件”和“处理事件”分离,用一个或多个Reactor线程不断轮询事件,再把事件分发给对应的Handler处理。在Netty里,这个模型被实现得特别纯粹,直接拆成了两组线程:一组专门负责接受连接(Acceptor),一组专门负责处理IO读写(IO Worker)。这种结构在业界被称为主从Reactor,boss是主Reactor,worker是从Reactor。

1.3 为什么必须拆开,而不是混在一起

最初我也觉得,既然都是EventLoop,都能轮询事件,那把accept和read混在一组线程里处理不是更省资源吗?但实际做一次压测就明白为什么不能混了。accept事件的特点是频率低但处理动作重,每来一个连接都要完成Channel的创建、配置、注册,还要把连接注册到某个worker线程上。而read/write事件的特点是频率极高,每次只做数据读写和业务回调。如果两者混在同一个线程池里,一旦某个连接的读写操作里有耗时业务,就会导致其他连接的accept事件得不到及时处理,新连接全部堆积在TCP backlog队列里,客户端连接超时,整体吞吐直接崩盘。把它们拆开之后,accept性能不会因为IO繁忙而受损,IO吞吐也不会因为accept抖动而打断,各自都能用最合理的线程数量去跑。

2. 认识bossGroup与workerGroup:具体分工与默认行为

2.1 官方定义的职责边界

先说职责边界。bossGroup做的事情,用一句话概括:只负责accept连接,然后把连接交接给workerGroup。具体到代码层面,bossGroup的EventLoop会绑定ServerSocketChannel,轮询这个Channel上的OP_ACCEPT事件。一旦有新连接到达,它会取出对应的SocketChannel,做参数配置,再通过childGroup.register(channel)把连接注册到某个workder EventLoop上,这一单就算干完了。

workerGroup做的事情更杂,也更重。连接从boss手里转接过来之后,后续所有的IO事件都由workerGroup负责。包括OP_READ、OP_WRITE、OP_CONNECT等。可以说,一条连接被accept之后,它的整个生命周期,从读数据到写数据再到连接断开,都在workerGroup的管辖范围内。如果你配置过ServerBootstrap,可能注意过有这么两个方法:handler()和childHandler()。前者设置的是ServerSocketChannel的Handler,跑在bossGroup上;后者设置的是每条SocketChannel的Handler,跑在workerGroup上。这个对应关系本身就说明了两个Group的分工边界。

2.2 默认线程数:一个容易被忽略的细节

大多数人在写代码时会显式指定线程数,比如new NioEventLoopGroup(1)和new NioEventLoopGroup(8)。但如果你不传,Netty有自己的默认值。看源码可以发现,MultithreadEventLoopGroup默认线程数是这么算的:

protected static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

也就是说,默认情况下每个Group都会创建“CPU核心数乘以2”个EventLoop。我一开始觉得这个数值有点谜,后来想明白了,NIO下的IO线程不会像BIO那样长时间阻塞等待,它能做大量非阻塞读写和事件分发,所以两倍于核心数的线程已经足够跑满CPU了。而且,Netty允许通过系统属性io.netty.eventLoopThreads覆盖这个默认值,这一点在调优时挺重要。

2.3 单参数group方法:复用同一组线程的隐晦行为

有一个场景容易把人绕晕:ServerBootstrap里只调用bootstrap.group(bossGroup),不传workerGroup。我刚学时也觉得奇怪,只给一个Group怎么能完成accept和read两件事?其实这是个很关键的设计,ServerBootstrap同时提供了两个重载的group方法。一个接收两个EventLoopGroup,另一个只接收一个。看单参版本的源码逻辑:

public ServerBootstrap group(EventLoopGroup group) { return group(group, group); }

所以,当你只传一个Group时,boss和worker会复用同一个EventLoopGroup。这意味着同一个线程既要处理accept事件,又要处理IO读写事件。对小规模的测试程序或者开发环境来说,这种写法能省资源,跑起来也没问题。但一旦并发连接量大,或者业务逻辑稍有耗时,accept和IO就产生竞争,容易出现连接建立慢、吞吐下降的问题。我自己的建议是,正式环境一定用双参版本,别偷懒。

3. 两者之间到底怎么协作:一条连接的全生命周期

3.1 连接接入的完整流转过程

要真正理解两者之间的联系,不能只记住“boss负责接入、worker负责读写”,你得把这段协作流程走一遍。我用最直白的方式拆解一下:

  1. 服务端启动时,bossGroup里有一个EventLoop线程绑定ServerSocketChannel,注册OP_ACCEPT事件,开始select轮询。
  2. 客户端发起TCP三次握手,内核协议栈完成握手后,把连接放入accept等待队列。
  3. boss线程的select()返回,检测到就绪的OP_ACCEPT事件,触发channelRead。
  4. 在ServerBootstrap的内部处理逻辑中,通过NioServerSocketChannel的pipeline触发一个acceptor动作,这个动作叫ServerBootstrapAcceptor。
  5. ServerBootstrapAcceptor会从boss thread的角度,把新SocketChannel做参数设置(比如TCP_NODELAY、SO_KEEPALIVE等),然后将它注册到childGroup的某一个EventLoop上。
  6. 这条连接从此归属这个workerEventLoop,后续的read、write、close事件都由这个线程处理。

这里面最值得关注的步骤是第5步,因为它是bossGroup和workerGroup产生联系的唯一桥梁。从线程角度看,连接在boss线程里被创建,然后在worker线程里被接管。中间涉及一次跨线程提交注册任务,Netty把这个任务通过next()方法选出的EventLoop的execute()提交到对端线程的任务队列里,完成了交接。

3.2 关键不变量:一个Channel只绑定一个EventLoop

Netty有一个非常重要的设计原则:一个Channel在注册之后,只会绑定到某一个固定的EventLoop上,并且在其整个生命周期内不会变更。这个原则贯穿全框架,boss里的ServerSocketChannel绑定的是boss线程,worker里的每条SocketChannel绑定的是某个worker线程。为什么要这么做?因为只有保证一个Channel的所有事件都在同一线程内处理,才能避免并发问题。所有handler方法都可以无锁执行,无需考虑多线程竞争。

这个设计在实际中带来的好处是巨大的,你可以在channelRead里直接改业务数据,而不用担心并发修改。但反向的影响也存在:如果某个Channel的处理逻辑特别耗时,会把绑定的这个EventLoop线程占住,导致其他绑定在同一线程上的Channel全部排队等待。这个问题我在后面“常见问题”部分还会展开讲。

3.3 连接到EventLoop绑定的选择方式

还有一个细节值得注意:EventLoopGroup在注册连接时是怎么从多个EventLoop里挑人的?看源码,它用的是round-robin轮询,也就是next()方法按顺序循环返回下一个EventLoop。这种方式的好处是公平,不会出现某个EventLoop负载特别高而另一个特别闲的情况。但这只是“相对”公平,因为new连接分配到哪个EventLoop,取决于注册时刻的轮询位置,并不会根据各EventLoop当前的真实负载动态调度。了解这一点对排查性能问题很有帮助:如果你观察到某个worker线程特别忙而其他线程很闲,不一定是Netty的问题,很可能是某些连接的流量天然比较大。

4. 实操配置:手写一个线程数量合理的Netty服务端

4.1 基础版完整代码

这部分直接给出一份能跑的代码,我习惯把它作为项目的启动骨架。一个标准的Netty服务端配置是这样的:

EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(8); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childOption(ChannelOption.TCP_NODELAY, true) .childOption(ChannelOption.SO_KEEPALIVE, true) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast(new StringDecoder()) .addLast(new StringEncoder()) .addLast(new ServerBusinessHandler()); } }); ChannelFuture future = bootstrap.bind(8080).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }

注意看几个关键点:bossGroup给了1个线程,workerGroup给了8个线程,这是我在多数业务场景下比较推荐的起手配置。boss只需要处理accept,1个线程足够,设多了反而浪费。worker的数量建议设置在CPU核心数的1到2倍之间,后续根据压测结果做调整。SO_BACKLOG设置的是TCP三次握手后内核中等待accept的队列长度,在瞬时高并发下如果设置过小,内核会直接丢弃新连接,客户端表现为连接建立失败或超时。

4.2 业务逻辑如何处理:别让IO线程背锅

很多人写完上面的代码就在ServerBusinessHandler里写耗时操作,比如读写数据库、调用远程接口、做复杂计算。我要特别提醒,这会直接把worker线程卡住。因为pipeline上的handler默认都运行在Netty的IO线程里,也就是绑定的那个workerEventLoop线程。一旦某个连接的业务处理耗时2秒,这个worker线程上其他被绑定的连接全部跟着延迟2秒。

正确的做法是用单独的EventExecutorGroup来处理耗时业务,让IO线程只负责收发和快速转发。改动很小:

EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(8); DefaultEventExecutorGroup businessGroup = new DefaultEventExecutorGroup(16); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast(new StringDecoder()) .addLast(new StringEncoder()) .addLast(businessGroup, new ServerBusinessHandler()); } });

这样一改,businessGroup会用自己的16个线程去执行ServerBusinessHandler的方法,IO线程从业务逻辑里解放出来。这个方法我在多个项目里用过,效果立竿见影,压测时P99延迟能降一个数量级。需要注意一点:一旦handler跑在businessGroup里,Channel上下文操作就涉及跨线程了,你如果要调用Channel.writeAndFlush,需要确保线程安全,Netty的writeAndFlush本身是线程安全的,但如果直接从业务线程里访问自定义的Channel属性,就要自己做同步处理。

4.3 优雅停机:shutdownGracefully不是摆设

上面代码的finally块里调用了shutdownGracefully(),这行代码容易被新人忽略。它的作用是优雅关闭线程池,让正在处理的任务和队列中的任务尽量处理完再退出,而不是粗暴地中断线程。如果在生产环境出现了接受到SIGTERM信号后瞬时大量连接断开的诡异问题,先检查这里。直接把ExecutorService.shutdownNow()的思路搬到Netty上,会杀掉正在执行IO事件处理的线程,造成数据丢失。

5. 源码视角:从构造到绑定,看两个Group如何被组织起来

5.1 NioEventLoopGroup构造时到底干了什么

理解源码是真正掌握两者区别的关键路径。当你new NioEventLoopGroup(8)时,构造链路大致是:NioEventLoopGroup构造函数调super,一层层到MultithreadEventLoopGroup,核心逻辑是创建一个长度为8的EventExecutor数组,每个元素都是一个NioEventLoop实例。每个NioEventLoop里又包含三样核心东西:

  • 一个Selector,负责事件轮询
  • 一个Thread,驱动这个selector循环跑起来
  • 一个MPSC队列(无界任务队列),接收跨线程提交的任务

线程并不是构造时就启动的,而是懒启动。第一次向EventLoop提交任务时,如果当前线程还未激活,会开启Thread并进入EventLoop的run()循环。run()里就是经典的select操作加上task队列的处理。

对bossGroup和workerGroup来说,除了构造参数和职责挂在不同的Channel上,它们本质上是完全同构的。区别不在内部实现,而是在ServerBootstrap的组装逻辑里。

5.2 ServerBootstrap的组装配接

看一下ServerBootstrap的核心方法group()和bind()。group()只是把两个组引用分别存在group和childGroup字段里。真正的关键步骤在bind()流程里:

  1. doBind()里先initAndRegister(),这会创建NioServerSocketChannel,并调用group.register(channel),注意这里注册用的是bossGroup。
  2. 于是ServerSocketChannel被绑定到bossGroup里的某个EventLoop上,boss线程开始select这个Channel上的accept事件。
  3. bind操作完成后,还有一步,在ServerSocketChannel的pipeline中添加一个内部Handler,叫ServerBootstrapAcceptor。这个Handler极其关键,它就坐在bossGroup里,负责在accept事件到达时执行前面说的交接逻辑。
  4. 你看ServerBootstrapAcceptor的channelRead方法,它是这样写的:
public void channelRead(ChannelHandlerContext ctx, Object msg) { final Channel child = (Channel) msg; child.pipeline().addLast(childHandler); childGroup.register(child).addListener(...); }

这里的一行childGroup.register(child),就是整个Netty里boss和worker协作的最终落点。它把新连接从boss线程的掌控中移交给workerGroup,workerGroup的next()选取一个EventLoop,把child注册进去,然后这个连接就正式进入worker的管辖范围。

5.3 childGroup.register的细节

这个register的实际执行过程也值得细看。childGroup.register(child)内部会找到next()返回的EventLoop,然后调用eventLoop.execute注册任务,因为EventLoop是单线程执行所有任务,所以这个注册操作会进入目标EventLoop的任务队列。也就是说,连接的真正注册动作发生在对应worker线程上,而不是在boss线程上完成的。这为后面所有IO事件的线程绑定打下了基础。一旦注册完成,worker的selector会把这条连接加入轮询集合,之后网络数据到达时,连接对应的OP_READ事件就能被worker线程捕获并触发pipeline上的业务Handler。

6. 常见问题与排查技巧实录

6.1 连接建立了,但服务端一直收不到数据

这个问题我见过很多次,尤其是初次接触Netty的开发者。表象是客户端connect()成功,但发送的数据服务端不打印。排查思路三步走:

  • 先确认childHandler是不是没配。如果没添加任何编码器或Handler,收到的数据会在pipeline尾部被自动释放,你当然看不到。
  • 检查pipeline里是否有人把消息消费掉但没有向后传递。比如自定义Handler里调用了msg.release(),或者处理完不调用ctx.fireChannelRead(),后续Handler就接不到数据。
  • 如果连接一直读不到任何事件,用jstack看线程栈,看对应worker线程是阻塞在哪。最怕的是业务Handler里调用了同步阻塞操作,比如阻塞队列take(),把worker线程卡死。

6.2 压测时连接数上不去,大量连接pending

这种现象通常有内因。在TCP层面,如果backlog队列满,内核会丢SYN包,客户端表现为connect超时。Netty的默认SO_BACKLOG是128,压测时根本不够。我将它调到1024或更高,同时设置好linux的somaxconn参数,问题基本解决。

另一种可能更隐蔽:bossGroup的线程数设为0。你没看错,有人为了“省资源”会这么干。NioEventLoopGroup(0)会被强制转成1,但如果使用其他实现方式导致线程数为0,accept事件没人处理,连接自然全部pending。我最终排查方式很简单,统计线程数和事件循环状态,看一眼就清楚。

6.3 线程数设得很大,性能反而下降了

一开始我犯过这个错误。认为线程越多处理越块,直接把workerGroup设为128。结果压测QPS不升反降。原因在于线程上下文切换和锁竞争。Netty的EventLoop是单线程串行处理任务,它内部需要维护selector的数据结构,跨线程提交任务时要加锁,线程数一多,这些同步开销就开始吞噬性能了。

经验值是worker线程不超过CPU核心数的2倍。在数据库越界查询这种短平快业务下,8核机器配16个worker线程非常合理。超过这个值,你压测观察到的变化曲线通常是先平缓,然后下降。

6.4 业务慢导致连锁拖垮

最典型的故障场景:一个耗时的channelRead业务把worker线程卡住,该线程上其他连接全部超时,而这些连接的超时重试又带来了更多业务请求涌向同一线程,最终整个workerGroup的线程逐步被“传爆”。这种事故我在生产环境处理过不止一次,解决思路非常直接:把耗时业务扔到自定义EventExecutorGroup里,或者干脆把业务逻辑做成异步,Handler写完数据后立即返回。

6.5 误用单组模式引发的全局阻塞

前面提到过bootstrap.group(bossGroup)会复用同一个组,如果同时没有设置异步业务线程,accept和IO读写在同一个线程池里竞争资源,遇到高并发场景,accept耗时和业务处理互相放大,压测图的表现是“开始正常,几分钟后延迟直线上升”。检查方法很简单,用jstack看线程栈,会发现同一个线程同时出现select操作和应用业务栈。解决办法就是拆成双组,把职责分开。

7. 我的一些性能调优心得与踩坑记录

7.1 别把调参当万能药,先想清瓶颈在哪

很多人拿到Netty第一件事就是调线程数,好像线程数越大越厉害,这是误区。我自己的调优惯例是:先用默认配置跑压测,然后把线程数从小到大一档一档调,同时监控CPU、内存、GC频率和QPS变化。如果线程数从8到16,QPS明显上升,说明瓶颈在线程数不足,继续加;如果线程数过了某个值,QPS反而下降,立即停手,说明已经进入上下文切换过度的区间。系统性能拐点是压测现场给出的,不是靠经验公式硬算出来的。

7.2 worker线程绑定业务模式的取舍

Netty的线程模型在极致并发场景下,最大的优势不是性能最顶尖,而是用户代码极简单——因为Channel和线程绑定的关系,Handler里基本不用考虑并发问题。但代价是“串行化”。如果业务是CPU密集型的,串行反而效率很高,因为数据都在缓存里。如果业务是加锁密集型的,比如访问一个全局ConcurrentHashMap并反复修改,那这个锁会变成全局瓶颈。我个人在这种情况下会考虑把业务完全异步化,让IO线程只管收发,业务线程单独去做状态协调。

7.3 保持安全的关闭顺序

项目停机时,我遇到过数据写一半的情况。当时是只关了bossGroup,没关workerGroup,导致一些半写入的连接突然中断。现在我的处理方式是:先关闭业务Executor,让它在有限时间内处理完队列里的任务;再关闭workerGroup,确保存量连接的事件处理完整收尾;最后关闭bossGroup,停止接受新连接。这样三步走,既能优雅停机,又不会丢数据。

7.4 明确你的连接需要什么样的EventLoop

写Netty服务端的时候,很多开发对bossGroup的线程数特别随意,随手写一个4。说实话,如果是高端口量的TCP长连接服务,512并发连接以内,boss设1就完全够用,因为它只做accept,不干别的。如果你的服务还承担了注册中心的健康检查、HTTP健康端点等额外职责,可以把boss设为2,但一般不建议超过4。worker通常设为CPU核心数乘以2,纯IO转发型服务可以适当多一点,但不要超过核心数的4倍,超过之后边际收益基本为零,甚至变负。

7.5 一次调优实例:从P99高延迟到压测平稳

最后说个具体的调优过程。我曾经负责过一个需要支撑大量长连接的消息推送服务,压测时发现P99达到250毫秒,P50只有8毫秒,明显有长尾效应。首先用jstack抓线程,发现好几个worker线程都阻塞在一个同步的数据库查询上。定位后做了三件事:数据库查询异步化,放入自定义Executor;业务Handler移除IO线程,挂到独立EventExecutorGroup;worker线程数从24下调到16。调整后P99降到35毫秒以下,P50基本不变。这次调优对我个人最大的启发是:Netty本身性能足够好,绝大多数“性能问题”其实出在把不该放到IO线程里的业务放了进去。先想清楚业务类型,再来谈线程配置,最后才考虑Netty本身的调参,顺序不能反。

以上这些内容,从线程模型、职责划分、协作流程到源码解读、问题排查和调优经验,基本覆盖了我这些年实践下来关于bossGroup和workerGroup的全部理解。你如果从零开始配一个Netty服务,直接参考第4节的代码骨架,遇到性能或稳定性问题,对比第6节的排查清单,我相信大部分坑都能避开。Netty的线程模型设计得并不复杂,复杂的是使用场景里的各种细节。多跑压测,多看线程栈,多注意跨线程的正确写法,这两组线程大概率不会再给你添麻烦。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 19:02:36

图即代码:用Mermaid、PlantUML和Graphviz构建可维护的工程图

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题第一次看到 “diagram-design” 这个标题&#xff0c;很多人会以为它只是教你怎么把图画得好看一点。但真正在工程、产品、咨询、教学这些场景里摸爬滚打过的人都知道&#xff0c;图的问题从来不是“好不…

作者头像 李华
网站建设 2026/10/21 18:58:59

问题合集整理全指南:从素材收集到索引架构的实操链路

接手过几分问题合集之后&#xff0c;我最大的感受是&#xff1a;整理这件事&#xff0c;做得好的和做得差的&#xff0c;差距不在“收了多少问题”&#xff0c;而在整理链路的设计。有人花一周把问题堆进一个文档&#xff0c;结果没人看&#xff1b;有人用零碎时间维护一份合集…

作者头像 李华
网站建设 2026/10/21 18:56:26

2026年大模型API价格对比与选型成本优化指南

1. 2026年3月的价格牌&#xff1a;先把今年的底价逻辑讲清楚 上个月我照例盘点了一轮主流大语言模型的API报价&#xff0c;结果发现2026年开年的价格体系&#xff0c;和两年前完全是两个物种。那时候大家讨论"一个Token多少钱"还算个新鲜词&#xff0c;现在连做外包的…

作者头像 李华
网站建设 2026/10/21 18:56:23

传统搜索引擎与智能搜索引擎比较研究:架构、关键技术及选型指南

简介&#xff1a;这份PDF文档围绕传统搜索引擎与智能搜索引擎展开比较研究&#xff0c;面向计算机、信息检索与人工智能方向的学习者和研究者&#xff0c;帮助厘清两类搜索引擎在结构、原理与关键技术上的差异与联系。资源包为单一PDF文件&#xff0c;大小约2.3MB&#xff0c;内…

作者头像 李华
网站建设 2026/10/21 18:55:09

diagram-design 实战:如何用一张图讲清楚复杂系统架构

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题 第一次看到“diagram-design”这个标题&#xff0c;很多人会以为它只是教你怎么画流程图、时序图或者架构图。但真正做过技术方案、写过设计文档、给团队做过架构评审的人都知道&#xff0c;画图这件事远…

作者头像 李华
网站建设 2026/10/21 18:54:36

SpringBoot+Vue+MySQL+MyBatis网上购物商城系统设计与实现全解析

经常有朋友问我&#xff0c;一个能上线、能跑通完整交易流程的网上购物商城系统&#xff0c;到底应该怎么设计和实现&#xff1f;框架教程网上多得是&#xff0c;但真到了把用户、商品、购物车、订单、支付这条链路串起来的时候&#xff0c;很多人就卡住了。最近我把一套基于Sp…

作者头像 李华