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

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


  • 首页

  • 归档

  • 搜索

Redis 实现限流的三种方式

发表于 2021-11-23

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

面对越来越多的高并发场景,限流显示的尤为重要。当然,限流有许多种实现的方式,Redis具有很强大的功能,我用Redis实践了三种的实现方式,可以较为简单的实现其方式。Redis不仅仅是可以做限流,还可以做数据统计,附近的人等功能,这些可能会后续写到。

第一种:基于Redis的setnx的操作

我们在使用Redis的分布式锁的时候,大家都知道是依靠了setnx的指令,在CAS(Compare and swap)的操作的时候,同时给指定的key设置了过期实践(expire),我们在限流的主要目的就是为了在单位时间内,有且仅有N数量的请求能够访问我的代码程序。所以依靠setnx可以很轻松的做到这方面的功能。比如我们需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,当请求的setnx数量达到20时候即达到了限流效果。代码比较简单就不做展示了。当然这种做法的弊端是很多的,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题

第二种:基于Redis的数据结构zset

其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。

而我们如果用Redis的list数据结构可以轻而易举的实现该功能

我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码public Response limitFlow(){
        Long currentTime = new Date().getTime();
        System.out.println(currentTime);
        if(redisTemplate.hasKey("limit")) {
            Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime -  intervalTime, currentTime).size();        // intervalTime是限流的时间 
            System.out.println(count);
            if (count != null && count > 5) {
                return Response.ok("每分钟最多只能访问5次");
            }
        }
        redisTemplate.opsForZSet().add("limit",UUID.randomUUID().toString(),currentTime);
        return Response.ok("访问成功");
    }

通过上述代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大。实现方式相对也是比较简单的。

第三种:基于Redis的令牌桶算法

提到限流就不得不提到令牌桶算法了。

令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。

也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。

依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码,只是简单实现

依靠List的leftPop来获取令牌

1
2
3
4
5
6
7
8
kotlin复制代码// 输出令牌
public Response limitFlow2(Long id){
        Object result = redisTemplate.opsForList().leftPop("limit_list");
        if(result == null){
            return Response.ok("当前令牌桶中无令牌");
        }
        return Response.ok(articleDescription2);
    }

再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成

1
2
3
4
5
java复制代码// 10S的速率往令牌桶中添加UUID,只为保证唯一性
    @Scheduled(fixedDelay = 10_000,initialDelay = 0)
    public void setIntervalTimeTask(){
        redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
    }

综上,代码实现起始都不是很难,针对这些限流方式我们可以在AOP或者filter中加入以上代码,用来做到接口的限流,最终保护你的网站。Redis其实还有很多其他的用处,他的作用不仅仅是缓存,分布式锁的作用。他的数据结构也不仅仅是只有String,Hash,List,Set,Zset。有兴趣的可以后续了解下他的GeoHash算法;BitMap,HLL以及布隆过滤器数据(Redis4.0之后加入,可以用Docker直接安装redislabs/rebloom)结构。

本文转载自: 掘金

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

CTF赛题分析之 Flask 目录穿越 0x01 前言 0x

发表于 2021-11-23

作者:rootkit

0x01 前言

最近身边有萌新想打ctf,我作为一个曾经接触过一点点ctf的业余菜鸡,就索性做了一道Web题。这篇文章主要是面向想开始打ctf的萌新,所以很多地方可能都比较简单。如有错误之处,欢迎各位指正。

0x02 Flask 简介

Flask库是一个非常小型的Python Web开发框架。它有两个主要依赖:路由、调试和 Web 服务器网关接口(Web Server Gateway Interface,WSGI)子系统由 Werkzeug(werkzeug.pocoo.org/)提供;模板系统由 Jinja2(jinja.pocoo.org/)提供。Werkzeu… 和 Jinjia2 都是由 Flask 的核心开发者开发而成。这里我们要重点了解一下路由。

Flask需要知道对每个 URL 请求该运行哪些代码,所以保存了一个 URL 到Python 函数之间的映射关系。处理 URL 和函数之间关系的程序称为路由。在 Flask 程序中定义路由的最简便方式,是使用程序实例提供的 app.route 修饰器,把修饰的函数注册为路由。下面的例子说明了如何使用这个修饰器声明路由:

1
2
3
python复制代码@app.route('/')
def index():
return '<h1>Hello World!</h1>'

这个例子就是把 index() 函数注册为程序根地址的处理程序。如果部署程序的服务器域名为 www.example.com,在浏览器中访问 www.example.com 后,会触发服务器执行 index() 函数。这个函数的返回值称为响应,是客户端接收到的内容。如果客户端是 Web 浏览器,响应就是显示给用户查看的文档。

要启动服务器也很简单,程序实例用 run 方法启动 Flask 集成的开发 Web 服务器即可:

1
2
python复制代码if __name__ == '__main__':
app.run(debug=True)

其中,name==’main‘ 是 Python 的惯常用法,在这里确保直接执行这个脚本时才启动开发Web 服务器。服务器启动后,会进入轮询,等待并处理请求。轮询会一直运行,直到程序停止,比如按Ctrl-C 键。有一些选项参数可被 app.run() 函数接受用于设置 Web 服务器的操作模式。在开发过程中启用调试模式会带来一些便利,比如说激活调试器和重载程序。要想启用调试模式,我们可以把 debug 参数设为 True。要想让所有主机都可以访问,可以设置参数host=”0.0.0.0”。

0x03 详细分析

赛题如下:

1637464531764

该Web题下载下来以后源码如下:

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
python复制代码import os
import json
from shutil import copyfile
from flask import Flask,request,render_template,url_for,send_from_directory,make_response,redirect
from werkzeug.middleware.proxy_fix import ProxyFix
from flask import jsonify
from hashlib import md5
import signal
from http.server import HTTPServer, SimpleHTTPRequestHandler

os.environ['TEMP']='/dev/shm'

app = Flask("access")
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1 ,x_proto=1)


@app.route('/',methods=['POST', 'GET'])
def index():
if request.method == 'POST':
f=request.files['file']
os.system("rm -rf /dev/shm/zip/media/*")
path=os.path.join("/dev/shm/zip/media",'tmp.zip')
f.save(path)
os.system('timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/')
os.system('rm /dev/shm/zip/media/tmp.zip')
return redirect('/media/')
response = render_template('index.html')
return response

@app.route('/media/',methods=['GET'])
@app.route('/media',methods=['GET'])
@app.route('/media/<path>',methods=['GET'])
def media(path=""):
npath=os.path.join("/dev/shm/zip/media",path)
if not os.path.exists(npath):
return make_response("404",404)
if not os.path.isdir(npath):
f=open(npath,'rb')
response = make_response(f.read())
response.headers['Content-Type'] = 'application/octet-stream'
return response
else:
fn=os.listdir(npath)
fn=[".."]+fn
f=open("templates/template.html")
x=f.read()
f.close()
ret="<h1>文件列表:</h1><br><hr>"
for i in fn:
tpath=os.path.join('/media/',path,i)
ret+="<a href='"+tpath+"'>"+i+"</a><br>"
x=x.replace("HTMLTEXT",ret)
return x


os.system('mkdir /dev/shm/zip')
os.system('mkdir /dev/shm/zip/media')

app.run(host="0.0.0.0",port=8080,debug=False,threaded=True)

接下来就开始详细介绍。

这是一个用来实现文件在线解压的网站,首先看首页对应的index函数,当访问网站首页时,可以采用GET和POST请求两种方式,对应的响应函数为index函数。题目里还有两个html文档,一个用来当上传文件时,一个用来渲染页面,这里就不贴出来了。当在首页通过html上传文档时,会执行index函数,会执行如下命令来删除文件:

