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

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


  • 首页

  • 归档

  • 搜索

一次线上事故引发的对 http 请求的结束的思考

发表于 2021-11-04

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

引言

近期在一次日常上线后,发现系统 SLA 下降很多,通过监测平台发现,SLA 下降的主要原因是接口超时。经过排查,发现最后原因是因为 mesh 某个组件升级后,对于 Transfer-Encoding: chunked 的 http 请求,会丢掉结束符,然后导致 http server 认为传输一直未结束,一直尝试读取后面的数据,导致最终请求超时。

思考与总结

引言中的事故引起了我对 http 请求如何结束极大的好奇,通过查阅相关资料之后,总结如下:

http 的连接

http 链接可以分为长连接与短连接,在 http/1.0 时代,默认使用的是短连接,而从 http/1.1 开始,默认使用长连接。那么什么又是短连接,什么又是长连接呢?

短连接,顾名思义,客户端和服务端每进行一次 http 操作,双方就会建立一次连接,操作结束,则连接断开。举个例子,对于一个 web 页面,包含了 javascript 文件,图像文件,css 文件等资源,浏览器每请求一个资源,就会建立一次 http 连接。

对于使用长连接的 http/1.0 协议,header 头会添加 Connection: keep-alive,从 http/1.1 开始,不再需要 keep-alive 这个取值,除非显示加上 Connection: close,否则默认就是长连接。在使用长连接的情况下,打开一个 web 页面加载各项资源,一般都会使用一开始建立好的那条连接。当然,长连接不等于永久连接,而是保持一段时间。

我们知道,tcp 是传输层协议,http 是应用层协议,因此 http 连接的本质还是 tcp 连接。tcp 每次连接开始会有三次握手的过程,连接结束会有四次挥手的过程。长连接可以省去大量的 tcp 连接建立、关闭所消耗的各种资源。对于频繁请求资源的用户而言,长连接是比较划算的。

当然,万事无绝对,这里还有一个问题,就是长连接需要持续多久呢?就会需要相应的连接存活检测,这也会消耗一部分资源。在长连接的应用场景下,客户端一般不会主动关闭连接,这也就会导致服务端建立的连接越来越多,直到服务端崩溃。因此服务端需要采取一些策略,比如关闭一些长时间没有读写连接,来避免服务端压力过大。

http 的结束

上面我们了解 http 的连接相关的内容,结合引言部分,那么 http 请求是如何结束的呢。

对于短连接,可以根据连接是否关闭来界定请求或者响应实体的边界,但是对于长连接,这种方法就不那么有效了,因为尽管可能所有的数据都已经传输完毕了,但是服务端并不知道所有数据是否都收到了,然后就只能白白等待,直到最终超时。

为了解决长连接遇到的这个问题,最直观的办法就是告诉服务端,这次发送的内容有多少,于是就有了另一个 header 头 Content-Length,服务端可以根据这个 header 头的值,判断这个请求是否已经传输完毕。俗话说,理想总是美好的,那么我们要怎么才能知道 Content-Length 的取值呢?对于很多网络请求,这个值不好精确计算,取大了,会造成资源浪费,且请求 pending,取小了,会造成请求截断,丢失信息。因此,我们迫切需要一个新的方法来界定请求或者响应的边界。

千呼万唤始出来,Transfer-Encoding header 头便应运而生,根据目前的 RFC 规范,这个 header 只有 chunked 一个取值。在头部加入 Transfer-Encoding: chunked 后,就表示这个报文采用了分块编码。报文采用一系列分块来传输,每个分块包含十六进制的长度值和数据,长度值占一行,长度不包括它结尾的 CRLF(\r\n)。最后一个分块长度值必须是 0,对应分块没有具体内容,表示实体结束。因此可以认为,当读到 \r\n0\r\n 时,表示结束。http 传输数据的结束判断流程如下所示:

否否是是是是否否否是http 传输数据是否是长连接?接收数据recv()=-1?Content-Length 是否存在?http 数据传输完毕接收 Content-Length 长度的数据接收完毕?Transfer-Encoding: chunked接收数据检测到 \r
0\r
?
Golang 中的 net/http 实现正如上面流程所示,具体代码如下:

1
2
3
4
5
6
Go复制代码...
t.ContentLength = rr.outgoingLength()
if t.ContentLength < 0 && len(t.TransferEncoding) == 0 && t.shouldSendChunkedRequestBody() {
t.TransferEncoding = []string{"chunked"}
}
...
1
2
3
4
5
6
7
8
9
10
11
Go复制代码// outgoingLength reports the Content-Length of this outgoing (Client) request.
// It maps 0 into -1 (unknown) when the Body is non-nil.
func (r *Request) outgoingLength() int64 {
if r.Body == nil || r.Body == NoBody {
return 0
}
if r.ContentLength != 0 {
return r.ContentLength
}
return -1
}

本文转载自: 掘金

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

全网最通俗易懂的讲解 git rebase和git mer

发表于 2021-11-04

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

前言

我们在上篇文章你想知道的所有Git分支命令都在这中提到了git merge 命令,可以将其中一个分支的代码合并到另一个分支中,其实还有另一种方法,通过 git rebase命令也可以实现,但是两者的实现方式还是有很大差异的,网上有很多文章都对其差异有讲解,但大多数说的都很模糊不够清晰,看的也是一知半解,如果一篇文章对读者而言做不到通熟易懂那就是对读者的不负责,本篇文章将使用最通熟易懂的语言让你彻底搞懂两者的差异。

