开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

React Router 6强势来袭(15分钟了解它的前世今

发表于 2021-11-16

本篇是译文,原文地址在文章末尾,如果有翻译问题欢迎指正和交流!

今天我们很高兴地宣布React Router v6的稳定版本。

这个版本已经发布了很长时间。我们上一次发布重大的 API 更改是在四年前的 2017 年 3 月,当时我们发布了第 4 版。你们中的一些人可能那时还没有出生。不用说,从那时到现在发生了很多事情:

  • React Router 的下载量从 2017 年 3 月的 34 万/月增长到 2021 年 10 月的 2100 万/月,增长了 60 倍以上(6000%)
  • 我们发布了没有重大更改的第 5 版(我已经在别处写过主要版本变更的原因)
  • 我们发布了 Reach Router,目前平均每月下载量约为 1300 万次
  • 引入了 React Hooks
  • 新冠

笔者备注:
关于版本的补充,本来是发布4.4版本的,但是因为react-router-dom依赖^react-router,注意^,可能会存在4.3react-router-dom依赖4.4react-router导致context不同从而引入问题,因此被迫升级到了5 react-router-dom 和 react-router的区别

我可以轻松地至少写几页关于上述每个要点及其对我们的业务和我们自 2014 年以来一直管理的开源项目的重要性。但我不想让你厌烦过去。在过去的几年里,我们都经历了很多。其中一些很艰难,但希望你也经历了一些新的成长,像我们一样。事实上,我们彻底改变了我们的商业模式!

今天,我想聚焦于未来,以及我们如何利用过去的经验为 React Router 项目和令人难以置信的 React 社区构建最强大的未来。会有代码。但我们还将讨论业务以及您对我们的期望(提示:
它丰富多彩)。

为什么是另一个主要版本?

发布新路由器的最大原因很容易是React hooks的出现。您可能还记得Ryan在 React Conf 2018 上向世界介绍 hooks的演讲,以及当您将基于类的 React 代码重构为 hooks 时,我们习惯使用 React 的“生命周期方法”编写的很多代码都消失了。如果你不记得那次演讲,你可能应该在这里停下来看看。我会等待。

尽管我们在 v5中(v5.1) 上添加了一些hooks,但 React Router v6 是真正使用 React hook 重新实现的。它们是如此高效的低级原语,以至于我们能够通过使用hooks来消除大量样板代码。这意味着您的 v6 代码将比 v5 代码更加紧凑和优雅。

此外,不仅仅是您的代码变得更小、更高效……对我们的实现也是对此!我们压缩后的 gzip 压缩包大小在 v6 中下降了 50% 以上! React Router 现在为你的总应用程序包增加了不到4kb,一旦通过你的打包器工具运行它并打开 tree-shaking,实际结果会更小。

可组合路由器

为了演示如何使用 v6 中的hooks改进您的代码,让我们从一些非常简单的事情开始,例如从当前 URL 路径名访问参数。React Router v6 提供了一个useParams()hooks(也在 5.1 中),允许您在任何需要的地方访问当前的 URL 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
js复制代码import { Routes, Route, useParams } from "react-router-dom";

function App() {
return (
<Routes>
<Route path="blog/:id" element={<BlogPost />} />
</Routes>
);
}

function BlogPost() {
// You can access the params here...
const { id } = useParams();
return (
<>
<PostHeader />
{/* ... */}
</>
);
}

function PostHeader() {
// or here. Just call the hook wherever you need it.
let { id } = useParams();
}

在 v5 或 v5 之前的版本中如何通过render prop或高阶组件实现同样的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
js复制代码// React Router v5 code
import * as React from "react";
import { Switch, Route } from "react-router-dom";

class App extends React.Component {
render() {
return (
<Switch>
<Route
path="blog/:id"
render={({ match }) => (
// Manually forward the prop to the <BlogPost>...
<BlogPost id={match.params.id} />
)}
/>
</Switch>
);
}
}

class BlogPost extends React.Component {
render() {
return (
<>
{/* ...and manually pass props down to children... booo */}
<PostHeader id={this.props.id} />
</>
);
}
}

Hooks 消除了<Route render>访问router内部状态 (match相关的逻辑)和 手动传递 props 以将该状态传递到子组件的必要。

另一种说法是在router context中考虑useParams()类似useState()的东西。router知道一些state(当前的 URL 参数),并允许您随时使用hooks访问它。如果没有hooks,我们需要手动将state传递到子孙组件中。

让我们看一下使用hooks的 React Router v6 如何比 v5 强大得多的另一个简单示例。假设您希望在当前location发生变化时向您的分析服务(类似google analysis)发送“pageview”事件。V6(v5.1+)中,使用useLocation()hooks可以帮你快速实现对应功能:

1
2
3
4
5
6
7
8
9
10
js复制代码import { useEffect } from "react";
import { useLocation } from "react-router-dom";

function App() {
const location = useLocation();
useEffect(() => {
window.ga("set", "page", location.pathname + location.search);
window.ga("send", "pageview");
}, [location]);
}

当然,由于hooks提供的功能组合,您可以将所有这些都包装到一个hooks中,例如:

1
2
3
4
5
6
js复制代码import { useAnalyticsTracking } from "./analytics";

function App() {
useAnalyticsTracking();
// ...
}

同样,如果没有hooks,你必须做一些hack,比如渲染一个独立的<Route path="/">渲染null,这样你就可以在location它发生变化时访问它。此外,如果没有useEffect(),您必须执行componentDidMount+componentDidUpdate以确保仅在location更改时发送页面浏览事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
js复制代码// React Router v5 code
import * as React from "react";
import { Switch, Route } from "react-router-dom";

class PageviewTracker extends React.Component {
trackPageview() {
let { location } = this.props;
window.ga("set", "page", location.pathname + location.search);
window.ga("send", "pageview");
}

componentDidMount() {
this.trackPageview();
}

componentDidUpdate(prevProps) {
if (prevProps.location !== this.props.location) {
this.trackPageview();
}
}

render() {
return null; // lol
}
}

class App extends React.Component {
return (
<>
{/* This route isn't really a piece of the UI, it's just here
so we can access the current location... */}
<Route path="/" component={PageviewTracker} />

<Switch>
{/* your actual routes... */}
</Switch>
</>
);
}

这个代码很疯狂,对吧?好吧,这些是没有hooks时无法避免的处理逻辑。

总结一下:我们正在发布 React Router 的新主要版本,以便您可以发布更小、更高效的应用程序,从而带来更好的用户体验。真的就是这么简单。

您可以在我们的 API 文档 中查看 v6中可用的hooks的完整列表。

还在用React.Component?别担心,我们仍然支持类组件!有关更多信息,请参阅此 GitHub 线程。

路由改进

记得react-nested-router吗?可能不是。但这就是react-router在 npm 上正式获得package name之前所说的 React Router (谢谢,Jared!)。React Router 一直在声明嵌套路由的,尽管我们表达它们的方式随着时间的推移略有变化。我将向您展示我们为 v6 提出的方案,但首先让我给您介绍一些关于 v3、v4/5 和 Reach Router 的背景故事。

在 v3 中,我们将<Route>元素直接嵌套在一个巨大的路由配置中,如本例所示。嵌套<Route>元素是可视化整个路线层次结构的好方法。然而,我们在 v3 中的实现使得代码拆分变得困难,因为所有的路由组件最终都在同一个包中(这是之前的React.lazy())。因此,随着您添加更多路线,您的捆绑包会不断增长。此外,<Route component>prop使得很难将custom props传递到您的组件。

在 v4 中,我们针对大型应用程序进行了优化。代码拆分(Code splitting)!而不是<Route>在 v4中嵌套元素,您只需嵌套自己的组件并将另一个组件放入<Switch>子组件中。您可以在此示例中看到它是如何工作的。这使得构建大型应用程序变得容易,因为拆分 React Router 应用程序的代码与拆分任何其他 React 应用程序的代码相同,您可以使用当时可用的几种不同工具之一在 React 中进行代码拆分,而这些工具与 React 无关路由器。然而,这种方法的一个意想不到的缺点是它<Route path>只会匹配 URL 路径名的开头,因为每个路由组件可能在深层的某个地方有更多的子路由。所以 React Router v5 应用程序必须使用<Route exact>每次他们没有子路由时(每条叶子路由)。

在我们的实验性Reach Router项目中,我们借鉴了 Preact Router 的想法并进行了自动路由排名,以尝试找出最匹配 URL 的路由,而不管它的定义顺序如何。这是对 v5<Switch>元素的重大改进,可帮助开发人员避免因以错误的顺序定义路由而导致的错误,从而创建无法访问的路由。然而,Reach Router 缺少<Route>组件在使用 TypeScript 时会造成一些痛苦,因为您的每个路由组件还必须接受特定于路由的 props,例如path(点击查看写了更多相关内容)。

所以,经过了这么多版本的变更,React Router v6到底需要什么?嗯,理想情况下,我们可以拥有迄今为止我们探索过的每个 API 的最佳功能,同时还避免了它们遇到的问题。具体来说,我们想要:

  • <Route>我们在 v3 中拥有的并置、嵌套的的可读性,但也支持代码拆分和将custom props传递到您的路由组件
  • 我们在 v4/5 中拥有的跨多个组件拆分路由的灵活性,而无需exact到处传递props
  • 我们在 Reach Router 中拥有的路由排名的功能,而不会弄乱路由组件的 prop 类型

哦,我们也会喜欢基于对象的路由API,在V3中,我们允许路由作为普通的JavaScript对象使用,而不必像在V4/5中。只能使用<Route>element、static match和render functions,如果想要像使用对象一样使用的话,还需要借助react-router-config包才行。

好吧,不用说,我们很高兴推出满足所有这些要求的路由 API。查看我们网站上文档中的 v6 API。它实际上看起来很像 v3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码import { render } from "react-dom";
import { BrowserRouter } from "react-router-dom";
// import your route components too

render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
</Routes>
</BrowserRouter>,
document.getElementById("root")
);

但是,如果您仔细观察,您会发现我们多年来的工作带来了一些细微的改进:

  • 我们正在使用 <Routes>而不是<Switch>。不是按顺序扫描路由,而是<Routes>自动为当前 URL 选择最好的路由。它还允许您在整个应用程序中传递路由,而不是<Router>像我们在 v3 中那样预先将它们全部定义为 prop 。
  • 该<Route element>prop可以让你通过自定义custom props(甚至children)到您的route elements。如果它是一个React.lazy()组件,它还可以简单的通过<React.Suspense>延迟加载您的路由元素。我们在从 v5 升级的说明中详细介绍了<Route element>API的优势。
  • 不需要在路由的叶子节点来声明<Route exact>以确保是否可以进行深度匹配,可以将route path定义为*来选择深入匹配,所以你可以像下面这样去拆分你的route config文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
js复制代码import { Routes, Route } from "react-router-dom";

function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route path="users" element={<Users />}>
<Route index element={<UsersIndex />} />

{/* This route will match /users/*, allowing more routing
to happen in the <UsersSplat> component */}
<Route path="*" element={<UsersSplat />} />
</Route>
</Route>
</Routes>
);
}

function UsersSplat() {
// More routes here! These won't be defined until this component
// mounts, preserving the dynamic routing semantics we had in v5.
// All paths defined here are relative to /users since this element
// renders inside /users/*
return (
<Routes>
<Route path=":id" element={<UserProfile />}>
<Route path="friends" element={<UserFriends />} />
<Route path="messages" element={<UserMessages />} />
</Route>
</Routes>
);
}

我很想在这里向您展示更多的路由API,但很难在博客文章中做到全部的都讲清楚。但幸运的是,您可以阅读代码。

因此,我将链接到一些示例,希望它们能比我在这里写的更响亮。每个示例都有一个按钮,允许您在在线编辑器中启动它,以便您可以使用它。

  • 我们的“基本”的例子,让您熟悉<Routes>,<Route>,<Outlet>,和<Link>的API
  • 使用 JavaScript 对象来定义路由而不是<Route>元素
  • 使用延迟加载(代码拆分)单个路由元素和嵌套路由 React.lazy()
  • 使用 basename在同一代码库中定义可移植的 React Router 应用程序,每个应用程序“安装”在不同的 URL 路径名前缀上

此处查看其余的 v6 示例,如果没有你想看的示例,请务必向我们发送 PR!

我们从 v3 借鉴的另一个功能是以新<Outlet>元素的形式实现对布局路由的支持。您可以在 v6 概述中阅读有关布局的更多信息。

这确实是我们设计过的最灵活、最强大的路由 API,我们通过使用它来构建的应用程序感到非常兴奋。

相对路由和链接

在React Router V6的另一个重大改进是:相对<Route path>和<Link to>。我们在React Router v5升级指南中花了很长的篇幅来指导你的使用。对于相对<Route path>和<Link to>,它基本上可以归纳为以下几点:

  • 相对<Route path>的值(path)总是相对于父路由。您不必再从 / 构建它们。
  • 相对<Link to>的值(to)总是相对于route path。如果它只包含一个搜索字符串(即<Link to="?framework=react">),它是相对于current location的pathname。
  • 相对<Link to>的值(to)相比比<a href>的值会更清晰一些,而且始终指向同一个路径,无论当前 URL 是否带有斜杠

另请参阅v5 升级指南中有关<Link to>值的说明,以了解有关相对<Link to>的值(to)为何比<a href>的指向更清晰以及如何使用前置..段返回父路由。

相对路由和链接 使Router的易用性迈出了一大步,因为不需要您在嵌套路由中构建绝对的<Route path>和<Link to>的路径。真的,这才是它应该一直工作的方式,我们认为您会真正享受以这种方式构建应用程序的简单和直观。

注意:绝对路径在 v6 中仍然有效,以帮助简化升级。如果您愿意,您甚至可以完全忽略相对路径并永远使用绝对路径。我们不会介意的。

升级到 React Router v6

我们想非常清楚这一点:React Router v6 是 React Router 之前所有版本的继承者,包括 v3 和 v4/5。它也是 Reach Router 的继任者。我们鼓励所有 React Router 和 Reach Router 的用户尽可能升级到 v6。我们对 v6 有一些大计划,当我们在 6.x 中引入一些非常酷的东西时,我们不希望您被排除在外!(是的,即使你v3 用户坚持你的onEnterhooks也不会想错过这个)。

但是,我们意识到让每个人都升级对于一组每月下载 3400 万次的库来说是一个非常雄心勃勃的目标。我们已经在为 React Router v5 用户开发一个向后兼容层,并将很快与多个客户进行测试。我们的计划是也为 Reach Router 用户开发一个类似的层。如果您有一个大型应用程序并且对升级到 v6 有所担忧,请不要担心。我们的向后兼容层正在开发中。此外,在可预见的未来,v5 将继续接收更新,所以不要着急。

如果您迫不及待并且想自己进行升级,这里有一些链接可以帮助您:

  • 从 v5 升级到 v6
  • 从 Reach Router迁移到 v6

除了官方升级指南,我还发布了一些注释,可以帮助您慢慢开始迁移。请记住,任何迁移的目标都是能够完成一些工作,然后将其发布。没有人喜欢长期运行的升级分支!

以下是关于已弃用模式的一些说明,以及您今天可以在尝试升级到 v6 之前在 v5 应用程序中实施的修复程序:

  • 在 v6 中处理重定向
  • 在 v6 中组合<Route>

同样,请不要因为执行迁移过程而感到有压力。我们认为 React Router v6 是我们构建过的最好的路由器,但您在工作中可能会遇到一些问题需要处理。当您准备升级时,我们会随时提供帮助。

如果您是 Reach Router 用户,会担心失去它提供的辅助功能,我们也在努力解决这个问题。事实证明,Reach Router 的自动焦点管理在某些情况下实际上比什么都不做更糟。我们意识到,为了正确管理焦点,我们需要更多的信息而不仅仅是位置变化。然而,这是一个值得的实验,我们学到了很多东西。我们的下一个项目将帮助您构建比以往任何时候都更易于访问的应用程序…

未来:Remix

React Router 为当今许多伟大和令人印象深刻的 Web 应用程序提供了基础。打开 Netflix、Twitter、Linear 或 Coinbase 等网络应用程序的开发者控制台,可以看到 React Router 被用于这些企业的旗舰应用程序,这对我来说是一种奇妙的感觉。这些公司中的每一家都拥有出色的人才和资源库,他们和许多其他公司选择在 React 和 React Router 上建立自己的业务。

人们真正喜欢 React Router 的一件事是:它可以完美胜任路由的职责并且不用出问题。它从未真正想要成为一个封闭的框架,因此它可以任用任何的技术栈。也许你是服务器渲染,也许不是。也许你是代码拆分,也许不是。也许您正在渲染一个带有客户端路由和数据的动态站点,或者您只是在渲染一堆静态页面。React Router 会很乐意做任何你想做的事。

但是如何构建应用程序的其余部分?路由只是一部分。数据加载和mutations呢?缓存和性能怎么处理?应该做服务器渲染吗?进行代码拆分的最佳方法是什么?您应该如何部署和托管您的应用程序?

碰巧的是,我们对所有这些都有一些非常强烈的意见。这就是我们构建 Remix的原因,这是一个新的网络框架,将帮助您构建更好的网站。

近年来,随着 Web 应用程序变得越来越复杂,前端 Web 开发团队承担了比以往任何时候都多得多的责任。他们不仅要知道如何编写 HTML、CSS 和 JavaScript。他们还需要了解 TypeScript、编译器和构建流水线。此外,他们需要了解bundlers和code splitting,并了解当客户浏览网站时应用程序如何加载。有很多事情要考虑!Remix 和令人惊叹的 Remix 社区将会为您团队提供额外帮助,可以帮您管理并就如何完成所有这些以及更多事情做出明智的选择。

我们已经在 Remix 上工作了一年多,最近获得了一些资金并聘请了一个团队来帮助我们构建它。我们将在年底之前根据开源许可证发布代码。而 React Router v6 是 Remix 的核心。随着 Remix 向前发展并变得越来越好,路由器也是如此。您将继续在 React Router 和 Remix 上看到我们源源不断的发布和改进。

我们非常感谢迄今为止我们所获得的所有支持,以及多年来信任我们的众多朋友和客户。我们真诚地希望您喜欢使用 React Router v6 和 Remix!

原文地址:remix.run/blog/react-…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文讲透自适应熔断的原理和实现

发表于 2021-11-16

为什么需要熔断

微服务集群中,每个应用基本都会依赖一定数量的外部服务。有可能随时都会遇到网络连接缓慢,超时,依赖服务过载,服务不可用的情况,在高并发场景下如果此时调用方不做任何处理,继续持续请求故障服务的话很容易引起整个微服务集群雪崩。
比如高并发场景的用户订单服务,一般需要依赖一下服务:

  1. 商品服务
  2. 账户服务
  3. 库存服务

假如此时 账户服务 过载,订单服务持续请求账户服务只能被动的等待账户服务报错或者请求超时,进而导致订单请求被大量堆积,这些无效请求依然会占用系统资源:cpu,内存,数据连接…导致订单服务整体不可用。即使账户服务恢复了订单服务也无法自我恢复。

