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

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


  • 首页

  • 归档

  • 搜索

震惊了!!nginx代理还能这么用

发表于 2021-10-13

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

通过nginx做反向代理相信大家都耳熟能详了,但是使用nginx做正向代理你玩过吗?

直接上结论,为什么使用nginx做正向代理。

nginx做正向代理的好处

  • 基于域名做白名单
  • 不用配置ssl证书!!!不用配置ssl证书!!!不用配置ssl证书!!!

初始化运行环境

安装必要的依赖组件,直接上脚本

1
2
shell复制代码yum -y update
yum -y install gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel patch git

下载nginx支持代理的模块

1
2
3
shell复制代码mkdir /usr/local/nginx_modules
cd /usr/local/nginx_modules
git clone https://github.com/chobits/ngx_http_proxy_connect_module.git

安装nginx

下载并解压

1
2
3
shell复制代码wget http://nginx.org/download/nginx-1.19.8.tar.gz
tar -zxvf nginx-1.19.8.tar.gz
cd nginx-1.19.8/

安装补丁

1
shell复制代码patch -p1 < /usr/local/nginx_modules/ngx_http_proxy_connect_module/patch/proxy_connect_rewrite_1018.patch

创建运行nginx用户&用户组

1
2
shell复制代码groupadd www
useradd -g www www

开始安装nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码./configure \
--user=www \
--group=www \
--prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-threads \
--add-module=/usr/local/nginx_modules/ngx_http_proxy_connect_module

make

make install

到此,nginx已经安装完成

校验安装

1
2
3
shell复制代码cd /usr/local/nginx
// 检查配置文件
./sbin/nginx -t

启动nginx

1
2
3
4
5
6
7
bash复制代码cd /usr/local/nginx
./sbin/nginx

ps -ef|grep nginx // 检查是否启动成功

./sbin/nginx -s stop // 停止
./sbin/nginx -s reload // 重新启动

关于如何加入开机启动等自行搜索,习惯了在目录下操作,这里就不展开了。

配置nginx正向代理

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
ini复制代码user  www;
worker_processes 4;


events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
server_names_hash_bucket_size 128;
client_max_body_size 20m;
client_body_buffer_size 256k;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
sendfile on;
tcp_nopush on;
keepalive_timeout 60;
tcp_nodelay on;
server {
listen 9080;

resolver 114.114.114.114 valid=60s ipv6=off;
resolver_timeout 5s;

proxy_connect;
proxy_connect_connect_timeout 10s;
proxy_connect_read_timeout 10s;
proxy_connect_send_timeout 10s;

location / {
set $forbiden Y;

if ($host = "www.baidu.com" ) {
set $forbiden N;
}

if ( $forbiden = Y){
return 403;
}
proxy_set_header Host $host;
proxy_pass http://$host;
}
}
}

特殊提醒

resolver 114.114.114.114 valid=60s ipv6=off; 这里ipv6一定要关掉,部分网站开启了ipv6的域名解析,目前不支持,会导致访问失败

域名访问白名单

nginx支持简答的逻辑判断,通过host变量获取当前请求的域名,$forbiden进行逻辑判断

客户端设置

基于浏览器界面操作

操作路径设置-高级-打开计算机代理设置,设置你的服务器IP+端口即可。

linux等基于命令行操作

1
2
shell复制代码 export http_proxy=http://$proxy_ip:$proxy_port
export https_proxy=http://$proxy_ip:$proxy_port

浏览器插件

推荐使用SwitchyOmega

本文转载自: 掘金

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

【微信小程序爬虫】表情包小程序图文视频教学,从零写起,保姆教

发表于 2021-10-13

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

Code皮皮虾 一个沙雕而又有趣的憨憨少年,和大多数小伙伴们一样喜欢听歌、游戏,当然除此之外还有写作的兴趣,emm…,日子还很长,让我们一起加油努力叭🌈

如果觉得写得不错的话,球球一个关注哦😉

本文已经收录至【微信小程序爬虫专栏】,有兴趣者可前往查看,皮皮虾专栏种类繁多,各种爬虫应有尽有

点我,点我,快点我查看😁


爬取分析

打开微信,搜索表情包小程序

image-20211010223052848

来到首页,点击查看更多

image-20211010223238590

今天就以这个热门表情包为例,进行爬取教学

image-20211010223320177

在进入这个页面的时候,Fiddler已经抓包到一个带有list路径的请求,凭借我皮皮虾多年的爬虫经验(手动狗头),这一定是带有图片数据的请求

image-20211010223436768

复制JSON数据到在线JSON解析工具中,解析后得到右图

image-20211010223752030

复制src或者cdnurl,进行访问都能得到这个图

image-20211010223659240


视频教学(附带源码)

B站视频地址

觉得不错的,球球有一个一键三连哦,哈哈😁

在这里插入图片描述


成果展示

image-20211010224011025

出于测试就爬了5页

image-20211010224057431

image-20211010224113160

image-20211010224123976

image-20211010224135348

🔥专栏分享

毛遂自荐,给大家推荐一下自己的专栏😁,欢迎小伙伴们收藏关注😊

力扣算法题解专区

小白学Java

MybatisPlus专栏

App爬虫专栏

PC端爬虫专栏

大厂面试题专栏


💖最后

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以一键三连哦!,感谢支持,我们下次再见~


一键三连.png

本文转载自: 掘金

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

阿里限流神器Sentinel夺命连环 17 问?

发表于 2021-10-13

1、前言

这是《spring Cloud 进阶》专栏的第五篇文章,这篇文章介绍一下阿里开源的流量防卫兵Sentinel,一款非常优秀的开源项目,经过近10年的双十一的考验,非常成熟的一款产品。往期文章如下:

  • 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?
  • openFeign夺命连环9问,这谁受得了?
  • 阿里面试这样问:Nacos、Apollo、Config配置中心如何选型?这10个维度告诉你!
  • 阿里面试败北:5种微服务注册中心如何选型?这几个维度告诉你!

文章目录如下:

2、什么是sentinel?

sentinel顾名思义:卫兵;在Redis中叫做哨兵,用于监控主从切换,但是在微服务中叫做流量防卫兵。

Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
  • 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 的主要特性如下图:

Sentinel 分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

总之一句话:sentinel真牛逼,完爆Hystrix………

3、sentinel和Hystrix有何区别?

不多说了,总之一句话:Hystrix赶紧放弃,用sentinel……

具体区别如下图:

4、sentinel版本如何选择?

由于陈某写的是Spring Cloud 进阶一个系列,使用的聚合项目,因此版本还是保持和之前文章一样,不清楚的可以看这篇:五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?

这里选择的spring-cloud-alibaba-dependencies的版本是2.2.1.RELEASE,因此sentinel版本选择1.7.1,大家可以根据自己的版本选择对应sentinel的版本,版本对应关系如下图:

注意:一定要按照官方推荐的版本适配,否则出现意想不到的BUG追悔莫及………

5、Sentinel 控制台如何安装?

sentinel和nacos一样,都有一个控制台,但是这里不用自己手动搭建一个微服务,官方已经搭建好了,只需要下载对应得jar包运行即可。下载地址:github.com/alibaba/Sen…

选择对应得版本下载即可,我这里选择1.7.1版本,下载的jar包如下图:

当然你可以通过源码构建:mvn clean package

注意:JDK版本必须>=1.8

此时我们只需要运行这个jar包即可,命令如下:

1
shell复制代码java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.1.jar

上述参数含义如下:

  • -Dserver.port:指定启动的端口,默认8080
  • -Dproject.name:指定本服务的名称
  • -Dcsp.sentinel.dashboard.server:指定sentinel控制台的地址,用于将自己注册进入实现监控自己

启动成功之后,浏览器访问:http://localhost:8080,登录页面如下图:

默认的用户名和密码:sentinel/sentinel

登录成功之后页面如下:

可以看到目前只有一个服务sentinel-dashboard被监控了,这个服务就是自己。

注意:上述参数都是可选的,没必要可以不填。

那么问题来了:默认的用户名和密码在生产环境上肯定不能用,如何修改呢?

从 Sentinel 1.6.0 起sentinel已经支持自定义用户名和密码了,只需要在执行jar命令时指定即可,命令如下:

1
shell复制代码java -Dsentinel.dashboard.auth.username=admin -Dsentinel.dashboard.auth.password=123 -jar sentinel-dashboard-1.7.1.jar

用户可以通过如下参数进行配置:

  • -Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名为 sentinel;
  • -Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel;
  • -Dserver.servlet.session.timeout=7200 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟;

注意:部署多台控制台时,session 默认不会在各实例之间共享,这一块需要自行改造。

除了用户名密码相关的配置,sentinel控制台还提供了其他的可配置选项,如下图:

6、微服务如何接入sentinel控制台?

微服务为什么要集成sentinel控制台,sentinel不是提供了相关的API吗?

其实Spring Boot 官方一直提倡约定>配置>编码的规则,能够不硬编码何乐而不为呢?

因此本文后续内容主要还是结合sentinel控制台进行讲解,关于API的使用大家可以按照官方文档学习,讲解的非常清楚。

好了,言归正传,微服务如何接入sentinel控制台呢?

1、新建微服务模块注册进入Nacos

这里的注册中心依然使用的是nacos,有不会用的请看专栏第一篇Nacos文章:五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?

新建一个微服务模块:sentinel-service9008,相关代码不贴出了。

相关配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码server:
port: 9008
spring:
application:
## 指定服务名称,在nacos中的名字
name: sentinel-service
cloud:
nacos:
discovery:
# nacos的服务地址,nacos-server中IP地址:端口号
server-addr: 127.0.0.1:8848
management:
endpoints:
web:
exposure:
## yml文件中存在特殊字符,必须用单引号包含,否则启动报错
include: '*'

源码全部会上传,获取方式看文末!

2、添加依赖

除了Nacos的依赖,还需要添加一个sentinel的依赖:

1
2
3
4
5
xml复制代码<!--sentinel的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

以上只贴出了sentinel相关依赖,nacos依赖不再贴了,见源码!

3、添加配置集成控制台

只需要添加如下配置即可集成sentinel控制台:

1
2
3
4
5
6
yaml复制代码spring:
cloud:
sentinel:
transport:
## 指定控制台的地址,默认端口8080
dashboard: localhost:8080

4、新建一个测试接口

下面新建一个测试接口,用于测试相关规则,如下:

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

@GetMapping("/test")
public String test(){
return "接收到一条消息--------";
}
}

5、启动微服务

启动9008这个微服务,然后浏览器输入:http://localhost:9008/sentinel/test,此时查看sentinel控制台,将会看见sentinel-service这个服务已经被监控了,如下图:

注意:sentinel是懒加载机制,只有访问过一次的资源才会被监控。

不过可以通过配置关闭懒加载,在项目启动时就连接sentinel控制台,配置如下:

1
2
3
4
yaml复制代码spring:
sentinel:
# 取消控制台懒加载,项目启动即连接Sentinel
eager: true

7、流量控制如何配置?

流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

QPS:每秒请求数,即在不断向服务器发送请求的情况下,服务器每秒能够处理的请求数量。

并发线程数:指的是施压机施加的同时请求的线程数量。

同一个资源可以创建多条限流规则,一条限流规则由以下元素组成:

  • resource:资源名,即限流规则的作用对象。
  • count: 限流阈值
  • grade:限流阈值类型(1:QPS 0:并发线程数),默认值QPS
  • limitApp:流控针对的调用来源,若为 default 则不区分调用来源,默认值default
  • strategy:判断的根据是资源自身**(0),还是根据其它关联资源 **(1),还是根据链路入口*(2)\*,默认值根据资源本身。
  • controlBehavior: 流控效果(直接拒绝(0) / 排队等待(2) / 预热冷启动(1)),默认值直接拒绝。

以上元素限流元素对应的类是com.alibaba.csp.sentinel.slots.block.flow.FlowRule,各元素如下图:

注意:各个元素的取值以及默认值一定要记住,后续配置将会用到。

以上几个元素在sentinel控制台对应规则如下图:

1、三种流控效果

流控效果总共分为三种,对应元素controlBehavior,分别如下:

快速失败

默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。

warm up

即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

注意:这一效果只针对QPS流控,并发线程数流控不支持。

