dream

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

0%

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

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

什么是领域驱动设计DDD

领域驱动设计(Domain-Driven Design,简称DDD)是由美国软件专家埃里克・埃文斯(Eric Evans)在2004年提出的软件设计方法论,旨在解决复杂软件系统开发过程中业务逻辑与技术实现之间的矛盾,提升软件系统的可维护性、可扩展性和灵活性。

说人话就是:

  • What: 它是一种设计思想、一种指导原则。
  • When: 设计微服务的时候,或者说,不知道怎么拆分微服务的时候。
  • Why:为什么要用它,上面其实说了,不知道怎么拆分微服务的时候,可以用它来指导你如何拆分微服务。
  • How:这个后面讲。

很多人都说,DDD是用来处理复杂业务逻辑的,那多复杂才算复杂业务呢?

这个问题,其实和微服务什么时候用是一个问题。

所有的技术都不是银弹。都有适合它的使用场景。

拿微服务来说,你一个小公司,就两三个开发,硬要上微服务,拆好几个服务出来,有什么意义吗?

是提升性能了?

是增加开发效率了?

都不是,你会发现拆分完以后,程序反而三高了。

  • 高复杂度:程序变得更加复杂了。
  • 高维护成本:程序的维护成本增加了、当有需求需要修改的时候、开发效率反而降低了。
  • 高运维成本:原来一台机器就满足了,你拆的服务多了,一台机器不够了。要么加机器性能要么加机器数量。

所以,适合很重要。

俗话说的好,见人说人话,见鬼说鬼话。技术也一样。

基本概念

  • 实体:使用充血模型实现的实体,既有属性、也有方法。
  • 值对象:只有属性的类。
  • 聚合根:一个特殊的实体,聚合的入口。
  • 聚合:聚合是一个概念、也可以理解成一个模块。聚合内包含了聚合根、实体、值对象。
  • 限界上下文:分割领域的边界、也是分割微服务的边界,通过这个边界明确这个接口属于哪个领域,也就是属于哪个微服务。每个领域有每个领域的上下文。
  • 领域:领域也就是我们的领域模型,也可以是一个微服务。
  • 子领域:一个领域可以分成多个子领域。这个就是粒度的问题了。
  • 领域事件:领域之间通信的方法。通过这个来调用其他的微服务。

还有一些核心领域、支撑领域、通用领域等,都是领域的一种,作用不同而已。

聚合

总结

我们主要介绍了领域的概念,包括如何划分,还有核心领域、通用领域以及支撑领域等。还介绍了一些DDD的基本概念。

领域的核心思想就是两点

  • 将我们的业务自顶向下的进行细分,逐步的拆解。
  • 划分出不同的领域,将我们有限的精力投入到最重要的事情当中。

文末福利

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

概念学习

概念学习

概念学习

概念学习

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

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

什么是领域驱动设计DDD

领域驱动设计(Domain-Driven Design,简称DDD)是由美国软件专家埃里克・埃文斯(Eric Evans)在2004年提出的软件设计方法论,旨在解决复杂软件系统开发过程中业务逻辑与技术实现之间的矛盾,提升软件系统的可维护性、可扩展性和灵活性。

说人话就是:

  • What: 它是一种设计思想、一种指导原则。
  • When: 设计微服务的时候,或者说,不知道怎么拆分微服务的时候。
  • Why:为什么要用它,上面其实说了,不知道怎么拆分微服务的时候,可以用它来指导你如何拆分微服务。
  • How:这个后面讲。

很多人都说,DDD是用来处理复杂业务逻辑的,那多复杂才算复杂业务呢?

这个问题,其实和微服务什么时候用是一个问题。

所有的技术都不是银弹。都有适合它的使用场景。

拿微服务来说,你一个小公司,就两三个开发,硬要上微服务,拆好几个服务出来,有什么意义吗?

是提升性能了?

是增加开发效率了?

都不是,你会发现拆分完以后,程序反而三高了。

  • 高复杂度:程序变得更加复杂了。
  • 高维护成本:程序的维护成本增加了、当有需求需要修改的时候、开发效率反而降低了。
  • 高运维成本:原来一台机器就满足了,你拆的服务多了,一台机器不够了。要么加机器性能要么加机器数量。

所以,适合很重要。

俗话说的好,见人说人话,见鬼说鬼话。技术也一样。

为什么大厂都开始使用DDD了?

回到我们的问题,为什么大厂都开始使用DDD了?

因为大厂人傻钱多

不是,有的人会说,因为大厂的业务足够复杂

说对了一半。

大厂通过DDD来指导微服务的拆分,解决了复杂的业务逻辑。

这里面有一些点,我们再细细的拆分一下。

为什么要拆分微服务?

再问大家一个问题,为什么要拆分微服务?

这个问题,千人千面。没有标准答案。

但是呢,总的有一些所谓的最佳实践

  1. 当公司成长了,流量增长了,单机很难支撑了。

比如,你是一个做电商的公司,你某天的成交量突然飙升。如何解决?最简单的做法,扩容机器。

如果你是单机系统,你扩容的机器其实相当于扩容了整个系统,但是,你只有某几个接口的流量很大而已,其他的接口白白浪费了机器的成本。

再比如。你家公司的流量不是突然飙升,而是每天都很大,但是呢,仅限于交易模块。和上面的问题是一样的。

  1. 当公司成长了,需求变多了。

很多人开发一个项目,代码写的很乱,大家每次合并代码都会出现一堆冲突

如果你拆分成微服务的话,天然的限制和约束就可以减少冲突,因为粒度变小了。

至于为什么拆分微服务,不再赘述了。

如何拆分微服务?

先给大家看两个例子吧。

拆分方案1

小李在一家电商公司A,A公司目前的代码架构如下:

ddd1-1

现在,A公司说,要开始拆分微服务了。小李负责拆分微服务。

小李一想,这个很简单啊,直接拆呗,一个模块一个服务就行了。交易量大只需要扩容交易服务,很完美啊。

所以,拆分完成以后,架构如下:

ddd1-2

一开始,小李觉得挺好,但是,逐渐发现问题了。

依赖严重,一个购买接口要跨域多个微服务。

既要从用户服务获取用户信息,又要从商品服务获取商品信息,还要从支付服务进行支付,还要从库存服务扣减库存,等等。。。

导致链路很长,接口的响应速度反而降低了。因为网络请求太多了。

写代码的时候又发现问题了,原来呢,只需要写逻辑就行了,现在还要写RPC接口。开发效率也降低了。

最后吧,代码写完了,又发现问题了,这事务怎么处理啊,只能上分布式事务了。开发成本又上去了。

结果就是,小李被领导一顿臭骂。

小李不语,只是默默承受着。。。

拆分方案2

小李觉得诸事不顺,跳槽去了另外一家电商公司B。

B公司也要拆分微服务了,领导见小李有过拆分微服务的经验,就将这个重要的任务交给了小李。

小李:。。。

小李无奈、只好继续重操旧业。

有了上次的失败经验,小李也学聪明了。

小李接下来复盘了上次的问题:

  1. 微服务粒度不对
  2. 接口链路太长导致速度下降
  3. 分布式事务等导致开发效率下降,且分布式事务也导致响应时间增加。

小李痛定思痛。要解决这几个问题。

终于,小李想到了好主意。

我不按照模块拆分不就完了!!!

我按照业务拆分。

比如,购买接口就放在交易服务里面。

那么他需要用户信息的时候自己取,不调用用户服务了,其他的逻辑也是。

这样确实解决了上面的一些问题,但是,购买还应该放在交易服务里面吗?

当需要增加一个接口的时候,我们如何判断它属于哪个服务呢?

小李又陷入了另外一个问题。

最后的结果,导致微服务里面代码很乱。

使用DDD拆分微服务

上面的两个案例,不知道大家遇到过没有呢?

还有很多其他的错误案例,大抵意思都差不多,就是不知道如何正确的拆分微服务。

DDD就是干这个活的。

1
DDD是指导我们如何正确拆分微服务的一种方法。
DDD的一些基本概念

DDD里面有很多的概念,很难一下子说清楚,因此,这里简单介绍一下。

  • 实体:使用充血模型实现的实体,既有属性、也有方法。
  • 值对象:只有属性的类。
  • 聚合根:一个特殊的实体,聚合的入口。
  • 聚合:聚合是一个概念、也可以理解成一个模块。聚合内包含了聚合根、实体、值对象。
  • 限界上下文:分割领域的边界、也是分割微服务的边界,通过这个边界明确这个接口属于哪个领域,也就是属于哪个微服务。每个领域有每个领域的上下文。
  • 领域:领域也就是我们的领域模型,也可以是一个微服务。
  • 子领域:一个领域可以分成多个子领域。这个就是粒度的问题了。
  • 领域事件:领域之间通信的方法。通过这个来调用其他的微服务。

还有一些核心领域、支撑领域、通用领域等,都是领域的一种,作用不同而已。

一个领域里面包含了多个子领域,如图所示。

ddd1-3

一个子领域里面包含了多个聚合,每个聚合里又有一个聚合根作为入口,还有若干个实体值对象

ddd1-4

通过DDD我们可以来拆分微服务。

DDD是围绕业务概念来进行领域建模的,后续的接口也是根据业务概念来划分到对应的领域中。从而解决开发过程中,业务演进的问题。

因为当业务改变了,那么领域就改变了,对应的代码也就跟着变就好了。

所以DDD和微服务不一样,不是一种具体的架构,只是一种指导方法。

它可以划分出清晰的业务边界也就是微服务的边界,从而让微服务的拆分更加符合业务。而不是乱拆分。

他也有一些步骤

  • 战略设计:战略设计从业务的视角上看待问题,建立领域边界、划分领域、子领域等等。
  • 战术设计:战术设计从技术的视角上看待问题,将领域转化成微服务,将业务实体转化成代码实体等等。
  • 事件风暴:通过事件风暴,大家一起来想业务,并将业务转化成领域、子领域、领域事件、实体等等。
电子商务DDD示例

首先,需要进行事件风暴,梳理出一些和业务逻辑有关的词汇

以电商举例,用户购买者订单商品库存收货地址商家价格购买行为下单支付取消订单退款发货等等。

接下来进行一些识别

识别什么呢?识别上面的概念,也就是哪些是实体值对象领域事件

说的再简单一些,名词就是实体或者值对象。如果只有属性值的就是值对象。动词就是领域事件。

比如上面的这些里面,哪些是领域事件呢?

  • 下单
  • 支付
  • 取消订单
  • 购买
  • 退款
  • 发货

哪些是实体呢?

  • 用户
  • 购买者
  • 商家
  • 商品

哪些是值对象呢?

  • 价格:价格仅仅是一个属性,价格变化的时候就是整个值对象进行变化。
  • 收货地址:收货地址也仅仅是一个属性或者一些属性,没有方法。
  • 库存:库存也同样。

有些人看到这里就会有疑问了?用户和购买者不是一个东西吗?

这里就需要提到另外一个概念了。限界上下文

这个在上面简单介绍过。这里再说一下。

这个概念有两个意思

  • 边界:用来划分领域的边界,也就是微服务的边界。
  • 上下文:在划分的边界之内,有着上下文。同样的一个东西,在不同领域里面,也就是在不同的上下文环境中,意思是不一样的。

比如笨蛋这个词吧。

在小情侣你侬我侬之间说,哎呀,你个笨蛋。就是打情骂俏的意思。当然不是说你真的笨了。

那换一个环境呢,你在学习的时候,老师跟你说:你真是个笨蛋!。这里就是真的在说你笨了。

所以呢,不同的环境,不同的上下文里面,一个词语的意思是不一样的。

这里也是,都是用户,但是在浏览商品的时候他只是用户。但是在购买的时候,他就是购买者了。

这样,他就在两个领域里面存在了。在用户领域里面是用户实体,在交易领域里面是购买者实体。

接下来可以进行聚合操作,也就是将意思相近、内容相近的放到一起,放到一个聚合里面。再根据聚合的内容划分出领域、子领域等。

比如

  • 用户聚合:里面就包含了用户实体。
  • 商品聚合:里面包含了商品实体、价格值对象、库存值对象。
  • 订单聚合:里面包含了订单实体、购买者实体、收货地址值对象。

。。。

每个聚合里面还要有一个聚合根作为聚合的入口。

接下来将聚合划分到领域中就可以了。

比如

  • 用户领域:包含了用户聚合
  • 交易领域:包含了商品聚合和订单聚合。也可以划分成两个子领域,一个子领域包含一个聚合。

。。。

还有一些领域事件。作为通信。

比如

  • 下单领域事件:用户聚合发起事件 -》 订单聚合接受事件。完成下单操作。

接下来我们需要把上面梳理出来的内容映射到代码上。

比如,我们将上面的内容放到代码里面。这里面有两种方式,聚合是DDD中的最小单元,所以可以把一个聚合部署为一个微服务。

当然了,也可以一个领域作为一个微服务,具体的情况,根据自身业务和流量这些具体考虑就可以了。

我们这里以领域做为微服务来示例:

  • 用户微服务
  • 交易微服务

先看用户微服务的文件夹吧,因为分层放在了下面介绍,所以这里的代码我们仅展示领域层的代码。

  • userAgg: userAgg文件夹,代表了用户聚合。
    • core: 聚合的核心代码,包含了聚合根、实体、值对象。一个文件夹。
      • userAggRoot.java: 用户聚合根实体,是一个java文件,一个class类。
    • event: 聚合的一些领域事件代码。
      • buyEvent.java: 下单的领域事件。

这个实体采用的是充血模型

