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

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


  • 首页

  • 归档

  • 搜索

关于 Linux 系统的 swap 交换空间

发表于 2021-11-18

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

你好,我是看山。

用 Ubuntu 已经将近 1 年了,最近重装了 16.04 之后,每天到下午 5 点左右,都会发现 Swap 交换空间有几百兆的写入,系统内存 8G,硬盘是 SSD,i5 处理器,配置中档,也没有启动什么大型软件,就是用 IDEA 做开发,虽然没有影响,但本着一颗求知的心,google 一下,第一篇是 《All about Linux swap space》,口气很大,直接翻译了。(本文成文是在 2016 年 5 月,虽然已经是 5 年前,但是 Linux 架构并没有发生变化,所以本文所述还是正确的。)

图片

linux-swap-and-drop_cache

Linux 将随机存储 RAM 称为内存页。交换技术就是将一页内存复制到预先设定的硬盘上的交换空间,来释放该页占用内存。物理内存和交换空间的和就是可提供的虚拟内存的总量。有两个原因证明交换技术是很重要的。首先,系统需要的内存量比物理内存更大时,系统内核可以把较少使用的内存页写到交换空间,把空闲出来的内存给当前的应用程序(进程)使用。其次,一个应用启动时使用的内存页,可能只是在初始化时使用,之后不会再用,操作系统就可以把这部分内存页写入交换空间,把空闲出来的内存给其他应用使用或作为磁盘高速缓存。但是,交换技术也有负面作用。相对于内存,硬盘读写速度慢。内存的读写速度可以使用纳秒衡量,但是硬盘的速度只能达到毫秒级,访问硬盘的速度比访问内存的速度慢成千上万倍。发生的交换越多,系统运行越慢。有时候会有过度的交换或内存页频繁的写入写出的抖动发生,因为系统既要保证应用正常运行,又要寻找空闲的内存。这种情况下,只能通过增加 RAM 来解决。Linux 有两种形式的交换空间:交换分区和交换文件。交换分区就是一个独立的硬盘,没有文件或内容。交换文件是文件系统中的一个特殊文件,独立于系统和数据文件之外。可以使用swapon -s命令查看 swap 空间,输出如下:

1
2
bash复制代码Filename  Type       Size       Used Priority
/dev/sda5 partition  859436  0       -1

每一行列出的都是系统正在使用的交换空间。这里的’Type’字段表明该交换空间是一个分区而不是文件,通过’Filename’可以知道交换分区是磁盘 sda5。’Size’字段磁盘大小,单位是 KB,’Used’字段是表示有多少交换空间被使用。’Priority’字段表示 Linux 系统的交换空间使用优先级。有一个重要的特性,如果在 Linux 系统中挂载两个(或更多)具有相同优先级的交换空间(最好是两个不同的设备),Linux 将交替使用,可以提升交换性能。

交换分区

要为系统添加一个额外的交换分区,首先你需要准备一个。第一步是确保分区标记为交换分区,第二步是将格式设置为 swap 文件系统。将分区标记为 swap 分区,以 root 权限运行:

1
bash复制代码fdisk -l /dev/hdb

将’/dev/hdb’替换为你的交换分区的磁盘。输出类似于:

1
2
sql复制代码Device Boot    Start      End           Blocks  Id      System
/dev/hdb1       2328    2434    859446  82      Linux swap / Solaris

如果分区没有标记为 swap 分区,你需要使用命令fdisk及参数 t 来声明。操作分区时要小心,你绝对不想删除重要的分区或把系统分区的标识改错。交换分区上的数据会丢失,所以每次改动都需要多次确认。还需要注意的是,Solaris 使用相同的 ID 作为 Linux 交换空间,所以需要小心不要杀掉 Solaris 分区。如果分区已经标记为 swap 分区,就需要通过 root 权限运行mkswap命令:

1
bash复制代码mkswap /dev/hdb1

如果运行没有错误,你的交换空间就开始使用。立即激活:

1
bash复制代码swapon /dev/hdb1

可以通过swapon -s来确认是否运行。为了在系统启动时自动挂载 swap 空间,需要在’/etc/fstab’文件中添加一些列的配置,swap 空间是特殊的文件系统,许多参数不可用。比如:

1
bash复制代码/dev/hdb1       none    swap    sw      0       0

检查你的交换空间是无需重新启动,你可以运行swapoff -a命令,然后运行swapon -a,再通过swapon -s检查。

交换文件

和交换分区类似,Linux 也支持使用交换文件,你可以创建、准备,以交换分区的方式挂载。交换文件的好处是,你不需要找一个空的分区或添加额外的交换分区磁盘。

使用dd命令创建一个空文件。创建一个 1G 的文件,比如:

1
bash复制代码dd if=/dev/zero of=/swapfile bs=1024 count=1048576

‘/swapfile’是交换文件的名字,’count’的 1048576 是文件大小,单位 KB。准备交换文件使用mkswap命令,类似于准备分区,不过这次是使用同一个交换文件:

1
bash复制代码mkswap /swapfile

同样的,挂载交换文件使用swapon命令:

1
bash复制代码swapon /swapfile

在’/etc/fstab’中输入下面的内容:

1
bash复制代码/swapfile       none    swap    sw      0       0

交换空间的大小

如果你有很大的内存,有可能没有交换空间,系统也能运行良好。但是如果物理内存耗光,系统就会崩溃,因为它没有其他缓解方式,所以最好还是提供一个交换空间,更何况磁盘比内存便宜很多。关键的问题是内存空间多大?老版的类 UNIX 操作系统要求交换空间是物理内存的两到三倍。现在的扩展版(比如 Linux)不需要这么多,但是如果你配置这些,他们也会使用。重要的原则如下:

  1. 对于桌面系统,使用系统内存的两倍的交换空间,将可以运行大量的应用程序(其中可能有很多闲置的),使更多的 RAM 用于主要的应用;
  2. 对于服务器,使用小量的交换空间(通常是物理内存的一半),这样你就可以通过监控交换空间的大小来预警是否需要增加 RAM;
  3. 对于老式台式机,使用尽可能大的交换空间

Linux 2.6 内核中增加一个新的内核参数’swappiness’,管理员可以通过该参数修改 Linux 交换方式。参数值从 0 到 100. 从本质上说,值越大,将引起越多内存页发生交换;值越小,就有越多的应用驻留在内存中,而交换空间是空闲的。内核维护者 Andrew Morton 说过,他在他的台式机中设置 swappiness 值是 100,说:“我的观点是,通过内核参数降低交换是错误的。你不需要几百兆的无用应用占用内存。把它放在磁盘上,把内存留给有用的东西。” Morton 的想法有一个漏洞,如果内存交换太快,应用响应就会下降,因为当应用窗口被点击时,应用正在从交换空间读入内存,就会感觉运行很慢。默认的’swappiness’值是 60。你可以使用 root 命令调整参数(作用到重启):

1
bash复制代码echo 50 > /proc/sys/vm/swappiness

如果你需要使参数永久有效,就需要修改’/etc/sysctl.conf’中的’vm.swappiness’参数。

结论

管理交换空间是系统管理的一个重要方面。有了良好的规划和合理的使用交换技术可以有很多好处。不要害怕实验,并且经常监控你的系统,以确保你得到你需要的结果。

写在最后

就目前来说,内存和 SSD 都开始降价,基本上很轻松就能把机器攒到 8G(RAM)+120G(SSD),这样的话,就个人用户的桌面系统而言,交换空间的作用被大大削弱,但是正如上面说的,如果没有交换空间,内存耗光的时候,机器就挂了。因为 SSD 不建议分多个分区,所以使用 swap file 的方式比较好,而且还可以多建几个 swap file 文件,提升交换性能。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法
  • 系统设计系列之如何设计一个短链服务
  • 系统设计系列之任务队列
  • 软件架构-缓存技术
  • 软件架构-事件驱动架构

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

Python爬虫从入门到精通(六)表单与爬虫登录问题 前言

发表于 2021-11-18

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

前言

前面的章节中,我们介绍了如果在客户端与服务器之间进行数据交换。我们可以使用GET方法和POST方法与服务器进行交互,敏感数据只应使用POST请求进行发送,以避免将书暴露在URL中。当然,服务器还支持其他HTTP方法,比如PUT和DELETE等方法,但这些方法在表单中都不支持。

一、关于表单

客户端的浏览器需要与网站服务器进行交互,服务器需要根据用户输入返回对应的信息。

来看w3c的一个例子:

www.w3school.com.cn/html/html_f…

关于GET,POST与服务器的交互方法,可以见5.2节。

下面我们重点来看一个怎么处理登录表单的问题。

二、管理cookie

1、使用cookie登录

HTTP协议本身是无状态的,怎么保存来过或登陆过网站的信息?

所以我们需要在HTTP协议之外通过某种机制来识别用户的身份。于是就有了Session和Cookie。

什么是Cookie,什么是Session?

会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Session。Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

Cookie意为“甜饼”,是由W3C组织提出,最早由Netscape社区发展的一种机制。目前Cookie已经成为标准,所有的主流浏览器如IE、Netscape、Firefox、Opera等都支持Cookie。由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。所以就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。

Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,

以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。

)​

