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

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


  • 首页

  • 归档

  • 搜索

(三)Nacos开发教程之服务数据持久化

发表于 2021-11-16

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

回顾一下

我们在之前的文章中已经讲完了如何安装部署Nacos服务,已经可以成功进入到下图的界面中了。

image.png

如图中,我们可以在服务列表中查看服务,也可以进行配置的一个管理、发布等,权限相关的功能也是具备的。

目前版本的Nacos,已经完全足以在企业级系统中稳定使用了。

发现问题

Nacos可以作为配置中心来使用,相对于SpringCloud Config组件,其图形化界面显然更加友好。

SpringCloud Config组件是将配置文件持久化到git服务器,或者是github、gitlab中,那么Nacos是如何持久化这些配置的呢。

默认情况下,Nacos是不会开启持久化的,需要手动配置。

Nacos的持久化是通过MySQL数据库来进行持久化的,所以需要先执行初始化SQL进行建库建表。

如何配置Nacos的持久化

如何配置Nacos的持久化呢,首先我们要创建相应的数据库和数据表,需要一个初始化SQL。

再者需要找到持久化的开关在哪个文件中。

初始化SQL

Nacos提供了初始化SQL,就在安装文件中,路径为:nacos/conf/nacos-mysql.sql,我这里是Linux环境,所以就不贴Windows上面的图了。

image.png

如果你打开了这个文件,就能明白了,这里面放置的就是Nacos持久化需要数据库和数据表信息。

直接去执行即可。

如何开启Nacos的持久化

还是在nacos/conf/目录下,需要操作application.properties文件,在此文件中添加上相关的数据源信息。

这里的数据源信息就是刚才我们创建的数据库信息。

1
2
3
4
5
ini复制代码spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/mynacos
db.user=root
db.password=123456

总结

以上就是Nacos进行持久化配置的全部内容了,希望大家可以在日常使用中有一定的基础,加油吧。

本文转载自: 掘金

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

译:如何在 Go 中嵌入 Python

发表于 2021-11-16

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

如果你看一下 新的 Datadog Agent,你可能会注意到大部分代码库是用 Go 编写的,尽管我们用来收集指标的检查仍然是用 Python 编写的。这大概是因为 Datadog Agent 是一个 嵌入了 CPython 解释器的普通 Go 二进制文件,可以在任何时候按需执行 Python 代码。这个过程通过抽象层来透明化,使得你可以编写惯用的 Go 代码而底层运行的是 Python。

视频

在 Go 应用程序中嵌入 Python 的原因有很多:

  • 它在过渡期间很有用;可以逐步将现有 Python 项目的部分迁移到新语言,而不会在此过程中丢失任何功能。
  • 你可以复用现有的 Python 软件或库,而无需用新语言重新实现。
  • 你可以通过加载去执行常规 Python 脚本来动态扩展你软件,甚至在运行时也可以。

理由还可以列很多,但对于 Datadog Agent 来说,最后一点至关重要:我们希望做到无需重新编译 Agent,或者说编译任何内容就能够执行自定义检查或更改现有检查。

嵌入 CPython 非常简单,而且文档齐全。解释器本身是用 C 编写的,并且提供了一个 C API 以编程方式来执行底层操作,例如创建对象、导入模块和调用函数。

在本文中,我们将展示一些代码示例,我们将会在与 Python 交互的同时继续保持 Go 代码的惯用语,但在我们继续之前,我们需要解决一个间隙:嵌入 API 是 C 语言,但我们的主要应用程序是 Go,这怎么可能工作?

介绍 cgo

有 很多好的理由 说服你为什么不要在堆栈中引入 cgo,但嵌入 CPython 是你必须这样做的原因。cgo 不是语言,也不是编译器。它是 外部函数接口Foreign Function Interface(FFI),一种让我们可以在 Go 中使用来调用不同语言(特别是 C)编写的函数和服务的机制。

当我们提起 “cgo” 时,我们实际上指的是 Go 工具链在底层使用的一组工具、库、函数和类型,因此我们可以通过执行 go build 来获取我们的 Go 二进制文件。下面是使用 cgo 的示例程序:

1
2
3
4
5
6
7
8
9
go复制代码package main

// #include <float.h>
import "C"
import "fmt"

func main() {
fmt.Println("Max float value of float is", C.FLT_MAX)
}

在这种包含头文件情况下,import "C" 指令上方的注释块称为“序言preamble”,可以包含实际的 C 代码。导入后,我们可以通过“C”伪包来“跳转”到外部代码,访问常量 FLT_MAX。你可以通过调用 go build 来构建,它就像普通的 Go 一样。

如果你想查看 cgo 在这背后到底做了什么,可以运行 go build -x。你将看到 “cgo” 工具将被调用以生成一些 C 和 Go 模块,然后将调用 C 和 Go 编译器来构建目标模块,最后链接器将所有内容放在一起。

你可以在 Go 博客 上阅读更多有关 cgo 的信息,该文章包含更多的例子以及一些有用的链接来做进一步了解细节。

现在我们已经了解了 cgo 可以为我们做什么,让我们看看如何使用这种机制运行一些 Python 代码。

嵌入 CPython:一个入门指南

从技术上讲,嵌入 CPython 的 Go 程序并没有你想象的那么复杂。事实上,我们只需在运行 Python 代码之前初始化解释器,并在完成后关闭它。请注意,我们在所有示例中使用 Python 2.x,但我们只需做很少的调整就可以应用于 Python 3.x。让我们看一个例子:

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

// #cgo pkg-config: python-2.7
// #include <Python.h>
import "C"
import "fmt"

func main() {
C.Py_Initialize()
fmt.Println(C.GoString(C.Py_GetVersion()))
C.Py_Finalize()
}

上面的例子做的正是下面 Python 代码要做的事:

1
2
go复制代码import sys
print(sys.version)

你可以看到我们在序言加入了一个 #cgo 指令;这些指令被会被传递到工具链,让你改变构建工作流程。在这种情况下,我们告诉 cgo 调用 pkg-config 来收集构建和链接名为 python-2.7 的库所需的标志,并将这些标志传递给 C 编译器。如果你的系统中安装了 CPython 开发库和 pkg-config,你只需要运行 go build 来编译上面的示例。