1
shell复制代码rm -rf /dev/shm/zip/media/*

然后拼接路径,并将上传的文件保存到这个路径:/dev/shm/zip/media/tmp.zip

之后执行命令解压:

1
shell复制代码timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/

解压后将压缩文件删除:

1
shell复制代码rm /dev/shm/zip/media/tmp.zip

第二个函数是media函数,有三种url请求都由这个函数来响应,其中有一个需要特别关注的,就是

1
python复制代码@app.route('/media/<path>',methods=['GET'])

这是动态路由,就是当请求的时候如果在media的后面再加上一个字符串,可以将这个字符串作为参数传递给path变量,执行media函数中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码def media(path=""):
npath=os.path.join("/dev/shm/zip/media",path)
if not os.path.exists(npath):
return make_response("404",404)
if not os.path.isdir(npath):
f=open(npath,'rb')
response = make_response(f.read())
response.headers['Content-Type'] = 'application/octet-stream'
return response
else:
fn=os.listdir(npath)
fn=[".."]+fn
f=open("templates/template.html")
x=f.read()
f.close()
ret="<h1>文件列表:</h1><br><hr>"
for i in fn:
tpath=os.path.join('/media/',path,i)
ret+="<a href='"+tpath+"'>"+i+"</a><br>"
x=x.replace("HTMLTEXT",ret)
return x

仔细观察可以看到,path作为参数会被拼接到npath变量中,然后当npath不存在时,返回404;当npath不是目录时,会使用open函数打开,并返回给请求者。题目中提到了flag位于根目录,而path我们又可以控制,那么我们只要能够通过一种方式,将npath变成/flag是不是就可以了呢?心里想,直接用相对路径../实现路径穿越不就妥了?这么简单的吗?

0x04 峰回路转

说干就干,我把path设置成../../../../flag,直接在浏览器请求example.com/media/../..… 结果发现不对,浏览器直接把我这些../全去掉了?url变成了example.com/media/flag,…? 第一反应是浏览器的问题,那就换个浏览器,结果发现也不行,然后想了想,那就用burp suite吧,结果还是不行。

最后,我在自己电脑上运行这个代码,开始各种调试,最后发现把斜杠换成两个反斜杠就可以了,能够实现路径穿越,读取上一层的文件内容,可是换成题目中就是不行。我仔细想了想应该是因为我本地是windows,而服务器是Linux,所以才不行的。眼看着时间不早了该睡觉了,还是没搞出来,于是喝了一杯茶,想了想,最后灵机一动,还是没想出来怎么搞。算了,睡觉吧。

到了第二天早上,我觉得这个必须得搞定。于是,我想到了Linux中的软链接!突然一下子就明白该怎么搞了!于是,打开我尘封已久的kali虚拟机,然后慢慢悠悠的敲下了如下命令:

成功创建了一个指向/flag的软链接,但是这个软链接怎么利用呢?这个网站既然是用来在线解压的,那我就把软链接打成一个压缩包传上去,它直接就会被解压到当前目录。然后我直接点击该文件,直接就下载下来一个车文件,打开即可看到flag内容为:

flag{NeV3r_trUSt_Any_C0mpresSeD_file}

最后终于搞定了,这一刻还是很开心的。作为一个业余ctf菜鸡选手,实在是不容易。还是要多做题,多开阔眼界,学习各种骚操作!

说明

关于合天网安实验室

合天网安实验室(www.hetianlab.com)-国内领先的实操型网络安全在线教育平台 真实环境,在线实操学网络安全 ; 实验内容涵盖:系统安全,软件安全,网络安全,Web安全,移动安全,CTF,取证分析,渗透测试,网安意识教育等。

相关实验练习

CTF实验室-从入门到实践-一站式CTF学习平台

本文转载自: 掘金

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

性能监控之Telegraf+InfluxDB+Grafana

发表于 2021-11-23

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

一、前言

本文主要介绍 Telegraf 在 window 上安装及监控入门

二、安装&部署

1.找到下载地址:portal.influxdata.com/downloads/

在这里插入图片描述

2.创建目录 C:\Program Files\Telegraf(如果安装在其他位置,请指定 -config 具有所需位置的参数)
3.解压软件包,将文件 telegraf.exe 和 telegraf.conf 文件放入 C:\Program Files\Telegraf

在这里插入图片描述

4.要将服务安装 到Windows 服务管理器中,以管理员身份在 CMD 中运行以下命令。如有必要,可以用双引号将文件
目录中的任何空格换行 "<file directory>":

1
bash复制代码C:\"Program Files"\Telegraf\telegraf.exe --service install

或者

1
bash复制代码C:\Program Files\Telegraf>telegraf.exe --service install

在这里插入图片描述

5.编辑 telegraf.conf 配置文件以满足要求。

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
bash复制代码###############################################################################
# INPUTS #
###############################################################################

# Windows Performance Counters plugin.
# These are the recommended method of monitoring system metrics on windows,
# as the regular system plugins (inputs.cpu, inputs.mem, etc.) rely on WMI,
# which utilize more system resources.
#
# See more configuration examples at:
# https://github.com/influxdata/telegraf/tree/master/plugins/inputs/win_perf_counters

[[inputs.win_perf_counters]]
[[inputs.win_perf_counters.object]]
# Processor usage, alternative to native, reports on a per core.
ObjectName = "Processor"
Instances = ["*"]
Counters = [
"% Idle Time",
"% Interrupt Time",
"% Privileged Time",
"% User Time",
"% Processor Time",
"% DPC Time",
]
Measurement = "win_cpu"
# Set to true to include _Total instance when querying for all (*).
IncludeTotal=true

[[inputs.win_perf_counters.object]]
# Disk times and queues
ObjectName = "LogicalDisk"
Instances = ["*"]
Counters = [
"% Idle Time",
"% Disk Time",
"% Disk Read Time",
"% Disk Write Time",
"Current Disk Queue Length",
"% Free Space",
"Free Megabytes",
]
Measurement = "win_disk"
# Set to true to include _Total instance when querying for all (*).
#IncludeTotal=false

[[inputs.win_perf_counters.object]]
ObjectName = "PhysicalDisk"
Instances = ["*"]
Counters = [
"Disk Read Bytes/sec",
"Disk Write Bytes/sec",
"Current Disk Queue Length",
"Disk Reads/sec",
"Disk Writes/sec",
"% Disk Time",
"% Disk Read Time",
"% Disk Write Time",
]
Measurement = "win_diskio"

[[inputs.win_perf_counters.object]]
ObjectName = "Network Interface"
Instances = ["*"]
Counters = [
"Bytes Received/sec",
"Bytes Sent/sec",
"Packets Received/sec",
"Packets Sent/sec",
"Packets Received Discarded",
"Packets Outbound Discarded",
"Packets Received Errors",
"Packets Outbound Errors",
]
Measurement = "win_net"

[[inputs.win_perf_counters.object]]
ObjectName = "System"
Counters = [
"Context Switches/sec",
"System Calls/sec",
"Processor Queue Length",
"System Up Time",
]
Instances = ["------"]
Measurement = "win_system"
# Set to true to include _Total instance when querying for all (*).
#IncludeTotal=false

[[inputs.win_perf_counters.object]]
# Example query where the Instance portion must be removed to get data back,
# such as from the Memory object.
ObjectName = "Memory"
Counters = [
"Available Bytes",
"Cache Faults/sec",
"Demand Zero Faults/sec",
"Page Faults/sec",
"Pages/sec",
"Transition Faults/sec",
"Pool Nonpaged Bytes",
"Pool Paged Bytes",
"Standby Cache Reserve Bytes",
"Standby Cache Normal Priority Bytes",
"Standby Cache Core Bytes",

]
# Use 6 x - to remove the Instance bit from the query.
Instances = ["------"]
Measurement = "win_mem"
# Set to true to include _Total instance when querying for all (*).
#IncludeTotal=false

[[inputs.win_perf_counters.object]]
# Example query where the Instance portion must be removed to get data back,
# such as from the Paging File object.
ObjectName = "Paging File"
Counters = [
"% Usage",
]
Instances = ["_Total"]
Measurement = "win_swap"

6.要验证它是否有效,请运行:

1
bash复制代码C:\"Program Files"\Telegraf\telegraf.exe --config C:\"Program Files"\Telegraf\telegraf.conf --test

或者

1
bash复制代码C:\Program Files\Telegraf>telegraf.exe --config telegraf.conf --test

要开始收集数据,请运行:

1
bash复制代码net start telegraf

7.其他操作
telegraf 可以通过 --service 管理自己的服务:

1
2
3
4
bash复制代码telegraf.exe --service install		#安装服务
telegraf.exe --service uninstall #删除服务
telegraf.exe --service start #启动服务
telegraf.exe --service stop #停止服务

三、集成Influxdb

找到 OUTPUTS 配置项

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
bash复制代码###############################################################################
# OUTPUTS #
###############################################################################

# Configuration for sending metrics to InfluxDB
[[outputs.influxdb]]
## The full HTTP or UDP URL for your InfluxDB instance.
##
## Multiple URLs can be specified for a single cluster, only ONE of the
## urls will be written to each interval.
# urls = ["unix:///var/run/influxdb.sock"]
# urls = ["udp://127.0.0.1:8089"]
urls = ["http://172.16.14.111:8086"]

## The target database for metrics; will be created as needed.
database = "bigscreen"

## If true, no CREATE DATABASE queries will be sent. Set to true when using
## Telegraf with a user without permissions to create databases or when the
## database already exists.
# skip_database_creation = false

## Name of existing retention policy to write to. Empty string writes to
## the default retention policy. Only takes effect when using HTTP.
# retention_policy = ""

## Write consistency (clusters only), can be: "any", "one", "quorum", "all".
## Only takes effect when using HTTP.
# write_consistency = "any"

## Timeout for HTTP messages.
timeout = "5s"

## HTTP Basic Auth
username = "telegraf"
password = "telegraf"

## HTTP User-Agent
# user_agent = "telegraf"

## UDP payload size is the maximum packet size to send.
# udp_payload = "512B"

## Optional TLS Config for use on HTTP connections.
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false

## HTTP Proxy override, if unset values the standard proxy environment
## variables are consulted to determine which proxy, if any, should be used.
# http_proxy = "http://corporate.proxy:3128"

## Additional HTTP headers
# http_headers = {"X-Special-Header" = "Special-Value"}

## HTTP Content-Encoding for write request body, can be set to "gzip" to
## compress body or "identity" to apply no encoding.
# content_encoding = "identity"

## When true, Telegraf will output unsigned integers as unsigned values,
## i.e.: "42u". You will need a version of InfluxDB supporting unsigned
## integer values. Enabling this option will result in field type errors if
## existing data has been written.
# influx_uint_support = false

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
25
bash复制代码[root@localhost tools]# sudo influx
Connected to http://localhost:8086 version 1.7.4
InfluxDB shell version: 1.7.4
Enter an InfluxQL query
> use bigscreen
Using database bigscreen
> SHOW MEASUREMENTS
name: measurements
name
----
bigscreen
nvidia_smi
win_cpu
win_disk
win_diskio
win_mem
win_net
win_perf_counters
win_swap
win_system
> SELECT * FROM "win_cpu" limit 1
name: win_cpu
time Percent_DPC_Time Percent_Idle_Time Percent_Interrupt_Time Percent_Privileged_Time Percent_Processor_Time Percent_User_Time host instance objectname
---- ---------------- ----------------- ---------------------- ----------------------- ---------------------- ----------------- ---- -------- ----------
1552012501000000000 0 81.72647857666016 0 4.6642279624938965 9.824928283691406 4.6642279624938965 DESKTOP-MLD0KTS 0 Processor

四、集成Grafana Dashboard

访问 grafana.com/dashboards?… 下载一个合适的 Dashboard 模版

在这里插入图片描述
Grafana 导入 Dashboard 模版

具体请参考 性能监控之Telegraf+InfluxDB+Grafana服务器实时监控

五、监控效果

Grafana Dashboard 最终效果如下:

在这里插入图片描述

相关资料:

  • github.com/zuozewei/bl…

本文转载自: 掘金

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

深入理解MySQL底层架构,看这一篇文章就够了!

发表于 2021-11-23

前面我们已经讲解了,我们的系统是如何与MySQL打交道的?,我们开发的系统与MySQL本身,都维护的有线程池,管理了所有连接。看下图回顾下:

)

从上图我们可以看到,我们通过数据库连接,把要执行的SQL语句发送给MySQL数据库进行增删改查就可以了。

然而MySQL数据库内部到底是怎么运转的呢?

1、网络连接让工作线程去具体执行

一般,网络服务器会分配一个线程或线程池去处理网络连接,把网络连接中读取出来的数据交给另外的线程或线程池处理。如下图所示:

)

当MySQL内部的工作线程从一个网络连接中读取一个SQL语句后,此时会如何处理这个SQL呢?

2、SQL接口,处理接收到的SQL语句

此时工作线程会把接收到的SQL语句交给一个叫SQL接口的组件执行。SQL接口(SQL interface),是一套执行SQL语句的接口,专门用于执行我们发送给MySQL的那些增删改查的SQL语句。

)

3、查询解析器:让MySQL读懂你的SQL

接下来SQL接口怎么处理SQL语句呢?MySQL必须理解你的SQL语法,才可以去执行,要理解SQL语法,就要靠查询解析器了。

查询解析器(parser),就是负责对SQL语句进行解析的。按照SQL语法,对我们按照SQL语法编写的SQL语句进行解析。比如对select name, age from user where id = 1这个语句。

1、我们要从user表里查询数据;

2、查询”id”字段值等于1的那行语句;

3、对查出来的那行数据提取name,age两个字段;

)

4、查询优化器:选择最优的查询路径

通过SQL解析器解析SQL语句,知道要干什么,那么怎么干性能最高呢?

比如,上面那个查询语句:select name, age from user where id = 1

可以有多种查询方式:

1、直接根据id定位到一行数据,然后从中获取name, age;

2、从表中把所有的id,name, age查出来,根据id过滤出来想要的数据;

上面是两种SQL查询方式(不代表MySQL的实现方式),两种查询方式都可以实现目标,哪种性能更好呢?

这就需要查询优化器告诉你。

查询优化器会告诉你,你应该按照一个什么样的步骤和顺序,去执行哪些操作,才能最快的获取结果。现在的图就变成这样了:

)

5、执行器;根据执行计划调用存储引擎

查询优化器选择了最优的查询路径,知道了按照一个什么样的顺序和步骤去执行这个SQL语句的计划,然后就需要执行器调用存储引擎的接口把SQL语句的逻辑给执行了。

比如,执行器可能会先调用存储引擎的一个接口,去获取user表中的第一行数据,然后判断这个数据的id字段是否等于我们期望的值,如果不是的话,就继续调用存储引擎的接口,获取user表的下一行数据。

基于上述思路,执行器,就会去根据我们的优化器生成的一套执行计划,不停的调用存储引擎的各种接口去完成SQL语句的执行。

)

5、调用存储引擎,真正执行SQL语句

执行器把执行计划交给最底层的存储引擎,就会真正的执行SQL语句了。

执行SQL语句,无非是增删改查数据,那么数据是存放在哪里呢?

数据要么是放在内存里,要么是放在磁盘上,所有存储引擎会按一定的步骤去查询内存缓存的数据,更新磁盘数据,等等。

MySQL的架构设计中,SQL接口,SQL解析器,查询优化器,都是通用的,但存储引擎是有很多种的。比如常见的innoDB,myisam。

互联网公司一般选用innoDB存储引擎。

)

本文转载自: 掘金

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

阿里云发布云原生加速器,携手生态企业拥抱数字时代 与成员企业

发表于 2021-11-23

简介:继去年推出云原生合作伙伴计划之后,阿里云正式发布云原生加速器,携手生态企业拥抱数字时代。

今天,千行百业都在拥抱云计算、拥抱云原生,进行数字化创新升级。作为国内最早实践云原生的企业,阿里巴巴在 2006 年就开始了在云原生领域的探索。为了更好地助力云原生领域生态伙伴的发展,继去年推出云原生合作伙伴计划之后,阿里云正式发布云原生加速器,携手生态企业拥抱数字时代。

与成员企业能力融合,价值共塑

阿里云此次发布云原生加速器,旨在挖掘云原生领域的优秀企业,通过阿里云强大的业务整合能力,助力伙伴在阿里云上形成百花齐放的云原生生态体系。

第一期云原生加速器将面向云原生应用、云原生 PaaS、云原生基础技术三大方向开启招募,由知名投资人、阿里云顶级技术专家、业务专家、阿里战投等组成专业的评委团队,最终将有 30 家企业入围首期云原生加速器。

招募方向包括,云原生应用:通用和垂直行业 SaaS、工具类 SaaS 等;云原生 PaaS:中间件、边缘计算、AI、物联网、低/无代码、行业 PaaS、iPaaS、低碳等;云原生基础技术:容器、微服务、可观测、DevOps、Serverless、容器存储、容器安全等。

在一年期加速成长计划中,将通过 2 次集结 +N 次业务对接,开放阿里云生态和业务资源,提供技术和产品支持,打造阿里云云原生加速器 “朋友圈” ,帮助伙伴快速成长。

在产品技术层面,阿里云云原生加速器将整合阿里云云原生技术产品线,与成员进行产品和解决方案共创,将为成员提供更高效、易用的平台和服务。入选成员还有机会成为阿里云云原生 MVP,助力成员获得更多资源与发展机会。

在业务层面,阿里云云原生加速器将协同不同行业及区域业务线,基于成员实际需求提供多元的合作模式与商业机会,共同拓展云原生应用场景。

在资本层面,阿里云云原生加速器将协同阿里体系投资机构为成员深度连接海量投资机构与专业的财务顾问,帮助成员快速、精准获得投资,全链条赋能企业融资。

在“内功”方面,阿里云云原生加速器将为成员解读行业政策与产业趋势,扩大品牌势能与市场规模,从战略规划与实施、人才组织与团队管理等全方面帮助成员完成能力升级。

庞大的创投导师阵容

阿里云云原生加速器将协同强大的顶级投资机构资源,邀请红杉资本中国基金合伙人邹家佳,GGV 合伙人吴陈尧,深创投董事总经理伊恩江,启明创投合伙人叶冠泰,云锋基金合伙人朱艺恺,银杏谷创始人陈向明作为创投导师,基于云原生领域相关细分赛道,从投资角度剖析产业发展趋势,为成员梳理未来的机遇与挑战。同时,IDG、嘉御资本、蓝驰创投、北极光、元璟资本、线性资本、戈壁创投、金沙江创投、明势资本、英诺天使、真格基金、湖畔山南、元禾原点、九合创投、盈动资本等创投机构的合伙人与董事也将受邀作为嘉宾,剖析行业价值,深度链接更多合作机会。

此外,阿里巴巴副总裁、阿里云智能基础产品事业部负责人蒋江伟,阿里巴巴副总裁、阿里云智能计算平台负责人贾扬清,阿里云智能副总裁、阿里巴巴达摩院秘书长刘湘雯,阿里巴巴研究员、阿里云智能云原生应用平台负责人丁宇等,也将与成员分享阿里云在生态构建与技术应用等方面的战略布局,与成员共商合作、共创行业未来。

原文链接

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

本文转载自: 掘金

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

我们的系统是如何与MySQL打交道的?

发表于 2021-11-23

目前行业里的大部分的开发人员,对MySQL的了解和掌握程度,大致停留在这样一个阶段,MySQL可以建库建表,可以建索引,可以增删改查。

所以很多伙伴眼里的数据库是这个样子的:

)

但实际在使用MySQL数据库的过程中,难免会遇到各种各样的问题,比如,SQL性能低,死锁。

然后解决这些问题的方式,就是上网搜索,按照别人的方法尝试着解决一下,可能最后解决了问题,但自己也没搞清为什么,只知道这样做就行了。

本文先给大家介绍下,我们平时开发的系统是如何与MySQL打交道的。

早些年我们去访问一个MySQL数据库,都需要一个MySQL驱动,然后通过jdbc的方式与数据库建立连接,然后执行各种SQL。

就像这样,现在maven里加入MySQL驱动,mysql-connector-java就是Java语言的MySQL驱动。

1
2
3
4
5
xml复制代码<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>

要访问数据库,MySQL驱动会先和数据库建立一个连接,就像这样;

)

与数据库建立连接后,系统才可以执行各种各样的SQL语句。

然而我们开发的系统,可能会与数据库建立很多的连接,不可能每次要执行SQL语句的时候都先建立一个连接吧,所以此时连接池就呼之欲出了。

)

比如,目前最常用的连接池是阿里的druid。

一个数据库可能会与很多系统建立很多连接,那么MySQL本身也必然要维护很多与系统之间的连接,所以MySQL内部也有一个连接池。于是我们开发的系统与MySQL交互就变成下图这样了:

)

MySQL中的连接池维护了与系统之间的多个数据库连接,而且你的系统与MySQL建立连接的时候,还会根据你传来的账号密码,进行验证。

本文转载自: 掘金

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

保证接口的幂等性的实现方案 一、概念 扩展 小结

发表于 2021-11-23

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

一、概念

幂等性最早是数学里面的一个概念,后来被用于计算机领域,用于表示任意多次请求均与一次请求执行的结果相同,也就是说对于一个接口而言,无论调用了多少次,最终得到的结果都是一样的。比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class IdempotentExample {
   // 变量
   private static int count = 0;
   /**
    * 非幂等性方法
    */
   public static void addCount() {
       count++;
  }
   /**
    * 幂等性方法
    */
   public static void printCount() {
       System.out.println(count);
  }
}