这时如果有一个主动保护机制应对这种场景的话订单服务至少可以保证自身的运行状态,等待账户服务恢复时订单服务也同步自我恢复,这种自我保护机制在服务治理中叫熔断机制。

熔断

熔断是调用方自我保护的机制(客观上也能保护被调用方),熔断对象是外部服务。

降级

降级是被调用方(服务提供者)的防止因自身资源不足导致过载的自我保护机制,降级对象是自身。

熔断这一词来源时我们日常生活电路里面的熔断器,当负载过高时(电流过大)保险丝会自行熔断防止电路被烧坏,很多技术都是来自生活场景的提炼。

工作原理

熔断器一般具有三个状态:

  1. 关闭:默认状态,请求能被到达目标服务,同时统计在窗口时间成功和失败次数,如果达到错误率阈值将会进入断开状态。
  2. 断开: 此状态下将会直接返回错误,如果有 fallback 配置则直接调用 fallback 方法。
  3. 半断开:进行断开状态会维护一个超市时间,到达超时时间开始进入 半断开 状态,尝试允许一部门请求正常通过并统计成功数量,如果请求正常则认为此时目标服务已恢复进入 关闭 状态,否则进入 断开 状态。半断开 状态存在的目的在于实现了自我修复,同时防止正在恢复的服务再次被大量打垮。

使用较多的熔断组件:

  1. hystrix circuit breaker(不再维护)
  2. hystrix-go
  3. resilience4j(推荐)
  4. sentinel(推荐)

什么是自适应熔断

基于上面提到的熔断器原理,项目中我们要使用好熔断器通常需要准备以下参数:

  1. 错误比例阈值:达到该阈值进入 断开 状态。
  2. 断开状态超时时间:超时后进入 半断开 状态。
  3. 半断开状态允许请求数量。
  4. 窗口时间大小。

实际上可选的配置参数还有非常非常多,参考 https://resilience4j.readme.io/docs/circuitbreaker

对于经验不够丰富的开发人员而言,这些参数设置多少合适心里其实并没有底。

那么有没有一种自适应的熔断算法能让我们不关注参数,只要简单配置就能满足大部分场景?

其实是有的,google sre提供了一种自适应熔断算法来计算丢弃请求的概率:

算法参数:

  1. requests: 窗口时间内的请求总数
  2. accepts:正常请求数量
  3. K:敏感度,K 越小越容易丢请求,一般推荐 1.5-2 之间

算法解释:

  1. 正常情况下 requests=accepts,所以概率是 0。
  2. 随着正常请求数量减少,当达到 requests == K* accepts 继续请求时,概率 P 会逐渐比 0 大开始按照概率逐渐丢弃一些请求,如果故障严重则丢包会越来越多,假如窗口时间内 accepts==0 则完全熔断。
  3. 当应用逐渐恢复正常时,accepts、requests 同时都在增加,但是 K*accepts 会比 requests 增加的更快,所以概率很快就会归 0,关闭熔断。

代码实现

接下来思考一个熔断器如何实现。

初步思路是:

  1. 无论什么熔断器都得依靠指标统计来转换状态,而统计指标一般要求是最近的一段时间内的数据(太久的数据没有参考意义也浪费空间),所以通常采用一个 滑动时间窗口 数据结构 来存储统计数据。同时熔断器的状态也需要依靠指标统计来实现可观测性,我们实现任何系统第一步需要考虑就是可观测性,不然系统就是一个黑盒。
  2. 外部服务请求结果各式各样,所以需要提供一个自定义的判断方法,判断请求是否成功。可能是 http.code 、rpc.code、body.code,熔断器需要实时收集此数据。
  3. 当外部服务被熔断时使用者往往需要自定义快速失败的逻辑,考虑提供自定义的 fallback() 功能。

下面来逐步分析 go-zero 的源码实现:

core/breaker/breaker.go

熔断器接口定义

兵马未动,粮草先行,明确了需求后就可以开始规划定义接口了,接口是我们编码思维抽象的第一步也是最重要的一步。

核心定义包含两种类型的方法:

Allow():需要手动回调请求结果至熔断器,相当于手动挡。

DoXXX():自动回调请求结果至熔断器,相当于自动挡,实际上 DoXXX() 类型方法最后都是调用
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
go复制代码	// 自定义判定执行结果
Acceptable func(err error) bool

// 手动回调
Promise interface {
// Accept tells the Breaker that the call is successful.
// 请求成功
Accept()
// Reject tells the Breaker that the call is failed.
// 请求失败
Reject(reason string)
}

Breaker interface {
// 熔断器名称
Name() string

// 熔断方法,执行请求时必须手动上报执行结果
// 适用于简单无需自定义快速失败,无需自定义判定请求结果的场景
// 相当于手动挡。。。
Allow() (Promise, error)

// 熔断方法,自动上报执行结果
// 自动挡。。。
Do(req func() error) error

// 熔断方法
// acceptable - 支持自定义判定执行结果
DoWithAcceptable(req func() error, acceptable Acceptable) error

// 熔断方法
// fallback - 支持自定义快速失败
DoWithFallback(req func() error, fallback func(err error) error) error

// 熔断方法
// fallback - 支持自定义快速失败
// acceptable - 支持自定义判定执行结果
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error
}

熔断器实现

circuitBreaker 继承 throttle,实际上这里相当于静态代理,代理模式可以在不改变原有对象的基础上增强功能,后面我们会看到 go-zero 这样做的原因是为了收集熔断器错误数据,也就是为了实现可观测性。

熔断器实现采用静态代理模式,看起来稍微有点绕脑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
go复制代码// 熔断器结构体
circuitBreaker struct {
name string
// 实际上 circuitBreaker熔断功能都代理给 throttle来实现
throttle
}
// 熔断器接口
throttle interface {
// 熔断方法
allow() (Promise, error)
// 熔断方法
// DoXXX()方法最终都会该方法
doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error
}

func (cb *circuitBreaker) Allow() (Promise, error) {
return cb.throttle.allow()
}

func (cb *circuitBreaker) Do(req func() error) error {
return cb.throttle.doReq(req, nil, defaultAcceptable)
}

func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error {
return cb.throttle.doReq(req, nil, acceptable)
}

func (cb *circuitBreaker) DoWithFallback(req func() error, fallback func(err error) error) error {
return cb.throttle.doReq(req, fallback, defaultAcceptable)
}

func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback func(err error) error,
acceptable Acceptable) error {
return cb.throttle.doReq(req, fallback, acceptable)
}

throttle 接口实现类:

loggedThrottle 增加了为了收集错误日志的滚动窗口,目的是为了收集当请求失败时的错误日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
go复制代码// 带日志功能的熔断器
type loggedThrottle struct {
// 名称
name string
// 代理对象
internalThrottle
// 滚动窗口,滚动收集数据,相当于环形数组
errWin *errorWindow
}

// 熔断方法
func (lt loggedThrottle) allow() (Promise, error) {
promise, err := lt.internalThrottle.allow()
return promiseWithReason{
promise: promise,
errWin: lt.errWin,
}, lt.logError(err)
}

// 熔断方法
func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool {
accept := acceptable(err)
if !accept {
lt.errWin.add(err.Error())
}
return accept
}))
}

func (lt loggedThrottle) logError(err error) error {
if err == ErrServiceUnavailable {
// if circuit open, not possible to have empty error window
stat.Report(fmt.Sprintf(
"proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s",
proc.ProcessName(), proc.Pid(), lt.name, lt.errWin))
}

return err
}

错误日志收集 errorWindow

errorWindow 是一个环形数组,新数据不断滚动覆盖最旧的数据,通过取余实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
go复制代码// 滚动窗口
type errorWindow struct {
reasons [numHistoryReasons]string
index int
count int
lock sync.Mutex
}

// 添加数据
func (ew *errorWindow) add(reason string) {
ew.lock.Lock()
// 添加错误日志
ew.reasons[ew.index] = fmt.Sprintf("%s %s", timex.Time().Format(timeFormat), reason)
// 更新index,为下一次写入数据做准备
// 这里用的取模实现了滚动功能
ew.index = (ew.index + 1) % numHistoryReasons
// 统计数量
ew.count = mathx.MinInt(ew.count+1, numHistoryReasons)
ew.lock.Unlock()
}

// 格式化错误日志
func (ew *errorWindow) String() string {
var reasons []string

ew.lock.Lock()
// reverse order
for i := ew.index - 1; i >= ew.index-ew.count; i-- {
reasons = append(reasons, ew.reasons[(i+numHistoryReasons)%numHistoryReasons])
}
ew.lock.Unlock()

return strings.Join(reasons, "\n")
}

看到这里我们还没看到实际的熔断器实现,实际上真正的熔断操作被代理给了 internalThrottle 对象。

1
2
3
4
go复制代码	internalThrottle interface {
allow() (internalPromise, error)
doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error
}

internalThrottle 接口实现 googleBreaker 结构体定义

1
2
3
4
5
6
7
8
9
go复制代码type googleBreaker struct {
// 敏感度,go-zero中默认值为1.5
k float64
// 滑动窗口,用于记录最近一段时间内的请求总数,成功总数
stat *collection.RollingWindow
// 概率生成器
// 随机产生0.0-1.0之间的双精度浮点数
proba *mathx.Proba
}

可以看到熔断器属性其实非常简单,数据统计采用的是滑动时间窗口来实现。

RollingWindow 滑动窗口

滑动窗口属于比较通用的数据结构,常用于最近一段时间内的行为数据统计。

它的实现非常有意思,尤其是如何模拟窗口滑动过程。

先来看滑动窗口的结构体定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码	RollingWindow struct {
// 互斥锁
lock sync.RWMutex
// 滑动窗口数量
size int
// 窗口,数据容器
win *window
// 滑动窗口单元时间间隔
interval time.Duration
// 游标,用于定位当前应该写入哪个bucket
offset int
// 汇总数据时,是否忽略当前正在写入桶的数据
// 某些场景下因为当前正在写入的桶数据并没有经过完整的窗口时间间隔
// 可能导致当前桶的统计并不准确
ignoreCurrent bool
// 最后写入桶的时间
// 用于计算下一次写入数据间隔最后一次写入数据的之间
// 经过了多少个时间间隔
lastTime time.Duration
}

window 是数据的实际存储位置,其实就是一个数组,提供向指定 offset 添加数据与清除操作。
数组里面按照 internal 时间间隔分隔成多个 bucket。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
go复制代码// 时间窗口
type window struct {
// 桶
// 一个桶标识一个时间间隔
buckets []*Bucket
// 窗口大小
size int
}

// 添加数据
// offset - 游标,定位写入bucket位置
// v - 行为数据
func (w *window) add(offset int, v float64) {
w.buckets[offset%w.size].add(v)
}

// 汇总数据
// fn - 自定义的bucket统计函数
func (w *window) reduce(start, count int, fn func(b *Bucket)) {
for i := 0; i < count; i++ {
fn(w.buckets[(start+i)%w.size])
}
}

// 清理特定bucket
func (w *window) resetBucket(offset int) {
w.buckets[offset%w.size].reset()
}

// 桶
type Bucket struct {
// 当前桶内值之和
Sum float64
// 当前桶的add总次数
Count int64
}

// 向桶添加数据
func (b *Bucket) add(v float64) {
// 求和
b.Sum += v
// 次数+1
b.Count++
}

// 桶数据清零
func (b *Bucket) reset() {
b.Sum = 0
b.Count = 0
}

window 添加数据:

  1. 计算当前时间距离上次添加时间经过了多少个 时间间隔,实际上就是过期了几个 bucket。
  2. 清理过期桶的数据
  3. 更新 offset,更新 offset 的过程实际上就是在模拟窗口滑动
  4. 添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
go复制代码// 添加数据
func (rw *RollingWindow) Add(v float64) {
rw.lock.Lock()
defer rw.lock.Unlock()
// 获取当前写入的下标
rw.updateOffset()
// 添加数据
rw.win.add(rw.offset, v)
}

// 计算当前距离最后写入数据经过多少个单元时间间隔
// 实际上指的就是经过多少个桶
func (rw *RollingWindow) span() int {
offset := int(timex.Since(rw.lastTime) / rw.interval)
if 0 <= offset && offset < rw.size {
return offset
}
// 大于时间窗口时 返回窗口大小即可
return rw.size
}

// 更新当前时间的offset
// 实现窗口滑动
func (rw *RollingWindow) updateOffset() {
// 经过span个桶的时间
span := rw.span()
// 还在同一单元时间内不需要更新
if span <= 0 {
return
}
offset := rw.offset
// 既然经过了span个桶的时间没有写入数据
// 那么这些桶内的数据就不应该继续保留了,属于过期数据清空即可
// 可以看到这里全部用的 % 取余操作,可以实现按照下标周期性写入
// 如果超出下标了那就从头开始写,确保新数据一定能够正常写入
// 类似循环数组的效果
for i := 0; i < span; i++ {
rw.win.resetBucket((offset + i + 1) % rw.size)
}
// 更新offset
rw.offset = (offset + span) % rw.size
now := timex.Now()
// 更新操作时间
// 这里很有意思
rw.lastTime = now - (now-rw.lastTime)%rw.interval
}

window 统计数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码// 归纳汇总数据
func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {
rw.lock.RLock()
defer rw.lock.RUnlock()

var diff int
span := rw.span()
// 当前时间截止前,未过期桶的数量
if span == 0 && rw.ignoreCurrent {
diff = rw.size - 1
} else {
diff = rw.size - span
}
if diff > 0 {
// rw.offset - rw.offset+span之间的桶数据是过期的不应该计入统计
offset := (rw.offset + span + 1) % rw.size
// 汇总数据
rw.win.reduce(offset, diff, fn)
}
}

googleBreaker 判断是否应该熔断

  1. 收集滑动窗口内的统计数据
  2. 计算熔断概率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码// 按照最近一段时间的请求数据计算是否熔断
func (b *googleBreaker) accept() error {
// 获取最近一段时间的统计数据
accepts, total := b.history()
// 计算动态熔断概率
weightedAccepts := b.k * float64(accepts)
// https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101
dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
// 概率为0,通过
if dropRatio <= 0 {
return nil
}
// 随机产生0.0-1.0之间的随机数与上面计算出来的熔断概率相比较
// 如果随机数比熔断概率小则进行熔断
if b.proba.TrueOnProba(dropRatio) {
return ErrServiceUnavailable
}

return nil
}

googleBreaker 熔断逻辑实现

熔断器对外暴露两种类型的方法

  1. 简单场景直接判断对象是否被熔断,执行请求后必须需手动上报执行结果至熔断器。

func (b *googleBreaker) allow() (internalPromise, error)

  1. 复杂场景下支持自定义快速失败,自定义判定请求是否成功的熔断方法,自动上报执行结果至熔断器。

func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error

Acceptable 参数目的是自定义判断请求是否成功。

1
go复制代码Acceptable func(err error) bool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
go复制代码// 熔断方法
// 返回一个promise异步回调对象,可由开发者自行决定是否上报结果到熔断器
func (b *googleBreaker) allow() (internalPromise, error) {
if err := b.accept(); err != nil {
return nil, err
}

return googlePromise{
b: b,
}, nil
}

// 熔断方法
// req - 熔断对象方法
// fallback - 自定义快速失败函数,可对熔断产生的err进行包装后返回
// acceptable - 对本次未熔断时执行请求的结果进行自定义的判定,比如可以针对http.code,rpc.code,body.code
func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
// 判定是否熔断
if err := b.accept(); err != nil {
// 熔断中,如果有自定义的fallback则执行
if fallback != nil {
return fallback(err)
}

return err
}
// 如果执行req()过程发生了panic,依然判定本次执行失败上报至熔断器
defer func() {
if e := recover(); e != nil {
b.markFailure()
panic(e)
}
}()
// 执行请求
err := req()
// 判定请求成功
if acceptable(err) {
b.markSuccess()
} else {
b.markFailure()
}

return err
}

// 上报成功
func (b *googleBreaker) markSuccess() {
b.stat.Add(1)
}

// 上报失败
func (b *googleBreaker) markFailure() {
b.stat.Add(0)
}

// 统计数据
func (b *googleBreaker) history() (accepts, total int64) {
b.stat.Reduce(func(b *collection.Bucket) {
accepts += int64(b.Sum)
total += b.Count
})

return
}

资料

微软 azure 关于熔断器设计模式

索尼参考微软的文档开源的熔断器实现

go-zero 自适应熔断器文档

项目地址

github.com/zeromicro/g…

欢迎使用 go-zero 并 star 支持我们!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在 Spring Boot 中使用搜索引擎 Elastics

发表于 2021-11-16

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战


Elasticsearch 建立在 Apache Lucene 之上,于 2010 年由 Elasticsearch NV(现为 Elastic)首次发布。据 Elastic 网站称,它是一个分布式开源搜索和分析引擎,适用于所有类型的数据,包括文本、数值 、地理空间、结构化和非结构化。Elasticsearch 操作通过 REST API 实现。主要功能是:

  • 将文档存储在索引中,
  • 使用强大的查询搜索索引以获取这些文档,以及
  • 对数据运行分析函数。

Spring Data Elasticsearch 提供了一个简单的接口来在 Elasticsearch 上执行这些操作,作为直接使用 REST API 的替代方法。
在这里,我们将使用 Spring Data Elasticsearch 来演示 Elasticsearch 的索引和搜索功能,并在最后构建一个简单的搜索应用程序,用于在产品库存中搜索产品。

代码示例

本文附有 GitHub 上的工作代码示例。

Elasticsearch 概念

Elasticsearch 概念
了解 Elasticsearch 概念的最简单方法是用数据库进行类比,如下表所示:

Elasticsearch -> 数据库
索引 -> 表
文档 -> 行
文档 -> 列

我们要搜索或分析的任何数据都作为文档存储在索引中。在 Spring Data 中,我们以 POJO 的形式表示一个文档,并用注解对其进行修饰以定义到 Elasticsearch 文档的映射。

与数据库不同,存储在 Elasticsearch 中的文本首先由各种分析器处理。默认分析器通过常用单词分隔符(如空格和标点符号)拆分文本,并删除常用英语单词。

如果我们存储文本“The sky is blue”,分析器会将其存储为包含“术语”“sky”和“blue”的文档。我们将能够使用“blue sky”、“sky”或“blue”形式的文本搜索此文档,并将匹配程度作为分数。

除了文本之外,Elasticsearch 还可以存储其他类型的数据,称为 Field Type(字段类型),如文档中 mapping-types (映射类型)部分所述。

启动 Elasticsearch 实例

在进一步讨论之前,让我们启动一个 Elasticsearch 实例,我们将使用它来运行我们的示例。有多种运行 Elasticsearch 实例的方法:

  • 使用托管服务
  • 使用来自 AWS 或 Azure 等云提供商的托管服务
  • 通过在虚拟机集群中自己安装 Elasticsearch
  • 运行 Docker 镜像
    我们将使用来自 Dockerhub 的 Docker 镜像,这对于我们的演示应用程序来说已经足够了。让我们通过运行 Docker run 命令来启动 Elasticsearch 实例:
