工业级Netty网关,京东是如何架构的?

news2024/11/18 22:36:08

说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,很多小伙伴拿到一线互联网企业如阿里、网易、有赞、希音、百度、滴滴的面试资格。

最近,尼恩指导一个小伙伴简历,写了一个《高并发网关项目》,此项目帮这个小伙拿到 字节/阿里/微博/汽车之家 面邀, 所以说,这是一个牛逼的项目。

为了帮助大家拿到更多面试机会,拿到更多大厂offer。

尼恩决定:给大家出一章视频介绍这个项目的架构和实操,《33章:10Wqps 高并发 Netty网关架构与实操》,预计月底发布。然后,提供一对一的简历指导,这里简历金光闪闪、脱胎换骨。

《33章:10Wqps 高并发 Netty网关架构与实操》 海报如下:

配合《33章:10Wqps 高并发 Netty网关架构与实操》, 尼恩会梳理几个工业级、生产级网关案例,作为架构素材、设计的素材。

前面梳理了

  • 《日流量200亿,携程网关的架构设计》
  • 《千万级连接,知乎如何架构长连接网关?》
  • 《日200亿次调用,喜马拉雅网关的架构设计》
  • 《100万级连接,爱奇艺WebSocket网关如何架构》
  • 《亿级长连接,淘宝接入层网关的架构设计》
  • 《单体120万连接,小爱网关如何架构?》
  • 《100万级连接,石墨文档WebSocket网关如何架构?》
  • 《2亿用户,B站API网关如何架构?》

除了以上的8个案例,这里,尼恩又找到一个漂亮的生产级案例:《工业级Netty网关,京东是如何架构?

注意,这又一个非常 牛逼的工业级、生产级网关案例

这些案例,并不是尼恩的原创。这些案例,仅仅是尼恩在《33章:10Wqps 高并发 Netty网关架构与实操》备课的过程中,在互联网查找资料的时候,收集起来的,供大家学习和交流使用。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取

文章目录

    • 说在前面
    • 工业级Netty网关,京东是如何架构?
    • 1、TCP网关的网络结构
    • 2、TCP网关长连接容器架构
    • 3、TCP网关Netty Server的IO模型
    • 4、TCP网关的线程模型
    • 5、TCP网关执行时序图
    • 6、TCP网关源码分析
      • 6.1 Session管理
      • 6.2 心跳
      • 6.3 数据上行
      • 6.4 数据下行
    • 说在最后:有问题可以找老架构取经
    • 推荐阅读

工业级Netty网关,京东是如何架构?

作者:张松然,京东商家研发部架构师

京麦是京东商城为其商家提供的一款后台管理工具,它能够让商家在不登录后台的情况下生成订单,快速完成订单下载和发货流程。这与淘宝的旺旺商家版(现已更名为淘宝千牛)类似。

本文主要阐述了京麦 TCP 网关的技术架构以及 Netty 的应用实践。

京东京麦商家管理平台从 2014 年开始搭建网关,从 HTTP 网关逐步升级为 TCP 网关。到了 2016 年,基于 Netty4.x+Protobuf3.x 技术,京麦构建了一个高可用、高性能和高稳定的 TCP 长连接网关,支持 PC 端和应用程序的上下行通信。

早期的京麦主要依靠 HTTP 和 TCP 长连接来发送消息通知,而没有将其应用于 API 网关。

然而,随着对 NIO 技术的深入了解和对 Netty 框架的熟练掌握,以及对系统通信稳定性要求的提高,京麦开始尝试运用 NIO 技术来实现 API 请求调用。这一设想在 2016 年终于实现,并成功支持业务运营。

得益于采用了 TCP 长连接容器、Protobuf 序列化、服务泛化调用框架等多种优化措施,京麦的 TCP 网关性能比 HTTP 网关提升了 10 倍以上,稳定性也显著超过了 HTTP 网关。

1、TCP网关的网络结构

通过 Netty 构建京麦 TCP 网关的长连接容器,作为网关接入层提供服务 API 请求调用。

客户端通过域名 + 端口访问 TCP 网关,不同域名对应不同运营商的 VIP,VIP 发布在 LVS 上,LVS 将请求转发给后端的 HAProxy,然后由 HAProxy 将请求转发给后端的 Netty 的 IP+Port。

主要,这个是高并发接入层的标准架构

LVS 将请求转发给后端的 HAProxy,经过 LVS 的请求,但响应是由 HAProxy 直接返回给客户端,这就是 LVS 的 DR 模式。

