BigBro`s Blog - Java日常
type: channel
link: https://ai.bigbro.xin/index.php/category/java/
items: IntelliJ IDEA远程Debug Linux的Java程序，找问题不要只会看日志了
items: Stream日常使用
items: 处理 Exception 的几种实践，很优雅，被很多团队采纳！
items: Java 设计模式：实战工厂方法模式
items: Java8 Stream 真香，赶紧来舔一舔
items: Spring Boot + Dataway ：接口不用写，配配就出来？
items: 三个水桶等分8升水的问题
items: MySQL的Limit 性能差？真的不能再用了？
items: 为什么Mysql的常用引擎都默认使用B+树作为索引？

IntelliJ IDEA远程Debug Linux的Java程序，找问题不要只会看日志了
type: item
date: 2021-12-14T00:47:00+00:00
description: 1 前言我们习惯于在本地开发的时候debug，能快速定位与解决问题，那部署在服务器上是不是就没有办法了呢？只能通过查看日志来定位？不是的，在远端的服务器上，我们一样可以debug。2 IDEA的debug我们先来看一下在IntelliJ IDEA直接debug是怎样的。先准备一个简单的Java程序：package com.pkslow.basic; import java.util.Map; public class RemoteDebug { public static void main(String[] args) { System.out.println("------------------start------------------"); System.out.println("get all the system environment"); Map<String, String> envs = System.getenv(); System.out.println(" print out the contains `HOME`"); System.out.println("------env HOME------"); envs.entrySet().stream() .filter(env -> env.getKey().contains("HOME")) .forEach(env -> { System.out.println(env.getKey() + ":" + env.getValue()); }); System.out.println("------------------end------------------"); } }功能很简单，获取所有系统环境变量，并打印出含有HOME字段的。Debug很简单，直接点击以下按钮就可以：相信大家都知道这一点，但应该很多人都不会注意，IDEA究竟做了什么，为什么就可以调试了呢？我们看一下控制台的日志就明白了：/Library/Java/JavaVirtualMachines/jdk1.8.0_212.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59313,suspend=y,server=n -javaagent:/Users/pkslow/Library/Caches/IntelliJIdea2019.3/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "/Library/Java/JavaVirtualMachines/jdk1.8.0_212.jdk/Contents/Home/jre/lib/charsets.jar:" com.pkslow.basic.RemoteDebug Connected to the target VM, address: '127.0.0.1:59313', transport: 'socket'简化一下，不重要的参数去掉：java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59313,suspend=y,server=n com.pkslow.basic.RemoteDebug这就是可以Debug的原因，利用了Java Agent原理。这个功能很强大，类似一个AOP，代理了Java程序，可以利用它进行调试、热部署等。3 调试本地程序我们先试试如何可以调试本地程序，不是直接在IDEA上调试。先要编译出class文件RemoteDebug.class，然后按package结构放好。我通过mvn clean compile来编译。启动程序，在target/classes/目录执行：$ java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:50050,suspend=y,server=y com.pkslow.basic.RemoteDebug Listening for transport dt_socket at address: 50050然后程序就会等待调试客户端的连接，不会往下执行。配置IDEA以进行调试：配置完成保存后，点击debug就可以了：程序已经进入debug模式：我们已经执行到了其中一行，现在看看服务端：与IDEA是同步的，并且确实已经控制了服务端Java的执行。4 远程调试Linux Java程序先把程序部署在Linux上：$ scp -P 22 ./com/pkslow/basi…

Stream日常使用
type: item
date: 2021-11-09T08:01:00+00:00
description: 1. Streams简介今天要讲的Stream指的是java.util.stream包中的诸多类。Stream可以方便的将之前的结合类以转换为Stream并以流式方式进行处理，大大的简化了我们的编程，Stream包中，最核心的就是interface Stream1.1 创建StreamStream的创建有很多方式，java引入Stream之后所有的集合类都添加了一个stream()方法，通过这个方法可以直接得到其对应的Stream。也可以通过Stream.of方法来创建：//Stream Creation String[] arr = new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); stream = Stream.of("a", "b", "c");复制代码1.2 Streams多线程如果我们想使用多线程来处理集合类的数据，Stream提供了非常方便的多线程方法parallelStream()：//Multi-threading List<String> list =new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("abc"); list.add("ccc"); list.add("ddd"); list.parallelStream().forEach(element -> doPrint(element));复制代码1.3 Stream的基本操作Stream的操作可以分为两类，一类是中间操作，中间操作返回Stream，因此可以级联调用。 另一类是终止操作，这类操作会返回Stream定义的类型。//Operations long count = list.stream().distinct().count();复制代码上面的例子中，distinct()返回一个Stream，所以可以级联操作，最后的count()是一个终止操作，返回最后的值。Matching Stream提供了anyMatch(), allMatch(), noneMatch()这三种match方式，我们看下怎么使用： //Matching boolean isValid = list.stream().anyMatch(element -> element.contains("h")); boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); 复制代码Filteringfilter() 方法允许我们对Stream中的数据进行过滤，从而得到我们需要的：Stream filterStream = list.stream().filter(element -> element.contains("d"));复制代码上面的例子中我们从list中选出了包含“d”字母的String。Mappingmap就是对Stream中的值进行再加工，然后将加工过后的值作为新的Stream返回。//Mapping Stream<String> mappingStream = list.stream().map(element -> convertElement(element)); private static String convertElement(String element) { return "element"+"abc"; }复制代码上的例子中我们把list中的每个值都加上了“abc”然后返回一个新的Stream。FlatMapflatMap和Map很类似，但是他们两个又有不同，看名字我们可以看到flatMap意思是打平之后再做Map。怎么理解呢？假如我们有一个CustBook类：@Data public class CustBook { List<String> bookName; }复制代码CustBook定义了一个bookName字段。先看一下Map返回的结果：List<CustBook> users = new ArrayList<>(); users.add(new CustBook()); Stream<Stream<String>> userStreamMap = users.stream().map(user -> user.getBookName().stream());复制代码在上面的代码中，map将每一个user都转换成了s…

Stream日常使用
type: item
date: 2021-11-09T08:01:00+00:00
description: 1. Streams简介今天要讲的Stream指的是java.util.stream包中的诸多类。Stream可以方便的将之前的结合类以转换为Stream并以流式方式进行处理，大大的简化了我们的编程，Stream包中，最核心的就是interface Stream1.1 创建StreamStream的创建有很多方式，java引入Stream之后所有的集合类都添加了一个stream()方法，通过这个方法可以直接得到其对应的Stream。也可以通过Stream.of方法来创建：//Stream Creation String[] arr = new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); stream = Stream.of("a", "b", "c");复制代码1.2 Streams多线程如果我们想使用多线程来处理集合类的数据，Stream提供了非常方便的多线程方法parallelStream()：//Multi-threading List<String> list =new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("abc"); list.add("ccc"); list.add("ddd"); list.parallelStream().forEach(element -> doPrint(element));复制代码1.3 Stream的基本操作Stream的操作可以分为两类，一类是中间操作，中间操作返回Stream，因此可以级联调用。 另一类是终止操作，这类操作会返回Stream定义的类型。//Operations long count = list.stream().distinct().count();复制代码上面的例子中，distinct()返回一个Stream，所以可以级联操作，最后的count()是一个终止操作，返回最后的值。Matching Stream提供了anyMatch(), allMatch(), noneMatch()这三种match方式，我们看下怎么使用： //Matching boolean isValid = list.stream().anyMatch(element -> element.contains("h")); boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); 复制代码Filteringfilter() 方法允许我们对Stream中的数据进行过滤，从而得到我们需要的：Stream filterStream = list.stream().filter(element -> element.contains("d"));复制代码上面的例子中我们从list中选出了包含“d”字母的String。Mappingmap就是对Stream中的值进行再加工，然后将加工过后的值作为新的Stream返回。//Mapping Stream<String> mappingStream = list.stream().map(element -> convertElement(element)); private static String convertElement(String element) { return "element"+"abc"; }复制代码上的例子中我们把list中的每个值都加上了“abc”然后返回一个新的Stream。FlatMapflatMap和Map很类似，但是他们两个又有不同，看名字我们可以看到flatMap意思是打平之后再做Map。怎么理解呢？假如我们有一个CustBook类：@Data public class CustBook { List<String> bookName; }复制代码CustBook定义了一个bookName字段。先看一下Map返回的结果：List<CustBook> users = new ArrayList<>(); users.add(new CustBook()); Stream<Stream<String>> userStreamMap = users.stream().map(user -> user.getBookName().stream());复制代码在上面的代码中，map将每一个user都转换成了s…

处理 Exception 的几种实践，很优雅，被很多团队采纳！
type: item
date: 2021-10-24T02:45:00+00:00
description: 在Java中处理异常并不是一个简单的事情。不仅仅初学者很难理解，即使一些有经验的开发者也需要花费很多时间来思考如何处理异常，包括需要处理哪些异常，怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范对异常的处理的原因。而团队之间的这些规范往往是截然不同的。本文给出几个被很多团队使用的异常处理最佳实践。在Finally块中清理资源或者使用try-with-resource语句当使用类似InputStream这种需要使用后关闭的资源时，一个常见的错误就是在try块的最后关闭资源。`public void doNotCloseResourceInTry() {` `FileInputStream inputStream = null;``try {` `File file = new File("./tmp.txt");` `inputStream = new FileInputStream(file);``// use the inputStream to read a file``// do NOT do this` `inputStream.close();` `} catch (FileNotFoundException e) {``log.error(e);` `} catch (IOException e) {``log.error(e);` `}``}`上述代码在没有任何exception的时候运行是没有问题的。但是当try块中的语句抛出异常或者自己实现的代码抛出异常，那么就不会执行最后的关闭语句，从而资源也无法释放。合理的做法则是将所有清理的代码都放到finally块中或者使用try-with-resource语句。`public void closeResourceInFinally() {` `FileInputStream inputStream = null;``try {` `File file = new File("./tmp.txt");` `inputStream = new FileInputStream(file);``// use the inputStream to read a file` `} catch (FileNotFoundException e) {``log.error(e);` `} finally {``if (inputStream != null) {``try {` `inputStream.close();` `} catch (IOException e) {``log.error(e);` `}` `}` `}``}``public void automaticallyCloseResource() {` `File file = new File("./tmp.txt");``try (FileInputStream inputStream = new FileInputStream(file);) {``// use the inputStream to read a file` `} catch (FileNotFoundException e) {``log.error(e);` `} catch (IOException e) {``log.error(e);` `}``}`指定具体的异常尽可能的使用最具体的异常来声明方法，这样才能使得代码更容易理解。`public void doNotDoThis() throws Exception {` `...``}``public void doThis() throws NumberFormatException {` `...``}`如上，NumberFormatException字面上即可以看出是数字格式化错误。对异常进行文档说明当在方法上声明抛出异常时，也需要进行文档说明。和前面的一点一样，都是为了给调用者提供尽可能多的信息，从而可以更好地避免/处理异常。异常处理的 10 个最佳实践，这篇也推荐看下。在Javadoc中加入throws声明，并且描述抛出异常的场景。`/**` `* This method does something extremely useful ...` `*` `* @param input` `* @throws MyBusinessException if ... happens` `*/``public void doSomething(String input) throws MyBusinessException {` `...``}`抛出异常的时候包含描述信息在抛出异常时，需要尽可能精确地描述问题和相关信息，这样无论是打印到日志中还是监控工具中，都能够更容易…

Java 设计模式：实战工厂方法模式
type: item
date: 2021-09-11T07:44:36+00:00
description: 一、前言好看的代码千篇一律，恶心的程序升职加薪。该说不说几乎是程序员就都知道或者了解设计模式，但大部分小伙伴写代码总是习惯于一把梭。无论多少业务逻辑就一个类几千行，这样的开发也可以归纳为三步；定义属性、创建方法、调用展示，Done！只不过开发一时爽，重构火葬场。好的代码不只为了完成现有功能，也会考虑后续扩展。在结构设计上松耦合易读易扩展，在领域实现上高内聚不对外暴漏实现细节不被外部干扰。而这就有点像家里三居(MVC)室、四居(DDD)室的装修，你不会允许几十万的房子把走线水管裸漏在外面，也不会允许把马桶放到厨房，炉灶安装到卫生间。谁发明了设计模式？ 设计模式的概念最早是由 克里斯托佛·亚历山大 在其著作 《建筑模式语言》 中首次提出的。 本书介绍了城市设计的 “语言”，提供了253个描述城镇、邻里、住宅、花园、房间及西部构造的模式， 而此类 “语言” 的基本单元就是模式。后来，埃里希·伽玛、 约翰·弗利赛德斯、 拉尔夫·约翰逊 和 理查德·赫尔姆 这四位作者接受了模式的概念。 1994 年， 他们出版了 《设计模式： 可复用面向对象软件的基础》 一书， 将设计模式的概念应用到程序开发领域中。其实有一部分人并没有仔细阅读过设计模式的相关书籍和资料，但依旧可以编写出优秀的代码。这主要是由于在经过众多项目的锤炼和对程序设计的不断追求，从而在多年编程历程上提炼出来的心得体会。而这份经验最终会与设计模式提到的内容几乎一致，同样会要求高内聚、低耦合、可扩展、可复用。你可能也遇到类似的经历，在学习一些框架的源码时，发现它里的某些设计和你在做开发时一样。我怎么学不会设计模式？ 钱也花了，书也买了。代码还是一坨一坨的！设计模式是由多年的经验提炼出来开发指导思想。就像我告诉你自行车怎么骑、汽车怎么开，但只要你没跑过几千公里，你能记住的只是理论，想上道依旧很慌！所以，本设计模式专题系列开始，会带着你使用设计模式的思想去优化代码。从而学习设计模式的心得并融入给自己。当然这里还需要多加练习，一定是人车合一，才能站在设计模式的基础上构建出更加合理的代码。二、工厂方法模式介绍工厂方法模式，图片来自 refactoringguru.cn工厂模式又称工厂方法模式，是一种创建型设计模式，其在父类中提供一个创建对象的方法， 允许子类决定实例化对象的类型。这种设计模式也是 Java 开发中最常见的一种模式，它的主要意图是定义一个创建对象的接口，让其子类自己决定实例化哪一个工厂类，工厂模式使其创建过程延迟到子类进行。简单说就是为了提供代码结构的扩展性，屏蔽每一个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可，同时，这也是去掉众多ifeslse的方式。当然这可能也有一些缺点，比如需要实现的类非常多，如何去维护，怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中，逐步降低。三、模拟发奖多种商品为了可以让整个学习的案例更加贴近实际开发，这里模拟互联网中在营销场景下的业务。由于营销场景的复杂、多变、临时的特性，它所需要的设计需要更加深入，否则会经常面临各种紧急CRUD操作，从而让代码结构混乱不堪，难以维护。在营销场景中经常会有某个用户做了一些操作；打卡、分享、留言、邀请注册等等，进行返利积分，最后通过积分在兑换商品，从而促活和拉新。那么在这里我们模拟积分兑换中的发放多种类型商品，假如现在我们有如下三种类型的商品接口；序号类型接口1优惠券CouponResult sendCoupon(String uId, String couponNumber, String uuid)2实物商品Boolean deliverGoods(DeliverReq req)3第三方爱奇艺兑换卡void grantToken(String bindMobileNumber, String cardId)从以上接口来看有如下信息：三个接口返回类型不同，有对象类型、布尔类型、还有一个空类型。入参不同，发放优惠券需要仿重、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。另外可能会随着后续的业务的发展，会新增其他种商品类型。因为你所有的开发需求都是随着业务对市场的拓展而带来的。四、用一坨坨代码实现ifelse实现需求public class PrizeController { private Logger logger = LoggerFactory.getLogger(PrizeController.class); public AwardRes awardToUser(AwardReq req) { String reqJson = JSON.toJSONString(req); AwardRes awardRes = null; try { logger.info("奖品…

Java8 Stream 真香，赶紧来舔一舔
type: item
date: 2021-09-07T06:29:40+00:00
description: 如今Java14已经发布许久了，Java15也在路上跑着了，然鹅不少小伙伴Java8的API应该还没用溜吧！今天跟各位小伙伴们聊聊Java Stream API的具体使用方法以及应用场景。举哥简单的例子，定义一个数组：String[] users = new String[]{"张三","李四","王二"};我们来使用之前的方式遍历：for(int i=0;i<users.length;i++){ System.out.println(users[i]); }亦或者使用：for(String u:users){ System.out.println(u); }使用最新的API实现方式：Stream<String> stream = Stream.of(users); stream.forEach(user-> System.out.println(user));然鹅好像并没有多么方便，仅仅少撸了一行代码，下面跟大家分享一个稍微复杂一点的例子。首先定义一个学生类：@Data @Builder public class Student { private String name; private Integer age; private Integer sex; private Double score; }定义基础数据：List<Student> list = new ArrayList<>(); list.add(new Student("张三",28,1,78.9)); list.add(new Student("李四",20,1,98.0)); list.add(new Student("王二",29,0,100.0)); list.add(new Student("张三",28,1,78.5)); Stream<Student> stream = list.stream();遍历：stream.forEach(stu-> System.out.println(stu.getName()));过滤性别：stream.filter(stu -> stu.getSex()==1) .forEach(stu -> System.out.println(stu.getName()));名字去重：stream.distinct().forEach(stu -> System.out.println(stu.getName()));年龄排序：stream.sorted(Comparator.comparingInt(Student::getAge)) .forEach(stu -> System.out.println(stu.getName()));排序反转：stream.sorted(Comparator.comparingInt(Student::getAge).reversed()) .forEach(stu -> System.out.println(stu.getName()));姓名集合：List<String> userList = stream.map(stu -> stu.getName()) .collect(Collectors.toList());分数求和：Optional<Double> totalScore = stream.map(Student::getScore) .reduce((x, y) -> x+y); System.out.println(totalScore.get());求分数最大值：Optional<Double> maxScore = stream.map(Student::getScore).reduce(Double::max);求分数最小值：Optional<Double> minScore = stream.map(Student::getScore).reduce(Double::min);取前三名同学：stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .limit(3) .forEach(System.out::println);扔了前三名同学：stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .skip(3) .forEach(System.out::println);卧槽太多了，撸主举不过来了，但是基本上常用的就这么几个。函数式编程写出的代码更加简洁且意图明确，使用stream接口让你从此告别for循环，小伙伴们赶紧入坑享受吧！！！

Spring Boot + Dataway ：接口不用写，配配就出来？
type: item
date: 2020-05-13T08:28:00+00:00
description: Dataway介绍Dataway 是基于 DataQL 服务聚合能力，为应用提供的一个接口配置工具。使得使用者无需开发任何代码就配置一个满足需求的接口。整个接口配置、测试、冒烟、发布。一站式都通过 Dataway 提供的 UI 界面完成。UI 会以 Jar 包方式提供并集成到应用中并和应用共享同一个 http 端口，应用无需单独为 Dataway 开辟新的管理端口。 这种内嵌集成方式模式的优点是，可以使得大部分老项目都可以在无侵入的情况下直接应用 Dataway。进而改进老项目的迭代效率，大大减少企业项目研发成本。 Dataway 工具化的提供 DataQL 配置能力。这种研发模式的变革使得，相当多的需求开发场景只需要配置即可完成交付。从而避免了从数据存取到前端接口之间的一系列开发任务，例如：Mapper、BO、VO、DO、DAO、Service、Controller 统统不在需要。Dataway 是 Hasor 生态中的一员，因此在 Spring 中使用 Dataway 首先要做的就是打通两个生态。根据官方文档中推荐的方式我们将 Hasor 和 Spring Boot 整合起来。这里是原文：https://www.hasor.net/web/extends/spring/for_boot.html第一步：引入相关依赖<dependency> <groupId>net.hasor</groupId> <artifactId>hasor-spring</artifactId> <version>4.1.3</version> </dependency> <dependency> <groupId>net.hasor</groupId> <artifactId>hasor-dataway</artifactId> <version>4.1.3-fix20200414</version> </dependency>hasor-spring 负责 Spring 和 Hasor 框架之间的整合。hasor-dataway 是工作在 Hasor 之上，利用 hasor-spring 我们就可以使用 dataway了。第二步：配置 Dataway，并初始化数据表dataway 会提供一个界面让我们配置接口，这一点类似 Swagger 只要jar包集成就可以实现接口配置。找到我们 springboot 项目的配置文件 application.properties# 是否启用 Dataway 功能（必选：默认false） HASOR_DATAQL_DATAWAY=true # 是否开启 Dataway 后台管理界面（必选：默认false） HASOR_DATAQL_DATAWAY_ADMIN=true # dataway API工作路径（可选，默认：/api/） HASOR_DATAQL_DATAWAY_API_URL=/api/ # dataway-ui 的工作路径（可选，默认：/interface-ui/） HASOR_DATAQL_DATAWAY_UI_URL=/interface-ui/ # SQL执行器方言设置（可选，建议设置） HASOR_DATAQL_FX_PAGE_DIALECT=mysqlDataway 一共涉及到 5个可以配置的配置项，但不是所有配置都是必须的。其中 HASOR_DATAQL_DATAWAY、HASOR_DATAQL_DATAWAY_ADMIN 两个配置是必须要打开的，默认情况下 Datawaty 是不启用的。Dataway 需要两个数据表才能工作，下面是这两个数据表的简表语句。下面这个 SQL 可以在 dataway的依赖 jar 包中 “META-INF/hasor-framework/mysql” 目录下面找到，建表语句是用 mysql 语法写的。CREATE TABLE `interface_info` ( `api_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', `api_method` varchar(12) NOT NULL COMMENT 'HttpMethod：GET、PUT、POST', `api_path` varchar(512) NOT NULL COMMENT '拦截路径', `api_status` int(2) NOT NULL COMMENT '状态：0草稿，1发布，2有变更，3禁用', `api_comment` varchar(255) NULL COMMENT '注释', `api_type` varchar(24) NOT NULL COMMENT '脚本类型：SQL、DataQL', `api_script` me…

三个水桶等分8升水的问题
type: item
date: 2020-04-06T14:59:00+00:00
description: 智力题目有三个容积分别为3升、5升、8升的水桶，其中容积为8升的水桶中装满了水，容积为3升和容积为5升的水桶都是空的。三个水桶都没有刻度，现在需要将大水桶中的8升水等分成两份，每份都是4升水，附加条件是只能这三个水桶，不能借助其他辅助容器。“恩，是的，这是一个很经典的问题。”“然而，我们并不能想全，不信请继续往下看。”答案”废话不多说，直接看方法吧。“第一种（7步）将8L的水桶中的水，倒满5L的水桶，这时：8L水桶为3L、5L水桶为5L、3L水桶为0L将5L的水桶中的水，倒满3L的水桶，这时：8L水桶为3L、5L水桶为2L、3L水桶为3L将3L的水桶中的水，倒入8L的水桶，这时：8L水桶为6L、5L水桶为2L、3L水桶为0L将5L的水桶中的水，倒入3L的水桶，这时：8L水桶为6L、5L水桶为0L、3L水桶为2L将8L的水桶中的水，倒入5L的水桶，这时：8L水桶为1L、5L水桶为5L、3L水桶为2L将5L的水桶中的水，倒满3L的水桶，这时：8L水桶为1L、5L水桶为4L、3L水桶为3L将3L的水桶中的水，倒入8L的水桶，这时：8L水桶为4L、5L水桶为4L、3L水桶为0L第二种（8步）将8L的水桶中的水，倒满3L的水桶，这时：8L水桶为5L、5L水桶为0L、3L水桶为3L将3L的水桶中的水，倒入5L的水桶，这时：8L水桶为5L、5L水桶为3L、3L水桶为0L将8L的水桶中的水，倒满3L的水桶，这时：8L水桶为2L、5L水桶为3L、3L水桶为3L将3L的水桶中的水，倒满5L的水桶，这时：8L水桶为2L、5L水桶为5L、3L水桶为1L将5L的水桶中的水，倒入8L的水桶，这时：8L水桶为7L、5L水桶为0L、3L水桶为1L将3L的水桶中的水，倒入5L的水桶，这时：8L水桶为7L、5L水桶为1L、3L水桶为0L将8L的水桶中的水，倒满3L的水桶，这时：8L水桶为4L、5L水桶为1L、3L水桶为3L将3L的水桶中的水，倒入5L的水桶，这时：8L水桶为4L、5L水桶为4L、3L水桶为0L我相信答案肯定不止两个，到底有多少种答案？带着这个疑问，我们来设计一个算法吧。问题分析人的思维解决这个问题的关键是怎么通过倒水凑出确定的1升水或能容纳1升水的空间。例如，当8L水桶或5L水桶或3L水桶有1L水时，都能快速倒出4L水。计算机思维“穷举法”水桶初始状态：8L水桶装满水，3L和5L的水桶为空。水桶最终状态：3L水桶为空，5L和8L的水桶各4L水。假设将每个状态下三个水桶中的水的体积作为status。从 status = array(8,0,0) 得到 status = array(4,4,0)。当然还会有一些限制：各个水桶的都有最大值：当前倒水之后各个水桶的状态，与历史倒水之后各个水桶的状态，不能相同。当前水桶为空时，不能倒给其他水桶。当前水桶为最大容积时，其他水桶不能再向这个水桶倒水。BucketWater.java桶的实体类，用于包装容量和剩余的水public class BucketWater implements Cloneable { private int index; private int capacity; private int remainWater; public BucketWater() { } public BucketWater(int index, int remainWater , int capacity) { this.index = index; this.remainWater = remainWater; this.capacity = capacity; } public int getRemainWater() { return remainWater; } public void setRemainWater(int remainWater) { this.remainWater = remainWater; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public int getCapacity() { return capacity; } public void setCapacity(int capacity) { this.capacity = capacity; } @Override public BucketWater clone() throws CloneNotSupportedException { return (BucketWater)super.clone(); } }BucketStatus.java桶状态的实体类，主要…

MySQL的Limit 性能差？真的不能再用了？
type: item
date: 2020-03-29T12:36:00+00:00
description: 一，前言首先说明一下MySQL的版本：mysql> select version(); +-----------+ | version() | +-----------+ | 5.7.17 |row in set (0.00 sec)表结构：mysql> desc test; +--------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +--------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | val | int(10) unsigned | NO | MUL | 0 | | | source | int(10) unsigned | NO | | 0 | |rows in set (0.00 sec)id为自增主键，val为非唯一索引。灌入大量数据，共500万：mysql> select count(*) from test; +----------+ | count(*) | +----------+ | 5242882 |row in set (4.25 sec)我们知道，当limit offset rows中的offset很大时，会出现效率问题：mysql> select * from test where val=4 limit 300000,5;idvalsource332762244332763244332764244332765244332766244rows in set (15.98 sec)为了达到相同的目的，我们一般会改写成如下语句：mysql> select * from test a inner join (select id from test where val=4 limit 300000,5) b on a.id=b.id;idvalsourceid33276224433276223327632443327632332764244332764233276524433276523327662443327662rows in set (0.38 sec)时间相差很明显。为什么会出现上面的结果？我们看一下select * from test where val=4 limit 300000,5;的查询过程：查询到索引叶子节点数据。根据叶子节点上的主键值去聚簇索引上查询需要的全部字段值。类似于下面这张图：像上面这样，需要查询300005次索引节点，查询300005次聚簇索引的数据，最后再将结果过滤掉前300000条，取出最后5条。MySQL耗费了大量随机I/O在查询聚簇索引的数据上，而有300000次随机I/O查询到的数据是不会出现在结果集当中的。肯定会有人问：既然一开始是利用索引的，为什么不先沿着索引叶子节点查询到最后需要的5个节点，然后再去聚簇索引中查询实际数据。这样只需要5次随机I/O，类似于下面图片的过程：其实我也想问这个问题。证实下面我们实际操作一下来证实上述的推论：为了证实select * from test where val=4 limit 300000,5是扫描300005个索引节点和300005个聚簇索引上的数据节点，我们需要知道MySQL有没有办法统计在一个sql中通过索引节点查询数据节点的次数。我先试了Handler_read_*系列，很遗憾没有一个变量能满足条件。我只能通过间接的方式来证实：InnoDB中有buffer pool。里面存有最近访问过的数据页，包括数据页和索引页。所以我们需要运行两个sql，来比较buffer pool中的数据页的数量。预测结果是运行select * from test a inner join (select id from test where val=4 limit 300000,5) b>之后，buffer pool中的数据页的数量远远少于select * from test where val=4 limit 300000,5;对应的数量，因为前一个sql只访问5次数据页，而后一个sql访问300005次数据页。select * from test where val=4 limit 300000,5 mysql> select index_name,count(*) from information_schema.INNODB_BUFFER…

为什么Mysql的常用引擎都默认使用B+树作为索引？
type: item
date: 2020-03-28T07:18:00+00:00
description: 一、前言为了讲清楚这个问题，阿粉先带大家了解一下什么是索引。我记得刚刚学习数据库的时候，老师喜欢用书本的目录来类比数据库的索引，并告诉我们索引能够像目录一样，让我们更快地找到想要找到的数据。如果是第一次接触索引，这个比喻能够让我们有一个直观的印象。但是当深入去学习索引的时候，我们不能继续持有索引即目录的思想，我们要跳出来去思考索引的本质是什么。二、索引的本质在没有索引的情况下，我们查找数据只能按照从头到尾的顺序逐行查找，每查找一行数据，意味着我们要到到磁盘相应的位置去读取一条数据。如果把查询一条数据分为到磁盘中查询和比对查询条件两步的话，到磁盘中查询的时间会远远大于比对查询条件的时间，这意味着在一次查询中，磁盘io占用了大部分的时间。更进一步地说，一次查询的效率取绝于磁盘io的次数，如果我们能够在一次查询中尽可能地降低磁盘io的次数，那么我们就能加快查询的速度。在知道了减少磁盘io能加快查询速度后，我们就要聚焦于如何减少磁盘io。如果按照原表逐行查询的话，n条数据就要查询n次，也就是O(N)的时间复杂度，为了减少磁盘io的次数，我们必须用一种查询时间复杂度更低的数据结构来保存数据。这种查询时间复杂度低的数据结构，我们称之为索引。所以通俗来说，索引其实就是某种数据结构，能充当索引的数据结构是多种多样的。三、索引的选择既然索引是一种便于查询的数据结构，如果大家对数据结构有一定了解的话，大概率会首选树型结构。毕竟树型结构普遍有着O(logN)的查询时间复杂度，而且插入删除数据的性能也比较平均。（可能你会说数组，哈希表的查询速度也很高啊，这个后面也会分析）虽然我们都已经知道Mysql中最常用的引擎像InnoDB和MyISAM，最终都选择了B+树作为索引，但是这里我还是打算从最常见的二叉树开始讲起，推导一下为什么最终选择了B+树作为索引，并比较一下几种树型结构在充当索引时的优劣。二叉树最普通的二叉树的问题在于他不能保证O(logN)的查询时间复杂度，我们看下面的图：由于插入的元素逐渐增大，元素始终在右边进行插入，好好的一棵二叉树最终变成了一条“链表”。在这种极端的情况下，二叉树的查询时间复杂度不再是O(logN)，而是退化为O(N)，这样显然不符合索引的要求。平衡二叉树（红黑树）像红黑树这样的平衡二叉树，无论如何插入元素，他都可以通过一些旋转的方法调整树的高度，使得整棵树的查询效率维持在O(logN)，如下图所示：这么来说他已经符合了成为索引的必备条件，但是最终没有选择他作为索引说明还有不足的地方。仔细看看可以发现平衡二叉树的每个节点只有两个孩子节点，如果一张表的数据量特别大，整棵树的高度也会随之上升。一个千万级别的表如果用平衡二叉树作为索引的话，树高将会达到二十多层。这也就意味着做一次查询需要二十多次磁盘io，这是一个不小的开销。那么有没有能在大数据量的情况下，还能保持一个较小树高的树型结构呢？B树和B+树答案就是B树。上面我们说到了平衡二叉树的瓶颈在于一个节点只有两个孩子节点，而B树一个节点可以存放N个孩子节点，这就完美解决了树高的问题，我们可以把B树称为平衡多叉树，B树作为索引如下图所示：但是以B树的结构作为索引仍有可以优化的地方，我们先看看最终的B+树，再仔细分析B+树在B树的基础上作了哪些改进，为什么B+树最终能够胜任索引的工作：从图片中可以看到B+树同样是一棵多差平衡树，和B树一样很好地解决了树高的问题。改进点一：但仔细看可以发现，B树的节点中既存储索引，也存储表对应的数据；而B+树的非叶子节点是不存储数据的，只存储索引，数据全部存储在叶子节点上。为什么要做这样的改进？我们做一次算术就知道了。假设树高为2，主键ID为bigint类型，长度为8字节，节点指针为6字节，一行数据记录的大小为1k，一次io操作能获得一页16k的数据。在索引为B+树的情况下，根节点能存储：16k / （6 + 8） = 1170 条索引指针；到了第一层，一共能指向 1170 1170 = 1368900 条索引指针；到了最底一层叶子节点，一个节点能存储16k / 1k = 16 条记录，一共能存储 1170 1170 * 16 = 21902400 条记录在B树的情况下，由于非叶子节点使用了大量空间存储数据，存放的索引指针肯定就少，最终整棵树如果想要存储和B+树一样多的数据就必须要增加树高，这样一来就增加了磁盘io，所以说B+树作为索引的性能比B树高。改进点二：叶子节点之间使用指针连接，提高区间访问效率。如果我们要进行范围查询，可以轻松通过B+树叶子节点之间的指针进行遍历，减少了不必要的磁盘io。总结看到这里，相信大家对为什么Mysql的常用引擎都默认使用B+树作为索引已经有了初步的认知。我们只要牢记一点：索引是为了减少磁盘io提高查询性能而存在的。…