简单看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class userAggRoot {
private Integer userId; //userId是这个实体中的一个值对象。
private String name; //name也是一个值对象。

public UserAggRoot getUserInfo(Integer userId) {
// 获取用户实体的信息
}

public void updateUserName(String name) {
// 更新用户名称
}

//getter和setter....
}

领域事件的代码,其实简单来说,因为跨越微服务了,所以可以直接通过消息队列来进行事件通信。

1
2
3
4
5
6
public class buyEvent {

public void buy() {
// 组装事件信息并且发送事件
}
}

再看一下交易微服务:

  • shopAgg: 代表了商品聚合,是一个文件夹
    • core:
      • shopAggRoot.java: 商品聚合根实体。
      • priceVO.java: 价格值对象的类,一个class。VO代表的是Value Object的意思。
      • inventoryVO.java: 库存值对象的类,一个class。
  • orderAgg: 代表了订单聚合,是一个文件夹
    • core:
      • orderAggRoot.java: 订单聚合根实体。
      • buyerEntity.java: 购买者实体,一个java的class。
      • addressVO.java: 收货地址值对象。
    • event:
      • orderEvent: 订单事件,发送订单事件可以给物流微服务,进行物流发货。还有积分服务进行积分处理等等。。。

代码类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class shopAggRoot {
private Integer shopId; // 商品id值对象
private String shopName; //商品名称值对象
private PriceVO price; //价格值对象
private InventoryVO inventory; //库存值对象

public void subInventory() {
// 扣减库存
// 1. 创建新的库存值对象

// 2. 替换inventory属性
// 。。。。
}
}

看一些值对象:

1
2
3
4
5
6
7
8
public class priceVO{
private BigDecimal originalPrice; //原价格
private BigDecimal currentPrice; //当前价格,可能是优惠后的价格等等
}

public class inventoryVO{
private Long inventory; //库存
}
DDD的分层架构

DDD的分层和传统的MVC分层不太一样。

传统的后端服务一般是这样的层次

  • controller:入口层
  • service:业务逻辑层
  • dao:数据层

DDD则是分成了下面四层

  • 用户接口层:也就是传统的入口层
  • 应用层:应用层调用领域层的聚合来完成操作,进行服务编排。可以调用多个聚合来共同完成操作。但是应用层和传统的业务逻辑层不同,它不负责完成业务逻辑,仅仅是服务编排,具体的业务逻辑由领域层实现。
  • 领域层:领域层是核心,包含了聚合、实体、值对象和聚合根。每个实体都是充血模型,包含了逻辑操作。
  • 基础层:基础层提供了基础服务,比如缓存、队列、数据库等等。

总结

我们简单的走了一遍DDD进行领域拆分。也让大家明白了为什么大厂会采用DDD。

因为大厂的微服务架构很完善了,他们的微服务复杂,使用DDD来指导微服务的拆分是一种有效的方法。

可以让代码和业务契合,当业务改变的时候代码随之改变。

文末福利

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

概念学习

概念学习

概念学习

概念学习

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

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

七招教你玩转DeepSeek,我求求你别再花钱买课了!

最近这段时间DeepSeek越来越火,很多人都开始使用了,以至于DeepSeek频繁出现“服务器繁忙,请稍后再试”问题。

其实,随着前几年OpenAI发布了ChatGPT开始,这种生成式AI大模型就开始爆火。

到今年DeepSeek爆火,又一次点燃了大家的热情。

但是。。。。。。

很多人还是不知道该怎么和AI聊天,毕竟AI不是真人,大家都知道AI能力强大,可是如何用好它呢?

很多人在使用过程中都发现AI给的回答不理想,和自己想要的相差甚远。

这是AI的问题?还是使用者的问题呢?

这就是信息差,有些人已经用AI年入百万了,有些人还觉得AI能力太弱,回答的问题乱七八糟

网上还有很多人靠着信息差挣钱割韭菜。

很多付费教程多如牛毛,还是有很多人乐此不疲。

今天,大头免费教你们如何使用AI,解放他们真正的能力,不要再去花钱买课了,不要再当韭菜了,那些钱你买点好吃的好玩的给自己它不香吗?

为什么大家用不好AI?

大家在使用过程中,可以问问自己,为什么同样都是AI,别人用的AI跟爱迪生似的,自己用起来就跟人工智障一样?

有的人可能知道一点,就是Prompt,中文就是AI提示词。也就是你需要问的准确,AI才能给你想要的回答。

来看一个例子:

背景:你要和对象出去旅游,出发地是北京,目的地是成都。两个人打算玩五一5天。
需求:你想要AI给你出一个旅游规划。

大家通常会这么询问AI。

给我写一个去成都的旅游攻略

来看一下AI的回答。

deepseek3-2

乍一看,这个推荐也像这么回事,但是你仔细看一看,会发现AI回答的很宽泛

  • 给出了时间建议
  • 给出了景点建议
  • 给出了交通住宿建议
  • 给出了3天的旅游规划

可是再看我们的需求,明显不符合我们的需求啊。

问题出在哪呢?出在我们给AI的问题上面!

我们问的问题不明确,至少在AI的理解里面,我们的问题不明确!

第一招:问题要具体

第一招:问题要具体

你不能只告诉他给一个成都的旅游攻略,这就像你问别人一样,他也只能给你一个宽泛的回答,一些简单的推荐而已。

✅ 正确示范:我要和我对象出去旅游,从北京到成都,给我规划一个五一的旅游攻略。

这里交代清楚了时间、地点、人物要素。

deepseek3-1

看看AI的回答。跟上面比明显更好了!

想要更细致的回答,还有其他的招式。可以继续优化。

第二招:设定角色,让AI知道自己是谁

第二招:设定角色,让AI知道自己是谁

解释一下,你可以给AI设定一个角色,让他代入进去,那么他就会更擅长做某事。

大家都知道,JAVA开发更擅长JAVA、川菜大厨更擅长做川菜、成都本地导游更擅长做成都的旅游规划。

数学专家更擅长数学题、物理专家更擅长物理题。当你赋予AI一个角色以后,AI就会摇身一变,变成数学专家、物理专家、川菜大厨、成都本地导游等。

这样的话,他就能更好的回答你的问题了!

❌ 错误示范:帮我优化这个代码…

✅ 正确示范:假设你是一个有十多年经验的JAVA专家,你会如何优化这个代码…

❌ 错误示范:给我写一个去成都的旅游攻略

✅ 正确示范:假设你是一个有二十多年导游经验的成都本地资深导游。我要和我对象出去旅游,从北京到成都,请给我规划一个五一的旅游攻略。

AI给出的回答如下,可以看到,给出来的回答更加好了。不仅有具体的时间安排,还有对应的推荐菜以及推荐玩法。还有本地人的放坑指南等等。

deepseek3-3

这份攻略明显更好了!

第三招:细化问题,逐步求精

有的时候,可能我们希望得到更加细化的东西,比如说上面的旅游攻略,我们还想知道住宿安排,交通推荐。

再比如交通上我们希望比较火车、高铁、飞机的价格和时间等等。

为什么不一次性给AI呢?

因为AI无法理解太过复杂的问题,他毕竟不是人类。但是你一步步跟他说,他就能明白了。每次简单一些。

这就像大象装进冰箱有几步

  1. 打开冰箱
  2. 放大象
  3. 关冰箱

我们也可以继续追问

  • 请给出详细的交通方案,首先给出火车的
  • 接下来给出高铁的
  • 接下来给出飞机的

❌ 错误示范:帮我规划一个五一的旅游攻略,从北京到成都,要求给出详细的交通方案、住宿方案、游玩方案。包括时间、价格、性价比等细节。

✅ 正确示范:帮我规划一个五一的旅游攻略,从北京到成都,第一步,给出交通方案的时间、价格信息。第二步,给出住宿的推荐、价格、和景点的距离。第三步,给出景点的游玩信息、高峰期、路线、门票。

deepseek3-3

第四招:结合上下文,深入交流

上面的结果AI确实按照我们的要求来了,但是结果并不是太好。

我们一次性给出三步,对于AI来说,还是有点理解困难

所以我们可以让AI结合上下文。来一步步问。

什么是上下文

就是在一次AI对话里面的所有内容。当我们点击开启新对话。就开启了一个新的上下文。这个对话里面的所有信息,都是上下文,AI可以根据上下文来回答我们的问题。

✅ 正确示范:假设你是一个有二十多年导游经验的成都本地资深导游。我要和我对象出去旅游,从北京到成都,请给我规划一个五一的旅游攻略。

接下来,我们再次输入:

  • 请帮我细化从北京到成都的交通方案,给出交通方案的时间、价格信息等
  • 请帮我细化成都的住宿推荐,给出住宿的推荐、价格、和景点的距离等
  • 根据这些信息帮我细化景点的游玩信息、高峰期、路线、门票等

deepseek3-3

可以看到同样的问题,AI的回答明显别上次的更加好了。还给出了一些方案对比。很适合我们选择。

第五招:提供参考方案

就像写论文的时候,大家都需要参考文献一样,如果你想让AI给你输出一些很好的内容,也可以给AI一些参考文献。

大家都知道,AI有一个联网搜索功能。这其实就是AI会去网上搜索一些文章做为参考文献

我们也可以自主给他一些参考文献。

❌ 错误示范:帮我写一遍古诗词

✅ 正确示范:假如你是一个诗人,请帮我写一遍类似《将进酒》的古诗词。

第六招:多轮对话引导

之前已经经过上下文了。那么基于上下文,我们就可以进行多轮提问。

有很多人使用AI的时候,问了一次得不到正确答案,就不再继续提问了。

❌ 错误示范:帮我写一个牙齿的广告词。

✅ 正确示范:帮我写一个牙齿的广告词。

接下来的回答不满意,我们可以继续让AI修改回答。

✅ 第二轮问题:要求体现出我们的技术,口腔技术一流。

接下来的回答可能还是不满意,我们继续引导AI修改。

✅ 第二轮问题:修改广告词,控制在10个字以内。

就这样一轮一轮的引导。通过不断的引导修正。最终可以得到我们想要的答案。

第七招:多个方案。不同角度对比选择

除了上面的方法以外,还可以让AI从不同的角度给出多个方案,我们自己从中选出一个适合的方案。

当然了,如果多个方案都不满意,我们还可以通过多轮对话的方式,引导AI再次给出多个方案。

❌ 错误示范:帮我写一个牙齿的广告词。

✅ 正确示范:从不同的角度,给出5个牙齿的广告词。

如果对5个不满意,可以继续多轮对话。

…….

一些万能模板推荐

👉 设定角色:如资深导游、JAVA专家等。
👉 设定字数要求:如广告词给出10个字以内的、爆款标题给出20个字以内的。论文开题报告1000字以内。
👉 设定不同角度
👉 设定不想要的回答
👉 给出参考文献
👉 给定格式、语气、输入输出等。

总结

通过这些技巧,可以让AI更好的为我们服务,你会发现AI变成了生活中的小助手,而不再是人工智障

工具永远都只是工具。如何用好工具才是我们应该做的。

没有完美的工具,只有完美使用工具的人类。

通过AI提效,解放双手。有更多的时间学习、玩耍、祝大家都升职加薪!!!

文末福利

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

概念学习

概念学习

概念学习

概念学习

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

还在手动管理资源?还在经常陷入OOM?一招教你快速解决

你是否还在为了手动管理资源而发愁?打开文件以后还要记得关闭文件,这些资源全部要手动管理,一旦忘了就有可能OOM!

一招教你使用try-with-resources来自动管理资源,不需要再手动关闭资源了,再也不怕忘记关闭资源了。

try-with-resources

在Java开发中,资源管理一直是一个重要的问题。

无论是文件操作、数据库连接还是网络通信,都需要确保资源在使用后能够正确关闭,以避免资源泄漏和潜在的错误。

try-with-resources语句的引入,为Java开发者提供了一种简洁而优雅的资源管理方式,大大简化了代码的复杂性,同时也提高了代码的可读性和安全性。

传统资源管理的痛点

首先,我们来看看try-with-resources解决了什么问题。

  1. 冗长的代码:传统资源管理的方式,通常需要在finally块里面手动关闭资源。但是这样既麻烦,又导致代码很多,而这些代码增加以后,比如多个资源关闭,嵌套关闭等,会导致可读性变差,代码难以管理。
  2. 容易出错:当代码复杂、可读性差、难以管理以后,就很容易出错了。还有如果在关闭资源的时候抛出异常,那么资源可能就无法关闭了,从而导致资源泄漏问题。
  3. 异常处理复杂:通常在finally中关闭资源的时候,还要在套一层try...catch,导致异常处理变多、复杂等问题

冗长的代码

try-with-resources出现之前,Java开发者通常使用try-finally语句来确保资源被正确关闭。例如,当操作文件时,代码通常如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("example.txt");
// 读取文件内容
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

这种写法虽然能够确保资源被关闭,但代码显得冗长且复杂。特别是当需要管理多个资源时,finally块中的关闭逻辑会变得更加繁琐。

容易出错

手动管理资源时,开发者需要在finally块中显式调用资源的close()方法。如果忘记关闭资源,或者在关闭资源时抛出异常而未正确处理,就可能导致资源泄漏或其他问题。例如:

1
2
3
FileInputStream inputStream = new FileInputStream("example.txt");
// 使用inputStream读取文件内容
inputStream.close(); // 如果这里抛出异常,资源可能无法正确关闭

异常处理复杂

finally块中关闭资源时,如果资源的close()方法抛出异常,而try块中也抛出了异常,处理这些异常会变得非常复杂。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("example.txt");
// 读取文件内容,可能抛出异常
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

