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

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


  • 首页

  • 归档

  • 搜索

JAVA通过BufferedImage进行图片的绘制,缩放,

发表于 2021-11-10

最近开发当中,通过JAVA对图片进行了很多的操作,之前很少接触这方面的知识,特此记录下来

读取图片

1
2
3
4
5
Java复制代码//读取图片  通过JAVA自带的ImageIO里面的read方法
BufferedImage bufImage = ImageIO.read(File input);
BufferedImage bufImage = ImageIO.read(URL input);
BufferedImage bufImage = ImageIO.read(InputStream input);
BufferedImage bufImage = ImageIO.read(ImageInputStream input)

保存图片

1
2
3
4
5
6
7
arduino复制代码/**
* image:RenderedImage 接口的实现类, BufferedImage 实现了 RenderedImage 接口
* formatName: 保存的图片格式的名称
* output: 结果输出位置
*/
ImageIO.write(RenderedImage image, String formatName, File output);
ImageIO.write(RenderedImage image, String formatName, OutputStream output);

绘制图片

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
arduino复制代码/**
* 创建一个指定宽高的图片内存对象
* width : 宽度
* height : 高度
* imageType : 图片类型 参数为BufferedImage 自身定义的常量
* TYPE_3BYTE_BGR :代表8位RGB分量图像
TYPE_INT_ARGB :代表8位RGBA颜色组件包装成整数像素的图像
......
*/
BufferedImage image = new BufferedImage(width, height, imageType);
//获取图片的画布
Graphics2D graphics = image.createGraphics();

//然后使用 Graphics 类在图片上绘制线段、矩形、图片、文本,设置背景颜色等等操作

// 设置画布颜色
void setColor(Color c)
// 设置字体颜色
void setFont(Font font)
// 设置线的宽度
setStroke(Stroke s)
// 设置背景颜色
void setBackground(Color c)
// 擦除某一区域(擦除后显示背景色)
void clearRect(int x, int y, int width, int height)
// 回收 Graphics 释放资源 操作完毕后一定要释放资源
void dispose()

// 绘制一条线段(如果两点为同一点,则绘制点)
void drawLine(int x1, int y1, int x2, int y2)
// 绘制一个矩形(空心)
void drawRect(int x, int y, int width, int height)
// 绘制一个椭圆(空心)
void drawOval(int x, int y, int width, int height)
// 绘制一个圆弧(弧线)
void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle)
// 绘制一张图片 ImageObserver:接收有关 Image 信息通知的异步更新接口,没用到直接传空
boolean drawImage(Image image, int x, int y, ImageObserver observer)
// 绘制一段文本
void drawString(String str, int x, int y)
...........................

缩放图片

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码/**
*@param width: 缩放后的宽度
*@param height: 缩放后的高度
*@param hints: 图像重采样算法的类型
*
* hints 参数取值为以下之一(Image 类中的常量):
* SCALE_AREA_AVERAGING: 使用 Area Averaging 图像缩放算法;
* SCALE_DEFAULT: 使用默认的图像缩放算法;
* SCALE_SMOOTH: 选择图像平滑度比缩放速度具有更高优先级的图像缩放算法。
* ......
*/
Image getScaledInstance(int width, int height, int hints);