1
2
3
shell复制代码docker run -p 9200:9200 \
-e "discovery.type=single-node" \
docker.elastic.co/elasticsearch/elasticsearch:7.10.0

执行此命令将启动一个 Elasticsearch 实例,侦听端口 9200。我们可以通过点击 URL http://localhost:9200 来验证实例状态,并在浏览器中检查结果输出:

1
2
3
4
5
6
7
8
9
10
json复制代码{
"name" : "8c06d897d156",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "Jkx..VyQ",
"version" : {
"number" : "7.10.0",
...
},
"tagline" : "You Know, for Search"
}

如果我们的 Elasticsearch 实例启动成功,应该看到上面的输出。

使用 REST API 进行索引和搜索

Elasticsearch 操作通过 REST API 访问。 有两种方法可以将文档添加到索引中:

  • 一次添加一个文档,或者
  • 批量添加文档。

添加单个文档的 API 接受一个文档作为参数。

对 Elasticsearch 实例的简单 PUT 请求用于存储文档如下所示:

1
2
3
4
json复制代码PUT /messages/_doc/1
{
"message": "The Sky is blue today"
}

这会将消息 - “The Sky is blue today”存储为“messages”的索引中的文档。

我们可以使用发送到搜索 REST API 的搜索查询来获取此文档:

1
2
3
4
5
6
7
json复制代码GET /messages/search
{
"query":
{
"match": {"message": "blue sky"}
}
}

这里我们发送一个 match 类型的查询来获取匹配字符串“blue sky”的文档。我们可以通过多种方式指定用于搜索文档的查询。Elasticsearch 提供了一个基于 JSON 的 查询 DSL(Domain Specific Language - 领域特定语言)来定义查询。

对于批量添加,我们需要提供一个包含类似以下代码段的条目的 JSON 文档:

1
2
json复制代码POST /_bulk
{"index":{"_index":"productindex"}}{"_class":"..Product","name":"Corgi Toys .. Car",..."manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"..Product","name":"CLASSIC TOY .. BATTERY"...,"manufacturer":"ccf"}

使用 Spring Data 进行 Elasticsearch 操作

我们有两种使用 Spring Data 访问 Elasticsearch 的方法,如下所示:

  • Repositories:我们在接口中定义方法,Elasticsearch 查询是在运行时根据方法名称生成的。
  • ElasticsearchRestTemplate:我们使用方法链和原生查询创建查询,以便在相对复杂的场景中更好地控制创建 Elasticsearch 查询。

我们将在以下各节中更详细地研究这两种方式。

创建应用程序并添加依赖项

让我们首先通过包含 web、thymeleaf 和 lombok 的依赖项,使用 Spring Initializr 创建我们的应用程序。添加 thymeleaf 依赖项以便增加用户界面。

在 Maven pom.xml 中添加 spring-data-elasticsearch 依赖项:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>

连接到 Elasticsearch 实例

Spring Data Elasticsearch 使用 Java High Level REST Client (JHLC) 连接到 Elasticsearch 服务器。JHLC 是 Elasticsearch 的默认客户端。我们将创建一个 Spring Bean 配置来进行设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Configuration
@EnableElasticsearch
Repositories(basePackages
= "io.pratik.elasticsearch.repositories")@ComponentScan(basePackages = { "io.pratik.elasticsearch" })
public class ElasticsearchClientConfig extends
AbstractElasticsearchConfiguration {
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {


final ClientConfiguration clientConfiguration =
ClientConfiguration
.builder()
.connectedTo("localhost:9200")
.build();


return RestClients.create(clientConfiguration).rest();
}
}

在这里,我们连接到我们之前启动的 Elasticsearch 实例。我们可以通过添加更多属性(例如启用 ssl、设置超时等)来进一步自定义连接。

为了调试和诊断,我们将在 logback-spring.xml 的日志配置中打开传输级别的请求/响应日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public class Product {
@Id
private String id;

@Field(type = FieldType.Text, name = "name")
private String name;

@Field(type = FieldType.Double, name = "price")
private Double price;

@Field(type = FieldType.Integer, name = "quantity")
private Integer quantity;

@Field(type = FieldType.Keyword, name = "category")
private String category;

@Field(type = FieldType.Text, name = "desc")
private String description;

@Field(type = FieldType.Keyword, name = "manufacturer")
private String manufacturer;


...
}

表达文档

在我们的示例中,我们将按名称、品牌、价格或描述搜索产品。因此,为了将产品作为文档存储在 Elasticsearch 中,我们将产品表示为 POJO,并加上 Field 注解以配置 Elasticsearch 的映射,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public class Product {
@Id
private String id;

@Field(type = FieldType.Text, name = "name")
private String name;

@Field(type = FieldType.Double, name = "price")
private Double price;

@Field(type = FieldType.Integer, name = "quantity")
private Integer quantity;

@Field(type = FieldType.Keyword, name = "category")
private String category;

@Field(type = FieldType.Text, name = "desc")
private String description;

@Field(type = FieldType.Keyword, name = "manufacturer")
private String manufacturer;


...
}

@Document 注解指定索引名称。

@Id 注解使注解字段成为文档的 _id,作为此索引中的唯一标识符。id 字段有 512 个字符的限制。

@Field 注解配置字段的类型。我们还可以将名称设置为不同的字段名称。

在 Elasticsearch 中基于这些注解创建了名为 productindex 的索引。

使用 Spring Data Repository 进行索引和搜索

存储库提供了使用 finder 方法访问 Spring Data 中数据的最方便的方法。Elasticsearch 查询是根据方法名称创建的。但是,我们必须小心避免产生低效的查询并给集群带来高负载。

让我们通过扩展 ElasticsearchRepository 接口来创建一个 Spring Data 存储库接口:

1
2
3
4
java复制代码public interface ProductRepository
extends ElasticsearchRepository<Product, String> {

}

此处 ProductRepository 类继承了 ElasticsearchRepository 接口中包含的 save()、saveAll()、find() 和 findAll() 等方法。

索引

我们现在将通过调用 save() 方法存储一个产品,调用 saveAll() 方法来批量索引,从而在索引中存储一些产品。在此之前,我们将存储库接口放在一个服务类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Service
public class ProductSearchServiceWithRepo {


private ProductRepository productRepository;


public void createProductIndexBulk(final List<Product> products) {
productRepository.saveAll(products);
}


public void createProductIndex(final Product product) {
productRepository.save(product);
}
}

当我们从 JUnit 调用这些方法时,我们可以在跟踪日志中看到 REST API 调用索引和批量索引。

搜索

为了满足我们的搜索要求,我们将向存储库接口添加 finder 方法:

1
2
3
4
5
6
7
8
java复制代码public interface ProductRepository
extends ElasticsearchRepository<Product, String> {
List<Product> findByName(String name);

List<Product> findByNameContaining(String name);
List<Product> findByManufacturerAndCategory
(String manufacturer, String category);
}

在使用 JUnit 运行 findByName() 方法时,我们可以看到在发送到服务器之前在跟踪日志中生成的 Elasticsearch 查询:

1
2
shell复制代码TRACE Sending request POST /productindex/_search? ..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"apple","fields":["name^1.0"],..}

类似地,通过运行
findByManufacturerAndCategory() 方法,我们可以看到使用两个 query_string 参数对应两个字段——“manufacturer”和“category”生成的查询:

1
2
shell复制代码TRACE .. Sending request POST /productindex/_search..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"samsung","fields":["manufacturer^1.0"],..}},{"query_string":{"query":"laptop","fields":["category^1.0"],..}}],..}},"version":true}

有多种方法命名模式可以生成各种 Elasticsearch 查询。

使用 ElasticsearchRestTemplate进行索引和搜索

当我们需要更多地控制我们设计查询的方式,或者团队已经掌握了 Elasticsearch 语法时,Spring Data 存储库可能就不再适合。

在这种情况下,我们使用 ElasticsearchRestTemplate。它是 Elasticsearch 基于 HTTP 的新客户端,取代以前使用节点到节点二进制协议的 TransportClient。

ElasticsearchRestTemplate 实现了接口 ElasticsearchOperations,该接口负责底层搜索和集群操的繁杂工作。

索引

该接口具有用于添加单个文档的方法 index() 和用于向索引添加多个文档的 bulkIndex() 方法。此处的代码片段显示了如何使用 bulkIndex() 将多个产品添加到索引“productindex”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Service
@Slf4j
public class ProductSearchService {


private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;


public List<String> createProductIndexBulk
(final List<Product> products) {


List<IndexQuery> queries = products.stream()
.map(product->
new IndexQueryBuilder()
.withId(product.getId().toString())
.withObject(product).build())
.collect(Collectors.toList());;

return elasticsearchOperations
.bulkIndex(queries,IndexCoordinates.of(PRODUCT_INDEX));
}
...
}

要存储的文档包含在 IndexQuery 对象中。bulkIndex() 方法将 IndexQuery 对象列表和包含在 IndexCoordinates 中的 Index 名称作为输入。当我们执行此方法时,我们会获得批量请求的 REST API 跟踪:

1
2
3
4
shell复制代码Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex","_id":"383..35"}}{"_class":"..Product","id":"383..35","name":"New Apple..phone",..manufacturer":"apple"}
..
{"_class":"..Product","id":"d7a..34",.."manufacturer":"samsung"}

接下来,我们使用 index() 方法添加单个文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Service
@Slf4j
public class ProductSearchService {


private static final String PRODUCT_INDEX = "productindex";

private ElasticsearchOperations elasticsearchOperations;


public String createProductIndex(Product product) {


IndexQuery indexQuery = new IndexQueryBuilder()
.withId(product.getId().toString())
.withObject(product).build();


String documentId = elasticsearchOperations
.index(indexQuery, IndexCoordinates.of(PRODUCT_INDEX));


return documentId;
}
}

跟踪相应地显示了用于添加单个文档的 REST API PUT 请求。

1
2
shell复制代码Sending request PUT /productindex/_doc/59d..987..:
Request body: {"_class":"..Product","id":"59d..87",..,"manufacturer":"dell"}

搜索

ElasticsearchRestTemplate 还具有 search() 方法,用于在索引中搜索文档。此搜索操作类似于 Elasticsearch 查询,是通过构造 Query 对象并将其传递给搜索方法来构建的。

Query 对象具有三种变体 - NativeQueryy、StringQuery 和 CriteriaQuery,具体取决于我们如何构造查询。让我们构建一些用于搜索产品的查询。

NativeQuery

NativeQuery 为使用表示 Elasticsearch 构造(如聚合、过滤和排序)的对象构建查询提供了最大的灵活性。这是用于搜索与特定制造商匹配的产品的 NativeQuery:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@Service
@Slf4j
public class ProductSearchService {


private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;


public void findProductsByBrand(final String brandName) {


QueryBuilder queryBuilder =
QueryBuilders
.matchQuery("manufacturer", brandName);


Query searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.build();


SearchHits<Product> productHits =
elasticsearchOperations
.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX));
}
}

在这里,我们使用 NativeSearchQueryBuilder 构建查询,该查询使用 MatchQueryBuilder 指定包含字段“制造商”的匹配查询。

StringQuery

StringQuery 通过允许将原生 Elasticsearch 查询用作 JSON 字符串来提供完全控制,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Service
@Slf4j
public class ProductSearchService {


private static final String PRODUCT_INDEX = "productindex";
private ElasticsearchOperations elasticsearchOperations;


public void findByProductName(final String productName) {
Query searchQuery = new StringQuery(
"{\"match\":{\"name\":{\"query\":\""+ productName + "\"}}}\"");

SearchHits<Product> products = elasticsearchOperations.search(
searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX_NAME));
...
}
}

在此代码片段中,我们指定了一个简单的 match 查询,用于获取具有作为方法参数发送的特定名称的产品。

CriteriaQuery

使用 CriteriaQuery,我们可以在不了解 Elasticsearch 任何术语的情况下构建查询。查询是使用带有 Criteria 对象的方法链构建的。每个对象指定一些用于搜索文档的标准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Service
@Slf4j
public class ProductSearchService {


private static final String PRODUCT_INDEX = "productindex";

private ElasticsearchOperations elasticsearchOperations;


public void findByProductPrice(final String productPrice) {
Criteria criteria = new Criteria("price")
.greaterThan(10.0)
.lessThan(100.0);


Query searchQuery = new CriteriaQuery(criteria);


SearchHits<Product> products = elasticsearchOperations
.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX_NAME));
}
}

在此代码片段中,我们使用 CriteriaQuery 形成查询以获取价格大于 10.0 且小于 100.0 的产品。

构建搜索应用程序

我们现在将向我们的应用程序添加一个用户界面,以查看产品搜索的实际效果。用户界面将有一个搜索输入框,用于按名称或描述搜索产品。输入框将具有自动完成功能,以显示基于可用产品的建议列表,如下所示:

我们将为用户的搜索输入创建自动完成建议。然后根据与用户输入的搜索文本密切匹配的名称或描述搜索产品。我们将构建两个搜索服务来实现这个用例:

  • 获取自动完成功能的搜索建议
  • 根据用户的搜索查询处理搜索产品的搜索
    服务类 ProductSearchService 将包含搜索和获取建议的方法。

GitHub 存储库中提供了带有用户界面的成熟应用程序。

建立产品搜索索引

productindex 与我们之前用于运行 JUnit 测试的索引相同。我们将首先使用 Elasticsearch REST API 删除 productindex,以便在应用程序启动期间使用从我们的 50 个时尚系列产品的示例数据集中加载的产品创建新的 productindex:

1
shell复制代码curl -X DELETE http://localhost:9200/productindex

如果删除操作成功,我们将收到消息 {"acknowledged": true}。

现在,让我们为库存中的产品创建一个索引。我们将使用包含 50 种产品的示例数据集来构建我们的索引。这些产品在 CSV 文件中被排列为单独的行。

每行都有三个属性 - id、name 和 description。我们希望在应用程序启动期间创建索引。请注意,在实际生产环境中,索引创建应该是一个单独的过程。我们将读取 CSV 的每一行并将其添加到产品索引中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@SpringBootApplication
@Slf4j
public class ProductsearchappApplication {
...
@PostConstruct
public void buildIndex() {
esOps.indexOps(Product.class).refresh();
productRepo.saveAll(prepareDataset());
}


private Collection<Product> prepareDataset() {
Resource resource = new ClassPathResource("fashion-products.csv");
...
return productList;
}
}

在这个片段中,我们通过从数据集中读取行并将这些行传递给存储库的 saveAll() 方法以将产品添加到索引中来进行一些预处理。在运行应用程序时,我们可以在应用程序启动中看到以下跟踪日志。

1
2
3
shell复制代码...Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"Hornby 2014 Catalogue","description":"Product Desc..talogue","manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"FunkyBuys..","description":"Size Name:Lar..& Smoke","manufacturer":"FunkyBuys"}{"index":{"_index":"productindex"}}.
...

使用多字段和模糊搜索搜索产品

下面是我们在方法 processSearch() 中提交搜索请求时如何处理搜索请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码@Service
@Slf4j
public class ProductSearchService {


private static final String PRODUCT_INDEX = "productindex";


private ElasticsearchOperations elasticsearchOperations;


public List<Product> processSearch(final String query) {
log.info("Search with query {}", query);

// 1. Create query on multiple fields enabling fuzzy search
QueryBuilder queryBuilder =
QueryBuilders
.multiMatchQuery(query, "name", "description")
.fuzziness(Fuzziness.AUTO);


Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.build();


// 2. Execute search
SearchHits<Product> productHits =
elasticsearchOperations
.search(searchQuery, Product.class,
IndexCoordinates.of(PRODUCT_INDEX));


// 3. Map searchHits to product list
List<Product> productMatches = new ArrayList<Product>();
productHits.forEach(searchHit->{
productMatches.add(searchHit.getContent());
});
return productMatches;
}...
}

在这里,我们对多个字段执行搜索 - 名称和描述。 我们还附加了 fuzziness() 来搜索紧密匹配的文本以解释拼写错误。

使用通配符搜索获取建议

接下来,我们为搜索文本框构建自动完成功能。 当我们在搜索文本字段中输入内容时,我们将通过使用搜索框中输入的字符执行通配符搜索来获取建议。

我们在 fetchSuggestions() 方法中构建此函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Service
@Slf4j
public class ProductSearchService {


private static final String PRODUCT_INDEX = "productindex";


public List<String> fetchSuggestions(String query) {
QueryBuilder queryBuilder = QueryBuilders
.wildcardQuery("name", query+"*");


Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.withPageable(PageRequest.of(0, 5))
.build();


SearchHits<Product> searchSuggestions =
elasticsearchOperations.search(searchQuery,
Product.class,
IndexCoordinates.of(PRODUCT_INDEX));

List<String> suggestions = new ArrayList<String>();

searchSuggestions.getSearchHits().forEach(searchHit->{
suggestions.add(searchHit.getContent().getName());
});
return suggestions;
}
}

我们以搜索输入文本的形式使用通配符查询,并附加 * 以便如果我们输入“red”,我们将获得以“red”开头的建议。我们使用 withPageable() 方法将建议的数量限制为 5。可以在此处看到正在运行的应用程序的搜索结果的一些屏幕截图:

结论

在本文中,我们介绍了 Elasticsearch 的主要操作——索引文档、批量索引和搜索——它们以 REST API 的形式提供。Query DSL 与不同分析器的结合使搜索变得非常强大。

Spring Data Elasticsearch 通过使用 Spring Data Repositories 或 ElasticsearchRestTemplate 提供了方便的接口来访问应用程序中的这些操作。

我们最终构建了一个应用程序,在其中我们看到了如何在接近现实生活的应用程序中使用 Elasticsearch 的批量索引和搜索功能。


  • 本文译自: Using Elasticsearch with Spring Boot - Reflectoring
  • 有关 ELK 套件请参考: ELK 教程 - 发现、分析和可视化你的数据

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于领域模型的微服务划分--实战案例解析

发表于 2021-11-16

前言

微服务的最大挑战之一是定义各个服务的边界。 一般的规则是服务应该只做“一件事”(参考SRP原则) — 但是,实践这条规则需要经过认真的考虑。 没有任何机械性的流程可以生成“适当的”设计。 必须深入考虑业务领域、需求和目标。 否则,最终可能得到一个杂乱无章的设计,它呈现一些不需要的特征,例如隐藏服务之间的依赖关系、紧密耦合,或者设计不佳的界面。

在[迈入微服务架构的第一关–服务边界划分]一文中,简单介绍了基于领域的微服务划分方法。受篇幅所限,没能更深一步分析领域模型的构建,很多同学表示有一种似懂非懂的感觉。借用林群院士的话说:假传万卷书,真传一案例。一个好的案例能让人瞬间领悟真谛,胜过读万卷书。本文将借用微软Azure的官方案例–无人机快递服务,详细介绍如何基于领域模型划分微服务,希望对同学们有帮助。

DDD基础知识回顾