我们通过一个实例来看一下怎么使用Cookie做登录的操作。有些时候爬虫只有登录之后才能抓取到网页中的信息。比如微博,知乎,人人网等。

关于Cookie的更详细信息,可以参看: www.w3cschool.cn/pegosu/skj8…

2、 ##补充知识 cookiejar的使用

Cookie有时间限制,有域的限制,有编码问题等等。如果自己来管理Cookie,会很繁琐,特别是当有多个Cookie需要管理时,想要很好的管理Cookie很困难。

当遇到网页登录后,返回302跳转的情况下,urllib2的Response会丢失Set-Cookie的信息,导致登录不成功。

我们需要一个通用的能处理Cookie的工具来自动处理Set-Cookie请求;自动管理过期的Cookie,自动在对应域下发特殊Cookie;为了应对这些问题,我们引入了CookieJar;

三、关于验证码(CAPTCHA)

)​

网站为了防止黑客程序的恶意欺诈和攻击,采取的一种防御措施。据说最早是paypal这家公司引入的技术,现在已经在互联网网站中被广泛使用。

一般处理验证码CAPTCHA有两种方式:


 1)在需要输入验证码时程序弹出图片让用户自己输入;


 2)使用图像识别技术来识别图中的信息;

光学字符识别 OCR:OCR(Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法,将形状翻译成计算机文字的过程;

程序处理复杂验证码的方法:

1.使用Google的开源项目 Tesseract;

安装Tesseract:

Ubuntu中安装:

sudo apt-get install tesseract-ocr

pip install pytesseract

训练与测试:www.cnblogs.com/cnlian/p/57…

简单的Python测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码from PIL import Image

from pytesseract import *

# 加载图片

image = Image.open('test1.jpg')

# 识别过程

text = image_to_string(image)

print(text)

2.使用百度AI等等:

​

\

​

本文转载自: 掘金

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

Python爬虫从入门到精通(五)动态网页的挑战 前言 一、

发表于 2021-11-18

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

前言

很多网站的数据,比如电商网站商品的价格,评论等等会采用动态加载的方式来加载,这样可能在爬虫程序刚刚访问时无法直接获取到相关数据。那么怎么应对这样的问题呢?

一、动态网页的使用场景

先看下面一个例子:

)​

这是京东上看一本书的场景。我们发现打开一本书之后,书的价格,排名等信息及书的评论信息不是在我们第一次打开网站时就立即加载进来的。而是通过二次请求或多次的异步请求获取的。这样的页面就是动态页面。

关于动态页面使用的场景:

希望异步刷新的场景。有些网页内容很多,一次加载完对服务器压力很大,而且有的用户不会去查看所有内容;

二、回到与HTTP服务器发送请求数据的原始方法

1、GET方法

GET把参数数据队列添加到URL中,Key和Value的各个字段一一对应;在URL中可以看到。

浏览器的URL中有些符号,字符不能被很好的识别。那么我们需要有一套编码的方式来传递信息。所以发送端需要做urlencode; 接收端需要做urldecode;

www.baidu.com/s?ie=utf-8&…

在线测试工具: tool.chinaz.com/tools/urlen…

1.www.baidu.com/s?wd=DNS

?xxx=yyy&time=zzz get 请求的标识

2.acb.com/login?name=…

login: name=zhangsan password=123

)​

2、 POST方法

通过一个例子来看POST方法的使用:

)​

)​ 这是有道翻译的页面,仔细观察会发现,当用户每次输入一个想要翻译的词语时,页面的URL信息并不发生任何改变。这是一个典型的异步使用Ajax的技术,用JSON格式进行数据的传递。
​

三、更加难以对付的动态网站

1、应对需要多次数据的交互模拟的网站

我们有时会遇到像淘宝这样的大型网站,对数据版权看得特别重的,它们的网站有大量的工程师和技术人员去维护,它们也可能在技术手段上采用多次交互数据包的方式来完成网站服务器与用户浏览器之间的交互。如果此时还采用传统的分析数据包的方式会比较的复杂,难度较高。那么,有没有一劳永逸的方法,来解决此类问题呢?

我们的解决方案是:Selenium + PhantomJS。

我们的爬虫其实就是在做模拟浏览器的行为。

2、 Selenium

一个Web自动化测试工具,最初是为了网站自动化测试而开发的;我们玩游戏有按键精灵;Selenium也可以做类似的事情,但是它是在浏览器中做这样的事情。

安装: sudo pip install selenium(pip install selenium)

在Python中 from selenium import webdriver 来测试是否装好

说明:想要用Python做自动化测试的童鞋们可以好好研究一下Selenium的使用。

3、 PhantomJS及浏览器

说明:我们上课用的时有界面的Firefox浏览器,以便于教学;

一个基于webkit无界面(headless)的浏览器,它可以把网站加载到内存中并执行页面上的JS,但它没有图形用户界面,所以耗费的资源比较少;

安装: sudo apt install phantomjs (此方法可能安装不完整,导致部分功能无法使用)

Linux Ubuntu下完全安装的方法(参看blog.csdn.net/m0_38124502…

)

Wget

bitbucket.org/ariya/phant…

cd 下载

tar -xvf phantomjs-2.1.1-linux-x86_64.tar.bz2

cd phantomjs-2.1.1-linux-x86_64/

cd bin/

sudo cp phantomjs /usr/bin

python -启动-> 浏览器进程phantomjs,

测试:

SpiderCodes\Phantomjs.. 对其中的例子helloworld.js, pageload.js

进行测试;

注意: ****有可能造成资源泄漏;为了避免这种事的发生,需要有个策略适当的时候去kill phantomjs进程。

)​

四、关于动态网站信息抓取的总结

总的来说,我们的爬虫要尽量模拟的看起来就像是真正的用户在浏览器上访问服务器网站的行为。如果我们使用GET或POST的方式来模拟浏览器与服务器间通信的行为,成本比较低,但是应对复杂的网站或者服务器精心防御的网站来说是很难骗过服务器的。Selenim+PhantomJS的方案则会让我们的程序看起来更像是普通的用户,但是它的效率相对而言会降低很多,速度也会慢很多。在大规模爬去数据时可能遇到许多新的挑战。(比如网站尺寸的设置,等待时间的设定等)

本文转载自: 掘金

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

系统设计系列之任务队列

发表于 2021-11-18

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

你好,我是看山。

在一些系统中,会有对某些任务状态进行跟踪,如果任务失败需要重新执行任务。本文主要是针对这种请求提出解决方案,因为时间原因,方案还没有在代码中实现。但是经过和 朋友 的推演,是目前能想到的比较有效的方案了。鉴于本人才疏学浅,如果有某位大神有更好的解决方案,请一定不吝赐教,感谢不尽。

  1. 问题描述

1.1 一个主任务,多个子任务

在当前的系统环境中,通常一个应用会有多个实例,即水平拆分,提升并发能力。正常情况下,一个实例接收到一条请求,即开始对该请求进行处理。如果该请求是命令当前实例对某一分类下所有商品重建索引,假设该分类下有 10000 个商品,即该实例在接下来一段时间要有大量资源投入到重建索引中。但是其他实例都在闲着,形成一人干活,众人围观的局面。

假如该任务正常结束,这种方式也是没什么太大的问题的。但是可能出现一种极端的情况,该实例对其中的 9999 件商品重建索引都成功了,恰巧重建最后一条时失败实例挂了,则当前任务即任务是失败的,那前面的 9999 件商品创建索引的工作就是白费的。

1.2 任务状态跟踪

在一个消息平台中,接收到的消息向目标地址发送失败后,在一段时间后需要再尝试发送几次,保证消息可达。如果经过几次重试之后,发送消息依然失败,那将消息状态置为失败,等待人工干预。

假设这个消息平台很不靠谱,或者目标服务不靠谱,经过一段时间后,重试任务累计到 3000。这 3000 条需要重试的任务不均匀的分布的各个时间段上,消息标识不是序列号,没法通过序列号段进行取数。在这种情况下,即使有个多个实例可以同时对这些消息重试,为了不遗漏、不重复,只能够简单的通过时间分组重试,这样就会有任务分配不均,无法很好发挥集群的问题处理协作能力。

  1. 解题思路

其实上面两种情况可以认为是一种,即一堆无状态的任务需要被执行。为了资源的有效利用,不应该同时有多个应用执行任务,而且当任务成功后,也不需要再次执行。

最直接和最简单的思路就是需要提供可存储任务的系统:

  1. 定时的或以监听的方式从该存储系统中获取任务列表
  2. 检查该任务是否被加锁,如果加锁,放弃执行该任务;如果未加锁,对该任务加锁
  3. 开始执行任务
  4. 执行结束后,将任务结果写入存储系统,并对任务解锁
  5. 重复 1 操作,如果发现任务成功执行,则跳过任务或归档任务
  1. 解决方案

3.1 轮询

根据上面的解题思路,定时轮询是最简单最直接的方案。

图片

轮询

如上图所示:

  1. JOB 任务定时从 1 中获取任务列表
  2. 循环操作任务列表中的任务
  3. 将任务结果写回数据库

