分类: java

  • 九点半助手

    快乐分享,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上的属性,而并未新建对象。

  • 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

  • SpringBoot整合Canal+RabbitMQ监听数据变更~

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

    来源:JAVA日知录

    • 需求
    • 步骤
    • 环境搭建
    • 整合SpringBoot Canal实现客户端
    • Canal整合RabbitMQ
    • SpringBoot整合RabbitMQ

    需求

    我想要在SpringBoot中采用一种与业务代码解耦合的方式,来实现数据的变更记录,记录的内容是新数据,如果是更新操作还得有旧数据内容。

    经过调研发现,使用Canal来监听MySQL的binlog变化可以实现这个需求,可是在监听到变化后需要马上保存变更记录,除非再做一些逻辑处理,于是我又结合了RabbitMQ来处理保存变更记录的操作。

    步骤

    • 启动MySQL环境,并开启binlog
    • 启动Canal环境,为其创建一个MySQL账号,然后以Slave的形式连接MySQL
    • Canal服务模式设为TCP,用Java编写客户端代码,监听MySQL的binlog修改
    • Canal服务模式设为RabbitMQ,启动RabbitMQ环境,配置Canal和RabbitMQ的连接,用消息队列去接收binlog修改事件

    环境搭建

    环境搭建基于docker-compose:

    version: "3"  
    services:  
        mysql:  
            network_mode: mynetwork  
            container_name: mymysql  
            ports:  
                - 3306:3306  
            restart: always  
            volumes:  
                - /etc/localtime:/etc/localtime  
                - /home/mycontainers/mymysql/data:/data  
                - /home/mycontainers/mymysql/mysql:/var/lib/mysql  
                - /home/mycontainers/mymysql/conf:/etc/mysql  
            environment:  
                - MYSQL_ROOT_PASSWORD=root  
            command:   
                --character-set-server=utf8mb4  
                --collation-server=utf8mb4_unicode_ci  
                --log-bin=/var/lib/mysql/mysql-bin  
                --server-id=1  
                --binlog-format=ROW  
                --expire_logs_days=7  
                --max_binlog_size=500M  
            image: mysql:5.7.20  
        rabbitmq:     
            container_name: myrabbit  
            ports:  
                - 15672:15672  
                - 5672:5672  
            restart: always  
            volumes:  
                - /etc/localtime:/etc/localtime  
                - /home/mycontainers/myrabbit/rabbitmq:/var/lib/rabbitmq  
            network_mode: mynetwork  
            environment:  
                - RABBITMQ_DEFAULT_USER=admin  
                - RABBITMQ_DEFAULT_PASS=123456  
            image: rabbitmq:3.8-management  
        canal-server:  
            container_name: canal-server  
            restart: always  
            ports:  
                - 11110:11110  
                - 11111:11111  
                - 11112:11112  
            volumes:  
                - /home/mycontainers/canal-server/conf/canal.properties:/home/admin/canal-server/conf/canal.properties  
                - /home/mycontainers/canal-server/conf/instance.properties:/home/admin/canal-server/conf/example/instance.properties  
                - /home/mycontainers/canal-server/logs:/home/admin/canal-server/logs  
            network_mode: mynetwork  
            depends_on:  
                - mysql  
                - rabbitmq  
                # - canal-admin  
            image: canal/canal-server:v1.1.5  

    我们需要修改下Canal环境的配置文件:canal.propertiesinstance.properties,映射Canal中的以下两个路径:

    • /home/admin/canal-server/conf/canal.properties

    配置文件中,canal.destinations意思是server上部署的instance列表,

    • /home/admin/canal-server/conf/example/instance.properties

    这里的/example是指instance即实例名,要和上面canal.properties内instance配置对应,canal会为实例创建对应的文件夹,一个Client对应一个实例

    以下是我们需要准备的两个配置文件具体内容:

    canal.properties

    ################################################  
    ########     common argument   ############  
    ################################################  
    # tcp bind ip  
    canal.ip =  
    # register ip to zookeeper  
    canal.register.ip =  
    canal.port = 11111  
    canal.metrics.pull.port = 11112  
    # canal instance user/passwd  
    # canal.user = canal  
    # canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458  
      
    # canal admin config  
    # canal.admin.manager = canal-admin:8089  
      
    # canal.admin.port = 11110  
    # canal.admin.user = admin  
    # canal.admin.passwd = 6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9  
      
    # admin auto register 自动注册  
    # canal.admin.register.auto = true  
    # 集群名,单机则不写  
    # canal.admin.register.cluster =  
    # Canal Server 名字  
    # canal.admin.register.name = canal-admin  
      
    canal.zkServers =  
    # flush data to zk  
    canal.zookeeper.flush.period = 1000  
    canal.withoutNetty = false  
    # tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ  
    canal.serverMode = tcp  
    # flush meta cursor/parse position to file  
    canal.file.data.dir = ${canal.conf.dir}  
    canal.file.flush.period = 1000  
    # memory store RingBuffer size, should be Math.pow(2,n)  
    canal.instance.memory.buffer.size = 16384  
    # memory store RingBuffer used memory unit size , default 1kb  
    canal.instance.memory.buffer.memunit = 1024   
    # meory store gets mode used MEMSIZE or ITEMSIZE  
    canal.instance.memory.batch.mode = MEMSIZE  
    canal.instance.memory.rawEntry = true  
      
    # detecing config  
    canal.instance.detecting.enable = false  
    #canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()  
    canal.instance.detecting.sql = select 1  
    canal.instance.detecting.interval.time = 3  
    canal.instance.detecting.retry.threshold = 3  
    canal.instance.detecting.heartbeatHaEnable = false  
      
    # support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery  
    canal.instance.transaction.size =  1024  
    # mysql fallback connected to new master should fallback times  
    canal.instance.fallbackIntervalInSeconds = 60  
      
    # network config  
    canal.instance.network.receiveBufferSize = 16384  
    canal.instance.network.sendBufferSize = 16384  
    canal.instance.network.soTimeout = 30  
      
    # binlog filter config  
    canal.instance.filter.druid.ddl = true  
    canal.instance.filter.query.dcl = false  
    canal.instance.filter.query.dml = false  
    canal.instance.filter.query.ddl = false  
    canal.instance.filter.table.error = false  
    canal.instance.filter.rows = false  
    canal.instance.filter.transaction.entry = false  
    canal.instance.filter.dml.insert = false  
    canal.instance.filter.dml.update = false  
    canal.instance.filter.dml.delete = false  
      
    # binlog format/image check  
    canal.instance.binlog.format = ROW,STATEMENT,MIXED   
    canal.instance.binlog.image = FULL,MINIMAL,NOBLOB  
      
    # binlog ddl isolation  
    canal.instance.get.ddl.isolation = false  
      
    # parallel parser config  
    canal.instance.parser.parallel = true  
    # concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()  
    canal.instance.parser.parallelThreadSize = 16  
    # disruptor ringbuffer size, must be power of 2  
    canal.instance.parser.parallelBufferSize = 256  
      
    # table meta tsdb info  
    canal.instance.tsdb.enable = true  
    canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}  
    canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;  
    canal.instance.tsdb.dbUsername = canal  
    canal.instance.tsdb.dbPassword = canal  
    # dump snapshot interval, default 24 hour  
    canal.instance.tsdb.snapshot.interval = 24  
    # purge snapshot expire , default 360 hour(15 days)  
    canal.instance.tsdb.snapshot.expire = 360  
      
    ################################################  
    ########     destinations    ############  
    ################################################  
    canal.destinations = canal-exchange  
    # conf root dir  
    canal.conf.dir = ../conf  
    # auto scan instance dir add/remove and start/stop instance  
    canal.auto.scan = true  
    canal.auto.scan.interval = 5  
    # set this value to 'true' means that when binlog pos not found, skip to latest.  
    # WARN: pls keep 'false' in production env, or if you know what you want.  
    canal.auto.reset.latest.pos.mode = false  
      
    canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml  
    #canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml  
      
    canal.instance.global.mode = spring  
    canal.instance.global.lazy = false  
    canal.instance.global.manager.address = ${canal.admin.manager}  
    #canal.instance.global.spring.xml = classpath:spring/memory-instance.xml  
    canal.instance.global.spring.xml = classpath:spring/file-instance.xml  
    #canal.instance.global.spring.xml = classpath:spring/default-instance.xml  
      
    #################################################  
    ########         MQ Properties      ############  
    #################################################  
    # aliyun ak/sk , support rds/mq  
    canal.aliyun.accessKey =  
    canal.aliyun.secretKey =  
    canal.aliyun.uid=  
      
    canal.mq.flatMessage = true  
    canal.mq.canalBatchSize = 50  
    canal.mq.canalGetTimeout = 100  
    # Set this value to "cloud", if you want open message trace feature in aliyun.  
    canal.mq.accessChannel = local  
      
    canal.mq.database.hash = true  
    canal.mq.send.thread.size = 30  
    canal.mq.build.thread.size = 8  
      
    #################################################  
    ########         RabbitMQ       ############  
    #################################################  
    rabbitmq.host = myrabbit  
    rabbitmq.virtual.host = /  
    rabbitmq.exchange = canal-exchange  
    rabbitmq.username = admin  
    rabbitmq.password = RabbitMQ密码  
    rabbitmq.deliveryMode =  

    此时canal.serverMode = tcp,即TCP直连,我们先开启这个服务,然后手写Java客户端代码去连接它,等下再改为RabbitMQ。

    通过注释可以看到,canal支持的服务模式有:tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ,即主流的消息队列都支持。

    instance.properties

    ################################################  
    # mysql serverId , v1.0.26+ will autoGen  
    #canal.instance.mysql.slaveId=123  
      
    # enable gtid use true/false  
    canal.instance.gtidon=false  
      
    # position info  
    canal.instance.master.address=mymysql:3306  
    canal.instance.master.journal.name=  
    canal.instance.master.position=  
    canal.instance.master.timestamp=  
    canal.instance.master.gtid=  
      
    # rds oss binlog  
    canal.instance.rds.accesskey=  
    canal.instance.rds.secretkey=  
    canal.instance.rds.instanceId=  
      
    # table meta tsdb info  
    canal.instance.tsdb.enable=true  
    #canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb  
    #canal.instance.tsdb.dbUsername=canal  
    #canal.instance.tsdb.dbPassword=canal  
      
    #canal.instance.standby.address =  
    #canal.instance.standby.journal.name =  
    #canal.instance.standby.position =  
    #canal.instance.standby.timestamp =  
    #canal.instance.standby.gtid=  
      
    # username/password  
    canal.instance.dbUsername=canal  
    canal.instance.dbPassword=canal  
    canal.instance.connectionCharset = UTF-8  
    # enable druid Decrypt database password  
    canal.instance.enableDruid=false  
    #canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==  
      
    # table regex  
    canal.instance.filter.regex=.*..*  
    # table black regex  
    canal.instance.filter.black.regex=mysql.slave_.*  
    # table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)  
    #canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch  
    # table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)  
    #canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch  
      
    # mq config  
    canal.mq.topic=canal-routing-key  
    # dynamic topic route by schema or table regex  
    #canal.mq.dynamicTopic=mytest1.user,topic2:mytest2..*,.*..*  
    canal.mq.partition=0  

    把这两个配置文件映射好,再次提醒,注意实例的路径名,默认是:/example/instance.properties

    修改canal配置文件

    我们需要修改这个实例配置文件,去连接MySQL,确保以下的配置正确:

    canal.instance.master.address=mymysql:3306  
    canal.instance.dbUsername=canal  
    canal.instance.dbPassword=canal  

    mymysql是同为docker容器的MySQL环境,端口3306是指内部端口。

    这里多说明一下,docker端口配置时假设为:13306:3306,那么容器对外的端口就是13306,内部是3306,在本示例中,MySQL和Canal都是容器环境,所以Canal连接MySQL需要满足以下条件:

    • 处于同一网段(docker-compose.yml中的mynetwork)
    • 访问内部端口(即3306,而非13306)

    dbUsername和dbPassword为MySQL账号密码,为了开发方便可以使用root/root,但是我仍建议自行创建用户并分配访问权限:

    # 进入docker中的mysql容器  
    docker exec -it mymysql bash  
    # 进入mysql指令模式  
    mysql -uroot -proot  
      
    # 编写MySQL语句并执行  
    > ...  
    -- 选择mysql  
    use mysql;  
    -- 创建canal用户,账密:canal/canal  
    create user 'canal'@'%' identified by 'canal';  
    -- 分配权限,以及允许所有主机登录该用户  
    grant SELECT, INSERT, UPDATE, DELETE, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%';  
      
    -- 刷新一下使其生效  
    flush privileges;  
      
    -- 附带一个删除用户指令  
    drop user 'canal'@'%';  

    用navicat或者shell去登录canal这个用户,可以访问即创建成功

    整合SpringBoot Canal实现客户端

    Maven依赖:

    1.1.5  
      
      
      
      com.alibaba.otter  
      canal.client  
      ${canal.version}  
      
      
      com.alibaba.otter  
      canal.protocol  
      ${canal.version}  
       

    新增组件并启动:

    import com.alibaba.otter.canal.client.CanalConnector;  
    import com.alibaba.otter.canal.client.CanalConnectors;  
    import com.alibaba.otter.canal.protocol.CanalEntry;  
    import com.alibaba.otter.canal.protocol.Message;  
    import org.springframework.boot.CommandLineRunner;  
    import org.springframework.stereotype.Component;  
      
    import java.net.InetSocketAddress;  
    import java.util.List;  
      
    @Component  
    public class CanalClient {  
      
        private final static int BATCH_SIZE = 1000;  
      
        public void run() {  
            // 创建链接  
            CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("localhost", 11111), "canal-exchange""canal""canal");  
            try {  
                //打开连接  
                connector.connect();  
                //订阅数据库表,全部表  
                connector.subscribe(".*..*");  
                //回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始拿  
                connector.rollback();  
                while (true) {  
                    // 获取指定数量的数据  
                    Message message = connector.getWithoutAck(BATCH_SIZE);  
                    //获取批量ID  
                    long batchId = message.getId();  
                    //获取批量的数量  
                    int size = message.getEntries().size();  
                    //如果没有数据  
                    if (batchId == -1 || size == 0) {  
                        try {  
                            //线程休眠2秒  
                            Thread.sleep(2000);  
                        } catch (InterruptedException e) {  
                            e.printStackTrace();  
                        }  
                    } else {  
                        //如果有数据,处理数据  
                        printEntry(message.getEntries());  
                    }  
                    //进行 batch id 的确认。确认之后,小于等于此 batchId 的 Message 都会被确认。  
                    connector.ack(batchId);  
                }  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                connector.disconnect();  
            }  
        }  
      
        /**  
         * 打印canal server解析binlog获得的实体类信息  
         */  
        private static void printEntry(List entrys) {  
            for (CanalEntry.Entry entry : entrys) {  
                if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {  
                    //开启/关闭事务的实体类型,跳过  
                    continue;  
                }  
                //RowChange对象,包含了一行数据变化的所有特征  
                //比如isDdl 是否是ddl变更操作 sql 具体的ddl sql beforeColumns afterColumns 变更前后的数据字段等等  
                CanalEntry.RowChange rowChage;  
                try {  
                    rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());  
                } catch (Exception e) {  
                    throw new RuntimeException("ERROR # parser of eromanga-event has an error , data:" + entry.toString(), e);  
                }  
                //获取操作类型:insert/update/delete类型  
                CanalEntry.EventType eventType = rowChage.getEventType();  
                //打印Header信息  
                System.out.println(String.format("================》; binlog[%s:%s] , name[%s,%s] , eventType : %s",  
                        entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),  
                        entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),  
                        eventType));  
                //判断是否是DDL语句  
                if (rowChage.getIsDdl()) {  
                    System.out.println("================》;isDdl: true,sql:" + rowChage.getSql());  
                }  
                //获取RowChange对象里的每一行数据,打印出来  
                for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {  
                    //如果是删除语句  
                    if (eventType == CanalEntry.EventType.DELETE) {  
                        printColumn(rowData.getBeforeColumnsList());  
                        //如果是新增语句  
                    } else if (eventType == CanalEntry.EventType.INSERT) {  
                        printColumn(rowData.getAfterColumnsList());  
                        //如果是更新的语句  
                    } else {  
                        //变更前的数据  
                        System.out.println("------->; before");  
                        printColumn(rowData.getBeforeColumnsList());  
                        //变更后的数据  
                        System.out.println("------->; after");  
                        printColumn(rowData.getAfterColumnsList());  
                    }  
                }  
            }  
        }  
      
        private static void printColumn(List columns) {  
            for (CanalEntry.Column column : columns) {  
                System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());  
            }  
        }  
    }  

    启动类Application:

    @SpringBootApplication  
    public class BaseApplication implements CommandLineRunner {  
        @Autowired  
        private CanalClient canalClient;  
      
        @Override  
        public void run(String... args) throws Exception {  
            canalClient.run();  
        }  
    }  

    启动程序,此时新增或修改数据库中的数据,我们就能从客户端中监听到

    不过我建议监听的信息放到消息队列中,在空闲的时候去处理,所以直接配置Canal整合RabbitMQ更好。

    Canal整合RabbitMQ

    修改canal.properties中的serverMode:

    canal.serverMode = rabbitMQ  

    修改instance.properties中的topic:

    canal.mq.topic=canal-routing-key  

    然后找到关于RabbitMQ的配置:

    #################################################  
    ########         RabbitMQ       ############  
    #################################################  
    # 连接rabbit,写IP,因为同个网络下,所以可以写容器名  
    rabbitmq.host = myrabbit  
    rabbitmq.virtual.host = /  
    # 交换器名称,等等我们要去手动创建  
    rabbitmq.exchange = canal-exchange  
    # 账密  
    rabbitmq.username = admin  
    rabbitmq.password = 123456  
    # 暂不支持指定端口,使用的是默认的5762,好在在本示例中适用  

    重新启动容器,进入RabbitMQ管理页面创建exchange交换器和队列queue:

    • 新建exchange,命名为:canal-exchange
    • 新建queue,命名为:canal-queue
    • 绑定exchange和queue,routing-key设置为:canal-routing-key,这里对应上面instance.propertiescanal.mq.topic

    顺带一提,上面这段可以忽略,因为在SpringBoot的RabbitMQ配置中,会自动创建交换器exchange和队列queue,不过手动创建的话,可以在忽略SpringBoot的基础上,直接在RabbitMQ的管理页面上看到修改记录的消息。

    SpringBoot整合RabbitMQ

    依赖:

    2.3.4.RELEASE  
      
      
      
      org.springframework.boot  
      spring-boot-starter-amqp  
      ${amqp.version}  
      

    application.yml

    spring:  
      rabbitmq:  
        #    host: myserverhost  
        host: 192.168.0.108  
        port: 5672  
        username: admin  
        password: RabbitMQ密码  
        # 消息确认配置项  
        # 确认消息已发送到交换机(Exchange)  
        publisher-confirm-type: correlated  
        # 确认消息已发送到队列(Queue)  
        publisher-returns: true  

    RabbitMQ配置类:

    @Configuration  
    public class RabbitConfig {  
        @Bean  
        public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {  
            RabbitTemplate template = new RabbitTemplate();  
            template.setConnectionFactory(connectionFactory);  
            template.setMessageConverter(new Jackson2JsonMessageConverter());  
      
            return template;  
        }  
      
        /**  
         * template.setMessageConverter(new Jackson2JsonMessageConverter());  
         * 这段和上面这行代码解决RabbitListener循环报错的问题  
         */  
        @Bean  
        public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {  
            SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();  
            factory.setConnectionFactory(connectionFactory);  
            factory.setMessageConverter(new Jackson2JsonMessageConverter());  
            return factory;  
        }  
    }  

    Canal消息生产者:

    public static final String CanalQueue = "canal-queue";  
    public static final String CanalExchange = "canal-exchange";  
    public static final String CanalRouting = "canal-routing-key";  

    /**  
     * Canal消息提供者,canal-server生产的消息通过RabbitMQ消息队列发送  
     */  
    @Configuration  
    public class CanalProvider {  
        /**  
         * 队列  
         */  
        @Bean  
        public Queue canalQueue() {  
            /**  
             * durable:是否持久化,默认false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在;暂存队列:当前连接有效  
             * exclusive:默认为false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable  
             * autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除  
             */  
            return new Queue(RabbitConstant.CanalQueue, true);  
        }  
      
        /**  
         * 交换机,这里使用直连交换机  
         */  
        @Bean  
        DirectExchange canalExchange() {  
            return new DirectExchange(RabbitConstant.CanalExchange, truefalse);  
        }  
      
        /**  
         * 绑定交换机和队列,并设置匹配键  
         */  
        @Bean  
        Binding bindingCanal() {  
            return BindingBuilder.bind(canalQueue()).to(canalExchange()).with(RabbitConstant.CanalRouting);  
        }  
    }  

    Canal消息消费者:

    /**  
     * Canal消息消费者  
     */  
    @Component  
    @RabbitListener(queues = RabbitConstant.CanalQueue)  
    public class CanalComsumer {  
        private final SysBackupService sysBackupService;  
      
        public CanalComsumer(SysBackupService sysBackupService) {  
            this.sysBackupService = sysBackupService;  
        }  
      
        @RabbitHandler  
        public void process(Map msg) {  
            System.out.println("收到canal消息:" + msg);  
            boolean isDdl = (boolean) msg.get("isDdl");  
      
            // 不处理DDL事件  
            if (isDdl) {  
                return;  
            }  
      
            // TiCDC的id,应该具有唯一性,先保存再说  
            int tid = (int) msg.get("id");  
            // TiCDC生成该消息的时间戳,13位毫秒级  
            long ts = (long) msg.get("ts");  
            // 数据库  
            String database = (String) msg.get("database");  
            // 表  
            String table = (String) msg.get("table");  
            // 类型:INSERT/UPDATE/DELETE  
            String type = (String) msg.get("type");  
            // 每一列的数据值  
            List> data = (List>) msg.get("data");  
            // 仅当type为UPDATE时才有值,记录每一列的名字和UPDATE之前的数据值  
            List> old = (List>) msg.get("old");  
      
            // 跳过sys_backup,防止无限循环  
            if ("sys_backup".equalsIgnoreCase(table)) {  
                return;  
            }  
      
            // 只处理指定类型  
            if (!"INSERT".equalsIgnoreCase(type)  
                    && !"UPDATE".equalsIgnoreCase(type)  
                    && !"DELETE".equalsIgnoreCase(type)) {  
                return;  
            }  
        }  
    }  

    测试一下,修改MySQL中的一条消息,Canal就会发送信息到RabbitMQ,我们就能从监听的RabbitMQ队列中得到该条消息。

  • 优雅的接口防刷处理方案

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

    来源:juejin.cn/post/7200366809407750181

    前言

    本文为描述通过Interceptor以及Redis实现接口访问防刷Demo

    这里会通过逐步找问题,逐步去完善的形式展示

    原理

    • 通过ip地址+uri拼接用以作为访问者访问接口区分
    • 通过在Interceptor中拦截请求,从Redis中统计用户访问接口次数从而达到接口防刷目的

    如下图所示

    工程

    项目地址:

    https://github.com/Tonciy/interface-brush-protection

    Apifox地址:Apifox 密码:Lyh3j2Rv

    其中,Interceptor处代码处理逻辑最为重要

    /**
     * @author: Zero
     * @time: 2023/2/14
     * @description: 接口防刷拦截处理
     */

    @Slf4j
    public class AccessLimintInterceptor  implements HandlerInterceptor {
        @Resource
        private RedisTemplate redisTemplate;

        /**
         * 多长时间内
         */

        @Value("${interfaceAccess.second}")
        private Long second = 10L;

        /**
         * 访问次数
         */

        @Value("${interfaceAccess.time}")
        private Long time = 3L;

        /**
         * 禁用时长--单位/秒
         */

        @Value("${interfaceAccess.lockTime}")
        private Long lockTime = 60L;

        /**
         * 锁住时的key前缀
         */

        public static final String LOCK_PREFIX = "LOCK";

        /**
         * 统计次数时的key前缀
         */

        public static final String COUNT_PREFIX = "COUNT";


        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

            String uri = request.getRequestURI();
            String ip = request.getRemoteAddr(); // 这里忽略代理软件方式访问,默认直接访问,也就是获取得到的就是访问者真实ip地址
            String lockKey = LOCK_PREFIX + ip + uri;
            Object isLock = redisTemplate.opsForValue().get(lockKey);
            if(Objects.isNull(isLock)){
                // 还未被禁用
                String countKey = COUNT_PREFIX + ip + uri;
                Object count = redisTemplate.opsForValue().get(countKey);
                if(Objects.isNull(count)){
                    // 首次访问
                    log.info("首次访问");
                    redisTemplate.opsForValue().set(countKey,1,second, TimeUnit.SECONDS);
                }else{
                    // 此用户前一点时间就访问过该接口
                    if((Integer)count                     // 放行,访问次数 + 1
                        redisTemplate.opsForValue().increment(countKey);
                    }else{
                        log.info("{}禁用访问{}",ip, uri);
                        // 禁用
                        redisTemplate.opsForValue().set(lockKey, 1,lockTime, TimeUnit.SECONDS);
                        // 删除统计
                        redisTemplate.delete(countKey);
                        throw new CommonException(ResultCode.ACCESS_FREQUENT);
                    }
                }
            }else{
                // 此用户访问此接口已被禁用
                throw new CommonException(ResultCode.ACCESS_FREQUENT);
            }
            return true;
        }
    }

    在多长时间内访问接口多少次,以及禁用的时长,则是通过与配置文件配合动态设置

    当处于禁用时直接抛异常则是通过在ControllerAdvice处统一处理 (这里代码写的有点丑陋)

    下面是一些测试(可以把项目通过Git还原到“【初始化】”状态进行测试)

    • 正常访问时

    • 访问次数过于频繁时

    自我提问

    上述实现就好像就已经达到了我们的接口防刷目的了

    但是,还不够

    为方便后续描述,项目中新增补充Controller,如下所示

    简单来说就是

    • PassCotrollerRefuseController
    • 每个Controller分别有对应的get,post,put,delete类型的方法,其映射路径与方法名称一致

    接口自由

    • 对于上述实现,不知道你们有没有发现一个问题
    • 就是现在我们的接口防刷处理,针对是所有的接口(项目案例中我只是写的接口比较少)
    • 而在实际开发中,说对于所有的接口都要做防刷处理,感觉上也不太可能(写此文时目前大四,实际工作经验较少,这里不敢肯定)
    • 那么问题有了,该如何解决呢?目前来说想到两个解决方案
    拦截器映射规则

    项目通过Git还原到”【Interceptor设置映射规则实现接口自由】”版本即可得到此案例实现

    我们都知道拦截器是可以设置拦截规则的,从而达到拦截处理目的

    1.这个AccessInterfaceInterceptor是专门用来进行防刷处理的,那么实际上我们可以通过设置它的映射规则去匹配需要进行【接口防刷】的接口即可

    2.比如说下面的映射配置

    3.这样就初步达到了我们的目的,通过映射规则的配置,只针对那些需要进行【接口防刷】的接口才会进行处理

    4.至于为啥说是初步呢?下面我就说说目前我想到的使用这种方式进行【接口防刷】的不足点:

    所有要进行防刷处理的接口统一都是配置成了 x 秒内 y 次访问次数,禁用时长为 z 秒

    • 要知道就是要进行防刷处理的接口,其 x, y, z的值也是并不一定会统一的
    • 某些防刷接口处理比较消耗性能的,我就把x, y, z设置的紧一点
    • 而某些防刷接口处理相对来说比较快,我就把x, y, z 设置的松一点
    • 这没问题吧
    • 但是现在呢?x, y, z值全都一致了,这就不行了
    • 这就是其中一个不足点
    • 当然,其实针对当前这种情况也有解决方案
    • 那就是弄多个拦截器
    • 每个拦截器的【接口防刷】处理逻辑跟上述一致,并去映射对应要处理的防刷接口
    • 唯一不同的就是在每个拦截器内部,去修改对应防刷接口需要的x, y, z值
    • 这样就是感觉会比较麻烦

    防刷接口映射路径修改后维护问题

    • 虽然说防刷接口的映射路径基本上定下来后就不会改变
    • 但实际上前后端联调开发项目时,不会有那么严谨的Api文档给我们用(这个在实习中倒是碰到过,公司不是很大,开发起来也就不那么严谨,啥都要自己搞,功能能实现就好)
    • 也就是说还是会有那种要修改接口的映射路径需求
    • 当防刷接口数量特别多,后面的接手人员就很痛苦了
    • 就算是项目是自己从0到1实现的,其实有时候项目开发到后面,自己也会忘记自己前面是如何设计的
    • 而使用当前这种方式的话,谁维护谁蛋疼
    自定义注解 + 反射

    咋说呢

    • 就是通过自定义注解中定义 x 秒内 y 次访问次数,禁用时长为 z 秒
    • 自定义注解 + 在需要进行防刷处理的各个接口方法上
    • 在拦截器中通过反射获取到各个接口中的x, y, z值即可达到我们想要的接口自由目的

    下面做个实现

    声明自定义注解

    Controlller中方法中使用

    Interceptor处逻辑修改(最重要是通过反射判断此接口是否需要进行防刷处理,以及获取到x, y, z的值)

    /**
     * @author: Zero
     * @time: 2023/2/14
     * @description: 接口防刷拦截处理
     */

    @Slf4j
    public class AccessLimintInterceptor  implements HandlerInterceptor {
        @Resource
        private RedisTemplate redisTemplate;
        /**
         * 锁住时的key前缀
         */

        public static final String LOCK_PREFIX = "LOCK";

        /**
         * 统计次数时的key前缀
         */

        public static final String COUNT_PREFIX = "COUNT";


        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //        自定义注解 + 反射 实现
            // 判断访问的是否是接口方法
            if(handler instanceof HandlerMethod){
                // 访问的是接口方法,转化为待访问的目标方法对象
                HandlerMethod targetMethod = (HandlerMethod) handler;
                // 取出目标方法中的 AccessLimit 注解
                AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
                // 判断此方法接口是否要进行防刷处理(方法上没有对应注解就代表不需要,不需要的话进行放行)
                if(!Objects.isNull(accessLimit)){
                    // 需要进行防刷处理,接下来是处理逻辑
                    String ip = request.getRemoteAddr();
                    String uri = request.getRequestURI();
                    String lockKey = LOCK_PREFIX + ip + uri;
                    Object isLock = redisTemplate.opsForValue().get(lockKey);
                    // 判断此ip用户访问此接口是否已经被禁用
                    if (Objects.isNull(isLock)) {
                        // 还未被禁用
                        String countKey = COUNT_PREFIX + ip + uri;
                        Object count = redisTemplate.opsForValue().get(countKey);
                        long second = accessLimit.second();
                        long maxTime = accessLimit.maxTime();

                        if (Objects.isNull(count)) {
                            // 首次访问
                            log.info("首次访问");
                            redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
                        } else {
                            // 此用户前一点时间就访问过该接口,且频率没超过设置
                            if ((Integer) count                             redisTemplate.opsForValue().increment(countKey);
                            } else {

                                log.info("{}禁用访问{}", ip, uri);
                                long forbiddenTime = accessLimit.forbiddenTime();
                                // 禁用
                                redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                                // 删除统计--已经禁用了就没必要存在了
                                redisTemplate.delete(countKey);
                                throw new CommonException(ResultCode.ACCESS_FREQUENT);
                            }
                        }
                    } else {
                        // 此用户访问此接口已被禁用
                        throw new CommonException(ResultCode.ACCESS_FREQUENT);
                    }
                }
            }
            return  true;
        }
    }

    由于不好演示效果,这里就不贴测试结果图片了

    项目通过Git还原到”【自定义主键+反射实现接口自由”版本即可得到此案例实现,后面自己可以针对接口做下测试看看是否如同我所说的那样实现自定义x, y, z 的效果

    嗯,现在看起来,可以针对每个要进行防刷处理的接口进行针对性自定义多长时间内的最大访问次数,以及禁用时长,哪个接口需要,就直接+在那个接口方法出即可

    感觉还不错的样子,现在网上挺多资料也都是这样实现的

    但是还是可以有改善的地方

    先举一个例子,以我们的PassController为例,如下是其实现

    下图是其映射路径关系

    同一个Controller的所有接口方法映射路径的前缀都包含了/pass

    我们在类上通过注解@ReqeustMapping标记映射路径/pass,这样所有的接口方法前缀都包含了/pass,并且以致于后面要修改映射路径前缀时只需改这一块地方即可

    这也是我们使用SpringMVC最常见的用法

    那么,我们的自定义注解也可不可以这样做呢?先无中生有个需求

    假设PassController中所有接口都是要进行防刷处理的,并且他们的x, y, z值就一样

    如果我们的自定义注解还是只能加载方法上的话,一个一个接口加,那么无疑这是一种很呆的做法

    要改的话,其实也很简单,首先是修改自定义注解,让其可以作用在类上

    接着就是修改AccessLimitInterceptor的处理逻辑

    AccessLimitInterceptor中代码修改的有点多,主要逻辑如下

    与之前实现比较,不同点在于x, y, z的值要首先尝试在目标类中获取

    其次,一旦类中标有此注解,即代表此类下所有接口方法都要进行防刷处理

    如果其接口方法同样也标有此注解,根据就近优先原则,以接口方法中的注解标明的值为准

    /**
     * @author: Zero
     * @time: 2023/2/14
     * @description: 接口防刷拦截处理
     */

    @Slf4j
    public class AccessLimintInterceptor implements HandlerInterceptor {
        @Resource
        private RedisTemplate redisTemplate;

        /**
         * 锁住时的key前缀
         */

        public static final String LOCK_PREFIX = "LOCK";

        /**
         * 统计次数时的key前缀
         */

        public static final String COUNT_PREFIX = "COUNT";


        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    //      自定义注解 + 反射 实现, 版本 2.0
            if (handler instanceof HandlerMethod) {
                // 访问的是接口方法,转化为待访问的目标方法对象
                HandlerMethod targetMethod = (HandlerMethod) handler;
                // 获取目标接口方法所在类的注解@AccessLimit
                AccessLimit targetClassAnnotation = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);
                // 特别注意不能采用下面这条语句来获取,因为 Spring 采用的代理方式来代理目标方法
                //  也就是说targetMethod.getClass()获得是class org.springframework.web.method.HandlerMethod ,而不知我们真正想要的 Controller
    //            AccessLimit targetClassAnnotation = targetMethod.getClass().getAnnotation(AccessLimit.class);
                // 定义标记位,标记此类是否加了@AccessLimit注解
                boolean isBrushForAllInterface = false;
                String ip = request.getRemoteAddr();
                String uri = request.getRequestURI();
                long second = 0L;
                long maxTime = 0L;
                long forbiddenTime = 0L;
                if (!Objects.isNull(targetClassAnnotation)) {
                    log.info("目标接口方法所在类上有@AccessLimit注解");
                    isBrushForAllInterface = true;
                    second = targetClassAnnotation.second();
                    maxTime = targetClassAnnotation.maxTime();
                    forbiddenTime = targetClassAnnotation.forbiddenTime();
                }
                // 取出目标方法中的 AccessLimit 注解
                AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
                // 判断此方法接口是否要进行防刷处理
                if (!Objects.isNull(accessLimit)) {
                    // 需要进行防刷处理,接下来是处理逻辑
                    second = accessLimit.second();
                    maxTime = accessLimit.maxTime();
                    forbiddenTime = accessLimit.forbiddenTime();
                    if (isForbindden(second, maxTime, forbiddenTime, ip, uri)) {
                        throw new CommonException(ResultCode.ACCESS_FREQUENT);
                    }
                } else {
                    // 目标接口方法处无@AccessLimit注解,但还要看看其类上是否加了(类上有加,代表针对此类下所有接口方法都要进行防刷处理)
                    if (isBrushForAllInterface && isForbindden(second, maxTime, forbiddenTime, ip, uri)) {
                        throw new CommonException(ResultCode.ACCESS_FREQUENT);
                    }
                }
            }
            return true;
        }

        /**
         * 判断某用户访问某接口是否已经被禁用/是否需要禁用
         *
         * @param second        多长时间  单位/秒
         * @param maxTime       最大访问次数
         * @param forbiddenTime 禁用时长 单位/秒
         * @param ip            访问者ip地址
         * @param uri           访问的uri
         * @return ture为需要禁用
         */

        private boolean isForbindden(long second, long maxTime, long forbiddenTime, String ip, String uri) {
            String lockKey = LOCK_PREFIX + ip + uri; //如果此ip访问此uri被禁用时的存在Redis中的 key
            Object isLock = redisTemplate.opsForValue().get(lockKey);
            // 判断此ip用户访问此接口是否已经被禁用
            if (Objects.isNull(isLock)) {
                // 还未被禁用
                String countKey = COUNT_PREFIX + ip + uri;
                Object count = redisTemplate.opsForValue().get(countKey);
                if (Objects.isNull(count)) {
                    // 首次访问
                    log.info("首次访问");
                    redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
                } else {
                    // 此用户前一点时间就访问过该接口,且频率没超过设置
                    if ((Integer) count                     redisTemplate.opsForValue().increment(countKey);
                    } else {
                        log.info("{}禁用访问{}", ip, uri);
                        // 禁用
                        redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                        // 删除统计--已经禁用了就没必要存在了
                        redisTemplate.delete(countKey);
                        return true;
                    }
                }
            } else {
                // 此用户访问此接口已被禁用
                return true;
            }
            return false;
        }
    }

    好了,这样就达到我们想要的效果了

    项目通过Git还原到”【自定义注解+反射实现接口自由-版本2.0】”版本即可得到此案例实现,自己可以测试万一下

    这是目前来说比较理想的做法,至于其他做法,暂时没啥了解到

    时间逻辑漏洞

    这是我一开始都有留意到的问题

    也是一直搞不懂,就是我们现在的所有做法其实感觉都不是严格意义上的x秒内y次访问次数

    特别注意这个x秒,它是连续,任意的(代表这个x秒时间片段其实是可以发生在任意一个时间轴上)

    我下面尝试表达我的意思,但是我不知道能不能表达清楚

    假设我们固定某个接口5秒内只能访问3次,以下面例子为例

    底下的小圆圈代表此刻请求访问接口

    按照我们之前所有做法的逻辑走

    1. 第2秒请求到,为首次访问,Redis中统计次数为1(过期时间为5秒)
    2. 第7秒,此时有两个动作,一是请求到,二是刚刚第二秒Redis存的值现在过期
    3. 我们先假设这一刻,请求处理完后,Redis存的值才过期
    4. 按照这样的逻辑走
    5. 第七秒请求到,Redis存在对应key,且不大于3, 次数+1
    6. 接着这个key立马过期
    7. 再继续往后走,第8秒又当做新的一个起始,就不往下说了,反正就是不会出现禁用的情况

    按照上述逻辑走,实际上也就是说当出现首次访问时,当做这5秒时间片段的起始

    第2秒是,第8秒也是

    但是有没有想过,实际上这个5秒时间片段实际上是可以放置在时间轴上任意区域的

    上述情况我们是根据请求的到来情况人为的把它放在【2-7】,【8-13】上

    而实际上这5秒时间片段是可以放在任意区域的

    那么,这样的话,【7-12】也可以放置

    而【7-12】这段时间有4次请求,就达到了我们禁用的条件了

    是不是感觉怪怪的

    想过其他做法,但是好像严格意义上真的做不到我所说的那样(至少目前来说想不到)

    之前我们的做法,正常来说也够用,至少说有达到防刷的作用

    后面有机会的话再看看,不知道我是不是钻牛角尖了

    路径参数问题

    假设现在PassController中有如下接口方法

    也就是我们在接口方法中常用的在请求路径中获取参数的套路

    但是使用路径参数的话,就会发生问题

    那就是同一个ip地址访问此接口时,我携带的参数值不同

    按照我们之前那种前缀+ip+uri拼接的形式作为key的话,其实是区分不了的

    下图是访问此接口,携带不同参数值时获取的uri状况

    这样的话在我们之前拦截器的处理逻辑中,会认为是此ip用户访问的是不同的接口方法,而实际上访问的是同一个接口方法

    也就导致了【接口防刷】失效

    接下来就是解决它,目前来说有两种

    1. 不要使用路径参数

    这算是比较理想的做法,相当于没这个问题

    但有一定局限性,有时候接手别的项目,或者自己根本没这个权限说不能使用路径参数

    1. 替换uri
    • 我们获取uri的目的,其实就是为了区别访问接口
    • 而把uri替换成另一种可以区分访问接口方法的标识即可
    • 最容易想到的就是通过反射获取到接口方法名称,使用接口方法名称替换成uri即可
    • 当然,其实不同的Controller中,其接口方法名称也有可能是相同的
    • 实际上可以再获取接口方法所在类类名,使用类名 + 方法名称替换uri即可
    • 实际解决方案有很多,看个人需求吧

    真实ip获取

    在之前的代码中,我们获取代码都是通过request.getRemoteAddr()获取的

    但是后续有了解到,如果说通过代理软件方式访问的话,这样是获取不到来访者的真实ip的

    至于如何获取,后续我再研究下http再说,这里先提个醒

    总结

    说实话,挺有意思的,一开始自己想【接口防刷】的时候,感觉也就是转化成统计下访问次数的问题摆了。后面到网上看别人的写法,又再自己给自己找点问题出来,后面会衍生出来一推东西出来,诸如自定义注解+反射这种实现方式。

    以前其实对注解 + 反射其实有点不太懂干嘛用的,而从之前的数据报表导出,再到基本权限控制实现,最后到今天的【接口防刷】一点点来进步去补充自己的知识点,而且,感觉写博客真的是件挺有意义的事情,它会让你去更深入的了解某个点,并且知识是相关联的,探索的过程中会牵扯到其他别的知识点,就像之前的写的【单例模式】实现,一开始就了解到懒汉式,饿汉式

    后面深入的话就知道其实会还有序列化/反序列化,反射调用生成实例,对象克隆这几种方式回去破坏单例模式,又是如何解决的,这也是一个进步的点,后续为了保证线程安全问题,牵扯到的synchronized,voliate关键字,继而又关联到JVM,JUC,操作系统的东西。

  • Java8 Lambda 表达式中的 forEach 如何提前终止?

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

    # 情景展示

    图片

    如上图所示,我们想要终止for循环,使用return。

    执行结果如下:

    图片

    我们可以看到,只有赵六没被打印出来,后续的数组元素依旧被执行了。

    也就是说,关键字”return”,在这里执行的效果相当于普通for循环里的关键词continue”。

    # 原因分析

    我们知道,在普通for循环里面,想要提前结束(终止)循环体使用”break”;

    结束本轮循环,进行下一轮循环使用”continue”;

    另外,在普通for里,如果使用”return”,不仅强制结束for循环体,还会提前结束包含这个循环体的整个方法。

    而在Java8中的forEach()中,”break”或”continue”是不被允许使用的,而return的意思也不是原来return代表的含义了。

    我们来看看源码:

    图片

    forEach(),说到底是一个方法,而不是循环体,结束一个方法的执行用什么?当然是return啦;

    java8的forEach()和JavaScript的forEach()用法是何其的相似

    Java不是万能的,不要再吐槽它垃圾了。

    # 解决方案

    方案一:使用原始的foreach循环

    图片

    使用过eclipse的老铁们应该知道,当我们输入:foreach,再按快捷键:Alt+/,就会出现foreach的代码提示。

    如上图所示,这种格式的for循环才是真正意义上的foreach循环。

    在idea中输入,按照上述操作是不会有任何代码提示的,那如何才能在idea中,调出来呢?

    图片

    for循环可以提前终止。

    方式一:break

    图片

    方式二:return(不推荐使用)

    图片

    方案二:抛出异常

    我们知道,要想结束一个方法的执行,正常的逻辑是:使用return;

    但是,在实际运行中,往往有很多不突发情况导致代码提前终止,比如:空指针异常,其实,我们也可以通过抛出假异常的方式来达到终止forEach()方法的目的。

    图片

    如果觉得这种方式不友好,可以再包装一层。

    图片

    这样,就完美了。

    这里,需要注意的一点是:要确保你forEach()方法体内不能有其它代码可能会抛出的异常与自己手动抛出并捕获的异常一样;

    否则,当真正该因异常导致代码终止的时候,因为咱们手动捕获了并且没做任何处理,岂不是搬起石头砸自己的脚吗?

    来源 | https://blog.csdn.net/weixin_39597399/article/details/114232746

    
    

  • Spring Cloud 的25连环炮!

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

    来源:Java后端面试官

    • 前言
    • Spring Cloud核心知识总结
    • 连环炮走起
    • 总结

    前言

    上周,一位朋友在面试被问到了Spring Cloud,然后结合他的反馈,今天我们继续走起SpringCloud面试连环炮。

    Spring Cloud核心知识总结

    下面是一张Spring Cloud核心组件关系图:

    图片

    从这张图中,其实我们是可以获取很多信息的,希望大家细细品尝。

    话不多说,我们直接开始 Spring Cloud 连环炮。

    连环炮走起

    1、什么是Spring Cloud ?

    Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。

    2、什么是微服务?

    微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。

    通俗地来讲:

    微服务就是一个独立的职责单一的服务应用程序。在 intellij idea 工具里面就是用maven开发的一个个独立的module,具体就是使用springboot 开发的一个小的模块,处理单一专业的业务逻辑,一个模块只做一个事情。

    微服务强调的是服务大小,关注的是某一个点,具体解决某一个问题/落地对应的一个服务应用,可以看做是idea 里面一个 module。

    3、Spring Cloud有什么优势

    使用 Spring Boot 开发分布式微服务时,我们面临以下问题

    • 与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。
    • 服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。
    • 冗余-分布式系统中的冗余问题。
    • 负载平衡 –负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。
    • 性能-问题 由于各种运营开销导致的性能问题。
    • 部署复杂性-Devops 技能的要求。

    4、微服务之间如何独立通讯的?

    同步通信:dobbo通过 RPC 远程过程调用、springcloud通过 REST  接口json调用等。

    异步:消息队列,如:RabbitMqActiveMKafka等消息队列。

    5、什么是服务熔断?什么是服务降级?

    熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在Spring Cloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。

    服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。

    Hystrix相关注解@EnableHystrix:开启熔断 @HystrixCommand(fallbackMethod=”XXX”),声明一个失败回滚处理函数XXX,当被注解的方法执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。

    6、请说说Eureka和zookeeper 的区别?

    Zookeeper保证了CP,Eureka保证了AP。

    A:高可用

    C:一致性

    P:分区容错性

    1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。

    2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:

    ①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。

    ②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)

    ③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。

    因此,Eureka可以很好地应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪

    7、SpringBoot和SpringCloud的区别?

    SpringBoot专注于快速方便得开发单个个体微服务。

    SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,

    为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务

    SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系.

    SpringBoot专注于快速、方便得开发单个微服务个体,SpringCloud关注全局的服务治理框架。

    8、负载平衡的意义什么?

    在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源 的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。

    9、什么是Hystrix?它如何实现容错?

    Hystrix是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。

    通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作。

    思考一下微服务:

    图片

    假设如果上图中的微服务9失败了,那么使用传统方法我们将传播一个异常。但这仍然会导致整个系统崩溃。

    随着微服务数量的增加,这个问题变得更加复杂。微服务的数量可以高达1000.这是hystrix出现的地方 我们将使用Hystrix在这种情况下的Fallback方法功能。我们有两个服务employee-consumer使用由employee-consumer公开的服务。

    简化图如下所示

    图片

    现在假设由于某种原因,employee-producer公开的服务会抛出异常。我们在这种情况下使用Hystrix定义了一个回退方法。这种后备方法应该具有与公开服务相同的返回类型。如果暴露服务中出现异常,则回退方法将返回一些值。

    10、什么是Hystrix断路器?我们需要它吗?

    由于某些原因,employee-consumer公开服务会引发异常。在这种情况下使用Hystrix我们定义了一个回退方法。如果在公开服务中发生异常,则回退方法返回一些默认值。

    图片

    如果firstPage method() 中的异常继续发生,则Hystrix电路将中断,并且员工使用者将一起跳过firtsPage方法,并直接调用回退方法。断路器的目的是给第一页方法或第一页方法可能调用的其他方法留出时间,并导致异常恢复。可能发生的情况是,在负载较小的情况下,导致异常的问题有更好的恢复机会 。

    图片

    11、说说 RPC 的实现原理

    首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编 解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列 化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服 务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果 返回。

    12、eureka自我保护机制是什么?

    当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。

    13、什么是Ribbon?

    ribbon是一个负载均衡客户端,可以很好地控制htt和tcp的一些行为。feign默认集成了ribbon

    14、什么是 Netflix Feign?它的优点是什么?

    Feign 是受到 Retrofit,JAXRS-2.0 和 WebSocket 启发的 java 客户端联编程序。

    Feign 的第一个目标是将约束分母的复杂性统一到 http apis,而不考虑其稳定性。

    特点:

    • Feign 采用的是基于接口的注解
    • Feign 整合了ribbon,具有负载均衡的能力
    • 整合了Hystrix,具有熔断的能力

    使用方式

    • 添加pom依赖。
    • 启动类添加@EnableFeignClients
    • 定义一个接口@FeignClient(name=“xxx”)指定调用哪个服务

    15、Ribbon和Feign的区别?

    1.Ribbon都是调用其他服务的,但方式不同。2.启动类注解不同,Ribbon是@RibbonClient feign的是@EnableFeignClients 3.服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。4.调用方式不同,Ribbon需要自己构建http请求,模拟http请求。

    16、Spring Cloud 的核心组件有哪些?

    • Eureka:服务注册于发现。
    • Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。
    • Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。
    • Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。
    • Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。

    17、说说Spring Boot和Spring Cloud的关系

    Spring Boot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务 而Spring Cloud专注于解决各个微服务之间的协调与配置,服务之间的通信,熔断,负载均衡等 技术维度并相同,并且Spring Cloud是依赖于Spring Boot的,而Spring Boot并不是依赖与Spring Cloud,甚至还可以和Dubbo进行优秀的整合开发

    总结

    • SpringBoot专注于快速方便的开发单个个体的微服务
    • SpringCloud是关注全局的微服务协调整理治理框架,整合并管理各个微服务,为各个微服务之间提供,配置管理,服务发现,断路器,路由,事件总线等集成服务
    • Spring Boot不依赖于Spring Cloud,Spring Cloud依赖于Spring Boot,属于依赖关系
    • Spring Boot专注于快速,方便的开发单个的微服务个体,Spring Cloud关注全局的服务治理框架

    18、说说微服务之间是如何独立通讯的?

    远程过程调用(Remote Procedure Invocation)

    也就是我们常说的服务的注册与发现,直接通过远程过程调用来访问别的service。

    优点 :简单,常见,因为没有中间件代理,系统更简单

    缺点 :只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应,降低了可用性,因为客户端和服务端在请求过程中必须都是可用的。

    消息

    使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。

    优点 :把客户端和服务端解耦,更松耦合,提高可用性,因为消息中间件缓存了消息,直到消费者可以消费,   支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应。

    缺点 :消息中间件有额外的复杂。

    19、Spring Cloud如何实现服务的注册?

    服务发布时,指定对应的服务名,将服务注册到 注册中心(Eureka 、Zookeeper)

    注册中心加@EnableEurekaServer,服务用@EnableDiscoveryClient,然后用ribbon或feign进行服务直接的调用发现。

    此题偏向于向实战,就看你是不是背面试题的,没有实战的人是不知道的。

    20、什么是服务熔断?

    在复杂的分布式系统中,微服务之间的相互调用,有可能出现各种各样的原因导致服务的阻塞,在高并发场景下,服务的阻塞意味着线程的阻塞,导致当前线程不可用,服务器的线程全部阻塞,导致服务器崩溃,由于服务之间的调用关系是同步的,会对整个微服务系统造成服务雪崩

    为了解决某个微服务的调用响应时间过长或者不可用进而占用越来越多的系统资源引起雪崩效应就需要进行服务熔断和服务降级处理。

    所谓的服务熔断指的是某个服务故障或异常一起类似显示世界中的“保险丝”当某个异常条件被触发就直接熔断整个服务,而不是一直等到此服务超时。

    服务熔断就是相当于我们电闸的保险丝,一旦发生服务雪崩的,就会熔断整个服务,通过维护一个自己的线程池,当线程达到阈值的时候就启动服务降级,如果其他请求继续访问就直接返回fallback的默认值

    21、了解Eureka自我保护机制吗?

    当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。

    22、熟悉 Spring Cloud Bus 吗?

    spring cloud bus 将分布式的节点用轻量的消息代理连接起来,它可以用于广播配置文件的更改或者服务直接的通讯,也可用于监控。如果修改了配置文件,发送一次请求,所有的客户端便会重新读取配置文件。

    23、Spring Cloud 断路器有什么作用?

    当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应,当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)。一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象,这时候断路器完全打开 那么下次请求就不会请求到该服务。

    半开:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭。关闭:当服务一直处于正常状态 能正常调用。

    24、了解Spring Cloud Config 吗?

    在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件Spring Cloud Config,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。

    Spring Cloud Config 组件中,分两个角色,一是config server,二是config client。

    使用方式:

    • 添加pom依赖
    • 配置文件添加相关配置
    • 启动类添加注解@EnableConfigServer

    25、说说你对Spring Cloud Gateway的理解

    Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。

    使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。

    参考;http://1pgqu.cn/M0NZo

    总结

    Spring Cloud目前相当的火热,也差不多是java开发者必备技能之一了。面试的时候被问,那也是正常不过了,很多人可能用来很久,但是没有去了解过原理,面试照样挂掉。背面试题,在很大层面上还是很有用的。但从长远角度来说,希望大家更深层次去学习、去实践。只有自己真的掌握,那才叫NB。

    
    

  • 12种接口优化的通用方案,我又偷偷学到一波~

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

    一、背景

    针对老项目,去年做了许多降本增效的事情,其中发现最多的就是接口耗时过长的问题,就集中搞了一次接口性能优化。本文将给小伙伴们分享一下接口优化的通用方案。

    图片

    二、接口优化方案总结

    1.批处理

    批量思想:批量操作数据库,这个很好理解,我们在循环插入场景的接口中,可以在批处理执行完成后一次性插入或更新数据库,避免多次IO。

    //批量入库
    batchInsert();

    2.异步处理

    异步思想:针对耗时比较长且不是结果必须的逻辑,我们可以考虑放到异步执行,这样能降低接口耗时。

    例如一个理财的申购接口,入账和写入申购文件是同步执行的,因为是T+1交易,后面这两个逻辑其实不是结果必须的,我们并不需要关注它的实时结果,所以我们考虑把入账和写入申购文件改为异步处理。如图所示:

    图片

    至于异步的实现方式,可以用线程池,也可以用消息队列,还可以用一些调度任务框架。

    3.空间换时间

    一个很好理解的空间换时间的例子是合理使用缓存,针对一些频繁使用且不频繁变更的数据,可以提前缓存起来,需要时直接查缓存,避免频繁地查询数据库或者重复计算。

    需要注意的事,这里用了合理二字,因为空间换时间也是一把双刃剑,需要综合考虑你的使用场景,毕竟缓存带来的数据一致性问题也挺令人头疼。

    这里的缓存可以是R2M,也可以是本地缓存、memcached,或者Map。

    举一个股票工具的查询例子:

    因为策略轮动的调仓信息,每周只更新一次,所以原来的调接口就去查库的逻辑并不合理,而且拿到调仓信息后,需要经过复杂计算,最终得出回测收益和跑赢沪深指数这些我们想要的结果。如果我们把查库操作和计算结果放入缓存,可以节省很多的执行时间。如图:

    图片

    4.预处理

    也就是预取思想,就是提前要把查询的数据,提前计算好,放入缓存或者表中的某个字段,用的时候会大幅提高接口性能。跟上面那个例子很像,但是关注点不同。

    举个简单的例子:理财产品,会有根据净值计算年化收益率的数据展示需求,利用净值去套用年化收益率计算公式计算的逻辑我们可以采用预处理,这样每一次接口调用直接取对应字段就可以了。

    5.池化思想

    我们都用过数据库连接池,线程池等,这就是池思想的体现,它们解决的问题就是避免重复创建对象或创建连接,可以重复利用,避免不必要的损耗,毕竟创建销毁也会占用时间。

    池化思想包含但并不局限于以上两种,总的来说池化思想的本质是预分配与循环使用,明白这个原理后,我们即使是在做一些业务场景的需求时,也可以利用起来。

    比如:对象池

    6.串行改并行

    串行就是,当前执行逻辑必须等上一个执行逻辑结束之后才执行,并行就是两个执行逻辑互不干扰,所以并行相对来说就比较节省时间,当然是建立在没有结果参数依赖的前提下。

    比如,理财的持仓信息展示接口,我们既需要查询用户的账户信息,也需要查询商品信息和banner位信息等等来渲染持仓页,如果是串行,基本上接口耗时就是累加的。如果是并行,接口耗时将大大降低。

    如图:

    图片

    7.索引

    加索引能大大提高数据查询效率,这个在接口设计之出也会考虑到,这里不再多赘述,随着需求的迭代,我们重点整理一下索引不生效的一些场景,希望对小伙伴们有所帮助。

    具体不生效场景不再一一举例,后面有时间的话,单独整理一下。

    图片

    8.避免大事务

    所谓大事务问题,就是运行时间较长的事务,由于事务一致不提交,会导致数据库连接被占用,影响到别的请求访问数据库,影响别的接口性能。

    举个例子:

    @Transactional(value = "taskTransactionManager", propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = {RuntimeException.class, Exception.class})
     public BasicResult purchaseRequest(PurchaseRecord record) {
         BasicResult result = new BasicResult();
         ...
         pushRpc.doPush(record);        
         result.setInfo(ResultInfoEnum.SUCCESS);
         return result;
     }

    所以为避免大事务问题,我们可以通过以下方案规避:

    1,RPC调用不放到事务里面

    2,查询操作尽量放到事务之外

    3,事务中避免处理太多数据

    9.优化程序结构

    程序结构问题一般出现在多次需求迭代后,代码叠加形成。会造成一些重复查询、多次创建对象等耗时问题。在多人维护一个项目时比较多见。解决起来也比较简单,我们需要针对接口整体做重构,评估每个代码块的作用和用途,调整执行顺序。

    10.深分页问题

    深分页问题比较常见,分页我们一般最先想到的就是 limit ,为什么会慢,我们可以看下这个SQL:

    select * from purchase_record where productCode = 'PA9044' and status=4 and id > 100000 limit 200

    这样优化的好处是命中了主键索引,无论多少页,性能都还不错,但是局限性是需要一个连续自增的字段

    11.SQL优化

    sql优化能大幅提高接口的查询性能,由于本文重点讲述接口优化的方案,具体sql优化不再一一列举,小伙伴们可以结合索引、分页、等关注点考虑优化方案。

    12.锁粒度避免过粗

    锁一般是为了在高并发场景下保护共享资源采用的一种手段,但是如果锁的粒度太粗,会很影响接口性能。

    关于锁粒度:就是你要锁的范围有多大,不管是synchronized还是redis分布式锁,只需要在临界资源处加锁即可,不涉及共享资源的,不必要加锁,就好比你要上卫生间,只需要把卫生间的门锁上就可以,不需要把客厅的门也锁上。

    错误的加锁方式:

    //非共享资源
    private void notShare(){
    }
    //共享资源
    private void share(){
    }
    private int right(){
        notShare();
        synchronized (this) {
            share();

        }
    }

    三、最后

    接口性能问题形成的原因思考

    我相信很多接口的效率问题不是一朝一夕形成的,在需求迭代的过程中,为了需求快速上线,采取直接累加代码的方式去实现功能,这样会造成以上这些接口性能问题。

    变换思路,更高一级思考问题,站在接口设计者的角度去开发需求,会避免很多这样的问题,也是降本增效的一种行之有效的方式。

    以上,共勉!

    作者:京东开发者

    来源:https://toutiao.io/posts/0kwkbbt

  • 帅呆!接口开发不用写Controller、Service、Dao、Mapper、XML、VO,全自动生成

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

    今天给小伙伴们介绍一个Java接口快速开发框架-magic-api

    简介

    magic-api 是一个基于 Java 的接口快速开发框架,编写接口将通过 magic-api 提供的 UI 界面完成,自动映射为 HTTP 接口,无需定义 Controller、Service、Dao、Mapper、XML、VO 等 Java 对象即可完成常见的 HTTP API 接口开发

    访问 http://localhost:9999/magic/web 进行操作

    文档地址:

    • https://ssssssss.org

    在线演示:

    • https://magic-api.ssssssss.org

    开源地址:

    • https://gitee.com/ssssssss-team/magic-api

    特性

    • 支持MySQL、MariaDB、Oracle、DB2、PostgreSQL、SQLServer 等支持jdbc规范的数据库
    • 支持非关系型数据库Redis、Mongodb
    • 支持集群部署、接口自动同步。
    • 支持分页查询以及自定义分页查询
    • 支持多数据源配置,支持在线配置数据源
    • 支持SQL缓存,以及自定义SQL缓存
    • 支持自定义JSON结果、自定义分页结果
    • 支持对接口权限配置、拦截器等功能
    • 支持运行时动态修改数据源
    • 支持Swagger接口文档生成
    • 基于magic-script脚本引擎,动态编译,无需重启,实时发布
    • 支持Linq式查询,关联、转换更简单
    • 支持数据库事务、SQL支持拼接,占位符,判断等语法
    • 支持文件上传、下载、输出图片
    • 支持脚本历史版本对比与恢复
    • 支持脚本代码自动提示、参数提示、悬浮提示、错误提示
    • 支持导入Spring中的Bean、Java中的类
    • 支持在线调试
    • 支持自定义工具类、自定义模块包、自定义类型扩展、自定义方言、自定义列名转换等自定义操作

    快速开始

    maven引入


     org.ssssssss
        magic-api-spring-boot-starter
        1.7.1

    修改application.properties
    server.port=9999
    #配置web页面入口
    magic-api.web=/magic/web
    #配置文件存储位置。当以classpath开头时,为只读模式
    magic-api.resource.location=/data/magic-api
    项目截图

    整体截图

    代码提示

    DEBUG

    参数提示

    远程推送

    历史记录

    数据源

    全局搜索