注意:请点击图像以查看清晰的视图!

LVS+Keepalived(DR模式)的深入学习,尼恩给大家写了一篇非常详细的、全面的文章, 建议大家收藏起,好好掌握:

《10Wqps网关接入层,LVS+Keepalived(DR模式)如何搭建?》

2、TCP网关长连接容器架构

TCP网关的核心组件是Netty,而Netty的NIO模型是Reactor反应堆模型(Reactor相当于有分发功能的多路复用器Selector)。

每一个连接对应一个Channel(多路指多个Channel,复用指多个连接复用了一个线程或少量线程,在Netty指EventLoop),一个Channel对应唯一的ChannelPipeline,多个Handler串行的加入到Pipeline中,每个Handler关联唯一的ChannelHandlerContext。TCP网关长连接容器的Handler就是放在Pipeline的中。

我们知道TCP属于OSI的传输层,所以建立Session管理机制构建会话层来提供应用层服务,可以极大的降低系统复杂度。所以,每一个Channel对应一个Connection,一个Connection又对应一个Session,Session由Session Manager管理,Session与Connection是一一对应,Connection保存着ChannelHandlerContext (ChannelHanderContext可以找到Channel), Session通过心跳机制来保持Channel的Active状态。

每一次Session的会话请求(ChannelRead)都是通过Proxy代理机制调用Service层,数据请求完毕后通过写入ChannelHandlerConext再传送到Channel中。

数据下行主动推送也是如此,通过Session Manager找到Active的Session,轮询写入Session中的ChannelHandlerContext,就可以实现广播或点对点的数据推送逻辑。如下图所示。

注意:请点击图像以查看清晰的视图!

京麦TCP网关使用Netty Channel进行数据通信,使用Protobuf进行序列化和反序列化,每个请求都将被封装成Byte二进制字节流,在整个生命周期中,Channel保持长连接,而不是每次调用都重新创建Channel,达到链接的复用。

我们接下来来看看基于Netty的具体技术实践。

3、TCP网关Netty Server的IO模型

具体的实现过程如下

  • 1)创建ServerBootstrap,设定BossGroup与WorkerGroup线程池;
  • 2)bind指定的port,开始侦听和接受客户端链接(如果系统只有一个服务端port需要监听,则BossGroup线程组线程数设置为1);
  • 3)在ChannelPipeline注册childHandler,用来处理客户端链接中的请求帧。

4、TCP网关的线程模型

TCP网关使用Netty的线程池,共三组线程池,分别为BossGroup、WorkerGroup和ExecutorGroup。

其中,BossGroup用于接收客户端的TCP连接,WorkerGroup用于处理I/O、执行系统Task和定时任务,ExecutorGroup用于处理网关业务加解密、限流、路由,及将请求转发给后端的抓取服务等业务操作。

注意:请点击图像以查看清晰的视图!

NioEventLoop是Netty的Reactor线程,其角色

  • 1)Boss Group:作为服务端Acceptor线程,用于accept客户端链接,并转发给WorkerGroup中的线程;
  • 2)Worker Group:作为IO线程,负责IO的读写,从SocketChannel中读取报文或向SocketChannel写入报文;
  • 3)Task Queue/Delay Task Queu:作为定时任务线程,执行定时任务,例如链路空闲检测和发送心跳消息等。

5、TCP网关执行时序图

注意:请点击图像以查看清晰的视图!

如上图所示,其中步骤一至步骤九是Netty服务端的创建时序,步骤十至步骤十三是TCP网关容器创建的时序。

步骤一:创建ServerBootstrap实例,ServerBootstrap是Netty服务端的启动辅助类。

步骤二:设置并绑定Reactor线程池,EventLoopGroup是Netty的Reactor线程池,EventLoop负责所有注册到本线程的Channel。

步骤三:设置并绑定服务器Channel,Netty Server需要创建NioServerSocketChannel对象。

步骤四:TCP链接建立时创建ChannelPipeline,ChannelPipeline本质上是一个负责和执行ChannelHandler的职责链。

步骤五:添加并设置ChannelHandler,ChannelHandler串行的加入ChannelPipeline中。

步骤六:绑定监听端口并启动服务端,将NioServerSocketChannel注册到Selector上。

步骤七:Selector轮训,由EventLoop负责调度和执行Selector轮询操作。