领域驱动设计 (DDD) 认为应该尽可能围绕业务功能而不是数据访问或消息传递等水平层来设计微服务。 此外,微服务应具有低耦合高内聚的特点。 如果在更新一个服务时无需同时更新其他服务,则该微服务是低耦合的。 如果微服务的职责单一且定义完善,例如管理用户帐户或跟踪投递历史记录,则它是高内聚的。 服务应该封装领域知识,使这些领域知识对于客户端而言处于抽象状态。 例如,客户端应该能够在无需知晓派遣算法或如何管理无人机群的情况下安排无人机。

DDD提供一个框架,可以让你顺利设计一组完善的微服务。 DDD 包括两个不同的阶段:战略和战术。 在 DDD 的战略模式中,可以定义系统的大规模结构。 战略模式有助于确保体系结构专注于业务功能。 战术性 DDD 提供一组可用于创建领域模型的设计模式。 这些模式包括实体、聚合和领域服务。 借助这些战术模式,可以设计低耦合高内聚的微服务。

域驱动设计 (DDD) 流程图

在本文和接下来的步骤中,我们将逐步执行以下步骤,将它们应用于无人机快递应用程序:

  1. 我们首先分析业务领域,以了解应用程序的功能要求。 该步骤输出领域的非正式说明,可将其优化成更正式的一组领域模型。
  2. 接下来,定义领域的限界上下文(Bounded Context)。 每个限界上下文包含一个领域模型,该模型表示大型系统的特定子域。
  3. 在限界上下文中,应用战术 DDD 模式以定义实体、聚合、领域服务。
  4. 使用前一步骤的结果标识出应用程序中的微服务。

请务必记住,DDD 是迭代的持续过程。 服务边界不是一成不变的。 随着应用程序的演变,你可以决定将某个服务分解成多个较小服务。

ℹ️ 本文未完整介绍全面的领域分析。 我们特意保留了简短内容来说明要点。 有关 DDD 的更多背景知识,我们建议阅读 Eric Evans 的 Domain-Driven Design(领域驱动设计),该书籍首次引入了该术语。 另一项优秀的参考资源是 Vaughn Vernon 撰写的《实现领域驱动设计》。也可以看考我的[领域驱动设计专栏]。

在 DDD 的战略阶段,我们要绘制业务领域的关系图,并定义领域模型的限界上下文。 在战术 DDD 阶段,需要更精确地定义领域模型。 战术模式在单个限界上下文中应用。 在微服务体系结构中,我们对实体和聚合模式特别感兴趣。 应用这些模式有助于识别应用程序中服务的自然边界。 作为一般原则,微服务应该不小于聚合,且不大于限界上下文。 首先,让我们了解战术模式。 然后,我们对无人机快递应用程序中的“交货”限界上下文应用这些模式。

战术模式概述

本部分将介绍战术 DDD 模式的简要概述,如果你已熟悉 DDD,则可以跳过本部分。 Eric Evans 著作的第 5 – 6 章,以及 Vaughn Vernon 的 Implementing Domain-Driven Design(实现域驱动的设计)一书中更详细地介绍了这些模式。

域驱动设计中的战术模式图

实体。 实体是一直保持唯一标识的对象。 例如,在银行应用程序中,客户和帐户就是实体。

  • 实体在系统中有唯一的标识符,使用该标识符可以查找和检索该实体。 这并不意味着,该标识符始终直接向用户公开。 它可能是数据库中的 GUID 或主键。
  • 一个标识可以跨多个限界上下文,并可能保留到应用程序生命期结束之后。 例如,银行帐号或政府颁发的身份证号不会与特定应用程序的生存期相关联。
  • 实体的属性可随时变化。 例如,某人的姓名或地址可能有变化,但他(她)仍是同一个人。
  • 一个实体可以包含对其他实体的引用。

值对象。 值对象没有标识。 它只由其属性值定义。 值对象也是不可变的。 若要更新值对象,始终需要创建一个新实例来替换旧实例。 值对象可以包含用于封装领域逻辑的方法,但这些方法不应该给对象的状态产生负面影响。 值对象的典型示例包括颜色、日期时间和货币值。

聚合。 聚合定义一个或多个实体的一致性边界。 一个聚合只包含一个根实体。 可以使用根实体的标识符执行查找。 从根开始的引用可以找到聚合中的其他任何实体。

聚合的作用是为事务一致性建模。 现实世界中的事物具有复杂的关系。 客户创建订单,订单包含产品,产品有供应商,等等。 如果应用程序修改了多个相关对象,它如何保证一致性?

传统应用程序通常使用数据库事务来实施一致性。 但是,在分布式应用程序中,这种做法通常不可行。 单个业务事务可能跨越多个数据存储、长时间运行,或者涉及第三方服务。 最终由应用程序而不是数据层来实施域所需的一致性。 这就是要为聚合建模的目的。

ℹ️ 聚合可以包含单个实体且不包含子实体。 聚合的定义由事务边界确定。

领域服务和应用服务。 在 DDD 术语中,服务是实现某种逻辑且不保存任何状态的对象。 Evans 区分 域服务(用于封装域逻辑)和 应用程序服务(提供技术功能,如用户身份验证或发送短信)。 领域服务通常用于对跨多个实体的行为建模。

ℹ️ 软件开发中广泛使用了“服务”一词。 此处的定义不直接与微服务相关。

领域事件。 发生某种情况时,可以使用领域事件来通知系统的其他部件。 顾名思义,领域事件应该表示领域中发生的某些情况。 例如,“在表中插入了记录”不是领域事件。 “已取消投递”是领域事件。 领域事件与微服务体系结构密切相关。 由于微服务为分发式且不共享数据存储,领域事件可为微服务提供相互协调的途径。

文章 服务间通信 更详细地讨论了异步消息传送。

还有其他几种 DDD 模式未在此处列出,包括工厂、Repository和模块。 开发微服务时,这些模式可能十分有用;但是,在微服务之间设计边界时,它们作用不大。

从领域模型到微服务

微服务的适当大小是什么? 我们经常听到有人说,“不要太大,也不要太小”— 这句话绝对正确,但实际上没有太大意义。 但是,如果从一个精心设计的领域模型着手,则规划出微服务就容易得多。

在为限界上下文标识了一组实体、聚合和领域服务之后,我们可以从领域模型转到应用程序设计。 下面是一个用于从域模型派生微服务的方法。

  1. 从限定上下文开始。 通常,微服务中的功能不应跨多个限定上下文。 根据定义,限界上下文标记特定领域模型的边界。 如果你发现微服务混用了不同的领域模型,可能意味着需要重新进行领域分析以优化领域模型。
  2. 接下来,查看领域模型中的聚合。 聚合通常是微服务的适当候选项。 合理设计的聚合能够体现一个设计优良的微服务的许多特征,例如:
* 聚合派生自业务要求,而不是数据访问或消息传递等技术因素。
* 聚合应具有较高的功能内聚性。
* 聚合是持久性的边界。
* 聚合应为松散耦合。
  1. 域服务也是微服务的适当候选项。 域服务是跨多个聚合的无状态操作。 典型的示例是涉及多个微服务的工作流。 我们将在无人机快递应用程序中看到此示例。
  2. 最后,考虑非功能性要求。 分析团队规模、数据类型、技术、可伸缩性、可用性和安全性需求等因素。 这些因素可能导致需要进一步将微服务分解成两个或更多个较小服务,或者相反的,将多个微服务合并成一个。

在应用程序中标识微服务之后,请根据以下条件验证设计:

  • 每个服务承担单一责任。
  • 服务之间不存在琐碎的调用。 如果将功能拆分成两个服务会导致它们过度琐碎,该症状的原因可能是这些功能属于同一个服务。
  • 每个服务足够小,独立工作的小团队即可构建它。
  • 两个或更多个服务的部署不应该存在相互依赖的关系。 应该始终可以在不重新部署其他任何服务的情况下部署某个服务。
  • 服务未紧密耦合,可独立演变。
  • 服务边界不会造成数据一致性或完整性方面的问题。 有时,必须通过将功能放入单个微服务来保持数据一致性。 话虽如此,但应该是否确实需要强一致性。 可通过某些策略来解决分布式系统中的最终一致性,分解服务的好处通常比管理最终一致性所存在的挑战更具效益。

最重要的是,必须追求实用,并记住领域驱动的设计是一个迭代过程。 如果有疑问,可以从粗粒度的微服务入手。 相比跨多个现有的微服务进行功能重构,将现有的微服务拆分成较小服务更加容易。

实战案例:无人机快递

Fabrikam, Inc. 正在推出无人机快递服务。 该公司经营无人机群。 各商家注册该服务,用户可以请求无人机收取要投递的商品。 当用户安排取件时,后端系统会分配一架无人机,并将估计的投递时间告知用户。 在投递过程中,用户可以通过持续更新的 ETA(Estimated time of arrival,预计到达时间) 跟踪无人机的位置。

此方案涉及到一个相当复杂的领域。 部分业务难题包括安排无人机、跟踪包裹、管理用户帐户,以及存储和分析历史数据。 此外,Fabrikam 希望快速投放市场扩张业务,同时添加新功能。 该应用程序需要在云环境运行,并附带较高的服务级别目标 (SLO)。 此外,Fabrikam 预期系统的不同部件在数据存储和查询方面具有截然不同的要求。 所有这些考虑因素促使 Fabrikam 为无人机快递应用程序选择了微服务体系结构。

分析领域 Analyze Domain

借助 DDD 方法设计微服务,可以使每个服务都能符合业务功能要求。 此方法有助于避免组织边界或技术选择左右你的设计。

在编写任何代码之前,需要获取所创建的系统的鸟瞰图。 DDD 首先构建业务领域并创建 域模型。 领域模型是业务领域的抽象模型。 它可以提取和组织领域知识,并为开发人员和领域专家提供通用语言。

首先,映射所有业务功能及它们之间的连接。 这可能需要领域专家、软件架构师和其他利益干系人的互相协作。 无需使用任何特定的形式。 可以直接草绘或者在白板上绘制关系图。

在绘制关系图时,可以开始标识离散的子域。 哪些功能密切相关? 哪些功能是业务的核心?哪些功能提供辅助服务? 什么是依赖项关系图? 在此初始阶段,不需要考虑技术或实施细节。 也就是说,应该注意应用程序要在哪个位置与 CRM、付款处理系统或计费系统等外部系统集成。

完成一些初始域分析之后,Fabrikam 团队绘制了一份描绘无人机快递领域的草图。

无人机快递领域图

  • 送货 (Shipping)位于关系图的中心,因为它是业务的核心。 关系图中的其他任何元素都是为了支持此功能。
  • 无人机管理 (Drone Management)也是业务的核心。 与无人机管理密切相关的功能包括 无人机维修,以及使用 预测分析 来预测无人机何时需要检修和维护。
  • ETA 分析 提供取件和投递的估计时间。
  • 如果包裹无法完全由无人机投递,则应用程序可以通过 第三方运输 来安排替代的运输方式。
  • 无人机共享 是核心业务的可能扩展。 公司的无人机在某些时段可能容量过剩,在这种情况下,可以出租无人机,以避免闲置。 初始版本未包括此功能。
  • 视频监督 (Video surveillance)是公司以后可以拓展到的另一个领域。
  • 用户帐户、开票 (Invoicing)和 呼叫中心 是支持核心业务的子域。

请注意,在此阶段,我们尚未做出有关实施或技术的任何决策。 某些子系统可能涉及到外部软件系统或第三方服务。 即便如此,应用程序也需要与这些系统和服务进行交互,因此,必须将它们包含在领域模型中。

ℹ️ 如果应用程序依赖于外部系统,则存在一种风险:外部系统的数据架构或 API 会渗入应用程序,最终暴露了体系结构设计。 不遵循最佳实践,并使用复杂数据架构或过时 API 的旧式系统尤其如此。 在这种情况下,必须在这些外部系统与应用程序之间妥善定义边界。 出于此目的,请考虑使用 扼杀者模式 或 防腐层模式 。

定义限界上下文 Define Bounded Context

领域模型将包含现实世界中事物的表示形式 — 用户、无人机、包裹等等。 但这并不意味着系统的每个部分都需要对相同的事物使用相同的表示形式。

例如,处理无人机维修和预测分析的子系统将需要表示无人机的许多物理特征,例如,其维护历史记录、里程、生产年份、型号、性能特征等。 但是,在安排投递时,我们并不需要关心这些方面。 计划子系统只需知道无人机是否可用,以及取件和交货的 ETA。

如果尝试为这两个子系统创建了单个模型,则会不必要地增大复杂性。 此外,模型会更难得到发展,因为任何更改都需要满足处理不同子系统的多个团队的要求。 因此,更好的做法通常是设计不同的模型,用于在两种不同的上下文中呈现相同的真实实体(在本例中为无人机)。 每个模型仅包含其特定上下文中相关的功能和属性。

这就是DDD限界上下文的 概念 发挥作用的地方。 限界上下文只是应用特定领域模型的领域中的边界。 在下图中,我们可以根据各种功能是否共享单个领域模型将功能分组。

image.png

限界上下文不一定相互独立。 在此图中,连接限界上下文的实线表示两个限界上下文交互的位置。 例如,“送货”依赖于“用户帐户”来获取有关客户的信息,并依赖于“无人机管理”来安排机群中的无人机。

在 Domain Driven Design(领域驱动设计)一书中,Eric Evans 描述了当某个领域模型与另一个限界上下文交互时,保持该模型完整性的多种模式。 微服务的主要原则之一是服务通过完善定义的 API 进行通信。 此方法对应于两种模式,即 Evans 所说的“开放主机服务(Open Host Service)”和“发布语言(Published Language)”。 “开放主机服务”的思路是子系统针对与它通信的其他子系统定义一个正式协议 (API)。 “发布语言”扩展了这种思路,以特定格式发布 API,其他团队可以直接用来编写客户端。 比如使用OpenAPI规范 ((以前称为 Swagger) )为 REST API 定义与语言无关的接口说明(以 JSON 或 YAML 格式表示)。

以下部分侧重于“送货”限界上下文。

定义实体、聚合及服务 Define Entities, Aggregates & Services

限界上下文图

首先,我们探讨“送货”限界上下文必须处理的场景。

  • 某个客户可以请求派遣无人机,到已在系统中注册的公司取件。
  • 寄件人生成了一个标记(条形码或 RFID)并粘贴在包裹上。
  • 无人机将会收取包裹,然后将包裹从起始位置投递到目标位置。
  • 当客户安排投递时,系统将会根据路线信息、天气情况和历史数据提供 ETA。
  • 当无人机起飞时,用户可以跟踪当前位置和最新的 ETA。
  • 在无人机收取包裹之前,客户可以取消投递。
  • 完成投递时,客户将收到通知。
  • 寄件人可以请求客户提供签名或指纹形式的收货确认信息。
  • 用户可以查找已完成投递的历史记录。

在这些场景中,开发团队确定了以下 实体。

  • 投递
  • 包裹
  • 无人机
  • 帐户
  • 确认
  • 通知
  • 标记

前四个项(“投递”、“包裹”、“无人机”和“帐户”)都是表示事务一致性边界的 聚合。 “确认”和“通知”是“投递”的子实体,“标记”是“包裹”的子实体。

此设计中的 值对象 包括 LOCATION、ETA、PackageWeight 和 PackageSize。

为便于演示,下面提供了“投递”聚合的 UML 关系图。 请注意,该聚合包含对其他聚合(包括“帐户”、“包裹”和“无人机”)的引用。

“投递”聚合的 UML 关系图

有两个领域事件:

  • 当无人机起飞时,“无人机”实体将发送 DroneStatus 事件,用于描述无人机的位置和状态(飞行中、已着陆)。
  • 每当投递阶段发生变化时,“投递”实体将发送 DeliveryTracking 事件。 这些事件包括 DeliveryCreated、DeliveryRescheduled、DeliveryHeadedToDropoff 和 DeliveryCompleted。

请注意,这些事件描述领域模型中有意义的事物。 它们描述有关域的某些信息,但不与特定的编程语言构造相关。

开发团队还确定了另一个功能领域,但该功能领域并不与前面所述的任何实体紧密相关。 系统的某个部分必须协调有关安排或更新投递的所有步骤。 因此,开发团队在设计中添加了两个 领域服务 :协调步骤的 计划程序 ,以及监视每个步骤的状态的 监督程序 ,以便检测是否有任何步骤失败或超时。这是 Scheduler Agent Supervisor模式的一个变体。修改后的域模型的关系图如下:

修改后的域模型的关系图

识别微服务 Identify Microservices

前面已标识四个聚合(“投递”、“包裹”、“无人机”和“帐户”)和两个领域服务(“计划程序”和“监督程序”)。

“投递”和“包裹”是微服务的优先候选项。 “计划程序”和“监督程序”协调其他微服务执行的活动,因此,将这些领域服务作为微服务比较有利。

“无人机”和“帐户”比较特别,它们属于其他限界上下文。 一种做法是让“计划程序”直接调用“无人机”和“帐户”限界上下文。 另一种做法是在“送货”限界上下文中创建“无人机”和“帐户”微服务。 这些微服务通过公开更适合“送货”上下文的 API 或数据架构,在限界上下文之间充当中介。

“无人机”和“帐户”限界上下文的详细信息超出了本文的范畴,因此我们在参考实现中创建了它们的模拟服务。 但在此情况下,需考虑一些因素:

  • 直接调入其他限界上下文会产生多大的网络开销?
  • 其他限界上下文的数据架构是否适用于此上下文,或者,专门针对此限界上下文定制一个架构是否更好?
  • 其他限界上下文是否为旧式系统? 如果是,则可以创建一个充当防腐层的服务,用于在旧式系统与新式应用程序之间进行转换。
  • 团队结构是什么? 是否能够方便地与负责其他限界上下文的团队通信? 如果不是,创建一个充当两个上下文之间的中介的服务可能有助于降低跨团队通信所产生的成本。

到目前为止,我们尚未考虑任何非功能性要求。 考虑到应用程序的吞吐量要求,开发团队决定创建一个负责接入客户端请求的独立“接入”微服务。 此微服务将传入的请求放入缓冲区进行处理,以此实施负载调节。 计划程序将从缓冲区读取请求,并执行工作流。

非功能性要求使得团队必须额外创建一个服务。 到目前为止,所有服务都与包裹的实时安排和投递过程相关。 但是,系统还需要在长期存储中存储每项投递的历史记录,以进行数据分析。 团队认为这是投递服务的责任。 但是,历史分析与现行操作的数据存储要求有较大的差别。 因此,团队决定创建一个独立的投递历史记录服务,用于侦听来自投递服务的 DeliveryTracking 事件,并将这些事件写入长期存储。

到这里微服务的划分基本完成,下图展示了最终的微服务设计:

设计图

参考

  • Domain-Driven Design(领域驱动设计) Eric Evans
  • Domain Modeling for Microservices
  • 扼杀者模式
  • 防腐层模式

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

mysql 基本知识及常用命令汇总(1)

发表于 2021-11-16

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

MySQL 是最流行的关系型数据库管理系统,RDBMS(Relational Database Management System:关系数据库管理系统)。

什么是数据库

数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。所谓的关系型数据库,是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据。

RDBMS 即关系数据库管理系统(Relational Database Management System)的特点:

  • 1.数据以表格的形式出现
  • 2.每行为各种记录名称
  • 3.每列为记录名称所对应的数据域
  • 4.许多的行和列组成一张表单
  • 5.若干的表单组成database