缩放的使用实例代码:

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
arduino复制代码  /**
* JAVA 图像等比缩放
* @param srcImageFile 缩放的图片
* @param destImageFile 缩放后的图片
* @param scale 缩放比例
* @return
*/
public static boolean scale(File srcImageFile, File destImageFile,float scale){
try {
//使用ImageIO的read方法读取图片
BufferedImage read = ImageIO.read(srcImageFile);
//获取缩放后的宽高
int width = (int) (read.getWidth()*scale);
int height = (int) (read.getHeight()*scale);
//调用缩放方法获取缩放后的图片
Image img = read.getScaledInstance(width , height, Image.SCALE_DEFAULT);
//创建一个新的缓存图片
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//获取画笔
Graphics2D graphics = image.createGraphics();
//将Image对象画在画布上,最后一个参数,ImageObserver:接收有关 Image 信息通知的异步更新接口,没用到直接传空
graphics.drawImage(img, 0, 0,null);
//一定要释放资源
graphics.dispose();
//获取到文件的后缀名
String fileName = srcImageFile.getName();
String formatName = fileName.substring(fileName.lastIndexOf(".") + 1);
//使用ImageIO的write方法进行输出
ImageIO.write(image,formatName,destImageFile);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}

裁剪图片

1
2
3
4
5
6
7
8
9
10
java复制代码/**
*
* @param x 裁剪时x的坐标(左上角)
* @param y 裁剪时y的坐标(左上角)
* @param width 裁剪后的图片宽度
* @param height 裁剪后的图片高度
*
* 裁剪后返回的是一个新的图片缓存对象
*/
BufferedImage getSubimage(int x,int y, int width ,int height )

裁剪图片实例代码:

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
arduino复制代码    /**
* JAVA裁剪图片
* @param srcImageFile 需要裁剪的图片
* @param x 裁剪时x的坐标(左上角)
* @param y 裁剪时y的坐标(左上角)
* @param width 裁剪后的图片宽度
* @param height 裁剪后的图片高度
* @param destImageFile 裁剪后的图片
* @return
*/
public static boolean cut(File srcImageFile, int x,int y,int width,int height,File destImageFile){
try {
//使用ImageIO的read方法读取图片
BufferedImage read = ImageIO.read(srcImageFile);
//调用裁剪方法
BufferedImage image = read.getSubimage(x, y, width, height);
//获取到文件的后缀名
String fileName = srcImageFile.getName();
String formatName = fileName.substring(fileName.lastIndexOf(".") + 1);
//使用ImageIO的write方法进行输出
ImageIO.write(image,formatName,destImageFile);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}

添加水印

1
2
3
4
5
6
7
java复制代码水印的添加主要是通过下面的方法进行的添加。
// 绘制一张图片 ImageObserver:接收有关 Image 信息通知的异步更新接口,没用到直接传空
boolean drawImage(Image image, int x, int y, ImageObserver observer)
//设置水印透明度
void setComposite(Composite comp)
// 绘制一段文本
void drawString(String str, int x, int y)

文本水印

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
arduino复制代码    /**
* JAVA添加文字水印
* @param srcImageFile 目标图片
* @param destImageFile 结果图片
* @param text 文字内容
* @param x 水印x坐标
* @param y 水印y坐标
* @return
*/
public static boolean watermarkText(File srcImageFile, File destImageFile, String text,int x,int y) {
try {
//使用ImageIO的read方法读取图片
BufferedImage read = ImageIO.read(srcImageFile);
Graphics2D graphics = read.createGraphics();
// 设置“抗锯齿”的属性
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 设置字体类型和大小
graphics.setFont(new Font("微软雅黑", Font.PLAIN, 20));
// 设置颜色
graphics.setColor(new Color(255,255,255));
// 添加文字
graphics.drawString(text,x,y);
graphics.dispose();
//获取到文件的后缀名
String fileName = srcImageFile.getName();
String formatName = fileName.substring(fileName.lastIndexOf(".") + 1);
//使用ImageIO的write方法进行输出
ImageIO.write(read,formatName,destImageFile);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}

图片水印

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
java复制代码    /**
* JAVA添加文字水印
* @param srcImageFile 目标图片
* @param destImageFile 结果图片
* @param waterImage 水印图片
* @param x 水印x坐标
* @param y 水印y坐标
* @return
*/
public static boolean watermarkImage(File srcImageFile,File destImageFile, File waterImage,int x,int y) {
try {
//使用ImageIO的read方法读取图片
BufferedImage read = ImageIO.read(srcImageFile);
BufferedImage image = ImageIO.read(waterImage);
//获取画布
Graphics2D graphics = read.createGraphics();
//设置透明度为0.5
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP,0.5f));
//添加水印
graphics.drawImage(image,x,y,null);
//关闭透明度
//graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
graphics.dispose();
//获取到文件的后缀名
String fileName = srcImageFile.getName();
String formatName = fileName.substring(fileName.lastIndexOf(".") + 1);
//使用ImageIO的write方法进行输出
ImageIO.write(read,formatName,destImageFile);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}

如何擦除水印?
第一种方法 :
// 擦除某一区域(擦除后显示背景色)
void clearRect(int x, int y, int width, int height)

第二种方法:色素替代法
找到水印的颜色编码,然后用背景色颜色编码替代。
代码实现:略 (这种清除水印的需求还是交给PS这种专业软件去做吧)

贝塞尔曲线

通常绘制线段直接使用一下的方法就可以了drawLine方法就可以了。但是在实现曲线的时候就很难看,所以需要用到贝塞尔曲线。
可以通过Path类来实现贝塞尔曲线的效果

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码BufferedImage image = new BufferedImage(500, 500, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = image.createGraphics();
// 设置“抗锯齿”的属性
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//获取路径操作
Path2D path = new GeneralPath();
//通过移动到指定的浮点精度指定的指定的坐标,向路径添加一个点。
path.moveTo(192, 18);
//添加一个弯曲段,由两个新定义的点,通过绘制一条二次曲线相交的当前坐标和指定的坐标 (x2,y2)的路径,使用指定的点 (x1,y1)作为二次参考
path.quadTo(120, 12, 253, 67);
graphics.draw(path);
graphics.dispose();

总结

JAVA操作图片的话算是一个非常冷门的功能了,最多就是图片的缩放,裁剪,水印这三种情况。如果需要进行一些额外的操作的话,就非常需要认真的查阅下JDK的API,并且JAVA操作图片调试起来也非常的不方便。总的来说就是复杂的图片操作需求还是交给更专业的软件(PS)来进行操作吧!!!
JDK1.8在线API

本文转载自: 掘金

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

java -jar和-cp的区别

发表于 2021-11-10

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

前言

在Linux中执行Java程序的时候,需要使用java -jar或者java -cp来指定要运行的jar,两种方式的运行机制的不一样的,今天就来盘点一下这两种运行方法的区别。

执行程序

假如我们有一个程序,这里把它打包成Test.jar,如何运行才能成功输出Hello World。

代码如下:

1
2
3
4
5
6
java复制代码package com.test;
public class Test {
public static void main(String[] args) {
System.out.println("Hello World");
}
}

我们常见的有以下两种方法运行:

1
2
java复制代码1. java -jar Test.jar
2. java -cp com.test.Test Test.jar

这里就先分析一下-jar运行方式的机制。

java -jar

我们解压jar包,里面的META-INF文件夹下都有MANIFEST.MF。

内容如图:


我们知道想要运行一个Java程序,必须要有一个main()方法作为入口,这样才能启动程序。

而java -jar是通过Main-Class来找到Test类并执行其中的main(),输出Hello World!如果你的MANIFEST.MF文件中没有Main-Class,就会提示Cant load main-class的错误。所以在导出jar包或者使用maven打包插件的的时候一定要指定main-class。

如图:

这样导出的时候,MF文件中就会有了入口类的信息。

java -cp

对于java -cp就不需要指定Main-Class来指定入口。因为在启动时,第一个参数就指定了你的入口类,第二个参数就是你的jar包。它会根据你的jar包找到第一个参数指定的Test类,来输出HelloWorld。

区别

假设我们这个程序的运行,需要依赖一个叫Dep.jar的包。

如果我们使用-jar的话,需要把Dep.jar一同打包到Test.jar中,因为-jar只能指定一个jar包.

如果是使用-cp,我们可以选择将Dep.jar放到Test.jar中,也可以选择使用以下命令来运行:

1
bash复制代码java -cp com.test.Test Test.jar:Dep.jar

cp其实就是classpath,在linux中多个jar包用 : 分割,代表了程序运行需要的所有jar包。

这样就可以不用将所有依赖都一起打包到Test.jar下。这样做的好处就是,假如修改了Test类,只上传修改后的Test.jar到服务器即可,不需要再将所有依赖再打包到Test.jar中再上传一遍,节约了时间。

本文转载自: 掘金

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

Netty的线程模型

发表于 2021-11-10

Netty介绍

原生NIO存在的问题

  1. NIO的类库和API复杂,使用起来比较麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等API的使用。
  2. 需要具备一些额外的技能:Java多线程、Reactor模式、必须对多线程和网络编程非常熟悉、才能写出高质量的NIO程序。
  1. 开发工作量和难度非常大:例如客户端面临重连、网络闪断、半包读写、失败缓存、网络阻塞和异常流的处理等等。
  2. JDK NIO的BUG:Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到JDK1.7版本该问题依然存在,没有被根本解决。
1
scss复制代码在NIO中通过Selector的轮询,查看当前通道是否有IO事件,根据JDK NIO API的描述,Selector的select()方法会一直阻塞,直到IO事件完成或超时,但是在Linux平台上有时会出问题,在某些场景下select()方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,他会导致线程陷入死循环,会让CPU飙升到100%,极大地影响到系统的可靠性,到目前为止,JDK都没有完全解决这个问题。

概述

Netty是JBoss提供的一个Java开源框架。提供异步的,基于事件驱动的网络应用程序框架,用以快速地开发高性能,高可靠性的网络IO程序。Netty是一个基于NIO的网络编程框架,使用Netty可以帮你快速、简单的开发一个网络应用,相当于简化和流程化了NIO的开发过程。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的Elasticsearch、Dubbo框架内部都采用了Netty。

Netty的强大之处:零拷贝、可拓展的事件模型;支持TCP、UDP、HTTP、WebSocket等协议;提供安全传输、压缩、大文件传输、编解码支持等等。

具备如下优点:
  1. 设计优雅,提供阻塞和非阻塞的Socket;
  2. 提供灵活可拓展的事件模型;
  3. 提供高度可定制的线程模型;
  4. 具备更高的性能和更大的吞吐量,使用零拷贝技术最小化不必要的内存复制,减少资源的消耗;
  1. 提供安全传输特性;
  2. 支持多种主流协议;预置多种编解码功能,支持用户开发私有协议。

线程模型

概述

不同的线程模式,对程序的性能有很大影响,在学习Netty线程模式之前,首先了解下各个线程模式,最后看看Netty线程模型有哪些优越性。

目前存在的线程模型有:

  • 传统的阻塞I/O服务模型
  • Reactor模型:根据Reactor的数量和处理资源池线程的数量不同,有三种典型的实现
    • 单Reactor 单线程
      • 单Reactor 多线程
    • 主从Reator 多线程

传统阻塞的 I/O 服务模型

采用阻塞 I/O 模式获取输入的数据,每个连接都需要独立的线程完成数据的输入,业务处理和数据返回的工作。

这种模式存在问题:

  • 当并发数很大的时候,会创建大量的线程,占用很大的系统资源。
  • 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成系统资源的浪费。

Reactor 模型

Reactor模式,通过一个或者多个输入同时传递给服务器处理器的模式,服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此Reactor模式也叫Dispatch模式。

Reactor模式使用IO复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理的关键。

下面我们分别看下Reactor的三种经典模式:

单 Reactor 单线程

  1. Selector 可以实现应用程序通过一个阻塞对象监听多路连接请求。
  2. Reactor 对象通过Selector监控客户端连接事件,收到事件之后通过 Dispatch进行分发。
  1. 建立连接,请求事件,则由Acceptor处理连接请求,然后创建一个Handler对象处理完成后的后续业务处理。
  2. Handler会完成Read->业务处理->Send 这样一个完成业务流程。
优点:

模型简单,没有多线程,进程通信,竞争等问题,全部都在一个线程中完成

缺点:
  1. 存在性能问题:只有一个线程,无法完全发挥多核CPU的性能,Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。
  2. 存在可靠性问题:线程意外终止或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

单 Reactor 多线程

  1. Reactor 对象通过 Selector 监控客户端请求事件,收到事件后,通过 Dispatch 进行分发。
  2. 如果建立连接请求,则右 Acceptor 通过 accept 处理连接请求。
  1. 如果不是连接请求,则由 Reactor 分发调用连接对应得Handler 处理。
  2. Handler 只负责响应事件,不做具体的业务处理(只做 read 和 send 操作),通过 read 读取数据后,会分发给后面的 Worker 线程池中的某个线程处理业务。
  1. Worker 线程池会分配独立线程完成真正的业务,并将结果返回给 Handler。
  2. Handler 收到响应后,通过 send 将结果返回给客户端。
优点:

可以充分利用多核CPU的处理能力

缺点:

多线程数据共享和访问比较复杂,Reactor 处理所有的事件的监听和响应,再单线程运行,再高并发场景容易出现性能瓶颈。

主从 Reactor 多线程

  1. Reactor 主线程 MainReactor 对象通过 select() 监听客户端连接事件,收到事件后,通过 Acceptor 处理客户端连接事件。
  2. 当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分配给 SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由SubReactor 监听后面的 IO事件。)
  1. SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理。
  2. 当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理。
  1. Handler 通过 Read 从连接上读取请求数据,将请求数据分发给 Worker 线程池进行业务处理。
  2. Worker 线程池会分配独立线程来完成真正的业务处理,并将结果返回给 Handler。Handler 通过 Send 向客户端发送响应数据。
  1. 一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个 SubReactor 线程。
优点:
  • MainReactor 线程与 SubReactor 线程的数据交互简单职责明确,MainReactor 线程只需要接收新连接,SubReactor 线程完成后续的业务处理。
  • MainReactor 线程与 SubReactor 线程的数据交互简单,MainReactor 线程只需要把新连接传给 SubReactor 线程,SubReactor 线程无需返回数据。
  • 多个 SubReactor 线程能够应对更高的并发请求
缺点:
  • 这种模式的缺点就是编程复杂度较高。但是由于其优点明显,再许多项目中被广泛应用,包括 Nginx,Memcached、Netty 等。
  • 这种模式也叫服务器的1+M+N 线程模式,即使用该模式开发的服务器包含一个(或多个, 1只表示相对较少)连接建立线程,+M 个IO线程+N 个业务处理线程。这是业界成熟的服务器程序设计模式。

本文转载自: 掘金

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

Prometheus 了解一下

发表于 2021-11-10

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

介绍

Prometheus是用来收集和存储服务器的实时数据(比如:CPU、硬盘、服务响应、日志等),通过其丰富的运算函数,可以计算得到很多的服务性能指标(比如:CPU占用率、api响应时长、api返回500数量等),可用于集成监控告警系统中。

架构图

基本原理

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。

关键流程

  1. Prometheus Daemon 负责定时去目标上抓取 metrics(指标)数据,每个抓取目标需要暴露一个 http 服务的接口给它定时抓取。Prometheus 支持通过配置文件、文本文件、Zookeeper、Consul、DNS SRV Lookup 等方式指定抓取目标。Prometheus 采用 PULL 的方式进行监控,即服务器可以直接通过目标 PULL 数据或者间接地通过中间网关来 Push 数据。
  2. Prometheus 在本地存储抓取的所有数据,并通过一定规则进行清理和整理数据,并把得到的结果存储到新的时间序列中。
  3. Prometheus 通过 PromQL 和其他 API 可视化地展示收集的数据。Prometheus 支持很多方式的图表可视化,例如 Grafana、自带的 Promdash 以及自身提供的模版引擎等等。Prometheus 还提供HTTP API的查询方式,自定义所需要的输出。
  4. PushGateway 支持 Client 主动推送 metrics 到 PushGateway,而 Prometheus 只是定时去 Gateway 上抓取数据。
  5. Alertmanager 是独立于 Prometheus 的一个组件,可以支持 Prometheus 的查询语句,提供十分灵活的报警方式。

Exporter的概念

exporter是 Prometheus 的一类数据采集组件的总称,负责从目标处搜集数据,并将其转化为Prometheus支持的格式。与传统的数据采集组件不同的是,它并不向中央服务器发送数据,而是等待中央服务器主动前来抓取,默认的抓取地址为 http://current\_ip:9100/metrics

node-exporter是Prometheus官方推荐的exporter,类似的还有

  • HAProxy exporter
  • Collectd exporter
  • SNMP exporter
  • MySQL server exporter
    …

运行

1.启动 node_exporter 容器
node-exporter 是 Prometheus 官方推荐的exporter

1
shell复制代码docker run --name node_exporter -d -v "/proc:/host/proc" -v "/sys:/host/sys" -v "/:/rootfs" prom/node-exporter --path.procfs /host/proc --path.sysfs /host/sys --collector.filesystem.ignored-mount-points "^/(sys|proc|dev|host|etc)($|/)"

2.编辑配置文件prometheus/prometheus.yml

1
2
3
4
5
6
7
8
9
10
yml复制代码global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'webservers'
static_configs:
- targets: ['<node exporter node IP>:9100']

3.启动prometheus容器

1
javascript复制代码docker run --name prometheus -d -p 9090:9090 -v ~/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

4.访问prometheus web
http://localhost:9090/

本文转载自: 掘金

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

【Go语言入门150题】 L1-055 谁是赢家 (10 分

发表于 2021-11-10

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

【题解】【PTA团体程序设计天梯赛】

L1-055 谁是赢家 (10 分) Go语言|Golang

某电视台的娱乐节目有个表演评审环节,每次安排两位艺人表演,他们的胜负由观众投票和 3 名评委投票两部分共同决定。规则为:如果一位艺人的观众票数高,且得到至少 1 名评委的认可,该艺人就胜出;或艺人的观众票数低,但得到全部评委的认可,也可以胜出。节目保证投票的观众人数为奇数,所以不存在平票的情况。本题就请你用程序判断谁是赢家。

输入格式:

输入第一行给出 2 个不超过 1000 的正整数 Pa 和 Pb,分别是艺人 a 和艺人 b 得到的观众票数。题目保证这两个数字不相等。随后第二行给出 3 名评委的投票结果。数字 0 代表投票给 a,数字 1 代表投票给 b,其间以一个空格分隔。

输出格式:

按以下格式输出赢家:

1
shell复制代码The winner is x: P1 + P2

其中x是代表赢家的字母,P1是赢家得到的观众票数,P2是赢家得到的评委票数。

输入样例1:

1
2
shell复制代码327 129
1 0 1

结尾无空行

输出样例1:

1
out复制代码The winner is a: 327 + 1

结尾无空行

思路:

基础的判断条件,分开讨论就行了。

我们可以先对所有的评委的投票进行计算总数,那么首先就要先用Go语言官方提供的fmt包进行Scan的输入值,
先输入两个正整数,然后再输入三个评委的给予的得分情况,那么我们就可以进行计算的统计了。

首先,先对三个进行求和,然后分开讨论,如果这三个的求和都是0的话,那么就是a赢了,如果这三个求和是3的话,就是b赢了,但是如果是a的票数加上3(因为只有三篇)再减去投票的总和的比b的小的话,就算a赢了。

代码如下:

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

import "fmt"

func main() {
var a,b,c,d,e int
_, _ = fmt.Scan(&a, &b) // 输入选手
_, _ = fmt.Scan(&c, &d, &e)// 输入情况
if c+d+e == 0 { // 如果票数是0的话,a就获胜了
fmt.Printf("The winner is a: %d + %d",a,0)
return
}
if c+d+e == 3 {// 如果票数是3的话,b就获胜了
fmt.Printf("The winner is b: %d + %d",b,3)
return
}
if a+3-(c+d+e) > b+c+d+e { // 如果a的票数比b多的话
fmt.Printf("The winner is a: %d + %d",a,3-(c+d+e))
}
if a+3-(c+d+e) < b+c+d+e { // 如果b比a多的话
fmt.Printf("The winner is a: %d + %d",b, c+d+e)
}
}

本文转载自: 掘金

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

Spring cloud Alibaba 集成zipkin分

发表于 2021-11-10

前言

大家好,我是作曲家种太阳,最近忙于业务压力,很久没有写文章

这篇是我的笔记

记录下学习过程,最近踩坑经验

如何在Spring cloud Alibaba 集成zipkin分布式日志追踪

查看日志链路,增加开发效率,回溯Bug

1.安装kafka

安装地址 : kafka.apache.org/quickstart

建议安装kafka_2.13-2.7.0版本

cd 安装包目录

打开zooper(因为kafa依赖于zk,所以先打开zk)

1
shell复制代码bin/zookeeper-server-start.sh config/zookeeper.properties

打开kafka

1
shell复制代码bin/kafka-server-start.sh  config/server.properties

2.安装zipkin

安装地址 : zipkin.io/pages/quick…

导入sql,创建mysql表,让zinKin数据持久化

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
sql复制代码--
-- Copyright 2015-2019 The OpenZipkin Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
-- in compliance with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software distributed under the License
-- is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
-- or implied. See the License for the specific language governing permissions and limitations under
-- the License.
--

CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`remote_service_name` VARCHAR(255),
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';

CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';

CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT,
`error_count` BIGINT,
PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

启动ZipKin并链接数据库和kafka:

1
2
3
4
shell复制代码 java -DKAFKA_BOOTSTRAP_SERVERS=127.0.0.1:9092 
-jar zipkin.jar --STORAGE_TYPE=mysql --MYSQL_HOST=120.77.213.159
--MYSQL_TCP_PORT=3307 --MYSQL_USER=root --MYSQL_PASS=abc123456
--MYSQL_DB=imooc_e_commerce

3.Spring cloud Alibaba中写入配置文件

在你需要ZinKin的微服务中的pom.xml中引入

1
2
3
4
5
6
7
8
9
10
html复制代码 
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>

在配置文件中引入ZipKin配置

1
2
3
4
5
6
7
8
9
10
11
12
13
yml复制代码spring:
kafka:
bootstrap-servers: 127.0.0.1:9092
producer:
retries: 3
consumer:
auto-offset-reset: latest
sleuth:
sampler:
# 抽样策略
probability: 1.0 #采样比例,1.0表示100% ,默认0.1
# 限速采集,Spring.sleuth.sampler.probability 属性值无效
rate: 100 #美妙间隔接受的trace量

最后阶段,测试

在你引入的zipkin服务中,随意发送几个请求
然后打开: http://127.0.0.1:9411/zipkin/

可以看你到

image.png

链路数据和日志都已经显示出来了,到这一步就成功了~

本文转载自: 掘金

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

记一次线上崩溃问题的排查过程

发表于 2021-11-10

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

关注公众号【高性能架构探索】,回复【pdf】,免费获取计算机经典书籍

大家好,我是雨乐!

前几天,突然收到报警,线上服务崩溃,然后自动重启。

由于正值双十一期间,业务以稳定为主,线上服务崩溃,这可不是一件小事,赶紧登陆线上服务器,分析原因,迅速解决。

借助这篇文章,记录下整个崩溃的分析和解决过程。

收到报警

上午上班后,正在划水,突然收到邮件报警,如下: 报警邮件

问题分析

马上登录线上服务器,gdb调试堆栈信息。

堆栈信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rust复制代码#0  0x0000003ab9a324f5 in raise () from /lib64/libc.so.6
#1  0x0000003ab9a33cd5 in abort () from /lib64/libc.so.6
#2  0x0000003abcebea8d in __gnu_cxx::__verbose_terminate_handler() () from /usr/lib64/libstdc++.so.6
#3  0x0000003abcebcbe6 in ?? () from /usr/lib64/libstdc++.so.6
#4  0x0000003abcebcc13 in std::terminate() () from /usr/lib64/libstdc++.so.6
#5  0x0000003abcebcd32 in __cxa_throw () from /usr/lib64/libstdc++.so.6
#6  0x00000000006966bf in Json::throwRuntimeError(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) ()
#7  0x0000000000681019 in Json::Reader::readValue() ()
#8  0x000000000068277c in Json::Reader::readArray(Json::Reader::Token&) ()
#9  0x0000000000681152 in Json::Reader::readValue() ()
#10 0x00000000006823a6 in Json::Reader::readObject(Json::Reader::Token&) ()
#11 0x00000000006810f5 in Json::Reader::readValue() ()
#12 0x0000000000680e6e in Json::Reader::parse(char const*, char const*, Json::Value&, bool) ()
#13 0x0000000000680c52 in Json::Reader::parse(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Json::Value&, bool) ()
......

在上面堆栈信息中可以看到在调用Json::Reader::parse后经过Json::Reader::readValue等调用,最后再调用Json::Reader::readValue时调用Json::throwRuntimeError抛出异常。

查看调用Json::throwRuntimeError函数的地方:

1
2
3
4
5
6
7
8
9
css复制代码src/lib_json/json_writer.cpp:    throwRuntimeError("commentStyle must be 'All' or 'None'");
src/lib_json/json_reader.cpp:  if (stackDepth_g >= stackLimit_g) throwRuntimeError("Exceeded stackLimit in readValue().");
src/lib_json/json_reader.cpp:  if (stackDepth_ >= features_.stackLimit_) throwRuntimeError("Exceeded stackLimit in readValue().");
src/lib_json/json_reader.cpp:    if (name.length() >= (1U<<30)) throwRuntimeError("keylength >= 2^30");
src/lib_json/json_reader.cpp:    throwRuntimeError(errs);
src/lib_json/json_value.cpp:    throwRuntimeError(
src/lib_json/json_value.cpp:    throwRuntimeError(
src/lib_json/json_value.cpp:JSONCPP_NORETURN void throwRuntimeError(JSONCPP_STRING const& msg)
src/lib_json/json_valueiterator.inl:  throwRuntimeError("ConstIterator to Iterator should never be allowed.");

进入对应的函数

1
2
3
4
5
6
7
scss复制代码bool Reader::readValue() {
  if (stackDepth_g >= stackLimit_g) throwRuntimeError("Exceeded stackLimit in readValue().");
  ++stackDepth_g;
  ... ...
  --stackDepth_g;
  return successful;
}

发现,在满足条件

1
复制代码stackDepth_g >= stackLimit_g

的时候,会调用throwRuntimeError,那么分析下stackDepth_g和stackLimit_g的声明定义:

1
2
ini复制代码static int const stackLimit_g = 1000;
static int       stackDepth_g = 0;

问题基本明了:

❝

stackDepth_g是个静态全局变量,线程不安全,而出问题的服务是多线程的

❞

在此准备吐槽下,笔者使用jsoncpp对象的时候,都是在线程内部一个局部变量,因此不会存在多线程访问同一个局部jsoncpp对象的时候,因此确定就是因为全局变量多线程访问导致的。一个开源的项目,里面竟然有全局变量,这在规范里面是不被允许的。

然后谷歌搜索了下大家都有过类似的问题,再次吐槽下。

问题解决

解决崩溃问题,首先需要看看是不是使用方式的问题,或者找一个线程安全的接口,再或者用其他库进行替换。

修改jsoncpp源码

为了解决线程安全的问题,有两种方案: 1、在操作全局变量的时候,加上mutex,这个无非对性能要求很高的业务一个致命打击,为了提高业务性能,所以内部锁都使用其他方式进行了优化,比如mutex使用双buffer方式进行了替换,虽然mutex的一个加锁解锁过程也就100ns。

2、将上述全局变量放入Json对象中,这样局部变量就不会存在崩溃现象,但是这种方案存在一个问题,就是改动点很大,且需要大量严格的测试,放弃。

所以综合考虑上述两点,决定采用其他更安全可靠的方式来解决线上崩溃问题。

使用rapidjson

之所以采用rapidjson,是因为线上几十个服务,大部分都使用rapidjson,只有线上崩溃的这个服务等少数几个服务,因为历史原因,用的jsoncpp。

先介绍下rapidjson,下述内容来自于rapidjson官网:

  • RapidJSON 是一个 C++ 的 JSON 解析器及生成器。它的灵感来自 RapidXml。
  • RapidJSON 小而全。它同时支持 SAX 和 DOM 风格的 API。SAX 解析器只有约 500 行代码。
  • RapidJSON 快。它的性能可与 strlen() 相比。可支持 SSE2/SSE4.2 加速。
  • RapidJSON 独立。它不依赖于 BOOST 等外部库。它甚至不依赖于 STL。
  • RapidJSON 对内存友好。在大部分 32/64 位机器上,每个 JSON 值只占 16 字节(除字符串外)。它预设使用一个快速的内存分配器,令分析器可以紧凑地分配内存。
  • RapidJSON 对 Unicode 友好。它支持 UTF-8、UTF-16、UTF-32 (大端序/小端序),并内部支持这些编码的检测、校验及转码。例如,RapidJSON 可以在分析一个 UTF-8 文件至 DOM 时,把当中的 JSON 字符串转码至 UTF-16。它也支持代理对(surrogate pair)及 “\u0000”(空字符)。

不过rapidjson为了性能,在使用上面需要极其小心。

❝

笔者之前踩过类似坑,局部字符串赋值给rapidjson对象,结果rapidjson并没有马上使用该局部字符串,而是在最后才会访问局部字符串里面的内容,而此时,局部字符串早已出了作用域,导致rapidjson获取的内容是乱码。

❞

结语

在使用开源项目的时候,一定要做好调研,必要的时候,能过一下源码实现(这个有点难😁),否则很容易入坑。

笔者在使用libcurl作为httpclient的时候,也因为触发了libcurl的一个bug,导致线上崩溃,当时连续通宵了两个晚上,才解决。

一入C++深似海,从此XX是路人。

以候捷在<<STL 源码剖析>>上的一句话作为本文的结束语:

❝

源码面前,了无秘密。

❞

共勉。

本文转载自: 掘金

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

阿里面试官问我:如何设计登录接口,十分钟内连续登录5次失败,

发表于 2021-11-10

2022年1月7日更新:
既然大家的讨论热情这么高,我觉得就有必要把这篇文章再好好完善一下,评论区里大家给出的比较一致的实现方式是使用redis,我就把实现的思路也加进来吧。


part 1 面试白板编程实现(原生JDK)

  常言道:字数越短问题越大。

  今天阿里的面试官小哥哥让我实现一个登录接口,同一个用户10分钟内连续登陆5次失败,则需要等到30分钟才能登陆。

  当然大佬估计一看到这种题目会很难过,一丁点算法都没有,妙解没意思。我上来就被唬住了。登录接口?10分钟内连续5次??等待30分钟才能登陆???登陆验证????

  问号一下子就冒出来了,当然最开始我想定义一个变量firstFailTime来记录第一次失败的时间,再仔细一想不对啊,firstFailTime是动态的额,要不断变化,单一个变量不好实现啊,第一次登录失败可以记录,但如果出现前十分钟失败了4次,第11分钟又失败了一次的话,firstFailTime应该往后取第二次失败登录的时间啊,我总不能手动定义100个变量吧。。。面试官看到估计脸都绿了。恨不得给我一个Mysql数据表,把每次登陆都给存下来,这样就可以很方便的查出某个时间区间登陆的情况。

  不慌,咱们虽然不是大佬,但一点一点分析还是可以的,沉住气!等等,刚刚说到数据库存所有的登录数据??其实思考到上面已经快接近了,我不能手动创建100个变量,但我可以用一种数据结构依次记录登录失败的时间啊,突然想到LRU算法对不对!!能从数据顺序看出来时间顺序的数据结构不就是链表吗!!!还有登录验证的问题,不如偷个懒,用一个boolean控制。解决,cool~

P.S:我没考虑开多个线程去测试,因为我个人感觉用户登录不会出现在高并发的环境里,几万个人同时登陆同一个账号想想就离谱……但为了保险起见我还是给map加了synchronize关键字。


Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
java复制代码package exam;

import java.util.LinkedList;

/**
* Created by Enzo Cotter on 2021/3/10.
*/
public class Person {
/**
* 重置时间
*/
private static final int RESET_TIME = 30;

/**
* 密码连续输入5次失败的持续时间
*/
private static final int DURATION = 10;

/**
* 最大输入失败次数
*/
private static final int MAX_TIMES = 5;

/**
* 用户id
*/
private String id;

/**
* 登录失败次数
*/
private int failCount;

/**
* 第一次失败的时间
*/
private long firstFailTime;

/**
* 登录失败的时间
*/
private LinkedList<Long> times;

private boolean lock;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public int getFailCount() {
return failCount;
}

public void setFailCount(int failCount) {
this.failCount = failCount;
}

public long getFirstFailTime() {
return firstFailTime;
}

public void setFirstFailTime(long firstFailTime) {
this.firstFailTime = firstFailTime;
}

public LinkedList<Long> getTimes() {
return times;
}

public void setTimes(LinkedList<Long> times) {
this.times = times;
}

public Person() {
}

public Person(String id, int failCount, long firstFailTime, LinkedList<Long> times, boolean lock) {
this.id = id;
this.failCount = failCount;
this.firstFailTime = firstFailTime;
this.times = times;
this.lock = false;
}

/**
* 密码输错了进入此方法
*/
public void isValid(){

long thisTime = System.currentTimeMillis() / 1000;

System.out.println("第一次登录失败时间" + thisTime);

// 超过30分钟,重置
if(thisTime > firstFailTime + RESET_TIME){
this.failCount = 1;
firstFailTime = thisTime;
times = new LinkedList<>();
times.addLast(thisTime);
this.lock = false;
return;
}else{ // 没有超过30分钟

if (lock){
System.out.println("账户锁定,请" + RESET_TIME + "分钟后再来");
return;
}

// 之前记录的第一次登录失败时间在10分钟之前了,要换
while(!times.isEmpty() && thisTime > times.getFirst() + DURATION){
times.removeFirst();
this.failCount --;
this.firstFailTime = times.isEmpty() ? thisTime : times.getFirst();
}

if(this.failCount >= 5 && thisTime < firstFailTime + DURATION){
System.out.println("10分钟内密码错误大于等于5次,登录失败");
times.addLast(thisTime);
this.lock = true;
}else if(failCount < MAX_TIMES){
this.failCount ++;
System.out.println("密码错误" + this.failCount + "次");
times.addLast(thisTime);
}
}
}
}

主类:

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
java复制代码package exam;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
* Created by Enzo Cotter on 2021/3/10.
*/
public class FlowLimit {

private static Map<String, Person> map = new HashMap<>();

/**
* 登录
* @param id
* @param flag 是否成功
*/
public static void login(String id, boolean flag){
if (flag){
// 登陆成功
return;
}else{
Person p = null;
// 登录失败
synchronized (map) {
p = map.get(id);
if (p == null){
p = new Person(id, 0, System.currentTimeMillis() / 1000,
new LinkedList<>(), false);
map.put(id, p);
return;
}
p.isValid();
}
}
}

public static void main(String[] args) {
for(int i = 0; i < 20; i ++){
login("aaa", false);
}
}
}

part 2 使用缓存来实现

实现步骤:

  1. 用户发起登录请求
  2. 后台验证是否失败次数过多,账户没有锁定的话就进入下面的步骤;否则直接返回
  3. 验证用户的账号 + 密码

3.1 验证成功:删除缓存
3.2 验证失败:统计最近10分钟时间窗口内的失败次数,如果达到5次则设置锁定缓存,返回

图解实现步骤:

image.png

代码实现细节:

  • 登录失败计数器的key设计为:一串字符串 + 用户名(假设具有唯一性)+ 登录失败的时间
  • 锁定登录操作的key设计为:一串字符串 + 用户名(假设具有唯一性)
1
2
3
4
5
java复制代码private static final String FAIL_COUNT_REDIS_KEY = "login_fail_count";

private static final String LOCK_REDIS_KEY = "login_lock";

private static final String SEPARATOR = ":";

用户登录服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Override
public String login(String username, String password) {
// 验证用户是否被登录锁定
boolean lock = isForbidden(username);
if (lock) {
return "Login authentication failed too many times. Please try again after " + unLockTime(username) + " minutes.";
}
// 验证用户名 + 密码
boolean isLogin = userRepository.checkUsernameAndPassword(username, password);
if (!isLogin) {
// 登录失败
setFailCounter(username);
return "login fail";
}
// 登录成功 移除失败计数器
deleteFilCounter(username);
return "login success";
}

登陆失败的话,就给登录失败次数加一:

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
java复制代码@Override
public void setFailCounter(String username) {
// 获取当前时间
Calendar cal = Calendar.getInstance();
String minute = fastDateFormat.format(cal);

// 登录失败次数 + 1
String key = String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username, minute);
Integer count = redisTemplate.opsForValue().get(key);
redisTemplate.opsForValue().increment(key, 1); // 如果key不存在的话就会以增量形式存储进来

if (count == null) {
redisTemplate.expire(key, 10, TimeUnit.MINUTES);
}

// 如果失败次数大于5次,锁定账户
List<String> windowsKeys = new ArrayList<>();
for (int i = 0; i < 10; i ++) {
windowsKeys.add(String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username, fastDateFormat.format(cal)));
cal.add(Calendar.MINUTE, -1);
}
List<Integer> countList = redisTemplate.opsForValue().multiGet(windowsKeys);

assert countList != null;

int total = 0;
for (Integer c : countList) {
total += c;
}
if (total >= maxFailTimes) {
forbidden(username);
}
}

如果登录成功,则删除失败次数计数器:

1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public void deleteFilCounter(String username) {
Calendar cal = Calendar.getInstance();
List<String> windowKeys = new ArrayList<>();
for (int i = 0; i < 10; i ++) {
windowKeys.add(String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username, fastDateFormat.format(cal)));
cal.add(Calendar.MINUTE, -1);
}
redisTemplate.delete(windowKeys);
}

失败次数超过5次则禁止登录,只需要设置一个缓存即可:

1
2
3
4
5
java复制代码@Override
public void forbidden(String username) {
String key = String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username);
redisTemplate.opsForValue().set(key, 1, 30, TimeUnit.MINUTES);
}

判断是否被禁止登录,只需要判断是否有上面方法设置的key即可:

1
2
3
4
5
6
7
8
9
java复制代码@Override
public boolean isForbidden(String username) {
try{
return Boolean.TRUE.equals(redisTemplate.hasKey(String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username)));
}catch (Exception e){
e.printStackTrace();
}
return false;
}