对于变量 count 来说,如果重复调用 addCount() 方法的话,会一直累加 count 的值,因为 addCount() 方法就是非幂等性方法;而 printCount() 方法只是用来打印控制台信息的。因此,它无论调用多少次结果都是一样的,所以它是幂等性方法。

幂等性的实现方案通常分为以下几类:

  • 前端拦截
  • 使用数据库实现幂等性
  • 使用 JVM 锁实现幂等性
  • 使用分布式锁实现幂等性
  1. 前端拦截

前端拦截是指通过 Web 站点的页面进行请求拦截,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态,避免用户重复点击。
核心的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
html复制代码<script>
   function subCli(){
       // 按钮设置为不可用
       document.getElementById("btn_sub").disabled="disabled";
       document.getElementById("dv1").innerText = "按钮被点击了~";
  }
</script>
<body style="margin-top: 100px;margin-left: 100px;">
   <input id="btn_sub" type="button"  value=" 提 交 "  onclick="subCli()">
   <div id="dv1" style="margin-top: 80px;"></div>
</body>

但前端拦截有一个致命的问题,如果是懂行的程序员或者黑客可以直接绕过页面的 JS 执行,直接模拟请求后端的接口,这样的话,我们前端的这些拦截就不能生效了。因此除了前端拦截一部分正常的误操作之外,后端的验证必不可少。

  1. 数据库实现