RDBMS 术语

在我们开始学习MySQL 数据库前,让我们先了解下RDBMS的一些术语:

  • 数据库: 数据库是一些关联表的集合。
  • 数据表: 表是数据的矩阵。在一个数据库中的表看起来像一个简单的电子表格。
  • 列: 一列(数据元素) 包含了相同类型的数据, 例如邮政编码的数据。
  • 行: 一行(=元组,或记录)是一组相关的数据,例如一条用户订阅的数据。
  • 冗余:存储两倍数据,冗余降低了性能,但提高了数据的安全性。
  • 主键:主键是唯一的。一个数据表中只能包含一个主键。你可以使用主键来查询数据。
  • 外键: 外键用于关联两个表。
  • 复合键:复合键(组合键)将多个列作为一个索引键,一般用于复合索引。
  • 索引: 使用索引可快速访问数据库表中的特定信息。索引是对数据库表中一列或多列的值进行排序的一种结构。类似于书籍的目录。
  • 参照完整性: 参照的完整性要求关系中不允许引用不存在的实体。与实体完整性是关系模型必须满足的完整性约束条件,目的是保证数据的一致性。

MySQL访问

C/S模式

  • 服务器端———mysqld.exe
  • 客户端————mysql.exe

1、开启MySQL服务(服务器端)
(1)“服务”中启/停
(2)命令提示符(管理员打开)
net start mysql80
net stop mysql80

2、客户端连接
客户端需要连接认证:
-h:主机地址(本机可省略)
-P:端口号(默认3306可省略)
-u:
-p:
(1)连接到本机上的MySQL
mysql -uroot -p123456
mysql -u root -p123456
mysql -u root -p-——》写入批处理文件中.bat(记事本编辑)
(2)连接到远程主机上的mysql
mysql -h远程主机的ip地址 -u root -p
3、退出连接
exit

数据类型

常用数据类型:

1
2
3
4
5
6
js复制代码int            //age
float(m,n) //price float(6,2)
char //固定长度 char(6)邮政编码,手机号码11, userName char(20)
varchar //可变长字符串 userName varchar(20)
text //大文本,一篇文章
datetime //日期时间类型:YYYY-MM-DD HH:MM:SS

更多数据类型:

1. 数值类型

类型 大小 范围(有符号) 范围(无符号) 用途
TINYINT 1 Bytes (-128,127) (0,255) 小整数值
SMALLINT 2 Bytes (-32 768,32 767) (0,65 535) 大整数值
MEDIUMINT 3 Bytes (-8 388 608,8 388 607) (0,16 777 215) 大整数值
INT或INTEGER 4 Bytes (-2 147 483 648,2 147 483 647) (0,4 294 967 295) 大整数值
BIGINT 8 Bytes (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) (0,18 446 744 073 709 551 615) 极大整数值
FLOAT 4 Bytes (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) 0,(1.175 494 351 E-38,3.402 823 466 E+38) 单精度 浮点数值
DOUBLE 8 Bytes (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) 0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) 双精度 浮点数值
DECIMAL 对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2 依赖于M和D的值 依赖于M和D的值 小数值

2.日期和时间类型

每个时间类型有一个有效值范围和一个”零”值,当指定不合法的MySQL不能表示的值时使用”零”值。
TIMESTAMP类型有专有的自动更新特性。

类型 大小 ( bytes) 范围 格式 用途
DATE 3 1000-01-01/9999-12-31 YYYY-MM-DD 日期值
TIME 3 ‘-838:59:59’/‘838:59:59’ HH:MM:SS 时间值或持续时间
YEAR 1 1901/2155 YYYY 年份值
DATETIME 8 1000-01-01 00:00:00/9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 混合日期和时间值
TIMESTAMP 4 1970-01-01 00:00:00/2038结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07 YYYYMMDD HHMMSS 混合日期和时间值,时间戳

3. 字符串类型

类型 大小 用途
CHAR 0-255 bytes 定长字符串
VARCHAR 0-65535 bytes 变长字符串
TINYBLOB 0-255 bytes 不超过 255 个字符的二进制字符串
TINYTEXT 0-255 bytes 短文本字符串
BLOB 0-65 535 bytes 二进制形式的长文本数据
TEXT 0-65 535 bytes 长文本数据
MEDIUMBLOB 0-16 777 215 bytes 二进制形式的中等长度文本数据
MEDIUMTEXT 0-16 777 215 bytes 中等长度文本数据
LONGBLOB 0-4 294 967 295 bytes 二进制形式的极大文本数据
LONGTEXT 0-4 294 967 295 bytes 极大文本数据

注意:char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。

CHAR 和 VARCHAR 类型类似,但它们保存和检索的方式不同。它们的最大长度和是否尾部空格被保留等方面也不同。在存储或检索过程中不进行大小写转换。

BINARY 和 VARBINARY 类似于 CHAR 和 VARCHAR,不同的是它们包含二进制字符串而不要非二进制字符串。也就是说,它们包含字节字符串而不是字符字符串。这说明它们没有字符集,并且排序和比较基于列值字节的数值值。

BLOB 是一个二进制大对象,可以容纳可变数量的数据。有 4 种 BLOB 类型:TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB。它们区别在于可容纳存储范围不同。

有 4 种 TEXT 类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。对应的这 4 种 BLOB 类型,可存储的最大长度不同,可根据实际情况选择。

数据库操作命令

1、创建数据库:

1
js复制代码create database [if not exists] dbName;

2、删除数据库:

1
ini复制代码drop database [if exists] dbName;

3、修改数据库:

1
ini复制代码alter database daName;

4、选择数据库:

在你连接到 MySQL 数据库后,可能有多个可以操作的数据库,所以你需要选择你要操作的数据库。

1
js复制代码use dbName;

5、查看数据库

1
js复制代码show databases;

数据表操作

1、创建数据表

  • 表名
  • 表字段名
  • 定义每个表字段
1
js复制代码CREATE TABLE table_name (column_name column_type);

常用的属性约束:

(1)非空约束

如果你不想字段为 NULL 可以设置字段的属性为 NOT NULL, 在操作数据库时如果输入该字段的数据为NULL ,就会报错。

1
2
3
java复制代码stuNu int not null,
stuName varchar(20) not null,
stuBirth datetime null(/不写)

(2)主键约束(primary key/PK)
唯一;非空;每张表都有主键(规范);可修改,一般不改
多字段主键(复合主键)
stuNu int primary key

(3)自动增长 auto_increment

AUTO_INCREMENT定义列为自增的属性,一般用于主键,数值会自动加1。

1
sql复制代码stuNu int primary key auto_increment
  • 自动增长/删除的值不再用/可能不连续
  • 置初值
1
2
3
js复制代码create table student(
stuNu int primary key auto_increment,
)auto_increment=201900

(4)默认值约束(default)

1
java复制代码stuSex char(2) not null default '男',

(5)唯一约束( unique)

1
scss复制代码userName varchar(20) unique,

(6)外键约束(foreign key constraint/FK)

1
复制代码student(stuId,stuName,stuAge,gradeId,gradeName,createDate,master)

实例:创建数据表 teacher:

1
2
3
4
5
6
js复制代码create table teacher(
-> id INT UNSIGNED AUTO_INCREMENT,
-> tName char(20) not null,
-> scoreName char(20) not null ,
-> teachYear int not null
-> )ENGINE=InnoDB DEFAULT CHARSET=utf8;

实例解析:

  • PRIMARY KEY关键字用于定义列为主键。
  • 您可以使用多列来定义主键,列间以逗号分隔。
  • ENGINE 设置存储引擎,CHARSET 设置编码。
  • MySQL命令终止符为分号 ;
  • -> 是换行符标识,不要复制。

2、显示数据表名称

1
js复制代码show tables;

3、查看建表语句

1
js复制代码show create table tbName;

4、查看表结构

1
js复制代码desc tableName;

5、删除数据表

1
js复制代码drop table tableName;

mysql数据库引擎

数据库引擎的定义:

是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySQL的核心就是插件式存储引擎。

常见的 mysql 数据库引擎

  • MyISAM: 拥有较高的插入,查询速度,但不支持事务。插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用。
  • InnoDB:5.5版本后Mysql的默认数据库,事务型数据库的首选引擎,支持ACID事务,支持行级锁定,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。
  • Memory:所有的数据都在内存中,拥有极高的插入,更新和查询效率,但是会占用和数据量成正比的内存空间。并且其内容会在Mysql重新启动时丢失,安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。
  • Archive :非常适合存储大量的独立的,作为历史记录的数据。因为它们不经常被读取。Archive拥有高效的插入速度,但其对查询的支持相对较差。Archive是归档的意思,在归档之后很多的高级功能就不再支持了,仅仅支持最基本的插入和查询两种功能。在MySQL 5.5版以前,Archive是不支持索引,但是在MySQL 5.5以后的版本中就开始支持索引了。Archive拥有很好的压缩机制,它使用zlib压缩库,在记录被请求时会实时压缩,所以它经常被用来当做仓库使用。

以上四种引擎的对比:

存储引擎的对比
特性 InnoDB MyISAM MEMORY Archive
事务安全 支持 无 无 无
存储限制 64TB 有 有 无
空间使用 高 低 低 高
内存使用 高 低 高 高
插入数据的速度 低 高 高 高
对外键的支持 支持 无 无 无
支持全文索引 无 支持 无 无
支持数索引 支持 支持 支持 无
支持哈希索引 无 无 支持 无
支持数据缓存 支持 无 无 无

其他引擎:

  • Federated: 将不同的Mysql服务器联合起来,逻辑上组成一个完整的数据库。非常适合分布式应用。Federated存储引擎可以使你在本地数据库中访问远程数据库中的数据,针对federated存储引擎表的查询会被发送到远程数据库的表上执行,本地是不存储任何数据的。
  • Cluster/NDB :高冗余的存储引擎,用多台数据机器联合提供服务以提高整体性能和安全性。适合数据量大,安全和性能要求高的应用
  • CSV: 逻辑上由逗号分割数据的存储引擎。它会在数据库子目录里为每个数据表创建一个.CSV文件。这是一种普通文本文件,每个数据行占用一个文本行。CSV存储引擎不支持索引。
  • BlackHole :黑洞引擎,写入的任何数据都会消失,一般用于记录binlog做复制的中继
  • BDB: 源自Berkeley DB,事务型数据库的另一种选择,支持COMMIT和ROLLBACK等其他事务特性

查看存储引擎

1
ini复制代码SHOW ENGINES;

注意:
在MySQL中,不需要在整个服务器中使用同一种存储引擎,针对具体的要求,可以对每一个表使用不同的存储引擎。

建表后的表操作修改

1、修改表名

1
css复制代码alter table tabName rename to newTabName;

2、添加字段

1
sql复制代码alter table tabName add stuName varchar(20) not null;

3、修改字段

1
sql复制代码alter table tabName change stuName newStuName varchar(50) null;

4、删除字段

1
sql复制代码alter table tabName drop stuName;

5、添加主键约束

1
sql复制代码alter table tabName add constraint pk_stuId primary key tabName(strId)

6、添加外键约束

1
sql复制代码alter table tabName add constraint fk_student_grade foreign key(gradeId) references grade(gradeId)

7、添加唯一约束

1
sql复制代码ALTER TABLE tabName ADD UNIQUE (username);

8、添加默认约束

1
sql复制代码ALTER TABLE tabName ALTER age SET DEFAULT 15;

删除默认约束

1
sql复制代码ALTER TABLE user2 ALTER age DROP DEFAULT;

9、插入数据

1
2
3
sql复制代码INSERT INTO table_name ( field1, field2,...fieldN )
VALUES
( value1, value2,...valueN );

10、查询数据

语法:

1
2
3
4
css复制代码SELECT column_name,column_name
FROM table_name
[WHERE Clause]
[LIMIT N][ OFFSET M]
  • 查询语句中你可以使用一个或者多个表,表之间使用逗号(,)分割,并使用WHERE语句来设定查询条件。
  • SELECT 命令可以读取一条或者多条记录。
  • 你可以使用星号(*)来代替其他字段,SELECT语句会返回表的所有字段数据
  • 你可以使用 WHERE 语句来包含任何条件。
  • 你可以使用 LIMIT 属性来设定返回的记录数。
  • 你可以通过OFFSET指定SELECT语句开始查询的数据偏移量。默认情况下偏移量为0。

11、where 子句

语法:

1
2
sql复制代码SELECT field1, field2,...fieldN FROM table_name1, table_name2...
[WHERE condition1 [AND [OR]] condition2.....
  • 查询语句中你可以使用一个或者多个表,表之间使用逗号, 分割,并使用WHERE语句来设定查询条件。
  • 你可以在 WHERE 子句中指定任何条件。
  • 你可以使用 AND 或者 OR 指定一个或多个条件。
  • WHERE 子句也可以运用于 SQL 的 DELETE 或者 UPDATE 命令。
  • WHERE 子句类似于程序语言中的 if 条件,根据 MySQL 表中的字段值来读取指定的数据。
1
sql复制代码SELECT * from runoob_tbl WHERE BINARY runoob_author='runoob.com';

11、修改数据

语法:

1
2
sql复制代码UPDATE table_name SET field1=new-value1, field2=new-value2
[WHERE Clause]
  • 你可以同时更新一个或多个字段。
  • 你可以在 WHERE 子句中指定任何条件。
  • 你可以在一个单独表中同时更新数据。
1
js复制代码UPDATE runoob_tbl SET runoob_title='学习 C++' WHERE runoob_id=3;

12、删除数据

1
sql复制代码DELETE FROM table_name [WHERE Clause]
  • 如果没有指定 WHERE 子句,MySQL 表中的所有记录将被删除。
  • 你可以在 WHERE 子句中指定任何条件
  • 您可以在单个表中一次性删除记录。

13、like 语句

1
2
3
sql复制代码SELECT field1, field2,...fieldN 
FROM table_name
WHERE field1 LIKE condition1 [AND [OR]] filed2 = 'somevalue'
  • 你可以在 WHERE 子句中指定任何条件。
  • 你可以在 WHERE 子句中使用LIKE子句。
  • 你可以使用LIKE子句代替等号 =。
  • LIKE 通常与 % 一同使用,类似于一个元字符的搜索。
  • 你可以使用 AND 或者 OR 指定一个或多个条件。
  • 你可以在 DELETE 或 UPDATE 命令中使用 WHERE…LIKE 子句来指定条件。

实例:

1
js复制代码SELECT * from runoob_tbl WHERE runoob_author LIKE '%COM';

o(╥﹏╥)o 今天有点累了,先不整理了,后边还有分组,连表查询,函数使用,排序,正则等,。

未完待续……

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go 语言中的label使用

发表于 2021-11-16

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战。

Go 语言中有 goto 这个功能,这个功能会影响代码的可读性,会让代码结构看起来比较乱。但是在处理多级嵌套时又非常有用。

最近有次阅读代码,就看到了这样的 case , 那就说一下这个功能吧。

Go语言也支持label(标签)语法:分别是break label和 goto label 、continue label

goto

goto 可以无条件的跳转执行的位置,但是不能跨函数,需要配合标签使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package gotocase

import (
"fmt"
"testing"
)

func TestGoto(t *testing.T) {
fmt.Println(1)
goto three //跳转
fmt.Println(2) // 这行将会被跳过
three:
fmt.Println(3)
}

执行结果如下:

1
2
3
4
5
xml复制代码=== RUN   TestGoto
1
3
--- PASS: TestGoto (0.00s)
PASS

goto 标签放上面,下面都可以的.

看下面的例子

1
2
3
4
5
6
7
8
go复制代码func TestGoto1(t *testing.T) {
one:
fmt.Println(1)
goto one //跳转
fmt.Println(2) // 这行将会被跳过

fmt.Println(3)
}

执行结果, 不断循环打印。1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1

break

break 一般用来跳出当前所在的循环, 但是我们有业务场景,需要使用到 跳出带外层循环怎么办?break label 跳出循环不再执行for循环里的代码。

可以使用 break 加标签的方式,举个例子。

1
2
3
4
5
6
7
8
9
10
11
go复制代码func TestBreak(t *testing.T) {
OUTER:
for {
fmt.Println(1)
for {
fmt.Println(2)
break OUTER
}
}
fmt.Println(3)
}

break标签只能用于for循环,不能和switch使用,在其他语言里switch与break是搭档

执行结果

1
2
3
4
5
6
diff复制代码=== RUN   TestBreak
1
2
3
--- PASS: TestBreak (0.00s)
PASS

这里要注意 一点
break label,break 的跳转标签(label)必须放在循环语句for前.

比如说, 下面的代码是不允许的

1
2
3
4
5
6
7
8
9
10
11
go复制代码func TestBreak1(t *testing.T) {
for {
fmt.Println(1)
for {
fmt.Println(2)
break OUTER
}
}
OUTER:
fmt.Println(3)
}

IDE 也会告诉你异常
在这里插入图片描述

continue

continue label 这个功能和 break 优点类似,区别在于 break 是强制终止, continue 是继续循环下一个迭代。

看个用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func TestContinue(t *testing.T) {
a := 10
Label:
for a < 20 {
if a == 15 {
a++
//fmt.Println(a)
continue Label
}
fmt.Println(a)
a++
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
diff复制代码=== RUN   TestContinue
10
11
12
13
14
16
17
18
19
--- PASS: TestContinue (0.00s)
PASS

欢迎关注工作号:程序员财富自由之路

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Oracle获取执行计划的方法(六脉神剑) 一、explai

发表于 2021-11-16

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

一、explain plan for

1
2
sql复制代码1、explain plan for &sql_text;
2、select * from table(dbms_xplan.display());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
sql复制代码SQL> set line222
SQL> set pagesize1000
SQL> explain plan for select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000;

Explained.

SQL> select * from table(dbms_xplan.display());

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 615168685

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 12 | 1404 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN | | 12 | 1404 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPT | 4 | 120 | 3 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| EMP | 12 | 1044 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - access("T1"."DEPTNO"="T2"."DEPTNO")
3 - filter("SAL">1000)

Note
-----
- dynamic sampling used for this statement (level=2)

20 rows selected.

_优点:_无需真正执行,方便快捷,与PLSQL工具的F5一样

_缺点:_没有相关统计信息输出;无法判断处理多少行;无法判断表被访问多少次。

二、set autotrace on

1
2
3
sql复制代码1、set autotrace traceonly
2、执行sql
3、set autotrace off
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
sql复制代码SQL> set autotrace -h
Usage: SET AUTOT[RACE] {OFF | ON | TRACE[ONLY]} [EXP[LAIN]] [STAT[ISTICS]]

SET AUTOTRACE OFF ---------------- 不生成AUTOTRACE 报告,这是缺省模式
SET AUTOTRACE ON EXPLAIN ------ AUTOTRACE只显示优化器执行路径报告
SET AUTOTRACE ON STATISTICS -- 只显示执行统计信息
SET AUTOTRACE ON ----------------- 包含执行计划和统计信息
SET AUTOTRACE TRACEONLY ------ 同set autotrace on,但是不显示查询输出

SQL> set autotrace traceonly
SQL> select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000;

12 rows selected.


Execution Plan
----------------------------------------------------------
Plan hash value: 615168685

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 12 | 1404 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN | | 12 | 1404 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPT | 4 | 120 | 3 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| EMP | 12 | 1044 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - access("T1"."DEPTNO"="T2"."DEPTNO")
3 - filter("SAL">1000)

Note
-----
- dynamic sampling used for this statement (level=2)


Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
15 consistent gets
0 physical reads
0 redo size
1935 bytes sent via SQL*Net to client
519 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
12 rows processed

_优点:_可以输出相关统计信息;可以通过traceonly来控制执行结果是否输出。

_缺点:_必须等到sql语句执行完毕;无法看到表被访问次数。

三、statistics_level=all

1
2
3
4
5
6
7
8
9
sql复制代码两种方式:
1、alter session set statistics_level=all;
2、执行sql
3、select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

或者执行sql时加上hint 'gather_plan_statistics'

1、select /*+ gather_plan_statistics */ * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000;
2、select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
sql复制代码SQL> alter session set statistics_level=all;

SQL> show parameter statistics_level

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
statistics_level string ALL

SQL> SELECT statistics_name
2 ,session_status
3 ,system_status
4 ,activation_level
5 ,session_settable
6 FROM v$statistics_level
7 ORDER BY statistics_name;

STATISTICS_NAME SESSION_ SYSTEM_S ACTIVAT SES
---------------------------------------------------------------- -------- -------- ------- ---
Active Session History ENABLED ENABLED TYPICAL NO
Adaptive Thresholds Enabled ENABLED ENABLED TYPICAL NO
Automated Maintenance Tasks ENABLED ENABLED TYPICAL NO
Bind Data Capture ENABLED ENABLED TYPICAL NO
Buffer Cache Advice ENABLED ENABLED TYPICAL NO
Global Cache CPU Statistics DISABLED DISABLED ALL NO
Global Cache Statistics ENABLED ENABLED TYPICAL NO
Longops Statistics ENABLED ENABLED TYPICAL NO
MTTR Advice ENABLED ENABLED TYPICAL NO
Modification Monitoring ENABLED ENABLED TYPICAL NO
PGA Advice ENABLED ENABLED TYPICAL NO
Plan Execution Sampling ENABLED ENABLED TYPICAL YES
Plan Execution Statistics ENABLED DISABLED ALL YES
SQL Monitoring ENABLED ENABLED TYPICAL YES
Segment Level Statistics ENABLED ENABLED TYPICAL NO
Shared Pool Advice ENABLED ENABLED TYPICAL NO
Streams Pool Advice ENABLED ENABLED TYPICAL NO
Threshold-based Alerts ENABLED ENABLED TYPICAL NO
Time Model Events ENABLED ENABLED TYPICAL YES
Timed OS Statistics ENABLED DISABLED ALL YES
Timed Statistics ENABLED ENABLED TYPICAL YES
Ultrafast Latch Statistics ENABLED ENABLED TYPICAL NO
Undo Advisor, Alerts and Fast Ramp up ENABLED ENABLED TYPICAL NO
V$IOSTAT_* statistics ENABLED ENABLED TYPICAL NO

24 rows selected.

SQL> alter session set statistics_level=all;

Session altered.

SQL> select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000;

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO DEPTNO DNAME LOC
---------- ---------- --------- ---------- ------------------ ---------- ---------- ---------- ---------- -------------- -------------
7499 ALLEN SALESMAN 7698 20-FEB-81 1600 300 30 30 SALES CHICAGO
7521 WARD SALESMAN 7698 22-FEB-81 1250 500 30 30 SALES CHICAGO
7566 JONES MANAGER 7839 02-APR-81 2975 20 20 RESEARCH DALLAS
7654 MARTIN SALESMAN 7698 28-SEP-81 1250 1400 30 30 SALES CHICAGO
7698 BLAKE MANAGER 7839 01-MAY-81 2850 30 30 SALES CHICAGO
7782 CLARK MANAGER 7839 09-JUN-81 2450 10 10 ACCOUNTING NEW YORK
7788 SCOTT ANALYST 7566 19-APR-87 3000 20 20 RESEARCH DALLAS
7839 KING PRESIDENT 17-NOV-81 5000 10 10 ACCOUNTING NEW YORK
7844 TURNER SALESMAN 7698 08-SEP-81 1500 0 30 30 SALES CHICAGO
7876 ADAMS CLERK 7788 23-MAY-87 1100 20 20 RESEARCH DALLAS
7902 FORD ANALYST 7566 03-DEC-81 3000 20 20 RESEARCH DALLAS
7934 MILLER CLERK 7782 23-JAN-82 1300 10 10 ACCOUNTING NEW YORK

12 rows selected.

SQL> select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 3264d6n6xacac, child number 1
-------------------------------------
select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000

Plan hash value: 615168685

----------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 12 |00:00:00.01 | 15 | | | |
|* 1 | HASH JOIN | | 1 | 12 | 12 |00:00:00.01 | 15 | 1321K| 1321K| 750K (0)|
| 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | |
|* 3 | TABLE ACCESS FULL| EMP | 1 | 12 | 12 |00:00:00.01 | 8 | | | |
----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - access("T1"."DEPTNO"="T2"."DEPTNO")
3 - filter("SAL">1000)

Note
-----
- dynamic sampling used for this statement (level=2)


25 rows selected.


--通过hint ’gather_plan_statistics'可以不需要设置ALL
SQL> select /*+ gather_plan_statistics */ * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000;

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO DEPTNO DNAME LOC
---------- ---------- --------- ---------- ------------------ ---------- ---------- ---------- ---------- -------------- -------------
7499 ALLEN SALESMAN 7698 20-FEB-81 1600 300 30 30 SALES CHICAGO
7521 WARD SALESMAN 7698 22-FEB-81 1250 500 30 30 SALES CHICAGO
7566 JONES MANAGER 7839 02-APR-81 2975 20 20 RESEARCH DALLAS
7654 MARTIN SALESMAN 7698 28-SEP-81 1250 1400 30 30 SALES CHICAGO
7698 BLAKE MANAGER 7839 01-MAY-81 2850 30 30 SALES CHICAGO
7782 CLARK MANAGER 7839 09-JUN-81 2450 10 10 ACCOUNTING NEW YORK
7788 SCOTT ANALYST 7566 19-APR-87 3000 20 20 RESEARCH DALLAS
7839 KING PRESIDENT 17-NOV-81 5000 10 10 ACCOUNTING NEW YORK
7844 TURNER SALESMAN 7698 08-SEP-81 1500 0 30 30 SALES CHICAGO
7876 ADAMS CLERK 7788 23-MAY-87 1100 20 20 RESEARCH DALLAS
7902 FORD ANALYST 7566 03-DEC-81 3000 20 20 RESEARCH DALLAS
7934 MILLER CLERK 7782 23-JAN-82 1300 10 10 ACCOUNTING NEW YORK

12 rows selected.


SQL> select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 1kxydxbgh08q2, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ * from emp t1,dept t2 where
t1.deptno=t2.deptno and sal >1000

Plan hash value: 615168685

----------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 12 |00:00:00.01 | 15 | | | |
|* 1 | HASH JOIN | | 1 | 12 | 12 |00:00:00.01 | 15 | 1321K| 1321K| 765K (0)|
| 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | |
|* 3 | TABLE ACCESS FULL| EMP | 1 | 12 | 12 |00:00:00.01 | 8 | | | |
----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - access("T1"."DEPTNO"="T2"."DEPTNO")
3 - filter("SAL">1000)

Note
-----
- dynamic sampling used for this statement (level=2)


26 rows selected.

优点:可以通过STRATS得出表被访问次数;可以通过E-Rows和A-Rows来判断预测行数和实际行数是否一致;可以通过Buffers来获取逻辑读数值。

_缺点:_需要sql语句执行完;必须将执行结果输出;看不出物理读数值。

四、dbms_xplan.display_cursor

1
2
3
4
sql复制代码1、获取sql_id
2、查看AWR和CURSOR中的执行计划
select * from table(dbms_xplan.display_awr('&sqlid'));
select * from table(dbms_xplan.display_cursor('&sqlid'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
sql复制代码--查看AWR和CURSOR中的执行计划
select * from table(dbms_xplan.display_awr('&sqlid'));
select * from table(dbms_xplan.display_cursor('&sqlid'));

SQL> desc dbms_xplan
FUNCTION DISPLAY_AWR RETURNS DBMS_XPLAN_TYPE_TABLE
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SQL_ID VARCHAR2 IN
PLAN_HASH_VALUE NUMBER(38) IN DEFAULT
DB_ID NUMBER(38) IN DEFAULT
FORMAT VARCHAR2 IN DEFAULT

FUNCTION DISPLAY_CURSOR RETURNS DBMS_XPLAN_TYPE_TABLE
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SQL_ID VARCHAR2 IN DEFAULT
CURSOR_CHILD_NO NUMBER(38) IN DEFAULT
FORMAT VARCHAR2 IN DEFAULT


SQL> select distinct sql_id,plan_hash_value from v$sql where sql_text like 'select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000%';

SQL_ID PLAN_HASH_VALUE
------------- ---------------
3264d6n6xacac 615168685

--获取AWR中的执行计划
SQL> select * from table(dbms_xplan.display_awr('3264d6n6xacac'));

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 3264d6n6xacac
--------------------
select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000

Plan hash value: 615168685

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 6 (100)| |
| 1 | HASH JOIN | | 12 | 1404 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPT | 4 | 120 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| EMP | 12 | 1044 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------

Note
-----
- dynamic sampling used for this statement (level=2)


19 rows selected.

--获取共享池中的执行计划
SQL> select * from table(dbms_xplan.display_cursor('3264d6n6xacac'));

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 3264d6n6xacac, child number 0
-------------------------------------
select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000

Plan hash value: 615168685

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 6 (100)| |
|* 1 | HASH JOIN | | 12 | 1404 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPT | 4 | 120 | 3 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| EMP | 12 | 1044 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - access("T1"."DEPTNO"="T2"."DEPTNO")
3 - filter("SAL">1000)

Note
-----
- dynamic sampling used for this statement (level=2)


25 rows selected.

_优点:_通过sql_id可以立即获取执行计划,无需执行;可以得到真实执行过的执行计划。

_缺点:_没有输出相关统计信息;无法判断处理行数;无法判断表访问次数。

五、event 10046 trace

1
2
3
4
5
sql复制代码1、设置10046事件
alter session set events '10046 trace name context forever,level 12';
2、tkprof格式化trace文件
tkprof /oracle/app/oracle/diag/rdbms/orcl11g/orcl11g/trace/orcl11g_ora_1706.trc,/home/oracle/events_10046.txt
3、查看trace文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
sql复制代码SQL> alter session set events '10046 trace name context forever,level 12';

Session altered.

SQL> select * from emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000;

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO DEPTNO DNAME LOC
---------- ---------- --------- ---------- ------------------ ---------- ---------- ---------- ---------- -------------- -------------
7499 ALLEN SALESMAN 7698 20-FEB-81 1600 300 30 30 SALES CHICAGO
7521 WARD SALESMAN 7698 22-FEB-81 1250 500 30 30 SALES CHICAGO
7566 JONES MANAGER 7839 02-APR-81 2975 20 20 RESEARCH DALLAS
7654 MARTIN SALESMAN 7698 28-SEP-81 1250 1400 30 30 SALES CHICAGO
7698 BLAKE MANAGER 7839 01-MAY-81 2850 30 30 SALES CHICAGO
7782 CLARK MANAGER 7839 09-JUN-81 2450 10 10 ACCOUNTING NEW YORK
7788 SCOTT ANALYST 7566 19-APR-87 3000 20 20 RESEARCH DALLAS
7839 KING PRESIDENT 17-NOV-81 5000 10 10 ACCOUNTING NEW YORK
7844 TURNER SALESMAN 7698 08-SEP-81 1500 0 30 30 SALES CHICAGO
7876 ADAMS CLERK 7788 23-MAY-87 1100 20 20 RESEARCH DALLAS
7902 FORD ANALYST 7566 03-DEC-81 3000 20 20 RESEARCH DALLAS
7934 MILLER CLERK 7782 23-JAN-82 1300 10 10 ACCOUNTING NEW YORK

12 rows selected.

SQL> alter session set events '10046 trace name context off';

Session altered.

SQL> select value from v$diag_info where name like 'Default Trace File%';

VALUE
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/oracle/app/oracle/diag/rdbms/orcl11g/orcl11g/trace/orcl11g_ora_1706.trc

--通过tkprof格式化trace文件
SQL> !tkprof /oracle/app/oracle/diag/rdbms/orcl11g/orcl11g/trace/orcl11g_ora_1706.trc,/home/oracle/events_10046.txt sys=no sort=prsela,exeela,fchela

TKPROF: Release 11.2.0.4.0 - Development on Fri Apr 16 11:29:10 2021

Copyright (c) 1982, 2011, Oracle and/or its affiliates. All rights reserved.



SQL> !cat /home/oracle/events_10046.txt

Trace file: /oracle/app/oracle/diag/rdbms/orcl11g/orcl11g/trace/orcl11g_ora_1706.trc
Sort options: prsela exeela fchela
********************************************************************************
count = number of times OCI procedure was executed
cpu = cpu time in seconds executing
elapsed = elapsed time in seconds executing
disk = number of physical reads of buffers from disk
query = number of buffers gotten for consistent read
current = number of buffers gotten in current mode (usually for update)
rows = number of rows processed by the fetch or execute call
********************************************************************************

SQL ID: 3264d6n6xacac Plan Hash: 615168685

select *
from
emp t1,dept t2 where t1.deptno=t2.deptno and sal >1000


call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 2 18 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 2 0.00 0.00 0 15 0 12
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 4 0.00 0.00 2 33 0 12

Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 83
Number of plan statistics captured: 1

Rows (1st) Rows (avg) Rows (max) Row Source Operation
---------- ---------- ---------- ---------------------------------------------------
12 12 12 HASH JOIN (cr=15 pr=0 pw=0 time=170 us cost=6 size=1404 card=12)
4 4 4 TABLE ACCESS FULL DEPT (cr=7 pr=0 pw=0 time=12 us cost=3 size=120 card=4)
12 12 12 TABLE ACCESS FULL EMP (cr=8 pr=0 pw=0 time=11 us cost=3 size=1044 card=12)


Elapsed times include waiting on following events:
Event waited on Times Max. Wait Total Waited
---------------------------------------- Waited ---------- ------------
db file sequential read 2 0.00 0.00
SQL*Net message to client 2 0.00 0.00
SQL*Net message from client 2 34.46 34.46
********************************************************************************

_优点:_可以查看SQL语句对应等待事件;函数调用的SQL将被列出;可以查看处理行数和物理逻辑读;可以看出解析时间和执行时间;可以跟踪整个程序包;

_缺点:_步骤较为繁琐;无法判断表被访问次数;

六、awrsqrpt.sql

1
2
3
4
sql复制代码1、@?/rdbms/admin/awrsqrpt.sql
2、输入begin snap和end snap
3、输入sql_id
4、查看sqlrpt报告
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
sql复制代码
SQL> @?/rdbms/admin/awrsqrpt.sql

Current Instance
~~~~~~~~~~~~~~~~

DB Id DB Name Inst Num Instance
----------- ------------ -------- ------------
1176847559 ORCL11G 1 orcl11g


Specify the Report Type
~~~~~~~~~~~~~~~~~~~~~~~
Would you like an HTML report, or a plain text report?
Enter 'html' for an HTML report, or 'text' for plain text
Defaults to 'html'
Enter value for report_type:

Type Specified: html


Instances in this Workload Repository schema
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

DB Id Inst Num DB Name Instance Host
------------ -------- ------------ ------------ ------------
* 1176847559 1 ORCL11G orcl11g orcl11g

Using 1176847559 for database Id
Using 1 for instance number


Specify the number of days of snapshots to choose from
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Entering the number of days (n) will result in the most recent
(n) days of snapshots being listed. Pressing <return> without
specifying a number lists all completed snapshots.


Enter value for num_days: 1

Listing the last day's Completed Snapshots

Snap
Instance DB Name Snap Id Snap Started Level
------------ ------------ --------- ------------------ -----
orcl11g ORCL11G 2 16 Apr 2021 10:05 1
3 16 Apr 2021 11:01 1
4 16 Apr 2021 11:48 1



Specify the Begin and End Snapshot Ids
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Enter value for begin_snap: 3
Begin Snapshot Id specified: 3

Enter value for end_snap: 4
End Snapshot Id specified: 4




Specify the SQL Id
~~~~~~~~~~~~~~~~~~
Enter value for sql_id: 3264d6n6xacac
SQL ID specified: 3264d6n6xacac

Specify the Report Name
~~~~~~~~~~~~~~~~~~~~~~~
The default report file name is awrsqlrpt_1_3_4.html. To use this name,
press <return> to continue, otherwise enter an alternative.

Enter value for report_name:

Using the report name awrsqlrpt_1_3_4.html

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

前后端、多语言、跨云部署,全链路追踪到底有多难? 全链路追踪

发表于 2021-11-16

简介: 完整的全链路追踪可以为业务带来三大核心价值:端到端问题诊断,系统间依赖梳理,自定义标记透传。

作者 | 涯海

全链路追踪的价值

链路追踪的价值在于“关联”,终端用户、后端应用、云端组件(数据库、消息等)共同构成了链路追踪的轨迹拓扑大图。这张拓扑覆盖的范围越广,链路追踪能够发挥的价值就越大。而全链路追踪就是覆盖全部关联 IT 系统,能够完整记录用户行为在系统间调用路径与状态的最佳实践方案。

完整的全链路追踪可以为业务带来三大核心价值:端到端问题诊断,系统间依赖梳理,自定义标记透传。

  • 端到端问题诊断:VIP 客户下单失败,内测用户请求超时,许多终端用户的体验问题,追根溯源就是由于后端应用或云端组件异常导致的。而全链路追踪是解决端到端问题最有效的手段,没有之一。
  • 系统间依赖梳理:新业务上线,老业务裁撤,机房搬迁/架构升级,IT 系统间的依赖关系错综复杂,已经超出了人工梳理的能力范畴,基于全链路追踪的拓扑发现,使得上述场景决策更加敏捷、可信。
  • 自定义标记透传:全链路压测,用户级灰度,订单追溯,流量隔离。基于自定义标记的分级处理&数据关联,已经衍生出了一个繁荣的全链路生态。然而,一旦发生数据断链、标记丢失,也将引发不可预知的逻辑灾难。

全链路追踪的挑战与方案

全链路追踪的价值与覆盖的范围成正比,它的挑战也同样如此。为了最大程度地确保链路完整性,无论是前端应用还是云端组件,无论是 Java 语言还是 Go 语言,无论是公有云还是自建机房,都需要遵循同一套链路规范,并实现数据互联互通。多语言协议栈统一、前/后/云(多)端联动、跨云数据融合是实现全链路追踪的三大挑战,如下图所示:

1、多语言协议栈统一

在云原生时代,多语言应用架构越来越普遍,利用不同语言特性,实现最佳的性能和研发体验成为一种趋势。但是,不同语言的成熟度差异,使得全链路追踪无法做到完全的能力一致。目前业界的主流做法是,先保证远程调用协议层格式统一,多语言应用内部自行实现调用拦截与上下文透传,这样可以确保基础的链路数据完整。

但是,绝大部分线上问题无法仅通过链路追踪的基础能力就能够有效定位并解决,线上系统的复杂性决定了一款优秀的 Trace 产品必须提供更加全面、有效的数据诊断能力,比如代码级诊断、内存分析、线程池分析、无损统计等等。充分利用不同语言提供的诊断接口,最大化的释放多语言产品能力是 Trace 能够不断向前发展的基础。

  • 透传协议标准化:全链路所有应用需要遵循同一套协议透传标准,保证链路上下文在不同语言应用间能够完整透传,不会出现断链或上下文缺失的问题。目前主流的开源透传协议包括 Jaeger、SkyWalking、ZipKin 等。
  • 最大化释放多语言产品能力:链路追踪除了最基础的调用链功能外,逐步衍生出了应用/服务监控,方法栈追踪,性能剖析等高阶能力。但是不同语言的成熟度导致产品能力差异较大,比如 Java 探针可以基于 JVMTI 实现很多高阶的边缘侧诊断。优秀的全链路追踪方案会最大化的释放每种语言的差异化技术红利,而不是一味的追求趋同平庸。

2、前后云(多)端联动

目前开源的链路追踪实现主要集中于后端业务应用层,在用户终端和云端组件(如云数据库)侧缺乏有效的埋点手段。主要原因是后两者通常由云服务商或三方厂商提供服务,依赖于厂商对于开源的兼容适配性是否友好。而业务方很难直接介入开发。

上述情况的直接影响是前端页面响应慢,很难直接定位到后端哪个应用或服务导致的,无法明确给出确定性的根因。同理,云端组件的异常也难以直接与业务应用异常划等号,特别是多个应用共享同一个数据库实例等场景下,需要更加迂回的手段进行验证,排查效率十分低下。

为了解决此类问题,首先需要云服务商更好的支持开源链路标准,添加核心方法埋点,并支持开源协议栈透传与数据回流(如阿里云 ARMS 前端监控支持 Jaeger 协议透传与方法栈追踪)。

其次,由于不同系统可能因为归属等问题,无法完成全链路协议栈统一,为了实现多端联动,需要由 Trace 系统提供异构协议栈的打通方案。

  • 异构协议栈打通

为了实现异构协议栈(Jaeger、SkyWalking、Zipkin)的打通,Trace 系统需要支持两项能力:一是协议栈转换与动态配置,比如前端向下透传了 Jaeger 协议,新接入的下游外部系统使用的则是 ZipKin B3 协议。在两者之间的 Node.js 应用可以接收 Jaeger 协议并向下透传 ZipKin 协议,保证全链路标记透传完整性。二是服务端数据格式转换,可以将上报的不同数据格式转换成统一格式进行存储,或者在查询侧进行兼容。前者维护成本相对较小,后者兼容性成本更高,但相对更灵活。

3、跨云数据融合

很多大型企业,出于稳定性或数据安全等因素考虑,选择了多云部署,比如国内系统部署在阿里云,海外系统部署在 AWS 云,涉及企业内部敏感数据的系统部署在自建机房等。多云部署已经成为了一种典型的云上部署架构,但是不同环境的网络隔离,以及基础设施的差异性,也为运维人员带来了巨大的挑战。

由于云环境间仅能通过公网通信,为了实现多云部署架构下的链路完整性,可以采用链路数据跨云上报、跨云查询等方式。无论哪种方式,目标都是实现多云数据统一可见,通过完整链路数据快速定位或分析问题。

  • 跨云上报

链路数据跨云上报的实现难度相对较低,便于维护管理,是目前云厂商采用的主流做法,比如阿里云 ARMS 就是通过跨云数据上报实现的多云数据融合。

跨云上报的优点是部署成本低,一套服务端便于维护;缺点是跨云传输会占用公网带宽,公网流量费用和稳定性是重要限制条件。跨云上报比较适合一主多从架构,绝大部分节点部署在一个云环境内,其他云/自建机房仅占少量业务流量,比如某企业 toC 业务部署在阿x云,企业内部应用部署在自建机房,就比较适合跨云上报的方式,如下图所示。

  • 跨云查询

跨云查询是指原始链路数据保存在当前云网络内,将一次用户查询分别下发,再将查询结果聚合进行统一处理,减少公网传输成本。

跨云查询的优点就是跨网传输数据量小,特别是链路数据的实际查询量通常不到原始数据量的万分之一,可以极大地节省公网带宽。缺点是需要部署多个数据处理终端,不支持分位数、全局 TopN 等复杂计算。比较适合多主架构,简单的链路拼接、max/min/avg 统计都可以支持。跨云查询实现有两种模式,一种是在云网络内部搭建一套集中式的数据处理终端,并通过内网专线打通用户网络,可以同时处理多个用户的数据;另一种是为每个用户单独搭建一套 VPC 内的数据处理终端。前者维护成本低,容量弹性更大;后者数据隔离性更好。

  • 其他方式

除了上述两种方案,在实际应用中还可以采用混合模式或仅透传模式。

混合模式是指将统计数据通过公网统一上报,进行集中处理(数据量小,精度要求高),而链路数据采用跨云查询方式进行检索(数据量大,查询频率低)。

仅透传模式是指每个云环境之间仅保证链路上下文能够完整透传,链路数据的存储与查询独立实现。这种模式的好处就是实现成本极低,每朵云之间仅需要遵循同一套透传协议,具体的实现方案可以完全独立。通过同一个 TraceId 或应用名进行人工串联,比较适合存量系统的快速融合,改造成本最小。

全链路追踪接入实践

前文详细介绍了全链路追踪在各种场景下面临的挑战与应对方案,接下来以阿里云 ARMS 为例,介绍一下如何从 0 到 1 构建一套贯穿前端、网关、服务端、容器和云组件的完整可观测系统。

  • Header 透传格式:统一采用 Jaeger 格式,Key 为 uber-trace-id, Value 为 {trace-id}:{span-id}:{parent-span-id}:{flags} 。
  • 前端接入:可以采用 CDN(Script 注入)或 NPM 两种低代码接入方式,支持 Web/H5、Weex 和各类小程序场景。
  • 后端接入:
  • Java 应用推荐优先使用 ARMS Agent,无侵入式埋点无需代码改造,支持边缘诊断、无损统计、精准采样等高阶功能。用户自定义方法可以通过 OpenTelemetry SDK 主动埋点。
  • 非 Java 应用推荐通过 Jaeger 接入,并将数据上报至 ARMS Endpoint,ARMS 会兼容多语言应用间的链路透传与展示。

阿里云 ARMS 目前的全链路追踪方案是基于 Jaeger 协议,正在开发 SkyWalking 协议,以便支持 SkyWalking 自建用户的无损迁移。前端、Java 应用与非 Java 应用全链路追踪的调用链效果如下图所示:

1、前端接入实践

ARMS 前端监控支持 Web/H5、Weex、支付宝和微信小程序等,本文以 Web 应用通过 CDN 方式接入 ARMS 前端监控为例,简要说明接入流程,详细接入指南参考 ARMS 前端监控官网文档。

  1. 登录 ARMS 控制台,在左侧导航栏中单击接入中心,点击选择前端 Web/H5 接入。
  2. 输入应用名称,点击创建;勾选SDK扩展配置项区域需要的选项,快捷生成待插入页面的BI探针代码。
  3. 选择异步加载,复制下面代码并粘贴至页面HTML中** **元素内部的第一行,然后重启应用。

为了实现前后端链路打通,上述探针代码中必须包含以下两个参数:

  1. enableLinkTrace:true // 表示开启前端链路追踪功能
  2. linkType: ‘tracing’ // 表示生成 Jaeger 协议格式的链路数据,Hearder 允许 uber-trace-id 透传

另外,如果 API 与当前应用非同源,还需要添加 enableApiCors: true 这个参数,并且后端服务器也需要支持跨域请求及自定义header 值,详情参考前后端链路关联文档。如需验证前后端链路追踪配置是否生效,可以打开控制台查看对应 API 请求的 Request Headers 中是否有 uber-trace-id 这个标识。

2、Java 应用接入实践

Java 应用推荐接入 ARMS JavaAgent,无侵入式探针开箱即用,无需修改业务代码,详细接入指南参考 ARMS 应用监控官网文档。

  1. 登录 ARMS 控制台,在左侧导航栏中单击接入中心,点击选择后端 Java 接入。
  2. 根据需要选择手动安装、脚本安装和容器服务安装任意方式。
  3. 根据操作指南确保探针下载并解压至本地,正确配置 appName、LicenseKey 和 javaagent 启动参数后,重启应用。

3、非 Java 应用接入实践

非 Java 应用可以通过开源 SDK(比如 Jaeger)将数据上报至 ARMS 接入点,详细接入指南参考 ARMS 应用监控官网文档。

  1. 登录 ARMS 控制台,在左侧导航栏中单击接入中心,点击选择后端 Go/C++/.NET/Node.js 等接入方式。
  2. 根据操作指南替换接入点 ,配置完成后重启应用。

全链路追踪只是开始,不是结束

从 2010 年谷歌发表 Dapper 论文开始,链路追踪已经发展了十多年。但是关于链路追踪的书籍或深度文章一直都比较少,大部分博客只是简单介绍一些开源的概念或 QuickStart,一个大型企业如何建设一套真正可用、好用、易用的链路追踪系统,需要填哪些坑,避哪些雷,很难找到比较系统、全面的答案。

全链路追踪接入只是 Tracing 的起点,选择适合自身业务架构的方案,可以避免一些弯路。但链路追踪不仅仅只是看看调用链和服务监控,如何向上赋能业务,衍生至业务可观测领域辅助业务决策?如何向下与基础设施可观测联动,提前发现资源类风险?后面还有很多的工作要做,期待更多同学一起加入分享。

相关链接

1、 ARMS 前端监控官网文档:help.aliyun.com/document\_d…

2、 前后端链路关联文档:help.aliyun.com/document\_d…

3、ARMS 应用监控官网文档:help.aliyun.com/document\_d…

4、ARMS 应用监控官网文档:help.aliyun.com/document\_d…

5、ARMS 控制台:arms.console.aliyun.com/?spm=ata.21…

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go语言

发表于 2021-11-16

对于你这个问题,其实目前很普遍,很多 Java 程序员因为工作需要,内部转岗,从 Java 转为 Go 开发。

Java 程序员已经有一定的编程基础,所以学习 Go 语言上手起来就会很快,个人不建议报名参加各类培训班,建议自学即可顺利转行。

如何学习 Go?

Go 语言学习路线图

「Go语言学习指南」一份涵盖大部分 Golang 程序员所需要掌握的核心知识、Go教程、Go开源书籍。学习 Go语言,首选 GoGuide。

项目地址:github.com/coderit666/…

)

按照这个学生下路线图去学习 Go 语言,很容易即可从 Java 转行到 Go。

Go 语言电子书推荐

Go-Web编程百度云链接:提取码:ty2c

Go并发实战链接:提取码:41zd

Go语言编程链接 提取码:dcu3

Go语言标准库链接:提取码:8kx9

Go语言程序设计:提取码:2uqt

Go语言圣经:提取码:7emu

Go语言实战:提取码:f7o2

Go语言学习笔记:提取码:7il3

\

1. Go 语言官网

对于已经有 Java 编程经验的程序员,学习新的语言,最快的方式就是从官网开始学起,官网即标准,里面涵盖了各类编程基础知识以及丰富的标准库。

The Go Programming Language​golang.google.cn/

2. Go 包

The Go Programming Language​golang.google.cn/pkg/

3. Go 语言中文文档

)

  • 开发环境
  • Go基础
  • 流程控制
  • 函数
  • 方法
  • 面向对象
  • 网络编程
  • 并发编程
  • 常用标准库
  • beego框架
  • gin框架
  • Iris框架
  • Echo框架
  • Go高级
  • 插件库
  • 项目
  • 开源仓库
  • 其他
  • 面试题
  • go中文标准文档
  • go专家编程
  • go设计模式
  • go公众号开发
  • 持续更新中…

Go语言中文文档​www.topgoer.com/

4..w3cschool

w3cschool.​www.w3cschool.cn/go/

这个网站,大多数编程爱好者应该都听过,学习过 Java 编程的朋友应该也比较熟悉了,这个网站中也有 Go 语言教程,特别适合新手。

)

5. Go 语言电子书

《GO圣经》很不错的入门书籍

《Go 语言实战》适合有一定的 Go 基础知识阅读学习

《GO Web 编程》,主要是介绍如何用 GO 进行 Web 开发,是从国外翻译来的一本书

《Go 学习笔记》,雨痕大佬的书,真的是学习笔记,对 Go 语言总结非常到位,每章都很精彩。

《GO 语言高级编程》,涵盖CGO、Go汇编、RPC实现、Web框架、分布式系统等高阶主题

6. Go 语言开源项目

这里开源君整理了 GitHub Top 10 + Go 语言开源项目,涵盖了 Go 语言从入门到精通到面试题的各类开源项目助力你成为 Go 语言高手,相信通过这 10 个 Go 语言开源项目的学习,可以助力你进阶。

开源指南:GitHub Top 10 + Go 语言开源项目(2021版)140 赞同 · 14 评论文章

开源君,专注分享 GitHub、码云优质开源项目,目前分享了诸多的开源项目,帮助了不少的朋友提升了技术与工作效率。

7 人赞同了该回答

已经是程序员了,建议先撸一遍

a tour of Go

)

\

再读

How to write Go code​golang.org/doc/code.html

再读

\

Effective Go​golang.org/doc/effective_go.html

就可以试着写代码解决问题了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux内存占用分析的几个方法,你知道几个?

发表于 2021-11-16

系统内存是硬件系统中必不可少的部分,定时查看系统内存资源运行情况,可以帮助我们及时发现内存资源是否存在异常占用,确保业务的稳定运行。

例如:定期查看公司的网站服务器内存使用情况,可以确保服务器的资源是否够用,或者发现服务器内存被占用异常可以及时解决,避免因内存不够导致无法访问网站或访问速度慢的问题。

因此,对于 Linux 管理员来说,在日常工作中能够熟练在 Linux 系统下检查内存的运行状况就变得尤为重要!

查看内存的运行状态并非难事,但是针对不同的情况使用正确的方式查看呢?

一口君整理了几个 个非常实用的 Linux 内存查看方法

1、free命令

2、 vmstat命令

3、 /proc/meminfo 命令

4、 top命令

5、 htop 命令

6、查看进程内存信息

Linux内存总览图

)

