Web Components从技术解析到生态应用个人心得指北 - zhoulujun - 博客园

mikel阅读(322)

来源: Web Components从技术解析到生态应用个人心得指北 – zhoulujun – 博客园

Web Components浅析

Web Components 是一种使用封装的、可重用的 HTML 标签、样式和行为来创建自定义元素的 Web 技术。

Web Components 自己本身不是一个规范,而是一套整体技术,包含下面3个独立规范:

  1. Custom Elements:允许开发者定义自己的 HTML 标签(考虑SEO,还是语义化为好)。
  2. Shadow DOM:用于封装样式和标记,不受外部 DOM 影响——天然自带Scope能力(本质是一组 JS API)。
  3. HTML Templates (<template> 和 <slot>):声明式的重用 HTML 代码片段。

     

但是,我觉得还应该包含:ES Modules

Custom Elements 和 Shadow DOM 的可靠性是确定的,毕竟是标准的一部分。

Custom Elements

在2008年W3C 发布了第一个HTML公开草案,其是就是可以使用自定义标签的——2000年W3C准备用XHTML来替代HTML4,结果被抛弃!

XHTML,或者更准确地说是 XHTML 1.0,是一种基于 XML 的标记语言,旨在在网页设计中取代HTML 4.01。它由 W3C 推出,其规范在2000年1月成为官方的推荐标准。XHTML1.0实际上是HTML 4.01的严格版本,并要求开发者遵循更加严格的语法规则——XHTML基于XML,它对标记的正确性有更高的要求:

  • XHTML 元素必须被正确地嵌套。
  • XHTML 元素必须被关闭
  • 标签名必须用小写字母。
  • XHTML 文档必须拥有根元素。
  • XHTML需要开发者在文档开头声明正确的DOCTYPE,而在实际的实践中,由于历史原因和混乱的标准,很多时候开发者并没有遵循正确的声明,导致页面以兼容模式而不是标准模式渲染。
  • 为了正确地作为XHTML传送,Web服务器需要设置MIME类型为application/xhtml+xml。不幸的是,一些浏览器对这种MIME类型的处理不理想,这使得开发者们更倾向于使用更通行的text/html,这实际上使XHTML变成了浏览器中被当作HTML解析的标记语言。

在 HTML5 之前,使用非标准标签通常会被视为不良实践,因为这可能导致不可预测的行为,尤其是在不同的浏览器之间。

然而,HTML5 引入了一种更加宽容的解析规则,允许这些非标准标签存在,浏览器不会因为碰到未知标签而破坏整个页面。即使如此,这些自定义标签没有任何默认的样式或行为,它们就像普通的 HTML 元素(默认为内联元素),除非通过 CSS 或 JavaScript 给予样式和行为。

自定义标签和自定义元素是两个相关但不同的概念。它们代表着 web 开发中自定义组件的不同方面和不同阶段的发展。

自定义标签与自定义元素

自定义标签(非标准标签)

自定义标签:Custom Tags、Non-standard Tags、User-defined Tags……

自定义标签仅在语义上是自定义的,而没有附加任何特殊的行为;

自定义元素(Custom Elements)

自定义元素是 Web Components 规范的一部分,它允许开发者创建完全定制化和可重用的 HTML 元素。

与仅仅创建一个新的标签名不同,自定义元素能够拥有自己独特的行为和属性。

Custom Elements 规范定义了如何注册新的元素、如何附加行为、以及如何处理元素的生命周期事件(如创建、连接到文档、断开连接和属性更改时)。

自定义元素通常使用 customElements.define() 方法在 JavaScript 中注册,这样,当元素被添加到 DOM 时,就会与一个 JavaScript 类关联起来。这个类继承自 HTMLElement,允许它具备 DOM 接口的所有特性,并添加自定义的逻辑和样式。这意味着自定义元素不仅仅是形式上的定制,而是实现了真正的封装和功能拓展。

区别总结

  • 语义:自定义标签仅在语义上是自定义的,而没有附加任何特殊的行为;相反,自定义元素通过 Custom Elements API 注册,并可以包括复杂的逻辑和状态。
  • 功能性:自定义元素支持完整的生命周期管理,提供创建时、附加到 DOM、属性变动等时机的钩子,而自定义标签则没有这些功能。
  • 标准化:自定义元素是 Web Components 的官方标准之一,得到了浏览器的广泛支持;而自定义标签顾名思义,是非标准的,它们允许存在,但并不是 HTML 规范的一部分。
  • 兼容性:自定义元素需要浏览器支持相关的标准,虽然现在大多数现代浏览器都提供了支持,但在一些旧的浏览器中可能需要 polyfills;而自定义标签通常哪种浏览器都能解析,只是作为普通的元素看待。

custom element生命周期

在custom element的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用:

  1. connectedCallback:当 custom element首次被插入文档DOM时,被调用。
  2. disconnectedCallback:当 custom element从文档DOM中删除时,被调用。
  3. adoptedCallback:当 custom element被移动到新的文档时,被调用。
  4. attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。

具体参看:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_custom_elements

 

Shadow DOM

Shadow DOM 主要目的:封装与隔离——相比iframe、frame更加轻量级。

  • IFrame是一个独立的html页面,shadow DOM是当前html页面的一个代码片段,
  • 不需要创建额外的渲染环境——不需要创建一个完整的文档环境,而是基于现有的上下文中创建封闭的DOM结构。

Shadow DOM接口是关键所在:它可以将一个隐藏的、独立的DOM附加到一个元素上,它以shadow root节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的DOM元素一样,但是这棵子树不在主DOM树中——即影子DOM是一种不属于主DOM树的独立的结构,所以Shadow DOM内部的元素始终不会影响到它外部的元素(除了:focus-within),这就为封装提供了便利

  • Shadow host: 一个常规DOM节点,Shadow DOM会被附加到这个节点上。
  • Shadow tree: Shadow DOM内部的DOM树。
  • Shadow boundary: Shadow DOM结束的地方,也是常规DOM开始的地方。
  • Shadow root: Shadow tree的根节点。

Shadow DOM都不是一个新事物,在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构,以一个有着默认播放控制按钮的<video>元素为例,我们所能看到的只是一个<video>标签,实际上,在它的Shadow DOM中,包含来一系列的按钮和其他控制器。

chrome设置Show user agent shadow DOMshadow DOM标签示例

其结构如下:

而现在,我们可以来自己制造相关的标签(如video类似的功能模块)

怎么使用Shadow DOM

看这个就好:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM

但是,https://github.com/Tencent/omi等框架应该更加适合你!

HTML templates(HTML 模板)

这个用过vue的理解应该不难:

  • <template>包含一个 HTML 片段,不会在文档初始化时渲染。<slot>插槽,类似占位符,可以填充自己的内容。、
  • <slot>插槽,类似占位符,可以填充自己的内容。

但是真的要用的话,还是用omi等类似的框架。

为什么不用原生API

这个问题就是,为什么要用JQuery?为什么放弃JQuery使用vue或react?

其是Web Components 了解一下就好。

为什么不推荐使用Web Components

React 和 Vue 在组件化开发方面有自己的实现,并没有直接采用 Web Components 作为内部实现——不过,它们两者都提供了与 Web Components 兼容的接口。

下面是我个人感觉他们放弃Web Components的原因:

React放弃Web Components

  • 封装性:React 组件经常需要和一个复杂的状态以及生命周期方法交互,这些都不是 Web Components 标准提供的。
  • 性能优化:React 的虚拟 DOM 可以通过最小化实际的 DOM 操作来提升性能,这一点在批量更新 UI 或大型应用中尤为明显。
  • 跨平台:React 不仅仅用于 web 开发(通过 React Native,它也被用于移动应用开发),而 Webb Components 专注于 web 标准和浏览器环境。
  • 生态系统:React 拥有非常庞大且成熟的生态系统,包括状态管理(如 Redux)、路由(如 React Router)等各种工具和库。

Vue3放弃Web Components

  • 响应式系统:Vue 的响应式系统使得数据和视图能够自动同步更新,而 Web Components 没有内建这样的响应式机制。
  • 模板语法:Vue 通过其简洁的模板语法扩展了普通的 HTML,使开发者可以更加容易地描述复杂的 UI 结构,而 Web Components 使用的是普通 HTML 搭配 JavaScript。
  • 工具链支持:Vue CLI 提供了非常强大的工具链支持,包括项目脚手架、开发服务器、热重载等,而这些在 Web Components 中不是直接可用的。
  • 生态系统:与 React 类似,Vue 也拥有广泛的插件和支持库,例如 Vuex、Vue Router 等,这些让 Vue 应用开发更为完善。

尽管 React 和 Vue 没有直接采用 Web Components 作为内部实现,但它们都提供了对 Web Components 的支持

2011年,Alex Russel首次提出了Web Components的概率并首次演示了demo,这时候整套技术包括三个方面:scoped css,shadow DOM和Web components。W3C也在此时开始推进Web Components规范。

2012年,HTML Template很快被实现,作为wrapper包裹内容,在页面加载时不使用,在之后运行时实例化。同时Shadow DOM V0标准发布并被实现,并且Ember和Angular开始计划支持Web Components,甚至基于它去做改造,但最终没有结果

至2018年,Web Components在主流浏览器中均被支持,但是并未达到普及程度,具体参看:https://caniuse.com/?search=Web%20Components%20

但是,比如视频播放器、SQL编辑器等超大件,还是非常适合Web Components的。不过这里还是推荐使用框架来做。

vue3项目Web Components案例

Vue 和 Web Components 是互补的技术,具体可以看官方文档:https://cn.vuejs.org/guide/extras/web-components

Vue 在 Custom Elements Everywhere 测试中取得了 100% 的分数。在 Vue 应用中使用自定义元素基本上与使用原生 HTML 元素的效果相同!

Vue 提供了一个和定义一般 Vue 组件几乎完全一致的 defineCustomElement 方法来支持创建自定义元素。这个方法接收的参数和 defineComponent 完全相同。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
  // 同平常一样的 Vue 组件选项——正常的 Vue 组件选项都有
  props: {},
  emits: {},
  template: `...`,
  // defineCustomElement 特有的:注入进 shadow root 的 CSS
  styles: [`/* inlined css */`]
})
// 注册自定义元素,注册之后,所有此页面中的 `<my-vue-element>` 标签 都会被升级
customElements.define('my-vue-element', MyVueElement)
// 你也可以编程式地实例化元素(必须在注册之后) :
document.body.appendChild(
  new MyVueElement({
    // 初始化 props(可选)
  })
)
// 也可以直接使用
export default defineComponent({
  setup(){
    return ()=>(<my-vue-element/>)
  }
})

这样用,是不是非常爽!

vue3 使用Web Components需要注意的点:

failed to resolve component

默认情况下,Vue 会优先尝试将一个非原生的 HTML 标签解析为一个注册的 Vue 组件,如果失败则会将其渲染为自定义元素。这种行为会导致在开发模式下的 Vue 发出“failed to resolve component”的警告。所以需要告诉 Vue 将某些确切的元素作为自定义元素处理并跳过组件解析。在 vite.config.ts 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
{resolve: {
  alias: {//组件提供模板选项,但是在 Vue 这个构建中不支持运行时编译
    'vue''vue/dist/vue.esm-bundler.js',//需要配置你的 bundler 别名 vue: vue/dist/vue.esm-bundler.js
    '@': resolve(__dirname, 'src')
  }
},
plugins: [vue({
  template: {
    compilerOptions: {
      isCustomElement: tag => tag.startsWith('cus-')// 以 cus- 开头的作为自定义元素处理
    }
  }
})]}

 

Provide / Inject API

Provide / Inject API 和相应的组合式 API 在 Vue 定义的自定义元素中都可以正常工作。但是请注意,依赖关系只在自定义元素之间起作用

但是为推荐费必要