如果想要获取到用户具体需要几分钟才能解锁(用于提示信息),只需要查询缓存的过期时间:

1
2
3
4
5
6
7
8
java复制代码private Long unLockTime(String username){
String key = String.join(SEPARATOR, LOCK_REDIS_KEY, username);
Long expireTime = redisTemplate.opsForValue().getOperations().getExpire(key, TimeUnit.MINUTES);
if (expireTime == null){
throw new RuntimeException("there is no unlock time");
}
return expireTime;
}

本文转载自: 掘金

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

23种设计模式之建造者模式

发表于 2021-11-10

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

基本介绍

1)建造者模式(Builder Pattern)又叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

2)建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。

传统方式解决盖房需求问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public abstract class AbstractHouse {

public abstract void buildBasic();

public abstract void buildWalls();

public abstract void roofed();

public void build(){
buildBasic();
buildWalls();
roofed();
}

}

分析

1)优点是比较好理解,简单易操作。

2)设计的程序结构,过于简单,没有设计缓存层对象,程序的扩展和维护不好.也就是说,这种设计方案,把产品(即:房子)和创建产品的过程(即:建房子流程)封装在一起,耦合性增强了。

3)解决方案:将产品和产品建造过程==解耦===>建造者模式.

建造者模式的四个角色

  1. Product(产品角色):一个具体的产品对象。
  1. Buiider(抽象建造者):创建一个Product对象的各个部件指定的接口。
  2. ConcreteBuilder(具体建造者):实现接口,构建和装配各个部件。
  3. Director(指挥者):构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。