数据库实现幂等性的方案有三个:

  • 通过悲观锁来实现幂等性
  • 通过唯一索引来实现幂等性
  • 通过乐观锁来实现幂等性
  1. JVM 锁实现

JVM 锁实现是指通过 JVM 提供的内置锁如 Lock 或者是 synchronized 来实现幂等性。使用 JVM 锁来实现幂等性的一般流程为:首先通过 Lock 对代码段进行加锁操作,然后再判断此订单是否已经被处理过,如果未处理则开启事务执行订单处理,处理完成之后提交事务并释放锁,执行流程如下图所示:

image.png
JVM 锁存在的最大问题在于,它只能应用于单机环境,因为 Lock 本身为单机锁,所以它就不适应于分布式多机环境。

  1. 分布式锁实现

分布式锁实现幂等性的逻辑是,在每次执行方法之前先判断是否可以获取到分布式锁,如果可以,则表示为第一次执行方法,否则直接舍弃请求即可
需要注意的是分布式锁的 key 必须为业务的唯一标识,我们通常使用 Redis 或者 ZooKeeper 来实现分布式锁;如果使用 Redis 的话,则用 set 命令来创建和获取分布式锁,执行示例如下:

1
2
3
csharp复制代码
127.0.0.1:6379> set lock true ex 30 nx
OK # 创建锁成功