回到代码,我们使用 Py_Initialize() 和 Py_Finalize() 来初始化和关闭解释器,并使用 Py_GetVersion C 函数来获取嵌入式解释器版本信息的字符串。

如果你想知道,所有我们需要放在一起调用 C 语言 Python API的 cgo 代码都是模板代码。这就是为什么 Datadog Agent 依赖 go-python 来完成所有的嵌入操作;该库为 C API 提供了一个 Go 友好的轻量级包,并隐藏了 cgo 细节。这是另一个基本的嵌入式示例,这次使用 go-python:

1
2
3
4
5
6
7
8
9
10
11
erlang复制代码package main

import (
python "github.com/sbinet/go-python"
)

func main() {
python.Initialize()
python.PyRun_SimpleString("print 'hello, world!'")
python.Finalize()
}

这看起来更接近普通 Go 代码,不再暴露 cgo,我们可以在访问 Python API 时来回使用 Go 字符串。嵌入式看起来功能强大且对开发人员友好,是时候充分利用解释器了:让我们尝试从磁盘加载 Python 模块。

在 Python 方面我们不需要任何复杂的东西,无处不在的“hello world” 就可以达到目的:

1
2
3
4
5
6
python复制代码# foo.py
def hello():
"""
Print hello world for fun and profit.
"""
print "hello, world!"

Go 代码稍微复杂一些,但仍然可读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// main.go
package main

import "github.com/sbinet/go-python"

func main() {
python.Initialize()
defer python.Finalize()

fooModule := python.PyImport_ImportModule("foo")
if fooModule == nil {
panic("Error importing module")
}

helloFunc := fooModule.GetAttrString("hello")
if helloFunc == nil {
panic("Error importing function")
}

// The Python function takes no params but when using the C api
// we're required to send (empty) *args and **kwargs anyways.
helloFunc.Call(python.PyTuple_New(0), python.PyDict_New())
}

构建时,我们需要将 PYTHONPATH 环境变量设置为当前工作目录,以便导入语句能够找到 foo.py 模块。在 shell 中,该命令如下所示:

1
2
css复制代码$ go build main.go && PYTHONPATH=. ./main
hello, world!

可怕的全局解释器锁

为了嵌入 Python 必须引入 cgo ,这是一种权衡:构建速度会变慢,垃圾收集器不会帮助我们管理外部系统使用的内存,交叉编译也很难。对于一个特定的项目来说,这些问题是否是可以争论的,但我认为有一些不容商量的问题:Go 并发模型。如果我们不能从 goroutine 中运行 Python,那么使用 Go 就没有意义了。

在处理并发、Python 和 cgo 之前,我们还需要知道一些事情:它就是全局解释器锁Global Interpreter Lock,即 GIL。GIL 是语言解释器(CPython 就是其中之一)中广泛采用的一种机制,可防止多个线程同时运行。这意味着 CPython 执行的任何 Python 程序都无法在同一进程中并行运行。并发仍然是可能的,锁是速度、安全性和实现简易性之间的一个很好的权衡,那么,当涉及到嵌入时,为什么这会造成问题呢?

当一个常规的、非嵌入式的 Python 程序启动时,不涉及 GIL 以避免锁定操作中的无用开销;在某些 Python 代码首次请求生成线程时 GIL 就启动了。对于每个线程,解释器创建一个数据结构来存储当前的相关状态信息并锁定 GIL。当线程完成时,状态被恢复,GIL 被解锁,准备被其他线程使用。

当我们从 Go 程序运行 Python 时,上述情况都不会自动发生。如果没有 GIL,我们的 Go 程序可以创建多个 Python 线程,这可能会导致竞争条件,从而导致致命的运行时错误,并且很可能出现分段错误导致整个 Go 应用程序崩溃。

解决方案是在我们从 Go 运行多线程代码时显式调用 GIL;代码并不复杂,因为 C API 提供了我们需要的所有工具。为了更好地暴露这个问题,我们需要写一些受 CPU 限制的 Python 代码。让我们将这些函数添加到前面示例中的 foo.py 模块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码# foo.py
import sys

def print_odds(limit=10):
"""
Print odds numbers < limit
"""
for i in range(limit):
if i%2:
sys.stderr.write("{}\n".format(i))

def print_even(limit=10):
"""
Print even numbers < limit
"""
for i in range(limit):
if i%2 == 0:
sys.stderr.write("{}\n".format(i))

我们将尝试从 Go 并发打印奇数和偶数,使用两个不同的 goroutine(因此涉及线程):

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复制代码package main

import (
"sync"

"github.com/sbinet/go-python"
)

func main() {
// The following will also create the GIL explicitly
// by calling PyEval_InitThreads(), without waiting
// for the interpreter to do that
python.Initialize()

var wg sync.WaitGroup
wg.Add(2)

fooModule := python.PyImport_ImportModule("foo")
odds := fooModule.GetAttrString("print_odds")
even := fooModule.GetAttrString("print_even")

// Initialize() has locked the the GIL but at this point we don't need it
// anymore. We save the current state and release the lock
// so that goroutines can acquire it
state := python.PyEval_SaveThread()

go func() {
_gstate := python.PyGILState_Ensure()
odds.Call(python.PyTuple_New(0), python.PyDict_New())
python.PyGILState_Release(_gstate)

wg.Done()
}()

go func() {
_gstate := python.PyGILState_Ensure()
even.Call(python.PyTuple_New(0), python.PyDict_New())
python.PyGILState_Release(_gstate)

wg.Done()
}()

wg.Wait()

// At this point we know we won't need Python anymore in this
// program, we can restore the state and lock the GIL to perform
// the final operations before exiting.
python.PyEval_RestoreThread(state)
python.Finalize()
}

在阅读示例时,你可能会注意到一个模式,该模式将成为我们运行嵌入式 Python 代码的习惯写法:

  1. 保存状态并锁定 GIL。
  2. 执行 Python。
  3. 恢复状态并解锁 GIL。

代码应该很简单,但我们想指出一个微妙的细节:请注意,尽管借用了 GIL 执行,有时我们通过调用 PyEval_SaveThread() 和 PyEval_RestoreThread() 来操作 GIL,有时(查看 goroutines 里面)我们对 PyGILState_Ensure() 和 PyGILState_Release() 来做同样的事情。