umi类图

image-20211109173907834

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
java复制代码public abstract class HouseBuilder {

protected House house=new House();

public abstract void buildBasic();

public abstract void buildWalls();

public abstract void roofed();

// 建造房子后返回实例
public House builHouse(){

return house;
}

}

public class CommonHouse extends HouseBuilder {
@Override
public void roofed() {

house.setRoofed("红木屋顶");
System.out.println("普通方子有红木屋顶");
}
}


public class HouseDirector {

private HouseBuilder houseBuilder;

HouseDirector(HouseBuilder houseBuilder){
setHouseBuilder(houseBuilder);
}

public void setHouseBuilder(HouseBuilder houseBuilder){

this.houseBuilder=houseBuilder;
}

public House createHouse(){

houseBuilder.buildBasic();
houseBuilder.buildWalls();
houseBuilder.roofed();
return houseBuilder.builHouse();
}
}

和工厂模式的区别

工厂是针对一组产品,建造者是一个产品的多个组件,维度不同

建造者模式在JDK的应用和源码分析

image-20211109193313886

image-20211109193326008

1
2
3
4
java复制代码public interface Appendable {

Appendable append(CharSequence csq) throws IOException;
}

源码中建造者模式角色分析

  1. Appendable接口定义了多个append方法(抽象方法),即Appendable为抽象建造者,定义了抽象方法
  2. AbstractStringBuilder 实现了Appendable接口方法,这里的AbstractStringBuilder已经是建造者,只是不能实例化
  3. StringBuilder即充当了指挥者角色,同时充当了具体的建造者,建造方法的实现是由AbstractStringBuilder完成,而StringBuilder继承了AbstractstringBuilder

