摘要:介绍在SaaS场景下如何技术选型,SaaS架构设计中关键的技术点等内容。

本文分享自华为云社区《DTSE Tech Talk丨第2期:1小时深度解读SaaS应用系统设计》,作者: 华为云社区精选。

DTSE Tech Talk是华为云开发者联盟推出的技术公开课直播栏目,解读云上前沿技术,畅聊开发应用实践。由专家团队授课,答疑解惑,助力开发者使用华为云开放能力进行应用构建、技术创新。

围绕当下许多企业青睐的SaaS应用开发,华为云开发者技术服务工程师在DTT首期带来主题为《SaaS云原生应用典型架构》技术分享,点击回看。本期直播为 “SaaS应用开发系列”课程第2期 ​《SaaS应用技术架构设计》,以下为精彩内容回顾。

1.SaaS技术栈选型,如何取最优解

SaaS技术栈作为SaaS应用的核心,可决定应用程序的可扩展性、功能性和可行性。 因此,如何根据企业需求做出关于最佳技术堆栈的决策,是首要思考的问题。根据SaaS技术栈选择原则,企业在选择的过程中,需要注意以下四点:

  • 按需引入
    按照SaaS技术架构建设与演进需求节奏,引入技术栈。例如企业前期业务量不是很大,基础性运维能力就能支撑业务。随着业务规模的扩大,租户数量增加,底层资源消耗也逐渐增加,企业就需要建立运维监控系统数据分析平台支持海量的日志采集、存储、分析及查询等需求。
  • 可驾驭
    尽可能选择团队熟悉、成熟技术栈,企业在使用CNCF的开源软件过程中,若出现问题,借助有强有力社区支持,就能够得到及时反馈,帮助自己解决问题。
  • 适当超前
    技术栈容量选择能够满足未来1~2年业务需求,在选择的过程中要考量架构的可扩展性,方便后期的扩容操作。
  • 性价比
    技术栈考虑实施的性价比,如部署、维护成本等。

以 SaaS-Housekeeper项目为例,SaaS-Housekeeper项目是华为云开发者团队基于SaaS项目技术支持实践。从上图可以看出,在开发过程中,华为云选择基于JAVA开发的Spring Boot和Spring-Cloud技术栈进行微服务应用开发,引入微服务引擎CSE解决配置热更新的场景问题,应用部署方面选择具有弹性伸缩能力的K8S集群。在数据库方面,选择的是RDS服务解决后期扩容问题,以及Redis解决分布式缓存问题。在基础运维方面,通过LTS解决日志采集、查询分析服务。

2 CSE+CCE:简化企业微服务开发,加快应用容器化部署

随着云原生技术的不断完善和发展,云原生技术及架构在架构演进、技术选型、构建现代化应用等工作中产生了深刻的影响。在企业上云的趋势下,越来越多的企业和开发者开始把业务与技术向云原生演进。在技术栈选型上,企业也趋向于容器化、微服务化以及基于云化应用中间件、数据库构建应用。

2.1 微服务开发

云原生+AI+大数据时代,微服务化和容器化是应用现代化的基本特点。开源Spring Cloud为开发人员构建微服务架构提供了完整的解决方案,成为很多开发者的选择。但基于Spring Cloud组件构建微服务平台,需要集成验证Hystrix、Ribbon、Zipkin、Prometheus等大量三方组件,门槛高,学习周期长。

华为云微服务引擎CSE是用于微服务应用的云中间件,为用户提供注册发现、服务治理、配置管理等高性能和高韧性的企业级云服务能力。而且,CSE可无缝兼容SpringCloud、ServiceComb等开源生态。用户也可结合其他云服务,快速构建云原生微服务体系,实现微服务应用的快速开发和高可用运维。CSE具有SpringCloud应用零门槛上云;治理能力开箱即用;实现配置分发同步及版本管理三大特点。

在《DTSE Tech Talk技术公开课丨第1期:要想不踩SaaS那些坑,得先了解“SaaS架构”》中提到,用户在微服务开发阶段与华为云CSE微服务引擎对接,只需导入华为云微服务SDK即可享受何种服务治理和管控能力,相比较开源Spring Cloud既简单又方便。基于Spring Cloud微服务开发的项目,对接CSE只需在Pom里把Spring Cloud换成Spring Cloud华为的组件。

一步迁移SpringCloud应用

2.2 容器化部署

