博客

  • 九点半助手

    快乐分享,Java干货及时送达👇

    点击复制

    打开浏览器访问查资料

    GitHub各位应该都很熟悉了,全球最大的开源社区,也是全球最大的同性交友网站~~,但是大部分同学使用GitHub应该就是通过别人的开源链接,点进去下载对应的项目,而真正使用Github来查找开源项目的还是少数,

    面试总得有几个和所求岗位相关的项目,如果应届生、转行的童鞋没有项目,就靠简单的javaSE或者其他语言基础那只能说“你太难了”。

    通过 Github ,你可以很方便的下载自己需要的项目,了解实时热点的项目,通过对优秀的开源项目的学习,更好的进行学习与提高

    图片

     

    那么如何使用Github高效率的查找项目呢?这篇文章带你了解一下

     

    仓库分几种?

    • 本地仓库:建立在本地的文件夹。
    • 远程仓库:建立在互联网的服务器内的文件夹。

    分布式版本控制系统

    • 配有两个仓库,在你的电脑上有一个 本地仓库 ,在远程的服务器上有一个 远程仓库 。
    • 我们在提交文件的时候会先提交到本地仓库,然后在有网络的情况下,再从本地仓库提交到网络上的远程仓库。
    • Git 就是一个典型的分布式版本控制系统
    • Github就担任了上述的远程仓库这一角色,就是一个存放在外网服务器上的一个文件夹。并且Github是免费的开源的托管平台

    什么是Git

    Git (读音为/gɪt/)是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。

    GitHub是一个面向开源及私有软件项目的托管平台,因为只支持git 作为唯一的版本库格式进行托管,故名GitHub。

    图片

    Github常用词含义

    • watch:会持续收到项目的动态
    • fork:复制某个项目到自己的仓库
    • star:点赞数,表示对该项目表示认可,点赞数越多的项目一般越火
    • clone:将项目下载到本地
    • follow:关注你感兴趣的作者,会收到他们的动态

    一个完整的项目界面

    图片
    • ① 此处是项目作者名/项目名
    • ② 此处是项目的点赞数,和fock数,越火的项目点赞和fock就会越多
    • ③ 项目的Description 和Website 和tags 也就是项目的说明和标签, 通过此处你可以一眼了解该项目的功能和简介
    • ④ 项目的commits提交数 ,一般比较好的项目,维护会比较频繁,更新也会频繁,提交数就会多
    • ⑤项目提交时间, 通过这里你可以看到项目的提交时间,防止自己下载了一些远古项目
    • README.md README.md文件是一个项目的入门手册,里面介绍了整个项目的使用、功能等等。所以README文件写得好不好,关系到这个项目能不能更容易的被其他人了解和使用。

    使用Github搜索项目

    一般人用Github的步骤 直接搜索,选择一下Languages 设置下项目排序顺序 就直接下载

    然后就是克隆仓库,阅读md,看项目源代码,看不懂,关闭项目,删除。

    图片

    这样是很难找到真正适合自己的项目的,

    GitHub里面有很多有价值的开源项目和代码,如何在海量的代码库中搜索我们需要的信息,那么接下来将带你了解下如何利用GitHub强大的搜索功能,来找到适合自己的项目

    GitHub的高级搜索

    GitHub有高级搜索功能,search/advanced可以输入关键字、代码库大小、包含作者、代码语、代码包含后缀文件名等。

    图片

    图片

    这里我们假设正要学习 Spring Boot,要找一个 Spring Boot的 Demo 来进行参考学习。

    精准搜索仓库标题、仓库描述、README

    in关键词限制搜索范围

    按照项目名/仓库名搜索(大小写不敏感)

    (1)公式

    • in:name xxx 项目名包含xxx
    • in:description xxx 项目描述包含xxx
    • in:readme xxx 项目介绍文档里含有xxx

    比如我搜索项目名里含有 Spring Boot 的 in:name Spring Boot

    会发现项目数量由17W变成了11W

    图片

    搜索项目描述里含有 Spring Boot 的 in:description Spring Boot

    图片

    stars或fork数量去查找

    一个项目 star 数的多少,一般代表该项目的受欢迎程度 越受欢迎的项目,star数和fork数一定也不会少

    (1)公式

    • stars:>xxx stars数大于xxx
    • stars:xx..xx stars数在xx…xx之间
    • forks:>xxx forks数大于xxx
    • forks:xx..xx forks数在xx…xx之间
     查找star数大于等于5000的springboot项目
         spring boot stars:>=5000
     查找fork数大于500的springcloud项目
         spring cloud forks:>500
     查找fork在100到200之间并且stars数在80到100之间的springboot项目
         spring boot forks:100..200 stars:80..100
    

    我们进一步缩小范围,Star数量过滤,要求Star数量大于3000

    in:name spring boot starts :> 3000
    

    可以看到只有一千多个项目供我们选择了

    图片

    按照地区和语言进行搜索

    很多时候我们的项目是要用我们会的语言,你找到了一个Python写的好项目,但是没学过Python,下载了也看不懂,同时,为了更好的阅读README.md帮助文档以及项目注释,我想很多同学都会想要下载中文的项目,当然英语顶呱呱的请忽略

    (1)公式

    • location:地区
    • language:语言
    语言为javaScript   
    language:javaScript   
    地区为china
    location: China
    

    如果你要寻找使用 javascript 语言的国产项目,整个搜索条件就是:language:javascript location:china,从搜索结果来看,我们找到了五百多万javascript 项目,近 21000 多名地区信息填写为 China 的 javascript 开发者,

    图片

    根据仓库大小搜索

    如果你只是想找一些小型的项目进行个人学习和开发,不想找特别复杂的,那么使用size关键字查找简单的 Demo,就成了你的首选

    (1)公式

    size:>= 数字
    

     

    注意:100代表100Kb 单位为Kb

     

    根据仓库是否在更新的搜索

    寻找项目当然是想要找到最新的项目,而不是好久都没有更新的老项目了,

    (1)公式

    • pushed:> YYYY-MM-DD 最后上传日期大于YYYY-MM-DD
    • created:> YYYY-MM-DD 创建日期大于YYYY-MM-DD

    比如我们想要寻找2020年最新更新的项目,可以用 pushed:>2020-01-03 Spring Boot ,这样子就可以找到今年一月份之后更新的最新项目

    图片

    根据某个人或组织进行搜索

    如果你想在GitHub 上找一下某个大神是不是提交了新的项目,可以对他们进行精准搜索

    (1)公式

    • user: name 查找某个用户
    • org: name 查找某个组织
    • followers:>=xxx 查找关注者数量超过xxx的开发者

    比方说我们想要找一下廖雪峰老师的python开源项目

    user:MichaelLiao language:python
    

    图片

    根据仓库的LICENSE搜索

    License是很多人容易忽略的一个问题

    开源项目的License(项目授权协议) 有的开源项目作者明确禁止商用了,但是你不知情下载了,并且使用了,这就会很麻烦,“非常友好”的协议,比较出名的有这几种:BSD、MPL(Mozilla)、Apache、MIT。这些协议不但允许项目的使用者使用开源库,有些还允许对开源库进行修改并重新分发。因此用起来特别爽。上述这几个协议在细节上有些小差异,大伙儿可以去它们官网瞧一下。

    以下这个网站,详细介绍了各个License的区别。

     

    • http://choosealicense.com/licenses/

     

    (1)公式

    -license:对应协议

    例如咱们要找协议是最为宽松的 Apache License 2 的代码,

    license:apache-2.0 Spring Boot
    

    图片

    awesome加强搜索

    Awesome 似乎已经成为不少 GitHub 项目喜爱的命名之一,Awesome 往往整合了大量的同一领域的资料,让大家可以更好的学习。

    (1)公式

    awesome 关键字 awesome 系列一般是用来收集学习、工具、书籍类相关的项目

    • 比如搜索优秀的python相关的项目,包括框架、教程等

    图片

    awesome-python,这个库提供了各个领域常见的python库支持。整体看下来,几乎涵盖了所有的常见的计算机领域,

    热门搜索(GitHub Trend 和 GitHub Topic)

    GitHub Trend 页面总结了每天/每周/每月周期的热门 Repositories 和 Developers,你可以看到在某个周期处于热门状态的开发项目和开发者

    图片

    GitHub Topic 展示了最新和最流行的讨论主题,在这里你不仅能够看到开发项目,还能看到更多非开发技术的讨论主题,

    图片

     

    作者:Z小旋

    来源:blog.csdn.net/as480133937/article/

    details/105611577

     

    
    

  • Spring 6 正式“抛弃”feign

    快乐分享,Java干货及时送达👇

    来源:https://juejin.cn/post/7173271507047546893

    近期,Spring 6 的第一个 GA 版本发布了,其中带来了一个新的特性——HTTP Interface。这个新特性,可以让开发者将 HTTP 服务,定义成一个包含特定注解标记的方法的 Java 接口,然后通过对接口方法的调用,完成 HTTP 请求。看起来很像使用 Feign 来完成远程服务调用,下面我们参考官方文档来完成一个 Demo。

    完成一个 Demo

    首先创建一个简单的 HTTP 服务,这一步可以创建一个简单的 Spring Boot 工程来完成。

    先创建一个实体类:

    public class User implements Serializable {

        private int id;
        private String name;
        // 省略构造方法、Getter和Setter
        @Override
        public String toString() {
            return id + ":" + name;
        }
    }

    再写一个简单的 Controller:

    @GetMapping("/users")
    public List list() {
        return IntStream.rangeClosed(1, 10)
                .mapToObj(i -> new User(i, "User" + i))
                .collect(Collectors.toList());
    }

    确保启动服务之后,能够从http://localhost:8080/users地址获取到一个包含十个用户信息的用户列表。

    下面我们新建一个 Spring Boot 工程。

    图片

    这里需要注意,Spring Boot 的版本至少需要是 3.0.0,这样它以来的 Spring Framework 版本才是 6.0 的版本,才能够包含 HTTP Interface 特性,另外,Spring Framework 6.0 和 Spring Boot 3.0 开始支持的 Java 版本最低是 17,因此,需要选择至少是 17 的 Java 版本。

    另外,需要依赖 Spring Web 和 Spring Reactive Web 依赖,原因下文中会提到。

    创建好新的 Spring Boot 工程后,首先需要定义一个 HTTP Interface 接口。最简单的定义如下即可:

    public interface UserApiService {
        @GetExchange("/users")
        List getUsers();
    }

    然后,我们可以写一个测试方法。

    @Test
    void getUsers() {
       WebClient client = WebClient.builder().baseUrl("http://localhost:8080/").build();
       HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
       UserApiService service = factory.createClient(UserApiService.class);
       List users = service.getUsers();
       for (User user : users) {
          System.out.println(user);
       }
    }

    最终回打印获取到的是个用户信息:

    1:User1
    2:User2
    ...
    9:User9
    10:User10

    以上是一个最简单的示例,下面我们看看其中的一些细节。

    GetExchange(HttpExchange)注解

    上文例子中的 GetExchange 注解代表这个方法代替执行一个 HTTP Get 请求,与此对应,Spring 还包含了其他类似的注解:

    图片

    这些注解定义在spring-web模块的org.springframework.web.service.annotation包下,除了 HttpExchange 之外,其他的几个都是 HttpExchange 的特殊形式,这一点与 Spring MVC 中的 RequestMapping/GetMapping 等注解非常相似。

    以下是 HttpExchange 的源码:

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Mapping
    @Reflective(HttpExchangeReflectiveProcessor.class)
    public @interface HttpExchange {

        @AliasFor("url")
        String value() default "";

        @AliasFor("value")
        String url() default "";

        String method() default "";

        String contentType() default "";

        String[] accept() default {};

    }

    在上面的例子中,我们只指定了请求的资源路径。

    UserApiService 实例的创建

    在上面例子中,我们定义的 HTTP Interface 接口是 UserApiService,在测试方法中,我们通过 HttpServiceProxyFactory 创建了 UserApiService 的实例,这是参考了 Spring 的官方文档的写法。

    你也可以将创建的过程写到一个 @Bean 方法中,从而可以将创建好的实例注入到其他的组件中。

    我们再定义 UserApiService 的时候,只是声明了一个接口,那具体的请求操作是怎么发出的呢,我们可以通过 DEBUG 模式看得出来,这里创建的 UserApiService 的实例,是一个代理对象:

    图片

    目前,Spring 还没有提供更方便的方式来创建这些代理对象,不过,之后的版本肯定会提供,如果你感兴趣的话,可以从 HttpServiceProxyFactory 的createClient方法的源码中看到一些与创建 AOP 代理相似的代码,因此,我推测 Spring 之后可能会增加类似的注解来方便地创建代理对象。

    其他特性

    除了上述例子中的简单使用之外,添加了 HttpExchange 的方法还支持各种类型的参数,这一点也与 Spring MVC 的 Controller 方法类似,方法的返回值也可以是任意自定义的实体类型(就像上面的例子一样),此外,还支持自定义的异常处理。

    为什么需要 Spring Reactive Web 的依赖

    上文中创建工程的时候,引入了 Spring Reactive Web 的依赖,在创建代理的service对象的时候,使用了其中的 WebClient 类型。这是因为,HTTP Interface 目前只内置了 WebClient 的实现,它属于 Reactive Web 的范畴。Spring 在会在后续版本中推出基于 RestTemplate 的实现。

    总结

    本文带你对 HTTP Interface 特性进行了简单的了解,我之后会深入研究这个特性,也会追踪后续版本中的改进并与你分享,欢迎点赞加关注。

    参考资料

    [1]https://juejin.cn/post/7162096952883019783: https://juejin.cn/post/7162096952883019783

    
    

  • Java 陷阱:慎用入参做返回值!!!

    快乐分享,Java干货及时送达👇

    来源:https://my.oschina.net/waylau/blog/4771348

    正常情况下,在Java中入参是不建议用做返回值的。除了造成代码不易理解、语义不清等问题外,可能还埋下了陷阱等你入坑。

    问题背景

    比如有这么一段代码:

    @Named
    public class AService {
    private SupplyAssignment localSupply = new SupplyAssignment();
        @Inject
        private BService bervice;

        public List calcSupplyAssignment()
           List supplyList 
    = bService.getLocalSupplyList(this.localSupply);
            …
           return supplyList;
        }
    }

    上面代码,服务A希望调用服务B,以获取supplyList,但同时,服务A又希望修改localSupply的状态值,未能避免修改calcSupplyAssignment接口的(不想改返回的类型),将localSupply作为了入参但同时也用作了返回值。

    服务B代码如下:

    @Named
    public class BService {

    public List getLocalSupplyList (SupplyAssignment localSupply)
        SupplyAssignment supplyAssignment 
    this.getSupplyAssignment();
            // 希望localSupply被重新赋值后返回
            localSupply = supplyAssignment;
            …
            return supplyList;

        }
    }

    在服务B代码内部,服务A的入参localSupply被传入,希望重新被supplyAssignment赋值而后返回新值。然而,这样做是无效的。

    问题原因

    先来看下编程语言中关于参数传递的类型:

    • 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
    • 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

    因为Java程序设计语言是采用的值传递,因为Java没有指针的概念。也就是说方法得到的是所有参数值的一个拷贝,方法并不能修改传递给它的任何参数变量的内容。

    因此,上述代码中,服务A调用服务B时,服务B的参数localSupply实际上是服务A的localSupply的一个拷贝,当然,这两个都是指向了同一个地址对象supplyAssignment1。

    图片

    当在服务B内部对参数localSupply进行重新赋值是localSupply = supplyAssignment,实际上,只是对B的参数localSupply做了从新赋值,B的参数localSupply会指向一个新的地址对象supplyAssignment2。

    图片

    从上图可以清晰看到,因此,服务A的localSupply和B的参数localSupply已经指向了不同的对象了,对B的参数localSupply做任何的修改,都不会影响服务A的localSupply的原值。

    这就是问题的原因,你希望服务B来修改服务A入参的状态,并将改后的值返回给服务A,但并不奏效。

    包含一些大佬的学习资料,且配套了相关的实践案例的最强Java并发编程笔记详解,关注公众号SpringForAll社区,回复:Java,即可免费领取!

    解决方案

    方案1:入参不要用作返回值

    有时确实想要入参做返回值,那看方案2。

    方案2:入参不要赋值新对象

    这个方案就是直接在入参的对象上做状态的修改,而不要去赋值新对象。还是这个图:

    图片

    在这个图中,只要我们是一直在B的参数localSupply修改的是supplyAssignment1的状态值,那结果就能反馈到服务A的localSupply上。如何实现?看下下面代码:

    @Named
    public class BService {

        public List getLocalSupplyList (SupplyAssignment localSupply)

            SupplyAssignment supplyAssignment 
    this.getSupplyAssignment();

            // 针对localSupply不能新建引用,只能重新赋值属性
            BeanUtils.copyProperties(supplyAssignment, localSupply);
            …
            return supplyList;

        }

    }

    在上面的方法中,我们用到了Spring的工具类BeanUtils,该类的copyProperties方法的实质是将supplyAssignment的属性值,赋值到了localSupply的属性上。

    这意味着我们是修改的B的参数localSupply上的属性,而并未新建对象。

  • 九种分布式ID解决方案,总有一款适合你!

    快乐分享,Java干货及时送达👇

    • 1、UUID
    • 2、数据库自增ID
      • 2.1、主键表
      • 2.2、ID自增步长设置
    • 3、号段模式
    • 4、Redis INCR
    • 5、雪花算法
    • 6、美团(Leaf)
    • 7、百度(Uidgenerator)
    • 8、滴滴(TinyID)
    • 总结比较

    背景

    在复杂的分布式系统中,往往需要对大量的数据进行唯一标识,比如在对一个订单表进行了分库分表操作,这时候数据库的自增ID显然不能作为某个订单的唯一标识。除此之外还有其他分布式场景对分布式ID的一些要求:

    • 趋势递增: 由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
    • 单调递增: 保证下一个ID一定大于上一个ID,例如排序需求。
    • 信息安全: 如果ID是连续的,恶意用户的扒取工作就非常容易做了;如果是订单号就更危险了,可以直接知道我们的单量。所以在一些应用场景下,会需要ID无规则、不规则。

    就不同的场景及要求,市面诞生了很多分布式ID解决方案。本文针对多个分布式ID解决方案进行介绍,包括其优缺点、使用场景及代码示例。

    1、UUID

    UUID(Universally Unique Identifier)是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,可以生成全球唯一的编码并且性能高效。

    JDK提供了UUID生成工具,代码如下:

    import java.util.UUID;

    public class Test {
        public static void main(String[] args) {
            System.out.println(UUID.randomUUID());
        }
    }

    输出如下

    b0378f6a-eeb7-4779-bffe-2a9f3bc76380

    UUID完全可以满足分布式唯一标识,但是在实际应用过程中一般不采用,有如下几个原因:

    • 存储成本高: UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
    • 信息不安全: 基于MAC地址生成的UUID算法会暴露MAC地址,曾经梅丽莎病毒的制造者就是根据UUID寻找的。
    • 不符合MySQL主键要求: MySQL官方有明确的建议主键要尽量越短越好,因为太长对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

    2、数据库自增ID

    利用Mysql的特性ID自增,可以达到数据唯一标识,但是分库分表后只能保证一个表中的ID的唯一,而不能保证整体的ID唯一。为了避免这种情况,我们有以下两种方式解决该问题。

    2.1、主键表

    通过单独创建主键表维护唯一标识,作为ID的输出源可以保证整体ID的唯一。举个例子:

    创建一个主键表

    CREATE TABLE `unique_id`  (
      `id` bigint NOT NULL AUTO_INCREMENT,
      `biz` char(1NOT NULL,
      PRIMARY KEY (`id`),
     UNIQUE KEY `biz` (`biz`)
    ENGINE = InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET =utf8;

    业务通过更新操作来获取ID信息,然后添加到某个分表中。

    BEGIN;

    REPLACE INTO unique_id (biz) values ('o') ;
    SELECT LAST_INSERT_ID();

    COMMIT;

    2.2、ID自增步长设置

    我们可以设置Mysql主键自增步长,让分布在不同实例的表数据ID做到不重复,保证整体的唯一。

    如下,可以设置Mysql实例1步长为1,实例1步长为2。

    查看主键自增的属性

    show variables like '%increment%'

    显然,这种方式在并发量比较高的情况下,如何保证扩展性其实会是一个问题。

    3、号段模式

    号段模式是当下分布式ID生成器的主流实现方式之一。其原理如下:

    • 号段模式每次从数据库取出一个号段范围,加载到服务内存中。业务获取时ID直接在这个范围递增取值即可。
    • 等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,新的号段范围是(max_id ,max_id +step]。
    • 由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新。

    例如 (1,1000] 代表1000个ID,具体的业务服务将本号段生成1~1000的自增ID。表结构如下:

    CREATE TABLE id_generator (
      id int(10NOT NULL,
      max_id bigint(20NOT NULL COMMENT '当前最大id',
      step int(20NOT NULL COMMENT '号段的长度',
      biz_type    int(20NOT NULL COMMENT '业务类型',
      version int(20NOT NULL COMMENT '版本号,是一个乐观锁,每次都更新version,保证并发时数据的正确性',
      PRIMARY KEY (`id`)

    这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。但同样也会存在一些缺点比如:服务器重启,单点故障会造成ID不连续。

    4、Redis INCR

    基于全局唯一ID的特性,我们可以通过Redis的INCR命令来生成全局唯一ID。

    Redis分布式ID的简单案例

    /**
     *  Redis 分布式ID生成器
     */

    @Component
    public class RedisDistributedId {

        @Autowired
        private StringRedisTemplate redisTemplate;

        private static final long BEGIN_TIMESTAMP = 1659312000l;

        /**
         * 生成分布式ID
         * 符号位    时间戳[31位]  自增序号【32位】
         * @param item
         * @return
         */

        public long nextId(String item){
            // 1.生成时间戳
            LocalDateTime now = LocalDateTime.now();
            // 格林威治时间差
            long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
            // 我们需要获取的 时间戳 信息
            long timestamp = nowSecond - BEGIN_TIMESTAMP;
            // 2.生成序号 --》 从Redis中获取
            // 当前当前的日期
            String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
            // 获取对应的自增的序号
            Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date);
            return timestamp 32 | increment;
        }

    }

    同样使用Redis也有对应的缺点:ID 生成的持久化问题,如果Redis宕机了怎么进行恢复?

    5、雪花算法

    Snowflake,雪花算法是有Twitter开源的分布式ID生成算法,以划分命名空间的方式将64bit位分割成了多个部分,每个部分都有具体的不同含义,在Java中64Bit位的整数是Long类型,所以在Java中Snowflake算法生成的ID就是long来存储的。具体如下:

    • 第一部分: 占用1bit,第一位为符号位,不适用
    • 第二部分: 41位的时间戳,41bit位可以表示241个数,每个数代表的是毫秒,那么雪花算法的时间年限是(241)/(1000×60×60×24×365)=69
    • 第三部分: 10bit表示是机器数,即 2^ 10 = 1024台机器,通常不会部署这么多机器
    • 第四部分: 12bit位是自增序列,可以表示2^12=4096个数,一秒内可以生成4096个ID,理论上snowflake方案的QPS约为409.6w/s

    雪花算法案例代码:

    public class SnowflakeIdWorker {

        // ==============================Fields===========================================
        /**
         * 开始时间截 (2020-11-03,一旦确定不可更改,否则时间被回调,或者改变,可能会造成id重复或冲突)
         */

        private final long twepoch = 1604374294980L;

        /**
         * 机器id所占的位数
         */

        private final long workerIdBits = 5L;

        /**
         * 数据标识id所占的位数
         */

        private final long datacenterIdBits = 5L;

        /**
         * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
         */

        private final long maxWorkerId = -1L ^ (-1L 
        /**
         * 支持的最大数据标识id,结果是31
         */

        private final long maxDatacenterId = -1L ^ (-1L 
        /**
         * 序列在id中占的位数
         */

        private final long sequenceBits = 12L;

        /**
         * 机器ID向左移12位
         */

        private final long workerIdShift = sequenceBits;

        /**
         * 数据标识id向左移17位(12+5)
         */

        private final long datacenterIdShift = sequenceBits + workerIdBits;

        /**
         * 时间截向左移22位(5+5+12)
         */

        private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

        /**
         * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
         */

        private final long sequenceMask = -1L ^ (-1L 
        /**
         * 工作机器ID(0~31)
         */

        private long workerId;

        /**
         * 数据中心ID(0~31)
         */

        private long datacenterId;

        /**
         * 毫秒内序列(0~4095)
         */

        private long sequence = 0L;

        /**
         * 上次生成ID的时间截
         */

        private long lastTimestamp = -1L;

        //==============================Constructors=====================================

        /**
         * 构造函数
         *
         */

        public SnowflakeIdWorker() {
            this.workerId = 0L;
            this.datacenterId = 0L;
        }

        /**
         * 构造函数
         *
         * @param workerId     工作ID (0~31)
         * @param datacenterId 数据中心ID (0~31)
         */

        public SnowflakeIdWorker(long workerId, long datacenterId) {
            if (workerId > maxWorkerId || workerId 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }

        // ==============================Methods==========================================

        /**
         * 获得下一个ID (该方法是线程安全的)
         *
         * @return SnowflakeId
         */

        public synchronized long nextId() {
            long timestamp = timeGen();

            //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
            if (timestamp             throw new RuntimeException(
                        String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }

            //如果是同一时间生成的,则进行毫秒内序列
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                //毫秒内序列溢出
                if (sequence == 0) {
                    //阻塞到下一个毫秒,获得新的时间戳
                    timestamp = tilNextMillis(lastTimestamp);
                }
            }
            //时间戳改变,毫秒内序列重置
            else {
                sequence = 0L;
            }

            //上次生成ID的时间截
            lastTimestamp = timestamp;

            //移位并通过或运算拼到一起组成64位的ID
            return ((timestamp - twepoch) //
                    | (datacenterId //
                    | (workerId //
                    | sequence;
        }

        /**
         * 阻塞到下一个毫秒,直到获得新的时间戳
         *
         * @param lastTimestamp 上次生成ID的时间截
         * @return 当前时间戳
         */

        protected long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp             timestamp = timeGen();
            }
            return timestamp;
        }

        /**
         * 返回以毫秒为单位的当前时间
         *
         * @return 当前时间(毫秒)
         */

        protected long timeGen() {
            return System.currentTimeMillis();
        }

        /**
         * 随机id生成,使用雪花算法
         *
         * @return
         */

        public static String getSnowId() {
            SnowflakeIdWorker sf = new SnowflakeIdWorker();
            String id = String.valueOf(sf.nextId());
            return id;
        }

        //=========================================Test=========================================

        /**
         * 测试
         */

        public static void main(String[] args) {
            SnowflakeIdWorker idWorker = new SnowflakeIdWorker(00);
            for (int i = 0; i 1000; i++) {
                long id = idWorker.nextId();
                System.out.println(id);
            }
        }
    }

    雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复。通常通过记录最后使用时间处理该问题。

    6、美团(Leaf)

    由美团开发,开源项目链接:

    • https://github.com/Meituan-Dianping/Leaf

    Leaf同时支持号段模式和snowflake算法模式,可以切换使用。

    snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。

    号段模式是对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的频率操作。相当于从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。

    7、百度(Uidgenerator)

    源码地址:

    • https://github.com/baidu/uid-generator

    中文文档地址:

    • https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

    UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。它是分布式的,并克服了雪花算法的并发限制。单个实例的QPS能超过6000000。需要的环境:JDK8+,MySQL(用于分配WorkerId)。

    百度的Uidgenerator对结构做了部分的调整,具体如下:

    时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年(2^28-1/86400/365),不过UidGenerator可以适当调整delta seconds、worker node id和sequence占用位数。

    8、滴滴(TinyID)

    由滴滴开发,开源项目链接:

    • https://github.com/didi/tinyid

    Tinyid是在美团(Leaf)的leaf-segment算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了tinyid-client客户端的接入方式,使用起来更加方便。但和美团(Leaf)不同的是,Tinyid只支持号段一种模式不支持雪花模式。Tinyid提供了两种调用方式,一种基于Tinyid-server提供的http方式,另一种Tinyid-client客户端方式。

    总结比较

    作者:叫我二蛋

    来源:wangbinguang.blog.csdn.net/article/

    details/129201971


  • Jenkins 真得很牛逼!只是大部分人不会用而已~(保姆级教程)

    快乐分享,Java干货及时送达👇

    文章来源:https://zhangzhuo.ltd/articles/2022/06/04/1654333399919.html


    目录

    • 什么是流水线
    • 声明式流水线
    • Jenkinsfile 的使用


    什么是流水线


    jenkins 有 2 种流水线分为声明式流水线脚本化流水线,脚本化流水线是 jenkins 旧版本使用的流水线脚本,新版本 Jenkins 推荐使用声明式流水线。文档只介绍声明流水线。

    1、声明式流水线


    在声明式流水线语法中,流水线过程定义在 Pipeline{}中,Pipeline 块定义了整个流水线中完成的所有工作,比如

    参数说明:
    • agent any:在任何可用的代理上执行流水线或它的任何阶段,也就是执行流水线过程的位置,也可以指定到具体的节点
    • stage:定义流水线的执行过程(相当于一个阶段),比如下文所示的 Build、Test、Deploy, 但是这个名字是根据实际情况进行定义的,并非固定的名字
    • steps:执行某阶段具体的步骤。
    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
        stages {
          stage('Build') {
            steps {
              echo 'Build'
            }
          }
          stage('Test') {
            steps {
              echo 'Test'
            }
          }
          stage('Deploy') {
            steps {
              echo 'Deploy'
          }
        }
      }
    }

    2、脚本化流水线


    在脚本化流水线语法中,会有一个或多个 Node(节点)块在整个流水线中执行核心工作

    参数说明:
    • node:在任何可用的代理上执行流水线或它的任何阶段,也可以指定到具体的节点
    • stage:和声明式的含义一致,定义流水线的阶段。Stage 块在脚本化流水线语法中是可选的,然而在脚本化流水线中实现 stage 块,可以清楚地在 Jenkins UI 界面中显示每个 stage 的任务子集。
    //Jenkinsfile (Scripted Pipeline)
    node {
      stage('Build') {
        echo 'Build'
      }
      stage('Test') {
        echo 'Test'
      }
      stage('Deploy') {
        echo 'Deploy'
      }
    }


    声明式流水线


    声明式流水线必须包含在一个 Pipeline 块中,比如是一个 Pipeline 块的格式

    pipeline {
      /* insert Declarative Pipeline here */
    }

    在声明式流水线中有效的基本语句和表达式遵循与 Groovy 的语法同样的规则,但有以下例外

    • 流水线顶层必须是一个 block,即 pipeline{}
    • 分隔符可以不需要分号,但是每条语句都必须在自己的行上
    • 块只能由 Sections、Directives、Steps 或 assignment statements 组成
    • 属性引用语句被当做是无参数的方法调用,比如 input 会被当做 input()。

    1、Sections


    声明式流水线中的 Sections 不是一个关键字或指令,而是包含一个或多个 Agent、Stages、 post、Directives 和 Steps 的代码区域块。

    1.1 Agent

    Agent 表示整个流水线或特定阶段中的步骤和命令执行的位置,该部分必须在 pipeline 块的顶层被定义,也可以在 stage 中再次定义,但是 stage 级别是可选的。

    any

    在任何可用的代理上执行流水线,配置语法

    pipeline {
      agent any
    }
    none

    表示该 Pipeline 脚本没有全局的 agent 配置。当顶层的 agent 配置为 none 时, 每个 stage 部分都需要包含它自己的 agent。配置语法

    pipeline {
      agent none
      stages {
        stage('Stage For Build'){
          agent any
        }
      }
    }
    label

    以节点标签形式选择某个具体的节点执行 Pipeline 命令,例如:agent { label 'my-defined-label' }。节点需要提前配置标签。

    pipeline {
      agent none
        stages {
          stage('Stage For Build'){
            agent { label 'role-master' }
            steps {
              echo "role-master"
            }
          }
        }
    }
    node

    和 label 配置类似,只不过是可以添加一些额外的配置,比如 customWorkspace(设置默认工作目录)

    pipeline {
      agent none
        stages {
          stage('Stage For Build'){
            agent {
              node {
                label 'role-master'
                customWorkspace "/tmp/zhangzhuo/data"
              }
            }
            steps {
              sh "echo role-master > 1.txt"
            }
          }
        }
    }
    dockerfile

    使用从源码中包含的 Dockerfile 所构建的容器执行流水线或 stage。此时对应的 agent 写法如下

    agent {
       dockerfile {
         filename 'Dockerfile.build'  //dockerfile文件名称
         dir 'build'                  //执行构建镜像的工作目录
         label 'role-master'          //执行的node节点,标签选择
         additionalBuildArgs '--build-arg version=1.0.2' //构建参数
       }
    }
    docker

    相当于 dockerfile,可以直接使用 docker 字段指定外部镜像即可,可以省去构建的时间。比如使用 maven 镜像进行打包,同时可以指定 args

    agent{
      docker{
        image '192.168.10.15/kubernetes/alpine:latest'   //镜像地址
        label 'role-master' //执行的节点,标签选择
        args '-v /tmp:/tmp'      //启动镜像的参数
      }
    }
    kubernetes

    需要部署 kubernetes 相关的插件,官方文档:

    https://github.com/jenkinsci/kubernetes-plugin/

    Jenkins 也支持使用 Kubernetes 创建 Slave,也就是常说的动态 Slave。配置示例如下

    • cloud: Configure Clouds 的名称,指定到其中一个 k8s

    • slaveConnectTimeout: 连接超时时间

    • yaml: pod 定义文件,jnlp 容器的配置必须有配置无需改变,其余 containerd 根据自己情况指定

    • workspaceVolume:持久化 jenkins 的工作目录。

      • persistentVolumeClaimWorkspaceVolume:挂载已有 pvc。
    workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "jenkins-agent", mountPath: "/", readOnly: "false")
    • nfsWorkspaceVolume:挂载 nfs 服务器目录
    workspaceVolume nfsWorkspaceVolume(serverAddress: "192.168.10.254", serverPath: "/nfs", readOnly: "false")
    • dynamicPVC:动态申请 pvc,任务执行结束后删除
    workspaceVolume dynamicPVC(storageClassName: "nfs-client", requestsSize: "1Gi", accessModes: "ReadWriteMany")
    • emptyDirWorkspaceVolume:临时目录,任务执行结束后会随着 pod 删除被删除,主要功能多个任务 container 共享 jenkins 工作目录。
    workspaceVolume emptyDirWorkspaceVolume()
    • hostPathWorkspaceVolume:挂载 node 节点本机目录,注意挂载本机目录注意权限问题,可以先创建设置 777 权限,否则默认 kubelet 创建的目录权限为 755 默认其他用户没有写权限,执行流水线会报错。
    workspaceVolume hostPathWorkspaceVolume(hostPath: "/opt/workspace", readOnly: false)
    示例
    agent {
      kubernetes {
          cloud 'kubernetes'
          slaveConnectTimeout 1200
          workspaceVolume emptyDirWorkspaceVolume()
          yaml '''
    kind: Pod
    metadata:
      name: jenkins-agent
    spec:
      containers:
      - args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)']
        image: '192.168.10.15/kubernetes/jnlp:alpine'
        name: jnlp
        imagePullPolicy: IfNotPresent
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/alpine:latest"
        imagePullPolicy: "IfNotPresent"
        name: "date"
        tty: true
      restartPolicy: Never
    '''
      }
    }
    1.2 agent 的配置示例

    kubernetes 示例

    pipeline {
      agent {
        kubernetes {
          cloud 'kubernetes'
          slaveConnectTimeout 1200
          workspaceVolume emptyDirWorkspaceVolume()
          yaml '''
    kind: Pod
    metadata:
      name: jenkins-agent
    spec:
      containers:
      - args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)']
        image: '192.168.10.15/kubernetes/jnlp:alpine'
        name: jnlp
        imagePullPolicy: IfNotPresent
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/alpine:latest"
        imagePullPolicy: "IfNotPresent"
        name: "date"
        tty: true
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/kubectl:apline"
        imagePullPolicy: "IfNotPresent"
        name: "kubectl"
        tty: true
      restartPolicy: Never
    '''
        }
      }
      environment 
    {
        MY_KUBECONFIG = credentials('kubernetes-cluster')
      }
      stages {
        stage('Data') {
          steps {
            container(name: 'date') {
              sh """
                date
              "
    ""
            }
          }
        }
        stage('echo') {
          steps {
            container(name: 'date') {
              sh """
                echo 'k8s is pod'
              "
    ""
            }
          }
        }
        stage('kubectl') {
          steps {
            container(name: 'kubectl') {
              sh """
                kubectl get pod -A  --kubeconfig $MY_KUBECONFIG
              "
    ""
            }
          }
        }
      }
    }

    docker 的示例

    pipeline {
      agent none
      stages {
        stage('Example Build') {
          agent { docker 'maven:3-alpine' }
          steps {
            echo 'Hello, Maven'
            sh 'mvn --version'
          }
        }
        stage('Example Test') {
          agent { docker 'openjdk:8-jre' }
          steps {
            echo 'Hello, JDK'
            sh 'java -version'
          }
        }
      }
    }
    1.3 Post

    Post 一般用于流水线结束后的进一步处理,比如错误通知等。Post 可以针对流水线不同的结果做出不同的处理,就像开发程序的错误处理,比如 Python 语言的 try catch。

    Post 可以定义在 Pipeline 或 stage 中,目前支持以下条件

    • always:无论 Pipeline 或 stage 的完成状态如何,都允许运行该 post 中定义的指令;
    • changed:只有当前 Pipeline 或 stage 的完成状态与它之前的运行不同时,才允许在该 post 部分运行该步骤;
    • fixed:当本次 Pipeline 或 stage 成功,且上一次构建是失败或不稳定时,允许运行该 post 中定义的指令;
    • regression:当本次 Pipeline 或 stage 的状态为失败、不稳定或终止,且上一次构建的 状态为成功时,允许运行该 post 中定义的指令;
    • failure:只有当前 Pipeline 或 stage 的完成状态为失败(failure),才允许在 post 部分运行该步骤,通常这时在 Web 界面中显示为红色
    • success:当前状态为成功(success),执行 post 步骤,通常在 Web 界面中显示为蓝色 或绿色
    • unstable:当前状态为不稳定(unstable),执行 post 步骤,通常由于测试失败或代码 违规等造成,在 Web 界面中显示为黄色
    • aborted:当前状态为终止(aborted),执行该 post 步骤,通常由于流水线被手动终止触发,这时在 Web 界面中显示为灰色;
    • unsuccessful:当前状态不是 success 时,执行该 post 步骤;
    • cleanup:无论 pipeline 或 stage 的完成状态如何,都允许运行该 post 中定义的指令。和 always 的区别在于,cleanup 会在其它执行之后执行。
    示例

    一般情况下 post 部分放在流水线的底部,比如本实例,无论 stage 的完成状态如何,都会输出一条 I will always say Hello again!信息

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example1') {
          steps {
            echo 'Hello World1'
          }
        }
        stage('Example2') {
          steps {
            echo 'Hello World2'
          }
        }
      }
      post {
        always {
          echo 'I will always say Hello again!'
        }
      }
    }

    也可以将 post 写在 stage,下面示例表示 Example1 执行失败执行 post。

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example1') {
          steps {
            sh 'ip a'
          }
          post {
            failure {
              echo 'I will always say Hello again!'
            }
          }
        }
      }
    }
    1.4 sepes

    Steps 部分在给定的 stage 指令中执行的一个或多个步骤,比如在 steps 定义执行一条 shell 命令

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example') {
          steps {
            echo 'Hello World'
          }
        }
      }
    }

    或者是使用 sh 字段执行多条指令

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
        stage('Example') {
          steps {
            sh """
               echo 'Hello World1'
               echo 'Hello World2'
            "
    ""
          }
        }
      }
    }

    2、Directives


    Directives 可用于一些执行 stage 时的条件判断或预处理一些数据,和 Sections 一致,Directives 不是一个关键字或指令,而是包含了 environment、options、parameters、triggers、stage、tools、 input、when 等配置。

    2.1 Environment

    Environment 主要用于在流水线中配置的一些环境变量,根据配置的位置决定环境变量的作用域。可以定义在 pipeline 中作为全局变量,也可以配置在 stage 中作为该 stage 的环境变量。该指令支持一个特殊的方法 credentials(),该方法可用于在 Jenkins 环境中通过标识符访问预定义的凭证。对于类型为 Secret Text 的凭证,credentials()可以将该 Secret 中的文本内容赋值给环境变量。对于类型为标准的账号密码型的凭证,指定的环境变量为 username 和 password,并且也会定义两个额外的环境变量,分别为MYVARNAME_USR和MYVARNAME_PSW。

    基本变量使用

    //示例
    pipeline {
      agent any
      environment {   //全局变量,会在所有stage中生效
        NAME= 'zhangzhuo'
      }
      stages {
        stage('env1') {
          environment { //定义在stage中的变量只会在当前stage生效,其他的stage不会生效
            HARBOR = 'https://192.168.10.15'
          }
          steps {
            sh "env"
          }
        }
        stage('env2') {
          steps {
            sh "env"
          }
        }
      }
    }

    使用变量引用 secret 的凭证

    //这里使用k8s的kubeconfig文件示例
    pipeline {
      agent any
      environment {
        KUBECONFIG = credentials('kubernetes-cluster')
      }
      stages {
        stage('env') {
          steps {
            sh "env"  //默认情况下输出的变量内容会被加密
          }
        }
      }
    }
    使用变量引用类型为标准的账号密码型的凭证

    这里使用 HARBOR 变量进行演示,默认情况下账号密码型的凭证会自动创建 3 个变量

    • HARBOR_USR:会把凭证中 username 值赋值给这个变量
    • HARBOR_PSW:会把凭证中 password 值赋值给这个变量
    • HARBOR:默认情况下赋值的值为usernamme:password
    //这里使用k8s的kubeconfig文件示例
    pipeline {
      agent any
      environment {
        HARBOR = credentials('harbor-account')
      }
      stages {
        stage('env') {
          steps {
            sh "env"
          }
        }
      }
    }
    2.2 Options

    Jenkins 流水线支持很多内置指令,比如 retry 可以对失败的步骤进行重复执行 n 次,可以根据不同的指令实现不同的效果。比较常用的指令如下:

    • buildDiscarder :保留多少个流水线的构建记录
    • disableConcurrentBuilds:禁止流水线并行执行,防止并行流水线同时访问共享资源导致流水线失败。
    • disableResume :如果控制器重启,禁止流水线自动恢复。
    • newContainerPerStage:agent 为 docker 或 dockerfile 时,每个阶段将在同一个节点的新容器中运行,而不是所有的阶段都在同一个容器中运行。
    • quietPeriod:流水线静默期,也就是触发流水线后等待一会在执行。
    • retry:流水线失败后重试次数。
    • timeout:设置流水线的超时时间,超过流水线时间,job 会自动终止。如果不加 unit 参数默认为 1 分。
    • timestamps:为控制台输出时间戳。

    定义在 pipeline 中

    pipeline {
      agent any
      options {
        timeout(time: 1, unit: 'HOURS')  //超时时间1小时,如果不加unit参数默认为1分
        timestamps()                     //所有输出每行都会打印时间戳
        buildDiscarder(logRotator(numToKeepStr: '3')) //保留三个历史构建版本
        quietPeriod(10)  //注意手动触发的构建不生效
        retry(3)    //流水线失败后重试次数
      }
      stages {
        stage('env1') {
          steps {
            sh "env"
            sleep 2
          }
        }
        stage('env2') {
          steps {
            sh "env"
          }
        }
      }
    }

    定义在 stage 中

    Option 除了写在 Pipeline 顶层,还可以写在 stage 中,但是写在 stage 中的 option 仅支持 retry、 timeout、timestamps,或者是和 stage 相关的声明式选项,比如 skipDefaultCheckout。处于 stage 级别的 options 写法如下

    pipeline {
      agent any
      stages {
        stage('env1') {
          options {   //定义在这里这对这个stage生效
            timeout(time: 2, unit: 'SECONDS'//超时时间2秒
            timestamps()                     //所有输出每行都会打印时间戳
            retry(3)    //流水线失败后重试次数
          }
          steps {
            sh "env && sleep 2"
          }
        }
        stage('env2') {
          steps {
            sh "env"
          }
        }
      }
    }
    2.3 Parameters

    Parameters 提供了一个用户在触发流水线时应该提供的参数列表,这些用户指定参数的值可以通过 params 对象提供给流水线的 step(步骤)。只能定义在 pipeline 顶层。

    目前支持的参数类型如下
    • string:字符串类型的参数。
    • text:文本型参数,一般用于定义多行文本内容的变量。
    • booleanParam:布尔型参数。
    • choice:选择型参数,一般用于给定几个可选的值,然后选择其中一个进行赋值。
    • password:密码型变量,一般用于定义敏感型变量,在 Jenkins 控制台会输出为*。
    插件 Parameters
    • imageTag:镜像 tag,需要安装 Image Tag Parameter 插件后使用
    • gitParameter:获取 git 仓库分支,需要 Git Parameter 插件后使用
    示例
    pipeline {
      agent any
      parameters {
        string(name: 'DEPLOY_ENV', defaultValue:  'staging', description: '1')   //执行构建时需要手动配置字符串类型参数,之后赋值给变量
        text(name:  'DEPLOY_TEXT', defaultValue: 'OnenTwonThreen', description: '2')  //执行构建时需要提供文本参数,之后赋值给变量
        booleanParam(name: 'DEBUG_BUILD',  defaultValue: true, description: '3')   //布尔型参数
        choice(name: 'CHOICES', choices: ['one''two''three'], description: '4')  //选择形式列表参数
        password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A  secret password')  //密码类型参数,会进行加密
        imageTag(name: 'DOCKER_IMAGE', description: '', image: 'kubernetes/kubectl', filter: '.*', defaultTag: '', registry: 'https://192.168.10.15', credentialId: 'harbor-account', tagOrder: 'NATURAL')   //获取镜像名称与tag
        gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',  tagFilter: '*', type: 'PT_BRANCH')
      }  //获取git仓库分支列表,必须有git引用
      stages {
        stage('env1') {
          steps {
            sh "env"
          }
        }
        stage('git') {
          steps {
            git branch: "$BRANCH", credentialsId: 'gitlab-key', url: 'git@192.168.10.14:root/env.git'   //使用gitParameter,必须有这个
          }
        }
      }
    }
    2.4 Triggers

    在 Pipeline 中可以用 triggers 实现自动触发流水线执行任务,可以通过 Webhook、Cron、 pollSCM 和 upstream 等方式触发流水线。

    Cron

    定时构建假如某个流水线构建的时间比较长,或者某个流水线需要定期在某个时间段执行构建,可以 使用 cron 配置触发器,比如周一到周五每隔四个小时执行一次

    注意:H 的意思不是 HOURS 的意思,而是 Hash 的缩写。主要为了解决多个流水线在同一时间同时运行带来的系统负载压力。

    pipeline {
      agent any
      triggers {
        cron('H */4 * * 1-5')   //周一到周五每隔四个小时执行一次
        cron('H/12 * * * *')   //每隔12分钟执行一次
        cron('H * * * *')   //每隔1小时执行一次
      }
      stages {
        stage('Example') {
          steps {
            echo 'Hello World'
          }
        }
      }
    }
    Upstream

    Upstream 可以根据上游 job 的执行结果决定是否触发该流水线。比如当 job1 或 job2 执行成功时触发该流水线

    目前支持的状态有 SUCCESSUNSTABLEFAILURENOT_BUILTABORTED 等。

    pipeline {
      agent any
      triggers {
        upstream(upstreamProjects: 'env', threshold: hudson.model.Result.SUCCESS)  //当env构建成功时构建这个流水线
      }
      stages {
        stage('Example') {
          steps {
            echo 'Hello World'
          }
        }
      }
    }
    2.5 Input

    Input 字段可以实现在流水线中进行交互式操作,比如选择要部署的环境、是否继续执行某个阶段等。

    配置 Input 支持以下选项
    • message:必选,需要用户进行 input 的提示信息,比如:“是否发布到生产环境?”;
    • id:可选,input 的标识符,默认为 stage 的名称;
    • ok:可选,确认按钮的显示信息,比如:“确定”、“允许”;
    • submitter:可选,允许提交 input 操作的用户或组的名称,如果为空,任何登录用户均可提交 input;
    • parameters:提供一个参数列表供 input 使用。

    假如需要配置一个提示消息为“还继续么”、确认按钮为“继续”、提供一个 PERSON 的变量的参数,并且只能由登录用户为 alice 和 bob 提交的 input 流水线

    pipeline {
      agent any
      stages {
        stage('Example') {
          input {
            message "还继续么?"
            ok "继续"
            submitter "alice,bob"
            parameters {
              string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
            }
          }
          steps {
            echo "Hello, ${PERSON}, nice to meet you."
          }
        }
      }
    }
    2.6 when

    When 指令允许流水线根据给定的条件决定是否应该执行该 stage,when 指令必须包含至少 一个条件。如果 when 包含多个条件,所有的子条件必须都返回 True,stage 才能执行。

    When 也可以结合 not、allOf、anyOf 语法达到更灵活的条件匹配。

    目前比较常用的内置条件如下
    • branch:当正在构建的分支与给定的分支匹配时,执行这个 stage。注意,branch 只适用于多分支流水线
    • changelog:匹配提交的 changeLog 决定是否构建,例如:when { changelog '.*^\[DEPENDENCY\] .+$' }
    • environment:当指定的环境变量和给定的变量匹配时,执行这个 stage,例如:when { environment name: 'DEPLOY_TO', value: 'production' }
    • equals:当期望值和实际值相同时,执行这个 stage,例如:when { equals expected: 2, actual: currentBuild.number }
    • expression:当指定的 Groovy 表达式评估为 True,执行这个 stage,例如:when { expression { return params.DEBUG_BUILD } }
    • tag:如果 TAG_NAME 的值和给定的条件匹配,执行这个 stage,例如:when { tag "release-" }
    • not:当嵌套条件出现错误时,执行这个 stage,必须包含一个条件,例如:when { not { branch 'master' } }
    • allOf:当所有的嵌套条件都正确时,执行这个 stage,必须包含至少一个条件,例如:when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
    • anyOf:当至少有一个嵌套条件为 True 时,执行这个 stage,例如:when { anyOf { branch 'master'; branch 'staging' } }

    示例:当分支为 main 时执行 Example Deploy 步骤

    pipeline {
      agent any
      stages {
        stage('Example Build') {
          steps {
            echo 'Hello World'
          }
        }
        stage('Example Deploy') {
          when {
            branch 'main' //多分支流水线,分支为才会执行。
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    也可以同时配置多个条件,比如分支是 production,而且 DEPLOY_TO 变量的值为 main 时,才执行 Example Deploy

    pipeline {
      agent any
      environment {
        DEPLOY_TO = "main"
      }
      stages {
        stage('Example Deploy') {
          when {
            branch 'main'
            environment name: 'DEPLOY_TO', value: 'main'
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    也可以使用 anyOf 进行匹配其中一个条件即可,比如分支为 main 或 DEPLOY_TO 为 main 或 master 时执行 Deploy

    pipeline {
      agent any
      stages {
        stage('Example Deploy') {
          when {
            anyOf {
              branch 'main'
              environment name: 'DEPLOY_TO', value: 'main'
              environment name: 'DEPLOY_TO', value: 'master'
            }
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    也可以使用 expression 进行正则匹配,比如当 BRANCH_NAME 为 main 或 master,并且 DEPLOY_TO 为 master 或 main 时才会执行 Example Deploy

    pipeline {
      agent any
      stages {
        stage('Example Deploy') {
          when {
            expression { BRANCH_NAME ==~ /(main|master)/ }
            anyOf {
              environment name: 'DEPLOY_TO', value: 'main'
              environment name: 'DEPLOY_TO', value: 'master'
            }
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    默认情况下,如果定义了某个 stage 的 agent,在进入该 stage 的 agent 后,该 stage 的 when 条件才会被评估,但是可以通过一些选项更改此选项。比如在进入 stage 的 agent 前评估 when, 可以使用 beforeAgent,当 when 为 true 时才进行该 stage

    目前支持的前置条件如下
    • beforeAgent:如果 beforeAgent 为 true,则会先评估 when 条件。在 when 条件为 true 时,才会进入该 stage
    • beforeInput:如果 beforeInput 为 true,则会先评估 when 条件。在 when 条件为 true 时,才会进入到 input 阶段;
    • beforeOptions:如果 beforeInput 为 true,则会先评估 when 条件。在 when 条件为 true 时,才会进入到 options 阶段;
    • beforeOptions 优先级大于 beforeInput 大于 beforeAgent

    示例

    pipeline {
      agent none
      stages {
        stage('Example Build') {
          steps {
            echo 'Hello World'
          }
        }
        stage('Example Deploy') {
          when {
            beforeAgent true
            branch 'main'
          }
          steps {
            echo 'Deploying'
          }
        }
      }
    }

    3、Parallel


    在声明式流水线中可以使用 Parallel 字段,即可很方便的实现并发构建,比如对分支 A、B、 C 进行并行处理

    pipeline {
      agent any
      stages {
        stage('Non-Parallel Stage') {
          steps {
            echo 'This stage will be executed first.'
          }
        }
        stage('Parallel Stage') {
          failFast true         //表示其中只要有一个分支构建执行失败,就直接推出不等待其他分支构建
          parallel {
            stage('Branch A') {
              steps {
                echo "On Branch A"
              }
            }
            stage('Branch B') {
              steps {
                echo "On Branch B"
              }
            }
            stage('Branch C') {
              stages {
                stage('Nested 1') {
                  steps {
                    echo "In stage Nested 1 within Branch C"
                  }
                }
                stage('Nested 2') {
                  steps {
                   echo "In stage Nested 2 within Branch C"
                  }
                }
              }
            }
          }
        }
      }
    }


    Jenkinsfile 的使用


    上面讲过流水线支持两种语法,即声明式和脚本式,这两种语法都支持构建持续交付流水线。并且都可以用来在 Web UI 或 Jenkinsfile 中定义流水线,不过通常将 Jenkinsfile 放置于代码仓库中(当然也可以放在单独的代码仓库中进行管理)。

    创建一个 Jenkinsfile 并将其放置于代码仓库中,有以下好处

    • 方便对流水线上的代码进行复查/迭代
    • 对管道进行审计跟踪
    • 流水线真正的源代码能够被项目的多个成员查看和编辑

    1、环境变量


    1.1 静态变量

    Jenkins 有许多内置变量可以直接在 Jenkinsfile 中使用,可以通过 JENKINS_URL/pipeline/syntax/globals#env 获取完整列表。目前比较常用的环境变量如下

    • BUILD_ID:当前构建的 ID,与 Jenkins 版本 1.597+中的 BUILD_NUMBER 完全相同
    • BUILD_NUMBER:当前构建的 ID,和 BUILD_ID 一致
    • BUILD_TAG:用来标识构建的版本号,格式为:jenkins-{BUILD_NUMBER}, 可以对产物进行命名,比如生产的 jar 包名字、镜像的 TAG 等;
    • BUILD_URL:本次构建的完整 URL,比如:http://buildserver/jenkins/job/MyJobName/17/%EF%BC%9B
    • JOB_NAME:本次构建的项目名称
    • NODE_NAME:当前构建节点的名称;
    • JENKINS_URL:Jenkins 完整的 URL,需要在 SystemConfiguration 设置;
    • WORKSPACE:执行构建的工作目录。

    示例如果一个流水线名称为print_env,第 2 次构建,各个变量的值。

    BUILD_ID:2
    BUILD_NUMBER:2
    BUILD_TAG:jenkins-print_env-2
    BUILD_URL:http://192.168.10.16:8080/job/print_env/2/
    JOB_NAME:print_env
    NODE_NAME:built-in
    JENKINS_URL:http://192.168.10.16:8080/
    WORKSPACE:/bitnami/jenkins/home/workspace/print_env

    上述变量会保存在一个 Map 中,可以使用 env.BUILD_ID 或 env.JENKINS_URL 引用某个内置变量

    pipeline {
      agent any
      stages {
        stage('print env') {
          parallel {
            stage('BUILD_ID') {
              steps {
                echo "$env.BUILD_ID"
              }
            }
            stage('BUILD_NUMBER') {
              steps {
                echo "$env.BUILD_NUMBER"
              }
            }
            stage('BUILD_TAG') {
              steps {
                echo "$env.BUILD_TAG"
              }
            }
          }
        }
      }
    }
    1.2 动态变量

    动态变量是根据某个指令的结果进行动态赋值,变量的值根据指令的执行结果而不同。如下所示

    • returnStdout:将命令的执行结果赋值给变量,比如下述的命令返回的是 clang,此时 CC 的值为“clang”。
    • returnStatus:将命令的执行状态赋值给变量,比如下述命令的执行状态为 1,此时 EXIT_STATUS 的值为 1。
    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      environment {
        // 使用 returnStdout
        CC = """${sh(
             returnStdout: true,
             script: 'echo -n "
    clang"'   //如果使用shell命令的echo赋值变量最好加-n取消换行
             )}"
    ""
        // 使用 returnStatus
        EXIT_STATUS = """${sh(
             returnStatus: true,
             script: 'exit 1'
             )}"
    ""
      }
      stages {
        stage('Example') {
          environment {
            DEBUG_FLAGS = '-g'
          }
          steps {
            sh 'printenv'
          }
        }
      }
    }

    2、凭证管理


    Jenkins 的声明式流水线语法有一个 credentials()函数,它支持 secret text(加密文本)、username 和 password(用户名和密码)以及 secret file(加密文件)等。接下来看一下一些常用的凭证处理方法。

    2.1 加密文本

    本实例演示将两个 Secret 文本凭证分配给单独的环境变量来访问 Amazon Web 服务,需要 提前创建这两个文件的 credentials(实践的章节会有演示),Jenkinsfile 文件的内容如下

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      environment {
        AWS_ACCESS_KEY_ID = credentials('txt1')
        AWS_SECRET_ACCESS_KEY = credentials('txt2')
      }
      stages {
        stage('Example stage 1') {
          steps {
            echo "$AWS_ACCESS_KEY_ID"
          }
        }
        stage('Example stage 2') {
          steps {
            echo "$AWS_SECRET_ACCESS_KEY"
          }
        }
      }
    }
    2.2 用户名密码

    本示例用来演示 credentials 账号密码的使用,比如使用一个公用账户访问 Bitbucket、GitLab、 Harbor 等。假设已经配置完成了用户名密码形式的 credentials,凭证 ID 为 harbor-account

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      environment {
        BITBUCKET_COMMON_CREDS = credentials('harbor-account')
      }
      stages {
        stage('printenv') {
          steps {
            sh "env"
          }
        }
    }

    上述的配置会自动生成 3 个环境变量

    • BITBUCKET_COMMON_CREDS:包含一个以冒号分隔的用户名和密码,格式为 username:password
    • BITBUCKET_COMMON_CREDS_USR:仅包含用户名的附加变量
    • BITBUCKET_COMMON_CREDS_PSW:仅包含密码的附加变量。
    2.3 加密文件

    需要加密保存的文件,也可以使用 credential,比如链接到 Kubernetes 集群的 kubeconfig 文件等。

    假如已经配置好了一个 kubeconfig 文件,此时可以在 Pipeline 中引用该文件

    //Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent {
        kubernetes {
          cloud 'kubernetes'
          slaveConnectTimeout 1200
          workspaceVolume emptyDirWorkspaceVolume()
          yaml '''
    kind: Pod
    metadata:
      name: jenkins-agent
    spec:
      containers:
      - args: ['$(JENKINS_SECRET)', '$(JENKINS_NAME)']
        image: '192.168.10.15/kubernetes/jnlp:alpine'
        name: jnlp
        imagePullPolicy: IfNotPresent
      - command:
          - "cat"
        image: "192.168.10.15/kubernetes/kubectl:apline"
        imagePullPolicy: "IfNotPresent"
        name: "kubectl"
        tty: true
      restartPolicy: Never
    '''
        }
      }
      environment 
    {
        MY_KUBECONFIG = credentials('kubernetes-cluster')
      }
      stages {
        stage('kubectl') {
          steps {
            container(name: 'kubectl') {
              sh """
                kubectl get pod -A  --kubeconfig $MY_KUBECONFIG
              "
    ""
            }
          }
        }
      }
    }

  • 17 个方面,综合对比 Kafka、RabbitMQ、RocketMQ、ActiveMQ 四个分布式消息队列

    快乐分享,Java干货及时送达👇

    本文将从,Kafka、RabbitMQ、ZeroMQ、RocketMQ、ActiveMQ 17 个方面综合对比作为消息队列使用时的差异。

    1. 资料文档

    Kafka:中,有 kafka 作者自己写的书,网上资料也有一些。

    rabbitmq:多,有一些不错的书,网上资料多。

    zeromq:少,没有专门写 zeromq 的书,网上的资料多是一些代码的实现和简单介绍。

    rocketmq:少,没有专门写 rocketmq 的书,网上的资料良莠不齐,官方文档很简洁,但是对技术细节没有过多的描述。

    activemq:多,没有专门写 activemq 的书,网上资料多。

    2. 开发语言

    Kafka:Scala

    rabbitmq:Erlang

    zeromq:c

    rocketmq:java

    activemq:java

    3. 支持的协议

    Kafka:自己定义的一套…(基于 TCP)

    rabbitmq:AMQP

    zeromq:TCP、UDP

    rocketmq:自己定义的一套…

    activemq:OpenWire、STOMP、REST、XMPP、AMQP

    4. 消息存储

    Kafka:内存、磁盘、数据库。支持大量堆积。

    kafka 的最小存储单元是分区,一个 topic 包含多个分区,kafka 创建主题时,这些分区会被分配在多个服务器上,通常一个 broker 一台服务器。分区首领会均匀地分布在不同的服务器上,分区副本也会均匀的分布在不同的服务器上,确保负载均衡和高可用性,当新的 broker 加入集群的时候,部分副本会被移动到新的 broker 上。根据配置文件中的目录清单,kafka 会把新的分区分配给目录清单里分区数最少的目录。默认情况下,分区器使用轮询算法把消息均衡地分布在同一个主题的不同分区中,对于发送时指定了 key 的情况,会根据 key 的 hashcode 取模后的值存到对应的分区中。

    rabbitmq:内存、磁盘。支持少量堆积。

    rabbitmq 的消息分为持久化的消息和非持久化消息,不管是持久化的消息还是非持久化的消息都可以写入到磁盘。持久化的消息在到达队列时就写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,当内存吃紧的时候会从内存中清除。非持久化的消息一般只存在于内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存。

    引入镜像队列机制,可将重要队列“复制”到集群中的其他 broker 上,保证这些队列的消息不会丢失。配置镜像的队列,都包含一个主节点 master 和多个从节点 slave,如果 master 失效,加入时间最长的 slave 会被提升为新的 master,除发送消息外的所有动作都向 master 发送,然后由 master 将命令执行结果广播给各个 slave,rabbitmq 会让 master 均匀地分布在不同的服务器上,而同一个队列的 slave 也会均匀地分布在不同的服务器上,保证负载均衡和高可用性。

    zeromq:消息发送端的内存或者磁盘中。不支持持久化。

    rocketmq:磁盘。支持大量堆积。

    commitLog 文件存放实际的消息数据,每个 commitLog 上限是 1G,满了之后会自动新建一个 commitLog 文件保存数据。ConsumeQueue 队列只存放 offset、size、tagcode,非常小,分布在多个 broker 上。ConsumeQueue 相当于 CommitLog 的索引文件,消费者消费时会从 consumeQueue 中查找消息在 commitLog 中的 offset,再去 commitLog 中查找元数据。关注工众号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能调优手册!ConsumeQueue 存储格式的特性,保证了写过程的顺序写盘(写 CommitLog 文件),大量数据 IO 都在顺序写同一个 commitLog,满 1G 了再写新的。加上 rocketmq 是累计 4K 才强制从 PageCache 中刷到磁盘(缓存),所以高并发写性能突出。

    activemq:内存、磁盘、数据库。支持少量堆积。

    5. 消息事务

    Kafka:支持

    rabbitmq:支持。客户端将信道设置为事务模式,只有当消息被 rabbitMq 接收,事务才能提交成功,否则在捕获异常后进行回滚。使用事务会使得性能有所下降

    zeromq:不支持

    rocketmq:支持

    activemq:支持

    6. 负载均衡

    Kafka:支持负载均衡。

    1、一个 broker 通常就是一台服务器节点。对于同一个 Topic 的不同分区,Kafka 会尽力将这些分区分布到不同的 Broker 服务器上,zookeeper 保存了 broker、主题和分区的元数据信息。分区首领会处理来自客户端的生产请求,kafka 分区首领会被分配到不同的 broker 服务器上,让不同的 broker 服务器共同分担任务。

    每一个 broker 都缓存了元数据信息,客户端可以从任意一个 broker 获取元数据信息并缓存起来,根据元数据信息知道要往哪里发送请求。

    2、kafka 的消费者组订阅同一个 topic,会尽可能地使得每一个消费者分配到相同数量的分区,分摊负载。

    3、当消费者加入或者退出消费者组的时候,还会触发再均衡,为每一个消费者重新分配分区,分摊负载。

    kafka 的负载均衡大部分是自动完成的,分区的创建也是 kafka 完成的,隐藏了很多细节,避免了繁琐的配置和人为疏忽造成的负载问题。

    4、发送端由 topic 和 key 来决定消息发往哪个分区,如果 key 为 null,那么会使用轮询算法将消息均衡地发送到同一个 topic 的不同分区中。如果 key 不为 null,那么会根据 key 的 hashcode 取模计算出要发往的分区。

    rabbitmq:对负载均衡的支持不好。

    1、消息被投递到哪个队列是由交换器和 key 决定的,交换器、路由键、队列都需要手动创建。

    rabbitmq 客户端发送消息要和 broker 建立连接,需要事先知道 broker 上有哪些交换器,有哪些队列。通常要声明要发送的目标队列,如果没有目标队列,会在 broker 上创建一个队列,如果有,就什么都不处理,接着往这个队列发送消息。假设大部分繁重任务的队列都创建在同一个 broker 上,那么这个 broker 的负载就会过大。(可以在上线前预先创建队列,无需声明要发送的队列,但是发送时不会尝试创建队列,可能出现找不到队列的问题,rabbitmq 的备份交换器会把找不到队列的消息保存到一个专门的队列中,以便以后查询使用)

    使用镜像队列机制建立 rabbitmq 集群可以解决这个问题,形成 master-slave 的架构,master 节点会均匀分布在不同的服务器上,让每一台服务器分摊负载。slave 节点只是负责转发,在 master 失效时会选择加入时间最长的 slave 成为 master。

    当新节点加入镜像队列的时候,队列中的消息不会同步到新的 slave 中,除非调用同步命令,但是调用命令后,队列会阻塞,不能在生产环境中调用同步命令。

    2、当 rabbitmq 队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。

    这种方式非常适合扩展,而且是专门为并发程序设计的。

    如果某些消费者的任务比较繁重,那么可以设置 basicQos 限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq 不再向这个消费者发送任何消息。

    3、对于 rabbitmq 而言,客户端与集群建立的 TCP 连接不是与集群中所有的节点建立连接,而是挑选其中一个节点建立连接。但是 rabbitmq 集群可以借助 HAProxy、LVS 技术,或者在客户端使用算法实现负载均衡,引入负载均衡之后,各个客户端的连接可以分摊到集群的各个节点之中。

    客户端均衡算法

    1. 轮询法。按顺序返回下一个服务器的连接地址。
    2. 加权轮询法。给配置高、负载低的机器配置更高的权重,让其处理更多的请求;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载。
    3. 随机法。随机选取一个服务器的连接地址。
    4. 加权随机法。按照概率随机选取连接地址。
    5. 源地址哈希法。通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算。
    6. 最小连接数法。动态选择当前连接数最少的一台服务器的连接地址。

    zeromq:去中心化,不支持负载均衡。本身只是一个多线程网络库。

    rocketmq:支持负载均衡。

    一个 broker 通常是一个服务器节点,broker 分为 master 和 slave,master 和 slave 存储的数据一样,slave 从 master 同步数据。

    1、nameserver 与每个集群成员保持心跳,保存着 Topic-Broker 路由信息,同一个 topic 的队列会分布在不同的服务器上。

    2、发送消息通过轮询队列的方式发送,每个队列接收平均的消息量。发送消息指定 topic、tags、keys,无法指定投递到哪个队列(没有意义,集群消费和广播消费跟消息存放在哪个队列没有关系)。

    tags 选填,类似于 Gmail 为每封邮件设置的标签,方便服务器过滤使用。目前只支 持每个消息设置一个 tag,所以也可以类比为 Notify 的 MessageType 概念。

    keys 选填,代表这条消息的业务关键词,服务器会根据 keys 创建哈希索引,设置后, 可以在 Console 系统根据 Topic、Keys 来查询消息,由于是哈希索引,请尽可能 保证 key 唯一,例如订单号,商品 Id 等。

    3、rocketmq 的负载均衡策略规定:Consumer 数量应该小于等于 Queue 数量,如果 Consumer 超过 Queue 数量,那么多余的 Consumer 将不能消费消息。这一点和 kafka 是一致的,rocketmq 会尽可能地为每一个 Consumer 分配相同数量的队列,分摊负载。

    activemq:支持负载均衡。可以基于 zookeeper 实现负载均衡。

    7. 集群方式

    Kafka:天然的‘Leader-Slave’无状态集群,每台服务器既是 Master 也是 Slave。

    分区首领均匀地分布在不同的 kafka 服务器上,分区副本也均匀地分布在不同的 kafka 服务器上,所以每一台 kafka 服务器既含有分区首领,同时又含有分区副本,每一台 kafka 服务器是某一台 kafka 服务器的 Slave,同时也是某一台 kafka 服务器的 leader。

    kafka 的集群依赖于 zookeeper,zookeeper 支持热扩展,所有的 broker、消费者、分区都可以动态加入移除,而无需关闭服务,与不依靠 zookeeper 集群的 mq 相比,这是最大的优势。

    rabbitmq:支持简单集群,’复制’模式,对高级集群模式支持不好。

    rabbitmq 的每一个节点,不管是单一节点系统或者是集群中的一部分,要么是内存节点,要么是磁盘节点,集群中至少要有一个是磁盘节点。

    在 rabbitmq 集群中创建队列,集群只会在单个节点创建队列进程和完整的队列信息(元数据、状态、内容),而不是在所有节点上创建。引入镜像队列,可以避免单点故障,确保服务的可用性,但是需要人为地为某些重要的队列配置镜像。

    zeromq:去中心化,不支持集群。

    rocketmq:常用 多对’Master-Slave’ 模式,开源版本需手动切换 Slave 变成 Master

    Name Server 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。

    Broker 部署相对复杂,Broker 分为 Master 与 Slave,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同的 BrokerName,不同的 BrokerId 来定义,BrokerId 为 0 表示 Master,非 0 表示 Slave。Master 也可以部署多个。每个 Broker 与 Name Server 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 Name Server。

    Producer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从 Name Server 取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态,可集群部署。

    Consumer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从 Name Server 取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳。Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,订阅规则由 Broker 配置决定。

    客户端先找到 NameServer, 然后通过 NameServer 再找到 Broker。

    一个 topic 有多个队列,这些队列会均匀地分布在不同的 broker 服务器上。rocketmq 队列的概念和 kafka 的分区概念是基本一致的,kafka 同一个 topic 的分区尽可能地分布在不同的 broker 上,分区副本也会分布在不同的 broker 上。

    rocketmq 集群的 slave 会从 master 拉取数据备份,master 分布在不同的 broker 上。

    activemq:支持简单集群模式,比如’主-备’,对高级集群模式支持不好。

    8. 管理界面

    Kafka:一般

    rabbitmq:好

    zeromq:无

    rocketmq:无

    activemq:一般

    9. 可用性

    Kafka:非常高(分布式)

    rabbitmq:高(主从)

    zeromq:高

    rocketmq:非常高(分布式)

    activemq:高(主从)

    10. 消息重复

    Kafka:支持 at least once、at most once

    rabbitmq:支持 at least once、at most once

    zeromq:只有重传机制,但是没有持久化,消息丢了重传也没有用。既不是 at least once、也不是 at most once、更不是 exactly only once

    rocketmq:支持 at least once

    activemq:支持 at least once

    11. 吞吐量 TPS

    Kafka:极大 Kafka 按批次发送消息和消费消息。发送端将多个小消息合并,批量发向 Broker,消费端每次取出一个批次的消息批量处理。

    rabbitmq:比较大

    zeromq:极大

    rocketmq:大

    rocketMQ :接收端可以批量消费消息,可以配置每次消费的消息数,但是发送端不是批量发送。

    activemq:比较大

    12. 订阅形式和消息分发

    Kafka:基于 topic 以及按照 topic 进行正则匹配的发布订阅模式。

    【发送】

    发送端由 topic 和 key 来决定消息发往哪个分区,如果 key 为 null,那么会使用轮询算法将消息均衡地发送到同一个 topic 的不同分区中。如果 key 不为 null,那么会根据 key 的 hashcode 取模计算出要发往的分区。

    【接收】

    1、consumer 向群组协调器 broker 发送心跳来维持他们和群组的从属关系以及他们对分区的所有权关系,所有权关系一旦被分配就不会改变除非发生再均衡(比如有一个 consumer 加入或者离开 consumer group),consumer 只会从对应的分区读取消息。

    2、kafka 限制 consumer 个数要少于分区个数,每个消息只会被同一个 Consumer Group 的一个 consumer 消费(非广播)。

    3、kafka 的 Consumer Group 订阅同一个 topic,会尽可能地使得每一个 consumer 分配到相同数量的分区,不同 Consumer Group 订阅同一个主题相互独立,同一个消息会被不同的 Consumer Group 处理。

    rabbitmq:提供了 4 种:direct, topic ,Headers 和 fanout。

    【发送】

    先要声明一个队列,这个队列会被创建或者已经被创建,队列是基本存储单元。

    由 exchange 和 key 决定消息存储在哪个队列。

    direct:发送到和 bindingKey 完全匹配的队列。

    topic:路由 key 是含有”.”的字符串,会发送到含有“*”、“#”进行模糊匹配的 bingKey 对应的队列。

    fanout:与 key 无关,会发送到所有和 exchange 绑定的队列

    headers:与 key 无关,消息内容的 headers 属性(一个键值对)和绑定键值对完全匹配时,会发送到此队列。此方式性能低一般不用

    【接收】

    rabbitmq 的队列是基本存储单元,不再被分区或者分片,对于我们已经创建了的队列,消费端要指定从哪一个队列接收消息。

    当 rabbitmq 队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。

    这种方式非常适合扩展,而且是专门为并发程序设计的。

    如果某些消费者的任务比较繁重,那么可以设置 basicQos 限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq 不再向这个消费者发送任何消息。

    zeromq:点对点(p2p)

    rocketmq:基于 topic/messageTag 以及按照消息类型、属性进行正则匹配的发布订阅模式

    【发送】

    发送消息通过轮询队列的方式发送,每个队列接收平均的消息量。发送消息指定 topic、tags、keys,无法指定投递到哪个队列(没有意义,集群消费和广播消费跟消息存放在哪个队列没有关系)。

    tags 选填,类似于 Gmail 为每封邮件设置的标签,方便服务器过滤使用。目前只支 持每个消息设置一个 tag,所以也可以类比为 Notify 的 MessageType 概念。

    keys 选填,代表这条消息的业务关键词,服务器会根据 keys 创建哈希索引,设置后, 可以在 Console 系统根据 Topic、Keys 来查询消息,由于是哈希索引,请尽可能 保证 key 唯一,例如订单号,商品 Id 等。

    【接收】

    1、广播消费。一条消息被多个 Consumer 消费,即使 Consumer 属于同一个 ConsumerGroup,消息也会被 ConsumerGroup 中的每个 Consumer 都消费一次。

    2、集群消费。一个 Consumer Group 中的 Consumer 实例平均分摊消费消息。例如某个 Topic 有 9 条消息,其中一个 Consumer Group 有 3 个实例,那么每个实例只消费其中的 3 条消息。即每一个队列都把消息轮流分发给每个 consumer。

    activemq:点对点(p2p)、广播(发布-订阅)

    点对点模式,每个消息只有 1 个消费者;

    发布/订阅模式,每个消息可以有多个消费者。

    【发送】

    点对点模式:先要指定一个队列,这个队列会被创建或者已经被创建。

    发布/订阅模式:先要指定一个 topic,这个 topic 会被创建或者已经被创建。

    【接收】

    点对点模式:对于已经创建了的队列,消费端要指定从哪一个队列接收消息。

    发布/订阅模式:对于已经创建了的 topic,消费端要指定订阅哪一个 topic 的消息。

    13. 顺序消息

    Kafka:支持。

    设置生产者的 max.in.flight.requests.per.connection 为 1,可以保证消息是按照发送顺序写入服务器的,即使发生了重试。kafka 保证同一个分区里的消息是有序的,但是这种有序分两种情况

    1、key 为 null,消息逐个被写入不同主机的分区中,但是对于每个分区依然是有序的

    2、key 不为 null , 消息被写入到同一个分区,这个分区的消息都是有序。

    rabbitmq:不支持

    zeromq:不支持

    rocketmq:支持

    activemq:不支持

    14. 消息确认

    Kafka:支持。

    1、发送方确认机制

    ack=0,不管消息是否成功写入分区

    ack=1,消息成功写入首领分区后,返回成功

    ack=all,消息成功写入所有分区后,返回成功。

    2、接收方确认机制

    自动或者手动提交分区偏移量,早期版本的 kafka 偏移量是提交给 Zookeeper 的,这样使得 zookeeper 的压力比较大,更新版本的 kafka 的偏移量是提交给 kafka 服务器的,不再依赖于 zookeeper 群组,集群的性能更加稳定。

    rabbitmq:支持。

    1、发送方确认机制,消息被投递到所有匹配的队列后,返回成功。如果消息和队列是可持久化的,那么在写入磁盘后,返回成功。支持批量确认和异步确认。

    2、接收方确认机制,设置 autoAck 为 false,需要显式确认,设置 autoAck 为 true,自动确认。

    当 autoAck 为 false 的时候,rabbitmq 队列会分成两部分,一部分是等待投递给 consumer 的消息,一部分是已经投递但是没收到确认的消息。如果一直没有收到确认信号,并且 consumer 已经断开连接,rabbitmq 会安排这个消息重新进入队列,投递给原来的消费者或者下一个消费者。

    未确认的消息不会有过期时间,如果一直没有确认,并且没有断开连接,rabbitmq 会一直等待,rabbitmq 允许一条消息处理的时间可以很久很久。

    zeromq:支持。

    rocketmq:支持。

    activemq:支持。

    15. 消息回溯

    Kafka:支持指定分区 offset 位置的回溯

    rabbitmq:不支持

    zeromq:不支持

    rocketmq:支持指定时间点的回溯

    activemq:不支持

    16. 消息重试

    Kafka:不支持,但是可以实现。

    kafka 支持指定分区 offset 位置的回溯,可以实现消息重试。

    rabbitmq:不支持,但是可以利用消息确认机制实现

    rabbitmq 接收方确认机制,设置 autoAck 为 false

    当 autoAck 为 false 的时候,rabbitmq 队列会分成两部分,一部分是等待投递给 consumer 的消息,一部分是已经投递但是没收到确认的消息。如果一直没有收到确认信号,并且 consumer 已经断开连接,rabbitmq 会安排这个消息重新进入队列,投递给原来的消费者或者下一个消费者。

    zeromq:不支持

    rocketmq:支持

    消息消费失败的大部分场景下,立即重试 99%都会失败,所以 rocketmq 的策略是在消费失败时定时重试,每次时间间隔相同。

    1、发送端的 send 方法本身支持内部重试,重试逻辑如下:

    a)至多重试 3 次;

    b)如果发送失败,则轮转到下一个 broker;

    c)这个方法的总耗时不超过 sendMsgTimeout 设置的值,默认 10s,超过时间不在重试。

    2、接收端。

    Consumer 消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer 消费消息失败通常可以分为以下两种情况:

    由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。定时重试机制,比如过 10s 秒后再重试。

    由于依赖的下游应用服务不可用,例如 db 连接不可用,外系统网络不可达等。

    即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况可以 sleep 30s,再消费下一条消息,减轻 Broker 重试消息的压力。

    activemq:不支持

    17. 并发度

    Kafka:高

    一个线程一个消费者,kafka 限制消费者的个数要小于等于分区数,如果要提高并行度,可以在消费者中再开启多线程,或者增加 consumer 实例数量。

    rabbitmq:极高

    本身是用 Erlang 语言写的,并发性能高。

    可在消费者中开启多线程,最常用的做法是一个 channel 对应一个消费者,每一个线程把持一个 channel,多个线程复用 connection 的 tcp 连接,减少性能开销。

    当 rabbitmq 队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。

    这种方式非常适合扩展,而且是专门为并发程序设计的。

    如果某些消费者的任务比较繁重,那么可以设置 basicQos 限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq 不再向这个消费者发送任何消息。

    zeromq:高

    rocketmq:高

    1、rocketmq 限制消费者的个数少于等于队列数,但是可以在消费者中再开启多线程,这一点和 kafka 是一致的,提高并行度的方法相同。

    修改消费并行度方法

    a) 同一个 ConsumerGroup 下,通过增加 Consumer 实例数量来提高并行度,超过订阅队列数的 Consumer 实例无效。

    b) 提高单个 Consumer 的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax

    2、同一个网络连接 connection,客户端多个线程可以同时发送请求,连接会被复用,减少性能开销。

    activemq:高

    单个 ActiveMQ 的接收和消费消息的速度在 1 万笔/秒(持久化 一般为 1-2 万, 非持久化 2 万以上),在生产环境中部署 10 个 Activemq 就能达到 10 万笔/秒以上的性能,部署越多的 activemq broker 在 MQ 上 latency 也就越低,系统吞吐量也就越高。

  • ES 不香吗,为啥还要 ClickHouse?

    快乐分享,Java干货及时送达👇

    文章来https://zhuanlan.zhihu.com/p/353296392

    目录
    • 架构和设计的对比
    • 查询对比实战
    • 总结

    前言


    Elasticsearch 是一个实时的分布式搜索分析引擎,它的底层是构建在Lucene之上的。简单来说是通过扩展Lucene的搜索能力,使其具有分布式的功能。ES通常会和其它两个开源组件logstash(日志采集)和Kibana(仪表盘)一起提供端到端的日志/搜索分析的功能,常常被简称为ELK。

    Clickhouse是俄罗斯搜索巨头Yandex开发的面向列式存储的关系型数据库。ClickHouse是过去两年中OLAP领域中最热门的,并于2016年开源。

    ES是最为流行的大数据日志和搜索解决方案,但是近几年来,它的江湖地位受到了一些挑战,许多公司已经开始把自己的日志解决方案从ES迁移到了Clickhouse,这里就包括:携程,快手等公司。


    架构和设计的对比


    ES的底层是Lucenc,主要是要解决搜索的问题。搜索是大数据领域要解决的一个常见的问题,就是在海量的数据量要如何按照条件找到需要的数据。搜索的核心技术是倒排索引和布隆过滤器。ES通过分布式技术,利用分片与副本机制,直接解决了集群下搜索性能与高可用的问题。

    ElasticSearch是为分布式设计的,有很好的扩展性,在一个典型的分布式配置中,每一个节点(node)可以配制成不同的角色,如下图所示:

    • Client Node,负责API和数据的访问的节点,不存储/处理数据
    • Data Node,负责数据的存储和索引
    • Master Node, 管理节点,负责Cluster中的节点的协调,不存储数据。

    ClickHouse是基于MPP架构的分布式ROLAP(关系OLAP)分析引擎。每个节点都有同等的责任,并负责部分数据处理(不共享任何内容)。ClickHouse 是一个真正的列式数据库管理系统(DBMS)。在 ClickHouse 中,数据始终是按列存储的,包括矢量(向量或列块)执行的过程。让查询变得更快,最简单且有效的方法是减少数据扫描范围和数据传输时的大小,而列式存储和数据压缩就可以帮助实现上述两点。Clickhouse同时使用了日志合并树,稀疏索引和CPU功能(如SIMD单指令多数据)充分发挥了硬件优势,可实现高效的计算。Clickhouse 使用Zookeeper进行分布式节点之间的协调。

    为了支持搜索,Clickhouse同样支持布隆过滤器。


    查询对比实战


    为了对比ES和Clickhouse的基本查询能力的差异,我写了一些代码(https://github.com/gangtao/esvsch)来验证。

    这个测试的架构如下:

    架构主要有四个部分组成:

    • ES stack ES stack有一个单节点的Elastic的容器和一个Kibana容器组成,Elastic是被测目标之一,Kibana作为验证和辅助工具。部署代码如下:
    version: '3.7'

    services:
      elasticsearch:
        image: docker.elastic.co/elasticsearch/elasticsearch:7.4.0
        container_name: elasticsearch
        environment:
          - xpack.security.enabled=false
          - discovery.type=single-node
        ulimits:
          memlock:
            soft: -1
            hard: -1
          nofile:
            soft: 65536
            hard: 65536
        cap_add:
          - IPC_LOCK
        volumes:
          - elasticsearch-data:/usr/share/elasticsearch/data
        ports:
          - 9200:9200
          - 9300:9300
        deploy:
          resources:
            limits:
              cpus: '4'
              memory: 4096M
            reservations:
              memory: 4096M

      kibana:
        container_name: kibana
        image: docker.elastic.co/kibana/kibana:7.4.0
        environment:
          - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
        ports:
          - 5601:5601
        depends_on:
          - elasticsearch

    volumes:
      elasticsearch-data:
        driver: local
    • Clickhouse stack Clickhouse stack有一个单节点的Clickhouse服务容器和一个TabixUI作为Clickhouse的客户端。部署代码如下:
    version: "3.7"
    services:
      clickhouse:
        container_name: clickhouse
        image: yandex/clickhouse-server
        volumes:
          - ./data/config:/var/lib/clickhouse
        ports:
          - "8123:8123"
          - "9000:9000"
          - "9009:9009"
          - "9004:9004"
        ulimits:
          nproc: 65535
          nofile:
            soft: 262144
            hard: 262144
        healthcheck:
          test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
          interval: 30s
          timeout: 5s
          retries: 3
        deploy:
          resources:
            limits:
              cpus: '4'
              memory: 4096M
            reservations:
              memory: 4096M

      tabixui:
        container_name: tabixui
        image: spoonest/clickhouse-tabix-web-client
        environment:
          - CH_NAME=dev
          - CH_HOST=127.0.0.1:8123
          - CH_LOGIN=default
        ports:
          - "18080:80"
        depends_on:
          - clickhouse
        deploy:
          resources:
            limits:
              cpus: '0.1'
              memory: 128M
            reservations:
              memory: 128M
    • 数据导入 stack 数据导入部分使用了Vector.dev开发的vector,该工具和fluentd类似,都可以实现数据管道式的灵活的数据导入。
    • 测试控制 stack 测试控制我使用了Jupyter,使用了ES和Clickhouse的Python SDK来进行查询的测试。

    用Docker compose启动ES和Clickhouse的stack后,我们需要导入数据,我们利用Vector的generator功能,生成syslog,并同时导入ES和Clickhouse,在这之前,我们需要在Clickhouse上创建表。ES的索引没有固定模式,所以不需要事先创建索引。

    创建表的代码如下:

    CREATE TABLE default.syslog(
        application String,
        hostname String,
        message String,
        mid String,
        pid String,
        priority Int16,
        raw String,
        timestamp DateTime('UTC'),
        version Int16
    ENGINE = MergeTree()
        PARTITION BY toYYYYMMDD(timestamp)
        ORDER BY timestamp
        TTL timestamp + toIntervalMonth(1);

    创建好表之后,我们就可以启动vector,向两个stack写入数据了。vector的数据流水线的定义如下:

    [sources.in]
    type = "generator"
    format = "syslog"
    interval = 0.01
    count = 100000

    [transforms.clone_message]
    type = "add_fields"
    inputs = ["in"]
    fields.raw = "{{ message }}"

    [transforms.parser]
    # General
    type = "regex_parser"
    inputs = ["clone_message"]
    field = "message" # optional, default
    patterns = ['^d*)>(?Pd) (?Pd{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z) (?Pw+.w+) (?Pw+) (?Pd+) (?PIDd+) - (?P.*)$']

    [transforms.coercer]
    type = "coercer"
    inputs = ["parser"]
    types.timestamp = "timestamp"
    types.version = "int"
    types.priority = "int"

    [sinks.out_console]
    # General
    type = "console"
    inputs = ["coercer"]
    target = "stdout"

    # Encoding
    encoding.codec = "json"


    [sinks.out_clickhouse]
    host = "http://host.docker.internal:8123"
    inputs = ["coercer"]
    table = "syslog"
    type = "clickhouse"

    encoding.only_fields = ["application", "hostname", "message", "mid", "pid", "priority", "raw", "timestamp", "version"]
    encoding.timestamp_format = "unix"

    [sinks.out_es]
    # General
    type = "elasticsearch"
    inputs = ["coercer"]
    compression = "none"
    endpoint = "http://host.docker.internal:9200"
    index = "syslog-%F"

    # Encoding

    # Healthcheck
    healthcheck.enabled = true

    这里简单介绍一下这个流水线:

    • http://source.in 生成syslog的模拟数据,生成10w条,生成间隔和0.01秒
    • transforms.clone_message 把原始消息复制一份,这样抽取的信息同时可以保留原始消息
    • transforms.parser 使用正则表达式,按照syslog的定义,抽取出application,hostname,message ,mid ,pid ,priority ,timestamp ,version 这几个字段
    • transforms.coercer 数据类型转化
    • sinks.out_console 把生成的数据打印到控制台,供开发调试
    • sinks.out_clickhouse 把生成的数据发送到Clickhouse
    • sinks.out_es 把生成的数据发送到ES

    运行Docker命令,执行该流水线:

    docker run 
      -v $(mkfile_path)/vector.toml:/etc/vector/vector.toml:ro 
      -p 18383:8383 
      timberio/vector:nightly-alpine

    数据导入后,我们针对一下的查询来做一个对比。ES使用自己的查询语言来进行查询,Clickhouse支持SQL,我简单测试了一些常见的查询,并对它们的功能和性能做一些比较。

    • 返回所有的记录
    # ES
    {
      "query":{
        "match_all":{}
      }
    }

    # Clickhouse
    "SELECT * FROM syslog"
    • 匹配单个字段
    # ES
    {
      "query":{
        "match":{
          "hostname":"for.org"
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE hostname='for.org'"
    • 匹配多个字段
    # ES
    {
      "query":{
        "multi_match":{
          "query":"up.com ahmadajmi",
            "fields":[
              "hostname",
              "application"
            ]
        }
      }
    }

    # Clickhouse、
    "SELECT * FROM syslog WHERE hostname='for.org' OR application='ahmadajmi'"
    • 单词查找,查找包含特定单词的字段
    # ES
    {
      "query":{
        "term":{
          "message":"pretty"
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE lowerUTF8(raw) LIKE '%pretty%'"
    • 范围查询, 查找版本大于2的记录
    # ES
    {
      "query":{
        "range":{
          "version":{
            "gte":2
          }
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE version >= 2"
    • 查找到存在某字段的记录
      ES是文档类型的数据库,每一个文档的模式不固定,所以会存在某字段不存在的情况;而Clickhouse对应为字段为空值
    # ES
    {
      "query":{
        "exists":{
          "field":"application"
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE application is not NULL"
    • 正则表达式查询,查询匹配某个正则表达式的数据
    # ES
    {
      "query":{
        "regexp":{
          "hostname":{
            "value":"up.*",
              "flags":"ALL",
                "max_determinized_states":10000,
                  "rewrite":"constant_score"
          }
        }
      }
    }

    # Clickhouse
    "SELECT * FROM syslog WHERE match(hostname, 'up.*')"
    • 聚合计数,统计某个字段出现的次数
    # ES
    {
      "aggs":{
        "version_count":{
          "value_count":{
            "field":"version"
          }
        }
      }
    }

    # Clickhouse
    "SELECT count(version) FROM syslog"
    • 聚合不重复的值,查找所有不重复的字段的个数
    # ES
    {
      "aggs":{
        "my-agg-name":{
          "cardinality":{
            "field":"priority"
          }
        }
      }
    }

    # Clickhouse
    "SELECT count(distinct(priority)) FROM syslog "

    我用Python的SDK,对上述的查询在两个Stack上各跑10次,然后统计查询的性能结果。

    我们画出出所有的查询的响应时间的分布:

    总查询时间的对比如下:

    通过测试数据我们可以看出Clickhouse在大部分的查询的性能上都明显要优于Elastic。在正则查询(Regex query)和单词查询(Term query)等搜索常见的场景下,也并不逊色。

    在聚合场景下,Clickhouse表现异常优秀,充分发挥了列村引擎的优势。

    注意,我的测试并没有任何优化,对于Clickhouse也没有打开布隆过滤器。可见Clickhouse确实是一款非常优秀的数据库,可以用于某些搜索的场景。当然ES还支持非常丰富的查询功能,这里只有一些非常基本的查询,有些查询可能存在无法用SQL表达的情况。


    总结


    本文通过对于一些基本查询的测试,对比了Clickhouse 和Elasticsearch的功能和性能,测试结果表明,Clickhouse在这些基本场景表现非常优秀,性能优于ES,这也解释了为什么用很多的公司应从ES切换到Clickhouse之上。

  • 为了减少延迟和卡顿,我对 MySQL 查询做了这些优化处理….

    前言

    在程序上线运行一段时间后,一旦数据量上去了,或多或少会感觉到系统出现延迟、卡顿等现象,出现这种问题,就需要程序员或架构师进行系统调优工作了。

    其中,大量的实践经验表明,调优的手段尽管有很多,但涉及到SQL调优的内容仍然是非常重要的一环,本文将结合实例,总结一些工作中可能涉及到的SQL优化策略;

    查询优化

    可以说,对于大多数系统来说,读多写少一定是常态,这就表示涉及到查询的SQL是非常高频的操作;

    前置准备,给一张测试表添加10万条数据

    使用下面的存储过程给单表造一批数据,将表换成自己的就好了

    create procedure addMyData()
     
     begin
     
      declare num int;
      set num =1;
      
      while num do
      
       insert into XXX_table values(
        replace(uuid(),'-',''),concat('测试',num),concat('cs',num),'123456'
       );
     
       set num =num +1;
      end while;
     
     end ;

    然后调用该存储过程

    call addMyData();

    本篇准备了3张表,分别为学生(student)表,班级(class)表,账户(account)表,各自有50万,1万和10万条数据用于测试;

    1、分页查询优化

    分页查询是开发中经常会遇到的,有一种情况是,当分页的数量非常大的时候,查询的时候往往非常耗时,比如查询student表,使用下面的sql查询,耗时达到0.2秒;

    实践经验告诉我们,越往后,分页查询效率越低,这就是分页查询的问题所在, 因为,当在进行分页查询时,如果执行 limit 400000,10 ,此时需要 MySQL 排序前4000 10 记 录,仅仅返回400000 - 4 00010 的记录,其他记录丢弃,查询排序的代价非常大

    优化思路:

    一般分页查询时,通过创建 覆盖索引 能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化;

    1) 在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容

    SELECT * FROM student t1,(SELECT id FROM student ORDER BY id LIMIT 400000,10) t2 WHERE t1.id =t2.id;

    执行上面的sql,可以看到响应时间有一定的提升;

    2)对于主键自增的表,可以把Limit 查询转换成某个位置的查询

    select * from student where id > 400000 limit 10;

    执行上面的sql,可以看到响应时间有一定的提升;

    2、关联查询优化

    在实际的业务开发过程中,关联查询可以说随处可见,关联查询的优化核心思路是,最好为关联查询的字段添加索引,这是关键,具体到不同的场景,还需要具体分析,这个跟mysql的引擎在执行优化策略的方案选择时有一定关系;

    2.1 左连接或右连接

    下面是一个使用left join 的查询,可以预想到这条sql查询的结果集非常大

    select t.* from student t left join class cs on t.classId = cs.id;

    为了检查下sql的执行效率,使用explain做一下分析,可以看到,第一张表即left join左边的表student走了全表扫描,而class表走了主键索引,尽管结果集较大,还是走了索引;

    针对这种场景的查询,思路如下:

    • 让查询的字段尽量包含在主键索引或者覆盖索引中;
    • 查询的时候尽量使用分页查询;

    关于左连接(右连接)的explain结果补充说明

    • 左连接左边的表一般为驱动表,右边的表为被驱动表;
    • 尽可能让数据集小的表作为驱动表,减少mysql内部循环的次数;
    • 两表关联时,explain结果展示中,第一栏一般为驱动表;
    2.2 关联查询关联的字段建立索引

    看下面的这条sql,其关联字段非表的主键,而是普通的字段;

    explain select u.* from tenant t left join `user` u on u.account = t.tenant_name where t.removed is null and u.removed is null;

    通过explain分析可以发现,左边的表走了全表扫描,可以考虑给左边的表的tenant_nameuser表的“ 各自创建索引;

    create index idx_name on tenant(tenant_name);

    create index idx_account on `user`(account);

    再次使用explain分析结果如下

    可以看到第二行type变为ref,rows的数量优化比较明显。这是由左连接特性决定的,LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引 。

    2.3 内连接关联的字段建立索引

    我们知道,左连接和右连接查询的数据分别是完全包含左表数据,完全包含右表数据,而内连接(inner join 或join) 则是取交集(共有的部分),在这种情况下,驱动表的选择是由mysql优化器自动选择的;

    在上面的基础上,首先移除两张表的索引

    ALTER TABLE `user` DROP INDEX idx_account;
    ALTER TABLE `tenant` DROP INDEX idx_name

    使用explain语句进行分析

    然后给user表的account字段添加索引,再次执行explain我们发现,user表竟然被当作是被驱动表了;

    此时,如果我们给tenant表的tenant_name加索引,并移除user表的account索引,得出的结果竟然都没有走索引,再次说明,使用内连接的情况下,查询优化器将会根据自己的判断进行选择;

    3、子查询优化

    子查询在日常编写业务的SQL时也是使用非常频繁的做法,不是说子查询不能用,而是当数据量超出一定的范围之后,子查询的性能下降是很明显的,关于这一点,本人在日常工作中深有体会;

    比如下面这条sql,由于student表数据量较大,执行起来耗时非常长,可以看到耗费了将近3秒;

    select st.* from student st where st.classId in (
     
     select id from class where id > 100
     
    );

    通过执行explain进行分析得知,内层查询 id > 100的子查询尽管用上了主键索引,但是由于结果集太大,带入到外层查询,即作为in的条件时,查询优化器还是走了全表扫描;

    针对上面的情况,可以考虑下面的优化方式

    select st.id from student st join class cl on st.classId = cl.id where cl.id > 100;

    子查询性能低效的原因

    • 子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表 ,然后外层查询语句从临时表中查询记录,查询完毕后,再撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询;
    • 子查询结果集存储的临时表,不论是内存临时表还是磁盘临时表都不能走索引 ,所以查询性能会受到一定的影响;
    • 对于返回结果集比较大的子查询,其对查询性能的影响也就越大;

    使用mysql查询时,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表 ,其速度比子查询要快 ,如果查询中使用索引的话,性能就会更好,尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代;

    一个真实的案例

    在下面的这段sql中,优化前使用的是子查询,在一次生产问题的性能分析中,发现某个tenant_id下的数据达到了35万多,这样直接导致某个列表页面的接口查询耗时达到了5秒左右;

    找到了问题的根源后,尝试使用上面的优化思路进行解决即可,优化后的sql大概如下,

    4、排序(order by)优化

    在mysql,排序主要有两种方式

    • Using filesort : 通过表索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sort。buffer中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序;
    • Using index : 通过有序的索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高;

    对于以上两种排序方式,Using index的性能高,而Using filesort的性能低,我们在优化排序操作时,尽量要优化为 Using index

    4.1 使用age字段进行排序

    由于age字段未加索引,查询结果按照age排序的时候发现使用了filesort,排序性能较低;

    给age字段添加索引,再次使用order by时就走了索引;

    4.2 使用多字段进行排序

    通常在实际业务中,参与排序的字段往往不只一个,这时候,就可以对参与排序的多个字段创建联合索引;

    如下根据stuno和age排序

    给stuno和age添加联合索引

    create index idx_stuno_age on `student`(stuno,age);

    再次分析时结果如下,此时排序走了索引

    关于多字段排序时的注意事项

    1)排序时,需要满足最左前缀法则,否则也会出现 filesort;

    在上面我们创建的联合索引顺序是stuno和age,即stuno在前面,而age在后,如果查询的时候调换排序顺序会怎样呢?通过分析结果发现,走了filesort;

    2)排序时,排序的类型保持一致

    在保持字段排序顺序不变时,默认情况下,如果都按照升序或者降序时,order by可以使用index,如果一个是升序,另一个是降序会如何呢?分析发现,这种情况下也会走filesort;

    5、分组(group by)优化

    group by 的优化策略和order by 的优化策略非常像,主要列举如下几个要点:

    • group by 即使没有过滤条件用到索引,也可以直接使用索引;
    • group by 先排序再分组,遵照索引建的最佳左前缀法则;
    • 当无法使用索引列时,增大 max_length_for_sort_datasort_buffer_size 参数的设置;
    • where效率高于having,能写在where限定的条件就不要写在having中了;
    • 减少使用order by,能不排序就不排序,或将排序放到程序去做。Order bygroupbydistinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的;
    • 如果sql包含了order bygroup bydistinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢;
    5.1 给group by的字段添加索引

    如果字段未加索引,分析结果如下,这种结果性能显然很低效

    给stuno添加索引之后

    给stuno和age添加联合索引

    如果不遵循最佳左前缀,group by 性能将会比较低效

    遵循最佳左前缀的情况如下

    6、count 优化

    count() 是一个聚合函数,对于返回的结果集,一行行判断,如果 count 函数的参数不是NULL,累计值就加 1,否则不加,最后返回累计值;

    用法:count(*)count(主键)count(字段)count(数字)

    如下列举了count的几种写法的详细说明

    经验值总结

    按照效率排序来看,count(字段) ,所以尽量使用 count(*)

    作者:逆风飞翔的小叔

    来源:blog.csdn.net/congge_study/article/

    details/127712927

  • 别再分库分表了,试试TiDB!

    快乐分享,Java干货及时送达👇

    来源:www.cnblogs.com/jiagooushi/archive/2023/03/24/17251486.html

    • 什么是NewSQL
    • 传统SQL的问题
      • 升级服务器硬件
      • 数据分片
    • NoSQL 的问题
      • 优点
      • 缺点
    • NewSQL 特性
    • NewSQL 的主要特性
    • 三种SQL的对比
    • TiDB怎么来的
    • TiDB社区版和企业版
    • TIDB核心特性
      • 水平弹性扩展
      • 分布式事务支持
      • 金融级高可用
      • 实时 HTAP
      • 云原生的分布式数据库
      • 高度兼容 MySQL
    • OLTP&OLAP(自学)
      • OLTP(联机事务处理)
      • OLAP(联机分析处理)
      • 特性对比
      • 设计角度区别
    • TiDB 整体架构
    • TiDB的优势
    • TiDB的组件
      • TiDB Server
      • PD(Placement Driver)Server
      • TiKV Server
      • TiSpark
      • TiFlash
    • TiKV整体架构
      • Region分裂与合并
      • Region调度
      • 分布式事务
    • 高可用架构
      • TiDB高可用
      • PD高可用
      • TiKV高可用
    • 应用场景
      • MySQL分片与合并
      • 直接替换MySQL
      • 数据仓库
      • 作为其他系统的模块
    • 应用案例
    • TiDB与MySQL兼容性对比
    • TiDB不支持的MySql特性
    • 自增ID
    • SELECT 的限制
    • 视图
    • 默认设置差异
      • 字符集
      • 排序规则
      • 大小写敏感
      • 参数解释
      • timestamp类型字段更新
      • 参数解释
      • 外键支持

    TiDB 是一个分布式 NewSQL 数据库。它支持水平弹性扩展、ACID 事务、标准 SQL、MySQL 语法和 MySQL 协议,具有数据强一致的高可用特性,是一个不仅适合 OLTP 场景还适合 OLAP 场景的混合数据库。

    TiDB是 PingCAP公司自主设计、研发的开源分布式关系型数据库,是一款同时支持在线事务处理与在线分析处理 (Hybrid Transactional and Analytical Processing, HTAP)的融合型分布式数据库产品,具备水平扩容或者缩容、金融级高可用、实时 HTAP、云原生的分布式数据库、兼容 MySQL 5.7 协议和 MySQL 生态等重要特性。目标是为用户提供一站式 OLTP (Online Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 解决方案。TiDB 适合高可用、强一致要求较高、数据规模较大等各种应用场景。

    什么是NewSQL

    数据库发展至今已经有3代了:

    1. SQL,传统关系型数据库,例如 MySQL
    2. noSQL,例如 MongoDB,Redis
    3. newSQL

    传统SQL的问题

    互联网在本世纪初开始迅速发展,互联网应用的用户规模、数据量都越来越大,并且要求7X24小时在线。

    传统关系型数据库在这种环境下成为了瓶颈,通常有2种解决方法:

    升级服务器硬件

    虽然提升了性能,但总有天花板。

    数据分片

    使用分布式集群结构

    对单点数据库进行数据分片,存放到由廉价机器组成的分布式的集群里,可扩展性更好了,但也带来了新的麻烦。

    以前在一个库里的数据,现在跨了多个库,应用系统不能自己去多个库中操作,需要使用数据库分片中间件。

    分片中间件做简单的数据操作时还好,但涉及到跨库join、跨库事务时就很头疼了,很多人干脆自己在业务层处理,复杂度较高。

    NoSQL 的问题

    后来 noSQL 出现了,放弃了传统SQL的强事务保证和关系模型,重点放在数据库的高可用性和可扩展性。

    优点

    • 高可用性和可扩展性,自动分区,轻松扩展
    • 不保证强一致性,性能大幅提升
    • 没有关系模型的限制,极其灵活

    缺点

    • 不保证强一致性,对于普通应用没问题,但还是有不少像金融一样的企业级应用有强一致性的需求。
    • 不支持 SQL 语句,兼容性是个大问题,不同的 NoSQL 数据库都有自己的 api 操作数据,比较复杂。

    NewSQL 特性

    NewSQL 提供了与 noSQL 相同的可扩展性,而且仍基于关系模型,还保留了极其成熟的 SQL 作为查询语言,保证了ACID事务特性。

    简单来讲,NewSQL 就是在传统关系型数据库上集成了 NoSQL 强大的可扩展性。

    传统的SQL架构设计基因中是没有分布式的,而 NewSQL 生于云时代,天生就是分布式架构。

    NewSQL 的主要特性

    • SQL 支持,支持复杂查询和大数据分析。
    • 支持 ACID 事务,支持隔离级别。
    • 弹性伸缩,扩容缩容对于业务层完全透明。
    • 高可用,自动容灾。

    三种SQL的对比

    图片

    TiDB怎么来的

    著名的开源分布式缓存服务 Codis 的作者,PingCAP联合创始人& CTO ,资深 infrastructure 工程师的黄东旭,擅长分布式存储系统的设计与实现,开源狂热分子的技术大神级别人物。即使在互联网如此繁荣的今天,在数据库这片边界模糊且不确定地带,他还在努力寻找确定性的实践方向。关注公z号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能优化手册!

    直到 2012 年底,他看到 Google 发布的两篇论文,如同棱镜般,折射出他自己内心微烁的光彩。这两篇论文描述了 Google 内部使用的一个海量关系型数据库 F1/Spanner ,解决了关系型数据库、弹性扩展以及全球分布的问题,并在生产中大规模使用。“如果这个能实现,对数据存储领域来说将是颠覆性的”,黄东旭为完美方案的出现而兴奋, PingCAP 的 TiDB 在此基础上诞生了。

    TiDB社区版和企业版

    TiDB分为社区版以及企业版,企业版收费提供服务以及安全性的支持

    图片

    TIDB核心特性

    水平弹性扩展

    通过简单地增加新节点即可实现 TiDB 的水平扩展,按需扩展吞吐或存储,轻松应对高并发、海量数据场景

    得益于 TiDB 存储计算分离的架构的设计,可按需对计算、存储分别进行在线扩容或者缩容,扩容或者缩容过程中对应用运维人员透明。

    分布式事务支持

    TiDB 100% 支持标准的 ACID 事务

    金融级高可用

    相比于传统主从 (M-S) 复制方案,基于 Raft 的多数派选举协议可以提供金融级的 100% 数据强一致性保证,且在不丢失大多数副本的前提下,可以实现故障的自动恢复 (auto-failover),无需人工介入

    数据采用多副本存储,数据副本通过 Multi-Raft 协议同步事务日志,多数派写入成功事务才能提交,确保数据强一致性且少数副本发生故障时不影响数据的可用性。可按需配置副本地理位置、副本数量等策略满足不同容灾级别的要求。

    实时 HTAP

    TiDB 作为典型的 OLTP 行存数据库,同时兼具强大的 OLAP 性能,配合 TiSpark,可提供一站式 HTAP 解决方案,一份存储同时处理 OLTP & OLAP 无需传统繁琐的 ETL 过程

    提供行存储引擎 TiKV、列存储引擎 TiFlash 两款存储引擎,TiFlash 通过 Multi-Raft Learner 协议实时从 TiKV 复制数据,确保行存储引擎 TiKV 和列存储引擎 TiFlash 之间的数据强一致。TiKV、TiFlash 可按需部署在不同的机器,解决 HTAP 资源隔离的问题。

    云原生的分布式数据库

    TiDB 是为云而设计的数据库,同 Kubernetes 深度耦合,支持公有云、私有云和混合云,使部署、配置和维护变得十分简单。TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。TiDB 对业务没有任何侵入性,能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题,专注于业务开发,极大的提升研发的生产力

    高度兼容 MySQL

    兼容 MySQL 5.7 协议、MySQL 常用的功能、MySQL 生态,应用无需或者修改少量代码即可从 MySQL 迁移到 TiDB。

    提供丰富的数据迁移工具帮助应用便捷完成数据迁移,大多数情况下,无需修改代码即可从 MySQL 轻松迁移至 TiDB,分库分表后的 MySQL 集群亦可通过 TiDB 工具进行实时迁移。

    OLTP&OLAP(自学)

    OLTP(联机事务处理)

    OLTP(Online Transactional Processing) 即联机事务处理,OLTP 是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,记录即时的增、删、改、查,比如在银行存取一笔款,就是一个事务交易

    联机事务处理是事务性非常高的系统,一般都是高可用的在线系统,以小的事务以及小的查询为主,评估其系统的时候,一般看其每秒执行的Transaction以及Execute SQL的数量。在这样的系统中,单个数据库每秒处理的Transaction往往超过几百个,或者是几千个,Select 语句的执行量每秒几千甚至几万个。典型的OLTP系统有电子商务系统、银行、证券等,如美国eBay的业务数据库,就是很典型的OLTP数据库。

    OLAP(联机分析处理)

    OLAP(Online Analytical Processing) 即联机分析处理,是数据仓库的核心部心,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。典型的应用就是复杂的动态报表系统

    在这样的系统中,语句的执行量不是考核标准,因为一条语句的执行时间可能会非常长,读取的数据也非常多。所以,在这样的系统中,考核的标准往往是磁盘子系统的吞吐量(带宽),如能达到多少MB/s的流量。

    特性对比

    OLTP和OLAP的特性对比

    OLTP OLAP
    实时性 OLTP 实时性要求高,OLTP 数据库旨在使事务应用程序仅写入所需的数据,以便尽快处理单个事务 OLAP 的实时性要求不是很高,很多应用顶多是每天更新一下数据
    数据量 OLTP 数据量不是很大,一般只读 / 写数十条记录,处理简单的事务 OLAP 数据量大,因为 OLAP 支持的是动态查询,所以用户也许要通过将很多数据的统计后才能得到想要知道的信息,例如时间序列分析等等,所以处理的数据量很大
    用户和系统的面向性 OLTP 是面向顾客的,用于事务和查询处理 OLAP 是面向市场的,用于数据分析
    数据库设计 OLTP 采用实体 – 联系 ER 模型和面向应用的数据库设计 OLAP 采用星型或雪花模型和面向主题的数据库设计

    设计角度区别

    OLTP OLAP
    用户 操作人员,低层管理人员 决策人员,高级管理人员
    功能 日常操作处理 分析决策
    主要工作 增、删、改 查询
    DB 设计 面向应用 面向主题
    数据 当前的,最新的细节,二维的,分立的 历史的,聚集的,多维集成的,统一的
    存取 读/写数十条记录 读上百万条记录
    工作单位 简单的事务 复杂的查询
    用户数 上千个 上百个
    DB 大小 100MB-GB 100GB-TB

    TiDB 整体架构

    TiDB的优势

    与传统的单机数据库相比,TiDB 具有以下优势:

    • 纯分布式架构,拥有良好的扩展性,支持弹性的扩缩容
    • 支持 SQL,对外暴露 MySQL 的网络协议,并兼容大多数 MySQL 的语法,在大多数场景下可以直接替换 MySQL
    • 默认支持高可用,在少数副本失效的情况下,数据库本身能够自动进行数据修复和故障转移,对业务透明
    • 支持 ACID 事务,对于一些有强一致需求的场景友好,例如:银行转账
    • 具有丰富的工具链生态,覆盖数据迁移、同步、备份等多种场景

    TiDB的组件

    要深入了解 TiDB 的水平扩展和高可用特点,首先需要了解 TiDB 的整体架构。TiDB 集群主要包括三个核心组件:TiDB Server,PD Server 和 TiKV Server,此外,还有用于解决用户复杂 OLAP 需求的 TiSpark 组件。关注公z号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能优化手册!

    在内核设计上,TiDB 分布式数据库将整体架构拆分成了多个模块,各模块之间互相通信,组成完整的 TiDB 系统。对应的架构图如下:

    图片

    architecture

    TiDB Server

    TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如 LVS、HAProxy 或 F5)对外提供统一的接入地址。

    PD (Placement Driver) Server

    Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:

    • 一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);
    • 二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);
    • 三是分配全局唯一且递增的事务 ID。

    PD 通过 Raft 协议保证数据的安全性。Raft 的 leader server 负责处理所有操作,其余的 PD server 仅用于保证高可用。建议部署奇数个 PD 节点

    TiKV Server

    TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。

    TiSpark

    TiSpark 作为 TiDB 中解决用户复杂 OLAP 需求的主要组件,将 Spark SQL 直接运行在 TiDB 存储层上,同时融合 TiKV 分布式集群的优势,并融入大数据社区生态。至此,TiDB 可以通过一套系统,同时支持 OLTP 与 OLAP,免除用户数据同步的烦恼。

    TiFlash

    TiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,主要的功能是为分析型的场景加速。

    TiKV整体架构

    与传统的整节点备份方式不同的,TiKV是将数据按照 key 的范围划分成大致相等的切片(下文统称为 Region),每一个切片会有多个副本(通常是 3 个),其中一个副本是 Leader,提供读写服务。TiKV 通过 PD 对这些 Region 以及副本进行调度,以保证数据和读写负载都均匀地分散在各个 TiKV 上,这样的设计保证了整个集群资源的充分利用并且可以随着机器数量的增加水平扩展。

    图片

    Region分裂与合并

    当某个 Region 的大小超过一定限制(默认是 144MB)后,TiKV 会将它分裂为两个或者更多个 Region,以保证各个 Region 的大小是大致接近的,这样更有利于 PD 进行调度决策。同样,当某个 Region 因为大量的删除请求导致 Region 的大小变得更小时,TiKV 会将比较小的两个相邻 Region 合并为一个。

    Region调度

    Region 与副本之间通过 Raft 协议来维持数据一致性,任何写请求都只能在 Leader 上写入,并且需要写入多数副本后(默认配置为 3 副本,即所有请求必须至少写入两个副本成功)才会返回客户端写入成功。

    当 PD 需要把某个 Region 的一个副本从一个 TiKV 节点调度到另一个上面时,PD 会先为这个 Raft Group 在目标节点上增加一个 Learner 副本(复制 Leader 的数据)。当这个 Learner 副本的进度大致追上 Leader 副本时,Leader 会将它变更为 Follower,之后再移除操作节点的 Follower 副本,这样就完成了 Region 副本的一次调度。

    Leader 副本的调度原理也类似,不过需要在目标节点的 Learner 副本变为 Follower 副本后,再执行一次 Leader Transfer,让该 Follower 主动发起一次选举成为新 Leader,之后新 Leader 负责删除旧 Leader 这个副本。

    分布式事务

    TiKV 支持分布式事务,用户(或者 TiDB)可以一次性写入多个 key-value 而不必关心这些 key-value 是否处于同一个数据切片 (Region) 上,TiKV 通过两阶段提交保证了这些读写请求的 ACID 约束。

    高可用架构

    高可用是 TiDB 的另一大特点,TiDB/TiKV/PD 这三个组件都能容忍部分实例失效,不影响整个集群的可用性。下面分别说明这三个组件的可用性、单个实例失效后的后果以及如何恢复。

    TiDB高可用

    TiDB 是无状态的,推荐至少部署两个实例,前端通过负载均衡组件对外提供服务。当单个实例失效时,会影响正在这个实例上进行的 Session,从应用的角度看,会出现单次请求失败的情况,重新连接后即可继续获得服务。单个实例失效后,可以重启这个实例或者部署一个新的实例。

    PD高可用

    PD 是一个集群,通过 Raft 协议保持数据的一致性,单个实例失效时,如果这个实例不是 Raft 的 leader,那么服务完全不受影响;如果这个实例是 Raft 的 leader,会重新选出新的 Raft leader,自动恢复服务。PD 在选举的过程中无法对外提供服务,这个时间大约是3秒钟。推荐至少部署三个 PD 实例,单个实例失效后,重启这个实例或者添加新的实例。

    TiKV高可用

    TiKV 是一个集群,通过 Raft 协议保持数据的一致性(副本数量可配置,默认保存三副本),并通过 PD 做负载均衡调度。单个节点失效时,会影响这个节点上存储的所有 Region。对于 Region 中的 Leader 结点,会中断服务,等待重新选举;对于 Region 中的 Follower 节点,不会影响服务。当某个 TiKV 节点失效,并且在一段时间内(默认 10 分钟)无法恢复,PD 会将其上的数据迁移到其他的 TiKV 节点上。

    应用场景

    MySQL分片与合并

    图片

    TiDB 应用的第一类场景是 MySQL 的分片与合并。对于已经在用 MySQL 的业务,分库、分表、分片、中间件是常用手段,随着分片的增多,跨分片查询是一大难题。TiDB 在业务层兼容 MySQL 的访问协议,PingCAP 做了一个数据同步的工具——Syncer,它可以把黄东旭 TiDB 作为一个 MySQL Slave,将 TiDB 作为现有数据库的从库接在主 MySQL 库的后方,在这一层将数据打通,可以直接进行复杂的跨库、跨表、跨业务的实时 SQL 查询。黄东旭提到,“过去的数据库都是一主多从,有了 TiDB 以后,可以反过来做到多主一从。”

    直接替换MySQL

    图片

    第二类场景是用 TiDB 直接去替换 MySQL。如果你的IT架构在搭建之初并未考虑分库分表的问题,全部用了 MySQL,随着业务的快速增长,海量高并发的 OLTP 场景越来越多,如何解决架构上的弊端呢?

    在一个 TiDB 的数据库上,所有业务场景不需要做分库分表,所有的分布式工作都由数据库层完成。TiDB 兼容 MySQL 协议,所以可以直接替换 MySQL,而且基本做到了开箱即用,完全不用担心传统分库分表方案带来繁重的工作负担和复杂的维护成本,友好的用户界面让常规的技术人员可以高效地进行维护和管理。另外,TiDB 具有 NoSQL 类似的扩容能力,在数据量和访问流量持续增长的情况下能够通过水平扩容提高系统的业务支撑能力,并且响应延迟稳定。

    数据仓库

    图片

    TiDB 本身是一个分布式系统,第三种使用场景是将 TiDB 当作数据仓库使用。TPC-H 是数据分析领域的一个测试集,TiDB 2.0 在 OLAP 场景下的性能有了大幅提升,原来只能在数据仓库里面跑的一些复杂的 Query,在 TiDB 2.0 里面跑,时间基本都能控制在 10 秒以内。当然,因为 OLAP 的范畴非常大,TiDB 的 SQL 也有搞不定的情况,为此 PingCAP 开源了 TiSpark,TiSpark 是一个 Spark 插件,用户可以直接用 Spark SQL 实时地在 TiKV 上做大数据分析。

    作为其他系统的模块

    图片

    TiDB 是一个传统的存储跟计算分离的项目,其底层的 Key-Value 层,可以单独作为一个 HBase 的 Replacement 来用,它同时支持跨行事务。TiDB 对外提供两个 API 接口,一个是 ACID Transaction 的 API,用于支持跨行事务;另一个是 Raw API,它可以做单行的事务,换来的是整个性能的提升,但不提供跨行事务的 ACID 支持。用户可以根据自身的需求在两个 API 之间自行选择。例如有一些用户直接在 TiKV 之上实现了 Redis 协议,将 TiKV 替换一些大容量,对延迟要求不高的 Redis 场景。

    应用案例

    图片

    TiDB与MySQL兼容性对比

    • TiDB支持MySQL 传输协议及其绝大多数的语法。这意味着您现有的MySQL连接器和客户端都可以继续使用。大多数情况下您现有的应用都可以迁移至 TiDB,无需任何代码修改。
    • 当前TiDB服务器官方支持的版本为MySQL 5.7 。大部分MySQL运维工具(如PHPMyAdmin, Navicat, MySQL Workbench等),以及备份恢复工具(如 mysqldump, Mydumper/myloader)等都可以直接使用。
    • 不过一些特性由于在分布式环境下没法很好的实现,目前暂时不支持或者是表现与MySQL有差异
    • 一些MySQL语法在TiDB中可以解析通过,但是不会做任何后续的处理 ,例如Create Table语句中Engine,是解析并忽略。

    TiDB不支持的MySql特性

    • 存储过程与函数
    • 触发器
    • 事件
    • 自定义函数
    • 外键约束
    • 临时表
    • 全文/空间函数与索引
    • ascii/latin1/binary/utf8/utf8mb4 的字符集
    • SYS schema
    • MySQL 追踪优化器
    • XML 函数
    • X-Protocol
    • Savepoints
    • 列级权限
    • XA 语法(TiDB 内部使用两阶段提交,但并没有通过 SQL 接口公开)
    • CREATE TABLE tblName AS SELECT stmt 语法
    • CHECK TABLE 语法
    • CHECKSUM TABLE 语法
    • GET_LOCKRELEASE_LOCK 函数

    自增ID

    TiDB 的自增列仅保证唯一,也能保证在单个 TiDB server 中自增,但不保证多个 TiDB server 中自增,不保证自动分配的值的连续性,建议不要将缺省值和自定义值混用,若混用可能会收 Duplicated Error 的错误信息。

    TiDB 可通过 tidb_allow_remove_auto_inc 系统变量开启或者关闭允许移除列的 AUTO_INCREMENT 属性。删除列属性的语法是:alter table modifyalter table change

    TiDB 不支持添加列的 AUTO_INCREMENT 属性,移除该属性后不可恢复。

    SELECT 的限制

    • 不支持 SELECT ... INTO @变量 语法。
    • 不支持 SELECT ... GROUP BY ... WITH ROLLUP 语法。
    • TiDB 中的 SELECT .. GROUP BY expr 的返回结果与 MySQL 5.7 并不一致。MySQL 5.7 的结果等价于 GROUP BY expr ORDER BY expr。而 TiDB 中该语法所返回的结果并不承诺任何顺序,与 MySQL 8.0 的行为一致。

    视图

    目前TiDB不支持 对视图进行UPDATE、INSERT、DELETE等写入操作

    默认设置差异

    字符集

    • TiDB 默认:utf8mb4
    • MySQL 5.7 默认:latin1
    • MySQL 8.0 默认:utf8mb4

    排序规则

    • TiDB 中 utf8mb4 字符集默认:utf8mb4_bin
    • MySQL 5.7 中 utf8mb4 字符集默认:utf8mb4_general_ci
    • MySQL 8.0 中 utf8mb4 字符集默认:utf8mb4_unicode_520_ci

    大小写敏感

    关于lower_case_table_names的配置

    • TiDB 默认:2,且仅支持设置该值为 2

    • MySQL 默认如下:

      • Linux 系统中该值为 0
    • Windows 系统中该值为 1

    • macOS 系统中该值为 2

    参数解释

    • lower_case_table_names=0 表名存储为给定的大小和比较是区分大小写的
    • lower_case_table_names = 1 表名存储在磁盘是小写的,但是比较的时候是不区分大小写
    • lower_case_table_names=2 表名存储为给定的大小写但是比较的时候是小写的

    timestamp类型字段更新

    默认情况下,timestamp类型字段所在数据行被更新时,该字段会自动更新为当前时间,而参数explicit_defaults_for_timestamp控制这一种行为。

    • TiDB 默认:ON,且仅支持设置该值为 ON
    • MySQL 5.7 默认:OFF
    • MySQL 8.0 默认:ON

    参数解释

    • explicit_defaults_for_timestamp=off,数据行更新时,timestamp类型字段更新为当前时间
    • explicit_defaults_for_timestamp=on,数据行更新时,timestamp类型字段不更新为当前时间。

    外键支持

    • TiDB 默认:OFF,且仅支持设置该值为 OFF
    • MySQL 5.7 默认:ON

  • SpringBoot 实现 MySQL 百万级数据量导出并避免 OOM 的解决方案

    快乐分享,Java干货及时送达👇

    动态数据导出是一般项目都会涉及到的功能。它的基本实现逻辑就是从mysql查询数据,加载到内存,然后从内存创建excel或者csv,以流的形式响应给前端。

    • 参考:https://grokonez.com/spring-framework/spring-boot/excel-file-download-from-springboot-restapi-apache-poi-mysql。

    SpringBoot下载excel基本都是这么干。

    虽然这是个可行的方案,然而一旦mysql数据量太大,达到十万级,百万级,千万级,大规模数据加载到内存必然会引起OutofMemoryError

    要考虑如何避免OOM,一般有两个方面的思路。

    一方面就是尽量不做呗,先怼产品下面几个问题啊:

    • 我们为什么要导出这么多数据呢?谁傻到去看这么大的数据啊,这个设计是不是合理的呢?
    • 怎么做好权限控制?百万级数据导出你确定不会泄露商业机密?
    • 如果要导出百万级数据,那为什么不直接找大数据或者DBA来干呢?然后以邮件形式传递不行吗?
    • 为什么要通过后端的逻辑来实现,不考虑时间成本,流量成本吗?
    • 如果通过分页导出,每次点击按钮只导2万条,分批导出难道不能满足业务需求吗?

    如果产品说 “甲方是爸爸,你去和甲方说啊”,“客户说这个做出来,才考虑付尾款!”,如果客户的确缺根筋要让你这样搞, 那就只能从技术上考虑如何实现了。

    从技术上讲,为了避免OOM,我们一定要注意一个原则:

    不能将全量数据一次性加载到内存之中。

    全量加载不可行,那我们的目标就是如何实现数据的分批加载了。实事上,Mysql本身支持Stream查询,我们可以通过Stream流获取数据,然后将数据逐条刷入到文件中,每次刷入文件后再从内存中移除这条数据,从而避免OOM。

    由于采用了数据逐条刷入文件,而且数据量达到百万级,所以文件格式就不要采用excel了,excel2007最大才支持104万行的数据。这里推荐:

    以csv代替excel。

    考虑到当前SpringBoot持久层框架通常为JPA和mybatis,我们可以分别从这两个框架实现百万级数据导出的方案。

    JPA实现百万级数据导出

    • 具体方案不妨参考:http://knes1.github.io/blog/2015/2015-10-19-streaming-mysql-results-using-java8-streams-and-spring-data.html。

    实现项目对应:

    • https://github.com/knes1/todo

    核心注解如下,需要加入到具体的Repository之上。方法的返回类型定义成Stream。Integer.MIN_VALUE告诉jdbc driver逐条返回数据。

    @QueryHints(value = @QueryHint(name = HINT_FETCH_SIZE, value = "" + Integer.MIN_VALUE))
    @Query(value = "select t from Todo t")
    Stream streamAll();

    此外还需要在Stream处理数据的方法之上添加@Transactional(readOnly = true),保证事物是只读的。

    同时需要注入javax.persistence.EntityManager,通过detach从内存中移除已经使用后的对象。

    @RequestMapping(value = "/todos.csv", method = RequestMethod.GET)
    @Transactional(readOnly = true)
    public void exportTodosCSV(HttpServletResponse response) {
     response.addHeader("Content-Type""application/csv");
     response.addHeader("Content-Disposition""attachment; filename=todos.csv");
     response.setCharacterEncoding("UTF-8");
     try(Stream todoStream = todoRepository.streamAll()) {
      PrintWriter out = response.getWriter();
      todoStream.forEach(rethrowConsumer(todo -> {
       String line = todoToCSV(todo);
       out.write(line);
       out.write("n");
       entityManager.detach(todo);
      }));
      out.flush();
     } catch (IOException e) {
      log.info("Exception occurred " + e.getMessage(), e);
      throw new RuntimeException("Exception occurred while exporting results", e);
     }
    }

    MyBatis实现百万级数据导出

    MyBatis实现逐条获取数据,必须要自定义ResultHandler,然后在mapper.xml文件中,对应的select语句中添加fetchSize="-2147483648"

    最后将自定义的ResultHandler传给SqlSession来执行查询,并将返回的结果进行处理。

    MyBatis实现百万级数据导出的具体实例

    以下是基于MyBatis Stream导出的完整的工程样例,我们将通过对比Stream文件导出和传统方式导出的内存占用率的差异,来验证Stream文件导出的有效性。

    我们先定义一个工具类DownloadProcessor,它内部封装一个HttpServletResponse对象,用来将对象写入到csv。

    public class DownloadProcessor {
        private final HttpServletResponse response;
         
        public DownloadProcessor(HttpServletResponse response) {
            this.response = response;
            String fileName = System.currentTimeMillis() + ".csv";
            this.response.addHeader("Content-Type""application/csv");
            this.response.addHeader("Content-Disposition""attachment; filename="+fileName);
            this.response.setCharacterEncoding("UTF-8");
        }
         
        public  void processData(E record) {
            try {
                response.getWriter().write(record.toString()); //如果是要写入csv,需要重写toString,属性通过","分割
                response.getWriter().write("n");
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    然后通过实现org.apache.ibatis.session.ResultHandler,自定义我们的ResultHandler,它用于获取java对象,然后传递给上面的DownloadProcessor处理类进行写文件操作:

    public class CustomResultHandler implements ResultHandler {

        private final DownloadProcessor downloadProcessor;
         
        public CustomResultHandler(
                DownloadProcessor downloadProcessor)
     
    {
            super();
            this.downloadProcessor = downloadProcessor;
        }
         
        @Override
        public void handleResult(ResultContext resultContext) {
            Authors authors = (Authors)resultContext.getResultObject();
            downloadProcessor.processData(authors);
        }
    }

    实体类:

    public class Authors {
        private Integer id;
        private String firstName;
         
        private String lastName;
         
        private String email;
         
        private Date birthdate;
         
        private Date added;
         
        public Integer getId() {
            return id;
        }
         
        public void setId(Integer id) {
            this.id = id;
        }
         
        public String getFirstName() {
            return firstName;
        }
         
        public void setFirstName(String firstName) {
            this.firstName = firstName == null ? null : firstName.trim();
        }
         
        public String getLastName() {
            return lastName;
        }
         
        public void setLastName(String lastName) {
            this.lastName = lastName == null ? null : lastName.trim();
        }
         
        public String getEmail() {
            return email;
        }
         
        public void setEmail(String email) {
            this.email = email == null ? null : email.trim();
        }
         
        public Date getBirthdate() {
            return birthdate;
        }
         
        public void setBirthdate(Date birthdate) {
            this.birthdate = birthdate;
        }
         
        public Date getAdded() {
            return added;
        }
         
        public void setAdded(Date added) {
            this.added = added;
        }
         
        @Override
        public String toString() {
            return this.id + "," + this.firstName + "," + this.lastName + "," + this.email + "," + this.birthdate + "," + this.added;
        }
    }

    Mapper接口:

    public interface AuthorsMapper {
       List selectByExample(AuthorsExample example);
        
       List streamByExample(AuthorsExample example)//以stream形式从mysql获取数据
    }

    Mapper xml文件核心片段,以下两条select的唯一差异就是在stream获取数据的方式中多了一条属性:fetchSize="-2147483648"

    select id="selectByExample" parameterType="com.alphathur.mysqlstreamingexport.domain.AuthorsExample" resultMap="BaseResultMap">
        select
        if test="distinct">
          distinct
        if>
        'false' as QUERYID,
        include refid="Base_Column_List" />
        from authors
        if test="_parameter != null">
          include refid="Example_Where_Clause" />
        if>
        if test="orderByClause != null">
          order by ${orderByClause}
        if>
      select>
      select id="streamByExample" fetchSize="-2147483648" parameterType="com.alphathur.mysqlstreamingexport.domain.AuthorsExample" resultMap="BaseResultMap">
        select
        if test="distinct">
          distinct
        if>
        'false' as QUERYID,
        include refid="Base_Column_List" />
        from authors
        if test="_parameter != null">
          include refid="Example_Where_Clause" />
        if>
        if test="orderByClause != null">
          order by ${orderByClause}
        if>
      select>

    获取数据的核心service如下,由于只做个简单演示,就懒得写成接口了。其中 streamDownload 方法即为stream取数据写文件的实现,它将以很低的内存占用从MySQL获取数据;此外还提供traditionDownload方法,它是一种传统的下载方式,批量获取全部数据,然后将每个对象写入文件。

    @Service
    public class AuthorsService {
        private final SqlSessionTemplate sqlSessionTemplate;
        private final AuthorsMapper authorsMapper;

        public AuthorsService(SqlSessionTemplate sqlSessionTemplate, AuthorsMapper authorsMapper) {
            this.sqlSessionTemplate = sqlSessionTemplate;
            this.authorsMapper = authorsMapper;
        }

        /**
         * stream读数据写文件方式
         * @param httpServletResponse
         * @throws IOException
         */

        public void streamDownload(HttpServletResponse httpServletResponse)
                throws IOException 
    {
            AuthorsExample authorsExample = new AuthorsExample();
            authorsExample.createCriteria();
            HashMap param = new HashMap();
            param.put("oredCriteria", authorsExample.getOredCriteria());
            param.put("orderByClause", authorsExample.getOrderByClause());
            CustomResultHandler customResultHandler = new CustomResultHandler(new DownloadProcessor (httpServletResponse));
            sqlSessionTemplate.select(
                    "com.alphathur.mysqlstreamingexport.mapper.AuthorsMapper.streamByExample", param, customResultHandler);
            httpServletResponse.getWriter().flush();
            httpServletResponse.getWriter().close();
        }

        /**
         * 传统下载方式
         * @param httpServletResponse
         * @throws IOException
         */

        public void traditionDownload(HttpServletResponse httpServletResponse)
                throws IOException 
    {
            AuthorsExample authorsExample = new AuthorsExample();
            authorsExample.createCriteria();
            List authors = authorsMapper.selectByExample (authorsExample);
            DownloadProcessor downloadProcessor = new DownloadProcessor (httpServletResponse);
            authors.forEach (downloadProcessor::processData);
            httpServletResponse.getWriter().flush();
            httpServletResponse.getWriter().close();
        }
    }

    下载的入口controller:

    @RestController
    @RequestMapping("download")
    public class HelloController {
        private final AuthorsService authorsService;

        public HelloController(AuthorsService authorsService) {
            this.authorsService = authorsService;
        }

        @GetMapping("streamDownload")
        public void streamDownload(HttpServletResponse response)
                throws IOException 
    {
            authorsService.streamDownload(response);
        }

        @GetMapping("traditionDownload")
        public void traditionDownload(HttpServletResponse response)
                throws IOException 
    {
            authorsService.traditionDownload (response);
        }
    }   

    实体类对应的表结构创建语句:

    CREATE TABLE `authors` (
      `id` int(11NOT NULL AUTO_INCREMENT,
      `first_name` varchar(50CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
      `last_name` varchar(50CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
      `email` varchar(100CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
      `birthdate` date NOT NULL,
      `added` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`)
    ENGINE=InnoDB AUTO_INCREMENT=10095 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

    这里有个问题:如何短时间内创建大批量测试数据到MySQL呢?一种方式是使用存储过程 + 大杀器 select insert 语句!不太懂?

    没关系,且看我另一篇文章 MySQL如何生成大批量测试数据 你就会明白了。如果你懒得看,我这里已经将生成的270多万条测试数据上传到网盘,你直接下载然后通过navicat导入就好了。

    • 链接:https://pan.baidu.com/s/1hqnWU2JKlL4Tb9nWtJl4sw
    • 提取码:nrp0

    有了测试数据,我们就可以直接测试了。先启动项目,然后打开jdk bin目录下的 jconsole.exe

    首先我们测试传统方式下载文件的内存占用,直接浏览器访问:http://localhost:8080/download/traditionDownload

    可以看出,下载开始前内存占用大概为几十M,下载开始后内存占用急速上升,峰值达到接近2.5G,即使是下载完成,堆内存也维持一个较高的占用,这实在是太可怕了,如果生产环境敢这么搞,不出意外肯定内存溢出。

    接着我们测试stream方式文件下载的内存占用,浏览器访问:http://localhost:8080/download/streamDownload,当下载开始后,内存占用也会有一个明显的上升,但是峰值才到500M。对比于上面的方式,内存占用率足足降低了80%!怎么样,兴奋了吗!

    我们再通过记事本打开下载后的两个文件,发现内容没有缺斤少两,都是2727127行,完美!

    感谢阅读,希望对你有所帮助 🙂 来源:

    blog.csdn.net/haohao_ding/article/details/123164771