先说结论:

  • merge 是一个合并操作,会将两个分支的修改合并在一起,默认操作的情况下会提交合并中修改的内容
  • merge 的提交历史记录了实际发生过什么,关注点在真实的提交历史上面
  • rebase 并没有进行合并操作,只是提取了当前分支的修改,将其复制在了目标分支的最新提交后面
  • rebase 操作会丢弃当前分支已提交的 commit,故不要在已经 push 到远程,和其他人正在协作开发的分支上执行 rebase 操作
  • merge 与 rebase 都是很好的分支合并命令,没有好坏之分,使用哪一个应由团队的实际开发需求及场景决定

image-20211104214737342

先来说说 git merge的实现方式

如上图所示,我们有两个分支,master 分支和 test 分支,test 分支是基于 master 分支在B处的提交节点创建的,在创建后 master 分支又经过迭代提交了两次,从C到D节点,test 分支也基于B往前继续更新了两次,到了F节点。两者从B开始就走向了分叉。

这时如果我们想将 test 分支合并到 master 分支,通过 merge 是如何工作的呢?

1
2
3
4
5
arduino复制代码//将分支切换到master分支
git checkout master
​
//把test分支合并到master分支
git merge test

image-20211104215627579

从图中可以看到,这里生成了一个新的提交G,是怎么生成的呢? merge 命令 它会把两个分支的最新快照(F、E 和 D、C)以及二者最近的共同祖先(B)进行三方合并,合并的结果是生成一个新的快照G(并提交)。

git rebase实现方式

1
2
3
4
5
arduino复制代码//将分支切换到master分支
git checkout master
​
//把test分支合并到master分支
git rebase test

从图就可以看出和 merge 命令不同

这里有个名词定义我们先简单说明一下

  • test:基分支、目标分支
  • master: 待变基分支,当前分支

官方解释: 当执行 rebase 操作时,git 会从两个分支的共同祖先开始提取待变基分支上的修改,然后将待变基分支指向基分支的最新提交,最后将刚才提取的修改应用到基分支的最新提交的后面。

这句话听起来可能绕绕的,不用怕,听小蛋给你好好翻译翻译:

我们结合具体例子来说明,当我们在master (待变基分支)上执行git rebase test(基分支)时,git就会从两者的共同祖先B开始,提取 master 分支上的修改,也就是 C,D 两个 commit ,提取到之后 git 会先保存起来,然后将master 分支指向 test 分支最新提交的节点,也就是F节点,然后把提取到的 C,D 接到F后面,在这个过程当中,会删除原来的C,D commit 记录,生成新的C‘,D’,虽然C’,D’和原来的C,Dcoommit的内容是一样的,但是 commit id 是不同的。

rebase 操作如果用一句话进行解释就是改变基底。master 分支原来的基底是A,现在变成了以 test 分支最新的提交F做为新的基底了。

总结

merge 和 rebase 这两者哪种操作更好,这是取决于不同的场景的。

当我们拉取公共分支最新代码的时候建议使用rebase,也就是git pull -r或git pull --rebase,但有个缺点就是 rebase 以后我就不知道我的当前分支最早是从哪个分支拉出来的了,因为基底变了嘛。(如果使用 merge ,多出无意义的一条提交记录)。

往公共分支上合代码的时候,使用 merge 。(如果使用 rebase ,那么其他开发人员想看主分支的历史,就不是原来的历史了,历史已经被你篡改了),例如主分支是 master 分支,小蛋我有一个 egg 分支,我在 egg 分支上写了很多垃圾代码,然后这时候我把 egg 分支通过 rebase 命令合并到 master 分支,那对于 master 分支来说,它之前到提交历史就没了,别的同事突然想看 master 分支以前的提交历史,其实就看不到了,发现只能看到我 egg 的提交历史,估计同事会把我的 egg 捏碎的,这种傻事可不要干哟。

本文转载自: 掘金

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

基于 Micrometer 封装的监控框架实践

发表于 2021-11-04

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

此背景也源于近期一个项目功能需求而来,本文对此进行整理,对 MicroMeter 实现监控埋点进行说明和扩展。

Micrometer

Micrometer 是一个用于基于 JVM 应用程序的度量工具库。它为最流行的监控系统的检测客户端提供了一个简单的门面(facade,类似于 slf4j)。Micrometer 旨在为项目提供在尽可能小的开销基础上,同时提供最大限度地提高的指标工作的可移植性(主要得力于其门面模式的设计,类似的还有像 opentracing 这类,不过 opentracing 和 slf4j 或者 Micrometer 在设计模式上还存在着本质的差别)。本文不重点关注在 Micrometer 基础知识或者概念介绍上,更多信息可以通过其官方文档获取,详见:Micrometer。

背景

这里先抛出项目背景:我们希望提供一个 sdk 或者 starter,使用尽可能简单的 api 或者注解,就可以完成核心方法的埋点。Micrometer 自己其实已经提供了类似 @Timer 这种类似的注解,但是其本身缺失 tag 唯独统计,而基于 api 方式的使用,虽然支持了 tag 能力,但是对于业务代码的侵入性又非常高,因此基于原生注解和 api 尚且不满足当前项目需求,所以需要对其进行扩展。

针对项目自身特性,我们对扩展 Micrometer 大体诉求如下:

  • 注解中支持 tag 能力
  • 支持在方法执行过程中增加 tag 维度

这两个诉求看起来很简单,但是这里会有几个必须要面对的问题:

  • 注解一般打在类或者方法上,支持 tag 很容易,但是 tag 中的值相对来说就比较固定,如果期望支持动态注入 tag 的 value,则需要支持将参数绑定到 value 上去
  • 基于注解的方式基本等于绑定 aop,aop 怎么处理 this 应用问题
  • 方法执行过程中增加 tag 一般是绑定 threadlocal 实现,怎么解决跨线程传递问题