如果try块和finally块都抛出异常,处理这些异常会变得非常复杂,容易导致错误。

什么是try-with-resources?

try-with-resourcesJava 7引入的一种语法结构,用于自动管理资源

它允许在try语句中声明一个或多个资源,这些资源必须实现了AutoCloseable接口或其子接口Closeable。

当try语句块执行完毕后,无论是否发生异常,try-with-resources都会自动调用资源的close()方法来关闭资源。

核心特性

  • 自动关闭资源:try-with-resources会自动调用资源的close()方法,确保资源被正确关闭。
  • 简化代码:无需手动在finally块中关闭资源,减少了代码冗余。
  • 支持多个资源:可以同时管理多个资源,每个资源之间用分号隔开。
  • 异常处理:如果资源的close()方法抛出异常,而try语句块中也抛出了异常,try语句块中抛出的异常会被传播,而资源关闭时抛出的异常会被抑制(suppressed)。

为什么使用try-with-resources

上面已经介绍了传统资源管理的痛点问题,所以使用try-with-resources主要就是解决这些痛点。

当然了,除了能解决这些问题以外,还有一些其他的好处。

毕竟,Java语言使用了这么长时间,能使它自身提供的一些特性,都是很有用的。

  • 解决传统资源管理的痛点:解决上述的痛点问题。
  • 提高代码的可读性和安全性:try-with-resources通过自动管理资源,简化了代码结构,提高了代码的可读性和安全性。它确保了资源在使用后能够正确关闭,减少了因资源泄漏导致的潜在问题。
  • 支持自定义资源类:除了Java标准库中提供的资源类(如FileInputStream、FileOutputStream等),我们还可以自定义实现AutoCloseable接口的资源类,以便在try-with-resources中使用。

何时使用try-with-resources

使用场景

上面已经介绍了很多了,接下来看一下,有哪些使用场景。

  • 文件操作:读取或写入文件时,使用FileInputStream、FileOutputStream等类。
  • 数据库操作:使用Connection、Statement、ResultSet等资源时。
  • 网络通信:使用Socket、ServerSocket等资源时。
  • 自定义资源:任何实现了AutoCloseable接口的资源类。

注意事项

没有任何方法是一种银弹。技术方案没有黑魔法!所以在使用的时候也需要注意一些事项。

  • 确保资源实现AutoCloseable接口:只有实现了AutoCloseable接口或Closeable接口的资源类才能在try-with-resources中使用。
  • 异常处理:虽然try-with-resources会自动关闭资源,但仍然需要处理可能抛出的异常。
  • 性能考虑:虽然try-with-resources简化了代码,但调用close()方法可能会增加一定的开销。在资源较多时,需要注意性能影响。

如何使用try-with-resources

经过上面的介绍,相信你已经对try-with-resources很了解了,接下来看一下到底该如何实战吧!

基本语法

try-with-resources语句允许在try块中声明一个或多个资源,这些资源必须实现了AutoCloseable接口或其子接口Closeable。当try块执行完毕后,无论是否发生异常,try-with-resources都会自动调用资源的close()方法来关闭资源。

1
2
3
4
5
try (资源声明) {
// 使用资源
} catch (异常类型 异常变量) {
// 处理异常
}

示例代码

以下是一个使用try-with-resources读取文件内容的示例:

可以看到,我们把资源的获取放在了try后面的小括号里面,而不是以前一样放在了大括号里面。

这样的话,就不需要在finally里面关闭资源了,因为会自动关闭。

这也是因为FileInputStream实现了Closeable接口,因此可以作为资源在try-with-resources中声明。

1
2
3
4
5
try (FileInputStream inputStream = new FileInputStream("example.txt")) {
// 使用inputStream读取文件内容
} catch (IOException e) {
e.printStackTrace();
}

管理多个资源

try-with-resources不仅可以管理单个资源,还可以同时管理多个资源。例如,当需要同时读取和写入文件时,可以这样写:

在这个例子中,inputStream和outputStream都被声明为资源,它们的close()方法都会在try语句块执行完毕后自动调用。

1
2
3
4
5
6
try (FileInputStream inputStream = new FileInputStream("input.txt");
FileOutputStream outputStream = new FileOutputStream("output.txt")) {
// 使用inputStream读取文件内容并写入outputStream
} catch (IOException e) {
e.printStackTrace();
}

自定义资源类

除了Java标准库中提供的资源类(如FileInputStream、FileOutputStream等),我们还可以自定义实现AutoCloseable接口的资源类,以便在try-with-resources中使用。例如:

只要我们自己的资源类,实现了AutoCloseable接口,就可以了。很简单吧!

1
2
3
4
5
6
public class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("资源已关闭");
}
}

接下来就可以和上面一样的使用方法来使用我们自定义的资源类了!

1
2
3
4
5
try (MyResource myResource = new MyResource()) {
// 使用myResource
} catch (Exception e) {
e.printStackTrace();
}

总结

try-with-resources是Java 7引入的一种非常有用的语法结构,

它能够自动管理资源,简化了资源关闭的代码,提高了代码的可读性和安全性。

通过实现AutoCloseable接口或Closeable接口,我们可以自定义资源类,并在try-with-resources中使用它们。

在实际开发中,合理使用try-with-resources可以有效避免资源泄漏,提高代码质量。

文末福利

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

概念学习

概念学习

概念学习

概念学习

DeepSeek R1本地部署

DeepSeek大火,但天下苦服务器繁忙,请稍后再试久矣.

近期,DeepSeek大模型大火,一举超越ChatGPT登顶下载榜首.

DeepSeek从很少人知道一下子变成了人尽皆知的大厂,招聘薪资更是开出了年薪百万的价格,应届生都可以去.可谓是梦中情厂.

但是,就连DeepSeek自己可能都没想到自己这么火.因此招架不住大家的热情,频繁的出现服务器繁忙,请稍后再试.

使用体验实在糟糕.

好在,DeepSeek开源了自己的大模型,我们可以将DeepSeek部署到本地进行使用,这样的话就可以不再担心服务器繁忙了.可以尽情的蹂躏DeepSeek了!!!

关于DeepSeek的技术有兴趣的可以看看他们的论文.
DeepSeekR1论文

  • DeepSeek-R1 遵循 MIT License,允许用户通过蒸馏技术借助 R1 训练其他模型。
  • DeepSeek-R1 上线 API,对用户开放思维链输出,通过设置 model=’deepseek-reasoner’ 即可调用。
  • DeepSeek 官网与 App 即日起同步更新上线。

DeepSeek-R1 在后训练阶段大规模使用了强化学习技术,在仅有极少标注数据的情况下,极大提升了模型推理能力。在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版。

环境准备

  1. 硬件要求

    • 需要至少一个 CPU 核心(推荐使用多核处理器)。
    • 内存建议至少 4GB,具体内存可以根据实际需求调整。
    • 磁盘空间建议至少 20GB 可用空间。
  2. 操作系统

    • Windows、Linux 或 macOS 均可支持。

蒸馏模型

DeepSeek本地部署的基本是蒸馏模型,简单理解为阉割版.

为什么?因为本地无法支持真正大模型的算力.

蒸馏模型虽然无法和完整版一样,但是胜在我们可以本地部署,自己玩.还避免了服务器繁忙的苦恼.毕竟,东西再好,你用不了也是白搭啊.

DeepSeek 在开源 DeepSeek-R1-ZeroDeepSeek-R1 两个 660B 模型的同时,通过 DeepSeek-R1 的输出,蒸馏了 6个小模型开源给社区,其中 32B 和 70B 模型在多项能力上实现了对标 OpenAI o1-mini 的效果。

deepseek2-1

满血版DeepSeek 671B的要求:

  • 显存需求:完整版(未量化)的显存需求极高,BF16精度下需 1342GB显存,即使使用FP16精度也需约 350GB显存
  • 硬件配置:需多节点分布式计算,例如8张NVIDIA A100/H100(每卡80GB显存)并行运行,或更高端的超算集群
  • 性能限制:单卡无法支持,即使最新RTX 5090(32GB显存)也无法有效运行,推理速度极低(低于每秒10个token)

deepseek2-1

看一下蒸馏版的要求和推荐配置.

版本名称 参数数量 显存需求 (FP16) 内存需求 推荐Mac配置 推荐Windows配置
DeepSeek-R1-1.5B 1.5B ~3GB 8GB+ M1/M2芯片,8GB统一内存[^2^] GTX 1650/RTX 2060,4GB+显存[^5^]
DeepSeek-R1-7B 7B ~14GB 16GB+ M1 Pro/M2 Pro,16GB统一内存[^2^] RTX 3060/4070 Ti,12GB显存[^2^]
DeepSeek-R1-14B 14B ~28GB 32GB+ M1 Max/M2 Max,32GB统一内存[^2^] RTX 4090/A100 40G,24GB+显存[^2^]
DeepSeek-R1-32B 32B ~64GB 64GB+ M1 Ultra/M2 Ultra,64GB统一内存[^2^] 2x RTX 4090/A100 80G,48GB+显存[^2^]
DeepSeek-R1-70B 70B ~140GB 128GB+ 需要更高配置的Mac Pro[^6^] 4x RTX 4090/A100 80G[^6^]

deepseek2-1

安装

介绍完以后,开始安装吧.

安装相对来说比较简单,可以使用Ollama这个东西.

直接在Ollama官网下载就可以了.

在如下界面,直接点击DownLoad就可以了.

deepseek2-1

接下来选择版本,Mac、Linux或者Windows.

deepseek2-1

等待下载完成以后,运行Ollama.这个东西可以理解为大模型的运行环境.

deepseek2-1

接下来点击Ollama官网左上角的Models可以准备大模型了.

deepseek2-1

选择我们要部署的DeepSeek R1大模型.

接下里可以选择要部署的版本.版本信息上面已经介绍过了.

deepseek2-8

选择以后右侧的命令会改变,直接复制右侧的命令即可.比如我选择7b版本,右侧的命令就是ollama run deepseek-r1:7b.

deepseek2-9

直接在命令行中输入这个命令就可以了.Windows使用CMD或者终端都可以.Mac使用终端或者Iterm2都可以.运行以后会先开始下载大模型.

在windows下,可以在下面的搜索里面输入CMD打开终端程序,或者按住键盘Win + R两个键,然后在里面输入CMD打开终端程序.

deepseek2-9

下载完成以后进入大模型的输入界面,是命令行格式的,可以直接输入.

比如我输入你是谁.回答中的标签里面对应的是思考的内容.

deepseek2-9

到这里其实就安装完成了,不过如果需要UI界面在浏览器中使用的话,也可以再安装一个UI界面.

后续如果还想运行的话依然是两个步骤

  1. 打开Ollama程序
  2. 运行命令ollama run deepseek-r1:7b, 注意替换成你自己的命令.

可视化界面安装

有很多人可能使用不惯命令行程序来输入,因此可以安装一个可视化界面,这样就和在DeepSeek官网使用一样了.

接下来我们看看如何安装UI界面吧!

可视化界面使用Open-Web UI提供的界面程序.可以先下载Docker,使用Docker运行Open-Web UI.

安装Docker

进入Docker官网,进行下载.选择要下载的版本.

deepseek2-9

下载以后进行安装,安装完成以后,打开程序,使用推荐配置就可以了.

deepseek2-9

安装Open Web-UI

接下来就可以安装Open Web-UI了.

使用如下命令即可.和之前一样,在CMD或者终端程序中运行如下命令.

1
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

接下来等待下载完成就可以了.

deepseek2-9

现在打开浏览器,输入http://localhost:3000/即可进入本地部署的可视化界面了!

第一次的话需要创建管理员账号,输入名称、邮箱、密码就可以了.

进来以后如图所示.

deepseek2-9

使用体验如下

deepseek2-9

总结

这样就算安装完成了,可以使用命令行或者界面,看个人喜好了!

通过以上步骤,您可以在本地部署 DeepSeek。请根据实际需求调整配置并确保所有依赖项已正确安装。如果在运行过程中遇到问题,请参考官方文档或联系技术支持团队。

希望这份指南对您有所帮助!

文末福利

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

概念学习

概念学习

概念学习

概念学习

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

“凌晨1点,你躺在床上第103次对自己说‘再看最后一个视频就睡’。手指机械地上滑,屏幕突然跳出一只圆滚滚的橘猫——和你上周走丢的那只简直一模一样。你瞬间清醒,长按屏幕点了收藏,下一秒,系统又推来三个宠物视频:一只撒娇的布偶、一只拆家的二哈,甚至还有宠物殡葬的科普……

你后背一凉:抖音怎么比男朋友还懂我?!”

「用户画像」是什么?抖音如何用代码“算”出你的喜好?揭秘背后的技术逻辑!

代码正在“偷窥”你的生活

“上瘾”背后的数据陷阱:

你刷到的每一个“刚刚好”的视频——深夜放毒的美食、周末必推的露营攻略、甚至分手后突然涌现的伤感BGM——都不是偶然。

真实案例:

1
2
程序员小李的“社死现场”:
“上周开会摸鱼搜了‘痔疮药’,当晚抖音首页全是肛肠医院广告……同事围观我手机时,我恨不得当场卸载APP!”

用户行为显微镜:

1
你的每一次点击、停留、点赞、划走,甚至视频看到一半突然锁屏——这些动作在后台早已被拆解成无数个“0”和“1”,拼凑出连你自己都未曾察觉的“兴趣密码”。

程序员如何用代码“造”出一个你?