插槽

在组件内部,可以像往常一样使用 <slot/> 渲染插槽。但是在解析最终生成的元素时,它只接受原生插槽语法:

  • 不支持作用域插槽。
  • 传递命名插槽时,请使用 slot attribute 而非 v-slot 指令

React项目Web Components案例

说实话,react原生来写干嘛呢?

如果要在react项目里面写,推荐使用 https://lit.dev/

或者使用https://github.com/Tencent/omi/ 来写个项目,打包成组件库,然后再业务里面使用!

 

 

Web Components 生态

Lit:Lit是一个轻量的库,但它依然保留了web组件的所有特性。

Omi:Web Components 框架.

Vaadin: Vaadin 是以java作为开发语言的前端框架,它提供了一套以Web Components为基础的丰富的企业级UI组件库,关键他和spring结合的非常爽,比GWT用起来。

Ionic Framework: 本来是为Angular构建的(4.x适配Angular、Vue 、React),Ionic4 Web端基于Web Components——具有更好的运行速度,相比以前版本的Ionic框架性能提升很多!优异的性能则让 Ionic 成为了构建高性能 PWA 的最佳 UI 框架

说实话吧,Web Components 相比周边生态还是没有起来。可以作为大型项目某些模块的补充技术!

svelte:前端框架新秀,原生支持Web Components。个人不是很了解,跳过!

Google 从 2013 年开始一直在持续推进的基于 Web Components 封装的类库,同时还开放了基于 Polymer 开发的组件集合 PolymerElements · GitHub 和开发周边。 2015 年 Google 正式发布 Polymer 1.0 ,注意时间点,当时还是Custom Elements v0 版标准 2017年Custom Elements v1 版标准在各大浏览器落地,Polymer 发布了 2.0,并且不再封装 Custom Elements API,不再默认使用shadowDOM、目标兼容各种框架,开始变成轻量级类库。

  • 由于起步几乎最早以及 google 背书,可能到现在也是影响用户数最大的 Web Components 基础库,Youtube 基于Polymer 对整站做了重构,Google 很多产品包括 Android 和 ChromeOS 平台也都用了 Polymer。

但个人觉得总体上相比与彼时流行的其他框架 Polymer 还是不温不火,Google 似乎也有同感、随着 Polymer 的轻量化升级,于是在 2018 年又发布了更现代化的 lit GitHub – lit/lit: Lit is a simple library for building fast, lightweight web components.包括 lit-html 模板渲染库 lit/packages/lit-html at main · lit/lit · GitHub和基于 lit-html 的 lit-element  lit/packages/lit-element at main · lit/lit · GitHub 创建 Web Component 的 base class 。 Lit-html 基于 ES 的模板自变量和 template 标签,用注释节点去动态填充,没有JSX 转换虚拟 dom的过程,把大部分模板创建渲染的事都交给浏览器去做,提供了轻量的 api 让我们可以在 JS 中写 HTML-Templates。 Lit-Element 的 Reactive properties 、Scoped styles 等面向现代化 JS 语法的特点让他现在很受欢迎。 Google 推荐新用户使用 lit,但也将 Polymer 推到了 3.0 版本,放弃了 HTML Imports 转向 JS modules,并且支持 Polymer 跟 lit 混用,目前持续又维护和支持,Slack Channel 上一直很活跃。

除了 Google 自己, 微软的 PWA stater GitHub – pwa-builder/pwa-starter: Welcome to the PWABuilder pwa-starter! Looking to build a new Progressive Web App and not sure where to get started? This is what you are looking for!,选择 lit 框架和 封装的 Web Components 作为基础库。 Adobe 基于 LitElement 封装并开放了 Spectrum Web ComponentsSap 基于 Lit-html 封装并开源了 ui5-webcomponents/02-custom-UI5-Web-Components.md at master · SAP/ui5-webcomponents · GitHubRed hat  GitHub – 1-Platform/op-components: One platform component library.等众多公司使用了 lit 开发自己的组件库或平台。

另一个类库 GitHub – skatejs/skatejs: Effortless custom elements powered by modern view libraries. 也是基于 lit-html 的。

 

Web Components  头部案例

目前生成环境使用Web Components 的案例有这些(非全部使用!)

Twitter

Twitter 2016 年开始将自己的嵌入式推文  从 iframe 切换成  ShadowDOM,减少了内存消耗、加快了渲染速度,并批量渲染的时候保持丝滑。Upcoming Change to Embedded Tweet Display on Web

Youtube

Youtube 作为 google 系的产品,很早就在全站用上了 Web Cmponents,并且开源了自己播放器组件 GitHub – GoogleWebComponents/google-youtube: YouTube video playback web component此外 google 开源的 Web Components 还是很多的,Google Web Components · GitHub ,包括地图、drive、日历等等。

Google Earth:

Google Earth 的网页版使用了Web Components技术来创建用户交互界面的一部分。

EA

EA 的游戏工作室分布在全球各地,为了保证不同团队和工作室的设计开发体验统一,EA 基于 Web Components 构建了自己的 Network Design System,同时也支持这自己的 UIaaS。

Github

github 对 Web Components 的使用很早,具体可以看:  How we use Web Components at GitHub | The GitHub Blog2014 年 Custom Elements v0 specification 出现的时候 github 就开始关注:Search · topic:web-components org:github · GitHub,并且开源了其中一系列 Web Components GitHub – github/github-elements: GitHub’s Web Component collection.2017 年 Custom Elements v1 版本在 chrome 和 safari 上相继实现之后,github 开始大范围使用

要知道 github 2018 年才刚刚完全移除 JQueryRemoving jQuery from GitHub.com frontend | The GitHub Blog 这既得益于 github 自身项目组件化的架构,也 Web Components 本身与框架无关的特性非常识合作老项目升级。

github 还开源了 用于开发Web Components 的库 Catalyst:GitHub – github/catalyst: Catalyst is a set of patterns and techniques for developing components within a complex application.

而他的思路借鉴了  Stimulus  和  LitElement

  • 既然提到了 Stimulus,就叉开讲讲这个东西,Stimulus 很适合对老项目改造,尤其是 ruby on rails、jsp 服务端渲染、没有 webpack 之类的前端工具链,技术栈多且混乱的项目。Stimulus 的思路就是通过 MutationObserver 监控元素的变化, 然后取元素、补绑事件或者修改引用。他的定位就很轻盈,就是配合HTML页面,提供动态交互支持,不像现在的很多框架,动辄就是整站重写。

同时 github 还开源了一个 View Component 框架用来在 ruby on rails 里面构建同构应用GitHub – github/view_component: A framework for building reusable, testable & encapsulated view components in Ruby on Rails.

SalesForce

SalesForce 作为一家 ToB 服务的公司,面对各种不同技术栈的客户,选择 Web Components 原因有两点,一是需要一套统一的通用组件面向所有客户,二是在很多特定领域,很多客户很难对他们的传统技术体系做大规模升级,而引入 Web Components 可以避免这类技术改造风险。

他们开源了自己的 Web Components 组件库 Component Library,并提供一整套基于 的企业级研发工具 GitHub – salesforce/lwc: LWC – A Blazing Fast, Enterprise-Grade Web Components Foundation除了通过 LWC,让客户可以在自己的环境中基于组件库配置、开发、部署应用,SalesForce 还开放了自己的 SalesForce 工作平台 ,平台为所有客户提供一站式配置、部署和升级的能力。

Oracle

Oracle 在 2017 年开始在自己的  GitHub – oracle/oraclejet: Oracle JET is a modular JavaScript Extension Toolkit for developers working on client-side applications. 构建工具中增加了对 CustomElement 的支持,在此之前是用的是 jQueryUI。Oracle 对 WebComponents 对态度其实很值得 ToB 同行学习,他并没有刻意想拜托 jQuery,而是让 WebComponents 与现有的 jQuery、Knockout 并行使用,只在新功能上推进 WebComponents ,保持老项目稳定,在历史遗留和新技术之间保持了合理的平衡。而在 jet 的生态方面,他们也在持续建设 Web Component 驱动的共享组件中心 Building the future of Oracle JET Ecosystem | by João Tiago | Digital Transformation Research Group | Medium

ING:

荷兰国际集团(ING)在他们的网站和网上银行平台中大量使用了 Web Components,他们通过使用 Lion Web Components 库共享跨项目的UI组件。

Comcast:

Comcast 的 Xfinity产品线中的某些web应用使用了 Web Components。

Adobe Spectrum:

该站点是一个基于 Web Components 的 UI 框架产品

 

 

参考文章:

神奇的Shadow DOM https://jelly.jd.com/article/6006b1045b6c6a01506c87ac

Vue3.2 实现 Web Components https://ainyi.com/125

https://www.albertaz.com/blog/web-components-ststus

 

 

转载本站文章《Web Components从技术解析到生态应用个人心得指北》,
请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/htmlBase/2012_0823_9020.html

mysql和redis库存扣减和优化 - Scotyzh - 博客园

mikel阅读(320)

来源: mysql和redis库存扣减和优化 – Scotyzh – 博客园

前言

大流量情况下的库存是老生常谈的问题了,在这里我整理一下mySQL和redis应对扣除库存的方案,采用jmeter进行压测。

JMETER设置

库存初始值50,线程数量1000个,1秒以内启动全部,一个线程循环2次,共2000个请求

MySQL方案

初始方案

    <update id="decreaseStock">
        UPDATE stock
        SET stock_num = stock_num - 1
        WHERE id = #{id}
    </update>

这种情况下,在并发条件肯定会出现超卖的

image-20240109153257263

进行修改:

    <update id="decreaseStock">
        UPDATE stock
        SET stock_num = stock_num - 1
        WHERE id = #{id} AND stock_num >= 1
    </update>

增加AND stock_num >= 1条件,即可避免超卖。

image-20240109153241745

相关代码:

    @PostMapping(value = "/decreaseStock/{id}")	
    public ResponseEntity<Object> decreaseStock(@PathVariable("id") Integer id) {
        int result = stockService.decreaseStock(id);
        return result == 1 ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
    }

压测情况:image-20240109170237724

根据Throught可知一秒可以处理200个事务(TPS)

如果说系统的并发量不高,则可以以这种方案进行防止库存超卖,但要注意,在可重复读隔离级别情况下,如果where的条件字段没有索引的话,进行update语句会使整个表被锁住,如果这里使用的where条件不是主键id而是product_name,那么需要给这个字段加索引。

在RR可重复读隔离级别下,如果where条件没有命中索引,那么会基于next-key lock(记录锁和间隙锁的组合)对整个表的所有记录加上这个锁,进行全表扫描,这个时候其他记录想要更新就会被阻塞。

但是不一定是有了索引就不会锁住整个表,这是由优化器决定的,可以使用Explain语句来查看当前语句是走的索引还是全表扫描,如果优化器走的还是全标扫描,可以使用 force index([index_name]) 强制使用某个索引。

改进

在MySQL情况下还能有其他方案来提升性能吗,在不借助Redis的情况(曾经面试招银网络被问了这道题)

我当时给出的回答是,把单个商品的库存比如50个库存,拆分成好几份,一份10个,5份库存,由于秒杀情况下流量很大,可以把这五份库存分别放到五个数据库里面,这样性能至少是原先方案的5倍,那么还会出现新的问题,就是有些问题,负载均衡上的问题,可能会出现某些库里还存在库存,但是请求却没有打进这个数据库,而是打到库存已经没有的数据库里面。我当时的想法是再搞个库存表,这个库存表采集各个商品的总库存以及商品在各个分库里面的库存数量,然后再写个服务,包含负载均衡的算法,将用户的请求平均打到各个分库去,当某个分库的库存达到0的时候,去通知该服务,服务将这个库剔除,使新的请求不会转发过去。实际这种情况也是存在问题的,高并发下库存为0的库来不及被剔除,也会导致请求被打到库存0的库。