但是这种方式可优化的地方很多,比如:

  • 如果有多个实例,每个实例在任务启动的时候取任务列表中的一部分,即分页取任务列表。这就需要保证任务列表可有效分页,并且需要保证任务平均分散在任务列表每页中。比如根据时间取列表,而且任务列表在时间轴上比较均匀。
  • 同一个任务执行过程中要有锁,不需要两个实例同时执行同一个任务
  • 任务执行过程中要有状态。当该任务执行还没有成功完成时,如果持有该任务的实例死亡,能够有其他实例重新执行该任务

这种方式是我接手代码中使用的方式,但是那个人没有对任务列表分页。正常情况下,任务列表很短,只有小于 100 条,而且获取任务列表周期是 5 分钟,运行完全没有问题。但是一旦任务集中输入的时候,每次都获取所有任务,可以想象,一个实例在某一时刻输入 3000 个任务,然后开始一个一个执行,任务执行时间无限延长。为了利用集群共同处理问题的能力,于是开始对代码进行改造,就是下面这种轮询+监听的方式。

3.2 轮询+监听

轮询+监听的方式也是有弊端的,后面慢慢说。

图片

轮询

如上图,很明显的可以看出,这个能够算是 3.1 的升级版(虽然是升级版,效果依然不佳)。

  1. JOB 任务定时从数据获取任务列表
  2. 循环操作任务列表,剔除不符合要求的任务
  3. 将符合要求的任务写入 zookeeper,在 taskPath 下创建任务节点。
  4. Listener 监听 taskPath 字节点事件,发现有任务节点创建事件,从 zookeeper 读取节点数据,开始执行任务
  5. 任务执行结束,将任务状态写回数据库

这种方式增强了任务执行效率,只要 JOB 定时规则设置合理,理论上任务会随机分配到各个监听实例中,并执行任务。这个方案中的短板在定时轮询和 zookeeper 压力:

  • 定时轮询:因为时间紧,所以没有抛弃一开始 JOB 轮询任务这部分。所以只能够利用 zookeeper 的分布式锁,集群中某一实例读取读取任务列表,并将任务写入 zookeeper。如果没有后面的问题,也是可以接受这种方式。
  • zookeeper 服务压力:因为 zookeeper 的节点监听是要创建长连接、而且经常要向 zookeeper 方法状态确认请求,所以如果任务节点比较多、且驻留时间较长的时候,对 zookeeper 服务器压力比较大。有弊必有利,如果服务器能够撑住这种压力,这种方式能够保证,任务节点的任何变化,能够被准实时的感知到,针对任务变化,迅速做出响应。

3.3 任务队列

分析前面两种方案的短板,以及加上之前的经验。其实解决方案就呼之欲出了:一个很长的任务列表,最快的方法是分组批量执行,即分页获取列表中任务,然后使用多线程批量执行这些任务。(至于每次取多少,使用多少线程执行只能根据不同的任务难度、任务周期来计算了):

  • 分页获取:分页的难度就在于分页要均匀,且有明显的分页标识,以便另外一个实例不会重复获取已经分页数据。最简单的数据结构就是 FIFO 队列,能够顺序读取队列中的数据。因为是集群环境,只需要这个队列能够实现数据排他(删除、隐藏或通过位移控制)读取即可。
  • 批量执行:批量执行最简单的方式是通过多线程并行执行任务,这点不难。

执行过程如下图所示:

图片

轮询

  1. producer 将任务数据写入数据库,做备份或记录任务状态使用
  2. producer 将任务数据写入任务队列中
  3. consumer 从任务队列中分页获取任务列表,批量执行。根据执行情况及执行状态,判断是否重新返回任务队列等待执行
  4. 执行成功的任务,将任务状态入库
  5. 执行失败的任务重新写回任务队列,等待再次被读取执行

这里需要考虑一种异常情况:如果某一实例的 consumer 读取任务列表,任务队列将已读取任务列表删除后,该实例死亡。在该方案中,将丢失该实例中的任务,下面的双任务队列的方式可以解决这个问题。

3.4 双任务队列

可以考虑这个一个例子,生产线上工人们在做工,从传送带上取一组零件进行检查。检查不合格重新放回生产线末尾,等待机器重新加工零件;检查合格装箱打包。传送带即任务队列;员工即 consumer;员工取一组零件后传送带上就没有这些零件,即任务被排他获取;零件合格装箱,即任务成功;零件不合格重新放回传送带,即任务失败。与上面的方案很类似。

假设,有一个员工取完零件并检查了一半了,有的装箱,有的打回,然后突然不想干了,直接走了。这个时候其工作台上就散落一堆未检查零件。如果有一个人巡逻检查各个工作台,发现无人职守且有散落零件的工作台,只要把工作台上的零件放回传送带,这些零件又能够被正常的检查。

将上面的例子应用到我们的方案中,就是一个双任务队列的模型,如下图所示:

图片

轮询

  1. producer 将任务数据写入数据库,做备份或记录任务状态使用
  2. producer 将任务数据写入任务队列中
  3. consumer 从任务队列中分页获取任务列表
  4. consumer 将任务列表写入第二任务队列,防止任务丢失
  5. 执行成功的任务,将任务状态入库
  6. 执行失败的任务重新写回任务队列,等待再次被读取执行
  7. 定时任务检查任务第二任务队列,找到无主任务
  8. 定时任务将从第二任务队列中获取的无主任务写回 producer

考虑这种情况:如果任务队列排他读取方式中使用的是数据读取后删除,那么 consumer 在读取数据之后,写入第二任务队列之前,所在实例死亡,任务依然会丢失。所以比较稳妥的办法是,任务队列的排他方式是屏蔽或位移。

  • 屏蔽,就是如果有一个 consumer 读取任务数据,则将改任务数据状态修改,其他 consumer 不能够再看到该条数据,等待 consumer 确认之后,则可以将数据删除或归档。
  • 位移是通过一个位移量记录当前读取位置,并设置锁,其他 consumer 等待当前处理任务,处理结束后,提交位移量,其他 consumer 可以读取数据。

4 任务队列的选择

4.1 RabbitMQ

在 RabbitMQ 中,可以通过监听的方式Channel.basicConsume获取队列中的任务消息,为了安全考虑,需要将第二个参数autoAck置为 false。这样当前的 consumer 读取消息之后,消息状态是 Unacked,这个时候其他 consumer 就不能够看到这条消息,只有主动调用Channel.basicAck确认之后,消息才会被删除。如果消息未被 ack 确认,当前 consumer 死亡,消息会被重新置为 Ready 状态,可以被其他 consumer 消费。这种即上面所说的屏蔽的方式,任务可以无序的执行。

为了可以尽可能的榨干集群中每个实例的资源,每个实例可以启用多个线程同时监听队列,即每个实例有多个 consumer,这样能够尽可能快的将消息出队。下面是简单的实例代码,先创建指向 RabbitMQ 集群的连接,然后通过 producer 向 RabbitMQ 服务发送数据,最后通过 consumer 订阅方式消费消息。

创建连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("/");
factory.setUsername("username");
factory.setPassword("password");
factory.setAutomaticRecoveryEnabled(true);
factory.setNetworkRecoveryInterval(10000);
factory.setConnectionTimeout(60);
Address[] addressArray = new Address[]{new Address("127.0.0.1", 5672)};
ExecutorService es = Executors.newFixedThreadPool(200, new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setDaemon(true);
        thread.setName("rabbitMQ-thread-" + thread.getId());
        return thread;
    }
});
Connection conn = factory.newConnection(es, addressArray);

简单的 producer:

1
2
java复制代码Channel channel = conn.createChannel();
channel.basicPublish("someExChange", "someQueue", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "Hello, world!".getBytes());

每个线程中 consumer 可以如下面的实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码final Thread currentThread = Thread.currentThread();
try {
    final Channel channel = conn.createChannel();
    channel.basicConsume("someQueue", false, "someConsumerTag",
            new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                        AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String routingKey = envelope.getRoutingKey();
                    String contentType = properties.getContentType();
                    long deliveryTag = envelope.getDeliveryTag();
                    String message = new String(body, "UTF-8");
                    logger.info("threadName={}, routingKey={}, contentType={}, deliveryTag={}, message={}",
                            currentThread.getName(), routingKey, contentType, deliveryTag, message);
                    // 任务处理开始
                    // ...
                    // 任务处理结束
                    channel.basicAck(deliveryTag, false);
                }
            });
} catch (IOException e) {
    logger.error("发生错误", e);
}

4.2 Kafka

Kafka 的设计是用于顺序存储日志,通过这种设计,可以变相的用于有序队列,这种有序队列可以用于有序任务。定义一个有 20 个 Partition 的 Topic,在集群中的每个实例中,启动 5 个线程作为 consumer 读取。(为了有效利用资源,Partition 的数量要大于等于 consumer 线程数,这样不会导致有些线程空闲,白白耗费资源)。

为了保证某一实例死亡后,其他实例可以继续上个实例未完成的任务,需要在每个任务消息处理结束后,调用ConsumerConnector.commitOffsets(true)来修改偏移量。这种即上面说的位移的方式。