步骤八:执行网络请求事件通知,轮询准备就绪的Channel,由EventLoop执行ChannelPipeline。

步骤九:执行Netty系统和业务ChannelHandler,依次调度并执行ChannelPipeline的ChannelHandler。

步骤十:通过Proxy代理调用后端服务,ChannelRead事件后,通过发射调度后端Service。

步骤十一:创建Session,Session与Connection是相互依赖关系。

步骤十二:创建Connection,Connection保存ChannelHandlerContext。

步骤十三:添加SessionListener,SessionListener监听SessionCreate和SessionDestory等事件。

6、TCP网关源码分析

6.1 Session管理

Session是客户端与服务端建立的一次会话链接,会话信息中保存着SessionId、连接创建时间、上次访问事件,以及Connection和SessionListener,在Connection中保存了Netty的ChannelHandlerContext上下文信息。Session会话信息会保存在SessionManager内存管理器中。

创建Session的源码

@Override
public synchronized Session createSession(String sessionId, ChannelHandlerContext ctx){
    Session session = sessions.get(sessionId);
    if (session != null){
        session.close();
    }
    session = new ExchangeSession();
    session.setSessionId(sessionId);
    session.setValid(true);
    session.setMaxInactiveInterval(this.getMaxInactiveInterval());
    session.setCreationTime(System.currentTimeMillis());
    session.setLastAccessedTime(System.currentTimeMillis());
    session.setSessionManager(this);
    session.setConnection(createTcpConnection(session, ctx));
    for (SessionListener listener : essionListeners){
        session.addSessionListener(listener);
    }
    return session;
}

通过源码分析,如果Session已经存在销毁Session,但是这个需要特别注意,创建Session一定不要创建那些断线重连的Channel,否则会出现Channel被误销毁的问题。因为如果在已经建立Connection(1)的Channel上,再建立Connection(2),进入session.close方法会将cxt关闭,Connection(1)和Connection(2)的Channel都将会被关闭。在断线之后再建立连接Connection(3),由于Session是有一定延迟,Connection(3)和Connection(1/2)不是同一个,但Channel可能是同一个。

所以,如何处理是否是断线重练的Channel,具体的方法是在Channel中存入SessionId,每次事件请求判断Channel中是否存在SessionId,如果Channel中存在SessionId则判断为断线重连的Channel,代码如下图所示。

private String getChannelSessionHook(ChannelHandlerContext ctx){
    return ctx.channel().attr (Constants.SERVER_SESSION_HOOK).get();
}

private void setChannelSessionHook(ChannelHandlerContext ctx, String sessionId){
    ctx.channel().attr(Constants.SERVER_SESSION_HOOK).set(sessionId);
}

6.2 心跳

心跳用于检测保持连接的客户端是否仍然活跃,客户端每隔一段时间发送一次心跳包到服务端,服务端收到心跳后更新 Session 的最后访问时间。

在服务端,长连接会话检测通过轮询 Session 集合来判断最后访问时间是否过期,如果过期,则关闭 Session 和 Connection,包括从内存中删除,同时注销 Channel 等。如下面代码所示。

Session session = tcpSessionManager.createSession(wrapper.getSessionId(), ctx);
session.addSessionListener(tcpHeartbeatListener);
session.connect();

tcpSessionManager.addSession(session);

通过源码分析,在每个Session创建成功之后,都会在Session中添加TcpHeartbeatListener这个心跳检测的监听,TcpHeartbeatListener是一个实现了SessionListener接口的守护线程,通过定时休眠轮询Sessions检查是否存在过期的Session,如果轮训出过期的Session,则关闭Session。如下面代码所示。

public void checkHeartBeat(){
    Session[] sessions = tcpSessionManager.getSessions();
    for (Session session : sessions){
        if (session.expire()){
            session.close();
            logger.info("heart is expire, clear sessionId:"+ session.getSessionId());
        }
    }
}

同时,注意到session.connect方法,在connect方法中会对Session添加的Listeners进行添加时间,它会循环调用所有Listner的sessionCreated事件,其中TcpHeartbeatListener也是在这个过程中被唤起。如下面代码所示。

private void addSessionEvent(){
    SessionEvent event = new SessionEvent(this);
    for (SessionListener listener : listeners){
        try{
            listener.sessionCreated(event);
            logger.info("SessionListener" + listener + ".sessionCreated() is invoked successfully!");
        } catch (Exception e){
            logger.error("addSessionEvent error.", e);
        }
    }
}