该图很好的描述了OS内存的使用和分配等详细信息。建议大家配合该图来一起学习和理解内存的一些概念。

一、free命令

free 命令可以显示当前系统未使用的和已使用的内存数目,还可以显示被内核使用的内存缓冲区。

  1. free 命令语法:

free [options]

free 命令选项:

1
2
3
4
5
6
7
8
bash复制代码-b # 以Byte为单位显示内存使用情况;
-k # 以KB为单位显示内存使用情况;
-m # 以MB为单位显示内存使用情况;
-g # 以GB为单位显示内存使用情况。
-o # 不显示缓冲区调节列;
-s<间隔秒数> # 持续观察内存使用状况;
-t # 显示内存总和列;
-V # 显示版本信息。
  1. free 命令实例
1
2
3
4
5
6
7
r复制代码free -t    # 以总和的形式显示内存的使用信息
free -h -s 10 # 周期性的查询内存使用信息,每10s 执行一次命令

free -h -c 10 #输出10次
在版本 v3.2.8,就是输出一次!需要配合 -s 使用。
在版本 v3.3.10,不加-s,就默认1秒输出一次。
free -V #查看版本号

)

下面先解释一下输出的内容:

)

二、vmstat 指令

vmstat命令是最常见的Linux/Unix监控工具,用于查看系统的内存存储信息,是一个报告虚拟内存统计信息的小工具,属于sysstat包。

vmstat 命令报告包括:进程、内存、分页、阻塞 IO、中断、磁盘、CPU。

可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况,IO读写情况。

这个命令是我查看Linux/Unix最喜爱的命令,一个是Linux/Unix都支持,二是相比top,我可以看到整个机器的CPU,内存,IO的使用情况,而不是单单看到各个进程的CPU使用率和内存使用率(使用场景不一样)。

  1. 命令格式:

vmstat -s(参数)
2. 举例

一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如:

root@local:~# vmstat 2 1
procs ———–memory———- —swap– —–io—- -system– —-cpu—-
r b swpd free buff cache si so bi bo in cs us sy id wa
1 0 0 3498472 315836 3819540 0 0 0 1 2 0 0 0 100 0

2表示每个两秒采集一次服务器状态,1表示只采集一次。

实际上,在应用过程中,我们会在一段时间内一直监控,不想监控直接结束vmstat就行了,例如:

)

这表示vmstat每2秒采集数据,按下ctrl + c结束程序,这里采集了3次数据我就结束了程序。

)

)

)

  1. 常见问题处理

常见问题及解决方法

如果r经常大于4,且id经常少于40,表示cpu的负荷很重。

如果pi,po长期不等于0,表示内存不足。

如果disk经常不等于0,且在b中的队列大于3,表示io性能不好。

1.如果在processes中运行的序列(process r)是连续的大于在系统中的CPU的个数表示系统现在运行比较慢,有多数的进程等待CPU。

2.如果r的输出数大于系统中可用CPU个数的4倍的话,则系统面临着CPU短缺的问题,或者是CPU的速率过低,系统中有多数的进程在等待CPU,造成系统中进程运行过慢。

3.如果空闲时间(cpu id)持续为0并且系统时间(cpu sy)是用户时间的两倍(cpu us)系统则面临着CPU资源的短缺。

当发生以上问题的时候请先调整应用程序对CPU的占用情况.使得应用程序能够更有效的使用CPU.同时可以考虑增加更多的CPU. 关于CPU的使用情况还可以结合mpstat, ps aux top prstat –a等等一些相应的命令来综合考虑关于具体的CPU的使用情况,和那些进程在占用大量的CPU时间.一般情况下,应用程序的问题会比较大一些.比如一些sql语句不合理等等都会造成这样的现象.

  1. 内存问题现象:

内存的瓶颈是由scan rate (sr)来决定的.scan rate是通过每秒的始终算法来进行页扫描的.如果scan rate(sr)连续的大于每秒200页则表示可能存在内存缺陷.同样的如果page项中的pi和po这两栏表示每秒页面的调入的页数和每秒调出的页数.如果该值经常为非零值,也有可能存在内存的瓶颈,当然,如果个别的时候不为0的话,属于正常的页面调度这个是虚拟内存的主要原理.

解决办法:

1.调节applications & servers使得对内存和cache的使用更加有效.

2.增加系统的内存.

3.Implement priority paging in s in pre solaris 8 versions by adding line “set priority paging=1” in /etc/system. Remove this line if upgrading from Solaris 7 to 8 & retaining old /etc/system file.