预热底层是根据令牌桶算法实现的,源码对应得类在com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController。

算法中有一个冷却因子coldFactor,默认值是3,即请求 QPS 从 threshold(阈值) / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。

比如设定QPS阈值为3,流控效果为warm up,预热时长为5秒,如下图:

这样配置之后有什么效果呢:QPS起初会从(3/3/=1)每秒通过一次请求开始预热直到5秒之后达到每秒通过3次请求。动态效果图如下:

从上述动画可以清楚的看见:前几秒是频繁流控的,直到5秒,QPS阈值达到了3。

具体算法原理请看:github.com/alibaba/Sen…

排队等待

匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。源码对应得类:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

注意:这一效果只针对QPS流控,并发线程数流控不支持。

简单举个栗子:你去大学食堂吃饭,只有一个阿姨在打饭,那么所有人都要排队打饭,每次只有一个人打到饭,其他人都在排队等待。

不同的是sentinel有个超时等待时间,一旦超过这个预定设置的时间将会被限流。

该方式作用如下图:

这种方式适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝所有请求。

比如设置QPS阈值为1,超时等待时间为10000毫秒,如下图:

此时的效果如下:

从上图可以看到:连续点击刷新请求,虽然设置了QPS阈值为1,但是并没有被限流,而是在等待,因为设置了超时等待时间为10秒。

具体算法原理请看:github.com/alibaba/Sen…

2、三种流控模式

流控模式总共分为三种,对应元素strategy,分别如下:

  • 直接拒绝:接口达到限流条件时,直接限流
  • 关联:当关联的资源达到阈值时,就限流自己
  • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)

下面来详细介绍下以上三种流控模式。

直接拒绝

顾名思义:默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。上面的几个例子都是配置了直接拒绝这个模式,这里不再详细介绍。

关联

典型的使用场景:一个是支付接口,一个是下单接口,此时一旦支付接口达到了阈值,那么订单接口就应该被限流,不然这边还在下单,消费者等待或者直接被拒绝支付将会极大的影响用户体验。

简而言之:A关联B,一旦B达到阈值,则A被限流

演示一下效果,创建以下两个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@RestController
@RequestMapping("/sentinel")
public class FlowLimitController {

/**
* 下单接口
* @return
*/
@GetMapping("/order")
public String order() {
return "下单成功..........";
}

/**
* 支付接口
* @return
*/
@GetMapping("/pay")
public String pay() {
return "支付成功..........";
}
}

此时的流控规则配置如下图:

注意:关联之后,这里设置的限流规则是对被关联资源,也就是/sentinel/pay这个资源,但是真正被限流则是/sentinel/order。

如何演示效果呢?很简单,只需要不断的请求/sentinel/pay达到阈值,然后在请求/sentinel/order。

利用POSTMAN不断向/sentinel/pay发出请求,然后浏览器请求/sentinel/order,结果如下图:

可以看到订单接口被限流了………….

3、两种统计类型

流控分为两种统计类型,分别是QPS,并发线程数,很多人不太明白这两种统计类型有什么区别?

举个栗子:陈某带了一个亿去银行存钱,但是银行大门保安要查健康码,每秒最多只能同时进入4个人,并且银行中只有两个工作人员工作,如下图:

此时的QPS含义:从保安到银行这一段,即是保安放行进入银行的人数。

此时并发线程数的含义:银行只有两个工作人员在工作,那么最多只能同时处理两个任务,这里并发线程数的阈值就是2。

8、降级规则如何配置?

熔断降级在日常生活中也是比较常见的,场景如下:

  • 股票市场的熔断,当价格触发到了熔点之后,会暂停交易一段时间,或者交易可以继续进行,但是报价会限制在一定的范围。
  • 电压过高导致保险丝触发熔断保护

在大型的分布式系统中,一个请求的依赖如下图:

如果这个时候,某个服务出现一些异常,比如:

  • 服务提供者不可用(硬件故障、程序bug、网络故障、用户请求量较大)
  • 重试导致的流量过大
  • 服务调用者使用同步调用,产生大量的等待线程占用系统资源,一旦线程资源被耗尽,调用者提供的服务也会变成不可用状态

那么将会导致整个服务不可用,用古话来讲就是:千里之堤毁于蚁穴。

所谓编程源于生活,架构师们根据生活的经验设计出了服务的熔断降级策略,很好的解决了这类问题。

熔断降级规则对应sentinel控制台的降级规则这一栏,如下图:

熔断降级涉及到的几个属性如下表:

源码中对应得类为:com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule。

三种熔断策略

Sentinel 提供以下几种熔断策略:

  1. 平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
  2. 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  3. 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

下面演示一个平均响应时间熔断,创建一个接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RestController
@RequestMapping("/sentinel/provider")
@Slf4j
public class FlowLimitController {

@GetMapping("/test")
public String test() throws InterruptedException {
//休眠3秒钟
Thread.sleep(3000);
log.info("收到一条消息----test");
return "接收到一条消息--------";
}
}

在控台为这个接口设置平均响应时间为200毫秒,时间窗口为1秒,大致意思:平均的响应时间大于200毫秒之后,在接下来的1秒时间内将会直接熔断,如下图:

使用Jmeter开启10个线程循环跑,然后在浏览器中访问这个接口,返回结果如下图:

为什么呢?由于的接口中休眠了3秒,平均响应时间肯定大于200毫秒,因此直接被熔断了。

注意:这里熔断后直接返回默认的信息,后面会介绍如何定制熔断返回信息。

9、热点参数如何限流?

顾名思义:热点就是经常访问的数据,很多时候肯定是希望统计某个访问频次Top K数据并对其进行限流。

比如秒杀系统中的商品ID,对于热点商品那一瞬间的并发量是非常可怕的,因此必须要对其进行限流。

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

注意:热点参数限流只针对QPS。

官方文档:github.com/alibaba/Sen…

概念理解了,来看下sentinel控制台如何设置热点参数限流,如下图:

规则对应得源码在com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule这个类中,各种属性含义如下图:

规则都懂了,下面我们通过实战来演示一下热点参数到底是如何限流的。

注意:热点参数限流只作用于八大基本类型。

1、创建一个资源

现在先创建一个service,用@SentinelResource这个注解定义一个资源,这个注解后续将会详细介绍,先忽略,代码如下:

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

/**
* @SentinelResource的value属性指定了资源名,一定要唯一
* blockHandler属性指定了兜底方法
*/
@Override
@SentinelResource(value = "OrderQuery",blockHandler = "handlerQuery")
public String query(String p1, String p2) {
log.info("查询商品,p1:{},p2:{}",p1,p2);
return "查询商品:success";
}

/**
* 对应得兜底方法,一旦被限流将会调用这个方法来处理
*/
public String handlerQuery(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false)String p2,
BlockException exception){
log.info("查询商品,p1:{},p2:{}",p1,p2);
return "查询商品:熔断了......";
}
}

上述代码什么意思呢?如下:

  • 如果query这个接口没有被限流则返回:查询商品:success
  • 如果query这个接口被限流了,则进入了兜底方法handlerQuery方法,返回:查询商品:熔断了……

2、创建controller接口

下面创建一个controller进行测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RestController
@RequestMapping("/sentinel/provider")
@Slf4j
public class FlowLimitController {
@Autowired
private FlowService flowService;

@GetMapping("/order/query")
public String query(@RequestParam(value = "p1",required = false) String p1, @RequestParam(value = "p2",required = false)String p2){
return flowService.query(p1,p2);
}

}

可以看到接口中有两个参数,分别是p1、p2。

3、添加热点参数限流规则

在sentinel控制台点击热点规则->新增热点限流规则,添加如下图规则:

上述配置的具体含义:当OrderQuery这个资源中的第0个参数QPS超过1秒1次将会被限流。这里参数索引是从0开始,第0个就是对应接口中的p1这个参数。

第一个测试:浏览器直接访问:http://localhost:9009/sentinel/provider/order/query?p1=22&p2=1222,连续点击将会看到这个接口被熔断降级了,如下图:

这也正是验证了上述的热点参数限流配置。

第二个测试:浏览器输入:http://localhost:9009/sentinel/provider/order/query?p2=1222,连续点击将会看到这个接口并没有被熔断降级,如下图:

注意:对于热点参数限流,只有包含指定索引的参数请求才会被限流,否则不影响。

此时产品说:ID为100的这个产品点击量太少了,你们赶紧调整下这个商品的限流规则。这个时候该怎么办呢?

别着急,sentinel显然考虑到了这一点,提供了参数例外项这项配置,针对产品需求配置如下:

从上图配置中,我们将参数值p1这个参数值等于100的时候,限流阈值设置成了100,也就是说p1=100这个请求QPS放宽到1秒请求100次以上才会被限流。

验证:浏览器输入地址:http://localhost:9009/sentinel/provider/order/query?p1=100,无论点击多么快,都没有被熔断降级,显然是配置生效了,如下图:

以上源码在sentinel-openfeign-provider9009这个模块中,文末有源码获取方式。

10、系统自适应如何限流?

前面热点参数、普通流量限流都是针对的某个接口,这里系统自适应限流针对是整个系统的入口流量,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

sentinel控制台对应如下图:

阈值类型有五种,分别如下:

  • Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

官方文档:github.com/alibaba/Sen…

系统规则的配置比较简单,这里以入口QPS为例进行演示,为了演示真实情况,清掉所有的限流规则,添加系统规则,如下图:

这个QPS系统规则一配置,该微服务中的所有接口都将会被这个规则限制,比如访问:http://localhost:9009/sentinel/provider/pay,连续点击,如下图:


可以看到已经被限流了,不仅是这个接口,所有接口都会生效。

注意:系统规则中的入口QPS这个规则不建议配置,一旦配置上了可能导致整个服务不可用。

11、如何自定义限流返回的异常信息?

在前面的例子中,无论是熔断降级还是被限流返回的异常信息都是Blocked by Sentinel (flow limiting),这个是Sentinel默认的异常信息。

很显然默认的异常信息并不能满足我们的业务需求,因此我们需要根据前后端规则制定自己的异常返回信息。

这里将会用到一个注解@SentinelResource,这个在上文也是提到过,这个注解中有两个关于限流兜底方法的属性,如下:

  • blockHandler: 对应处理 BlockException 的函数名称。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。
  • blockHandlerClass:指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

官方文档:github.com/alibaba/Sen…

使用@SentinelResource注解自定义一个限流异常返回信息,先自定义一个资源,指定兜底方法为handler,代码如下:

第二步:写个对应得兜底方法,必须在同一个类中,代码如下:

第三步:对资源QueryOrder新增一个限流规则,如下图:

第四步:写个controller,代码就不晒了,自己写吧,哈哈。。。。

第五步:调用接口,疯狂点击,将会出现兜底方法中定义的返回信息,如下图:

到这儿基本算是成功了,但是有个问题:兜底方法必须要和业务方法放在同一个类中,这样代码耦合度不是很高吗?

@SentinelResource提供一个属性blockHandlerClass,完美的解决了这一个问题,能够将兜底方法单独放在一个类中,下面来介绍一下。

第一步:新建一个单独的类CommonHandler来放置兜底方法,代码如下:

第二步:在@SentinelResource注解中指定blockHandlerClass为上面的类,blockHandler指定兜底方法名,代码如下:

好了,至此就完成了,自己照着试试吧…….

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

12、如何对异常进行降级处理?

程序员每天都在制造BUG,没有完美的代码,也没有完美的程序员,针对代码的运行时异常我们无法避免,但是我们可以当出现异常的时候进行捕获并做出相应的处理,我们称之为降级处理。

异常的降级还是要用到@SentinelResource注解,其中相关的几个属性如下:

  • fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象
  • fallbackClass:指定对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

1.8.0 版本开始,defaultFallback 支持在类级别进行配置。

注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理。

官方文档:github.com/alibaba/Sen…

下面定义一个创建订单的接口,手动制造一个1/0异常,代码如下:

上述接口并没有进行异常降级处理,因此调用该接口直接返回了异常信息,非常不友好,如下图:

我们可以使用fallback指定异常降级的兜底方法,此时业务方法改造如下:

使用fallbackClass属性指定单独一个类处理异常降级,降低了代码的耦合度,fallback属性指定了降级兜底的方法,代码如下:

此时再次访问接口,虽然有异常,但是返回的确实降级兜底方法中的返回信息,如下图:

到了这里基本满足了异常降级的处理需求,但是仍然有个疑问:能否只用一个方法处理全部的异常?

答案是:能,必须能,此时就要用到defaultFallback 这个属性了,指定默认的降级兜底方法,此时的业务方法变成如下代码:

defaultFallback属性指定了默认的降级兜底方法,这个方法代码如下:

好了,异常降级处理到这儿已经介绍完了,但是仍然有一个问题:若 blockHandler 和 fallback 都进行了配置,那么哪个会生效?

结论:若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandler、fallback 和 defaultFallback,则被限流降级时会将 BlockException 直接抛出。

将createOrder这个业务接口改造一下,同时指定blockHandler和fallback,代码如下:

此时不配置任何规则,直接访问接口,可以看到这里直接进入了异常降级处理,如下图:

我们对createOrder这个资源配置降级规则:60秒内如果出现2个以上的异常直接限流,如下图:

此时我们再次访问这个接口,可以看到前两次直接进入了fallback指定的方法中(并未达到限流的异常数阈值),两次之后就被限流了,进入了blockHandler方法中,效果如下图:

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

13、sentinel的黑白名单如何设置?

顾名思义,黑名单就是拉黑呗,拉黑就是不能访问了呗,sentinel能够针对请求来源进行是否放行,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

sentinel控制台对应得规则配置如下图:

该规则对应得源码为com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule,几个属性如下:

  • resource:资源名,即限流规则的作用对象。
  • limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB。
  • strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式。

官方文档:github.com/alibaba/Sen…

这里有个问题:请求来源是什么,怎么获取?

Sentinel提供了一个接口RequestOriginParser,我们可以实现这个接口根据自己业务的规则解析出请求来源名称。

下面我以IP作为区分请求来源,代码如下:

然后将127.0.0.1设置为黑名单,如下图:

直接访问:http://127.0.0.1:9009/sentinel/rate/order/query?id=1002,结果如下图:

可以看到被限流了哦……………..

好了,黑白名单就介绍到这里。

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

14、限流规则如何持久化?

Sentinel默认限流规则是存储在内存中,只要服务重启之后对应得限流规则也会消失,实际的生产中肯定是不允许这种操作,因此限流规则的持久化迫在眉睫。

sentinel官方文档提供了两种持久化模式,分别如下:

但是官方推荐使用Push模式,下面陈某就Push模式介绍一下持久化限流规则。这里使用Nacos作为配置中心。

盗用官方一张架构图,如下:

1、添加依赖

这里需要添加一个依赖,如下:

1
2
3
4
xml复制代码<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

2、配置文件中配置相关信息

既然使用到了Nacos作为配置中心,肯定是要配置相关的地址、dataId…

在application.yml配置文件中添加如下配置:

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
yaml复制代码spring:
cloud:
sentinel:
## nacos持久化配置
datasource:
## 配置流控规则,名字任意
ds-flow:
nacos:
## nacos的地址
server-addr: 127.0.0.1:8848
## 配置ID
dataId: ${spring.application.name}-flow
## 配置分组,默认是DEFAULT_GROUP
groupId: DEFAULT_GROUP
## 配置存储的格式
data-type: json
## rule-type设置对应得规则类型,总共七大类型,在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中有体现
rule-type: flow
## 配置降级规则,名字任意
ds-degrade:
nacos:
## nacos的地址
server-addr: 127.0.0.1:8848
## 配置ID
dataId: ${spring.application.name}-degrade
## 配置分组,默认是DEFAULT_GROUP
groupId: DEFAULT_GROUP
## 配置存储的格式
data-type: json
## rule-type设置对应得规则类型,总共七大类型,在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中有体现
rule-type: degrade

上述配置仅仅展示了和持久化相关的一些配置,其他相关的配置代码就不贴了,稍后自己看源码。

spring.cloud.sentinel.datasource下可以配置多个规则,陈某这里只配置了限流和降级规则,其他规则自己尝试配一下,不同规则通过rule-type区分,其取值都在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中,对应着sentinel中的几大统计规则。

3、在Nacos添加对应的规则配置

上述配置中对应的限流(flow)规则如下图:

上述配置中对应的降级(degrade)规则如下图:

先不纠结JSON数据里面到底是什么,先看效果,全部发布之后,Nacos中总共有了两个配置,如下图:

上图中可以看到我们的两种规则已经在Nacos配置好了,来看一下sentinel中是否已经生效了,如下图:

哦了,已经生效了,由于是push模式,只要nacos中点击发布配置,相关规则配置就会推送到sentinel中。

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

伏笔:push模式只能保证Nacos中的修改推送到sentinel控制台,**但是sentinel控制台的限流规则修改如何推送到Nacos呢?**别着急,下面将会介绍…………..

4、JSON中到底怎么写?

很多人好奇JOSN中的配置到底怎么写?其实很简单,陈某在介绍各种规则的时候都明确告诉你每种规则对应源码中的实现类,比如流控规则对应的类就是com.alibaba.csp.sentinel.slots.block.flow.FlowRule,JOSN中各个属性也是来源于这个类。

下面陈某列出各个规则的JSON配置,开发中照着改即可。

1、流控规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
json复制代码[
{
// 资源名
"resource": "/test",
// 针对来源,若为 default 则不区分调用来源
"limitApp": "default",
// 限流阈值类型(1:QPS;0:并发线程数)
"grade": 1,
// 阈值
"count": 1,
// 是否是集群模式
"clusterMode": false,
// 流控效果(0:快速失败;1:Warm Up(预热模式);2:排队等待)
"controlBehavior": 0,
// 流控模式(0:直接;1:关联;2:链路)
"strategy": 0,
// 预热时间(秒,预热模式需要此参数)
"warmUpPeriodSec": 10,
// 超时时间(排队等待模式需要此参数)
"maxQueueingTimeMs": 500,
// 关联资源、入口资源(关联、链路模式)
"refResource": "rrr"
}
]

2、降级规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码[
{
// 资源名
"resource": "/test1",
"limitApp": "default",
// 熔断策略(0:慢调用比例,1:异常比率,2:异常计数)
"grade": 0,
// 最大RT、比例阈值、异常数
"count": 200,
// 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)
"slowRatioThreshold": 0.2,
// 最小请求数
"minRequestAmount": 5,
// 当单位统计时长(类中默认1000)
"statIntervalMs": 1000,
// 熔断时长
"timeWindow": 10
}
]

3、热点规则

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
json复制代码[
{
// 资源名
"resource": "/test1",
// 限流模式(QPS 模式,不可更改)
"grade": 1,
// 参数索引
"paramIdx": 0,
// 单机阈值
"count": 13,
// 统计窗口时长
"durationInSec": 6,
// 是否集群 默认false
"clusterMode": 默认false,
//
"burstCount": 0,
// 集群模式配置
"clusterConfig": {
//
"fallbackToLocalWhenFail": true,
//
"flowId": 2,
//
"sampleCount": 10,
//
"thresholdType": 0,
//
"windowIntervalMs": 1000
},
// 流控效果(支持快速失败和匀速排队模式)
"controlBehavior": 0,
//
"limitApp": "default",
//
"maxQueueingTimeMs": 0,
// 高级选项
"paramFlowItemList": [
{
// 参数类型
"classType": "int",
// 限流阈值
"count": 222,
// 参数值
"object": "2"
}
]
}
]

4、系统规则

负值表示没有阈值检查。不需要删除参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
// RT
"avgRt": 1,
// CPU 使用率
"highestCpuUsage": -1,
// LOAD
"highestSystemLoad": -1,
// 线程数
"maxThread": -1,
// 入口 QPS
"qps": -1
}
]

5、授权规则

1
2
3
4
5
6
7
8
9
10
json复制代码[
{
// 资源名
"resource": "sentinel_spring_web_context",
// 流控应用
"limitApp": "/test",
// 授权类型(0代表白名单;1代表黑名单。)
"strategy": 0
}
]

注意:对于上述JOSN中的一些可选属性不需要的时候可以删除。

官方文档:github.com/alibaba/Sen…

15、限流规则如何推送到Nacos进行持久化?

sentinel默认的持久化只能从nacos推送到sentinel控制台,但是实际生产中肯定是双向修改都能推送的,这个如何解决呢?

其实sentinel官方文档就有说到解决方法,不过需要自己修改sentinel控制台的源码来实现。

这个还是比较复杂的,sentinel只帮我们实现了流控规则的demo,其他的还是要自己修改,这点不太人性化….

在这之前需要自己下载对应版本的sentinel控制台的源码,地址:github.com/alibaba/Sen…

流控规则源码修改

在源码的test目录下有sentinel提供的demo,分别有apollo、nacos、zookeeper,如下图:

这里我们是Nacos,因此只需要nacos包下面的demo。修改步骤如下:

1、去掉sentinel-datasource-nacos依赖的scop

这个sentinel-datasource-nacos依赖默认是<scope>test</scope>,因此我们需要去掉这个,如下:

1
2
3
4
5
xml复制代码<!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

如果你集成的zookeeper或者apollo,则把相应的依赖也要修改。

2、复制test环境下的nacos整个包到main下

将这个nacos包复制到com.alibaba.csp.sentinel.dashboard.rule这个包下,如下图:

3、将FlowControllerV2中的代码复制到FlowControllerV1中

com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2这个是sentinel提供的demo,只需要将其中的代码全部覆盖到com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1中。

4、修改FlowControllerV1中的代码

直接覆盖掉当然不行,还要做一些修改,如下:

  • 修改RequestMapping中的请求url为/v1/flow
  • 修改ruleProvider、rulePublisher的依赖,修改后的代码如下:
1
2
3
4
5
6
7
8
java复制代码	@Autowired
//使用nacos的依赖
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
//使用nacos的依赖
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

5、注意nacos的相关配置

com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil这个工具类中对应的是限流规则在nacos中的一些配置项,有groupId、dataId…对应的配置如下:

需要两边统一,可以自己修改。

com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfig这个类中有个方法如下图:

默认指定的nacos地址是本地的,这个需要修改。

6、完成

以上步骤已经改造了sentinel控制台的流控规则,打包启动控制台代码,命令如下:

1
java复制代码mvn clean install -DskipTests=true -pl sentinel-dashboard -am

启动后在控制台添加流控规则,可以看到也会同步推送到nacos,包括增删改。

其他规则修改也很简单,照葫芦画瓢,这里就不再详细说了,后面会单独出一篇文章详细说一下。

16、集群流控如何做?

首先一个简单的问题:**为什么需要集群流控?**单机流控不香吗?原因如下:

  • 对于微服务要想保证高可用,必须是集群,假设有100个集群,那么想要设置流控规则,是不是每个微服务都要设置一遍?维护成本太高了
  • 单体流控还会造成流量不均匀的问题,出现总流控阈值没有达到某些微服务已经被限流了,这个是非常糟糕的问题,因此实际生产中对于集群不推荐单体流控。

那么如何解决上述的问题呢?sentinel为我们提供了集群流控的规则。思想很简单就是提供一个专门的server来统计调用的总量,其他的实例都与server保持通信。

集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。

集群流控中共有两种身份:

  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

sentinel的集群限流有两种模式,分别如下:

  • 独立模式(Alone):即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。
  • 嵌入模式(Embedded):即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。

下面就以嵌入模式为例介绍一下如何配置。

就以sentinel-openfeign-provider9009这个模块作为演示,直接启动三个集群,端口分别为9009、9011、9013,如下图:

启动成功,在sentinel控制台将会看到有三个实例已经被监控了,如下图:

此时只需要在控制台指定一个服务为token server,其他的为token client,集群流控->新增token server,操作如下图:

选取一个作为服务端,另外两个作为客户端,此时就已经配置好了,如下图:

此时就可以添加集群流控规则了,可以在sentinel控制台直接添加,也可以通过Nacos直接配置,下图是通过Nacos配置的,如下图:

Nacos推送成功后将会在sentinel控制台看到这条流控规则的配置,如下图:

OK,至此集群流控到这儿就介绍完了,配置好之后可以自己试一下效果,陈某就不再演示了。