下面就展开对 Micrometer 扩展的具体实践介绍。

Micrometer 监控框架扩展实践

针对上述问题,我们先一个个来解决。首先是注解中支持 tag 能力,这个并没有什么技术含量,做法就是抛弃 Micrometer 原生的注解,通过自定义注解来实现。如下:

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复制代码/**
* @ClassName MeterMetrics
* @Description
*
* @MeterMetrics(name = "xx", type = MeterMetricsEnum.COUNT, tags = {@MeterTag(key = "key", value = "value"),...})
* public void test() {...}
*
* @Author glmapper
* @Date 2021/11/4 16:46
* @Version 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MeterMetrics {

/**
* describe tags name
* @return
*/
String key() default "";

/**
* describe tags value
* @return
*/
String value() default "";
}

既然注解提供了,则我们需要处理它,下面是对这个注解的切面实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
@Pointcut("@annotation(com.glmapper.bridge.boot.anno.MeterMetrics)")
public void meterMetricsCut() {
}

@Around("meterMetricsCut()")
public Object meterMetricsAround(ProceedingJoinPoint joinPoint) throws Throwable {
//... 解析注解上的 tag,然后上报
}

@AfterThrowing(value = "meterMetricsCut()", throwing = "ex")
public void meterMetricsThrowing(JoinPoint joinPoint, Throwable ex) throws Throwable {
//... 解析注解上的 tag,然后上报
}

至此对于初始模型是具备了,但是实际和使用还差的很远,下面是扩展细节。

支持 spel 表达式,实现 tag 值绑定参数

举个例子,期望将参数中的某个值作为 tag 的 value,如下:

1
2
3
4
5
java复制代码@GetMapping("/resp")
@MeterMetrics(key = "app", value = "#testModel.getName()")
public String getResp(TestModel testModel){
return "SUCCESS";
}