关于内存的使用情况还可以结ps aux top prstat –a等等一些相应的命令来综合考虑关于具体的内存的使用情况,和那些进程在占用大量的内存.

一般情况下,如果内存的占用率比较高,但是,CPU的占用很低的时候,可以考虑是有很多的应用程序占用了内存没有释放,但是,并没有占用CPU时间,可以考虑应用程序,对于未占用CPU时间和一些后台的程序,释放内存的占用。

r 表示运行队列(就是说多少个进程真的分配到CPU),我测试的服务器目前CPU比较空闲,没什么程序在跑,当这个值超过了CPU数目,就会出现CPU瓶颈了。

这个也和top的负载有关系,一般负载超过了3就比较高,超过了5就高,超过了10就不正常了,服务器的状态很危险。

top的负载类似每秒的运行队列。如果运行队列过大,表示你的CPU很繁忙,一般会造成CPU使用率很高。

  1. 常见性能问题分析

IO/CPU/men连锁反应

1.free急剧下降
2.buff和cache被回收下降,但也无济于事
3.依旧需要使用大量swap交换分区swpd
4.等待进程数,b增多
5.读写IO,bi bo增多
6.si so大于0开始从硬盘中读取
7.cpu等待时间用于 IO等待,wa增加

内存不足

1.开始使用swpd,swpd不为0

2.si so大于0开始从硬盘中读取

io瓶颈

1
2
3
4
5
6
7
8
erlang复制代码1.读写IO,bi bo增多超过2000
2.cpu等待时间用于 IO等待,wa增加 超过20
3.sy 系统调用时间长,IO操作频繁会导致增加 >30%
4.wa io等待时间长
iowait% <20% 良好
iowait% <35% 一般
iowait% >50%
5.进一步使用iostat观察

CPU瓶颈:load,vmstat中r列

1
2
3
4
5
6
bash复制代码1.反应为CPU队列长度
2.一段时间内,CPU正在处理和等待CPU处理的进程数之和,直接反应了CPU的使用和申请情况。
3.理想的load average:核数*CPU数*0.7
CPU个数:grep 'physical id' /proc/cpuinfo | sort -u
核数:grep 'core id' /proc/cpuinfo | sort -u | wc -l
4.超过这个值就说明已经是CPU瓶颈了

三、/proc/meminfo

用途:用于从/proc文件系统中提取与内存相关的信息。这些文件包含有 系统和内核的内部信息。其实 free 命令中的信息都来自于 /proc/meminfo 文件。/proc/meminfo 文件包含了更多更原始的信息,只是看起来不太直观。

  1. 查看方法:

cat /proc/meminfo
2. 实例及信息解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
ruby复制代码peng@ubuntu:~$ cat /proc/meminfo
MemTotal: 2017504 kB //所有可用的内存大小,
物理内存减去预留位和内核使用。系统从加电开始到引导完成,firmware/BIOS要预留一
些内存,内核本身要占用一些内存,最后剩下可供内核支配的内存就是MemTotal。这个值
在系统运行期间一般是固定不变的,重启会改变。
MemFree: 511052 kB //表示系统尚未使用的内存。
MemAvailable: 640336 kB //真正的系统可用内存,
系统中有些内存虽然已被使用但是可以回收的,比如cache/buffer、slab都有一部分可
以回收,所以这部分可回收的内存加上MemFree才是系统可用的内存
Buffers: 114348 kB //用来给块设备做缓存的内存,(文件系统的 metadata、pages)
Cached: 162264 kB //分配给文件缓冲区的内存,例如vi一个文件,就会将未保存的内容写到该缓冲区
SwapCached: 3032 kB //被高速缓冲存储用的交换空间(硬盘的swap)的大小
Active: 555484 kB //经常使用的高速缓冲存储器页面文件大小
Inactive: 295984 kB //不经常使用的高速缓冲存储器文件大小
Active(anon): 381020 kB //活跃的匿名内存
Inactive(anon): 244068 kB //不活跃的匿名内存
Active(file): 174464 kB //活跃的文件使用内存
Inactive(file): 51916 kB //不活跃的文件使用内存
Unevictable: 48 kB //不能被释放的内存页
Mlocked: 48 kB //系统调用 mlock
SwapTotal: 998396 kB //交换空间总内存
SwapFree: 843916 kB //交换空间空闲内存
Dirty: 128 kB //等待被写回到磁盘的
Writeback: 0 kB //正在被写回的
AnonPages: 572776 kB //未映射页的内存/映射到用户空间的非文件页表大小
Mapped: 119816 kB //映射文件内存
Shmem: 50212 kB //已经被分配的共享内存
Slab: 113700 kB //内核数据结构缓存
SReclaimable: 68652 kB //可收回slab内存
SUnreclaim: 45048 kB //不可收回slab内存
KernelStack: 8812 kB //内核消耗的内存
PageTables: 27428 kB //管理内存分页的索引表的大小
NFS_Unstable: 0 kB //不稳定页表的大小
Bounce: 0 kB //在低端内存中分配一个临时buffer作为跳转,把位
于高端内存的缓存数据复制到此处消耗的内存
WritebackTmp: 0 kB //FUSE用于临时写回缓冲区的内存
CommitLimit: 2007148 kB //系统实际可分配内存
Committed_AS: 3567280 kB //系统当前已分配的内存
VmallocTotal: 34359738367 kB //预留的虚拟内存总量
VmallocUsed: 0 kB //已经被使用的虚拟内存
VmallocChunk: 0 kB //可分配的最大的逻辑连续的虚拟内存
HardwareCorrupted: 0 kB //表示“中毒页面”中的内存量
即has failed的内存(通常由ECC标记). ECC代表“纠错码”. ECC memory能够纠正小错误并检测较大错误;
在具有非ECC内存的典型PC上,内存错误未被检测到.如果使用ECC检测到无法纠正的错误(在内存或缓存中,
具体取决于系统的硬件支持),则Linux内核会将相应的页面标记为中毒.
AnonHugePages: 0 kB //匿名大页
【/proc/meminfo的AnonHugePages==所有进程的/proc/<pid>/smaps中AnonHugePages之和】
ShmemHugePages: 0 kB //用于共享内存的大页
ShmemPmdMapped: 0 kB
CmaTotal: 0 kB //连续内存区管理总量
CmaFree: 0 kB //连续内存区管理空闲量
HugePages_Total: 0 //预留HugePages的总个数
HugePages_Free: 0 //池中尚未分配的 HugePages 数量,
真正空闲的页数等于HugePages_Free - HugePages_Rsvd
HugePages_Rsvd: 0 //表示池中已经被应用程序分配但尚未使用的 HugePages 数量
HugePages_Surp: 0 //这个值得意思是当开始配置了20个大页,现在修改配置为16,那么这个参数就会显示为4,一般不修改配置,这个值都是0
Hugepagesize: 2048 kB //大内存页的size
//指直接映射(direct mapping)的内存大小,从代码上来看,值记录管理页表占用的内存,就是描述线性映射空间中,有多个空间分别使用了2M/4K/1G页映射
DirectMap4k: 96128 kB
DirectMap2M: 2000896 kB
DirectMap1G: 0 kB

注意这个文件显示的单位是kB而不是KB,1kB=1000B,但是实际上应该是KB,1KB=1024B

还可以使用命令 less /proc/meminfo 直接读取该文件。通过使用 less 命令,可以在长长的输出中向上和向下滚动,找到你需要的内容。

从中我们可以很清晰明了的看出内存中的各种指标情况,例如 MemFree的空闲内存和SwapFree中的交换内存。
3. 代码实例

负责输出/proc/meminfo的源代码是:

fs/proc/meminfo.c : meminfo_proc_show()
static int meminfo_proc_show(struct seq_file *m, void *v)
{
struct sysinfo i;
unsigned long committed;
long cached;
long available;
unsigned long pages[NR_LRU_LISTS];
int lru;

si_meminfo(&i);
si_swapinfo(&i);
committed = percpu_counter_read_positive(&vm_committed_as);

cached = global_node_page_state(NR_FILE_PAGES) -
total_swapcache_pages() - i.bufferram;
if (cached < 0)
cached = 0;

for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++)
pages[lru] = global_node_page_state(NR_LRU_BASE + lru);

available = si_mem_available();

show_val_kb(m, “MemTotal: “, i.totalram);
show_val_kb(m, “MemFree: “, i.freeram);
show_val_kb(m, “MemAvailable: “, available);
show_val_kb(m, “Buffers: “, i.bufferram);
show_val_kb(m, “Cached: “, cached);
show_val_kb(m, “SwapCached: “, total_swapcache_pages());
show_val_kb(m, “Active: “, pages[LRU_ACTIVE_ANON] +
pages[LRU_ACTIVE_FILE]);
show_val_kb(m, “Inactive: “, pages[LRU_INACTIVE_ANON] +
pages[LRU_INACTIVE_FILE]);
show_val_kb(m, “Active(anon): “, pages[LRU_ACTIVE_ANON]);
show_val_kb(m, “Inactive(anon): “, pages[LRU_INACTIVE_ANON]);
show_val_kb(m, “Active(file): “, pages[LRU_ACTIVE_FILE]);
show_val_kb(m, “Inactive(file): “, pages[LRU_INACTIVE_FILE]);
show_val_kb(m, “Unevictable: “, pages[LRU_UNEVICTABLE]);
show_val_kb(m, “Mlocked: “, global_zone_page_state(NR_MLOCK));

#ifdef CONFIG_HIGHMEM
show_val_kb(m, “HighTotal: “, i.totalhigh);
show_val_kb(m, “HighFree: “, i.freehigh);
show_val_kb(m, “LowTotal: “, i.totalram - i.totalhigh);
show_val_kb(m, “LowFree: “, i.freeram - i.freehigh);
#endif

#ifndef CONFIG_MMU
show_val_kb(m, “MmapCopy: “,
(unsigned long)atomic_long_read(&mmap_pages_allocated));
#endif

show_val_kb(m, “SwapTotal: “, i.totalswap);
show_val_kb(m, “SwapFree: “, i.freeswap);
show_val_kb(m, “Dirty: “,
global_node_page_state(NR_FILE_DIRTY));
show_val_kb(m, “Writeback: “,
global_node_page_state(NR_WRITEBACK));
show_val_kb(m, “AnonPages: “,
global_node_page_state(NR_ANON_MAPPED));
show_val_kb(m, “Mapped: “,
global_node_page_state(NR_FILE_MAPPED));
show_val_kb(m, “Shmem: “, i.sharedram);
show_val_kb(m, “Slab: “,
global_node_page_state(NR_SLAB_RECLAIMABLE) +
global_node_page_state(NR_SLAB_UNRECLAIMABLE));

show_val_kb(m, “SReclaimable: “,
global_node_page_state(NR_SLAB_RECLAIMABLE));
show_val_kb(m, “SUnreclaim: “,
global_node_page_state(NR_SLAB_UNRECLAIMABLE));
seq_printf(m, “KernelStack: %8lu kB\n”,
global_zone_page_state(NR_KERNEL_STACK_KB));
show_val_kb(m, “PageTables: “,
global_zone_page_state(NR_PAGETABLE));
#ifdef CONFIG_QUICKLIST
show_val_kb(m, “Quicklists: “, quicklist_total_size());
#endif

show_val_kb(m, “NFS_Unstable: “,
global_node_page_state(NR_UNSTABLE_NFS));
show_val_kb(m, “Bounce: “,
global_zone_page_state(NR_BOUNCE));
show_val_kb(m, “WritebackTmp: “,
global_node_page_state(NR_WRITEBACK_TEMP));
show_val_kb(m, “CommitLimit: “, vm_commit_limit());
show_val_kb(m, “Committed_AS: “, committed);
seq_printf(m, “VmallocTotal: %8lu kB\n”,
(unsigned long)VMALLOC_TOTAL >> 10);
show_val_kb(m, “VmallocUsed: “, 0ul);
show_val_kb(m, “VmallocChunk: “, 0ul);

#ifdef CONFIG_MEMORY_FAILURE
seq_printf(m, “HardwareCorrupted: %5lu kB\n”,
atomic_long_read(&num_poisoned_pages) << (PAGE_SHIFT - 10));
#endif

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
show_val_kb(m, “AnonHugePages: “,
global_node_page_state(NR_ANON_THPS) * HPAGE_PMD_NR);
show_val_kb(m, “ShmemHugePages: “,
global_node_page_state(NR_SHMEM_THPS) * HPAGE_PMD_NR);
show_val_kb(m, “ShmemPmdMapped: “,
global_node_page_state(NR_SHMEM_PMDMAPPED) * HPAGE_PMD_NR);
#endif

#ifdef CONFIG_CMA
show_val_kb(m, “CmaTotal: “, totalcma_pages);
show_val_kb(m, “CmaFree: “,
global_zone_page_state(NR_FREE_CMA_PAGES));
#endif

hugetlb_report_meminfo(m);

arch_report_meminfo(m);

return 0;
}

四、top 指令

用途:用于打印系统中的CPU和内存使用情况。输出结果中,可以很清晰的看出已用和可用内存的资源情况。top 最好的地方之一就是发现可能已经失控的服务的进程 ID 号(PID)。有了这些 PID,你可以对有问题的任务进行故障排除(或 kill)。

语法

1
css复制代码top [-] [d delay] [q] [c] [S] [s] [i] [n] [b]

参数说明:

1
2
3
4
5
6
7
8
yaml复制代码d : 改变显示的更新速度,或是在交谈式指令列( interactive command)按 s
q : 没有任何延迟的显示速度,如果使用者是有 superuser 的权限,则 top 将会以最高的优先序执行
c : 切换显示模式,共有两种模式,一是只显示执行档的名称,另一种是显示完整的路径与名称
S : 累积模式,会将己完成或消失的子进程 ( dead child process ) 的 CPU time 累积起来
s : 安全模式,将交谈式指令取消, 避免潜在的危机
i : 不显示任何闲置 (idle) 或无用 (zombie) 的进程
n : 更新的次数,完成后将会退出 top
b : 批次档模式,搭配 "n" 参数一起使用,可以用来将 top 的结果输出到档案内

举例

图片

第一行,任务队列信息,同 uptime 命令的执行结果

系统时间:02:19:10 运行时间:up 2:26 min, 当前登录用户:1 user 负载均衡(uptime) load average: 0.00, 0.06, 0.07 average后面的三个数分别是1分钟、5分钟、15分钟的负载情况。load average数据是每隔5秒钟检查一次活跃的进程数,然后按特定算法计算出的数值。如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了

第二行,Tasks — 任务(进程)

总进程:229 total, 运行:1 running, 休眠:163 sleeping, 停止: 0 stopped, 僵尸进程: 0 zombie

第三行,cpu状态信息

0.7%us【user space】— 用户空间占用CPU的百分比。1.0%sy【sysctl】— 内核空间占用CPU的百分比。0.0%ni【】— 改变过优先级的进程占用CPU的百分比 97.9%id【idolt】— 空闲CPU百分比 0.3%wa【wait】— IO等待占用CPU的百分比 0.0%hi【Hardware IRQ】— 硬中断占用CPU的百分比 0.0%si【Software Interrupts】— 软中断占用CPU的百分比

第四行,内存状态

2017504 total, 653616 free, 1154200 used, 209688 buff/cache【缓存的内存量】

第五行,swap交换分区信息

998396 total, 771068 free, 227328 used. 635608 avail Mem

第七行以下:各进程(任务)的状态监控

PID — 进程id USER — 进程所有者 PR — 进程优先级 NI — nice值。负值表示高优先级,正值表示低优先级 VIRT — 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES RES — 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA SHR — 共享内存大小,单位kb S —进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 %CPU — 上次更新到现在的CPU时间占用百分比 %MEM — 进程使用的物理内存百分比 TIME+ — 进程使用的CPU时间总计,单位1/100秒 COMMAND — 进程名称(命令名/命令行)

常用实例

显示进程信息

# top

显示完整命令

# top -c

以批处理模式显示程序信息

# top -b

以累积模式显示程序信息

# top -S

设置信息更新次数

top -n 2

//表示更新两次后终止更新显示

设置信息更新时间

# top -d 3

//表示更新周期为3秒

显示指定的进程信息

# top -p 139

//显示进程号为139的进程信息,CPU、内存占用率等

显示更新十次后退出

top -n 10

五、htop 指令

htop 它类似于 top 命令,但可以让你在垂直和水平方向上滚动,所以你可以看到系统上运行的所有进程,以及他们完整的命令行。

可以不用输入进程的 PID 就可以对此进程进行相关的操作 (killing, renicing)。

htop快照:

图片

可以使用快捷键

1
2
3
4
5
6
7
8
9
10
11
12
makefile复制代码F1,h,?:查看htop使用说明,
F2,s :设置选项
F3,/ :搜索进程
F4,\ :过滤器,输入关键字搜索
F5,t :显示属性结构
F6,<,>:选择排序方式
F7, [,:减少进程的优先级(nice)
F8,] :增加进程的优先级(nice)
F9,k :杀掉选中的进程
F10,q:退出htop
u:显示所有用户,并可以选中某一特定用户的进程
U:取消标记所有的进程

第1行-第4行:显示CPU当前的运行负载,有几核就有几行,我的是1核

Mem:显示内存的使用情况,3887M大概是3.8G,此时的Mem不包含buffers和cached的内存,所以和free -m会不同Swp:显示交换空间的使用情况,交换空间是当内存不够和其中有一些长期不用的数据时,ubuntu会把这些暂时放到交换空间中

其他信息可以参考top命令说明。

PS:如果你终端没安装 htop,先通过指令来安装。sudo apt-get update sudo apt install htop

六、查看制定进程的内存

通过/proc/procid/status查看进程内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
yaml复制代码peng@ubuntu:~$ cat /proc/4398/status
Name: kworker/0:0 //进程名
Umask: 0000
State: I (idle) //进程的状态
//R (running)", "S (sleeping)", "D (disk sleep)", "T (stopped)", "T(tracing stop)", "Z (zombie)", or "X (dead)"
Tgid: 4398 //线程组的ID,一个线程一定属于一个线程组(进程组).
Ngid: 0
Pid: 4398 //进程的ID,更准确的说应该是线程的ID.
PPid: 2 //当前进程的父进程
TracerPid: 0 //跟踪当前进程的进程ID,如果是0,表示没有跟踪
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 64 //当前分配的文件描述符,该值不是上限,如果打开文件超过64个文件描述符,将以64进行递增
Groups: //启动这个进程的用户所在的组
NStgid: 4398
NSpid: 4398
NSpgid: 0
NSsid: 0
Threads: 1
SigQ: 0/7640
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: ffffffffffffffff
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
Speculation_Store_Bypass: vulnerable
Cpus_allowed: 00000000,00000000,00000000,00000001
Cpus_allowed_list: 0
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 5
nonvoluntary_ctxt_switches: 0

总结:

确定内存使用情况是Linux运维工程师必要的技能,尤其是某个应用程序变得异常和占用系统内存时。当发生这种情况时,知道有多种工具可以帮助你进行故障排除十分方便的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…327328329…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%