我们说过当从 Python 操作多线程时,解释器负责创建存储当前状态所需的数据结构,但是当同样的事情发生在 C API 时,我们来负责处理。

当我们用 go-python 初始化解释器时,我们是在 Python 上下文中操作的。因此,当调用 PyEval_InitThreads() 时,它会初始化数据结构并锁定 GIL。我们可以使用 PyEval_SaveThread() 和 PyEval_RestoreThread() 对已经存在的状态进行操作。

在 goroutines 中,我们从 Go 上下文操作,我们需要显式创建状态并在完成后将其删除,这就是 PyGILState_Ensure() 和 PyGILState_Release() 为我们所做的。

释放 Gopher

在这一点上,我们知道如何处理在嵌入式解释器中执行 Python 的多线程 Go 代码,但在 GIL 之后,另一个挑战即将来临:Go 调度程序。

当一个 goroutine 启动时,它被安排在可用的 GOMAXPROCS 线程之一上执行,参见此处 可了解有关该主题的更多详细信息。如果一个 goroutine 碰巧执行了系统调用或调用 C 代码,当前线程会将线程队列中等待运行的其他 goroutine 移交给另一个线程,以便它们有更好的机会运行; 当前 goroutine 被暂停,等待系统调用或 C 函数返回。当这种情况发生时,线程会尝试恢复暂停的 goroutine,但如果这不可能,它会要求 Go 运行时找到另一个线程来完成 goroutine 并进入睡眠状态。 goroutine 最后被安排给另一个线程,它就完成了。

考虑到这一点,让我们看看当一个 goroutine 被移动到一个新线程时,运行一些 Python 代码的 goroutine 会发生什么:

  1. 我们的 goroutine 启动,执行 C 调用并暂停。GIL 被锁定。
  2. 当 C 调用返回时,当前线程尝试恢复 goroutine,但失败了。
  3. 当前线程告诉 Go 运行时寻找另一个线程来恢复我们的 goroutine。
  4. Go 调度器找到一个可用线程并恢复 goroutine。
  5. goroutine 快完成了,并在返回之前尝试解锁 GIL。
  6. 当前状态中存储的线程 ID 来自原线程,与当前线程的 ID 不同。
  7. 崩溃!

所幸,我们可以通过从 goroutine 中调用运行时包中的 LockOSThread 函数来强制 Go runtime 始终保持我们的 goroutine 在同一线程上运行:

1
2
3
4
5
6
7
8
scss复制代码go func() {
runtime.LockOSThread()

_gstate := python.PyGILState_Ensure()
odds.Call(python.PyTuple_New(0), python.PyDict_New())
python.PyGILState_Release(_gstate)
wg.Done()
}()

这会干扰调度器并可能引入一些开销,但这是我们愿意付出的代价。

结论

为了嵌入 Python,Datadog Agent 必须接受一些权衡:

  • cgo 引入的开销。
  • 手动处理 GIL 的任务。
  • 在执行期间将 goroutine 绑定到同一线程的限制。

为了能方便在 Go 中运行 Python 检查,我们很乐意接受其中的每一项。但通过意识到这些权衡,我们能够最大限度地减少它们的影响,除了为支持 Python 而引入的其他限制,我们没有对策来控制潜在问题:

  • 构建是自动化和可配置的,因此开发人员仍然需要拥有与 go build 非常相似的东西。
  • Agent 的轻量级版本,可以使用 Go 构建标签,完全剥离 Python 支持。
  • 这样的版本仅依赖于在 Agent 本身硬编码的核心检查(主要是系统和网络检查),但没有 cgo 并且可以交叉编译。

我们将在未来重新评估我们的选择,并决定是否仍然值得保留 cgo;我们甚至可以重新考虑整个 Python 是否仍然值得,等待 Go 插件包 成熟到足以支持我们的用例。但就目前而言,嵌入式 Python 运行良好,从旧代理过渡到新代理再简单不过了。

你是一个喜欢混合不同编程语言的多语言者吗?你喜欢了解语言的内部工作原理以提高你的代码性能吗?


本文转载自: 掘金

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

详细讲解服务幂等性设计

发表于 2021-11-16

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

引子

在日常工作中的一些技术设计方案评审会上,经常会有提到注意服务接口的幂等性问题,最近就有个同学就跑到跟前问我,幂等性到底是个啥?

在目前分布式/微服务化的今天,提供的服务能力丰富多样,基于 HTTP 协议的 Web API 是时下最为流行的一种分布式服务提供方式,对于服务的幂等性保障尤为重要。

我想了想,觉得有必要好好给大家普及一下才行。

今天计划就关于服务幂等性的一系列问题,在此将材料总结整理,分享给大家~

1、何为幂等性?

幂等(idempotence),来源于数学中的一个概念,例如:幂等函数/幂等方法(指用相同的参数重复执行,并能获得相同结果的函数,这些函数不影响系统状态,也不用担心重复执行会对系统造成改变)。

简单理解即:多次调用对系统的产生的影响是一样的,即对资源的作用是一样的。

幂等性

幂等性强调的是外界通过接口对系统内部的影响, 只要一次或多次调用对某一个资源应该具有同样的副作用就行。

注意:这里指对资源造成的副作用必须是一样的,但是返回值允许不同!

2、幂等性主要场景有哪些?

根据上面对幂等性的定义我们得知:产生重复数据或数据不一致,这个绝大部分是由于发生了重复请求。

这里的重复请求是指同一个请求在一些情况下被多次发起。

导致这个情况会有哪些场景呢?

  • 微服务架构下,不同微服务间会有大量的基于 http,rpc 或者 mq 消息的网络通信,会有第三个情况【未知】,也就是超时。如果超时了,微服务框架会进行重试。
  • 用户交互的时候多次点击,无意地触发多笔交易。
  • MQ 消息中间件,消息重复消费
  • 第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调
  • 其他中间件/应用服务根据自身的特性,也有可能进行重试。

3、幂等性的作用是什么?

幂等性主要保证多次调用对资源的影响是一致的。

在阐述作用之前,我们利用资源处理应用来说明一下:

HTTP 与数据库的 CRUD 操作对应:

PUT :CREATE

GET :READ

POST :UPDATE

DELETE :DELETE

(其实不光是数据库,任何数据如文件图表都是这样)

1)查询

1
sql复制代码SELECT * FROM users WHERE xxx;

不会对数据产生任何变化,天然具备幂等性。

2)新增

1
sql复制代码INSERT INTO users (user_id, name) VALUES (1, 'zhangsan');

case1:带有唯一索引(如:user\_id),重复插入会导致后续执行失败,具有幂等性;

case2:不带有唯一索引,多次插入会导致数据重复,不具有幂等性。

3)修改

case1:直接赋值,不管执行多少次 score 都一样,具备幂等性。

1
ini复制代码UPDATE users SET score = 30 WHERE user_id = 1;

case2:计算赋值,每次操作 score 数据都不一样,不具备幂等性。

1
ini复制代码UPDATE users SET score = score + 30 WHERE user_id = 1;

4)删除

case1:绝对值删除,重复多次结果一样,具备幂等性。

1
ini复制代码DELETE FROM users WHERE id = 1;

case2:相对值删除,重复多次结果不一致,不具备幂等性。

1
css复制代码DELETE top(3) FROM users;

总结:通常只需要对写请求(新增 &更新)作幂等性保证。

4、如何解决幂等性问题?

我们在网上搜索幂等性问题的解决方案,会有各种各样的解法,但是如何判断哪种解决方案对于自己的业务场景是最优解,这种情况下,就需要我们抓问题本质。

经过以上分析,我们得到了解决幂等性问题就是要控制对资源的写操作。

我们从问题各个环节流程来分析解决:

幂等性问题分析

4.1 控制重复请求

控制动作触发源头,即前端做幂等性控制实现

相对不太可靠,没有从根本上解决问题,仅算作辅助解决方案。

主要解决方案:

  • 控制操作次数,例如:提交按钮仅可操作一次(提交动作后按钮置灰)
  • 及时重定向,例如:下单/支付成功后跳转到成功提示页面,这样消除了浏览器前进或后退造成的重复提交问题。

4.2 过滤重复动作

控制过滤重复动作,是指在动作流转过程中控制有效请求数量。

1)分布式锁

利用 Redis 记录当前处理的业务标识,当检测到没有此任务在处理中,就进入处理,否则判为重复请求,可做过滤处理。

订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key,如果不存在,则向 Redis 增加 Key 为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的 Key。通过 Redis 做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。

分布式锁相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。

2)token 令牌

应用流程如下:

1)服务端提供了发送 token 的接口。执行业务前先去获取 token,同时服务端会把 token 保存到 redis 中;

2)然后业务端发起业务请求时,把 token 一起携带过去,一般放在请求头部;

3)服务器判断 token 是否存在 redis 中,存在即第一次请求,可继续执行业务,执行业务完成后将 token 从 redis 中删除;

4)如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码不被重复执行。

3)缓冲队列

把所有请求都快速地接下来,对接入缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的请求数据。

优点:同步转异步,实现高吞吐。

缺点:不能及时返回处理结果,需要后续监听处理结果的异步返回数据。

4.3 解决重复写

实现幂等性常见的方式有:悲观锁(for update)、乐观锁、唯一约束。

1)悲观锁(Pessimistic Lock)

简单理解就是:假设每一次拿数据,都有认为会被修改,所以给数据库的行或表上锁。

当数据库执行 select for update 时会获取被 select 中的数据行的行锁,因此其他并发执行的 select for update 如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。

select for update 获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。(注意 for update 要用在索引上,不然会锁表)

1
2
3
4
5
6
ini复制代码START TRANSACTION; 
# 开启事务
SELETE * FROM users WHERE id=1 FOR UPDATE;
UPDATE users SET name= 'xiaoming' WHERE id = 1;
COMMIT;
# 提交事务

2)乐观锁(Optimistic Lock)

简单理解就是:就是很乐观,每次去拿数据的时候都认为别人不会修改。更新时如果 version 变化了,更新不会成功。

不过,乐观锁存在失效的情况,就是常说的 ABA 问题,不过如果 version 版本一直是自增的就不会出现 ABA 的情况。

1
2
3
ini复制代码UPDATE users 
SET name='xiaoxiao', version=(version+1)
WHERE id=1 AND version=version;

缺点:就是在操作业务前,需要先查询出当前的 version 版本

另外,还存在一种:状态机控制

例如:支付状态流转流程:待支付->支付中->已支付

具有一定要的前置要求的,严格来讲,也属于乐观锁的一种。

3)唯一约束

常见的就是利用数据库唯一索引或者全局业务唯一标识(如:source+序列号等)。

这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。

全局 ID 生成方案:

  • UUID:结合机器的网卡、当地时间、一个随记数来生成 UUID;
  • 数据库自增 ID:使用数据库的 id 自增策略,如 MySQL 的 auto_increment。
  • Redis 实现:通过提供像 INCR 和 INCRBY 这样的自增原子命令,保证生成的 ID 肯定是唯一有序的。
  • 雪花算法-Snowflake:由 Twitter 开源的分布式 ID 生成算法,以划分命名空间的方式将 64-bit 位分割成多个部分,每个部分代表不同的含义。

**小结:**按照应用上的最优收益,推荐排序为:乐观锁 > 唯一约束 > 悲观锁。

5、总结

通常情况下,非幂等问题,主要是由于重复且不确定的写操作造成的。

1、解决重复的主要思考点

从请求全流程,控制重复请求触发以及重复数据处理

  • 客户端 控制发起重复请求
  • 服务端 过滤重复无效请求
  • 底层数据处理 避免重复写操作

2、控制不确定性主要思考点

从服务设计思路上做改变,尽量避免不确定性:

  • 统计变量改为数据记录方式
  • 范围操作改为确定操作

后记

听了我以上大段的讲述后,他好像收获感满满的似的说:大概理解了…

但是出于自身责任感,我还得叮嘱他几句:

1)幂等性处理 虽然复杂了业务处理,也可能会降低接口的执行效率,但是为了保证系统数据的准确性,是非常有必要的;

2)遇到问题,善于发现并挖掘本质问题,这样解决起来才能高效且精准;