“你以为你在刷视频?其实是抖音在用用户画像和推荐算法反向‘刷’你!”

  • 行为追踪
    • 你滑动屏幕的0.5秒里,后台如何通过埋点代码实时捕获你的动作?(Hint:API接口暗中“贴”在你的手指上)
  • 数据炼金术
    • 每天数亿用户产生PB级数据,系统怎样在毫秒间从海量内容中捞出“最对你胃口”的那条视频?(分布式架构:让十万台服务器替你“打工”)
  • 算法攻心计
    • 为什么连你妈都不知道你爱吃螺蛳粉,抖音却能在三次滑动内精准推荐?(协同过滤算法:找到和你“臭味相投”的陌生人)

今天,我们从代码层面掀开抖音的“读心术”

接下来,你将看到:

  • 你的‘数据分身’如何被埋点、清洗、打标签,最终成为算法眼中的‘透明人’;
  • 推荐系统怎样用‘召回-排序-多样性控制’三连招,让你欲罢不能;
  • 程序员为扛住全民刷抖音的流量,在后台默默承受的‘福报’(高并发场景下的极限操作)。

用户画像:你的“数据分身”是如何炼成的?

数据采集:代码像“狗仔队”一样追踪你

  1. 埋点监听:

前端埋点:用户每次点击、滑动、点赞时,客户端自动触发埋点事件(如 onClick、onScroll),通过API将行为数据上报到服务器。

1
2
3
4
5
6
7
8
// 伪代码示例:点赞事件埋点
video.likeButton.addEventListener('click', () => {
trackEvent('video_like', {
video_id: '123',
user_id: '456',
timestamp: Date.now()
});
});

后端日志:服务器接收请求后,记录用户IP、设备ID、请求路径等原始日志,形成“用户行为流水账”。

  1. 设备指纹:
  • 通过 User-Agent 获取手机型号、操作系统;
  • 利用GPS、Wi-Fi SSID/IP定位常驻区域(比如“北京海淀区中关村打工人”);
  • 甚至通过陀螺仪数据判断用户是躺着刷还是坐着刷(影响推荐时段策略)。

热知识:

为什么你切换到4G网络,推荐内容突然变“土味”?
——因为系统发现你从一线城市CBD的WiFi切换到老家县城的基站IP,立刻调高“下沉市场”内容权重!

数据清洗与存储:把“生肉”加工成“熟数据”

技术流程:

  1. 实时流处理(如Kafka + Flink):
  • 用户滑动屏幕后,行为数据在 5毫秒内 进入Kafka消息队列;
  • Flink实时过滤无效数据(比如误触导致的0.1秒停留)。
  1. 离线分析(如Hadoop + Hive):
  • 每天凌晨启动MapReduce任务,统计用户长期行为(例如“过去30天每晚8点必看游戏直播”)。
  1. 存储优化:
  • 列式存储(HBase):将用户标签按列存储,快速查询“所有喜欢猫的用户”;
  • Redis缓存:高频标签(如“近期热搜词”)常驻内存,应对瞬时高并发请求。

标签体系:给你的兴趣“贴满小纸条”

标签类型与算法逻辑:

  1. 基础标签(直接提取):
  • 性别:通过头像、昵称、搜索词NLP分析(比如昵称含“喵”大概率女性);
  • 消费能力:根据手机型号(iPhone 15 Pro Max vs 千元机)、是否开通抖音会员。
  1. 兴趣标签(行为分析):
  • 短期兴趣:基于实时行为(例如连续刷10条滑雪视频,立刻打上“滑雪”标签);
  • 长期兴趣:通过TF-IDF算法从历史行为中提取关键词(比如“编程”“显卡评测”)。
  1. 动态标签(实时调整):
  • 衰减机制:上周的“螺蛳粉”标签权重每天下降10%,防止过期兴趣干扰;
  • 突发兴趣:若用户突然搜索“三亚旅游”,权重瞬间提升至0.8,触发紧急推荐。

热知识

为什么你只是看了一眼美女视频,系统却疯狂推荐?
——因为算法发现该视频的“完播率”达95%,且你的“停留时长比均值高3秒”,立刻判定为潜在兴趣!

技术难点:如何让“数据分身”逼近真实的你?

  • 去重与纠错:
    • 用布隆过滤器(Bloom Filter)排除重复点击(比如误触同一视频3次);
    • 通过行为序列分析识别“借用手机场景”(例如家长手机突然出现小学生爱看的动画片)。
  • 冷启动问题:
    • 新用户首次打开抖音时,推荐“地域热门+设备均价内容”(比如深圳用户默认推科技测评,县城用户推本地新闻)。
  • 存储成本优化:
    • 对低频标签(如“用户5年前点赞过的冷门歌曲”)使用廉价对象存储(如AWS S3),高频标签用SSD硬盘扛压力。

推荐算法:如何让代码“猜”中你的心思?

召回层:从“大海捞针”到“精准撒网”

技术目标:从亿级视频池中快速筛选出 千分之一 的候选内容。

核心策略:

  1. 协同过滤(CF):
  • User-Based:找到和你行为相似的用户,推荐他们喜欢的视频。
1
2
3
4
5
6
7
# 伪代码:计算用户相似度(余弦相似度)  
def user_similarity(user1, user2):
common_videos = user1.likes & user2.likes
dot_product = sum(user1.likes[v] * user2.likes[v] for v in common_videos)
norm_user1 = sqrt(sum(val**2 for val in user1.likes.values()))
norm_user2 = sqrt(sum(val**2 for val in user2.likes.values()))
return dot_product / (norm_user1 * norm_user2)
  • Item-Based:推荐与你喜欢过的视频相似的内容(例如:看完《流浪地球》后推《三体》解说)。
  1. 内容召回:
  • 用NLP提取视频标题/字幕关键词(如“露营”“新手教程”),匹配用户兴趣标签。
  • 图像识别分析视频封面(比如宠物视频中的猫狗品种)。
  1. 热门兜底:
  • 新用户首次打开APP时,直接推荐当日全站Top 100视频(避免“冷启动”尴尬)。

冷知识

为什么你的“小众爱好”突然被推荐?
——因为协同过滤发现某个和你有 97%相似度 的用户,昨晚点赞了这条视频,系统立刻“抄作业”推给你!

排序层:给视频“贴分数”,争夺你的注意力

技术目标:对召回层的千条候选视频,预测你对每条内容的 点击率(CTR) 和 完播率,精准排序。

模型架构:

  1. 特征工程:
  • 用户特征:标签权重、近期行为序列(如“最近3次点击均与露营相关”)。
  • 内容特征:视频时长、关键词、作者粉丝量、实时点赞增速。
  • 环境特征:当前时段(深夜推助眠视频)、网络类型(WiFi下推高清长视频)。
  1. 深度学习模型:
  • DNN(深度神经网络):将特征向量输入多层网络,输出点击概率。
  • 多任务学习:同时预测点击率、点赞率、评论率,综合计算“吸引力总分”。
  • 实时更新:模型每隔15分钟增量训练,吸收最新用户反馈数据。
1
2
3
4
5
6
7
8
9
# 伪代码:排序模型预测(简化版)  
def predict_ctr(user_features, video_features):
# 拼接用户特征和内容特征
input_vector = concat(user_features, video_features)
# 深度神经网络推理
ctr = dnn_model.predict(input_vector)
# 加入多样性惩罚(避免同类视频扎堆)
ctr *= diversity_penalty
return ctr

热知识

为什么你刚买的手机,抖音立马推测评视频?
——因为排序模型发现“设备型号”与“开箱测评”类视频的 特征交叉权重 突然飙升,判定你处于“购物决策期”!

多样性控制:打破“信息茧房”的代码心机

技术目标:防止推荐内容过于单一(例如全是美女视频),引发用户厌倦。

核心策略:

  1. 类别打散:
  • 每刷10条视频,强制插入1条非兴趣标签内容(如教育、新闻)。
  • 使用 MAB(多臂老虎机算法) 试探用户对新类型的反应。
  1. 时间衰减:
  • 连续推荐同一标签视频后,逐渐降低该标签权重(例如“露营”连推5次后权重减半)。
  1. 突发热点:
  • 监测全网爆款视频(如“某明星官宣”),强行插入到推荐流中,无论用户是否感兴趣。

热知识

为什么你总能看到老板的“正能量鸡汤视频”?
——因为系统默认所有用户对“平台热门内容”有 最低曝光阈值,哪怕你点了100次“不感兴趣”!

实时反馈:你的每一次滑动,都是算法的“调参器”

技术链条:

  1. 动作捕获:
  • 划走(负反馈):触发降权(降低相似内容权重);
  • 完播 + 点赞(正反馈):权重飙升,类似内容立即进入下一轮推荐。
  1. 流式更新:
  • 用户行为数据通过 Kafka 实时流入Flink,5秒内更新用户画像。
  • 排序模型根据实时特征调整下一次推荐结果(比如你刚点赞“滑雪”,下一条立刻推滑雪装备广告)。
  1. 后端架构挑战:
  • 毫秒级响应:从用户滑动到推荐更新,全程不超过100ms(需分布式计算 + 缓存优化)。
  • 数据一致性:确保画像更新与推荐结果同步(使用分布式锁或事务消息)。

后端工程师的挑战:高并发下的毫秒级博弈

实时性:你的“滑动”有多快,代码就得跑多快

技术场景:

用户每次滑动屏幕,系统需在 100ms内 完成:

  1. 捕获行为 → 2. 更新画像 → 3. 召回排序 → 4. 返回推荐结果

核心方案:

  • 流式计算(Apache Flink):
    • 行为数据实时进入Flink流水线,通过状态(State)管理快速更新用户标签权重。
1
2
3
4
5
6
// 伪代码:Flink实时处理用户滑动事件  
DataStream<UserAction> actions = env.addSource(kafkaSource);
actions
.keyBy(userId)
.process(new UpdateProfileFunction()) // 实时更新画像
.addSink(recommendSink); // 触发推荐计算
  • 分布式存储:
    • 用户画像分片存储(如按UserID哈希分片到不同Redis集群),避免单点瓶颈。

高并发:每秒百万级请求下的“极限求生”

技术场景:

晚高峰时段,抖音推荐系统每秒处理 数百万次请求,相当于春运期间所有火车站同时检票。

架构设计:

  1. 水平扩展:
  • 无状态服务:推荐API服务实例可动态扩容(Kubernetes自动伸缩);
  • 分片负载均衡:用户请求按地域/设备类型路由到最近机房。
  1. 缓存风暴防御:
  • 多级缓存:
    • 本地缓存(Guava Cache)→ Redis集群 → 数据库。
  • 缓存击穿方案:
    • 使用分布式锁(如Redisson)防止单个热点Key失效导致数据库雪崩。
1
2
3
4
5
6
7
8
9
10
// 伪代码:缓存击穿防护  
RLock lock = redisson.getLock("VIDEO:123");
if (lock.tryLock()) {
try {
data = db.getVideo(123);
redis.set("VIDEO:123", data, 60);
} finally {
lock.unlock();
}
}
  1. 降级熔断:
  • 若召回服务超时,自动降级为“热门视频兜底”;
  • 使用Hystrix或Sentinel监控服务熔断状态。

热知识:

为什么明星官宣时抖音不崩?
——后台早已启动“ 大V预案 ”:提前预热缓存、限流非核心接口(比如评论加载),把资源留给推荐系统。

数据隐私:在“精准推荐”与“窥私骂名”间走钢丝

技术挑战:

既要利用用户数据优化推荐,又要避免法律风险(如GDPR、中国个人信息保护法)。

解决方案:

  1. 匿名化处理:
  • 用户标签与真实身份脱钩,使用哈希加密的UUID替代UserID。
  1. 差分隐私:
  • 在数据统计时注入噪声(如拉普拉斯噪声),防止通过数据反推个人身份。
1
2
3
4
5
# 伪代码:拉普拉斯噪声注入  
def laplace_noise(data, epsilon):
scale = 1.0 / epsilon
noise = np.random.laplace(0, scale)
return data + noise
  1. 权限隔离:
  • 开发/运维人员访问生产数据需动态申请权限,操作日志全程审计。

热知识:

为什么你从未搜索“植发”,却总看到生发广告?
——因为系统通过“协同过滤”发现,和你同年龄、同地域的男性用户都在点击这类广告!

容灾与一致性:当机房爆炸时,代码如何“自救”?

极端场景应对:

  1. 多活架构:
  • 用户流量分流到北京、上海、深圳三地机房,数据库通过Paxos/Raft协议同步数据。
  1. 故障转移:
  • 若某机房网络中断,5秒内切换DNS解析至备用机房(你只会感觉视频加载稍卡一下)。
  1. 最终一致性:
  • 允许短暂画像延迟(如你刚点赞的视频下一刷才生效),用消息队列保证数据最终一致。

总结

上头解密
你以为你在抖音刷视频,实则是无数行代码在“刷”你。

每一次滑动、点赞、停留,都在为你的“数字分身”添砖加瓦。

用户画像像一位24小时不打烊的侦探,通过埋点、流式计算、标签体系,将你的兴趣拆解得明明白白;推荐算法则化身精准的注意力猎手,用协同过滤、深度学习、实时反馈,操控你的每一次心跳加速。

文末福利

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

概念学习

概念学习

概念学习

概念学习

DeepSeek百万年薪招聘:AI领域人才争夺战的“顶配”条件与薪资揭秘

随着人工智能技术的飞速发展,中国AI初创企业DeepSeek(杭州深度求索人工智能基础技术研究有限公司)凭借其高性价比的大模型与开源创新举措,迅速成为全球科技界的焦点。为支撑业务扩张与技术突破,DeepSeek近期发布了大规模招聘计划,52个在招职位涵盖深度学习、系统研发、UI设计等领域,并开出了年薪最高154万元的诱人待遇,引发行业热议