官方文档:github.com/alibaba/Sen…

17、网关限流如何配置?

这一块内容在后续介绍到网关的时候会详细讲,这里就不再细说了,有想要了解的可以看官方文档。

官方文档:github.com/alibaba/Sen…

18、整合openFeign如何实现熔断降级?

这个在上篇openFeign的文章中有详细介绍:openFeign夺命连环9问,这谁受得了?陈某这里就不再重复介绍了,有不知道的可以看上面这篇文章。

最后说一句

陈某码字不易,这篇文章写了两周,如果觉得不错,点赞、转发、在看、收藏支持一下,谢谢!

以上源码已经上传GitHub,需要的公众号【码猿技术专栏】回复关键词9528获取。

本文转载自: 掘金

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

看完这篇文章还不会顺序表和链表,请寄刀片给我

发表于 2021-10-13

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

一、线性表

1
2
scss复制代码线性表(linear list )是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

image.png

二、顺序表

💦 顺序表的概念和结构

1
复制代码顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

❗ 静态顺序表:使用定长数组存储 ❕

    缺点就是小了不够用,大了浪费

image.png

❗ 动态顺序表:使用动态开辟的数组存储 ❕

    可根据自己的需要调整大小

image.png

💦 顺序表各接口实现 (动图分析)

1
2
3
复制代码 静态顺序表只适用于确定知道需要存多少数据的场景。
静态顺序表的定长数组导致 N 固定了,空间开多了浪费,开少了不够用。
所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表以及增删查改等功能

这里新建三个文件:

    1️⃣ SeqList.h文件,用于函数声明

    2️⃣ SeqList.c文件,用于函数的定义

    3️⃣ Test.c文件,用于测试函数

⭕ 接口1:定义结构体SLT

1
2
3
4
5
6
7
C复制代码typedef int SQDataType;
typedef struct SeqList
{
SQDataType* a;//指向动态开辟的空间
int size;//有效数据个数
int capacity;//当前容量
}SLT;

⭕ 接口2:初始化结构体 (SeqListInit)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
C复制代码void SeqListInit(SLT* psl)
{
assert(psl);
psl->a = NULL;
psl->size = psl->capacity = 0;
}

📐 测试

在这里插入图片描述

⭕ 接口3:检查容量 (SeqListCheckCapcity)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码void SeqListCheckCapcity(SLT* psl)
{
//一般情况为了避免频繁插入数据而增容,或者一下开辟很大的空间,我们一般是每次增容2倍
assert(psl);
if (psl->size == psl->capacity)
{
//第一次是增容4*sizeof(SQDataType)
size_t newcapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
int* temp = (SQDataType*)realloc(psl->a, newcapacity * sizeof(SQDataType));
if (temp != NULL)
psl->a = temp;
else
return;
psl->capacity = newcapacity;
}
}

📐 测试

在这里插入图片描述

⭕ 接口4:指定位置插入数据 (SeqListInser)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码void SeqListInser(SLT* psl, size_t pos, SQDataType x)
{
assert(psl);
//因为pos是size_t类型的,所以不用断言它是否小于0了,不过还是建议加上
assert(pos > 0 && pos <= psl->size + 1);
//判断是否要增容
SeqListCheckCapcity(psl);
int start = pos - 1;
int end = psl->size - 1;
while (start <= end)
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[pos - 1] = x;
psl->size++;
}

📐 测试

在这里插入图片描述

⭕ 接口5:输出数据 (SeqListPrint)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
c复制代码void SeqListPrint(SLT* psl)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}

📐 测试

image.png

⭕ 接口6:尾插 (SeqListPushBack)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
c复制代码void SeqListPushBack(SLT* psl, SQDataType x)
{
assert(psl);
//根据分析尾部插入的话,传size+1吻合SeqListInser函数
SeqListInser(psl, psl->size + 1, x);
}

📐 测试

在这里插入图片描述

⭕ 接口7:头插 (SeqListPushFront)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
c复制代码void SeqListPushFront(SLT* psl, SQDataType x)
{
assert(psl);
SeqListInser(psl, 1, x);//根据分析头部插入的话,传1吻合SeqListInser函数
}

📐 测试

在这里插入图片描述

⭕ 接口8:指定位置删除数据 (SeqListErase)

请添加图片描述

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码void SeqListErase(SLT* psl, size_t pos)
{
assert(psl);
//因为pos是size_t类型的,所以不用断言它是否小于0了,不过还是建议加上
assert(pos > 0 && pos <= psl->size);
size_t start = pos - 1;
while (start < psl->size - 1)
{
psl->a[start] = psl->a[start + 1];
start++;
}
psl->size--;
}

📐 测试

在这里插入图片描述

⭕ 接口9:尾删 (SeqListPopBack)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
c复制代码void SeqListPopBack(SLT* psl)
{
assert(psl);
SeqListErase(psl, psl->size);
}

📐 测试

在这里插入图片描述

⭕ 接口10:头删 (SeqListPopFront)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
c复制代码void SeqListPopFront(SLT* psl)
{
assert(psl);
SeqListErase(psl, 1);
}

📐 测试

在这里插入图片描述

⭕ 接口11:查找 (SeqListFind)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码//找到返回下标,找不到返回-1
int SeqListFind(SLT* psl, SQDataType x)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}

📐 测试

在这里插入图片描述

⭕ 接口12:统计 (SeqListSize)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
c复制代码size_t SeqListSize(SLT* psl)
{
assert(psl);
return psl->size;
}

📐 测试

在这里插入图片描述

⭕ 接口13:修改 (SeqListAt)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
c复制代码size_t SeqListAt(SLT* psl, size_t pos, SQDataType x)
{
assert(psl);
assert(pos > 0 && pos <= psl->size);
psl->a[pos - 1] = x;
}

📐 测试

在这里插入图片描述

⭕ 接口14:销毁 (SeqListDestory)

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
c复制代码void SeqListDestory(SLT* psl)	
{
assert(psl);
if (psl->a)
{
free(psl->a);
psl->a = NULL;
}
psl->size = psl->capacity = 0;
}

📐 测试

在这里插入图片描述


📝 完整代码

     ❗ SeqList.h ❕

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
c复制代码#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SQDataType;
typedef struct SeqList
{
SQDataType* a;//指向动态开辟的空间
int size;//有效数据个数
int capacity;//当前容量
}SLT;

//初始化
void SeqListInit(SLT* psl);
//检查容量
void SeqListCheckCapcity(SLT* psl);
//尾部插入
void SeqListPushBack(SLT* psl, SQDataType x);
//头部插入
void SeqListPushFront(SLT* psl, SQDataType x);
//尾部删除
void SeqListPopBack(SLT* psl);
//头部删除
void SeqListPopFront(SLT* psl);
//显示
void SeqListPrint(SLT* psl);
//查找
int SeqListFind(SLT* psl, SQDataType x);
//从某个值的位置插入
void SeqListInser(SLT* psl, size_t pos, SQDataType x);
//从某个值的位置删除
void SeqListErase(SLT* psl, size_t pos);
//统计当前有多少数据 - 不要认为这样没必要,这是规范的操作
size_t SeqListSize(SLT* psl);
//修改
size_t SeqListAt(SLT* psl, size_t pos, SQDataType x);
//销毁
void SeqListDestory(SLT* psl);

     ❗ SeqList.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
c复制代码#include"SeqList.h"
//初始化
void SeqListInit(SLT* psl)
{
assert(psl);
psl->a = NULL;
psl->size = psl->capacity = 0;
}
//检查容量
void SeqListCheckCapcity(SLT* psl)
{
assert(psl);
//检查容量以决定要不要增容 - 2倍增容(一般情况为了避免频繁插入数据增容,或者一下开辟很大的空间,我们一般是每次增容2倍
if (psl->size == psl->capacity)
{
size_t newcapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
int* temp = (SQDataType*)realloc(psl->a, newcapacity * sizeof(SQDataType));
if (temp != NULL)
psl->a = temp;
else
return;
psl->capacity = newcapacity;
}
}
//尾部插入
void SeqListPushBack(SLT* psl, SQDataType x)
{
/*assert(psl);
SeqListCheckCapcity(psl);
psl->a[psl->size] = x;
psl->size++;*/
assert(psl);
SeqListInser(psl, psl->size + 1, x);//根据分析尾部插入的话,传size+1吻合SeqListInser函数
}
//头部插入
void SeqListPushFront(SLT* psl, SQDataType x)
{
//assert(psl);
//SeqListCheckCapcity(psl);
////挪动数据
//int end = psl->size - 1;
//while (end >= 0)
//{
// psl->a[end + 1] = psl->a[end];
// --end;
//}
//psl->a[0] = x;
//psl->size++;

assert(psl);
SeqListInser(psl, 1, x);//根据分析头部插入的话,传1吻合SeqListInser函数

}
//尾部删除
void SeqListPopBack(SLT* psl)
{
//assert(psl);
//assert(psl->size > 0);//如果没有数据就报错
//psl->size--;

assert(psl);
SeqListErase(psl, psl->size);
}
//头部删除
void SeqListPopFront(SLT* psl)
{
//assert(psl);
//assert(psl->size > 0);//如果没有数据就报错
//int start = 0;
//while (start < psl->size - 1)
//{
// psl->a[start] = psl->a[start + 1];
// start++;
//}
//psl->size--;

assert(psl);
SeqListErase(psl, 1);
}
//显示数据
void SeqListPrint(SLT* psl)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
//销毁
void SeqListDestory(SLT* psl)
{
assert(psl);
if (psl->a)
{
free(psl->a);
psl->a = NULL;
}
psl->size = psl->capacity = 0;
}
//查找 - 找到返回下标,找不到返回-1
int SeqListFind(SLT* psl, SQDataType x)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
//从某个位置插入,但是区间只能在第1个元素之前1个元素到最后1个元素之后1个元素
void SeqListInser(SLT* psl, size_t pos, SQDataType x)
{
assert(psl);
assert(pos > 0 && pos <= psl->size + 1);//因为pos是size_t类型的,所以不用断言它是否小于0了,不过还是建议加上
SeqListCheckCapcity(psl);
int start = pos - 1;
int end = psl->size - 1;
while (start <= end)
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[pos - 1] = x;
psl->size++;
}
//从某个位置的值删除
void SeqListErase(SLT* psl, size_t pos)
{
assert(psl);
assert(pos > 0 && pos <= psl->size);//因为pos是size_t类型的,所以不用断言它是否小于0了,不过还是建议加上
size_t start = pos - 1;
while (start < psl->size - 1)
{
psl->a[start] = psl->a[start + 1];
start++;
}
psl->size--;
}
//统计当前有多少数据 - 其实不要认为这样没必要,这是一些规范的基本操作
size_t SeqListSize(SLT* psl)
{
assert(psl);
return psl->size;
}
//修改
size_t SeqListAt(SLT* psl, size_t pos, SQDataType x)
{
assert(psl);
assert(pos > 0 && pos <= psl->size);
psl->a[pos - 1] = x;
}

     ❗ Test.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
c复制代码#include"SeqList.h"
void TestSeqList1()
{
SLT s;
//初始化
SeqListInit(&s);
//尾插
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPushBack(&s, 6);
SeqListPrint(&s);//显示数据
//头插
SeqListPushFront(&s, -1);
SeqListPushFront(&s, -2);
SeqListPushFront(&s, -3);
SeqListPrint(&s);//显示数据
//从某个值的位置插入
SeqListInser(&s, 1, -4);
SeqListPrint(&s);//显示数据
//尾删
SeqListPopBack(&s);
SeqListPrint(&s);//显示数据
//头删
SeqListPopFront(&s);
SeqListPrint(&s);//显示数据
////从某个值的位置删除
SeqListErase(&s, 8);
SeqListPrint(&s);//显示数据
//修改
SeqListAt(&s, 1, 3);
SeqListPrint(&s);//显示数据
//查找 - 找到返回下标,找不到返回-1
printf("%d\n", SeqListFind(&s, 2));
//统计
printf("%d\n", SeqListSize(&s));
//销毁
SeqListDestory(&s);
}

int main()
{
//当然也可以写一个菜单,这里就不写了。但这里有几个注意的点:建议先写函数模块,测试完后没有问题再写菜单是最合适的
TestSeqList1();
return 0;
}