在 kafka 中有一种可变的使用方式,可以是任务有序或无序:

  • 有序:通过 producer 向 kafka 写数据的时候,设置一个 key(kafka 通过对 key 做 hash,将数据写入对应 partition 中),如果设置的 key 固定,则 partition 固定,读取的 consumer 即相对固定(说相对是因为 consumer 会隔一段时间做负载均衡,所以可能会切换 consumer)。在这种方式中,任务是有序执行的。缺点就是,集群中只会有一个实例能够获得读取数据的权利,其他实例都在等待。只有当这个实例死亡,才会有其他实例获得权利,继续上个实例未尽的事业。
  • 无序:在通过 producer 写数据的时候,可以将 key 中加一个变化的值,使数据均匀的分布在不同的 partition 中,这样不同的实例的 consumer 就都可以读取数据了。

producer 代码实例(示例代码为有序方式,无序方式只需要根据实际情况修改 job-key 即可):

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
java复制代码import static org.apache.kafka.clients.producer.ProducerConfig.*;

Properties properties = new Properties();
properties.put(BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
properties.put(ACKS_CONFIG, "all");// 0, 1, all
properties.put(BUFFER_MEMORY_CONFIG, "33554432");
properties.put(COMPRESSION_TYPE_CONFIG, "none");// none, gzip, snappy
properties.put(RETRIES_CONFIG, "0");
properties.put(BATCH_SIZE_CONFIG, "16384");
properties.put(CLIENT_ID_CONFIG, "someClientId");
properties.put(LINGER_MS_CONFIG, "0");
properties.put(MAX_REQUEST_SIZE_CONFIG, "1048576");
properties.put(RECEIVE_BUFFER_CONFIG, "32768");
properties.put(SEND_BUFFER_CONFIG, "131072");
properties.put(TIMEOUT_CONFIG, "30000");
properties.put(KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
properties.put(VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);

ProducerRecord<String, String> topic = new ProducerRecord<>("mq-job-topic", "job-key", "{id:1}");
kafkaProducer.send(topic, new Callback() {
    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        if (exception == null) {
            logger.info("topic={}, partition={}, offset={}", metadata.topic(), metadata.partition(), metadata.offset());
        } else {
            logger.error("producer 发送消息失败", exception);
        }
    }
});

kafkaProducer.close();

consumer 代码实例:

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
java复制代码Properties properties = new Properties();
properties.put("zookeeper.connect", "127.0.0.1:2181/kafka");
properties.put("fetch.message.max.bytes", "1048576");
properties.put("group.id", "someGroupId");
properties.put("auto.commit.enable", "false");
properties.put("auto.offset.reset", "largest");// smallest, largest

final ConsumerConnector connector = new KafkaConsumerFactory(properties).build();
Map<String, Integer> topicCountMap = new HashMap<>();
topicCountMap.put("mq-job-topic", 10);
Map<String, List<KafkaStream<byte[], byte[]>>> messageStreams = connector.createMessageStreams(topicCountMap);
List<KafkaStream<byte[], byte[]>> kafkaStreams = messageStreams.get("mq-job-topic");
ExecutorService executorService = Executors.newFixedThreadPool(10, new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    }
});
for (final KafkaStream<byte[], byte[]> kafkaStream : kafkaStreams) {
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            for (MessageAndMetadata<byte[], byte[]> messageAndMetadata : kafkaStream) {
                try {
                    String key = new String(messageAndMetadata.key(), "UTF-8");
                    String message = new String(messageAndMetadata.message(), "UTF-8");
                    logger.info("message={}, key={}", message, key);
                    // 任务处理开始
                    // ...
                    // 任务处理结束
                    connector.commitOffsets(true);
                } catch (Exception e) {
                    logger.error("发生异常", e);
                }
            }
        }
    }, null);
}

5 写在最后

虽然没有在项目中确实的使用这种解决方案,但是已经通过 demo 进行了技术验证。另外,分布式队列可以根据不同的需求选择 RabbitMQ(任务无序)或 Kafka(任务有序、无序),当然绝不限于这两种,还可以有很多其他的选择。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法
  • 系统设计系列之如何设计一个短链服务
  • 系统设计系列之任务队列
  • 软件架构-缓存技术
  • 软件架构-事件驱动架构

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

563 二叉树的坡度 简单二叉树递归题

发表于 2021-11-18

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

题目描述

这是 LeetCode 上的 563. 二叉树的坡度 ,难度为 简单。

Tag : 「二叉树」、「DFS」

给定一个二叉树,计算整个树的坡度 。

一个树的 节点的坡度 定义即为,该节点左子树的节点之和和右子树节点之和的 差的绝对值 。

如果没有左子树的话,左子树的节点之和为 000 ;没有右子树的话也是一样。空结点的坡度是 000 。

整个树 的坡度就是其所有节点的坡度之和。

示例 1:

1
2
3
4
5
6
7
8
9
ini复制代码输入:root = [1,2,3]

输出:1

解释:
节点 2 的坡度:|0-0| = 0(没有子节点)
节点 3 的坡度:|0-0| = 0(没有子节点)
节点 1 的坡度:|2-3| = 1(左子树就是左子节点,所以和是 2 ;右子树就是右子节点,所以和是 3 )
坡度总和:0 + 0 + 1 = 1

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码输入:root = [4,2,9,3,5,null,7]

输出:15

解释:
节点 3 的坡度:|0-0| = 0(没有子节点)
节点 5 的坡度:|0-0| = 0(没有子节点)
节点 7 的坡度:|0-0| = 0(没有子节点)
节点 2 的坡度:|3-5| = 2(左子树就是左子节点,所以和是 3 ;右子树就是右子节点,所以和是 5 )
节点 9 的坡度:|0-7| = 7(没有左子树,所以和是 0 ;右子树正好是右子节点,所以和是 7 )
节点 4 的坡度:|(3+5+2)-(9+7)| = |10-16| = 6(左子树值为 3、5 和 2 ,和是 10 ;右子树值为 9 和 7 ,和是 16 )
坡度总和:0 + 0 + 0 + 2 + 7 + 6 = 15

示例 3:

1
2
3
ini复制代码输入:root = [21,7,14,1,1,2,2,3,3]

输出:9

提示:

  • 树中节点数目的范围在 [0,104][0, 10^4][0,104] 内
  • −1000<=Node.val<=1000-1000 <= Node.val <= 1000−1000<=Node.val<=1000

递归

根据题目对「坡度」的定义,我们可以直接写出对应的递归实现。

代码:

1
2
3
4
5
6
7
8
9
10
Java复制代码class Solution {
public int findTilt(TreeNode root) {
if (root == null) return 0;
return findTilt(root.left) + findTilt(root.right) + Math.abs(getSum(root.left) - getSum(root.right));
}
int getSum(TreeNode root) {
if (root == null) return 0;
return getSum(root.left) + getSum(root.right) + root.val;
}
}
  • 时间复杂度:每个节点被访问的次数与其所在深度有关。复杂度为 O(n2)O(n^2)O(n2)
  • 空间复杂度:忽略递归来带的额外空间消耗。复杂度为 O(1)O(1)O(1)

递归

上述解法之所以为 O(n2)O(n^2)O(n2) 的时间复杂度,是因为我们将「计算子树坡度」和「计算子树权值和」两个操作分开进行。