AI人才如此值钱的当下,我们程序员该何去何从?

想要拿到如此高的薪资,需要什么条件?

DeepSeek甚至给校招实习生开出了1000元每天的工资,这些实习生的工资已经超过了大部分一线城市的程序员了。

最重要的在于一句话不看经验只看能力。只要你能力够好,那么Deepseek就可以给你高薪。

如同之前马斯克发的一句话,只要你能力够好,我们就可以给你发offer。

公司背景与行业趋势

DeepSeek成立于2023年7月,由量化投资巨头幻方量化创立,2025年1月发布的DeepSeek-R1和DeepSeek-V3大模型因“低成本、高性能”引发全球关注,用户量激增,日活跃用户(DAU)突破2000万,跻身全球AI产品榜第二。此次招聘规模超现有团队1/3(现有员工约150人),凸显其快速扩张需求。

行业层面,AI人才缺口持续扩大。《2024年度人才迁徙报告》显示,大模型相关岗位占据技术岗平均月薪前十名的半数,算法工程师需求激增,预计到2030年中国AI人才缺口将达400万

招聘特点与竞争门槛

  • 学历放宽,校招优先:部分岗位(如核心系统研发工程师)对本科生开放,且校招薪酬高于社招,吸引年轻人才。
  • 成果导向:深度学习研究员需以论文、竞赛成绩或开源项目证明研究能力,强调“创新品味”与对AGI的信仰。
  • 抗压能力与热情:公司更青睐“年轻、反应敏捷、有冲劲”的候选人,要求具备持续学习能力和跨学科视野。

核心岗位与薪资结构

  1. 深度学习研究员-AGI
  • 薪资水平:月薪8万至11万元,按14薪计算,年薪最高达154万元。
  • 岗位要求:
    • 精通机器学习和深度学习,需具备创新研究能力;
    • 熟练掌握至少两种编程语言(如Python、C++);
    • 有国际顶会(如NeurIPS、ICML)或顶级期刊论文发表经验;
      • 在知名AI竞赛中取得优异成绩者优先。
  • 目标人群:博士学历为主,优秀硕士或本科生亦可参与实习。
  1. 核心系统研发工程师(校招)
  • 薪资范围:月薪6万至9万元,年薪最高126万元。
  • 岗位要求:围绕大模型预训练、算法优化等展开工程化实现,要求熟悉软硬件协同开发,具备高性能计算经验

deepseek1

  1. 全栈开发工程师
  • 薪资水平:月薪4万至7万元,年薪最高98万元。
  • 能力要求:扎实的编程能力,优秀的设计能力和代码品味,较强的责任心。对主流开源软件有深入了解并对此有贡献者优先。具有自我驱动和自我管理能力。

deepseek1

  1. 实习生岗位
  • AGI大模型实习生日薪500至1000元,按每月22天计算,月薪可达1.1万至2.2万元,要求掌握PyTorch框架,并有论文或开源项目经验。主要是顶会论文。

deepseek1

我们如何努力破局?

目前有3类人急需掌握AI能力

  1. 还没入行但是想入行的
  2. 已经是程序员但是想破局的
  3. 本科生、研究生、博士生这类学生。

其实,看到这些招聘信息,我们可以提炼出几点信息。

  1. 不看经验只看能力:所以能力尤为重要,我们需要尽快提升自己,那么如何提升自己?
  2. 名校毕业生:如何成为名校毕业生?
  3. 顶会论文:如何发表顶会论文?

如何提升自己

提升自己的能力最重要的是持续学习能力,要持续不断的学习新的知识。利用业余时间、碎片时间等。

学习知识也是有技巧的,需要系统性学习有目的的去学习、找到学习方法、有正确的学习路径

在现在这个互联网发达的社会,网上的学习资料成百上千,如何选择?

大头作为工作了快10年的开发,当过上市公司的架构师,当过大厂资深研发,管理过10人团队

大头会持续分享自身的经验以及学习方法、系统性的学习路径、教程。

所有的信息都由大头本身经过严格的筛选,大家不需要自己去选择,以免走歪路。如果有想学习的小伙伴可以私信大头,免费1对1交流。

目前大头正在输出mysql数据库零基础教程。后续还会输出更多的系统性教程

如何提升学历

从上面的招聘也可以看出来,虽然是看能力,可其实大多数要求还是要本科的。像顶会论文这些也需要名校的学生才能发布,其他的学校教学能力有限,学生很难发布顶会论文。

尤其现在大环境不好,大头最开始工作的时候,完全不看学历,现在,就连外包都要求统招本科了。

大头也遇见过很多优秀的人才,干的好好的,今年却被突然要求本科毕业证,哪怕是非统招的也可以,要不然就会面临不续合同、被裁员等。

因此,提升学历也很重要。

但是,如此多的提升学历的方法,如此多付费的广告,该如何选择呢?

现在提升学历无非几种方案。

  • 非统招
    • 成考/业余/函授/非脱产:这四个现在是一样的,没区别,今年开始统一叫非脱产形式了。需要参加成人高考。相对来说比较简单,入学以后多数学校需要在周末去上课,参加考试,有平时分,毕业很简单。
    • 网络教育:无需上课,网上学习,参加考试即可毕业。也是很多机构推的方案。不过现在已经取消了。所以大家不要再相信了。
    • 国家开放大学:入学以后有的需要到学校学习,和非脱产类似,但是比非脱产容易毕业。
    • 自考:极难、没有入学一说,只要参加这个专业所有课程的考试并且考试合格就可以了。也不用到学校学习,适合有自制力且有时间的人。
  • 统招:参加高考上大学,很多人有误区,以为现在不能高考了,其实没有年龄限制,只要你想还是可以去参加的。

上面说的是本科,那么对于研究生来说也是一样的。

  • 非统招:参加研究生统一考试,入学以后工作日晚上和周末去上课,和统招的统一毕业条件,因此学习年限更久。学费更多。但是入学分数要低一些。因为竞争者少。
  • 统招:参加研究生统一考试,正常上课毕业。
  • 在职:在职研究生属于只有毕业证没有学位证的研究生,是以前的一种形式,以后会被非统招代替。在职研究生入学简单,毕业简单。

如何发表顶会论文

要满足顶会论文的要求,需从选题创新性、实验设计、论文写作、格式规范、投稿流程等多个环节系统把控。

此外,需要不断的阅读优秀的文献,比如其他的顶会论文,学习他们的技巧。

需要有不错的idea和综合能力支撑你的论文发表。

平常可以多练习写作。

对于已经工作的该如何做呢?

可以多撰写软著、专利等。

再比如提升自身的影响力,发表书籍等。

总结

DeepSeek的高薪招聘不仅反映了AI行业对顶尖人才的渴求,也揭示了技术驱动型企业“以薪酬换速度”的竞争策略。对于求职者而言,扎实的学术背景、工程能力与持续创新意识,是叩开百万年薪大门的核心钥匙。未来,随着AGI研发的深入,这场人才争夺战或将愈演愈烈。

文末福利

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

概念学习

概念学习

概念学习

概念学习

支付宝P0事故

大家好,我是大头。
就在2025年1月16号,支付宝平台被曝出现了P0级事故,真可谓是2025年的过年惊喜啊。

怎么回事呢?
有网友晒出,在昨天下午14:40-14:45的这段时间里,通过支付宝支付时,均被提示“政府补贴”,减免优惠20%。

P0

而这些优惠包括支付以及转账,什么概念呢?

也就是说假设你手里有1万块钱,你把1万转给你媳妇,你只需要支付8000,你媳妇收到1万,白嫖2000。你媳妇再转给你,又能白嫖2000.

来钱简直不要太快啊。如果你有100万呢?一次就是20万啊。

不过呢,真这样可能有点刑。。。

那么什么是P0事故呢?在互联网大厂里面,如何对事故定级呢?

为什么支付宝可以如此快的处理这种事故?

如果我们的公司遇到这种事故该如何处理?

什么是P0事故

在互联网企业中,P0代表最高级别的事故,通常是核心业务重要功能不可用,且影响范围广泛。以目前这起事故的严重程度来看,绝对是妥妥的P0级别事故了。

除此之外,还会对于涉及到的金额进行资金定损,比如这一次事故中,造成了多少资金损失。

大家别看只是短短的5分钟,对于支付宝这个体量来说,其造成的损失数额绝对不小,不过对于支付宝来说,可能洒洒水啦。

那么除了P0事故来说,还有别的事故等级吗?

当然是有的,还有P1、P2等等,其中P0是最严重的。

相信对于程序员来说,最关心的一点还是,出了这么大的事故,对我有什么影响吗?

不同的事故等级对应的影响也不一样。

  • 最低级的事故:可能只是通报批评,下次注意
  • 有一定损失或者影响的事故:比如P2事故,可能会影响你的当年绩效了
  • 有不少损失或者影响的事故:比如P1事故,那么不仅影响你当年的绩效奖金了,还会影响到你的评优、内部的奖项以及最重要的晋升,还可能影响到你直属leader的绩效奖金。
  • 有很大损失或者影响的事故:比如P0事故,那么不光影响上面那些,还包括了你leader的评优、奖项以及晋升。可能还会导致你们团队负责人的绩效、评优、晋升。再严重一些还会收到辞退。

为什么支付宝可以如此快的处理这种事故?

大厂通常都有完善的上线流程以及监控处理。

如何监控呢?

  • 链路服务监控
  • 日志机制
  • 报警机制

大厂内部都有自己研发的一整套监控报警系统,所以可以快速的发现事故,并依据一整套完善的事故处理机制进行处理。

那对于我们其他公司来说如何处理呢?

对于链路服务监控,我们可以使用SkyWalking来进行监控,以及日志收集,那么当日志收集以后我们还需要进行报警,比如发送报警邮件、短信或者微信提醒。

SkyWalking是什么?

SkyWalking是分布式系统的应用程序性能监控工具,专为微服务、云原生和基于容器(Kubernetes)架构而设计。

下面是SkyWalking官网。

https://skywalking.apache.org/

那么当发现事故以后,我们第一步该做什么?定位问题,然后修改?不不不….

当然是第一时间回滚止损了。

接下来才是定位问题、修复。

这么一来就会发现,还需要完善的上线流程、回滚方案。事故处理方案。

对于很多小问题的修改或者需求,我们可能会下意识的觉得,就改动这么点东西,没问题,上了吧。

如果这个时候出了问题,你可能就傻了,根本没想过出问题的解决方案。而且出的问题可能和你改动的东西八竿子打不着,你都想不到是因为你上线导致的。

所以大家切记,上线方案很重要!!!

经验主义害死人啊!!!实践才是检验真理的唯一标准!!!

如果我们的公司遇到这种事故该如何处理

接下来,如果我们的公司遇到这种事故该如何处理?

首先,像上面说的,我们需要几个方案

  1. 完善的监控机制,可以使用SkyWalking
  2. 完善的日志机制,可以帮助我们快速定位问题
  3. 完善的报警机制,可以帮助我们发现问题
  4. 完善的上线流程,这里包括代码Review,回滚方案,git分支管理方案等。可以帮助我们快速的解决问题
  5. 完善的事故处理流程,当事故发生以后我们该如何处理

监控机制

使用 SkyWalking 即可,官网已经放在上面了,可以根据官方文档进行学习。

日志机制

不同的日志等级代表不同的日志,并且分开不同的文件,还有在一次请求的时候,需要有唯一ID来检查这一次请求的所有日志。

此外,哪些地方需要记录日志呢?

  • 输入
  • 输出
  • 重要处理节点
  • 重要计算结果
  • 异常处理
  • 其他

根据这些来快速定位问题。

这里要注意:

1
不要在日志中记录一些容易报错的代码,你不能因为日志报错而影响主流程。

报警机制

当监控检测到错误的时候,我们可以进行报警通知,通过邮件、微信、短信等手段进行通知相关人员。

这样可以快速的让相关人员知道出现事故了。接下来才能解决。

上线流程

上线流程最重要的几点

  • 代码Review
  • 完善的测试
  • 回滚方案

上线的时候一定要经过这些,哪怕是需求再小 or 改动再小。

因为你永远不知道意外和明天哪一个先来。。。

事故处理流程

当发现事故以后,如何处理就成为了重点。

当报警机制触发以后,我们首先应该根据上线方案进行回滚。

回滚以后,我们需要发送一些通知,让其他成员知道回滚了。以防止有人稀里糊涂的又把代码发布了或者覆盖了。

接下来根据日志信息配合测试一起快速的定位问题。

……

最终将问题修复。

再次上线。

并将这次事故记录,复盘。

总结

话说回来了。那么支付宝这次事故。钱会不会追回呢?

P0

答案是不会追回资金,支付宝还是很强大的,选择了自己承担这笔损失,当然了,具体哪些程序员倒霉就不知道了。

如果你收到追回短信等,不要相信,都是骗子!!!

netty

socket

今天,小白的老师让小白写一个服务器,小白学艺不精,过来向大头求救了。

netty服务器

netty服务器

netty服务器

那么socket究竟是什么呢?套接字接口(socket interface)是一组函数,它们和Unix I /O函数结合起来,用以创建网络应用。从Linux内核的角度来看, 一个套接字就是通信的一个端点。从Linux程序的角度来看,套接字就是一个有相应描述符的打开文件。

下图来源于CSAPP的书。

page table