3)选择自身业务场景适合的解决方案,而不要去硬套一些现成的技术实现,无论是组合还是创新,要记住适合的才是最好的。

愿大家能够掌握问题分析以及解决的能力,都不要一上来就急于解决问题,可以多做些深入分析,了解本质问题之后再考虑解决办法进行解决。

希望今天的讲解对大家有所帮助,Thanks for reading!

  • END -

作者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长。

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。

Thanks for reading!

本文转载自: 掘金

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

力扣第九十六题-不同的二叉搜索树 前言 一、思路 二、实现

发表于 2021-11-16

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

前言

力扣第九十六题 不同的二叉搜索树 如下所示:

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

1
2
ini复制代码输入: n = 3
输出: 5

一、思路

这一题比上一题不同的是:只关心二叉搜素树的种树,而不关系树的节点了。

所以一开始我的思路也是用递归来做的,去除了保留路径的部分,仅得到左孩子的个数和右孩子的个数即可。7代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public int dfs(int start, int end) {
int ret = 0;
if (start > end) {
return 1;
}
// 枚举可行根节点
for (int i = start; i <= end; i++) {
// 找到所有的左子树
int left = dfs(start, i - 1);

// 找到所有的右子树
int right = dfs(i + 1, end);
ret += left * right;
}
return ret;
}

但是很可惜,在 n=18 时会提示超出时间限制!

那既然不能用 递归 来自顶向下,那就只能使用 动态规划 来实现 自底向上 了。

首先我们先看一下 递归 中调用的地方,我们发现在递归中一直在求 1 ~ i-1 和 i+1 ~ n 左孩子和右孩子个数。

我们不妨设 dp[i] 为长度为 i 的不同二叉搜索树的个数。

可以有 dp[n] = dp[0]dp[n-1] + dp[1]dp[n-2] + dp[2]dp[n-3] + ... + dp[n-1]dp[0]

公式解释:

因为 dp[n] 就是选择 1 ~ n 中的某一个元素 i 为根,再将 1 ~ i-1 中的元素作为左孩子, i+1 ~ n 中的元素作为右孩子。

dp[0]dp[n-1] 就表示选择了第一个元素作为根有多少种结果。

此外,因为二叉搜索树的特殊性。对于 1, 2, 3 和 1, 2, 4 组成的二叉搜索树的种数是相同的。所以我们在公式中可以忽略元素的值,而只关注元素的个数,因为相同元素个数组成的二叉搜索树的种树是相同的。

二、实现

实现代码

实现代码与思路中保持一致