6.3 数据上行

数据上行特指从客户端发送数据到服务端,数据从ChannelHander的channelRead方法获取数据。数据包括创建会话、发送心跳、数据请求等。这里注意的是,channelRead的数据包括客户端主动请求服务端的数据,以及服务端下行通知客户端的返回数据,所以在处理object数据时,通过数据标识区分是请求-应答,还是通知-回复。如下面代码所示。

public void channelRead(ChannelHandlerContext ctx, Object o) throws Exception{
    try{
        if (o instanceof MessageBuf.JMTransfer) {
            SystemMessage sMsg = generateSystemMessage(ctx);
            MessageBuf.JMTransfer message = (MessageBuf.JMTransfer) o;
            //inbound
            if(message.getFormat() == SEND) {
                MessageWrapper wrapper = proxy.invoke(sMsg, message);
                if (wrapper != null)
                    this.receive(ctx, wrapper);
            }
            // outbound
            if (message.getFormat() == REPLY) {
                notify.reply(message);
            }
        }else{
            logger. warn("TcpServerHandler channelRead message is not proto.");
        }
    }catch (Exception e) {
        logger.error("TcpServerHandler TcpServerHandler handler error.", e);
        throw e;
    }
}

6.4 数据下行

数据下行通过MQ广播机制到所有服务器,所有服务器收到消息后,获取当前服务器所持有的所有Session会话,进行数据广播下行通知。

如果是点对点的数据推送下行,数据也是先广播到所有服务器,每个服务器判断推送的端是否是当前服务器持有的会话,如果判断消息数据中的信息是在当前服务,则进行推送,否则抛弃。如下面代码所示。

private Notifyfuture doSendAsync(long seq, Messagelrapper wrapper, int timeout) throws Exception {
    if (wrapper == null) {
        throw new Exception("wrapper cannot be null.");
    }
    String sessionId = wrapper.getSessionId();
    if (StringUtils.isBlank(sessionId)) {
        throw new Exception("sessionId cannot be null.")
    }
    if (tcpConnector.exist sessionId)) {
        //start.
        final NotifyFuture future = new NotifyFuture(timeout);
        this.futureMap.put(seq, future);
        tcpConnector.send(sessionId, wrapper.getBody());
        future.setSentTime(System.currentTimeMillis()); // 置为已发送return future.
    } else {
        // tcpConnector not exist sessionId
        return null;
    }
}

通过源码分析,数据下行则通过NotifyProxy的方式发送数据,需要注意的是Netty是NIO,如果下行通知需要获取返回值,则要将异步转同步,所以NotifyFuture是实现java.util.concurrent.Future的方法,通过设置超时时间,在channelRead获取到上行数据之后,通过seq来关联NotifyFuture的方法。如下面代码所示。

public void reply (MessageBuf.JMTransfer message) throws Exception {
    try {
        long seg = message.getSeq();
        final NotifyFuture future = this.futureMap.get(seg);
        if (future != null){
            future.setSuccess(true);
            futureMap.remove(seg);
        }
    } catch (Exception e) {
        throw e;
    }
}

下行的数据通过TcpConnector的send方法发送,send方式则是通过ChannelHandlerContext的writeAndFlush方法写入Channel,并实现数据下行,这里需要注意的是,之前有另一种写法就是cf.await,通过阻塞的方式来判断写入是否成功,这种写法偶发出现BlockingOperationException的异常。如下面代码所示。

ChannelFuture cf = cxt.writeAndFlush(message);
cf.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture future) throws PushException{
        if (future.isSuccess()) {
            logger.debug("send success.");
        } else {
            throw new PushException("Failed to send message.");
        }
        Throwable cause = future.cause() ;
        if (cause != null) {
            throw new PushException(cause);
        }
    }
});

使用阻塞获取返回值的写法

boolean success = true;
boolean sent = true;
int timeout = 60;
try {
    ChannelFuture cf = cxt.write(message);
    cxt.flush();
    if (sent){
        success = cf.await(timeout);
    }
    if (cf.isSuccess()) {
        logger.debug("send success.");
    }
    Throwable cause = cf.cause();
    if (cause != null) {
        this.fireError(new PushException(cause));
    }
} catch (Throwable e) {
    this.fireError(new PushException("Failed to send message, cause:" + e. getMessage(),e));
}

关于BlockingOperationException的问题我在StackOverflow进行提问,非常幸运的得到了Norman Maurer(Netty的核心贡献者之一)的解答