这张图清晰的表明了socket是个啥,从逻辑上说,这玩意就是个接口,从这个接口能发送和接收数据,从物理上说,这玩意就是个文件,发送数据就是往这个文件里面写入数据,接收数据就是从这个文件里面读取数据。所以Socket也是个IO操作。只不过这个文件的数据不存储在你电脑的硬盘里面,而是通过网络发送出去了。

通过socket函数可以打开一个文件,返回一个文件描述符。文件描述符可以简单的理解为文件的ID,唯一标识,一般默认打开的3个文件描述符就是标准输入,标准输出,错误输出,对应0、1、2.假设我们打开了一个socket文件,描述符是3.

接下来通过服务器通过bind函数,可以将这个socket文件,和一个IP还有端口号进行绑定。绑定以后写入这个文件的数据就会从这个IP端口读取出来或者发送出去。

socket分为客户端和服务器,客户端主动发起请求,服务器被动接受请求,listen函数就是告诉Linux内核,我这个socket是一个服务器,而不是一个客户端。

最后通过accept函数,来等待客户端的连接.accept函数会返回一个新的文件描述符,通过这个新的文件进行传递数据,而老的文件仅仅负责建立连接。

下图为accept的时候服务器的状态,这个时候等待连接。
netty服务器

当连接建立以后,会通过新的文件描述符4进行通信。

netty服务器

基于上述理论知识,学过哲学的都知道,实践是检验真理的唯一标准。所以小白用java实现了第一版socket服务器。端口号是8888,祝看到这里的兄弟们发发发发。

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
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {

private static final String FILE_PATH = "HelloWorld.java";
private static String readFileAsString(String filePath) throws IOException {
StringBuilder contentBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String currentLine;
while ((currentLine = br.readLine()) != null) {
System.out.println(currentLine); // 打印读取的每一行
contentBuilder.append(currentLine).append("\n");
}
}
return contentBuilder.toString();
}
public static void main(String[] args) {
int port = 8888; // 服务器监听的端口号
try {
// 创建服务器端的ServerSocket,绑定端口号
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server is running and listening on port " + port);

// 服务器无限循环,等待客户端连接
while (true) {
// 服务器调用accept()方法,阻塞并等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected.");

try {
// 读取HTTP请求
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
StringBuilder request = new StringBuilder();
while ((inputLine = in.readLine()) != null && !inputLine.isEmpty()) {
request.append(inputLine).append("\n");
}

// 发送HTTP响应
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String fileContent = readFileAsString(FILE_PATH);
long contentLength = fileContent.getBytes().length;
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Type: application/json\r\n" +
"Content-Length: " + contentLength + "\r\n" +
"Connection: close\r\n" +
"\r\n" +
fileContent;
out.println(response);

// 关闭连接
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Client says: hello world!");

// 关闭连接
clientSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

这个代码里面没有bindlisten。这是因为被java封装起来了。如果看看ServerSocket里面的代码就能看见这两个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public ServerSocket(int port) throws IOException {
//调用了另外一个构造函数
this(port, 50, null);
}

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
// 参数检测
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException("Port value out of range: " + port);
if (backlog < 1)
backlog = 50;

// 创建了一个实现类
this.impl = createImpl();
try {
// 调用了bind方法
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch (IOException | SecurityException e) {
close();
throw e;
}
}

上面能看到调用了bind方法。再看看bind里面呢。能看到这个里面有getImpl().bindgetImpl().listen。这就是上面说的bindlisten函数了。

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
/**
* 将服务器Socket绑定到指定的端点地址并设置监听队列长度
* 此方法确保Socket未关闭且未绑定,并验证端点地址和backlog参数的合法性
* 如果满足所有条件,则进行绑定和监听设置
*
* @param endpoint 要绑定的端点地址,如果为null,则创建一个未指定端口的InetSocketAddress实例
* @param backlog 监听队列的长度,如果小于1,则使用默认值50
* @throws IOException 如果绑定或监听过程中发生I/O错误
* @throws SocketException 如果Socket已关闭、已绑定、地址未解析或不支持的地址类型
* @throws IllegalArgumentException 如果端点地址类型不受支持
*/
public void bind(SocketAddress endpoint, int backlog) throws IOException {
// 检查Socket是否已关闭
if (isClosed())
throw new SocketException("Socket is closed");
// 检查Socket是否已绑定
if (isBound())
throw new SocketException("Already bound");
// 如果未指定端点地址,则创建一个未指定端口的InetSocketAddress实例
if (endpoint == null)
endpoint = new InetSocketAddress(0);
// 检查端点地址是否为InetSocketAddress类型
if (!(endpoint instanceof InetSocketAddress epoint))
throw new IllegalArgumentException("Unsupported address type");
// 检查端点地址是否已解析
if (epoint.isUnresolved())
throw new SocketException("Unresolved address");
// 检查backlog参数是否合法
if (backlog < 1)
backlog = 50;

// 安全检查,确保有权限监听指定端口
@SuppressWarnings("removal")
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkListen(epoint.getPort());

// 同步块,确保线程安全
synchronized (stateLock) {
// 再次检查Socket是否已关闭或绑定
if (closed)
throw new SocketException("Socket is closed");
if (bound)
throw new SocketException("Already bound");
// 调用实现类的方法进行实际的绑定和监听设置
getImpl().bind(epoint.getAddress(), epoint.getPort());
getImpl().listen(backlog);
// 设置绑定状态为true
bound = true;
}
}

第一版socket

第一版socket

让我们用wrk压测一下。可以发现很垃圾。仅仅处理了4个请求,虽然响应时间很快。可是吞吐量太低了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 55.33ms 326.77us 55.72ms 50.00%
Req/Sec 40.00 0.00 40.00 100.00%
Latency Distribution
50% 55.44ms
75% 55.72ms
90% 55.72ms
99% 55.72ms
4 requests in 30.10s, 852.00B read
Socket errors: connect 7967, read 196512, write 14, timeout 0
Requests/sec: 0.13
Transfer/sec: 28.30B

第一版socket

第一版socket

第一版socket

接下来,上线程池,小白实现了第二版代码。这次的代码加入了线程池,所有的客户端请求建立以后通过线程池的线程进行处理。建立10个线程。

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
import java.io.*;
import java.net.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleHttpServer {
private static final int PORT = 8888;
private static final String FILE_PATH = "HelloWorld.java";

public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("Server is running at http://localhost:" + PORT);

while (true) {
Socket clientSocket = serverSocket.accept();
threadPool.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static class ClientHandler implements Runnable {
private final Socket clientSocket;

public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}

@Override
public void run() {
try {
// 读取HTTP请求
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
StringBuilder request = new StringBuilder();
while ((inputLine = in.readLine()) != null && !inputLine.isEmpty()) {
request.append(inputLine).append("\n");
}

// 发送HTTP响应
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String fileContent = readFileAsString(FILE_PATH);
long contentLength = fileContent.getBytes().length;
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Type: application/json\r\n" +
"Content-Length: " + contentLength + "\r\n" +
"Connection: close\r\n" +
"\r\n" +
fileContent;
out.println(response);

// 关闭连接
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}

private String readFileAsString(String filePath) throws IOException {
StringBuilder contentBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String currentLine;
while ((currentLine = br.readLine()) != null) {
System.out.println(currentLine); // 打印读取的每一行
contentBuilder.append(currentLine).append("\n");
}
}
return contentBuilder.toString();
}
}
}

接下来在看压测结果。同样30s,处理了13个请求,处理时间虽然略有上升,但是整体性能提升了很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 118.94ms 32.73ms 148.30ms 53.85%
Req/Sec 17.50 21.62 60.00 83.33%
Latency Distribution
50% 147.83ms
75% 148.02ms
90% 148.25ms
99% 148.30ms
13 requests in 30.10s, 2.70KB read
Socket errors: connect 7967, read 198469, write 1, timeout 0
Requests/sec: 0.43
Transfer/sec: 91.99B

第一版socket

第一版socket

IO

在 Java 中,BIO、NIO 和 AIO 是三种不同的 I/O 模型,它们各自的工作原理和使用场景有所不同。下面简要说明这三种 I/O 模型:

BIO(Blocking I/O,阻塞 I/O)

BIO就类似于你去胖东来,但是人太多了,要排队进去,而且你这个时候走了,那么你的位置就没了,需要重新排队,所以你不能离开,你只能排队等着!!!这就是所谓的同步阻塞。

第一版socket

原理:
BIO 是传统的 I/O 模型,在这个模型中,每次读取或写入数据时,线程都会被阻塞,直到操作完成。
每个连接对应一个线程,线程会一直阻塞,直到 I/O 操作完成后才能继续执行其他任务。

特性:

  • 阻塞:每次读写操作都会等待数据的到来或写入完成,阻塞当前线程。
  • 线程绑定:每个客户端连接都会占用一个独立的线程。
  • 简单直观:编程模型简单,适合小规模应用,但不适合高并发的场景。

优缺点:

  • 优点:实现简单,编程模型直观。
  • 缺点:性能差,尤其是在高并发场景下,线程过多导致资源浪费(线程上下文切换开销大)。

使用场景:
适合低并发、连接数不多的应用场景,例如小型 Web 服务。

NIO(New I/O,新 I/O)

NIO通过操作系统提供的IO多路复用机制,可以实现多线程的性能,因为IO多路复用,本质上是单线程的,所以省去了操作系统线程切换的开销。

拿胖东来说,BIO你只能等着,但如果你是NIO,那么胖东来说,我给你发个号吧,你是88号,接下来你爱干啥干啥去,等我叫88号了,你过来就行了。但是呢,我只负责叫号,你离远了听不见那是你的事,你的时不时的过来问我一下,到88号了吗?到了你就进去,没到你就等着。

第一版socket

从代码来说,就是通过select进行订阅。比如select订阅文件描述符3号的状态,操作系统如果发现有数据从网络中传输过来,那么就写入3号文件,写完以后,操作系统就改变3号文件的状态,这个时候select如果过来判断3号的状态,发现变了,就知道可以执行对应的操作了,比如服务器开始运行,然后读取文件内容发送给客户端。

第一版socket

原理:
NIO 提供了 非阻塞 I/O 的能力,支持多路复用技术,允许一个线程同时处理多个 I/O 操作。
通过使用 Selector 和 Channel,NIO 可以在一个线程中处理多个客户端的 I/O 请求。
非阻塞:读取和写入操作不会阻塞线程,如果没有数据可用,线程会继续执行其他任务。

特性:

  • 非阻塞 I/O:可以通过轮询和事件通知的方式处理 I/O 请求,而不必阻塞等待。
  • 多路复用:通过 Selector 实现一个线程管理多个 Channel,处理多个连接。
  • 高效:减少了线程的开销,适用于高并发场景。

优缺点:

  • 优点:比 BIO 更高效,适合高并发、高负载的应用。
  • 缺点:编程模型较为复杂,需要理解 Selector、Channel、Buffer 的使用方式,开发成本较高。

使用场景:
高并发、大规模连接的网络应用,如服务器(HTTP、Chat Server 等)、数据库连接池、文件上传下载等。

AIO(Asynchronous I/O,异步 I/O)

AIO就是说,在胖东来排队的时候,你找他要号的时候,还留了个手机号码,告诉他,到我了你就给我打电话,他给你88号,等叫到88号了,他就给你打电话说,到你了,过来吧。和NIO的区别就是你不用老过来问了,到了我打电话叫你。

第一版socket

原理:
AIO 是 Java 7 引入的异步 I/O 模型,它的关键特性是完全异步,即操作完成后系统会通过回调通知应用程序。
在 AIO 中,线程发起 I/O 请求后,不会阻塞等待,而是返回,等 I/O 操作完成时,操作系统会通知 Java 程序,程序再通过回调函数获取结果。

特性:

  • 异步:I/O 操作发起后,线程立即返回,不会等待操作完成。
  • 事件驱动:操作完成后,系统通过回调通知应用程序处理结果。
  • 无需轮询:不同于 NIO,AIO 不需要手动轮询 Selector,操作完成后直接通过回调处理结果。

优缺点:

  • 优点:可以进一步减少线程和上下文切换,性能非常高,适合超高并发的应用。
  • 缺点:复杂度最高,需要理解回调和事件处理机制,调试较为困难。

使用场景:
适用于极高并发的应用场景,如实时数据流处理、大规模分布式系统等。

特性 BIO NIO AIO
阻塞方式 阻塞(每个操作阻塞当前线程) 非阻塞(通过轮询和事件驱动) 异步(操作完成后通过回调通知)
线程管理 每个连接一个线程 一个线程处理多个连接 一个线程发起请求,回调通知结果
编程复杂度
性能 性能差,适用于低并发场景 性能优越,适用于高并发场景 性能最优,适用于超高并发场景
适用场景 小规模服务,低并发应用 高并发应用,如 Web 服务器 极高并发应用,如大数据流处理

NIO实现

说了这么多,小白也实现了NIO版本的服务器。代码如下

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
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.Iterator;
import java.util.Set;

public class NIOHttpServer {

private static final int PORT = 8888;
private static final String FILE_TO_SERVE = "HelloWorld.java";

public static void main(String[] args) {
try {
// 1. 打开一个 Selector,用于管理多个通道的 I/O 事件
Selector selector = Selector.open();

// 2. 打开一个 ServerSocketChannel,用于监听客户端连接
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 3. 绑定到指定端口,并配置为非阻塞模式
serverSocketChannel.bind(new java.net.InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);

// 4. 将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("NIO HTTP Server started on port " + PORT);

while (true) {
// 5. 阻塞等待事件发生(或超时返回)
selector.select();

// 6. 获取所有准备好的事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();

while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 移除已处理的事件

if (key.isAcceptable()) {
// 处理新的客户端连接
handleAccept(key);
} else if (key.isReadable()) {
// 处理客户端请求
handleRead(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

// 接受客户端连接
private static void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverSocketChannel.accept();
clientChannel.configureBlocking(false);
// 注册客户端通道到 Selector,监听 READ 事件
clientChannel.register(key.selector(), SelectionKey.OP_READ);
System.out.println("Accepted new connection from " + clientChannel.getRemoteAddress());
}

// 读取客户端请求并响应
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 读取客户端发送的数据
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
clientChannel.close();
return;
}

buffer.flip();
byte[] requestData = new byte[buffer.remaining()];
buffer.get(requestData);
String request = new String(requestData);
System.out.println("Received request:\n" + request);

// 读取 HelloWorld.java 文件内容
String fileContent;
try {
fileContent = Files.readString(Paths.get(FILE_TO_SERVE));
System.out.println("Read file content:\n" + fileContent);
} catch (IOException e) {
fileContent = "Error reading file: " + e.getMessage();
System.err.println(fileContent);
}

// 构造 HTTP 响应
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Type: application/json\r\n" +
"Content-Length: " + fileContent.length() + "\r\n" +
"\r\n" +
fileContent;

// 将响应写入客户端
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
clientChannel.close(); // 响应完成后关闭连接
}
}

在执行一下压测,结果表明处理了24个请求,吞吐量大大增加了。性能有明显提升,而且更加稳定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 145.80ms 9.32ms 170.26ms 87.50%
Req/Sec 14.88 8.95 30.00 62.50%
Latency Distribution
50% 142.46ms
75% 142.72ms
90% 168.73ms
99% 170.26ms
24 requests in 30.09s, 4.52KB read
Socket errors: connect 7967, read 192317, write 0, timeout 0
Requests/sec: 0.80
Transfer/sec: 153.92B

第一版socket

第一版socket

第一版socket

小白又把代码重新改了一遍。

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
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Set;

public class NIOHttpServer {

private static final int PORT = 8888;
private static final String FILE_PATH = "HelloWorld.java";

public static void main(String[] args) {
try {
new NIOHttpServer().start();
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
e.printStackTrace();
}
}

public void start() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new java.net.InetSocketAddress(PORT));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("Server started on port " + PORT);

while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();

if (key.isAcceptable()) {
handleAccept(key, selector);
} else if (key.isReadable()) {
handleRead(key);
}
}
}
}

private void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted connection from " + clientChannel.getRemoteAddress());
}

private void handleRead(SelectionKey key) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);

try {
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
return;
}

buffer.flip();
String request = new String(buffer.array(), 0, buffer.limit());
System.out.println("Received request: \n" + request);

String fileContent = readFileContent(FILE_PATH);
String response = constructHttpResponse(fileContent);
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());