1
2
3
4
5
6
7
8
9
10
java复制代码    public int numTrees(int n) {
int[] dp = new int[n +1];
dp[0] = dp[1] = 1; // 0个和1个元素都只有一种结果
for (int i=2; i<= n; i++) {
for (int j=1; j<=i; j++) {
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}

测试代码

1
2
3
4
java复制代码    public static void main(String[] args) {
int ret = new Number96().numTrees(3);
System.out.println(ret);
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

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

JDK 17 switch模式匹配

发表于 2021-11-16

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

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

简介

Pattern Matching for switch(Preview)

在switch中使用模式匹配,预览版本。

预览版本

  • 有可能在之后的版本删除
  • 有可能计划进一步增强

在 Java 16 中, JEP 394 扩展了 instanceof 运算符以采用类型模式并执行模式匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码static String formatter(Object o){
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}

在Java 17中, JEP406

switch 是模式匹配的完美匹配!如果我们扩展 switch 语句和表达式以适用于任何类型,并允许使用模式而不是常量进行 case 标签,那么我们可以更清晰可靠地重写上述代码

1
2
3
4
5
6
7
8
9
java复制代码static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
  • 好处
+ 语义更清晰:“参数 o 最多匹配以下条件之一”
+ 在这种情况下,我们更有可能在 O(1) 时间内执行调度(==效率高==)。我们用断点调试这两段代码


    - **if...else**


    逐行校验![](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/f9a18f38d6c4c00327145a2c2d7bdbce99d13ae4df0fefe58e61a00acfd0bde6)


    - **switch**


    直接到default![](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/8e06dce3c57de5c7fe6cbd3beb5516f56d4026bff99c5a33a80b88eb02e95bf7)
  • switch模式匹配的目标
+ 通过允许模式出现在 case 中,扩展 switch 表达式和语句的表现力和适用性。
+ 允许switch的case使用null
+ 引入两种新的模式:保护模式,允许使用任意布尔表达式来改进模式匹配逻辑,以及带括号的模式,以解决一些解析歧义。
+ 确保所有现有的 switch 表达式和语句在没有更改的情况下继续编译并以相同的语义执行。
+ 确保和旧版本switch表达式和语句的兼容性

语法

增强 switch 语句和表达式两种方式

  • 扩展case语句中除常量外,还可以使用模式匹配
  • 除了案例中的模式,还有两种新的模式:保护模式和括号模式。

对于模式匹配有四个特点

  • 增强的类型检查:选择器表达式的类型包括:基本类型或任何引用类型(包括null)。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }

static void typeTester(Object o) {
switch (o) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case Color c -> System.out.println("Color with " + Color.values().length + " values");
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array of ints of length" + ia.length);
default -> System.out.println("Something else");
}
}
  • switch 表达式和语句的完整性
+ 存在子父类关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 错误
static void error(Object o) {
switch(o) {
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
case String s -> // 编译错误 - CharSequence是String的父类,提示该模式已经由前一个模式匹配
System.out.println("A string: " + s);
default -> {
break;
}
}
}

// 正确
static void error(Object o) {
switch(o) {
case String s -> System.out.println("A string: " + s);
case CharSequence cs -> System.out.println("A sequence of length " + cs.length());
default -> {
break;
}
}
}
+ 缺少default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 错误
static int coverage(Object o) {
return switch (o) { // 编译错误,类型匹配不完整,缺少default
case String s -> s.length();
case Integer i -> i;
};
}
// 正确
static int coverage(Object o) {
return switch (o) {
case String s -> s.length();
case Integer i -> i;
default -> 0;
};
}
+ 类型匹配检查


sealed是密封类的语法,我们会在文章后续进行探讨。


表示可以实现接口S的类有 A,B,C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
final class C implements S {}

// 错误
static void switchStatementComplete(S s) {
switch (s) { // 编译错误,缺少类型B
case A a :
System.out.println("A");
break;
case C c :
System.out.println("C");
break;
};
}
// 正确
static int testSealedCoverage(S s) {
return switch (s) {
case A a -> 1;
case B b -> 2;
case C c -> 3;
};
}
  • 模式变量声明的范围
+ case语句后箭头右侧可以出现的内容:
    - 表达式
    - 代码块
    - throw语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码static void test(Object o) {
switch (o) {
case Character c -> { // 代码块
if (c.charValue() == 7) {
System.out.println("Ding!");
}
System.out.println("Character");
}
case Integer i -> // throw语句
throw new IllegalStateException("Invalid Integer argument of value " + i.intValue());
default -> {
break;
}
}
}
  • 保护模式和括号模式。
    • 保护模式:我们可以添加一种称为保护模式的新模式,写作 【表达式1】 && 【表达式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
java复制代码class Shape {}
class Rectangle extends Shape {}
class Triangle extends Shape { int calculateArea() { ... } }

static void testTriangle(Shape s) {
switch (s) {
case null:
break;
case Triangle t:
if (t.calculateArea() > 100) {
System.out.println("Large triangle");
break;
}
default:
System.out.println("A shape, possibly a small triangle");
}
}
// 优化
static void testTriangle(Shape s) {
switch (s) {
case Triangle t && (t.calculateArea() > 100) -> // 括号模式
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Non-triangle");
}
}
  • 处理null值
    • 在case中可以使用null
    • 以及null引出的新的模式:case null,其他 ->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码static void test(Object o) {
switch (o) {
case null -> System.out.println("null!");
case String s -> System.out.println("String");
default -> System.out.println("Something else");
}
}

static void test(Object o) {
switch (o) {
case null,default -> //
System.out.println("Something else (包括null)");
}
}

参考资源

  • openjdk.java.net/jeps/406
  • www.oracle.com/java/techno…

下期预告

下期我们将一起学习探讨Sealed Classes密封类

本文转载自: 掘金

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

爬虫 - Scrapy 框架介绍与安装

发表于 2021-11-16

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

image

1. Scrapy 框架介绍

  • Scrapy是Python开发的一个快速,高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy = Scrach+Python
  • Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试、信息处理和历史档案等大量应用范围内抽取结构化数据的应用程序框架,广泛用于工业
  • Scrapy 使用Twisted 这个异步网络库来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。Scrapy是由Twisted写的一个受欢迎的Python事件驱动网络框架,它使用的是非堵塞的异步处理

1.1 为什么要使用Scrapy?

  • 它更容易构建和大规模的抓取项目
  • 它内置的机制被称为选择器,用于从网站(网页)上提取数据
  • 它异步处理请求,速度十分快
  • 它可以使用自动调节机制自动调整爬行速度
  • 确保开发人员可访问性

1.2 Scrapy的特点

  • Scrapy是一个开源和免费使用的网络爬虫框架
  • Scrapy生成格式导出如:JSON,CSV和XML
  • Scrapy内置支持从源代码,使用XPath或CSS表达式的选择器来提取数据
  • Scrapy基于爬虫,允许以自动方式从网页中提取数据

1.3 Scrapy的优点

  • Scrapy很容易扩展,快速和功能强大;
  • 这是一个跨平台应用程序框架(在Windows,Linux,Mac OS和BSD)。
  • Scrapy请求调度和异步处理;
  • Scrapy附带了一个名为Scrapyd的内置服务,它允许使用JSON Web服务上传项目和控制蜘蛛。
  • 也能够刮削任何网站,即使该网站不具有原始数据访问API;

1.4 整体架构大致如下:

image

最简单的单个网页爬取流程是spiders > scheduler > downloader > spiders > item pipeline

1.5 Scrapy运行流程大概如下:

  1. 引擎从调度器中取出一个链接(URL)用于接下来的抓取
  2. 引擎把URL封装成一个请求(Request)传给下载器
  3. 下载器把资源下载下来,并封装成应答包(Response)
  4. 爬虫解析Response
  5. 解析出实体(Item),则交给实体管道进行进一步的处理
  6. 解析出的是链接(URL),则把URL交给调度器等待抓取

1.6 Scrapy主要包括了以下组件:

  • 引擎(Scrapy)
    • 用来处理整个系统的数据流处理, 触发事务(框架核心)
  • 调度器(Scheduler)
    • 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    • 用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    • 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    • 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
  • 下载器中间件(Downloader Middlewares)
    • 位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应
  • 爬虫中间件(Spider Middlewares)
    • 介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出
  • 调度中间件(Scheduler Middewares)
    • 介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应

2 安装

2.1 默认安装

  1. 执行pip install scrapy
  2. 报错,让我们安装VC++的环境,并给出网站让我们去下载
  3. 下载的文件visualcppbuildtools_full ,下载下来后,直接双击安装,下一步下一步,就可以了(twisted的安装环境)
  4. 执行pip install scrapy

2.2 whl安装

  1. 因为scrapy需要twisted的环境,所以我们直接去下载whl文件
  2. 根据自己的Python版本在www.lfd.uci.edu/~gohlke/pyt…
  3. 下载好后,Twisted-17.9.0-cp35-cp35m-win_amd64.whl,将这个文件存放到$python_home/script里面
  4. 执行pip install Twisted-17.9.0-cp35-cp35m-win_amd64.whl 这是在安装twisted
  5. 执行pip install scrapy

2.3 直接copy

  1. 从别人已经安装好的twisted的python文件夹中,直接拷出来
  2. 放到自己的Python目录里,$python_home/lib/site-packages

注:windows平台需要依赖pywin32

1
vbnet复制代码ModuleNotFoundError: No module named 'win32api'

pip install pywin32

本文转载自: 掘金

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

Shiro与SpringBoot整和

发表于 2021-11-16

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

什么是Shiro

Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。

Shiro基本结构

在这里插入图片描述

其三个核心组件:Subject, SecurityManager 和 Realms

Subject:即“当前操作用户”

SecurityManager:它是Shiro框架的核心,负责管理所有用户的安全操作。典型的Facade
模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

快速在SpringBoot上应用Shiro

  1. 导入shiro与spring的整和依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码
<!--导入连接数据库mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--导入shiro与spring的整和依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

2、配置用户的realm

当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

继承了AuthorizingRealm接口的实现类可以被称为realm。继承后要实现两个方法:doGetAuthorizationInfo(PrincipalCollection principalCollection)和doGetAuthenticationInfo(AuthenticationToken authenticationToken)
一个是用来完成授权操作,一个是用来完成认证操作。

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
java复制代码public class UserRealm extends AuthorizingRealm {

@Autowired
private UserService userService;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权操作");

SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
User user=(User)SecurityUtils.getSubject().getPrincipal();
info.addStringPermission(user.getPerms());//添加相应的用户权限

return info;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

System.out.println("执行了认证操作");

UsernamePasswordToken token=(UsernamePasswordToken) authenticationToken;
//认证用户名,密码
User user=userService.getUserByName(token.getUsername());

if(!token.getUsername().equals(user.getUsername())){
return null;//抛出异常UnknowAccountException
}

//密码认证
return new SimpleAuthenticationInfo(user,user.getPwd(),"");

}
}