事实上,我们可以在计算子树权值和的时候将坡度进行累加,从而将复杂度降为 O(n)O(n)O(n)。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码class Solution {
int ans;
public int findTilt(TreeNode root) {
dfs(root);
return ans;
}
int dfs(TreeNode root) {
if (root == null) return 0;
int l = dfs(root.left), r = dfs(root.right);
ans += Math.abs(l - r);
return l + r + root.val;
}
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.563 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour… 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文转载自: 掘金

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

Java基础学习17之IO流、字节流、字符流 Java基础学

发表于 2021-11-18

Java基础学习17之IO流、字节流、字符流

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

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻。

IO— 字节流与字符流

image-20210818173800455

字符流和字节流最本质的区别就是只有一个字节流是原生的操作,二字符流是经过处理后的操作。经过磁盘数据保存所保存的支持的数据类型只有:字节,所有磁盘中的数据必须先读到内存后才可以操作,内存里面会帮助我们把字节变为字符。字符更加适合处理中文。

字节操作流:OutputStream,InputStream;

字符操作流:Writer,Reader。

但是不管是字节流还是字符流的操作,本身都表示资源操作,而执行所有的资源都会按照如下几个步骤进行,下面以文件操作为例(对文件进行读,写操作):

  1. 要根据文件创建File对象
  2. 根据字节流或字符流的子类实例化我们的父类对象
  3. 进行数据的读取、写入操作
  4. 关闭流(clone())

对于IO操作属于资源处理,所有的对于资源的处理进行处理完成后必须进行关闭,否则资源就再也无法执行。

字节输出流:OutputStream

Java.io.OutputStream主要的功能是进行字节数据的输出的,而这个类的定义如下:

1
java复制代码public abstract class OutputStream extends Object implements Closeable, Flushable

发现OutputStream类定义的时候实现了两个接口:Closeable,Flushable,这两个接口定义如下:

Closeable: Flushable:
public interface Closeableextends AutoCloseable{ public void close() throws IOException; } public interface Closeable{ public void flush() throws IOException; }

当取得了OutputStream类实例化对象之后,下面肯定要进行输出操作,在OutputStream类之中定义了三个方法:

输出单个字节数组数据:public abstract void write(int b) throws IOException

输出一组字节数组数据:public void write(byte[] b) throws IOException

输出部分字节数组数据:public void write(byte[] b,int off,int len) throws IOException

提示:对于Closeable继承的AutoCloseable接口

AuotCloseable实在JDK1.7的时候又增加了一个新的接口,但是这个接口的定义和Closeable定义是完全一样的,我个人认为:有可能在一些其他的类上出现了自动的关闭功能,Closeable是手工关闭,AutoCloseable属于自动关闭。

但是对于Closeable和Flushable这两个接口实话而言不需要关注,因为从最早的习惯对于flush()和close()连个方法都是直接在OutputStream类之中定义的,所以很少去关心这些父接口问题。

对于OutputStream类而言发现其本身定义的是一个抽象类(abstract class),按照抽象类的使用原则来讲,如果要想为父类实例化,那么就需要使用子类,就需要定义抽象的子类,而现在如果要执行的是文件操作,则可以使用FileOutputStream子类完成。如果按照面向对象的开发原则,子类要为抽象类进行对象的实例化,而后调用的方法以父类中定义的方法为主,而具体的实现找实例化这个父类的子类完成,也就是说在整个的操作之中,用户最关心的只有子类的构造方法:

实例化FileOutputStream(新建数据):public FileOutputStream([File file) throws FileNotFoundException

实例化FileOutputStream(追加数据):public FileOutputStream(File file,boolean append) throws FileNotFoundException

image-20210818181217500

实现文件内容的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.day14.demo;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

public class OutputDemo {
public static void main(String[] args) throws Exception {
//1.定义文件路径
File file = new File("f:" + File.separator + "test" + File.separator + "hello.txt");
if(!file.getParentFile().exists()){//父路径不存在
file.getParentFile().mkdirs();//创建父目录
}
//2.要输出的数据
String str = "Hello,zsr!!!!";
//3.实例化对象
FileOutputStream stream = new FileOutputStream(file);
//4.将内容写进stream
stream.write(str.getBytes());//输出数据,要将输出的数据变为字节数组输出
//5.关闭流
stream.close();
}
}

这里默认执行时进行文档内容的覆写,如果不希望进行文档内容的覆写可以直接将FileOutputStream改为

1
java复制代码FileOutputStream stream = new FileOutputStream(file,true);

如果对写入的内容需要换行操作必须使用\r\n进行换行操作。

字节输入流:InputStream

如果现在要从指定的数据源之中读取数据,使用InputStream,而这个类的定义如下:

1
java复制代码public abstract class InputStreamextends Objectimplements Closeable

发现InputStream只实现了Closeable接口

在InputStream之中定义了三个读取数据的方法:

读取单个字节:public abstract int read() throws IOException

说明:每次执行read()方法都会读取一个数据源的指定数据,如果现在发现已经读取到了结尾则返回-1.

读取多个字节:public int read(byte[] b) throws IOException

说明:如果现在要读取的数据小于开辟的字节数组,这个时候read()方法的返回值int返回的是数据个数;如果现在开辟的字节数组小于读取的长度,则这个时候返回就是长度;如果数据已经读完了,则这个时候的int返回的是-1.

读取指定多个字节:public int read(byte[] b,int off,int len) throws IOException

说明:每次只读取传递数组的部分内容,如果读取满了,返回就是长度;如果没有读取满,返回的就是读取的个数;如果读取到最后没有数据了,就返回-1

image-20210818192150209

既然InputStream为抽象类,那么这个抽象类要使用就必须有子类,现在是通过文件读取内容,肯定使用FileInputStream子类进行操作,与OutputStream类的使用一样,对于FileInputStream也只关心构造方法:

FileInputStream类构造方法:public FileInputStream(File file) throws FileNotFoundException

文件信息的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package com.day14.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;


public class InputDemo {
public static void main(String[] args) throws Exception {
File file = new File("f:" + File.separator + "test" + File.separator + "hello.txt");
if(!file.exists()){
System.out.println("指定文件不存在!!");
}else{
FileInputStream is = new FileInputStream(file);
byte[] result = new byte[1024];
int length = is.read(result);
System.out.println("读取的内容为:" + new String(result,0,length));
}
}
}

字符输出流:Writer

Writer类也是一个专门用来数据输出的操作类,对于中文数据来说是比较友好的,这个类定义:

1
2
3
java复制代码public abstract class Writer
extends Object
implements Appendable, Closeable, Flushable

在Writer类之中定义的writer()方法都是以字符数据为主,但是在这些方法之中,只关心一个:

输出一个字符串:public void write(String str) throws IOException

如果要操作文件肯定使用FileWriter子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.day14.demo;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class WriterDemo {
public static void main(String[] args) throws IOException {
//1.定义文件路径
File file = new File("f:" + File.separator + "test" + File.separator + "hello.txt");
if(!file.getParentFile().exists()){//父路径不存在
file.getParentFile().mkdirs();//创建父目录
}
//2.要输出的数据
String str = "我正在学java这门课程!!!!\r\n";
//如果想要进行内容不覆盖的直接使用true就可以了
//FileWriter out = new FileWriter(file,true);
FileWriter out = new FileWriter(file);
out.write(str);
out.close();
}
}

字符输入流:Reader

Reader是进行字符数据读取的一个操作类,其定义:

1
2
3
java复制代码public abstract class Reader
extends Object
implements Readable, Closeable

在Writer类之中存在了直接输出一个字符串数据的方法,可是在Reader类之中并没有定义这样的方法,只是定义了三个按照字符串读取的方法?为什么会这样?

因为在使用OutputStream输出数据的时候,其程序可以输出的大小一定是程序可以承受的数据的大小,那么如果说使用InputStream读取的时候,可能被读取的数据非常大,那么如果一次性全读进来,就会出现问题,所以只能一个一个的进行读取。

Reader依然是抽象类,那么如果从文件读取,依然使用FileReader类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.day14.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class ReadDemo {
public static void main(String[] args) throws IOException {
File file = new File("f:" + File.separator + "test" + File.separator + "hello.txt");
if(!file.exists()){
System.out.println("指定文件不存在!!");
}else{
FileReader reader = new FileReader(file);
char[] result = new char[1024];
int length = reader.read(result);
System.out.println("读取的内容为:" + new String(result,0,length));
}
}
}

字符比字节的好处就是在于字符串数据的支持上,而这个好处还只是在Writer()类中体现,所以与字节流相比,字符流的操作并不是对等的关系。

字节流与字符流区别

通过我们一系统的分析,可以发现字节流和字符流的代码操作区别不大,如果从我们实际的使用,我们字节流是优先考虑,只有再我们使用中文的时候才考虑使用字符流,因为所有的字符都需要通过内存缓冲来进行处理。

image-20210819114608877

既然读数据需要缓存的处理,那么写数据也同样需要。如果使用字符流没有进行刷新,那么我们的内容可能再缓存之中,所以必须进行强制刷新才能得到完整的数据内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.day14.demo;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class WriterDemo {
public static void main(String[] args) throws IOException {
//1.定义文件路径
File file = new File("f:" + File.separator + "test" + File.separator + "hello.txt");
if(!file.getParentFile().exists()){//父路径不存在
file.getParentFile().mkdirs();//创建父目录
}
//2.要输出的数据
String str = "我正在学java这门课程!!!!\r\n";
//如果想要进行内容不覆盖的直接使用true就可以了
//FileWriter out = new FileWriter(file,true);
FileWriter out = new FileWriter(file);
out.write(str);
out.flush();
}
}

在以后的IO处理的时候,如果处理的是图片、音乐、文字都可以使用字节流,只有再处理中文的时候才会使用字符流。

转换流

现在对于IO操作就存在了字节流和字符流两种操作,那么对于这两种操作流之间也是可以进行转换的,而转换的操作类有两个:

将字节输出流变为字符输出流(OutputStream->Writer)——OutputStreamWriter;

将字节输入流变为字符输入流(InputStream->Reader)——InputStreaReader。

OutputStreamWriter InputStreamReader
public class OutputStreamWriterextends Writer public class InputStreamReaderextends Reader
public OutputStreamWriter(OutputStream out) public InputStreamReader(InputStream in)

image-20210819115616861

通过以上的继承结构和构造方法可以清楚发现,既然OutputStreamWriter是Writer的子类,那么必然OutputStreamWriter可以通过Writer类执行对象的向上转型进行接收,而同时这个OutputStreamWriter类的构造方法可以接收OutputStream,这样就可以完成转型。

将字节输出流变为字符输出流

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 com.day14.demo;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

public class OutWriterDemo {
public static void main(String[] args) throws Exception {
//1.定义文件路径
File file = new File("f:" + File.separator + "test" + File.separator + "hello.txt");
if(!file.getParentFile().exists()){//父路径不存在
file.getParentFile().mkdirs();//创建父目录
}
//2.要输出的数据
String str = "Hello,world!!!!";
//3.实例化对象
OutputStream stream = new FileOutputStream(file,true);
//4.将内容写进stream
Writer out = new OutputStreamWriter(stream);
out.write(str);
//5.关闭流
out.close();
}
}

对于文件操作可以使用FileInputStream,FileOutputStream,FileReader,FileWriter四个类,那么下面分别观察这四个类的继承结构。

观察FileInputStream,FileOutoutStream类的继承结构

FileInputStream FileOutoutStream
java.lang.Object java.io.InputStream java.io.FilterInputStream java.lang.Object java.io.OutputStream java.io.FileOutputStream

观察FileReader,FileWriter类的继承结构

FileReader FileWrite
java.lang.Object java.io.Reader java.io.InputStreamReader java.io.FileReader java.lang.Object java.io.Writer java.io.OutputStreamWriter java.io.FileWriter

image-20210819120636862

通过以上的继承关系也可以发现,实际上所有的字符数据都是需要进行转换的,依靠转换流完成,以后真正保存或者是传输的数据是不可能有字符的,全部都是字节,而字节只是在电脑之中处理后的结果。

本文转载自: 掘金

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

springboot项目使用谷歌的kaptcha生成验证码超

发表于 2021-11-18

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

一 、为啥要使用验证码

验证码(CAPTCHA) 是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止:恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,实际上用验证码是现在很多网站通行的方式,我们利用比较简易的方式实现了这个功能。今天给大家介绍一下kaptcha的和springboot一起使用的简单例子,kaptcha是谷歌的开源的工具类,本文使用的是第三方封装的jar包,由于是前后端分离项目,不能将验证码存在session作用域中,同时也是考虑到安全和跨域的问题,本文还需将验证码存在数据库中用于登录的时候的校验。

二、使用

  1. 引用maven坐标依赖

1
2
3
4
5
pom复制代码      	<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>

2.验证码数据库表实现

1
2
3
4
5
6
sql复制代码CREATE TABLE `sys_captcha` (
`uuid` char(36) NOT NULL COMMENT 'uuid',
`code` varchar(6) NOT NULL COMMENT '验证码',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统验证码';

3.生成和校验代码的核心代码

service层

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
java复制代码    // 将验证码生成的bean注入进来	
@Autowired
private Producer producer;
/**
* 验证码生成
* @param uuid
* @return
*/
@Override
public BufferedImage getCaptcha(String uuid) {
if(StringUtils.isBlank(uuid)){
throw new RRException("uuid不能为空");
}
//1. 生成文字验证码
String code = producer.createText();

SysCaptchaEntity captchaEntity = new SysCaptchaEntity();
captchaEntity.setUuid(uuid);
captchaEntity.setCode(code);
//2. 设置5分钟后过期
captchaEntity.setExpireTime(DateUtils.addDateMinutes(new Date(), 5));
this.save(captchaEntity);

return producer.createImage(code);
}

/**
* 验证码校验
* @param uuid uuid
* @param code 验证码
* @return
*/
@Override
public boolean validate(String uuid, String code) {
SysCaptchaEntity captchaEntity = this.getOne(new QueryWrapper<SysCaptchaEntity>().eq("uuid", uuid));
if(captchaEntity == null){
return false;
}

//删除验证码,不管这次校验是否成功这个验证码都失效了,验证码都是一次性的,所以可以删除掉了,减少垃圾数据
this.removeById(uuid);

if(captchaEntity.getCode().equalsIgnoreCase(code) && captchaEntity.getExpireTime().getTime() >= System.currentTimeMillis()){
return true;
}

return false;
}

Controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码	/**
* 验证码
*/
@GetMapping("captcha.jpg")
public void captcha(HttpServletResponse response, String uuid)throws IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
//获取图片验证码
BufferedImage image = sysCaptchaService.getCaptcha(uuid);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}

具体的代码可以参考:
==github.com/Dr-Water/ra…

  1. 使用思路

  1. 前端每次请求新的验证码的时候都会带一个uuid,每次的uuid都不一样
  2. 后端使用这个uuid作为这个验证码的唯一标识生成一个验证码保存在数据库中,并将验证码图片返回给前端
  3. 前端进行登录认证的再次将生成验证码的uuid传到后端,后端根据这个uuid去查询数据库,并校验验证码是否正确

三、一些优秀的参考链接

www.jianshu.com/p/a3525990c…
Javaweb—谷歌kaptcha图片验证码的使用
springboot整合kaptcha验证码

本文转载自: 掘金

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

《Kubernetes权威指南》笔记2 存储相关资源对象 安

发表于 2021-11-18

有状态的集群

一开始用的是 StatefulSet,后来改用 K8s Operator。

Job

除了无状态集群(Deployment)和有状态集群(Statefulset),还有批处理应用。为了支持这类应用,Kubernetes 引入了新的资源对象——Job。

Jobs控制器提供了两个控制并发数的参数:completions 和 parallelism,completions表示需要运行任务数的总数,parallelism 表示并发运行的个数。

Job所控制的Pod副本是短暂运行的,可以将其视为一组容器,其中的每个容器都仅运行一次。

后来,Kubernetes 增加了 CronJob,可以周期性地执行某个任务。

ConfigMap

有多个副本部署在不同的机器上时,配置文件的分发就成为一个让人头疼的问题,所以很多分布式系统都有一个配置中心组件,来解决这个问题。但配置中心通常会引入新的API,从而导致应用的耦合和侵入。Kubernetes则采用了一种简单的方案来规避这个问题,如图1.13所示,具体做法如下:

  1. 用户将配置文件的内容保存到 ConfigMap 中。
  2. 在建模用户应用时,在Pod里将ConfigMap定义为特殊的Volume进行挂载。在Pod被调度到某个具体Node上时,ConfigMap里的配置文件会被自动还原到本地目录下,然后映射到Pod里指定的配置目录下,这样用户的程序就可以无感知地读取配置了。
  3. 在ConfigMap的内容发生修改后,Kubernetes会自动重新获取ConfigMap的内容,并在目标节点上更新对应的文件。

Secret

它解决的是对敏感信息的配置问题,比如数据库的用户名和密码、应用的数字证书、Token、SSH密钥及其他需要保密的敏感配置。在 Kubernetes 1.7 版本以后,Secret 中的数据才可以以加密的形式进行保存,之前是以BASE64编码格式存放的。

HPA(Horizontal Pod Autoscaler)

即自动控制Pod数量的增加或减少。通过追踪分析指定Deployment控制的所有目标Pod的负载变化情况,来确定是否需要有针对性地调整目标Pod的副本数量。

VPA

根据容器资源使用率自动推测并设置Pod合理的CPU和内存的需求指标,从而更加精确地调度Pod。VPA目前属于比较新的特性,也不能与HPA共同操控同一组目标Pod。

存储相关资源对象

静态存储(Volume)

Volume 是 Pod 中能够被多个容器访问的共享目录,与 Docker 的 Volume 并不太一样。

  1. emptyDir 用作临时目录或多容器共享目录
  2. hostPath 是在Pod上挂载宿主机上的文件或目录,用于永久储存或访问宿主机数据
  3. 公有云 Volume
  4. 其他,如 configmap 和 secret

动态存储

相关概念:Persistent Volume、StorageClass、PVC。

安全相关资源对象

只有通过认证的用户才能通过Kubernetes的API Server查询、创建及维护相应的资源对象。

Service Account 代表 Pod 应用的账号,Service Account是通过Secret来保存对应的用户(应用)身份凭证的,当Pod里的容器被创建时,Kubernetes会把对应的Secret对象中的身份信息(ca.crt、Token等)持久化保存到容器里固定位置的本地文件中,因此当容器里的用户进程通过Kubernetes提供的客户端API去访问API Server时,这些API会自动读取这些身份信息文件,并将其附加到HTTPS请求中传递给API Server以完成身份认证逻辑。

在身份认证通过以后,就涉及“访问授权”的问题,这就是RBAC(Role-Based Access Control)要解决的问题了。

本文转载自: 掘金

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

如何使用 MySQL 慢查询日志进行性能优化 - Profi

发表于 2021-11-18

当我们开始关注数据库整体性能优化时,我们需要一套 MySQL 查询分析工具。特别是在开发中大型项目时,往往有数百个查询分布在代码库中的各个角落,并实时对数据库进行大量访问和查询。如果没有一套趁手的分析方法和工具,就很难发现在执行过程中代码的效率瓶颈,我们需要通过这套工具去定位 SQL 语句在执行中缓慢的问题和原因。

本教程带领大家学习和实践 MySQL Server 内置的查询分析工具 —— 慢查询日志、mysqldumpslow、Profiling,详细讲解如何使用他们提升代码执行效率。如果你想根据自己的工作流开发一套数据库查询管理工具,推荐使用卡拉云。只要你会写 SQL,无需会前端也可以轻松搭建属于自己的后台查询工具,详见本文文末。

一. 有关 MySQL 慢查询日志

1.慢查询日志是什么?

MySQL 慢查询日志是用来记录 MySQL 在执行命令中,响应时间超过预设阈值的 SQL 语句。

记录这些执行缓慢的 SQL 语句是优化 MySQL 数据库效率的第一步。

默认情况下,慢查询日志功能是关闭的,需要我们手动打开。当然,如果不是调优需求的话,一般也不建议长期启动这个功能,因为开启慢查询多少会对数据库的性能带来一些影响。慢查询日志支持将记录写入文件,当然也可以直接写入数据库的表中。

2.配置并打开慢查询日志

(1)在 MySQL Server 中临时开启慢查询功能

在 MySQL Server 中,默认情况慢查询功能是关闭的,我们可以通过查看此功能的状态

1
sql复制代码show variables like 'slow_query_log';

slow_query_log

如上图所示,慢查询日志(slow_query_log )的状态为关闭。

我们可以使用以下命令开启并配置慢查询日志功能,在 mysql 中执行以下命令:

1
2
3
4
5
ini复制代码SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/kalacloud-slow.log';
SET GLOBAL log_queries_not_using_indexes = 'ON';
SET SESSION long_query_time = 1;
SET SESSION min_examined_row_limit = 100;

SET GLOBAL slow_query_log :全局开启慢查询功能。

SET GLOBAL slow_query_log_file :指定慢查询日志存储文件的地址和文件名。

SET GLOBAL log_queries_not_using_indexes:无论是否超时,未被索引的记录也会记录下来。

SET SESSION long_query_time:慢查询阈值(秒),SQL 执行超过这个阈值将被记录在日志中。

SET SESSION min_examined_row_limit:慢查询仅记录扫描行数大于此参数的 SQL。

**特别注意:**在实践中常常会碰到无论慢查询阈值调到多小,日志就是不被记录。这个问题很有可能是 min_examined_row_limit 行数过大,导致没有被记录。min_examined_row_limit 在配置中常被忽略,这里要特别注意。

接着我们来执行查询语句,看看配置。(在 MySQL Server 中执行)

1
2
3
4
sql复制代码show variables like 'slow_query_log%';
show variables like 'log_queries_not_using_indexes';
show variables like 'long_query_time';
show variables like 'min_examined_row_limit';

show-variables-like

以上修改 MySQL 慢查询配置的方法是用在临时监测数据库运行状态的场景下,当 MySQL Server 重启时,以上修改全部失效并恢复原状。

扩展阅读:六类 MySQL 触发器使用教程及应用场景实战案例

(2)将慢查询设置写入 MySQL 配置文件,永久生效

虽然我们可以在命令行中对慢查询进行动态设置,但动态设置会随着重启服务而失效。如果想长期开启慢查询功能,需要把慢查询的设置写入 MySQL 配置文件中,这样无论是重启服务器,还是重启 MySQL ,慢查询的设置都会保持不变。

MySQL conf 配置文件通常在 /etc 或 /usr 中。我们可以使用 find 命令找到配置文件具体的存放位置。

1
arduino复制代码sudo find /etc -name my.cnf

sudo-find

找到位置后,使用 nano 编辑 my.cnf 将慢查询设置写入配置文件。

1
bash复制代码sudo nano /etc/mysql/my.cnf
1
2
3
4
5
6
ini复制代码[mysqld]

slow-query-log = 1
slow-query-log-file = /var/log/mysql/localhost-slow.log
long_query_time = 1
log-queries-not-using-indexes

使用 nano 打开配置文件,把上面的的代码写在 [mysqld] 的下面即可。 ctrl+X 保存退出。

1
复制代码sudo systemctl restart mysql

重启 MySQL Server 服务,使刚刚修改的配置文件生效。

**特别注意:**直接在命令行中设置的慢查询动态变量与直接写入 my.cnf 配置文件的语法有所不同。

扩展阅读:10种 MySQL 管理工具 横向测评 - 免费和付费到底怎么选?

举例:动态变量是slow_query_log,写入配置文件是slow-query-log。这里要特别注意。

更多 MySQL 8.0 动态变量语法可查看 MySQL 官方文档。

二. 使用慢查询功能记录日志

到这里我们已经配置好慢查询功能所需要的一切。下面咱们写一个示例,在这个示例中我们来一起学习如何查看和分析慢查询日志。

你可以打开两个连接到服务器的命令行窗口,一个用来写 MySQL 代码,另一个用来查看日志。

注意:以下教程中,有些代码是在命令行中执行,有些是在 MySQL Server 中执行,请注意分辨。

登录 MySQL Server,创建一个数据库,写入一组示例数据。

1
2
3
4
sql复制代码CREATE DATABASE kalacloud_demo;
USE kalacloud_demo;
CREATE TABLE users ( id TINYINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) );
INSERT INTO users (name) VALUES ('Jack Ma'),('Lei Jun'),('Wang Xing'),('Pony Ma'),('Zhang YiMing'),('Ding Lei'),('Robin Li'),('Xu Yong'),('Huang Zheng'),('Richard Liu');

为了保证大家与教程配置保持一致,咱们一起使用动态变量,再设置一边慢查询参数。

在 MySQL Server 中执行以下 SQL 代码:

1
2
3
4
5
ini复制代码SET GLOBAL slow_query_log = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/kalacloud-slow.log';
SET GLOBAL log_queries_not_using_indexes = 1;
SET long_query_time = 10;
SET min_examined_row_limit = 0;

现在我们有了一个表中有数据的示例数据库。慢查询功能也已经打开,我们特意把时间阈值(long_query_time)设置为 10 并且把最小行(min_examined_row_limit)设置为 0。

接着我们来运行一段代码测试一下:

1
2
ini复制代码USE kalacloud_demo;
SELECT * FROM users WHERE id = 1;

使用主键索引对表进行 select 查询,这种查询速度非常快,又使用了索引。因此慢查询日志中不会有任何记录。

我们打开慢查询日志,验证一下是否有记录,在命令行中执行以下命令:

1
matlab复制代码sudo cat /var/log/mysql/kalacloud-slow.log

可以看到kalacloud-slow.log还没有任何记录。
kalacloud-slow-log

接着我们在 MySQL Server 中执行以下代码:

1
ini复制代码SELECT * FROM users WHERE name = 'Wang Xing';

这段查询代码使用非索引列(name)来进行查询,所以慢查询日志在会记录下这个查询。

我们打开日志查看记录变化:

1
matlab复制代码sudo cat /var/log/mysql/kalacloud-slow.log

通过 cat 查看 log

我们可以看到这个非索引查询,已经被记录在慢查询日志中了。

再举个例子。我们提高最小检查行(min_examined_row_limit)的检查行数设置为 100,然后再执行查询。

在 MySQL Server 中执行以下代码:

1
2
ini复制代码SET min_examined_row_limit = 100;
SELECT * FROM users WHERE name = 'Zhang YiMing';

执行后,再打开 kalacloud-slow.log ,可以看到条小于 100 行的查询,没有被记录到日志中。

特别注意:如果慢查询日志中,没有记录任何数据,可以检查以下内容。

(1)创建日志的目录权限问题,是否有对应的权限。

1
2
3
4
bash复制代码cd /var/log
mkdir mysql
chmod 755 mysql
chown mysql:mysql mysql

(2)另一个可能是查询变量配置问题,把 my.conf 文件内有关慢查询的配置清干净,然后重启服务,重新配置。看看是不是这里出的问题。

扩展阅读:如何将 MySQL 的查询结果保存到文件

三. 慢查询日志记录参数详解

接着我们来讲解慢查询日志应该如何分析

慢查询日志分析

日志中信息的说明:

  • Time :被日志记录的代码在服务器上的运行时间。
  • User@Host:谁执行的这段代码。
  • Query_time:这段代码运行时长。
  • Lock_time:执行这段代码时,锁定了多久。
  • Rows_sent:慢查询返回的记录。
  • Rows_examined:慢查询扫描过的行数。

这些被记录的信息非常有意义,所有超过阈值的代码都会被记录在日志中,我们可以通过这些信息找到 MySQL 查询时效率不佳的代码,有助于我们优化 MySQL 性能。

扩展阅读:如何在 MySQL 里查询数据库中带有某个字段的所有表名

四. 使用 mysqldumpslow 工具对慢查询日志进行分析

实际工作中,慢查询日志可不像上文描述的那样,仅仅有几行记录。现实中慢查询日志会记录大量慢查询信息,写入也非常频繁。日志记录的内容会越来越长,分析数据也变的困难。 好在 MySQL 内置了 mysqldumpslow 工具,它可以把相同的 SQL 归为一类,并统计出归类项的执行次数和每次执行的耗时等一系列对应的情况。

我们先来执行几行代码让慢查询日志记录下来,然后再用 mysqldumpslow 进行分析。

上文我们把min_examined_row_limit 设置为 100,在这里,我们要将它改为 0 ,慢查询才能有记录。在 MySQL Server 中执行以下代码:

1
ini复制代码SET min_examined_row_limit = 0;

接着我们执行几条查询命令:

1
2
3
ini复制代码SELECT * FROM users WHERE name = 'Wang Xing';
SELECT * FROM users WHERE name = 'Huang Zheng';
SELECT * FROM users WHERE name = 'Zhang YiMing';

根据前文的慢查询设置,这三条记录都将被记录在日志中。

现在,我们切换到命令行的窗口中,执行 mysqldumpslow 命令:

1
c复制代码sudo mysqldumpslow -s at /var/log/mysql/kalacloud-slow.log

返回的数据:

mysqldumpslow

我们可以看到,返回的数据中,已经把三条类似的 SQL 语句记录抽象成一条记录SELECT * FROM users WHERE name = 'S' 并且针对这条记录列出了对应的总量和平均量的记录。

常见的 mysqldumpslow 命令 平时大家也可以根据自己的常用需求来总结,存好这些脚本备用。

  • mysqldumpslow -s at -t 10 kalacloud-slow.log:平均执行时长最长的前 10 条 SQL 代码。
  • mysqldumpslow -s al -t 10 kalacloud-slow.log:平均锁定时间最长的前10条 SQL 代码。
  • mysqldumpslow -s c -t 10 kalacloud-slow.log:执行次数最多的前10条 SQL 代码。
  • mysqldumpslow -a -g 'user' kalacloud-slow.log:显示所有 user 表相关的 SQL 代码的具体值
  • mysqldumpslow -a kalacloud-slow.log:直接显示 SQL 代码的情况。

mysqldumpslow 的参数命令

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
sql复制代码Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]