这段代码中,#testModel.getName() 是一个 spel 表达式,其目的在于能够将自定义注解的 key-value 能够和方法参数绑定起来。这里对应的切面方法的核心处理大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码private final SpelExpressionParser parser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer pmDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 支持从参数中通过 spel 表达式提取对应的值
*/
public String getSpelContent(String spelKey, JoinPoint pjp) {
Expression expression = parser.parseExpression(spelKey);
EvaluationContext context = new StandardEvaluationContext();
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Object[] args = pjp.getArgs();
String[] paramNames = pmDiscoverer.getParameterNames(methodSignature.getMethod());
for(int i = 0 ; i < args.length ; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 注意这里可能会有 npe,具体实现是需要关注一下
return expression.getValue(context).toString();
}

这里需要有几个注意点,首先是上面的 TestModel 必须提供构造函数(spel 自身机制决定),其次是解析流程不应该对主业务流程产生任何影响,也就是当产生异常时,需要内部吃掉,不扩散。

那么到这里,我们解决了注解支持 tag 和 绑定参数两个能力。下面是实现方法内部增加 tag 的能力。

允许在方法中增加 tag 信息

埋点如果期望收集更丰富的维度信息,仅通过参数或者手动指定是不行的,这里举个简单的例子,你的参数是个 token 串,但是在检验这个 token 时,你期望知道这个 token 解析之后的一些信息,比如用户信息、权限信息等,所以这些仅能够在方法内部才能拿到的数据,当需要将其加入到 tag 中去时,也需要提供适当的渠道。

这里提供一个基于 ThreadLocal 实现透传的实现思路:

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
java复制代码public class TokenMetricHolder {
// TAG_CACHE
private static ThreadLocal<MetricTag> TAG_CACHE = new ThreadLocal<>();
public static void metricToken(TokenModel tokenModel, String from) {
try {
MetricTag.MetricTagBuilder builder = MetricTag.builder();
// ... 为 builder 添加属性
MetricTag metricTag = builder.build();
TAG_CACHE.set(metricTag);
} catch (Exception ex) {
// do not block main process
LOGGER.error("error to fill metric tags.", ex);
}
}
// 注意 remove,避免内存泄漏风险
public static MetricTag getAndRemove() {
MetricTag metricTag = TAG_CACHE.get();
TENANT_CACHE.remove();
return metricTag;
}

@Data
@Builder
public static class MetricTag {
// 这里是需要收集的一些 tag 指标项,比如 userName
private String userName;
}

通过 TokenMetricHolder 内部维持的 TAG_CACHE,在一个请求上下文内,方法内部填充的 tag 信息,我们在 aop 中可以轻松获取,这样就可以丰富指标统计维度。

解决 this 引用问题

aop 中,当方法内部调用另一个方法时(a 方法中调用 b), 对 b 的 aop 会失效,原因在于当前 b 的对象引用时 this,而并非是代理对象。

1
2
3
4
java复制代码public void a(){
b();
}
public voidb(){//...}

网上对此的解释一抓一大把,这里我们仅给实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
/**
* com.glmapper.bridge.boot.holder#ProxyHolder
* get current proxy object, if get null, return default value with specified
*
* @param t
* @return
*/
public static <T> T getCurrentProxy(T t) {
if (t == null) {
throw new IllegalArgumentException("t should be current object(this).");
}

try {
return (T) AopContext.currentProxy();
} catch (IllegalStateException ex) {
return t;
}
}

将上述直接调用 b 方法修改为 ProxyHolder.getCurrentProxy(this).b() 即可。

丰富 MeterMetrics,支持多组 tag

上面 MeterMetrics 仅有 key 和 value,略显单薄,下面我们继续扩展 MeterMetrics 的能力,可以允许其指定 name,指定指标类型,支持 tag 数据,如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public @interface MeterMetrics {
MeterMetricsEnum type() default MeterMetricsEnum.TIMER;
MeterTag[] tags() default {};
String name() default "";
}

public @interface MeterTag {
String key() default "";
String value() default "";
boolean spel() default false;
}

使用展示及总结

关于代码后续会托管到 github 上去,目前还缺少对于 starter 的封装。下面展示具体的使用效果

1
2
3
4
5
java复制代码@MeterMetrics(name = "test", 
tags = {@MeterTag(key = "appName", value = "#requestModel.getApp_name()"),
@MeterTag(key = "fixedKey", value = "fixedValue")},
type = MeterMetricsEnum.COUNTER)
public void test(@Validated RequestModel requestModel) {...}

本文对于基于 Micrometer 埋点扩展提供了一个可行的实现思路,并在实际的业务场景中进行了使用。在原生 api 基础上,通过扩展注解和 api,解决了一系列实际业务场景中可能需要面临的一些问题。

如果本篇文章对你有一点点帮助,请点个赞,感谢!

本文转载自: 掘金

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

Go高阶17,反射机制,反射三定律! 反射 静态类型 反射三

发表于 2021-11-04

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

对于反射,之前的文章已经有所介绍,传送门:《运行时反射,深度解析!》,此处我们讲下反射三定律。

反射

Go语言提供了一种机制,在运行时可以更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。

官方对此有个非常简明的介绍,两句话耐人寻味:

  1. 反射提供一种让程序检查自身结构的能力
  2. 反射是困惑的源泉

静态类型

我们知道,Go是静态类型语言,例如”int”、”float32”、”[]byte”等等。每个变量在编译时就确定了自身的静态类型。

特殊的静态类型 interface

interface 类型是一种特殊的类型,它代表方法集合,可以用来存放任何实现了其方法的值。

最特殊的 interface 类型为空 interface 类型,即 interface {} ,interface用来表示一组方法集
合,所有实现该方法集合的类型都被认为是实现了该接口。所以空 interface 类型的方法集合为空,也就是说所有类型都可以认为是实现了该接口。

所以一个空interface类型变量可以存放所有值,这也是有些人认为Go是动态类型的原因,这是个错觉。

反射三定律

interface 类型有个(value,type)对,Go 提供了用来提取 interface 的 value 和 type 的方法,反射就是检查 interface 的这个(value, type)对的。

  • reflect.Type 提供一组接口处理 interface 的类型,即(value, type)中的 type。
  • reflect.Value 提供一组接口处理 interface 的值,即(value, type)中的 value。

反射第一定律:反射可以将 interface 类型变量转换成反射对象

通过反射获取一个变量的值和类型,示例:

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

import (
"fmt"
"reflect"
)

func main(){
var x float64 = 3.4
t := reflect.TypeOf(x)
fmt.Println("type:", t)

v := reflect.ValueOf(x)
fmt.Println("value", v)
}

运行结果:

1
2
go复制代码type: float64
value 3.4

反射是针对 interface 类型的变量,TypeOf() 和 ValueOf() 接受的参数都是 interface{} 类型的,即 x 值是被转成了 interface 传入的。

反射第二定律:反射可以将反射对象还原成 interface 对象

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

import (
"fmt"
"reflect"
)

func main(){
var x float64 = 3.4
v := reflect.ValueOf(x) //v is reflext.Value
var y float64 = v.Interface().(float64)
fmt.Println("value", y)
}

运行结果:

1
go复制代码value 3.4

对象 x 转换成反射对象 v,v 又通过 Interface() 接口转换成了 interface 对象,interface 对象通过.(float64)类型断言获取 float64 类型的值。

断言格式为:s = x.(T),意思是如果 x 所持有的元素如果同样实现了 T 接口,那么就把值传递给 s。

反射第三定律:反射对象可修改,value值必须是可设置的

通过反射可以将 interface 类型变量转换成反射对象,可以使用该反射对象设置其持有的值。

错误示例:

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

import (
"reflect"
)

func main(){
var x float64 = 3.4
v := reflect.ValueOf(x) //v is reflext.Value
v.SetFloat(6.6) //Error
}

上面程序会发生 panic ,原因即是 v 是不可修改的。

传入 reflect.ValueOf() 函数的其实是 x 的值,而非 x 本身。即通过 v 修改其值是无法影响 x 的,所以会报错。

如果构建 v 时使用 x 的地址就可实现修改了,但此时 v 代表的是指针地址,我们要设置的是指针所指向的内容,也即我们想要修改的是 *v 。 那怎么通过 v 修改 x 的值呢?

reflect.Value 提供了 Elem() 方法,可以获得指针指向的 value 。

示例:

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

import (
"fmt"
"reflect"
)

func main(){
var x float64 = 3.4
v := reflect.ValueOf(&x)
v.Elem().SetFloat(6.6)
fmt.Println("x :", v.Elem().Interface())
}

运行结果:

1
go复制代码x : 6.6

本文转载自: 掘金

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

Python异常 第一部分:::异常 第二部分:::异常处理

发表于 2021-11-04

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

第一部分:::异常

注意:当代码出现了异常,后面的代码都不会执行。

异常本身是类

小知识点:

在 Python 中所有的异常都是继承自 BaseException

直接分为四大类:

SystemExit:Python退出异常

KeyboardInterrupt: 键盘打断(Ctrl+C)

GeneratorExit: 生成器退出

Exception: 普通异常(只会使用这部分的异常)

第二部分:::异常处理

(1)基本的try…except…

举例子:

1
2
3
4
5
6
python复制代码try:
print(11) # 这是正确的,会打印出来
print(a) # a没定义,所以会出现异常
print(22) # 因为上面一句出现了异常,所以即使这句是正确的,也不会打印
except:
print('这里出现了异常')

输出:

11

这里出现了异常

注意:try 后面必须跟上 except

(2)捕获具体的异常

except 后面可以写上捕获具体的异常类型

还可以通过as 把捕获的异常信息 储存到后面的变量里面

例子:

1
2
3
4
5
6
7
python复制代码try:
print(11)
print(a)
print(22)
except NameError as t: #注意这个具体的异常必须和上面出现的异常一样。可以换成Exception,这是所有异常的父类
print('这里出现了异常')
print('错误的原因是:%s'%t)

输出:

11

这里出现了异常

错误的原因是:name ‘a’ is not defined

(3)捕获多种异常

写法一:可以写多个except

1
2
3
4
5
6
python复制代码try:
pass
except TabError:
pass
except NameError:
pass

写法二:在except后面用括号把多种异常类型给括起来

1
2
3
4
python复制代码try:
pass
except (NameError,TabError):
pass

如果不确定异常类型:

1
2
3
4
python复制代码try:
pass
except Exception: #因为Exception里面包括所有普通异常
pass

关于 Exception 及其 子类 的解释:

代码中会出现的异常都是 Exception 的子类, 因此在 except 中只需要在最后加上 Exception 即可

在捕获异常的过程中,会从上倒下依次对比异常,找到之后就不会再往后查找

(4)更加丰富的结构:

1
2
3
4
5
6
7
8
9
10
11
php复制代码try:
print(11)
print(a)
print(22)
except Exception as result:
print('捕获到了异常,会执行我下面的代码对异常进行处理')
print(result)
else:
print('没有捕获到异常,执行我')
finally:
print('不管有没有异常,我都会执行')

注意事项:

1.try 下面放可能出错的代码

2.except try下面的代码出错后,会执行except下面的代码

4.finally 不管try下面的代码有没有出错,始终都会执行

(5)扩展(自定义异常类型)

raise 主动抛出异常

格式:raise 异常类型

注意:raise是主动抛出后面写的异常类型

1.可以自己写自定义的异常:

1
2
3
python复制代码class WuMou(Exception):
pass
raise WuMou('出现错误')

输出为:

1
2
3
4
arduino复制代码Traceback (most recent call last):
File "C:/my/pycharm_work/ceshi.py", line 3, in <module>
raise WuMou('出现错误')
__main__.WuMou: 出现错误

可以捕获这个异常:

1
2
3
4
5
6
7
python复制代码class WuMou(Exception):
pass

try:
raise WuMou('出现错误')
except WuMou as h:
print(h)

输出为:

出现错误

2.例子:

1
java复制代码raise NameError('出现错误啦')

输出为:

1
2
3
4
arduino复制代码Traceback (most recent call last):
File "C:/my/pycharm_work/ceshi.py", line 2, in <module>
raise NameError('出现错误啦')
NameError: 出现错误啦

本文转载自: 掘金

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

迎接新一代微服务架构

发表于 2021-11-04

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

微服务是近些年来软件架构中的热名词,也是一个很大的概念,不同人对它的理解都各不相同,甚至在早期微服务架构中出现了一批四不像的微服务架构产品,有人把单纯引入 Spring Boot、Spring Cloud 等框架的应用服务也称之为微服务架构,但这却只是将它作为服务的 Web 容器而已。

随着微服务的火热,越来越多的团队开始实践,将微服务纷纷落地,并投入生产。但随着微服务规模的不断壮大,每增加一个微服务,就可能会增加一些依赖的基础设施和第三方的配置,比如 Kafka 、Redis 实例等,相应 CI/CD 的配置也会增加或调整。 同时随着微服务数量增多、业务复杂性的提升及需求的多样性等(如,对接第三方异构系统等),服务间通信的错综复杂,一步步地将微服务变得更加臃肿,服务治理也是难上加难,而这些问题在单体架构中是很容易解决的。为此,有人开始怀疑当初微服务化是否是明智之选,甚至考虑回归到传统单体应用。

正如下图所示,PPT 中的微服务总是美好的,但现实中的微服务却是一团糟糕,想甩甩不掉,越看越糟心。难道就没有办法了么?

现实中和PPT中的微服务对比

1、传统微服务架构面临的挑战

面对上述暴露出的问题,并在传统微服务架构下,经过实践的不断冲击,面临了更多新的挑战,综上所述,产生这些问题的原因有以下这几点:

  • 过于绑定特定技术栈。 当面对异构系统时,需要花费大量精力来进行代码的改造,不同异构系统可能面临不同的改造。
  • 代码侵入度过高。 开发者往往需要花费大量的精力来考虑如何与框架或 SDK 结合,并在业务中更好的深度融合,对于大部分开发者而言都是一个高曲线的学习过程。
  • 多语言支持受限。 微服务提倡不同组件可以使用最适合它的语言开发,但是传统微服务框架,如 Spring Cloud 则是 Java 的天下,多语言的支持难度很大。这也就导致在面对异构系统对接时的无奈,或选择退而求其次的方案了。
  • 老旧系统维护难。 面对老旧系统,很难做到统一维护、治理、监控等,在过度时期往往需要多个团队分而管之,维护难度加大。

上述这些问题在传统微服务架构中都是在所难免,我们都知道技术演进来源于实践中不断的摸索,将功能抽象、解耦、封装、服务化。 随着传统微服务架构暴露出的这些问题,将迎来新的挑战,让大家纷纷寻找其他解决方案。

2、迎来新一代微服务架构

为了解决传统微服务面临的问题,以应对全新的挑战,微服务架构也进一步演化,最终催生了Service Mesh 的出现,迎来了新一代微服务架构,也被称为“下一代微服务”。为了更好地理解 Service Mesh 的概念和存在的意义,我们来回顾一下这一演进过程。

1.1 耦合阶段

在微服务架构中,服务发现、负载均衡、熔断等能力是微服务架构中重要的组成部分。微服务化之后,服务更加的分散,复杂度变得更高,起初开发者将诸如熔断、超时等功能和业务代码封装在一起,使服务具备了网络管控的能力,如下图所示。

耦合阶段

这种方案虽然易于实现,但从设计角度来讲却存在一定的缺陷。

  • 基础设施功能(如,服务发现,负载均衡、熔断等)和业务逻辑高度耦合。
  • 每个微服务都重复实现了相同功能的代码。
  • 管理困难。如果某个服务的负载均衡发生变化,则调用它的相关服务都需要更新变化。
  • 开发者不能集中精力只关注于业务逻辑开发。

1.2 公共库 SDK

基于上面存在的问题,很容易会想到将基础设施功能设计为一个公共库 SDK,让服务的业务逻辑与这些公共功能降低耦合度,提高重复利用率,更重要的是开发者只需要关注公共库 SDK 的依赖及使用,而不必关注实现这些公共功能,从而更加专注于业务逻辑的开发,比如 Spring Cloud 框架是类似的方式。如下图所示:

公共库SDK阶段

实际上即便如此,它仍然有一些不足之处。

  • 这些公共库 SDK 存在较为陡峭的学习成本,需要耗费开发人员一定的时间和人力与现有系统集成,甚至需要考虑修改现有代码进行整合。
  • 这些公共库 SDK 一般都是通过特定语言实现,缺乏多语言的支持,在对现有系统整合时有一定的局限性。
  • 公共库 SDK 的管理和维护依然需要耗费开发者的大量精力,并需专门人员来进行管理维护。

1.3 Sidecar 模式

有了上面公共库 SDK 的启发,加上跨语言问题、更新后的发布和维护等问题,人们发现更好的解决方案是把它作为一个代理,服务通过这个透明的代理完成所有流量的控制。

这就是典型的 Sidecar 代理模式,也被翻译为”边车”代理,它作为与其他服务通信的桥梁,为服务提供额外的网络特性,并与服务独立部署,对服务零侵入,更不会受限于服务的开发语言和技术栈,如下图所示。

Sidecar模式阶段

以 Sidecar 模式进行通信代理,实现了基础实施层与业务逻辑的完全隔离,在部署、升级时带来了便利,做到了真正的基础设施层与业务逻辑层的彻底解耦。另一方面,Sidecar 可以更加快速地为应用服务提供更灵活的扩展,而不需要应用服务的大量改造。Sidecar 可以实现以下主要功能:

  • 服务注册。 帮助服务注册到相应的服务注册中心,并对服务做相关的健康检查。
  • 服务路由。 当应用服务调用其它服务时,Sidecar 可以帮助从服务发现中找到相应的服务地址,完成服务路由功能。
  • 服务治理。 Sidecar 可以完全拦截服务进出的流量,并对其进行相应的调用链跟踪、熔断、降级、日志监控等操作,将服务治理功能集中在 Sidecar 中实现。
  • 集中管控。 整个微服务架构体系下的所有服务完全可以通过 Sidecar 来进行集中管控,完成对服务的流控、下线等。

于是,应用服务终于可以做到跨语言开发、并更专注于业务逻辑的开发。

1.4 Service Mesh

把 Sidecar 模式充分应用于一个庞大的微服务架构系统,为每个应用服务配套部署一个 Sidecar 代理,完成服务间复杂的通信,最终就会得到一个如下图所示的网络拓扑结构,这就是 Service Mesh,又称之为“服务网格“。

Service Mesh阶段

至此,迎来了新一代微服务架构——Service Mesh,它彻底解决了传统微服务架构所面临的问题。

本文转载自: 掘金

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

RuoYi-Vue 前后端分离版代码浅析-登录逻辑-i18n

发表于 2021-11-04

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

前言

本节介绍RuoYi-Vue的ruoyi-admin模块中的系统登录模块SysLoginController 部分的代码,这个接口中都是和登录相关的逻辑,登录这里才是一个系统最为精髓的地方,普通的增删改查也可以,使用oauth2,使用cas,各种各样的登录代码体现了不同的设计思路和技术功底,但是跳不过的是如何返回提示信息给调用方,如果只有一种语言,肯定无法帮助我们实现服务多种语言的用户的想法。所以本节我们介绍Ruoyi中的文本国际化的代码。

internationalization(i18n)

我们讨论文本国际化时不可避免就要说到i18n,可能很多人都不了解为什么要叫i18n,其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数,主要是为了拼写简单,所以我们简写为i18n。

前端实现i18n

前端实现i18n根据你使用的框架可以使用vue i18n,也可以使用jquery-i18n-properties

后端实现i18n

Ruoyi中为了实现文本国际化,直接使用了Spring 中自带的MessageSource和LocaleContextHolder来帮助简化国际化。在Common层中有MessageUtils这个帮助类可以简化我们的国际化之路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;


/**
* 获取i18n资源文件
*
* @author ruoyi
*/
public class MessageUtils {
/**
* 根据消息键和参数 获取消息 委托给spring messageSource
*
* @param code 消息键
* @param args 参数
* @return 获取国际化翻译值
*/
public static String message(String code, Object... args) {
MessageSource messageSource = SpringUtils.getBean(MessageSource.class);
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
}
}

在我们的application.yml中我们需要进行属性的配置

1
2
3
4
5
6
java复制代码# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages

将需要使用的语言文件放入resources中的i18n/messages这里就可以了

image.png

当我们需要使用的时候

1
2
3
4
5
6
7
8
9
java复制代码@RestController
@RequestMapping("/api")
public class TestController {

@PostMapping("/test")
public String test() {
return MessageUtils.message("user.login.success");
}
}

对应的提示语需要在两个语言文件中都进行配置

1
2
3
4
yml复制代码默认
user.login.success=登录成功
en中
user.login.success=loginsucess

当我们调用的时候在header中添加上Accept-Language=en时,可以看到返回的提示语已经是英文了。

image.png

本文转载自: 掘金

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

【Spring Boot 快速入门】十五、Spring Bo

发表于 2021-11-04

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

前言

   嗨,大家好,相信做开发的朋友经常会听到“爬虫”。原先针对爬虫比较好的语言是Python,有相对成熟的库和插件,针对其他语言的不是很友好。后来其他语言也出现了零星的爬虫框架或者类库。本次咱们将基于Java语言搭建一个简单的爬虫示例,本示例仅供大家学习使用,切勿做商业和非法项目。

Jsoup

   爬虫是按照人们既定的网络规则,自动抓取网站或其他载体上信息的程序或者脚本。

   本次针对将以Java开发语言基于Jsoup搭建一个简单的爬虫Demo。那么首先大家要了解什么是Jsoup。

   Jsoup是基于Java的HTML解析器。能够根据前端HTML、css解析网站上的信息。Jsoup提供了针对Java开发语言非常强大实用的API,可操作HTML元素、属性、文本,使用DOM或CSS选择器来查找、取出数据。当然,如果入门Jsoup也需要有一定的前端基础,才能快速入手。

快速开始

引入pom

   上面已经简单了解了Jsoup的功能和特点。下面将开始搭建一个基于java开发语言的Spring Boot集成Jsoup搭建一个简单的爬虫Demo。其中Jsoup的版本为1.11.3,Spring Boot的版本为2.3.0.RELEASE。Jsoup的依赖如下:

1
2
3
4
5
js复制代码         <dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>

获取链接

   提取一个网站的信息,首先要在查询到自己需要的信息,然后提取自己需要信息的URL。例如前段时间掘金举办的中秋节活动。当时就基于某电商网站的阅饼销售数据进行了一个简单的查询。只提取了一个页面的信息。包含价格、店铺、商品名称等基础信息。只是学习使用,仅作为测试使用,禁止大量提取。
本次使用的URL是如下:

1
js复制代码https://search.****.com/Search?keyword=%E6%9C%88%E9%A5%BC&qrst=1&spm=2.1.0&stock=1&pvid=9ac3cb4efb544d6f98239432761506f0&page=11&s=296&click=0

建立连接

   我们已经提取到信息所在页面的URL,那么接下来就是建立连接了,建立连接直接使用Jsoup的connect就可以,创建一个Connection对象。这样Java就和网站建立连接了。基础代码如下:

1
js复制代码 Connection connect = Jsoup.connect(url);

获取网页

   在网站建立连接之后,直接通过Connection的get方法去获取Document内容。

1
js复制代码 Document document = connect.get();

   通过输出Document对象,可以看到Document是包含大量的HTML,那么久可以根据这个Document对象进行数据提取了。
图片.png

选中信息对象

可以通过对Document对象的select方法获得具体的文本内容,这就涉及到前端的基础知识了。例如类选择器、元素选择器、通配选择器等。根据具体的元素,选择即可。如下 选择class 下的.goods-list-v2。元素为ul的li属性。这样的话就提取到页面所有的商品信息了。

1
js复制代码 Elements rootselect = document.select(".goods-list-v2 ul li");

获取信息

   当获取到所有的商品信息之后,就可以根据css选择器,针对需要提前的元素进行select了。本次提取商品的售价、店铺名称和商品名称。如下代码。

1
2
3
4
5
6
js复制代码    Elements novelname = ele.select(".p-price strong i");
String price = novelname.text();
Elements author = ele.select(".J_im_icon a");
String shop = author.first().text();
Elements sumadvice = ele.select(".p-name a em");
String goodsName = sumadvice.last().text();

数据处理

   其中根据Elements 的属性,提取text文本信息。经过数据简单处理之后,得到如下信息。

图片.png

结语

   好了,一个简单的集成Jsoup搭建一个简单的爬虫Demo就完成了,是不是很简单。还有更多功能需要大家去探索喽。仅供学习使用,禁止非法使用获取数据,要遵循互联网安全法,不要触犯法律。

   感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

    作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

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

Java多线程初体验

发表于 2021-11-04

一、什么是多线程?

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。
——《百度百科》

二、怎么创建Java多线程

在Java语言中,关于线程的类有Thread类,关于多线程的接口有Runnable和Callable接口(JDK1.5新增),要在Java中实现多线程,则必须实现对应的接口或继承对应的类。

方法一:继承Thread类

步骤:

  1. 创建一个类继承Thread类
  2. 重写Thread类中的run()方法
  3. 创建Thread类的子类的对象
  4. 通过对象调用start()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class ThreadTest{
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4.通过对象调用start()方法
t1.start();
//3.创建Thread类的子类的对象
MyThread t2 = new MyThread();
//4.通过对象调用start()方法
t2.start();
}
}
//1.创建一个类继承Thread类
class MyThread extends Thread{
//2.重写Thread类中的run()方法
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}

多线程可以创建多个Thread类的子类的对象,每一个对象就是一个线程,每一个线程分别调用自己的start()方法。

方法二:实现Runnable接口

步骤:

  1. 创建一个实现了Runnable接口的类
  2. 实现Runnable接口中的run()方法
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
//2. 实现Runnable接口中的run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}

public class RunnableTest {
public static void main(String[] args) {
//3. 创建实现类的对象
MThread m1 = new MThread();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread thread = new Thread(m1);
//5. 通过Thread类的对象调用start()方法。
thread.start();
Thread thread1 = new Thread(m1);
thread1.start();
}
}

使用实现Runnable接口的方法创建多线程,实现了Runnable接口的类只需要实例化一个对象即可,将对象传入到Thread类的构造器中,创建多个不同的Thread类,每一个Thread类的对象就是一个线程。

方法三:实现Callable接口(JDK1.5新增)

步骤:

  1. 创建一个实现Callable接口的实现类
  2. 实现Callable接口的call()方法,将此线程需要执行的操作声明在该方法中
  3. 创建Callable接口实现类的对象
  4. 将Callable实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并调用start()方法
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复制代码//1. 创建一个实现Callable接口的实现类
class NumThread implements Callable{
//实现Callable接口的call()方法,将此线程需要执行的操作声明在该方法中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
System.out.println(i);
sum += i;
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3. 创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4. 将Callable实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask1 = new FutureTask(numThread);
//5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并调用start()方法
new Thread(futureTask1).start();
FutureTask futureTask2 = new FutureTask(numThread);
new Thread(futureTask2).start();
try {
//get()方法的返回值即为FutureTask构造器参数Callable实现类重写的call()方法的返回值
Object value = futureTask1.get();
System.out.println(value);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

使用该方法实现多线程的好处是多线程的具体方法可以有返回值,使用FutureTask类的对象的get()方法可以获取到call()方法的返回值。
该方法多线程只需要创建一个实现Callable接口的对象,将对象传入到FutureTask中创建多个FutureTask类的对象,再将FutureTask对象传入到Thread类中生成Thread类的对象,每一个Thread类的对象就是一个线程。

方法四:使用线程池

步骤:

  1. 提供一个实现了Runnable或Callable接口的类,并重写其中的run()或call()方法
  2. 提供指定线程数量的线程池
  3. 执行指定线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
  4. 关闭连接池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码//1. 提供一个实现了Runnable或Callable接口的类,并重写其中的run()或call()方法
class NumThread implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2.执行指定线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumThread()); //适用于Runnable
// service.submit(); //适用于Callable
//3.关闭连接池
service.shutdown();
}
}

使用线程池的优点:

  1. 提高响应速度(减少了创建线程的时间)
  2. 降低资源消耗(重复利用线程池中的线程,不需要每次使用都创建)
  3. 便于线程管理

本文转载自: 掘金

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

Mysql 温故知新系列【表字段维护】

发表于 2021-11-04

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

在 Mysql 温故知新系列 【表创建】 - 掘金 (juejin.cn) 这篇文章中,讲到了如何创建表,在建表时,有哪些常规的操作,如定义自增主键,唯一字段,非空属性,携带默认值,以及指定外键。但这些操作,是我们建表时提前规划好的,实际上,我们会在对字段做增减以及修改等操作,这就是本章的目的

表维护

字段添加

在结尾追加

1
sql复制代码alter table <表名> add <新字段名><数据类型>[约束条件];

这种方式,字段会添加到表的最后

image.png

在开头添加

1
sql复制代码alter table <表名> add <新字段名> <数据类型> [约束条件] first;

mysql 默认是在表的最后添加新的字段,我们可以使用关键字 first 将添加的字段限定在表的字段的第一行

image.png

添加在表的任意位置

除上两种方式,mysql 还支持在表中间任意位置插入新字段。这需要使用关键字 after 指定新字段插入表中现有字段的哪一个后边

1
sql复制代码alter table <表名> add <新字段名> <数据类型> [约束条件] after <已经存在的字段名>;

image.png

字段删除

在新项目中,我们设计的表经过多次字段添加,以及长时间的运行后,会有一些字段不再使用

处于优化表结构的目的,我们需要移除这些冗余的字段,相关命令如下:

1
sql复制代码alter table table_name drop column field

image.png

前面测试插入的 uuid 列已经移除

字段修改

重命名字段

1
sql复制代码alter table <表名> change <旧字段名> <新字段名> <新数据类型> [约束条件];

image.png

uname -> username,并且还修改了他的默认值

字段调整

1
sql复制代码alter table <表名> modify <字段名> <数据类型>

image.png

不仅修改了长度,还修改了默认值,并添加了唯一约束

追加外键

1
2
3
4
sql复制代码alter table <表名> add foreign key [自定义外键名](<表中外键字段>) references <关联表>(<关联表主键>);

-- 示例
alter table user add foreign key [fk_user_dept_id](dept_id) references dept(id);

删除外键

1
2
3
4
sql复制代码alter table <表名> drop foreign key <外键约束名>

-- 以上一步添加的外键 fk_user_dept_id 为例
alter table user drop foreign key fk_user_dept_id;

本文转载自: 掘金

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

1…419420421…956

开发者博客

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