最终结论大致分析出,在执行write方法时,Netty会判断current thread是否就是分给该Channe的EventLoop,如果是则行线程执行IO操作,否则提交executor等待分配。

当执行await方法时,会从executor里fetch出执行线程,这里就需要checkDeadLock,判断执行线程和current threads是否时同一个线程,如果是就检测为死锁抛出异常BlockingOperationException。

说在最后:有问题可以找老架构取经

架构之路,充满了坎坷

架构和高级开发不一样 , 架构问题是open/开放式的,架构问题是没有标准答案的

正由于这样,很多小伙伴,尽管耗费很多精力,耗费很多金钱,但是,遗憾的是,一生都没有完成架构升级

所以,在架构升级/转型过程中,确实找不到有效的方案,可以来找40岁老架构尼恩求助.

前段时间一个小伙伴,他是跨专业来做Java,现在面临转架构的难题,但是经过尼恩几轮指导,顺利拿到了Java架构师+大数据架构师offer 。所以,如果遇到职业不顺,找老架构师帮忙一下,就顺利多了。

推荐阅读

《百亿级访问量,如何做缓存架构设计》

《多级缓存 架构设计》

《消息推送 架构设计》

《阿里2面:你们部署多少节点?1000W并发,当如何部署?》

《美团2面:5个9高可用99.999%,如何实现?》

《网易一面:单节点2000Wtps,Kafka怎么做的?》

《字节一面:事务补偿和事务重试,关系是什么?》

《网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?》

《亿级短视频,如何架构?》

《炸裂,靠“吹牛”过京东一面,月薪40K》

《太猛了,靠“吹牛”过顺丰一面,月薪30K》

《炸裂了…京东一面索命40问,过了就50W+》

《问麻了…阿里一面索命27问,过了就60W+》

《百度狂问3小时,大厂offer到手,小伙真狠!》

《饿了么太狠:面个高级Java,抖这多硬活、狠活》

《字节狂问一小时,小伙offer到手,太狠了!》

《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

掌握Golang匿名函数

一个全面的指南,以理解和使用Golang中的匿名函数 Golang以其简单和高效而闻名,赋予开发人员各种编程范式。其中一项增强代码模块化和灵活性的功能就是匿名函数。在这篇正式的博客文章中,我们将踏上探索Golang匿名函数深度的旅程。通过真实世…

机器学习之Sigmoid函数

文章目录 Sigmoid函数是一种常用的数学函数,通常用于将实数映射到一个特定的区间。它的形状类似于"S"形状曲线,因此得名。Sigmoid函数在机器学习、神经网络和统计学中经常被使用,主要用于二元分类和处理概率值。 Sigmoid函数的一般…

【蓝桥】契合匹配

一、题目 1、题目描述 小蓝有很多齿轮,每个齿轮的凸起和凹陷分别用一个字符表示,一个字符串表示一个齿轮。 如果这两个齿轮的对应位分别是同一个字母的大小写,我们称这个两个齿轮是契合的。 例如:AbCDeFgh 和 aBcdEfGH 就是契合…

基于html+js编写的生命游戏

前言 本文将介绍一个基于htmljs的生命游戏,该项目只有一个html代码,无任何其他以来,UI方面采用了vueelement-plus进行渲染,游戏的界面基于canvas进行渲染,先来看一下成果。 我不知道游戏规则有没有写错,感…

Vue-3.2自定义创建项目

基于VueCli自定义创建项目架子 选择第三个 空格选中,再空格取消 选择vue2 其实就是mode模式,之后再去修改就可以,history和hash 选择less 无分号规范(标准化),目前最流行的 将配置文件放在单独的文件中 是否…

Linux环境配置安装Redis

Windows版本因官网不在提供与支持,以下基于linux环境安装 前提: 1.一台linux服务器 2.服务器已安装gcc 安装 1、官网下载 https://redis.io/download/ 对应压缩包 2、上传压缩包至服务器并解压缩 tar -zxvf redis-stable.tar.gz3、cd 至该目录下 4、…

双周总结#002 - 红树林

红树林公园,一棵单独生长在海岸边的树,下面一根根树立的幼苗,是从它的根茎上生长出来的。傍晚落潮后,会有一只只小螃蟹在这里浪荡。当然,也会有海鸟在这里进食。 文档 深入了解 Commonjs 和 Es Module1 Web 开发中&am…