Redis方案

将库存暂时放到Redis,然后从Redis进行库存扣减,能大大提升性能

压测结果:

image-20240109170010201

可见性能几乎是MySQL的10倍了,但是这样子在Redis里面会导致超卖

要确保Redis不超买,需要先查询当前的数量,如果大于0则进行扣减,并且查询和扣减需要为原子性,这里就需要借助lua脚本,将这两次操作写到一起。

加了Lua脚本的代码:

    private static final String LUA_DECRESE_STOCK_PATH = "lua/decreseStock.lua";

    @PostMapping(value = "/decreaseStockByRedis/{id}")
    public ResponseEntity<Object> decreaseStockByRedis(@PathVariable("id") Integer id) {

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
        redisScript.setResultType(Long.class);
        
        // 执行Lua脚本
        Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList(id));

        // 返回结果判断
        return (result != null && result == 1) ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
    }

lua脚本放在resource/lua/decreseStock.lua

local key = KEYS[1]

-- 检查键是否存在
local exists = redis.call('EXISTS', key)
if exists == 1 then
    -- 键存在,获取值
    local value = redis.call('GET', key)
    if tonumber(value) > 0 then
        -- 如果值大于0,则递减
        redis.call('DECR', key)
        return 1  -- 表示递减成功
    else
        return 0  -- 表示递减失败,值不大于0
    end
else
    return -1  -- 表示递减失败,键不存在
end

Redis同步库存到MySQL

但是在Redis扣减了库存,总需要同步到MySQL里面

@PostMapping(value = "/decreaseStockByRedis/{id}")
    public ResponseEntity<Object> decreaseStockByRedis(@PathVariable("id") Integer id) {

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
        redisScript.setResultType(Long.class);

        // 执行Lua脚本
        Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(id));
        int dataBaselResult = 0;
        if (redisResult == 1) {
            dataBaselResult = stockService.decreaseStock(id);
        }
        // 返回结果判断
        return (dataBaselResult == 1 && redisResult == 1) ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
    }

直接按照上述代码来写,删Redis后同时将库存同步到MySQL,相当于使用了Redis性能又没有提升。

其实选择了Redis来进行库存扣减,那么MySQL的库存并不需要去实时进行更新,只需要库存达到最终一致性即可,即先对Redis的库存进行更新,然后再异步同步到MySQL的库存。

如果使用spring的异步线程来解决,会不会出现同步MySQL失败导致数据最终不一致呢,在流量很多的情况下,系统本身就处于压力大的情况,再使用异步线程会占用额外的资源,最好的方法是引入MQ,把库存的同步信息交给MQ,MQ再交到消费系统,进行减库存的操作,由MQ保证消息被消费,实现最终一致性。

部分代码如下,由MQ product发出,再由consumer进行消费:

    private final DecreaseStockProduce decreaseStockProduce;

    @PostMapping(value = "/decreaseStockByRedis/{id}")
    public ResponseEntity<Object> decreaseStockByRedis(@PathVariable("id") String id) {

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
        redisScript.setResultType(Long.class);

        // 执行Lua脚本
        Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(id));
        if (redisResult == 1) {
            // 发送消息
            try {
                DecreaseStockEvent decreaseStockEvent = DecreaseStockEvent.builder()
                        .id(id)
                        .build();
                SendResult sendResult = decreaseStockProduce.sendMessage(decreaseStockEvent);
                if (!Objects.equals(sendResult.getSendStatus(), SendStatus.SEND_OK)) {
                    log.error("消息发送错误,请求参数:{}", id);
                }
            } catch (Exception e) {
                log.error("消息发送错误,请求参数:{}", id, e);
            }
        }

        // 返回结果判断
        return (redisResult == 1) ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
    }

MQ [TIMEOUT_CLEAN_QUEUE] broker busy问题

这里直接压测会报下面的错误,并且这个时候查看redis库存已经减到0,到是MySQL只减到了37

针对MQ [TIMEOUT_CLEAN_QUEUE] broker busy问题,需要去修改MQ的broker.conf文件

image-20240111165139373

针对TIMEOUT_CLEAN_QUEUE broker busy问题,需要去修改MQ的broker.conf文件,上述的201ms超时了,我这里将等待时间改为400,并且将线程数设置为64,这个线程数可以根据实际压测情况进行调整。

# 发消息线程池数量
sendMessageThreadPoolNums=64
# 拉消息线程池数量
pullMessageThreadPoolNums=64
waitTimeMillsInSendQueue=400

现在再进行压测,发现tps能跑到1000,相比直接入库mysql的200已经是提升很大了。

虽然性能提高,也实现库存的同步,但这个性能下还是会存在一些问题:

比如MQ消息发送失败、或者MySQL库存扣减失败,并且实际情况还有订单的生成和库存之间的一致性也要考虑。

对于上述这些问题,可以查看我的另外一篇博客:

RocketMQ事务消息在订单创建和库存扣减的使用 – Scotyzh – 博客园 (cnblogs.com)

【经典问题】mysql和redis数据一致性问题 - Scotyzh - 博客园

mikel阅读(304)

来源: 【经典问题】mysql和redis数据一致性问题 – Scotyzh – 博客园

前言

MySQL和Redis数据一致性算是个很经典的问题,在之前也看到过很多相关的文章,最近心血来潮,想把一致性问题的解决方案和存在问题都总结一下。

不推荐方案

1 先更新MySQL,再更新Redis。

image-20240103204207546

如上图有两个请求要同时进行更新操作,在并发情况下,B请求虽然更新时间晚于A请求,但是可能因为网络延迟问题,导致本来A请求要先更新Redis的操作晚于B请求更新Redis的操作,最终导致了MySQL出现数据不一致。

2 先更新Redis,在更新MySQL。

image-20240103204224684

这种情况其实等同于第一种情况。

3 先删除Redis缓存,再更新MySQL。

image-20240103204235856

A请求对数据的更新操作晚于请求B的读取操作,导致B请求将数据库的旧值又写回缓存,删除缓存在这种情况下没有意义。

推荐方案

1 先删除Redis缓存,再更新MySQL,再删一次Redis缓存(延迟双删)

image-20240103205637142

在第三种情况中,出现了删除缓存后被其他请求更新为旧值的情况,那么在这种情况下,再删除一遍缓存不就可以解决问题了。这里第二次删除缓存的时间必须在B请求回写旧值之后,所以要社招好第二次删除缓存的等待时间,根据业务实际耗时来定,假设B请求回写缓存要300ms,那么A请求可以设置等待500ms再进行缓存删除。

但是上面这种情况也会出现问题,比如延迟双删的时候删除缓存失败怎么办。

这个时候可以借助MQ重试机制。如下图:

image-20240104092831624

将删除的请求放到MQ队列里面,然后系统再从MQ里面取出删除请求的操作,由于MQ支持失败重试,删除失败后会继续投递消息。

2 先更新MySQL,再删除Redis缓存。

image-20240104094222584

在上面这种情况下,请求B出现了读取了一次旧值,如果对于业务是一致性要求没那么强的话(比如秒杀,减库存),这种方案也是可以的,误差范围是可以接收的,只存在这么一次数值是旧的情况。

当然还有特殊情况如下:

image-20240104095635485

当B请求先查询Redis,这个时候redis刚好缓存失效,B请求就会去MySQL查询旧值,后续B请求回写旧值的请求又晚于A请求删除缓存的请求,导致缓存里面放的是旧值。

但是这种情况出现需要 同时满足以下两个条件:

(1)缓存刚好失效

(2)读请求回写缓存的时间晚于写请求回写缓存的时间

上述两个条件同时成立的概率是极小的,综上来说,这种方案还是不错的,复杂度也不高,但同时也是可能存在删除缓存失败的特殊情况导致误差。

3 先更新MySQL,通过 Binlog,异步更新 Redis

image-20240104101137111

A请求更新完MySQL,借助Canal进行监听并把相关的修改记录推送到MQ,MQ经过消费系统拉取消息对Redis进行更新,如果在Redis更新之前,有新的读请求,依然会导致数据不一致性的问题,但是这种方案能够实现最终一致性。

在这里Canal作为一个组件,监听binlog和发送消息到MQ都由Canal完成。

方案总结

前三种方案都是不推荐使用的。对于推荐使用的方案,从实时性和技术复杂度来说,先写数据库再删除缓存是比较好的选择。如果要确保最终一致性的话,可以用binlog异步更新缓存的方案。

RocketMQ事务消息在订单创建和库存扣减的使用 - Scotyzh - 博客园

mikel阅读(260)

来源: RocketMQ事务消息在订单创建和库存扣减的使用 – Scotyzh – 博客园

前言

下单的过程包括订单创建,还有库存的扣减,为提高系统的性能,将库存放在redis扣减,则会涉及到Mysql和redis之间的数据同步,其中,这个过程还涉及到,必须是订单创建成功才进行库存的扣减操作。其次,还涉及到库存的同步,需要保证订单创建成功和redis里的库存都扣减成功,再将库存数据同步到Mysql,为了实现上述这里情况,可以借助RocketMQ的事务型消息来实现。

流程图

流程图如下,这里引入了stocklog,即订单流水表,通过判断stocklog的状态来决定是否commite消息去同步mysql,这里stocklog状态为成功的前提是订单入库和redis库存扣减成功。

image-20240116094823532

RocketMQ事务消息

在第五步执行成功返回可能因为网络状况卡住,但是stocklog状态已经得到修改

如果返回成功 MQ事务就会commit这条消息

如果没有返回成功 MQ事务会去轮询stocklog有没有被修改

一直五次轮询发现没有被修改就会回滚这条消息,这个消息相当于被删掉,不会让消费系统消费到

这条消息commit后,就会被MQ的消费者消费,对MySQL的实际库存进行更新

stock_log的意义

这里是为了保证订单的插入和redis库存扣减都成功,才进行后续异步操作MySQL,本身的存在就是为了辅助这个本地事务的成功执行再进行后续的操作,保证一致性。

这里再说一下我之前面试遇到的一个问题:既然先对redis扣减库存再MQ异步是去操作MySQL数据库扣减库存,这样子是为了提高性能,那么这套流程一开始就操作MySQL,性能会有提升吗?答案肯定有的,这里操作数据库是将订单流水入库,并没有涉及到锁,并发下不会因为行锁而影响性能。而针对某个产品的库存扣减,直接操作MySQL进行Update操作,会对这一行加上行锁,其他请求都需要阻塞等待行锁的释放。

需要的SQL表

这里简化一下下单的流程,不涉及用户表,只涉及到库存表,库存流水表,订单表。

order表

CREATE TABLE `order` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `product_num` int(11) DEFAULT NULL COMMENT '产品数量',
  PRIMARY KEY (`id`),
  KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stock表

CREATE TABLE `stock` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '库存id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `product_name` varchar(255) DEFAULT NULL COMMENT '产品名字',
  `stock_num` int(11) DEFAULT NULL COMMENT '产品库存',
  PRIMARY KEY (`id`),
  UNIQUE KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品Id唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

stock_log表