💨 输出结果

在这里插入图片描述

⚠ 关于数组越界有几个注意的点

1
2
3
4
5
6
7
8
9
10
11
c复制代码#include<stdio.h>
int main02()
{
int a[10] = { 0 };
a[9] = 10;
//a[10] = 10;//err
//a[11] = 10;//err
a[12] = 10;//ok
a[20] = 10;//ok
return 0;
}

    ▶ 从以上就可以知道越界不一定报错,系统对越界的检查是抽查的形式,越界读一般是无法检查的;越界写也有可能无法检查,但是如果修改到标志位就会被检查出来

    ▶ 所谓标志位,就是如果越界写把标志位修改了,它就会报错,但是它不可能把后面的数据都设为标志位并检查,这也是为什么后面的数据无法检查的原因

❓ 为什么有了顺序表,还需要有链表这样的数据结构 ❔

    其实不难想象顺序表一定有一些缺陷,而链表恰恰可以优化缺陷

    1️⃣ 中间或头部的插入和删除,需要挪动数据,时间复杂度为O(N)

    2️⃣ 增容需要申请空间,拷贝数据,释放旧空间,会有不少的消耗

    3️⃣ 增容一般是以2倍的形式进行增长,势必会有一定的空间浪费。例如当前容量为100,增容后容量为200,我们只需要再继续插入5个数据,后面就没有数据了,那么将会浪费95个空间

三、链表

💦 链表的概念和结构

1
复制代码链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

🎗 这里就来实现一个简单的链表(有三个文件:SList.h、SList.c、Test.c)

在这里插入图片描述

     ❗ SList.h文件 ❕

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码#pragma once

#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;

void SListPrint(SLTNode* phead);

    ❗ SList.c文件 ❕

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码#include"SList.h"

void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}

    ❗ Test.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
25
26
27
28
29
c复制代码#include"SList.h"

void TestSList1()
{
SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
n1->data = 1;

SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
n2->data = 2;

SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
n3->data = 3;

SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
n4->data = 4;

n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;

SLTNode* plist = n1;
SListPrint(plist);
}
int main()
{
TestSList1();
return 0;
}

💨 结果

在这里插入图片描述

 
在这里插入图片描述

⚠ 注意

    1️⃣ 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续

    2️⃣ 现实中的结点一般都是从堆上申请出来的

    3️⃣ 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

假设在32位系统上,结点中值域为int类型,则一个节点的大小为8个字节,则也可能有下述链表:

在这里插入图片描述

💦 链表的分类

1
2
复制代码实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
注:本章只了解单链表

1️⃣ 单向或者双向

在这里插入图片描述

2️⃣ 带头或者不带头

在这里插入图片描述

3️⃣ 循环或者非循环

在这里插入图片描述

🎗 虽然有这么多的链表结构,但是实际中最常用的只有两种结构

1️⃣ 无头单向非循环链表

在这里插入图片描述

2️⃣ 带头双向循环链表

在这里插入图片描述

⚠ 注意:

    ▶ 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

    ▶ 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

💦 单链表各接口实现 (动图分析)

这里写了三个文件:

1️⃣ SList.h文件,用于函数声明

2️⃣ SList.c文件,用于函数的定义

3️⃣ Test.c文件,用于测试函数


⭕ 首先需要定义结构体SLTNode

1
2
3
4
5
6
c复制代码typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//存储整型数据
struct SListNode* next;//指向下一个节点的地址
}SLTNode;

并定义plist变量 -> Test.c

1
c复制代码SLTNode* plist = NULL;

⭕ 接口1:开辟空间 (BuySListNode)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码SLTNode* BuySListNode(SLTDataType x)
{
//每次调用开辟一个节点的空间
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
printf("malloc fail\n");
exit(-1);
}
node->data = x;
node->next = NULL;
//开辟成功,返回地址
return node;
}

⭕ 接口2:输出数据 (SListPrint)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码void SListPrint(SLTNode* phead)
{
//据情况分析,此处不需要断言,因为我们想在空链表时输出NULL
//assert(phead);

SLTNode* cur = phead;
//遍历
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}

⭕ 接口3:尾插数据 (SListPushBack),详解请看下图:

请添加图片描述

函数原型:

在这里插入图片描述

函数实现:

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
c复制代码void SListPushBack(SLTNode** pphead, SLTDataType x)
{
//据析,指针可能为空,但是指针的地址不可能为空,所以需要断言(pphead就是plist的地址),且这里不能断言*pphead,因为这里空链表是可以处理的
assert(pphead);
//特殊情况
//1、空链表
if (*pphead == NULL)
{
//调用BuySListNode去开辟新节点
SLTNode* newnode = BuySListNode(x);
*pphead = newnode;
}
//2、非空链表
else
{
SLTNode* tail = *pphead;
//找尾 - NULL
while(tail->next != NULL)
{
tail = tail->next;
}
//开辟节点
SLTNode* newnode = BuySListNode(x);
tail->next = newnode;
}
}

📐 测试

在这里插入图片描述

⭕ 接口4:头插数据 (SListPushFront),详解请看下图:

请添加图片描述

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
c复制代码void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//对于首插来说,没有特殊情况,以下代码适用于空链表和非穿链表
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}

📐 测试

在这里插入图片描述

⭕ 接口5:尾删数据 (SListPopBack),详解请看下图(2种方式):

方式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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
c复制代码void SListPopBack(SLTNode** pphead)
{
//据析,这里需要断言plist的地址是否传成NULL了;以及没有节点的情况
assert(pphead);
assert(*pphead);
//特殊情况
//一个节点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//多个节点的情况 - 找尾
//1.先指向第一个节点的位置
SLTNode* tail = *pphead;

//2.删尾(错误示范) - 这样删尾会造成野指针(因为每个节点都是一个局部节点,如果这样释放掉后,则前一个节点的next就是一个野指针)
/*while (tail->next != NULL)
{
tail = tail->next;
}
free(tail);
tail = NULL; */

//2.删尾(正确示范1)
/*while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;*/

//2.删尾(正确示范2) - 双指针
SLTNode* prev = NULL;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail->next);
prev->next = NULL;
}
}

📐 测试

在这里插入图片描述

⭕ 接口6:头删数据 (SListPopFront),详解请看下图

请添加图片描述

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码void SListPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
//以下代码能适用于一个节点和多个节点,如果只有一个节点的情况,先备份NULL,就将NULL赋值于*pphead
//备份第2个节点的地址
SLTNode* temp = (*pphead)->next;
//释放第一个节点
free(*pphead);
//再将拷贝的节点链接起来
(*pphead) = temp;
}

📐 测试

在这里插入图片描述、

⭕ 接口7:查找数据 (SListFind)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c复制代码SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
//据析,如果是空链表时,这里就无法查找,所以需要断言
assert(phead);
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;//返回x所在节点的地址
}
else
{
cur = cur->next;
}
}
return NULL;//找不到返回空
}

⭕ 接口8:指定的数之前插入数据 (SlistInser),不支持尾插,详解请看下图

请添加图片描述

因此可以看出单链表不适合在指定的数的前面插入的,因为它需要前面一个节点的地址

函数原型:

在这里插入图片描述

函数实现:

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
c复制代码void SlistInser(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//据析,这里需要调用SlistFind函数来配合使用,所以这里不需要判断是否为空链表,因为SlistFind函数内已经断言过了
assert(pphead);
assert(pos);
//特殊情况
//1、调用头插的接口
if (*pphead == pos)
{
SListPushFront(pphead, x);
}
//2、非头插入 - 不包含尾插
else
{
//找pos位置的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySListNode(x);
//注意这里它们前后链接时可以颠倒
newnode->next = pos;
prev->next = newnode;
}
}

📐 测试

在这里插入图片描述

⭕ 接口9:指定的数之后插入数据 (SlistInser),不支持头插,详解请看下图

请添加图片描述

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
c复制代码void SlistInserAfter(SLTNode* pos, SLTDataType x)
{
//据析,这里也不需要判断空链表的情况
assert(pos);
SLTNode* newnode = BuySListNode(x);
//注意,必须先把pos后面节点的地址交给newnode->next,再将newnode的地址交给pos->next;两者不能颠倒
newnode->next = pos->next;
pos->next = newnode;
}

📐 测试

在这里插入图片描述

⭕ 接口10:指定的数删除 (SlistErase),详解请看下图

请添加图片描述

因此可以看出单链表不适合在删除指定的数,因为它需要前面一个节点的地址

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码void SlistErase(SLTNode** pphead, SLTNode* pos)
{
//据析,这里也不需要判断空链表的情况
assert(pphead);

//1、调用头删
if (pos == *pphead)
{
SListPopFront(pphead);
}
//2、其余节点 - 包括尾删
else
{
//找pos位置的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}

📐 测试

在这里插入图片描述

⭕ 接口11:指定的数之后删除 (SlistEraseAfter),详解请看下图

请添加图片描述

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码void SlistEraseAfter(SLTNode* pos)
{
//据析如果那个数后面为NULL,就删除不了,需要断言
assert(pos);
assert(pos->next);

//拷贝一份
SLTNode* temp = pos->next->next;
free(pos->next);
pos->next = NULL;
pos->next = temp;
}

📐 测试

在这里插入图片描述

⭕ 接口12:统计 (SListSize)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码int SListSize(SLTNode* phead)
{
//这里不需要断言
int size = 0;
SLTNode* cur = phead;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}

📐 测试

在这里插入图片描述

⭕ 接口13:判空 (SListSize)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码bool SListEmpty(SLTNode* phead)
{
//判空,空是真,非空是假

//写法1
//return phead == NULL ? true : false;

//写法2
//if (phead == NULL)
//{
// return true;
//}
//else
//{
// return false;
//}

//写法3
return phead == NULL;
}

📐 测试

在这里插入图片描述

⭕ 接口14:销毁 (SListDestory)

函数原型:

在这里插入图片描述

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}

*pphead = NULL;
}

📐 测试

在这里插入图片描述


📝 完整代码

     ❗ SList.h ❕

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
c复制代码#pragma once

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//存储整型数据
struct SListNode* next;//指向下一个节点的地址
}SLTNode;

//只读的函数接口
void SListPrint(SLTNode* phead);
int SListSize(SLTNode* phead);
bool SListEmpty(SLTNode* phead);
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
SLTNode* BuySListNode(SLTDataType x);
void SListInserAfter(SLTNode* pos, SLTDataType x);
void SListEraseAfter(SLTNode* pos);
void SListDestory(SLTNode** pphead);

//读写的函数接口
void SListPushBack(SLTNode** pphead, SLTDataType x);
void SListPushFront(SLTNode** pphead, SLTDataType x);
void SListPopBack(SLTNode** pphead);
void SListPopFront(SLTNode** pphead);
void SListInser(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SListErase(SLTNode** pphead, SLTNode* pos);

    ❗ SList.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
c复制代码#include"SList.h"


void SListPrint(SLTNode* phead)
{
//据情况分析,此处不需要断言,因为我们想在空链表时输出NULL
//assert(phead);

SLTNode* cur = phead;
//遍历
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}

SLTNode* BuySListNode(SLTDataType x)
{
//每次调用开辟一个节点的空间
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
printf("malloc fail\n");
exit(-1);
}

node->data = x;
node->next = NULL;
//开辟成功,返回地址
return node;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
//据析,指针可能为空,但是指针的地址不可能为空,所以需要断言(pphead就是plist的地址),且这里不能断言*pphead,因为这里空链表是可以处理的
assert(pphead);
//特殊情况
//1、空链表
if (*pphead == NULL)
{
//调用BuySListNode去开辟新节点
SLTNode* newnode = BuySListNode(x);
*pphead = newnode;
}
//2、非空链表
else
{
SLTNode* tail = *pphead;
//找尾 - NULL
while(tail->next != NULL)
{
tail = tail->next;
}
//开辟节点
SLTNode* newnode = BuySListNode(x);
tail->next = newnode;
}
}

void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//对于首插来说,没有特殊情况,以下代码适用于空链表和非空链表
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}

