-
下拉列表(下拉框)可以确保用户仅从预先给定的选项中进行选择,这样不仅能减少数据输入错误,还能节省时间提高效率。在MS Excel中,我们可以通过 “数据验证” 提供的选项来创建下拉列表,但如果要在Java程序中通过代码实现这一功能,可能需要借助一些第三方库。本文将分享两种使用免费Java库在Excel中创建下拉列表的方法。操作Excel的免费Java控件:Free Spire.XLS for Java. (下载后手动引入jar包或者通过Maven仓库安装均可)方法一:基于字符串数组中的值来创建Excel下拉列表该方法是通过 Free Spire.XLS for Java 提供的 IDataValidation 接口下的 setValue() 方法先定义一个字符串数组作为列表项,然后再通过将 isSuppressDropDownArrow() 方法的参数设置为false 来应用下拉箭头。该方法就等同于在Excel “数据验证” 选项中的 “来源” 中直接输入一串内容作为列表项。Java代码:import com.spire.xls.*;import java.awt.*; public class ExcelDropdownList { public static void main(String[] args) { //创建Workbook对象 Workbook workbook = new Workbook(); //获取第一张工作表 Worksheet sheet = workbook.getWorksheets().get(0); //在指定单元格中添加文本 sheet.getCellRange("B2").setValue("职员"); sheet.getCellRange("B3").setValue("张三"); sheet.getCellRange("C2").setValue("部门"); //设置字体和单元格样式 sheet.getCellRange("B2:C2").getStyle().getFont().isBold(true); sheet.getCellRange("B2:C2").getStyle().getFont().setColor(Color.BLUE); sheet.getCellRange("B2:C3").getStyle().getFont().setSize(11); sheet.getCellRange("B2:C3").setRowHeight(18); sheet.getCellRange("B2:C3").setColumnWidth(12); //设置下拉列表的值 sheet.getCellRange("C3").getDataValidation().setValues(new String[]{"财务部", "采购部", "销售部", "行政部"}); //在指定单元格中创建下拉列表 sheet.getCellRange("C3").getDataValidation().isSuppressDropDownArrow(false); //保存结果文件 workbook.saveToFile("Excel下拉列表.xlsx", ExcelVersion.Version2013); }}方法二:基于单元格区域的内容来创建Excel下拉列表该方法是通过 Validation 类的 setDataRange() 方法指定一个单元格区域中的内容作为下拉列表的数据源。该方法更加灵活,创建后如果你想更新下拉列表的选项,直接更新单元格中的数据即可。效果如图:Java代码:import com.spire.xls.*;import java.awt.*; public class DropdownList { public static void main(String[] args) { //创建Workbook对象 Workbook workbook = new Workbook(); //获取第一张工作表 Worksheet sheet = workbook.getWorksheets().get(0); //在指定单元格中添加文本 sheet.getCellRange("B2").setValue("职员"); sheet.getCellRange("B3").setValue("张三"); sheet.getCellRange("C2").setValue("部门"); sheet.getCellRange("A11").setValue("财务部"); sheet.getCellRange("A12").setValue("采购部"); sheet.getCellRange("A13").setValue("销售部"); sheet.getCellRange("A14").setValue("行政部"); //设置字体和单元格样式 sheet.getCellRange("B2:C2").getStyle().getFont().isBold(true); sheet.getCellRange("B2:C2").getStyle().getFont().setColor(Color.BLUE); sheet.getCellRange("B2:C3").getStyle().getFont().setSize(11); sheet.getCellRange("B2:C3").setRowHeight(18); sheet.getCellRange("B2:C3").setColumnWidth(12); //将指定的单元格区域的内容作为数据源来创建下拉列表 sheet.getCellRange("C3:C4").getDataValidation().setDataRange(sheet.getCellRange("A11:A14")); //保存结果文件 workbook.saveToFile("Excel下拉列表2.xlsx", ExcelVersion.Version2013); }}转载自https://www.cnblogs.com/Yesi/p/18051989
-
在处理PDF文档时,有时需要为文档中的每一页添加页眉和页脚,以包含一些有用的信息,如文档标题、章节名称、日期、页码等。对于需要自动化处理的场景,或者需要在大量文档中添加一致的页眉和页脚,可以通过编程的方式来实现。本文将介绍如何使用Java为PDF文件添加页眉、页脚。所需工具:Free Spire.PDF for Java 免费库。可以点击链接下载最新版本并手动添加引用到你的Java项目中,同时也支持通过Mave仓库安装。方法简介:在以下示例中,我们用到了 PdfCanvas 类的DrawString()、DrawImage() 和 DrawLine() 方法,分别用于在PDF页面上指定位置绘制文本、图片和线条。如果要添加动态的信息如页码、页数、章节编号等,则可以使用Free Spire.PDF for Java提供的PdfPageNumberField、PdfPageCountField 和 PdfSectionNumberField 类,然后再使用Draw() 方法将其绘制到PDF页眉或页脚位置。示例代码:1. 使用Java在PDF中插入页眉import com.spire.pdf.PdfDocument;import com.spire.pdf.PdfPageBase;import com.spire.pdf.graphics.*; import java.awt.*; public class PDFHeader { public static void main(String[] args) { //加载PDF文档 PdfDocument pdf = new PdfDocument(); pdf.loadFromFile("考核.pdf"); //加载图片 PdfImage headerImage = PdfImage.fromFile("logo.jpg"); //获取图片宽度 float width = headerImage.getWidth(); PdfUnitConvertor unitCvtr = new PdfUnitConvertor(); float pointWidth = unitCvtr.convertUnits(width, PdfGraphicsUnit.Pixel, PdfGraphicsUnit.Point); //指定页眉文本 String headerText = "年度绩效考核\nAAA有限责任公司"; //创建字体 PdfTrueTypeFont font = new PdfTrueTypeFont(new Font("宋体", Font.BOLD, 12),true); //创建画笔 PdfBrush brush = PdfBrushes.getRed(); PdfPen pen = new PdfPen(PdfBrushes.getBlack(), 1.0f); //遍历所有PDF页面 for (int i = 0; i < pdf.getPages().getCount(); i++) { //获取指定页面 PdfPageBase page = pdf.getPages().get(i); //在顶部空白区域绘制图像 page.getCanvas().drawImage(headerImage, page.getActualSize().getWidth() - pointWidth - 55, 20); //在顶部空白区域绘制文本 page.getCanvas().drawString(headerText, font, brush, 55, 30); //在顶部空白区域绘制一条线 page.getCanvas().drawLine(pen, new Point(55, 70), new Point((int)page.getActualSize().getWidth() - 55, 70)); } //保存生成PDF文件 pdf.saveToFile("PDF页眉.pdf"); pdf.dispose(); }}以上代码遍历了PDF文档的所有页面,并在每一页的页眉位置添加了文本、图片和分隔线,运行后效果图如下:2. 使用Java在PDF中插入页脚import com.spire.pdf.PdfDocument;import com.spire.pdf.PdfPageBase;import com.spire.pdf.automaticfields.PdfCompositeField;import com.spire.pdf.automaticfields.PdfPageCountField;import com.spire.pdf.automaticfields.PdfPageNumberField;import com.spire.pdf.graphics.*; import java.awt.*;import java.awt.geom.Dimension2D;import java.awt.geom.Point2D; public class PDFFooter { public static void main(String[] args) { //加载PDF文档 PdfDocument pdf = new PdfDocument(); pdf.loadFromFile("考核.pdf"); //创建字体 PdfTrueTypeFont font = new PdfTrueTypeFont(new Font("宋体", Font.BOLD, 12),true); //创建画笔 PdfBrush brush = PdfBrushes.getBlack(); PdfPen pen = new PdfPen(PdfBrushes.getBlack(), 1.0f); //创建页码字段 PdfPageNumberField pageNumberField = new PdfPageNumberField(); //创建页数字段 PdfPageCountField pageCountField = new PdfPageCountField(); //创建一个复合字段,将页数字段和页码字段合并为一个字符串 PdfCompositeField compositeField = new PdfCompositeField(font, brush, "第{0}页/共{1}页", pageNumberField, pageCountField); //获取文本尺寸 Dimension2D fontSize = font.measureString(compositeField.getText()); //获取页面尺寸 Dimension2D pageSize = pdf.getPages().get(0).getSize(); //设置复合字段的位置 compositeField.setLocation(new Point2D.Double((pageSize.getWidth() - fontSize.getWidth())/2, pageSize.getHeight() - 45)); //遍历PDF页面 for (int i = 0; i < pdf.getPages().getCount(); i++) { //获取指定页 PdfPageBase page = pdf.getPages().get(i); //在底部空白区域绘制一条线 page.getCanvas().drawLine(pen, new Point(72, (int) (pageSize.getHeight() - 60)), new Point((int)pageSize.getWidth() - 72, (int) pageSize.getHeight()- 60)); //在底部空白区域绘制复合字段 compositeField.draw(page.getCanvas()); } //保存生成PDF文件 pdf.saveToFile("PDF页脚.pdf"); pdf.dispose(); }}以上代码遍历了PDF文档的所有页面,并在每一页的页脚位置添加了对应的页码、页数信息以及一条分隔线,运行后效果图如下:小结:在不破坏PDF文档布局的情况下,我们可以使用Free Spire.PDF for Java免费库将所需的元素绘制到PDF页面顶部和底部的指定位置来生成自定义页眉页脚,提高PDF文档的可读性和专业性。转载自https://www.cnblogs.com/Yesi/p/18094584
-
JAVA实现树的遍历
-
1.日志系统概述 关于日志系统,其要支撑的核心能力无非是日志的存储以及查看,最好的查看方式当然是实现可视化。目前市面上有成熟的解决方案——ELK,即elastic search+logstash+kibana。前文中我们已经聊过了ELK这条线,本文主要就是基于ELK并在其中加一个MQ作为中间层来流量削峰、异步写日志。 这里首先要声明的是,虽然本文在日志系统中使用到了MQ,但MQ真的是必要的嘛? 这个要看系统的体量了。除非是超大型的分布式架构,服务上百个并且并发量较高,才会考虑用MQ来做一层缓存从而来降低IO压力。如果不是上述情况的话是没有必要上MQ来做一个中间层的。日志作为系统中掺入的"沙子",其量本来就不会很大,一次API调用平均能产生一条日志吗?其实是不见的是吧。所以就这点数据量上MQ这种吞吐量的中间层简直就是杀鸡用牛刀,过度设计,徒增了系统的复杂度了。MQ更多的时候是拿来做移步任务或者定时任务的,用来做业务上的流量削峰或者异步的去做些事情。比如异步的下订单、订单超时取消等。绝大多数时候我们的日志系统的架构,直接让存储去直面日志IO都是能轻轻松松顶得住的。所谓的让存储去直面日志的IO是什么意思?就是比如我走了ELK这条线,那么就直接讲日志往es里面丢就对了。ELK这么用前面已经有文章介绍过了。本文还是聊一聊假设真的到了很极限的中间需要引入MQ的情况。 ELK的搭建这里就不赘述了,前面有文章详细聊过: https://bugman.blog.csdn.net/article/details/135964825?spm=1001.2014.3001.5502 https://bugman.blog.csdn.net/article/details/136017853?spm=1001.2014.3001.5502 https://bugman.blog.csdn.net/article/details/136066171?spm=1001.2014.3001.5502 这里我们只需要关注几个点: 应用的日志如何推到mq中 logstash如何去取mq中存放的日志 2.环境搭建 ELK相关内容: MQ我们选择rabbitMQ,作为一个开箱即食的MQ,rabbitMQ的下载安装网上文章车载斗量,此处就不赘述了。 3.应用如何推日志到MQ 写日志肯定是JAVA的日志框架来负责的,前面有文章已经详细的介绍了JAVA的日志框架: 【JAVA日志框架】JUL,JDK原生日志框架详解。_jul jdk-CSDN博客 JAVA的日志框架总的来说架构都是大同小异的,都是由不同的appender(有的里面叫handler其实都是一个东西)来向不同的地方写日志: 既然要往rabbitMQ里面写日志,那当然就要一个rabbitMQ的appender了。这个appender在哪里?在rabbitMQ的JAVA API依赖中: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> </dependency> 然后配置一下日志框架的配置文件即可,这里我们以spring boot默认的日志框架logback为例,在其配置文件中配置好rabbitMQ的appender即可: <configuration> <!-- 定义 RabbitMQ 连接 --> <appender name="RABBIT" class="com.github.logback.amqp.AmqpAppender"> <host>localhost</host> <!-- RabbitMQ 主机地址 --> <port>5672</port> <!-- RabbitMQ 端口 --> <username>guest</username> <!-- RabbitMQ 用户名 --> <password>guest</password> <!-- RabbitMQ 密码 --> <exchange>logs</exchange> <!-- RabbitMQ 交换机 --> <routingKey>logstash</routingKey> <!-- RabbitMQ 路由键 --> <declareExchange>true</declareExchange> <!-- 是否声明交换机 --> <exchangeType>fanout</exchangeType> <!-- 交换机类型 --> <durable>true</durable> <!-- 是否持久化消息 --> <applicationId>myApplication</applicationId> <!-- 应用程序标识 --> <!-- 其他可选配置 --> <!--<declareQueue>true</declareQueue>--> <!--<queue>logQueue</queue>--> <!--<declareBinding>true</declareBinding>--> </appender> <!-- 定义日志输出格式 --> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern> </layout> <!-- 根日志输出到 RabbitMQ --> <root level="INFO"> <appender-ref ref="RABBIT"/> </root> </configuration> 4.logstash如何去MQ中取日志 logstash的input可以理解为插件,既然是插件当然就有很多中类型,其中就包括rabbitMQ的(自然也有其它的),下面是logstash从MQ中取数据然后推给es的一份示例: input { rabbitmq { host => "localhost" # RabbitMQ 主机地址 port => 5672 # RabbitMQ 端口 user => "guest" # RabbitMQ 用户名 password => "guest" # RabbitMQ 密码 queue => "logQueue" # RabbitMQ 队列名 durable => true # 是否持久化队列 ack => true # 是否需要手动确认消息 threads => 1 # 线程数 } } output { stdout { codec => rubydebug } # 输出到控制台,可选 elasticsearch { hosts => ["localhost:9200"] # Elasticsearch 主机地址 index => "logstash-%{+YYYY.MM.dd}" # Elasticsearch 索引名 } } 5.如何兼顾分布式链路追踪 这里顺带讨论一个问题,就是在ELK体系中如何去实现分布式链路跟踪。分布式链路跟踪相关内容前面有文章详细讨论过: https://bugman.blog.csdn.net/article/details/135258207?spm=1001.2014.3001.5502 https://bugman.blog.csdn.net/article/details/135258207?spm=1001.2014.3001.5502 其实在ELK中实现分布式链路追踪的方式很简单,思路如下: 仍然在应用侧上链路追踪技术来统一日志格式,然后要进行查询追踪的时候直接使用Kibana的搜索和过滤功能来仅显示与特定跟踪ID或请求ID相关的日志消息,或者利用Kibana的图表功能,将日志数据与分布式追踪数据结合起来,创建可视化的图表和仪表板。你可以根据需要显示请求的整个路径、每个步骤的响应时间、错误率等指标。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/Joker_ZJN/article/details/136357635
-
请问按照要求修改了java目录为src\com\huawei\codecraft后,提交zip文件依然提示程序编译异常
-
大家好,二月的合集又来了,本次涵盖了java,linux,spirng诸多内容供大家学习。 1.Vue中的$nextTick有什么作用?【转】 https://bbs.huaweicloud.com/forum/thread-02109144317537776031-1-1.html 2.基于Python的地图绘制教程【转】 https://bbs.huaweicloud.com/forum/thread-0240144319944756030-1-1.html 3.hcache 介绍(1)--Ehcache 功能特性 【转】 https://bbs.huaweicloud.com/forum/thread-0268144320319800023-1-1.html 4.Java 操作 XML(1)--DOM 方式处理 XML【转】 https://bbs.huaweicloud.com/forum/thread-02109144320463478034-1-1.html 5.Java操作XML(3)--StAX方式处理XML 【转】 https://bbs.huaweicloud.com/forum/thread-0240144320615655031-1-1.html 6.Java操作XML(4)--使用woodstox处理XML【转】 https://bbs.huaweicloud.com/forum/thread-0270144320690142025-1-1.html 7.Java操作XML(5)--使用JDOM处理XML【转】 https://bbs.huaweicloud.com/forum/thread-0240144320817429032-1-1.html 8.Java操作XML(6)--使用dom4j处理XML【转】 https://bbs.huaweicloud.com/forum/thread-02101144320893290020-1-1.html 9.基于原生Go语言开发一个博客系统【转】 https://bbs.huaweicloud.com/forum/thread-0269144558086016006-1-1.html 10.详解Golang如何使用Debug库优化代码【转】 https://bbs.huaweicloud.com/forum/thread-0225144558556243005-1-1.html 11.Python3中的指针你了解吗【转】 https://bbs.huaweicloud.com/forum/thread-0274144558593208011-1-1.html 12.Python绘图实现坐标轴共享与复用详解【转】 https://bbs.huaweicloud.com/forum/thread-02104144558721772010-1-1.html 13.使用Golang开发一个简易版shell【转】 https://bbs.huaweicloud.com/forum/thread-0226144559187797004-1-1.html 14.python导入其它py文件的实现步骤【转】 https://bbs.huaweicloud.com/forum/thread-0284144559442791006-1-1.html 15.Python property函数的具体使用【转】 https://bbs.huaweicloud.com/forum/thread-0259144559705810008-1-1.html 16.正则表达式的神奇世界之表达、匹配和提取全解析【转】 https://bbs.huaweicloud.com/forum/thread-02104144569438876012-1-1.html 17.正则去除中括号(符号)及里面包含的内容(最新推荐)【转】 https://bbs.huaweicloud.com/forum/thread-0226144569604407008-1-1.html 18.Javaweb项目启动Tomcat常见的报错解决方案【转】 https://bbs.huaweicloud.com/forum/thread-0274144571948983013-1-1.html 19.SpringBoot+Vue前后端分离实现审核功能的示例【转】 https://bbs.huaweicloud.com/forum/thread-0269144572025869007-1-1.html 20.Java中Collections.sort()排序方法举例详解【转】 https://bbs.huaweicloud.com/forum/thread-0269144572170830008-1-1.html 21.Java中回调函数 (callback) 及其实际应用场景【转】 https://bbs.huaweicloud.com/forum/thread-0259144572260172012-1-1.html 22.Redis实现商品秒杀的示例代码【转】 https://bbs.huaweicloud.com/forum/thread-0274144574350854015-1-1.html
-
前言回调函数在编程中是一种常见的设计模式,它允许一个函数在特定的时刻或条件下调用另一个函数。在Java中,我们可以通过接口和匿名内部类实现回调函数。本文将详细介绍Java中的回调函数,并提供相关代码示例。一、回调函数的概念回调函数是一种将函数作为参数传递给另一个函数的方法。当特定事件或条件发生时,被传递的函数将被调用。这种方式可以让我们在不修改原有代码的情况下,灵活地扩展和定制功能。这种设计模式在许多编程语言中都有应用,它的主要优点是提高了代码的模块化程度和可重用性。二、Java中的回调函数实现在Java中,我们可以通过接口和实现接口的类来实现回调函数。下面是一个简单的示例:定义一个回调接口:public interface Callback { void onCallback(String message); } 这个接口定义了一个名为onCallback的方法,该方法接受一个字符串作为参数。创建一个类,该类接受回调接口作为参数,并在特定条件下调用回调方法: public class Caller { private Callback callback; public Caller(Callback callback) { this.callback = callback; } public void doSomething() { // 执行一些操作... String message = "操作完成"; callback.onCallback(message); } } 在这个类中,我们定义了一个名为doSomething的方法。这个方法在执行一些操作后,会调用回调接口的onCallback方法。实现回调接口并创建Caller对象: public class Main { public static void main(String[] args) { Callback callback = new Callback() { @Override public void onCallback(String message) { System.out.println("回调函数被调用: " + message); } }; Caller caller = new Caller(callback); caller.doSomething(); } } 在这个例子中,我们创建了一个实现了Callback接口的匿名内部类,并将其传递给Caller类的构造函数。然后,我们调用Caller类的doSomething方法。当doSomething方法执行完毕后,它会调用我们传递给它的回调函数。三、使用Lambda表达式简化回调函数从Java 8开始,我们可以使用Lambda表达式简化回调函数的实现。以下是使用Lambda表达式的示例: public class Main { public static void main(String[] args) { Callback callback = message -> System.out.println("回调函数被调用: " + message); Caller caller = new Caller(callback); caller.doSomething(); } } 通过使用Lambda表达式,我们可以更简洁地实现回调函数,提高代码的可读性。四、回调函数的应用场景回调函数在Java中有许多应用场景。例如,我们可以使用回调函数来处理异步操作。在异步编程中,我们经常需要在某个操作完成后执行一些操作,但是我们无法预知这个操作何时完成。在这种情况下,我们可以使用回调函数。一个具体的例子是我们在springboot中使用RabbitMQ时,通常需要保障生产者投递消息的可靠性,rabbitmq为我们提供了这样一种方式,即生产者确认机制,这个机制就是利用回调函数实现的,当交换机收到生产者提供的消息之后,会调用我们实现的回调函数,然后我们可以在回调函数中实现一些自己的处理逻辑,从而实现发送者的可靠性。另一个常见的应用场景是在图形用户界面(GUI)编程中。在GUI编程中,我们经常需要在用户进行某些操作(如点击按钮)时执行一些操作。我们可以将这些操作封装在回调函数中,然后在用户进行操作时调用这些回调函数。五、回调函数的注意事项5.1接口设计合理设计回调接口,确保回调函数的参数和返回值类型与实际需求匹配,从而避免出现类型错误或不一致的问题。5.2. 空指针异常在使用回调函数时,需要注意空指针异常的处理。例如,在调用回调函数之前,需要进行空值检查,以确保回调函数的实例不为空。5.3. 逻辑复杂性当回调逻辑较为复杂时,可能会导致代码难以维护和理解。因此,在设计回调函数时,应尽量保持逻辑简洁明了,避免过于复杂的嵌套和逻辑判断。5.4. 性能影响在使用回调函数时,由于涉及到多个类之间的交互,可能会引入一定的性能开销。因此,在需要高性能的场景中,应谨慎使用回调函数,以避免性能影响。
-
1.介绍Collections.sort()方法的参数为一个List集合,用于给集合进行排序。Collections.sort()内部进行了方法重载,可以只传入一个List集合参数,也可以传入一个List集合参数和一个Comparator接口对象并实现其中的compare方法2.Comparator接口下的compare方法升序排列 public static void main(String[] args) { Integer[] nums = new Integer[]{3, 7, 9, 2, 1}; Arrays.sort(nums, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } }); for (Integer i : nums) { System.out.print(i + " "); // 1 2 3 7 9 } }降序排列public static void main(String[] args) { Integer[] nums = new Integer[]{3, 7, 9, 2, 1}; Arrays.sort(nums, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); for (Integer i : nums) { System.out.print(i + " ");9 7 3 2 1 } }所以更多时候我们是直接记住了compare(int o1, int o2)方法 return o1 - o2 是升序,return o2 - o1 是降序。为什么会这样写呢?我们不妨看一下sort(T[] a, Comparator<? super T> c)方法public static <T> void sort(T[] a, Comparator<? super T> c) { if (c == null) { sort(a); } else { if (LegacyMergeSort.userRequested) legacyMergeSort(a, c); else TimSort.sort(a, 0, a.length, c, null, 0, 0); } }可以看出他是进去了else内,不妨先进入legacyMergeSort看一下private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) { T[] aux = a.clone(); if (c==null) mergeSort(aux, a, 0, a.length, 0); else mergeSort(aux, a, 0, a.length, 0, c); }这里很明显也是进去了else内,继续看mergeSortprivate static void mergeSort(Object[] src,Object[] dest,int low, int high, int off,Comparator c) { int length = high - low; // Insertion sort on smallest arrays if (length < INSERTIONSORT_THRESHOLD) { for (int i=low; i<high; i++) for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--) swap(dest, j, j-1); return; } // Recursively sort halves of dest into src int destLow = low; int destHigh = high; low += off; high += off; int mid = (low + high) >>> 1; mergeSort(dest, src, low, mid, -off, c); mergeSort(dest, src, mid, high, -off, c); // If list is already sorted, just copy from src to dest. This is an // optimization that results in faster sorts for nearly ordered lists. if (c.compare(src[mid-1], src[mid]) <= 0) { System.arraycopy(src, low, dest, destLow, length); return; } // Merge sorted halves (now in src) into dest for(int i = destLow, p = low, q = mid; i < destHigh; i++) { if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0) dest[i] = src[p++]; else dest[i] = src[q++]; } }这一段的代码关键就是如下部分 if (length < INSERTIONSORT_THRESHOLD) { for (int i=low; i<high; i++) for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--) swap(dest, j, j-1); return; } 可以看到这里面调用了compare方法,当方法的返回值大于0的时候就将数组的前一个数和后一个数做交换。以升序为例来讲解,升序的话compare方法就 return o1 - o2,那么就是 return dest[j-1] - dest[j]。当 dest[j-1] > dest[j] 时,就进行交换。当 dest[j-1] <= dest[j] 时位置不变,从而达到数组升序。降序也是一样的道理。
-
重定向:1)什么是重定向? 服务器向浏览器发送一个状态码302 及一个消息头location(location 的值是一个地址),浏览器会立即向 location所指定的地址发送一个新的请求。我们把这样一种机制叫重定向。2)编程: response.sendRedirect(String url);3)需要注意的问题 在重定向之前,不能够有任何的输出;如果response 缓存当中有数据,在重定向之前,会自动清空。4)重定向的特点: a,地址任意 b,浏览器地址栏地址会变化(即变化为跳转之后的地址)。转发:1)什么是转发? 一个web组件(servlet/jsp)将未完成的处理交给另外一个web组件继续完成。转发所涉的各个web组件可以共享request和response 对象。 2)编程 step1 绑定数据到request对象上。 step2 获得转发器 RequestDispatcher rd = request.getRequestDispatcher(String url); step3 转发 rd.forward(request,response); servlet:负责业务逻辑处理(包括数据访问) 。 jsp:负责生成界面。 3)需要注意的问题: 在转发之前,response缓存的数据会被清空。
-
所谓生命周期,指的是servlet容器如何创建servlet实例、分配其资源、调用其方法、并销毁其实例的整个过程。阶段一: 实例化(就是创建servlet对象,调用构造器)在如下两种情况下会迚行对象实例化。第一种情况: 当请求到达容器时,容器查找该servlet对象是否存在,如果不存在,才会创建实例。第二种情况:容器在启动时,或者新部署了某个应用时,会检查web.xml当中,servlet是否有 load-on-starup 配置。如果有,则会创建该servlet实例。 load-on-starup 参数值越小,优先级越高(最小值为0,优先级最高)。阶段二: 初始化为servlet分配资源,调用init(ServletConfig config);方法 config 对象可以用来访问servlet的初始化参数。初始化参数是使用init-param配置的参数。init可以override。阶段三: 就绪/调用有请求到达容器,容器调用servlet对象的service()方法。HttpServlet的service()方法,会依据请求方式来调用doGet()或者doPost()方法。但是,这两个do 方法默认情况下,会抛出异常,需要子类去override。阶段四: 销毁容器依据自身的算法,将不再需要的servlet对象删除掉。在删除之前,会调用servlet对象的 destroy()方法。destroy()方法用于释放资源。在servlet的整个生命周期当中,init,destroy只会执行一次,而service 方法会执行多次。
-
【问题来源】黑龙江农信社【问题简要】开发 vxml 版本自助语音流程时,本地修改代码后,服务器疑似存在缓存问题【问题类别】vxml 版本自主语音流程开发【AICC解决方案版本】AICC 版本:AICC 8.0.71【问题现象描述】在本地开发 vxml 版本自助语音流程时,通过华为配置管理系统,将流程文件连接到本地项目。华为服务器疑似存在缓存问题。如:在原有main请求响应代码中,播放音频文件 1050.wav,本地修改代码为 busy.wav后,通过电话拨号。提示音依旧播放的是 1050.wav 内容重启华为服务器 或过了不确定时长的时间后,才能生效变成 busy.wav还有个问题,如下图:经常不定时的出现 这个错误,造成电话直接挂机麻烦帮忙定位下造成这两个问题的原因,谢谢
-
前言OOM 几乎是笔者工作中遇到的线上 bug 中最常见的,一旦平时正常的页面在线上出现页面崩溃或者服务无法调用,查看服务器日志后你很可能会看到“Caused by: java.lang.OutOfMlemoryError: Java heap space” 这样的提示,那么毫无疑问表示的是 Java 堆内存溢出了。其中又当属集合内存溢出最为常见。你是否有过把整个数据库表查出来的全字段结果直接赋值给一个 List 对象?是否把未经过过滤处理的数据赋值给 Set 对象进行去重操作?又或者是在高并发的场景下创建大量的集合对象未释放导致 JVM 无法自动回收?Java 堆内存溢出我的解决方案的核心思路有两个:一是从代码入手进行优化;二是从硬件层面对机器做合理配置。一、代码优化下面先说从代码入手怎么解决。1.1Stream 流自分页/** * 以下示例方法都在这个实现类里,包括类的继承和实现 */ @Service public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService{}在循环里使用 Stream 流的 skip()+limit() 来实现自分页,直至取出所有数据,不满足条件时终止循环 /** * 避免集合内存溢出方法(一) * @return */ private List<StudyVO> getList(){ ArrayList<StudyVO> resultList = new ArrayList<>(); //1、数据库取出源数据,注意只拿 id 字段,不至于溢出 List<String> idsList = this.list(new LambdaQueryWrapper<Study>() .select(Study::getId)).stream() .map(Study::getId) .collect(Collectors.toList()); //2、初始化循环 boolean loop = true; long number = 0; long perSize = 5000; while (loop){ //3、skip()+limit()组合,限制每次只取固定数量的 id List<String> ids = idsList.stream() .skip(number * perSize) .limit(perSize) .collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(ids)){ //根据第3步的 id 去拿数据库的全字段数据,这样也不至于溢出,因为一次只是 5000 条 List<StudyVO> voList = this.listByIds(ids).stream() .map(e -> e.copyProperties(StudyVO.class)) .collect(Collectors.toList()); //addAll() 方法也比较关键,快速地批量添加元素,容量是比较大的 resultList.addAll(voList); } //4、判断是否跳出循环 number++; loop = ids.size() == perSize; } return resultList; }1.2数据库分页这里是用数据库语句查询符合条件的指定条数,循环查出所有数据,不满足条件就跳出循环 /** * 避免集合内存溢出方法(二) * @param param * @return */ private List<StudyVO> getList(String param){ ArrayList<StudyVO> resultList = new ArrayList<>(); //1、构造查询条件 String id = ""; //2、初始化循环 boolean loop = true; int perSize = 5000; while (loop){ //分页,固定每次循环都查 5000 条 Page<Study> studyPage = this.page(new Page<> (NumberUtils.INTEGER_ZERO, perSize), wrapperBuilder(param, id)); if (Objects.nonNull(studyPage)){ List<Study> studyList = studyPage.getRecords(); if (CollectionUtils.isNotEmpty(studyList)){ //3、每次截取固定数量的标识,数组下标减一 id = studyList.get(perSize - NumberUtils.INTEGER_ONE).getId(); //4、判断是否跳出循环 loop = studyList.size() == perSize; //添加进返回的 VO 集合中 resultList.addAll(studyList.stream() .map(e -> e.copyProperties(StudyVO.class)) .collect(Collectors.toList())); } else { loop = false; } } } return resultList; } /** * 条件构造 * @param param * @param id * @return */ private LambdaQueryWrapper<Study> wrapperBuilder(String param, String id){ LambdaQueryWrapper<Study> wrapper = new LambdaQueryWrapper<>(); //只查部分字段,按照 id 的降序排列,形成顺序 wrapper.select(Study::getUserAvatar) .eq(Study::getOpenId, param) .orderByAsc(Study::getId); if (StringUtils.isNotBlank(id)){ //这步很关键,只查比该 id 值大的数据 wrapper.gt(Study::getId, id); } return wrapper; }1.3其它思考以上从根本上还是解决不了内存里处理大量数据的问题,取出 50w 数据放内存的风险就很大了。以下是我的其它解决思路:从业务上拆解:明确什么情况下需要后端处理这么多数据?是否可以考虑在业务流程上进行拆解?或者用其它形式的页面交互代替?数据库设计:数据一般都来源于数据库,库/表设计的时候尽量将表与表之间解耦,表字段的颗粒度放细,即多表少字段,查询时只拿需要的字段;数据放在磁盘:比如放到 MQ 里存储,然后取出的时候注意按固定数量批次取,并且注意释放资源;异步批处理:如果业务对实时性要求不高的话,可以异步批量把数据添加到文件流里,再存入到 OSS 中,按需取用;定时任务处理:询问产品经理该功能或者实现是否是结果必须的?是否一定要同步处理?可以考虑在一个时间段内进行多次操作,缓解大数据量的问题;咨询大数据团队:寻求大数据部门团队的专业支持,对于处理海量数据他们是专业的,看能不能提供一些可参考的建议。二、硬件配置核心思路:加大服务器内存,合理分配服务器的堆内存,并设置好弹性伸缩规则,当触发告警时自动伸缩扩容,保证系统的可用性。2.1云服务器配置以下是阿里云 ECS 管理控制台的编辑页面,可以对 CPU 和内存进行配置。在 ECS 实例伸缩组创建完成后,即可以根据业务规模去创建一个自定义伸缩配置,在业务量大的时候会触发自动伸缩。阿里云 ECS 管理如果是部署在私有云服务器,需要对具体的 JVM 参数进行调优的话,可能还得请团队的资深大佬、或者运维团队的老师来帮忙处理。三、文章小结本篇文章主要是记录一次线上集合内存溢出问题的处理思路,在之后的文章中我会分享一些关于真实项目中处理高并发、缓存的使用、异步/解耦等内容,敬请期待。那么今天的分享到这里就结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!转载自https://www.cnblogs.com/CodeBlogMan/p/18022444
-
在日常开发中,Date工具类使用频率相对较高,大家通常都会这样写: public static Date getData(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } public static Date getDataByFormat(String date, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); return sdf.parse(date); } 这很简单啊,有什么争议吗? 你应该听过“时区”这个名词,大家也都知道,相同时刻不同时区的时间是不一样的。 因此在使用时间时,一定要给出时区信息。 public static void getDataByZone(String param, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); // 默认时区解析时间表示 Date date = sdf.parse(param); System.out.println(date + ":" + date.getTime()); // 东京时区解析时间表示 sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo")); Date newYorkDate = sdf.parse(param); System.out.println(newYorkDate + ":" + newYorkDate.getTime()); } public static void main(String[] args) throws ParseException { getDataByZone("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss"); } 对于当前的上海时区和纽约时区,转化为 UTC 时间戳是不同的时间。 对于同一个本地时间的表示,不同时区的人解析得到的 UTC 时间一定是不同的,反过来不同的本地时间可能对应同一个 UTC。 格式化后出现的时间错乱。 public static void getDataByZoneFormat(String param, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); Date date = sdf.parse(param); // 默认时区格式化输出 System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date)); // 东京时区格式化输出 TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo")); System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date)); } public static void main(String[] args) throws ParseException { getDataByZoneFormat("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss"); } 我当前时区的 Offset(时差)是 +8 小时,对于 +9 小时的纽约,整整差了1个小时,北京早上 10 点对应早上东京 11 点。 看看Java 8是如何解决时区问题的: Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,处理时区问题更简单清晰。 public static void getDataByZoneFormat8(String param, String format) throws ParseException { ZoneId zone = ZoneId.of("Asia/Shanghai"); ZoneId tokyoZone = ZoneId.of("Asia/Tokyo"); ZoneId timeZone = ZoneOffset.ofHours(2); // 格式化器 DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format); ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(param, dtf), zone); // withZone设置时区 DateTimeFormatter dtfz = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z"); System.out.println(dtfz.withZone(zone).format(date)); System.out.println(dtfz.withZone(tokyoZone).format(date)); System.out.println(dtfz.withZone(timeZone).format(date)); } public static void main(String[] args) throws ParseException { getDataByZoneFormat8("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss"); } Asia/Shanghai对应+8,对应2023-11-10 10:00:00; Asia/Tokyo对应+9,对应2023-11-10 11:00:00; timeZone 是+2,所以对应2023-11-10 04:00:00; 在处理带时区的国际化时间问题,推荐使用jdk8的日期时间类: 通过ZoneId,定义时区; 使用ZonedDateTime保存时间; 通过withZone对DateTimeFormatter设置时区; 进行时间格式化得到本地时间; 思路比较清晰,不容易出错。 在与前端联调时,报了个错,java.lang.NumberFormatException: multiple points,起初我以为是时间格式传的不对,仔细一看,不对啊。 百度一下,才知道是高并发情况下SimpleDateFormat有线程安全的问题。 下面通过模拟高并发,把这个问题复现一下: public static void getDataByThread(String param, String format) throws InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(5); SimpleDateFormat sdf = new SimpleDateFormat(format); // 模拟并发环境,开启5个并发线程 for (int i = 0; i < 5; i++) { threadPool.execute(() -> { for (int j = 0; j < 2; j++) { try { System.out.println(sdf.parse(param)); } catch (ParseException e) { System.out.println(e); } } }); } threadPool.shutdown(); threadPool.awaitTermination(1, TimeUnit.HOURS); } 果不其然,报错。还将2023年转换成2220年,我勒个乖乖。 在时间工具类里,时间格式化,我都是这样弄的啊,没问题啊,为啥这个不行?原来是因为共用了同一个SimpleDateFormat,在工具类里,一个线程一个SimpleDateFormat,当然没问题啦! 可以通过TreadLocal 局部变量,解决SimpleDateFormat的线程安全问题。 public static void getDataByThreadLocal(String time, String format) throws InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(5); ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat(format); } }; // 模拟并发环境,开启5个并发线程 for (int i = 0; i < 5; i++) { threadPool.execute(() -> { for (int j = 0; j < 2; j++) { try { System.out.println(sdf.get().parse(time)); } catch (ParseException e) { System.out.println(e); } } }); } threadPool.shutdown(); threadPool.awaitTermination(1, TimeUnit.HOURS); } 看一下SimpleDateFormat.parse的源码: public class SimpleDateFormat extends DateFormat { @Override public Date parse(String text, ParsePosition pos){ CalendarBuilder calb = new CalendarBuilder(); Date parsedDate; try { parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDate.before(defaultCenturyStart)) { parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } } } class CalendarBuilder { Calendar establish(Calendar cal) { boolean weekDate = isSet(WEEK_YEAR) && field[WEEK_YEAR] > field[YEAR]; if (weekDate && !cal.isWeekDateSupported()) { // Use YEAR instead if (!isSet(YEAR)) { set(YEAR, field[MAX_FIELD + WEEK_YEAR]); } weekDate = false; } cal.clear(); // Set the fields from the min stamp to the max stamp so that // the field resolution works in the Calendar. for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { cal.set(index, field[MAX_FIELD + index]); break; } } } ... } } 先new CalendarBuilder(); 通过parsedDate = calb.establish(calendar).getTime();解析时间; establish方法内先cal.clear(),再重新构建cal,整个操作没有加锁; 上面几步就会导致在高并发场景下,线程1正在操作一个Calendar,此时线程2又来了。线程1还没来得及处理 Calendar 就被线程2清空了。 因此,通过编写Date工具类,一个线程一个SimpleDateFormat,还是有一定道理的。 ———————————————— 原文链接:https://blog.csdn.net/guorui_java/article/details/135611142
-
为什么需要熔断 微服务集群中,每个应用基本都会依赖一定数量的外部服务。有可能随时都会遇到网络连接缓慢,超时,依赖服务过载,服务不可用的情况,在高并发场景下如果此时调用方不做任何处理,继续持续请求故障服务的话很容易引起整个微服务集群雪崩。比如高并发场景的用户订单服务,一般需要依赖一下服务: 商品服务 账户服务 库存服务 假如此时 账户服务 过载,订单服务持续请求账户服务只能被动的等待账户服务报错或者请求超时,进而导致订单请求被大量堆积,这些无效请求依然会占用系统资源:cpu,内存,数据连接...导致订单服务整体不可用。即使账户服务恢复了订单服务也无法自我恢复。 这时如果有一个主动保护机制应对这种场景的话订单服务至少可以保证自身的运行状态,等待账户服务恢复时订单服务也同步自我恢复,这种自我保护机制在服务治理中叫熔断机制。 熔断 熔断是调用方自我保护的机制(客观上也能保护被调用方),熔断对象是外部服务。 降级 降级是被调用方(服务提供者)的防止因自身资源不足导致过载的自我保护机制,降级对象是自身。 熔断这一词来源时我们日常生活电路里面的熔断器,当负载过高时(电流过大)保险丝会自行熔断防止电路被烧坏,很多技术都是来自生活场景的提炼。 工作原理 熔断器一般具有三个状态: 关闭:默认状态,请求能被到达目标服务,同时统计在窗口时间成功和失败次数,如果达到错误率阈值将会进入断开状态。 断开:此状态下将会直接返回错误,如果有 fallback 配置则直接调用 fallback 方法。 半断开:进行断开状态会维护一个超市时间,到达超时时间开始进入 半断开 状态,尝试允许一部门请求正常通过并统计成功数量,如果请求正常则认为此时目标服务已恢复进入 关闭 状态,否则进入 断开 状态。半断开 状态存在的目的在于实现了自我修复,同时防止正在恢复的服务再次被大量打垮。 使用较多的熔断组件: hystrix circuit breaker(不再维护) hystrix-go resilience4j(推荐) sentinel(推荐) 什么是自适应熔断 基于上面提到的熔断器原理,项目中我们要使用好熔断器通常需要准备以下参数: 错误比例阈值:达到该阈值进入 断开 状态。 断开状态超时时间:超时后进入 半断开 状态。 半断开状态允许请求数量。 窗口时间大小。 实际上可选的配置参数还有非常非常多,参考 https://resilience4j.readme.io/docs/circuitbreaker 对于经验不够丰富的开发人员而言,这些参数设置多少合适心里其实并没有底。 那么有没有一种自适应的熔断算法能让我们不关注参数,只要简单配置就能满足大部分场景? 其实是有的,google sre提供了一种自适应熔断算法来计算丢弃请求的概率: 算法参数: requests:窗口时间内的请求总数 accepts:正常请求数量 K:敏感度,K 越小越容易丢请求,一般推荐 1.5-2 之间 算法解释: 正常情况下 requests=accepts,所以概率是 0。 随着正常请求数量减少,当达到 requests == K* accepts 继续请求时,概率 P 会逐渐比 0 大开始按照概率逐渐丢弃一些请求,如果故障严重则丢包会越来越多,假如窗口时间内 accepts==0 则完全熔断。 当应用逐渐恢复正常时,accepts、requests 同时都在增加,但是 K*accepts 会比 requests 增加的更快,所以概率很快就会归 0,关闭熔断。 代码实现 接下来思考一个熔断器如何实现。 初步思路是: 无论什么熔断器都得依靠指标统计来转换状态,而统计指标一般要求是最近的一段时间内的数据(太久的数据没有参考意义也浪费空间),所以通常采用一个 滑动时间窗口 数据结构 来存储统计数据。同时熔断器的状态也需要依靠指标统计来实现可观测性,我们实现任何系统第一步需要考虑就是可观测性,不然系统就是一个黑盒。 外部服务请求结果各式各样,所以需要提供一个自定义的判断方法,判断请求是否成功。可能是 http.code 、rpc.code、body.code,熔断器需要实时收集此数据。 当外部服务被熔断时使用者往往需要自定义快速失败的逻辑,考虑提供自定义的 fallback() 功能。 下面来逐步分析 go-zero 的源码实现: core/breaker/breaker.go 熔断器接口定义 兵马未动,粮草先行,明确了需求后就可以开始规划定义接口了,接口是我们编码思维抽象的第一步也是最重要的一步。 核心定义包含两种类型的方法: Allow():需要手动回调请求结果至熔断器,相当于手动挡。 DoXXX():自动回调请求结果至熔断器,相当于自动挡,实际上 DoXXX() 类型方法最后都是调用 DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error // 自定义判定执行结果 Acceptable func(err error) bool // 手动回调 Promise interface { // Accept tells the Breaker that the call is successful. // 请求成功 Accept() // Reject tells the Breaker that the call is failed. // 请求失败 Reject(reason string) } Breaker interface { // 熔断器名称 Name() string // 熔断方法,执行请求时必须手动上报执行结果 // 适用于简单无需自定义快速失败,无需自定义判定请求结果的场景 // 相当于手动挡。。。 Allow() (Promise, error) // 熔断方法,自动上报执行结果 // 自动挡。。。 Do(req func() error) error // 熔断方法 // acceptable - 支持自定义判定执行结果 DoWithAcceptable(req func() error, acceptable Acceptable) error // 熔断方法 // fallback - 支持自定义快速失败 DoWithFallback(req func() error, fallback func(err error) error) error // 熔断方法 // fallback - 支持自定义快速失败 // acceptable - 支持自定义判定执行结果 DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error } 熔断器实现 circuitBreaker 继承 throttle,实际上这里相当于静态代理,代理模式可以在不改变原有对象的基础上增强功能,后面我们会看到 go-zero 这样做的原因是为了收集熔断器错误数据,也就是为了实现可观测性。 熔断器实现采用静态代理模式,看起来稍微有点绕脑。 // 熔断器结构体 circuitBreaker struct { name string // 实际上 circuitBreaker熔断功能都代理给 throttle来实现 throttle }// 熔断器接口 throttle interface { // 熔断方法 allow() (Promise, error) // 熔断方法 // DoXXX()方法最终都会该方法 doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error } func (cb *circuitBreaker) Allow() (Promise, error) { return cb.throttle.allow() } func (cb *circuitBreaker) Do(req func() error) error { return cb.throttle.doReq(req, nil, defaultAcceptable) } func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error { return cb.throttle.doReq(req, nil, acceptable) } func (cb *circuitBreaker) DoWithFallback(req func() error, fallback func(err error) error) error { return cb.throttle.doReq(req, fallback, defaultAcceptable) } func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error { return cb.throttle.doReq(req, fallback, acceptable) } throttle 接口实现类: loggedThrottle 增加了为了收集错误日志的滚动窗口,目的是为了收集当请求失败时的错误日志。 // 带日志功能的熔断器 type loggedThrottle struct { // 名称 name string // 代理对象 internalThrottle // 滚动窗口,滚动收集数据,相当于环形数组 errWin *errorWindow } // 熔断方法 func (lt loggedThrottle) allow() (Promise, error) { promise, err := lt.internalThrottle.allow() return promiseWithReason{ promise: promise, errWin: lt.errWin, }, lt.logError(err) } // 熔断方法 func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool { accept := acceptable(err) if !accept { lt.errWin.add(err.Error()) } return accept })) } func (lt loggedThrottle) logError(err error) error { if err == ErrServiceUnavailable { // if circuit open, not possible to have empty error window stat.Report(fmt.Sprintf( "proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s", proc.ProcessName(), proc.Pid(), lt.name, lt.errWin)) } return err } 错误日志收集 errorWindow errorWindow 是一个环形数组,新数据不断滚动覆盖最旧的数据,通过取余实现。 // 滚动窗口 type errorWindow struct { reasons [numHistoryReasons]string index int count int lock sync.Mutex } // 添加数据 func (ew *errorWindow) add(reason string) { ew.lock.Lock() // 添加错误日志 ew.reasons[ew.index] = fmt.Sprintf("%s %s", timex.Time().Format(timeFormat), reason) // 更新index,为下一次写入数据做准备 // 这里用的取模实现了滚动功能 ew.index = (ew.index + 1) % numHistoryReasons // 统计数量 ew.count = mathx.MinInt(ew.count+1, numHistoryReasons) ew.lock.Unlock() } // 格式化错误日志 func (ew *errorWindow) String() string { var reasons []string ew.lock.Lock() // reverse order for i := ew.index - 1; i >= ew.index-ew.count; i-- { reasons = append(reasons, ew.reasons[(i+numHistoryReasons)%numHistoryReasons]) } ew.lock.Unlock() return strings.Join(reasons, "\n") } 看到这里我们还没看到实际的熔断器实现,实际上真正的熔断操作被代理给了 internalThrottle 对象。 internalThrottle interface { allow() (internalPromise, error) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error } internalThrottle 接口实现 googleBreaker 结构体定义 type googleBreaker struct { // 敏感度,go-zero中默认值为1.5 k float64 // 滑动窗口,用于记录最近一段时间内的请求总数,成功总数 stat *collection.RollingWindow // 概率生成器 // 随机产生0.0-1.0之间的双精度浮点数 proba *mathx.Proba } 可以看到熔断器属性其实非常简单,数据统计采用的是滑动时间窗口来实现。 RollingWindow 滑动窗口 滑动窗口属于比较通用的数据结构,常用于最近一段时间内的行为数据统计。 它的实现非常有意思,尤其是如何模拟窗口滑动过程。 先来看滑动窗口的结构体定义: RollingWindow struct { // 互斥锁 lock sync.RWMutex // 滑动窗口数量 size int // 窗口,数据容器 win *window // 滑动窗口单元时间间隔 interval time.Duration // 游标,用于定位当前应该写入哪个bucket offset int // 汇总数据时,是否忽略当前正在写入桶的数据 // 某些场景下因为当前正在写入的桶数据并没有经过完整的窗口时间间隔 // 可能导致当前桶的统计并不准确 ignoreCurrent bool // 最后写入桶的时间 // 用于计算下一次写入数据间隔最后一次写入数据的之间 // 经过了多少个时间间隔 lastTime time.Duration } window 是数据的实际存储位置,其实就是一个数组,提供向指定 offset 添加数据与清除操作。数组里面按照 internal 时间间隔分隔成多个 bucket。 // 时间窗口 type window struct { // 桶 // 一个桶标识一个时间间隔 buckets []*Bucket // 窗口大小 size int } // 添加数据 // offset - 游标,定位写入bucket位置 // v - 行为数据 func (w *window) add(offset int, v float64) { w.buckets[offset%w.size].add(v) } // 汇总数据 // fn - 自定义的bucket统计函数 func (w *window) reduce(start, count int, fn func(b *Bucket)) { for i := 0; i < count; i++ { fn(w.buckets[(start+i)%w.size]) } } // 清理特定bucket func (w *window) resetBucket(offset int) { w.buckets[offset%w.size].reset() } // 桶 type Bucket struct { // 当前桶内值之和 Sum float64 //当前桶的add总次数 Count int64 } // 向桶添加数据 func (b *Bucket) add(v float64) { // 求和 b.Sum += v // 次数+1 b.Count++ } // 桶数据清零 func (b *Bucket) reset() { b.Sum = 0 b.Count = 0 } window 添加数据: 计算当前时间距离上次添加时间经过了多少个 时间间隔,实际上就是过期了几个 bucket。 清理过期桶的数据 更新 offset,更新 offset 的过程实际上就是在模拟窗口滑动 添加数据 // 添加数据 func (rw *RollingWindow) Add(v float64) { rw.lock.Lock() defer rw.lock.Unlock() // 获取当前写入的下标 rw.updateOffset() // 添加数据 rw.win.add(rw.offset, v) } // 计算当前距离最后写入数据经过多少个单元时间间隔 // 实际上指的就是经过多少个桶 func (rw *RollingWindow) span() int { offset := int(timex.Since(rw.lastTime) / rw.interval) if 0 <= offset && offset < rw.size { return offset } // 大于时间窗口时 返回窗口大小即可 return rw.size } // 更新当前时间的offset // 实现窗口滑动 func (rw *RollingWindow) updateOffset() { // 经过span个桶的时间 span := rw.span() // 还在同一单元时间内不需要更新 if span <= 0 { return } offset := rw.offset // 既然经过了span个桶的时间没有写入数据 // 那么这些桶内的数据就不应该继续保留了,属于过期数据清空即可 // 可以看到这里全部用的 % 取余操作,可以实现按照下标周期性写入 // 如果超出下标了那就从头开始写,确保新数据一定能够正常写入 // 类似循环数组的效果 for i := 0; i < span; i++ { rw.win.resetBucket((offset + i + 1) % rw.size) } // 更新offset rw.offset = (offset + span) % rw.size now := timex.Now() // 更新操作时间 // 这里很有意思 rw.lastTime = now - (now-rw.lastTime)%rw.interval } window 统计数据: // 归纳汇总数据 func (rw *RollingWindow) Reduce(fn func(b *Bucket)) { rw.lock.RLock() defer rw.lock.RUnlock() var diff int span := rw.span() // 当前时间截止前,未过期桶的数量 if span == 0 && rw.ignoreCurrent { diff = rw.size - 1 } else { diff = rw.size - span } if diff > 0 { // rw.offset - rw.offset+span之间的桶数据是过期的不应该计入统计 offset := (rw.offset + span + 1) % rw.size // 汇总数据 rw.win.reduce(offset, diff, fn) } } googleBreaker 判断是否应该熔断 收集滑动窗口内的统计数据 计算熔断概率 // 按照最近一段时间的请求数据计算是否熔断 func (b *googleBreaker) accept() error { // 获取最近一段时间的统计数据 accepts, total := b.history() // 计算动态熔断概率 weightedAccepts := b.k * float64(accepts) // https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101 dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1)) // 概率为0,通过 if dropRatio <= 0 { return nil } // 随机产生0.0-1.0之间的随机数与上面计算出来的熔断概率相比较 // 如果随机数比熔断概率小则进行熔断 if b.proba.TrueOnProba(dropRatio) { return ErrServiceUnavailable } return nil } googleBreaker 熔断逻辑实现 熔断器对外暴露两种类型的方法 简单场景直接判断对象是否被熔断,执行请求后必须需手动上报执行结果至熔断器。 func (b *googleBreaker) allow() (internalPromise, error) 复杂场景下支持自定义快速失败,自定义判定请求是否成功的熔断方法,自动上报执行结果至熔断器。 func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error Acceptable 参数目的是自定义判断请求是否成功。 Acceptable func(err error) bool // 熔断方法 // 返回一个promise异步回调对象,可由开发者自行决定是否上报结果到熔断器 func (b *googleBreaker) allow() (internalPromise, error) { if err := b.accept(); err != nil { return nil, err } return googlePromise{ b: b, }, nil } // 熔断方法 // req - 熔断对象方法 // fallback - 自定义快速失败函数,可对熔断产生的err进行包装后返回 // acceptable - 对本次未熔断时执行请求的结果进行自定义的判定,比如可以针对http.code,rpc.code,body.code func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { // 判定是否熔断 if err := b.accept(); err != nil { // 熔断中,如果有自定义的fallback则执行 if fallback != nil { return fallback(err) } return err } // 如果执行req()过程发生了panic,依然判定本次执行失败上报至熔断器 defer func() { if e := recover(); e != nil { b.markFailure() panic(e) } }() // 执行请求 err := req() // 判定请求成功 if acceptable(err) { b.markSuccess() } else { b.markFailure() } return err } // 上报成功 func (b *googleBreaker) markSuccess() { b.stat.Add(1) } // 上报失败 func (b *googleBreaker) markFailure() { b.stat.Add(0) } // 统计数据 func (b *googleBreaker) history() (accepts, total int64) { b.stat.Reduce(func(b *collection.Bucket) { accepts += int64(b.Sum) total += b.Count }) return } 原文链接: https://mp.weixin.qq.com/s?__biz=Mzg2ODU1MTI0OA==&mid=2247484672&idx=1&sn=43067f7af6b3c6233c15a14cb2ed7505&utm_source=tuicool&utm_medium=referral ———————————————— 原文链接:https://blog.csdn.net/m0_67645544/article/details/123738412
上滑加载中
推荐直播
-
华为云码道-AI时代应用开发利器2026/03/18 周三 19:00-20:00
童得力,华为云开发者生态运营总监/姚圣伟,华为云HCDE开发者专家
本次直播由华为专家带你实战应用开发,看华为云码道(CodeArts)代码智能体如何在AI时代让你的创意应用快速落地。更有华为云HCDE开发者专家带你用码道玩转JiuwenClaw,让小艺成为你的AI助理。
回顾中 -
Skill 构建 × 智能创作:基于华为云码道的 AI 内容生产提效方案2026/03/25 周三 19:00-20:00
余伟,华为云软件研发工程师/万邵业(万少),华为云HCDE开发者专家
本次直播带来两大实战:华为云码道 Skill-Creator 手把手搭建专属知识库 Skill;如何用码道提效 OpenClaw 小说文本,打造从大纲到成稿的 AI 原创小说全链路。技术干货 + OPC创作思路,一次讲透!
回顾中 -
码道新技能,AI 新生产力——从自动视频生成到开源项目解析2026/04/08 周三 19:00-21:00
童得力-华为云开发者生态运营总监/何文强-无人机企业AI提效负责人
本次华为云码道 Skill 实战活动,聚焦两大 AI 开发场景:通过实战教学,带你打造 AI 编程自动生成视频 Skill,并实现对 GitHub 热门开源项目的智能知识抽取,手把手掌握 Skill 开发全流程,用 AI 提升研发效率与内容生产力。
回顾中
热门标签