其中,ex 是用来设置超时时间的;而 nx 是 not exists 的意思,用来判断键是否存在。如果返回的结果为“OK”,则表示创建锁成功,否则表示重复请求,应该舍弃。

分析

幂等性问题看似“高大上”其实说白了就是如何避免重复请求提交的问题,出于安全性的考虑,我们必须在前后端都进行幂等性验证,同时幂等性问题在日常工作中又特别常见,解决的方案也有很多,但考虑到分布式系统情况,我们应该优先使用分布式锁来实现。

扩展

1.幂等性注意事项

幂等性的实现与判断需要消耗一定的资源,因此不应该给每个接口都增加幂等性判断,要根据实际的业务情况和操作类型来进行区分。例如,我们在进行查询操作和删除操作时就无须进行幂等性判断。查询操作查一次和查多次的结果都是一致的,因此我们无须进行幂等性判断。删除操作也是一样,删除一次和删除多次都是把相关的数据进行删除(这里的删除指的是条件删除而不是删除所有数据),因此也无须进行幂等性判断。

  1. 幂等性的关键步骤

实现幂等性的关键步骤分为以下三个:

  • 每个请求操作必须有唯一的 ID,而这个 ID 就是用来表示此业务是否被执行过的关键凭证,例如,订单支付业务的请求,就要使用订单的 ID 作为幂等性验证的 Key;
  • 每次执行业务之前必须要先判断此业务是否已经被处理过;
  • 第一次业务处理完成之后,要把此业务处理的状态进行保存,比如存储到 Redis 中或者是数据库中,这样才能防止业务被重复处理。
  1. 数据库实现幂等性

使用数据库实现幂等性的方法有三种:

  • 通过悲观锁来实现幂等性
  • 通过唯一索引来实现幂等性
  • 通过乐观锁来实现幂等性

1.悲观锁

使用悲观锁实现幂等性,一般是配合事务一起来实现,在没有使用悲观锁时,我们通常的执行过程是这样的,首先来判断数据的状态,执行 SQL 如下:

1
sql复制代码select status from table_name where id='xxx';

最后再进行状态的修改:

1
sql复制代码update table_name set status='xxx';

但这种情况因为是非原子操作,所以在高并发环境下可能会造成一个业务被执行两次的问题,当一个程序在执行中时,而另一个程序也开始状态判断的操作。因为第一个程序还未来得及更改状态,所以第二个程序也能执行成功,这就导致一个业务被执行了两次。

在这种情况下我们就可以使用悲观锁来避免问题的产生,实现 SQL 如下所示:

1
2
3
4
5
sql复制代码begin;  # 1.开始事务
select * from table_name where id='xxx' for update; # 2.查询状态
insert into table_name (id) values ('xxx'); # 3.添加操作
update table_name set status='xxx'; # 4.更改操作
commit; # 5.提交事务

在实现的过程中需要注意以下两个问题:

  • 如果使用的是 MySQL 数据库,必须选用 innodb 存储引擎,因为 innodb 支持事务;
  • id 字段一定要是主键或者是唯一索引,不然会锁表,影响其他业务执行。

2.唯一索引

我们可以创建一个唯一索引的表来实现幂等性,在每次执行业务之前,先执行插入操作,因为唯一字段就是业务的 ID,因此如果重复插入的话会触发唯一约束而导致插入失败。在这种情况下(插入失败)我们就可以判定它为重复提交的请求。

唯一索引表的创建示例如下:

1
2
3
4
5
6
sql复制代码CREATE TABLE `table_name` (
  `id` int NOT NULL AUTO_INCREMENT,
  `orderid` varchar(32) NOT NULL DEFAULT '' COMMENT '唯一id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_orderid` (`orderid`) COMMENT '唯一约束'
) ENGINE=InnoDB;

3.乐观锁

乐观锁是指在执行数据操作时(更改或添加)进行加锁操作,其他时间不加锁,因此相比于整个执行过程都加锁的悲观锁来说,它的执行效率要高很多。

乐观锁可以通过版本号来实现,例如以下 SQL:

1
sql复制代码update table_name set version=version+1 where version=0;

小结

幂等性不但可以保证程序正常执行,还可以杜绝一些垃圾数据以及无效请求对系统资源的消耗。上面介绍了幂等性的 6 种实现方式,包括前端拦截、数据库悲观锁实现、数据唯一索引实现、数据库乐观锁实现、JVM 锁实现,以及分布式锁的实现等方案,其中前端拦截无法防止懂行的人直接绕过前端进行模拟请求的操作。因此后端一定要实现幂等性处理,推荐的做法是使用分布式锁来实现,这样的解决方案更加通用。

本文转载自: 掘金

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

NGINX 入门到企业级应用实践-基础篇

发表于 2021-11-23

这是一系列免费的知识,有图文版和视频版,你现在看到的是图文版。

NGINX 系列课分为三篇,基础篇、进阶篇和企业实践篇,你现在正在阅读的是基础篇。

视频版发布在我自己的社区,喜欢看视频的朋友可前往社区,微信扫码登录或微信授权登录后即可播放。

写在前面

基础篇学习目的:了解 NGINX,并能够自己动手操作,能独立完成负载均衡配置,并绑定域名,实现通过域名访问后端服务。

NGINX系列课课学习目的:了解 NGINX、能够自己独立完成负载均衡配置、能够自己搭建高可用的企业级生产环境、对 NGINX 进行监控。

听过视频课的朋友已经能够独立完成负载均衡配置了,还提交了作业

image.png

我们这半年,将会输出非常多公开课,有图文版、有视频版。重要的是,这些都是免费的!!! 来了就能听。课程清单如下

image.png

其中绿色标注的是已经发布的内容,红色的是正在准备素材的内容。

NGINX 基础篇图文版

好的,正题来了,开始吧。

如果平时接触后端或者服务器比较少的朋友可能会问,NGINX 是什么?

关于它是什么,我们可以引用 NGINX 官网和百度百科中的介绍,NGINX 是一款高性能的 HTTP 服务器,同时也是一款反向代理服务器(NGINX 官网原文称为 reverse proxy server)。除了支持 HTTP 协议外,还支持邮件协议、TCP/UDP 等。

它能够做什么?

在我看来,它其实是一款网关。作用 1 请求转发,作用 2 限流,作用 3 鉴权,作用 4 负载均衡。上面提到的反向代理 reverse proxy server,可以归类到请求转发。

正向代理,反向代理???

太多道理我们就不讲了,可以阅读其他平台上关于这个问题的解读 zhuanlan.zhihu.com/p/25707362

这里我们简单总结一下,正向代理代理的对象是客户端,反向代理代理的对象是服务端。

做爬虫的朋友们,平时你们用的 IP 代理就是正向代理,爬虫程序通过代理,将请求转发给后端。而我们提到的 NGINX 反向代理则是将客户端的请求转发到后端。从上面讲到的文章里借几张图

image.png

image.png

用 NGINX 的公司多吗?

大部分公司都有用到 NGINX,大至 Google Meta(Facebook) Amazon Alibaba Tencent HUAWEI,小至全世界 70%+ (我猜的,实际比这更多)的互联网企业,社区使用的也是 NGINX

安装 NGINX

安装基于 Ubuntu20.04,云服务器。基础篇先通过快速安装,让我们可以操作起来,学一些基础,后续进阶篇会有编译安装。

打开 Terminal,执行 sudo apt install nginx -y,等待命令执行即完成安装。安装完成后它会自行启动,大家访问自己服务器的地址即可,例如我的服务器 IP 是 101.42.137.185,那我访问的是 http://101.42.137.185

image.png