void SListPopBack(SLTNode** pphead)
{
//据析,这里需要断言plist的地址是否传成NULL了;以及没有节点的情况
assert(pphead);
assert(*pphead);
//特殊情况
//一个节点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//多个节点的情况 - 找尾
//1.先指向第一个节点的位置
SLTNode* tail = *pphead;

//2.删尾(错误示范) - 这样删尾会造成野指针(因为每个节点都是一个局部节点,如果这样释放掉后,则前一个节点的next就是一个野指针)
/*while (tail->next != NULL)
{
tail = tail->next;
}
free(tail);
tail = NULL; */

//2.删尾(正确示范1)
/*while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;*/

//2.删尾(正确示范2) - 双指针
SLTNode* prev = NULL;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail->next);
prev->next = NULL;
}
}

void SListPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);

//以下代码能适用于一个节点和多个节点,如果只有一个节点的情况,先备份NULL,就将NULL赋值于*pphead
//备份第2个节点的地址
SLTNode* temp = (*pphead)->next;
//释放第一个节点
free(*pphead);
//再将拷贝的节点链接起来
(*pphead) = temp;
}

int SListSize(SLTNode* phead)
{
//这里不需要断言
int size = 0;
SLTNode* cur = phead;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}

bool SListEmpty(SLTNode* phead)
{
//判空,空是真,非空是假

//写法1
//return phead == NULL ? true : false;

//写法2
//if (phead == NULL)
//{
// return true;
//}
//else
//{
// return false;
//}

//写法3
return phead == NULL;
}

SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
//据析,如果是空链表时,这里就无法查找,所以需要断言
assert(phead);
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;//返回x所在节点的地址
}
else
{
cur = cur->next;
}
}
return NULL;//找不到返回空
}

void SListInser(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//据析,这里需要调用SlistFind函数来配合使用,所以这里不需要判断是否为空链表,因为SlistFind函数内已经断言过了
assert(pphead);
assert(pos);
//特殊情况
//1、调用头插的接口
if (*pphead == pos)
{
SListPushFront(pphead, x);
}
//2、非头插入 - 不包含尾插
else
{
//找pos位置的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySListNode(x);
//注意这里它们前后链接时可以颠倒
newnode->next = pos;
prev->next = newnode;
}
}

void SListInserAfter(SLTNode* pos, SLTDataType x)
{
//据析,这里也不需要判断空链表的情况
assert(pos);
SLTNode* newnode = BuySListNode(x);
//注意,必须先把pos后面节点的地址交给newnode->next,再将newnode的地址交给pos->next;两者不能颠倒
newnode->next = pos->next;
pos->next = newnode;
}

void SListErase(SLTNode** pphead, SLTNode* pos)
{
//据析,这里也不需要判断空链表的情况
assert(pphead);

//1、调用头删
if (pos == *pphead)
{
SListPopFront(pphead);
}
//2、其余节点 - 包括尾删
else
{
//找pos位置的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
void SListEraseAfter(SLTNode* pos)
{
//据析如果那个数后面为NULL,就删除不了,需要断言
assert(pos);
assert(pos->next);

//拷贝一份
SLTNode* temp = pos->next->next;
free(pos->next);
pos->next = NULL;
pos->next = temp;
}

void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}

*pphead = NULL;
}

    ❗ Test.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
c复制代码#include"SList.h"

void TestSList1()
{
SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
n1->data = 1;

SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
n2->data = 2;

SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
n3->data = 3;

SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
n4->data = 4;

n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;

SLTNode* plist = n1;
SListPrint(plist);
}

void TestSList2()
{
//定义plist变量
SLTNode* plist = NULL;
//尾插
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
//头插
SListPushFront(&plist, 0);
SListPushFront(&plist, -1);
SListPushFront(&plist, -2);
SListPrint(plist);
//尾删
SListPopBack(&plist);
SListPrint(plist);
//头删
SListPopFront(&plist);
SListPrint(plist);
//查找 - 查找空链表时,断言报错;查找失败返回NULL;查找成功返回x所在的节点的地址
SLTNode* pos = SListFind(plist, 0);
if (pos)
{
printf("找到了\n");
}
//指定的数之前插入数据 - 此函数实现不了尾插
pos = SListFind(plist, -1);
if (pos)//找到了才插入数据
{
SListInser(&plist, pos, -2);
}
SListPrint(plist);
//指定的数之后插入数据 - 此函数实现不了头插
pos = SListFind(plist, 3);
if (pos)
{
SListInserAfter(pos, 4);
}
SListPrint(plist);
//指定的数删除
pos = SListFind(plist, 4);
if (pos)
{
SListErase(&plist, pos);
}
SListPrint(plist);
//删除指定的数之后的数
pos = SListFind(plist, 2);
if (pos)
{
SListEraseAfter(pos);
}
SListPrint(plist);
//统计
printf("%d\n", SListSize(plist));
//判空,空是真,非空是假
printf("%d\n", SListEmpty(plist));
//销毁
SListDestory(&plist);
}

int main()
{
TestSList2();
return 0;
}

💨 结果

在这里插入图片描述

四、顺序表和链表的区别和联系

不同点 顺序表 链表
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问 支持 O(1) 不支持 O(N)
任意位置插入或删除元素 可能需要搬移元素,效率低 只需要修改指针指向
插入 动态顺序表,空间不够时需要扩容 没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁
缓存利用率、缓存命中率 高 低

💨 总结:

链表和顺序表没有谁更优,它们各有优缺点,相辅相成

备注:缓存利用率参考存储体系结构以及局部原理性

在这里插入图片描述

在这里插入图片描述

假设写了一个程序,实现分别对顺序表和链表上的每个数据+1,那么这个程序会编译成指令,然后CPU再执行
CPU运算的速度很快,那内存的速度跟不上,CPU一般就不会直接访问内存,而是把要访问的数据先加载到缓存体系,如果是 ≤ 8byte的数据 (寄存器一般是8byte),会直接到寄存器;而大的数据,会到三级缓存,CPU直接跟缓存交互。
CPU执行指令运算要访问内存,先要取0x10 (假设) 的数据,拿0x10去缓存中找,发现没有 (不命中) ,这时会把主存中这个地址开始的一段空间都读进来 (缓存)。
如果是整型数组,下一次访问0x14、0x18… 就会命中
如果是链表,先取第1个节点0x60,不命中;再取第2个0x90,不命中…。这样会造成一定的缓存污染

相关CPU缓存博客

本文转载自: 掘金

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

【算法练习02】5最长回文子串——滑动窗口

发表于 2021-10-13

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。


⭐本文收录于github-JavaExpert,技术专家修炼之路,包含Java完整学习路线及配套学习资料,欢迎star⭐

回文的意思是正着念和倒着念一样,如:上海自来水来自海上

——leetcode此题热评

在对联中就有回文的手法,“上海自来水来自海上”,大家有下联了吗

iShot2021-10-13 11.38.07.png

前言

哈喽,大家好,我是一条。

糊涂算法,难得糊涂

今天来一道中等题,看看自己功力几何?

Question

5. 最长回文子串

难度:中等

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

1
2
3
4
5
> arduino复制代码输入:s = "babad"
> 输出:"bab"
> 解释:"aba" 同样是符合题意的答案。
>
>

示例 2:

1
2
3
4
> ini复制代码输入:s = "cbbd"
> 输出:"bb"
>
>

示例 3:

1
2
3
4
> ini复制代码输入:s = "a"
> 输出:"a"
>
>

示例 4:

1
2
3
4
> ini复制代码输入:s = "ac"
> 输出:"a"
>
>

提示:

1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成

Solution

之前我们做过一道题是:最长不重复子串。当时使用的是滑动窗口法。

这道题增加了回文的约束,那么该怎么处理回文,可以在纸上写一个回文串观察一下,有什么特点呢?

从中心点向两端发散

对吧,所以我们要滑动这个中心点,并对每一个中心点,进行左右扩展,判断左右字符是否相等即可。

  • 把null和空字符串的特殊情况处理掉
  • 有一个可滑动且大小可变的窗口,窗口左端(start)不动,右端(end)向后移动
  • 由于字符串长度有奇数和偶数两种,所以我们需要同时计算从一个字符开始扩展和从两个字符之间开始扩展
  • 找出两种扩展中相对长的作为当前最大值
  • 新的start要从何处开始,如何扩展字符串只需要考虑的问题

还有一种马拉车算法更加快速,但有一定难度,面试一般不会出现,感兴趣的同学可以看一下

Code

所有leetcode代码已同步至github

欢迎star

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复制代码/**
* @author 一条coding
*/
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}

Result

复杂度分析

  • 时间复杂度:O(N)

image-20210802132533270

Last

独脚难行,孤掌难鸣,一个人的力量终究是有限的,一个人的旅途也注定是孤独的。当你定好计划,怀着满腔热血准备出发的时候,一定要找个伙伴,和唐僧西天取经一样,师徒四人团结一心才能通过九九八十一难。所以,请加入组队刷题。

本文转载自: 掘金

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

SQL注入 🤔

发表于 2021-10-13

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

什么是sql注入

SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编写时的疏忽,通过SQL语句,实现无账号登录,甚至篡改数据库。

SQL注入攻击的总体思路

  1:寻找到SQL注入的位置

  2:判断服务器类型和后台数据库类型

  3:针对不同的服务器和数据库特点进行SQL注入攻击

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码String sql = "select * from user_table where username=
' "+userName+" ' and password=' "+password+" '";

--当输入了上面的用户名和密码,上面的SQL语句变成:
SELECT * FROM user_table WHERE username=
'’or 1 = 1 -- and password='’

"""
--分析SQL语句:
--条件后面username=”or 1=1 用户名等于 ” 或1=1 那么这个条件一定会成功;

--然后后面加两个-,这意味着注释,它将后面的语句注释,让他们不起作用,这样语句永远都--能正确执行,用户轻易骗过系统,获取合法身份。
--这还是比较温柔的,如果是执行
SELECT * FROM user_table WHERE
username='' ;DROP DATABASE (DB Name) --' and password=''
--其后果可想而知…
"""

防御SQL注入方法

注意:但凡有SQL注入漏洞的程序,都是因为程序要接受来自客户端用户输入的变量或URL传递的参数,并且这个变量或参数是组成SQL语句的一部分,对于用户输入的内容或传递的参数,我们应该要时刻保持警惕,这是安全领域里的「外部数据不可信任」的原则,纵观Web安全领域的各种攻击方式,大多数都是因为开发者违反了这个原则而导致的,所以自然能想到的,就是从变量的检测、过滤、验证下手,确保变量是开发者所预想的。

1、检查变量数据类型和格式

  如果你的SQL语句是类似where id={$id}这种形式,数据库里所有的id都是数字,那么就应该在SQL被执行前,检查确保变量id是int类型;如果是接受邮箱,那就应该检查并严格确保变量一定是邮箱的格式,其他的类型比如日期、时间等也是一个道理。总结起来:只要是有固定格式的变量,在SQL语句执行前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很大程度上可以避免SQL注入攻击。

比如,我们前面接受username参数例子中,我们的产品设计应该是在用户注册的一开始,就有一个用户名的规则,比如5-20个字符,只能由大小写字母、数字以及一些安全的符号组成,不包含特殊字符。此时我们应该有一个check_username的函数来进行统一的检查。不过,仍然有很多例外情况并不能应用到这一准则,比如文章发布系统,评论系统等必须要允许用户提交任意字符串的场景,这就需要采用过滤等其他方案了。

2、过滤特殊符号

  对于无法确定固定格式的变量,一定要进行特殊符号过滤或转义处理。

3、绑定变量,使用预编译语句

  MySQL的mysqli驱动提供了预编译语句的支持,不同的程序语言,都分别有使用预编译语句的方法

  实际上,绑定变量使用预编译语句是预防SQL注入的最佳方式,使用预编译的SQL语句语义不会发生改变,在SQL语句中,变量用问号?表示,黑客即使本事再大,也无法改变SQL语句的结构

什么是sql预编译

1.1:预编译语句是什么

  通常我们的一条sql在db接收到最终执行完毕返回可以分为下面三个过程:

  • 词法和语义解析
  • 优化sql语句,制定执行计划
  • 执行并返回结果

  我们把这种普通语句称作Immediate Statements。  

  但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。