3、 配置自己的配置类

  • 创建用户个性化的realme
  • 创建DefaultWebSercurityManger默认网络安全管理器
  • 创建ShirofilterFactoryBean
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
java复制代码@Configuration
public class ShiroConfig {

//ShirofilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
Map<String,String> map=new LinkedHashMap<>();
/**
* anon:无需认证就可以访问
* authc:认证了才能访问
* user:必须有记住我功能才能访问
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
*/

map.put("/level1/*","perms[vip1]");//设置对应的访问权限集合:vip1可以访问
map.put("/level2/*","perms[vip2]");
map.put("/level3/*","perms[vip3]");
bean.setFilterChainDefinitionMap(map);

bean.setUnauthorizedUrl("/unauthoriz");
//设置登陆页面
bean.setLoginUrl("/toLogin");


return bean;

}

//DefaultWebSercurityManger
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm")UserRealm userRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;


}



//realme
@Bean(name="userRealm")
public UserRealm userRealm(){
return new UserRealm();
}

@Bean
public ShiroDialect shiroDialect(){
//thymeleaf与shiro整和
return new ShiroDialect();
}

}

创建的realm必须添加@Bean注解,交给spring托管,后面需要注入给DefaultWebSercurityManger

创建ShiroFilterFactoryBean 时你可以设置过滤请求以及相应的访问权限。通过LinkedHashMap<>()放入ShiroFilterFactoryBean 中。

完成以上操作之后,就可以达到访问指定界面先要登陆获得相应的权限。

Shiro与Thymleaf

shiro与Thymleaf也有着良好的整和。用户结合两者可以达到:不同权限用户在同一界面看到不同的内容。比如
vip1看到:
在这里插入图片描述

vip2用户可以看到:
在这里插入图片描述
vip3用户看到:
在这里插入图片描述
导入相关依赖即可使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<!--导入Thymeleaf依赖-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--thymeleaf与shiro整和-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>

只需在前端页面加上标签即可:shiro:hasPermission=""
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码 <div class="column" shiro:hasPermission="vip1">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>

设置登录用户才能看得到的内容:在对应的标签上加上shiro:Authenticated
设置没登入用户看的到的内容:在对应的标签上加上shiro:NotAuthenticated

本文转载自: 掘金

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

MQ基础

发表于 2021-11-16

MQ即消息队列。消息队列是一种先进先出的数据结构。

image.png

为什么要使用MQ?

MQ的主要使用场景有以下3种:

  • 应用解耦

系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。

解耦1.png

使用消息队列解耦合,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中,用户的下单操作正常完成。当物流系统回复后,补充处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。

解耦2.png

  • 流量削峰

mq-5.png

应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。

mq-6.png

一般情况,为了保证系统的稳定性,如果系统负载超过阈值,就会阻止用户请求,这会影响用户体验,而如果使用消息队列将请求缓存起来,等待系统处理完毕后通知用户下单完毕,这样总不能下单体验要好。

  • 数据分发

mq-1.png

通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。

mq-2.png

MQ的优缺点:

优点:解耦、削峰、数据分发。

缺点:

  • 系统可用性降低
    系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
    如何保证MQ的高可用?
  • 系统复杂度提高

MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?

  • 一致性问题

A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败。
如何保证消息数据处理的一致性?

MQ产品的比较

常见的MQ产品包括Kafka、ActiveMQ、RabbitMQ、RocketMQ。

MQ比较.png

本文转载自: 掘金

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

若依系统分页工具学习-PageHelper篇四

发表于 2021-11-16

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

在上一篇文章“若依系统分页工具学习-PageHelper篇三”中,我们通过下载并查看PageHelper-Spring-Boot-Starter源码,了解到PageHelper是通过实现MyBatis拦截器接口org.apache.ibatis.plugin.Interceptor的PageInterceptor类从而实现对SQL重写的。

那么问题来了XDM, PageInterceptor又是具体如何重写SQL的呢?

我们知道拦截器一般是通过反射机制实现的。(本文暂不展开说)。

拦截器中最重要的方法一般命名为intercept,形式一般是下面代码的样子:

1
2
3
4
5
6
java复制代码@Override
public Object intercept(Invocation invocation) {
// 逻辑代码前
invocation.invoke(); // 执行所拦截的实际代码
// 逻辑代码后
}

这里的invocation.invoke()实际就是相当于我们去执行接口代码或者是执行SQL,如此我们便可以全局的对程序接口或者SQL做某些修改。

我们来看一下PageInterceptor的拦截器代码:

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
java复制代码@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
//处理查询总数,返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}

代码有点多,我们来逐行分析:

前4句,获取拦截中的参数:

1
2
3
4
5
java复制代码Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];

类型依次为MappedStatement,Object,RowBounds,ResultHandler,为了更清楚,我们简单列出其结构:大致可以根据其参数了解这个参数大概是什么类型

  1. MappedStatement (resource, configuration, id, fetchSize, timeout, statementType, resultSetType, sqlSource, cache, parameterMap, resultMaps, …, sqlCommandType, keyGenerator,keyProperties, keyColumns, databaseId, statementLog, LanguageDriver, resultSets);
  2. Object, 不做叙述;
  3. RowBounds: offset, limit, (类中还有常量NO_ROW_OFFSET=0, NO_ROW_LIMIT = 2147483647);
  4. ResultHandler<T>:只有一个抽象方法:
1
java复制代码public abstract  void handleResult(org.apache.ibatis.session.ResultContext<? extends T> arg0);

又是从哪里拦截到的这几个参数呢?或者说,我们拦截的到底是个什么东东?

PageHelper的wiki文档给出了答案:org.apache.ibatis.executor.Executor.

wiki中说:

在 MyBatis 的拦截器的文档部分,我们知道 Executor 中的 query 方法可以被拦截

其query方法形式为:

1
2
3
4
5
java复制代码<E> List<E> query(
MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException;

[手动狗头]看,是不是对应上了4个参数!

话说谁发明的springboot这种接口与实现模式,源码看的类似了,一顿翻找还是漏掉不少源码。

本文转载自: 掘金

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

分布式事务(三)-SAGA(简单又实用) 简介 事务组成 实

发表于 2021-11-16

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

简介

SAGA事务模式的历史十分悠久,比分布式事务的概念提出还要更早。SAGA的意思是“长篇故事、长篇记叙、一长串事件”,它起源于1987年普林斯顿大学的赫克托 · 加西亚 · 莫利纳(Hector Garcia Molina)和肯尼斯 · 麦克米伦(Kenneth Salem)在ACM发表的一篇论文《SAGAS》。

文中提出了一种如何提升“长时间事务”(Long Lived Transaction)运作效率的方法,大致思路是把一个大事务分解为可以交错运行的一系列子事务的集合。原本提出SAGA的目的,是为了避免大事务长时间锁定数据库的资源,后来才逐渐发展成将一个分布式环境中的大事务,分解为一系列本地事务的设计模式。

Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

事务组成

SAGA由两部分操作组成。

一部分是把大事务拆分成若干个小事务,将整个分布式事务T分解为n个子事务,我们命名为T1,T2,…,Ti,…,Tn。每个子事务都应该、或者能被看作是原子行为。如果分布式事务T能够正常提交,那么它对数据的影响(最终一致性)就应该与连续按顺序成功提交子事务Ti等价。

另一部分是为每一个子事务设计对应的补偿动作,我们命名为C1,C2,…,Ci,…,Cn。Ti与Ci必须满足以下条件:

  • Ti与Ci都具备幂等性;
  • Ti与Ci满足交换律,即不管是先执行Ti还是先执行Ci,效果都是一样的;
  • Ci必须能成功提交,即不考虑Ci本身提交失败被回滚的情况,如果出现就必须持续重试直至成功,或者要人工介入。

如果T1到Tn均成功提交,那么事务就可以顺利完成。否则,我们就要采取以下两种恢复策略之一:

  • 正向恢复(Forward Recovery): 如果Ti事务提交失败,则一直对Ti进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,比如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。
  • 反向恢复(Backward Recovery): 如果Ti事务提交失败,则一直执行Ci对Ti进行补偿,直至成功为止(最大努力交付)。这里要求Ci必须(在持续重试后)执行成功。反向恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。

实现方式

常用实现方式有两种:一个是集中式协调器的实现方式,一个基于事件的实现方式。

集中式实现

集中式的实现一般是通过一个Saga对象来追踪所有的Saga子任务的调用情况,根据调用情况来决定是否需要调用对应的补偿方面,协调器和调用方是在一个进程中的。集中式的Saga实现方式比较直观并且容易控制,问题是业务耦合程度会比较高。

例子:已订单流程为例,完成流程包含以下服务

  • Order Service:订单服务
  • Payment Service:支付服务
  • Stock Service:库存服务
  • Delivery Service:物流服务
    集中式的实现如下:

参考:blog.couchbase.com/saga-patter…

image.png

  1. 订单服务创建一笔新订单,将订单状态设置为”待处理”,然后让Order Saga Orchestrator(OSO)开启创建订单事务。(Order Service和Order Saga Orchestrator 大多数情况可以是一个模式)
  2. OSO发送一个Execute Payment给支付服务,支付服务完成扣款并回复Payment Executed消息。
  3. OSO发送一个Prepare Order给库存服务,库存服务完成库存扣减和备货,并回复Order Prepared消息。
  4. OSO发送一个Deliver Order给物流服务,物流服务完成配送,并回复Order Delivered消息。
  5. OSO向订单服务发送”订单结束命令”给订单服务,订单服务将订单状态设置为”完成”。

回滚示例图:

image.png

我们现实中使用的就是这种案例,只是稍微有些变化,简单讲下,也是创建订单业务,创建订单有主要三步操作:

1.扣减库存
2.扣减优惠券
3.落库
以上三步会多线程rpc调用相关服务,如果全部成功,订单正常创建成功,如果有一步失败或者超时,就发失败回滚消息(失败回滚消息要保证发送成功),各服务保证能正常回滚。

基于事件的实现方式

在基于事件的方式中,第一个服务执行完本地事务之后,会产生一个事件。其它服务会监听这个事件,触发该服务本地事务的执行,并产生新的事件。

例子参考:blog.couchbase.com/saga-patter…

例子:同样订单流程,每个服务执行都会产生一个事件,下一个服务监听,下一个服务执行完后,再产生新的事件

正向流程:
image.png
逆向流程:库存失败,需要回滚支付

image.png

Saga模式的优劣势

优势

  • 一阶段提交本地数据库事务,无锁,高性能;
  • 参与者可以采用事务驱动异步执行,高吞吐;
  • 补偿服务即正向服务的“反向”,易于理解,易于实现;

劣势

Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。如果两个Saga事务同时操作同一资源就会遇到我们操作多线程临界资源的的情况,就会产生更新丢失,脏数据读取等问题。

阿里开源组件Seata Saga 服务设计经验

Saga 服务设计 - 允许空补偿
image.png
Saga 服务设计 - 防悬挂控制

image.png

Saga 服务设计 - 幂等控制

image.png

Saga 设计 - 自定义事务恢复策略
image.png

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文转载自: 掘金

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

1…321322323…956

开发者博客

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