while (responseBuffer.hasRemaining()) {
clientChannel.write(responseBuffer);
}

clientChannel.close();
} catch (IOException e) {
System.err.println("Error handling client request: " + e.getMessage());
try {
clientChannel.close();
} catch (IOException ex) {
System.err.println("Error closing client connection: " + ex.getMessage());
}
}
}

private String constructHttpResponse(String bodyContent) {
String headers = constructHttpHeaders(bodyContent.length());
return headers + bodyContent;
}

private String constructHttpHeaders(int contentLength) {
return "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Length: " + contentLength + "\r\n" +
"\r\n";
}

private String readFileContent(String filePath) {
try {
Path path = Paths.get(filePath);
return Files.readString(path);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
return "File not found or error reading file.";
}
}
}

netty

netty

零拷贝技术

现在代码中读取文件内容在返回的过程如下图所示。可以看到一共有7步,有多次IO,而且内存里面有两份一样的数据,空间占用大。

netty

零拷贝是操作系统提供的一种技术,操作系统提供了一个函数,通过这个函数就可以实现零拷贝,看一下零拷贝的过程吧。

零拷贝可以减少IO操作次数,减少内存空间占用,内存里面实际上只有一份内容,网络发送读取的就是那个内容。没有发送内存拷贝动作。

netty

用零拷贝技术重写代码

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
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Set;

public class NIOZeroCopyHttpServer {

private static final int PORT = 8888;
private static final String FILE_PATH = "HelloWorld.java";

public static void main(String[] args) {
try {
new NIOZeroCopyHttpServer().start();
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
e.printStackTrace();
}
}

public void start() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new java.net.InetSocketAddress(PORT));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("Server started on port " + PORT);

while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();

if (key.isAcceptable()) {
handleAccept(key, selector);
} else if (key.isReadable()) {
handleRead(key);
}
}
}
}

private void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted connection from " + clientChannel.getRemoteAddress());
}

private void handleRead(SelectionKey key) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);

try {
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
return;
}

buffer.flip();
String request = new String(buffer.array(), 0, buffer.limit());
System.out.println("Received request: \n" + request);

sendFileUsingZeroCopy(clientChannel, FILE_PATH);
} catch (IOException e) {
System.err.println("Error handling client request: " + e.getMessage());
try {
clientChannel.close();
} catch (IOException ex) {
System.err.println("Error closing client connection: " + ex.getMessage());
}
}
}

private void sendFileUsingZeroCopy(SocketChannel clientChannel, String filePath) {
try (FileChannel fileChannel = FileChannel.open(Paths.get(filePath))) {
long fileSize = fileChannel.size();
String headers = constructHttpHeaders(fileSize);
ByteBuffer headerBuffer = ByteBuffer.wrap(headers.getBytes());

// Write headers first
while (headerBuffer.hasRemaining()) {
clientChannel.write(headerBuffer);
}

// 零拷贝
long position = 0;
while (position < fileSize) {
position += fileChannel.transferTo(position, fileSize - position, clientChannel);
}

clientChannel.close();
} catch (IOException e) {
System.err.println("Error sending file: " + e.getMessage());
}
}

private String constructHttpHeaders(long contentLength) {
return "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Length: " + contentLength + "\r\n" +
"Connection: close\r\n" +
"\r\n";
}
}

再次使用wrk压测。可以看到同样30s内,处理了48个请求,吞吐量再次提升。性能更加强大了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 100.27ms 64.55ms 208.10ms 72.92%
Req/Sec 36.45 88.79 303.00 90.91%
Latency Distribution
50% 62.63ms
75% 200.92ms
90% 204.08ms
99% 208.10ms
48 requests in 30.10s, 9.66KB read
Socket errors: connect 7967, read 195016, write 3, timeout 0
Requests/sec: 1.59
Transfer/sec: 328.50B

netty

零拷贝+线程池

小白又给代码加上了线程池。代码如下

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
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NIOThreadZeroCopyHttpServer {

private static final int PORT = 8888;
private static final String FILE_PATH = "HelloWorld.java";
private static final int THREAD_POOL_SIZE = 10;

private final ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

public static void main(String[] args) {
try {
new NIOThreadZeroCopyHttpServer().start();
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
e.printStackTrace();
}
}

public void start() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new java.net.InetSocketAddress(PORT));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("Server started on port " + PORT);

while (true) {
selector.select(); // Block until events are available
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // Ensure key is removed to prevent re-processing

if (!key.isValid()) {
continue; // Skip invalid keys
}

try {
if (key.isAcceptable()) {
handleAccept(key, selector);
}
} catch (CancelledKeyException e) {
System.err.println("CancelledKeyException: " + e.getMessage());
}
}
}
}

private void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.setOption(java.net.StandardSocketOptions.TCP_NODELAY, true);
clientChannel.register(selector, SelectionKey.OP_READ);

System.out.println("Accepted connection from " + clientChannel.getRemoteAddress());

// Submit task to thread pool for processing
threadPool.submit(() -> handleRequest(clientChannel));
}

private void handleRequest(SocketChannel clientChannel) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
return;
}

buffer.flip();
String request = new String(buffer.array(), 0, buffer.limit());
System.out.println("Received request: \n" + request);

sendFileUsingZeroCopy(clientChannel, FILE_PATH);
} catch (IOException e) {
System.err.println("Error handling client request: " + e.getMessage());
try {
clientChannel.close();
} catch (IOException ex) {
System.err.println("Error closing client connection: " + ex.getMessage());
}
}
}

private void sendFileUsingZeroCopy(SocketChannel clientChannel, String filePath) {
try (FileChannel fileChannel = FileChannel.open(Paths.get(filePath))) {
long fileSize = fileChannel.size();
String headers = constructHttpHeaders(fileSize);
ByteBuffer headerBuffer = ByteBuffer.wrap(headers.getBytes());

// Write headers first
while (headerBuffer.hasRemaining()) {
clientChannel.write(headerBuffer);
}

// Use zero-copy to send file content
long position = 0;
while (position < fileSize) {
position += fileChannel.transferTo(position, fileSize - position, clientChannel);
}

clientChannel.close();
} catch (IOException e) {
System.err.println("Error sending file: " + e.getMessage());
}
}

private String constructHttpHeaders(long contentLength) {
return "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Length: " + contentLength + "\r\n" +
"Connection: close\r\n" +
"\r\n";
}
}

压测结果如下,整体性能有明显提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.64ms 8.85ms 27.32ms 82.47%
Req/Sec 113.30 99.51 300.00 80.00%
Latency Distribution
50% 2.09ms
75% 13.33ms
90% 23.46ms
99% 27.32ms
113 requests in 30.10s, 22.73KB read
Socket errors: connect 7967, read 191435, write 12, timeout 0
Requests/sec: 3.75
Transfer/sec: 773.29B

netty

netty

BOSS-Worker模式

之前的代码里面,Worker线程接收到请求以后进行工作,这个时候就会占用这个请求连接,在工作。就相当于干了两份活

  • 接收请求
  • 处理任务

将这两个活,分给两个人,就能提升效率,BOSS线程池仅仅接收请求,再把请求分配给Worker线程,Worker线程仅仅需要处理任务。还有一个好处就是,BOSS可以有多个,比如多个领导负责分配任务。

新的代码如下

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
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NIOBossWorkerHttpServer {
private static final int PORT = 8888;
private static final int BUFFER_SIZE = 1024;
private static final int BOSS_THREAD_COUNT = 1; // Boss 线程数
private static final int WORKER_THREAD_COUNT = 10; // Worker 线程数
private static final String FILE_PATH = "HelloWorld.java";

public static void main(String[] args) throws IOException {
// Boss 线程池
ExecutorService bossThreadPool = Executors.newFixedThreadPool(BOSS_THREAD_COUNT);
// Worker 线程池
ExecutorService workerThreadPool = Executors.newFixedThreadPool(WORKER_THREAD_COUNT);

// 创建一个主选择器(Boss Selector)
Selector bossSelector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.register(bossSelector, SelectionKey.OP_ACCEPT);

System.out.println("HTTP Server started on port " + PORT);

// Boss 线程负责分发连接
bossThreadPool.submit(() -> {
try {
while (true) {
bossSelector.select(); // 等待事件
Iterator<SelectionKey> iterator = bossSelector.selectedKeys().iterator();

while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();

if (key.isAcceptable()) {
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
System.out.println("Accepted new connection: " + clientChannel.getRemoteAddress());

// 将新连接交给 Worker 线程池处理
workerThreadPool.submit(() -> handleClient(clientChannel));
}
}
}
} catch (IOException e) {
System.err.println("Error in Boss thread: " + e.getMessage());
}
});
}

/**
* Worker 线程处理客户端连接的读写操作
*/
private static void handleClient(SocketChannel clientChannel) {
try (Selector workerSelector = Selector.open()) {
clientChannel.register(workerSelector, SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);

while (clientChannel.isOpen()) {
workerSelector.select(); // 等待事件
Iterator<SelectionKey> iterator = workerSelector.selectedKeys().iterator();

while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();

if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
buffer.clear();
int bytesRead = channel.read(buffer);
if (bytesRead == -1) {
closeChannel(channel);
return;
}

// 请求解析 (这里只简单返回文件内容作为响应)
buffer.flip();
sendFile(channel, FILE_PATH);
}
}
}
} catch (IOException e) {
System.err.println("Error in Worker thread: " + e.getMessage());
} finally {
closeChannel(clientChannel);
}
}

/**
* 使用零拷贝技术发送文件
*/
private static void sendFile(SocketChannel clientChannel, String filePath) {
try (FileChannel fileChannel = FileChannel.open(Paths.get(filePath))) {
long fileSize = fileChannel.size();

// 构造 HTTP 响应头
String headers = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Length: " + fileSize + "\r\n" +
"\r\n";
ByteBuffer headerBuffer = ByteBuffer.wrap(headers.getBytes());

// 发送响应头
while (headerBuffer.hasRemaining()) {
clientChannel.write(headerBuffer);
}

// 使用零拷贝发送文件内容
long position = 0;
while (position < fileSize) {
position += fileChannel.transferTo(position, fileSize - position, clientChannel);
}
System.out.println("File sent successfully to: " + clientChannel.getRemoteAddress());
} catch (IOException e) {
System.err.println("Error sending file: " + e.getMessage());
}
}

/**
* 关闭通道
*/
private static void closeChannel(Channel channel) {
if (channel != null && channel.isOpen()) {
try {
channel.close();
System.out.println("Channel closed.");
} catch (IOException e) {
System.err.println("Error closing channel: " + e.getMessage());
}
}
}
}