如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。

  所谓预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化,一般称这类语句叫Prepared Statements或者Parameterized Statements

预编译语句的优势在于归纳为:一次编译、多次运行,省去了解析优化等过程;此外预编译语句能防止sql注入。

当然就优化来说,很多时候最优的执行计划不是光靠知道sql语句的模板就能决定了,往往就是需要通过具体值来预估出成本代价。

为什么PrepareStatement可以防止sql注入

  原理是采用了预编译的方法,先将SQL语句中可被客户端控制的参数集进行编译,生成对应的临时变量集,再使用对应的设置方法,为临时变量集里面的元素进行赋值,赋值函数setString(),会对传入的参数进行强制类型检查和安全检查,所以就避免了SQL注入的产生。

1
2
csharp复制代码# Preparement样式为
select*from tablename where username=? and password=?
  • 该SQL语句会在得到用户的输入之前先用数据库进行预编译,这样的话不管用户输入什么用户名和密码的判断始终都是并的逻辑关系,防止了SQL注入
  • 简单总结,参数化能防注入的原因在于,语句是语句,参数是参数,参数的值并不是语句的一部分,数据库只按语句的语义跑,至于跑的时候是带一个普通背包还是一个怪物,不会影响行进路线,无非跑的快点与慢点的区别。

mybatis是如何防止SQL注入的(# 优于 $ )

1、首先看一下下面两个sql语句的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码
<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">

select id, username, password, role

from user

where username = #{username,jdbcType=VARCHAR}

and password = #{password,jdbcType=VARCHAR}



</select>
1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码
<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">

select id, username, password, role

from user

where username = ${username,jdbcType=VARCHAR}

and password = ${password,jdbcType=VARCHAR}

</select>

mybatis中的#和$的区别:

1、#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。

如:where username=#{username},如果传入的值是111,那么解析成sql时的值为where username=”111”, 如果传入的值是id,则解析成的sql为where username=”id”. 

2、将传入的数据直接显示生成在sql中。 如:whereusername=将传入的数据直接显示生成在sql中。
如:where username=将传入的数据直接显示生成在sql中。 如:whereusername={username},如果传入的值是111,那么解析成sql时的值为where username=111;

如果传入的值是;drop table user;,则解析成的sql为:select id, username, password, role from user where username=;drop table user;

3、#方式能够很大程度防止sql注入,方式无法防止Sql注入。   4、方式无法防止Sql注入。
  4、方式无法防止Sql注入。   4、方式一般用于传入数据库对象,例如传入表名.

5、一般能用#的就别用,若不得不使用“,若不得不使用“,若不得不使用“{xxx}”这样的参数,要手工地做好过滤工作,来防止sql注入攻击。

6、在MyBatis中,“xxx”这样格式的参数会直接参与SQL编译,从而不能避免注入攻击。但涉及到动态表名和列名时,只能使用“{xxx}”这样格式的参数会直接参与SQL编译,从而不能避免注入攻击。但涉及到动态表名和列名时,只能使用“xxx”这样格式的参数会直接参与SQL编译,从而不能避免注入攻击。但涉及到动态表名和列名时,只能使用“{xxx}”这样的参数格式。所以,这样的参数需要我们在代码中手工进行处理来防止注入。 【结论】在编写MyBatis的映射语句时,尽量采用“#{xxx}”这样的格式。若不得不使用“${xxx}”这样的参数,要手工地做好过滤工作,来防止SQL注入攻击。

mybatis是如何做到防止sql注入的

  MyBatis框架作为一款半自动化的持久层框架,其SQL语句都要我们自己手动编写,这个时候当然需要防止SQL注入。其实,MyBatis的SQL是一个具有“输入+输出”的功能,类似于函数的结构,参考上面的两个例子。其中,parameterType表示了输入的参数类型,resultType表示了输出的参数类型。回应上文,如果我们想防止SQL注入,理所当然地要在输入参数上下功夫。上面代码中使用#的即输入参数在SQL中拼接的部分,传入参数后,打印出执行的SQL语句,会看到SQL是这样的:

1
sql复制代码select id, username, password, role from user where username=? and password=?

  不管输入什么参数,打印出的SQL都是这样的。这是因为MyBatis启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。

  【底层实现原理】MyBatis是如何做到SQL预编译的呢?其实在框架底层,是JDBC中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译

本文转载自: 掘金

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

讲真,这几个完整的开源Java项目能让你的能力提高一大截

发表于 2021-10-13

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

hi ,大家好,我是三天打鱼,两天晒网的小六六,欢迎大家关注我的公众号:”六脉神剑的程序人生”,一起学习,一起进步

前言

今天有一个读者问了,一个很神奇的问题:

7579024b-f75c-49ee-8365-fc10e9893922.jpg

看到这个问题的我,真的一时不知道回答,本来就想回我没发出去的文字算了,大不了这个粉丝把我拉黑,我像是缺粉丝的人吗?

image.png

但是我一看名字呀,我擦,这是我铁粉呀,我总共就没几个粉丝,我又把回复给删了,好好的给这个小老弟好好总结一下,于是给他回复了下面的内容,先稳稳我这个宝贵的粉丝

1bcf6d3a-8a37-4f29-b23a-a7507db6c048.jpg

大家发现没有,我基本上是秒回复这个粉丝,而粉丝都是爱答不理的,卑微的小六六

image.png

GitHub

先说说github吧,我相信只要你半只脚踏入了这个行业,就不可能没有听过这个网站,毕竟是全球最大的同性交友社区嘛

image.png

小六六自己也在里面交了几十个好友了,

image.png

当然,除了交友,他里面的学习内容是真的乡,随便几个项目都可以让你的薪资翻倍,这边小六六给你们介绍几个Java Web相关的项目让你们看看是不是。学完 升职 ,加薪 稳稳的,如果撸完不加薪,你来找六哥

image.png

学之思开源考试系统

github 地址:github.com/mindskip/xz…

大家都知道之前小六六就是做在线教育这块,那这块的话无非就是教务教学教研系统,网校系统,题库考试系统。然后之前就是觉得这个非常的不错,非常值得去看看。

image.png

学之思开源考试系统是一款 java + vue 的前后端分离的考试系统。主要优点是开发、部署简单快捷、界面设计友好、代码结构清晰。支持web端和微信小程序,能覆盖到pc机和手机等设备。 支持多种部署方式:集成部署、前后端分离部署、docker部署。

推荐它的原因是,它是一个完整的项目,并不是一个脚手架,如果你能去了解里面的整理逻辑,你就可以完整的从0到1去熟悉在线教育这块的一个整理业务的闭环。包含了后台管理 PC端 H5 和小程序,简直无敌。

小程序端

image.png

后台

image.png

PC

image.png

微人事

github:github.com/lenve/vhr

微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发,项目加入常见的企业级应用所涉及到的技术点,例如 Redis、RabbitMQ 等。

看着像一个很简单的后台管理系统,但是呢,贵在功能比较全,而且技术栈也不复杂,对应应届生用来学习还是非常不错的

image.png

image.png

mall

github:github.com/macrozheng/…
mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现,采用Docker容器化部署。前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。

说实话,商城的开源项目有很多,而且我很讨厌它的体验网站的时候要关注公众号,但是呢?我后面为啥要推荐它呢?还不是因为看到

mall学习教程 github.com/macrozheng/… 它把每一个组件的使用,都有教学文档,这就很细了,应该是全网最细的人了,能做到这样 我都怀疑是不是一个团队再搞了

image.png

image.png

image.png

毕竟人家这门课程卖几k呢?白嫖党能说什么呢?真香

image.png

pig

gitee:gitee.com/log4j/pig

后台管理框架 是这样的我之前发现github是有pig的代码的,但是我今天去搜没搜到,奇怪了,所以我就只能给大家gitee的地址了

  • 基于 Spring Cloud 2020 、Spring Boot 2.5、 OAuth2 的 RBAC 权限管理系统
  • 基于数据驱动视图的理念封装 element-ui,即使没有 vue 的使用经验也能快速上手
  • 提供对常见容器化支持 Docker、Kubernetes、Rancher2 支持
  • 提供 lambda 、stream api 、webflux 的生产实践

image.png

说实话,这个并不是一个完整的项目,就是一个脚手架,但是呢,我为啥会推荐呢?哈哈 还不是因为六哥给他提过PR哈哈,飘了哈,其实真正的原因是,这个框架他也值得去推荐,你想想就那么几个后台,人家一直在更新,一直在打磨,所以呢是写的真的可以的,对于细节的把控是真的细。

对于看开源项目的一些心得吧

小六六虽然不是说经常去参与一些大型框架的开发,但是一些脚手架的开发多多少少还是接触过,也试着去看看我们Spring的源码,Nacos的源码等等,可能大部分都是走马观花吧,但是我把我知道的一些经验,和心得体会分享给大家,让大家不要再问出,我应该怎么看github上的代码这种问题了

首先要跑起来

92603948d9c5d9dd55831ad7146cfb5b.gif

看任何一个项目,除非他是一个库,如果是一个项目,那么第一点一定是跑起来,先不管它三七二十八,我先跑起来,那肯定又会有人问 六哥 我应该怎么跑起来呢?放心,作者肯定想到了,如果它连这个都做不好,那这个项目肯定火不起来的

一定要把它导入到你熟悉的idea工具中去读

f8708bd0e75137d3122dc909eb2fad34.jpg

这个也非常重要,很多朋友读源码,就直接是去github上读,我的天,你搞笑来的吗,兄弟!导入到自己的idea工具的好处是什么呢?就是你可以做全局的搜索,和去做一些代码的debug,如果你跑起来了,然后再去做debug一条条线去看,慢慢的源码就会变的很轻松。

多百度

image.png

一个成熟的开源框框,肯定有很多人对它有写过博客,这些博客,能帮助你从各方面去了解这个项目,虽然一篇博客对你的帮助有限,但是如果你看多了,那么你就会慢慢的形成自己对这个开源框架的一个理解体系。非常重要哦

找项目的单元测试代码

image.png

在源码中找项目的单元测试代码。然后,我们再将项目导入开发工具,直接运行项目中的单元测试。 这是可能是最快捷、有效的掌握、上手开源库的方法。

结束

好了,六脉神剑第二章,看了我给你分享的,快去卷起来了吧! 哈哈! 小六六的分享到这了,欢迎关注我,三天打鱼,两天晒网。

本文转载自: 掘金

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

【云计算】后起之秀Pulumi Vs 当代王者 Terraf

发表于 2021-10-13

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

一、前言

实现多云管理的基础设施即代码的工具包括Terraform、Pulumi等等,Terraform更为流行,使用更加广泛。在使用Terraform管理基础设施时,有一个最大的痛点:“配置语法太过简单,导致配置繁琐,需要额外地学习HasiCorp创造的表达式语言DSL-HCL”。作为后起之秀,也许使用Pulumi能帮助我们解决这个问题。

二、what is Pulumi

Pulumi 是一个满足基础设施即代码思想的自动管理平台,使用 Python/TypeScript/Go/Dotnet等众多常见的开发语言编写好声明式的资源配置,就能实现一键创建/修改/销毁各类云资源。其可通过下载的 CLI、运行时、库和托管服务协同工作,以提供一种可靠的方式来供应、更新和管理云基础架构。下面的代码演示使用Python语言编程创建云资源,是不是对于研发人员很友好,可读性很强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码import pulumi
import pulumi_aws as aws
​
size = 't2.micro'
ami = aws.get_ami(most_recent="true",
                owners=["137112412989"],
                filters=[{"name":"name","values":["amzn-ami-hvm-*"]}])
​
group = aws.ec2.SecurityGroup('webserver-secgrp',
  description='Enable HTTP access',
  ingress=[
      { 'protocol': 'tcp', 'from_port': 22, 'to_port': 22, 'cidr_blocks': ['0.0.0.0/0'] }
  ])
​
server = aws.ec2.Instance('webserver-www',
  instance_type=size,
  vpc_security_group_ids=[group.id], # reference security group from above
  ami=ami.id)
​
pulumi.export('publicIp', server.public_ip)
pulumi.export('publicHostName', server.public_dns)

三、Pulumi Vs Terraform

3.1 两者渊源

Pulumi 和 Terraform 有很多相似之处,都满足通过编写代码的形式在任何云上创建、部署和管理基础设施。任何云包括国外的AWS、Azure、GCP,当然也包括国内的云,包括AliYun、TencentCloud、UCloud等等。

同时两者在GitHub上都提供了开源的社区版本,开源协议满足商业上的使用行为。Pulumi是站在Terraform Provider 肩膀上发展而来的,它干了一件聪明的事情,也就是提供了 pulumi-tf-provider-boilerplate工具进行重新打包,这个工具充当了bridge的作用。

3.2 两者区别

根据 Pulumi 官方介绍,Pulumi 和 Terraform 两者在语言支持,状态管理,Provider支持,开源协议上存在着主要的区别。

【语言支持】 Pulumi 支持的语言包括:

  • Python
  • TypeScript
  • JavaScript
  • Go
  • C#
  • F#
  • VB
  • …

Terraform正式支持的语言只包括HCL,Json声明式语言,如果要把实验性的Terraform-cdk产品算上的话,那在未来它还会支持:

  • TypeScript
  • Python
  • Java
  • C#
  • Go

下面这张图很好的说明了两者目前在语言上支持上的差异。

【状态,可视化管理】

从根本上来说,状态管理是terraform 用户体验很是差的一环,因为没有提供相应的功能,客户只能本身在开源社区里找解决方案。而对于pulumi,因为其使用各开发语言实现,能够很是方便地在代码里面使用各种数据库进行数据操作。同时terraform对于状态的展示,以及部署过程中可视化展现,几乎未做这方面的支持,用户体验不佳。而对于pulumi,其目标明确地支持状态管理和可视化操作,切实地解决了用户的痛点。

除此上面提到的状态管理,pulumi还做到了对状态中的隐私内容进行真正的加密,这一点很关键。

【Provider 支持】

上文也说到,Pulumi通过转换工具支持了terraform 所有的Provider。同时Pulumi 深度支持K8s等云原生技术,支持了Terraform无法表达的高级部署场景。 这包括Prometheus-based canaries、 automatic Envoy sidecar injection等等。云计算浪潮之后云原生是一种趋势,可以清晰地认识到,Pulumi深耕于云原生,励志成为下一代DevOps工具的领头羊。

【开源协议】

Terraform 使用较弱的 Copyleft Mozilla Public License 2.0。 相反,Pulumi 开源项目使用宽松且商业友好的 Apache License 2.0。其开源内容包括核心 Pulumi 存储库、所有开源 Pulumi 资源提供程序(例如 Azure Native 提供程序)、转换实用程序(如 tf2pulumi)以及其他有用的项目。

【可重复使用的组件】

由于拥有真正的语言,我们可以构建更高层次的抽象。使用TypeScript等其它语言构建抽象,重复使用模块,相较于使用YAML的文内容,有了大幅度地减少。

【Lambda无服务功能】

可以用自身熟悉的语言使用lambda表达式来编写无服务器函数,而不需要一行YAML或其它声明式语言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
php复制代码import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
​
const example = new aws.lambda.FunctionEventInvokeConfig("example", {
  functionName: aws_lambda_alias.example.function_name,
  destinationConfig: {
      onFailure: {
          destination: aws_sqs_queue.example.arn,
      },
      onSuccess: {
          destination: aws_sns_topic.example.arn,
      },
  },
});

除了上面提到的内容,Pulumi在重命名重构上也支持的相当好,并且也提供了Tranformation 的机制,允许我们用代码定义对资源定义的修改逻辑。

四、总结

通过上面的分析比较,pulumi有这么多突出的优点,那是不是我们必然选择pulumi作为我们的Iac工具了。那确实不一定!首先我们分析比较两个关键词的Google热度指数(2021年10月10日),蓝色曲线代表terraform ,红色曲线代表Pulumi。

图片.png

结果很明显,terraform 作为一个2014年开始构建生态的产品,是比作为2017年初出茅庐的Pulumi有着更多的热度。Pulumi的生态比terraform 还差得远。而且terraform 也许会很快上市,到时候对Pulumi就是资本的降维打击了。所以选用Pulumi要慎重考虑。

图片.png

然后同时分析Github的关注度(2021年10月10日),总的来说terraform的关注度是远远高于Pulumi,这一点也侧面印证了Google热度指数。但对于这个结果也要辩证地看待,毕竟Pulumi作为一个后起之秀,就目前的关注度,社区活跃度已经是相当不错了。

同时从另一方面来说,Pulumi提供了开发人员熟悉的代码描述资源,表现力足够,但会不会难以阅读和调试?这样Dev + Ops真的会难以分开了。这样组织架构也会发生调整,研发和运维一体了。到底这个变化是好还是坏,就看团队怎么辩证的看待了。

本文转载自: 掘金

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

小知识系列 查询数据库数据的元信息 简介 使用ResultS

发表于 2021-10-13

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」。

简介

java中数据库的操作相信大家都不陌生,JDK提供了java.sql包来规范对数据库的各种操作。我们最常用的操作就是从数据库的ResultSet中获取数据,其实这个包中还有一个非常有用的类叫做ResultSetMetaData,可以通过这个类来获取查询数据的元信息,一起来看看吧。

使用ResultSet

java.sql.ResultSet是一个通用的规范,用来表示从数据库获取到的数据。

通常来说,我们通过connection来创建Statement,然后通过执行查询语句来得到:

1
2
3
ini复制代码Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT a, b FROM TABLE2");

resultSet提供了各种getter方法,来获取结果集中的各种数据。可以通过index,也可以通过column名称来获取。

当然,使用index它的效率会更高,index是从1开始的。如果是通过列名来获取数据,传入的列名是大小写不敏感的,如果结果数据中有多个匹配的类,则会返回最先匹配的那一列。

在get的过程中,JDBC driver会尝试将结果数据的数据库类型转换成为对应的java类型。

JDBC 2.0 API,ResultSet也可以进行更新和插入操作,可能我们很少这样做,一般都是先构造好数据后直接插入。

先看下更新操作:

1
2
3
scss复制代码         rs.absolute(5); // 将游标移动到第5行
rs.updateString("SITE", "www.flydean.com"); // 将SITE更新为www.flydean.com
rs.updateRow(); // 更新到数据库中

再看下插入操作:

1
2
3
4
5
6
7
scss复制代码  
rs.moveToInsertRow(); // 将游标移动到插入行
rs.updateString(1, "www.flydean.com"); // 将插入行的第一列更新为www.flydean.com
rs.updateInt(2,35); // 更新第二列为35
rs.updateBoolean(3, true); // 更新第三列为true
rs.insertRow();
rs.moveToCurrentRow();

使用ResultSetMetaData

有了ResultSet,我们可以通过它的getMetaData方法,来获取结果集的元数据。

什么是元数据呢?元数据又叫做Metadata,是用来描述数据属性的数据。

1
csharp复制代码   ResultSetMetaData getMetaData() throws SQLException;

举个具体的例子:

1
2
3
4
ini复制代码       ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM TABLE2");
ResultSetMetaData rsmd = rs.getMetaData();
int numberOfColumns = rsmd.getColumnCount();
boolean b = rsmd.isSearchable(1);

ResultSetMetaData提供了很多非常有用的元数据检测方法:

我们可以拿到列的名称、类型、字段长度、是否为空等很多有意义的数据。

这个元数据有什么用呢?

通过元数据,我们可以拿到数据库的描述文件,从而可以自动创建对应的数据库表格的映射关系,从而减少手动代码的输入,非常的方便。

用过MybatisPlus的朋友可能知道,它提供了一个AutoGenerator,可以自动生成mapper对象和对应的xml文件,非常好用,大家可以试一试。

总结

以上就是ResultSet和ResultSetMetaData的介绍,大家学会了吗?

本文已收录于 www.flydean.com/02-db-resul…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

详解Java异常(Exception)处理及常见异常

发表于 2021-10-13

本文正在参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

很多事件并非总是按照人们自己设计意愿顺利发展的,经常出现这样那样的异常情况。例如: 你计划周末郊游,计划从家里出发→到达目的→游泳→烧烤→回家。但天有不测风云,当你准备烧烤时候突然天降大雨,只能终止郊游提前回家。“天降大雨”是一种异常情况,你的计划应该考虑到这样的情况,并且应该有处理这种异常的预案。

计算机程序的编写也需要考虑处理这些异常情况。异常(exception)是在运行程序时产生的一种异常情况,已经成为了衡量一门语言是否成熟的标准之一。目前的主流编程语言java也提供了异常处理机制。

异常简介

Java中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。

在 Java 中一个异常的产生,主要有如下三种原因:

  1. Java 内部错误发生异常,Java 虚拟机产生的异常。
  2. 编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
  3. 通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。

Java 通过面向对象的方法来处理异常。在一个方法的运行过程中,如果发生了异常,则这个方法会产生代表该异常的一个对象,并把它交给运行时的系统,运行时系统寻找相应的代码来处理这一异常。

我们把生成异常对象,并把它提交给运行时系统的过程称为拋出(throw)异常。运行时系统在方法的调用栈中查找,直到找到能够处理该类型异常的对象,这一个过程称为捕获(catch)异常。

例 1

为了更好地理解什么是异常,下面来看一段非常简单的 Java 程序。下面的示例代码实现了允许用户输入 1~3 以内的整数,其他情况提示输入错误。

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复制代码package io.renren.config;

import java.util.Scanner;
/**
* Created by LiYangYong
*/
public class TestException {
public static void main(String[] args) {
System.out.println("请输入您的选择:(1~3 之间的整数)");
Scanner input = new Scanner(System.in);
int num = input.nextInt();
switch (num) {
case 1:
System.out.println("one");
break;
case 2:
System.out.println("two");
break;
case 3:
System.out.println("three");
break;
default:
System.out.println("error");
break;
}
}
}

正常情况下,用户会按照系统的提示输入 1~3 之间的数字。但是,如果用户没有按要求进行输入,例如输入了字母,则程序在运行时将会发生异常,运行结果如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码请输入您的选择:(1~3 之间的整数)
223sdf
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at io.renren.config.TestException.main(TestException.java:11)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Process finished with exit code 1

异常类型

为了能够及时有效地处理程序中的运行错误,Java 专门引入了异常类。在 Java 中所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。Throwable 类下有两个异常分支 Exception 和 Error,如图 1 所示。

)​
由图 2 可以知道,Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常。其中异常类 Exception 又分为运行时异常和非运行时异常,这两种异常有很大的区别,也称为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

  • Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
  • Error 定义了在通常环境下不希望被程序捕获的异常。一般指的是 JVM 错误,如堆栈溢出。