CREATE TABLE `stock_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '库存id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `amount` int(11) DEFAULT NULL COMMENT '库存变化数量',
  `status` int(11) DEFAULT NULL COMMENT '状态0->初始化,1->成功,2->回滚',
  PRIMARY KEY (`id`),
  KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键代码

OrderController类

@Controller
@RequestMapping("/order")
@RequiredArgsConstructor
@Slf4j
public class OrderController {

    private final OrderService orderService;

    private final StockLogService stockLogService;

    private final DecreaseStockProducer decreaseStockProducer;

    private final StockService stockService;

    private final RedisTemplate redisTemplate;

    @PostMapping(value = "/create/{id}")
    public ResponseEntity<Object> create(@PathVariable("id") Integer productId) {
        // 检查redis是否有库存0的标识
        if (redisTemplate.hasKey("product_stock_invalid_" + productId)) {
            return new ResponseEntity<>("库存不足", HttpStatus.OK);
        }

        // 先创建库存流水 这里默认一次只能扣减数量1的库存
        StockLog stockLog = StockLog.builder()
                .amount(1)
                .productId(productId)
                .status(0)
                .build();
        stockLogService.save(stockLog);

        // 发送事务消息
        try {
            DecreaseStockEvent decreaseStockEvent = DecreaseStockEvent.builder()
                    .productId(productId)
                    .stockLogId(stockLog.getId())
                    .build();
            SendResult sendResult = decreaseStockProducer.sendMessageInTransaction(decreaseStockEvent);
            if (!Objects.equals(sendResult.getSendStatus(), SendStatus.SEND_OK)) {
                log.error("事务消息发送错误,请求参数productId:{}", productId);
            }
        } catch (Exception e) {
            log.error("消息发送错误,请求参数:{}", productId, e);
        }

        return new ResponseEntity<>("created successfully", HttpStatus.OK);
    }

StockStatusCheckerListener类,执行本地事务和检查事务

@Slf4j
@RocketMQTransactionListener
@RequiredArgsConstructor
public class StockStatusCheckerListener implements RocketMQLocalTransactionListener {

    private final OrderService orderService;

    private final StockLogService stockLogService;

    private final TransactionTemplate transactionTemplate;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
        log.info("message: {}, args: {}", message, arg);
        TypeReference<MessageWrapper<DecreaseStockEvent>> typeReference = new TypeReference<MessageWrapper<DecreaseStockEvent>>() {};
        MessageWrapper<DecreaseStockEvent> messageWrapper = JSON.parseObject(new String((byte[]) message.getPayload()), typeReference);
        DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
        log.info("decreaseStockEvent info : {}", decreaseStockEvent);
        try {
            orderService.createOrder(decreaseStockEvent.getProductId(), decreaseStockEvent.getStockLogId());
        } catch (Exception e) {
            log.error("插入订单失败, decreaseStockEvent info : {}", decreaseStockEvent, e);
            // 触发回查
            //设置对应的stockLog为回滚状态
            StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
            stockLog.setStatus(2);
            stockLogService.updateById(stockLog);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        return RocketMQLocalTransactionState.COMMIT;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        log.info("message: {}, args: {}", message);
        MessageWrapper<DecreaseStockEvent> messageWrapper = (MessageWrapper) message.getPayload();
        DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
        StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
        if (stockLog == null) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        // 已经被扣减了库存
        if (stockLog.getStatus().intValue() == 1) {
            return RocketMQLocalTransactionState.COMMIT;
            // 初始化状态
        } else if (stockLog.getStatus().intValue() == 0) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }

}

MQ相关代码,使用模板方法

DecreaseStockProducer,消息生产者,实现了一些指定方法

@Slf4j
@Component
public class DecreaseStockProducer extends AbstractCommonSendProduceTemplate<DecreaseStockEvent> {

    private final ConfigurableEnvironment environment;

    public DecreaseStockProducer(@Autowired RocketMQTemplate rocketMQTemplate, @Autowired ConfigurableEnvironment environment) {
        super(rocketMQTemplate);
        this.environment = environment;
    }

    @Override
    protected BaseSendExtendDTO buildBaseSendExtendParam(DecreaseStockEvent messageSendEvent) {
        return BaseSendExtendDTO.builder()
                .eventName("库存同步到mysql")
                .keys(String.valueOf(messageSendEvent.getProductId()))
                .topic(environment.resolvePlaceholders(StockMQConstant.STOCK_TOPIC_KEY))
                .tag(environment.resolvePlaceholders(StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY))
                .sentTimeout(2000L)
                .build();
    }

    @Override
    protected Message<?> buildMessage(DecreaseStockEvent messageSendEvent, BaseSendExtendDTO requestParam) {
        String keys = StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();
        return MessageBuilder
                .withPayload(new MessageWrapper(requestParam.getKeys(), messageSendEvent))
                .setHeader(MessageConst.PROPERTY_KEYS, keys)
                .setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag())
                .build();
    }
}

AbstractCommonSendProduceTemplate,发送消息的类

@Slf4j
@RequiredArgsConstructor
public abstract class AbstractCommonSendProduceTemplate<T> {

    private final RocketMQTemplate rocketMQTemplate;

    /**
     * 构建消息发送事件基础扩充属性实体
     *
     * @param messageSendEvent 消息发送事件
     * @return 扩充属性实体
     */
    protected abstract BaseSendExtendDTO buildBaseSendExtendParam(T messageSendEvent);

    /**
     * 构建消息基本参数,请求头、Keys...
     *
     * @param messageSendEvent 消息发送事件
     * @param requestParam     扩充属性实体
     * @return 消息基本参数
     */
    protected abstract Message<?> buildMessage(T messageSendEvent, BaseSendExtendDTO requestParam);

   

    /**
     * 事务消息事件通用发送
     *
     * @param messageSendEvent 事务消息发送事件
     * @return 消息发送返回结果
     */
    public SendResult sendMessageInTransaction(T messageSendEvent) {
        BaseSendExtendDTO baseSendExtendDTO = buildBaseSendExtendParam(messageSendEvent);
        SendResult sendResult;
        try {
            StringBuilder destinationBuilder = StrUtil.builder().append(baseSendExtendDTO.getTopic());
            if (StrUtil.isNotBlank(baseSendExtendDTO.getTag())) {
                destinationBuilder.append(":").append(baseSendExtendDTO.getTag());
            }
            sendResult = rocketMQTemplate.sendMessageInTransaction(
                    destinationBuilder.toString(),
                    buildMessage(messageSendEvent, baseSendExtendDTO),
                    null
            );
            log.info("[{}] 消息发送结果:{},消息ID:{},消息Keys:{}", baseSendExtendDTO.getEventName(), sendResult.getSendStatus(), sendResult.getMsgId(), baseSendExtendDTO.getKeys());
        } catch (Throwable ex) {
            log.error("[{}] 消息发送失败,消息体:{}", baseSendExtendDTO.getEventName(), JSON.toJSONString(messageSendEvent), ex);
            throw ex;
        }
        return sendResult;
    }

OrderService的createOrder方法:

@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    private final OrderMapper orderMapper;

    private final StockLogMapper stockLogMapper;

    private final RedisTemplate redisTemplate;

    private final TransactionTemplate transactionTemplate;

    private static final String LUA_DECRESE_STOCK_PATH = "lua/decreseStock.lua";

    @Override
    public void createOrder(Integer productId, Integer stockLogId) {

        // 减少Redis里面的库存
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
        redisScript.setResultType(Long.class);


        // 执行Lua脚本
        Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(String.valueOf(productId)));

        if (redisResult < 1L) {
            throw new RuntimeException("库存售罄");
        }

        // 编程式事务
        transactionTemplate.executeWithoutResult(status -> {
            try {
                // 事务性操作
                Order order = Order.builder()
                        .productId(productId)
                        .productNum(1)
                        .build();
                orderMapper.insert(order);

                // 改stockLog
                StockLog stockLog = stockLogMapper.selectOne(new QueryWrapper<StockLog>().eq("id", stockLogId));
                if (stockLog == null) {
                    throw new RuntimeException("该库存流水不存在");
                }
                stockLog.setStatus(1);
                stockLogMapper.updateById(stockLog);
                // 如果操作成功,不抛出异常,事务将提交
            } catch (Exception e) {
                // 如果操作失败,抛出异常,事务将回滚 并且需要补偿redis的库存
                redisTemplate.opsForValue().increment(String.valueOf(productId));
                status.setRollbackOnly();
            }
        });

    }
}

redis的lua脚本代码如下,这里只会在库存大于0的时候进行扣减,先检查库存,再扣减。如果库存为0,在redis里面setIfAbsent该商品售罄的标识,这样子在controller查询到售罄就直接return

local key = KEYS[1]

-- 检查键是否存在
local exists = redis.call('EXISTS', key)
if exists == 1 then
    -- 键存在,获取值
    local value = redis.call('GET', key)
    if tonumber(value) > 0 then
        -- 如果值大于0,则递减
        redis.call('DECR', key)
        return 1  -- 表示递减成功
    else
        local prefix = "product_stock_invalid_"
        local stock_invalid_tag = prefix .. KEYS[1]
        local exists_tag = redis.call('EXISTS', stock_invalid_tag)
        if exists_tag == 0 then
            -- 键不存在,设置键的值
            redis.call('SET', stock_invalid_tag, "true")
        return 0  -- 表示递减失败,值不大于0
        end
    end
else
    return -1  -- 表示递减失败,键不存在
end

MQ的consumer:

@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
        topic = StockMQConstant.STOCK_TOPIC_KEY,
        selectorExpression = StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY,
        consumerGroup = StockMQConstant.STOCK_DEREASE_STOCK_CG_KEY
)
public class DecreaseStockConsumer implements RocketMQListener<MessageWrapper<DecreaseStockEvent>> {

    private final StockService stockService;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void onMessage(MessageWrapper<DecreaseStockEvent> message) {
        DecreaseStockEvent decreaseStockEvent = message.getMessage();
        Integer productId = decreaseStockEvent.getProductId();
        try {
            stockService.decreaseStock(productId);
        } catch (Exception e) {
            log.error("库存同步到mysql失败,productId:{}", productId, e);
            throw e;
        }
    }
}

stockService.decreaseStock()方法如下

    public int decreaseStock(Integer productId) {
        return stockMapper.decreaseStock(productId);
    }

相关的SQL语句

    <update id="decreaseStock">
        UPDATE stock
        SET stock_num = stock_num - 1
        WHERE id = #{id} AND stock_num >= 1
    </update>

消息重复消费问题

我们知道,MQ可能会存在重复消费的问题,包括我在压测的时候,就存在了重复消费,导致MySQL的库存最终比redis库存要少,重复扣减了MySQL的库存,针对这种情况,应该解决幂等性问题。

在前面我们用MessageWrapper来包装消息体的时候,每次new一个MessageWrapper都会生成新的UUID,我们将这UUID存到Redis里面来保证幂等性

/**
 * 消息体包装器
 */
@Data
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 消息发送 Keys
     */
    @NonNull
    private String keys;

    /**
     * 消息体
     */
    @NonNull
    private T message;

    /**
     * 唯一标识,用于客户端幂等验证
     */
    private String uuid = UUID.randomUUID().toString();

    /**
     * 消息发送时间
     */
    private Long timestamp = System.currentTimeMillis();
}

修改后的扣减库存方法,先判断redis里面有没有存在已经扣除了库存的标识,有就直接返回

@Service
@RequiredArgsConstructor
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {

    private final StockMapper stockMapper;

    private final RedisTemplate redisTemplate;

    @Override
    public int decreaseStock(Integer productId, String UUID) {
        if(redisTemplate.hasKey("decrease_mark_" + UUID)) {
            return 0;
        }
        redisTemplate.opsForValue().set("decrease_mark_" + UUID, "true", 24, TimeUnit.HOURS);
        return stockMapper.decreaseStock(productId);
    }
}

下面是上述demo的代码地址,修改数据库和mysql地址即可使用

scottyzh/stock-demo: RocketMQ事务消息在订单生成和扣减库存的应用 (github.com)

Asp .Net Core 系列:集成 Ocelot+Nacos+Swagger+Cors实现网关、服务注册、服务发现 - Code技术分享 - 博客园

mikel阅读(346)

来源: Asp .Net Core 系列:集成 Ocelot+Nacos+Swagger+Cors实现网关、服务注册、服务发现 – Code技术分享 – 博客园

 

简介

什么是 Ocelot ?

Ocelot是一个开源的ASP.NET Core微服务网关,它提供了API网关所需的所有功能,如路由、认证、限流、监控等。

Ocelot是一个简单、灵活且功能强大的API网关,它可以与现有的服务集成,并帮助您保护、监控和扩展您的微服务。

以下是Ocelot的一些主要功能:

  1. 路由管理:Ocelot允许您定义路由规则,将请求路由到正确的微服务。
  2. 认证和授权:Ocelot支持多种认证机制,如JWT、OAuth等,并允许您定义访问控制策略,确保只有授权的用户才能访问特定的API。
  3. 限流和速率限制:Ocelot提供了一些内置的限流和速率限制功能,以确保您的服务不会受到过度的请求压力。
  4. 监控和日志:Ocelot可以收集和显示各种度量指标,帮助您了解您的服务的性能和行为。此外,它还可以将日志记录到各种日志源,以便您进行分析和故障排除。
  5. 集成:Ocelot可以与现有的服务集成,包括Kubernetes、Consul等。
  6. 易于扩展:Ocelot的设计使其易于扩展,您可以编写自己的中间件来处理特定的逻辑,例如修改请求或响应、添加自定义的认证机制等。
  7. 可扩展的配置:Ocelot使用JSON配置文件进行配置,这意味着您可以轻松地根据需要进行配置更改,而无需重新编译代码。

总之,Ocelot是一个功能强大且易于使用的API网关,可以帮助您保护、监控和扩展您的微服务。

官网:https://ocelot.readthedocs.io/en/latest/index.html

什么是 Nacos ?

Nacos是一个易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是Dynamic Naming and Configuration Service的首字母简称。Nacos提供了一组简单易用的特性集,包括动态服务发现、服务配置、服务元数据及流量管理等功能,帮助用户快速实现微服务的发现、配置和管理。Nacos还支持多种服务注册方式和服务发现方式,如DNS、RPC、原生SDK和OpenAPI等。

此外,Nacos致力于提供更敏捷和容易的微服务平台构建、交付和管理。它是构建以“服务”为中心的现代应用架构(例如微服务范式、云原生范式)的服务基础设施,能够支持动态DNS服务权重路由和动态DNS服务等特性。

官网:https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html

什么是 Swagger ?

Swagger是一种规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。它是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法、参数和模型紧密集成到服务器端的代码,允许API始终保持同步。

此外,Swagger还提供了一个文档工具,可自动生成Web服务的API文档,使开发人员能够更轻松地理解和使用API。它还提供了一个测试工具,可以模拟对Web服务的API请求并验证响应。

什么是 Cors ?

CORS(跨来源资源共享,Cross-Origin Resource Sharing)是一种机制,允许Web应用程序在未经服务器明确许可的情况下,通过浏览器向服务器发送跨域请求。CORS是一种W3C规范,旨在解决Web应用程序中的跨域问题,以促进Web应用程序的安全性和可扩展性。

在Web应用程序中,浏览器会遵循同源策略(Same-Origin Policy),即默认只允许来自同一域的页面之间进行通信。然而,随着Web应用程序的发展,越来越多的应用程序需要与不同域的资源进行交互,例如使用第三方API或进行跨域请求。为了解决这个问题,CORS规范允许服务器通过设置适当的HTTP标头来明确地允许跨域请求。

当浏览器向服务器发送跨域请求时,服务器可以在响应头中包含一个Access-Control-Allow-Origin标头,指定允许哪些源进行跨域请求。浏览器会检查这个标头,如果允许的源与请求的源匹配,则允许跨域请求。此外,CORS规范还定义了其他一些相关的标头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers等,以进一步控制跨域请求的行为。

通过使用CORS机制,Web应用程序可以更安全、更有效地进行跨域请求,提高了应用程序的可扩展性和用户体验。

Asp .Net Core 集成 Ocelot

要在ASP.NET Core中集成Ocelot,您可以按照以下步骤进行操作:

  1. 安装Ocelot NuGet包:
    在您的ASP.NET Core项目中,打开终端或NuGet包管理器控制台,并运行以下命令来安装Ocelot的NuGet包:
dotnet add package Ocelot
  1. 添加Ocelot配置文件:
{
  "Routes": [ //这里注意一下版本(旧版本用ReRoutes)
    {
      "DownstreamPathTemplate": "/api/{controller}", //下游路径模板
      "DownstreamScheme": "http", //下游方案
      //"DownstreamHostAndPorts": [
      //  {
      //    "Host": "localhost",
      //    "Port": "5014"
      //  }
      //], //下游主机和端口
      "UpstreamPathTemplate": "/api/product/{controller}", //上游路径模板
      "UpstreamHttpMethod": [], //上游请求方法,可以设置特定的 HTTP 方法列表或设置空列表以允许其中任何方法
      "ServiceName": "api-product-service", //请求服务名称
      "LoadBalancerOptions": {
        "Type": "LeastConnection" //负载均衡算法:目前 Ocelot 有RoundRobin 和LeastConnection算法
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5015", //进行标头查找和替换以及某些管理配置
    "ServiceDiscoveryProvider": {
      "Type": "Nacos"
    }
  },
  "Nacos": {
    "ServerAddresses": [ "http://127.0.0.1:8848" ], //服务地址
    "UserName": "nacos",  //用户名
    "Password": "nacos", //密码
    "ServiceName": "api-gateway", //服务名称
    //"Namespace": "",  //命名空间
    //"GroupName": "DEFAULT_GROUP" //组名,
    //"ClusterName": "DEFAULT", // 集群名称
    "ListenInterval": 1000,   //监听
    "RegisterEnabled": true, // 注册是否启动
    "InstanceEnabled": true   //实例是否启动
  },
  "Url": "http://*:5015"
}
  1. 配置Ocelot服务:
builder.Services.AddOcelot();

Configure方法中配置请求管道并添加Ocelot中间件:

app.UseOcelot().Wait();

网关集成 Nacos

要将Naocs集成Ocelot到中,您可以按照以下步骤进行操作:

  1. 下载Ocelot.Provider.Nacos 源码,导入Ocelot.Provider.Nacos 项目

    github:https://github.com/softlgl/Ocelot.Provider.Nacos

    修改Ocelot.Provider.Nacos 源码,在Ocelot 22版本中 IServiceDiscoveryProvider接口中的Get方法变成了GetAsync

    升级各Nuget包

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Ocelot.ServiceDiscovery.Providers;
    using Ocelot.Values;
    using Nacos.V2;
    using Microsoft.Extensions.Options;
    using Ocelot.Provider.Nacos.NacosClient.V2;
    using NacosConstants = Nacos.V2.Common.Constants;
    
    namespace Ocelot.Provider.Nacos
    {
        public class Nacos : IServiceDiscoveryProvider
        {
            private readonly INacosNamingService _client;
            private readonly string _serviceName;
            private readonly string _groupName;
            private readonly List<string> _clusters;
    
            public Nacos(string serviceName, INacosNamingService client, IOptions<NacosAspNetOptions> options)
            {
                _serviceName = serviceName;
                _client = client;
                _groupName = string.IsNullOrWhiteSpace(options.Value.GroupName) ? 
                   NacosConstants.DEFAULT_GROUP : options.Value.GroupName;
                _clusters = (string.IsNullOrWhiteSpace(options.Value.ClusterName) ? NacosConstants.DEFAULT_CLUSTER_NAME : options.Value.ClusterName).Split(",").ToList();
            }
    
            public async  Task<List<Service>> GetAsync()
            {
                var services = new List<Service>();
    
                var instances = await _client.GetAllInstances(_serviceName, _groupName, _clusters);
    
                if (instances != null && instances.Any())
                {
                    services.AddRange(instances.Select(i => new Service(i.InstanceId, new ServiceHostAndPort(i.Ip, i.Port), "", "", new List<string>())));
                }
    
                return await Task.FromResult(services);
            }
        }
    }
    
  2. 配置Ocelot:
    在Ocelot的配置中,您需要指定Nacos作为服务发现和配置的提供者。在Ocelot的配置文件(例如appsettings.json)中,添加以下内容:
{
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5015", //进行标头查找和替换以及某些管理配置
    "ServiceDiscoveryProvider": {
      "Type": "Nacos" //指定Nacos
    }
  },
  "Nacos": {
    "ServerAddresses": [ "http://127.0.0.1:8848" ], //服务地址
    "UserName": "nacos",  //用户名
    "Password": "nacos", //密码
    "ServiceName": "api-gateway", //服务名称
    //"Namespace": "",  //命名空间
    //"GroupName": "DEFAULT_GROUP" //组名,
    //"ClusterName": "DEFAULT", // 集群名称
    "ListenInterval": 1000,   //监听
    "RegisterEnabled": true, // 注册是否启动
    "InstanceEnabled": true   //实例是否启动
  }
}
  1. 启动Ocelot:
    在您的ASP.NET Core应用程序中启动Ocelot。您可以在Startup.cs文件中添加以下代码:
builder.Services.AddOcelot().AddNacosDiscovery("Nacos");

下游配置 Nacos

  1. 安装必要的NuGet包:

    在Visual Studio中打开你的项目,通过NuGet包管理器安装Nacos.AspNetCore包。可以通过NuGet包管理器控制台运行以下命令来安装:

Install-Package Nacos.AspNetCore
  1. 配置Nacos客户端:

    appsettings.json文件中添加Nacos服务的配置信息,例如服务器地址、端口、命名空间等信息。示例配置如下:

{
  "Nacos": {
    "ServerAddresses": [ "http://127.0.0.1:8848" ],
    //命名空间GUID,public默认没有
    //"Namesapce": "",
    "UserName": "nacos",
    "Password": "nacos",
    // 配置中心
    //"Listeners": [
    //  {
    //    "Group": "dev",
    //    "DataId": "api-product-service",
    //    "Optional": false
    //  }
    //],
    // 服务发现
    "Ip": "localhost", // Nacos 注册时如果没有指定IP,那么就按照本机的IPv4 Address
    "Port": "5014", //端口
    "ServiceName": "api-product-service" // 服务名称
    //"GroupName": "",
    // 权重
    //"Weight": 100
  }
}
  1. 配置依赖注入:

    如果你需要在你的应用程序中使用Nacos服务,可以在Startup.csConfigureServices方法中注册Nacos服务的依赖注入。示例如下:

builder.Services.AddNacosAspNet(builder.Configuration,"Nacos");

配置跨域(Cors)

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Cors
{
    public static class CorsServiceExtensions
    {
        private readonly static string PolicyName = "MCodeCors";

        /// <summary>
        /// 添加跨域
        /// </summary>
        /// <param name="services">服务集合</param>
        /// <returns></returns>
        public static IServiceCollection AddMCodeCors(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
            //origin microsoft.aspnetcore.cors      
            return services.AddCors(options =>
            {
                options.AddPolicy(PolicyName, policy =>
                {
                    policy.SetIsOriginAllowed(_ => true).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
                });
            });
        }
        /// <summary>
        /// 使用跨域
        /// </summary>
        /// <param name="app">应用程序建造者</param>
        /// <returns></returns>
        public static IApplicationBuilder UseMCodeCors(this IApplicationBuilder app)
        {
            return app.UseCors(PolicyName);
        }
    }
}

网关和微服务中配置Swagger

SwaggerOptions

using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// Swagger配置
    /// </summary>
    public class SwaggerOptions
    {
        /// <summary>
        /// 服务名称
        /// </summary>
        public string ServiceName { get; set; }

        /// <summary>
        /// API信息
        /// </summary>
        public OpenApiInfo ApiInfo { get; set; }

        /// <summary>
        /// Xml注释文件
        /// </summary>
        public string[] XmlCommentFiles { get; set; }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="serviceName">服务名称</param>
        /// <param name="apiInfo">API信息</param>
        /// <param name="xmlCommentFiles">Xml注释文件</param>
        public SwaggerOptions(string serviceName, OpenApiInfo apiInfo, string[] xmlCommentFiles = null)
        {
            ServiceName = !string.IsNullOrWhiteSpace(serviceName) ? serviceName : throw new ArgumentException("serviceName parameter not config.");
            ApiInfo = apiInfo;
            XmlCommentFiles = xmlCommentFiles;
        }
    }
}

SwaggerEndPoint

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// Swagger终端
    /// </summary>
    public class SwaggerEndPoint
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 地址
        /// </summary>
        public string Url { get; set; }
    }
}

OcelotSwaggerOptions

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// 网关Swagger配置
    /// </summary>
    public class OcelotSwaggerOptions
    {
        public List<SwaggerEndPoint> SwaggerEndPoints { get; set; }
    }
}

SwaggerServiceExtensions

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// Swagger 服务扩展
    /// </summary>
    public static class SwaggerServiceExtensions
    {
        /// <summary>
        /// 添加 Swagger 服务
        /// </summary>
        /// <param name="services"></param>
        /// <param name="swaggerOptions"></param>
        /// <returns></returns>
        public static IServiceCollection AddMCodeSwagger(this IServiceCollection services, SwaggerOptions swaggerOptions)
        {
            services.AddSingleton(swaggerOptions);

            SwaggerGenServiceCollectionExtensions.AddSwaggerGen(services, c =>
            {
                c.SwaggerDoc(swaggerOptions.ServiceName, swaggerOptions.ApiInfo);

                if (swaggerOptions.XmlCommentFiles != null)
                {
                    foreach (string xmlCommentFile in swaggerOptions.XmlCommentFiles)
                    {
                        string str = Path.Combine(AppContext.BaseDirectory, xmlCommentFile);
                        if (File.Exists(str)) c.IncludeXmlComments(str, true);
                    }
                }

                SwaggerGenOptionsExtensions.CustomSchemaIds(c, x => x.FullName);

                c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer",
                    BearerFormat = "JWT",
                    Description = "请输入 bearer 认证"
                });


                c.AddSecurityRequirement(new OpenApiSecurityRequirement
                                              {
                                                  {
                                                      new OpenApiSecurityScheme
                                                      {
                                                          Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "bearerAuth" }
                                                      },
                                                      new string[] {}
                                                  }
                                              });
            });

            return services;
        }

        /// <summary>
        /// 使用 Swagger UI
        /// </summary>
        /// <param name="app"></param>
        /// <returns></returns>
        public static IApplicationBuilder UseMCodeSwagger(this IApplicationBuilder app)
        {
            string serviceName = app.ApplicationServices.GetRequiredService<SwaggerOptions>().ServiceName;

            SwaggerUIBuilderExtensions.UseSwaggerUI(SwaggerBuilderExtensions.UseSwagger(app), c =>
            {
                c.SwaggerEndpoint("/swagger/" + serviceName + "/swagger.json", serviceName);
            });
            return app;
        }


        public static IServiceCollection AddMCodeOcelotSwagger(this IServiceCollection services, OcelotSwaggerOptions ocelotSwaggerOptions)
        {
            services.AddSingleton(ocelotSwaggerOptions);
            SwaggerGenServiceCollectionExtensions.AddSwaggerGen(services);
            return services;
        }

        public static IApplicationBuilder UseMCodeOcelotSwagger(this IApplicationBuilder app)
        {
            OcelotSwaggerOptions ocelotSwaggerOptions = app.ApplicationServices.GetService<OcelotSwaggerOptions>();

            if (ocelotSwaggerOptions == null || ocelotSwaggerOptions.SwaggerEndPoints == null)
            {
                return app;
            }

            SwaggerUIBuilderExtensions.UseSwaggerUI(SwaggerBuilderExtensions.UseSwagger(app), c =>
            {
                foreach (SwaggerEndPoint swaggerEndPoint in ocelotSwaggerOptions.SwaggerEndPoints)
                {
                    c.SwaggerEndpoint(swaggerEndPoint.Url, swaggerEndPoint.Name);
                }
            });
            return app;
        }
    }
}

效果

image

image

其他文章:Asp .Net Core 系列:集成 Ocelot+Consul+Swagger+Cors实现网关、服务注册、服务发现

.NET使用QuestPDF高效地生成PDF文档 - 追逐时光者 - 博客园

mikel阅读(286)

来源: .NET使用QuestPDF高效地生成PDF文档 – 追逐时光者 – 博客园

前言

在.NET平台中操作生成PDF的类库有很多如常见的有iTextSharp、PDFsharp、Aspose.PDF等,今天我们分享一个用于生成PDF文档的现代开源.NET库:QuestPDF,本文将介绍QuestPDF并使用它快速实现发票PDF文档生成功能。

QuestPDF介绍

QuestPDF 是一个用于生成 PDF 文档的现代开源 .NET 库。QuestPDF 由简洁易用的 C# Fluent API 提供全面的布局引擎。轻松生成 PDF 报告、发票、导出等。QuestPDF它提供了一个布局引擎,在设计时考虑了完整的分页支持。与其他库不同,它不依赖于 HTML 到 PDF 的转换,这在许多情况下是不可靠的。相反,它实现了自己的布局引擎,该引擎经过优化,可以满足所有与分页相关的要求。

QuestPDF License

分为社区版、专业版、和企业版。

项目源代码

创建一个控制台应用

创建一个名为QuestPDFTest的控制台应用。

安装QuestPDF Nuget包

搜索:QuestPDF包进行安装。

 

快速实现发票PDF文档生成

创建InvoiceModel

namespace QuestPDFTest
{
    public class InvoiceModel
    {

        /// <summary>
        /// 发票号码
        /// </summary>
        public int InvoiceNumber { getset; }

        /// <summary>
        /// 发票开具日期
        /// </summary>
        public DateTime IssueDate { getset; }

        /// <summary>
        /// 发票到期日期
        /// </summary>
        public DateTime DueDate { getset; }

        /// <summary>
        /// 卖方公司名称
        /// </summary>
        public string SellerCompanyName { getset; }

        /// <summary>
        /// 买方公司名称
        /// </summary>
        public string CustomerCompanyName { getset; }

        /// <summary>
        /// 订单消费列表
        /// </summary>
        public List<OrderItem> OrderItems { getset; }

        /// <summary>
        /// 备注
        /// </summary>
        public string Comments { getset; }
    }

    public class OrderItem
    {
        /// <summary>
        /// 消费类型
        /// </summary>
        public string Name { getset; }

        /// <summary>
        /// 消费金额
        /// </summary>
        public decimal Price { getset; }

        /// <summary>
        /// 消费数量
        /// </summary>
        public int Quantity { getset; }
    }
}

CreateInvoiceDetails

namespace QuestPDFTest
{
    public class CreateInvoiceDetails
    {
        private static readonly Random _random = new Random();

        public enum InvoiceType
        {
            餐饮费,
            交通费,
            住宿费,
            日用品,
            娱乐费,
            医疗费,
            通讯费,
            教育费,
            装修费,
            旅游费
        }

        /// <summary>
        /// 获取发票详情数据
        /// </summary>
        /// <returns></returns>
        public static InvoiceModel GetInvoiceDetails()
        {
            return new InvoiceModel
            {
                InvoiceNumber = _random.Next(1_000, 10_000),
                IssueDate = DateTime.Now,
                DueDate = DateTime.Now + TimeSpan.FromDays(14),
                SellerCompanyName = "追逐时光者",
                CustomerCompanyName = "DotNetGuide技术社区",
                OrderItems = Enumerable
                .Range(120)
                .Select(_ => GenerateRandomOrderItemInfo())
                .ToList(),
                Comments = "DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。"
            };
        }

        /// <summary>
        /// 订单信息生成
        /// </summary>
        /// <returns></returns>
        private static OrderItem GenerateRandomOrderItemInfo()
        {
            var types = (InvoiceType[])Enum.GetValues(typeof(InvoiceType));
            return new OrderItem
            {
                Name = types[_random.Next(types.Length)].ToString(),
                Price = (decimal)Math.Round(_random.NextDouble() * 1002),
                Quantity = _random.Next(110)
            };
        }
    }
}

CreateInvoiceDocument

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    public class CreateInvoiceDocument : IDocument
    {
        /// <summary>
        /// 获取Logo的的Image对象
        /// </summary>
        public static Image LogoImage { get; } = Image.FromFile("dotnetguide.png");

        public InvoiceModel Model { get; }

        public CreateInvoiceDocument(InvoiceModel model)
        {
            Model = model;
        }

        public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

        public void Compose(IDocumentContainer container)
        {
            container
                .Page(page =>
                {
                    //设置页面的边距
                    page.Margin(50);

                    //字体默认大小18号字体
                    page.DefaultTextStyle(x => x.FontSize(18));

                    //页眉部分
                    page.Header().Element(BuildHeaderInfo);

                    //内容部分
                    page.Content().Element(BuildContentInfo);

                    //页脚部分
                    page.Footer().AlignCenter().Text(text =>
                    {
                        text.CurrentPageNumber();
                        text.Span(" / ");
                        text.TotalPages();
                    });
                });
        }

        #region 构建页眉部分
        void BuildHeaderInfo(IContainer container)
        {
            container.Row(row =>
            {
                row.RelativeItem().Column(column =>
                {
                    column.Item().Text($"发票编号 #{Model.InvoiceNumber}").FontFamily("fangsong").FontSize(20).SemiBold().FontColor(Colors.Blue.Medium);

                    column.Item().Text(text =>
                    {
                        text.Span("发行日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.IssueDate:d}");
                    });

                    column.Item().Text(text =>
                    {
                        text.Span("终止日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.DueDate:d}");
                    });
                });

                //在当前行的常量项中插入一个图像
                row.ConstantItem(130).Image(LogoImage);
            });
        }

        #endregion

        #region 构建内容部分

        void BuildContentInfo(IContainer container)
        {
            container.PaddingVertical(40).Column(column =>
            {
                column.Spacing(20);

                column.Item().Row(row =>
                {
                    row.RelativeItem().Component(new AddressComponent("卖方公司名称", Model.SellerCompanyName));
                    row.ConstantItem(50);
                    row.RelativeItem().Component(new AddressComponent("客户公司名称", Model.CustomerCompanyName));
                });

                column.Item().Element(CreateTable);

                var totalPrice = Model.OrderItems.Sum(x => x.Price * x.Quantity);
                column.Item().PaddingRight(5).AlignRight().Text($"总计: {totalPrice}").FontFamily("fangsong").SemiBold();

                if (!string.IsNullOrWhiteSpace(Model.Comments))
                    column.Item().PaddingTop(25).Element(BuildComments);
            });
        }

        /// <summary>
        /// 创建表格
        /// </summary>
        /// <param name="container">container</param>
        void CreateTable(IContainer container)
        {
            var headerStyle = TextStyle.Default.SemiBold();

            container.Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.ConstantColumn(25);
                    columns.RelativeColumn(3);
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                });

                table.Header(header =>
                {
                    header.Cell().Text("#").FontFamily("fangsong");
                    header.Cell().Text("消费类型").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("花费金额").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("数量").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("总金额").Style(headerStyle).FontFamily("fangsong");
                    //设置了表头单元格的属性
                    header.Cell().ColumnSpan(5).PaddingTop(5).BorderBottom(1).BorderColor(Colors.Black);
                });

                foreach (var item in Model.OrderItems)
                {
                    var index = Model.OrderItems.IndexOf(item) + 1;

                    table.Cell().Element(CellStyle).Text($"{index}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).Text(item.Name).FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Quantity}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}").FontFamily("fangsong");
                    static IContainer CellStyle(IContainer container) => container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5);
                }
            });
        }

        #endregion

        #region 构建页脚部分

        void BuildComments(IContainer container)
        {
            container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).Column(column =>
            {
                column.Spacing(5);
                column.Item().Text("DotNetGuide技术社区介绍").FontSize(14).FontFamily("fangsong").SemiBold();
                column.Item().Text(Model.Comments).FontFamily("fangsong");
            });
        }

        #endregion
    }

    public class AddressComponent : IComponent
    {
        private string Title { get; }
        private string CompanyName { get; }

        public AddressComponent(string title, string companyName)
        {
            Title = title;
            CompanyName = companyName;
        }

        public void Compose(IContainer container)
        {
            container.ShowEntire().Column(column =>
            {
                column.Spacing(2);

                column.Item().Text(Title).FontFamily("fangsong").SemiBold();
                column.Item().PaddingBottom(5).LineHorizontal(1);
                column.Item().Text(CompanyName).FontFamily("fangsong");
            });
        }
    }
}

Program

using QuestPDF;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 1、请确保您有资格使用社区许可证,不设置的话会报异常。
            Settings.License = LicenseType.Community;

            // 2、禁用QuestPDF库中文本字符可用性的检查
            Settings.CheckIfAllTextGlyphsAreAvailable = false;

            // 3、PDF Document 创建
            var invoiceSourceData = CreateInvoiceDetails.GetInvoiceDetails();
            var document = new CreateInvoiceDocument(invoiceSourceData);

            // 4、生成 PDF 文件并在默认的查看器中显示
            document.GeneratePdfAndShow();
        }
    }
}

完整示例源代码

https://github.com/YSGStudyHards/QuestPDFTest

示例运行效果图

注意问题

中文报异常

QuestPDF.Drawing.Exceptions.DocumentDrawingException:“Could not find an appropriate font fallback for glyph: U-53D1 '发'. Font families available on current environment that contain this glyph: Microsoft JhengHei, Microsoft JhengHei UI, Microsoft YaHei, Microsoft YaHei UI, SimSun, NSimSun, DengXian, FangSong, KaiTi, SimHei, FZCuHeiSongS-B-GB. Possible solutions: 1) Use one of the listed fonts as the primary font in your document. 2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. However, this may result with text glyphs being incorrectly rendered without any warning.”

加上这段代码:

// 2、禁用QuestPDF库中文本字符可用性的检查
Settings.CheckIfAllTextGlyphsAreAvailable = false;

原因:

默认情况下,使用 QuestPDF 生成 PDF 文档时,它会检查所使用的字体是否支持文本中的所有字符,并在发现不能显示的字符时输出一条警告消息。这个选项可以确保文本中的所有字符都能正确地显示在生成的 PDF 文件中。

中文乱码问题

解决方案:

假如Text(“”)中为汉字一定要在后面加上FontFamily(“fangsong”)[仿宋字体]或FontFamily(“simhei”)[黑体字体],否则中文无法正常显示。

项目源码地址

更多项目实用功能和特性欢迎前往项目开源地址查看👀,别忘了给项目一个Star支持💖。

GitHub地址:https://github.com/QuestPDF/QuestPDF

文档地址:https://www.questpdf.com/api-reference/

优秀项目和框架精选

该项目已收录到C#/.NET/.NET Core优秀项目和框架精选中,关注优秀项目和框架精选能让你及时了解C#、.NET和.NET Core领域的最新动态和最佳实践,提高开发工作效率和质量。坑已挖,欢迎大家踊跃提交PR推荐或自荐(让优秀的项目和框架不被埋没🤞)。

https://github.com/YSGStudyHards/DotNetGuide/blob/main/docs/DotNet/DotNetProjectPicks.md

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。
  • 在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。

5分钟教会你如何在生产环境debug代码 - 欧阳码农 - 博客园

mikel阅读(441)

来源: 5分钟教会你如何在生产环境debug代码 – 欧阳码农 – 博客园

前言

有时出现的线上bug在测试环境死活都不能复现,靠review代码猜测bug出现的原因,然后盲改代码直接在线上测试明显不靠谱。这时我们就需要在生产环境中Debug代码,快速找到bug的原因,然后将锅丢出去。

生产环境的代码一般都是关闭source map和经过混淆的,那么如何进行Debug代码呢?我一般都是使用这两种方式debug线上代码:“通过console找到源代码打断点”和“通过network面板的Initiator找到源代码打断点”。

通过console找到源代码打断点

打开浏览器控制台的console面板,在上面找到由bug导致抛出的报错信息或者在代码里面通过console.log打的日志。然后点击最右边的文件名称跳转到具体的源码位置,直接在代码中打上断点就可以debug代码了。

如果点击右边的文件名后出现这种404报错的情况。
could-not-load-content-for-webpack://***-(fetch-through-target-failed:-unsupported-url-scheme;-fallback:-http-error:-status-code-404,-net:: ERR_UNKNOWN_URL_SCHEME)

只需要点击控制台右边倒数第三个图标setting(设置),将preferences(偏好设置)中的Enable JavaScript source maps(启用 JavaScript 源代码映射)取消勾选后再重新点console最右边的文件名称即可。

这种方式很简单就可以找到源代码,但是有的bug是没有报错信息的,而且我们也不可能到处都给代码加上console.log,所以这种方式有一定的局限性。

通过network面板的Initiator找到源代码打断点

将鼠标放到请求的Initiator(启动器)后,就会显示当前请求完整的调用链中的方法和函数。假如请求是由A函数中发起的,B函数调用了A函数,C函数又调用了B函数。那么这种情况中Initiator就会按照顺序依次将A、B、C函数都列出来。

了解了Initiator的作用思路就清晰了,我们只需要找到离bug最近的一个接口请求,然后从调用链中找到我们需要的方法或者函数就可以了。

这时有的小伙伴又会说了,线上的代码都是经过混淆的,原本代码中的函数和变量经过混淆后已经都不是原本的名字了,那么我们怎么知道调用栈中哪个是我们想要找的函数呢?

确实函数和变量名称经过混淆后已经变得面目全非了,但是对象中的方法和属性名称是不会被修改的,还是会保留原本的名字。比如我们有一个对象名字叫user,user中有个名叫dance的方法。经过混淆后user对象的名字可能已经变成了U,但是dance方法还是叫原本的名字,不会被修改。利用这一点我们可以在调用栈中找到我们熟悉的对象方法名称就可以很快的定位到源代码。

举个例子,我们当前有个service/common.js文件

import axios from "axios";

const urls = {
  messageList: "http://127.0.0.1:3000/api/getMessageList",
};

const methods = {
  getMessageList() {
    return axios({
      method: "get",
      url: urls.messageList,
    });
  },
};

export default {
  urls,
  methods,
};

业务组件中这样调用

import CommonService from "@/service/common.js";

async function initData() {
  const res = await CommonService.methods.getMessageList();
  const formatData: Array<Message> = handleFormatData(res.data.list);
  messageList.value = formatData;
}

Initiator调用栈中就可以很容易的找到getMessageList方法,并且我们知道getMessageList方法是我们的initData调用的。那么在调用栈中getMessageList的上一个就是我们想要找的源代码位置,点击文件名称就可以跳转到目标源代码具体的位置。

如果跳转到源代码后代码是被压缩的状态,点左下角的花括号将代码格式化。找到具体的定位后,经过比对其实混淆后的代码和源代码其实差别不是特别大,debug代码还是很容易的。

这时有的小伙伴又会问了,假如我们出现bug的地方没有接口请求怎么办呢?

这种情况也可以利用Initiator调用栈找到对应的源代码js文件,然后搜索你知道的属性和方法名字,因为属性和方法名称在混淆的过程中是不会被重写的。这样也可以找到源代码的位置。

总结

这篇文章主要介绍了两种在线上debug源码的方法。第一种方法是在控制台找到console输出,点击console右边的文件名称跳转到源码进行debug。第二种方式通过请求的Initiator调用栈,找到源代码中对应的方法,点击文件名称也可以跳转到源代码具体的位置。

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!

这才是你应该了解的Redis数据结构! - lyxlucky - 博客园

mikel阅读(260)

来源: 这才是你应该了解的Redis数据结构! – lyxlucky – 博客园

Redis,作为一种高性能的内存数据库,支持多种数据结构,从简单的字符串到复杂的哈希表。在这篇博文中,我们将深入探讨Redis的一些主要数据结构,并通过详细的例子展示它们的使用。

1. 字符串 (String)

1.1 存储和获取

Redis中的字符串是二进制安全的,可以存储任何数据。让我们通过一个简单的例子来演示:

# 存储字符串
SET my_key "Hello, Redis!"

# 获取字符串
GET my_key

在这个例子中,我们使用SET命令将字符串”Hello, Redis!”存储在my_key中,并通过GET命令获取它。

1.2 字符串操作

Redis提供了丰富的字符串操作,比如拼接、截取等。让我们看一个例子:

# 拼接字符串
APPEND my_key ", How are you?"

# 获取更新后的字符串
GET my_key

在这里,我们使用APPEND命令将”, How are you?”拼接到之前的字符串后面。

2. 列表 (List)

2.1 添加和获取元素

列表是一个有序的字符串元素集合。我们可以使用LPUSHLRANGE来添加和获取元素:

# 添加元素到列表的头部
LPUSH my_list "Apple"
LPUSH my_list "Banana"
LPUSH my_list "Orange"

# 获取列表的元素
LRANGE my_list 0 -1

在这个例子中,我们通过LPUSH命令将”Apple”、”Banana”和”Orange”添加到my_list的头部,并通过LRANGE命令获取整个列表。

2.2 列表操作

Redis提供了许多列表操作,比如裁剪、弹出等。让我们看一个例子:

# 裁剪列表,保留前两个元素
LTRIM my_list 0 1

# 弹出列表的最后一个元素
RPOP my_list

# 获取更新后的列表
LRANGE my_list 0 -1

在这里,我们使用LTRIM命令裁剪列表,保留前两个元素,然后使用RPOP命令弹出最后一个元素。

3. 集合 (Set)

3.1 添加和获取元素

集合是一个无序、唯一元素的集合。我们可以使用SADDSMEMBERS来添加和获取元素:

# 添加元素到集合
SADD my_set "Red"
SADD my_set "Green"
SADD my_set "Blue"

# 获取集合的所有元素
SMEMBERS my_set

在这个例子中,我们通过SADD命令将”Red”、”Green”和”Blue”添加到my_set,并通过SMEMBERS获取所有元素。

3.2 集合操作

Redis支持多种集合操作,比如交集、并集等。让我们看一个例子:

# 添加另一个集合
SADD my_set_2 "Green"
SADD my_set_2 "Yellow"

# 计算集合的交集
SINTER my_set my_set_2

在这里,我们通过SINTER命令计算my_setmy_set_2的交集。

4. 有序集合 (Sorted Set)

4.1 添加和获取元素

有序集合是一种集合,其中的每个元素都关联了一个分数,这使得我们可以按照分数排序元素。下面是一个示例:

# 向有序集合添加元素
ZADD my_zset 1 "Apple"
ZADD my_zset 2 "Banana"
ZADD my_zset 3 "Orange"

# 获取有序集合的所有元素
ZRANGE my_zset 0 -1 WITHSCORES

在这个例子中,我们使用ZADD命令向my_zset添加了三个元素,并通过ZRANGE命令获取所有元素及其分数。

4.2 有序集合操作

我们可以执行许多操作,例如查找特定排名范围的元素,或根据分数范围来查询元素。例如:

# 根据分数范围获取元素
ZRANGEBYSCORE my_zset 1 2

# 获取特定元素的排名
ZRANK my_zset "Banana"

5. 哈希 (Hash)

5.1 添加和获取元素

哈希是一种键值对集合,非常适合存储对象。以下是一个示例:

# 向哈希添加数据
HSET my_hash name "Alice"
HSET my_hash age "30"
HSET my_hash city "New York"

# 获取哈希中的所有键值对
HGETALL my_hash

在这个例子中,我们使用HSET命令向my_hash中添加了三个键值对,并用HGETALL获取了所有键值对。

5.2 哈希操作

哈希结构提供了丰富的操作,比如只获取所有的键或值,或者删除特定的键。例如:

# 获取所有键
HKEYS my_hash

# 获取所有值
HVALS my_hash

# 删除一个键
HDEL my_hash name

6. HyperLogLog

6.1 添加元素

HyperLogLog 是用于估计基数(集合中不重复元素的数量)的数据结构。下面是一个示例:

# 添加元素到 HyperLogLog
PFADD my_hyperloglog "Apple"
PFADD my_hyperloglog "Banana"
PFADD my_hyperloglog "Orange"

在这个例子中,我们使用 PFADD 命令向 my_hyperloglog 添加了三个元素。

6.2 估算基数

HyperLogLog 提供了估算基数的功能:

# 估算基数
PFCOUNT my_hyperloglog

这个命令返回 HyperLogLog 中不同元素的估算数量。

HyperLogLog 在处理大型数据集时非常有用,因为它能够以固定的内存消耗来估算基数,而不需要存储所有元素。

7. Bitmaps

7.1 设置和获取位

Bitmaps 是一种位图数据结构,可以用于存储和处理位信息。下面是一个简单的示例:

# 设置位
SETBIT my_bitmap 0 1
SETBIT my_bitmap 2 1

# 获取位的值
GETBIT my_bitmap 0
GETBIT my_bitmap 1

在这个例子中,我们使用 SETBIT 命令设置了位,然后使用 GETBIT 命令获取了相应位的值。

7.2 位操作

Bitmaps 还支持位操作,例如按位与、按位或、按位异或等:

# 按位与
BITOP AND result_bitmap my_bitmap1 my_bitmap2

# 按位或
BITOP OR result_bitmap my_bitmap1 my_bitmap2

# 按位异或
BITOP XOR result_bitmap my_bitmap1 my_bitmap2

这些位操作可以用于处理多个位图之间的关系。

Bitmaps 在一些场景下非常有用,例如统计用户的在线状态、记录用户的行为等。使用 Bitmaps 可以在占用较少内存的情况下高效地处理大量位信息。

8. Streams

8.1 添加消息

Streams 是一种日志数据结构,允许你按时间顺序添加、读取和消费消息。以下是一个简单的示例:

# 添加消息到 Stream
XADD mystream * name John age 30

# 添加另一条消息
XADD mystream * name Jane age 25

在这个例子中,我们使用 XADD 命令向名为 mystream 的 Stream 添加了两条消息。

8.2 读取消息

可以使用 XRANGE 命令按范围读取消息:

# 读取所有消息
XRANGE mystream - +

这将返回 mystream 中的所有消息。

Streams 在处理事件日志、消息队列等场景中非常有用,因为它允许按时间顺序组织和检索消息。

9. Geospatial 数据结构

9.1 添加地理位置

Geospatial 数据结构可以用来存储地理位置的信息,比如经度和纬度。以下是一个简单的示例:

# 添加地理位置信息
GEOADD locations 13.361389 38.115556 "Palermo"
GEOADD locations 15.087269 37.502669 "Catania"

在这个例子中,我们使用 GEOADD 命令添加了两个地理位置信息,分别是 “Palermo” 和 “Catania”。

9.2 查询附近的位置

可以使用 GEODIST 命令计算两个位置之间的距离,或者使用 GEORADIUS 命令查找附近的位置:

# 计算两个位置之间的距离
GEODIST locations "Palermo" "Catania" km

# 查找附近的位置
GEORADIUS locations 15 37 100 km

这些命令使得在地理信息系统中进行位置相关的操作变得非常方便。

结语

通过这些详细的例子,我们深入了解了Redis的数据结构。当我们在实际项目中选择合适的数据结构时,这些例子将为我们提供有力的指导。希望这篇博文对你加深对Redis数据结构的理解有所帮助。如果你有其他关于Redis的问题,欢迎留言讨论!

全流程机器视觉工程开发(一)环境准备,paddledetection和labelme-CSDN博客

mikel阅读(306)

来源: 全流程机器视觉工程开发(一)环境准备,paddledetection和labelme-CSDN博客

前言
我现在在准备做一个全流程的机器视觉的工程,之前做了很多理论相关的工作。大概理解了机器视觉的原理,然后大概了解了一下,我发现现在的库其实已经很发展了,完全不需要用到非常多的理论,只需要知道开发过程就可以了,甚至paddlex已经直接有了傻瓜式模型训练的软件,所以我现在准备来做一个全流程机器视觉工程开发,不涉及过多理论。

准备
现在准备一下机器视觉工程的前情提要。

我准备使用paddledetection来做机器视觉。什么是paddleDetection?你可以理解为paddlepaddle对于目前主流的机器学习模型做了一些整合,只需要使用paddleDetection库就可以做一个很方便的训练、预测等工作。

准备好paddledetection之后,也就是我们的模型工具之后,还需要对现有图片做一些简单的划分工作,这里就需要用到labelme工具来进行.

 

环境安装
我这个教程和别的教程不太一样。因为年代久远,paddledetection库的原始安装方式已经不太适用了,所以我这里重新写一个paddledetection安装方式。

主要流程大概如下:

安装anaconda
安装paddle库
安装CUDA库
去github上下载paddledetection仓库
给自己安装pycocotools和lap库
直接安装paddledetection的依赖包requirements.txt
安装paddledetection
流程
安装anaconda
这步略,不知道的可以浏览:Anaconda安装教程(超详细版)

安装paddle库
这步略,参考paddle官网,不行就自己在csdn上搜,或者看我往期
这里给出官网链接:开始使用

安装CUDA库
这步略,参考本人往期文章:简易机器学习笔记(十)Windows下 PaddlePaddle配置CUDA加速环境

去github上下载paddledetection仓库
github链接:PaddleDetection
你要做的就是直接把这个仓库clone到本地,拉下来的项目大概是这样的

里面是这一大堆东西,暂时先不管是干嘛的,只需要先放在这里就可以了。

pycocotools和lap库
到一般的教程了,这里会告诉你直接去安装requirements.txt,但是很多人现在可能会直接报错numpy的问题,这个可能是因为库实在是年久失修了,主要出问题的库实际上就那么两个,一个是pycocotools,一个是lap
首先可以尝试一下能不能直接安装这两个库,也就是直接尝试以下两条命令

pip install pycocotools
pip install lap

一般情况下这个pycocotools是没问题的,出问题的是这个lap库,我这里主要演示lap库怎么手动安装,pycocotools也是同理

首先我们找到两个库的github地址:

pycocotools
lap

把这两个库clone到本地,大概是这样

在cmd中使用python尝试安装这个setup.py文件,指令大概是:

#path/to/setup.py指代setup.py的路径
python path/to/setup.py install

注意这条指令需要使用setuptools,怎么安装这个库不过多赘述了

一般这样手动安装就可以正常安装成功了,pycocotools和lap库都是这样安装的。

直接安装paddledetection的依赖包requirements.txt
lap库和pycocotools安装完毕后,基本上问题就不大了。现在只需要使用以下指令来对paddledetection包中的requirements进行安装就行了

#path/to/requirements.txt 指代paddledetection库下的requirements.txt的路径
pip install -r path/to/requirements.txt

我们可以打开requirements.txt来看一看,里面也只有一些库的名字而已

 

安装paddledetection
到这里基本上就快安装完了,只需要最后一步,就是安装paddledetection,流程和安装lap库差不多
#path/to/setup.py 指代paddledetection库下的setup.py的路径
python path/to/setup.py install

数据标注工具labelme
刚刚我们安装完了paddledetection库,也就是准备好模型,接下来要准备的就是数据标注工具labelme

这个比较简单,直接参考博客:添加链接描述
————————————————
版权声明:本文为CSDN博主「Leventure_轩先生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Andius/article/details/135675515

浅谈6种流行的API架构风格 - 追逐时光者 - 博客园

mikel阅读(307)

来源: 浅谈6种流行的API架构风格 – 追逐时光者 – 博客园

浅谈6种流行的API架构风格

前言

API在现代软件开发中扮演着重要的角色,它们是不同应用程序之间的桥梁。编写业务API是日常开发工作中最常见的一部分,选择合适的API框架对项目的成功起到了至关重要的作用。本篇文章将浅谈一下当前6种流行的API架构风格的优点、缺点以及适用场景。

6种流行的API架构风格图

SOAP

SOAP全拼:Simple Object Access Protocol

  • 优点:SOAP 是一种基于 XML 的通信协议,具有良好的跨平台和跨语言支持。它提供了丰富的安全性和事务管理功能,并支持复杂的消息交换模式。
  • 缺点:SOAP 在处理大量数据时可能效率较低,因为它使用了冗长的 XML 格式,并且需要较多的带宽和处理能力。
  • 适用场景:SOAP 适用于需要高安全性和复杂数据交换的企业级应用程序和 Web 服务场景,尤其是需要实现事务处理和消息传递机制的场景。

RESTful

RESTful全拼:Representational State Transfer

  • 优点:RESTful 一种基于现有 Web标准和 HTTP协议的设计和构建网络应用程序的架构风格,旨在提供一种简洁、可扩展、可靠和可互操作的方式来进行网络通信。它具有良好的可伸缩性、可缓存性和可见性,并支持多种数据格式(如:JSON、XML等)。
  • 缺点:缺乏标准化、安全性问题、粒度问题、难以处理复杂逻辑、复杂性问题和版本管理问题。
  • 适用场景:RESTful 适用于构建 Web 应用程序和移动应用程序的 API,特别是那些需要简单和易于使用的场景。

GraphQL

  • 优点:GraphQL 是一种由 Facebook 开发的查询语言和运行时执行环境。它允许客户端精确地指定所需的数据,并减少了网络传输的数据量。GraphQL 还提供了强大的类型系统和自动文档生成。
  • 缺点:GraphQL 在处理大型查询和复杂数据模型时可能存在性能问题,因为它需要在运行时解析查询,并执行多个数据源之间的数据获取操作。
  • 适用场景:GraphQL 适用于需要灵活数据获取和精确控制的应用程序,特别是面向移动设备的应用程序和需要聚合多个数据源的场景。

gRPC

gRPC全拼:Google Remote Procedure Call

  • 优点:gRPC 是一种高性能、开源的远程过程调用框架,基于 Protocol Buffers(protobuf)序列化协议。它提供了强大的类型系统、双向流和流式数据传输的支持。
  • 缺点:gRPC 对网络稳定性有较高的要求,不太适合部署在不可靠的网络环境中。
  • 适用场景:gRPC 适用于构建分布式系统和微服务架构,特别是那些需要高性能和强类型约束的场景。

WebSocket

  • 优点:WebSocket 提供了全双工通信的能力,允许服务器主动向客户端推送数据。它具有低延迟、高吞吐量和实时性的特点。
  • 缺点:WebSocket 对于服务器和客户端都需要保持长时间的连接,这可能增加服务器的负载,并且需要较高的网络稳定性。
  • 适用场景:WebSocket 适用于实时通信和实时数据更新的应用程序,特别是聊天应用、协作工具和实时游戏等场景。

Webhook

  • 优点:Webhook 是一种通过 HTTP 请求将事件通知发送给预定义 URL 的机制。它能够实时推送数据并触发自定义的后续操作。
  • 缺点:Webhook 需要事先配置目标 URL,并且对于每个事件都需要建立一个独立的 Webhook。此外,Webhook 不支持请求-响应模式。
  • 适用场景:Webhook 适用于需要实时事件通知和与其他应用程序集成的场景,特别是信息发布、应用程序集成和自动化工作流等场景。

总结

这些 API 架构风格都各有优点和适用场景,您可以根据具体需求选择适合的架构风格来构建和设计 API。