华为云容器引擎CCE是基于业界主流的Docker和Kubernetes开源技术构建的容器服务,提供众多契合企业大规模容器集群场景的功能。而且,云容器引擎深度整合华为云高性能的计算(ECS/BMS)、网络(VPC/EIP/ELB)、存储(EVS/OBS/SFS)等服务,并支持GPU、NPU、ARM、FPGA等异构计算架构,支持多可用区(Available Zone,简称AZ)、多区域(Region)容灾等技术构建高可用Kubernetes集群。

作为全球首批通过CNCF基金会Kubernetes一致性认证的容器服务,CCE在系统可靠性、高性能、开源社区兼容性等多个方面具有独特的优势,满足企业在构建容器云方面的各种需求。CCE具有如下价值:

  • 提高资源利用率(成本优化)

    • 榨干资源价值(虚机规格固定,容器规格灵活)
    • 秒级弹性伸缩(虚机分钟级弹性,容器秒级弹性)
    • 根据部分企业案例,在总体业务增长 20% 的情况下,综合节省成本可达到30%
  • 提高运维效率(节省人力)
    • 自动化(CI/CD,自动弹性伸缩)
    • 上线/升级时间,从小时级变成分钟级

3 在多租户模式下,如何选择设计路由策略

实现多租户的设计原理,实际上是需要做到应用层和数据层的共享和隔离,然后通过一种路由机制,可以根据不同的策略,将用户的请求路由到指定的计算集群即可。

3.1 应用层多租模式

如果企业已经规划好了SaaS应用多租和开发内容,那是时候考虑SaaS应用部署环境的问题。

在SaaS应用场景,线下IDC部署显然不是一个很好的选择,相较于基于云上虚拟机部署模式,采用Kubernetes集群模式部署SaaS应用应该是首选。云容器引擎CCE提供高度可扩展的、高性能的企业级Kubernetes集群,充分利用云上弹性能力、丰富存储类型,支持实现SaaS业务降成本、动态扩容、高可靠性等需求。云容器引擎CCE可以为云上构建SaaS应用提供不同多租隔离模式:

基于CCE Pod模式,租户间通过Pod进行隔离,每个Pod包含多个Container。

  • 优点:简单,成本低,共享运行态资源,适用应用无法拆分的单体应用
  • 缺点:隔离性弱、安全性弱、服务出现不可用状态会影响所有租户

基于CCE NameSpace模式,不同租户的业务微服务部署在同一个CCE集群中的不同的NameSpace中,支持资源的逻辑隔离;不同租户的路由、升级等策略部署到同一个配置中心的不同的配置组中,支持策略逻辑隔离;

  • 优点:租户逻辑隔离,成本适中,隔离性适中,安全性适中
  • 缺点:多租资源命名、管理等较复杂

基于环境模式,不同租户的业务微服务在不同的环境中

  • 优点:租户基础设施物理隔离,成本高、隔离性高、安全性高
  • 缺点:多集群,多配置中心,管理最复杂

在共享资源池模式下,Kubernetes集群为SaaS应用开发者提供的namespace隔离方式能够带来很大的帮助。华为云容器引擎CCE在namespace粒度提供了网络隔离、资源配额限制以及RBAC权限管理策略等租户管控策略。

网络隔离方面,CCE基于Kubernetes的网络策略功能进行了加强,通过配置网络策略,允许在同个集群内实现网络的隔离,也就是可以在某些实例(Pod)之间架起防火墙。在华为云上提供了vpc网络和容器隧道网络,仅“容器隧道网络”模式的集群支持网络隔离。

资源配额限制方面,通过设置命名空间级别的资源配额,实现多租户在共享集群资源的情况下限制团队、租户可以使用的资源总量,包括限制命名空间下创建某一类型对象的数量以及对象消耗计算资源(CPU、内存)的总量。

在业务比较平稳的SaaS系统中,可以用于租户业务资源的隔离,CCE集群支持CPU/内存配额限制、网络隔离、QoS限速等策略。以QoS限速策略为例,由于不同租户资源可能部署在同一节点上,导致不同业务容器之间存在带宽抢占的情况,容易造成业务抖动。为了解决这个问题,您可以通过对Pod间互访进行QoS限速来解决这个问题当然,也可以通过亲和性策略,将不同租户的pod调度到不同节点上,避免网络资源抢占。

3.2 数据层多租模式