建造者模式的注意事项和细节

1)客户端(使用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象

2)每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象

3)可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程

4)增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”

5)建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

6)如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化导致系统变得很庞大,因此在这种情况下,要考虑是否选择建造者模式.

7)抽象工厂模式Vs建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品

本文转载自: 掘金

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

LeetCode 165 比较版本号【c++/java详细

发表于 2021-11-10

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

1、题目

给你两个版本号 version1 和 version2 ,请你比较它们。

版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。

返回规则如下:

  • 如果 version1 > version2 返回 1,
  • 如果 version1 < version2 返回 -1,
  • 除此之外返回 0。

示例 1:

1
2
3
arduino复制代码输入:version1 = "1.01", version2 = "1.001"
输出:0
解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"

示例 2:

1
2
3
ini复制代码输入:version1 = "1.0", version2 = "1.0.0"
输出:0
解释:version1 没有指定下标为 2 的修订号,即视为 "0"

示例 3:

1
2
3
arduino复制代码输入:version1 = "0.1", version2 = "1.1"
输出:-1
解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1" 。0 < 1,所以 version1 < version2

示例 4:

1
2
ini复制代码输入:version1 = "1.0.1", version2 = "1"
输出:1

示例 5:

1
2
ini复制代码输入:version1 = "7.5.2.4", version2 = "7.5.3"
输出:-1