Parse and summarize the MySQL slow query log. Options are

--verbose verbose
--debug debug
--help write this text to standard output
-v verbose
-d debug
-s ORDER what to sort by (al, at, ar, c, l, r, t), 'at' is default
al: average lock time
ar: average rows sent
at: average query time
c: count
l: lock time
r: rows sent
t: query time
-r reverse the sort order (largest last instead of first)
-t NUM just show the top n queries
-a don't abstract all numbers to N and strings to 'S'
-n NUM abstract numbers with at least n digits within names
-g PATTERN grep: only consider stmts that include this string
-h HOSTNAME hostname of db server for *-slow.log filename (can be wildcard),
default is '*', i.e. match all
-i NAME name of server instance (if using mysql.server startup script)
-l don't subtract lock time from total time

常用的参数讲解:

-s

  • al:平均锁定时间
  • at:平均查询时间 [默认]
  • ar:平均返回记录时间
  • c:count 总执行次数
  • l:锁定时间
  • r:返回记录
  • t:查询时间

-t:返回前 N 条的数据

-g:可写正则表达,类似于 grep 命令,过滤出需要的信息。如,只查询 X 表的慢查询记录。

-r:rows sent 总返回行数。

mysqldumpslow 日志查询工具好用就好用在它特别灵活,又可以合并同类项式的分析慢查询日志。我们在日常工作的使用中,就能够体会 mysqldumpslow 的好用之处。