在SaaS架构中,注重的就是数据的 “独立性”,也是隔离性。如何在共有的一套系统架构与服务,仍可以保障客户的数据相对独立的正常使用。 一般地,以支持多租户的运行技术总体可分为三种:共享数据库,共享Schema;共享数据库,基于Schema隔离;数据库隔离

  • 共享数据库,共享Schemas
    所有租户数据保存在共享数据库的共享表中,通过租户标识区分租户记录;每个租户访问相同的数据库和表,通过应用层数据访问控制保证数据的安全访问/隔离。

    • 优点:最低的资源成本、最大限度的使用DB资源;DB的维护管理成本降低;
    • 缺点:共享数据库,共享Schema无法保证Schema扩展互相透明,增加了扩展的复杂度;数据隔离在应用层实现,隔离性较弱;租户的数据的备份恢复只能按记录操作;
  • 共享数据库,基于Schemas隔离
    每个租户的数据保存在自己的表中,但共享同一个物理数据库,每个租户连接相同的数据库(相同的连接配置);但访问不同的表(Schema设置)。

    • 优点:租户间有较弱的关系、影响较小(仍然可以轻松扩展、通过DB User保证数据安全和隔离;降低资源和数据库维护成本);
    • 缺点:不同租户访问相同的数据库,租户间的访问性能相互影响;数据备份恢复相互干扰;单DB租户数量受限;
  • 基于数据库隔离
    每个租户的数据保存在一个物理上独立的数据库实例中(RDS),每个租户连接自己特定的数据库。

    • 优点:租户间完全没有影响(轻松扩展、数据备份恢复简单、数据安全的隔离);
    • 缺点:更高的资源成本和数据库的维护管理成本

当了解租户创建部分如何设计之后,我们需要考虑租户识别和租户路由问题。

3.3 前端路由策略

首先介绍前端路由策略。通常域名租户映射和HTTP请求参数支持实现租户ID向后端传递。在租户ID传递给后端之后,如存在共享资源池的情况,涉及租户上下文传递,需要配置路由策略,如使用租户路由插件,实现不同租户在逻辑隔离条件下,实现资源安全访问。

根据访问方式在应用入口可以使用路由策略方案有两种,分别如下:

  • 方案一:不同租户分配到不同的域名,用不同租户域名实现租户区分,通过域名映射,路由到不同租户应用。
  • 方案二:采用统一域名,通过不同请求参数,如增加租户字段,进行租户区分,将租户请求路由到后端应用。

3.4 后端租户路由策略

在共享资源池模式下,还要考虑后端租户路由情况。在多租户条件下,租户间是通过代码逻辑方式实现隔离,DNS解析不同租户的域名是解析到同一个IP地址,即多个用户访问同一套资源环境,提供安全稳定的路由策略,将用户请求路由精确路由到租户所属的资源区,如下图所示:

  • 租户标识可以是域名、租户id或者app id等,租户标识进入网关时需要做安全认证,避免横向夺权;
  • 租户标识从访问入口起就要一路携带,直到租户隔离区域的终点,例如独享资源模式最简单,进入网关就可以去掉标识;如果是数据库隔离的,进入数据库就可以去掉标识;如果数据库共享根据字段隔离,标识就要放到record中;
  • 根据租户标识,服务把租户请求路由到不同的链路中,路由可以是云解析器,负载均衡或者服务本身,华为云开源的租户插件saas-tenant-router-starter可以帮助我们在服务中对租户请求进行路由。

4 SaaS-housekeeper项目实践解读

服务内的租户路由,本质上是把租户标识放在每个请求的维度。以Java为例,传统的Java web项目,可直接通过Request Context Holder获取租户标识,这个类使用了Thread Local作为线程隔离。在使用多线程的项目中,例如reactor或项目中使用了hystrix线程池隔离模式,threadlocal变量会在线程传中丢失,此类情况可选择与作用范围对应的容器,例如可使用Hystrix Request Valuable Default作为标识的存储容器,自主代码也可依赖其线程池创建的方式来传递租户标识以及租户相关信息。