如果页面显示的是 Welcome to nginx 字样,说明服务正常。如果没有,请检查安装时 Terminal 输出的错误信息,或者检查自己的防火墙、安全组策略等(如果不懂,或者怎么操作也不对,可以通过社区之前发布的 Linux 云服务器公开课学习)

NGINX 基本工作原理和模块关系简述

NGINX 有一个主进程和多个工作进程。主进程用于维护自身运转,例如读取配置、解析配置、维护工作进程、重新载入配置等等;工作进程才是具体响应请求的进程。

工作进程数可在配置文件中调整。

NGINX 由模块组成,这些模块受配置文件中的配置操控,也就是说配置文件决定了 NGINX 的工作方式。

这里还是引用其他文章,就不自己一一写明了。NGINX 原理和架构可以参考 zhuanlan.zhihu.com/p/133257100,实际上在初期我们需要关注的只有一个地方,也就是模块那部分,随便看看做个大体了解即可,不必深入。

NGINX 的信号

信号,这里指的是控制信号。信号是控制 NGINX 工作状态的模块,信号语法格式为

1
复制代码nginx -s signal

常用的信号有

1
2
3
4
arduino复制代码stop 快速关停
quit 正常关停
reload 重新载入配置
reopen 重新打开日志文件

NGINX 的正确关停,是 nginx -s quit 它可以让 NGINX 处理完已经开始的工作后再退出。

NGINX 配置说明

基于之前的社区公开课,我们可以在正式讲 NGINX 配置前先看看它的应用程序管理配置。通过 status 命令找到 NGINX 的 Server 配置文件

1
lua复制代码> systemctl status nginx

查看 NGINX 的 Server 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码[Unit]
Description=A high performance web server and a reverse proxy server
Documentation=man:nginx(8)
After=network.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid
TimeoutStopSec=5
KillMode=mixed

[Install]
WantedBy=multi-user.target

看到 ExecStart 选项,可以确定 NGINX 安装在 /usr/sbin/nginx,这个配置文件与我们之前的公开课 Linux 云服务器公开课讲到的知识遥相呼应,这里提一下。

查找默认的主配置文件

配置文件部分正式开始

NGINX 有主配置文件和辅助配置文件,主配置文件默认名称为 nginx.conf,默认存放在 /etc/nginx/nginx.conf。辅助配置文件的路径受主配置文件控制,具体路径通过主配置文件设置,辅助配置的文件名称和路径都可更改,文件名通常以 conf 结尾。

安装完成后如果你不知道主配置文件在哪,可以通过默认路径查找,或者通过 find 命令搜索。

1
2
arduino复制代码> sudo find / -name nginx.conf
/etc/nginx/nginx.conf