提示:

  • 1 <= version1.length, version2.length <= 500
  • version1 和version2仅包含数字和 '.'
  • version1 和 version2 都是 有效版本号
  • version1 和 version2 的所有修订号都可以存储在 32 位整数 中

2、思路

(双指针) O(max(n,m))O(max(n,m))O(max(n,m))

比较两个版本号大小,版本号由修订号组成,中间使用'.'分隔,越靠近字符串前边,修订号的优先级越大。当v1 > v2时返回 1,当v1 < v2时返回 -1,相等时返回 0。

样例:


如样例所示,v1= 1.02.3, v2 = 1.02.2,前两个修订号都相等,v1的第三个修订号大于v2的第三个修订号,因此v1 > v2,返回1。下面来讲解双指针的做法。

我们使用两个指针i和j分别指向两个字符串的开头,然后向后遍历,当遇到小数点'.'时停下来,并将每个小数点'.'分隔开的修订号解析成数字进行比较,越靠近前边,修订号的优先级越大。根据修订号大小关系,返回相应的数值。

实现细节:

1
2
c复制代码// 将一段连续的字符串转换成数字 
while(i < v1.size() && v1[i] != '.') num1 = num1 * 10 + v1[i++] - '0';

这样做可以直接去前导0,同时将字符串转换成数字也便于比较大小。