本节不讨论关于 Error 类型的异常处理,因为它们通常是灾难性的致命错误,不是程序可以控制的。接下来将讨论 Exception 类型的异常处理。

运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生。

非运行时异常是指 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException 等以及用户自定义的 Exception 异常(一般情况下不自定义检查异常)。

表 1 和表 2 分别列出了 java.lang 中定义的运行时异常和非运行时异常的类型及作用。

异常类型 说明
ArithmeticException 算术错误异常,如以零做除数
ArraylndexOutOfBoundException 数组索引越界
ArrayStoreException 向类型不兼容的数组元素赋值
ClassCastException 类型转换异常
IllegalArgumentException 使用非法实参调用方法
lIIegalStateException 环境或应用程序处于不正确的状态
lIIegalThreadStateException 被请求的操作与当前线程状态不兼容
IndexOutOfBoundsException 某种类型的索引越界
NullPointerException 尝试访问 null 对象成员,空指针异常
NegativeArraySizeException 再负数范围内创建的数组
NumberFormatException 数字转化格式异常,比如字符串到 float 型数字的转换无效
TypeNotPresentException 类型未找到
异常类型 说明
ClassNotFoundException 没有找到类
IllegalAccessException 访问类被拒绝
InstantiationException 试图创建抽象类或接口的对象
InterruptedException 线程被另一个线程中断
NoSuchFieldException 请求的域不存在
NoSuchMethodException 请求的方法不存在
ReflectiveOperationException 与反射有关的异常的超类

好了、今天就分享到这儿吧,我是小奥、下期见~~

打卡 文章 更新 76/ 100天

​

本文转载自: 掘金

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

1…493494495…956

开发者博客

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