public class TenantContext {private static final HystrixRequestVariableDefault<Map<String, String>> TENANT_KEY =new HystrixRequestVariableDefault<>();public static String getDomain() {return Optional.ofNullable(TENANT_KEY.get()).orElse(new HashMap<>()).get(Constants.TENANT_DOMAIN);}
/*** 初始化时设置domain,直到会话结束时才会销毁** @param domain 标识*/public static void setDomain(String domain) {HystrixRequestContext.initializeContext();Map<String, String> variableMap = new HashMap<>(2);variableMap.put(Constants.TENANT_DOMAIN, domain);// 默认主库variableMap.put(Constants.DB_STRATEGY, Constants.DB_SLAVE);TENANT_KEY.set(variableMap);}

SaaSHousekeeper项目以域名作为租户标识, TENENT_DOMAIN是请求的域名,每个请求到达服务时,过滤器会拦截请求,然后把租户标识存放到HystrixRequestValuableDefault中,如果非用户请求,也需要把租户标识初始化。

解决租户存储的问题之后,接下来需要解决租户路由标识是如何在服务中如何传递的。在实现租户标识传递时,可能会遇到不同场景,如跨微服务调用以及MQ、ES访问等,需要采用合理的策略传递租户标识。如:

情形一:Open Feign调用其他微服务时带上租户标识

/*** feign调用请求头添加租户标识** @since 2022-02-28*/
public class FeignRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {requestTemplate.header("tenantDomain", TenantContext.getDomain());}
}

在这个场景中,Open Feign启动一个拦截器,通过这个拦截器能够拦截用户请求,然后获取租户的唯一标识,通过Template字段,存储到请求头里。

情形二:使用MQ时,在消息头中传递租户标识

rabbitTemplate.setBeforePublishPostProcessors(new MessagePostProcessor() {@Overridepublic Message postProcessMessage(Message message) throws AmqpException {String tenantDomain = TenantContext.getDomain();log.info("发送消息前的tenantDomain : " + tenantDomain);// 消息发送前保存租户标识Optional.ofNullable(tenantDomain).ifPresent(domain -> message.getMessageProperties().setHeader("tenantDomain", tenantDomain));return message;}
});

在获取标识之后,如何映射到后端的数据源中,让请求与数据源建立链接。面对这个情况,租户路由在进行数据请求时,通过拦截器拦截数据请求,从请求上下文中获取租户标识,根据标识从mapping表中获取该租户绑定的数据源以及schema,然后把这些连接设置到数据请求连接中,再继续下一步。具体操作步骤如下:

第一步:获取绑定的数据源组

public DataSourceGroup getDataSourceGroup(String key) {String groupName = defaultSource;if (bindingMap != null && !bindingMap.isEmpty() && bindingMap.containsKey(key)) {// 配置指定的数据源有效时groupName = bindingMap.get(key).getGroupName();}groupName = StringUtils.isBlank(groupName) ? defaultSource : groupName;// 配置显示绑定的数据源不存在,直接抛出异常Optional.ofNullable(groupName).orElseThrow(() -> new RoutingException(key + " No Binding Data Source!"));DataSourceGroup dataSourceGroup = groupMap.get(groupName);// 指定数据源无效时, 从扩展的数据源适配器中获取数据源组Optional.ofNullable(dataSourceGroup).orElseThrow(() -> new RuntimeException(key + " Binding Data Source group not exists"));log.warn("{} select DataSource {} success!", key, groupName);return dataSourceGroup;
}

第二步:获取schema

@Override
public Object intercept(Invocation invocation) throws Exception {Connection coon = (Connection) invocation.getArgs()[0];// 获取传递的租户标识String domain = TenantContext.getDomain();// 租户标识获取配置的对应schema以及数据源绑定信息DataSourceBindingProperty bindingProperty = dynamicRoutingDataSource.getBidingProperty(domain);String catalog = null;if (bindingProperty != null && StringUtils.isNotBlank(bindingProperty.getSchema())) {// 配置指定的schema, 优先级最高catalog = bindingProperty.getSchema();} else {// 未指定schema, 则使用当前生效的适配器逻辑获取schema, 默认适配器规则为使用catalog = schemaAdapter.getSchema(domain);}if (catalog != null) {log.warn("{} select schema {}", domain, catalog);coon.setCatalog(catalog);}return invocation.proceed();
}

示例代码来源: saas-tenant-router-starter

在SaaS应用场景中,经常遇到更新一些参数或者新增一些信息连接。在租户参数配置更新实际操作方面会遇到一些问题,当遇到增加新租户或租户的资源信息改变时,需要动态改变租户路由的映射表(variableMap)。

如果Mapping表使用数据库或redis存储,io消耗会比较大,建议使用本地存储。还可以用事件机制来刷新Mapping表,例如spring cloud bus 事件和k8s configmap热更新的event模式。

方法一:K8s configmap模式

server:port: 8300
spring:profiles:active: localapplication:name: saas-housekeeper-ordercloud:kubernetes:reload:enabled: true #配置更新时重新启动开关打开mode: polling #主动拉取模式strategy: eventconfig:enabled: truenamespace: housekeepername: saas-housekeeper-config

方法二:基于spring cloud bus实现消息总线

在数据库应用场景中,在增加新租户的时候,系统必须为新租户建立新的数据库,而当数据库版本更新,系统也必须为每个租户的数据库更新数据,必须借助一些工具来降低运维复杂度。在SaaSHousekeeper项目中,采用了flyway作为数据版本的管理工具,为租户创建新数据库、插入基础数据,维护数据库版本的更新。

Flyway.configure().dataSource(url, username, password).schemas(schema).load().migrate();

虽然SaaS-housekeeper项目是一个标准化方案,但是它能满足用户在应用的过程中的定制化调整。

在SaaS-housekeeper项目的家政业务场景中,A租户想做清洁服务,B租户想做月子服务,服务的内容和规格和收费计量都不一样,怎么把这些内容让租户自己定义呢?这些表的设计就是把服务的定义,规格的定义,选项的定义,各种组合的价格都变成客户可自配置的内容。

SaaSHousekeeper项目在服务发布的设计上,就是把家政业务场景的元数据定义交给租户,根据租户需求配置。

SaaSHousekeeper项目不仅在后端拥有定制化功能,在前端同样也可以进行定制化调整。以前端页面为例,很多用户希望自己的应用展示企业或个人自己独特的风格,SaaSHousekeeper项目除了app定制,小程序定制或web模板定制外,也可以在同一个web前端配置不同的展示风格,SaaS-housekeeper项目的SaaS应用前端可在用户根据域名登陆时获取对应租户的设定来配置风格主题

为了更好的推动云原生SaaS应用开发,华为云开发者团队基于SaaS项目技术支持实践,沉淀了SaaS应用开发相关套件中,包括微服务架构、多租隔离设计、多租户路由、数据存储多租设计、数据源管理等,希望能够为企业级开发者提供SaaS应用改造和技术构建升级提供技术参考。如果您在云原生应用开发或者技术选型等过程中有任何技术问题,都可以给华为云开发者技术团队提issues,我们将及时响应您的需求。也欢迎来自企业、个人开发者参与内容贡献。

相关资料

应用开发文档:文档中心
参考示例代码:HuaweiCloudDeveloper: 为开发者提供SaaS、云容器、微服务、serverless、AI等应用构建的技术参考文档、samples代码,如果您在云原生应用开发或者技术选型等过程中有任何技术问题,都可以给华为云开发者技术团队提issues,我们将及时响应您的需求。也欢迎来自企业、个人开发者参与内容贡献。
问题咨询和专家服务预约(需注册华为云账号):https://support.developer.huaweicloud.com/feedback/?ticket=ST-5385866-mPu9vjwIeAGISrz1rXBAdwt7-sso

下期预告

本期课程,主要给大家介绍了在saas场景下,如何技术选型,saas架构设计中关键的技术点等内容。
下节课,我们将给大家深入介绍多租路由开源插件能力,帮助企业快速实现saas化改造和技术架构升级。8月25日,我们不见不散。

点击关注,第一时间了解华为云新鲜技术~

DTSE Tech Talk丨第2期:1小时深度解读SaaS应用系统设计相关推荐

  1. DTSE Tech Talk丨第3期:解密数据隔离方案,让SaaS应用开发更轻松

    摘要:解读云上前沿技术,畅聊开发应用实践.专家团队授课,答疑解惑,助力开发者使用华为云开放能力进行应用构建.技术创新. 围绕当下许多企业青睐的SaaS应用开发,华为云DTSE技术布道师李良龙为大家带来 ...

  2. 免费教材丨第56期:《深度学习导论及案例分析》、《谷歌黑板报-数学之美》

    小编说  离春节更近了!  本期教材        本期为大家发放的教材为:<深度学习导论及案例分析>.<谷歌黑板报-数学之美>两本书,大家可以根据自己的需要阅读哦! < ...

  3. 免费教材丨第58期:机器学习相关汇总资料大放送(中)

    小编说  上期为大家发放的教材为:经过整理的机器学习相关的经典书籍及经典论文,本期将继续为大家发放哦! 本期教材 本期为大家发放的教材为:经过整理的机器学习相关的教学视频和讲义,可能会跟之前的有重复, ...

  4. 未雨绸缪,迎接运维新时代—— Tech Neo第十六期技术沙龙

    运维发展历程与工业革命异曲同工,工业的三次革命分别是机械化.电气化与信息化,运维则是原始手工.脚本与自动化工具.那么工业4.0悄然来临的今天,智能化又将会给运维带来哪些影响?坦白讲,AIOps是新概念 ...

  5. 免费教材丨第55期:Python机器学习实践指南、Tensorflow 实战Google深度学习框架

    小编说  时间过的好快啊,小伙伴们是不是都快进入寒假啦?但是学习可不要落下哦!  本期教材  本期为大家发放的教材为:<Python机器学习实践指南>.<Tensorflow 实战G ...

  6. 免费教材丨第52期:人工智能(复杂问题求解的结构和策略)、人工智能哲学

    小编说  过去1个月里,因为工作原因未能按时给大家发放教材,请大家谅解,从本周开始,我们正常发放哦,仍旧是每周一期,每期2本课程,欢迎大家按需领取哦! 另外,给大家做个预告,我们的留言赠实体书活动也将 ...

  7. 免费教材丨第51期:数学基础课程----概率论教程、机器学习中的数学基础

    小编说 过去几个月里,有不少人联系我,向我表达他们对人工智能.数据科学.对利用机器学习技术探索统计规律性,开发数据驱动的产品的热情.但是,我发现他们中有些人实际上缺少为了获取有用结果的必要的数学直觉和 ...

  8. 免费教材丨第49期:数学基础课程----漫画线性代数、微积分超入门

    小编说 彭亮老师的<深度学习基础>和<深度学习进阶>共计56讲的视频课程已经发放结束了,接下来我们发放什么教材呢? 过去几个月里,有不少人联系我,向我表达他们对人工智能.数据科 ...

  9. 免费教材丨第48期:业界大牛中文教学视频《深度学习:进阶》第25-28讲

    小编说 我们将继续发放彭老师的<深度学习:进阶>课程,本期发放第25-28讲,本教材由麦子学院提供,现表示感谢.本教学视频为中文教学,代码讲解为主,通俗易懂哦! 彭亮简介 美国犹他州立大学 ...

最新文章

  1. vue部署到服务器 接口调用不了_Python 调用 Azure API 实现服务器自动部署
  2. Linux实用命令总结
  3. 在Forms验证模式下,实现多个站点(SubDomain相同)共享同一用户登录状态
  4. 百里香Spring测试的意见
  5. 多线程蜂鸣器研究,友善之臂Smart210开发版
  6. 自学python能学成吗-自学Python能学会吗 零基础怎么学
  7. 海森矩阵和半正定矩阵
  8. 解决启动eureka报错Unable to start web ... nested exception is org.springframework.boot.web.server.WebS
  9. 一线二线城市工作的区别
  10. 苹果手机各种型号图片_八款iPhone详细规格参数对比 你会买哪款?
  11. Python模拟随机游走
  12. 【墨菲安全实验室】“Dirty Pipe”的故事-Linux 内核提权漏洞 (CVE-2022-0847)
  13. php解决时间2038问题,PHP实例:关于PHP转换超过2038年日期出错的问题解决
  14. qcustomplot时间坐标轴画直线_QCustomplot使用分享(六) 坐标轴和网格线
  15. 组合最优化——线性规划基本定理
  16. 解决 raise ReadTimeoutError(self._pool, None, ‘Read timed out.‘)
  17. 针对网页开发者的滚动锚定功能
  18. 微信支付常见错误和统一下单错误码详情
  19. hash,hashcode,hashmap以及bucket怎么理解
  20. 什么是WEB服务器、应用服务器

热门文章

  1. linux c++ 获取当前时间毫秒_Linux内核中的形形色色的“钟表”,你了解多少?
  2. 聊聊Dubbo(一):为何选择
  3. TIPTOP 4GL 弹窗
  4. C#使用WinAPI中 WinExec 调用外部exe程序
  5. 美化windows 完美仿真Vista -- Vista风格包4.0
  6. 极乐小程序榜单(第六期)
  7. linux 半条命补丁,半条命中文字幕补丁
  8. 目录 -- Vue.js 3.0 企业级管理后台开发实战 基于Element Plus
  9. 零基础通关PMP!看我怎么学?
  10. Arduino UNO v3改进版 CH340G 驱动安装-windows10/11