具体过程如下:

  • 1、定义两个指针 i和j,初始化i = 0,j = 0。
  • 2、两个指针分别遍历两个字符串,将每个小数点'.'分隔开的修订号解析成数字,并进行大小比较:
    • 如果 num1 > num2,返回 1;
    • 如果 num1 < num2,返回 -1;
  • 3、i++,j++,两个指针都后移一步,进行下一轮的修订号解析比较。
  • 4、如果遍历完两个字符串都没有返回相应结果,说明两个字符串相等,返回0。

时间复杂度分析: 两个字符串各遍历一遍,因此时间复杂度为 O(max(n,m))O(max(n,m))O(max(n,m)) ,n和m分别是两个字符串的长度。

3、c++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码class Solution {
public:
int compareVersion(string v1, string v2) {
int i = 0, j = 0;
while(i < v1.size() || j < v2.size())
{
int num1 = 0, num2 = 0;
while(i < v1.size() && v1[i] != '.') num1 = num1 * 10 + v1[i++] - '0';
while(j < v2.size() && v2[j] != '.') num2 = num2 * 10 + v2[j++] - '0';
if(num1 > num2) return 1;
else if( num1 < num2) return -1;
i++,j++;
}
return 0;
}
};

4、java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码class Solution {
public int compareVersion(String v1, String v2) {
int i = 0, j = 0;
int n = v1.length(), m = v2.length();
while(i < n || j < m)
{
int num1 = 0, num2 = 0;
while(i < n && v1.charAt(i) != '.') num1 = num1 * 10 + v1.charAt(i++) - '0';
while(j < m && v2.charAt(j) != '.') num2 = num2 * 10 + v2.charAt(j++) - '0';
if(num1 > num2) return 1;
else if( num1 < num2) return -1;
i++; j++;
}
return 0;
}
}

原题链接: 165. 比较版本号
在这里插入图片描述

本文转载自: 掘金

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

1…379380381…956

开发者博客

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