主配置文件基本结构和作用。使用 cat /etc/nginx/nginx.conf 可列出文件内容。如果你不懂,那么可以通过社区之前发布的的公开课 Linux 云服务器公开课学习具体的 Linux 文件查看指令。

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
ini复制代码user www-data;  # 用户
worker_processes auto; 工作进程数
pid /run/nginx.pid; # 进程文件
include /etc/nginx/modules-enabled/*.conf; # 插件模块配置

events {
      worker_connections 768; # 允许同时连接的连接数
      # multi_accept on;
}

http {
      sendfile on;
      tcp_nopush on;
      tcp_nodelay on;
      keepalive_timeout 65;
      types_hash_max_size 2048;
      ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
      ssl_prefer_server_ciphers on;

      access_log /var/log/nginx/access.log;
      error_log /var/log/nginx/error.log;
      include /etc/nginx/conf.d/*.conf; # 辅助配置文件路径
      include /etc/nginx/sites-enabled/*;
}


# 示例
#mail {
...
#}

这里列出的配置文件我做了适当的调整,删除了被注释的内容,保留了有效内容。重要项的含义都用中文以注释的形式标记在上面了。

看到配置,你肯定有点懵,这都是些啥啊。接下来我们来学习 NGINX 配置文件的基础语法。

NGINX 配置文件基础语法

NGINX 配置文件中的配置项成为指令,指令分为简单指令和块指令。简单的指令由指令名称和参数组成,以空格进行分隔并以英文分号结尾,例如

1
arduino复制代码worker_processes auto;

其中 worker_processes 是指令,这个指令的作用是设置工作进程数。auto 代表进程数的数量,可以是数字也可以是 auto(根据 CPU 数量按固定数学公式计算,一般是 CPU+1)。

块指令语法格式与简单指令相似,单以花括号包裹更多的简单指令,例如

1
2
3
4
5
markdown复制代码http {
server {
...
}
}

上下文/语境

上下文有些地方也称语境,如果块指令内包含其他指令,则这个块指令称为上下文。常见的上下文例如

1
2
3
4
vbscript复制代码events
http
server
location

有一个隐藏的上下文指令,main。它不需要显示声明,所有指令的最外层就是 main 的范围。main 作为其他上下文的参考,例如 events 和 http 必须在 main 范围中;server 必须在 http 中;location 必须在 server 中;以上限定是固定的,不可以随意放置,否则无法运行 NGINX 程序,但能够在日志里看到错误提示信息。

讲了这么多,你一定乏了,拿我们来动手吧!

使用 NGINX 为后端程序配置代理

一个简单的 WEB 服务,例如下面这个 flask 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码from flask import Flask
from flask_restful import Resource, Api
​
app = Flask(__name__)
api = Api(app)
​
​
class HelloWorld(Resource):
  def get(self):
      app.logger.info("receive a request, and response '穿甲兵技术社区'")
      return {'message': '穿甲兵技术社区', "address": "https://chuanjiabing.com"}
​
​
api.add_resource(HelloWorld, '/')
​
if __name__ == '__main__':
  app.run(debug=True, host="127.0.0.1", port=6789)

将内容写入到服务器上的某个文件,例如 /home/ubuntu/ke.py。

启动前记得安装相关的 Python 库 pip3 install flask-restful

在 Ubuntu 20.04 上默认带有新版 Python,环境什么的不用担心。运行这个 Web 后端服务 python3 /home/ubuntu/ke.py

完成后端的启动后,我们来配置 NGINX

通过前面查看主配置文件可知,辅助配置文件的目录为 /etc/nginx/conf.d,那么现在我们在辅助配置文件目录新增配置文件

1
2
3
4
5
6
7
8
9
10
ini复制代码> sudo vim /etc/nginx/conf.d/ke.conf

server {
  listen 8000;
  server_name localhost;

  location / {
      proxy_pass http://localhost:6789;
  }
}

检查配置文件是否正确

1
2
3
vbnet复制代码> sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

重新载入配置

1
markdown复制代码> sudo nginx -s reload

浏览器访问 http://ip:port 例如我的服务器 http://101.42.137.185:8000/

就可以看到后端的输出了

NGINX 日志文件

默认分为正常日志和内部错误日志,日志路径可在主配置文件中设置

1
2
lua复制代码/var/log/nginx/access.log
/var/log/nginx/error.log

查看正常日志

1
2
3
matlab复制代码> cat /var/log/nginx/access.log
117.183.211.177 - - [19/Nov/2021:20:18:46 +0800] "GET / HTTP/1.1" 200 107 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36"
117.183.211.177 - - [19/Nov/2021:20:18:48 +0800] "GET /favicon.ico HTTP/1.1" 404 209 "http://101.42.137.185:8000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36"

官方文档-日志格式 nginx.org/en/docs/htt…

默认的日志格式

1
2
3
dart复制代码log_format compression '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $bytes_sent '
                      '"$http_referer" "$http_user_agent" "$gzip_ratio"';

可在主配置文件中自行配置,具体配置项参考官方文档。

使用 NGINX 为前端程序配置代理

一个简单的 HTML 文档

1
2
xml复制代码> vim /home/ubuntu/index.html
<html><meta charset="utf-8"/><body><title>穿甲兵技术社区</title><div><p>穿甲兵技术社区<p><a>https://chuanjiabing.com</a></div><body></html>

无论是大型前端项目还是中小型前端项目,一般来讲都需要编译为 HTML 文档,然后使用类似 NGINX 这样的应用提供可访问的服务。

注意:一些 Vue/React 的服务有可能会做服务端渲染部署,但大部分还是编译为 HTML。这里的简单示例和那些前端工程项目在配置上并没有什么区别。作为示例,不用纠结,学习 NGINX 才是要紧的。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码> sudo vim /etc/nginx/conf.d/page.conf

server {
  listen 1235;
  server_name localhost;
  charset utf-8;
   
  location / {
      root /home/ubuntu/;
      index index.html index.htm;
  }
}

基于 NGINX 实现负载均衡

想象一下场景,例如现在你服务器上的后端服务主要是用于格式化时间,有很多爬虫程序需要调用它,而且还需要确保服务稳定可用。

场景延伸:假设你逆向了一个 JS 算法,现在所有爬虫都需要在发出请求前调用这个算法生成 sign 值,带着值去请求。如果你把 JS 代码放在 Python/Golang 这类代码里做本地调用执行,那么你改动算法时需要改动/重新部署所有爬虫程序,但做成 WEB 服务,只需要改动/重启这个 WEB 服务就可以了。

现在 1 个后端服务的情况下有 2 个明显缺点:

1、服务性能不够,请求太多会导致程序卡顿,响应速度慢,影响整体效率;

2、服务整体不稳定,一旦进程退出或者服务器死机,那服务将不可访问;

使用负载均衡的好处

1、启动多个后端服务,配置负载均衡,让请求按需(例如轮流)转发到它门那里进行处理,那么就能够承担更多的工作需求;

2、一个 NGINX 负载多个后端服务,当一个服务或者几个服务出现进程退出的情况,还有其他服务在工作;

NGINX 只需要引入 proxy_pass 指令和对应的 upstream 上下文即可实现负载均衡。一个简单的负载均衡配置例如

⚠️ 实验前,请先启动多个后端程序。可以将刚才的 Flask 代码复制到另一个文件(例如 /home/ubuntu/main.py,但记得需要改动里面的端口号,建议改为跟教程一样的 6799),如果你想在网页上看到负载的效果,可以在响应内容处用 6789/6799 来区分具体是那个后端程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码# /etc/nginx/conf.d/ke.conf 内容改为
upstream backend{
  server localhost:6789;
  server localhost:6799;
}

server {
  listen 8000;
  server_name localhost;

  location / {
      proxy_pass http://backend;
  }
}

保存后重新载入配置即可

1
markdown复制代码> sudo nginx -s reload

多次访问 http://101.42.137.185:8000/,可以看到页面上显示的内容是 6789 和 6799 这两个后端服务交替返回信息,这说明负载均衡配置成功。

image.png

image.png

域名解析与配置实践

打开云服务商控制台(后续以腾讯云为例,因为教程录制时使用的是腾讯云轻量级服务器),其他云服务商界面有差异,请大家见机行事。

在搜索框处搜索域名解析(腾讯的的是 DNSPOD)

image.png

进入找到要解析的域名(这里的前提是你自己已经买了域名,做好备案。如果没有,那看我操作也可以),点击解析

image.png

点击添加记录

在主机记录处输入子域名名称(例如 ke)、在记录值处输入服务器 IP 地址后选择保存即可,其他选项默认。

image.png

完成云服务器控制台的设置后,还不可以通过域名访问到我们服务器上的应用

前往服务器改动 NGINX 辅助配置文件,更改端口、绑定域名

1
2
3
4
ini复制代码> sudo vim/etc/nginx/conf.d/ke.conf
# 改动 server 上下文中的 listen 和 server_name
listen 80;
server_name ke.chuanjiabing.com;

记得重载配置

1
markdown复制代码> sudo nginx -s reload

然后就可以通过域名 http://ke.chuanjiabing.com/ 访问服务了

image.png

课后作业:在社区课程帖子下晒出后端程序的 NGINX 负载均衡配置截图,3 张。一张是配置截图;另外两张是浏览器访问时负载配置生效的截图。

后续进阶篇和企业实践篇的课程大纲如下,后续课程的学习目的:能够在工作中很好的应用 NGINX,完成企业级生产环境部署和监控告警

NGINX 进阶篇

NGINX 负载均衡策略理论

编译安装 NGINX

基于 NGINX 实现权限验证

基于 NGINX 实现访问限流

基于 NGINX 的简单反爬虫

基于 NGINX 实现不停机更新

NGINX 企业级实践篇

NGINX 的 HTTPS 配置实践

NGINX 插件安装

NGINX 数据监控实战

NGINX 生产环境高可用部署实践

本文转载自: 掘金

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

Redis分布式锁

发表于 2021-11-23

1.分布式锁简介

在同一个单体应用内部,我们往往使用Synchroized或者Lock的方式来解决多线程之间的安全问题,但是在分布式的架构下,在多个应用之间,就需要一种比较高级的锁机制来处理不同应用之间的线程安全问题。解决方案就是分布式锁。

  1. Redis分布式锁

1.原理

Redis分布式锁机制,主要是依靠两个命令来完成。setnx和expire

  • setnx:当key不存在时,将key设置为value,存在则不做任何操作,并且返回0setnx key value
  • expire:设置key的过期时间 expire key time

步骤: 1. 当key不存在时去创建key,并设置value和对应的过期时间,返回1。成功加锁

  1. 如果key存在,则会返回0,此时抢锁失败。

  2. 当持有锁的线程释放锁时,手动删除key,或者过期时间到了,自动删除key->释放锁

问题:

1.如果加锁成功了,但是设置过期时间失败,但是此时系统挂掉了,那么这个锁就永远不会被释放,此时其他线程就会一直拿不到这个锁了。

解决:

1.setnxd的同时,也设置过期时间,让一起执行。set key value [过期时间类型EX Second| PX millisenconds] [NX|XX]

其中EX,PX,NX,XX的描述信息为:

image.png 2.使用Lua脚本进行处理,命令如下

EVAL script numkeys key [key...] arg [arg...]

  • script: 时一段Lua5.1脚本程序,它会被运行在Redis服务器上下文中
  • numkeys:用户指定键名参数的个数

在Lua脚本中,可以使用redis.call()命令来执行Redis命令。例如

eval "return redis.call('set', KEY[1], ARGV[1])" 1 foo bar

这段脚本可以实现将foo的值设置为bar

  • 1: 代表单数的个数,为一个
  • foo:key[1]
  • bar: ARGV[1]

2.实现

  • 加锁 加锁就是调用set key PX NX命令设置一个key值
  • 解锁 解锁就是删除指定key,即释放了锁
  • 注意: 要保证操作的原子性,如果把判断锁和释放锁分开处理的话,可能会出现误解锁的情况,所以最好保持执行的原子性,即使用Lua脚本把判断锁和是加锁,释放锁都放在一起

3.锁过期问题

如果某个业务操作规定时10s,锁设置的时20s,但是由于操作过于复杂,导致锁的执行时间超过了20s,拿此时业务会在无锁的情况下执行,此时就会发生数据紊乱的情况。 解决:

  1. 乐观锁方式,增加版本号 通过根据锁的版本号来判断,当前线程持有的锁是否还存在,如果对比后版本号不一致,则直接拒绝处理业务。但是这种方式会入侵代码。
  2. 通过watch dog自动延期机制 当客户端加锁成功了,就会启动一个后台守护线程,该线程每10s会检查一下,如果客户端还持有锁,就会不断的延长锁key的生存时间。注:watchDog只在未指定显示的加锁时间才有效,即默认设置key过期时间的情况下。
  1. Redisson分布式锁

1.简介

Redisson是基于Netty的Redis客户端。不但能操作原生的Redis数据结构,还为使用者提供了一系列具有分布式特性的常用工具类,实现了分布式锁。

  1. Redisson分布式锁和JUC的Lock方法相似。RLock接口继承了Lock接口。
  2. 所得结构是Hash -key:锁的名字 -字段:UUID+threadId -值:代表的重入次数 !
  3. 锁的重入 当锁重入一次,重入值就会+1,并且过期时间也会更新

2.加锁原理

  1. 判断有没有“DISLOCK”,如果没有,设置UUID:1=1,并且设置它的过期时间
  2. 如果key和字段都存在,进行锁重入,执行命令incrby UUID:1 1 ,结果:DISLOCK:{UUID:1 2}
  3. 客户端2进入,判断有没有KEY,没有字段,返回过期时间,客户端2自旋等待

3.释放锁

1.判断KEY是否存在,如果不存在返回nil,如果存在,使用hincrby-1 把重入次数减1 2.减完后,counter>0值依然大于0,则返回0 3.剪完后,counter<=0,则删除key,并使用publish广播锁释放消息

本文转载自: 掘金

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

Eureka服务注册

发表于 2021-11-23

Eureka服务注册(已停更)

详情介绍就不多说,网上文档官网文档介绍很清楚,这里直接上代码。

创建maven父工程

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
xml复制代码<!--    重点修改打包方式 pom-->
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>8.0.26</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>

<!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version -->
<dependencyManagement>
<dependencies>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>

什么不是服务注册?

  • 在父工程中添加两个子工程分别是支付模块cloud-provider-payment8001和客户模块cloud-consumer-order80
  • 支付模块配置
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
xml复制代码<artifactId>cloud-provider-payment8001</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

配置yml

1
2
3
4
5
6
7
8
9
10
yml复制代码server:
port: 8001

spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/cloud?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root

这里新建一个简单数据库就可,一个id,一个name,并配置mapper

1
2
3
4
5
6
7
8
9
java复制代码 @GetMapping("/payment/get/{id}")
public Result getPayment(@PathVariable("id") Long id){
Payment Result = paymentService.getPaymentById(id);
log.info("******查找结果"+Result+Result);
if(Result != null){
return new Result(200,"查找成功);
}
return new Result(444,"查找失败");
}

统一结果返回

1
2
3
4
5
6
7
8
9
java复制代码public class Result<T> {
private Integer code;
private String message;
private T data;

public Result (Integer code,String message){
this(code,message,null);
}
}
  • 客户模块配置
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
xml复制代码artifactId>cloud-consumer-order80</artifactId>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1
2
yml复制代码server:
port: 80

配置RestTemplate,后面会详细解释

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
public class ApplicationContextConfig
{
@Bean
public RestTemplate getRestTemplate()
{
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));//乱码
return restTemplate;
}
}

Controller,获得支付模块接口并调用

1
2
3
4
5
6
7
java复制代码 private static final String   PAYMENT_URL = "http://localhost:8001";

@GetMapping("/consumer/get/{id}")
public String getPaymentById(@PathVariable("id") Long id){
String result = restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id, String.class);
return result;
}
  • 测试

开启服务测试

eureka1.jpg

成功调用

Eureka服务注册

  • 新建服务注册中心模块cloud-eureka-server7001
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
xml复制代码 <artifactId>cloud-eureka-server7001</artifactId>

<dependencies>
<!--eureka-server 这里一定是server表示注册中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--boot web actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般通用配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>

yml配置

1
2
3
4
5
6
7
8
9
10
11
12
yml复制代码server:
port: 7001

eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#单机
defaultZone: http://eureka7001.com:7001/eureka/

启动类

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableEurekaServer //开启服务中心
public class EurekaServer7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaServer7001.class, args);
}
}
  • 配置服务端与客服端

pom都新增

1
2
3
4
5
xml复制代码  <!--     这里是 client  -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

order80配置yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yml复制代码spring:
application:
name: cloud-order-service

eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#单机
defaultZone: http://localhost:7001/eureka
instance:
instance-id: order80
# #访问路径可以显示IP地址
prefer-ip-address: true

启动类

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableEurekaClient //开启注册
public class MainOrder80 {
public static void main(String[] args) {
SpringApplication.run(MainOrder80.class,args);
}
}

payment8001配置是一样的只需要改一下名字

  • 测试

启动7001-》8001-》80

eureka2.jpg

浏览器输入注册中心地址http://localhost:7001/

查看结果看到相应配置以及被注册进来的消费者生产者

eureka3.jpg
这时候测试80端口接口还是行得通的。

  • 集群

集群多个注册中心和多个服务业务,那么久多写出一个eureka7002当做集群控制中心,这里如果是在本地电脑操作,就要修改C:\Windows\System32\drivers\etc中hosts文件添加

1
2
txt复制代码127.0.0.1       eureka7002.com
127.0.0.1 eureka7001.com

eureka7002配置和eureka7001差不多也可以直接复制修改名字,重点在于这两个注册中心也要互相注册,将指向本身的url指向要集群的url即可。

1
2
3
yml复制代码    service-url:
#集群指向其它eureka
defaultZone: http://eureka7002.com:7002/eureka/

多个支付模块集群配置几乎和payment8001几乎一样,注意点是application-name

cloud-payment-service ,还有一点就是现在有两个注册中心,需要同时注册进入两个注册中心用逗号隔开就好

1
2
3
4
5
yml复制代码   service-url:
#单机版
# defaultZone: http://localhost:7001/eureka
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  • 测试

eureka7001.jpg

eureka7002.jpg

这时调用80端口查询接口就有不同的效果。更改一下8001,8002Controller代码

1
2
3
4
5
6
7
8
9
10
11
java复制代码 @Value("${server.port}")
private String port;
@GetMapping("/payment/get/{id}")
public Result getPayment(@PathVariable("id") Long id){
Payment Result = paymentService.getPaymentById(id);
log.info("******查找结果"+Result+Result+"port"+port);
if(Result != null){
return new Result(200,"查找成功"+"port"+port,Result);
}
return new Result(444,"查找失败"+"port"+port);
}

更改80端口代码,本来80接口是写死查询8001。我们这里要改为注册中心暴露的端口CLOUD-PAYMENT-SERVICE也就是application-name。

1
2
java复制代码//    private static final String   PAYMENT_URL = "http://localhost:8001";
private static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

修改配置类加入@LoadBalanced 赋予RestTemplate负载均衡的能力

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}
}

测试结果,看到结果我们会发现多发送请求就会访问不同的端口。说明我们已经成功了。

eureka80.jpg

eureka802.jpg

总结

  1. 这点对点的遍历中理解很简单,就模块之间的方法调用。但是如果服务很多用户量急剧增加,量变引起质变,怎么去判断有多少提供的服务,有多少客户?

eureka4.jpg

  1. 那就用到eureka服务注册,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。 哪还有一个问题就是注册中心程序崩了怎么办或者服务提供程序崩了怎么办?

eureka5.jpg

  1. 这时就引出的集群概念,试想你的注册中心只有一个only one, 它出故障了那就完犊子谁也不认识谁老死不相往来,会导致整个为服务环境不可用,所以搭建Eureka注册中心集群 ,实现负载均衡+故障容错,而服务提供接口也是这个道理。
  2. 小知识点
  • Eureka自我保护功能eureka.server.enable-self-preservation=true默认开启。

这个功能就是在服务可能停止运行之后不会直接将服务删除,而是默认应该是90秒删除,排除假死的可能。这种情况就相当于人停止心跳大脑不会直接死亡还有抢救的机会,假死现象不能放弃每一个病人。

1
2
yml复制代码eviction-interval-timer-in-ms: 2000  //设置假死时间
eureka.server.enable-self-preservation=true//开启关闭
  • 发现payment8001、8002中有相同的实体类entities,所以我们将这个相同的实体类拿出来放入一个新模块cloud-api-commons中。

配置pom加入需要的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>

包结构最好和8001、8002一样。将cloud-api-commons通过maven打包。最好是先clean然后install

删除8001/8002中实体类包然后通过maven导入上面打好的包

1
2
3
4
5
java复制代码  <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

多多指教,QQ1819220754可以交流学习经验

更新

zookeeper

Ribbon

OpenFeign

本文转载自: 掘金

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

1…219220221…956

开发者博客

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