另外 mysqldumpslow 的使用参数也可在 MySQL 8.0 使用手册 中找到。

扩展阅读:如何查看 MySQL 数据库、表、索引容量大小?找到占用空间最大的表

五. Profilling - MySQL 性能分析工具

为了更精准的定位一条 SQL 语句的性能问题,我们需要拆分这条语句运行时到底在什么地方消耗了多少资源。 我们可以使用 Profilling 工具来进行这类细致的分析。我们可通过 Profilling 工具获取一条 SQL 语句在执行过程中对各种资源消耗的细节。

进入 MySQL Server 后,执行以下代码,启动 Profilling

1
ini复制代码SET SESSION profiling = 1;

检查 profiling 的状态

1
scss复制代码SELECT @@profiling;

返回数据: 0 表示未开启,1 表示已开启。
profiling

执行需要定位问题的 SQL 语句。

1
2
ini复制代码USE kalacloud_demo;
SELECT * FROM users WHERE name = 'Jack Ma';

查看 SQL 语句状态。

1
ini复制代码SHOW PROFILES;

打开 profiling 后,SHOW PROFILES; 会显示一个将 Query_ID 链接到 SQL 语句的表。

show-profiles
Query_ID:SQL 语句的 ID 编号。
Duration:SQL 语句执行时长。
Query:具体的 SQL 语句。