两道关于顺序表的经典算法

文章目录 力扣:[移除元素](https://leetcode.cn/problems/remove-element/)[力扣:88. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) 力扣:移除元素 题目 给你一个数组 nums 和一个值 val,你需要 原地 移…

JAVA实战项目 超市商品管理系统

师傅开发的实战项目,感觉不错,拿出来分享分享。 目录 一、摘要1.1 简介1.2 项目录屏 二、研究内容三、系统设计3.1 用例图3.2 时序图3.3 类图3.4 E-R图 四、系统实现4.1 登录4.2 注册4.3 主页4.4 超市区域管理4.5 超市货架管理4.6 商品类型管理4.7 超市商…

JDBC操作BLOB类型字段

JDBC中Statement接口本身不能直接操作BLOB数据类型 操作BLOB数据类型需要使用PreparedStatement或者CallableStatement(存储过程) 这里演示通过PreparedStatement操作数据库BLOB字段 设置最大传入字节 一般是4M 可以通过以下命令修改 set global max_allowed_packet1024*1…

C语言,洛谷题,赦免战俘

先上答案&#xff0c;再对答案进行解释&#xff1a; #include <stdio.h> int arr[1025][1025] { 0 }; void fun(int bian,int x ,int y) {if (bian 2)//进入if再出去if之后&#xff0c;结束递归&#xff0c;因为递归在else里面{arr[x][y] 0;}else{int i 0;int j 0;…

【Linux】:Linux中Shell命令及其运行原理/权限的理解

Shell命令以及运行原理 Linux严格意义上说的是一个操作系统&#xff0c;我们称之为“核心&#xff08;kernel&#xff09;“ &#xff0c;但我们一般用户&#xff0c;不能直接使用kernel 而是通过kernel的“外壳”程序&#xff0c;也就是所谓的shell&#xff0c;来与kernel沟通…

SpringCloud之Gateway整合Sentinel服务降级和限流

1.下载Sentinel.jar可以图形界面配置限流和降级规则 地址:可能需要翻墙 下载jar文件 2.引入maven依赖 <!-- spring cloud gateway整合sentinel的依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-s…

从基础到卷积神经网络(第14天)

1. PyTorch 神经网络基础 1.1 模型构造 1. 块和层 首先&#xff0c;回顾一下多层感知机 import torch from torch import nn from torch.nn import functional as Fnet nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))X torch.rand(2, 20) # 生成随机…

苍穹外卖(七) Spring Task 完成订单状态定时处理

Spring Task 完成订单状态定时处理, 如处理支付超时订单 Spring Task介绍 Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑。 应用场景: 信用卡每月还款提醒 火车票售票系统处理未支付订单 入职纪念日为用户发送通知 点外…

C++:多态讲解

多态 1.多态的概念2.多态的定义和实现2.1多态构成条件2.2虚函数2.3虚函数的重写(覆盖)2.4 C11 override 和 final2.5重载、重写(覆盖)、隐藏(重定义)的对比 3.抽象类4.多态的原理5.单继承和多继承关系的虚函数表5.1单继承5.2多继承5.3菱形继承和多态 1.多态的概念 多态的概念&…

【Vue面试题二十三】、你了解vue的diff算法吗?说说看

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;你了解vue的diff算法吗&…

MFC-对话框

目录 1、模态和非模态对话框&#xff1a; &#xff08;1&#xff09;、对话框的创建 &#xff08;2&#xff09;、更改默认的对话框名称 &#xff08;3&#xff09;、创建模态对话框 1&#xff09;、创建按钮跳转的界面 2&#xff09;、在跳转的窗口添加类 3&#xff0…

树莓派:64位 RPI OS(Bookworm) 更换国内源

几天前新的RPI OS发布了。官方的发版说明里明确注明已经基于Debian Bookworm了。总的来说切到国内源&#xff08;清华&#xff09;跟Bullseye差不多&#xff0c;细节上只有一丢丢不同&#xff08;non-free变成了non-free-firmware&#xff09;。 老规矩&#xff0c;仍然是修改…

二、深度测试(Z Test)

1.是什么 ①从渲染管线出发 ②书面上理解 所谓深度测试&#xff0c;就是针对当前对象在屏幕上&#xff08;更准确的说是frame buffer&#xff09;对应的像素点&#xff0c;讲对象自身的深度值与当前该像素点缓存的深度值进行比较&#xff0c;如果通过了&#xff0c;本对象再改…