当BOSS数量为1个的时候,压测结果。可以看到吞吐量大大提升了一波。整体性能更好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 326.78ms 66.97ms 1.18s 95.07%
Req/Sec 7.73 9.44 40.00 87.74%
Latency Distribution
50% 310.27ms
75% 340.00ms
90% 369.37ms
99% 488.59ms
912 requests in 30.10s, 166.80KB read
Socket errors: connect 7967, read 187538, write 0, timeout 0
Requests/sec: 30.30
Transfer/sec: 5.54KB

当BOSS数量为4个的时候,压测结果,能看到性能再次提升了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 221.00ms 25.30ms 331.05ms 85.11%
Req/Sec 23.14 14.81 70.00 50.55%
Latency Distribution
50% 211.31ms
75% 226.24ms
90% 257.96ms
99% 297.11ms
1350 requests in 30.10s, 246.66KB read
Socket errors: connect 7967, read 242633, write 0, timeout 0
Requests/sec: 44.85
Transfer/sec: 8.19KB

netty

netty

netty

netty

Netty是一个NIO客户端服务器框架,可以快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化了网络编程,如TCP和UDP套接字服务器。

netty就是基于上述的网络编程实现的,区别就是netty封装的更好,更完善,做了一些优化,支持了更多的协议。参考netty官网的架构图。

可以看到支持TCP、UDP, 支持大文件传输,支持压缩,支持HTTP、HTTPS、SMTP邮件、Google Protobuf一般用来实现RPC,通过事件模型实现,还实现了零拷贝技术。

netty

如何使用netty来实现上面的服务器

通过netty实现的话,代码简单了很多,只需要关注读取文件并返回给客户端这个逻辑就可以了。但是其根本模型没有变化,比如BOSS和Worker,还有零拷贝,线程池,NIO技术,这些是netty的核心,他只是增加了更多的逻辑,支持,优化等。

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
package org.example;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;


import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
private static final int PORT = 8888;

public static void main(String[] args) {
// Boss 线程池: 接收客户端连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// Worker 线程池: 处理 I/O 操作
EventLoopGroup workerGroup = new NioEventLoopGroup(10);

try {
// 创建 ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(io.netty.channel.socket.nio.NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new HttpServerCodec()); // HTTP 编解码
ch.pipeline().addLast(new HttpObjectAggregator(65536)); // 处理 HTTP 请求体
ch.pipeline().addLast(new ChunkedWriteHandler()); // 支持大文件传输
ch.pipeline().addLast(new HelloWorldServerHandler()); // 自定义处理逻辑
}
});

// 绑定端口并启动
ChannelFuture f = b.bind(PORT).sync();
System.out.println("Server started on port 8888...");

// 等待服务器关闭
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}

class HelloWorldServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
// 读取文件内容
File file = new File("./HelloWorld.java");

if (file.exists()) {
byte[] fileContent = Files.readAllBytes(file.toPath());
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.copiedBuffer(fileContent));

// 设置响应头
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());

// 发送响应
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} else {
// 文件不存在时返回 404
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND,
Unpooled.copiedBuffer("File Not Found".getBytes(CharsetUtil.UTF_8)));

response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());

ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
}

压测结果,性能很高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.39ms 12.28ms 113.55ms 92.66%
Req/Sec 192.24 343.10 2.51k 88.08%
Latency Distribution
50% 4.41ms
75% 9.55ms
90% 17.22ms
99% 72.18ms
15693 requests in 30.10s, 3.02MB read
Socket errors: connect 7967, read 22110, write 1, timeout 0
Requests/sec: 521.32
Transfer/sec: 102.84KB

并发测试对比

上面都是10个线程并发10000测试的结果。我们现在改成并发1000,再做个对比。

netty实现的压测结果,和10000没什么区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 94.20ms 178.90ms 896.17ms 86.02%
Req/Sec 324.69 443.24 2.20k 85.19%
Latency Distribution
50% 10.05ms
75% 49.68ms
90% 365.89ms
99% 724.64ms
16390 requests in 30.05s, 3.16MB read
Socket errors: connect 0, read 36092, write 117, timeout 0
Requests/sec: 545.36
Transfer/sec: 107.58KB

我们自己实现的BOSS-Worker代码,压测结果,1000并发的性能提升很明显。因为我们的实现更加原始,而netty的实现更重。更注重高并发,对于低并发而言我们的实现更好,这也说明我们确实实现了netty的核心功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 205.91us 529.03us 75.25ms 99.30%
Req/Sec 15.51k 5.85k 25.50k 62.44%
Latency Distribution
50% 177.00us
75% 207.00us
90% 245.00us
99% 518.00us
1389306 requests in 30.09s, 247.76MB read
Socket errors: connect 0, read 10787, write 0, timeout 0
Requests/sec: 46169.43
Transfer/sec: 8.23MB

接下来是线程池+NIO+零拷贝的实现压测结果。对于低并发的性能同样很好,但是比BOSS-Worker实现低了很多很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.38ms 13.68ms 201.55ms 91.31%
Req/Sec 102.92 287.63 2.54k 92.41%
Latency Distribution
50% 3.65ms
75% 6.38ms
90% 18.18ms
99% 68.71ms
11677 requests in 30.08s, 2.29MB read
Socket errors: connect 0, read 454831, write 242, timeout 0
Requests/sec: 388.15
Transfer/sec: 78.09KB

接下来试试NIO+零拷贝的实现压测结果。可以看到结果也很棒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.49ms 9.53ms 178.38ms 93.80%
Req/Sec 127.52 313.84 2.42k 91.95%
Latency Distribution
50% 2.21ms
75% 4.10ms
90% 8.91ms
99% 41.68ms
13680 requests in 30.06s, 2.69MB read
Socket errors: connect 0, read 80895, write 7, timeout 0
Requests/sec: 455.12
Transfer/sec: 91.56KB

再看看NIO的实现压测结果,因为没有零拷贝,所以执行时间增加了很多。尤其是最大延迟,吞吐量也有降低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.24ms 27.62ms 675.49ms 96.24%
Req/Sec 125.39 301.56 3.01k 91.31%
Latency Distribution
50% 2.22ms
75% 9.52ms
90% 24.23ms
99% 45.71ms
11657 requests in 30.10s, 2.08MB read
Socket errors: connect 0, read 87555, write 1308, timeout 0
Requests/sec: 387.33
Transfer/sec: 70.73KB

最后看看BIO的实现吧。可以看到吞吐量还是低了不少的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Running 30s test @ http://localhost:8888
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 55.22ms 48.14ms 181.62ms 65.71%
Req/Sec 83.94 221.86 1.33k 93.39%
Latency Distribution
50% 48.44ms
75% 68.44ms
90% 153.94ms
99% 170.59ms
5304 requests in 30.09s, 1.08MB read
Socket errors: connect 0, read 60714, write 723, timeout 0
Requests/sec: 176.29
Transfer/sec: 36.67KB

整体总结而言,主要的提升手段是NIO和BOSS-Worker模型,零拷贝也有一定的性能提升。

netty

黄子韬送车,抖音却出现bug了,又有一个人的年终奖泡汤了

1月10日下午4点,黄子韬发布视频,称晚上7点将直播送车,并再次声明规则:“你只要来了都可以参与。”此前,黄子韬曾在直播中承诺,其短视频账号超过1500万粉丝将赠送国产汽车10台。消息一出,黄子韬的评论区立马成为了网友的许愿池,大家纷纷在评论区留言自己想要的车型。

当晚,黄子稻进行直播,在线观看人数达到了1100多万人,点赞数更是爆了!

抖音重大bug

点赞数持续上涨,最终更是超过了Int的最大值2147483647。也就是超过了231 - 1。

接下来,神奇的一幕发生了,点赞数变成了负数!

list1.png

大家知道这是怎么回事吗?

在Java中,只存在有符号整数,而没有无符号整数。具体各个类型的大小如下表所示。

类型 最小值 最大值
byte -128 127
short -32768 32767
int -2147483648 2147483647
long -9223372036854775808 9223372036854775807

什么是有符号整数无符号整数呢?

有符号整数(Signed Integer)

有符号整数是指可以表示正数、负数和零的整数类型。在计算机中,有符号整数通常使用最高位(即最左边的位)作为符号位来表示正负。最高位为0表示正数,最高位为1表示负数。

示例

8位有符号整数(byte)

范围:-128 到 127
二进制表示:
0000 0000 表示 0
0000 0001 表示 1
0111 1111 表示 127
1000 0000 表示 -128
1000 0001 表示 -127
1111 1111 表示 -1

32位有符号整数(int)

范围:-2,147,483,648 到 2,147,483,647
二进制表示:
0000 0000 0000 0000 0000 0000 0000 0000 表示 0
0000 0000 0000 0000 0000 0000 0000 0001 表示 1
0111 1111 1111 1111 1111 1111 1111 1111 表示 2,147,483,647
1000 0000 0000 0000 0000 0000 0000 0000 表示 -2,147,483,648
1000 0000 0000 0000 0000 0000 0000 0001 表示 -2,147,483,647
1111 1111 1111 1111 1111 1111 1111 1111 表示 -1

无符号整数(Unsigned Integer)

无符号整数是指只能表示非负数(即零和正数)的整数类型。无符号整数没有符号位,所有位都用于表示数值,因此其范围是从0到最大值。

示例

8位无符号整数

范围:0 到 255
二进制表示:
0000 0000 表示 0
0000 0001 表示 1
0111 1111 表示 127
1000 0000 表示 128
1111 1111 表示 255

32位无符号整数

范围:0 到 4,294,967,295
二进制表示:
0000 0000 0000 0000 0000 0000 0000 0000 表示 0
0000 0000 0000 0000 0000 0000 0000 0001 表示 1
0111 1111 1111 1111 1111 1111 1111 1111 表示 2,147,483,647
1000 0000 0000 0000 0000 0000 0000 0000 表示 2,147,483,648
1111 1111 1111 1111 1111 1111 1111 1111 表示 4,294,967,295

特性 有符号整数 无符号整数
范围 包含负数、零和正数 只包含零和正数
最高位 用于表示符号 用于表示数值
8位 -128 到 127 0 到 255
16位 -32768 到 32767 0 到 65535
32位 -2,147,483,648 到 2,147,483,647 0 到 4,294,967,295
64位 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 0 到 18,446,744,073,709,551,615

在Java中,没有原生的无符号整数类型,但可以使用BigInteger类来模拟无符号整数。

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
import java.math.BigInteger;

public class IntegerExample {
public static void main(String[] args) {
// 有符号整数
byte signedByte = 127;
short signedShort = 32767;
int signedInt = 2147483647;
long signedLong = 9223372036854775807L;

System.out.println("有符号 byte 最大值: " + signedByte);
System.out.println("有符号 short 最大值: " + signedShort);
System.out.println("有符号 int 最大值: " + signedInt);
System.out.println("有符号 long 最大值: " + signedLong);

// 无符号整数(使用 BigInteger 模拟)
BigInteger unsignedByte = BigInteger.valueOf(255);
BigInteger unsignedShort = BigInteger.valueOf(65535);
BigInteger unsignedInt = BigInteger.valueOf(4294967295L);
BigInteger unsignedLong = BigInteger.valueOf(18446744073709551615L);

System.out.println("无符号 byte 最大值: " + unsignedByte);
System.out.println("无符号 short 最大值: " + unsignedShort);
System.out.println("无符号 int 最大值: " + unsignedInt);
System.out.println("无符号 long 最大值: " + unsignedLong);
}
}

输出结果。

1
2
3
4
5
6
7
8
有符号 byte 最大值: 127
有符号 short 最大值: 32767
有符号 int 最大值: 2147483647
有符号 long 最大值: 9223372036854775807
无符号 byte 最大值: 255
无符号 short 最大值: 65535
无符号 int 最大值: 4294967295
无符号 long 最大值: 18446744073709551615

有符号整数溢出

有符号整数溢出是指在进行算术运算时,结果超出了该整数类型所能表示的范围。由于有符号整数有固定的范围,当运算结果超过这个范围时,就会发生溢出。溢出会导致结果“回绕”到范围的另一端,这可能会导致一些意外的行为。

下图展示了有符号整数的整个范围,将范围看作一个圆,我特意切开了这个圆,左边表示正数,右边表示负数。从这里可以看到,当正数的最大值再加上1,就会变成负数的最大值。

list1.png

这个是为什么呢,可以从二进制看到答案。

32位最大值是2147483647,用二进制表示如下,也就是符号为是0表示正数,剩下31位是1.

1
01111111111111111111111111111111

这个数再加1是多少呢,二进制加法和十进制加法是一样的,逢2进1。那么一直进位,二进制结果如下。

1
10000000000000000000000000000000

可以看到结果是符号位是1,剩下31位是0.这个结果就是-2147483648

真实点赞数

接下来,我们可以算一下,真实的点赞数到底是多少呢?

假设当前的点赞数是-1671321474

已知:

  1. 当前是有符号整数
  2. 当前是Int类型
  3. 当Int类型达到最大值2147483647以后再加1就会溢出。

所以真实的点赞数 = 0 + Int最大值 + 1(溢出到最小值) + x = -1671321474
= -2147483648 + x = -1671321474

因此 x = -1671321474 + 2147483648 = 476162174

也就是说真实的点赞数 = 2147483647 + 1 + 476162174 = 2623645822

也就是26亿。千分位表示:2,623,645,822

总结

有人说服务端是好的,只是安卓端的老代码使用的是Int,因此造成了这个Bug,这不就是传说中的屎山代码吗?

也不知道又有哪些人要倒霉的没有年终奖啦,大家也要注意这个问题啊~

我相信最开始的程序员也没有想到一场直播的点赞数能干爆Int吧~