注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

三个月内遭遇的第二次比特币勒索

早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错. 用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消. To recover your lost Dat...
继续阅读 »

早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错.



用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消.




To recover your lost Database and avoid leaking it: Send us 0.05 Bitcoin (BTC) to our Bitcoin address 3F4hqV3BRYf9JkPasL8yUPSQ5ks3FF3tS1 and contact us by Email with your Server IP or Domain name and a Proof of Payment. Your Database is downloaded and backed up on our servers. Backups that we have right now: mm_wiki, shuang. If we dont receive your payment in the next 10 Days, we will make your database public or use them otherwise.



(按照今日比特币价格,0.05比特币折合人民币4 248.05元..)


大多时候不使用该服务器上安装的mysql,因而账号和端口皆为默认,密码较简单且常见,为在任何地方navicat也可连接,去掉了ip限制...对方写一个脚本,扫描各段ip地址,用常见的几个账号和密码去"撞库",几千几万个里面,总有一两个能得手.


被窃取备份而后删除的两个库,一个是来搭建该wiki系统,另一个是用来亲测mysql主从同步,详见此篇,价值都不大




实践告诉我们,不要用默认账号,不要用简单密码,要做ip限制。…



  • 登录服务器,登录到mysql:



mysql -u root -p





  • 修改密码:


尝试使用如下语句来修改



set password for 用户名@yourhost = password('新密码');



结果报错;查询得知是最新版本更改了语法,需用



alter user 'root'@'localhost' identified by 'yourpassword';




成功~


但在navicat里,原连接依然有效,而输入最新的密码,反倒是失败



打码部分为本机ip


在服务器执行


-- 查询所有用户


select user from mysql.user;


再执行


select host,user,authentication_string from mysql.user;



user及其后的host组合在一起,才构成一个唯一标识;故而在user表中,可以存在同名的root


使用


alter user 'root'@'%' identified by 'xxxxxx';

注意主机此处应为%


再使用


select host,user,authentication_string from mysql.user;

发现 "root@%" 对应的authentication_string已发生改变;


在navicat中旧密码已失效,需用最新密码才可登录


参考:


mysql 5.7 修改用户密码




关于修改账号,可参考此




这不是第一次遭遇"比特币勒索",在四月份,收到了这么一封邮件:



后来证明这是唬人的假消息,但还是让我学小扎,把Mac的摄像头覆盖了起来..


作者:fliter
来源:juejin.cn/post/7282666367239995392
收起阅读 »

让生成式 AI 触手可及:火山引擎推出 NVIDIA NIM on VKE 最佳部署实践

技术行业近来对大语言模型(LLM)的关注正开始转向生产环境的大规模部署,将 AI 模型接入现有基础设施以优化系统性能,包括降低延迟、提高吞吐量,以及加强日志记录、监控和安全性等。然而这一路径既复杂又耗时,往往需要构建专门的平台和流程。在部署 AI 模型的过程中...
继续阅读 »

技术行业近来对大语言模型(LLM)的关注正开始转向生产环境的大规模部署,将 AI 模型接入现有基础设施以优化系统性能,包括降低延迟、提高吞吐量,以及加强日志记录、监控和安全性等。然而这一路径既复杂又耗时,往往需要构建专门的平台和流程。

在部署 AI 模型的过程中,研发团队通常需要执行以下步骤:

环境搭建与配置:首先需要准备和调试运行环境,这包括但不限于 CUDA、Python、PyTorch 等依赖项的安装与配置。这一步骤往往较为复杂,需要细致地调整各个组件以确保兼容性和性能。

模型优化与封装:接下来进行模型的打包和优化,以提高推理效率。这通常涉及到使用 NVIDIA TensorRT 软件开发套件或 NVIDIA TensorRT-LLM 库等专业工具来优化模型,并根据性能测试结果和经验来调整推理引擎的配置参数。这一过程需要深入的 AI 领域知识,并且工具的使用具有一定的学习成本。

模型部署:最后,将优化后的模型部署到生产环境中。对于非容器化环境,资源的准备和管理也是一个需要精心策划的环节。

为了简化上述流程并降低技术门槛,火山引擎云原生团队推出基于 VKE 的 NVIDIA NIM 微服务最佳实践。通过结合 NIM 一站式模型服务能力,以及火山引擎容器服务 VKE 在成本节约和极简运维等方面的优势,这套开箱即用的技术方案将帮助企业更加快捷和高效地部署 AI 模型。

AI 微服务化:NVIDIA NIM

NVIDIA NIM 是一套经过优化的企业级生成式 AI 微服务,它包括推理引擎,通过 API 接口对外提供服务,帮助企业和个人开发者更简单地开发和部署 AI 驱动的应用程序。

NIM 使用行业标准 API,支持跨多个领域的 AI 用例,包括 LLM、视觉语言模型(VLM),以及用于语音、图像、视频、3D、药物研发、医学成像等的模型。同时,它基于 NVIDIA Triton™ Inference Server、NVIDIA TensorRT™、NVIDIA TensorRT-LLM 和 PyTorch 构建,可以在加速基础设施上提供最优的延迟和吞吐量。

为了进一步降低复杂度,NIM 将模型和运行环境做了解耦,以容器镜像的形式为每个模型或模型系列打包。其在 Kubernetes 内的部署形态如下:


NVIDIA NIM on Kubernetes

火山引擎容器服务 VKE(Volcengine Kubernetes Engine)通过深度融合新一代云原生技术,提供以容器为核心的高性能 Kubernetes 容器集群管理服务,可以为 NIM 提供稳定可靠高性能的运行环境,实现模型使用和运行的强强联合。

同时,模型服务的发布和运行也离不开发布管理、网络访问、观测等能力,VKE 深度整合了火山引擎高性能计算(ECS/裸金属)、网络(VPC/EIP/CLB)、存储(EBS/TOS/NAS)、弹性容器实例(VCI)等服务,并与镜像仓库、持续交付、托管 Prometheus、日志服务、微服务引擎等云产品横向打通,可以实现 NIM 服务构建、部署、发布、监控等全链路流程,帮助企业更灵活、更敏捷地构建和扩展基于自身数据的定制化大型语言模型(LLMs),打造真正的企业级智能化、自动化基础设施。


NVIDIA NIM on VKE 部署流程

下面,我们将介绍 NIM on VKE 的部署流程,助力开发者快速部署和访问 AI 模型。

准备工作

部署 NVIDIA NIM 前,需要做好如下准备:

1. VKE 集群中已安装 csi-nas / prometheus-agent / vci-virtual-kubelet / cr-credential-controller 组件

2. 在 VKE 集群中使用相适配的 VCI GPU 实例规格,具体软硬件支持情况可以查看硬件要求

3. 创建 NAS 实例,作为存储类,用于模型文件的存储

4. 创建 CR(镜像仓库) 实例,用于托管 NIM 镜像

5. 开通 VMP(托管 Prometheus)服务

6. 向 NVIDIA 官方获取 NIM 相关镜像的拉取权限(下述以 llama3-8b-instruct:1.0.0 为例),并生成 API Key

部署

1. 在国内运行 NIM 官方镜像时,为了避免网络访问影响镜像拉取速度,可以提前拉取相应 NIM 镜像并上传到火山引擎镜像仓库 CR,操作步骤如下:


2. Download the code locally, go to the Helm Chart directory of the code, and push Helm Chart to Container Registry (Helm version > 3.7):

下载代码到本地,进入到代码的 helm chart 目录中,把 helm chart 推送到镜像仓库(helm 版本大于 3.7):


3. 在 vke 的应用中心的 helm 应用中选择创建 helm 应用,并选择对应 chart,集群信息,并点击 values.yaml 的编辑按钮进入编辑页


4. 覆盖 values 内容为如下值来根据火山引擎环境调整参数配置,提升部署性能,点击确定完成参数改动,再继续在部署页点击确定完成部署


5. 若 Pod 日志出现如下内容或者 Pod 状态变成 Ready,说明服务已经准备好:


6. 在 VKE 控制台获取 LB Service 地址(Service 名称为-nim-llm)


7. 访问 NIM 服务


The output is as follows:

会有如下输出:


监控

NVIDIA NIM 在 Grafana Dashboard 上提供了丰富的观测指标,详情可参考 Observability

在 VKE 中,可通过如下方法搭建 NIM 监控:

1. 参考文档搭建 Grafana:https://www.volcengine.com/docs/6731/126068

2. 进入 Grafana 中,在 dashboard 菜单中选择 import:


3. 观测面板效果如下:


结语

相比构建大模型镜像,基于 VKE 使用 NVIDIA NIM 部署和访问模型有如下优点:

● 易用性:NIM 提供了预先构建好的模型容器镜像,用户无需从头开始构建和配置环境,配合 VKE 与 CR 的应用部署能力,极大简化了部署过程

● 性能优化:NIM 的容器镜像是经过优化的,可以在 NVIDIA GPU 上高效运行,充分利用 VCI 的硬件性能

● 模型选择:NIM 官方提供了多种大语言模型,用户可以根据需求选择合适的模型,部署在 VKE 中仅需对values.yaml 配置做修改即可

● 自动更新:通过 NGC,NIM 可以自动下载和更新模型,用户无需手动管理模型版本

● 可观测性:NIM 内置了丰富的观测指标,配合 VKE 与 VMP 观测能力开箱即用

目前火山引擎容器服务 VKE 已开放个人用户使用,为个人和企业用户提供高性能、高可靠、极致弹性的企业级容器管理能力,结合 NIM 强大易用的模型部署服务,进一步帮助开发者快速部署 AI 模型,并提供高性能、开箱即用的模型 API 服务。(作者:李双)

收起阅读 »

好好的短链,url?1=1为啥变成了url???1=1

运营小伙伴突然找到我们说,我们的一个短链有三个? 第一反应就是不可能,但是事实胜于雄辩,还真的就是和运营小伙伴说的一模一样。 到底发生了什么呢?跟着我一起Review一下。 一、URL结构 1.1 URL概述 URL(统一资源定位符)是一个用于标识互联网上资...
继续阅读 »

运营小伙伴突然找到我们说,我们的一个短链有三个?


第一反应就是不可能,但是事实胜于雄辩,还真的就是和运营小伙伴说的一模一样。


到底发生了什么呢?跟着我一起Review一下。



一、URL结构


1.1 URL概述


URL(统一资源定位符)是一个用于标识互联网上资源的地址。一个典型的URL结构通常包括以下几个部分:


image.png



  1. 协议(Scheme) :也称为"服务方式",位于URL的开头,指定了浏览器与服务器之间通信的方式。常见的协议有http(超文本传输协议)、https(安全超文本传输协议)、ftp(文件传输协议)等。

  2. 子域名(Subdomain) :可选部分,位于域名之前,通常用于区分不同的服务或组织。例如,在sub.example.com中,sub是子域名。

  3. 域名(Domain Name) :URL的核心部分,用于唯一标识一个网站。通常是一个组织或公司的名字,如example.com

  4. 端口号(Port) :可选部分,用于指定服务器上的特定服务。如果省略,浏览器将使用默认端口,例如httphttps的默认端口是80和443。

  5. 路径(Path) :指定服务器上的资源位置。路径可以包含多个部分,用斜杠/分隔。例如,在/path/to/resource中,path/to/resource是资源的路径。

  6. 查询字符串(Query String) :可选部分,位于路径之后,用于传递额外的参数或数据。查询字符串以问号?开始,后面跟着一系列的参数,参数之间用和号&分隔。例如,在?key1=value1&key2=value2中,key1key2是参数名,value1value2是对应的值。

  7. 片段标识符(Fragment Identifier) :可选部分,用于指向页面内的特定部分。片段标识符以井号#开始,通常用于锚点链接。例如,在#section2中,section2是页面内的一个锚点。


1.2 URL示例


示例:一个完整的URL示例可能是下面这样的


https://www.xxx.com:8080/path/to/resource?key1=value1&key2=value2#section2

在上面的示例中,详细拆解如下:



https 是协议。
http://www.xxx.com 是域名。
8080 是端口号。
/path/to/resource 是路径。
key1=value1&key2=value2 是查询字符串。
#section2 是片段标识符。



二、URL的意义


URL(统一资源定位符)的意义在于它提供了一种标准化的方法来标识和访问互联网上的资源。它是互联网的基础构件之一,它不仅使得资源的定位和访问变得简单,还支持了互联网的组织、导航、安全和分享等多种功能。以下是URL的几个关键意义:


image.png


这些意义做开发的都懂,不懂的就自己百度吧,这里不做赘述。


三、硬菜:url?1=1为啥变成了url???1=1


3.1 故事背景


我们有一个自己的短链项目,用户访问短链的时候,我们自己服务器会进行重定向,这样的好处是分享出去的链接都是很短的,会有效提升用户的使用体验。


短链触发和服务器的交互流程如下:


sequenceDiagram
用户->>+短链: 点击
短链->>+服务器: 请求
服务器->>+服务器: 找到映射的长链地址
服务器->>+用户: 重定向到长链
用户->>+长链: 请求并得到响应

3.2 事故现场


上面弄清楚了短链的基本触发流程,那我我们看看到底发生了什么。



  • 客户端事故现场截图


image.png



从这个截图就可以明显的看出,这里有三个?,这是不合理的...




  • 数据库存储的事故现场数据截图


image.png


哎,数据库里面只有一个问号吧?


3.3 问题分析和解决方案



  • 问题分析


上面数据库看着正常的,别着急,咱们换个方式看看,我们执行下面这个SQL看看数据存储的实际长度是多少。


SELECT
LENGTH(
CONVERT ( full_link USING utf8 )) AS actual_length
FROM
t_short_link
WHERE
id = '0fcc75b3e1b243c4b36d71b1d58b3b41';

执行结果:


image.png



上面sql执行实际得到的长度是52,但是我们长链的实际长度却是49,那么问题就出来了,数据库里面多了两个我们肉眼看不见的字符,三个问号就是这个来的




  • 解决方案


从上面分析了事故现场,我们已经知道是多了两个字符了,删掉即可。


注意:因为数据库看不到,所以不能直接编辑,可以选择一些可以看到的编辑器编辑之后更新,例如notepad++。


3.4 额外发现


在写文章的时候,我将连接复制到了掘金的MD编辑器,发现这里也是暴露了问题,上面提到的解决方案,大家也是可以复制进来然后删除多余字符的。


image.png


四、总结


程序员大多数都非常自信,相信自己的代码没有bug,相信有bug也不是我的问题,有的时候怼天怼地。


但是真的遇到问题,需要三思而后行,谋定而后动;是不是自己的问题,先检查检查,避免后面发现是自己的问题很尴尬。



希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。


同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。


感谢您的支持和理解!



作者:竹子爱揍功夫熊猫
来源:juejin.cn/post/7399985723674394633
收起阅读 »

太方便了!Arthas,生产问题大杀器

一、一个难查的生产问题一天,小王发现生产环境上偶发性地出现某接口耗时过高,但在测试环境又无法复现,小王一筹莫展😔。小王“幻想”到:如果有个工具能记录生产上各个函数的耗时该多好,这样一看不就知道时间花在哪了?这不是幻想,Arthas 已经帮我们解决了这个问题。在...
继续阅读 »

一、一个难查的生产问题

一天,小王发现生产环境上偶发性地出现某接口耗时过高,但在测试环境又无法复现,小王一筹莫展😔。小王“幻想”到:如果有个工具能记录生产上各个函数的耗时该多好,这样一看不就知道时间花在哪了?
这不是幻想,Arthas 已经帮我们解决了这个问题。在介绍它之前,我们先了解下相关背景。

二、动态追踪

现在互联网和大家生活的各个方面都息息相关。相应地,互联网应用的用户规模也变得越来越大。江湖大了,什么风浪都有。开发者们不断被各种诡异问题打扰,接口耗时过大、CPU 占用过高、内存溢出、只有生产环境会报错......
这些问题出现的概率可能是千分之一、乃至万分之一。如果我们能不修改代码、不修改配置、不重启服务,就能看到程序内部在执行什么,这该多好,再大的问题心里也有底了。
动态追踪技术出现了,它诞生于21 世纪初。Sun Microsystems 公司的工程师在解决一个复杂问题时被繁琐的排查过程所困扰,痛定思痛,他们创造了 DTrace 动态跟踪框架。DTrace 奠定了动态追踪的基础,Bryan Cantrill, Mike Shapiro, and Adam Leventhal 三位作者也多次获得行业荣誉。
动态追踪技术出现的时间早,但 Java 语言相关的调试工具链一直不太完善。直到进入移动互联网时代,Java 的发展才进入了快车道。2018 年,Alibaba 开源 Arthas,Java 的动态追踪才真正好用起来
动态追踪可以看作是构建了一个运行时“只读数据库”,这个数据库内部保存了实时变化的进程运行信息,我们通过调用这个“数据库”开放的接口,就能看到进程内部发生了什么。
经验丰富的读者可能会有疑问,现在微服务都用上了 Skywalking 这样的分布式链路追踪技术,通过它也能分阶段地看到各个部分的执行情况,为什么还需要 Arthas?
Arthas 有两大特点:

  1. 低侵入;不需要程序中进行额外配置,更不需要手动埋点。
  2. 功能强大;Arthas 提供了四十多种命令:从查看线程调用链,到查看输入、输出,到反编译代码等,应有尽有。

对于排查接口耗时长这样的情况,Skywalking 可以和 Arthas 配合起来,先用 Skywalking 定位出异常微服务,再用 Arthas 分析单个进程的情况,找到根因。

三、Arthas常用场景

相信你对动态追踪有了基本的了解,Arthas 可以理解为动态追踪在 Java 领域落地的具体工具。下面以场景助学,大家可以参考这些方案,因事制宜来解决自己的问题。

Arthas 的安装和基础使用见官方文档:Introduction | arthas

3.1.接口慢/吞吐量低

在文章开头,小王就遇到了这个问题。现在小王依靠老道的排查经验确定了 MathGame 服务肯定有问题,但具体的点却找不到。小王仔细学习了这篇文章,决定用 Arthas 分以下三步来排查:

  1. profile 明确整体的耗时情况

    profile 命令支持为应用生成火焰图,在 Arthas 终端输入以下命令:

    # 开始对应用中当前执行的活动采样 30 秒,采样结束后默认会生成 HTML 文件
    [arthas@5555]$ profiler start -d 30

    打开 HTML 文件能看到这样的结构:

    image.png
    火焰图

    MathGame 类下的 run 方法占用了大部分的执行时间,接下来我们看看 run 方法内部的耗时情况。

  2. trace 详细查看单个调用的内部耗时

    [arthas@5555]$ trace --skipJDKMethod false demo.MathGame run

    PrintStream 类 print 方法占据了 87% 的时间,这是 JDK 自带的类,这说明我们程序本身并无耗时问题,但 MathGame 类的 primeFactors 方法抛出了异常,我们可以看看具体的异常,再思考怎么优化。

    image.png
    run方法的trace流

    另外,trace 可以选择性地进行调用拦截,比如设置只拦截大于 20ms 的调用:

    [arthas@5555]$ trace demo.MathGame run '#cost > 20'
  3. watch 查看真实的调用数据

    拦截 primeFactors 方法抛出的异常:

    [arthas@5555]$ watch demo.MathGame primeFactors -e "throwExp"

    image.png
    拦截异常

小王从大到小、逐步分析,找出了问题的原因是 primeFactors 抛出了异常,修正参数后,程序恢复了正常。

3.2.CPU 占用过高

CPU 是程序运行的核心计算资源,一旦出现 CPU 占用过高,必定对大部分用户的访问耗时产生影响。针对这类问题,要定位出有问题的线程,并获取该线程当前执行的代码位置
使用 top + jstack 命令可以定位这类问题(见参考资料三),Arthas 也提供了更便捷的一体化工具:

  1. 定位目标线程

    # 调用线程看板,并刷新数据三次
    [arthas@5555]$ dashboard -n 3

    image.png
    示例程序的CPU占用不算高

    DashBoard 刷新三次后,在最新状态中发现示例程序里自己的线程 “main” 占用不算高。说明程序运行正常。如果是要排错,这里就要找出 CPU 占用最高的用户线程的 ID

  2. 查看目标线程执行的代码位置

    # “1” 是上一步定位到的 main 的线程ID
    [arthas@5555]$ thread 1

    image.png

    线程正在“睡觉”,没什么大问题。

3.3 生产环境的效果和测试不一样

有些时候你发现:测试环境正常,但生产就报错了。这类问题主要靠做好上线流程的管控,但也有可能是打包的依赖库出现冲突,造成程序行为不一致。接下来,我们看看怎么用 Arthas 反编译代码,以及怎么对比依赖库的版本。

  1. 反编译代码
    # demo.MathGame 是目标类的全限定名
    [arthas@5555]$ jad demo.MathGame

image.png

  1. 查看目标类所属的依赖包

    # demo.MathGame 是目标类的全限定名
    [arthas@5555]$ sc -d demo.MathGame

    image.png
    目标类所属的包

    如果这里是依赖包,code-source 还可以显示所属包的版本。这样就可以对比本地的代码,从而在打包时设置正确的依赖版本。

3.4 内存溢出

生产问题中内存溢出也有不小的比例。内存溢出的关键是找出高内存占用的对象。命令行操作会比较麻烦,建议转储 Heap Dump 等文件后,通过 Eclipse Memory Analyzer(MAT) 等工具进行分析。

四、运行 Arthas 报错

在有些运行环境下,Arthas 会出现报错。对于以下两种情况,读者可参照文档解决:

五、参考资料


作者:立子
来源:juejin.cn/post/7308230350374256666

收起阅读 »

SpringBoot 这么实现动态数据源切换,就很丝滑!

大家好,我是小富~ 简介 项目开发中经常会遇到多数据源同时使用的场景,比如冷热数据的查询等情况,我们可以使用类似现成的工具包来解决问题,但在多数据源的使用中通常伴随着定制化的业务,所以一般的公司还是会自行实现多数据源切换的功能,接下来一起使用实现自定义注解的形...
继续阅读 »

大家好,我是小富~


简介


项目开发中经常会遇到多数据源同时使用的场景,比如冷热数据的查询等情况,我们可以使用类似现成的工具包来解决问题,但在多数据源的使用中通常伴随着定制化的业务,所以一般的公司还是会自行实现多数据源切换的功能,接下来一起使用实现自定义注解的形式来实现一下。


基础配置


yml配置


pom.xml文件引入必要的Jar


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
</parent>
<groupId>com.dynamic</groupId>
<artifactId>springboot-dynamic-datasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<mybatis.plus.version>3.5.3.1</mybatis.plus.version>
<mysql.connector.version>8.0.32</mysql.connector.version>
<druid.version>1.2.6</druid.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- springboot核心包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql驱动包 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<!-- lombok工具包 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

管理数据源


我们应用ThreadLocal来管理数据源信息,通过其中内容的get,set,remove方法来获取、设置、删除当前线程对应的数据源。


/**
* ThreadLocal存放数据源变量
*
* @author 公众号:程序员小富
* @date 2023/11/27 11:02
*/

public class DataSourceContextHolder {

private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();

/**
* 获取当前线程的数据源
*
* @return 数据源名称
*/

public static String getDataSource() {
return DATASOURCE_HOLDER.get();
}

/**
* 设置数据源
*
* @param dataSourceName 数据源名称
*/

public static void setDataSource(String dataSourceName) {
DATASOURCE_HOLDER.set(dataSourceName);
}

/**
* 删除当前数据源
*/

public static void removeDataSource() {
DATASOURCE_HOLDER.remove();
}
}

重置数据源


创建 DynamicDataSource 类并继承 AbstractRoutingDataSource,这样我们就可以重置当前的数据库路由,实现切换成想要执行的目标数据库。


import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;

/**
* 重置当前的数据库路由,实现切换成想要执行的目标数据库
*
* @author 公众号:程序员小富
* @date 2023/11/27 11:02
*/

public class DynamicDataSource extends AbstractRoutingDataSource {

public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}

/**
* 这一步是关键,获取注册的数据源信息
* @return
*/

@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}

配置数据库


在 application.yml 中配置数据库信息,使用dynamic_datasource_1dynamic_datasource_2两个数据库


spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 12345
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 12345
driver-class-name: com.mysql.cj.jdbc.Driver

再将多个数据源注册到DataSource.


import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
* 注册多个数据源
*
* @author 公众号:程序员小富
* @date 2023/11/27 11:02
*/

@Configuration
public class DateSourceConfig {

@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource dynamicDatasourceMaster() {
return DruidDataSourceBuilder.create().build();
}

@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource dynamicDatasourceSlave() {
return DruidDataSourceBuilder.create().build();
}

@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>();
// 设置默认的数据源为Master
DataSource defaultDataSource = dynamicDatasourceMaster();
dataSourceMap.put("master", defaultDataSource);
dataSourceMap.put("slave", dynamicDatasourceSlave());
return new DynamicDataSource(defaultDataSource, dataSourceMap);
}
}

启动类配置


在启动类的@SpringBootApplication注解中排除DataSourceAutoConfiguration,否则会报错。


@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)


到这多数据源的基础配置就结束了,接下来测试一下


测试切换


准备SQL


创建两个库dynamic_datasource_1、dynamic_datasource_2,库中均创建同一张表 t_dynamic_datasource_data。


CREATE TABLE `t_dynamic_datasource_data` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`source_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);

dynamic_datasource_1.t_dynamic_datasource_data表中插入


insert int0 t_dynamic_datasource_data (source_name) value ('dynamic_datasource_master');

dynamic_datasource_2.t_dynamic_datasource_data表中插入


insert int0 t_dynamic_datasource_data (source_name) value ('dynamic_datasource_slave');

手动切换数据源


这里我准备了一个接口来验证,传入的 datasourceName 参数值就是刚刚注册的数据源的key。


/**
* 动态数据源切换
*
* @author 公众号:程序员小富
* @date 2023/11/27 11:02
*/

@RestController
public class DynamicSwitchController {

@Resource
private DynamicDatasourceDataMapper dynamicDatasourceDataMapper;

@GetMapping("/switchDataSource/{datasourceName}")
public String switchDataSource(@PathVariable("datasourceName") String datasourceName) {
DataSourceContextHolder.setDataSource(datasourceName);
DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return dynamicDatasourceData.getSourceName();
}
}

传入参数master时:127.0.0.1:9004/switchDataSource/master



传入参数slave时:127.0.0.1:9004/switchDataSource/slave



通过执行结果,我们看到传递不同的数据源名称,已经实现了查询对应的数据库数据。


注解切换数据源


上边已经成功实现了手动切换数据源,但这种方式顶多算是半自动,下边我们来使用注解方式实现动态切换。


定义注解


我们先定一个名为DS的注解,作用域为METHOD方法上,由于@DS中设置的默认值是:master,因此在调用主数据源时,可以不用进行传值。


/**
* 定于数据源切换注解
*
* @author 公众号:程序员小富
* @date 2023/11/27 11:02
*/

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
// 默认数据源master
String value() default "master";
}

实现AOP


定义了@DS注解后,紧接着实现注解的AOP逻辑,拿到注解传递值,然后设置当前线程的数据源


import com.dynamic.config.DataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;

/**
* 实现@DS注解的AOP切面
*
* @author 公众号:程序员小富
* @date 2023/11/27 11:02
*/

@Aspect
@Component
@Slf4j
public class DSAspect {

@Pointcut("@annotation(com.dynamic.aspect.DS)")
public void dynamicDataSource() {
}

@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)) {
DataSourceContextHolder.setDataSource(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
}


测试注解


再添加两个接口测试,使用@DS注解标注,使用不同的数据源名称,内部执行相同的查询条件,看看结果如何?


@DS(value = "master")
@GetMapping("/dbMaster")
public String dbMaster() {
DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
return dynamicDatasourceData.getSourceName();
}


@DS(value = "slave")
@GetMapping("/dbSlave")
public String dbSlave() {
DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
return dynamicDatasourceData.getSourceName();
}


通过执行结果,看到通过应用@DS注解也成功的进行了数据源的切换。


事务管理


在动态切换数据源的时候有一个问题是要考虑的,那就是事务管理是否还会生效呢?


我们做个测试,新增一个接口分别插入两条记录,其中在插入第二条数据时将值设置超过了字段长度限制,会产生Data too long for column异常。


    /**
* 验证一下事物控制
*/

// @Transactional(rollbackFor = Exception.class)
@DS(value = "slave")
@GetMapping("/dbTestTransactional")
public void dbTestTransactional() {

DynamicDatasourceData datasourceData = new DynamicDatasourceData();
datasourceData.setSourceName("test");
dynamicDatasourceDataMapper.insert(datasourceData);

DynamicDatasourceData datasourceData1 = new DynamicDatasourceData();
datasourceData1.setSourceName("testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest");
dynamicDatasourceDataMapper.insert(datasourceData1);
}

经过测试发现执行结果如下,即便实现动态切换数据源,本地事务依然可以生效。



  • 不加上@Transactional注解第一条记录可以插入,第二条插入失败

  • 加上@Transactional注解两条记录都不会插入成功


本文案例地址:github.com/chengxy-nds…


作者:程序员小富
来源:juejin.cn/post/7316202800663363594
收起阅读 »

京东企业业务前端监控实践

作者:零售企业业务苏子刚 监控的背景和意义 在现代前端开发中,接入监控系统是一个很重要的环节,它可以帮助开发者、产品、运营了解应用的性能表现,用户的实际体验以及潜在的错误和问题,从而进一步优化用户体验,帮助产品升级迭代。 背景 •应用复杂性增加:随着单页应用(...
继续阅读 »

作者:零售企业业务苏子刚


监控的背景和意义


在现代前端开发中,接入监控系统是一个很重要的环节,它可以帮助开发者、产品、运营了解应用的性能表现,用户的实际体验以及潜在的错误和问题,从而进一步优化用户体验,帮助产品升级迭代。


背景


应用复杂性增加:随着单页应用(SPA)和渐进式网页应用(PWA)的流行,前端应用变得越来越复杂。


网页加载性能要求提高:用户对网页加载速度和交互响应的要求越来越高,性能成为影响用户体验的关键因素。


多样化的设备和网络环境:用户通过各种设备和网络环境访问应用,这些因素都可能影响应用的性能和用户体验。


敏捷开发和持续部署:敏捷开发和 CI/CD 的实践要求开发团队能够快速的响应问题,并持续改进产品。


意义


对应用性能监控,提升用户体验:监控页面的加载时间,交互时间等性能指标,帮助团队优化代码,提升用户体验。


错误追踪,快速定位并解决问题:捕获前端错误和异常,快速定位问题源头,缩短故障的修复时间。


用户行为分析,指导产品快速迭代:了解用户如何与应用互动,哪些功能用户喜欢,哪些路径导致转化,从而指导产品迭代升级。


业务指标监控,保证主流程的稳定性:监控关键业务流程,如:购物车、商详、结算页等黄流,确保业务流程的稳定性。


告警系统,异常情况能够快速响应:通过定时任务自动化巡检/实时监控,当出现性能异常时,能够快速响应。


监控的类别


从上面可看出,通过监控能够促使我们的系统更加健壮、稳定,体验更好。那我们团队也从去年开始逐步将各个应用接入了监控。到目前为止,我们的监控分为两个部分:


•实时监控:成功集成了 SGM 集团内部监控平台,并引入了相应的 SDK 来对前端应用进行实时监控。目前,我们所涉及的 100+ 个应用程序已经完全接入该系统。通过配置有效的告警机制,我们能够及时捕捉并上报线上环境中的错误和异常,确保业务的稳定运行。


•定时任务巡检:实现了定时任务的自动化设置,用于定期执行自动巡检,并自动上报检测结果,还能激活告警系统。通过这种方式,我们能够确保持续监控系统健康状况,并在发现潜在问题时能够迅速响应。


◦使用 Chrome 插件快速创建 UI 测试脚本,随后在 UI啄木鸟平台 上配置定时执行这些脚本进行系统巡检。这一流程实现了自动化的界面测试,确保应用的用户界面按预期工作,同时及时发现并解决潜在的 UI 问题;


◦自主研发一套脚本,这些脚本通过启动一个 Node.js 服务来进行系统巡检。这种方法使我们能够灵活地监控系统性能和功能,及时发现并处理潜在的问题,从而保障系统的稳定运行;


监控整体架构


image.png


监控建设实践


实时监控


通过整合 SGM 监控平台和告警系统,我们实现了对所有接入应用的实时监控。一旦检测到异常,系统会立即通过多种方式(如咚咚、邮件、电话等)通知相关团队成员,确保问题能够被迅速发现和解决。为了提高告警的有效性,我们精心设计了告警策略,确保只有真正的异常情况才会触发告警。以下是我们在优化告警设置过程中积累的一些关键经验和建议:


1.精确度与敏感度的平衡:过于敏感的告警会导致频繁的误报,而设置过于宽松则可能错过关键的异常。找到合适的平衡点至关重要。


2.分级告警机制:根据问题的严重程度设置不同级别的告警,以便采取相应的响应措施。紧急问题可以通过电话直接通知,而较为轻微的问题可以通过邮件或即时消息通知。


3.持续优化告警规则:定期回顾和分析告警的准确性和响应情况,根据实际情况调整告警规则,以提高告警的准确性和有效性。


4.明确责任分配:确保每个告警都有明确的责任人,这样一旦发生异常,能够迅速有人响应。


5.培训和意识提升:对团队成员进行定期的培训,提高他们对告警系统的理解和重视,确保每个人都能正确响应告警。


这些经验和建议是我们在实践中不断摸索和尝试的结果,希望能够帮助你们更有效地管理和响应系统告警,确保应用的稳定运行。


WEB端

用户体验



用户体验是指用户在使用网站过程中的整体感受、情绪和态度,它包括与产品交互的全程。对于开发者而言,打造出色的用户体验意味着关注网站的加载速度、视觉稳定性、交互响应时间、渲染性能等关键因素。Google 提出了一系列 Web 性能标准指标,这些指标旨在量化用户体验的不同层面。SGM 性能监控平台紧跟这些标准,对几项关键指标进行监控并提供反馈,以确保用户能够获得流畅且愉悦的网站使用体验。



•LCP:页面加载过程中最大的内容元素(图片、视频等)渲染完成的时间点。也可近似看作是首屏加载时间。Google 标准是 小于等于 2500ms。









最初,我们遵循了 Google 提出的标准来精确设定我们的告警系统。









配置告警后,我们注意到告警频率较高,主要是因为多数系统的实际网页最大内容绘制(LCP)值普遍超过了预期的 2.5s 标准。然而,这些告警并未实质性影响用户的正常使用体验。为了优化告警机制,我们经过仔细评估后,决定将部分系统的 LCP 阈值暂时调整至 5s,并相应调整了告警级别,以更合理地反映系统性能对用户体验的实际影响。


显然,当前调整的 LCP 阈值 5s 并非我们的最终目标。这一调整是基于应用当前的性能状况所做的临时措施,旨在优化告警的频率和质量。我们计划随着对各个应用性能的持续改进,最终将 LCP 阈值恢复至标准的 2.5s。


•CLS:从页面开始加载到生命周期状态更改为隐藏期间发生的所有意外布局偏移的累计得分。Web 给出的性能指标是 < 0.1


•FCP:从网页开始加载到有内容渲染的耗时。标准性能指标 1.8s


•FID:用户首次发出交互指令到页面可以响应为止的时间差。标准性能指标 100ms


•TTFB:客户端接收到服务器返回的第一个字节响应信息的耗时。标准性能指标 1000ms









从最初启用告警到目前决定关闭大部分告警,我们的决策基于以下考虑:


1.用户体验未受显著影响:即使某些性能指标超出了标准值,我们观察到这并未对用户操作网站造成实质性影响。


2.避免告警疲劳:频繁的告警可能导致开发团队对通知产生麻木感,从而忽视那些真正关键的、影响网站健康的告警。


3.指标作为优化参考:这些性能指标更多地被视为优化网站时的参考点,而对用户的直觉体验影响甚微。


4.有针对性的优化:在网站优化过程中,我们会将这些指标作为健康检查的一部分,进行针对性的改进,而无需依赖告警来频繁提醒。


基于这些理由,我们目前选择只对 LCP 指标保持告警启用,以确保关注到可能影响用户加载体验的关键性能问题。


健康度指标告警开启标准值优化后值告警阀值告警级别
LCP开启<=2500ms针对部分应用 <=5000ms1min 内 耗时>=5000ms(持续优化并更新阀值到2500ms) 调用次数:50 连续次数:1次通知
FCP关闭<=1800ms--
CLS关闭<=0.1--
FID关闭<=100ms--
TTFB关闭<=1000ms--

页面性能



网页性能涉及从加载开始到完全可交互的整个过程,包括页面内容的下载、解析和渲染等多个阶段。在 SGM 监控平台上,我们能够追踪到一系列与页面加载性能相关的指标,如页面加载总时长、DNS 解析时长、DOM 加载完成时间、用户的浏览器和地理分布、首次渲染(白屏)时间、用户行为追踪、性能重现和热力图分析等。这些丰富的数据为我们优化页面性能提供了宝贵的参考。特别是首次渲染时间,或称为白屏时间,是我们监控中特别关注的一个关键指标,因为它直接影响用户的首次印象和整体体验。



•白屏时间:这一指标能够向开发者展示哪些页面出现了白屏现象。在利用此指标之前,我们需要为每个应用单独配置白屏时间的监控参数,以确保准确地捕捉到首次内容呈现的时刻。这有助于我们识别并优化那些影响用户首次加载体验的关键页面。









为了监控白屏时间,我们必须在应用的全局配置中的白屏监控项下,指定每个页面的 URL 及其关键元素。同时,我们还需设定监控的起始时间点和超时阈值。值得注意的是,URL 配置支持正则表达式,这为我们提供了灵活性,以匹配和监控一系列相似的页面路径。









•白屏检测的机制:白屏检测机制的核心在于验证页面上的关键元素是否已经被渲染。所谓关键元素,指的是在配置过程中指定的用于检测的 DOM 元素。这些元素的渲染情况是判断页面是否白屏的依据。


•告警设置:白屏告警功能已经启用,但目前还处于初期阶段,一些功能尚待完善。例如,当前的 URL 配置尚未支持正则表达式匹配。










面对当前的局限性,我们采取了双策略应对。一方面,我们利用白屏告警功能直接监控页面的白屏情况;另一方面,我们通过分析页面加载性能指标中的白屏时间来间接监测潜在的白屏问题。在设置平均耗时时间和告警级别时,我们综合考虑了多个因素,包括用户的网络环境、告警的发生频率以及告警的实际适用性,以确保监控方案的有效性和合理性。



网页性能告警开启优化前值优化后值告警阀值告警级别
首包时间关闭----
页面完全加载时间关闭----
白屏时间开启<=5000ms<=10000ms1min 内 耗时>=10000ms,后期采用白屏告警替代 调用次数:30 连续次数:5 次紧急

•此外,我们的页面追踪功能包括用户行为回溯、页面性能重现以及行为轨迹热力图,这些工具允许我们从多个维度和场景对用户行为进行深入分析,极大地便利了问题的诊断和排查。


JSError监控



我们通过配置错误关键词来匹配控制台的报错信息。报错阈值的设定可以参考各个项目的 QPS,因为在项目实施了恰当的降级策略后,即便控制台出现报错,页面通常仍能正常访问,不会对用户体验造成影响。因此,这个阈值可以适当设置得较高。



这里需要特别注意 “Script error” 的错误,这种错误给不到任何对我们有用的信息。所以需要采用一定的手段避免出现类似的报错:


•这种错误也称之为跨域错误,所以首先,我们需要开启跨域资源共享功能(CORS)。


<script src="http://xxxdomain.com/home.js" crossorigin></script>

•针对 Vue 项目,由于 Vue 重写了 window.onerror 事件,所以我们需要在 Vue 项目中增加 错误处理:


Vue.config.errorHandler = (err, vm, info)=> {
if (err) {
try {
console.error(err);
window.__sgm__.error(err)
} catch (e) {}
}
};

•在某些情况下,“Script error” 可能是无关紧要的,我们可以选择忽略这类特定的错误。为此,可以关闭这些特定错误的监控,具体的错误可以通过它们的 hash 在错误日志中进行识别和过滤。









关键指标关键字(支持正则匹配)触发次数告警级别
js错误null、undefined、error、map、filter、style、length...周期:1min ,错误次数:50/100/200(可参考 QPS 值设置),连续次数:1次严重

API请求监控



这里我们的告警设置主要关注 HTTP 状态码和业务错误码。这两项指标的异常表明我们的应用可能遇到了问题,需要我们迅速进行检查和处理以确保系统的正常运行。



首先,我们必须在应用的监控配置中设定数据采集参数:









关键指标错误码业务域名触发次数告警级别
http错误!200(Http响应非200报警)xx1.jd.com xx2.jd.com周期:1min 错误次数:1 总调用次数:50 连续次数:1严重
业务失败码errCode(根据实际业务线设置 -1,-2等)

针对业务失败码:


1.由于现有应用跨不同业务条线存在异常码的差异,我们需要针对每个业务线收集并配置其特定的异常码。


应用来源标准响应针对性告警
慧采PC 企业购{ "code":null, "success":true, "msg":"操作成功", "result":{} }业务异常码 code ·-1,-2 ·...
锦礼{ "code": 000, "data": {}, "msg": "操作成功" }业务异常码 code ·! (1000 && 3001等 )
color·-1 echo ·1 echo
其他.......

2、对于新增应用,我们实施了后端服务异常码的标准化。因此,在监控方面,我们只需要配置一套统一的标准来进行监控。


{
"50000X": "程序异常,内部",
"500001": "程序异常,上游",
"500002": "程序异常,xx",
"500003": "程序异常,xx",
...
}

资源错误



这里通常指的是 css、js、图片等资源的加载错误



关键指标告警开启告警阀值告警级别
资源错误开启周期:1min 错误次数:200(也可参照QPS进行设置) 连续次数:1严重

对于图片加载错误,只需在项目中实施适当的降级方案。在应用的监控配置中,我们可以设置为不收集图片错误相关的数据。









再来举个例子:


在企业业务的封闭场景中,例如慧采平台,我们集成了埋点 JavaScript 脚本。然而,由于某些客户的网络环境,导致我们的埋点相关静态资源延迟加载。









治理方案:









自定义上报



每个业务流程的关键节点或核心功能实施了专门的监控措施,以便对任何异常状况进行跟踪、监控并及时上报。



目前,几条业务线已经实施了自定义上报机制,其主要目的包括:


•利用自定义上报来捕获接口异常的详细信息,如入参和出参,以便在线上出现异常时,能够依据上报的数据快速进行问题的诊断和定位。


•在复杂的环境下,准确追踪用户行为导致的错误,并利用上报的信息进行有效的问题排查和定位。


锦礼酷兜: 用户在选择地址后,系统未能根据所选地址提供正确的信息,导致页面加载出现异常。


由于这个 H5 页面被嵌入到用户的 App 内,开发者难以直接复现用户遇到的问题。因此,我们利用监控平台的自定义上报功能来收集相关信息,以辅助进行问题排查。


•上报地址组件返回值









•上报接口入参









•然后根据自定义上报日志查看具体信息









E卡:外部引用资源异常上报(设备指纹,eid 等)


在结算页面提交订单时,系统需要获取设备ID。为此,我们实施了降级方案,并通过自定义上报机制对此过程进行监控,以确保流程的顺利执行。


降级方案:如果获取不到,后端会生成 uuid 给到前端。
















企业购注销pc: 我们需要集成科技 SDK,以便在页面上完成用户注销后自动跳转到指定的 URL。上线后,收到客户反馈指出在完成注销流程后页面未能正确跳转。









接口异常: 在接口异常中,添加自定义监控,查看入参和出参信息。










尽管我们已经在应用中引入了自定义监控以便更好地观察和定位问题,但我们仍需进一步细化和规范化这些监控措施。目前,我们正积极对各业务线的功能点进行梳理,以实现更深入的细化监控。我们的目标是为每个业务线、每个应用的关键链路和功能点定制针对各种异常情况的精细化自定义监控(例如,某页面按钮的显示或点击异常)。



自定义告警告警开启告警阀值告警级别
自定义编码开启1min内 调用次数:50 连续次数:1次警告

小程序端


与 Web 端相比,小程序的监控存在一些差异,主要是缺少了如 LCP(最大内容绘制)等特定性能指标。然而,性能问题、JavaScript错误、资源加载错误等其他监控指标仍然可以被捕获。此外,小程序官方和开发者工具都提供了性能检测工具,这些工具便于开发者查看和优化小程序应用的性能。本文将不深入介绍 Web 端的监控指标,而是专注于介绍小程序中独有的监控和分析工具。



小程序官方后台



可以分析接口数据、js 分析等。











利用 SGM 的监控功能结合小程序官方的分析工具,对我们的小程序进行综合优化,是一个有效的策略。



原生应用

基础监控:mPaas、烛龙、SGM


mPaaS崩溃监控。应用崩溃对用户体验有显著影响,是移动端监控中的一个关键指标。通过不断的监控和优化,京东慧采移动端的崩溃率已经降至较低水平,目前的平均用户崩溃率大约为0.03122%。









烛龙:启动耗时、首屏耗时、启动且首焦耗时、卡顿。为了改善首屏加载时间,京东慧采采用了烛龙监控平台并实施了相应的优化措施。优化后,应用的整体性能显著提升,其中 Android 平台的tp95耗时大约为 2764ms,iOS 平台的tp95耗时大约为1791ms,均达到了较低的水平。
















SGM:网络、WebView、原生页面等指标。


业务监控


京东慧采在多个业务模块中,其中登录、商详、订单详情 3 个模块接入了业务监控。


登录:登录接入 SGM 监控平台,自定义了整个流程错误码,并配置了告警规则


(1)600:登录正常流程(必要是可白名单开启)


(2)601:登录防刷验证流程异常监控


(3)602:登录魔方验证流程异常监控


(4)603:登录流程异常监控























商详、订单详情:在商品详情和订单详情页面,我们集成了业务监控 SDK。通过移动配置平台,我们下发了监控规则,以便在接口返回的数据不符合预期时,能够上报错误信息。目前,这些监控信息被上报到崩溃分析平台的自定义异常模块中,以便进行进一步的分析和优化。


(1)接口请求是否成功。


(2)banner 楼层:是否为空、楼层类型是否正常(1 原生)、数据/大小图地址是否为空。


(3)商品信息楼层:是否为空、楼层类型是否正常(2 动态)、数据/商品名称/价格是否为空。


(4)服务楼层:是否为空、楼层类型是否正常(1 原生)、数据/服务信息是否为空。


(5)spu 和物流楼层:是否为空、楼层类型是否正常(1 原生)、数据/sup信息是否为空。


(6)其他:其他/按钮数据/按钮名称是否为空、按钮类型是否正常(排除1/2/3/4/5/6/20000)。


订单详情监控信息:


(1)接口请求是否成功。


(2)基础信息楼层:是否为空、楼层类型是否正常(2 动态)、数据/地址信息是否为空。


(3)商品信息楼层:是否为空、楼层类型是否正常(2 动态)、数据/商品列表是否为空。


(4)支付信息楼层:是否为空、楼层类型是否正常(2 动态)、数据是否为空。


(5)价格楼层:是否为空、楼层类型是否正常(2 动态)、数据是否为空。












定时巡检


定时巡检可以通过两种方法实现:


•利用 UI 啄木鸟平台配置定时执行的任务。


•使用团队开发的自定义脚本,并通过自动启动的服务来执行这些检测任务。


UI 啄木鸟

定时巡检的主要目的是确保每个项目的核心流程在每次迭代中保持稳定。通过配置定时任务,我们能够及时发现并解决线上问题,从而维护系统的稳定性。


什么是 UI 啄木鸟


UI啄木鸟平台,由京东集团-京东科技开发,是一个自动化巡检工具,其主要功能包括:


•Chrome插件:用于录制项目的用户交互步骤。


•定时任务平台:用于在服务器端定期执行已录制的脚本进行巡检。


在使用Chrome插件过程中,我们遇到了一些问题。与京东科技团队沟通后,我们获得了共同开发和升级插件的机会。目前,我们已经添加了新功能并修复了一些已知问题。


1.打开录制和停止录制按钮,分别开启监听和关闭拦截页面事件功能;


2.新增点击录制后保留当前录制步骤功能;


3.在执行事件过程中,禁止再次点击执行,避免执行顺序错乱;


4.点击事件可切换操作类型为 focus 事件,便于监听滚动条滑动;


5.在步骤复现情况下,调整判断元素选择器和屏幕位置的顺序,避免位置出入点击位置错位;


6....


使用chrome扩展程序进行安装即可。









怎么使用


•新建录制脚本









•点击详情,开启录制









•监听步骤









•关联到啄木鸟平台









•啄木鸟平台(调试、配置 Cookie,开启定时任务)









自启动巡检工具


自动化巡检工具能够检测页面上的多种元素和链接,包括 a 标签的外链、接口返回的链接、鼠标悬停元素、点击元素,以及跳转后 URL 的有效性。该工具尤其适合用于频道页,这些页面通常通过投放广告和配置通天塔链接来生成。在大型促销活动期间,我们可以运行脚本来验证广告和通天塔链接的有效性。目前,工具的功能还相对有限,并且对于广告组等特定接口的支持不够通用,这也是我们计划逐步改进和优化的方向。



功能检测


•检测所有 a 标签和所有接口响应数据中包含所有通天塔活动外链是否有效。









{
"cookieThor":"", // 是否依赖cookie等登录态,无需请传空
"urlPattern": "pro\.jd\.com",// 匹配的链接,这里只匹配通天塔
"urls": ["https://b.jd.com/s?entry=newuser"] // 将要检测的url,可填多个
}

运行结果:









•检测鼠标 hover 事件,收集用户交互后的接口响应数据,检测所有活动外联是否有效。









{
"cookieThor": "",
"urlPattern": "///pro.jd.com/",
"urls": [
{
"url": "https://b.jd.com/s?entry=newuser",
"hoverElements": [
{
"item": "#focus-category-id .focus-category-item", // css样式选择器
"target": ".focus-category-item-subtitle"
}
]
}
]
}








•检查 click 事件,收集用户交互后的接口响应数据,检测所有通天塔活动外联是否有效。









{
"cookieThor": "",
"urlPattern": "///pro.jd.com/",
"urls": [
{
"url": "https://b.jd.com/s?entry=newuser",
"clickElements": [
{
"item": "#recommendation-floor .drip-tabs-tab"
}
]
}
]
}








•检测点击后,跳转后的链接有效性。









{
"cookieThor": "",
"urlPattern": "///pro.jd.com/",
"urls": [
{
"url": "https://b.jd.com/s?entry=newuser",
"clickElements": [
{
"item": ".recommendation-product-wrapper .jdb-sku-wrapper"
}
]
}
]
}








检测原理









监控后暴露的问题


•慧采 jsError:


用于埋点数据上报的API,但在一些封闭环境中,由于网络环境,客户可能无法及时访问埋点或指纹识别等 SDK,导致频繁的报错。为了解决这个问题,我们可以通过引入 try...catch 语句来捕获异常,并结合使用队列机制,以确保埋点数据能够在网络条件允许时正常上报。这样既避免了错误的频繁发生,也保障了数据上报的完整性和准确性。


public exposure(exposureId: ExposureId, jsonParam = {}, eventParam = '') {
this.execute(() => {
try {
const exposure = new MPing.inputs.Exposure(exposureId);
exposure.eventParam = eventParam; // 设置click事件参数
exposure.jsonParam = serialize(jsonParam); // 设置json格式事件参数,必须是合法的json字符串
console.log('上报 曝光 info >> ', exposure);
new MPing().send(exposure);
} catch (e) {
console.error(e);
}
});
}

private execute(fn: CallbackFunction) {
if (this._MPing === null) {
this._Queue.push(fn);
} else {
fn();
}
}

•企业购订详异常:
















总结


接入前


在过去,我们的应用上线后,对线上运行状况了解不足,错误发生时往往依赖于用户或运营团队的反馈,这使我们常常处于被动应对状态,有时甚至会导致严重的线上问题,如白屏、设备兼容性问题和异常报错等。为了改变这一局面,我们开始寻找和实施解决方案。从去年开始,我们逐步将所有 100+ 应用接入了监控系统,现在我们能够实时监控应用状态,及时发现并解决问题,大大提高了我们的响应速度和服务质量。


接入后


自从接入监控系统后,我们的问题发现和处理方式从被动等待用户反馈转变为了主动监测。现在我们能够即时发现并快速解决问题。此外,我们还利用监控平台对若干应用进行了性能优化,并为关键功能点制定了预先的降级策略,以保障应用的稳定运行。


•页面健康度监控表明,我们目前已有 50+ 个项目的性能评分达到或超过 85 分。通过分析监控数据,我们对每个应用的各个页面进行了详细分析和梳理。在确保流程准确无误的基础上,我们对那些性能较差的页面进行了持续的优化。目前,我们仍在对一些关键但性能表现一般的应用进行进一步的优化工作。
















•通过配置 JavaScript 错误和资源错误的告警机制,我们在项目迭代过程中及时解决了多个 JavaScript 问题,有效降低了错误率和告警频率。正如前文所述,对于慧采PC 这类封闭环境,客户公司的网络策略可能会阻止埋点 SDK 和设备指纹等 JavaScript 资源的加载,导致全局变量获取时出现错误。我们通过实施异常捕获和队列机制,不仅规避了部分错误,还确保了埋点数据的准确上报。









•通过收集 HTTP 错误码和业务失败码,并设置相应的告警机制,我们能够在接到告警通知的第一时间内分析并解决问题。例如,我们成功地解决了集团内部遇到的 “color404” 问题等。这种做法加快了问题的响应和解决速度,提高了服务的稳定性和用户满意度。


•自定义监控:通过给接口异常做入参、出参的上报、在关键功能点上报有效信息等,能够在应用出现异常时,快速定位问题并及时修复。









•通过实施定时任务巡检,我们有效地避免了迭代更新上线可能对整个流程造成的影响。同时,巡检工具的使用也确保了外部链接的有效性,进一步保障了应用的稳定运行和用户体验。


规划


监控是一个持续长期的过程,我们致力于不断完善,确保系统的稳定性和安全性。基于现有的监控能力,我们计划实施以下几项优化措施:


1.应用性能提升:我们将持续优化我们的应用,目标是让 90% 以上的应用性能评分达到 90 分以上,并对资源错误和 JavaScript 错误进行有效管理。


2.深化监控细节:我们将扩展监控的深度和广度,确保能够捕捉到所有潜在的异常情况。例如,如果一个仅限采购账号使用的按钮错误地显示了,这应该触发自定义异常上报,并在代码层面实施降级处理。


3.巡检工具升级:我们将继续升级我们的 Chrome 巡检插件,提高其智能化程度和覆盖范围,以保持线上主要流程的健壮性和稳定性。


结尾


我们是企业业务大前端团队,会持续针对各端优化升级我们的监控策略,如果您有任何疑问或者有更好的建议,我们非常欢迎您的咨询和交流。


作者:京东零售技术
来源:juejin.cn/post/7400271712359186484
收起阅读 »

快速理解 并发量、吞吐量、日活、QPS、TPS、RPS、RT、PV、UV、DAU、GMV

并发与并行 并发:由于CPU数量或核心数量不够,多个任务并不一定是同时进行的,这些任务交替执行(分配不同的CPU时间片,进程或者线程的上下文切换),所以是伪并行。 并行:多个任务可以在同一时刻同时执行,通常需要多个或多核处理器,不需要上下文切换,真正的并行。...
继续阅读 »

并发与并行



  • 并发:由于CPU数量或核心数量不够,多个任务并不一定是同时进行的,这些任务交替执行(分配不同的CPU时间片,进程或者线程的上下文切换),所以是伪并行。

  • 并行:多个任务可以在同一时刻同时执行,通常需要多个或多核处理器,不需要上下文切换,真正的并行。


并发量(Concurrency)



  • 概念:并发或并行,是程序和运维本身要考虑的问题。而并发量,通常是不考虑程序并发或并行执行,只考虑一个服务端程序单位时间内同时可接受并响应多少个请求,通常以秒为单位,也可乘以86400,以天为单位。

  • 计算方法,通常通过一些压测工具,例如ApiPost压测,或者ab压测来统计,依ab为例:


Window系统:Apache下bin目录有个ab.exe
CentOS系统:yum -y install httpd-tools.x86_64

ab -c 并发数 -n 请求数 网址
ab -c 10 -n 150 127.0.0.1/ 表示对127.0.0.1这个地址,用10个并发一共请求了150次。而不是1500次,
Time taken for tests: 1.249 seconds,说明并发量为 150 / 1.249 ≈ 120 并发,表示系统最多可承载120个并发每秒。

吞吐量(Throughput)



  • 概念:吞吐量是指系统在单位时间能够处理多少个请求,TPS、QPS都是吞吐量的量化指标。
    相比于QPS这些具有清晰定义的书面用语,吞吐量偏向口语化。


日活



  • 概念:每日活跃用户的数量,通常偏向非技术指标用语,这个概念没有清晰的定义,销售运营嘴里的日活,可能是只有一个人1天访问100次,就叫做日活100,也可以说是日活1,中位数日活50,显然意义不大。


QPS(Query Per Second)



  • 概念:每秒查询次数,通常是对读操作的压测指标。服务器在一秒的时间内能处理多少量的请求。和并发量概念差不多,并发量高,就能应对更多的请求。

  • 计算方法,通常通过一些压测工具,例如ApiPost压测,或者ab压测来统计,依ab为例:


ab -c 10 -n 150 127.0.0.1/
其中返回一行数据:
Requests per second: 120.94 [#/sec] (mean)
表示该接口QPS在120左右。

TPS(Transactions Per Second)



  • 概念:每秒处理的事务数目,通常是对写操作的压测指标。这里的事务不是数据库事务,是指服务器接收到请求,再到处理完后响应的过程。TPS表示一秒事件能够完成几次这样的流程。


TPS对比QPS



  • QPS:偏向统计查询性能,一般不涉及数据写操作。

  • TPS:偏向统计写入性能,如插入、更新、删除等。


RPS(Request Per Second)



  • 概念:每秒请求数,和QPS、TPS概念差不多。没有过于清晰的定义,看你怎么用。


RT(Response Time)



  • 概念:响应时间间隔,是指用户发起请求,到接收到请求的时间间隔,越少越好,应当控制在0~150毫秒之间。


PV(Page view)



  • 概念:浏览次数统计,一般以天为单位。范围可以是单个页面,也可以是整个网站,一千个用户一天对该页面访问一万次,那该页面PV就是一万。


UV(Unique Visitor)



  • 概念:唯一访客数。时间单位通常是天,1万个用户一天访问该网站十万次,那么UV是一万。

  • 实现方案:已登录的用户可通过会话区分,未登录的用户可让客户端创建一个唯一标识符当做临时的token用于区分用户。


DAU(Daily Active Use)



  • 概念:日活跃用户数量,来衡量服务的用户粘性以及服务的衰退周期。统计方案各不相同,这要看对活跃的定义,访问一次算活跃,还是在线时长超10分钟算活跃,还是用户完成某项指标算活跃。


GMV(Gross Merchandise Volume)



  • 概念:单位时间内的成交总额。多用于电商行业,一般包含拍下未支付订单金额。


作者:小松聊PHP进阶
来源:juejin.cn/post/7400281441803403275
收起阅读 »

丸辣!BigDecimal又踩坑了

丸辣!BigDecimal又踩坑了 前言 小菜之前在国内的一家电商公司自研电商项目,在那个项目中是以人民币的分为最小单位使用Long来进行计算 现在小菜在跨境电商公司也接到了类似的计算需求,在小菜火速完成提交代码后,却被技术leader给叫过去臭骂了一顿 技术...
继续阅读 »

丸辣!BigDecimal又踩坑了


前言


小菜之前在国内的一家电商公司自研电商项目,在那个项目中是以人民币的分为最小单位使用Long来进行计算


现在小菜在跨境电商公司也接到了类似的计算需求,在小菜火速完成提交代码后,却被技术leader给叫过去臭骂了一顿


技术leader让小菜将类型改为BigDecimal,小菜苦思不得其解,于是下班后发奋图强,准备搞懂BigDecimal后再对代码进行修改


...


在 Java 中,浮点类型在进行运算时可能会产生精度丢失的问题


尤其是当它们表示非常大或非常小的数,或者需要进行高精度的金融计算时


为了解决这个问题,Java 提供了 BigDecimal


BigDecimal 使用各种字段来满足高精度计算,为了后续的描述,这里只需要记住两个字段


precision字段:存储数据十进制的位数,包括小数部分


scale字段:存储小数的位数


BigDecimal的使用方式再后续踩坑中进行描述,最终总结出BigDecimal的最佳实践


BigDecimal的坑


创建实例的坑


错误示例:


在BigDecimal有参构造使用浮点型,会导致精度丢失


BigDecimal d1 = new BigDecimal(6.66);

正确的使用方法应该是在有参构造中使用字符串,如果一定要有浮点数则可以使用BigDecimal.valueOf


private static void createInstance() {
//错误用法
BigDecimal d1 = new BigDecimal(6.66);

//正确用法
BigDecimal d2 = new BigDecimal("6.66");
BigDecimal d3 = BigDecimal.valueOf(6.66);

//6.660000000000000142108547152020037174224853515625
System.out.println(d1);
//6.66
System.out.println(d2);
//6.66
System.out.println(d3);
}

toString方法的坑


当数据量太大时,使用BigDecimal.valueOf的实例,使用toString方法时会采用科学计数法,导致结果异常


BigDecimal d2 = BigDecimal.valueOf(123456789012345678901234567890.12345678901234567890);
//1.2345678901234568E+29
System.out.println(d2);

如果要打印正常结果就要使用toPlainString,或者使用字符串进行构造


private static void toPlainString() {
BigDecimal d1 = new BigDecimal("123456789012345678901234567890.12345678901234567890");
BigDecimal d2 = BigDecimal.valueOf(123456789012345678901234567890.12345678901234567890);

//123456789012345678901234567890.12345678901234567890
System.out.println(d1);
//123456789012345678901234567890.12345678901234567890
System.out.println(d1.toPlainString());

//1.2345678901234568E+29
System.out.println(d2);
//123456789012345678901234567890.12345678901234567890
System.out.println(d2.toPlainString());
}

比较大小的坑


比较大小常用的方法有equalscompareTo


equals用于判断两个对象是否相等


compareTo比较两个对象大小,结果为0相等、1大于、-1小于


BigDecimal使用equals时,如果两数小数位数scale不相同,那么就会认为它们不相同,而compareTo则不会比较小数精度


private static void compare() {
BigDecimal d1 = BigDecimal.valueOf(1);
BigDecimal d2 = BigDecimal.valueOf(1.00);

// false
System.out.println(d1.equals(d2));
// 0
System.out.println(d1.compareTo(d2));
}

在BigDecimal的equals方法中能看到,小数位数scale不相等则返回false


public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
//小数精度不相等 返回 false
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);

return this.inflated().equals(xDec.inflated());
}

因此,BigDecimal比较时常用compareTo,如果要比较小数精度才使用equals


运算的坑


常见的运算包括加、减、乘、除,如果不了解原理的情况就使用会存在大量的坑


在运算得到结果后,小数位数可能与原始数据发生改变,加、减运算在这种情况下类似


当原始数据为1.00(2位小数位数)和5.555(3位小数位数)相加/减时,结果的小数位数变成3位


	private static void calc() {
BigDecimal d1 = BigDecimal.valueOf(1.00);
BigDecimal d2 = BigDecimal.valueOf(5.555);

//1.0
System.out.println(d1);
//5.555
System.out.println(d2);
//6.555
System.out.println(d1.add(d2));
//-4.555
System.out.println(d1.subtract(d2));
}

在加、减运算的源码中,会选择两数中小数位数(scale)最大的当作结果的小数位数(scale)


private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
//用差值来判断使用哪个scale
long sdiff = (long) scale1 - scale2;
if (sdiff == 0) {
//scale相等时
return add(xs, ys, scale1);
} else if (sdiff < 0) {
int raise = checkScale(xs,-sdiff);
long scaledX = longMultiplyPowerTen(xs, raise);
if (scaledX != INFLATED) {
//scale2大时用scale2
return add(scaledX, ys, scale2);
} else {
BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
//scale2大时用scale2
return ((xs^ys)>=0) ? // same sign test
new BigDecimal(bigsum, INFLATED, scale2, 0)
: valueOf(bigsum, scale2, 0);
}
} else {

int raise = checkScale(ys,sdiff);
long scaledY = longMultiplyPowerTen(ys, raise);
if (scaledY != INFLATED) {
//scale1大用scale1
return add(xs, scaledY, scale1);
} else {
BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
//scale1大用scale1
return ((xs^ys)>=0) ?
new BigDecimal(bigsum, INFLATED, scale1, 0)
: valueOf(bigsum, scale1, 0);
}
}
}

再来看看乘法


原始数据还是1.00(2位小数位数)和5.555(3位小数位数),当进行乘法时得到结果的小数位数为5.5550(4位小数)


private static void calc() {
BigDecimal d1 = BigDecimal.valueOf(1.00);
BigDecimal d2 = BigDecimal.valueOf(5.555);

//1.0
System.out.println(d1);
//5.555
System.out.println(d2);
//5.5550
System.out.println(d1.multiply(d2));
}

实际上1.00会被优化成1.0(上面代码示例的结果也显示了),在进行乘法时会将scale进行相加,因此结果为1+3=4位


public BigDecimal multiply(BigDecimal multiplicand) {
//小数位数相加
int productScale = checkScale((long) scale + multiplicand.scale);

if (this.intCompact != INFLATED) {
if ((multiplicand.intCompact != INFLATED)) {
return multiply(this.intCompact, multiplicand.intCompact, productScale);
} else {
return multiply(this.intCompact, multiplicand.intVal, productScale);
}
} else {
if ((multiplicand.intCompact != INFLATED)) {
return multiply(multiplicand.intCompact, this.intVal, productScale);
} else {
return multiply(this.intVal, multiplicand.intVal, productScale);
}
}
}

而除法没有像前面所说的运算方法有规律性,因此使用除法时必须要指定保留小数位数以及舍入方式


进行除法时可以立马指定保留的小数位数和舍入方式(如代码d5)也可以除完再设置保留小数位数和舍入方式(如代码d3、d4)


private static void calc() {
BigDecimal d1 = BigDecimal.valueOf(1.00);
BigDecimal d2 = BigDecimal.valueOf(5.555);

BigDecimal d3 = d2.divide(d1);
BigDecimal d4 = d3.setScale(2, RoundingMode.HALF_UP);
BigDecimal d5 = d2.divide(d1, 2, RoundingMode.HALF_UP);
//5.555
System.out.println(d3);
//5.56
System.out.println(d4);
//5.56
System.out.println(d5);
}

RoundingMode枚举类提供各种各样的舍入方式,RoundingMode.HALF_UP是常用的四舍五入


除了除法必须指定小数位数和舍入方式外,建议其他运算也主动设置进行兜底,以防意外的情况出现


计算价格的坑


在电商系统中,在订单中会有购买商品的价格明细


比如用完优惠卷后总价为10.00,而买了三件商品,要计算每件商品花费的价格


这种情况下10除3是除不尽的,那我们该如何解决呢?


可以将除不尽的余数加到最后一件商品作为兜底


private static void priceCalc() {
//总价
BigDecimal total = BigDecimal.valueOf(10.00);
//商品数量
int num = 3;
BigDecimal count = BigDecimal.valueOf(num);
//每件商品价格
BigDecimal price = total.divide(count, 2, RoundingMode.HALF_UP);
//3.33
System.out.println(price);

//剩余的价格 加到最后一件商品 兜底
BigDecimal residue = total.subtract(price.multiply(count));
//最后一件价格
BigDecimal lastPrice = price.add(residue);
//3.34
System.out.println(lastPrice);
}

总结


普通的计算可以以最小金额作为计算单位并且用Long进行计算,而面对汇率、计算量大的场景可以采用BigDecimal作为计算单位


创建BigDecimal有两种常用的方式,字符串作为构造的参数以及浮点型作为静态方法valueOf��参数,后者在数据大/小的情况下toString方法会采用科学计数法,因此最好使用字符串作为构造器参数的方式


BigDecimal比较大小时,如果需要小数位数精度都相同就采用equals方法,忽略小数位数比较可以使用compareTo方法


BigDecimal进行运算时,加减运算会采用原始两个数据中精度最长的作为结果的精度,乘法运算则是将两个数据的精度相加得到结果的精度,而除法没有规律,必须指定小数位数和舍入模式,其他运算方式也建议主动设置小数位数和舍入模式进行兜底


当遇到商品平摊价格除不尽的情况时,可以将余数加到最后一件商品的价格进行兜底


最后(不要白嫖,一键三连求求拉~)


本篇文章被收入专栏 Java,感兴趣的同学可以持续关注喔


本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~


有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~


关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜


作者:菜菜的后端私房菜
来源:juejin.cn/post/7400096469723643956
收起阅读 »

Java中的双冒号运算符(::)及其应用

在Java 8引入的Lambda表达式和函数式接口之后,双冒号运算符(::)成为了一项重要的功能。它可以将方法或构造函数作为参数传递,简化了编码和提升了代码的可读性。本文将介绍Java中的双冒号运算符及其常见应用场景。 双冒号运算符(::)的语法 双冒号运算符...
继续阅读 »

在Java 8引入的Lambda表达式和函数式接口之后,双冒号运算符(::)成为了一项重要的功能。它可以将方法或构造函数作为参数传递,简化了编码和提升了代码的可读性。本文将介绍Java中的双冒号运算符及其常见应用场景。


双冒号运算符(::)的语法


双冒号运算符的语法是类名/对象名::方法名。具体来说,它有三种不同的使用方式:



  1. 作为静态方法的引用:ClassName::staticMethodName

  2. 作为实例方法的引用:objectReference::instanceMethodName

  3. 作为构造函数的引用:ClassName::new


静态方法引用


首先,我们来看一下如何使用双冒号运算符引用静态方法。假设有一个Utils类,其中有一个静态方法processData


public class Utils {
public static void processData(String data) {
System.out.println("Processing data: " + data);
}
}

我们可以使用双冒号运算符将该方法作为参数传递给其他方法:


List<String> dataList = Arrays.asList("data1", "data2", "data3");
dataList.forEach(Utils::processData);

上述代码等效于使用Lambda表达式的方式:


dataList.forEach(data -> Utils.processData(data));

通过使用双冒号运算符,我们避免了重复写Lambda表达式,使代码更加简洁和易读。


实例方法引用


双冒号运算符还可以用于引用实例方法。假设我们有一个User类,包含了一个实例方法getUserInfo


public class User {
public void getUserInfo() {
System.out.println("Getting user info...");
}
}

我们可以通过双冒号运算符引用该实例方法:


User user = new User();
Runnable getInfo = user::getUserInfo;
getInfo.run();

上述代码中,我们创建了一个Runnable对象,并将user::getUserInfo作为方法引用赋值给它。然后,通过调用run方法来执行该方法引用。


构造函数引用


在Java 8之前,要使用构造函数创建对象,需要通过写出完整的类名以及参数列表来调用构造函数。而使用双冒号运算符,我们可以将构造函数作为方法引用,实现更加简洁的对象创建方式。


假设有一个Person类,拥有一个带有name参数的构造函数:


public class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

我们可以使用双冒号运算符引用该构造函数并创建对象:


Supplier<Person> personSupplier = Person::new;
Person person = personSupplier.get();
person.getName(); // 调用实例方法

上述代码中,我们使用Person::new将构造函数引用赋值给Supplier接口,然后通过get方法创建了Person对象。


总结


本文介绍了Java中双冒号运算符(::)的语法及其常见的应用场景。通过双冒号运算符,我们可以更方便地引用静态方法、实例方法和构造函数,使得代码更加简洁和可读。双冒号运算符是Java 8引入的重要特性,对于函数式编程和Lambda表达式的使用起到了积极的推动作用。


希望本文能够帮助您理解和应用双冒号运算符,提高Java开发的效率和代码质量。如有任何问题或疑惑,欢迎提问!


作者:每天一个技术点
来源:juejin.cn/post/7316532841923805184
收起阅读 »

揭秘外卖平台的附近公里设计

背景 相信大家都有点外卖的时候去按照附近公里排序的习惯,那附近的公里是怎么设计的呢?今天shigen带你一起揭秘。 分析 我们先明确一下需求,每个商家都有一个地址对吧,我们也有一个地址,我们点餐的时候,就是以我们自己所在的位置为圆心,向外辐射,这一圈上有一堆的...
继续阅读 »

背景


相信大家都有点外卖的时候去按照附近公里排序的习惯,那附近的公里是怎么设计的呢?今天shigen带你一起揭秘。


分析


我们先明确一下需求,每个商家都有一个地址对吧,我们也有一个地址,我们点餐的时候,就是以我们自己所在的位置为圆心,向外辐射,这一圈上有一堆的商家。类似我下方的图展示:



想到了位置,我们自然想到了卫星定位,想到了二维的坐标。那这个需求我们有什么好的设计方案吗?


redis的GEO地理位置坐标这个数据结构刚好能解决我们的需求。


GEO


GEO 是一种地理空间数据结构,它可以存储和处理地理位置信息。它以有序集合(Sorted Set)的形式存储地理位置的经度和纬度,以及与之关联的成员。


以下是 Redis GEO 的一些常见操作:



  1. GEOADD key longitude latitude member [longitude latitude member ...]:将一个或多个地理位置及其成员添加到指定的键中。 示例:GEOADD cities -122.4194 37.7749 "San Francisco" -74.0059 40.7128 "New York"

  2. GEODIST key member1 member2 [unit]:计算两个成员之间的距离。 示例:GEODIST cities "San Francisco" "New York" km

  3. GEOPOS key member [member ...]:获取一个或多个成员的经度和纬度。 示例:GEOPOS cities "San Francisco" "New York"

  4. GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]:根据给定的经纬度和半径,在指定范围内查找与给定位置相匹配的成员。 示例:GEORADIUS cities -122.4194 37.7749 100 km WITHDIST COUNT 5


Redis 的 GEO 功能可用于许多应用场景,例如:



  • 位置服务:可以存储城市、商店、用户等位置信息,并通过距离计算来查找附近的位置。

  • 地理围栏:可以存储地理围栏的边界信息,并检查给定的位置是否在围栏内。

  • 最短路径:可以将城市或节点作为地理位置,结合图算法,查找两个位置之间的最短路径。

  • 热点分析:可以根据位置信息生成热力图,统计热门区域或目标位置的访问频率。


Redis 的 GEO 功能提供了方便且高效的方式来存储和操作地理位置信息,使得处理地理空间数据变得更加简单和快速。



默默的说一句,redis在路径规划下边竟然也这么厉害!



好的,那我们就来开始实现吧。今天我的操作还是用代码来展示,毕竟经纬度在控制台输入可能会出错。


代码实现


今天的案例是将湖北省武汉市各个区的数据存储在redis中,并以我所在的位置计算离别的区距离,以及我最近10km内的区。数据来源



我的测试代码如下,其中的运行结果也在对应的注释上有显示。



因为代码图片的宽度过长,导致代码字体很小,在移动端可尝试横屏观看;在PC端可尝试右键在新标签页打开图片。




以上的代码案例也参考:Redis GEO 常用 RedisTemplate API(Java),感谢作者提供的代码案例支持。


总结


对于需要存储地理数据和需要进行地理计算的需求,可以尝试使用redis进行解决。当然,elasticsearch也提供了对应的数据类型支持。


作者:shigen01
来源:juejin.cn/post/7275595571733282853
收起阅读 »

全网显示IP归属地,免费可用,快来看看

前言 经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢? 某些收费平台的API 我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服...
继续阅读 »

前言


经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢?



某些收费平台的API


我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服务通常是收费的,而且免费额度有限,适合测试使用,但如果要在生产环境中使用,很可能不够支撑需求。



离线库推荐


那么,有没有免费的离线API库呢?UP现在推荐一个强大的离线库给大家,一个准确率高达99.9%的离线IP地址定位库,查询速度仅需0.0x毫秒,而且数据库仅10兆字节大小。此库提供了Java、PHP、C、Python、Node.js、Golang、C#等多种查询绑定,同时支持Binary、B树和内存三种查询算法。



这个库大家可以在GitHub上搜索:ip2region,即可找到该开源库。


使用


下面使用Java代码给大家演示下如何使用这个IP库,该库目前支持多重主流语言。


1、引入依赖


<dependency>
   <groupId>org.lionsoul</groupId>
   <artifactId>ip2region</artifactId>
   <version>2.7.0</version>
</dependency>

2、下载离线库文件 ip2region.xdb



3、简单使用代码


下面,我们通过Java代码,挑选某个国内的IP进行测试,看看会输出什么样的结果


public class IpTest {

   public static void main(String[] args) throws Exception {
       // 1、创建 searcher 对象 (修改为离线库路径)
       String dbPath = "C:\Users\Administrator\Desktop\ip2region.xdb";
       Searcher searcher = null;
       try {
           searcher = Searcher.newWithFileOnly(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、查询
       String ip = "110.242.68.66";
       try {
           long sTime = System.nanoTime(); // Happyjava
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }

       // 3、关闭资源
       searcher.close();

       // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
  }

}


输出结果为:


{region: 中国|0|河北省|保定市|联通, ioCount: 3, took: 1192 μs}

其中,region的格式为 国家|区域|省份|城市|ISP,缺省的地域信息默认是0。


当然,这个库不只是支持国内的IP,也支持国外的IP。



其他语言可以参考该开源库的说明文档。


总结


这是一个准确率非常高的离线库,如果项目里有IP定位需求的,可以试下该库。


作者:happyjava
来源:juejin.cn/post/7306334713992708122
收起阅读 »

如何创建一张被浏览器绝对信任的 https 自签名证书?

在一些前端开发场景中,需要在本地创建 https 服务,Node.js 提供了 https 模块帮助开发者快速创建 https 的服务器,示例代码如下: const https = require('https') const fs = require('fs...
继续阅读 »

在一些前端开发场景中,需要在本地创建 https 服务,Node.js 提供了 https 模块帮助开发者快速创建 https 的服务器,示例代码如下:


const https = require('https')
const fs = require('fs')
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
}
const server = https.createServer(options, (req, res) => {
res.writeHead(200)
res.end('hello world\n')
})
server.listen(8080)

与创建 http 服务最大的区别在于:https 服务需要证书。因此需要在 options 选项中提供 keycert 两个字段。大部分前端不知道如何创建 keycert,虽然网上能查到一些 openssl 命令,但也不知道是什么含义。所谓授人以鱼不如授人以渔,这里先从一些基本概念讲起,然后一步步教大家如何创建一个可以被浏览器绝对信任的自签名证书。


密码学知识


首先要知道加密学中非常重要的一个算法:公开密钥算法(Public Key Cryptography),也称为非对称加密算法(Asymmetrical Cryptography),算法的密钥是一对,分别是公钥(public key)和私钥(private key),一般私钥由密钥对的生成方(比如服务器端)持有,避免泄露,而公钥任何人都可以持有,也不怕泄露。



一句话总结:公钥加密、私钥解密


公私钥出了用于加解密之外,还能用于数字签名。因为私钥只有密钥对的生成者持有,用私钥签署(注意不是加密)一条消息,然后发送给任意的接收方,接收方只要拥有私钥对应的公钥,就能成功反解被签署的消息。


一句话总结:私钥加签、公钥验证


由于只有私钥持有者才能“签署”消息,如果不考虑密钥泄露的问题,就不能抵赖说不是自己干的。


数字签名和信任链


基于数字签名技术,假设 A 授权给 B,B 授权给 C,C 授权给 D,那么 D 就相当于拿到了 A 的授权,这就形成了一个完整的信任链。因此:



  • 信任链建立了一条从根证书颁发机构(Root CA)到最终证书持有人的信任路径

  • 每个证书都有一个签名,验证这个签名需要使用颁发该证书的机构的公钥

  • 信任链的作用是确保接收方可以验证数字签名的有效性,并信任签名所代表的身份


以 https 证书在浏览器端被信任为例,整个流程如下:



可以看到,在这套基础设施中,涉及到很多参与方和新概念,例如:



  • 服务器实体:需要申请证书的实体(如某个域名的拥有者)

  • CA机构:签发证书的机构

  • 证书仓库:CA 签发的证书全部保存到仓库中,证书可能过期或被吊销。

  • 证书校验方:校验证书真实性的软件,例如浏览器、客户端等。


这些参与方、概念和流程的集合被称为公钥基础设施(Public Key Infrastructure)


X.509 标准


为了能够将这套基础设施跑通,需要遵循一些标准,最常用的标准是 X.509,其内容包括:



  • 如何定义证书文件的结构(使用 ANS.1 来描述证书)

  • 如何管理证书(申请证书的流程,审核身份的标准,签发证书的流程)

  • 如何校验证书(证书签名校验,校验实体属性,比如的域名、证书有效期等)

  • 如何对证书进行撤销(包括 CRL 和 OCSP 协议等概念)


X.509标准在网络安全中广泛使用,特别是在 TLS/SSL 协议中用于验证服务器和客户端的身份。在 Node.js 中,当 tls 通道创建之后,可以通过下面两个方法获取客户端和服务端 X.509 证书:



生成证书


要想生成证书,首要有一个私钥(private key),私钥的生成方式为:


$ openssl genrsa -out key.pem 2048

会生成一个 key.pem 文件,内容如下:


-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCB....
-----END PRIVATE KEY-----

然后,还需要一个与私钥相对应的公钥(public key),生成方式:


$ openssl req -new -sha256 -key key.pem -out csr.pem

按照提示操作即可,下面是示例输入(中国-浙江省-杭州市-西湖-苏堤-keliq):


You are about to be asked to enter information that will be incorporated
int0 your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Zhejiang
Locality Name (eg, city) []:Hangzhou
Organization Name (eg, company) [Internet Widgits Pty Ltd]:West Lake
Organizational Unit Name (eg, section) []:Su Causeway
Common Name (e.g. server FQDN or YOUR name) []:keliq
Email Address []:email@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:123456
An optional company name []:XiHu

生成的文件内容如下:


-----BEGIN CERTIFICATE REQUEST-----
MIIDATCCAekCAQAwgY8xCzAJBgNVBAYTAkNOMREw...
-----END CERTIFICATE REQUEST-----

公钥文件创建之后,接下来有两个选择:



  • 将其发送给 CA 机构,让其进行签名

  • 自签名


自签名


如果是本地开发,我们选择自签名的方式就行了,openssl 同样提供了命令:


$ openssl x509 -req -in csr.pem -signkey key.pem -out cert.pem
Certificate request self-signature ok
subject=C = CN, ST = Zhejiang, L = Hangzhou, O = West Lake, OU = Su Causeway, CN = keliq, emailAddress = email@example.com

最终得到了 cert.pem,内容如下:


-----BEGIN CERTIFICATE-----
MIIDpzCCAo8CFAf7LQmMUweTSW+ECkjc7g1uy3jCMA0...
-----END CERTIFICATE-----

到这里,所有环节都走完了,再来回顾一下,总共生成了三个文件,环环相扣:



  • 首先生成私钥文件 key.pem

  • 然后生成与私钥对应的公钥文件 csr.pem

  • 最后用公私钥生成证书 cert.pem


实战——信任根证书


用上面自签名创建的证书来创建 https 服务:


const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
}

启动之后,如果你在浏览器中访问,会发现出错了:



命名是 https 服务,为什么浏览器说不是私密连接呢?因为自签名证书默认是不被浏览器信任的,只需要将 cert.pem 拖到钥匙里面即可,然后修改为「始终信任」,过程中需要验证指纹或者输入密码:



如果你觉得上述流程比较繁琐,可以用下面的命令行来完成:


$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain cert.pem

实战——指定主题备用名称


然而,即使添加了钥匙串信任,问题似乎并没有解决,报错还是依旧:



仔细看,其实报错信息发生了变化,从原来的 NET::ERR_CERT_AUTHORITY_INVALID 变成了 NET::ERR_CERT_COMMON_NAME_INVALID,这又是怎么回事呢?我们点开高级按钮看一下详细报错:



这段话的意思是:当前网站的 SSL 证书中的通用名称(Common Name)与实际访问的域名不匹配。


证书中会包含了一个通用名称字段,用于指定证书的使用范围。如果证书中的通用名称与您访问的域名不匹配,浏览器会出现NET::ERR_CERT_COMMON_NAME_INVALID错误。


一句话描述,证书缺少了主题备用名称(subjectAltName),而浏览器校验证书需要此字段。为了更好的理解这一点,我们可以用下面的命令查看证书的完整信息:


$ openssl x509 -in cert.pem -text -noout

输出结果如下:


Certificate:
Data:
Version: 1 (0x0)
Serial Number:
07:fb:2d:09:8c:53:07:93:49:6f:84:0a:48:dc:ee:0d:6e:cb:78:c2
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = CN, ST = Zhejiang, L = Hangzhou, O = West Lake, OU = Su Causeway, CN = keliq, emailAddress = email@example.com
Validity
Not Before: Nov 15 06:29:36 2023 GMT
Not After : Dec 15 06:29:36 2023 GMT
Subject: C = CN, ST = Zhejiang, L = Hangzhou, O = West Lake, OU = Su Causeway, CN = keliq, emailAddress = email@example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ac:63:b1:f1:7a:69:aa:84:ef:9d:0e:be:c1:f7:
80:3f:6f:59:e1:7d:c5:c6:db:ff:2c:f3:99:12:7f:
...
Exponent: 65537 (0x10001)
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
70:d9:59:10:46:dc:7b:b3:19:c8:bd:4b:c5:70:4f:89:b6:6a:
53:1c:f2:35:27:c8:0a:ed:a8:0a:13:1f:46:3e:e7:a7:ff:1f:
...

我们发现,这个证书并没有 Subject Alternative Name 这个字段,那如何增加这个字段呢?有两种方式:


指定 extfile 选项


$ openssl x509 -req \
-in csr.pem \
-signkey key.pem \
-extfile <(printf "subjectAltName=DNS:localhost") \
-out cert.pem

再次用命令查看证书详情,可以发现 Subject Alternative Name 字段已经有了:


...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:localhost
X509v3 Subject Key Identifier:
21:65:8F:93:49:BC:DF:8C:17:1B:6C:43:AC:31:3C:A9:34:3C:CB:77
...

用新生成的 cert.pem 启动 https 服务,再次访问就正常了,可以点击小锁查看证书详细信息:



但是如果把 localhost 换成 127.0.0.1 的话,访问依然被拒绝,因为 subjectAltName 只添加了 localhost 这一个域名,所以非 localhost 域名使用此证书的时候,浏览器就会拒绝。



新建 .cnf 文件


这次我们新建一个 ssl.cnf 文件,并在 alt_names 里面多指定几个域名:


[req]
prompt = no
default_bits = 4096
default_md = sha512
distinguished_name = dn
x509_extensions = v3_req

[dn]
C=CN
ST=Zhejiang
L=Hangzhou
O=West Lake
OU=Su Causeway
CN=keliq
emailAddress=keliq@example.com

[v3_req]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName=@alt_names

[alt_names]
DNS.1 = localhost
IP.2 = 127.0.0.1
IP.3 = 0.0.0.0

然后一条命令直接生成密钥和证书文件:


$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 \
-config openssl.cnf \
-keyout key.pem \
-out cert.pem

再次查看证书详情,观察 Subject Alternative Name 字段:


...
X509v3 extensions:
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Subject Alternative Name:
DNS:localhost, IP Address:127.0.0.1, IP Address:0.0.0.0
X509v3 Subject Key Identifier:
B6:FC:1E:68:CD:8B:97:D0:80:0E:F1:18:D3:39:86:29:90:0B:9D:1F
...

这样无论是访问 localhost 还是 127.0.0.1 或者 0.0.0.0,浏览器都能够信任。



作者:乔珂力
来源:juejin.cn/post/7301574056720744483
收起阅读 »

云音乐贵州机房迁移总体方案回顾

一、背景2023年确定要将云音乐整体服务搬迁至贵州机房,项目需要在各种限制条件下,保障2000+应用、100w+QPS的服务稳定迁移,是云音乐历史上规模最大、人员最多、难度最高的技术项目。在此过程中,解决了大量历史技术债务,同时化解了大量新增系统性风险。以下为...
继续阅读 »

一、背景

2023年确定要将云音乐整体服务搬迁至贵州机房,项目需要在各种限制条件下,保障2000+应用、100w+QPS的服务稳定迁移,是云音乐历史上规模最大、人员最多、难度最高的技术项目。在此过程中,解决了大量历史技术债务,同时化解了大量新增系统性风险。以下为总体方案回顾。

二、项目难点

  • 迁移规模大
    • 此次需要云音乐以及旗下独立App的服务均整体迁移至贵州。涉及2000+应用、100w+QPS的稳定迁移,同时涉及中间件、存储、机房、三方依赖服务等整体的搬迁,搬迁规模大。
  • 业务复杂度高
    • 场景复杂。迁移规模大,带来更广的业务场景覆盖。而不同的场景对数据一致性要求不同、延迟敏感度不同。迁移方案需要考虑各种场景带来的问题,并提供标准化的解决方案。
    • 服务间依赖复杂。此次带来约2000+应用的搬迁,各服务间的调用和依赖情况复杂,在分批迁移方案中需要协调,以及解决迁移期间跨机房30msRT上升带来的问题。
  • 历史积弊多
    • 贵州迁移前,存在诸多历史技术积弊,影响着全站整体的稳定性。
  • 新增风险大
    • 贵州迁移带来诸多新增风险,且风险大、解决难度高。
    • 部分场景无法做到真实环境全流程预演。
    • 在基础技术建设上,也有一些不足的情况,影响整体搬迁执行效率、迁移准确性。
  • 限制条件严苛
    • 云音乐有着大量的用户基数,此次搬迁要求:不停机迁移、不产生P2及以上事故。除此之外还有机器、网络带宽、网络稳定性、网络RT、迁移方案等限制条件。
  • 事项推进&协调难度大
    • 此次搬迁规模大,同样,参与人员规模大,整体协调难度大
    • 此外带来较多的人因风险。可能因极小的细节未执行到位,就会造成全局事故。

三、重点限制&要求

  • 尽可能少采购或不采购额外的机器,贵州和杭州无法完全对等部署。
  • 杭州与贵州的长传带宽控制在200Gbps以内,且存在闪断的可能性,各迁移方案需要重点考虑闪断带来的影响。
  • 贵州机房与杭州机房之间网络延迟约30ms,各方迁移方案需重点考虑机房延迟带来的影响。
  • 业务可用性要求:不影响核心重点业务场景的可用性,不出现P2及以上事故。
  • 控制迁移方案对业务代码的侵入。

四、分批方案

1. 分批的原则

1.1 团队/领域间解耦

大团队/领域之间的迁移方案尽可能解耦,分不同批次搬迁。好处:

  • 可以将问题拆分、领域清晰。
  • 大数据、算法、云音乐技术中心串行搬迁,可以实现机器资源池共享,降低机器采购成本。
  • 降低单一团队/领域切流时问题处理复杂度。

1.2 服务端流量自闭环

云音乐服务端需要将流量闭环在同一个机房,避免产生跨区域调用。

云音乐经过微服务之后,目前存在千+服务,各服务间依赖复杂。在贵州机房与杭州机房之间网络延迟约30ms的背景下,每产生一次跨区域调用,则RT上升30ms。

1.3 C端优先

优先迁移C端相关的应用及其资源,其次B端。

关于此处,会有同学认为优先B端可能会更稳,但优先采用B端优先,会有如下问题:

  • B端服务搬迁后,腾挪的机器有限。
  • B端服务与C端服务相差较大,即使B端服务先行搬迁无问题,也不足以证明C端服务就一定没问题。

对于如何保障C端服务搬迁的稳定性,在文章后续章节展开。

1.4 在可用资源范围内

迁移期间,需要在贵州准备与杭州同等规模的机器资源,因此批次不可能不受到资源的限制。其主要受限制资源为:

  • 机器资源
  • 贵州&杭州的长传带宽资源

因此,按照以上原则进行分批后,若资源仍不足,再根据团队/领域拆分出第二批

2. 最终分批方案

基于以上原则,最终分批方案如下所示 贵州机房迁移分批概览.png

  • 大数据、算法、技术中心串行搬迁。
  • 心遇因强依赖云信IM服务,与云信服务独立搬迁
  • 技术中心应用基本一批次全部搬迁完成。
  • 技术中心的转码、公技侧后台、质量侧系统在第二批次搬迁完成。

五、切流方案

1. 切流的原则

1.1 可灰度

能够按照用户ID、设备ID、IP、流量标几个维度逐步灰度切流。

  • 利于预热。在服务启动后,缓存、连接池需要随请求逐步预热,若流量直接全部打过来,可能会将服务打垮。
  • 利于测试。能够灰度测试整体功能,避免大面积异常。

1.2 可回滚

尽管做了各种稳定性保障来避免回滚,但是如遇到极端情况,仍有整体回滚的可能性。因此搬迁方案必须可回滚。

1.3 控制长传带宽

在切流过程中,杭州和贵州之间会有大量的服务访问、数据传输,从而可能突破长传带宽200Gbps的限制。因此切流方案中必须减少不必要的跨区域流量。

2. 切流方案

2.1 切流点选择

贵州机房迁移切流点选择.png

服务端整体通用架构简化后,如上图所示,因此有如下几个切入点:

  • 客户端切流。客户端通过动态切换域名配置,可实现流量的切换。切流算法可以与网关使用保持一致,我们在贵州迁移中就采用了此方案,从而大幅降低贵州与杭州的长传带宽。
  • DNS切换。因DNS存在缓存过期,不适合作为流量控制的主要手段。在贵州迁移中,我们主要用其作为长尾流量的切换的手段。
  • 四层LB切流、Nginx切流。主要由SA侧负责,因自动化和操作复杂度等因素,在贵州迁移中,四层LB切流只用于辅助切流手段,Nginx因过高的人工操作复杂度,不用于切流。
  • 网关切流。网关作为服务端广泛接触的首要流量入口,其系统建设相对完善、自动化程度较高,因此作为主要切流手段。在此次迁移中,网关支持按用户ID、设备ID、IP进行按比例切流。
  • 定时任务、MQ切换。主要用于定时任务、MQ的流量切换。
  • RPC流量控制。RPC流量路由策略与网关保持一致,依据切流比例,进行RPC流量调用。从而避免跨机房RT的不可控。
  • 存储层切换。主要负责存储的切换。

2.2 存储层迁移策略

云音乐业务场景较多,不同场景下对数据一致性的要求也不一样,例如:营收下的订单类场景需要数据强一致性,而点赞需要数据最终一致性即可。

在涉及不同的存储时,也有着多种多样的迁移策略。对此,中间件以及各存储层支持了不同的迁移策略选择,各个业务基于不同的场景,选择正确的策略。迁移策略主要如下:

类型迁移策略
DB读本地写远程、读远程写远程、读本地写本地、禁写
Redis读写远程+需要禁写、读本地写远程+需要禁写、读写本地
Memcached异步双写、同步双写、不同步

2.3 切流步骤

对以上切入点再次进行分类,可再次简化为流量层切流、存储层切换。在正式切流时,我们按照如下步骤进行切流。 贵州机房迁移-切流步骤.png

3. 回滚方案

先存储层按序切换,然后流量层按序切换。 贵州机房迁移-回滚步骤.png

六、稳定性保障&治理

1. 全域的稳定性风险

  • 全域的稳定性风险。我们在做一般的活动稳定性保障时,一般从活动的主链路出发,再梳理相关依赖,从而整理出稳定性保障&治理的重点。而这种方法确不适用于贵州机房迁移,从前面的分批概览图可得知:此次贵州机房迁移带来全域的稳定性风险。
  • 墨菲定律:"如果一件事情有出错的可能性,那么它最终一定会出错。"
  • 业界没有类似的经验可参考

因此整个项目组也在摸着石头过河,在此过程中,既有大的方案的设计,也有细枝末节的问题发现和推进处理。总结起来,我们总共从以下几个方面着手进行稳定性保障:

  • 信息梳理&摸查
  • 新增风险发现&处理
  • 历史技术债务处理
  • 标准化接入
  • 监控告警增强
  • 应急预案保障
  • 业务侧技术方案保障
  • 杭州集群下线保障

2. 信息梳理&摸查

盘点梳理机器资源情况、网络带宽、迁移期间服务可用性要求等全局限制条件,从而确定分批方案、迁移思路。

2.1 机器资源盘点

主要盘点核数、内存。在此过程中,也推进了资源利用率优化、废弃服务下线等事宜。 通过如下公式计算机器资源缺口:搬迁机器缺口 = 搬迁所需数量 -(可用数量+可优化数量)

2.2 长传带宽盘点

需要控制云音乐的长传带宽总量 <= 相对安全的带宽量 相对安全的带宽量 = (长传带宽总量 / 2 x 0.8) - 已被占用带宽量

2.3 迁移期间服务可用性要求

若业务允许全站停服迁移、或仅保障少量核心服务不挂,那么整体迁移方案会简单很多。因此业务对迁移期间的可用性要求,关乎着搬迁方案如何设计。 最终讨论后确定,需要:迁移不产生P2及以上事故

2.4 服务间跨区域调用RT摸查

基于Trace链路,预测分批情况下RT增长情况。

3. 新增系统性风险

此次贵州迁移主要带来的新增系统性风险是:

  • 因公网质量问题,带来迁移后用户体验差的风险。
  • 因跨机房延迟30ms ,带来的业务侧应用雪崩风险。
  • 因跨机房传输网络不稳定,带来的整体系统性风险。
  • 因杭州和贵州机房同时部署,带来的服务节点数量、API数量、RPC数量翻倍风险
  • 因大规模数据变更,带来的系统性能风险。
  • 因新机房建设、搬迁,带来的底层基础设施风险。
  • 因全域团队协作、大范围配置变更&发布,带来的人因操作、协作风险。

3.1 因公网质量问题,带来迁移后用户体验差的风险

贵州公网质量如何?迁移至贵州之后是否会因公网质量问题,导致用户体验差?由于云音乐用户基数大,且注重用户体验,这个是必须提前摸清的问题。若公网质量真的存在较大问题,云音乐可能会停止贵州迁移项目。

对此,我们通过如下方式进行了公网质量验证和保障:

  1. 通过客户端预埋逻辑,抽样检测同时请求杭州和贵州机房的RT差异。
  2. 通过RT的差异,再下钻分析杭州和贵州机房的差异点。
  3. 解决或排除机房、客户端、域名配置等差异,最终得出公网质量的差异。
  4. 在正式切流前,解决完成客户端、机房等差异,保障整体网络请求质量。
  5. 通过QA侧的整体测试。

3.2 因跨机房延迟30ms ,带来的业务侧应用雪崩风险

云音乐C端服务当前的RT普遍在5~70ms之间,若增加30ms,可能会导致请求堆积、线程池打爆等风险。为避免此风险,我们从如下几个方面入手:

  • 尽可能同一批次搬迁,避免长期跨机房调用。
  • 同一批次应用,基于用户ID、设备ID、IP进行Hash,实现同机房调用优先。
  • 无法同一批次搬迁的应用。
    • 确保会只跨一次,避免因循环调用等原因导致的多次跨机房。
    • 需提供降级方案,对服务弱依赖。
  • 服务需通过QA侧的测试。

3.3 因跨机房传输网络不稳定,带来的整体系统性风险

跨机房网络的现状和参考数据:

  • 共计2条线,单条带宽为:100Gbps,但建议保持单条利用率在80%及以下。
  • 参考网易北京与杭州的长传带宽质量。
    • 可能会出现单条中断的情况,在网络侧的表现为网络抖动。若单条线中断,那么发生故障的请求会重连至另一条线。
    • 极低概率出现2条线全部中断的情况。

基于以上现状,需要重点考虑并解决:

  • 各中间件、存储在切流期间,长传网络出现问题时的表现、应对和兜底措施。例如ZK重连、重连失败后的重连风暴问题。
  • 各服务在切流完成后,若仍长期使用长传网络,若长传网络出现问题的表现、应对和兜底措施。

在贵州迁移项目中,我们对以上重点问题进行了梳理和解决,并制定了各种应急预案和极端情况下的回滚方案。

3.4 因杭州和贵州机房同时部署,带来的服务节点数量、API数量、RPC数量翻倍风险

因杭州和贵州机房同时部署,带来的服务节点数量、API数量、RPC数量翻倍风险

在服务节点数量、API数量、RPC数量翻倍后,主要对底层依赖带来连接、重连上的冲击,以及原有连接数上限的冲击。

在我们实际搬迁中,也因遗漏了这一点,导致线上ZK出现瓶颈,进而ZK挂掉的问题。其主要表现为在网关场景下存在数据推送瓶颈。最终通过网关侧的ZK拆分解决该问题。

除此之外,DB、Memcached、Redis、MQ等资源的连接数也可能会超过原先设定的上限,需要评估后进行调整。

3.5 因大规模数据变更,带来的系统性能风险

大规模数据变更的场景包含但不限于:

  • 批量调整配置中心值,因达到配置中心的性能瓶颈,导致配置变更时间过长,或服务挂掉。
  • 批量的服务部署、重启,因达到K8S、构建机的性能瓶颈,导致部署、重启时间过长,或服务挂掉。
  • 对迁移当晚核心路径上的服务进行集中访问、操作,因达到服务的性能瓶颈,导致访问超时、白屏、数据延迟、或服务挂掉的问题。

针对以上风险,我们重点对配置中心、K8S、贵州迁移管控平台等系统进行了性能优化,以支撑整体迁移。

3.6 因新机房建设、搬迁带来的底层基础设施风险。

因新机房建设、搬迁带来的底层基础设施风险包含但不限于:

  • 同城双活能力的缺失。为应对此风险,我们在逻辑上继续保留同城双活的能力,并暂时通过机房不同楼层的部署架构,来尽可能弥补同城双活能力的缺失。
  • 机器上架、环境搭建、网络传输等需确保达到验收标准。为应对此风险,运维侧提供相关方案保障整体环境,并最终通过业务侧QA验收。

3.7 因全域团队协作、大范围变更&发布,带来的人因操作、协作风险

在贵州迁移前,已经有多次发生因配置变更错误带来的事故。而此项目带来从未有过的全域迁移,全域协作,大范围变更&发布,风险不可谓不高。在此过程中,通过了许多方式来保障事项的落地,其中比较关键的点,也是项目成功的关键点包括:

  • 各部门领导与同事的支持。
  • 分工明确。在战略、战术、细节、事项推进等多个点均有相关人员把控,各司其职。
  • 各项信息的细化梳理&定位。
  • 定期的沟通协作会议,通过敏捷式项目管理,进行滚动式问题发现。
  • 问题发现、治理、验证必须闭环。
  • 尽可能中心系统化、自动化处理。无法自动化的,则提供标准化实施手册。
  • 重点问题,case by case,one by one。

4. 历史技术债务处理

在贵州迁移项目中,比较突出的历史债务处理有:

  • ZK强依赖问题
  • 在线业务Kafka迁移Nydus。
  • 配置硬编码
  • 服务间依赖改造
  • 资源优化&控制
  • 心遇依赖拆分
  • 元信息不准确
  • 组件版本过于陈旧问题
  • 测试环境自动化部署成功率低
  • 租户多集群拆分为多应用

4.1 ZK强依赖问题

ZK的不稳定已导致云音乐最高出现P1级事故,在贵州迁移项目中,因网络环境、机房环境、迁移复杂度等因素,ZK服务挂掉的概率极大,因此必须不能对其强依赖。

最终中间件侧对其改造,支持ZK发生故障时,其注册信息降级到本地内存读取。并推进相关依赖方进行升级改造。

4.2 在线业务Kafka迁移Nydus。

Nydus作为云音乐主力MQ产品,相较开源Kafka有更好的监控、运维等能力,Kafka在云音乐在线业务中已不再推荐使用。在贵州迁移中,MQ也需要进行两地切换/切流。

主要收益:

  • 在线业务稳定性
  • Kafka机器资源回收
  • MQ切流特性&历史债务收敛

在推进层面:

  • 第一里程碑:生产者完成双写
  • 第二里程碑:消费者完成双消费
  • 第三里程碑:完成废弃TOPIC下线、代码下线等收尾工作

4.3 配置硬编码

在贵州迁移项目中,需要做大量的配置迁移、变更。其主要为:机房名、集群名、机器IP、机器Ingress域名的变化。而这些在配置中心、代码、自动化脚本、JVM参数中均有存在,此外,IP黑白名单还可能涉及到外部厂商的改造变更。

在具体推进上,采用自动化扫描+人工梳理结合,并辅以标准化改造指引文档。

  • 自动化扫描:通过代码扫描、配置中心扫描、JVM参数扫描、连接扫描等方式进行问题发现。
  • 人工梳理:外部厂商、不受Git管控的脚本、以及运维侧的配置(例如:存储层访问权限的黑白名单等)、以及自动化扫描可能的遗漏,由各研发、运维人员再次自行梳理。

4.4 服务间依赖改造

核心应对杭州与贵州跨机房30ms RT和长传网络不稳定的风险。对循环调用、不合理依赖、强依赖进行改造。

  • 减少不必要依赖。
  • 必须不能出现服务跨机房强依赖。
  • 不能因循环调用导致跨机房RT飙升。

4.5 资源优化&控制

因贵州需要与杭州同等容量部署,可能存在资源不足的情况。对此需要:

  • 统一服务的资源利用率标准,推进资源利用率改造
  • 对部分服务进行合并、下线、缩容处理。

4.6 心遇依赖拆分

因心遇强依赖云信,且云信IM为心遇核心业务功能,最终确定心遇为独立批次搬迁。因此心遇依赖的中台服务、存储、算法&大数据相关任务,均需拆分出来,不能与云音乐耦合,否则会产生跨机房调用,影响服务稳定性。

4.7 元信息不准确

在此次迁移中,存在较多的元信息不准确的问题,例如:

不足项解释
应用的元信息需要补充、更新1. 应用归属的团队信息不准确
2. 应用的废弃、待废弃状态未知
3. 测试应用、非业务应用信息偏杂乱
应用团队归属信息多处维护,未统一应用在多个平台均有维护,且均存在维护不准确的问题
应用的各项依赖信息不全应用依赖的db、redis、memcached资源,以及在配置中心的key无法全面准确拉取
应用的各项依赖信息可视化、系统化建设不足1. 应用依赖的组件版本、依赖的存储资源等,缺乏友好的可视化查询能力。
2. 各项信息之间的关联性建设不足
底层中间件、存储元信息不全1. 不同的ZK集群的用处缺乏统一维护。
2. 各项元信息反查调用源IP、集群、应用、团队、负责人的能力不足

以上问题在迁移中,通过脚本、1对1沟通确认、手动梳理等多种方式进行了临时处理,在贵州迁移后,仍需再全面的系统性规划。

4.8 组件版本过于陈旧问题

有较多的应用长期不升级,与最新版本跨度较大,存在较多的兼容性问题,需要人工进行升级处理。升级流程大致如下: 人工升级流程.png

在迁移中期,我们进行了自动升级平台建设,基本支持以上升级流程自动化。

4.9 测试环境自动部署成功率低

因此次迁移涉及全部的应用在不同环境的部署,全部人工操作的效率过低,因此我们在非线上环境均由脚本自动化部署,而测试环境由于维护不足,部署成功率较低。

4.10 租户多集群拆分为多应用

当前贵州迁移时整体会按照应用维度进行迁移、切流到贵州。因此对于中台租户型应用、多地域注册类型的应用需要拆分。

5. 标准化接入

除了以上提到的历史技术债务处理和新增系统性风险,公共技术侧大都提供了标准化的接入、改造治理方式。例如:

  • 贵州迁移中间件方案汇总。涵盖所有涉及中间件的迁移、切流、迁移策略、接入等指导方案。
  • 贵州迁移升级指导。涵盖自动升级与手动升级、脚手架应用与非脚手架应用的升级方案。
  • 贵州迁移线上部署指导。涵盖贵州线上部署前的各项必要准备事项,以及特殊应用的注意事项。
  • 贵州迁移监控大盘观测指导。涵盖各类迁移监控的观测指导。
  • 中台、多地域注册拆分指导。涵盖中台租户、多地域注册类型应用的拆分指导方案,以及整体的拆分流程、验证要点等。
  • ddb、redis、memcached、KSchedule等非标治理。涵盖各中间件、存储的非标风险列表、处理办法等。
  • 杭州集群下线指导。涵盖杭州集群如何观察、缩容、下线、机器回收的指导方案。

6. 监控告警

在监控告警层面,主要提供了:

  • 贵州迁移整体大盘监控。提供了迁移相关全局比例,异常流量,异常比例,能够区分是迁移导致的还是本身杭州服务就有问题导致。同时集成资源层相关指标,判断是单个资源有问题还是全部资源有问题。
  • 贵州迁移应用监控。提供了单个应用的贵州迁移监控,应用贵州杭州流量比例,异常流量,异常比例,能够区分是贵州还是杭州的问题。同时有资源相关的指标。
  • 杭州集群与贵州集群的哨兵监控对比分析。提供指定应用的杭州和贵州集群在CPU利用率、线程池满、异常比例、RT超时等维度的对比。
  • 全局/应用的SLO监控。提供核心指标受损监控。
  • 应用层面的系统监控。研发可通过哨兵、APM来查看定位具体的问题。

7. 应急预案

在贵州迁移期间,基于以上风险,主要准备如下应急预案:

  • 客户端截流。在开启后,客户端将访问本地或CDN缓存,不再向服务端发送请求。
  • 全站服务QPS限流至安全阈值。在开启后,全站的后端服务将限流调整至较低的安全阈值上,在极端情况下,避免因跨机房RT、跨机房传输、跨机房访问等因素的性能瓶颈引起服务端雪崩。
  • 长传带宽监控&限流。在开启后,部分离线数据传输任务将会被限流。保障在线业务的带宽在安全水位下。
  • 回滚方案。当出现重大问题,且无法快速解决时,逐步将存储、流量切回杭州。
  • 外网逃生通道。当出现长传网络完全中断,需要回滚至杭州。通过外网逃生通道实现配置、核心数据的回滚。
  • 业务领域内的应急预案。各业务领域内,需要考虑切流前的主动降级预案、切流中的应急预案。
  • 批量重启。当出现局部服务必须通过重启才能解决的问题时,将会启用批量重启脚本实现快速重启。当出现全局服务必须通过重启才能解决问题时,需要当场评估问题从而选择全量重启或全量回滚至杭州。

8. 业务技术侧方案

业务技术侧方案重点包含但不限于:

  • 应用搬迁范围、搬迁批次梳理明确。当上下游依赖的应用处于不同批次时,需要跨团队沟通协调。
  • 明确业务影响,从而确定各应用的中间件、存储迁移策略。
  • 历史技术债务处理
  • 标准化接入
  • 核心场景稳定性保障方案
  • 核心指标监控建设完善。
  • 切流SOP。包括切流前(前2天、前1天、前5分钟)、切流中、切流后各阶段的执行事项。
  • 切流降级方案、应急预案
  • 切流停止标准

9. 杭州集群下线

在服务迁移至贵州后,若杭州仍有流量调用,需排查流量来源,并推进流量下线或转移至贵州。先缩容观察,无正常流量、CDN回源等之后,再做集群下线。

七、测试&演练

此次贵州迁移,在各应用标准化治理之后,通过系统批量工具完成贵州各项环境的搭建、测试环境的批量部署。

1. 测试环境演练

1.1 准备事项

在测试演练开始前,我们重点做了如下准备:

  • 贵州测试环境批量创建。通过迁移工具,实现贵州测试集群的批量创建、配置批量迁移等。
  • 应用自动化升级。通过自动升级平台,实现大规模应用的批量升级,支持了各组件、各应用的多次快速验证、快速升级。
  • 测试环境自动化部署。通过自动化部署脚本,为支持测试环境能够多次、高效演练。
  • SOP梳理&平台建设。通过SOP平台,将SOP文档沉淀为系统能力,实现各SOP能力的系统化。
  • 迁移监控大盘建设。通过细化梳理监控指标,构建监控大盘,掌握各应用、各组件在切流期间的表现。

1.2 执行步骤

贵州机房迁移测试环境演练执行步骤.png 在测试环境演练,总体思路是逐步扩大验证范围,最终达到全局基本功能基本验证通过。以下为主要演练顺序,每一步视执行结果,再选择是否重复执行。

顺序验证事项
1验证中间件内部逻辑是否正确:
1. 网关、RPC、存储层路由策略是否正确。
2.验证监控大盘是否正确
3.验证SOP平台是否正确
4....
2验证存储层切换是否正确
3逐一对各业务团队进行演练:
1.加深各团队对切流能力的感知。
2.验证收集中间件、存储在各领域的表现。
3.验证各团队、各领域迁移策略的合理性
4对BFF、FaaS等特殊应用类型进行演练

2. 线上环境演练

因测试环境和线上环境仍存在较大的差异,需要摸清线上真实情况,在演练原则和演练目标上均较测试环境演练有更严格、细致的要求。

2.1 演练原则

  • 不对线上数据产生污染;
  • 不产生线上 P2 以上事故。

2.2 演练目标

分类目标内容
公技演练目标1. 切流验证,网关,rpc,贵州迁移大盘监控
2.网关切流比例、快慢,数据库 ddb 贵州跨机房建连对业务影响
3.端上切流,网关切流验证
业务演练目标1.流量切换,贵州跨机房对业务影响
2.业务指标和SLO
3.业务预案有效性验证
4.RT变化情况
存储演练目标1.ddb 复制延迟,连接数(由于跨机房创建DDB连接非常慢, 主要观察流量到贵州后新建连接对应用和数据库影响及恢复情况)
2.redis数据同步、整体表现
网络演练目标1.跨机房延迟情况
2.跨机房带宽实际占用
3.网络带宽占用监控

2.3 演练终止条件

  • P0、P1 核心场景 SLO 95%以下;
  • 用户舆情增长波动明显;
  • 跨机房网络大规模异常;
  • 大量业务指标或者数据异常;
  • 贵州流量达到预定 90%。

3. 独立App迁移验证

在云音乐主站正式切流前,先对云音乐旗下独立App进行了线上搬迁验证,保障云音乐迁移时的稳定性。

八、系统沉淀

1. SOP平台

SOP即标准作业程序(Standard Operating Procedure),源自传统工业领域,强调将某项操作以标准化、流程化的方式固化下来。

SOP平台将标准化、流程化的操作进行系统化呈现,并对接各中间件平台,实现操作效率的提升。在贵州迁移过程中,能够实现多部门信息同步、信息检查,并显著降低批量操作的出错概率、执行效率,降低人因风险。同时也可为后续其他大型项目提供基础支撑。

2. 自动升级平台

自动升级平台串联代码升级变更、测试部署、测试验证、线上发布、线上检测,实现升级生命周期重要节点的自动化。在贵州迁移过程中,显著提升整体升级、验证、部署效率。同时可为后续的大规模组件升级、组件风险治理、组件兼容性摸查、Sidecar式升级提供基础支撑。

九、不足反思

1. 元信息建设仍然不足

精准筛选出每项事宜涉及的范围,是顺利进行各项风险治理的前提条件。在此次贵州机房迁移中也暴露出元信息建设不足的问题。

不足项解释
应用的元信息需要补充、更新1. 应用归属的团队信息不准确
2. 应用的废弃、待废弃状态未知
3. 测试应用、非业务应用信息偏杂乱
应用团队归属信息多处维护,未统一应用在多个平台均有维护,且均存在维护不准确的问题
应用的各项依赖信息不全应用依赖的db、redis、memcached资源,以及在配置中心的key无法全面准确拉取
应用的各项依赖信息可视化、系统化建设不足1. 应用依赖的组件版本、依赖的存储资源等,缺乏友好的可视化查询能力。
2. 各项信息之间的关联性建设不足
底层中间件、存储元信息不全1. 不同的ZK集群的用处缺乏统一维护。
2. 各项元信息反查调用源IP、集群、应用、团队、负责人的能力不足

2. 各项元信息的创建、更新、销毁标准化、系统化

在贵州迁移过程中,做了历史技术债务处理、标准化接入方式,后续可针对各项元信息的创建、更新、销毁进行标准化、系统化建设。例如:

  • 应用、集群的创建和销毁需要前置校验、审批。以及后期的架构治理扫描。
  • 借助组件升级平台,实现组件发布、升级的标准化、系统化。
  • DB、Redis、Memcached、ZK的申请、使用、接入等标准化、防劣化。

3. 应用配置标准化

目前应用可做配置的入口有:配置中心、properties文件、props文件、JVM参数、硬编码。不同的中间件提供出的配置方式也各有不同,所以各应用的配置比较五花八门。因此可做如下改进:

  • 明确各种配置入口的使用标准。比如:什么时候建议用配置中心?什么时候建议用JVM参数?
  • 在组件提供侧、应用研发侧均有一定的宣贯、提示。避免配置方式过于杂乱。
  • 提供配置统一上报的能力。助力元信息的建设。

4. 批处理能力需再进一步增强

在贵州机房迁移中,除了SOP平台和自动升级平台的系统沉淀外,业务中间件、Horizon部署平台都提供了一定的工具支撑,从而在一定程度上提升了整体迁移的效率。在之后,随着对效率、系统间融合的要求的提高。需要继续在功能、性能、稳定性等多个层面,继续对批处理、系统间融合进行系统化建设。例如:

  • 批量拉取、筛选指定条件的应用以及相关依赖信息。
  • 基于指定的环境、团队、应用、集群等维度,进行服务的批量重启、部署。此处需要进一步提升测试环境部署成功率
  • 基于指定的应用、集群等维度,进行批量的服务复制、配置复制。

5. ZK稳定性、可维护性优化

在贵州迁移中,ZK的问题相对突出,对此也投入了比较多的人力去排查、解决以及推进风险治理。后续仍需要在ZK的稳定性、可维护性上探讨进一步优化的可能性:

  • ZK元信息的维护和使用标准。明确各ZK集群的用处、各ZK Path的用处,ZK集群间隔离、复用的标准,并推进相关标准化治理。
  • ZK故障时,因开启降级至内存,业务无法重启服务。若故障期间叠加其他事故,则会导致其他事故被放大。
  • 其他稳定性、可维护性梳理

6. 公技侧稳定性保障长效机制和系统化建设

尽管在贵州机房迁移中,做了大量的稳定性保障措施,但依赖每个研发对各自负责领域的理解、运维能力。是否能在团队管理、设施管理、服务管理、稳定性管理、架构设计等多方面,探索出一套可持续的长效保障机制?并进行一定的稳定性系统化建设?从而避免点状问题随机发生。

7. 组件生产、发布、治理能力增强

贵州迁移中涉及大量的组件变更与发布,以及业务侧组件升级与治理。组件可以从生产侧和使用侧进行分析,而组件生命周期主要由2条主线贯穿:

  • 组件生产发布线:组件的生产、测试验证、发布。
  • 组件风险治理线:风险定义、风险发现、升级推进、升级验证 组件治理.png 依据此分类,服务端的组件管理仍有较多可提升空间。



作者:网易云音乐技术团队
来源:juejin.cn/post/7389952004791894016
收起阅读 »

网站刚线上就被攻击了,随后我一顿操作。。。

大家好,我是冰河~~ 自己搭建的网站刚上线,短信接口就被一直攻击,并且攻击者不停变换IP,导致阿里云短信平台上的短信被恶意刷取了几千条,加上最近工作比较忙,就直接在OpenResty上对短信接口做了一些限制,采用OpenResty+Lua的方案成功动态封禁了频...
继续阅读 »

大家好,我是冰河~~


自己搭建的网站刚上线,短信接口就被一直攻击,并且攻击者不停变换IP,导致阿里云短信平台上的短信被恶意刷取了几千条,加上最近工作比较忙,就直接在OpenResty上对短信接口做了一些限制,采用OpenResty+Lua的方案成功动态封禁了频繁刷短信接口的IP。


一、临时解决方案


由于事情比较紧急,所以,当发现这个问题时,就先采用快速的临时方案解决。


(1)查看Nginx日志发现被攻击的IP 和接口


[root@binghe ~]# tail -f /var/log/nginx/access.log

发现攻击者一直在用POST请求 /fhtowers/user/getVerificationCode这个接口


2024-07-28-001.png


(2)用awk和grep脚本过滤nginx日志,提取攻击短信接口的ip(一般这个接口是用来发注册验证码的,一分钟如果大于10次请求的话就不是正常的访问请求了,大家根据自己的实际情况更改脚本)并放到一个txt文件中去,然后重启nginx


[root@binghe ~]# cat denyip.sh
#!/bin/bash
nginx_home=/usr/local/openresty/nginx
log_path=/var/log/nginx/access.log
tail -n5000 $log_path | grep getVerification | awk '{print $1}' |sort | uniq -c | sort -nr -k1 | head -n 100 |awk '{if($1>10)print ""$2""}' >$nginx_home/denyip/blocksip.txt
/usr/bin/nginx -s reload

(3)设置Nginx去读取用脚本过滤出来的blocksip.txt(注意一下,我这里的Nginx是用的openresty,自带识别lua语法的,下面会有讲openresty的用法)


location =  /fhtowers/user/getVerificationCode {  #短信接口
access_by_lua '
local f = io.open("/usr/local/openresty/nginx/denyip/blocksip.txt") #黑名单列表
for line in f:lines() do
if ngx.var.http_x_forwarded_for == line then #如果ip在黑名单列表里直接返回403
ngx.exit(ngx.HTTP_FORBIDDEN)
end
end
'
;
proxy_pass http://appservers; #不在名单里就转发给后台的tomcat服务器
}

(4)把过滤脚本放进crontab任务里,一分钟执行一次


[root@binghe ~]# crontab -e
*/1 * * * * sh /root/denyip.sh

(5)查看一下效果,发现攻击者的请求都被返回403并拒绝了


2024-07-28-002.png


二、OpenResty+Lua方案


临时方案有效果后,再将其调整成使用OpenResty+Lua脚本的方案,来一张草图。


2024-07-28-003.png


接下来,就是基于OpenResty和Redis实现自动封禁访问频率过高的IP。


2.1 安装OpenResty


安装使用 OpenResty,这是一个集成了各种 Lua 模块的 Nginx 服务器,是一个以Nginx为核心同时包含很多第三方模块的Web应用服务器,使用Nginx的同时又能使用lua等模块实现复杂的控制。


(1)安装编译工具、依赖库


[root@test1 ~]# yum -y install readline-devel pcre-devel openssl-devel gcc

(2)下载openresty-1.13.6.1.tar.gz 源码包,并解压;下载ngx_cache_purge模块,该模块用于清理nginx缓存;下载nginx_upstream_check_module模块,该模块用于ustream健康检查。


[root@test1 ~]# cd /usr/local/
[root@test1 local]# wget https://openresty.org/download/openresty-1.13.6.1.tar.gz
[root@test1 local]# tar -zxvf openresty-1.13.6.1.tar.gz
[root@test1 local]# cd openresty-1.13.6.1/bundle
[root@test1 local]# wget http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz
[root@test1 local]# tar -zxvf ngx_cache_purge-2.3.tar.gz
[root@test1 local]# wget https://github.com/yaoweibin/nginx_upstream_check_module/archive/v0.3.0.tar.gz
[root@test1 local]# tar -zxvf v0.3.0.tar.gz

(3)配置需安装的模块


# ./configure --help可查询需要安装的模块并编译安装
[root@test1 openresty-1.13.6.1]# ./configure --prefix=/usr/local/openresty --with-luajit --with-http_ssl_module --user=root --group=root --with-http_realip_module --add-module=./bundle/ngx_cache_purge-2.3/ --add-module=./bundle/nginx_upstream_check_module-0.3.0/ --with-http_stub_status_module
[root@test1 openresty-1.13.6.1]# make && make install

(4)创建一个软链接方便启动停止


[root@test1 ~]# ln -s /usr/local/openresty/nginx/sbin/nginx   /bin/nginx

(5)启动nginx


[root@test1 ~]# nginx  #启动
[root@test1 ~]# nginx -s reload #reload配置

如果启动时候报错找不到PID的话就用以下命令解决(如果没有更改过目录的话,让它去读nginx的配置文件就好了)


[root@test1 ~]# /usr/local/openresty/nginx/sbin/nginx  -c /usr/local/openresty/nginx/conf/nginx.conf 

2024-07-28-004.png


随后,打开浏览器访问页面。


2024-07-28-005.png


(6)在Nginx上测试一下能否使用Lua脚本


[root@test1 ~]# vim /usr/local/openresty/nginx/conf/nginx.conf

在server里面加一个


location /lua {
default_type text/plain;
content_by_lua ‘ngx.say(“hello,lua!”)’;
}

2024-07-28-006.png


加完后重新reload配置。


[root@test1 ~]# nginx  -s reload

在浏览器里输入 ip地址/lua,出现下面的字就表示Nginx能够成功使用lua了


2024-07-28-007.png


2.2 安装Redis


(1)下载、解压、编译安装


[root@test1 ~]# cd /usr/local/
[root@test1 local]# wget http://download.redis.io/releases/redis-6.0.1.tar.gz
[root@test1 local]# tar -zxvf redis-6.0.1.tar.gz
[root@test1 local]# cd redis-6.0.1
[root@test1 redis-6.0.1]# make
[root@test1 redis-6.0.1]# make install

(2)查看是否安装成功


[root@test1 redis-6.0.1]# ls -lh /usr/local/bin/
[root@test1 redis-6.0.1]# redis-server -v
Redis server v=3.2.5 sha=00000000:0 malloc=jemalloc-4.0.3 bits=64 build=dae2abf3793b309d

(3)配置redis 创建dump file、进程pid、log目录


[root@test1 redis-6.0.1]# cd /etc/
[root@test1 etc]# mkdir redis
[root@test1 etc]# cd /var/
[root@test1 var]# mkdir redis
[root@test1 var]# cd redis/
[root@test1 redis]# mkdir data log run

(4)修改配置文件


[root@test1 redis]# cd /usr/local/redis-6.0.1/
[root@test1 redis-6.0.1]# cp redis.conf /etc/redis/6379.conf
[root@test1 redis-6.0.1]# vim /etc/redis/6379.conf
#绑定的主机地址
bind 192.168.1.222
#端口
port 6379
#认证密码(方便测试不设密码,注释掉)
#requirepass
#pid目录
pidfile /var/redis/run/redis_6379.pid
#log存储目录
logfile /var/redis/log/redis.log
#dump目录
dir /var/redis/data
#Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize yes

(5)设置启动方式


[root@test1 redis-6.0.1]# cd /usr/local/redis-6.0.1/utils/
[root@test1 utils]# cp redis_init_script /etc/init.d/redis
[root@test1 utils]# vim /etc/init.d/redis #根据自己实际情况修改

/etc/init.d/redis文件的内容如下。


#!/bin/sh
#
# Simple Redis init.d script conceived to work on Linux systems
# as it does use of the /proc filesystem.

REDISPORT=6379
EXEC=/usr/local/bin/redis-server
CLIEXEC=/usr/local/bin/redis-cli

PIDFILE=/var/run/redis_${REDISPORT}.pid
CONF="/etc/redis/${REDISPORT}.conf"

case "$1" in
start)
if [ -f $PIDFILE ]
then
echo "$PIDFILE exists, process is already running or crashed"
else
echo "Starting Redis server..."
$EXEC $CONF
fi
;;
stop)
if [ ! -f $PIDFILE ]
then
echo "$PIDFILE does not exist, process is not running"
else
PID=$(cat $PIDFILE)
echo "Stopping ..."
$CLIEXEC -p $REDISPORT shutdown
while [ -x /proc/${PID} ]
do
echo "Waiting for Redis to shutdown ..."
sleep 1
done
echo "Redis stopped"
fi
;;
*)
echo "Please use start or stop as first argument"
;;
esac

增加执行权限,并启动Redis。


[root@test1 utils]# chmod a+x /etc/init.d/redis   #增加执行权限
[root@test1 utils]# service redis start #启动redis

(6)查看redis是否启动


2024-07-28-008.png


2.3 Lua访问Redis


(1)连接redis,然后添加一些测试参数


[root@test1 utils]# redis-cli -h 192.168.1.222 -p 6379
192.168.1.222:6379> set "123" "456"
OK

(2)编写连接Redis的Lua脚本


[root@test1 utils]# vim /usr/local/openresty/nginx/conf/lua/redis.lua
local redis = require "resty.redis"
local conn = redis.new()
conn.connect(conn, '192.168.1.222', '6379') #根据自己情况写ip和端口号
local res = conn:get("123")
if res==ngx.null then
ngx.say("redis集群中不存在KEY——'123'")
return
end
ngx.say(res)

(3)在nginx.conf配置文件中的server下添加以下location


[root@test1 utils]# vim /usr/local/openresty/nginx/conf/nginx.conf
location /lua_redis {
default_type text/plain;
content_by_lua_file /usr/local/openresty/nginx/conf/lua/redis.lua;
}

随后重新reload配置。


[root@test1 utils]# nginx  -s reload   #重启一下Nginx

(4)验证Lua访问Redis的正确性


在浏览器输入ip/lua_redis, 如果能看到下图的内容表示Lua可以访问Redis。


2024-07-28-009.png


准备工作已经完成,现在要实现OpenResty+Lua+Redis自动封禁并解封IP了。3.4


2.4 OpenResty+Lua实现


(1)添加访问控制的Lua脚本(只需要修改Lua脚本中连接Redis的IP和端口即可)


ok, err = conn:connect(“192.168.1.222”, 6379)

注意:如果在Nginx或者OpenResty的上层有用到阿里云的SLB负载均衡的话,需要修改一下脚本里的所有…ngx.var.remote_addr,把remote_addr替换成从SLB获取真实IP的字段即可,不然获取到的IP全都是阿里云SLB发过来的并且是处理过的IP,同时,这些IP全都是一个网段的,根本没有办法起到封禁的效果)。


完整的Lua脚本如下所示。


[root@test1 lua]# vim /usr/local/openresty/nginx/conf/lua/access.lua
local ip_block_time=300 --封禁IP时间(秒)
local ip_time_out=30 --指定ip访问频率时间段(秒)
local ip_max_count=20 --指定ip访问频率计数最大值(秒)
local BUSINESS = ngx.var.business --nginx的location中定义的业务标识符,也可以不加,不过加了后方便区分

--连接redis
local redis = require "resty.redis"
local conn = redis:new()
ok, err = conn:connect("192.168.1.222", 6379)
conn:set_timeout(2000) --超时时间2秒

--如果连接失败,跳转到脚本结尾
if not ok then
goto FLAG
end

--查询ip是否被禁止访问,如果存在则返回403错误代码
is_block, err = conn:get(BUSINESS.."-"..ngx.var.remote_addr)
if is_block == '1' then
ngx.exit(403)
goto FLAG
end

--查询redis中保存的ip的计数器
ip_count, err = conn:get(BUSINESS.."-"..ngx.var.remote_addr)

if ip_count == ngx.null then --如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out
res, err = conn:set(BUSINESS.."-"..ngx.var.remote_addr, 1)
res, err = conn:expire(BUSINESS.."-"..ngx.var.remote_addr, ip_time_out)
else
ip_count = ip_count + 1 --存在则将单位时间内的访问次数加1

if ip_count >= ip_max_count then --如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time
res, err = conn:set(BUSINESS.."-"..ngx.var.remote_addr, 1)
res, err = conn:expire(BUSINESS.."-"..ngx.var.remote_addr, ip_block_time)
else
res, err = conn:set(BUSINESS.."-"..ngx.var.remote_addr,ip_count)
res, err = conn:expire(BUSINESS.."-"..ngx.var.remote_addr, ip_time_out)
end
end

-- 结束标记
::FLAG::
local ok, err = conn:close()

(2)在需要做访问限制的location里加两段代码即可,这里用刚才的/lua做演示


[root@test1 lua]# vim /usr/local/openresty/nginx/conf/nginx.conf

2024-07-28-010.png


主要是添加如下配置。


access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;

其中,set $business “lua” 是为了把IP放进Redis的时候标明是哪个location的,可以不加这个配置。


随后,重新reload配置。


[root@test1 lua]# nginx -s reload #修改完后重启nginx

(3)打开浏览器访问192.168.1.222/lua 并一直按F5刷新。


2024-07-28-011.png


随后,连接Redis,查看IP的访问计数。


[root@test1 ~]# redis-cli -h 192.168.1.222 -p 6379

发现redis已经在统计访问lua这个网页ip的访问次数了


2024-07-28-012.png


这个key的过期时间是30秒,如果30秒没有重复访问20次这个key就会消失,所以说正常用户一般不会触发这个封禁的脚本。


2024-07-28-013.png


当30秒内访问超过了20次,发现触发脚本了,变成了403


2024-07-28-014.png


再次查看Redis的key,发现多了一个lua-block-192.168.1.158,过期时间是300秒,就是说在300秒内这个ip无法继续访问192.168.1.222/lua这个页面了。


2024-07-28-015.png


过五分钟后再去访问这个页面,又可以访问了。


2024-07-28-016.png


这个脚本的目的很简单:一个IP如果在30秒内其访问次数达到20次则表明该IP访问频率太快了,因此将该IP封禁5分钟。同时由于计数的KEY在Redis中的超时时间设置成了30秒,所以如果两次访问间隔时间大于30秒将会重新开始计数。


大家也可以将这个脚本优化成,第一次封禁5分钟,第二次封禁半小时,第三次封禁半天,第四次封禁三天,第五次永久封禁等等。


好了,今天就到这儿吧,我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7399109720457543721
收起阅读 »

既然有了Kubernetes,为什么还需要 Istio?

如果您听说过Service Mesh并尝试过Istio,您可能会有以下问题: 为什么 Istio 运行在 Kubernetes 上? Kubernetes 和服务网格在云原生应用架构中分别扮演什么角色? Istio 在哪些方面对Kubernetes进行了扩展...
继续阅读 »

image.png


如果您听说过Service Mesh并尝试过Istio,您可能会有以下问题:



  1. 为什么 Istio 运行在 Kubernetes 上?

  2. Kubernetes 和服务网格在云原生应用架构中分别扮演什么角色?

  3. Istio 在哪些方面对Kubernetes进行了扩展?它解决了什么问题?

  4. Kubernetes、Envoy 和 Istio 之间是什么关系?


本文将带您了解 Kubernetes 和 Istio 的内部工作原理。另外,我还会介绍 Kubernetes 中的负载均衡方法,并解释为什么有了 Kubernetes 还需要 Istio。





Kubernetes 本质上是通过声明性配置进行应用程序生命周期管理,而服务网格本质上是提供应用程序间流量、安全管理和可观察性。如果你已经使用 Kubernetes 搭建了一个稳定的应用平台,那么如何为服务之间的调用设置负载均衡和流量控制呢?这就是服务网格发挥作用的地方。


Envoy 引入了 xDS 协议,该协议受到各种开源软件的支持,例如Istio、MOSN等。Envoy 将 xDS 贡献给服务网格或云原生基础设施。Envoy 本质上是一个现代版本的代理,可以通过 API 进行配置,并基于它衍生出许多不同的使用场景——例如 API 网关、服务网格中的 sidecar 代理和边缘代理。


本文包含以下内容:



  • kube-proxy 作用的描述。

  • Kubernetes 对于微服务管理的局限性。

  • 介绍 Istio 服务网格的功能。

  • Kubernetes、Envoy 和 Istio 服务网格中一些概念的比较。


Kubernetes 与服务网格


下图展示了 Kubernetes 和 Service Mesh(每个 pod 一个 sidecar 模型)中的服务访问关系。



流量转发


Kubernetes 集群中的每个节点都会部署一个 kube-proxy 组件,该组件与 Kubernetes API Server 通信,获取集群中服务的信息,然后设置 iptables 规则,将服务请求直接发送到对应的 Endpoint(属于该集群的 pod)。同一组服务)。


服务发现



Istio 可以跟随 Kubernetes 中的服务注册,还可以通过控制平面中的平台适配器与其他服务发现系统对接;然后使用数据平面的透明代理生成数据平面配置(使用 CRD,存储在 etcd 中)。数据平面的透明代理作为sidecar容器部署在每个应用服务的pod中,所有这些代理都需要请求控制平面同步代理配置。代理是“透明的”,因为应用程序容器完全不知道代理的存在。该进程中的 kube-proxy 组件也需要拦截流量,只不过 kube-proxy 拦截进出 Kubernetes 节点的流量,而 sidecar 代理拦截进出 pod 的流量。


服务网格的缺点


由于 Kubernetes 每个节点上运行有很多 pod,将原有的 kube-proxy 路由转发功能放在每个 pod 中会增加响应延迟(由于 sidecar 拦截流量时的跳数更多)并消耗更多资源。为了以细粒度的方式管理流量,将添加一系列新的抽象。这会进一步增加用户的学习成本,但随着技术的普及这种情况会慢慢得到缓解。


服务网格的优点


kube-proxy 设置是全局的,无法对每个服务进行精细控制,而服务网格通过 sidecar 代理将流量控制从 Kubernetes 的服务层中取出,从而实现更大的弹性。


Kube-Proxy 的缺点


首先,如果转发的 Pod 无法正常服务,它不会自动尝试另一个 Pod。每个 pod 都有健康检查机制,当 pod 出现健康问题时,kubelet 会重启 pod,kube-proxy 会删除相应的转发规则。此外,nodePort 类型的服务无法添加 TLS 或更复杂的消息路由机制。


Kube-proxy 实现了 Kubernetes 服务的多个 pod 实例之间的流量负载均衡,但是如何对这些服务之间的流量进行细粒度控制——例如将流量按百分比划分到不同的应用程序版本(这些版本都是同一个应用程序的一部分)服务但在不同的部署上),或者进行灰度发布和蓝绿发布?


Kubernetes 社区提供了一种使用 Deployment 进行灰度发布方法,这本质上是一种通过修改 pod 标签将不同 pod 分配给部署服务的方法


Kubernetes Ingress 与 Istio 网关


如上所述,kube-proxy 只能在 Kubernetes 集群内路由流量。Kubernetes 集群的 Pod 位于 CNI 创建的网络中。入口(在 Kubernetes 中创建的资源对象)是为了集群外部的通信而创建的。它由位于 Kubernetes 边缘节点上的入口控制器驱动,负责管理南北流量。Ingress 必须对接各种 Ingress Controller,例如nginx ingress 控制器。Ingress仅适用于HTTP流量,使用简单。它只能通过匹配有限数量的字段(例如服务、端口、HTTP 路径等)来路由流量。这使得无法路由 MySQL、Redis 和各种 RPC 等 TCP 流量。这就是为什么你会看到人们在入口资源注释中编写 nginx 配置语言。直接路由南北流量的唯一方法是使用服务的 LoadBalancer 或 NodePort,前者需要云供应商支持,后者需要额外的端口管理。


Istio Gateway 的功能与 Kubernetes Ingress 类似,负责进出集群的南北向流量。Istio Gateway 描述了一种负载均衡器,用于承载进出网格边缘的连接。该规范描述了一组开放端口以及这些端口使用的协议、用于负载均衡的 SNI 配置等。 Gateway 是一个 CRD 扩展,它也重用了 sidecar 代理的功能;详细配置请参见Istio 网站


Envoy


Envoy 是 Istio 中默认的 sidecar 代理。Istio 基于 Enovy 的 xDS 协议扩展了其控制平面。在谈论 Envoy 的 xDS 协议之前,我们需要先熟悉一下 Envoy 的基本术语。以下是 Envoy 中的基本术语及其数据结构列表;请参阅Envoy 文档了解更多详细信息。



基本术语


以下是您应该了解的 Enovy 基本术语。



  • Downstream:下游主机连接 Envoy,发送请求,接收响应;即发送请求的主机

  • Upstream:上游主机接收来自 Envoy 的连接和请求并返回响应;即接收请求的主机

  • Listener:Listener 是一个命名的网络地址(例如端口、UNIX 域套接字等);下游客户端可以连接到这些侦听器。Envoy 向下游主机公开一个或多个侦听器以进行连接。

  • Cluster:集群是 Envoy 连接的一组逻辑上相同的上游主机。Envoy 通过服务发现来发现集群的成员。或者,可以通过主动健康检查来确定集群成员的健康状态。Envoy 通过负载均衡策略决定集群中的哪个成员来路由请求。


Envoy 中可以设置多个监听器,每个监听器可以设置一个过滤器链(过滤器链表),并且过滤器是可扩展的,以便我们可以更轻松地操纵流量的行为——例如设置加密、私有 RPC 等。


xDS 协议由 Envoy 提出,是 Istio 中默认的 sidecar 代理,但只要实现了 xDS 协议,理论上就可以在 Istio 中用作 sidecar 代理——比如蚂蚁集团开源的MOSN。



Istio 是一个功能非常丰富的服务网格,包括以下功能。



  • 流量管理:这是Istio最基本的功能。

  • 策略控制:启用访问控制系统、遥测捕获、配额管理、计费等。

  • 可观察性:在 sidecar 代理中实现。

  • 安全身份验证:Citadel 组件执行密钥和证书管理。


Istio 中的流量管理


Istio 中定义了以下 CRD 来帮助用户进行流量管理。



  • 网关:网关描述了运行在网络边缘的负载均衡器,用于接收传入或传出的 HTTP/TCP 连接。

  • VirtualService:VirtualService 实际上将 Kubernetes 服务连接到 Istio 网关。它还可以执行其他操作,例如定义一组在寻址主机时应用的流量路由规则。

  • DestinationRule:DestinationRule 定义的策略决定流量经过路由后的访问策略。简而言之,它定义了流量的路由方式。其中,这些策略可以定义为负载均衡配置、连接池大小和外部检测(用于识别并驱逐负载均衡池中不健康的主机)配置。

  • EnvoyFilter:EnvoyFilter 对象描述代理服务的过滤器,可以自定义 Istio Pilot 生成的代理配置。这种配置一般初级用户很少使用。

  • ServiceEntry:默认情况下,Istio 服务网格中的服务无法发现网格之外的服务。ServiceEntry 允许将其他条目添加到 Istio 内的服务注册表中,从而允许网格中自动发现的服务访问并路由到这些手动添加的服务。


Kubernetes、xDS、Istio


回顾了 Kubernetes 的 kube-proxy 组件、xDS 和 Istio 中流量管理的抽象之后,现在让我们仅在流量管理方面对这三个组件/协议进行比较(请注意,这三个组件并不完全相同)。


要点



  • Kubernetes 的本质是应用程序生命周期管理,特别是部署和管理(伸缩、自动恢复、发布)。

  • Kubernetes 为微服务提供了可扩展且高弹性的部署和管理平台。

  • 服务网格基于透明代理,通过 sidecar 代理拦截服务之间的流量,然后通过控制平面配置管理它们的行为。

  • 服务网格将流量管理与 Kubernetes 解耦,无需 kube-proxy 组件来支持服务网格内的流量;通过提供更接近微服务应用程序层的抽象来管理服务间流量、安全性和可观察性。

  • xDS 是服务网格配置的协议标准之一。

  • 服务网格是 Kubernetes 中服务的更高级别抽象。


概括


如果说 Kubernetes 管理的对象是 Pod,那么 Service Mesh 管理的对象就是服务,所以只要用 Kubernetes 来管理微服务,然后应用 Service Mesh 就可以了。如果您甚至不想管理服务,那么可以使用像Knative这样的无服务器平台z。


作者:轻松Ai享生活
来源:juejin.cn/post/7310878133720301604
收起阅读 »

2024我给公司亏钱了,数据一致性问题真的马虎不得

最近五阳遇到了线上资损问题,我开始重视分布式事务的数据一致性问题,拿我擅长的场景分析下。 举个🌰例子 在付费会员场景,用户购买会员后享受会员权益。在会员售后场景,用户提交售后,系统需要冻结权益并且原路赔付退款。 系统如何保证冻结权益和订单退款的数据一致性呢?当...
继续阅读 »

最近五阳遇到了线上资损问题,我开始重视分布式事务的数据一致性问题,拿我擅长的场景分析下。


举个🌰例子


在付费会员场景,用户购买会员后享受会员权益。在会员售后场景,用户提交售后,系统需要冻结权益并且原路赔付退款。


系统如何保证冻结权益和订单退款的数据一致性呢?当无法保证数据一致时,会导致什么问题呢?


标题业务结果
订单退款,但未冻结权益平台资金损失
订单未退款,但权益冻结用户资金损失
订单退款,权益冻结正常

通过这个例子可以看到电商场景中,数据不一致可能会导致资金损失。这是电商场景对数据一致性要求高的原因,很多资损(资金损失)问题都是源于数据不一致。


如何理解数据一致性,一致性体现在哪里?


狭义上的数据一致是指:数据完全相同,在数据库主从延迟场景,主从数据一致是指:主数据副本和从数据副本,数据完全相同,客户端查询主库和查询从库得到的结果是相同的,也就是一致的。


除数据多副本场景使用数据一致性的概念之外,扩展后其他场景也使用这个概念。例如分布式事务中,多个事务参与者各自维护一种数据,当多种数据均处于合法状态且符合业务逻辑的情况下,那就可以说整体处于数据一致了。(并不像副本场景要求数据完全相同)


例如会员订单有支付状态和退款状态,会员优惠券有未使用状态和冻结状态。 在一次分布式事务执行前后,订单和优惠券的状态是一致的,即会员订单退款、会员券冻结;会员订单未退款,会员券状态为可使用;


此外还有异构数据一致性,超时一致。异构数据一致性是指同一种数据被异构到多种存储中间件。例如本地缓存、Redis缓存和数据库,即三级缓存的数据一致性。还有搜索场景,需要保证数据库数据和 ElasticSearch数据一致性,这也是分布式事务问题。


一致性和原子性的区别


原子性 指的是事务是一个不可分割的最小工作单元,事务中的操作要么全部成功,要么全部失败。


一致性 指的是事务执行前后,所有数据均处于一致性状态,一致性需要原子性的支持。如果没有实现原子性,一致性也无法实现。一致性在原子性的基础上,还要求实现数据的正确性。例如在同一个事务中实现多商品库存扣减,多个SQL除了保证同时成功同时失败外,还需要保证操作的正确性。如果所有SQL都返回成功了,但是数据是错误的,这无法接受。这就是一致性的要求。


由此可见,数据一致性本身就要求了数据是正确的。


隔离性是指:其他事务并发访问同一份数据时,多个事务之间应该保持隔离性。隔离性级别:如读未提交、‌读已提交、‌可重复读和串行化。


隔离性强调的是多个事务之间互不影响的程度,一致性强调的是一个事务前后,数据均处于一致状态。


什么是强一致性


在分布式事务场景,强一致性是指:任何一个时刻,看到各个事务参与者的数据都是一致的。系统不存在不一致的情况。


值得一提的是,CAP理论指出,数据存在多副本情况下,要保证强一致性(在一个绝对时刻,两份数据是完全一致的)需要牺牲可用性。


也就是说系统发现自身处于不一致状态时,将向用户返回失败状态。直至数据一致后,才能返回最新数据,这将牺牲可用性。


为了保证系统是可用的,可以返回旧的数据,但是无法保证强一致性。


会员售后能保证强一致性吗?


会员售后关键的两个动作:权益冻结和订单退款。 两者能保证强一致性吗?答案是不能。假设权益冻结是一个本地事务性操作,但是订单退款包括订单状态流程、支付系统资金流转等等。


如此复杂的流程难以保证任意一个绝对时刻,用户看到权益冻结后,资金一定到账了;这是售后场景 无法达到强一致性的根本原因。


最终一致性和强一致性


最终一致性不要求系统在任意一个时刻,各参与方数据都是一致的,它要求各参与方数据在一定时间后处于一致状态。


最终一致性没有明确这个时间是多长,所以有人说最终一致性就是没有一致性,谁知道多久一定能保证一致呢。


保证最终一致性的手段有哪些?


TCC


TCC 包含 Try、Confirm 和 Cancel 三个操作。



  1. 确保每个参与者(服务)都实现了 Try、Confirm 和 Cancel 操作。

  2. 确保在业务逻辑中,如果 Try 操作成功,后续必须执行 Confirm 操作以完成事务;如果 Try 失败或者 Cancel 被调用,则执行 Cancel 操作撤销之前的操作。


需要说明,如果Confirm执行失败,需要不停不重试Confirm,不得执行Cancel。 按照TCC的语义 Try操作已经锁定了、预占了资源。 Confirm在业务上一定是可以成功的。


TCC的问题在于 分布式事务的任意一个操作都应该提供三个接口,每个参与者都需要提供三个接口,整体交互协议复杂,开发成本高。当发生嵌套的分布式事务时,很难保证所有参与者都实现TCC规范。


为什么TCC 方案包含Try


如果没有Try阶段,只有Confirm和 Cancel阶段,如果Confirm失败了,则调用Cancel回滚。为什么Tcc不这样设计呢?


引入Try阶段,为保证不发生状态回跳的情况。


Try阶段是预占资源阶段,还未实际修改资源。设想资金转账场景, A账户向B账户转账100元。 在Try阶段 A账户记录了预扣100元,B 账户记录了预收100元。 如果A账户不足100元,那么Try阶段失败,调用Cancel回滚,这种情况,在任意时刻,A、B用户视角转账是失败的。


Try阶段成功,则调用Confirm接口,最终A账户扣100元,B收到100元。虽然这无法保证在某个时刻,A、B账户资金绝对一致。但是如果没有Try阶段,那么将发生 状态回跳的情况;


状态回跳:即A账户操作成功了,但是B账户操作失败,A账户资金又被回滚了。那么用户A 看到自己的账户状态就是 钱被扣了,但是过一会钱又回来了。


同理B账户也可能遇到收到钱了,但是过一会钱又没了。在转账场景,这种回跳情况几乎是不能接受的。


引入了Try阶段,就能保证不发生状态回跳的情况。


最大努力通知


最大努力通知是指通知方通过一定的机制最大努力将业务处理结果通知到接收方。一般用于最终一致性时间敏感度低的场景,并且接收方的结果不会影响到发起方的结果。即接收方处理失败时,发送方不会跟随回滚。


在电商场景,很多场景使用最大努力通知型作为数据一致性方案。


会员售后如何保证最终一致性?


回到开头的问题,以下是数据不一致的两种情况。


标题业务结果
订单退款,但未冻结权益平台资金损失
订单未退款,但权益冻结用户资金损失
订单退款,权益冻结正常

平台资损难以追回,当发生容易复现的平台资损时,会引来更多的用户“薅羊毛”,资损问题将进一步放大,所以平台资损一定要避免。


当发生用户资损时,用户可以通过客服向平台追诉,通过人工兜底的方式,可以保证“最终”数据是一致的。


所以基于这个大的原则,大部分系统都是优先冻结会员权益,待用户权益明确冻结后,才根据实际冻结情况,向用户退款。这最大程度上保证了售后系统的资金安全。


会员售后整体上是最大努力通知一致性方案。当权益冻结后,系统通过可重试的消息触发订单退款,且务必保证退款成功(即便是人工介入情况下)。虽说是最大努力通知型,但不代表一致性弱,事实上支付系统的稳定性要求是最高级别的,订单退款成功的可靠性是能得到保证的。


如果权益冻结是分布式事务,如何保证一致性


一开始我们假设权益冻结是一个本地事务,能保证强一致性,这通常与实际不符。权益包含很多玩法,例如优惠券、优惠资格、会员日等等,权益冻结并非是本地事务,而是分布式事务,如何保证一致性呢?


方案1 TCC方案


假设 权益系统包含三个下游系统,身份系统(记录某个时间段是会员)、优惠券系统、优惠资格系统。 TCC方案将要求三方系统均实现 Try、Confirm、Cancel接口。开发成本和上下游交付协议比较复杂。


方案2 无Try 的 TCC方案


假设无Try阶段,直接Confirm修改资源,修改失败则调用Cancel,那么就会出现状态跳回情况,即优惠券被冻结了,但是后面又解冻了。


这种情况下,系统只需要实现扣减资源和回滚资源 两种接口。 系统设计大大简化


方案3 Prepare + Confirm


Prepare: 检查接口,即检查资源是否可以被修改,但是不会锁定资源。


Confirm: 修改资源接口,实际修改资源状态。


如果Prepare失败,则返回执行失败,由于未预占用资源,所以无需回滚资源。在Prepare成功后,则立即调用Confirm。如果Confirm执行失败,则人工介入。


一定需要回滚吗?


在一致性要求高的场景,需要资源回滚能力,保证系统在一定时间后处于一致状态。如果没有回滚,势必导致在某些异常情况下,系统处于不一致状态,且无法自动恢复。


会员售后场景虽然对资金较为敏感,但不需要资源回滚。理由如下



  1. 将订单退款置为 权益成功冻结之后,可以保证系统不出现平台资损。即权益未完全冻结,订单是不是退款的。

  2. 用户资损情况可以通过人工客服兜底解决。


在上述方案3中,先通过Prepare阶段验证了参与方都是可以冻结的,在实际Confirm阶段这个状态很难发生改变。所以大概率Confirm不会失败的。


只有极低的概率发生Confirm失败的情况,即用户在权益冻结的一瞬间,使用优惠券,这将导致资源状态发生改变。


解决此类问题,可以在权益冻结后,评估冻结结果,根据实际的冻结结果,决定如何赔付用户,赔付用户多少钱。所以用户并发用券,也不会影响资金安全。


人工兜底与数据一致性


程序员应该在业务收益、开发成本、数据不一致风险等多个角度评估系统设计的合理性。


会员售后场景,看似数据一致性要求高,仿佛数据不一致,就会产生严重的资损问题。但实际分析后,系统并非需要严格的一致性。


越是复杂的系统设计,系统稳定性越差。越是简洁的系统设计,系统稳定性越高。当选择了复杂的系统设计提高数据一致性,必然需要付出更高的开发成本和维护成本。往往适得其反。


当遇到数据一致性挑战时,不妨跳出技术视角,尝试站在产品视角,思考能否适当调整一下产品逻辑,容忍系统在极端情况下,有短暂时间数据不一致。人工兜底处理极端情况。


大多数情况下,产品经理会同意。


作者:五阳
来源:juejin.cn/post/7397013935105769523
收起阅读 »

Docker容器日志过大?有没有比较简单的方式解决?

Docker容器日志过大?有没有比较简单的方式解决? 1. 问题描述 当我们尝试查看特定 Docker 容器的日志时,通常会使用 docker logs <容器名称> 命令。然而,有时候会发现控制台持续输出日志信息,持续时间可能相当长,直到最终打印...
继续阅读 »

Docker容器日志过大?有没有比较简单的方式解决?


1. 问题描述


当我们尝试查看特定 Docker 容器的日志时,通常会使用 docker logs <容器名称> 命令。然而,有时候会发现控制台持续输出日志信息,持续时间可能相当长,直到最终打印完成。这种现象往往源自对 Docker 容器日志长时间未进行处理,导致日志积累过多,占用了系统磁盘空间。因此,为了释放磁盘空间优化系统性能,我们可以采取一些简单而有效的方法来处理这些庞大的日志文件


2. docker日志处理机制


需要处理问题,那我们肯定要先了解docker的日志处理机制,了解了基本的机制,能够帮助我们更好的理解问题并解决问题。


2.1 日志查看


docker logs <容器名称>可以查看docker容器的输出日志,但是这里的日志主要包含标准输出标准错误输出,一些容器可能会把日志输出到某个日志文件中,比如tomcat,这样使用docker logs <容器名称>命令是无法查看的。


注意docker logs命令查看的是容器的全部日志,当日志量很大时会对容器的运行造成影响,可以通过docker logs --tail N container name查看最新N行的数据,N是一个整数。


2.2 处理机制


当我们启动一个docker容器时,实际上时作为docker daemon的一个子进程运行的,docker daemon可以拿到容器里进程的标准输出与标准错误输出,并通过docker的log driver模块来处理,大致图示如下:


image-20240307144725996


上面图中所列举的就是所支持的Log Driver:



  • none:容器没有日志,docker logs不输出任何内容

  • local:日志以自定义格式存储

  • json-file:日志以json格式存储,默认的Log Driver

  • syslog:将日志写入syslog。syslog守护程序必须在主机上运行

  • journald:将日志写入journald。journald守护程序必须在主机上运行

  • gelf:将日志写入Graylog Extended Log Format端点,如Graylog或Logstash

  • fluentd:将日志写入fluentd。fluentd守护程序必须在主机上运行

  • awslogs:将日志写入Amazon CloudWatch Logs

  • splunk:通过HTTP Event Collector将日志写入splunk

  • etwlogs:将日志作为ETW(Event Tracing for Windows)事件写入。只在Windows平台可用

  • gcplogs:将日志写入Google Cloud Platform Logging

  • logentries:将日志写入Rapid7 Logentries


可以使用命令docker info | grep "Logging Driver"


image-20240307145038976


2.3 默认的json-file


json-file Log Driver是Docker默认启用的Driver,将容器的STDOUT/STDERR输出以json的格式写到宿主机的磁盘,日志文件路径为 /var/lib/docker/containers/{container_id}/{container_id}-json.log


格式是这样的:


image-20240307153128792


json-file将每一行日志封装到一个json字符串中。


json-file支持如下配置:



  • max-size:单个日志文件的最大大小,单位可以为k、m、g,默认是-1,表示日志文件可以无限大。

  • max-file:最多可以存多少个日志文件,默认数量是1,当默认数量大于1时,每个日志文件达到最大存储大小,且数量达到设置数量,产生新日志时会删除掉最旧的一个日志文件。

  • labels:指定日志所使用到的标签,使用逗号分割。比如traceId,message两个标签。

  • env:指定与日志相关的环境变量,使用逗号分割

  • env-rejex:一个正则表达式来匹配与日志相关的环境变量

  • compress:是否压缩日志文件


3. 如何解决?


3.1 查看日志大小


我们可以通过如下脚本获取当前所有容器的日志大小,这里时使用docker默认的json-file的形式:


#!/bin/sh 
echo "======== docker containers logs file size ========"

logs=$(find /var/lib/docker/containers/ -name *-json.log)

for log in $logs
do
ls -lh $log
done

执行脚本:


image-20240307155549512


json-file的命令开头的一小串字符时容器的id。


例如我有一个docker容器id是2de6f164ee11,我们可以适当修改shell脚本,查看某一个容器的日志大小。


logs=$(find /var/lib/docker/containers/ -name *-json.log | grep "2de6f164ee11")

3.2 删除日志


如果docker容器正在运行,使用rm -rf的方式删除日志后,磁盘空间并没有释放。原因是在Linux或者Unix系统中,通过rm -rf或者文件管理器删除文件,将会从文件系统的目录结构上解除链接(unlink)。如果文件是被打开的(有一个进程正在使用),那么进程将仍然可以读取该文件,磁盘空间也一直被占用。


正确的方式是直接使用命令改写日志文件。


cat /dev/null > *-json.log


  • cat: 是一个命令,用于连接文件并打印它们的内容到标准输出(通常是终端)。

  • /dev/null: 是一个特殊的设备文件,向它写入的内容会被丢弃,读取它将会立即返回结束符。

  • >: 是重定向操作符,将命令的输出重定向到文件。

  • *-json.log: 是通配符,用于匹配当前目录中所有以 -json.log 结尾的文件。


可以使用如下脚本,直接处理所有的日志文件:


#!/bin/sh 
echo "======== start clean docker containers logs ========"

logs=$(find /var/lib/docker/containers/ -name *-json.log)

for log in $logs
do
echo "clean logs : $log"
cat /dev/null > $log
done

echo "======== end clean docker containers logs ========"

注意,虽然使用这种方式可以删除日志,释放磁盘,但是过一段时间后,日志又会涨回来,所以要从根本上解决问题,只需要添加两个参数。没错!就是上面所讲到的max-size和max-file。


3.3 治本操作


在运行docker容器时,添加上max-size和max-file可以解决日志一直增长的问题。


docker run -it --log-opt max-size=10m --log-opt max-file=3 alpine ash

这段启动命令表示总共有三个日志文件,每个文件的最大大小时10m,这样就能将该容器的日志大小控制在最大30m。


4. 总结


在运行容器时,我们就应该优先考虑如何处理日志的问题,后面不必为容器运行后所产生的巨大日志而手足无措。


当然需要删除无用日志可以通过3.1,3.2的操作完成,建议在运行容器的时候加上max-size和max-file参数或者至少加上max-size参数。





作者:MuShanYu
来源:juejin.cn/post/7343178660069179432
收起阅读 »

好烦啊,1个SQL干崩核心系统长达12小时!

前言1个SQL干崩核心系统长达12小时!分享一下这次的故障排查过程1.故障现象大周末的接到项目组的电话,反馈应用从凌晨4点开始持续卡顿,起初并未关注,到下午2点左右,核心系统是彻底干绷了,远程接入后发现,数据库后台有大量的异常等待事件enq:TX -...
继续阅读 »

前言

1个SQL干崩核心系统长达12小时!分享一下这次的故障排查过程

1.故障现象

大周末的接到项目组的电话,反馈应用从凌晨4点开始持续卡顿,起初并未关注,到下午2点左右,核心系统是彻底干绷了,远程接入后发现,数据库后台有大量的异常等待事件

enq:TX -index contention
cursor: pin S wait on X
direct path read

通过监控发现服务器IO和CPU使用率已经高达90%
整个数据库算是夯住了!
根据经验判断应该是性能的问题

2.排查过程

2.1 AWR分析

对于这种性能的问题,首先采集到AWR报告并结合ASH报告分析一下

Direct path read事件尽然排到了第一位!占DB time高达63%,这个等待事件是让一些不常使用的大表数据(冷数据),在全表扫描时,每次都从磁盘读到用户的私有内存(PGA),而不要去挤占有限的、宝贵的、频繁使用的数据(热数据)所在的共享内存(SGA-buffer cache)。

2.2 定位异常SQL

对该TOP SQL分析发现,sql执行频繁,怀疑是执行计划发生变化,如果不把导致问题的根本原因找到,那么很有可能下次还会再发生!

2.3 分析执行计划

通过定位SQL Id,我们去看内存中的执行计划,明显看到了执行计划发生了变化,全表扫占用大量的IO,这里查看执行计划的方法很多。

--该方法是从共享池得到
如果SQL已被age out出share pool,则查找不到
select * from table
(dbms_xplan.display_cursor('&sql_id',null,'typical'));

--该方法是通过awr中得到
select * from table(dbms_xplan.display_awr('&sql_id'));

此时再追踪历史的执行计划发现,从凌晨故障发生开始,执行计划就发生了变化,SQL执行耗费到CPU的平均时间高达上百秒,历史执行计划再次验证了我的判断!

2.4 故障定位

跟业务确认得知,在凌晨业务人员发现,存储空间不够,删除了分区的来释放空间,此处相当于对表结构做了修改,执行计划发生了变化,再加上故障SQL的对应分区,统计信息一直未收集导致这次执行计划发生改变!

3.处理过程

1.定位到SQL的内存地址,从内存中刷出执行计划
select address,hash_value,
executions,parse_calls 
from  v$sqlarea where
sql_id='4ca86dg34xg62';

--刷出内存
exec sys.dbms_shared_pool.purge('C000000A4C502F40,4103674309','C');  

2.收集分区统计信息

BEGIN
    -- 为整个表加上统计信息(包括所有分区)
    DBMS_STATS.GATHER_TABLE_STATS(
        ownname     => 'YOUR_SCHEMA'-- 替换为你的模式名
        tabname     => 'YOUR_PARTITIONED_TABLE'-- 替换为你的分区表名
        cascade     => TRUE-- 收集所有分区的统计信息
        estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE, -- 自动估算采样百分比
        method_opt  => 'FOR ALL COLUMNS SIZE AUTO'-- 为所有列自动决定采样大小
        degree      => DBMS_STATS.DEFAULT_DEGREE -- 使用默认并行度
    );
END;
/

此时我们再次查看执行计划,正确了!

4.技能拓扑

分区索引的失效,会引起执行计划的改变

1.TRUNCATEDROP 操作可以导致该分区表的全局索引失效,
而分区索引依然有效,如果操作的分区没有数据,
那么不会影响索引的状态。
需要注意的是,
对分区表的 ADD 操作对分区索引和全局索引没有影响。

2.如果执行 SPLIT 的目标分区含有数据,
那么在执行 SPLIT 操作后,全局索引和分区索引都会
被被置为 UNUSABLE
如果执行 SPLIT 的目标分区没有数据,
那么不会影响索引的状态。

3.对分区表执行 MOVE 操作后,
全局索引和分区索引都会被置于无效状态。

4.对于分区表而言,除了 ADD 操作之外,
TRUNCATEDROPEXCHANGE 和 SPLIT 
操作均会导致全局索引失效,
但是可以加上 UPDATE GLOBAL INDEXES 子句让全局索引不失效。

在 12C 之前的版本,对分区表进行删除分区或者 TRUNCATE 分区,合并或者分裂分区MOVE 分区等 DDL 操作时,分区表上的全局索引会失效,通常要加上 UPDATE GLOBAIINDEXES 或者 ONLINE 关键字,可是加上这些关键字之后,本来很快的 DDL 操作可能就要花费很长的时间,而且还要面临锁的问题。“

Oracle 12C推出了分区表全局索引异步维护特性这个特性有效的解决了这个问题,在对分区表进行上述 DDL 操作时,既能快速完成操作,也能保证全局索引有效,然后通过调度JOB 在固定的时候对全局索引进行维护。“

5.总结

警惕Oracle数据库性能“隐形杀手”——Direct Path Read, 如果不把导致问题的根本原因找到,那么很有可能下次还会再发生!


作者:IT邦德
来源:juejin.cn/post/7387610960159473676
收起阅读 »

一个高并发项目到落地的心酸路

前言 最近闲来没事,一直在掘金上摸鱼,看了不少高并发相关的文章,突然有感而发,想到了几年前做的一个项目,也多少和高并发有点关系。 这里我一边回忆落地细节一边和大家分享下,兴许能给大家带来点灵感。 正文 需求及背景 先来介绍下需求,首先项目是一个志愿填报系统。 ...
继续阅读 »

前言


最近闲来没事,一直在掘金上摸鱼,看了不少高并发相关的文章,突然有感而发,想到了几年前做的一个项目,也多少和高并发有点关系。

这里我一边回忆落地细节一边和大家分享下,兴许能给大家带来点灵感。


正文


需求及背景


先来介绍下需求,首先项目是一个志愿填报系统。

核心功能是两块,一是给考试填报志愿,二是给老师维护考生数据。

本来这个项目不是我们负责,奈何去年公司负责这个项目的组遭到了甲方严重的投诉,说很多考生用起来卡顿,甚至把没填上志愿的责任归到系统上。

甲方明确要求,如果这年再出现这种情况,公司在该省的所有项目将面临被替换的风险。

讨论来讨论去,最后公司将任务落到我们头上时,已经是几个月后的事了,到临危受命阶段,剩下不到半年时间。

虽然直属领导让我们不要有心理负担,做好了表扬,做不好锅也不是我们的,但明显感觉到得到他的压力,毕竟一个不小心就能上新闻。


分析


既然开始做了,再说那些有的没的就没用了,直接开始分析需求。

首先,业务逻辑并不算复杂,难点是在并发和数据准确性上。与客户沟通后,大致了解了并发要求后,于是梳理了下。



  1. 考生端登录接口、考生志愿信息查询接口需要4W QPS

  2. 考生保存志愿接口,需要2W TPS

  3. 报考信息查询4W QPS

  4. 老师端需要4k QPS

  5. 导入等接口没限制,可以异步处理,只要保证将全部信息更新一遍在20分钟以内即可,同时故障恢复的时间必须在20分钟以内(硬性要求)

  6. 考生端数据要求绝对精准,不能出现遗漏、错误等和考生操作不一致的数据

  7. 数据脱敏,防伪

  8. 资源是有限的,提供几台物理机

    大的要求就这么多,主要是在有限资源下需要达到如此高的并发确实需要思考思考,一般的crud根本达不到要求。


方案研讨


接下来我会从当时我们切入问题的点开始,从前期设计到项目落地整个过程的问题及思考,一步步去展示这个项目如何实现的

首先,我们没有去设计表,没有去设计接口,而是先去测试。测试什么?测试我们需要用到或可能用到的中间件是否满足需求


MySQL


首先是MySQL,单节点MySQL测试它的读和取性能,新建一张user表。

向里面并发插入数据和查询数据,得到的TPS大概在5k,QPS大概在1.2W。

查询的时候是带id查询,索引列的查询不及id查询,差距大概在1k。

insert和update存在细微并发差距,但基本可以忽略,影响更新性能目前最大的问题是索引。

如果表中带索引,将降低1k-1.5k的TPS。

目前结论是,mysql不能达到要求,能不能考虑其他架构,比如mysql主从复制,写和读分开。

测试后,还是放弃,主从复制结构会影响更新,大概下降几百,而且单写的TPS也不能达到要求。

至此结论是,mysql直接上的方案肯定是不可行的


Redis


既然MySQL直接查询和写入不满足要求,自然而然想到加入redis缓存。于是开始测试缓存,也从单节点redis开始测试。

get指令QPS达到了惊人的10w,set指令TPS也有8W,意料之中也惊喜了下,仿佛看到了曙光。

但是,redis容易丢失数据,需要考虑高可用方案


实现方案


既然redis满足要求,那么数据全从redis取,持久化仍然交给mysql,写库的时候先发消息,再异步写入数据库。

最后大体就是redis + rocketMQ + mysql的方案。看上去似乎挺简单,当时我们也这样以为 ,但是实际情况却是,我们过于天真了。

这里主要以最重要也是要求最高的保存志愿信息接口开始攻略


故障恢复

第一个想到的是,这些个节点挂了怎么办?

mysql挂了比较简单,他自己的机制就决定了他即使挂掉,重启后仍能恢复数据,这个可以不考虑。

rocketMQ一般情况下挂掉了可能会丢失数据,经过测试发现,在高并发下,确实存在丢消息的现象。

原因是它为了更加高效,默认采用的是异步落盘的模式,这里为了保证消息的绝对不丢失,修改成同步落盘模式。

然后是最关键的redis,不管哪种模式,redis在高并发下挂掉,都会存在丢失数据的风险。

数据丢失对于这个项目格外致命,优先级甚至高于并发的要求。

于是,问题难点来到了如何保证redis数据正确,讨论过后,决定开启redis事务。

保存接口的流程就变成了以下步骤:

1.redis 开启事务,更新redis数据

2.rocketMQ同步落盘

3.redis 提交事务

4.mysql异步入库

我们来看下这个接口可能存在的问题。

第一步,如果redis开始事务或更新redis数据失败,页面报错,对于数据正确性没有影响

第二步,如果rocketMQ落盘报错,那么就会有两种情况。

情况一,落盘失败,消息发送失败,好像没什么影响,直接报错就可。

情况二,如果发送消息成功,但提示发送失败(无论什么原因),这时候将导致mysql和redis数据的最终不一致。

如何处理?怎么知道是redis的有问题还是mysql的有问题?出现这种情况时,如果考生不继续操作,那么这条错误的数据必定无法被更新正确。

考虑到这个问题,我们决定引入一个时间戳字段,同时启动一个定时任务,比较mysql和redis不一致的情况,并自主修复数据。

首先,redis中记录时间戳,同时在消息中也带上这个时间戳并在入库时记录到表中。

然后,定时任务30分钟执行一次,比较redis中的时间戳是否小于mysql,如果小于,便更新redis中数据。如果大于,则不做处理。

同时,这里再做一层优化,凌晨的时候执行一个定时任务,比较redis中时间戳大于mysql中的时间戳,连续两天这条数据都存在且没有更新操作,将提示给我们手动运维。

然后是第三步,消息提交成功但是redis事务提交失败,和第二步处理结果一致,将被第二个定时任务处理。

这样看下来,即使redis崩掉,也不会丢失数据。


第一轮压测


接口实现后,当时怀着期待,信息满满的去做了压测,结果也是当头棒喝。

首先,数据准确性确实没有问题,不管突然kill掉哪个环节,都能保证数据最终一致性。

但是,TPS却只有4k不到的样子,难道是节点少了?

于是多加了几个节点,但是仍然没有什么起色。问题还是想简单了。


重新分析


经过这次压测,之后一个关键的问题被提了出来,影响接口TPS的到底是什么???

一番讨论过后,第一个结论是:一个接口的响应时间,取决于它最慢的响应时间累加,我们需要知道,这个接口到底慢在哪一步或哪几步?

于是用arthas看了看到底慢在哪里?

结果却是,最慢的竟然是redis修改数据这一步!这和测试的时候完全不一样。于是针对这一步,我们又继续深入探讨。

结论是:

redis本身是一个很优秀的中间件,并发也确实可以,选型时的测试没有问题。

问题出在IO上,我们是将考生的信息用json字符串存储到redis中的(为什么不保存成其他数据结构,因为我们提前测试过几种可用的数据结构,发现redis保存json字符串这种性能是最高的),

而考生数据虽然单条大小不算大,但是在高并发下的上行带宽却是被打满的。

于是针对这种情况,我们在保存到redis前,用gzip压缩字符串后保存到redis中。

为什么使用gzip压缩方式,因为我们的志愿信息是一个数组,很多重复的数据其实都是字段名称,gzip和其他几个压缩算法比较后,综合考虑到压缩率和性能,在当时选择了这种压缩算法。

针对超过限制的字符串,我们同时会将其拆封成多个(实际没有超过三个的)key存储。


继续压测


又一轮压测下来,效果很不错,TPS从4k来到了8k。不错不错,但是远远不够啊,目标2W,还没到它的一半。

节点不够?加了几个节点,有效果,但不多,最终过不了1W。

继续深入分析,它慢在哪?最后发现卡在了rocketMQ同步落盘上。

同步落盘效率太低?于是压测一波发现,确实如此。

因为同步落盘无论怎么走,都会卡在rocketMQ写磁盘的地方,而且因为前面已经对字符串压缩,也没有带宽问题。

问题到这突然停滞,不知道怎么处理rocketMQ这个点。

同时,另一个同事在测试查询接口时也带来了噩耗,查询接口在1W2左右的地方就上不去了,原因还是卡在带宽上,即使压缩了字符串,带宽仍被打满。

怎么办?考虑许久,最后决定采用较常规的处理方式,那就是数据分区,既然单个rocketMQ服务性能不达标,那么就水平扩展,多增加几个rocketMQ。

不同考生访问的MQ不一样,同时redis也可以数据分区,幸运的是正好redis有哈希槽的架构支持这种方式。

而剩下的问题就是如何解决考生分区的方式,开始考虑的是根据id进行求余的分区,但后来发现这种分区方式数据分布及其不均匀。

后来稍作改变,根据正件号后几位取余分区,数据分布才较为均匀。有了大体解决思路,一顿操作后继续开始压测。


一点小意外


压测之后,结果再次不如人意,TPS和QPS双双不增反降,继续通过arthas排查。

最后发现,redis哈希槽访问时会在主节点先计算key的槽位,而后再将请求转到对应的节点上访问,这个计算过程竟然让性能下降了20%-30%。

于是重新修改代码,在java内存中先计算出哈希槽位,再直接访问对应槽位的redis。如此重新压测,QPS达到了惊人的2W,TPS也有1W2左右。

不错不错,但是也只到了2W,在想上去,又有了瓶颈。

不过这次有了不少经验,马上便发现了问题所在,问题来到了nginx,仍然是一样的问题,带宽!

既然知道原因,解决起来也比较方便,我们将唯一有大带宽的物理机上放上两个节点nginx,通过vip代理出去,访问时会根据考生分区信息访问不同的地址。


压测


已经记不清第几轮压测了,不过这次的结果还算满意,主要查询接口QPS已经来到了惊人的4W,甚至个别接口来到6W甚至更高。

胜利已经在眼前,唯一的问题是,TPS上去不了,最高1W4就跑不动了。

什么原因呢?查了每台redis主要性能指标,发现并没有达到redis的性能瓶颈(上行带宽在65%,cpu使用率也只有50%左右)。

MQ呢?MQ也是一样的情况,那出问题的大概率就是java服务了。分析一波后发现,cpu基本跑到了100%,原来每个节点的最大链接数基本占满,但带宽竟然还有剩余。

静下心来继续深入探讨,连接数为什么会满了?原因是当时使用的SpringBoot的内置容器tomcat,无论如何配置,最大连接数最大同时也就支持1k多点。

那么很简单的公式就能出来,如果一次请求的响应时间在100ms,那么1000 * 1000 / 100 = 10000。

也就是说单节点最大支持的并发也就1W,而现在我们保存的接口响应时间却有300ms,那么最大并发也就是3k多,目前4个分区,看来1W4这个TPS也好像找到了出处了。

接下来就是优化接口响应时间的环节,基本是一步一步走,把能优化的都优化了一遍,最后总算把响应时间控制在了100ms以内。

那么照理来说,现在的TPS应该会来到惊人的4W才对。


再再次压测


怀着忐忑又激动的心情,再一次进入压测环节,于是,TPS竟然来到了惊人的2W5。

当时真心激动了一把,但是冷静之后却也奇怪,按正常逻辑,这里的TPS应该能达到3W6才对。

为了找到哪里还有未发现的坑(怕上线后来惊喜),我们又进一步做了分析,最后在日志上找到了些许端倪。

个别请求在链接redis时报了链接超时,存在0.01%的接口响应时间高于平均值。

于是我们将目光投向了redis连接数上,继续一轮监控,最终在业务实现上找到了答案。

一次保存志愿的接口需要执行5次redis操作,分别是获取锁、获取考生信息、获取志愿信息、修改志愿信息、删除锁,同时还有redis的事务。

而与之相比,查询接口只处理了两次操作,所以对于一次保存志愿的操作来看,单节点的redis最多支持6k多的并发。

为了验证这个观点,我们尝试将redis事务和加锁操作去掉,做对照组压测,发现并发确实如预期的一样有所提升(其实还担心一点,就是抢锁超时)。


准备收工


至此,好像项目的高并发需求都已完成,其他的就是完善完善细节即可。

于是又又又一次迎来了压测,这一次不负众望,重要的两个接口均达到了预期。

这之后便开始真正进入业务实现环节,待整个功能完成,在历时一个半月带两周的加班后,终于迎来了提测。


提测后的问题


功能提测后,第一个问题又又又出现在了redis,当高并发下突然kill掉redis其中一个节点。

因为用的是哈希槽的方式,如果挂掉一个节点,在恢复时重新算槽将非常麻烦且效率很低,如果不恢复,那么将严重影响并发。

于是经过讨论之后,决定将redis也进行手动分区,分区逻辑与MQ的一致。

但是如此做,对管理端就带来了一定影响,因为管理端是列表查询,所以管理端获取数据需要从多个节点的redis中同时获取。

于是管理端单独写了一套获取数据分区的调度逻辑。

第二个问题是管理端接口的性能问题,虽然管理端的要求没考生端高,但扛不住他是分页啊,一次查10个,而且还需要拼接各种数据。

不过有了前面的经验,很快就知道问题出在了哪里,关键还是redis的连接数上,为了降低链接数,这里采用了pipeline拼接多个指令。


上线


一切准备就绪后,就准备开始上线。说一下应用布置情况,8+4+1+2个节点的java服务,其中8个节点考生端,4个管理端,1个定时任务,2个消费者服务。

3个ng,4个考生端,1个管理端。

4个RocketMQ。

4个redis。

2个mysql服务,一主一从,一个定时任务服务。

1个ES服务。

最后顺利上线,虽然发生了个别线上问题,但总体有惊无险,

而真是反馈的并发数也远没有到达我们的系统极限,开始准备的水平扩展方案也没有用上,无数次预演过各个节点的宕机和增加分区,一般在10分钟内恢复系统,不过好在没有排上用场。


最后


整个项目做下来感觉越来越偏离面试中的高并发模式,说实在的也是无赖之举,

偏离的主要原因我认为是项目对数据准确性的要求更高,同时需要完成高并发的要求。

但是经过这个项目的洗礼,在其中也收获颇丰,懂得了去监控服务性能指标,然后也加深了中间件和各种技术的理解。

做完之后虽然累,但也很开心,毕竟在有限的资源下去分析性能瓶颈并完成项目要求后,还是挺有成就感的。

再说点题外话,虽然项目成功挽回了公司在该省的形象,也受到了总公司和领导表扬,但最后也就这样了,

实质性的东西一点没有,这也是我离开这家公司的主要原由。不过事后回想,这段经历确实让人难忘,也给我后来的工作带来了很大的帮助。

从以前的crud,变得能去解决接口性能问题。这之前一遇上,可能两眼茫然或是碰运气,现在慢慢的会根据蛛丝马迹去探究优化方案。

不知道我在这个项目的经历是否能引起大家共鸣?希望这篇文章能对你有所帮助。


作者:青鸟218
来源:juejin.cn/post/7346021356679675967
收起阅读 »

什么是系统的鲁棒性?

嗨,你好啊,我是猿java 现实中,系统面临的异常情况和不确定性因素是不可避免的。例如,网络系统可能会遭受网络攻击、服务器宕机等问题;金融系统可能会受到市场波动、黑天鹅事件等因素的影响;自动驾驶系统可能会遇到天气恶劣、道路状况复杂等情况。 在这些情况下,系统的...
继续阅读 »

嗨,你好啊,我是猿java


现实中,系统面临的异常情况和不确定性因素是不可避免的。例如,网络系统可能会遭受网络攻击、服务器宕机等问题;金融系统可能会受到市场波动、黑天鹅事件等因素的影响;自动驾驶系统可能会遇到天气恶劣、道路状况复杂等情况。


在这些情况下,系统的鲁棒性就显得尤为重要,它能够确保系统能够正确地处理各种异常情况,保持正常运行。因此,这篇文章我们将分析什么是系统的鲁棒性?如何保证系统的鲁棒性?


什么是系统的鲁棒性?


鲁棒性,英文为 Robustness,它是一个多学科的概念,涉及控制理论、计算机科学、工程学等领域。


在计算机领域,系统的鲁棒性是指系统在面对各种异常情况和不确定性因素时,仍能保持稳定运行和正常功能的能力。


鲁棒性是系统稳定性和可靠性的重要指标,一个具有良好鲁棒性的系统能够在遇到各种异常情况时做出正确的响应,不会因为某些异常情况而导致系统崩溃或失效。


鲁棒性要求系统在在遇到各种异常情况都能正常工作,各种异常很难具像化,这看起来是一种比较理想的情况,那么系统的鲁棒性该如何评估呢?


系统鲁棒性的评估


系统的鲁棒性可以从多个方面来考虑和评估,这里主要从三个方面进行评估:


首先,系统的设计和实现应该考虑到各种可能的异常情况,并采取相应的措施来应对


例如,在网络系统中,可以采用防火墙、入侵检测系统等技术来保护系统免受网络攻击;在金融系统中,可以采用风险管理技术来降低市场波动对系统的影响;在自动驾驶系统中,可以采用传感器融合、路径规划等技术来应对复杂的道路状况。


其次,系统在面临异常情况时应该具有自我修复和自我调整的能力


例如,当网络系统遭受攻击时,系统应该能够及时发现并隔离攻击源,同时自动恢复受影响的服务;当金融系统受到市场波动影响时,系统应该能够自动调整投资组合,降低风险;当自动驾驶系统面临复杂道路状况时,系统应该能够根据实时的道路情况调整行驶策略。


此外,系统的鲁棒性还包括对数据异常和不确定性的处理能力


在现实生活中,数据往往会存在各种异常情况,例如数据缺失、噪声数据等。系统应该能够对这些异常数据进行有效处理,保证系统的正常运行。同时,系统也应该能够对数据的不确定性进行有效处理,例如通过概率模型、蒙特卡洛方法等技术来处理数据不确定性,提高系统的鲁棒性。


鲁棒性的架构策略


对于系统的鲁棒性,有没有一些可以落地的策略?


如下图,展示了一些鲁棒性的常用策略,核心思想是:事前-事中-事后


image.png


预防故障(事前)


对于技术人员来说,要有防范未然的意识,因此,对于系统故障要有预防措施,主要的策略包括:



  • 代码质量:绝大部分软件系统是脱离不了代码,因此代码质量是预防故障很核心的一个前提。

  • 脱离服务:脱离服务(Removal from service)这种策略指的是将系统元素临时置于脱机状态,以减轻潜在的系统故障。

  • 替代:替代(Substitution)这种策略使用更安全的保护机制-通常是基于硬件的-用于被视为关键的软件设计特性。

  • 事务:事务(Transactions)针对高可用性服务的系统利用事务语义来确保分布式元素之间交换的异步消息是原子的、一致的、隔离的和持久的。这四个属性被称为“ACID属性”。

  • 预测模型:预测模型(Predictive model.)结合监控使用,用于监视系统进程的健康状态,以确保系统在其标称操作参数内运行,并在检测到预测未来故障的条件时采取纠正措施。

  • 异常预防:异常预防(Exception prevention)这种策略指的是用于防止系统异常发生的技术。

  • 中止:如果确定某个操作是不安全的,它将在造成损害之前被中止(Abort)。这种策略是确保系统安全失败的常见策略。

  • 屏蔽:系统可以通过比较几个冗余的上游组件的结果,并在这些上游组件输出的一个或多个值不同时采用投票程序,来屏蔽(Masking)故障。

  • 复盘:复盘是对事故的整体分析,发现问题的根本原因,查缺补漏,找到完善的方案。


检测故障(事中)


当故障发生时,在采取任何关于故障的行动之前,必须检测或预测故障的存在,故障检测策略主要包括:



  • 监控:监控(Monitor)是用于监视系统的各个其他部分的健康状态的组件:处理器、进程、输入/输出、内存等等。

  • **Ping/echo:**Ping/echo是指在节点之间交换的异步请求/响应消息对,用于确定通过相关网络路径的可达性和往返延迟。

  • 心跳:心跳(Heartbeat)是一种故障检测机制,它在系统监视器和被监视进程之间进行周期性的消息交换。

  • 时间戳:时间戳(Timestamp)这种策略用于检测事件序列的不正确性,主要用于分布式消息传递系统。

  • 条件监测:条件检测(Condition monitoring.)这种策略涉及检查进程或设备中的条件或验证设计过程中所做的假设。

  • 合理性检查:合理性检查(Sanity checking)这种策略检查特定操作或计算结果的有效性或合理性。

  • 投票:投票(Voting)这种策略的最常见实现被称为三模块冗余(或TMR),它使用三个执行相同操作的组件,每个组件接收相同的输入并将其输出转发给投票逻辑,用于检测三个输出状态之间的任何不一致。

  • 异常检测:异常检测(Exception detection)这种策略用于检测改变执行正常流程的系统状态。

  • 自检测:自检测(Self-test)要求元素(通常是整个子系统)可以运行程序来测试自身的正确运行。自检测程序可以由元素自身启动,或者由系统监视器不时调用。


故障恢复(事后)


故障恢复是指系统出现故障之后如何恢复工作。这是对团队应急能力的一个极大考验,如何在系统故障之后,将故障时间缩小到最短,将事故损失缩减到最小?这往往决定了一个平台,一个公司的声誉,决定了很多技术人员的去留。故障恢复的策略主要包括:



  • 冗余备用:冗余备用(Redundant spare)有三种主要表现形式:主动冗余(热备用)、被动冗余(温备用)和备用(冷备用)。

  • 回滚:回滚(Rollback)允许系统在检测到故障时回到先前已知良好状态,称为“回滚线”——回滚时间。

  • 异常处理:异常处理(Exception handling)要求在检测到异常之后,系统必须以某种方式处理它。

  • 软件升级:软件升级(Software upgrade)的目标是在不影响服务的情况下实现可执行代码映像的在线升级。

  • 重试:重试(Retry)策略假定导致故障的故障是暂时的,重试操作可能会取得成功。

  • 忽略故障行为:当系统确定那些消息是虚假的时,忽略故障行为(Ignore faulty behavior)要求忽略来自特定来源的消息。

  • 优雅降级:优雅降级(Graceful degradation)这种策略在元素故障的情况下保持最关键的系统功能,放弃较不重要的功能。

  • 重新配置:使用重新配置(Reconfiguration),系统尝试通过将责任重新分配给仍在运行的资源来从系统元素的故障中恢复,同时尽可能保持关键功能。


上述这些策略看起来很高大上,好像离你很远,但是其实很多公司都有对应的措施,比如:系统监控,系统告警,数据备份,分布式,服务器集群,多活,降级策略,熔断机制,复盘等等,这些术语应该就和我们的日常开发息息相关了。


总结


系统的鲁棒性是指系统在面对各种异常情况和不确定性因素时,仍能保持稳定运行和正常功能的能力。系统鲁棒性看似一个理想的状态,却是业界一直追求的终极目标,比如,系统稳定性如何做到 5个9(99.999%),甚至是 6个9(99.9999%),这就要求技术人员时刻保持工匠精神、在自己的本职工作上多走一步,只有在各个相关岗位的共同协作下,才能确保系统的鲁棒性。


学习交流


如果你觉得文章有帮助,请帮忙点个赞呗,关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7393312386571370536
收起阅读 »

身份认证的尽头竟然是无密码 ?

概述 几乎所有的系统都会面临安全认证相关的问题,但是安全相关的问题是一个很麻烦的事情。因为它不产生直接的业务价值,而且处理起来复杂繁琐,所以很多时都容易被忽视。很多后期造成重大的安全隐患,往往都是前期的不重视造成的。但庆幸的是安全问题是普遍存在的,而且大家面临...
继续阅读 »

概述


几乎所有的系统都会面临安全认证相关的问题,但是安全相关的问题是一个很麻烦的事情。因为它不产生直接的业务价值,而且处理起来复杂繁琐,所以很多时都容易被忽视。很多后期造成重大的安全隐患,往往都是前期的不重视造成的。但庆幸的是安全问题是普遍存在的,而且大家面临的问题几乎相同,所以可以制定行业标准来规范处理,甚至是可以抽出专门的基础设施(例如:AD、LDAP 等)来专门解决这类共性的问题。总之,关于安全问题非常复杂而且麻烦,对于大多数 99% 的系统来说,不要想着在安全问题领域上搞发明和创新,容易踩坑。而且行业的标准解决方案已经非常成熟了。经过长时间的检验。所以在安全领域,踏踏实实的遵循规范和标准就是最好的安全设计。


HTTP 认证


HTTP 认证协议的最初是在 HTTP/1.1标准中定义的,后续由 IETF 在 RFC 7235 中进行完善。HTTP 协议的主要涉及两种的认证机制。


HTTP 认证的对话框


基本认证


常见的叫法是 HTTP Basic,是一种对于安全性不高,以演示为目的的简单的认证机制(例如你家路由器的登录界面),客户端用户名和密码进行 Base64 编码(注意是编码,不是加密)后,放入 HTTP 请求的头中。服务器在接收到请求后,解码这个字段来验证用户的身份。示例:


GET /some-protected-resource HTTP/1.1
Host: example.com
Authorization: Basic dXNlcjpwYXNzd29yZA==

虽然这种方式简单,但并不安全,因为 base64 编码很容易被解码。建议仅在 HTTPS 协议下使用,以确保安全性。


摘要认证


主要是为了解决 HTTP Basic 的安全问题,但是相对也更复杂一些,摘要认证使用 MD5 哈希函数对用户的密码进行加密,并结合一些盐值(可选)生成一个摘要值,然后将这个值放入请求头中。即使在传输过程中被截获,攻击者也无法直接从摘要中还原出用户的密码。示例:


GET /dir/index.html HTTP/1.1
Host: example.com
Authorization: Digest username="user", realm="example.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"

**补充:**另在 RFC 7235 规范中还定义当用户没有认证访问服务资源时应返回 401 Unauthorized 状态码,示例:


HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area"

这一规范目前应用在所有的身份认证流程中,并且沿用至今。


Web 认证


表单认证


虽然 HTTP 有标准的认证协议,但目前实际场景中大多应用都还是基于表单认证实现,具体步骤是:



  1. 前端通过表单收集用户的账号和密码

  2. 通过协商的方式发送服务端进行验证的方式。


常见的表单认证页面通常如下:


html>
<html>
<head>
    <title>Login Pagetitle>
head>
<body>
    <h2>Login Formh2>
    <form action="/perform_login" method="post">
        <div class="container">
            <label for="username"><b>Usernameb>label>
            <input type="text" placeholder="Enter Username" name="username" required>
            
            <label for="password"><b>Passwordb>label>
            <input type="password" placeholder="Enter Password" name="password" required>
            
            <button type="submit">Loginbutton>
        div>
    form>
body>
html>

为什么表单认证会成为主流 ?主要有以下几点原因:



  • 界面美化:开发者可以创建定制化的登录界面,可以与应用的整体设计风格保持一致。而 HTTP 认证通常会弹出一个很丑的模态对话框让用户输入凭证。

  • 灵活性:可以在表单里面自定义更多的逻辑和流程,比如多因素认证、密码重置、记住我功能等。这些功能对于提高应用的安全性和便利性非常重要。

  • 安全性:表单认证可以更容易地结合现代的安全实践,背后也有 OAuth 2 、Spring Security 等框架的主持。


表单认证传输内容和格式基本都是自定义本没啥规范可言。但是在 2019 年之后 web 认证开始发布标准的认证协议。


WebAuthn


WebAuthn 是一种彻底抛弃传统密码的认证,完全基于生物识别技术和实体密钥作为身份识别的凭证(有兴趣的小伙伴可以在 github 开启 Webauhtn 的 2FA 认证体验一下)。在 2019 年 3 月,W3C 正式发布了 WebAuthn 的第一版规范。


webauthn registration


相比于传统的密码,WebAuthn 具有以下优势:



  1. 减少密码泄露:传统的用户名和密码登录容易受到钓鱼攻击和数据泄露的影响。WebAuthn,不依赖于密码,不存在密码丢失风险。

  2. 提高用户体验:用户不需要记住复杂的密码,通过使用生物识别等方式可以更快捷、更方便地登录。

  3. 多因素认证:WebAuthn 可以作为多因素认证过程中的一部分,进一步增强安全性。使用生物识别加上硬件密钥的方式进行认证,比短信验证码更安全。


总的来说,WebAuthn 是未来的身份认证方式,通过提供一个更安全、更方便的认证方式,目的是替代传统的基于密码的登录方法,从而解决了网络安全中的一些长期问题。WebAuthn 目前已经得到流程的浏览器厂商(Chrome、Firefox、Edge、Safari)、操作系统(WIndows、macOS、Linux)的广泛支持。


实现效果


当你的应用接入 WebAuthn 后,用户便可以通过生物识别设备进行认证,效果如下:


WebAuthn login


实现原理


WebAuthn 实现较为复杂,这里不做详细描述,具体可参看权威的官方文档,大概交互过程可以参考以下时序图:


webauthn 交互时序图


登录流程大致可以分为以下步骤:



  1. 用户访问登录页面,填入用户名后即可点击登录按钮。

  2. 服务器返回随机字符串 Challenge、用户 UserID。

  3. 浏览器将 Challenge 和 UserID 转发给验证器。

  4. 验证器提示用户进行认证操作。

  5. 服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。


WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能;



备注:你可以通过访问 webauthn.me 了解到更多消息的信息



文章不适合加入过多的演示代码,想要手上体验的可以参考 okta 官方给出基于 Java 17 和 Maven 构建的 webauthn 示例程序,如下:



作者:肖卫卫讲编程
来源:juejin.cn/post/7354632375446061083
收起阅读 »

如何优雅的给SpringBoot部署的jar包瘦身?

一、需求背景 我们知道Spring Boot项目,是可以通过java -jar 包名 启动的。 那为什么Spring Boot项目可以通过上述命令启动,而其它普通的项目却不可以呢? 原因在于我们在通过以下命令打包时 mvn clean package 一般的...
继续阅读 »

一、需求背景


我们知道Spring Boot项目,是可以通过java -jar 包名 启动的。


那为什么Spring Boot项目可以通过上述命令启动,而其它普通的项目却不可以呢?


原因在于我们在通过以下命令打包时


mvn clean package

一般的maven项目的打包命令,不会把依赖的jar包也打包进去的,所以这样打出的包一般都很小



但Spring Boot项目的pom.xml文件中一般都会带有spring-boot-maven-plugin插件。


该插件的作用就是会将依赖的jar包全部打包进去。该文件包含了所有的依赖和资源文件。


也就会导致打出来的包比较大。



打完包就可以通过java -jar 包名 启动,确实是方便了。


但当一个系统上线运行后,肯定会有需求迭代和Bug修复,那也就免不了进行重新打包部署。


我们可以想象一种场景,线上有一个紧急致命Bug,你也很快定位到了问题,就改一行代码的事情,当提交代码并完成构建打包并交付给运维。


因为打包的jar很大,一直处于上传中.......


如果你是老板肯定会发火,就改了一行代码却上传几百MB的文件,难道没有办法优化一下吗?


如今迭代发布是常有的事情,每次都上传一个如此庞大的文件,会浪费很多时间。


下面就以一个小项目为例,来演示如何瘦身。


二、瘦身原理


这里有一个最基础 SpringBoot 项目,整个项目代码就一个SpringBoot启动类,单是打包出来的jar就有20多M;


我们通过解压命令,看下jar的组成部分。


tar -zxvf spring-boot-maven-slim-1.0.0.jar


我们可以看出,解压出来的包有三个模块


分为 BOOT-INF,META-INF,org 三个部分


打开 BOOT-INF



classes: 当前项目编译好的代码是放在 classes 里面的,classes 部分是非常小的。


lib: 我们所依赖的 jar 包都是放在 lib 文件夹下,lib部分会很大。


看了这个结构我们该如何去瘦身呢?


项目虽然依赖会很多,但是当版本迭代稳定之后,依赖基本就不会再变动了。


如果可以把这些不变的依赖提前都放到服务器上,打包的时候忽略这些依赖,那么打出来的Jar包就会小很多,直接提升发版效率。


当然这样做你肯定有疑问?


既然打包的时候忽略这些依赖,那通过java -jar 包名 还可以启动吗?


这种方式打的包,在项目启动时,需要通过-Dloader.path指定lib的路径,就可以正常启动


java -Dloader.path=./lib -jar xxx.jar

三、瘦身实例演示


1、依赖拆分配置


只需要在项目pom.xml文件中添加下面的配置:


 <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar,
必须项目中的某些模块,会经常变动,那么就应该将其坐标写进来
如果没有则nothing ,表示不打包依赖 -->

<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
</plugin>

<!--拷贝依赖到jar外面的lib目录-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--指定的依赖路径-->
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

再次打包


mvn clean package


发现target目录中多了个lib文件夹,里面保存了所有的依赖jar。



自己业务相关的jar也只有小小的168kb,相比之前20.2M,足足小了100多倍;


这种方式打的包,在项目启动时,需要通过-Dloader.path指定lib的路径:


java -Dloader.path=./lib -jar spring-boot-maven-slim-1.0.0.jar


虽然这样打包,三方依赖的大小并没有任何的改变,但有个很大的不同就是我们自己的业务包和依赖包分开了;


在不改变依赖的情况下,也就只需要第一次上传lib目录到服务器,后续业务的调整、bug修复,在没调整依赖的情况下,就只需要上传更新小小的业务包即可;


2、自己其它项目的依赖如何处理?


我们在做项目开发时,除了会引用第三方依赖,也会依赖自己公司的其它模块。


比如



这种依赖自己其它项目的工程,也是会经常变动的,所以不宜打到外部的lib,不然就会需要经常上传更新。


那怎么做了?


其实也很简单 只需在上面的插件把你需要打进jar的填写进去就可以了


<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar,如果没有则nothing -->
<includes>
<include>
<groupId>com.jincou</groupId>
<artifactId>xiaoxiao-util</artifactId>
</include>
</includes>
</configuration>
</plugin>

这样只有include中所有添加依赖依然会打进当前业务包中。


四、总结


使用瘦身部署,你的业务包确实小了 方便每次的迭代更新,不用每次都上传一个很大的 jar 包,从而节省部署时间。


但这种方式也有一个弊端就是增加了Jar包的管理成本,多人协调开发,构建的时候,还需要专门去关注是否有人更新依赖。


作者:二进制狂人
来源:juejin.cn/post/7260772691501301817
收起阅读 »

不使用代理,我是怎么访问Github的

背景 最近更换了 windows系统的电脑, git clone 项目的时候会连接超时的错误,不管我怎么把环境变量放到终端里尝试走代理都无果,于是开始了排查 以下命令是基于 git bash 终端使用的 检测问题 通过 ssh -T git@github....
继续阅读 »

背景


最近更换了 windows系统的电脑, git clone 项目的时候会连接超时的错误,不管我怎么把环境变量放到终端里尝试走代理都无果,于是开始了排查



以下命令是基于 git bash 终端使用的



检测问题


通过 ssh -T git@github.com 命令查看,会报如下错误:


ssh: connect to host github.com port 22: : Connection timed out


思索了一下,难道是端口的问题吗, 于是从 overflow 上找到回答:


修改 ~/.ssh/config 路径下的内容,增加如下


Host github.com
Hostname ssh.github.com
Port 443

这段配置实际上是让 github.com 走 443 端口去执行,评论上有些说 22端口被占用,某些路由器或者其他程序会占用它,想了一下有道理,于是使用 vim ~/.ssh/config 编辑加上,结果...


ssh: connect to host github.com port 443: : Connection timed out


正当我苦苦思索,为什么 ping github.com 超时的时候,脑子里突然回忆起那道久违的八股文面试题: “url输入网址到浏览器上会发生什么",突然顿悟:是不是DNS解析出了问题,找不到服务器地址?


网上学到一行命令,可以在终端里看DNS服务器的域名解析


nslookup baidu.com

先执行一下 baidu.com 的,得到如下:


Server:		119.6.6.6
Address: 119.6.6.6#53

Non-authoritative answer:
Name: baidu.com
Address: 110.242.68.66
Name: baidu.com
Address: 39.156.66.10

再执行一下 nslookup github.com ,果然发现不对劲了:


Name:	github.com
Address: 127.0.0.1

返回了 127.0.0.1,这不对啊,笔者可是读过书的,这是本地的 IP 地址啊,原来是这一步出了问题..


解决问题


大部分同学应该都改过本地的 DNS 域名映射文件,这也是上面那道八股文题中回答的知识点之一,我们打开资源管理器输入一下路径改一下:


C:\Windows\System32\drivers\etc\hosts



MacOs的同学可以在终端使用 sudo vi /etc/hosts 命令修改



在下面加上下面这一行, 其中 140.82.113.4 是 github 的服务器地址,添加后就可以走本地的域名映射了


140.82.113.4 github.com

保存之后,就可以不使用代理,快乐访问 github.com 了,笔者顺利的完成了梦想第一步: git clone


结语


我是饮东,欢迎点赞关注,我们江湖再会


作者:饮东
来源:juejin.cn/post/7328112739335372810
收起阅读 »

简单聊聊使用lombok 的争议

大家好,我是G探险者。 项目里,因为我使用了Lombok插件,然后代码走查的时候被领导点名了。 我心想,这么好用的插件,为啥不推广呢,整天写那些烦人的setter,getter方法就不嫌烦么? 领导既然不让用,自然有他的道理。 于是我查了一番关于lomb...
继续阅读 »

大家好,我是G探险者。


项目里,因为我使用了Lombok插件,然后代码走查的时候被领导点名了。


image.png


我心想,这么好用的插件,为啥不推广呢,整天写那些烦人的setter,getter方法就不嫌烦么?


image.png


领导既然不让用,自然有他的道理。


image.png
于是我查了一番关于lombok的一些绯闻。就有了这篇文章。


首先呢,Lombok 是一个在 Java 项目中广泛使用的库,旨在通过注解自动生成代码,如 getter 和 setter 方法,以减少重复代码并提高开发效率。然而,Lombok 的使用也带来了一些挑战和争议,特别是关于代码的可读性和与 Java Bean 规范的兼容性。


Lombok 基本使用


示例代码


不使用 Lombok:


public class User {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

// 其他 getter 和 setter
}

使用 Lombok:


import lombok.Data;

@Data
public class User {
private String name;
private int age;
// 无需显式编写 getter 和 setter
}

Lombok 的争议



  1. 代码可读性和透明度:Lombok 自动生成的代码在源代码中不直接可见,可能对新开发者造成困扰。

  2. 工具和 IDE 支持:需要特定的插件或配置,可能引起兼容性问题。

  3. 与 Java Bean 规范的兼容性:Lombok 在处理属性命名时可能与 Java Bean 规范产生冲突,特别是在属性名以大写字母开头的情况。


下面我就列举一个例子进行说明。


属性命名的例子


假设有这么一个属性,aName;


标准 Java Bean 规范下:



  • 属性 aName 的setter getter 方法应为 setaName() getaName()

  • 但是 Lombok 可能生成 getAName()


这是因为Lombok 在生成getter和setter方法时,将属性名的首字母也大写,即使它是小写的。所以对于aName属性,Lombok生成的方法可能是getAName()和setAName()。


在处理JSON到Java对象的映射时,JSON解析库(如Jackson或Gson)会尝试根据Java Bean规范匹配JSON键和Java对象的属性。它通常期望属性名的首字母在getter和setter方法中是小写的。因此,如果JSON键为"aName",解析库会寻找setaName()方法。


所以,当你使用Lombok的@Data注解,且Lombok生成的setter方法为setAName()时,JSON解析库可能找不到匹配的方法来设置aName属性,因为它寻找的是setaName()。


这种差异可能在 JSON 到 Java 对象的映射中引起问题。


Java Bean 命名规范


根据 Java Bean 规范,属性名应遵循驼峰式命名法:



  • 单个单词的属性名应全部小写。

  • 多个单词组成的属性名每个单词的首字母通常大写。


结论


Lombok 是一个有用的工具,可以提高编码效率并减少冗余代码。但是,在使用它时,团队需要考虑其对代码可读性、维护性和与 Java Bean 规范的兼容性。在决定是否使用 Lombok 时,项目的具体需求和团队的偏好应该是主要的考虑因素。


作者:G探险者
来源:juejin.cn/post/7310786611805863963
收起阅读 »

token是用来鉴权的,session是用来干什么的?

使用JWT进行用户认证和授权,而Session在一定程度上起到了辅助作用。 让我们详细讨论JWT和Session在这种结合模式中的各自作用以及为什么需要Session。 JWT的作用 用户认证:JWT包含了用户的身份信息和权限信息,客户端每次请求时将JWT发...
继续阅读 »

使用JWT进行用户认证和授权,而Session在一定程度上起到了辅助作用。
让我们详细讨论JWT和Session在这种结合模式中的各自作用以及为什么需要Session。


JWT的作用



  1. 用户认证:JWT包含了用户的身份信息和权限信息,客户端每次请求时将JWT发送给服务器,服务器通过验证JWT来确认用户身份。

  2. 无状态性:JWT不需要在服务器端存储用户会话信息,因此服务器可以是无状态的,便于扩展和负载均衡。


Session的作用



  1. 附加的安全层:即使JWT是无状态的,但在某些应用场景中,仅依赖JWT可能存在一些安全问题,例如Token的泄露或滥用。Session可以作为一个额外的安全层,确保Token即使有效,也必须在服务器的Session管理器中存在对应的会话。

  2. 管理Token的生命周期:通过Session,可以更方便地管理Token的生命周期,例如强制用户重新登录、手动注销Token等操作。

  3. 控制“记住我”功能:如果用户选择了“记住我”选项,Session可以记录这个状态,并在JWT过期后,通过Session来决定是否允许继续使用旧的Token。


为什么需要创建Session


尽管JWT可以在无状态环境中使用,但Session的引入带来了以下好处:



  1. 防止Token滥用:通过在服务器端验证Session,可以确保即使Token有效,也必须是经过服务器端认证的,从而防止Token被恶意使用。

  2. 支持用户主动注销:当用户选择注销时,可以直接删除服务器端的Session记录,确保Token即使没有过期,也无法再被使用。

  3. 提供更精细的控制:通过Session,可以实现更精细的权限控制和用户状态管理,例如强制下线、会话过期时间控制等。

  4. 状态追踪:在某些场景下,追踪用户状态是必要的,例如监控用户的活跃度、登录历史等,这些信息可以通过Session进行管理。


结合JWT和Session的优势


结合使用JWT和Session,可以同时利用两者的优点,实现安全性和扩展性的平衡:



  1. 无状态认证:JWT可以实现无状态认证,便于系统的水平扩展和负载均衡。

  2. 状态管理和安全性:Session可以提供额外的状态管理和安全性,确保Token的使用更加安全可靠。


代码示例


以下是一个简化的代码示例,展示了如何在用户登录时创建JWT和Session:


java
Copy code
public LoginResponse login(String username, String password) throws AuthException {
// 验证用户名和密码
User user = userService.authenticate(username, password);
if (user == null) {
throw new AuthException("Invalid username or password");
}

// 生成JWT Token
String token = createJwt(user.getId(), user.getRoles());

// 创建会话
sessionManagerApi.createSession(token, user);

// 返回Token
return new LoginResponse(token);
}

public void createSession(String token, User user) {
LoginUser loginUser = new LoginUser();
loginUser.setToken(token);
loginUser.setUserId(user.getId());
loginUser.setRoles(user.getRoles());

sessionManagerApi.saveSession(token, loginUser);
}

在请求验证时,首先验证JWT的有效性,然后检查Session中是否存在对应的会话:


java
Copy code
@Override
public DefaultJwtPayload validateToken(String token) throws AuthException {
try {
// 1. 先校验jwt token本身是否有问题
JwtContext.me().validateTokenWithException(token);

// 2. 获取jwt的payload
DefaultJwtPayload defaultPayload = JwtContext.me().getDefaultPayload(token);

// 3. 如果是7天免登陆,则不校验session过期
if (defaultPayload.getRememberMe()) {
return defaultPayload;
}

// 4. 判断session里是否有这个token
LoginUser session = sessionManagerApi.getSession(token);
if (session == null) {
throw new AuthException(AUTH_EXPIRED_ERROR);
}

return defaultPayload;
} catch (JwtException jwtException) {
if (JwtExceptionEnum.JWT_EXPIRED_ERROR.getErrorCode().equals(jwtException.getErrorCode())) {
throw new AuthException(AUTH_EXPIRED_ERROR);
} else {
throw new AuthException(TOKEN_PARSE_ERROR);
}
} catch (io.jsonwebtoken.JwtException jwtSelfException) {
throw new AuthException(TOKEN_PARSE_ERROR);
}
}

总结


在这个场景中,JWT用于无状态的用户认证,提供便捷和扩展性;Session作为辅助,提供额外的安全性和状态管理。通过这种结合,可以充分利用两者的优点,确保系统既具备高扩展性,又能提供细致的安全控制。


作者:云原生melo荣
来源:juejin.cn/post/7383017171180568630
收起阅读 »

记一种不错的缓存设计思路

之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。 场景 假设有个以下格式的接口: GET /api?keys={key1,key2,key3,...}&types={1,2,3,...} 其中 keys 是业务...
继续阅读 »

之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。


场景


假设有个以下格式的接口:


GET /api?keys={key1,key2,key3,...}&types={1,2,3,...}


其中 keys 是业务主键列表,types 是想要取到的信息的类型。


请求该接口需要返回业务主键列表对应的业务对象列表,对象里需要包含指定类型的信息。


业务主键可能的取值较多,千万量级,type 取值范围为 1-10,可以任意组合,每种 type 对应到数据库是 1-N 张表,示意:


redis-cache-design.drawio.png


现在设想这个接口遇到了性能瓶颈,打算添加 Redis 缓存来改善响应速度,应该如何设计?


设计思路


方案一:


最简单粗暴的方法是直接使用请求的所有参数作为缓存 key,请求的返回内容为 value。


方案二:


如果稍做一下思考,可能就会想到文首我提到的觉得不错的思路了:



  1. 使用 业务主键:表名 作为缓存 key,表名里对应的该业务主键的记录作为 value;

  2. 查询时,先根据查询参数 keys,以及 types 对应的表,得到所有 key1:tb_1_1key1:tb_1_2 这样的组合,使用 Redis 的 mget 命令,批量取到所有缓存中存在的信息,剩下没有命中的,批量到数据库里查询到结果,并放入缓存;

  3. 在某个表的数据有更新时,只需刷新 涉及业务主键:该表名 的缓存,或令其失效即可。


小结


在以上两种方案之间做评估和选择,考虑几个方面:



  • 缓存命中率;

  • 缓存数量、占用空间大小;

  • 刷新缓存是否方便;


稍作思考和计算,就会发现此场景下方案二的优势。


另外,就是需要根据实际业务场景,如业务对象复杂度、读写次数比等,来评估合适的缓存数据的粒度和层次,是对应到某一级组合后的业务对象(缓存值对应存储 + 部分逻辑),还是最基本的数据库表/字段(存储的归存储,逻辑的归逻辑)。


作者:mzlogin
来源:juejin.cn/post/7271597656118394899
收起阅读 »

这可能是开源界最好用的行为验证码工具

💂 个人网站: IT知识小屋 🤟 版权: 本文由【IT学习日记】原创、需要转载请联系博主 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 写在前面 大家好,这里是IT学习日记。今日推荐项目:tianai-captcha行为验证码工具...
继续阅读 »




  • 💂 个人网站: IT知识小屋

  • 🤟 版权: 本文由【IT学习日记】原创、需要转载请联系博主

  • 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦





写在前面


大家好,这里是IT学习日记。今日推荐项目:tianai-captcha行为验证码工具


1000+优质开源项目推荐进度:6/1000。如需更多类型优质项目推荐,请在文章后留言。


工具简介


tianai-captcha行为验证码工具:分为 Go 和 Java 两个版本。支持多种验证方式,包括随机验证、曲线匹配、滑块验证、增强版滑块验证、旋转验证、滑动还原、角度验证、刮刮乐、文字点选、图标点选及语序点选等。


行为验证码工具


该系统能够快速集成到个人项目或系统中,显著提高开发效率。


功能展示



  • 随机型验证码


随机型验证码



  • 曲线匹配验证码


曲线匹配验证码



  • 滑动验证增强版验证码


滑动验证增强版验证码



  • 滑块验证码


滑块验证码



  • 旋转验证码


image



  • 滑动还原验证码


滑动还原验证码



  • 角度验验证码


角度验验证码



  • 刮刮乐验验证码


刮刮乐验验证码



  • 文字点选验证码


文字点选验证码



  • 图标验证码


图标验证码


架构设计


tianai-captcha 验证码整体分为 生成器(ImageCaptchaGenerator)、校验器(ImageCaptchaValidator)、资源管理器(ImageCaptchaResourceManager) 其中生成器、校验器、资源管理器等都是基于接口模式实现可插拔的,可以替换为自定义实现,灵活度高



  • 生成器 (ImageCaptchaGenerator)

    主要负责生成行为验证码所需的图片。

  • 校验器 (ImageCaptchaValidator)

    主要负责校验用户滑动的行为轨迹是否合规。

  • 资源管理器 (ImageCaptchaResourceManager)

    主要负责读取验证码背景图片和模板图片等。



    • 资源存储 (ResourceStore)

      负责存储背景图和模板图。

    • 资源提供者 (ResourceProvider)

      负责将资源存储器中对应的资源转换为文件流。一般资源存储器中存储的是图片的 URL 地址或 ID,资源提供者则负责将 URL 或其他 ID 转换为真正的图片文件。



  • 图片转换器 (ImageTransform)

    主要负责将图片文件流转换成字符串类型,可以是 Base64 格式、URL 或其他加密格式,默认实现为 Base64 格式。


工具集成


引入依赖


<!-- maven 导入 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.4.1</version>
</dependency>



  • 使用 ImageCaptchaGenerator生成器生成验证码


public class Test {
public static void main(String[] args) throws InterruptedException {
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
ImageTransform imageTransform = new Base64ImageTransform();
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(true);
/*
生成滑块验证码图片, 可选项
SLIDER (滑块验证码)
ROTATE (旋转验证码)
CONCAT (滑动还原验证码)
WORD_IMAGE_CLICK (文字点选验证码)

更多验证码支持 详见 cloud.tianai.captcha.common.constant.CaptchaTypeConstant
*/
ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
System.out.println(imageCaptchaInfo);

// 负责计算一些数据存到缓存中,用于校验使用
// ImageCaptchaValidator负责校验用户滑动滑块是否正确和生成滑块的一些校验数据; 比如滑块到凹槽的百分比值
ImageCaptchaValidator imageCaptchaValidator = new BasicCaptchaTrackValidator();
// 这个map数据应该存到缓存中,校验的时候需要用到该数据
Map<String, Object> map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo);
}
}



  • 使用ImageCaptchaValidator校验器 验证


public class Test2 {
public static void main(String[] args) {
BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator();

ImageCaptchaTrack imageCaptchaTrack = null;
Map<String, Object> map = null;
Float percentage = null;
// 用户传来的行为轨迹和进行校验
// - imageCaptchaTrack为前端传来的滑动轨迹数据
// - map 为生成验证码时缓存的map数据
boolean check = sliderCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess();
// // 如果只想校验用户是否滑到指定凹槽即可,也可以使用
// // - 参数1 用户传来的百分比数据
// // - 参数2 生成滑块是真实的百分比数据
check = sliderCaptchaValidator.checkPercentage(0.2f, percentage);
}
}


工具获取


工具下载gitee.com/dromara/tia…


在线体验captcha.tianai.cloud/


如果这篇文章对您有帮助,请**“彦祖们”**一定帮我点个 “关注”“点赞”,这对我非常重要。我将会继续推荐更多优质项目和新闻。




作者:IT学习日记v
来源:juejin.cn/post/7391351326153965568
收起阅读 »

java就能写爬虫还要python干嘛?

爬虫学得好,牢饭吃得饱!!!切记!!! 相信大家多少都会接触过爬虫相关的需求吧,爬虫在绝大多数场景下,能够帮助客户自动的完成部分工作,极大的减少人工操作。目前更多的实现方案可能都是以python为实现基础,但是作为java程序员,咱们需要知道的是,以java...
继续阅读 »

爬虫学得好,牢饭吃得饱!!!切记!!!



相信大家多少都会接触过爬虫相关的需求吧,爬虫在绝大多数场景下,能够帮助客户自动的完成部分工作,极大的减少人工操作。目前更多的实现方案可能都是以python为实现基础,但是作为java程序员,咱们需要知道的是,以java 的方式,仍然可以很方便、快捷的实现爬虫。下面将会给大家介绍两种以java为基础的爬虫方案,同时提供案例供大家参考。



一、两种方案


传统的java实现爬虫方案,都是通过jsoup的方式,本文将采用一款封装好的框架【webmagic】进行实现。同时针对一些特殊的爬虫需求,将会采用【selenium-java】的进行实现,下面针对两种实现方案进行简单介绍和演示配置方式。


1.1 webmagic


官方文档:webmagic.io/


1.1.1 简介


使用webmagic开发爬虫,能够非常快速的实现简单且逻辑清晰的爬虫程序。


四大组件



  • Downloader:下载页面

  • PageProcessor:解析页面

  • Scheduler:负责管理待抓取的URL,以及一些去重的工作。通常不需要自己定制。

  • Pipeline:获取页面解析结果,数持久化。


Spider



  • 启动爬虫,整合四大组件


1.1.2 整合springboot


webmagic分为核心包和扩展包两个部分,所以我们需要引入如下两个依赖:



<properties>
<webmagic.version>0.7.5</webmagic.version>
</properties>

<!--WebMagic-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>${webmagic.version}</version>
</dependency>

到此为止,我们就成功的将webmagic引入进来了,具体使用,将在后面的案例中详细介绍。


1.2 selenium-java


官网地址:http://www.selenium.dev/


1.2.1 简介


selenium是一款浏览器自动化工具,它能够模拟用户操作浏览器的交互。但前提是,我们需要在使用他的机器(windows/linux等)安装上它需要的配置。相比于webmigc的安装,它要繁琐的多了,但使用它的原因,就是为了解决一些webmagic做不到的事情。


支持多种语言:java、python、ruby、javascript等。其使用代码非常简单,以java为例如下:


package dev.selenium.hello;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class HelloSelenium {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();

driver.get("https://selenium.dev");

driver.quit();
}
}

1.2.2 安装


无论是在windows还是linux上使用selenium,都需要两个必要的组件:



  • 浏览器(chrome)

  • 浏览器驱动 (chromeDriver)


需要注意的是,要确保上述两者的版本保持一致。


下载地址


chromeDriver:chromedriver.storage.googleapis.com/index.html


windows


windows的安装相对简单一些,将chromeDriver.exe下载至电脑,chrome浏览器直接官网下载相应安装包即可。严格保证两者版本一致,否则会报错。


在后面的演示程序当中,只需要通过代码指定chromeDriver的路径即可。


linux


linux安装才是我们真正的使用场景,java程序通常是要部署在linux环境的。所以我们需要linux的环境下安装chrome和chromeDriver才能实现想要的功能。


首先要做的是判断我们的linux环境属于哪种系统,是ubuntucentos还是其他的种类,相应的shell脚本都是不同的。


我们采用云原生的环境,所有的服务均以容器的方式部署,所以要在每一个服务端容器内部安装chrome和chromeDiver。我们使用的是Alpine Linux,一个轻量级linux发行版,非常适合用来做Docker镜像。


我们可以通过apk --help去查看相应的命令,我直接给出安装命令:


# Install Chrome for Selenium
RUN apk add gconf
RUN apk add chromium
RUN apk add chromium-chromedriver

上面的内容,可以放在DockerFile文件中,在部署的时候,会直接将相应组件安装在容器当中。


需要注意的是,在Alpine Linux中自带的浏览器是chromiumchromium-chromedriver,且版本相应较低,但是足够我们的需求所使用了。


/ # apk search chromium
chromium-68.0.3440.75-r0
chromium-chromedriver-68.0.3440.75-r0

1.2.3 整合springboot


我们只需要在爬虫模块引入依赖就好了:


<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>

二、三个案例


下面通过三个简单的案例,给大家实际展示使用效果。


2.1 爬取省份街道


使用webmagic进行省份到街道的数据爬取。注意,本文只提供思路,不提供具体爬取网站信息,请同学们自己根据使用选择。


接下来搭建webmagic的架子,其中有几个关键点:



  • 创建页面解析类,实现PageProcessor。


import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;

/**
* 页面解析
*
* @author wjbgn
* @date 2023/8/15 17:25
**/

public class TestPageProcessor implements PageProcessor {
@Override
public void process(Page page) {

}

@Override
public Site getSite() {
return site;
}

/**
* 初始化Site配置
*/

private Site site = Site.me()
// 重试次数
.setRetryTimes(3)
//编码
.setCharset(StandardCharsets.UTF_8.name())
// 超时时间
.setTimeOut(10000)
// 休眠时间
.setSleepTime(1000);
}


  • 实现PageProcessor后,要重写其方法process(Page page),此方法是我们实现爬取的核心(页面解析)。通常省市区代码分为6级,所以常见的网站均是按照层级区分,我们是从省份开始爬取,即从第三层开始爬取。



    • 初始化变量


    @Override
    public void process(Page page) {
    // 市级别
    Integer type = 3;
    // 初始化结果明细
    RegionCodeDTO regionCodeDTO = new RegionCodeDTO();
    // 带有父子关系的结果集合
    List<Map<String, Object>> list = new ArrayList();
    // 页面所有元素集合
    List<String> all = new ArrayList<>();
    // 页面中子页面的链接地址
    List<String> urlList = new ArrayList<>();
    }


    • 根据不同级别,获取相应页面不同的元素


    if (CollectionUtil.isEmpty(all)) {
    // 爬取所有的市,编号,名称
    all = page.getHtml().css("table.citytable").css("tr").css("a", "text").all();
    // 爬取所有的城市下级地址
    urlList = page.getHtml().css("table.citytable").css("tr").css("a", "href").all()
    .stream().distinct().collect(Collectors.toList());
    if (CollectionUtil.isEmpty(all)) {
    // 区县级别
    type = 4;
    all = page.getHtml().css("table.countytable").css("tr.countytr").css("td", "text").all();
    // 获取区
    all.addAll(page.getHtml().css("table.countytable").css("tr.countytr").css("a", "text").all());

    urlList = page.getHtml().css("table.countytable").css("tr").css("a", "href").all()
    .stream().distinct().collect(Collectors.toList());
    if (CollectionUtil.isEmpty(all)) {
    // 街道级别
    type = 5;
    all = page.getHtml().css("table.towntable").css("tr").css("a", "text").all();
    urlList = page.getHtml().css("table.towntable").css("tr").css("a", "href").all()
    .stream().distinct().collect(Collectors.toList());
    if (CollectionUtil.isEmpty(all)) {
    // 村,委员会
    type = 6;
    List<String> village = new ArrayList<>();
    all = page.getHtml().css("table").css("tr.villagetr").css("td", "text").all();
    for (int i = 0; i < all.size(); i++) {
    if (i % 3 != 1) {
    village.add(all.get(i));
    }
    }
    all = village;
    }
    }
    }
    }


    • 定义一个实体类RegionCodeDTO,用来存放临时获取的code,url以及父子关系等内容:


    public class RegionCodeDTO {

    private String code;

    private String parentCode;

    private String name;

    private Integer type;

    private String url;

    private List<RegionCodeDTO> regionCodeDTOS;
    }


    • 接下来对页面获取的内容(code、name、type)进行组装和临时存储,添加到children中:


    // 初始化子集
    List<RegionCodeDTO> children = new ArrayList<>();
    // 初始化临时节点数据
    RegionCodeDTO region = new RegionCodeDTO();
    // 解析页面结果集all当中的数据,组装到region 和 children当中
    for (int i = 0; i < all.size(); i++) {
    if (i % 2 == 0) {
    region.setCode(all.get(i));
    } else {
    region.setName(all.get(i));
    }
    if (StringUtils.isNotEmpty(region.getCode()) && StringUtils.isNotEmpty(region.getName())) {
    region.setType(type);
    // 添加子集到集合当中
    children.add(region);
    // 重新初始化
    region = new RegionCodeDTO();
    }
    }


    • 组装页面链接,并将页面链接组装到children当中。


    // 循环遍历页面元素获取的子页面链接
    for (int i = 0; i < urlList.size(); i++) {
    String url = null;
    if (StringUtils.isEmpty(urlList.get(0))) {
    continue;
    }
    // 拼接链接,页面的子链接是相对路径,需要手动拼接
    if (urlList.get(i).contains(provinceEnum.getCode() + "/")) {
    url = provinceEnum.getUrlPrefixNoCode();
    } else {
    url = provinceEnum.getUrlPrefix();
    }
    // 将链接放到临时数据子集对象中
    if (urlList.get(i).substring(urlList.get(i).lastIndexOf("/") + 1, urlList.get(i).indexOf(".html")).length() == 9) {
    children.get(i).setUrl(url + page.getUrl().toString().substring(page.getUrl().toString().indexOf(provinceEnum.getCode() + "/") + 3
    , page.getUrl().toString().lastIndexOf("/")) + "/" + urlList.get(i));
    } else {
    children.get(i).setUrl(url + urlList.get(i));
    }
    }


    • 将children添加到结果对象当中


    // 将子集放到集合当中
    regionCodeDTO.setRegionCodeDTOS(children);


    • 在下面的代码当中将进行两件事儿:



      • 处理下一页,通过page的addTargetRequests方法,可以进行下一页的跳转,此方法参数可以是listString和String,即支持多个页面跳转和单个页面的跳转。

      • 将数据传递到Pipeline,用于数据的存储,Pipeline的实现将在后面具体说明。




    // 定义下一页集合
    List<String> nextPage = new ArrayList<>();
    // 遍历上面的结果子集内容
    regionCodeDTO.getRegionCodeDTOS().forEach(regionCodeDTO1 -> {
    // 组装下一页集合
    nextPage.add(regionCodeDTO1.getUrl());
    // 定义并组装结果数据
    Map<String, Object> map = new HashMap<>();
    map.put("regionCode", regionCodeDTO1.getCode());
    map.put("regionName", regionCodeDTO1.getName());
    map.put("regionType", regionCodeDTO1.getType());
    map.put("regionFullName", regionCodeDTO1.getName());
    map.put("regionLevel", regionCodeDTO1.getType());
    list.add(map);
    // 推送数据到pipeline
    page.putField("list", list);
    });
    // 添加下一页集合到page
    page.addTargetRequests(nextPage);


  • 当本次process方法执行完后,将会根据传递过来的链接地址,再次执行process方法,根据前面定义的读取页面元素流程的代码,将不符合type=3的内容,所以将会进入到下一级4的爬取过程,5、6级别原理相同。


    image.png


  • 创建Pipeline,用于编写数据持久化过程。经过上面的逻辑,已经将所需内容全部获取到,接下来将通过pipline进行数据存储。首先定义pipeline,并实现其process方法,获取结果内容,具体存储数据的代码就不展示了,需要注意的是,此处pipeline没有通过spring容器托管,需要调用业务service需要使用SpringUtils进行获取:


    public class RegionDataPipeline implements Pipeline{


    @Override
    public void process(ResultItems resultItems, Task task) {
    // 获取service
    IXXXXXXXXXService service = SpringUtils.getBean(IXXXXXXXXXService.class);
    // 获取内容
    List<Map<String, String>> list = (List<Map<String, String>>) resultItems.getAll().get("list");
    // 解析数据,转换为对应实体类
    // service.saveBatch
    }


  • 启动爬虫


    //启动爬虫
    Spider.create(new RegionCodePageProcessor(provinceEnum))
    .addUrl(provinceEnum.getUrl())
    .addPipeline(new RegionDataPipeline())
    //此处不能小于2
    .thread(2).start()



2.2 爬取网站静态图片


爬取图片是最常见的需求,我们通常爬取的网站都是静态的网站,即爬取的内容都在网页上面渲染完成的,我们可以直接通过获取页面元素进行抓取。


可以参考下面的文章,直接拉取网站上的图片:juejin.cn/post/705138…


针对获取到的图片网络地址,直接使用如下方式进行下载即可:


url = new URL(imageUrl);
//打开连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求方式为"GET"
conn.setRequestMethod("GET");
//超时响应时间为10秒
conn.setConnectTimeout(10 * 1000);
//通过输入流获取图片数据
InputStream is = conn.getInputStream();

2.3 爬取网站动态图片


在2.2中我们可以很快地爬取到对应的图片,但是在另外两种场景下,我们获取图片将会不适用上面的方式:



  • 需要拼图,且多层的gis相关图片,此种图片将会在后期进行复杂的图片处理(按位置拼接瓦片,多层png图层叠加),才能获取到我们想要的效果。

  • 动态js加载的图片,直接无法通过css、xpath获取。


所以在这种情况下我们可以使用开篇介绍的selenium-java来解决,本文使用的仅仅是截图的功能,来达到我们需要的效果。具体街区全屏代码如下所示:


public File getItems() {
// 获取当前操作系统
String os = System.getProperty("os.name");
String path;
if (os.toLowerCase().startsWith("win")) {
//windows系统
path = "driver/chromedriver.exe";
} else {
//linux系统
path = "/usr/bin/chromedriver";
}
WebDriver driver = null;
// 通过判断 title 内容等待搜索页面加载完毕,间隔秒
try {
System.setProperty("webdriver.chrome.driver", path);
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-gpu");
chromeOptions.addArguments("--window-size=940,820");
driver = new ChromeDriver(chromeOptions);
// 截图网站地址
driver.get(UsaRiverConstant.OBSERVATION_POINT_URL);
// 休眠用于网站加载
Thread.sleep(15000);
// 截取全屏
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
return screenshotAs;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
driver.quit();
}
}

如上所示,我们获取的是整个页面的图片,还需要对截取的图片进行相应的剪裁,保留我们需要的区域,如下所示:


public static void cutImg(InputStream inputStream, int x, int y, int width, int height, OutputStream outputStream) {//图片路径,截取位置坐标,输出新突破路径
InputStream fis = inputStream;
try {
BufferedImage image = ImageIO.read(fis);
//切割图片
BufferedImage subImage = image.getSubimage(x, y, width, height);
Graphics2D graphics2D = subImage.createGraphics();
graphics2D.drawImage(subImage, 0, 0, null);
graphics2D.dispose();
//输出图片
ImageIO.write(subImage, "png", outputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

三、小结


通过如上两个组件的简单介绍,足够应付在java领域的大多数爬取场景。从页面数据、到静态网站图片,在到动态网站的图片截取。本文以提供思路为主,原理请参考相应的官方文档。


爬虫学得好,牢饭吃得饱!!!切记!!!


作者:我犟不过你
来源:juejin.cn/post/7267532912617177129
收起阅读 »

原来Optional用起来这么清爽!

前言 大家好,我是捡田螺的小男孩。 最近在项目中,看到一段很优雅的代码,用Optional 来判空的。我贴出来给大家看看: //遍历打印 userInfoList for (UserInfo userInfo : Optional.ofNullable(use...
继续阅读 »

前言


大家好,我是捡田螺的小男孩


最近在项目中,看到一段很优雅的代码,用Optional 来判空的。我贴出来给大家看看:


//遍历打印 userInfoList
for (UserInfo userInfo : Optional.ofNullable(userInfoList)
.orElse(new ArrayList<>())) {
//print userInfo
}

这段代码因为Optional的存在,优雅了很多,因为userInfoList可能为null,我们通常的做法,是先判断不为空,再遍历:


if (!CollectionUtils.isEmpty(userInfoList)) {
for (UserInfo userInfo:userInfoList) {
//print userInfo
}
}

显然,Optional让我们的判空更加优雅啦、



  • 关注公众号:捡田螺的小男孩(很多后端干货文章)


1. 没有Optional,传统的判空?


如果只有上面这一个例子的话,大家会不会觉得有点意犹未尽呀。那行,田螺哥再来一个。


假设有一个订单信息类,它有个地址属性。


要获取订单地址的城市,会有这样的代码:


String city = orderInfo.getAddress().getCity();

这块代码会有啥问题呢?是的,可能报空指针问题!为了解决空指针问题,一般我们可以这样处理:


if (orderInfo != null) {
Address address = orderInfo.getAddress();
if (address != null) {
String city = address.getCity();
}
}

这种写法显然有点丑陋。为了更加优雅一点,我们可以使用Optional


     String city = Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)
.orElseThrow(() ->
new IllegalStateException("OrderInfo or Address is null"));

这样是不是优雅一点,好了这例子也介绍完了。你们知道,田螺哥很细的。当然,是指写文章很细哈


image.png


有些伙伴,可能第一眼看那个Optional优化后的代码有点生疏。因此,接下来,给介绍Optional相关API


2. Optional API简介


2.1 ofNullable(T value)、empty()、of(T value)


因为我们上面的例子,使用到了 Optional.ofNullable(T value),第一个函数就讲它啦。源码如下:


  public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

如果value为null,就返回 empty(),否则返回 of(value)函数。接下来,我们看Optional的empty()of(value) 函数


public final class Optional<T> {

private static final Optional<?> EMPTY = new Optional<>();

public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}

显然, empty()函数的作用就是返回EMPTY对象。


of(value) 函数会返回Optional的构造函数


 public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}

对于 Optional的构造函数:


private Optional(T value) {
this.value = Objects.requireNonNull(value);
}

public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}


  • 当value值为空时,会报NullPointerException

  • 当value值不为空时,能正常构造Optional对象。


2.2 orElseThrow(Supplier<? extends X> exceptionSupplier)、orElse(T other) 、orElseGet(Supplier<? extends T> other)


上面的例子,我们用到了orElseThrow


 .orElseThrow(() -> new IllegalStateException("OrderInfo or Address is null"));

那我们先来介绍一下它吧:


public final class Optional<T> {

private final T value;

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}

很简单就是,如果value不为null,就返回value,否则,抛出函数式exceptionSupplier的异常。


一般情况,跟orElseThrow函数功能相似的还有orElse(T other)orElseGet(Supplier<? extends T> other)


public T orElse(T other) {
return value != null ? value : other;
}

对于orElse,如果value不为null,就返回value ,否则返回 other


public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

对于orElseGet,如果value不为null,就返回value ,否则返回执行函数式other后的结果。


2.3 map 和 flatMap


我们上面的例子,使用到了map(Function<? super T, ? extends U> mapper)


Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)

我们先来介绍一下它的:


  public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}

public boolean isPresent() {
return value != null;
}

其实这段源码很简答,先是做个空值检查,接着就是value的存在性检查,最后就是应用函数并返回新的 Optional```


.map相似的,还有个flatMap,如下:


public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}

可以发现,它两差别并不是很大,主要就是体现在入参所接受类型不一样。


2.4 isPresent 和ifPresent


我们在使用Optional的过程中呢,有些时候,会使用到isPresentifPresent,他们有点像,一个就是判断value值是否为空,另外一个就是判断value值是否为空,再去做一些操作。比如:


public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}

即判断value值是否为空,然后做一下函数式的操作。


举个例子,这段代码:


if(userInfo!=null){
doBiz(userInfo);
}

用了isPresent 可以优化为:


 Optional.ofNullable(userInfo)
.ifPresent(u->{
doBiz(u);
});

优雅永不过时,嘻嘻~


作者:捡田螺的小男孩
来源:juejin.cn/post/7391065678546386953
收起阅读 »

我毕业俩月,就被安排设计了公司第一个负载均衡方案,真头大

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 今天我想和大家聊一段我自己毕业初期的一段经历,当领导给你安排了无从下手的任务,难以解决的技术问题时,该怎么办? 本文会从我的一段经历出发,分析在初入职场、刚刚成为一个程序的时候,...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


今天我想和大家聊一段我自己毕业初期的一段经历,当领导给你安排了无从下手的任务,难以解决的技术问题时,该怎么办?


本文会从我的一段经历出发,分析在初入职场、刚刚成为一个程序的时候,遇到自己无解决的问题,和自己的一些解决问题的思考。


如果你工作不满三年,一定要往下看,相信一定会对你有帮助。但如果你已经工作3年以上,那么你应该不会遇到下面的问题啦,但也欢迎你往下看,也许会有不同的收获。


先讲故事


每个人都有自己的职场新人期,这个阶段你会敏感、迷茫。


记得在毕业的2个月后,我的组长给我布置了一个任务,实现服务的热部署。



热部署是什么?


热部署是一种技术,它允许在应用程序运行时不中断地更新或替换软件组件,如代码或配置。这种技术主要应用于Web应用程序和分布式系统中,旨在减少停机时间,提高开发效率,并增强应用程序的可用性。



那时候我们的服务器是单实例,每次升级,一定要先停掉服务,替换war包,然后重启tomcat。想升级,必须要在半夜客户下班的时候升级。


作为一个职场小白,技术小白,第一次接到这种任务的时候,我整个人是一脸懵逼的。


那时候报错的堆栈信息我都看不懂,还得请教组里的同事,CRUD还没整明白呢。就要去解决这个架构问题,可想而知我那时候有多崩溃。


其实选择让我一个毕业2个月的应届生来做,能发现两个基本的背景



  • 公司没有标准化的解决方案,没有人知道你的解决方案是否对错。

  • 包括我的组长、组内老同事在内,也没有人了解如何在生产环境实现热部署,没人可以提供帮助。


因此在这件事的起步阶段,非常的困难。最重要的是,我还不会调研行业的标准方案是什么,刚毕业的我只能埋头苦干,不断试错。


持续的没有进展,也让我内心非常焦虑。多次周会上被问及这件事情,也完全不知道如何汇报进展,因为我自己也不知道,到底什么时候能解决。


记得那时候上厕所、接水的时候,我都是低头不语装作看不见,生怕他问我一句:“那件事情的进度怎么样了?”


现在想起自己那时候的无助和不安,我都会微微一笑。


很庆幸是在计算机行业,即使是用百度,也能够搜到大把的信息,那段时间也了解到了像Nginx、Redis这样的中间件,最终也是用Nginx,实现了最基本的目标,可以在保证客户使用的情况下,正常的升级我们的系统了。


55818e8add97e7aa56644136df8d2b4a.jpg


是的,毕业后的第一家公司的负载均衡和不停机服务发布,是由一个小小毕业生来做的,前期内心有多煎熬,上线的那一刻就有多自豪。


故事讲完了,我相信很多职场人,都会面临这样的场景吧,或许是一个功能不知道怎么去实现,又或者一个方案不知道如何去设计,不敢面对,不敢说出来。


为什么会这样


在职场中,我们大概可以用四个阶段概括



  1. 新人期

  2. 发展期

  3. 成熟期

  4. 衰退期


上面我自己的一小段经历,遇到了新人期中最容易面临的问题,技能上的迷茫,和职场上的迷茫。


技能上的迷茫


我们可能发现自己有好多东西不会,什么都要学,却不知道自己该如何入手。


在大厂里,周围人看起来都是大牛,可以看着他们侃侃而谈,虽然自己只能茫然四顾,但起码有着标杆和榜样。


而在普通传统行业,身边连个会的人都没有,自己的摸索更像是盲人摸象。


职场上的迷茫


不知道事情该办成什么样,领导会不会不满意,对我的考核会不会有影响。


又或者主动说了有困难,领导会不会觉得我能力不足?


面对多重困难,我该如何汇报自己的进度?没有进度怎么办。


思考、建立正确的认知


界定问题,比问题本身要重要


技术日新月异,新技术是学不完的,解决问题的方式更是多种多样的。所有成熟的方案,都不是一蹴而就的,而是通过发现问题、解决问题一步步优化完善来的。


所以我们有一个清晰的认知,我们很难得到最优的解决方案,而是把注意力放在问题本身,看看我们到底要解决什么问题。


例如我一开始不知道热部署的含义,但我知道的是,公司想解决服务的不停机升级问题。那么我最终引入了Nginx,通过反向代理,部署多个服务就好了。提供的解决方案,完成了预期,我认为对于一个新人来讲就是合格的。



PS:事实上我在使用了Nginx完成了负载均衡后,我的组长曾和我说,你这个方案感觉不太行,不是领导想要的效果。但点到为止了,也没有什么指导,或者改进措施,最终也用这个方案在生产去部署。



注重当下


职场规则是明确的,可以在员工手册、职级要求中看到。但职场中的潜规则是不明确的,你无法摸清领导最真实的想法


为什么这么说呢?



  • 领导可能就是想历练你,给你有挑战的事情,想看看你做到什么程度,你的能力边界在哪里

  • 领导对你的预期就是能够解决,也不需要他的指导


不同的想法,对你的要求截然不同。


历练你的时候,即使事情没有结果,领导也不会多说什么,或许这就是一个当下受限于公司运维、资源而难以做到的事情。


但领导相信你能够解决问题的时候,你必须要尽量完成,不然确实会影响到对你的一些看法。


对于职场新手期,你短时间内是无法建立一个良好的向上沟通渠道的(如何建立向上沟通渠道,我们后面再说)。所以对你而言,与其揣摩想法, 不如界定好问题,然后注重当下,解决好遇到的每一个困难。


WechatIMG65268.jpg


两个方法


目标拆分


有一个关于程序员的经典段子:这个工作已经做完了80%,剩下的**20%**还要用和前面的一样时间。


遇到无从下手,不知如何去解决的问题,是怎么给出一个可执行的分解。


重点来了,拆分的,一定要可执行。每个人对于可执行的区分,是有很大不同的。不同的地方在于可执行的定义,你是否能清楚地知道这个问题该如何解决。


比如文章开头的故事,如果时间回到那一刻,我会这么列出计划



  1. 了解什么是热部署,方案调研

  2. 了解Nginx是什么,学会使用基本的命令

  3. 多个服务之间如何同步信息,比如上一秒在A服务,下一秒在B服务

  4. 进行验证,线上部署


学会借力



手里有把锤子,看见什么都是钉子。



我们更倾向于用我们手里的某种工具,去解决所有问题。


在遇到问题时,我们的大脑会根据以往经验做出预判,从而形成思维定势,使得我们倾向于使用熟悉的工具或方法来解决问题。然而,并不是所有问题都是钉子,一旦碰到超出我们经验边界的事情,我们可能会束手无策。


还是分享一段经历:


在字节的时候,我遇到过一个问题场景,就是如何保证集群服务器的本地缓存,保持一致。


直白来讲,我要开一个500人的会议,我准备了500份资料,那么在资料可能会修改的情况下,如何保证大家手里资料,都是最新的?


加载、更新,我能想到的就是用消息队列广播,系统收到消息的时候,每一台服务器都走一遍加载逻辑。


但有一个问题我解决不了,几百台服务器同时请求,如何解决突发的压力问题呢?如果500个人同时去找会议组织人打印,排队不说了,打印机得忙冒烟。


我给身边的一个技术大拿说了我的问题,他说,你可以用公司的一个中间件啊,数据只需要一次加载,然后服务器去下载就好了。是哈,用一个超级打印机打印出500份,分发下去就好了,不用每个人亲自来取呀。


就是几句点拨,同步给我了这一个信息,我了解了一下这个中间件,完美解决了我的问题。


因此,学会借力。找到公司大佬,找到网上的大佬,买杯咖啡、发个红包,直接了当的说出你的问题,咨询解决方案,从更高层次,降维打击你的问题。


当然,一定要在自己思考完、没有结果的情况下,再去请教,能够自己研究明白的,比如使用相关的,不要去麻烦别人。


e3dabd377c13de2e6af617c4b4035b47.jpeg


说在最后


文章到这里就要结束啦,很感谢你能看到最后。


职场初期遇到无从下手的任务时,我们应该建立正确的认知,界定问题,并注重当下


也分享给你了两个行之有效的方法,目标拆分学会借力,当然,这一切都离不开你的行动和坚持,这不是方法,而是一个技术人最基本的素质,所以就不多说啦。


不知道你在职场中初期遇到无从下手的问题时,你是怎么处理的呢,欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,让我做你的垫脚石,帮你解决你遇到的问题呀,欢迎一起交流~


作者:东东拿铁
来源:juejin.cn/post/7362905254725648438
收起阅读 »

美团多场景建模的探索与实践

本文介绍了美团到家/站外投放团队在多场景建模技术方向上的探索与实践。基于外部投放的业务背景,本文提出了一种自适应的场景知识迁移和场景聚合技术,解决了在投放中面临外部海量流量带来的场景数量丰富、场景间差异大的问题,取得了明显的效果提升。希望能给大家带来一些启发或...
继续阅读 »

本文介绍了美团到家/站外投放团队在多场景建模技术方向上的探索与实践。基于外部投放的业务背景,本文提出了一种自适应的场景知识迁移和场景聚合技术,解决了在投放中面临外部海量流量带来的场景数量丰富、场景间差异大的问题,取得了明显的效果提升。希望能给大家带来一些启发或帮助。



1 引言


美团到家Demand-Side Platform(下文简称DSP)平台,主要负责在美团外部媒体上进行商品或者物料的推荐和投放,并不断优化转化效果。随着业务的不断发展与扩大,DSP对接的外部渠道越来越丰富、展示形式越来越多样,物料展示场景的差异性愈发明显(如开屏、插屏、信息流、弹窗等)。


例如,用户在午餐时间更容易点击【某推荐渠道下】【某App】【开屏展示位】的快餐类商家的物料而不是【信息流展示位】的啤酒烧烤类商家物料。场景间差异的背后本质上是用户意图和需求的差异,因此模型需要对越来越多的场景进行定制化建设,以适配不同场景下用户的个性化需求。


业界经典的Mixture-of-Experts架构(MoE,如MMoE、PLE、STAR[1]等)能一定程度上适配不同场景下用户的个性化需求。这种架构将多个Experts的输出结果通过一个门控网络进行权重分配和组合,以得到最终的预测结果。早期,我们基于MoE架构提出了使用物料推荐渠道进行场景划分的多场景建模方案。然而,随着业务的不断壮大,场景间的差异越来越大、场景数量也越来越丰富,这版模型难以适应业务发展,不能很好地解决DSP背景下存在的以下两个问题:



  1. 负迁移现象:以推荐渠道为例,由于不同推荐渠道的流量在用户分布、行为习惯、物料展示形式等方面存在差异,其曝光数、点击率也不在同一个数量级(如下图1所示,不同渠道间点击率相差十分显著),数据呈现典型的“长尾”现象。如果使用推荐渠道进行多场景建模的依据,一方面模型会更倾向于学习到头部渠道的信息,对于尾部渠道会存在学习不充分的问题,另一方面尾部渠道的数据也会给头部渠道的学习带来“噪声”,导致出现负迁移。

  2. 数据稀疏难以收敛:DSP会在外部不同媒体上进行物料展示,而用户在访问外部媒体时,其所处的时空背景、上下文信息、不同App以及物料展示位等信息共同构成了当前的场景,这样的场景在十万的量级,每个场景的数据又十分稀疏,导致模型难以在每个场景上得到充分的训练。


在面对此类建模任务时,业界现有的方法是在不同场景间进行知识迁移。例如,SAML[2]模型采用辅助网络来学习场景的共享知识并迁移至各场景的独有网络;ADIN[3]和SASS[4]模型使用门控单元以一种细粒度的方式来选择和融合全局信息到单场景信息中。然而,在DSP背景中复杂多变的流量背景下,场景差异性导致了场景数量的急剧增长,现有方法无法在巨量稀疏场景下有效。


因此,在本文中我们提出了DSP背景下的自适应场景建模方案(AdaScene, Adaptive Scenario Model),同时从知识迁移和场景聚合两个角度进行建模。AdaScene通过控制知识迁移的程度来最大化不同场景共性信息的利用,并使用稀疏专家聚合的方式利用门控网络自动选择专家组成场景表征,缓解了负迁移现象;同时,我们利用损失函数梯度指导场景聚合,将巨大的推荐场景空间约束到有限范围内,缓解了数据稀疏问题,并实现了自适应场景建模方案。


图1 不同渠道规模差异


2 自适应场景建模


在本节开始前,我们先介绍多场景模型的建模方式。多场景模型采用输入层 Embedding + 混合专家(Mixture-of-Experts, MoE)的建模范式,其中输入信息包括了用户侧、商家侧以及场景上下文特征。多场景模型的损失由各场景的损失聚合而成,其损失函数形式如下:



其中,KK为场景数量,αiα_i为各场景的损失权重值。


我们提出的AdaScene自适应场景模型主要包含以下2个部分:场景知识迁移(Knowledge Transfer)模块以及场景聚合(Scene Aggregation)模块,其模型结构如下图2所示。场景知识迁移模块自适应地控制不同场景间的知识共享程度,并通过稀疏专家网络自动选择 K 个专家构成自适应场景表征。场景聚合模块通过离线预先自动化衡量所有场景间损失函数梯度的相似度,继而通过最大化场景相似度来指导场景的聚合。


图2 自适应场景建模AdaScene示意图


该模型结构的整体损失函数如以下公式所示:



其中,αk\alpha_{k}为每个场景组的损失函数所对应的系数,GkG_k为第kk个场景组下的的场景数量,GG为某种场景组的划分方式。


下面,我们分别介绍自适应场景知识迁移和场景聚合的建模方案。


2.1 自适应场景知识迁移


在多场景建模中,场景定义方式决定了场景专家的学习样本,很大程度上影响着模型对场景的拟合能力,但无论采用哪种场景定义方式,不同场景间用户分布都存在重叠,用户行为模式也会有相似性。


为提升不同场景间共性的捕捉能力,我们从场景特征和场景专家两个维度探索场景知识迁移的方法,在以物料推荐渠道×App×展示形态作为多场景建模Base模型的基础上,构建了如下图3所示的自适应场景知识迁移模型(Adaptive Knowledge Transfer Network, AKTN)。该模型建立了场景共享参数与私有参数的知识迁移桥梁,能够自适应地控制知识迁移的程度、缓解负迁移现象。


图3 AKTN(Adaptive Knowledge Transfer Network)



  • 场景特征适配:通过Squeeze-and-Excitation Network[5]构建场景适应层(Scene Adaption Layer),其结构可表示为FSE=FC(ReLU(FC(x)))F_{SE}= FC( ReLU( FC(x))),其中FCFC表示全连接层,ReLUReLU为激活函数。由于不同场景对原始特征的关注程度存在较大差异,该层能够根据不同场景的信息生成原始特征的权重,并利用这些权重对输入特征进行相应的变换,实现场景特定的个性化输入表征,提高模型的场景信息捕捉能力。

  • 场景知识迁移:使用GRU门控单元构建场景知识迁移层(Scene Transfer Layer)。GRU门控单元通过场景上下文信息对来自全局场景专家和当前场景专家的信息流动进行控制,筛选出符合当前场景的有用信息;并且,该结构能以层级方式进行堆叠,不断对场景输出进行修正。


场景特征适配在输入层根据场景信息对不同特征进行权重适配,筛选出当前场景下模型最关注的特征;场景知识迁移在隐层专家网络中进行知识迁移,控制共享专家中共性信息向场景独有信息的流动,使得场景共性信息得以传递。


这两种知识迁移方式互为补充、相辅相成,共同提升多场景模型的预估能力。我们对比了不同模块的实验效果,具体结果如下表1所示。可以看出,引入场景知识迁移和特征权重优化在头部、尾部渠道都能带来一定提升,其中尾部小流量场景上(见下表1子场景2、3)有更为明显的提升,可见场景知识迁移缓解了场景之间的负迁移现象。


表1 AKTN实验效果


相关研究和实践表明[6][7][8],稀疏专家网络对于提高计算效率和增强模型效果非常有用。因此,我们在AKTN模型的基础上,在专家层进一步优化多场景模型。具体的,我们将场景知识迁移层替换为自动化稀疏专家选择方法,通过门控网络从大规模专家中选取与当前场景最相关的KK个构成自适应场景表征,其选择过程如下图4所示:


图4 稀疏专家网络示意图


在实践中,我们通过使用可微门控网络对专家进行有效组合,以避免不相关任务之间的负迁移现象。同时大规模专家网络的引入扩大了多场景模型的选择空间,更好地支持了门控网络的选择。考虑到多场景下的海量流量和复杂场景特征,在业界调研的基础上对稀疏专家门控网络进行了探索。


具体而言,我们对以下稀疏门控方法进行了实践:



  • 方法一:通过KLKL散度衡量子场景与各专家之间的相似度,以此选择与当前场景最匹配的kk个专家。在实现方式上,使用场景*专家的二维矩阵计算相似性,并通过KLKL散度选择出最适合的kk个专家。

  • 方法二:每个子场景配备一个专家选择门控网络,mm个场景则有mm个门控网络。对于每个场景的门控网络,配备kk个单专家选择器[9],每个单专家选择器负责从nn个专家中选择一个作为当前场景的专家(nn为Experts个数)。在实践中,为提高训练效率,我们对单专家选择器中权重较小的值进行截断,保证每个单专家选择器仅选择一个专家。


在离线实验中,我们以物料推荐渠道 * 展示形态作为场景定义,对上述稀疏门控方法进行了尝试,离线效果如下表2所示:


表2 稀疏门控方法效果


可以看出,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。相较于常见的以截断方式为主的门控网络,使用二进制编码的方式使得其在不损失其他专家网络信息的同时,能够更好地收敛到目标专家数量,同时其可微性使得其在以梯度为基础的优化算法中训练更加稳定。


同时,为了验证稀疏门控网络能否有效区分不同场景并捕捉到场景间差异性,我们使用nn=16个专家中选择KK=7个的例子,对验证集中不同场景下各专家的利用率、选择专家的平均权重进行了可视化分析(如图5-图7所示),实验结果表明该方法能够有效地选择出不同的专家对场景进行表达。


例如,图6中KP_1更多地选择第5个专家,而KP_2更倾向于选择第15个专家。并且,不同场景对各专家的使用率以及选择专家的平均权重也有着明显的差异性,表明该方法能够捕捉到细分场景下流量的差异性并进行差异化的表达。


图5 同渠道下不同展示形式专家分布


图6 开屏展示不同渠道的专家分布


图7 信息流展示不同渠道的专家分布


实验证明,在通过大规模专家网络对每个场景进行建模的同时,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。 同时,为了进一步探索Experts个数对模型性能的影响,我们在方法二的基础上通过调整专家个数和topK比例设计了多组对比实验,实验结果如下表3所示:


表3 方法二调参实验


从实验数据可以看出,大规模的Experts结构会带来正向的离线收益;并且随着选取专家个数比例的增加(表3横轴),模型整体的表现效果也有上升的趋势。


2.2 自适应场景聚合


理想情况下,一条请求(流量)可以看作一个独立的场景。但如引言所述,随着DSP业务持续发展,不同的物料展示渠道、形式、位置等持续增加,每个场景的数据十分稀疏,我们无法对每个细分场景进行有效训练。因此,我们需要对各个推荐场景进行聚类、合并。我们使用场景聚合的方法对此问题进行求解,通过衡量所有场景间的相似度,并最大化该相似度来指导场景的聚合,解决了数据稀疏导致难以收敛的问题。具体的,我们将该问题表示为:



其中GG表示某种分组方式,fsif_{s_i}为场景sis_i在分组GkG_k内与其他场景的总体相似度。在将NN个场景聚合成KK个场景组的过程中,我们需要找到使得场景间整体相似度最大的分组方式GG^{\ast}


因此,我们在2.1节场景知识迁移模型的基础上,增加了场景聚合部分,提出了基于Two-Stage策略进行训练的场景聚合模型:



  • Stage 1:基于相似度衡量方法对各场景的相似度进行归纳,并以最大化分组场景的相似度为目标找到各场景的最优聚合方式(如Scene1与Scene 4可聚合为场景组合Scene Gr0up SGA);

  • Stage 2:基于Stage 1得到的场景聚合方式,以交叉熵损失为目标函数最小化各场景下的交叉熵损失。


其中,Stage 2与2.1节中所述一致,本节主要针对Stage 1进行阐述。我们认为,一个有效的场景聚合方法应该能自适应地应对流量变化的趋势,能够发现场景之间的内在联系并依据当前流量特点自动适配聚合方法。我们首先想到的是从规则出发,将人工先验知识作为场景聚合的依据,按照推荐渠道、展示形式以及两者叉乘的方式进行了相应迭代。然而这类场景聚合方式需要可靠的人工经验来支撑,且在应对海量流量时不能迅速捕捉到其中的变化。


因此,我们对场景之间关系的建模方法进行了相关的探索。首先,我们通过离线训练时场景之间的表征迁移和组合训练来评估场景之间的影响,但这种方式存在组合空间巨大、训练耗时较长的问题,效率较低。


在多任务的相关研究中[10][11][12][13],使用梯度信息对任务之间的关系进行建模是一种有效的方法。类似的在多场景模型中,能够根据各场景损失函数的梯度信息对场景间的相似度进行建模,因此我们采用多专家网络并基于梯度信息自动化地对场景之间的相似度进行求解,模型示意如下图8所示:


图8 场景聚合示意图


基于上述思路,我们对场景之间的关系建模方法进行了以下尝试:


1. Gradient Regulation


基于梯度信息能够对场景信息进行潜在表示这一认知,我们在损失函数中加入各场景损失函数关于专家层梯度距离的正则项,整体的损失函数如下所示,该正则项的系数λsi,sj\lambda_{s_i,s_j}表示场景之间的相似度,distdist为常见的评估梯度之间距离的方法,比如l1l_1l2l_2距离。



2. Lookahead Strategy



3. Meta Weights


Lookahead Strategy该方法对场景间的关系进行了显式建模,但是这种根据损失函数的变化计算场景相关系数的策略存在着训练不稳定、波动较大的现象,无法像Gradient Regulation这一方法对场景相似度进行求解。


因此,我们引入了场景间的相关性系数矩阵(meta weights),结合前两种方法对该问题进行如下建模,通过场景sis_i的数据对其与其他场景的相关性系数λsisj\lambda_{s_i \to s_j}进行更新,同时基于该参数对全局的参数模型WW进行优化。针对这种典型的两层优化问题,我们基于MAML[14]方法进行求解,并将meta weights作为场景间的相似度。



我们以推荐渠道和展示形式(是否开屏)的多场景模型作为Base,对上述3种方法做了探索。为了提高训练效率,我们在设计 Stage 1 模型时做了以下优化:



我们对每个方法的GAUC进行了比较,实验效果如下表4所示。相较于人工规则,基于梯度的场景聚合方法都能带来效果的明显提升,表明损失函数梯度能在一定程度上表示场景之间的相似性,并指导多场景进行聚合。


表4 场景聚合实验数据


为了更全面的展现场景聚合对于模型预估效果的影响,我们选取Meta Weights进行分组数量的调优实验,具体的实验结果如下表5所示。可以发现:随着分组数的增大,GAUC提升也越大,此时各场景间的负迁移效应减弱;但分组超过一定数量时,场景间总体的相似度减小,GAUC呈下降趋势。


表5 不同聚合场景数量实验数据


此外,我们对Meta Weigts方法中部分场景间的关系进行了可视化分析,分析结果如下图9所示。以场景作为坐标轴,图中的每个方格表示各场景间的相似度,颜色的深浅表示渠道间的相似程度大小。


图9 部分细分场景下的相似度示例


从图中可以发现,以渠道和展示形式为粒度的细分场景下,该方法能够学习到不同场景间的相关性,例如A渠道下的信息流(s16)与其他场景的相关性较低,会将其作为独立的场景进行预估,而B渠道下的开屏展示(s9)与C渠道开屏展示(s8)相关性较高,会将其聚合为一个场景进行预估,同时该相似度矩阵不是对称的,这也说明各场景间相互的影响存在着差异。


3 总结与展望


通过多场景学习的探索和实践,我们深入挖掘了推荐模型在不同场景下的建模能力,并分别从场景知识迁移、场景聚合方向进行了尝试和优化,这些尝试提供了更好的理解和解释推荐模型对不同类型流量和场景的应对能力。然而,这只是多场景学习研究的开始,后续我们会探索并迭代以下方向:



  • 更好的场景划分方式:当前多场景的划分主要还是依据渠道(渠道*展示形态)作为流量的划分方式,未来会在媒体、展示位、媒体*时间等维度上进行更详细地探索;

  • 端到端的流量聚合方式:在进行流量聚合时,使用了Two-Stage的策略进行聚合。然而,这种方式不能充分地利用流量数据中相关的信息。因此,需要探索端到端的流量场景聚合方案将更直接和有效地提高推荐模型的能力。


结合多场景学习,在未来的研究中将不断探索新的方法和技术,以提高推荐模型对不同场景和流量类型的建模能力,创造更好的用户体验以及商业价值。


4 作者简介


王驰、森杰、树立、文帅、尹华、肖雄等,均来自美团到家事业群/到家研发平台。


5 参考文献



作者:美团技术团队
来源:juejin.cn/post/7278597227785551883
收起阅读 »

DDD项目落地之充血模型实践 | 京东云技术团队

背景: 充血模型是DDD分层架构中实体设计的一种方案,可以使关注点聚焦于业务实现,可有效提升开发效率、提升可维护性; 1、DDD项目落地整体调用关系 调用关系图中的Entity为实体,从进入领域服务(Domin)时开始使用,直到最后返回。 2、实体设计 充血...
继续阅读 »

背景:


充血模型是DDD分层架构中实体设计的一种方案,可以使关注点聚焦于业务实现,可有效提升开发效率、提升可维护性;


1、DDD项目落地整体调用关系


调用关系图中的Entity为实体,从进入领域服务(Domin)时开始使用,直到最后返回。


image (26).png


2、实体设计


充血模型是实体设计的一种方法,简单来说,就是一种带有具体行为方法和聚合关联关系的特殊实体;


关于实体设计,需要明白的关键词为:领域服务->聚合->聚合根->实体->贫血模型->充血模型



聚合与聚合根:


聚合是一种关联关系,而聚合根就是这个关系成立的基础,没有聚合根,这个聚合关系就无法成立;


举个例子,存在3个实体:用户、用户组、用户组关联关系,这3个实体形成的关联关系就是聚合,而用户实体就是这个聚合中的聚合根;



实体:


定义在领域层,是领域层的重要元素,从领域划分到工程实践落地,都应该围绕实体进行,DDD中的实体和数据库表不只是1对1关系,可能是1对多或者仅为内存中的对象;


贫血模型:


实体不带有任何行为方法,也不带有聚合关联关系,作用基本相当于值对象(ValueObject),仅作为值传递的对象,和传统三层项目架构中的实体具有相同作用,不建议使用。补充说明:一般我们使用的DTO就可以被当做是值对象



充血模型:


实体中带有具有行为方法和聚合关联关系,行为方法是说create、save、delete等封装了一类可以指代行为的方法,比如在用户实体对象中具有用户组实体的引用,这样当我们需要操作用户组时,只通过用户实体进行操作就可以。


工程实践中,建议采用充血模型,好处是隐藏胶水代码,提升代码可读性,使关注点聚焦于业务实现。



充血模型在实践中的问题:


行为代码量过多,导致实体内部臃肿膨胀,难以阅读,难以维护,对于这种问题,我们需要根据实体行为的代码量多少来采取不同的解决方案。


解决方案:


场景1:行为不会导致实体臃肿的情况下,在实体中完成行为定义


public CooperateServicePackageConfig save() {    
// 直接调用基础设施层进行保存
cooperateServicePackageConfigRepository.save(this);
return this;
}

场景2:行为导致实体臃肿的情况下,采用外部定义行为的方式,核心思想是借助其他类实现行为代码定义,将臃肿代码外移,保留干净的实体行为:


1)创建工具类,将某个实体中的行为定义其中,实体负责调用该工具类


public CooperateServicePackageConfig save() {    
// 将处理过程放在工具类中
ServicePackageSaveUtils.save(this);
return this;
}

2)创建新实体,将该实体的使用场景明确至某个细分行为,比如一个聚合根(ExampleEntity)的保存可能涉及到5个实体的保存,那么我们定义一个ExampleSaveEntity实体,专门用来处理该聚合下的保存行为



实践经验:


1、关于spring bean注入:充血模型在实体中使用静态注入方法实现。例:


private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);

2、充血模型的实体序列化,排除非必要属性,在一些redis对象缓存时可能会用到。例:


// 使用注解排除序列化属性
@Getter(AccessLevel.NONE)
private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);

// 使用注解排除序列化属性
@JSONField(serialize = false)
private ServicePackageConfig servicePackageConfig;

// 使用注解排除序列化 get 方法
@Transient
@JSONField(serialize = false)
public static CooperateServicePackageRepositoryQuery getAllCodeQuery(Long contractId) {
CooperateServicePackageRepositoryQuery repositoryQuery = new CooperateServicePackageRepositoryQuery();
repositoryQuery.setContractIds(com.google.common.collect.Lists.newArrayList(contractId));
repositoryQuery.setCode(RightsPlatformConstants.CODE_ALL);
return repositoryQuery;
}

3、利用Set方法建立聚合绑定关系。例:


public void setServiceSkuInfos(List<ServiceSkuInfo> serviceSkuInfos) {    
if (CollectionUtils.isEmpty(serviceSkuInfos))
{
return;
}
this.serviceSkuInfos = serviceSkuInfos;
List<String> allSkuNoSet = serviceSkuInfos
.stream()
.map(one -> one.getSkuNo())
.collect(Collectors.toList());
String skuJoinStr = Joiner.on(GlobalConstant.SPLIT_CHAR).join(allSkuNoSet);
this.setSkuNoSet(skuJoinStr);}


作者:京东健康 张君毅


来源:京东云开发者社区



作者:京东云开发者
来源:juejin.cn/post/7264235181778190373
收起阅读 »

域名还能绑定动态IP?真是又涨见识了,再也不用购买固定IP了!赶快收藏

大家好,我是冰河~~ 一般家庭网络的公网IP都是不固定的,而我又想通过域名来访问自己服务器上的应用,也就是说:需要通过将域名绑定到动态IP上来实现这个需求。于是乎,我开始探索实现的技术方案。 通过在网上查阅一系列的资料后,发现阿里云可以做到实现动态域名解析DD...
继续阅读 »

大家好,我是冰河~~


一般家庭网络的公网IP都是不固定的,而我又想通过域名来访问自己服务器上的应用,也就是说:需要通过将域名绑定到动态IP上来实现这个需求。于是乎,我开始探索实现的技术方案。


通过在网上查阅一系列的资料后,发现阿里云可以做到实现动态域名解析DDNS。于是乎,一顿操作下来,我实现了域名绑定动态IP。这里,我们以Python为例实现。



小伙伴们注意啦:Java版源码已提交到:github.com/binghe001/m…



好了,说干就干,我们开始吧,走起~~


图片


阿里云DDNS前置条件



  • 域名是在阿里云购买的

  • 地址必须是公网地址,不然加了解析也没有用


通过阿里云提供的SDK,然后自己编写程序新增或者修改域名的解析,达到动态解析域名的目的;主要应用于pppoe拨号的环境,比如家里设置了服务器,但是外网地址经常变化的场景;再比如公司的pppoe网关,需要建立vpn的场景。


安装阿里云SDK


需要安装两个SDK库,一个是阿里云核心SDK库,一个是阿里云域名SDK库;


阿里云核心SDK库


pip install aliyun-python-sdk-core

阿里云域名SDK库


pip install aliyun-python-sdk-domain

阿里云DNSSDK库


pip install aliyun-python-sdk-alidns

设计思路



  • 获取阿里云的accessKeyId和accessSecret

  • 获取外网ip

  • 判断外网ip是否与之前一致

  • 外网ip不一致时,新增或者更新域名解析记录


实现方案


这里,我直接给出完整的Python代码,小伙伴们自行替换AccessKey和AccessSecret。


#!/usr/bin/env python
#coding=utf-8

# 加载核心SDK
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException

# 加载获取 、 新增、 更新、 删除接口
from aliyunsdkalidns.request.v20150109 import DescribeSubDomainRecordsRequest, AddDomainRecordRequest, UpdateDomainRecordRequest, DeleteDomainRecordRequest

# 加载内置模块
import json,urllib

# AccessKey 和 Secret 建议使用 RAM 子账户的 KEY 和 SECRET 增加安全性
ID = 'xxxxxxx'
SECRET = 'xxxxxx'

# 地区节点 可选地区取决于你的阿里云帐号等级,普通用户只有四个,分别是杭州、上海、深圳、河北,具体参考官网API
regionId = 'cn-hangzhou'

# 配置认证信息
client = AcsClient(ID, SECRET, regionId)

# 设置主域名
DomainName = 'binghe.com'

# 子域名列表 列表参数可根据实际需求增加或减少值
SubDomainList = ['a', 'b', 'c']

# 获取外网IP 三个地址返回的ip地址格式各不相同,3322 的是最纯净的格式, 备选1为 json格式 备选2 为curl方式获取 两个备选地址都需要对获取值作进一步处理才能使用
def getIp():
# 备选地址:1, http://pv.sohu.com/cityjson?ie=utf-8 2,curl -L tool.lu/ip
with urllib.request.urlopen('http://www.3322.org/dyndns/getip') as response:
html = response.read()
ip = str(html, encoding='utf-8').replace("\n", "")
return ip

# 查询记录
def getDomainInfo(SubDomain):
request = DescribeSubDomainRecordsRequest.DescribeSubDomainRecordsRequest()
request.set_accept_format('json')

# 设置要查询的记录类型为 A记录 官网支持A / CNAME / MX / AAAA / TXT / NS / SRV / CAA / URL隐性(显性)转发 如果有需要可将该值配置为参数传入
request.set_Type("A")

# 指定查记的域名 格式为 'test.binghe.com'
request.set_SubDomain(SubDomain)

response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')

# 将获取到的记录转换成json对象并返回
return json.loads(response)

# 新增记录 (默认都设置为A记录,通过配置set_Type可设置为其他记录)
def addDomainRecord(client,value,rr,domainname):
request = AddDomainRecordRequest.AddDomainRecordRequest()
request.set_accept_format('json')

# request.set_Priority('1') # MX 记录时的必选参数
request.set_TTL('600') # 可选值的范围取决于你的阿里云账户等级,免费版为 600 - 86400 单位为秒
request.set_Value(value) # 新增的 ip 地址
request.set_Type('A') # 记录类型
request.set_RR(rr) # 子域名名称
request.set_DomainName(domainname) #主域名

# 获取记录信息,返回信息中包含 TotalCount 字段,表示获取到的记录条数 0 表示没有记录, 其他数字为多少表示有多少条相同记录,正常有记录的值应该为1,如果值大于1则应该检查是不是重复添加了相同的记录
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
relsult = json.loads(response)
return relsult

# 更新记录
def updateDomainRecord(client,value,rr,record_id):
request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
request.set_accept_format('json')

# request.set_Priority('1')
request.set_TTL('600')
request.set_Value(value) # 新的ip地址
request.set_Type('A')
request.set_RR(rr)
request.set_RecordId(record_id) # 更新记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值

response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
return response

# 删除记录
def delDomainRecord(client,subdomain):
info = getDomainInfo(subdomain)
if info['TotalCount'] == 0:
print('没有相关的记录信息,删除失败!')
elif info["TotalCount"] == 1:
print('准备删除记录')
request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
request.set_accept_format('json')

record_id = info["DomainRecords"]["Record"][0]["RecordId"]
request.set_RecordId(record_id) # 删除记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
result = client.do_action_with_exception(request)
print('删除成功,返回信息:')
print(result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查后再操作!")

# 有记录则更新,没有记录则新增
def setDomainRecord(client,value,rr,domainname):
info = getDomainInfo(rr + '.' + domainname)
if info['TotalCount'] == 0:
print('准备添加新记录')
add_result = addDomainRecord(client,value,rr,domainname)
print(add_result)
elif info["TotalCount"] == 1:
print('准备更新已有记录')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
cur_ip = getIp()
old_ip = info["DomainRecords"]["Record"][0]["Value"]
if cur_ip == old_ip:
print ("新ip与原ip相同,不更新!")
else:
update_result = updateDomainRecord(client,value,rr,record_id)
print('更新成功,返回信息:')
print(update_result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查删除后再操作!")


IP = getIp()

# 循环子域名列表进行批量操作
for x in SubDomainList:
setDomainRecord(client,IP,x,DomainName)

# 删除记录测试
# delDomainRecord(client,'b.jsoner.com')

# 新增或更新记录测试
# setDomainRecord(client,'192.168.3.222','a',DomainName)

# 获取记录测试
# print (getDomainInfo(DomainName, 'y'))

# 批量获取记录测试
# for x in SubDomainList:
# print (getDomainInfo(DomainName, x))

# 获取外网ip地址测试
# print ('(' + getIp() + ')')

Python脚本的功能如下:



  • 获取外网ip地址。

  • 获取域名解析记录。

  • 新增域名解析记录。

  • 更新域名解析记录。

  • 删除域名解析记录 (并不建议将该功能添加在实际脚本中)。

  • 批量操作,如果记录不存在则添加记录,存在则更新记录。


另外,有几点需要特别说明:



  • 建议不要将删除记录添加进实际使用的脚本当中。

  • 相同记录是同一个子域名的多条记录,比如 test.binghe.com。

  • 脚本并没有验证记录类型,所以同一子域名下的不同类型的记录也会认为是相同记录,比如:有两条记录分别是 test.binghe.com 的 A 记录 和 test.binghe.com 的 AAAA 记录,会被认为是两条相同的 test.binghe.com 记录.如果需要判定为不同的记录,小伙伴们可以根据上述Python脚本自行实现。

  • 可以通过判断获取记录返回的 record_id 来实现精确匹配记录。


最后,可以将以上脚本保存为文件之后,通过定时任务,来实现定期自动更新ip地址。




作者:冰_河
来源:juejin.cn/post/7385106262009004095
收起阅读 »

接口不能对外暴露怎么办?

在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。 面对这样的情况,我们该如何实现呢? 1. 内外网接口微服务隔离 将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服...
继续阅读 »

在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。


面对这样的情况,我们该如何实现呢?


1. 内外网接口微服务隔离


将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。


该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。


该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。


2. 网关 + redis 实现白名单机制


在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。


该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;


不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;


另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。


3. 方案三 网关 + AOP


相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。


我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。


根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。


该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;


同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。


当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。


图片


具体实操



下面就方案三,进行具体的代码演示。



首先在网关侧,需要对进来的请求header添加外网标识符: from=public


@Component
public class AuthFilter implements GlobalFilterOrdered {
    @Override
    public Mono < Void > filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {
         return chain.filter(
         exchange.mutate().request(
         exchange.getRequest().mutate().header('id''').header('from''public').build())
         .build()
         );
    }

    @Override
    public int getOrder () {
        return 0;
    }
 }

接着,编写内外网访问权限判断的AOP和注解


@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
 @Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
 public void onlyIntranetAccessOnClass () {}
 @Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
 public void onlyIntranetAccessOnMethed () {
 }

 @Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' )
 public void before () {
     HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
     String from = hsr.getHeader ( 'from' );
     if ( !StringUtils.isEmptyfrom ) && 'public'.equals ( from )) {
        log.error ( 'This api is only allowed invoked by intranet source' );
        throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
            }
     }
 }

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccess {
}

最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可


@GetMapping ( '/role/add' )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
    return '该接口只允许内部服务调用';
}



4. 网关路径匹配

在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。




该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。







使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。
譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。

图片




作者:程序员蜗牛
来源:juejin.cn/post/7389092138900717579
收起阅读 »

Spring Boot集成pf4j实现插件开发功能

1.什么是pf4j? 一个插件框架,用于实现插件的动态加载,支持的插件格式(zip、jar)。 核心组件 **Plugin:**是所有插件类型的基类。每个插件都被加载到一个单独的类加载器中以避免冲突。 **PluginManager:**用于插件管理的所有方...
继续阅读 »

1.什么是pf4j?


一个插件框架,用于实现插件的动态加载,支持的插件格式(zip、jar)。


核心组件



  • **Plugin:**是所有插件类型的基类。每个插件都被加载到一个单独的类加载器中以避免冲突。

  • **PluginManager:**用于插件管理的所有方面(加载、启动、停止)。您可以使用内置实现作为JarPluginManager, ZipPluginManager, DefaultPluginManager(它是一个JarPluginManager+ ZipPluginManager),或者您可以从AbstractPluginManager(仅实现工厂方法)开始实现自定义插件管理器。

  • **PluginLoader:**加载插件所需的所有信息(类)。

  • **ExtensionPoint:**是应用程序中可以调用自定义代码的点。这是一个java接口标记。任何 java 接口或抽象类都可以标记为扩展点(实现ExtensionPoint接口)。

  • **Extension:**是扩展点的实现。它是一个类上的 Java 注释


场景


有一个spring-boot实现的web应用,在某一个业务功能上提供扩展点,用户可以基于SDK实现功能扩展,要求可以管理插件,并且能够在业务功能扩展点处动态加载功能。


2.代码工程


实验目的


实现插件动态加载,调用 卸载


Demo整体架构



  • pf4j-api:定义可扩展接口。

  • pf4j-plugins-01:插件项目,可以包含多个插件,需要实现 plugin-api 中定义的接口。所有的插件jar包,放到统一的文件夹中,方便管理,后续只需要加载文件目录路径即可启动插件。

  • pf4j-app:主程序,需要依赖 pf4j-api ,加载并执行 pf4j-plugins-01 。


pf4j-api


导入依赖


<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.0.1</version>
</dependency>

自定义扩展接口,集成 ExtensionPoint ,标记为扩展点


package com.et.pf4j;

import org.pf4j.ExtensionPoint;

public interface Greeting extends ExtensionPoint {

String getGreeting();

}

打包给其他项目引用


pf4j-plugins-01


如果你想要能够控制插件的生命周期,你可以自定义类集成 plugin 重新里面的方法


/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.pf4j.demo.welcome;

import com.et.pf4j.Greeting;
import org.apache.commons.lang.StringUtils;

import org.pf4j.Extension;
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;

/**
* @author Decebal Suiu
*/

public class WelcomePlugin extends Plugin {

public WelcomePlugin(PluginWrapper wrapper) {
super(wrapper);
}

@Override
public void start() {
System.out.println("WelcomePlugin.start()");
// for testing the development mode
if (RuntimeMode.DEVELOPMENT.equals(wrapper.getRuntimeMode())) {
System.out.println(StringUtils.upperCase("WelcomePlugin"));
}
}

@Override
public void stop() {
System.out.println("WelcomePlugin.stop()");
}

@Extension
public static class WelcomeGreeting implements Greeting {

@Override
public String getGreeting() {
return "Welcome ,my name is pf4j-plugin-01";
}

}

}

打成jar或者zip包,方便主程序加载


pf4j-app


加载插件包


package com.et.pf4j;

import org.pf4j.JarPluginManager;
import org.pf4j.PluginManager;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.nio.file.Paths;
import java.util.List;

@SpringBootApplication
public class DemoApplication {

/* public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}*/

public static void main(String[] args) {


// create the plugin manager
PluginManager pluginManager = new JarPluginManager(); // or "new ZipPluginManager() / new DefaultPluginManager()"

// start and load all plugins of application
//pluginManager.loadPlugins();

pluginManager.loadPlugin(Paths.get("D:\\IdeaProjects\\ETFramework\\pf4j\\pf4j-plugin-01\\target\\pf4j-plugin-01-1.0-SNAPSHOT.jar"));
pluginManager.startPlugins();
/*
// retrieves manually the extensions for the Greeting.class extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
System.out.println("greetings.size() = " + greetings.size());
*/

// retrieve all extensions for "Greeting" extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
for (Greeting greeting : greetings) {
System.out.println(">>> " + greeting.getGreeting());
}

// stop and unload all plugins
pluginManager.stopPlugins();
//pluginManager.unloadPlugins();


}
}

3.测试


运行DemoApplication.java 里面的mian函数,可以看到插件加载,调用以及卸载情况


4.引用



作者:HBLOG
来源:juejin.cn/post/7389912762045251584
收起阅读 »

我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱

我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱 文档地址 xuejm.gitee.io/easy-query-… GITHUB地址 github.com/xuejmnet/ea… GITEE地址 gitee.com/xuejm/easy-… 为...
继续阅读 »

我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱


文档地址 xuejm.gitee.io/easy-query-…


GITHUB地址 github.com/xuejmnet/ea…


GITEE地址 gitee.com/xuejm/easy-…


为什么要用orm


众所邹知orm的出现让本来以sql实现的复杂繁琐功能大大简化,对于大部分程序员而言一个框架的出现是为了生产力的提升.。dbc定义了交互数据库的规范,任何数据库的操作都是只需要满足jdbc规范即可,而orm就是为了将jdbc的操作进行简化。我个人“有幸”体验过.net和java的两个大orm,只能说差距很大,当然语言上的一些特性也让java在实现orm上有着比较慢的进度,譬如泛型的出现,lambda的出现。


一个好的orm我觉得需要满足以下几点



  • 强类型,如果不支持强类型那么和手写sql没有区别

  • 能实现80%的纯手写sql的功能,好的orm需要覆盖业务常用功能

  • 支持泛型,“如果一个orm连泛型都不支持那么就没有必要存在”这是一句现实但是又很残酷的结论,但是泛型会大大的减少开发人员的编写错误率

  • 不应该依赖过多的组件,当然这并不是orm特有的,任何一个库其实依赖越少越不易出bug


其实说了这么多总结一下就是一个好的orm应该有ide的提示外加泛型约束帮助开发可以非常顺滑的把代码写下去,并且错误部分可以完全的在编译期间提现出来,运行时错误应该尽可能少的去避免。


为什么放弃mybatis


首先如果你用过其他语言的orm那么再用java的mybatis就像你用惯了java的stream然后去自行处理数据过滤,就像你习惯了kotlin的语法再回到java语法,很难受。这种难受不是自动挡到手动挡的差距,而且自动挡到手推车的差距。


xml配置sql也不知道是哪个“小天才”想出来的,先不说写代码的时候java代码和xml代码跳来跳去,而且xml下>,<必须要配合CDATA不然xml解析就失败,别说转义,我写那玩意在加转义你确定让我后续看得眼睛不要累死吗?美名其曰xml和代码分离方便维护,但是你再怎么方便修改了代码一样需要重启,并且因为代码写在xml里面导致动态条件得能力相对很弱。并且我也不知道mybatis为什么天生不支持分页,需要分页插件来支持,难道一个3202年的orm了还需要这样吗,很难搞懂mybatis的作者难道不写crud代码的吗?有些时候简洁并不是偷懒的原因,当然也有可能是架构的问题导致的。


逻辑删除的功能我觉得稍微正常一点的企业一定都会有这个功能,但是因为使用了myabtis,因为手写sql,所以常常会忘记往sql中添加逻辑删除字段,从而导致一些奇奇怪怪的bug需要排查,因为这些都是编译器无法体现的错误,因为他是字符串,因为mybatis把这个问题的原因指向了用户,这一点他很聪明,这个是用户的错误而不是框架的,但是框架要做的就是尽可能的将一些重复工作进行封装隐藏起来自动完成。


可能又会有一些用户会说所见即所得这样我才能知道他怎么执行了,但是现在哪个orm没有sql打印功能,哪个orm框架执行的sql和打印的sql是不一样的,不是所见即所得。总体而言我觉得mybatis充其量算是sqltemlate,比sqlhelper好的地方就是他是参数化防止sql注入。当然最主要的呀一点事难道java程序员不需要修改表,不需要动表结构,不需要后期维护的吗还是说java程序员写一个项目就换一个地方跳槽,还是说java程序员每个方法都有单元测试。我在转java后理解了一点,原来这就是你们经常说的java加班严重,用这种框架加班不严重就有鬼了。


为什么放弃mybatis衍生框架


有幸在201几年再网上看到了mybatis-plus框架,这块框架一出现就吸引了我,因为他在处理sql的方式上和.net的orm很相似,起码都是强类型,起码不需要java文件和xml文件跳来跳去,平常50%的代码也是可以通过框架的lambda表达式来实现,我个人比较排斥他的字符串模式的querywrapper,因为一门强类型语言缺少了强类型提示,在编写代码的时候会非常的奇怪。包括后期的重构,当然如果你的代码后续不需要你维护那么我觉得你用哪种方式都是ok的反正是一次性的,能出来结果就好了。


继续说mybatis-plus,因为工作的需要再2020年左右针对内部框架进行改造,并且让mybatis-plus支持强类型gr0up by,sum,min,max,any等api。

这个时候其实大部分情况下已经可以应对了,就这样用了1年左右这个框架,包括后续的update的increment,decrement


update table set column=column-1 where id=xxx and column>1

全部使用lambda强类型语法,可以应对多数情况,但是针对join始终没有一个很好地方法。直到我遇到了mpj也就是mybatis-plus-join,但是这个框架也有问题,就是这个逻辑删除在join的子表上不生效,需要手动处理,如果生效那么在where上面,不知道现在怎么样了,当时我也是自行实现了让其出现在join的on后面,但是因为实现是需要实现某个接口的,所以并没有pr代码.
首先定义一个接口


public interface ISoftDelete {
Boolean getDeleted();
}

//其中join mapper是我自己的实现,主要还是`WrapperFunction`的那段定义
@Override
public Scf4jBaseJoinLinq<T1,TR> on(WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> onFunction) {
WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> join= on->{
MPJAbstractLambdaWrapper<T1, ?> apply = onFunction.apply(on);
if(ISoftDelete.class.isAssignableFrom(joinClass)){
SFunction deleted = LambdaHelper.getFunctionField(joinClass, "deleted", Boolean.class);
apply.eq(deleted,false);
}
return apply;
};
joinMapper.setJoinOnFunction(query->{
query.innerJoin(joinClass,join);
});
return joinMapper;
}

虽然实现了join但是还是有很多问题出现和bug。



  • 比如不支持vo对象的返回,只能返回数据库对象自定义返回列,不然就是查询所有列

  • 再比如如果你希望你的对象update的时候填充null到数据库,那么只能在entity字段上添加,这样就导致这个字段要么全部生效要么全部不生效.

  • 批量插入不支持默认居然是foreach一个一个加,当然这也没关系,但是你真的想实现批处理需要自己编写很复杂的代码并且需要支持全字段。而不是null列不填充

  • MetaObjectHandler,支持entityinsertupdate但是不支持lambdaUpdateWrapper,有时候当前更新人和更新时间都是需要的,你也可以说数据库可以设置最后更新时间,但是最后修改人呢?

  • 非常复杂的动态表名,拜托大哥我只是想改一下表名,目前的解决方案就是try-finally每次用完都需要清理一下当前线程,因为tomcat会复用线程,通过threadlocal来实现,话说pagehelper应该也是这种方式实现的吧
    当然其他还有很多问题导致最终我没办法忍受,选择了自研框架,当然我的框架自研是参考了一部分的freesql和sqlsuagr的api,并且还有java的beetsql的实现和部分方法。毕竟站在巨人的肩膀上才能看的更远,不要问我为什么不参考mybatis的,我觉得mybatis已经把简单问题复杂化了,如果需要看懂他的代码是一件很得不偿失的事情,最终我发现我的选择是正确的,我通过参考beetsql的源码很快的清楚了java这边应该需要做的事情,为我编写后续框架节约了太多时间,这边也给beetsql打个广告 https://gitee.com/xiandafu/beetlsql


自研orm有哪些特点


easy-query一款无任何依赖的java全新高性能orm支持 单表 多表 子查询 逻辑删除 多租户 差异更新 联级一对一 一对多 多对一 多对多 分库分表(支持跨表查询分页等) 动态表名 数据库列高效加解密支持like crud拦截器 原子更新 vo对象直接返回


文档地址 xuejm.gitee.io/easy-query-…


GITHUB地址 github.com/xuejmnet/ea…


GITEE地址 gitee.com/xuejm/easy-…



  • 强类型,可以帮助团队在构建和查询数据的时候拥有id提示,并且易于后期维护。

  • 泛型可以控制我们编写代码时候的一些低级错误,比如我只查询一张表,但是where语句里面可以使用不存在上下文的表作为条件,进一步限制和加强表达式

  • easy-query提供了三种模式分别是lambda,property,apt proxy其中lambda表达式方便重构维护,property只是性能最好,apt proxy方便维护,但是重构需要一起重构apt文件


单表查询


//根据条件查询表中的第一条记录
List<Topic> topics = easyQuery
.queryable(Topic.class)
.limit(1)
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LIMIT 1
<== Total: 1

//根据条件查询id为3的集合
List<Topic> topics = easyQuery
.queryable(Topic.class)
.where(o->o.eq(Topic::getId,"3").eq(Topic::geName,"4")
.toList();

==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` = ? AND t.`name` = ?
==> Parameters: 3(String),4(String)
<== Total: 1

多表


 Topic topic = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.leftJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where(o -> o.eq(Topic::getId, "3"))
.firstOrNull();

==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LEFT JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Total: 1

List<BlogEntity> blogEntities = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
//join查询select必须要带对应的返回结果,可以是自定义dto也可以是实体对象,如果不带对象则返回t表主表数据
.select(BlogEntity.class, (t, t1) -> t1.columnAll())
.toList();

==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic t INNER JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: 3(String)
<== Total: 1

子查询



```java
//SELECT * FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<BlogEntity> subQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "1"));


List<Topic> x = easyQuery
.queryable(Topic.class).where(o -> o.exists(subQueryable.where(q -> q.eq(o, BlogEntity::getId, Topic::getId)))).toList();


==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE EXISTS (SELECT 1 FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ? AND t1.`id` = t.`id`)
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 3(ms)
<== Total: 1


//SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<String> idQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "123"))
.select(String.class, o -> o.column(BlogEntity::getId));//如果子查询in string那么就需要select string,如果integer那么select要integer 两边需要一致
List<Topic> list = easyQuery
.queryable(Topic.class).where(o -> o.in(Topic::getId, idQueryable)).toList();


==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE t.`id` IN (SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?)
==> Parameters: false(Boolean),123(String)
<== Time Elapsed: 2(ms)
<== Total: 0

自定义逻辑删除




//@Component //如果是spring
public class MyLogicDelStrategy extends AbstractLogicDeleteStrategy {
/**
* 允许datetime类型的属性
*/

private final Set<Class<?>> allowTypes=new HashSet<>(Arrays.asList(LocalDateTime.class));
@Override
protected SQLExpression1<WherePredicate<Object>> getPredicateFilterExpression(LogicDeleteBuilder builder,String propertyName) {
return o->o.isNull(propertyName);
}

@Override
protected SQLExpression1<ColumnSetter<Object>> getDeletedSQLExpression(LogicDeleteBuilder builder, String propertyName) {
// LocalDateTime now = LocalDateTime.now();
// return o->o.set(propertyName,now);
//上面的是错误用法,将now值获取后那么这个now就是个固定值而不是动态值
return o->o.set(propertyName,LocalDateTime.now())
.set("deletedUser",CurrentUserHelper.getUserId());
}

@Override
public String getStrategy() {
return "MyLogicDelStrategy";
}

@Override
public Set<Class<?>> allowedPropertyTypes() {
return allowTypes;
}
}

//为了测试防止数据被删掉,这边采用不存在的id
logicDelTopic.setId("11xx");
//测试当前人员
CurrentUserHelper.setUserId("easy-query");
long l = easyQuery.deletable(logicDelTopic).executeRows();

==> Preparing: UPDATE t_logic_del_topic_custom SET `deleted_at` = ?,`deleted_user` = ? WHERE `deleted_at` IS NULL AND `id` = ?
==> Parameters: 2023-04-01T23:15:13.944(LocalDateTime),easy-query(String),11xx(String)
<== Total: 0

差异更新




  • 要注意是否开启了追踪spring-boot下用@EasyQueryTrack注解即可开启

  • 是否将当前对象添加到了追踪上下文 查询添加asTracking或者 手动将查询出来的对象进行easyQuery.addTracking(Object entity)



TrackManager trackManager = easyQuery.getRuntimeContext().getTrackManager();
try{
trackManager.begin();
Topic topic = easyQuery.queryable(Topic.class)
.where(o -> o.eq(Topic::getId, "7")).asTracking().firstNotNull("未找到对应的数据");
String newTitle = "test123" + new Random().nextInt(100);
topic.setTitle(newTitle);
long l = easyQuery.updatable(topic).executeRows();
}finally {

trackManager.release();
}

==> Preparing: UPDATE t_topic SET `title` = ? WHERE `id` = ?
==> Parameters: test1239(String),7(String)
<== Total: 1

关联查询


一对一


学生和学生地址


//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.toList();
//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.select(SchoolStudentVO.class,o->o.columnAll()
.columnInclude(SchoolStudent::getSchoolStudentAddress,SchoolStudentVO::getSchoolStudentAddress))
.toList();

多对一


学生和班级


//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.toList();
//自定义列
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass,s->s.column(SchoolClassVO::getId))
)
.toList();

//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass)
)
.toList();

一对多


班级和学生


//数据库对像查询
List<SchoolClass> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.toList();
//vo自定义列映射返回
List<SchoolClassVO> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolStudents,SchoolClassVO::getSchoolStudents))
.toList();

多对多


班级和老师


      List<SchoolClass> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers,1))
.toList();
List<SchoolClassVO> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolTeachers,SchoolClassVO::getSchoolTeachers))
.toList();

动态报名


List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a -> "aa_bb_cc")
.where(o -> o.eq(BlogEntity::getId, "123")).toList();


==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0



List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a->{
if("t_blog".equals(a)){
return "aa_bb_cc1";
}
return "xxx";
})
.where(o -> o.eq(BlogEntity::getId, "123")).toList();


==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc1 t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0




List<BlogEntity> x_t_blog = easyQuery
.queryable(Topic.class)
.asTable(o -> "t_topic_123")
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.asTable("x_t_blog")
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
.select(BlogEntity.class, (t, t1) -> t1.columnAll()).toList();

==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic_123 t INNER JOIN x_t_blog t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: false(Boolean),3(String)
<== Total: 0

最后


感谢各位看到最后,希望以后我的开源框架可以帮助到您,如果您觉得有用可以点点star,这将对我是极大的鼓励


更多文档信息可以参考git地址或者文档


文档地址 xuejm.gitee.io/easy-query-…


GITHUB地址 github.com/xuejmnet/ea…


GITEE地址 gitee.com/xuejm/easy-…


作者:xuejm
来源:juejin.cn/post/7259926933008908325
收起阅读 »

压缩炸弹,Java怎么防止

一、什么是压缩炸弹,会有什么危害 1.1 什么是压缩炸弹 压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不...
继续阅读 »

一、什么是压缩炸弹,会有什么危害


1.1 什么是压缩炸弹


压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。


以下是安全测试几种经典的压缩炸弹


graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)

A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。


压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。


压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。



1.2 压缩炸弹会有什么危害


graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px

压缩炸弹可能对计算机系统造成以下具体的破坏:



  1. 资源耗尽:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。

  2. 磁盘空间耗尽:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。

  3. 系统崩溃:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。

  4. 拒绝服务攻击:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。

  5. 数据丢失:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。



重要提示:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。



二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹


2.1 个人有没有方法可以检测压缩炸弹?


有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:


graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)

A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


  1. 安全软件和防病毒工具(推荐):使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。

  2. 文件大小限制:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。

  3. 文件类型过滤:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。


2.2 Java怎么防止压缩炸弹


在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:


graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px


  1. 解压缩算法的限制:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。

  2. 设置解压缩操作的资源限制:使用Java的java.util.zipjava.util.jar等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。

  3. 使用安全的解压缩库:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。

  4. 文件类型验证和过滤:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。

  5. 异步解压缩操作:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。

  6. 安全策略和权限控制:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。


2.2.1 使用解压算法的限制来实现防止压缩炸弹


在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制来实现防止压缩炸弹。


先来看看我们实现的思路


graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L

style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

实现流程说明如下:



  1. 首先,通过给定的 file 参数创建一个 ZipFile 对象,用于打开要解压缩的 ZIP 文件。

  2. zipFileSize 变量用于计算解压缩后的文件总大小。

  3. 使用 zipFile.entries() 方法获取 ZIP 文件中的所有条目,并通过 while 循环逐个处理每个条目。

  4. 对于每个条目,使用 entry.getSize() 获取条目的未压缩大小,并将其累加到 zipFileSize 变量中。

  5. 如果 zipFileSize 超过了给定的 size 参数,说明解压后的文件大小超过了限制,此时会调用 deleteDir() 方法删除已解压的文件夹,并抛出 IllegalArgumentException 异常,以防止压缩炸弹攻击。

  6. 创建一个 File 对象 unzipped,表示解压后的文件或目录在输出文件夹中的路径。

  7. 如果当前条目是一个目录,且 unzipped 不存在,则创建该目录。

  8. 如果当前条目不是一个目录,确保 unzipped 的父文件夹存在。

  9. 创建一个 FileOutputStream 对象 fos,用于将解压后的数据写入到 unzipped 文件中。

  10. 通过 zipFile.getInputStream(entry) 获取当前条目的输入流。

  11. 创建一个缓冲区 buffer,并使用循环从输入流中读取数据,并将其写入到 fos 中,直到读取完整个条目的数据。

  12. 最后,在 finally 块中关闭 fos 和 zipFile 对象,确保资源的释放。


实现代码工具类


import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class FileBombUtil {

/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/

public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;

/**
* 文件超限提示
*/

public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";

/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/

public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}

fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);

byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}

}

/**
* 递归删除目录文件
*
* @param dir 目录
*/

private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}

}

测试类


import java.io.File;

/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class Test {

public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}

}

三、总结


文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。


文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。


总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。


在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:




  1. 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。

  2. 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。

  3. 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。



作者:竹子爱揍功夫熊猫
来源:juejin.cn/post/7289667869557178404
收起阅读 »

一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO

在现代软件架构中,不同类型的类扮演着不同的角色,共同构成了一个清晰、模块化和可维护的系统。以下是对实体类(Entity)、数据传输对象(DTO)、领域对象(Domain Object)、持久化对象(Persistent Object)、业务对象(Busines...
继续阅读 »

image.png


在现代软件架构中,不同类型的类扮演着不同的角色,共同构成了一个清晰、模块化和可维护的系统。以下是对实体类(Entity)、数据传输对象(DTO)、领域对象(Domain Object)、持久化对象(Persistent Object)、业务对象(Business Object)、应用对象(Application Object)、数据访问对象(Data Access Object, DAO)、服务层(Service Layer)和控制器层(Controller Layer)的总体介绍:



不同领域作用


POJO (Plain Old Java Object)



  • 定义:POJO 是一个简单的Java对象,它不继承任何特定的类或实现任何特定的接口,除了可能实现 java.io.Serializable 接口。它通常用于表示数据,不包含业务逻辑。

  • 案例的体现



    • UserEntity 类可以看作是一个POJO,因为它主要包含数据字段和标准的构造函数、getter和setter方法。

    • UserDTO 类也是一个POJO,它用于传输数据,不包含业务逻辑。




VO (Value Object)



  • 定义:VO 是一个代表某个具体值或概念的对象,通常用于表示传输数据的结构,不涉及复杂的业务逻辑。


VO(View Object)的特点:



  • 展示逻辑:VO通常包含用于展示的逻辑,例如格式化的日期或货币值。

  • 用户界面相关:VO设计时会考虑用户界面的需求,可能包含特定于视图的属性。

  • 可读性:VO可能包含额外的描述性信息,以提高用户界面的可读性。


实体类(Entity)



  • 作用:代表数据库中的一个表,是数据模型的实现,通常与数据库表直接映射。

  • 使用场景:当需要将应用程序的数据持久化到数据库时使用。


数据传输对象(DTO)



  • 作用:用于在应用程序的不同层之间传输数据,特别是当需要将数据从服务层传输到表示层或客户端时。

  • 使用场景:进行数据传输,尤其是在远程调用或不同服务间的数据交换时。


领域对象(Domain Object)



  • 作用:代表业务领域的一个实体或概念,通常包含业务逻辑和业务状态。

  • 使用场景:在业务逻辑层处理复杂的业务规则时使用。


持久化对象(Persistent Object)



  • 作用:与数据存储直接交互的对象,通常包含数据访问逻辑。

  • 使用场景:执行数据库操作,如CRUD(创建、读取、更新、删除)操作。


业务对象(Business Object)



  • 作用:封装业务逻辑和业务数据,通常与领域对象交互。

  • 使用场景:在业务逻辑层实现业务需求时使用。


应用对象(Application Object)



  • 作用:封装应用程序的运行时配置和状态,通常不直接与业务逻辑相关。

  • 使用场景:在应用程序启动或运行时配置时使用。


数据访问对象(Data Access Object, DAO)



  • 作用:提供数据访问的抽象接口,定义了与数据存储交互的方法。

  • 使用场景:需要进行数据持久化操作时,作为数据访问层的一部分。


服务层(Service Layer)



  • 作用:包含业务逻辑和业务规则,协调应用程序中的不同组件。

  • 使用场景:处理业务逻辑,执行业务用例。


控制器层(Controller Layer)



  • 作用:处理用户的输入,调用服务层的方法,并返回响应结果。

  • 使用场景:处理HTTP请求和响应,作为Web应用程序的前端和后端之间的中介。


案例介绍



  1. 用户注册



    • DTO:用户注册信息的传输。

    • Entity:用户信息在数据库中的存储形式。

    • Service Layer:验证用户信息、加密密码等业务逻辑。



  2. 商品展示



    • Entity:数据库中的商品信息。

    • DTO:商品信息的传输对象,可能包含图片URL等不需要存储在数据库的字段。

    • Service Layer:获取商品列表、筛选和排序商品等。



  3. 订单处理



    • Domain Object:订单的业务领域模型,包含订单状态等。

    • Business Object:订单处理的业务逻辑。

    • DAO:订单数据的持久化操作。



  4. 配置加载



    • Application Object:应用程序的配置信息,如数据库连接字符串。



  5. API响应



    • Controller Layer:处理API请求,调用服务层,返回DTO作为响应。




案例代码


视图对象(VO)


一个订单系统,我们需要在用户界面展示订单详情:


// OrderDTO - 数据传输对象
public class OrderDTO {
private Long id;
private String customerName;
private BigDecimal totalAmount;
// Constructors, getters and setters
}

// OrderVO - 视图对象
public class OrderVO {
private Long id;
private String customerFullName; // 格式化后的顾客姓名
private String formattedTotal; // 格式化后的总金额,如"$1,234.56"
private String orderDate; // 格式化后的订单日期

// Constructors, getters and setters

public OrderVO(OrderDTO dto) {
this.id = dto.getId();
this.customerFullName = formatName(dto.getCustomerName());
this.formattedTotal = formatCurrency(dto.getTotalAmount());
this.orderDate = formatDateTime(dto.getOrderDate());
}

private String formatName(String name) {
// 实现姓名格式化逻辑
return name;
}

private String formatCurrency(BigDecimal amount) {
// 实现货币格式化逻辑
return "$" + amount.toString();
}

private String formatDateTime(Date date) {
// 实现日期时间格式化逻辑
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
}

实体类(Entity)



package com.example.model;

import javax.persistence.*;

@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String username;

@Column(nullable = false)
private String password;

// Constructors, getters and setters
public UserEntity() {}

public UserEntity(String username, String password) {
this.username = username;
this.password = password;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

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

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

数据传输对象(DTO)



package com.example.dto;

public class UserDTO {
private Long id;
private String username;

// Constructors, getters and setters
public UserDTO() {}

public UserDTO(Long id, String username) {
this.id = id;
this.username = username;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

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

领域对象(Domain Object)



package com.example.domain;

public class UserDomain {
private Long id;
private String username;

// Business logic methods
public UserDomain() {}

public UserDomain(Long id, String username) {
this.id = id;
this.username = username;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

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

// Additional domain-specific methods
}

领域对象通常包含业务领域内的概念和逻辑。在订单系统中,这可能包括订单状态、订单项、总价等。


package com.example.domain;

import java.util.List;

public class OrderDomain {
private String orderId;
private List items; // 订单项列表
private double totalAmount;
private OrderStatus status; // 订单状态

// Constructors, getters and setters

public OrderDomain(String orderId, List items) {
this.orderId = orderId;
this.items = items;
this.totalAmount = calculateTotalAmount();
this.status = OrderStatus.PENDING; // 默认状态为待处理
}

private double calculateTotalAmount() {
double total = 0;
for (OrderItemDomain item : items) {
total += item.getPrice() * item.getQuantity();
}
return total;
}

// 业务逻辑方法,例如更新订单状态
public void processPayment() {
// 处理支付逻辑
if (/* 支付成功条件 */) {
this.status = OrderStatus.PAYMENT_COMPLETED;
}
}

// 更多业务逻辑方法...
}

持久化对象(Persistent Object)



package com.example.model;

public class UserPersistent extends UserEntity {
// Methods to interact with persistence layer, extending UserEntity
}

业务对象(Business Object)



package com.example.service;

public class UserBO {
private UserDomain userDomain;

public UserBO(UserDomain userDomain) {
this.userDomain = userDomain;
}

// Business logic methods
public void performBusinessLogic() {
// Implement business logic
}
}

 OrderBO 业务对象通常封装业务逻辑,可能包含领域对象,并提供业务操作的方法。


package com.example.service;

import com.example.domain.OrderDomain;

public class OrderBO {
private OrderDomain orderDomain;

public OrderBO(OrderDomain orderDomain) {
this.orderDomain = orderDomain;
}

// 执行订单处理的业务逻辑
public void performOrderProcessing() {
// 例如,处理订单支付
orderDomain.processPayment();
// 其他业务逻辑...
}

// 更多业务逻辑方法...
}

应用对象(Application Object)



package com.example.config;

public class AppConfig {
private String environment;
private String configFilePath;

public AppConfig() {
// Initialize with default values or environment-specific settings
}

// Methods to handle application configuration
public void loadConfiguration() {
// Load configuration from files, environment variables, etc.
}

// Getters and setters
}

数据访问对象(Data Access Object)



package com.example.dao;

import com.example.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserDAO extends JpaRepository {
// Custom data access methods if needed
UserEntity findByUsername(String username);
}

OrderDAO DAO 提供数据访问的抽象接口,定义了与数据存储交互的方法。在Spring Data JPA中,可以继承JpaRepository并添加自定义的数据访问方法。


package com.example.dao;

import com.example.domain.OrderDomain;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderDAO extends JpaRepository { // 主键类型为String
// 自定义数据访问方法,例如根据订单状态查询订单
List findByStatus(OrderStatus status);
}

服务层(Service Layer)



package com.example.service;

import com.example.dao.UserDAO;
import com.example.dto.UserDTO;
import com.example.model.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
private final UserDAO userDAO;

@Autowired
public UserService(UserDAO userDAO) {
this.userDAO = userDAO;
}

public UserDTO getUserById(Long id) {
UserEntity userEntity = userDAO.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
return convertToDTO(userEntity);
}

private UserDTO convertToDTO(UserEntity entity) {
UserDTO dto = new UserDTO();
dto.setId(entity.getId());
dto.setUsername(entity.getUsername());
return dto;
}

// Additional service methods
}

 OrderService服务层协调用户输入、业务逻辑和数据访问。它使用DAO进行数据操作,并可能使用业务对象来执行业务逻辑。


package com.example.service;

import com.example.dao.OrderDAO;
import com.example.domain.OrderDomain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderService {
private final OrderDAO orderDAO;

@Autowired
public OrderService(OrderDAO orderDAO) {
this.orderDAO = orderDAO;
}

public List findAllOrders() {
return orderDAO.findAll();
}

public OrderDomain getOrderById(String orderId) {
return orderDAO.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found"));
}

public void processOrderPayment(String orderId) {
OrderDomain order = getOrderById(orderId);
OrderBO orderBO = new OrderBO(order);
orderBO.performOrderProcessing();
// 更新订单状态等逻辑...
}

// 更多服务层方法...
}

控制器层(Controller Layer)



package com.example.controller;

import com.example.dto.UserDTO;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUserById(id);
}

// Additional controller endpoints
}


总结


这些不同类型的类和层共同构成了一个分层的软件架构,每一层都有其特定的职责和功能。这种分层方法有助于降低系统的复杂性,提高代码的可维护性和可扩展性。通过将业务逻辑、数据访问和用户界面分离,开发人员可以独立地更新和测试每个部分,从而提高开发效率和应用程序的稳定性。


历史热点文章



作者:肖哥弹架构
来源:juejin.cn/post/7389212915302105098
收起阅读 »

我去,怎么http全变https了

项目场景: 在公司做的一个某地可视化项目。 部署采用的是前后端分离部署,图片等静态资源请求一台minio服务器。 项目平台用的是http图片资源的服务器用的是https 问题描述 在以https请求图片资源时,图片请求成功报200。 【现象1】: 继图片后续...
继续阅读 »

项目场景:


在公司做的一个某地可视化项目。


部署采用的是前后端分离部署,图片等静态资源请求一台minio服务器。


项目平台用的是http
图片资源的服务器用的是https




问题描述


在以https请求图片资源时,图片请求成功报200。

【现象1】: 继图片后续的请求,后续此域名和子域名下的的url均由http变为https

【现象2】: 界面阻塞报错,无法交互


原因分析:


经过现象查阅,发现出现该现象与浏览器的HSTS有关。


什么是HSTS ?


HTTP的Strict-Transport-Security(HSTS)请求头是一种网络安全机制,用于告诉浏览器仅通过HTTPS与服务器通信,而不是HTTP。它的作用主要有以下几点:



  1. 防止协议降级攻击:当浏览器接收到HSTS响应头后,它会将该网站添加到HSTS列表中,并在后续的访问中强制使用HTTPS,即使用户或攻击者尝试通过HTTP访问该网站,浏览器也会自动将其重定向到HTTPS。

  2. 减少中间人攻击的风险:通过确保所有通信都通过加密的HTTPS进行,可以降低中间人攻击(MITM)的风险,因为攻击者无法轻易地截获或篡改传输的数据。

  3. 提高网站的安全性:HSTS可以作为网站安全策略的一部分,帮助保护用户的敏感信息,如登录凭据、支付信息等。

  4. 简化安全配置:对于网站管理员来说,HSTS可以减少需要维护的安全配置,因为浏览器会自动处理HTTPS的重定向。

  5. 提高用户体验:由于浏览器会自动处理重定向,用户不需要担心访问的是HTTP还是HTTPS版本,可以更顺畅地浏览网站。


HSTS的配置可以通过max-age指令来设置浏览器应该记住这个策略的时间长度,还可以使用includeSubDomains指令来指示所有子域名也应该遵循这个策略。此外,还有一个preload选项,允许网站所有者将他们的网站添加到浏览器的预加载HSTS列表中,这样用户在第一次访问时就可以立即应用HSTS策略。


于是在我发现该相关的响应头确有此物


image.png




解决方案:


那就取决于服务器是在哪里设置的该请求头。可能是在Nginx,Lighttpd,PHP等等,将该响应头配置去除


作者:阿亻
来源:juejin.cn/post/7382386471272448035
收起阅读 »

再有人问你WebSocket为什么牛逼,就把这篇文章发给他!

点赞再看,Java进阶一大半 2008年6月诞生了一个影响计算机世界的通信协议,原先需要二十台计算机资源才能支撑的业务场景,现在只需要一台,这得帮"抠门"老板们省下多少钱,它就是大名鼎鼎的WebSocket协议。很快在下一年也就是2009年的12月,Goog...
继续阅读 »

点赞再看,Java进阶一大半



2008年6月诞生了一个影响计算机世界的通信协议,原先需要二十台计算机资源才能支撑的业务场景,现在只需要一台,这得帮"抠门"老板们省下多少钱,它就是大名鼎鼎的WebSocket协议。很快在下一年也就是2009年的12月,Google浏览器就宣布成为第一个支持WebSocket标准的浏览器。


WebSocket的推动者和设计者就是下面的Michael Carter,他设计的WebSocket协议技术现在每天在全地球有超过20亿的设备在使用。


在这里插入图片描述


逮嘎猴,我是南哥。


一个Java进阶的领路人,今天指南的是WebSocket,跟着南哥我们一起Java进阶。


本文收录在我开源的《Java进阶指南》中,一份帮助小伙伴们进阶Java、通关面试的Java学习面试指南,相信能帮助到你在Java进阶路上不迷茫。南哥希望收到大家的 ⭐ Star ⭐支持,这是我创作的最大动力。GitHub地址:github.com/hdgaadd/Jav…


1. WebSocket概念


1.1 为什么会出现WebSocket



面试官:有了解过WebSocket吗?



一般的Http请求我们只有主动去请求接口,才能获取到服务器的数据。例如前后端分离的开发场景,自嘲为切图仔实际扮猪吃老虎的前端大佬找你要一个配置信息的接口,我们后端开发三下两下开发出一个RESTful架构风格的API接口,只有当前端主动请求,后端接口才会响应。


但上文这种基于HTTP的请求-响应模式并不能满足实时数据通信的场景,例如游戏、聊天室等实时业务场景。现在救世主来了,WebSocket作为一款主动推送技术,可以实现服务端主动推送数据给客户端。大家有没听说过全双工、半双工的概念。



全双工通信允许数据同时双向流动,而半双工通信则是数据交替在两个方向上传输,但在任一时刻只能一个方向上有数据流动



HTTP通信协议就是半双工,而数据实时传输需要的是全双工通信机制,WebSocket采用的便是全双工通信。举个微信聊天的例子,企业微信炸锅了,有成百条消息轰炸你手机,要实现这个场景,大家要怎么设计?用iframe、Ajax异步交互技术配合以客户端长轮询不断请求服务器数据也可以实现,但造成的问题是服务器资源的无端消耗,运维大佬直接找到你工位来。显然服务端主动推送数据的WebSocket技术更适合聊天业务场景。


1.2 WebSocket优点



面试官:为什么WebSocket可以减少资源消耗?



大家先看看传统的Ajax长轮询和WebSocket性能上掰手腕谁厉害。在websocket.org网站提供的Use Case C的测试里,客户端轮询频率为10w/s,使用Poling长轮询每秒需要消耗高达665Mbps,而我们的新宠儿WebSocet仅仅只需要花费1.526Mbps,435倍的差距!!


在这里插入图片描述


为什么差距会这么大?南哥告诉你,WebSocket技术设计的目的就是要取代轮询技术和Comet技术。Http消息十分冗长和繁琐,一个Http消息就要包含了起始行、消息头、消息体、空行、换行符,其中请求头Header非常冗长,在大量Http请求的场景会占用过多的带宽和服务器资源。


大家看下百度翻译接口的Http请求,拷贝成curl命令是非常冗长的,可用的消息肉眼看过去没多少。


curl ^"https://fanyi.baidu.com/mtpe-individual/multimodal?query=^%^E6^%^B5^%^8B^%^E8^%^AF^%^95&lang=zh2en^" ^
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Cache-Control: max-age=0" ^
-H "Connection: keep-alive" ^
-H ^"Cookie: BAIDUID=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; BAIDUID_BFESS=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; ab_sr=1.0.1_NDhjYWQyZmRjOWIwYjI3NTNjMGFiODExZWFiMWU4NTY4MjA2Y2UzNGQwZjJjZjI1OTdlY2JmOThlNzk1ZDAxMDljMTA2NTMxYmNlM1OTQ1MTE0ZTI3Y2M0NTIzMzdkMmU2MGMzMjc1OTRiM2EwNTJQ==; RT=^\^"z=1&dm=baidu.com&si=b9941642-0feb-4402-ac2b-a913a3eef1&ss=ly866fx&sl=4&tt=38d&bcn=https^%^3A^%^2F^%^2Ffclog.baidu.com^%^2Flog^%^2Fweirwood^%^3Ftype^%^3Dp&ld=ccy&ul=jes^\^"^" ^
-H "Sec-Fetch-Dest: document" ^
-H "Sec-Fetch-Mode: navigate" ^
-H "Sec-Fetch-Site: same-origin" ^
-H "Sec-Fetch-User: ?1" ^
-H "Upgrade-Insecure-Requests: 1" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H ^"sec-ch-ua: ^\^"Not/A)Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"126^\^", ^\^"Google Chrome^\^";v=^\^"126^\^"^" ^
-H "sec-ch-ua-mobile: ?0" ^
-H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" &

而WebSocket是基于帧传输的,只需要做一次握手动作就可以让客户端和服务端形成一条通信通道,这仅仅只需要2个字节。我搭建了一个SpringBoot集成的WebSocket项目,浏览器拷贝WebSocket的Curl命令十分简洁明了,大家对比下。


curl "ws://localhost:8080/channel/echo" ^
-H "Pragma: no-cache" ^
-H "Origin: http://localhost:8080" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Sec-WebSocket-Key: VoUk/1sA1lGGgMElV/5RPQ==" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H "Upgrade: websocket" ^
-H "Cache-Control: no-cache" ^
-H "Connection: Upgrade" ^
-H "Sec-WebSocket-Version: 13" ^
-H "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits"

如果你要区分Http请求或是WebSocket请求很简单,WebSocket请求的请求行前缀都是固定是ws://


2. WebSocket实践


2.1 集成WebSocket服务器



面试官:有没动手实践过WebSocket?



大家要在SpringBoot使用WebSocket的话,可以集成spring-boot-starter-websocket,引入南哥下面给的pom依赖。


	<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
dependencies>

感兴趣点开spring-boot-starter-websocket依赖的话,你会发现依赖所引用包名为package jakarta.websocket。这代表SpringBoot其实是集成了Java EE开源的websocket项目。这里有个小故事,Oracle当年决定将Java EE移交给Eclipse基金会后,Java EE就进行了改名,现在Java EE更名为Jakarta EE。Jakarta是雅加达的意思,有谁知道有什么寓意吗,评论区告诉我下?


我们的程序导入websocket依赖后,应用程序就可以看成是一台小型的WebSocket服务器。我们通过@ServerEndpoint可以定义WebSocket服务器对客户端暴露的接口。


@ServerEndpoint(value = "/channel/echo")

而WebSocket服务器要推送消息给到客户端,则使用package jakarta.websocket下的Session对象,调用sendText发送服务端消息。


    private Session session;

@OnMessage
public void onMessage(String message) throws IOException{
LOGGER.info("[websocket] 服务端收到客户端{}消息:message={}", this.session.getId(), message);
this.session.getAsyncRemote().sendText("halo, 客户端" + this.session.getId());
}

看下getAsyncRemote方法返回的对象,里面是一个远程端点实例。


    RemoteEndpoint.Async getAsyncRemote();

2.2 客户端发送消息



面试官:那客户端怎么发送消息给服务器?



客户端发送消息要怎么操作?这点还和Http请求很不一样。后端开发出接口后,我们在Swagger填充参数,点击Try it out,Http请求就发过去了。


但WebSocket需要我们在浏览器的控制台上操作,例如现在南哥要给我们的WebSocket服务器发送Halo,JavaGetOffer,可以在浏览器的控制台手动执行以下命令。


websocket.send("Halo,JavaGetOffer");

实践的操作界面如下。


在这里插入图片描述




作者:Java进阶指南针
来源:juejin.cn/post/7388025457821810698
收起阅读 »

MySQL 9.0 创新版发布,大失所望。。

大家好,我是程序员鱼皮。2024 年 7 月 1 日,MySQL 发布了 9.0 创新版本。区别于我们大多数开发者常用的 LTS(Long-Term Support)长期支持版本,创新版本的发布会更频繁、会更快地推出新的特性和变更,可以理解为 “尝鲜版”,适合...
继续阅读 »

大家好,我是程序员鱼皮。2024 年 7 月 1 日,MySQL 发布了 9.0 创新版本。区别于我们大多数开发者常用的 LTS(Long-Term Support)长期支持版本,创新版本的发布会更频繁、会更快地推出新的特性和变更,可以理解为 “尝鲜版”,适合追求前沿技术的同学体验。



我通过阅读官方文档,完整了解了本次发布的新特性,结果怎么说呢,唉,接着往下看吧。。。


下面鱼皮带大家 “尝尝鲜”,来看看 MySQL 9.0 创新版本有哪些主要的变化。


新特性


1、Event 相关 SQL 语句可以被 Prepared


在 MySQL 中,事件(Events)是一种可以在预定时间执行的调度任务,比如定期清理数据之类的,就可以使用事件。


MySQL 9.0 对事件 SQL 提供了 Prepared 支持,包括:



  • CREATE EVENT

  • ALTER EVENT

  • DROP EVENT


prepared 准备语句是一种预编译的 SQL 语句模板,可以在执行时动态地传入参数,从而提高查询的性能和安全性。


比如下面就是一个准备语句,插入的数据可以动态传入:


PREPARE stmt_insert_employee FROM 'INSERT INTO employees (name, salary) VALUES (?, ?)';

2、Performance Schema 新增 2 张表


MySQL 的 Performance Schema 是一个用于监视 MySQL 服务器性能的工具。它提供了一组动态视图和表,记录了 MySQL 服务器内部的活动和资源使用情况,帮助开发者进行性能分析、调优和故障排除。


本次新增的表:



  1. variables_metadata 表:提供关于系统变量的一般信息。包括 MySQL 服务器识别的每个系统变量的名称、作用域、类型、范围和描述。此表的 MIN_VALUE 和 MAX_VALUE 列旨在取代已弃用的 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列。

  2. global_variable_attributes 表:提供有关服务器分配给全局系统变量的属性-值对的信息。


3、SQL 语句优化


现在可以使用以下语法将 EXPLAIN ANALYZE(分析查询执行计划和性能的工具)的 JSON 输出保存到用户变量中:


EXPLAIN ANALYZE FORMAT=JSON INTO @variable select_stmt

随后,可以将这个变量作为 MySQL 的任何 JSON 函数的 JSON 参数使用。


4、向量存储


AI 的发展带火了向量数据库,我们可以利用向量数据库存储喂给 AI 的知识库和文档。


虽然 MySQL 官方更新日志中并没有提到对于向量数据存储的支持,但是网上有博主在 MySQL 9.0 社区版中进行了测试,发现其实已经支持了向量存储,如图:


图片来源于网络


在此之前,MySQL 推出过一个专门用于分析处理和高性能查询的数据库变体 HeatWave,本来以为只会在 HeatWave 中支持向量存储,没想到社区版也能使用。如果是真的,那可太好了。



5、其他


此外,还优化了 Windows 系统上 MySQL 的安装和使用体验。


废弃和移除


1)在 MySQL 8.0 中,已移除了在 MySQL 8.0 中已废弃的 mysql_native_password 认证插件,并且服务器现在拒绝来自没有 CLIENT_PLUGIN_AUTH 能力的旧客户端程序的 mysql_native 认证请求。为了向后兼容性,mysql_native_password 仍然在客户端上可用;客户端内置的认证插件已转换为动态加载插件。


这些更改还涉及移除以下服务器选项和变量:



  • --mysql-native-password 服务器选项

  • --mysql-native-password-proxy-users 服务器选项

  • default_authentication_plugin 服务器系统变量


2)Performance Schema 中 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列现在已废弃,并可能在将来的 MySQL 版本中移除。开发者应该改为使用 variables_metadata 表的 MIN_VALUE 和 MAX_VALUE 列。


3)ER_SUBQUERY_NO_1_ROW 已从忽略包含 IGNORE 关键字的语句的错误列表中移除。这样做的原因如下:



  • 忽略这类错误有时会导致将 NULL 插入非空列(对于未转换的子查询),或者根本不插入任何行(使用 subquery_to_derived 的子查询)。

  • 当子查询转换为与派生表联接时,行为与未转换查询不同。


升级到 9.0 后,如果包含 SELECT 语句的 UPDATE、DELETE 或 INSERT 语句使用了包含多行结果的标量子查询,带有 IGNORE 关键字的语句可能会引发错误。



总结


看了本次 MySQL 9.0 创新版的更新,说实话,大失所望。在这之前,网上有很多关于 MySQL 9.0 版本新特性的猜测,结果基本上都没有出现。毕竟距离 MySQL 上一次发布的大版本 8.0 已经时隔 6 年,本来以为这次 MySQL 会有一些王炸的新特性,结果呢,本次除了修复了 100 多个 Bug 之外,几乎没啥对开发者有帮助的点。别说没帮助了,我估计很多同学在看这篇文章前都没接触过这些有变更的特性。



我们最关注的,无非就是使用难度、成本和性能提升对吧,最好是什么代码都不用改,直接升级个数据库的版本,性能提升个几倍,还能跟老板吹一波牛皮。


你看看隔壁的 PostgreSQL,这几年,都已经从 11 更新到 17 版本了,AI 时代人家也早就能通过插件支持存储向量数据了。MySQL 你这真的是创新么?


最后,MySQL 9.0 创新版本的下载地址我就不放了,咱还是老老实实用 5.7 和 8.0 版本,MySQL 的新版本,还有很长一条路要走呀!




作者:程序员鱼皮
来源:juejin.cn/post/7387999151411920931
收起阅读 »

.zip 结尾的域名很危险吗?有多危险?

Google 于 2023 年 5 月 10 日全面开放了以 .zip 结尾的域名,这一举动引起了安全研究人员和社区的警惕,他们担心该通用顶级域名(gTLD,Generic top-level domains)会被用于创建足以迷惑计算机高手的恶意 URL。 2...
继续阅读 »

Google 于 2023 年 5 月 10 日全面开放了以 .zip 结尾的域名,这一举动引起了安全研究人员和社区的警惕,他们担心该通用顶级域名(gTLD,Generic top-level domains)会被用于创建足以迷惑计算机高手的恶意 URL。


2023 年 5 月 3 日,Google 宣布了包括 .zip.mov 在内的 8 个全新的通用顶级域名:



  • .dad

  • .phd

  • .prof

  • .esq

  • .foo

  • .zip

  • .mov

  • .nexus


并于 5 月 10 日通过 Google Domains 向公众开放注册。



Google Domains 是 Google 提供的一项域名注册和管理服务,支持用户搜索和注册域名



Zip Domain for Malicious Attacks


小心!含有 .zip 的恶意 URL


安全研究员 Bobby Rauch 指出(The Dangers of Google’s .zip TLD),要警惕含有 .zip、Unicode 字符(特别是 U+2044、U+2215 等)以及 @ 符号的恶意 URL。这类恶意 URL 迷惑性极强,甚至能欺骗十分有经验的用户。


若点击 https://google.com@bing.com 这个 URL,实际访问的是 https://bing.com。这是因为根据 RFC 3986 Uniform Resource Identifier (URI): Generic Syntax 的规定,@ 符号之前的 google.com 应识别为用户信息,其后的 bing.com 才是主机名(域名)。我们可以借助常用的编程语言来确认这一点,如利用 PHP 的 parse_url() 函数:


<?php
var_dump(parse_url("https://google.com@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(8) "bing.com"
["user"]=>
string(10) "google.com"
}

然而,若 @ 之前有正斜杠 /,如 https://google.com/search@bing.com,则浏览器会将 /search@bing.com 部分识别为路径,最终访问的是 https://google.com/ 下的文件 search@bing.com。由于没有这个文件,结果自然是 404。


<?php
var_dump(parse_url("https://google.com/search@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(10) "google.com"
["path"]=>
string(16) "/search@bing.com"
}

Bobby Rauch 就是利用了上述规则,创建了一个恶意 URL,


https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip

乍看之下,这个 URL 似乎是用于从 GitHub 上下载 v1271 这个特定版本的 Kubernetes 的链接。但实际上 parse_url() 函数的解析结果显示,真正要访问的域名却是 v1271.zip 而不是 github.com


<?php
var_dump(parse_url("https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(9) "v1271.zip"
["user"]=>
string(63) "github.com?__kubernetes?__kubernetes?__archive?__refs?__tags?__"
}

若你不小心点击了这类域名,那么恭喜你,很可能喜提一个 evil.exe(请注意动画演示中的左下角)。



仅凭肉眼可能难以分辨以下两个 URL 的区别吧:


https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕
https:/
/github.com/kubernetes/kubernetes/archive/refs/tags/

但若调整一下字体,则可以发现端倪,


image-20240625184157641


恶意 URL 中的正斜杠 / 根本不是真正的 /(U+002F),


image-20240625184841985


而是下面这个看起来很像 / 的 Unicode 字符:


image-20240625185113911


由于恶意 URL 中并没有使用真正的 /,因此根据 RFC 3986 的规定,@ 之前的部分github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕ 尽管看似域名与路径,但实际上却是用户信息(真够长的)。


在刚刚的动画演示中,Bobby Rauch 其实还使用了另一个迷惑人的小伎俩——在电子邮件客户端上,将 @ 的字号大小更改为 1,让这个特殊字符几乎看不到,从而更隐秘地伪装了恶意 URL。


对于由以 .zip 结尾的域名带来的安全隐患,Bobby Rauch 给出的建议是,在单击 URL 之前,先将鼠标悬停在该 URL 上并检查浏览器底部显示真正要访问的 URL。


作者:胡译胡说
来源:juejin.cn/post/7384244866875146290
收起阅读 »

听说你会架构设计?来,弄一个打车系统

目录引言网约车系统需求设计概要设计详细设计体验优化小结1.引言1.1 台风来袭深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日...
继续阅读 »

目录

  1. 引言
  2. 网约车系统
    1. 需求设计
    2. 概要设计
    3. 详细设计
    4. 体验优化
  3. 小结

1.引言

1.1 台风来袭

深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。

对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日已经停课、晚上 7 点后停运。

由于下午 4 点停市,于是大部分公司都早早下班。其中有赶点下班的,像这样:

有提前下班的,像这样:

还有像我们这样要居家远程办公的:

1.2 崩溃打车

下午 4 点左右,公交和地铁都人满为患。

于是快下班(居家办公)的时候就想着打个车回家,然而打开滴滴之后:

排队人数 142 位,这个排队人数和时长,让我的心一下就拔凉拔凉的。

根据历史经验,在雨天打上车的应答时间得往后推半个小时左右。更何况,这还是台风天气!

滴滴啊滴滴,你就不能提前准备一下嘛,这个等待时长,会让你损失很多订单分成的。

但反过来想,这种紧急预警,也不能完全怪打车平台,毕竟,车辆调度也是需要一定时间的。在这种大家争相逃命(bushi 的时候,周围的车辆估计也不太够用。

卷起来

等着也是等着,于是就回到公司继续看技术文章。这时我突然想到,经过这次车辆紧急调度之后,如果我是滴滴的开发工程师,需要怎么处理这种情况呢?

如果滴滴的面试官在我眼前,他又会怎么考量候选人的技术深度和产品思维呢?

2. 设计一个“网约车系统”

面试官:“滴滴打车用过是吧!看你简历里写道会架构设计是吧,如果让你设计一个网约车系统,你会从哪些方面考虑呢?”

2.1 需求分析

网约车系统(比如滴滴)的核心功能是把乘客的打车订单发送给附件的网约车司机,司机接单后,到上车点接送乘客,乘客下车后完成订单。

其中,司机通过平台约定的比例抽取分成(70%-80%不等),乘客可以根据第三方平台的信用值(比如支付宝)来开通免密支付,在下车后自动支付订单。用例图如下:

乘客和司机都有注册登录功能,分属于乘客用户模块和司机用户模块。网约车系统的另外核心功能是乘客打车,订单分配,以及司机送单。

2.2 概要设计

网约车系统是互联网+共享资源的一种模式,目的是要把车辆和乘客结合起来,节约已有资源的一种方式,通常是一辆网约车对多个用户。

所以对于乘客和司机来说,他们和系统的交互关系是不同的。比如一个人一天可能只打一次车,而一个司机一天得拉好几趟活。

故我们需要开发两个 APP 应用,分别给乘客和司机打车和接单,架构图如下:

1)乘客视角

如上所示:乘客在手机 App 注册成为用户后,可以选择出发地和目的地,进行打车。

打车请求通过负载均衡服务器,经过请求转发等一系列筛选,然后到达 HTTP 网关集群,再由网关集群进行业务校验,调用相应的微服务。

例如,乘客在手机上获取个人用户信息,收藏的地址信息等,可以将请求转发到用户系统。需要叫车时,将出发地、目的地、个人位置等信息发送至打车系统

2)司机视角

如上图所示:司机在手机 App 注册成为用户并开始接单后,打开手机的位置信息,通过 TCP 长连接定时将自己的位置信息发送给平台,同时也接收平台发布的订单消息。

司机 App 采用 TCP 长连接是因为要定时发送和接收系统消息,若采用 HTTP 推送:

一方面对实时性有影响,另一方面每次通信都得重新建立一次连接会有失体面(耗费资源)。

司机 App:每 3~5 秒向平台发送一次当前的位置信息,包括车辆经纬度,车头朝向等。TCP 服务器集群相当于网关,只是以 TCP 长连接的方式向 App 提供接入服务,地理位置服务负责管理司机的位置信息。

3)订单接收

网关集群充当业务系统的注册中心,负责安全过滤,业务限流,请求转发等工作。

业务由一个个独立部署的网关服务器组成,当请求过多时,可以通过负载均衡服务器将流量压力分散到不同的网关服务器上。

当用户打车时,通过负载均衡服务器将请求打到某一个网关服务器上,网关首先会调用订单系统,为用户创建一个打车订单(订单状态为 “已创建”),并存库。

然后网关服务器调用打车系统,打车系统将用户信息、用户位置、出发地、目的地等数据封装到一个消息包中,发送到消息队列(比如 RabbitMQ),等待系统为用户订单分配司机。

4)订单分配

订单分配系统作为消息队列的消费者,会实时监听队列中的订单。当获取到新的订单消息时,订单分配系统会将订单状态修改为 “订单分配中”,并存库。

然后,订单分配系统将用户信息、用户位置、出发地、目的地等信息发送给订单推送 SDK

接着,订单推送 SDK 调用地理位置系统,获取司机的实时位置,再结合用户的上车点,选择最合适的司机进行派单,然后把订单消息发送到消息告警系统。这时,订单分配系统将订单状态修改为 “司机已接单” 状态。

订单消息通过专门的消息告警系统进行推送,通过 TCP 长连接将订单推送到匹配上的司机手机 App。

5)拒单和抢单

订单推送 SDK 在分配司机时,会考虑司机当前的订单是否完成。当分配到最合适的司机时,司机也可以根据自身情况选择 “拒单”,但是平台会记录下来评估司机的接单效率。

打车平台里,司机如果拒单太多,就可能在后续的一段时间里将分配订单的权重分数降低,影响自身的业绩。

订单分派逻辑也可以修改为允许附加的司机抢单,具体实现为:

当订单创建后,由订单推送 SDK 将订单消息推送到一定的地理位置范围内的司机 App,在范围内的司机接收到订单消息后可以抢单,抢单完成后,订单状态变为“已派单”。

2.3 详细设计

打车平台的详细设计,我们会关注网约车系统的一些核心功能,如:长连接管理、地址算法、体验优化等。

1)长连接的优势

除了网页上常用的 HTTP 短连接请求,比如:百度搜索一下,输入关键词就发起一个 HTTP 请求,这就是最常用的短连接。

但是大型 APP,尤其是涉及到消息推送的应用(如 QQ、微信、美团等应用),几乎都会搭建一套完整的 TCP 长连接通道。

一张图看懂长连接的优势:

图片来源:《美团点评移动网络优化实践》

通过上图,我们得出结论。相比短连接,长连接优势有三:

  1. 连接成功率高
  2. 网络延时低
  3. 收发消息稳定,不易丢失

2)长连接管理

前面说到了长连接的优势是实时性高,收发消息稳定,而打车系统里司机需要定期发送自身的位置信息,并实时接收订单数据,所以司机 App 采用 TCP 长连接的方式来接入系统。

和 HTTP 无状态连接不同的是,TCP 长连接是有状态的连接。所谓无状态,是指每次用户请求可以随意发送到某一台服务器上,且每台服务器的返回相同,用户不关心是哪台服务器处理的请求。

当然,现在 HTTP2.0 也可以是有状态的长连接,我们此处默认是 HTTP1.x 的情况。

而 TCP 长连接为了保证传输效率和实时性,服务器和用户的手机 App 需要保持长连接的状态,即有状态的连接。

所以司机 App 每次信息上报或消息推送时,都会通过一个特定的连接通道,司机 App 接收消息和发送消息的连接通道是固定不变的。

因此,司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,架构图如下:

为了保证每次消息的接收和推送都能找到对应通道,我们需要维护一个司机 App 到 TCP 服务器的映射关系,可以用 Redis 进行保存。

当司机 App 第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,司机 App 会通过用户长连接管理系统重新申请一个服务器连接(可用地址存储在 Zookeeper 中),TCP 连接服务器后再刷新 Redis 的缓存。

3)地址算法

当乘客打车后,订单推送 SDK 会结合司机所在地理位置,结合一个地址算法,计算出最适合的司机进行派单。

目前,手机收集地理位置一般是收集经纬度信息。经度范围是东经 180 到西经 180,纬度范围是南纬 90 到北纬 90。

我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。

根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识司机和乘客的位置信息。再通过 Redis 的 GeoHash 算法,来获取乘客附加的所有司机信息。

GeoHash 算法的原理是将乘客的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有司机

它的实现用到了跳表数据结构,具体实现为:

将某个市区的一块范围作为 GeoHash 的 key,这个市区范围内所有的司机存储到一个跳表中,当乘客的地理位置出现在这个市区范围时,获取该范围内所有的司机信息。然后进一步筛选出最近的司机信息,进行派单。

4)体验优化

1. 距离算法

作为线上派单,通过距离运算来分配订单效果一定会比较差,因为 Redis 计算的是两点之间的空间距离,但司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。

所以,后续需综合行驶距离(而非空间距离)、司机车头朝向以及上车点进行路径规划,来计算区域内每个司机到达乘客的距离和时间。

更进一步,如果区域内有多个乘客和司机,就要考虑所有人的等待时间,以此来优化用户体验,节省派单时间,提升盈利额。

2. 订单优先级

如果打车订单频繁取消,可根据司机或乘客行为进行判责。判责后给乘客和司机计算信誉分,并告知用户信誉分会影响乘客和司机的使用体验,且关联到派单的优先级。

司机接单优先级

综合考虑司机的信誉分,投诉次数,司机的接单数等等,来给不同信誉分的司机分配不同的订单优先级。

乘客派单优先级

根据乘客的打车时间段,打车距离,上车点等信息,做成用户画像,以合理安排司机,或者适当杀熟(bushi。

PS:目前有些不良打车平台就是这么做的 🐶  甚至之前爆出某打车平台,会根据不同的手机系统,进行差异收费。

4. 小结

4.1 网约车平台发展

目前,全球网约车市场已经达到了数千亿美元的规模,主要竞争者包括滴滴、Uber、Grab 等公司。在中国,滴滴作为最大的网约车平台已经占据了绝大部分市场份额。

网约车的核心商业逻辑比较简单,利益关联方主要为平台、司机、车辆、消费者。

平台分别对接司机、车辆【非必选项,有很多司机是带车上岗】和乘客,通过有效供需匹配赚取整个共享经济链省下的钱。

具体表现为:乘客和司机分别通过网约平台打车和接单,平台提供技术支持。乘客为打车服务付费,平台从交易金额中抽成(10%-30%不等)。

据全国网约车监管信息交互平台统计,截至 2023 年 2 月底,全国共有 303 家网约车平台公司取得网约车平台经营许可。

这些平台一部分是依靠高德打车、百度地图、美团打车为代表的网约车聚合平台;另一部分则是以滴滴出行、花小猪、T3 为代表的出行平台

4.2 网约车平台现状

随着出行的解封,网约车平台重现生机。

但由于部分网约车聚合平台的准入门槛太低,所以在过去一段时间里暴露出愈来愈多的问题。如车辆、司机合规率低,遇到安全事故,产生责任纠纷,乘客维权困难等等。

由于其特殊的模式,导致其与网约车运营商存在责任边界问题,一直游离在法律边缘。

但随着网约车聚合平台的监管不断落地,全国各地都出行了一定的监管条例。

比如某打车平台要求车辆将司机和乘客的沟通记录留档,除了司机与乘客的在线沟通记录必须保存以外,还需要一个语音电话或车载录音转换,留存一段时间备查。

有了这些人性化的监管条例和技术的不断创新,网约车平台或许会在未来的一段时间内,继续蓬勃发展。

后话

面试官:嗯,又专又红,全面发展!这小伙子不错,关注了~


作者:xin猿意码
来源:juejin.cn/post/7275211391102746684
收起阅读 »

何时使用Elasticsearch而不是MySql

MySQL 和 Elasticsearch 是两种不同的数据管理系统,它们各有优劣,适用于不同的场景。本文将从以下几个方面对它们进行比较和分析: 数据模型 查询语言 索引和搜索 分布式和高可用 性能和扩展性 使用场景 数据模型 MySQL 是一个关系型数据...
继续阅读 »

MySQL 和 Elasticsearch 是两种不同的数据管理系统,它们各有优劣,适用于不同的场景。本文将从以下几个方面对它们进行比较和分析:



  • 数据模型

  • 查询语言

  • 索引和搜索

  • 分布式和高可用

  • 性能和扩展性

  • 使用场景


数据模型


MySQL 是一个关系型数据库管理系统(RDBMS),它使用表(table)来存储结构化的数据,每个表由多个行(row)和列(column)组成,每个列有一个预定义的数据类型,例如整数、字符串、日期等。MySQL 支持主键、外键、约束、触发器等关系型数据库的特性,以保证数据的完整性和一致性。


Elasticsearch 是一个基于 Lucene 的搜索引擎,它使用文档(document)来存储半结构化或非结构化的数据,每个文档由多个字段(field)组成,每个字段可以有不同的数据类型,例如文本、数字、布尔、数组等。Elasticsearch 支持动态映射(dynamic mapping),可以根据数据自动推断字段的类型和索引方式。


MySQL 和 Elasticsearch 的数据模型有以下几点区别:



  • MySQL 的数据模型是严格的,需要事先定义好表的结构和约束,而 Elasticsearch 的数据模型是灵活的,可以随时添加或修改字段。

  • MySQL 的数据模型是二维的,每个表只有行和列两个维度,而 Elasticsearch 的数据模型是多维的,每个文档可以有嵌套的对象或数组。

  • MySQL 的数据模型是关系型的,可以通过连接(join)多个表来查询相关的数据,而 Elasticsearch 的数据模型是非关系型的,不支持连接操作,需要通过嵌套文档或父子文档来实现关联查询。



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



查询语言


MySQL 使用标准的 SQL 语言来查询和操作数据,SQL 语言是一种声明式的语言,可以通过简洁的语法来表达复杂的逻辑。SQL 语言支持多种查询类型,例如选择(select)、插入(insert)、更新(update)、删除(delete)、聚合(aggregate)、排序(order by)、分组(gr0up by)、过滤(where)、连接(join)等。


Elasticsearch 使用 JSON 格式的查询 DSL(Domain Specific Language)来查询和操作数据,查询 DSL 是一种基于 Lucene 查询语法的语言,可以通过嵌套的 JSON 对象来构建复杂的查询。查询 DSL 支持多种查询类型,例如全文检索(full-text search)、结构化检索(structured search)、地理位置检索(geo search)、度量检索(metric search)等。


MySQL 和 Elasticsearch 的查询语言有以下几点区别:



  • MySQL 的查询语言是通用的,可以用于任何关系型数据库系统,而 Elasticsearch 的查询语言是专用的,只能用于 Elasticsearch 系统。

  • MySQL 的查询语言是字符串形式的,需要拼接或转义特殊字符,而 Elasticsearch 的查询语言是 JSON 形式的,可以直接使用对象或数组表示。

  • MySQL 的查询语言是基于集合论和代数运算的,可以进行集合操作和数学运算,而 Elasticsearch 的查询语言是基于倒排索引和相关度评分的,可以进行全文匹配和相似度计算。


索引和搜索


MySQL 使用 B+树作为主要的索引结构,B+树是一种平衡多路搜索树,它可以有效地存储和检索有序的数据。MySQL 支持主键索引、唯一索引、普通索引、全文索引等多种索引类型,以加速不同类型的查询。MySQL 也支持外部存储引擎,例如 InnoDB、MyISAM、Memory 等,不同的存储引擎有不同的索引和锁机制。


Elasticsearch 使用倒排索引作为主要的索引结构,倒排索引是一种将文档中的词和文档的映射关系存储的数据结构,它可以有效地支持全文检索。Elasticsearch 支持多种分词器(analyzer)和分词过滤器(token filter),以对不同语言和场景的文本进行分词和处理。Elasticsearch 也支持多种搜索类型,例如布尔搜索(boolean search)、短语搜索(phrase search)、模糊搜索(fuzzy search)、通配符搜索(wildcard search)等,以实现不同精度和召回率的检索。


MySQL 和 Elasticsearch 的索引和搜索有以下几点区别:



  • MySQL 的索引是基于数据的值的,可以精确地定位数据的位置,而 Elasticsearch 的索引是基于数据的内容的,可以近似地匹配数据的含义。

  • MySQL 的索引是辅助的,需要手动创建和维护,而 Elasticsearch 的索引是主要的,自动创建和更新。

  • MySQL 的索引是局部的,只针对单个表或列,而 Elasticsearch 的索引是全局的,涵盖所有文档和字段。


分布式和高可用


MySQL 是一个单机数据库系统,它只能运行在一台服务器上,如果服务器出现故障或负载过高,就会影响数据库的可用性和性能。为了解决这个问题,MySQL 提供了多种复制(replication)和集群(cluster)方案,例如主从复制(master-slave replication)、双主复制(master-master replication)、MySQL Cluster、MySQL Fabric 等,以实现数据的冗余和负载均衡。


Elasticsearch 是一个分布式数据库系统,它可以运行在多台服务器上,形成一个集群(cluster)。每个集群由多个节点(node)组成,每个节点可以承担不同的角色,例如主节点(master node)、数据节点(data node)、协调节点(coordinating node)等。每个节点可以存储多个索引(index),每个索引可以划分为多个分片(shard),每个分片可以有多个副本(replica)。Elasticsearch 通过一致性哈希算法(consistent hashing algorithm)来分配分片到不同的节点上,并通过心跳检测(heartbeat check)来监控节点的状态。如果某个节点出现故障或加入集群,Elasticsearch 会自动进行分片的重新分配和平衡。


MySQL 和 Elasticsearch 的分布式和高可用有以下几点区别:



  • MySQL 的分布式和高可用是可选的,需要额外配置和管理,而 Elasticsearch 的分布式和高可用是内置的,无需额外操作。

  • MySQL 的分布式和高可用是基于复制或共享存储的,需要保证数据一致性或可用性之间的权衡,而 Elasticsearch 的分布式和高可用是基于分片和副本的,可以根据需求调整数据冗余度或容错能力。

  • MySQL 的分布式和高可用是静态的,需要手动扩展或缩容集群规模,而 Elasticsearch 的分布式和高可用是动态的,可以自动适应集群变化。


性能和扩展性


MySQL 是一个面向事务(transaction)的数据库系统,它支持 ACID 特性(原子性、一致性、隔离性、持久性),以保证数据操作的正确性和完整性。MySQL 使用锁机制来实现事务隔离级别(isolation level),不同的隔离级别有不同的并发性能和一致性保证。MySQL 也使用缓冲池(buffer pool)来缓存数据和索引,以提高查询效率。MySQL 的性能主要取决于硬件资源、存储引擎、索引设计、查询优化等因素。


Elasticsearch 是一个面向搜索(search)的数据库系统,它支持近实时(near real-time)的索引和查询,以保证数据操作的及时性和灵活性。Elasticsearch 使用分片和副本来实现数据的分布式存储和并行处理,不同的分片数和副本数有不同的写入吞吐量和读取延迟。Elasticsearch 也使用缓存(cache)和内存映射文件(memory-mapped file)来加速数据和索引的访问,以提高搜索效率。Elasticsearch 的性能主要取决于集群规模、分片策略、文档结构、查询复杂度等因素。


MySQL 和 Elasticsearch 的性能和扩展性有以下几点区别:



  • MySQL 的性能和扩展性是有限的,它受到单机资源、锁竞争、复制延迟等因素的限制,而 Elasticsearch 的性能和扩展性是无限的,它可以通过增加节点、分片、副本等方式来水平扩展集群。

  • MySQL 的性能和扩展性是以牺牲搜索能力为代价的,它不能支持复杂的全文检索和相关度评分,而 Elasticsearch 的性能和扩展性是以牺牲事务能力为代价的,它不能保证数据操作的原子性和一致性。

  • MySQL 的性能和扩展性是以提高写入速度为目标的,它优化了数据插入和更新的效率,而 Elasticsearch 的性能和扩展性是以提高读取速度为目标的,它优化了数据检索和分析的效率。


使用场景


MySQL 和 Elasticsearch 适用于不同的使用场景,根据不同的业务需求,可以选择合适的数据库系统或组合使用两者。以下是一些常见的使用场景:



  • 如果需要存储结构化或半结构化的数据,并且需要保证数据操作的正确性和完整性,可以选择 MySQL 作为主要数据库系统。例如,电商网站、社交网络、博客平台等。

  • 如果需要存储非结构化或多样化的数据,并且需要支持复杂的全文检索和相关度评分,可以选择 Elasticsearch 作为主要数据库系统。例如搜索引擎、日志分析、推荐系统等。

  • 如果需要存储和分析大量的时序数据,并且需要支持实时的聚合和可视化,可以选择Elasticsearch作为主要数据库系统。例如,物联网、监控系统、金融市场等。

  • 如果需要同时满足上述两种需求,并且可以容忍一定程度的数据不一致或延迟,可以将 MySQL 作为主数据库系统,并将部分数据同步到 Elasticsearch 作为辅助数据库系统。例如新闻网站、电影网站、招聘网站等。


自此本文讲解内容到此结束,感谢您的阅读,希望本文对您有所帮助。


作者:程序员wayn
来源:juejin.cn/post/7264528507932327948
收起阅读 »

Easy-Es:像mybatis-plus一样,轻松操作ES

0. 引言 es的java客户端不太友好的语法一直饱受诟病,书写一个查询语句可能需要书写一大串的代码,如果能像mybatis--plus一样,支持比较灵活方便的语句生成器那就好了。 于是为elasticsearch而生的ORM框架Easy-Es诞生了,使用及其...
继续阅读 »

0. 引言


es的java客户端不太友好的语法一直饱受诟病,书写一个查询语句可能需要书写一大串的代码,如果能像mybatis--plus一样,支持比较灵活方便的语句生成器那就好了。


于是为elasticsearch而生的ORM框架Easy-Es诞生了,使用及其方便快捷,今天我们就一起来学习easy-es,对比看看原生java-client方便之处在哪儿。


1. Easy-Es简介


Easy-Es是以elasticsearch官方提供的RestHighLevelClient为基础,而开发的一款针对es的ORM框架,类似于es版的mybatis-plus,可以让开发者无需掌握es复杂的DSL语句,只要会mysql语法即可使用es,快速实现es客户端语法


官方文档:http://www.easy-es.cn/
在这里插入图片描述


2. Easy-Es使用


1、引入依赖


<!-- 引入easy-es最新版本的依赖-->
<dependency>
<groupId>org.dromara.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>2.0.0-beta3</version>
</dependency>

<!-- 排除springboot中内置的es依赖,以防和easy-es中的依赖冲突-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.14.0</version>
</dependency>

2、添加配置项,这里只配置了几个基本的配置项,更多配置可参考官网文档:easy-es 配置介绍


easy-es: 
# es地址、账号密码
address: 192.168.244.11:9200
username: elastic
password: elastic

3、在启动类中添加es mapper文件的扫描路径


@EsMapperScan("com.example.easyesdemo.mapper")

在这里插入图片描述


4、创建实体类,通过@IndexName注解申明索引名称及分片数, @IndexField注解申明字段名、数据类型、分词器等,更多介绍参考官方文档:essy-es 注解介绍


@IndexName(value = "user_easy_es")
@Data
public class UserEasyEs {

@IndexId(type = IdType.CUSTOMIZE)
private Long id;

private String name;

private Integer age;

private Integer sex;

@IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART, searchAnalyzer = Analyzer.IK_SMART)
private String address;

@IndexField(fieldType = FieldType.DATE, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private Date createTime;

private String createUser;

}

5、创建mapper类,继承BaseEsMapper类,注意这里的mapper一定要创建到第3步中设置的mapper扫描路径下com.example.easyesdemo.mapper


public interface UserEsMapper extends BaseEsMapper<UserEasyEs> {
}

6、创建controller,书写创建索引、新增、修改、查询的接口


@RestController
@RequestMapping("es")
@AllArgsConstructor
public class UserEsController {

private final UserEsMapper userEsMapper;

/**
* 创建索引
* @return
*/

@GetMapping("create")
public Boolean createIndex(){
return userEsMapper.createIndex();
}

@GetMapping("save")
public Integer save(Long id){
UserEasyEs user = new UserEasyEs();
user.setId(id);
user.setName("用户"+id);
user.setAddress("江苏省无锡市滨湖区");
user.setAge(30);
user.setSex(1);
user.setCreateUser("admin");
user.setCreateTime(new Date());
Long count = userEsMapper.selectCount(EsWrappers.lambdaQuery(UserEasyEs.class).eq(UserEasyEs::getId, id));
if(count > 0){
return userEsMapper.updateById(user);
}else{
return userEsMapper.insert(user);
}
}

@GetMapping("search")
public List<UserEasyEs> search(String name, String address){
List<UserEasyEs> userEasyEs = userEsMapper.selectList(
EsWrappers.lambdaQuery(UserEasyEs.class)
.eq(UserEasyEs::getName, name)
.match(UserEasyEs::getAddress, address)
);
return userEasyEs;
}

}

7、分别调用几个接口



  • 创建索引
    在这里插入图片描述
    kibana中查询索引,发现创建成功
    在这里插入图片描述

  • 新增接口
    这里新增了4笔
    在这里插入图片描述
    数据新增成功
    在这里插入图片描述

  • 数据查询


在这里插入图片描述
如上便是针对easy-es的简单使用,这里的用法都与mp类似,上手相当简单,不用再写那些复杂的DSL语句了


3. 拓展介绍



  • 条件构造器



上述演示,我们构造查询条件时,使用了EsWrappers来构造条件,用法与mp及其类型,大家根据提示就可以推导出方法如何书写,更详细的使用说明可以查看官方文档:easy-es 条件构造器介绍




  • 索引托管



如果想要自动根据创建的es实体类来创建对应的索引,那么只需要调整索引的托管模式为非手动模式即可,因为这里我不需要自动同步数据,所以选择非平滑模式



easy-es:
global-config:
process_index_mode: not_smoothly


其中三种模式的区别为:
平滑模式:smoothly,索引的创建、数据更新迁移等都由easy-es自动完成
非平滑模式:not_smoothly,索引自动创建,但不会自动迁移数据
手动模式:manual,全部操作由用户手动完成,默认模式




  • 数据同步



如果数据源是来自mysql, 那么建议使用canal来进行同步,canal的使用可在我主页搜索。
其次还有DataX, Logstash等同步工具,当然你也可以使用easy-es提供的CRUD接口,来手动同步数据




  • 日志打印



通过开启日志,可以在控制台打印执行的DSL语句,更加方便我们在开发阶段进行问题排查



logging:
level:
tracer: trace # 开启trace级别日志,在开发时可以开启此配置,则控制台可以打印es全部请求信息及DSL语句,为了避免重复,开启此项配置后,可以将EE的print-dsl设置为false.


  • 聚合查询
    easy-es实现的聚合查询,只要是针对gr0up by这类聚合,也就是es中的Terms aggregation,以及最大值、最小值、平均值、求和,而对于其他类型的聚合,还在不断更新中,但这里大家也需要了解,es的聚合和mysql的聚合完全是不一样的维度和复杂度,es支持非常多的聚合查询,所以其他类型的实现还需要借助RestHighLevelClient来实现


我们利用easy-es来实现下之前书写的聚合案例
在这里插入图片描述


@RestController
@AllArgsConstructor
@RequestMapping("order")
public class OrderEsController {

private final OrderTestEsMapper orderEsMapper;

@GetMapping("search")
public String search(){
SearchResponse search = orderEsMapper.search(EsWrappers.lambdaQuery(OrderTest.class).groupBy(OrderTest::getStatus));

// 包装查询结果
Aggregations aggregations = search.getAggregations();
Terms terms = (Terms)aggregations.asList().get(0);
List<? extends Terms.Bucket> buckets = terms.getBuckets();
HashMap<String,Long> statusRes = new HashMap<>();
buckets.forEach(bucket -> {
statusRes.put(bucket.getKeyAsString(),bucket.getDocCount());
});
System.out.println("---聚合结果---");
System.out.println(statusRes);
return statusRes.toString();
}
}

可以看到实际上的查询语句就一行,而其他的都是对返回结果的封装,因为es本身返回的数据是封装到嵌套的对象中的,所以我们需要对其进行包装


对比原始的查询语句,其易用性上的提升还是很明显的
在这里插入图片描述


4. 总结


至此对easy-es的介绍就结束了,可以看到如果是针对es实现CRUD上,easy-es表现出非常好的便捷性,而在复杂的聚合查询中,仍然还有进步空间,目前还需要借助RestHighLevelClient,但easy-es的出现,为未来提供更好用的ES ORM框架,提供了希望和方向


文中演示代码见:gitee.com/wuhanxue/wu…


作者:wu55555
来源:juejin.cn/post/7271896547594682428
收起阅读 »

研发都认为DBA很Low?我反手一个嘴巴子

前言我用十多年的DBA经验告诉你,如果你作为研发觉得DBA很Low,你是会吃苦头的“你以为DBA就是安装一下数据库,管理一下数据库?你丢个SQL给DBA优化下?你的日志爆满了DBA给你清理一下?DBA帮你安装下中间件?你以为的DBA只是做这些事?”秉持着和平交...
继续阅读 »

前言

我用十多年的DBA经验告诉你,如果你作为研发觉得DBA很Low,你是会吃苦头的

“你以为DBA就是安装一下数据库,管理一下数据库?你丢个SQL给DBA优化下?你的日志爆满了DBA给你清理一下?DBA帮你安装下中间件?你以为的DBA只是做这些事?”

秉持着和平交流的学习态度,我这里精选了几位高赞粉丝的精彩回答

1.救火能力

1.1 调优

IT界并没有一个通行的 ”拳头“ 来判断谁low,谁更low。有时候,研发写的程序,新功能发布后,就出现磁盘IO出现瓶颈了、或者CPU飙高到100%了,但是这个时候,只是表象,只知道Linux机器的资源耗尽了,DBA得先找到资源消耗在哪了,才能进一步分析原因,用数据说话是应用的问题,才能责令程序员整改。

SQL调优是一个复杂的过程,涉及多个方面,包括但不限于SQL语句的编写、索引的使用、表的连接策略、数据库的统计信息、系统资源的利用等。调优的难度取决于多个因素,包括查询的复杂性、数据量、硬件资源、数据库的工作负载和现有的优化策略。

在这里给大家分享一个执行计划变,1个SQL把系统干崩的情景,由于业务用户检索数据范围过大,导致执行计划谓词越界,通过矫正执行计划及开启操作系统大页,服务器DB一直存在的CPU高负载从75%降低到25%!

生产问题,瞬息万变,DBA要同时熟悉业务,并对硬件、网络要精通,要在这样的复杂情况下作出正确的决策,这一点我想难度不小吧。

1.2 高可用

数据库高可用是指DB集群中任何一个节点的故障都不会影响用户的使用,连接到故障节点的用户会被自动转移到健康节点,从用户感受而言, 是感觉不到这种切换。

那么DBA在高可用的配置方面,下面就是某制造业大厂,应用层的链接方式

--jdbc应用端的连接
jdbc:oracle:thin:@(DESCRIPTION =
(ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP)(HOST = rac1-vip)
(PORT = 1521))(ADDRESS = (PROTOCOL = TCP)(HOST = rac2-vip)
(PORT = 1521))(LOAD_BALANCE = no)(FAILOVER = yes))
(CONNECT_DATA =(SERVER = DEDICATED)(SERVICE_NAME = dbserver)))

那么这种配置FAILOVER = yes,Net会从多个地址中按顺序选择一个地址进行连接,直到连接成功为止,那么就会保证数据库单节点故障,自动的切换,高可用是故障发生的第一个救命的稻草,系统上线前一定要测试好,才能确保数据库的高可用,这期间DBA功不可没!

还有客户要求选择的一套国产数据库支持核心业务,那么作为DBA在选型及业务适配上就发挥作用了,跟研发确认发现应用是兼容PG的,而且客户要求要同时兼容OLAT和OLTP业务,看下以下这套openGauss国产数据库的高可用架构。

1.openGauss高可用:CM
通过配置VIP故障转移,OLTP连接VIP,进行事物交易
同时支持动态配置CM集群故障切换策略和数据库集群脑裂故障恢复策略,
从而能够尽可能确保集群数据的完整性和一致性。

2.写重定向,报表分析业务连接,支持读写分离
主备节点开启控制参数 enable_remote_execute=on之后
通过备库发起的写操作,会重定向到主库执行

2.监控能力

这方面我是最有发言权了,SA一直是我的本职工作,从机房硬件部署、弱电以及数据库的安装实施,很多东西需要依赖于DBA来做,全力保障应用的稳定性,而且监控到的指标随时可以推送到邮件以及微信。这期间我也发现了很多天窗,原来还可以这么干?

2.1 服务器监控

首先监控Linux服务嘛,那肯定是要全方位系统的监控,网络、磁盘、CPU、内存等等,这才叫监控,那么其实给大家推荐一款免费的监控工作

Prometheus提供了从指标暴露,到指标抓取、存储和可视化,以及最后的监控告警等组件。

数据库监控

Zabbix聚焦于帮助用户通过性能优化和功能升级来快速响应业务需求,从而满足客户的高期望值,并提升IT运维人员的生产力。在可扩展性与性能、稳定性与高可用、可观测性几个领域获得持续提升。监控做不好,救火救到老!拿下Zabbix,现在!立刻!马上!!

1.监控Oracle
博客地址:
https://jeames.blog.csdn.net/article/details/126825934
2.监控PostgreSQL
博客地址:
https://jeames.blog.csdn.net/article/details/120300581
3.监控MySQL
博客地址:
https://jeames.blog.csdn.net/article/details/126825934

3 数据源赋能者

从AI、智能化到云迁移和安全性,业务和技术趋势不断重塑DBA在组织中的角色.DBA 群体站在时代的岔路口,国产数据库太多了应该怎么选?DBA 会被云上数据库抛弃吗?应该如何应对新时代挑战?职业终点在哪里?

1.云数据库解决方案
DBA要善于利用云原生保障数据安全和优化成本

2.数据安全与合规
随着数据保护法律的出台、日益严峻的网络攻击,
DBA必须掌握加密、访问控制和审计等技能

3.灾难恢复和业务连续性
随着企业愈加依赖数据的连续性,
快速恢复丢失数据并最大限度地减少停机时间至关重要

4.自动化和脚本编写
自动化和脚本编写对于DBA管理重复性任务和提高效率尤为关键

5.有效的沟通和协作
有效的沟通和协作仍然是DBA的重要技能。
能够向同事清楚地传达技术信息、与跨职能团队合作,
打破IT部门和业务部门之间的信息差,确保数据库的策略与组织目标保持一致。

4.总结

在一个公司写了屎山代码的研发,可以拍拍屁股走人,然后继续去下一个企业再写个屎山。反正不会追着代码跨省找你。而一个搞崩了系统的DBA,这个闯祸经历将成为他的黑历史,并影响到他未来的就业.因为需要专业DBA的好企业,基本都是几百台服务器起步的大项目,难免不会查背景,这就导致DBA如果想干得好,圈子会越来越小,请记住是干得好,不是混得好,混是会出事的。

好了,以上就是我对DBA的理解了,有不足之处还望指正。


作者:IT邦德
来源:juejin.cn/post/7386505099848646710
收起阅读 »

MySQL 高级(进阶)SQL 语句

MySQL 高级(进阶)SQL 语句 1. MySQL SQL 语句 1.1 常用查询 常用查询简单来说就是 增、删、改、查 对 MySQL 数据库的查询,除了基本的查询外,有时候需要对查询的结果集进行处理。 例如只取 10 条数据、对查询结果进行排序或分组等...
继续阅读 »

MySQL 高级(进阶)SQL 语句


1. MySQL SQL 语句


1.1 常用查询


常用查询简单来说就是 增、删、改、查


对 MySQL 数据库的查询,除了基本的查询外,有时候需要对查询的结果集进行处理。 例如只取 10 条数据、对查询结果进行排序或分组等等


1、按关键字排序
PS:类比于windows 任务管理器
使用 SELECT 语句可以将需要的数据从 MySQL 数据库中查询出来,如果对查询的结果进行排序,可以使用 ORDER BY 语句来对语句实现排序,并最终将排序后的结果返回给用户。这个语句的排序不光可以针对某一个字段,也可以针对多个字段


(1)语法
SELECT column1, column2, … FROM table_name ORDER BY column1, column2, …


ASC|DESC
ASC 是按照升序进行排序的,是默认的排序方式,即 ASC 可以省略。SELECT 语句中如果没有指定具体的排序方式,则默认按 ASC方式进行排序。


DESC 是按降序方式进 行排列。当然 ORDER BY 前面也可以使用 WHERE 子句对查询结果进一步过滤。


准备工作:


create database k1;
use k1;
create table location (Region char(20),Store_Name char(20));
insert int0 location values('East','Boston');
insert int0 location values('East','New York');
insert int0 location values('West','Los Angeles');
insert int0 location values('West','Houston');

create table store_info (Store_Name char(20),Sales int(10),Date char(10));
insert int0 store_info values('Los Angeles','1500','2020-12-05');
insert int0 store_info values('Houston','250','2020-12-07');
insert int0 store_info values('Los Angeles','300','2020-12-08');
insert int0 store_info values('Boston','700','2020-12-08');

1.2 SELECT


显示表格中一个或数个字段的所有数据记录 语法:SELECT "字段" FROM "表名";


SELECT Store_Name FROM location;

d1.png


SELECT Store_Name FROM Store_Info;

d2.png


1.3 DISTINCT


不显示重复的数据记录


语法:SELECT DISTINCT "字段" FROM "表名";


SELECT DISTINCT Store_Name FROM Store_Info;

d3.png


1.4 AND OR


且 或


语法:SELECT "字段" FROM "表名" WHERE "条件1" {[AND|OR] "条件2"}+ ;


d4.png


1.5 in


显示已知的值的数据记录


语法:SELECT "字段" FROM "表名" WHERE "字段" IN ('值1', '值2', ...);


SELECT * FROM store_info WHERE Store_Name IN ('Los Angeles', 'Houston');

b5.png


1.6 BETWEEN


显示两个值范围内的数据记录


语法:SELECT "字段" FROM "表名" WHERE "字段" BETWEEN '值1' AND '值2';


d6.png


2. 通配符 —— 通常与 LIKE 搭配 一起使用



% :百分号表示零个、一个或多个字符


_ :下划线表示单个字符


'A_Z':所有以 'A' 起头,另一个任何值的字符,且以 'Z' 为结尾的字符串。例如,'ABZ' 和 'A2Z' 都符合这一个模式,而 'AKKZ' 并不符合 (因为在 A 和 Z 之间有两个字符,而不是一个字符)。


'ABC%': 所有以 'ABC' 起头的字符串。例如,'ABCD' 和 'ABCABC' 都符合这个模式。 '%XYZ': 所有以 'XYZ' 结尾的字符串。例如,'WXYZ' 和 'ZZXYZ' 都符合这个模式。


'%AN%': 所有含有 'AN'这个模式的字符串。例如,'LOS ANGELES' 和 'SAN FRANCISCO' 都符合这个模式。


'_AN%':所有第二个字母为 'A' 和第三个字母为 'N' 的字符串。例如,'SAN FRANCISCO' 符合这个模式,而 'LOS ANGELES' 则不符合这个模式。



2.1 LIKE


匹配一个模式来找出我们要的数据记录


语法:SELECT "字段" FROM "表名" WHERE "字段" LIKE {模式};


SELECT * FROM store_info WHERE Store_Name like '%os%';

d7.png


2.2 ORDER BY


按关键字排序


语法:SELECT "字段" FROM "表名" [WHERE "条件"] ORDER BY "字段" [ASC, DESC];


注意ASC 是按照升序进行排序的,是默认的排序方式。 DESC 是按降序方式进行排序


SELECT Store_Name,Sales,Date FROM store_info ORDER BY Sales DESC;

d8.png


3. 函数


3.1数学函数


abs(x)返回 x 的绝对值
rand()返回 0 到 1 的随机数
mod(x,y)返回 x 除以 y 以后的余数
power(x,y)返回 x 的 y 次方
round(x)返回离 x 最近的整数
round(x,y)保留 x 的 y 位小数四舍五入后的值
sqrt(x)返回 x 的平方根
truncate(x,y)返回数字 x 截断为 y 位小数的值
ceil(x)返回大于或等于 x 的最小整数
floor(x)返回小于或等于 x 的最大整数
greatest(x1,x2...)返回集合中最大的值,也可以返回多个字段的最大的值
least(x1,x2...)返回集合中最小的值,也可以返回多个字段的最小的值

SELECT abs(-1), rand(), mod(5,3), power(2,3), round(1.89);
SELECT round(1.8937,3), truncate(1.235,2), ceil(5.2), floor(2.1), least(1.89,3,6.1,2.1);

d9.png


3.2 聚合函数


avg()返回指定列的平均值
count()返回指定列中非 NULL 值的个数
min()返回指定列的最小值
max()返回指定列的最大值
sum(x)返回指定列的所有值之和

SELECT avg(Sales) FROM store_info;

SELECT count(Store_Name) FROM store_info;
SELECT count(DISTINCT Store_Name) FROM store_info;

SELECT max(Sales) FROM store_info;
SELECT min(Sales) FROM store_info;

SELECT sum(Sales) FROM store_info;

d10.png


d11.png


3.3 字符串函数


trim()返回去除指定格式的值
concat(x,y)将提供的参数 x 和 y 拼接成一个字符串
substr(x,y)获取从字符串 x 中的第 y 个位置开始的字符串,跟substring()函数作用相同
substr(x,y,z)获取从字符串 x 中的第 y 个位置开始长度为 z 的字符串
length(x)返回字符串 x 的长度
replace(x,y,z)将字符串 z 替代字符串 x 中的字符串 y
upper(x)将字符串 x 的所有字母变成大写字母
lower(x)将字符串 x 的所有字母变成小写字母
left(x,y)返回字符串 x 的前 y 个字符
right(x,y)返回字符串 x 的后 y 个字符
repeat(x,y)将字符串 x 重复 y 次
space(x)返回 x 个空格
strcmp(x,y)比较 x 和 y,返回的值可以为-1,0,1
reverse(x)将字符串 x 反转

d12.png


如 sql_mode 开启了 PIPES_AS_CONCAT,"||" 视为字符串的连接操作符而非或运算符,和字符串的拼接函数Concat相类似,这和Oracle数据库使用方法一样的


SELECT Region || ' ' || Store_Name FROM location WHERE Store_Name = 'Boston';
SELECT substr(Store_Name,3) FROM location WHERE Store_Name = 'Los Angeles';
SELECT substr(Store_Name,2,4) FROM location WHERE Store_Name = 'New York'

d13.png


SELECT TRIM ([ [位置] [要移除的字符串] FROM ] 字符串);


**[位置]:的值可以为 LEADING (起头), TRAILING (结尾), BOTH (起头及结尾)。 **


[要移除的字符串]:从字串的起头、结尾,或起头及结尾移除的字符串。缺省时为空格。


SELECT TRIM(LEADING 'Ne' FROM 'New York');

SELECT Region,length(Store_Name) FROM location;

SELECT REPLACE(Region,'ast','astern')FROM location;

d14.png


4. GR0UP BY


对GR0UP BY后面的字段的查询结果进行汇总分组,通常是结合聚合函数一起使用的


GR0UP BY 有一个原则



  • 凡是在 GR0UP BY 后面出现的字段,必须在 SELECT 后面出现;

  • 凡是在 SELECT 后面出现的、且未在聚合函数中出现的字段,必须出现在 GR0UP BY 后面


语法:SELECT "字段1", SUM("字段2") FROM "表名" GR0UP BY "字段1";


SELECT Store_Name, SUM(Sales) FROM store_info GR0UP BY Store_Name ORDER BY sales desc;

d15.png


5. 别名


字段別名 表格別名


语法:SELECT "表格別名"."字段1" [AS] "字段別名" FROM "表格名" [AS] "表格別名";


SELECT A.Store_Name Store, SUM(A.Sales) "Total Sales" FROM store_info A GR0UP BY A.Store_Name;

d16.png


6. 子查询


子查询也被称作内查询或者嵌套查询,是指在一个查询语句里面还嵌套着另一个查询语 句。子查询语句是先于主查询语句被执行的,其结果作为外层的条件返回给主查询进行下一 步的查询过滤


连接表格,在WHERE 子句或 HAVING 子句中插入另一个 SQL 语句


语法:SELECT "字段1" FROM "表格1" WHERE "字段2" [比较运算符] #外查询 (SELECT "字段1" FROM "表格2" WHERE "条件"); #内查询


[比较运算符]


可以是符号的运算符,例如 =、>、<、>=、<= ;也可以是文字的运算符,例如 LIKE、IN、BETWEEN


SELECT SUM(Sales) FROM store_info WHERE Store_Name IN
(SELECT Store_Name FROM location WHERE Region = 'West');

SELECT SUM(A.Sales) FROM store_info A WHERE A.Store_Name IN
(SELECT Store_Name FROM location B WHERE B.Store_Name = A.Store_Name);

d17.png


7. EXISTS


用来测试内查询有没有产生任何结果,类似布尔值是否为真 #如果有的话,系统就会执行外查询中的SQL语句。若是没有的话,那整个 SQL 语句就不会产生任何结果。


语法:SELECT "字段1" FROM "表格1" WHERE EXISTS (SELECT \* FROM "表格2" WHERE "条件");


SELECT SUM(Sales) FROM store_info WHERE EXISTS (SELECT * FROM location WHERE Region = 'West');

d18.png


8. 连接查询


准备工作


create database k1;
use k1;
create table location (Region char(20),Store_Name char(20));
insert int0 location values('East','Boston');
insert int0 location values('East','New York');
insert int0 location values('West','Los Angeles');
insert int0 location values('West','Houston');

create table store_info (Store_Name char(20),Sales int(10),Date char(10));
insert int0 store_info values('Los Angeles','1500','2020-12-05');
insert int0 store_info values('Houston','250','2020-12-07');
insert int0 store_info values('Los Angeles','300','2020-12-08');
insert int0 store_info values('Boston','700','2020-12-08');

f1.png


UPDATE store_info SET store_name='Washington' WHERE sales=300;

f2.png


inner join(内连接):只返回两个表中联结字段相等的行


left join(左连接):返回包括左表中的所有记录和右表中联结字段相等的记录


right join(右连接):返回包括右表中的所有记录和左表中联结字段相等的记录


f3.png


8.1 内连接


MySQL 中的内连接就是两张或多张表中同时符合某种条件的数据记录的组合。通常在 FROM 子句中使用关键字 INNER JOIN 来连接多张表,并使用 ON 子句设置连接条件,内连接是系统默认的表连接,所以在 FROM 子句后可以省略 INNER 关键字,只使用 关键字 JOIN。同时有多个表时,也可以连续使用 INNER JOIN 来实现多表的内连接,不过为了更好的性能,建议最好不要超过三个表


(1) 语法 求交集


SELECT column_name(s)FROM table1 INNER JOIN table2 ON table1.column_name = table2.column_name;

SELECT * FROM location A INNER JOIN store_info B on A.Store_Name = B.Store_Name ;

f4.png


内连查询:通过inner join 的方式将两张表指定的相同字段的记录行输出出来


8.2 左连接


左连接也可以被称为左外连接,在 FROM 子句中使用 LEFT JOIN 或者 LEFT OUTER JOIN 关键字来表示。左连接以左侧表为基础表,接收左表的所有行,并用这些行与右侧参 考表中的记录进行匹配,也就是说匹配左表中的所有行以及右表中符合条件的行。


SELECT * FROM location A LEFT JOIN store_info B on A.Store_Name = B.Store_Name ;

f5.png


左连接中左表的记录将会全部表示出来,而右表只会显示符合搜索条件的记录,右表记录不足的地方均为 NULL


8.3 右连接


右连接也被称为右外连接,在 FROM 子句中使用 RIGHT JOIN 或者 RIGHT OUTER JOIN 关键字来表示。右连接跟左连接正好相反,它是以右表为基础表,用于接收右表中的所有行,并用这些记录与左表中的行进行匹配


SELECT * FROM location A RIGHT JOIN store_info B on A.Store_Name = B.Store_Name ;

f6.png


9. UNION ----联集


将两个SQL语句的结果合并起来,两个SQL语句所产生的字段需要是同样的数据记录种类


UNION :生成结果的数据记录值将没有重复,且按照字段的顺序进行排序


语法:[SELECT 语句 1] UNION [SELECT 语句 2];


SELECT Store_Name FROM location UNION SELECT Store_Name FROM store_info;

f7.png


UNION ALL :将生成结果的数据记录值都列出来,无论有无重复


语法:[SELECT 语句 1] UNION ALL [SELECT 语句 2];


SELECT Store_Name FROM location UNION ALL SELECT Store_Name FROM store_info;

f8.png


9.1 交集值


取两个SQL语句结果的交集


SELECT A.Store_Name FROM location A INNER JOIN store_info B ON A.Store_Name = B.Store_Name;

SELECT A.Store_Name FROM location A INNER JOIN store_info B USING(Store_Name);

f9.png


取两个SQL语句结果的交集,且没有重复


SELECT DISTINCT A.Store_Name FROM location A INNER JOIN store_info B USING(Store_Name);

SELECT DISTINCT Store_Name FROM location WHERE (Store_Name) IN (SELECT Store_Name FROM store_info);

SELECT DISTINCT A.Store_Name FROM location A LEFT JOIN store_info B USING(Store_Name) WHERE B.Store_Name IS NOT NULL;

SELECT A.Store_Name FROM (SELECT B.Store_Name FROM location B INNER JOIN store_info C ON B.Store_Name = C.Store_Name) A
GR0UP BY A.Store_Name;

SELECT A.Store_Name FROM
(SELECT DISTINCT Store_Name FROM location UNION ALL SELECT DISTINCT Store_Name FROM store_info) A
GR0UP BY A.Store_Name HAVING COUNT(*) > 1;

f10.png


f11.png


9.2 无交集值


显示第一个SQL语句的结果,且与第二个SQL语句没有交集的结果,且没有重复


SELECT DISTINCT Store_Name FROM location WHERE (Store_Name) NOT IN (SELECT Store_Name FROM store_info);

SELECT DISTINCT A.Store_Name FROM location A LEFT JOIN store_info B USING(Store_Name) WHERE B.Store_Name IS NULL;

SELECT A.Store_Name FROM
(SELECT DISTINCT Store_Name FROM location UNION ALL SELECT DISTINCT Store_Name FROM store_info) A
GR0UP BY A.Store_Name HAVING COUNT(*) = 1;

f12.png


10. case


是 SQL 用来做为 IF-THEN-ELSE 之类逻辑的关键字


语法:


SELECT CASE ("字段名")
WHEN "条件1" THEN "结果1"
WHEN "条件2" THEN "结果2"
...
[ELSE "结果N"]
END
FROM "表名";

"条件" 可以是一个数值或是公式。 ELSE 子句则并不是必须的。


SELECT Store_Name, CASE Store_Name 
WHEN 'Los Angeles' THEN Sales * 2
WHEN 'Boston' THEN 2000
ELSE Sales
END
"New Sales",Date
FROM store_info;

#"New Sales" 是用于 CASE 那个字段的字段名。

f13.png


11. 正则表达式


匹配模式描述实例
^匹配文本的结束字符‘^bd’ 匹配以 bd 开头的字符串
$匹配文本的结束字符‘qn$’ 匹配以 qn 结尾的字符串
.匹配任何单个字符‘s.t’ 匹配任何 s 和 t 之间有一个字符的字符串
*匹配零个或多个在它前面的字符‘fo*t’ 匹配 t 前面有任意个 o
+匹配前面的字符 1 次或多次‘hom+’ 匹配以 ho 开头,后面至少一个m 的字符串
字符串匹配包含指定的字符串‘clo’ 匹配含有 clo 的字符串
p1|p2匹配 p1 或 p2‘bg|fg’ 匹配 bg 或者 fg
[...]匹配字符集合中的任意一个字符‘[abc]’ 匹配 a 或者 b 或者 c
[^...]匹配不在括号中的任何字符‘[^ab]’ 匹配不包含 a 或者 b 的字符串
{n}匹配前面的字符串 n 次‘g{2}’ 匹配含有 2 个 g 的字符串
{n,m}匹配前面的字符串至少 n 次,至多m 次‘f{1,3}’ 匹配 f 最少 1 次,最多 3 次

语法:SELECT "字段" FROM "表名" WHERE "字段" REGEXP {模式};


SELECT * FROM store_info WHERE Store_Name REGEXP 'os';
SELECT * FROM store_info WHERE Store_Name REGEXP '^[A-G]';
SELECT * FROM store_info WHERE Store_Name REGEXP 'Ho|Bo';

f14.png


12. 存储过程


存储过程是一组为了完成特定功能的SQL语句集合。


存储过程在使用过程中是将常用或者复杂的工作预先使用SQL语句写好并用一个指定的名称存储起来,这个过程经编译和优化后存储在数据库服务器中。当需要使用该存储过程时,只需要调用它即可。存储过程在执行上比传统SQL速度更快、执行效率更高。


存储过程的优点


1、执行一次后,会将生成的二进制代码驻留缓冲区,提高执行效率


2、SQL语句加上控制语句的集合,灵活性高


3、在服务器端存储,客户端调用时,降低网络负载


4、可多次重复被调用,可随时修改,不影响客户端调用


5、可完成所有的数据库操作,也可控制数据库的信息访问权限


12.1 创建存储过程


DELIMITER $$							#将语句的结束符号从分号;临时改为两个$$(可以是自定义)
CREATE PROCEDURE Proc() #创建存储过程,过程名为Proc,不带参数
-> BEGIN #过程体以关键字 BEGIN 开始
-> select * from Store_Info; #过程体语句
-> END $$ #过程体以关键字 END 结束
DELIMITER ; #将语句的结束符号恢复为分号

实例


DELIMITER $$							#将语句的结束符号从分号;临时改为两个$$(可以自定义)
CREATE PROCEDURE Proc5() #创建存储过程,过程名为Proc5,不带参数
-> BEGIN #过程体以关键字 BEGIN 开始
-> create table user (id int (10), name char(10),score int (10));
-> insert int0 user values (1, 'cyw',70);
-> select * from cyw; #过程体语句
-> END $$ #过程体以关键字 END 结束
DELIMITER ; #将语句的结束符号恢复为分号

f15.png


12.2 调用存储过程


CALL Proc;

f16.png


12.3 查看存储过程


SHOW CREATE PROCEDURE [数据库.]存储过程名; #查看某个存储过程的具体信息


SHOW CREATE PROCEDURE Proc;

SHOW PROCEDURE STATUS [LIKE '%Proc%'] \G

f17.png


12.4 存储过程的参数


**IN 输入参数:**表示调用者向过程传入值(传入值可以是字面量或变量)


**OUT 输出参数:**表示过程向调用者传出值(可以返回多个值)(传出值只能是变量)


**INOUT 输入输出参数:**既表示调用者向过程传入值,又表示过程向调用者传出值(值只能是变量)


DELIMITER $$				
CREATE PROCEDURE Proc6(IN inname CHAR(16))
-> BEGIN
-> SELECT * FROM store_info WHERE Store_Name = inname;
-> END $$
DELIMITER ;

CALL Proc6('Boston');

f18.png


12.5 修改存储过程


ALTER PROCEDURE <过程名>[<特征>... ]
ALTER PROCEDURE GetRole MODIFIES SQL DATA SQL SECURITY INVOKER;
MODIFIES sQLDATA:表明子程序包含写数据的语句
SECURITY:安全等级
invoker:当定义为INVOKER时,只要执行者有执行权限,就可以成功执行。

12.6 删除存储过程


存储过程内容的修改方法是通过删除原有存储过程,之后再以相同的名称创建新的存储过程。如果要修改存储过程的名称,可以先删除原存储过程,再以不同的命名创建新的存储过程。


DROP PROCEDURE IF EXISTS Proc;		
#仅当存在时删除,不添加 IF EXISTS 时,如果指定的过程不存在,则产生一个错误

f19.png


13. 条件语句


if-then-else ···· end if


mysql> delimiter $$
mysql>
mysql> CREATE PROCEDURE proc8(IN pro int)
->
-> begin
->
-> declare var int;
-> set var=pro*2;
-> if var>=10 then
-> update t set id=id+1;
-> else
-> update t set id=id-1;
-> end if;
-> end $$

mysql> delimiter ;

f20.png


f21.png


14. 循环语句


while ···· end while


mysql> delimiter $$
mysql>
mysql> create procedure proc9()
-> begin
-> declare var int(10);
-> set var=0;
-> while var<6 do
-> insert int0 t values(var);
-> set var=var+1;
-> end while;
-> end $$

mysql> delimiter ;

f22.png


15. 视图表 create view


15.1 视图表概述


视图,可以被当作是虚拟表或存储查询。


视图跟表格的不同是,表格中有实际储存数据记录,而视图是建立在表格之上的一个架构,它本身并不实际储存数据记录。


临时表在用户退出或同数据库的连接断开后就自动消失了,而视图不会消失。
视图不含有数据,只存储它的定义,它的用途一般可以简化复杂的查询。


比如你要对几个表进行连接查询,而且还要进行统计排序等操作,写sql语句会很麻烦的,用视图将几个表联结起来,然后对这个视图进行查询操作,就和对一个表查询一样,很方便。


15.2 视图表能否修改?


首先我们需要知道,视图表保存的是select语句的定义,所以视图表可不可以修改需要视情况而定。



  • 如果 select 语句查询的字段是没有被处理过的源表字段,则可以通过视图表修改源表数据;

  • 如果select 语句查询的字段是被 gr0up by语句或 函数 处理过的字段,则不可以直接修改视图表的数据。


create view v_store_info as select store_name,sales from store_info;

update v_store_info set sales=1000 where store_name='Houston';

f23.png


create view v_sales as select store_name,sum(sales) from store_info gr0up by store_name having sum(sales)>1000;

update v_sales set store_name='xxxx' where store_name='Los Angeles';

f24.png


f25.png


15.3 基本语法


15.3.1 创建视图表


语法
create view "视图表名" as "select 语句";

create view v_region_sales as select a.region region,sum(b.sales) sales from location a 
inner join store_info b on a.store_name = b.store_name gr0up by region;

f26.png


15.4 查看视图表


语法
select * from 视图表名;

select * from v_region_sales;

f27.png


15.5 删除视图表


语法
drop view 视图表名;

drop view v_region_sales;

f28.png


15.6 通过视图表求无交集值


将两个表中某个字段的不重复值进行合并


只出现一次(count =1 ) ,即无交集


通过


create view 视图表名 as select distinct 字段 from 左表 union all select distinct 字段 from 右表;

select 字段 from 视图表名 gr0up by 字段 having count(字段)=1;

#先建立视图表
create viem v_union as select distinct store_name from location union all select distinct store_name from store_info;

f29.png


#再通过视图表求无交集
select store_name from v_union gr0up by store_name having count(*)=1;

f30.png


作者:lc111
来源:juejin.cn/post/7291952951047929868
收起阅读 »

哇塞,新来个架构师,把Nacos注册中心讲得炉火纯青,佩服佩服~~

大家好,我是三友~~ 今天就应某位小伙伴的要求,来讲一讲Nacos作为服务注册中心底层的实现原理 不知你是否跟我一样,在使用Nacos时有以下几点疑问: 临时实例和永久实例是什么?有什么区别? 服务实例是如何注册到服务端的? 服务实例和服务端之间是如何保活的...
继续阅读 »

大家好,我是三友~~


今天就应某位小伙伴的要求,来讲一讲Nacos作为服务注册中心底层的实现原理


不知你是否跟我一样,在使用Nacos时有以下几点疑问:



  • 临时实例和永久实例是什么?有什么区别?

  • 服务实例是如何注册到服务端的?

  • 服务实例和服务端之间是如何保活的?

  • 服务订阅是如何实现的?

  • 集群间数据是如何同步的?CP还是AP?

  • Nacos的数据模型是什么样的?

  • ...


本文就通过探讨上述问题来探秘Nacos服务注册中心核心的底层实现原理。


虽然Nacos最新版本已经到了2.x版本,但是为了照顾那些还在用1.x版本的同学,所以本文我会同时去讲1.x版本和2.x版本的实现


临时实例和永久实例


临时实例和永久实例在Nacos中是一个非常非常重要的概念


之所以说它重要,主要是因为我在读源码的时候发现,临时实例和永久实例在底层的许多实现机制是完全不同的


临时实例


临时实例在注册到注册中心之后仅仅只保存在服务端内部一个缓存中,不会持久化到磁盘


这个服务端内部的缓存在注册中心届一般被称为服务注册表


当服务实例出现异常或者下线之后,就会把这个服务实例从服务注册表中剔除


永久实例


永久服务实例不仅仅会存在服务注册表中,同时也会被持久化到磁盘文件中


当服务实例出现异常或者下线,Nacos只会将服务实例的健康状态设置为不健康,并不会对将其从服务注册表中剔除


所以这个服务实例的信息你还是可以从注册中心看到,只不过处于不健康状态


这是就是两者最最最基本的区别



当然除了上述最基本的区别之外,两者还有很多其它的区别,接下来本文还会提到



这里你可能会有一个疑问



为什么Nacos要将服务实例分为临时实例和永久实例?



主要还是因为应用场景不同


临时实例就比较适合于业务服务,服务下线之后可以不需要在注册中心中查看到


永久实例就比较适合需要运维的服务,这种服务几乎是永久存在的,比如说MySQL、Redis等等



MySQL、Redis等服务实例可以通过SDK手动注册



对于这些服务,我们需要一直看到服务实例的状态,即使出现异常,也需要能够查看时实的状态



所以从这可以看出Nacos跟你印象中的注册中心不太一样,他不仅仅可以注册平时业务中的实例,还可以注册像MySQL、Redis这个服务实例的信息到注册中心



在SpringCloud环境底下,一般其实都是业务服务,所以默认注册服务实例都是临时实例


当然如果你想改成永久实例,可以通过下面这个配置项来完成


spring
  cloud:
    nacos:
      discovery:
        #ephemeral单词是临时的意思,设置成false,就是永久实例了
        ephemeral: false

这里还有一个小细节


在1.x版本中,一个服务中可以既有临时实例也有永久实例,服务实例是永久还是临时是由服务实例本身决定的


但是2.x版本中,一个服务中的所有实例要么都是临时的要么都是永久的,是由服务决定的,而不是具体的服务实例


所以在2.x可以说是临时服务永久服务




为什么2.x把临时还是永久的属性由实例本身决定改成了由服务决定?



其实很简单,你想想,假设对一个MySQL服务来说,它的每个服务实例肯定都是永久的,不会出现一些是永久的,一些是临时的情况吧


所以临时还是永久的属性由服务本身决定其实就更加合理了


服务注册


作为一个服务注册中心,服务注册肯定是一个非常重要的功能


所谓的服务注册,就是通过注册中心提供的客户端SDK(或者是控制台)将服务本身的一些元信息,比如ip、端口等信息发送到注册中心服务端


服务端在接收到服务之后,会将服务的信息保存到前面提到的服务注册表中


1、1.x版本的实现


在Nacos在1.x版本的时候,服务注册是通过Http接口实现的



代码如下



整个逻辑比较简单,因为Nacos服务端本身就是用SpringBoot写的


但是在2.x版本的实现就比较复杂了


2、2.x版本的实现


2.1、通信协议的改变


2.x版本相比于1.x版本最主要的升级就是客户端和服务端通信协议的改变,由1.x版本的Http改成了2.x版本gRPC



gRPC是谷歌公司开发的一个高性能、开源和通用的RPC框架,Java版本的实现底层也是基于Netty来的



之所以改成了gRPC,主要是因为Http请求会频繁创建和销毁连接,白白浪费资源


所以在2.x版本之后,为了提升性能,就将通信协议改成了gRPC


根据官网显示,整体的效果还是很明显,相比于1.x版本,注册性能总体提升至少2倍



虽然通信方式改成了gRPC,但是2.x版本服务端依然保留了Http注册的接口,所以用1.x的Nacos SDK依然可以注册到2.x版本的服务端



2.2、具体的实现


Nacos客户端在启动的时候,会通过gRPC跟服务端建立长连接



这个连接会一直存在,之后客户端与服务端所有的通信都是基于这个长连接来的


当客户端发起注册的时候,就会通过这个长连接,将服务实例的信息发送给服务端


服务端拿到服务实例,跟1.x一样,也会存到服务注册表


除了注册之外,当注册的是临时实例时,2.x还会将服务实例信息存储到客户端中的一个缓存中,供Redo操作


所谓的Redo操作,其实就是一个补偿机制,本质是个定时任务,默认每3s执行一次


这个定时任务作用是,当客户端与服务端重新建立连接时(因为一些异常原因导致连接断开)


那么之前注册的服务实例肯定还要继续注册服务端(断开连接服务实例就会被剔除服务注册表)


所以这个Redo操作一个很重要的作用就是重连之后的重新注册的作用



除了注册之外,比如服务订阅之类的操作也需要Redo操作,当连接重新建立,之前客户端的操作都需要Redo一下



小总结


1.x版本是通过Http协议来进行服务注册的


2.x由于客户端与服务端的通信改成了gRPC长连接,所以改成通过gRPC长连接来注册


2.x比1.x多个Redo操作,当注册的服务实例是临时实例是,出现网络异常,连接重新建立之后,客户端需要将服务注册、服务订阅之类的操作进行重做


这里你可能会有个疑问



既然2.x有Redo机制保证客户端与服务端通信正常之后重新注册,那么1.x有类似的这种Redo机制么?



当然也会有,接下往下看。


心跳机制


心跳机制,也可以被称为保活机制,它的作用就是服务实例告诉注册中心我这个服务实例还活着



在正常情况下,服务关闭了,那么服务会主动向Nacos服务端发送一个服务下线的请求


Nacos服务端在接收到请求之后,会将这个服务实例从服务注册表中剔除


但是对于异常情况下,比如出现网络问题,可能导致这个注册的服务实例无法提供服务,处于不可用状态,也就是不健康


而此时在没有任何机制的情况下,服务端是无法知道这个服务处于不可用状态


所以为了避免这种情况,一些注册中心,就比如Nacos、Eureka,就会用心跳机制来判断这个服务实例是否能正常


在Nacos中,心跳机制仅仅是针对临时实例来说的,临时实例需要靠心跳机制来保活


心跳机制在1.x和2.x版本的实现也是不一样的


1.x心跳实现


在1.x中,心跳机制实现是通过客户端和服务端各存在的一个定时任务来完成的


在服务注册时,发现是临时实例,客户端会开启一个5s执行一次的定时任务



这个定时任务会构建一个Http请求,携带这个服务实例的信息,然后发送到服务端



在Nacos服务端也会开启一个定时任务,默认也是5s执行一次,去检查这些服务实例最后一次心跳的时间,也就是客户端最后一次发送Http请求的时间



  • 当最后一次心跳时间超过15s,但没有超过30s,会把这服务实例标记成不健康

  • 当最后一次心跳超过30s,直接把服务从服务注册表中剔除



这就是1.x版本的心跳机制,本质就是两个定时任务


其实1.x的这个心跳还有一个作用,就是跟上一节说的gRPC时Redo操作的作用是一样的


服务在处理心跳的时候,发现心跳携带这个服务实例的信息在注册表中没有,此时就会添加到服务注册表


所以心跳也有Redo的类似效果


2.x心跳实现


在2.x版本之后,由于通信协议改成了gRPC,客户端与服务端保持长连接,所以2.x版本之后它是利用这个gRPC长连接本身的心跳来保活


一旦这个连接断开,服务端就会认为这个连接注册的服务实例不可用,之后就会将这个服务实例从服务注册表中提出剔除


除了连接本身的心跳之外,Nacos还有服务端的一个主动检测机制


Nacos服务端也会启动一个定时任务,默认每隔3s执行一次


这个任务会去检查超过20s没有发送请求数据的连接


一旦发现有连接已经超过20s没发送请求,那么就会向这个连接对应的客户端发送一个请求


如果请求不通或者响应失败,此时服务端也会认为与客户端的这个连接异常,从而将这个客户端注册的服务实例从服务注册表中剔除


所以对于2.x版本,主要是两种机制来进行保活:



  • 连接本身的心跳机制,断开就直接剔除服务实例

  • Nacos主动检查机制,服务端会对20s没有发送数据的连接进行检查,出现异常时也会主动断开连接,剔除服务实例


小总结


心跳机制仅仅针对临时实例而言


1.x心跳机制是通过客户端和服务端两个定时任务来完成的,客户端定时上报心跳信息,服务端定时检查心跳时间,超过15s标记不健康,超过30s直接剔除


1.x心跳机制还有类似2.x的Redo作用,服务端发现心跳的服务信息不存在会,会将服务信息添加到注册表,相当于重新注册了


2.x是基于gRPC长连接本身的心跳机制和服务端的定时检查机制来的,出现异常直接剔除


健康检查


前面说了,心跳机制仅仅是临时实例用来保护的机制


而对于永久实例来说,一般来说无法主动上报心跳


就比如说MySQL实例,肯定是不会主动上报心跳到Nacos的,所以这就导致无法通过心跳机制来保活


所以针对永久实例的情况,Nacos通过一种叫健康检查的机制去判断服务实例是否活着


健康检查跟心跳机制刚好相反,心跳机制是服务实例向服务端发送请求


而所谓的健康检查就是服务端主动向服务实例发送请求,去探测服务实例是否活着



健康检查机制在1.x和2.x的实现机制是一样的


Nacos服务端在会去创建一个健康检查任务,这个任务每次执行时间间隔会在2000~7000毫秒之间


当任务触发的时候,会根据设置的健康检查的方式执行不同的逻辑,目前主要有以下三种方式:



  • TCP

  • HTTP

  • MySQL


TCP的方式就是根据服务实例的ip和端口去判断是否能连接成功,如果连接成功,就认为健康,反之就任务不健康


HTTP的方式就是向服务实例的ip和端口发送一个Http请求,请求路径是需要设置的,如果能正常请求,说明实例健康,反之就不健康


MySQL的方式是一种特殊的检查方式,他可以执行下面这条Sql来判断数据库是不是主库



默认情况下,都是通过TCP的方式来探测服务实例是否还活着


服务发现


所谓的服务发现就是指当有服务实例注册成功之后,其它服务可以发现这些服务实例


Nacos提供了两种发现方式:



  • 主动查询

  • 服务订阅


主动查询就是指客户端主动向服务端查询需要关注的服务实例,也就是拉(pull)的模式


服务订阅就是指客户端向服务端发送一个订阅服务的请求,当被订阅的服务有信息变动就会主动将服务实例的信息推送给订阅的客户端,本质就是推(push)模式



在我们平时使用时,一般来说都是选择使用订阅的方式,这样一旦有服务实例数据的变动,客户端能够第一时间感知


并且Nacos在整合SpringCloud的时候,默认就是使用订阅的方式


对于这两种服务发现方式,1.x和2.x版本实现也是不一样


服务查询其实两者实现都很简单


1.x整体就是发送Http请求去查询服务实例,2.x只不过是将Http请求换成了gRPC的请求


服务端对于查询的处理过程都是一样的,从服务注册表中查出符合查询条件的服务实例进行返回


不过对于服务订阅,两者的机制就稍微复杂一点


在Nacos客户端,不论是1.x还是2.x都是通过SDK中的NamingService#subscribe方法来发起订阅的



当有服务实例数据变动的时,客户端就会回调EventListener,就可以拿到最新的服务实例数据了


虽然1.x还是2.x都是同样的方法,但是具体的实现逻辑是不一样的


1.x服务订阅实现


在1.x版本的时候,服务订阅的处理逻辑大致会有以下三步:


第一步,客户端在启动的时候,会去构建一个叫PushReceiver的类


这个类会去创建一个UDP Socket,端口是随机的



其实通过名字就可以知道这个类的作用,就是通过UDP的方式接收服务端推送的数据的


第二步,调用NamingService#subscribe来发起订阅时,会先去服务端查询需要订阅服务的所有实例信息


之后会将所有服务实例数据存到客户端的一个内部缓存中



并且在查询的时候,会将这个UDP Socket的端口作为一个参数传到服务端


服务端接收到这个UDP端口后,后续就通过这个端口给客户端推送服务实例数据


第三步,会为这次订阅开启一个不定时执行的任务



之所以不定时,是因为这个当执行异常的时候,下次执行的时间间隔就会变长,但是最多不超过60s,正常是10s,这个10s是查询服务实例是服务端返回的



这个任务会去从服务端查询订阅的服务实例信息,然后更新内部缓存


这里你可能会有个疑问



既然有了服务变动推送的功能,为什么还要定时去查询更新服务实例信息呢?



其实很简单,那就是因为UDP通信不稳定导致的


虽然有Push,但是由于UDP通信自身的不确定性,有可能会导致客户端接收变动信息失败


所以这里就加了一个定时任务,弥补这种可能性,属于一个兜底的方案。


这就是1.x版本的服务订阅的实现



2.x服务订阅的实现


讲完1.x的版本实现,接下来就讲一讲2.x版本的实现


由于2.x版本换成了gRPC长连接的方式,所以2.x版本服务数据变更推送已经完全抛弃了1.x的UDP做法


当有服务实例变动的时候,服务端直接通过这个长连接将服务信息发送给客户端


客户端拿到最新服务实例数据之后的处理方式就跟1.x是一样了


除了处理方式一样,2.x也继承了1.x的其他的东西


比如客户端依然会有服务实例的缓存


定时对比机制也保留了,只不过这个定时对比的机制默认是关闭状态


之所以默认关闭,主要还是因为长连接还是比较稳定的原因


当客户端出现异常,接收不到请求,那么服务端会直接跟客户端断开连接


当恢复正常,由于有Redo操作,所以还是能拿到最新的实例信息的


所以2.x版本的服务订阅功能的实现大致如下图所示



这里还有个细节需要注意


在1.x版本的时候,任何服务都是可以被订阅的


但是在2.x版本中,只支持订阅临时服务,对于永久服务,已经不支持订阅了


小总结


服务查询1.x是通过Http请求;2.x通过gRPC请求


服务订阅1.x是通过UDP来推送的;2.x就基于gRPC长连接来实现的


1.x和2.x客户端都有服务实例的缓存,也有定时对比机制,只不过1.x会自动开启;2.x提供了一个开个,可以手动选择是否开启,默认不开启


数据一致性


由于Nacos是支持集群模式的,所以一定会涉及到分布式系统中不可避免的数据一致性问题


1、服务实例的责任机制


再说数据一致性问题之前,先来讨论一下服务实例的责任机制


什么是服务实例的责任机制?


比如上面提到的服务注册、心跳管理、监控检查机制,当只有一个Nacos服务时,那么自然而言这个服务会去检查所有的服务实例的心跳时间,执行所有服务实例的健康检查任务



但是当出现Nacos服务出现集群时,为了平衡各Nacos服务的压力,Nacos会根据一定的规则让每个Nacos服务只管理一部分服务实例的



当然每个Nacos服务的注册表还是全部的服务实例数据




这个管理机制我给他起了一个名字,就叫做责任机制,因为我在1.x和2.x都提到了responsible这个单词


本质就是Nacos服务对哪些服务实例负有心跳监测,健康检查的责任。


2、CAP定理和BASE理论


谈到数据一致性问题,一定离不开两个著名分布式理论



  • CAP定理

  • BASE理论


CAP定理中,三个字母分别代表这些含义:



  • C,Consistency单词的缩写,代表一致性,指分布式系统中各个节点的数据保持强一致,也就是每个时刻都必须一样,不一样整个系统就不能对外提供服务

  • A,Availability单词的缩写,代表可用性,指整个分布式系统保持对外可用,即使从每个节点获取的数据可能都不一样,只要能获取到就行

  • P,Partition tolerance单词的缩写,代表分区容错性。


所谓的CAP定理,就是指在一个分布式系统中,CAP这三个指标,最多同时只能满足其中的两个,不可能三个都同时满足



为什么三者不能同时满足?


对于一个分布式系统,网络分区是一定需要满足的


而所谓分区指的是系统中的服务部署在不同的网络区域中



比如,同一套系统可能同时在北京和上海都有部署,那么他们就处于不同的网络分区,就可能出现无法互相访问的情况


当然,你也可以把所有的服务都放在一个网络分区,但是当网络出现故障时,整个系统都无法对外提供服务,那这还有什么意义呢?


所以分布式系统一定需要满足分区容错性,把系统部署在不同的区域网络中


此时只剩下了一致性和可用性,它们为什么不能同时满足?


其实答案很简单,就因为可能出现网络分区导致的通信失败。


比如说,现在出现了网络分区的问题,上图中的A网络区域和B网络区域无法相互访问


此时假设往上图中的A网络区域发送请求,将服务中的一个值 i 属性设置成 1



如果保证可用性,此时由于A和B网络不通,此时只有A中的服务修改成功,B无法修改成功,此时数据AB区域数据就不一致性,也就没有保证数据一致性


如果保证一致性,此时由于A和B网络不通,所以此时A也不能修改成功,必须修改失败,否则就会导致AB数据不一致


虽然A没修改成功,保证了数据一致性,AB还是之前相同的数据,但是此时整个系统已经没有写可用性了,无法成功写数据了。


所以从上面分析可以看出,在有分区容错性的前提下,可用性和一致性是无法同时保证的。


虽然无法同时一致性和可用性,但是能不能换种思路来思考一下这个问题


首先我们可以先保证系统的可用性,也就是先让系统能够写数据,将A区域服务中的i修改成1


之后当AB区域之间网络恢复之后,将A区域的i值复制给B区域,这样就能够保证AB区域间的数据最终是一致的了


这不就皆大欢喜了么


这种思路其实就是BASE理论的核心要点,优先保证可用性,数据最终达成一致性。


BASE理论主要是包括以下三点:



  • 基本可用(Basically Available):系统出现故障还是能够对外提供服务,不至于直接无法用了

  • 软状态(Soft State):允许各个节点的数据不一致

  • 最终一致性,(Eventually Consistent):虽然允许各个节点的数据不一致,但是在一定时间之后,各个节点的数据最终需要一致的


BASE理论其实就是妥协之后的产物。


3、Nacos的AP和CP


Nacos其实目前是同时支持AP和CP的


具体使用AP还是CP得取决于Nacos内部的具体功能,并不是有的文章说的可以通过一个配置自由切换。


就以服务注册举例来说,对于临时实例来说,Nacos会优先保证可用性,也就是AP


对于永久实例,Nacos会优先保证数据的一致性,也就是CP


接下来我们就来讲一讲Nacos的CP和AP的实现原理


3.1、Nacos的AP实现


对于AP来说,Nacos使用的是阿里自研的Distro协议


在这个协议中,每个服务端节点是一个平等的状态,每个服务端节点正常情况下数据是一样的,每个服务端节点都可以接收来自客户端的读写请求



当某个节点刚启动时,他会向集群中的某个节点发送请求,拉取所有的服务实例数据到自己的服务注册表中



这样其它客户端就可以从这个服务节点中获取到服务实例数据了


当某个服务端节点接收到注册临时服务实例的请求,不仅仅会将这个服务实例存到自身的服务注册表,同时也会向其它所有服务节点发送请求,将这个服务数据同步到其它所有节点



所以此时从任意一个节点都是可以获取到所有的服务实例数据的。


即使数据同步的过程发生异常,服务实例也成功注册到一个Nacos服务中,对外部而言,整个Nacos集群是可用的,也就达到了AP的效果


同时为了满足BASE理论,Nacos也有下面两种机制保证最终节点间数据最终是一致的:



  • 失败重试机制

  • 定时对比机制


失败重试机制是指当数据同步给其它节点失败时,会每隔3s重试一次,直到成功


定时对比机制就是指,每个Nacos服务节点会定时向所有的其它服务节点发送一些认证的请求


这个请求会告诉每个服务节点自己负责的服务实例的对应的版本号,这个版本号随着服务实例的变动就会变动


如果其它服务节点的数据的版本号跟自己的对不上,那就说明其它服务节点的数据不是最新的


此时这个Nacos服务节点就会将自己负责的服务实例数据发给不是最新数据的节点,这样就保证了每个节点的数据是一样的了。


3.2、Nacos的CP实现


Nacos的CP实现是基于Raft算法来实现的


在1.x版本早期,Nacos是自己手动实现Raft算法


在2.x版本,Nacos移除了手动实现Raft算法,转而拥抱基于蚂蚁开源的JRaft框架


在Raft算法,每个节点主要有三个状态



  • Leader,负责所有的读写请求,一个集群只有一个

  • Follower,从节点,主要是负责复制Leader的数据,保证数据的一致性

  • Candidate,候选节点,最终会变成Leader或者Follower


集群启动时都是节点Follower,经过一段时间会转换成Candidate状态,再经过一系列复杂的选择算法,选出一个Leader




这个选举算法比较复杂,完全值得另写一篇文章,这里就不细说了。不过立个flag,如果本篇文章点赞量超过28个,我连夜爆肝,再来一篇。



当有写请求时,如果请求的节点不是Leader节点时,会将请求转给Leader节点,由Leader节点处理写请求


比如,有个客户端连到的上图中的Nacos服务2节点,之后向Nacos服务2注册服务


Nacos服务2接收到请求之后,会判断自己是不是Leader节点,发现自己不是


此时Nacos服务2就会向Leader节点发送请求,Leader节点接收到请求之后,会处理服务注册的过程


为什么说Raft是保证CP的呢?


主要是因为Raft在处理写的时候有一个判断过程



  • 首先,Leader在处理写请求时,不会直接数据应用到自己的系统,而是先向所有的Follower发送请求,让他们先处理这个请求

  • 当超过半数的Follower成功处理了这个写请求之后,Leader才会写数据,并返回给客户端请求处理成功

  • 如果超过一定时间未收到超过半数处理成功Follower的信号,此时Leader认为这次写数据是失败的,就不会处理写请求,直接返回给客户端请求失败


所以,一旦发生故障,导致接收不到半数的Follower写成功的响应,整个集群就直接写失败,这就很符合CP的概念了。


不过这里还有一个小细节需要注意


Nacos在处理查询服务实例的请求直接时,并不会将请求转发给Leader节点处理,而是直接查当前Nacos服务实例的注册表


这其实就会引发一个问题


如果客户端查询的Follower节点没有及时处理Leader同步过来的写请求(过半响应的节点中不包括这个节点),此时在这个Follower其实是查不到最新的数据的,这就会导致数据的不一致


所以说,虽然Raft协议规定要求从Leader节点查最新的数据,但是Nacos至少在读服务实例数据时并没有遵守这个协议


当然对于其它的一些数据的读写请求有的还是遵守了这个协议。



JRaft对于读请求其实是做了很多优化的,其实从Follower节点通过一定的机制也是能够保证读到最新的数据



数据模型


在Nacos中,一个服务的确定是由三部分信息确定



  • 命名空间(Namespace):多租户隔离用的,默认是public

  • 分组(Gr0up):这个其实可以用来做环境隔离,服务注册时可以指定服务的分组,比如是测试环境或者是开发环境,默认是DEFAULT_GR0UP

  • 服务名(ServiceName):这个就不用多说了


通过上面三者就可以确定同一个服务了


在服务注册和订阅的时候,必须要指定上述三部分信息,如果不指定,Nacos就会提供默认的信息


不过,在Nacos中,在服务里面其实还是有一个集群的概念



在服务注册的时候,可以指定这个服务实例在哪个集体的集群中,默认是在DEFAULT集群下


在SpringCloud环境底下可以通过如下配置去设置


spring
  cloud:
    nacos:
      discovery:
        cluster-name: sanyoujavaCluster

在服务订阅的时候,可以指定订阅哪些集群下的服务实例


当然,也可以不指定,如果不指定话,默认就是订阅这个服务下的所有集群的服务实例


我们日常使用中可以将部署在相同区域的服务划分为同一个集群,比如杭州属于一个集群,上海属于一个集群


这样服务调用的时候,就可以优先使用同一个地区的服务了,比跨区域调用速度更快。


总结


到这,终终终于总算是讲完了Nacos作为注册中心核心的实现原理




作者:zzyang90
来源:juejin.cn/post/7347325319198048283
收起阅读 »

你居然还去服务器上捞日志,搭个日志收集系统难道不香么!

1 ELK日志系统 经典的ELK架构或现被称为Elastic Stack。Elastic Stack架构为Elasticsearch + Logstash + Kibana + Beats的组合: Beats负责日志的采集 Logstash负责做日志的聚合和...
继续阅读 »

1 ELK日志系统


经典的ELK架构或现被称为Elastic Stack。Elastic Stack架构为Elasticsearch + Logstash + Kibana + Beats的组合:



  • Beats负责日志的采集

  • Logstash负责做日志的聚合和处理

  • ES作为日志的存储和搜索系统

  • Kibana作为可视化前端展示


整体架构图:


img


2 EFK日志系统


容器化场景中,尤其k8s环境,用户经常使用EFK架构。F代表Fluent Bit,一个开源多平台的日志处理器和转发器。Fluent Bit可以:



  • 让用户从不同来源收集数据/日志

  • 统一并发到多个目的地

  • 完全兼容Docker和k8s环境



3 PLG日志系统


3.1 Prometheus+k8s日志系统




PLG


Grafana Labs提供的另一个日志解决方案PLG逐渐流行。PLG架构即Promtail + Loki + Grafana的组合:
img


Grafana,开源的可视化和分析软件,允许用户查询、可视化、警告和探索监控指标。Grafana主要提供时间序列数据的仪表板解决方案,支持超过数十种数据源。


Grafana Loki是一组可以组成一个功能齐全的日志堆栈组件,与其它日志系统不同,Loki只建立日志标签的索引而不索引原始日志消息,而是为日志数据设置一组标签,即Loki运营成本更低,效率还提高几个数量级。



Loki设计理念


Prometheus启发,可实现可水平扩展、高可用的多租户日志系统。Loki整体架构由不同组件协同完成日志收集、索引、存储等。


各组件如下,Loki’s Architecture深入了解。Loki就是like Prometheus, but for logs。
img


Promtail是一个日志收集的代理,会将本地日志内容发到一个Loki实例,它通常部署到需要监视应用程序的每台机器/容器上。Promtail主要是用来发现目标、将标签附加到日志流以及将日志推送到Loki。截止到目前,Promtail可以跟踪两个来源的日志:本地日志文件和systemd日志(仅支持AMD64架构)。


4 PLG V.S ELK


4.1 ES V.S Loki


ELK/EFK架构确实强,经多年实际环境验证。存储在ES中的日志通常以非结构化JSON对象形式存储在磁盘,且ES为每个对象都建索引,以便全文搜索,然后用户可特定查询语言搜索这些日志数据。


而Loki数据存储解耦:



  • 既可在磁盘存储

  • 也可用如Amazon S3云存储系统


Loki日志带有一组标签名和值,只有标签对被索引,这种权衡使它比完整索引操作成本更低,但针对基于内容的查询,需通过LogQL再单独查询。


4.2 Fluentd V.S Promtail


相比Fluentd,Promtail专为Loki定制,它可为运行在同一节点的k8s Pods做服务发现,从指定文件夹读取日志。Loki类似Prometheus的标签方式。因此,当与Prometheus部署在同一环境,因为相同的服务发现机制,来自Promtail的日志通常具有与应用程序指标相同的标签,统一标签管理。


4.3 Grafana V.S Kibana


Kibana提供许多可视化工具来进行数据分析,高级功能如异常检测等机器学习功能。Grafana针对Prometheus和Loki等时间序列数据打造,可在同一仪表板上查看日志指标。


参考



作者:JavaEdge在掘金
来源:juejin.cn/post/7295623585364082739
收起阅读 »

超级火爆的前端视频方案 FFmpeg ,带你体验一下~

前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ ffmpeg FFmpeg 是一个开源的、跨平台的多媒体框架,它可以用来录制、转换和流式传输音频和视频。它包括了一系列的库和工具,用于处理多媒体内容,比如 l...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~



ffmpeg


FFmpeg 是一个开源的、跨平台的多媒体框架,它可以用来录制、转换和流式传输音频和视频。它包括了一系列的库和工具,用于处理多媒体内容,比如 libavcodec(一个编解码库),libavformat(一个音视频容器格式库),libavutil(一个实用库),以及 ffmpeg 命令行工具本身。


FFmpeg 被广泛用于各种应用中,包括视频转换、视频编辑、视频压缩、直播流处理等。它支持多种音视频编解码器和容器格式,因此能够处理几乎所有类型的音视频文件。由于其功能强大和灵活性,FFmpeg 成为了许多视频相关软件和服务的底层技术基础。


很多网页都是用 ffmpeg 来进行视频切片,比如一个视频很大,如果通过一个连接去请求整个视频的话,那势必会导致加载时间过长,严重阻碍了用户观感


所以很多视频网站都会通过视频切片的方式来优化用户观感,就是一部分一部分地去加载出来,这样有利于用户的体验



安装 ffmpeg


安装包下载


首先到 ffmpeg 的安装网页:http://www.gyan.dev/ffmpeg/buil…



下载解压后将文件夹改名为 ffmpeg



环境变量配置


环境变量配置是为了能在电脑上使用 ffmpeg 命令行




体验 ffmpeg


先准备一个视频,比如我准备了一个视频,总共 300 多 M



视频切片


并在当前的目录下输入以下的命令


 ffmpeg -i jhys.mkv 
-c:v libx264
-c:a aac
-hls_time 60
-hls_segment_type mpegts
-hls_list_size 0
-f hls
-max_muxing_queue_size 1024
output.m3u8

接着 ffmpeg 会帮你将这个视频进行分片



直到切片步骤执行完毕,我们可以看到视频已经别切成几个片了



在这个命令中:



  • -i input_video.mp4 指定了输入视频文件。

  • -c:v libx264 -c:a aac 指定了视频和音频的编解码器。

  • -hls_time 10 指定了每个 M3U8 片段的时长,单位为秒。在这里,每个片段的时长设置为 10 秒。

  • -hls_segment_type mpegts 指定了 M3U8 片段的类型为 MPEG-TS。

  • -hls_list_size 0 设置 M3U8 文件中包含的最大片段数。这里设置为 0 表示没有限制。

  • -f hls 指定了输出格式为 HLS。

  • -max_muxing_queue_size 1024 设置了最大复用队列大小,以确保输出不会超过指定大小。

  • 最后输出的文件为 output.m3u8


视频播放


创建一个简单的前端项目




可以看到浏览器会加载所有的视频切片





作者:Sunshine_Lin
来源:juejin.cn/post/7361998447908864011
收起阅读 »