执行以下 SQL 代码,将 [# Query_ID] 替换为我们要分析的 SQL 代码Query_ID的编号。

1
ini复制代码SHOW PROFILE CPU, BLOCK IO FOR QUERY [# Query_ID];

即

1
ini复制代码SHOW PROFILE CPU, BLOCK IO FOR QUERY 4;

show-profiles-all

Status 是执行查询过程中的具体步骤,Duration 是完成该步骤所需的时间(以秒为单位)。

我们可以根据这些细节来具体分析,如何优化对应的 SQL 代码。

六. 慢查询教程总结

慢查询是让我们看到数据库真实运行状态的工具,对服务器和数据库性能优化有着指导性的意义。无论是生产环境、开发、QA,都可以谨慎的打开慢查询来记录性能日志。

我们可以先把动态变量long_query_time 设置的大一些,观察一下,然后在进行微调。有了慢查询日志,我们就有了优化性能的方向和目标,再使用 mysqldumpslow 和 profiling 进行宏观和微观的日志分析。找到低效 SQL 语句的细节,进行微调,最终使我们的系统可以获得最佳执行性能。

至此,MySQL 慢查询日志我们就讲解完了,如果你周期性的查看 log 日志,可以使用卡拉云搭一个日志看板,自己不仅查看、分析数据方便,还可以一键分享给组内的小伙伴共享数据。

卡拉云是新一代低代码开发工具,免安装部署,可一键接入包括 MySQL 在内的常见数据库及 API。不仅可以像命令行一样灵活,还可根据自己的工作流,定制开发。无需繁琐的前端开发,只需要简单拖拽,即可快速搭建企业内部工具。数月的开发工作量,使用卡拉云后可缩减至数天,欢迎使用我开发的卡拉云。

卡拉云可快速接入的常见数据库及 API

卡拉云可快速接入的常见数据库及 API

卡拉云可根据公司工作流需求,轻松搭建数据看板,并且可分享给组内的小伙伴共享数据

快速搭建企业内部工具

仅需拖拽一键生成前端代码,简单一行代码即可映射数据到指定组件中。

卡拉云搭建数据库看板

卡拉云可直接添加导出按钮,导出适用于各类分析软件的数据格式,方便快捷。立即开通卡拉云,快速搭建属于你自己的后台管理系统。

有关 MySQL 教程,可继续拓展学习:

  • 如何远程连接 MySQL 数据库,阿里云腾讯云外网连接教程
  • 如何在 MySQL / MariaDB 中导入导出数据,导入导出数据库文件、Excel、CSV
  • 如何在两台服务器之间迁移 MySQL 数据库 阿里云腾讯云迁移案例
  • MySQL 中如何实现 BLOB 数据类型的存取,BLOB 有哪些应用场景?
  • 如何使用 MySQL Workbench 操作 MySQL / MariaDB 数据库中文指南

本文转载自: 掘金

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

EasyC++31,内联函数

发表于 2021-11-18

大家好,我是梁唐。

这是EasyC++系列的第31篇,来聊聊内联函数。

想要追求更好阅读体验的同学,可以点击访问github仓库:EasyLeetCode。

内联函数

内联函数是C++当中为了提高程序运行效率的设计,老实讲我没有在其他语言当中看到类似的设计。它和常规函数之间的主要区别不在于编写的方式,而是在于C++编译器会将内联函数组合到程序当中执行。

要解释这个过程会稍稍有些复杂,我们需要从编译的过程说起。对于编译型语言而言,编译器做的事情是把人类写出来人能读懂的代码翻译成机器能够识别、执行的机器语言,一般是一串十六进制的指令。随后计算机逐步执行这些指令,完成我们想要的功能。

当我们调用函数时,其实本质上是指令跳转,先记录下当前运行的指令位置,跳转到函数所在的指令位置进行执行,执行完成之后再跳转回来。这个当中除了跳转之外,还会发生一些参数的传递和拷贝,需要一定的开销。

而使用内联函数,本质上可以理解成使用相应的函数代码代替了函数调用。可以简单理解成把函数当中的代码拷贝了一份粘贴到了函数调用的位置,代替了函数跳转。举个例子,比如说我们有一个函数来计算坐标到原点的距离:

1
2
3
4
5
6
7
8
C++复制代码include<cmath>

double distance(double x, double y) {
return sqrt(x * x + y * y);
}

double x = 3.0, y = 4.0;
double d = distance(x, y);

当我们使用了内联函数之后,它相当于把函数的代码拷贝了一份粘贴到了调用的位置:

1
2
C++复制代码double x = 3.0, y = 4.0;
double d = sqrt(x * x + y * y);

这也就是内联的含义,使用了内联函数之后,程序无须跳转到另外一个位置进行执行,可以节省掉跳转所带来的开销。因此运行效率要比普通函数更快,但代价是需要占用更多的内存。比如我们调用了10次内联函数,相当于代码拷贝了十份。

内联函数的使用非常简单,就是在函数定义之前加上inline关键字。

需要注意的是,有的时候我们虽然加上了inline关键字但编译器并不一定会遵照执行。有些编译器会有函数规模的限制,并且会限制内联函数禁止调用自己,也就是不能递归。

还有一点是内联函数虽然有内联机制,但是函数的传参依然是值传递,也就是说会发生拷贝,和普通函数一致。

在C语言当中没有inline特性,C语言是使用宏定义来实现类似的功能。但宏定义并不是通过参数传递,而是代替机械替换实现的。

比如:

1
2
3
C++复制代码#define SQUARE(x) x*x

double a = SQUARE(3.4 + 3.5);

这样我们得到的结果会是3.4 + 3.5 * 3.4 + 3.5,也就是说宏定义只是机械地替换代码,并不是函数式的调用。所以要实现类似inline函数的效果,可以使用括号:

1
C++复制代码#define SQUARE(x) ((x) * (x))

本文转载自: 掘金

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

1…300301302…956

开发者博客

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