作者: chenyl

  • 首次力压 macOS!这次 Linux 杀疯了!!

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

    文章来:【公众号:量子位】


    2022年是Linux桌面版之年。

    一位来自亚马逊K8s团队的程序员在自己最新的博客上这样写道。

    何出此言?

    原来是根据Stack Overflow 2022年开发者调查结果得出。

    该报告显示,2022年将Linux作为主要操作系统的比例已经达到了40.23%,不仅超过了macOS,还将差距拉到了9%

    要知道,去年这俩还基本持平,差距仅为0.13%。

    而且,这还不算15%的用户选择WSL的情况,即在Windows系统上运行Linux子系统。

    打出生时就为服务器而生的Linux,真的这么火了?

    首次力压macOS

    Stack Overflow今年这份调查一共有7万多人参与。

    操作系统方面,主要分为“个人使用”和“工作使用”,调查大家在这两种情况下最常用的操作系统。

    结果是无论哪种情况,Linux系统都超过了macOS,尤其以个人使用为甚。

    具体来说,在接收到的71503份结果中,有28765位调查者在个人使用方面选择了Linux系统,占比为40.23%;

    有22217位选择了macOS,占比为31.07%。两者差距近10%。

    而在工作使用方面,选择Linux系统的达到了28523位,占比39.89%,和个人使用基本持平;

    选择macOS的则有23578位,占比32.97%,比个人使用要多一些(这是macOS最特别的地方)。但它和Linux的差距仍达到了近7%。

    除此之外,还有15%左右的人无论是在个人使用还是工作场景都会选择微软的WSL(Windows Subsystem for Linux),进一步证明Linux的受欢迎程度。

    而从往年数据来看,Linux的受欢迎程度一直小步攀升,今年是首次与macOS的差距拉开这么多。

    所以,难怪开头的程序员管今年叫“Linux桌面版之年”。

    具体来看,2018-2020年之间,Linux的数据分别为23.2%、25.6%、 26.6%,一直屈居第三位。

    2021年是分水岭,Linux首次以0.13%的微妙差距超过macOS,成为第二名。

    不过在工作场景中,macOS还是更胜一筹(30.04%VS25.17%)

    到了2022年,Linux一下子就在个人和工作两方面都大比分超过了macOS。

    如Stack Overflow官方所说,这证明了开源软件的吸引力

    当然,它和Windows系统的差距还是不少,后者仍然是三大操作系统里的王者。

    而除了操作系统本身,其他调查的数据也显示,Linux在Steam平台的市场份额近来也一直在提升。

    2022年1月,该平台上Linux玩家占比1.06%,而到了11月,这个数字涨到了1.44%,而这主要归功于Steam Deck这款掌机的上市(Windows仍然是统治地位的96.11%)

    就在2022年10月的Akademy 2022会议上,相关人员透露,Steam Deck的出货量已超过100万个,同时还有一大批延期订单在处理。

    Linux真的这么火了吗?

    还是有网友对如上数据提出了质疑。

    这主要是因为Stack Overflow这个调查中,几大操作系统的数据总和加起来不再等于100%

    TA表示,这个结果说明在选择“您最主要的操作系统时”,很多人都不止选了一个。

    这个数据对于主要只将它用于工作/专业场景的人来说,高得令人难以置信;对于经常在日常也使用Linux的开发人员来说,又低得要命。

    很多人仍然不习惯Linux,他们吐槽的理由包括不太友好的用户UI(即使Ubuntu也让他们受不了)、安装麻烦、包管理复杂等等。

    不过,还是有不少人认为Linux确实越来越火了。

    一位网友表示,Linux的数据或许还会再高一些,毕竟有用户可能本身使用Windows或Mac桌面,但却主要通过远程终端或虚拟机在Linux系统上工作。

    另一位网友则称自己在过去五年里,亲身经历Linux在他们的工作环境中从“很奇怪”、“不常见”变成“再正常不过的事儿”

    甚至有几个非技术岗位的朋友也开始考虑是否要在Thinkpad上运行Linux。

    在TA看来,Linux兴起的因素有很多,包括云的兴起、Linux桌面发行版的成熟、Linux是树莓派等产品的默认/唯一选项、开发者软件越来越支持多平台,以及特别是Linux的硬件兼容性越来越好(以Manjaro版本为甚)等。

    当然,还有人就是喜欢Linux的无广告,和定制化的能力。

    转移到Linux系统的人还有很多,比如这位:

    不仅自己基本放弃Mac,还希望自己公司的员工都转移到Linux上。

    只不过,TA称唯一的阻碍因素是还没有为Linux硬件和软件找到一个好的MDM(移动设备管理)解决方案。

    最后有意思的是,有人既无法抵抗Linux的吸引力,也无法放下macOS,于是“私人用Linux,工作用macOS就成了一个很好的妥协”。

    你最常用什么系统?为什么?

    One More Thing

    最后,再来看看2022年的Stack Overflow开发者调查报告还有哪些亮点。

    1、编程语言方面,Rust已连续第七年成为最受喜爱的语言,约87%的开发人员表示他们希望继续使用它。

    同时,它与Python、TypeScript一起成为最想学习的前三大新语言。

    2、2021年,Git还是大家最常用的基础工具,完全碾压其后的Docker、Yarn等。今年Docker已取代Git夺得第一,使用率从55%增长到69%。

    此外,本项调查还显示,相比专业开发人员,正在学习编码的人更有可能使用3D工具来自学3D VR和AR技术:Unity 3D(23%VS8%)和Unreal Engine(9%VS3%)

    3、Docker和Kubernetes分别位列最受喜爱和想要学习的工具第一和第二位。随着Docker的数据从去年的30%增加到今年的37%,可以看出大家想要使用Docker的愿望并没有放缓。


    4、Phoenix取代Svelte成为最受欢迎的Web框架。Angular.js连续三年成为开发者最讨厌的框架,React.js连续五年成为开发者最想学习的框架。

    5、收入最高的语言仍然是Clojure。

    工具方面,Chef开发人员薪水最高,但它也是开发者最恐怖的工具之一。

    数据库系统方面,收入最高的前三是DynamoDB、Couchbase和Cassandra。

    6、喜欢在线学习编程的人数从60%上升到了70%,相比年轻人(18岁以下),45岁以上的受访者喜欢从书本上学习。

    7、62%的受访者每天花费超过30分钟解决问题;25%的人每天花费一个多小时。

    对于一个由50名开发人员组成的团队来说,每周花费在搜索答案/解决方案上的时间总计333-651小时。

    8、85%的开发人员表示,他们的公司支持远程办公。

    完整报告:
    https://survey.stackoverflow.co/2022/#section-most-popular-technologies-operating-system

    参考链接:
    [1]
    https://www.justingarrison.com/blog/year-of-linux-desktop/
    [2]https://survey.stackoverflow.co/2022/#section-most-popular-technologies-operating-system

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

    快乐分享,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,操作系统的东西。

  • 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队列中得到该条消息。

  • 船新 IDEA 2023.1 正式发布,新特性真香!

    大家好,昨晚看到 IDEA 官推宣布 IntelliJ IDEA 2023.1 正式发布了。简单看了一下,发现这次的新版本包含了许多改进,进一步优化了用户体验,提高了便捷性。

    至于是否升级最新版本完全是个人意愿,如果觉得新版本没有让自己感兴趣的改进,完全就不用升级,影响不大。软件的版本迭代非常正常,正确看待即可,不持续改进就会慢慢被淘汰!

    根据官方介绍:

    IntelliJ IDEA 2023.1 针对新的用户界面进行了大量重构,这些改进都是基于收到的宝贵反馈而实现的。官方还实施了性能增强措施,使得 Maven 导入更快,并且在打开项目时 IDE 功能更早地可用。由于后台提交检查,新版本提供了简化的提交流程。IntelliJ IDEA Ultimate 现在支持 Spring Security 匹配器和请求映射导航。

    下面对这个版本的一些比较有意思的改进进行详细介绍。

    新 UI 增强(测试版)

    针对收到的有关 IDE 新用户界面的反馈,IntelliJ IDEA 官方实施了一些更新,以解决最受欢迎的请求。引入了紧凑模式,通过缩小间距和元素提供更加集中的 IDE 外观和感觉。新 UI 现在提供一个选项来垂直分割工具窗口区域,并方便地排列窗口,就像旧 UI 一样。主窗口标题栏中的运行小部件已经重新设计,使其外观不显眼且更易于查看。

    在项目打开时更早提供 IDE 功能

    IntelliJ IDEA 官方通过在智能模式下执行扫描文件以建立索引的过程来改进了 IDE 启动体验,这样即可使 IDE 的全部功能在启动过程中更早地可用。当打开一个项目时,IntelliJ IDEA 2023.1 会使用上一次与该项目的会话中存在的缓存,并同时查找要建立索引的文件。如果扫描中没有发现任何更改,则 IDE 将准备就绪,消除了之前由于启动时进行索引而导致的延迟。

    更快地导入 Maven 项目

    更快地导入 Maven 项目

    官方通过优化依赖解析以及重新设计导入和配置 facets 的过程,显著提高了 IDE 在导入 Maven 项目时的性能。

    后台提交检查

    后台提交检查

    官方重新设计了 Git 和 Mercurial 的提交检查行为,以加速整个提交过程。现在,在提交但尚未推送之前会在后台执行检查。

    Spring Security 匹配器和请求映射的导航

    Spring Security 匹配器和请求映射的导航

    为了简化查看应用安全规则,IntelliJ IDEA Ultimate 2023.1 提供了从 Spring 控制器到安全匹配器的轻松导航。该导航可以从安全匹配器到控制器以及反向工作。

    全 IDE 缩放

    全 IDE 缩放

    在 v2023.1 中,可以完全放大和缩小 IDE,同时增加或缩减所有 UI 元素的大小。从主菜单中,选择 View | Appearance(视图 | 外观),调整 IDE 的缩放比例。此外,您可以在 Settings/Preferences | Keymap | Main Menu | View | Appearance(设置/偏好设置 | 按键映射 | 主菜单 | 视图 | 外观)中指定调用这些操作的自定义快捷键。

    新的 Java 检查

    新的 Java 检查

    官方为了帮助保持代码整洁和无错误,升级了一些现有的 Java 检查,并添加了新的检查。格式不正确字符串检查现在报告不符合常见 Java 语法的非法时间转换。冗余字符串操作检查现在能够检测到多余的 StringBuilder.toString() 调用,并提供一个快速修复来将它们替换为 contentEquals(),以便您不会创建中间 String 对象。它还报告 String 构造函数调用中不必要的参数,并建议一个快速修复来删除它们。在这篇博客文章中了解更多关于 IntelliJ IDEA 2023.1 其他代码检查改进。

    Java 20 支持

    Java 20 支持

    继续减少 Java 开发人员认知负荷,IntelliJ IDEA 2023.1 支持最新更新添加到 Java 20 中,包括语言特性模式匹配和记录模式的更改。

    改进了 Extract Method(提取方法)重构

    改进了 Extract Method(提取方法)重构

    官方通过引入选项来升级提取方法重构,即使所选代码片段具有需要返回的多个变量也可以应用该选项。在这些情况下,IDE 首先建议将这些变量封装到一个新记录或 bean 类中,然后执行方法提取。

    VM Options(虚拟机选项)字段中的自动补全

    VM Options(虚拟机选项)字段中的自动补全

    自动补全功能以及集成到 Run/Debug configuration(运行/调试配置)弹出窗口的 VM Options(虚拟机选项)字段中。现在,输入标志的名称时,IDE 会建议可用命令行选项的列表。这适用于 -XX:-X 选项,以及一些未由 IntelliJ IDEA 自动配置的标准选项,如 -ea,但不适用于 -cp–release

    Spring Security 6 支持

    Spring Security 6 支持

    IntelliJ IDEA Ultimate 2023.1 提供了更新的支持,可以导航到 Spring Security 6 中引入的 API 的 URL 映射和安全角色。

    Apache Dubbo 支持

    IntelliJ IDEA 实现了一个新的专用插件,集成了 Apache Dubbo,将该框架的功能作为 IntelliJ IDEA 对 Spring 的支持的一部分。

    Structure(结构)工具窗口中的 VCS 状态颜色提示

    Structure(结构)工具窗口中的 VCS 状态颜色提示

    针对 GitHub 改进了代码审查工作流

    针对 GitHub 改进了代码审查工作流

    为了简化在 IDE 中审查代码的过程,重做了 Pull Request(拉取请求)工具窗口。它现在为您打开的每个拉取请求提供一个专用标签页。标签页会立即显示已更改文件的列表,但它提供的信息比先前更少,让您可以更好地专注于当前任务。现在,可以通过一个新增的专属按钮轻松执行拉取请求当前状态下最相关的操作。

    参考资料

    IntelliJ IDEA 2023.1 更多改进的介绍请参考官方文档:https://www.jetbrains.com/zh-cn/idea/whatsnew/

    
    

  • 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。

    
    

  • 面试官:一台服务器最大能支持多少条 TCP 连接?问倒一大片。。。

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

    来源:juejin.cn/post/7162824884597293086

    • 一台服务器最大能打开的文件数
    • 调整服务器能打开的最大文件数示例
    • 一台服务器最大能支持多少连接
    • 一台客户端机器最多能发起多少条连接
    • 其他
    • 相关实际问题
    图片

    之前有一位读者诉苦,有次面试,好不容易(今年行情大家都懂的)熬到到技术终面,谁知道面试官突然放个大招问他:一台服务器最大能支持多少条 TCP 连接,把他直接给问懵逼了 。。。。(请自行脑补那尴尬的场面与气氛)。

    所以,今天就来讨论一下这个问题。

    一台服务器最大能打开的文件数

    限制参数

    我们知道在Linux中一切皆文件,那么一台服务器最大能打开多少个文件呢?Linux上能打开的最大文件数量受三个参数影响,分别是:

    • fs.file-max (系统级别参数) :该参数描述了整个系统可以打开的最大文件数量。但是root用户不会受该参数限制(比如:现在整个系统打开的文件描述符数量已达到fs.file-max ,此时root用户仍然可以使用ps、kill等命令或打开其他文件描述符)。
    • soft nofile(进程级别参数) :限制单个进程上可以打开的最大文件数。只能在Linux上配置一次,不能针对不同用户配置不同的值。
    • fs.nr_open(进程级别参数) :限制单个进程上可以打开的最大文件数。可以针对不同用户配置不同的值。

    这三个参数之间还有耦合关系,所以配置值的时候还需要注意以下三点:

    1. 如果想加大soft nofile,那么hard nofile参数值也需要一起调整。如果因为hard nofile参数值设置的低,那么soft nofile参数的值设置的再高也没有用,实际生效的值会按照二者最低的来。
    2. 如果增大了hard nofile,那么fs.nr_open也都需要跟着一起调整(fs.nr_open参数值一定要大于hard nofile参数值)。如果不小心把hard nofile的值设置的比fs.nr_open还大,那么后果比较严重。会导致该用户无法登录,如果设置的是*,那么所有用户都无法登录。
    3. 如果加大了fs.nr_open,但是是用的echo “xxx” > ../fs/nr_open命令来修改的fs.nr_open的值,那么刚改完可能不会有问题,但是只要机器一重启,那么之前通过echo命令设置的fs.nr_open值便会失效,用户还是无法登录。所以非常不建议使用echo的方式修改内核参数!!!

    调整服务器能打开的最大文件数示例

    假设想让进程可以打开100万个文件描述符,这里用修改conf文件的方式给出一个建议。如果日后工作里有类似的需求可以作为参考。

    vim /etc/sysctl.conf

    fs.file-max=1100000 // 系统级别设置成110万,多留点buffer  
    fs.nr_open=1100000 // 进程级别也设置成110万,因为要保证比 hard nofile大

    使上面的配置生效sysctl -p

    vim /etc/security/limits.conf
        
    // 用户进程级别都设置成100完  
    soft nofile 1000000  
    hard nofile 1000000

    一台服务器最大能支持多少连接

    我们知道TCP连接,从根本上看其实就是client和server端在内存中维护的一组【socket内核对象】(这里也对应着TCP四元组:源IP、源端口、目标IP、目标端口),他们只要能够找到对方,那么就算是一条连接。那么一台服务器最大能建立多少条连接呢?

    • 由于TCP连接本质上可以理解为是client-server端的一对socket内核对象,那么从理论上将应该是【2^32 (ip数) * 2^16 (端口数)】条连接(约等于两百多万亿)。
    • 但是实际上由于受其他软硬件的影响,我们一台服务器不可能能建立这么多连接(主要是受CPU和内存限制)。

    如果只以ESTABLISH状态的连接来算(这些连接只是建立,但是不收发数据也不处理相关的业务逻辑)那么一台服务器最大能建立多少连接呢?以一台4GB内存的服务器为例!

    • 这种情况下,那么能建立的连接数量主要取决于【内存的大小】(因为如果是)ESTABLISH状态的空闲连接,不会消耗CPU(虽然有TCP保活包传输,但这个影响非常小,可以忽略不计)。
    • 我们知道一条ESTABLISH状态的连接大约消耗【3.3KB内存】,那么通过计算得知一台4GB内存的服务器,【可以建立100w+的TCP连接】(当然这里只是计算所有的连接都只建立连接但不发送和处理数据的情况,如果真实场景中有数据往来和处理(数据接收和发送都需要申请内存,数据处理便需要CPU),那便会消耗更高的内存以及占用更多的CPU,并发不可能达到100w+)。

    上面讨论的都是进建立连接的理想情况,在现实中如果有频繁的数据收发和处理(比如:压缩、加密等),那么一台服务器能支撑1000连接都算好的了,所以一台服务器能支撑多少连接还要结合具体的场景去分析,不能光靠理论值去算。抛开业务逻辑单纯的谈并发没有太大的实际意义。

    服务器的开销大头往往并不是连接本身,而是每条连接上的数据收发,以及请求业务逻辑处理!!!

    一台客户端机器最多能发起多少条连接

    我们知道客户端每和服务端建立一个连接便会消耗掉client端一个端口。一台机器的端口范围是【0 ~ 65535】,那么是不是说一台client机器最多和一台服务端机器建立65535个连接呢(这65535个端口里还有很多保留端口,可用端口可能只有64000个左右)?

    由TCP连接的四元组特性可知,只要四元组里某一个元素不同,那么就认为这是不同的TCP连接。所以需要分情况讨论:

    情况一 】、如果一台client仅有一个IP,server端也仅有一个IP并且仅启动一个程序,监听一个端口的情况下,client端和这台server端最大可建立的连接条数就是 65535 个。

    因为源IP固定,目标IP和端口固定,四元组中唯一可变化的就是【源端口】,【源端口】的可用范围又是【0 ~ 65535】,所以一台client机器最大能建立65535个连接。

    情况二 】、如果一台client有多个IP(假设客户端有 n 个IP),server端仅有一个IP并且仅启动一个程序,监听一个端口的情况下,一台client机器最大能建立的连接条数是:n * 65535 个。

    因为目标IP和端口固定,有 n 个源IP,四元组中可变化的就是【源端口】+ 【源IP】,【源端口】的可用范围又是【0 ~ 65535】,所以一个IP最大能建立65535个连接,那么n个IP最大就能建立 n * 65535个连接了。

    以现在的技术,给一个client分配多个IP是非常容易的事情,只需要去联系你们网管就可以做到。

    情况三 】、如果一台client仅有一个IP,server端也仅有一个IP但是server端启动多个程序,每个程序监听一个端口的情况下(比如server端启动了m个程序,监听了m个不同端口),一台client机器最大能建立的连接数量为:65535 * m。

    源IP固定,目标IP固定,目标端口数量为m个,可变化的是源端口,而源端口变化范围是【0 ~ 65535】,所以一台client机器最大能建立的TCP连接数量是 65535 * m个。

    • 其余情况类推,但是客户端的可用端口范围一般达不到65535个,受内核参数net.ipv4.ip_local_port_range限制,如果要修改client所能使用的端口范围,可以修改这个内核参数的值。
    • 所以,不光是一台server端可以接收100w+个TCP连接,一台client照样能发出100w+个连接。

    其他

    • 三次握手里socket的全连接队列长度由参数net.core.somaxconn来控制,默认大小是128,当两台机器离的非常近,但是建立连接的并发又非常高时,可能会导致半连接队列或全连接队列溢出,进而导致server端丢弃握手包。然后造成client超时重传握手包(至少1s以后才会重传),导致三次握手连接建立耗时过长。我们可以调整参数net.core.somaxconn来增加去按连接队列的长度,进而减小丢包的影响
    • 有时候我们通过 ctrl + c方式来终止了某个进程,但是当重启该进程的时候发现报错端口被占用,这种问题是因为【操作系统还没有来得及回收该端口,等一会儿重启应用就好了】
    • client程序在和server端建立连接时,如果client没有调用bind方法传入指定的端口,那么client在和server端建立连接的时候便会自己随机选择一个端口来建立连接。一旦我们client程序调用了bind方法传入了指定的端口,那么client将会使用我们bind里指定的端口来和server建立连接。所以不建议client调用bind方法,bind函数会改变内核选择端口的策略
    public static void main(String[] args) throws IOException {  
        SocketChannel sc = SocketChannel.open();  
       // 客户端还可以调用bind方法  
        sc.bind(new InetSocketAddress("localhost", 9999));  
        sc.connect(new InetSocketAddress("localhost", 8080));  
        System.out.println("waiting..........");  
    }
    • 在Linux一切皆文件,当然也包括之前TCP连接中说的socket。进程打开一个socket的时候需要创建好几个内核对象,换一句直白的话说就是打开文件对象吃内存,所以Linux系统基于安全角度考虑(比如:有用户进程恶意的打开无数的文件描述符,那不得把系统搞奔溃了),在多个位置都限制了可打开的文件描述符的数量。
    • 内核是通过【hash表】的方式来管理所有已经建立好连接的socket,以便于有请求到达时快速的通过【TCP四元组】查找到内核中对应的socket对象。

    在epoll模型中,通过红黑树来管理epoll对象所管理的所有socket,用红黑树结构来平衡快速删除、插入、查找socket的效率。

    相关实际问题

    在网络开发中,很多人对一个基础问题始终没有彻底搞明白,那就是一台机器最多能支撑多少条TCP连接。不过由于客户端和服务端对端口使用方式不同,这个问题拆开来理解要容易一些。

    注意,这里说的是客户端和服务端都只是角色,并不是指某一台具体的机器。例如对于我们自己开发的应用程序来说,当他响应客户端请求的时候,他就是服务端。当他向MySQL请求数据的时候,他又变成了客户端。

    “too many open files” 报错是怎么回事,该如何解决

    你在线上可能遇到过too many open files这个错误,那么你理解这个报错发生的原理吗?如果让你修复这个错误,应该如何处理呢?

    • 因为每打开一个文件(包括socket),都需要消耗一定的内存资源。为了避免个别进程不受控制的打开了过多文件而让整个服务器奔溃,Linux对打开的文件描述符数量有限制。如果你的进程触发到内核的限制,那么”too many open files” 报错就产生了。
    • 可以通过修改fs.file-max 、soft nofile、fs.nr_open这三个参数的值来修改进程能打开的最大文件描述符数量。

    需要注意这三个参数之间的耦合关系!

    一台服务端机器最大究竟能支持多少条连接

    因为这里要考虑的是最大数,因此先不考虑连接上的数据收发和处理,仅考虑ESTABLISH状态的空连接。那么一台服务端机器上最大可以支持多少条TCP连接?这个连接数会受哪些因素的影响?

    • 在不考虑连接上数据的收发和处理的情况下,仅考虑ESTABLISH状态下的空连接情况下,一台服务器上最大可支持的TCP连接数量基本上可以说是由内存大小来决定的。
    • 四元组唯一确定一条连接,但服务端可以接收来自任意客户端的请求,所以根据这个理论计算出来的数字太大,没有实际意义。另外文件描述符限制其实也是内核为了防止某些应用程序不受限制的打开【文件句柄】而添加的限制。这个限制只要修改几个内核参数就可以加大。
    • 一个socket大约消耗3kb左右的内存,这样真正制约服务端机器最大并发数的就是内存,拿一台4GB内存的服务器来说,可以支持的TCP连接数量大约是100w+。
    一条客户端机器最大究竟能支持多少条连接

    和服务端不同的是,客户端每次建立一条连接都需要消耗一个端口。在TCP协议中,端口是一个2字节的整数,因此范围只能是0~65535。那么客户单最大只能支持65535条连接吗?有没有办法突破这个限制,有的话有哪些办法?

    • 客户度每次建立一条连接都需要消耗一个端口。从数字上来看,似乎最多只能建立65535条连接。但实际上我们有两种办法破除65535这个限制。

    方式一,为客户端配置多IP 方式二,分别连接不同的服务端

    • 所以一台client发起百万条连接是没有任何问题的。
    做一个长连接推送产品,支持1亿用户需要多少台机器

    假设你是系统架构师,现在老板给你一个需求,让你做一个类似友盟upush这样的产品。要在服务端机器上保持一个和客户端的长连接,绝大部分情况下连接都是空闲的,每天也就顶多推送两三次左右。总用户规模预计是1亿。那么现在请你来评估一下需要多少台服务器可以支撑这1亿条长连接。

    • 对于长连接推送模块这种服务来说,给客户端发送数据只是偶尔的,一般一天也就顶多一两次。绝大部分情况下TCP连接都是空闲的,CPU开销可以忽略。
    • 再基于内存来考虑,假设服务器内存是128G的,那么一台服务器可以考虑支持500w条并发。这样会消耗掉大约不到20GB内存用来保存这500w条连接对应的socket。还剩下100GB以上的内存来应对接收、发送缓冲区等其他的开销足够了。所以,一亿用户,仅仅需要20台服务器就差不多够用了!


  • 还在手动配置Nginx?试试这款可视化管理工具吧,用起来够优雅!

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

    今天给大家介绍一款 Nginx 可视化管理界面,非常好用,小白也能立马上手。

    nginx-proxy-manager 是一个反向代理管理系统,它基于 NGINX,具有漂亮干净的 Web UI。还可以获得受信任的 SSL 证书,并通过单独的配置、自定义和入侵保护来管理多个代理。它是开源的,斩获 11.8K 的 Star 数。

    特征

    • 基于 Tabler(https://tabler.github.io/) 的美观安全的管理界面
    • 无需了解 Nginx 即可轻松创建转发域、重定向、流和 404 主机
    • 使用 Let’s Encrypt 的免费 SSL 或提供您自己的自定义 SSL 证书
    • 主机的访问列表和基本 HTTP 身份验证
    • 高级 Nginx 配置可供超级用户使用
    • 用户管理、权限和审核日志

    安装

    1、安装 Docker 和 Docker-Compose

    2、创建一个docker-compose.yml文件

    version: '3'
    services:
      app:
        image: 'jc21/nginx-proxy-manager:latest'
        restart: unless-stopped
        ports:
          - '80:80'
          - '81:81'
          - '443:443'
        volumes:
          - ./data:/data
          - ./letsencrypt:/etc/letsencrypt

    3、运行

    docker-compose up -d

    #如果使用的是 docker-compose-plugin
    docker compose up -d

    4、访问网页

    运行成功后,访问 http://127.0.0.1:81 就能看到界面啦

    5、登录

    网站默认账号和密码为

    账号:admin@example.com
    密码:changeme

    登录成功后第一次要求修改密码,按照步骤修改即可!

    6、登录成功主界面

    实战:设置后台管理界面的反向代理

    这里,我们就用 http://a.test.com 来绑定我们的端口号为81的后台管理界面,实现浏览器输入 http://a.test.com 即可访问后台管理界面,并且设置HTTPS。

    1、前提

    • 安装好Nginx Proxy Manager
    • 拥有一个域名
    • 将 http://a.test.com 解析到安装Nginx Proxy Manager的服务器ip地址上

    2、反向代理操作

    先用ip:81 访问后台管理界面,然后输入账号密码进入后台。

    点击绿色图标的选项

    点击右边Add Proxy Host ,在弹出的界面Details选项中填写相应的字段。

    • Domain Names: 填写要反向代理的域名,这里就是http://a.test.com
    • Forward Hostname / IP: 填写的ip值见下文解释
    • Forward Port: 反向代理的端口,这里就是81
    • Block Common Exploits: 开启后阻止一些常见漏洞
    • 其余两个暂不知作用

    Forward Hostname / IP填写说明

    如果搭建的服务和nginx proxy manager服务所在不是一个服务器,则填写能访问对应服务的IP。如果都在同一台服务器上,则填写在服务器中输入ip addr show docker0 命令获取得到的ip。

    这里不填127.0.0.1的原因是使用的是docker容器搭建web应用,docker容器和宿主机即服务器不在同一个网络下,所以127.0.0.1并不能访问到宿主机,而ip addr show docker0获得的ip地址就是宿主机地址。

    接下来即可用a.test.com 访问后台管理界面,此时还只是http协议,没有https。不过此时就可以把之前的81端口关闭了,输入a.test.com 访问的是服务器80端口,然后在转发给内部的81端口。

    3、申请ssl证书

    申请一个a.test.com 证书,这样就可以提供https访问了。

    在Nginx Proxy Manager管理后台,选择Access Lists->Add SSL Certificate->Let's Encrypt选项。

    按照下图方式填写,点击Save就可以了

    4、设置HTTPS

    进入反向代理设置界面,编辑上文创建的反代服务,选择SSL选项,下拉菜单中选择我们申请的证书,然后可以勾选Force SSL即强制HTTPS。

    总结

    以上就是本教程的全部内容,更多的使用教程,大家可以访问官方文档。

    官方文档:https://nginxproxymanager.com/guide/

  • MySQL 索引失效跑不出这 8 个场景

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

    SQL 写不好 加班少不了  日常工作中SQL 是必不可少的一项技术 但是很多人不会过多的去关注SQL问题。

    一是数据量小,二是没有意识到索引的重要性。本文主要是整理 SQL失效场景,如果里面的细节你都知道,那你一定是学习能力比较好的人,膜拜~

    写完这篇文章 我感觉自己之前知道的真的是 “目录” 没有明白其中的内容,如果你能跟着节奏看完文章,一定会有收获,至少我写完感觉思维通透很多,以后百分之九十的 SQl索引问题 和 面试这方面问题都能拿捏两。


    基础数据准备





    准备一个数据表作为 数据演示  这里面一共 创建了三个索引

    • 联合索引  snames_codeaddress

    • 主键索引  id

    • 普通索引  height


    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;

    -- ----------------------------
    -- Table structure for student
    -- ----------------------------
    DROP TABLE IF EXISTS `student`;
    CREATE TABLE `student`  (
      `id` int(11NOT NULL AUTO_INCREMENT,
      `sname` varchar(20CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `s_code` int(100NULL DEFAULT NULL,
      `address` varchar(100CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `height` double NULL DEFAULT NULL,
      `classid` int(11NULL DEFAULT NULL,
      `create_time` datetime(0NOT NULL ON UPDATE CURRENT_TIMESTAMP(0),
      PRIMARY KEY (`id`USING BTREE,
      INDEX `普通索引`(`height`USING BTREE,
      INDEX `联合索引`(`sname``s_code``address`USING BTREE
    ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of student
    -- ----------------------------
    INSERT INTO `student` VALUES (1'学生1'1'上海'1701'2022-11-02 20:44:14');
    INSERT INTO `student` VALUES (2'学生2'2'北京'1802'2022-11-02 20:44:16');
    INSERT INTO `student` VALUES (3'变成派大星'3'京东'1853'2022-11-02 20:44:19');
    INSERT INTO `student` VALUES (4'学生4'4'联通'1904'2022-11-02 20:44:25');


    正文





    上面的SQL 我们已经创建好基本的数据  在验证之前 先带着几个问题

    我们先从上往下进行验证


    最左匹配原则





    写在前面:我很早之前就听说过数据库的最左匹配原则,当时是通过各大博客论坛了解的,但是这些博客的局限性在于它们对最左匹配原则的描述就像一些数学定义一样,往往都是列出123点,满足这123点就能匹配上索引,否则就不能。

    最左匹配原则就是指在联合索引中,如果你的 SQL 语句中用到了联合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个联合索引去进行匹配,我们上面建立了联合索引 可以用来测试最左匹配原则 snames_codeaddress

    请看下面SQL语句 进行思考 是否会走索引

    -- 联合索引 sname,s_code,address

    1、select create_time from student where sname = "变成派大星"  -- 会走索引吗?

    2select create_time from student where s_code = 1   -- 会走索引吗?

    3select create_time from student where address = "上海"  -- 会走索引吗?

    4select create_time from student where address = "上海" and s_code = 1 -- 会走索引吗?

    5select create_time from student where address = "上海" and sname = "变成派大星"  -- 会走索引吗?

    6select create_time from student where sname = "变成派大星" and address = "上海"  -- 会走索引吗?

    7select create_time from student where sname = "变成派大星" and s_code = 1 and address = "上海"  -- 会走索引吗?

    凭你的经验 哪些会使用到索引呢 ?可以先思考一下 在心中记下数字

    走索引例子

    EXPLAIN  select create_time from student where sname = "变成派大星"  -- 会走索引吗?

    未走索引例子

    EXPLAIN select create_time from student where address = "上海" and s_code = 1 -- 会走索引吗?

    走的全表扫描 rows = 4

    如果你内心的答案没有全部说对就接着往下看

    最左匹配原则顾名思义:最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、。

    例如:s_code = 2 如果建立(snames_code)顺序的索引,是匹配不到(snames_code)索引的;

    但是如果查询条件是sname = “变成派大星” and s_code = 2或者a=1(又或者是s_code = 2 and sname = “变成派大星” )就可以,因为优化器会自动调整snames_code的顺序

    再比如sname = “变成派大星” and s_code > 1 and address = “上海”  address是用不到索引的,因为s_code字段是一个范围查询,它之后的字段会停止匹配。

    不带范围查询 索引使用类型

    带范围使用类型

    根据上一篇文章的讲解 可以明白 ref 和range的含义  级别还是相差很多的

    思考

    为什么左链接一定要遵循最左缀原则呢?

    验证

    看过一个比较好玩的回答:

    你可以认为联合索引是闯关游戏的设计
    例如你这个联合索引是state/city/zipCode
    那么state就是第一关 city是第二关, zipCode就是第三关
    你必须匹配了第一关,才能匹配第二关,匹配了第一关和第二关,才能匹配第三关

    这样描述不算完全准确 但是确实是这种思想

    要想理解联合索引的最左匹配原则,先来理解下索引的底层原理。索引的底层是一颗B+树,那么联合索引的底层也就是一颗B+树,只不过联合索引的B+树节点中存储的是键值。由于构建一棵B+树只能根据一个值来确定索引关系,所以数据库依赖联合索引最左的字段来构建 文字比较抽象 我们看一下

    加入我们建立 A,B 联合索引 他们在底层储存是什么样子呢?

    • 橙色代表字段 A

    • 浅绿色 代表字段B

    图解:

    我们可以看出几个特点

    • A 是有顺序的  1,1,2,2,3,4

    • B 是没有顺序的 1,2,1,4,1,2 这个是散列的

    • 如果A是等值的时候 B是有序的  例如 (1,1),(1,2) 这里的B有序的 (2,1),(2,4) B 也是有序的

    这里应该就能看出 如果没有A的支持 B的索引是散列的 不是连续的

    再细致一点 我们重新创建一个表

    DROP TABLE IF EXISTS `leftaffix`;

    CREATE TABLE `leftaffix`  (

      `a` int(11NOT NULL AUTO_INCREMENT,

      `b` int(11NULL DEFAULT NULL,

      `c` int(11NULL DEFAULT NULL,

      `d` int(11NULL DEFAULT NULL,

      `e` varchar(11CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,

      PRIMARY KEY (`a`USING BTREE,

      INDEX `联合索引`(`b``c``d`USING BTREE

    ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
     
    -- ----------------------------
    -- Records of leftaffix
    -- ----------------------------
    INSERT INTO `leftaffix` VALUES (1111'1');

    INSERT INTO `leftaffix` VALUES (2222'2');

    INSERT INTO `leftaffix` VALUES (3322'3');

    INSERT INTO `leftaffix` VALUES (4311'4');

    INSERT INTO `leftaffix` VALUES (5235'5');

    INSERT INTO `leftaffix` VALUES (6644'6');

    INSERT INTO `leftaffix` VALUES (7888'7');
    SET FOREIGN_KEY_CHECKS = 1;

    在创建索引树的时候会对数据进行排序 根据最左缀原则  会先通过 B 进行排序 也就是 如果出现值相同就 根据 C 排序 如果 C相同就根据D 排序 排好顺序之后就是如下图:

    索引的生成就会根据图二的顺序进行生成  我们看一下 生成后的树状数据是什么样子

    解释一些这个树状图  首先根据图二的排序 我们知道顺序 是 1111a  2222b 所以 在第三层 我们可以看到 1111a 在第一层 2222b在第二层  因为 111

    简化一下就是这个样子

    但是这种顺序是相对的。这是因为MySQL创建联合索引的规则是首先会对联合索引的最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序。所以B=2这种查询条件没有办法利用索引。

    看到这里还可以明白一个道理 为什么我们建立索引的时候不推荐建立在经常改变的字段 因为这样的话我们的索引结构就要跟着你的改变而改动 所以很消耗性能


    补充

    评论区老哥的提示 最左缀原则可以通过跳跃扫描的方式打破 简单整理一下这方面的知识

    这个是在 8.0 进行的优化

    MySQL8.0版本开始增加了索引跳跃扫描的功能,当第一列索引的唯一值较少时,即使where条件没有第一列索引,查询的时候也可以用到联合索引。

    比如我们使用的联合索引是 bcd  但是b中字段比较少 我们在使用联合索引的时候没有 使用 b 但是依然可以使用联合索引MySQL联合索引有时候遵循最左前缀匹配原则,有时候不遵循。


    小总结

    前提 如果创建 b,c,d 联合索引面

    • 如果 我where 后面的条件是c = 1 and d = 1为什么不能走索引呢 如果没有b的话 你查询的值相当于 *11 我们都知道*是所有的意思也就是我能匹配到所有的数据

    • 如果 我 where 后面是 b = 1 and d =1 为什么会走索引呢?你等于查询的数据是 1*1 我可以通过前面 1 进行索引匹配 所以就可以走索引

    • 最左缀匹配原则的最重要的就是 第一个字段

    我们接着看下一个失效场景


    select *





    思考

    这里是我之前的一个思维误区 select * 不会导致索引失效 之前测试发现失效是因为where 后面的查询范围过大 导致索引失效 并不是Select * 引起的  但是为什么不推荐使用select *

    解释

    • 增加查询分析器解析成本。

    • 增减字段容易与 resultMap 配置不一致。

    • 无用字段增加网络 消耗,尤其是 text 类型的字段。


    在阿里的开发手册中,大面的概括了上面几点。

    在使用Select * 索引使用正常

    虽然走了索引但是 也不推荐这种写法 为什么呢?

    首先我们在上一个验证中创建了联合索引 我们使用B=1 会走索引  但是 与直接查询索引字段不同  使用SELECT*,获取了不需要的数据,则首先通过辅助索引过滤数据,然后再通过聚集索引获取所有的列,这就多了一次b+树查询,速度必然会慢很多,减少使用select * 就是降低回表带来的损耗。

    也就是 Select * 在一些情况下是会走索引的 如果不走索引就是 where 查询范围过大 导致MySQL 最优选择全表扫描了 并不是Select * 的问题

    上图就是索引失效的情况

    范围查找也不是一定会索引失效 下面情况就会索引生效就是 级别低 生效的原因是因为缩小了范围


    小总结

    • select * 会走索引

    • 范围查找有概率索引失效但是在特定的情况下会生效 范围小就会使用 也可以理解为 返回结果集小就会使用索引

    • mysql中连接查询的原理是先对驱动表进行查询操作,然后再用从驱动表得到的数据作为条件,逐条的到被驱动表进行查询。

    • 每次驱动表加载一条数据到内存中,然后被驱动表所有的数据都需要往内存中加载一遍进行比较。效率很低,所以mysql中可以指定一个缓冲池的大小,缓冲池大的话可以同时加载多条驱动表的数据进行比较,放的数据条数越多性能io操作就越少,性能也就越好。所以,如果此时使用select * 放一些无用的列,只会白白的占用缓冲空间。浪费本可以提高性能的机会。

    • 按照评论区老哥的说法 select * 不是造成索引失效的直接原因 大部分原因是 where 后边条件的问题 但是还是尽量少去使用select * 多少还是会有影响的



    使用函数





    使用在Select 后面使用函数可以使用索引 但是下面这种做法就不能

    因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。

    不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

    这种写法我没使用过 感觉情况比较少 也比较容易注意到这种写法


    计算操作





    这个情况和上面一样 之所以会导致索引失效是因为改变了索引原来的值 在树中找不到对应的数据只能全表扫描

    因为索引保存的是索引字段的原始值,而不是 b – 1 表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。

    下面这种计算方式就会使用索引

    Java比较熟悉的可能会有点疑问,这种对索引进行简单的表达式计算,在代码特殊处理下,应该是可以做到索引扫描的,比方将 b – 1 = 6 变成 b = 6 – 1。是的,是能够实现,但是 MySQL 还是偷了这个懒,没有实现。


    小总结

    总而言之 言而总之 只要是影响到索引列的值 索引就是失效


    Like %





    1.这个真的是难受哦  因为经常使用这个 所以还是要小心点 在看为什么失效之前 我们先看一下 Like % 的解释

    • %百分号通配符: 表示任何字符出现任意次数(可以是0次).

    • _下划线通配符: 表示只能匹配单个字符,不能多也不能少,就是一个字符.

    • like操作符: LIKE作用是指示mysql后面的搜索模式是利用通配符而不是直接相等匹配进行比较.


    注意: 如果在使用like操作符时,后面的没有使用通用匹配符效果是和=一致的,

    SELECT * FROM products WHERE products.prod_name like '1000';

    2.匹配包含”Li”的记录(包括记录”Li”) :

    SELECTFROM products WHERE products.prod_name like '%Li%';

    3.匹配以”Li”结尾的记录(包括记录”Li”,不包括记录”Li “,也就是Li后面有空格的记录,这里需要注意)

    SELECT * FROM products WHERE products.prod_name like '%Li';

    在左不走 在右走

    右:虽然走 但是索引级别比较低主要是模糊查询 范围比较大 所以索引级别就比较低

    左:这个范围非常大 所以没有使用索引的必要了 这个可能不是很好优化 还好不是一直拼接上面的

    小总结

    索引的时候和查询范围关系也很大 范围过大造成索引没有意义从而失效的情况也不少


    使用Or导致索引失效





    这个原因就更简单了

    在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效 举个例子,比如下面的查询语句,b 是主键,e 是普通列,从执行计划的结果看,是走了全表扫描。

    优化

    这个的优化方式就是 在Or的时候两边都加上索引

    就会使用索引 避免全表扫描


    in使用不当





    首先使用In 不是一定会造成全表扫描的 IN肯定会走索引,但是当IN的取值范围较大时会导致索引失效,走全表扫描

    in 在结果集 大于30%的时候索引失效

    not in 和 In的失效场景相同


    order By





    这一个主要是Mysql 自身优化的问题 我们都知道OrderBy 是排序 那就代表我需要对数据进行排序 如果我走索引 索引是排好序的 但是我需要回表 消耗时间 另一种 我直接全表扫描排序 不用回表 也就是

    • 走索引 + 回表

    • 不走索引 直接全表扫描

    Mysql 认为直接全表扫面的速度比 回表的速度快所以就直接走索引了  在Order By 的情况下 走全表扫描反而是更好的选择

    子查询会走索引吗

    答案是会 但是使用不好就不会


    大总结





    转自:进阶的派大星

    链接:https://juejin.cn/post/7161964571853815822

    
    

  • 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