bt365体育投注.主頁欢迎您!!

    <acronym id="zvmrr"></acronym>
    <td id="zvmrr"></td>
  • <tr id="zvmrr"><label id="zvmrr"></label></tr>
  • <acronym id="zvmrr"></acronym>
  • MrZ

    MrZ 查看完整档案

    填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
    编辑
    _ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

    发布推广广告信息,该用户账号已被停用

    个人动态

    MrZ 发布了文章 · 2020-12-30

    史上最全面‘java监听器’解读,读完就能用进项目

    Web监听器导图详解

      监听器是JAVA Web开发中很重要的内容,其中涉及到的知识,可以参考下面导图:

    ?

    一、Web监听器

    1. 什么是web监听器?

    ==============

      web监听器是一种Servlet中的特殊的类,它们能帮助开发者监听web中的特定事件,比如ServletContext,HttpSession,ServletRequest的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控。

    1. 监听器常用的用途

    ============

      通常使用Web监听器做以下的内容:

      统计在线人数,利用HttpSessionLisener

      加载初始化信息:利用ServletContextListener

      统计网站访问量

      实现访问监控

    1. 接下来看看一个监听器的创建以及执行过程

    =======================

    ?  首先需要创建一个监听器,实现某种接口,例如我想实现一个对在线人数的监控,可以创建如下的监听器:

    public class MyListener implements HttpSessionListener{
        private int userNumber = 0;
        public void sessionCreated(HttpSessionEvent arg0) {
            userNumber++;
            arg0.getSession().setAttribute("userNumber", userNumber);
        }
        public void sessionDestroyed(HttpSessionEvent arg0) {
            userNumber--;
            arg0.getSession().setAttribute("userNumber", userNumber);
        }
    }

      然后在web.xml中配置该监听器,在web-app中添加:

     <listener>
          <listener-class>com.test.MyListener</listener-class>
      </listener>

      在JSP中添加访问人数:

    <body>
        在线人数:<%=session.getAttribute("userNumber") %><br/>
    </body>

      当我使用我的浏览器访问时,执行结果如下:

    ?

      当打开另一个浏览器访问时:

    ?

      由于打开另一个浏览器访问,相当于另一个会话,因此在线人数会增加。

      对于3.0版本的Servlet来说,还支持使用注解的方式进行配置。

      那么接下来看看都有哪些监听器以及方法吧!

    二、监听器的分类

    1. 按照监听的对象划分:

    ==============

      按照监听对象的不同可以划分为三种:

      ServletContext监控:对应监控application内置对象的创建和销毁。

      当web容器开启时,执行contextInitialized方法;当容器关闭或重启时,执行contextDestroyed方法。

      实现方式:直接实现ServletContextListener接口:

    public class MyServletContextListener implements ServletContextListener{
        public void contextDestroyed(ServletContextEvent sce) {
    
        }
        public void contextInitialized(ServletContextEvent sce) {
    
        }
    }

      HttpSession监控:对应监控session内置对象的创建和销毁。

      当打开一个新的页面时,开启一个session会话,执行sessionCreated方法;当页面关闭session过期时,或者容器关闭销毁时,执行sessionDestroyed方法。

      实现方式:直接实现HttpSessionListener接口:

    public class MyHttpSessionListener implements HttpSessionListener{
        public void sessionCreated(HttpSessionEvent arg0) {
    
        }
        public void sessionDestroyed(HttpSessionEvent arg0) {
    
        }
    }

      ServletRequest监控:对应监控request内置对象的创建和销毁。

      当访问某个页面时,出发一个request请求,执行requestInitialized方法;当页面关闭时,执行requestDestroyed方法。

      实现方式,直接实现ServletRequestListener接口:

    public class MyServletRequestListener implements ServletRequestListener{
        public void requestDestroyed(ServletRequestEvent arg0) {
    
        }
        public void requestInitialized(ServletRequestEvent arg0) {
    
        }
    }

    1. 按照监听事件划分:

    =============

      2.1 监听事件自身的创建和销毁:同上面的按对象划分。

      2.2 监听属性的新增、删除和修改:

      监听属性的新增、删除和修改也是划分成三种,分别针对于ServletContext、HttpSession、ServletRequest对象:

      ServletContext,实现ServletContextAttributeListener接口:

      通过调用ServletContextAttribtueEvent的getName方法可以得到属性的名称。

    public class MyServletContextAttrListener implements ServletContextAttributeListener {
        public void attributeAdded( ServletContextAttributeEvent hsbe )
        {
            System.out.println( "In servletContext added :name = " + hsbe.getName() );
        }
    
    
        public void attributeRemoved( ServletContextAttributeEvent hsbe )
        {
            System.out.println( "In servletContext removed :name = " + hsbe.getName() );
        }
    
    
        public void attributeReplaced( ServletContextAttributeEvent hsbe )
        {
            System.out.println( "In servletContext replaced :name = " + hsbe.getName() );
        }
    }

      HttpSession,实现HttpSessionAttributeListener接口:

    public class MyHttpSessionAttrListener implements HttpSessionAttributeListener {
        public void attributeAdded( HttpSessionBindingEvent hsbe )
        {
            System.out.println( "In httpsession added:name = " + hsbe.getName() );
        }
    
    
        public void attributeRemoved( HttpSessionBindingEvent hsbe )
        {
            System.out.println( "In httpsession removed:name = " + hsbe.getName() );
        }
    
    
        public void attributeReplaced( HttpSessionBindingEvent hsbe )
        {
            System.out.println( "In httpsession replaced:name = " + hsbe.getName() );
        }
    }

      ServletRequest,实现ServletRequestAttributeListener接口:

    public class MyServletRequestAttrListener implements ServletRequestAttributeListener {
        public void attributeAdded( ServletRequestAttributeEvent hsbe )
        {
            System.out.println( "In servletrequest added :name = " + hsbe.getName() );
        }
    
    
        public void attributeRemoved( ServletRequestAttributeEvent hsbe )
        {
            System.out.println( "In servletrequest removed :name = " + hsbe.getName() );
        }
    
    
        public void attributeReplaced( ServletRequestAttributeEvent hsbe )
        {
            System.out.println( "In servletrequest replaced :name = " + hsbe.getName() );
        }
    }

      2.3 监听对象的状态:

      针对某些POJO类,可以通过实现HttpSessionBindingListener接口,监听POJO类对象的事件。例如:

    public class User implements HttpSessionBindingListener,Serializable{
    
        private String username;
        private String password;
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public void valueBound(HttpSessionBindingEvent hsbe) {
            System.out.println("valueBound name: "+hsbe.getName());
        }
    
        public void valueUnbound(HttpSessionBindingEvent hsbe) {
            System.out.println("valueUnbound name: "+hsbe.getName());
        }
    
    }

      Session数据的钝化与活化:

      由于session中保存大量访问网站相关的重要信息,因此过多的session数据就会服务器性能的下降,占用过多的内存。因此类似数据库对象的持久化,web容器也会把不常使用的session数据持久化到本地文件或者数据中。这些都是有web容器自己完成,不需要用户设定。

      不用的session数据序列化到本地文件中的过程,就是钝化;

      当再次访问需要到该session的内容时,就会读取本地文件,再次放入内存中,这个过程就是活化。

      类似的,只要实现HttpSeesionActivationListener接口就是实现钝化与活化事件的监听:

    public class User implements HttpSessionBindingListener,
    HttpSessionActivationListener,Serializable{
    
        private String username;
        private String password;
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public void valueBound(HttpSessionBindingEvent hsbe) {
            System.out.println("valueBound name: "+hsbe.getName());
        }
    
        public void valueUnbound(HttpSessionBindingEvent hsbe) {
            System.out.println("valueUnbound name: "+hsbe.getName());
        }
    
        public void sessionDidActivate(HttpSessionEvent hsbe) {
            System.out.println("sessionDidActivate name: "+hsbe.getSource());
        }
    
        public void sessionWillPassivate(HttpSessionEvent hsbe) {
            System.out.println("sessionWillPassivate name: "+hsbe.getSource());
        }
    
    }

    三、Servlet版本与Tomcat版本

      首先看一下Tomcat官网给出的匹配:

    ?

      如果版本不匹配,那么tomcat是不能发布该工程的,首先看一下版本不匹配时,会发生什么!

      我试图创建一个web工程,并且选取了Servlet3.0版本:

    ?

      然后我想要在tomcat6中发布,可以看到报错了!

      JDK版本不对....这是在平时开发如果对Servlet不熟悉的web新手,常犯的错误。

    ?

    解决方法:

      1 在创建时,直接发布到Tomcat容器中,此时Servlet仅仅会列出Tomcat支持的版本:

    ?

      2 修改工程Servlet版本配置信息,文件为:工作目录SessionExample.settingsorg.eclipse.wst.common.project.facet.core.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <faceted-project>
      <runtime name="Apache Tomcat v6.0"/>
      <fixed facet="java"/>
      <fixed facet="wst.jsdt.web"/>
      <fixed facet="jst.web"/>
      <installed facet="java" version="1.7"/>
      <installed facet="jst.web" version="2.5"/>
      <installed facet="wst.jsdt.web" version="1.0"/>
    </faceted-project>

    四、getAttribute与getParameter的区别

      这部分是对JSP的扩展,经常在JSP或者Servlet中获取数据,那么getAttribute与getParameter有什么区别呢?

    1. 从获取到数据的来源来说:

    ================

      getAttribtue获取到的是web容器中的值,比如:

      我们在Servlet中通过setAttribute设定某个值,这个值存在于容器中,就可以通过getAttribute方法获取;

      getParameter获取到的是通过http传来的值,比如这样一个http请求:

    http:localhost:8080/test/test.html?username=xingoo

      还有其他的GET和POST方式,都可以通过getParameter来获取。

    1. 从获取到的数据类型来说:

    ================

      getAttribute返回的是一个对象,Object。

      getParameter返回的是,前面页面中某个表单或者http后面参数传递的值,是个字符串。

    原文:https://juejin.cn/post/691197...

    推荐阅读

    =====

    程序员年薪百万的飞马计划你听说过吗?

    为什么阿里巴巴的程序员成长速度这么快?

    从事开发一年的程序员能拿到多少钱?

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-29

    总结面试过程中的各种套路,让你从自我介绍就给面试官下套;

    背景

    本篇是,他从?_秋招历程、校招结构化面试、 offer 选择_?等三个方面进行了总结和经验分享。

    还总结面试过程中的各种套路,让你从自我介绍就给面试官下套。其实不只是在校招面试中,社招其实也一样,其中 “给面试官下套” 是个不错的方法,值得借鉴和学习。(详见文中第二部分),另外,文末赠送优质数据结构算法+大厂面试真题学习材料,有需要的同学请自取。

    下面开始正文吧:

    11 月底,在经历了充分的思想斗争后,我终于下定决心寄出了三方,历时三个多月的秋招也终于尘埃落定。过去的三个多月里,面试时间可能只有一个月左右,剩下的两个多月的时间都在等待结果和纠结,因此通过本文总结这段时间我的秋招历程和感悟。

    本文大纲如下图所示:

    ?

    一、秋招历程

    本人本科毕业于 985 高校,硕士就读于国内 Top2,本硕专业都是软件工程。在本科期间有过华为和腾讯两段实习经历,在研究生期间,我发现自己对科研的兴趣确实有限,因此平时较多参与实验室科研项目的落地应用。

    今年由于疫情影响,2020 年上半年大部分时间我都在远程办公,导师分配的任务也稍有减轻,因此悄悄在字节和阿里实习了半年。

    就业方向

    在秋招开始之前,我考虑的方向主要包括:

    • (1) 读博:导师在博士生考试前和我长谈建议我读博,待遇上也给出了比较好的条件。如果我确实有科研天赋并且热爱科研,那么读博真的是一个非常好的选择,可惜以上两个前提我都不具备;
    • (2) 选调:由于自己在本硕期间都有非常多的学生工作经历,目前也担任学院学生工作的重要职务,因此很长一段时间内都考虑直接参加中央或者省委选调工作,但是最终因为一些个人原因还是选择放弃;
    • (3) 技术:选择大多数人选择的方向,秋招最终还是主要聚焦于技术开发类的岗位,本文也主要介绍这方面的基本情况。

    面试情况

    和一些大牛相比,我的秋招面试经历并不算多,一共只投递了十几家公司,最终拿到了国网南瑞研究院、交通银行总行、阿里、腾讯、字节、华为、猿辅导、完美世界的 offer,具体的情况可以看下表:

    阿里

    • 流程:实习(笔试+5 面)+转正答辩
    • 结果:Offer

    阿里的面试总体来讲是比较标准的结构化面试,但是面试流程实在太长了,从投递简历到完成面试,大约历时 40-50 天。

    而实习转正后的 offer 发放等了大约 20 天,薪资沟通等了 50-60 天,贯穿秋招的头尾,非常考验心态。

    腾讯

    • 流程:笔试+3 面
    • 结果:Offer

    相比于头部的几家互联网公司面试,我个人感觉腾讯的面试反而是比较“水”的。两次专业面试都是电话面试,且以项目交流为主。

    腾讯的内推和自主投递没有流程上的区别,只是内推能在一开始锁定心仪的部门。我一开始不了解情况自主投递简历,导致后续无法内推,简历只能由部门随机锁定。捞我的部门是腾讯某著名游戏工作室,最终顺利拿到 offer。

    字节

    • 流程:实习(3 面)+ 绿色通道 1 面
    • 结果:Offer

    字节的整体面试流程非常紧凑,实习简历投递后第二天就开始沟通面试,一个下午直接完成 3 面,再隔一天就沟通 offer,还允许远程实习,因此计划 3~6 月在字节顺带实习 3 个月。

    由于当时不是暑假,实验室压力、学生工作压力、实习工作压力都聚集在一起,让我度过了极其痛苦的三个月,几乎每天都没有休息。

    6 月份我提出离职放弃转正答辩,在之后的校招过程中只参加一次专业面试就直接获得校招 offer。

    华为

    • 流程:笔试+3 面
    • 结果:Offer

    华为的面试流程感受还是非常友好的,会有 HR 单独联系,及时沟通面试进度和状态。

    另外令我惊讶的是多次主动沟通感兴趣的工作方向,并针对个人做出非常详细的职业规划,有一段时间几乎是每天打一次电话。

    最后的整体评级和薪资待遇也非常有诚意。华为的二面很有可能是压力面,只要保持心态就能顺利过关。

    国网南瑞

    • 流程:1 面
    • 结果:Offer

    因为来学校进行宣讲,所以现场投递了简历,面试 20 多分钟就直接通过了。南瑞是国家电网子公司,网络风评不太好,不过通过特批给了一个超出预期相对有诚意的待遇,不过相比互联网还是有较大差距。

    交行总行

    • 流程:免笔试免面试
    • 结果:Offer

    学校有人才推荐计划,填了一些表格交上去,随后安排了一次不到 10 分钟的面试,通知免笔试免面试直接参与体检环节,随后直接发 offer,薪资待遇都是统一的标准。

    猿辅导

    • 流程:笔试+3 面
    • 结果:Offer

    猿辅导号称是 WLB 的典范,一直宣称“年薪至少 40 万,7 点下班”,面试号称“具有挑战性”,但实际面试流程一周一面,且面试题目难度也很一般,无法深挖项目,只会简单的基础题问答和做题,每次面试两道题目左右,基本都是 leetcode 原题。

    完美世界

    • 流程:3 面
    • 结果:Offer

    完美世界 K-lab 计划号称 48 小时极速发 offer,由于是校招早期,因此就参与面试练习练习。

    由于还是北京疫情期间无法回校,所以安排远程面试超出了“48 小时”,但整体流程还是比较速度,面试结束后也很快收到意向书。

    网易

    • 流程:笔试+1 面
    • 结果:挂

    我投递的是网易有道的 Java 开发岗位,面试安排在出发回京返校前 1 个小时,1 面全程深挖各大技术栈的底层原理,面试官非常和蔼可亲,面试体验极佳,可惜我水平不高,一问三不知,过了两周流程就变灰了。

    快手

    • 流程:1 面
    • 结果:挂

    按照大多数人的经历,快手的面试基本也应该是一次性面完,我面试的是基础平台,在做题的时候出现了比较大的失误,偏离了题目重点,把问题复杂化,所以一面结束后面试官直接就说结束面试,“以后等消息”。

    商汤

    • 流程:笔试+3 面
    • 结果:放弃

    商汤的面试流程中规中矩,有 HR 专人对接,但是每次面试都要相隔一到两周之后才有消息,流程也拖得很长。

    有趣的是其中一次面试过程中面试官问我是否认识本科的一位同学,可能是也投递了同一部门。最终三面时由于已有更好的 offer,所以就直接放弃面试了。

    小结

    相比于身边的一些同学,我没有选择海投,而是在不同领域选择一些有特点的公司有针对性的投递简历,努力提高简历投递的“命中率”。

    秋招是一个长期的过程,在获得同领域一些比较满意的 offer 后,我就没有继续面试同领域的没有特殊优势的其他公司。

    这样做一方面减少了无效的面试次数,有更多时间进行有针对性的准备,也能兼顾实验室导师的工作;另一方面在最后选择的过程中也能突出每家公司的优势特色,选择时也更有区分度。

    二、校招结构化面试

    综合我的实习和校招面试经历,我认为准备面试应当包括五个方面,即自我介绍、基础知识、项目经历、原理解析和手写算法。

    1. 自我介绍

    自我介绍是几乎所有面试的第一步骤,自我介绍配合简历会给面试官建立第一印象。我们知道在平时生活中,如果你喜欢一个人,那么这个人做的一切都会是美好的,如果你讨厌一个人,那么不管他做什么你都会看不顺眼。

    面试中也是同理,一个好的初始印象可能会淡化之后面试中自己的失误,而把重点聚焦于自己的长处上。

    在我看来,一次自我介绍至少应该包括:

    (1) 基本信息,毕业院校;

    (2) 实习、项目、竞赛经历和成果;

    (3) 自己擅长的技术栈;

    一般在自我介绍时,面试官很可能在查看简历,这时候需要对面试官进行后续面试问题的引导。

    例如如果自己对某些课程掌握非常深入,可以在教育经历中简要谈谈自己的课程情况,如果对自己的一个项目准备非常充分,可以加大自我介绍时该项目的比重,但切忌一下子说完让面试官无问题可问,而是有意识的留一些常见问题的缺口,例如分布式、效率优化等关键词,并针对这些关键词着重准备。

    此外,注意避免一些常见的简历介绍误区,例如“精通”这类给自己挖坑的词汇。

    2. 基础知识

    对于一些企业的技术初面,面试官可能不会和你讨论项目的技术细节,而是已经准备好了一系列的面试题,此时面试就变成面试官读题,自己答题的环节。这类基础知识问答包括计算机网络、操作系统、计算机组成原理、语言特性、数据库原理等方面的内容。例如:

    • (网络)输入域名后的流程是什么?七/四层模型是怎样的?TCP 的拥塞控制方法是什么?
    • (操作系统)进程和线程的区别是什么?死锁的如何产生、避免?分段、分页与虚拟内存的系列问题、CPU 调度的系列问题等;
    • (计算机组成原理)指令执行的基本过程是什么?
    • (数据库原理)存储引擎的区别是什么?索引底层实现的原理是什么?
    • (语言特性)Java 垃圾回收机制是怎样的?Java 虚拟机包括哪些部分?Js 闭包的原理是什么?go routine 的调度是如何进行的?

    对于这些问题,最直接的办法就是直接看已有的面试题整理,在一些博客或是牛客论坛上有大量的总结材料,对于有一定基础的同学直接看材料就能基本回忆起之前所学的课程。

    近两年由于大家越来越善于背题,出题的难度也在逐渐增加,偶尔有一些确实不会的题目直接承认即可,也不用不懂装懂强行回答,反而可能引起面试官的反感。

    3. 项目经历

    投递技术开发类岗位的同学基本都需要准备一些拿得出手的项目。项目经历是最无法临时准备的部分,在一些企业中项目深挖讨论反而会占面试的大部分时间。

    在我看来,准备描述自己的项目经历可以包括以下几点:

    • (1) 描述清楚项目的背景和需要解决的问题;
    • (2) 用了什么样的技术方法;
    • (3) 项目取得了怎样的成果;
    • (4) 自己在项目中是怎样的角色,负责哪些工作;

    在我实习和秋招面试的过程中,尽管简历上列出了最具代表性的三个项目,但是每次详细介绍的项目实际只有一到两个。

    对于如何描述自己的项目经历,完全可以像自我介绍一样准备好时间稍长一些的介绍模版,并至少准备好回答如下问题:

    在这个项目中,你遇到的难点是什么?你是如何解决的?

    项目介绍本身并不需要回答这个问题,而是面试官基本都会问这个问题。

    此外,通过多次面试,我发现每个项目介绍后面试官所问的问题都是有限的几个,因此可以通过多次面试提前准备好更多的项目问题回答,在交流过程中展现出自己从容、清晰的一面。

    4. 原理解析

    在我看来,这是整个面试过程中非常容易加分的部分。我们可以根据自己已有的项目、自我介绍中频繁出现的关键词,用心准备两到三个可以深挖的点。

    这里的原理解析不是仅仅是自己“看过别人写的解析文档”,而是自己深入理解,并能“有条理地讲述给别人听”。

    可以选择的方向例如:Tomcat、Spring、Redis、Kafka 的架构和源码实现、数据库引擎的实现、操作系统内核的实现、分布式一致性算法的源码实现、以及其他在自己项目中出现的问题等。

    选择深入准备的方向并不是随机的,而是确实在自己的项目中发挥了重要用途,并解决实际问题的关键难点。如果说基础知识重在广度和准确性,那么原理解析就要重在深度和思考性,描述自己的理解和思考,并能经得起面试官“步步紧逼”的询问。

    准备好可以深入探讨的点后,就可以在自我介绍、项目介绍过程中有意识的挖坑,频繁提起关键词,并留下含糊的描述性语句吸引面试官提问。(石头注:哈哈,都是套路啊)

    而在交流的过程中,也无需完整背诵千字大论文,而是由上而下,从整体到局部逐步解释。如果面试官强行讨论自己不熟悉的领域,直接简短说明不太了解即可,长时间支支吾吾无法清晰表达反而会导致减分。

    5. 手写算法

    在秋招开始前,我最担心的就是手写代码这一环节,对比身边一些将 leetcode 题库刷完的同学,我刷过的题目数量可能只有零头,不过在手写代码上也没有出过严重的问题。

    在我看来,平时没有刷题习惯的同学也无需对这个环节太过担心,只要有针对性进行准备,基本都能顺利完成。

    在临时突击刷题方案中,“数量”并不是重要因素,“重复”才是重点,我比较推荐的一个刷题方案是:

    • (1) 专题练习阶段:按 leetcode 标签专题刷题,如字符串、DFS、动态规划、树、双指针、排序等,选择出现频率较高的简单和中等难度题目。对于常见的标签,做到能理解其常见解题思路即可;
    • (2) 精选题库阶段:可以选择 leetcode 热门 100 题,或者剑指 offer 练习题刷题,此时需要注意重复刷题,例如完整做完剑指 offer 练习题后再刷一遍,争取看到题目就能想到思路,独立快速完成题目;

    在秋招准备阶段,我个人一共刷了 150 题左右,在面试的手写算法环节基本都顺利完成。

    此外,在手写算法的过程中一定要注意代码规范,注意异常输入的处理和代码整洁性,另外:

    • 如果暂时没有思路,可以试图从面试官那里获取提示,部分面试官甚至可以接受换题的要求。
    • 如果有一些思路,可以尝试积极和面试官沟通获取一些提示。
    • 如果确信自己无法解决问题,那么要求提示或者换其他题目总比留白要好。/

    三、 offer 选择

    关于如何选择 offer,可能见仁见智,基本都会从薪资待遇、平台发展、城市选择、亲友关系、工作压力等很多方面打分权衡,但落实到实际中,我自己也根本无法确认每个部分的比重,有时候可能真的只有“follow your heart”。

    对于我自己来讲,最终纠结的主要是阿里、腾讯、华为三家公司。具体而言:

    • 从薪资待遇上来讲,三家公司基本都给到了 SSP,总包来看腾讯>华为>阿里;
    • 从地域来看,由于自己是 xx 人,选择的就业地点希望在江浙沪一带,三家公司的工作地点也都满足要求;
    • 从打听的工作时间来看,基本是腾讯>华为>阿里(仅是特定部门的工作时间,而非公司整体的工作时间);
    • 从部门业务来看,三家公司的业务都算比较核心且都能接受;
    • 从技术的契合程度来看,阿里>华为>腾讯,腾讯游戏需要自己完全转换技术栈,且发展方向稍有受限。

    此外,我也综合考虑了工作地所在城市的生活成本、亲友的期望等问题,把最终的候选公司确定为阿里、腾讯两家,尽管每家公司都有其优势和劣势,但至少我都能接受其中的任意一种选择。

    在漫长的纠结、沟通之后,我最后选取了最简单的方式:抛硬币。不管是开心接收抛硬币的结果,还是希望赶紧捡起来再抛一次,我都会知道自己内心真实的选择。

    后记

    数据结构和算法是重中之重,这里我跟大家推荐一本 Leetcode 算法笔记,质量还挺不错的,推荐给大家参考。获取方式,点赞此文后添加助手vx:bjmsb10 即可获取。

    ?

    图片?

    最后,求关注,求关注,求关注,希望能和大家积极交流讨论,一起学习、共同进步。

    推荐阅读

    程序员年薪百万的飞马计划你听说过吗?

    从事开发一年的程序员能拿到多少钱?

    程序员50W年薪的知识体系与成长路线。

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-26

    【mybatis-plus】什么是乐观锁?如何实现“乐观锁”

    “乐观锁”这个词以前我也没听过。上次在测试需求的时候,查询数据库发现有一个?version?字段,于是请教开发这个字干嘛使,

    人家回复我:乐观锁,解决并发更新用的。当时大家都忙,咱也不敢多问。

    今天就来折腾一下“乐观锁”。

    一、什么是乐观锁

    乐观锁其实用一句话来形容其作用就是:当要更新一条记录的时候,希望这条记录没有被别人更新,从而实现线程安全的数据更新。

    结合下场景,记得那是一张库存表,有一个字段记录商品库存,涉及多个地方都有可能去更新它:

    1. 程序A 查询到了这条数据,得到库存是800,准备+200更新成1000,但是还没更新。
    2. 程序B 也查询到了这条数据,得到库存是800,准备-200更新成600,并且提交更新了。

    那么,这时候A再提交更新之后,B就会发现明明是自己是800-200=600,怎么最后变成了1000?

    这就是因为A的事务导致了B的数据更新丢失。

    文字可能读起来比较晦涩,有请灵魂画手:

    ?

    正常情况下:

    • 按先后顺序是, A先更新成1000,然后B再拿1000-200,更新成800,这样B就没异议了。
    • 或者实在要2个同时更新,那也只能有一个成功,这样也没异议。

    二、MP来实现乐观锁

    乐观锁的实现,通过增加一个字段,比如version,来记录每次的更新。

    查询数据的时候带出version的值,执行更新的时候,会再去比较version,如果不一致,就更新失败。

    还是用之前的user表,增加了新的字段?version?。

    1.在实体类里增加对于的字段,并且加上自动填充(你也可以每次手动填充)

    @Data
    public class User {

    @TableId(type = IdType.ID_WORKER)
    private Long id;
    private String name;
    private Integer age;
    private String email;
    
    @TableField(fill = FieldFill.INSERT)        // 新增的时候填充数据
    private Date createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE) // 新增或修改的时候填充数据
    private Date updateTime;
    
    @TableField(fill = FieldFill.INSERT)
    @Version
    private Integer version; // 版本号

    }

    @Component //此注解表示 将其交给spring去管理
    public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        this.setFieldValByName("createTime", new Date(), metaObject);
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName("version", 0, metaObject); //新增就设置版本值为0
    }
    
    
    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("updateTime", new Date(), metaObject);
    }

    }

    1. 配置插件

    为了便于管理,可以见一个包,用于存放各种配置类,顺便把配置在启动类里的mapper扫描也换到这里来。

    package com.pingguo.mpdemo.config;

    import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    // 配置扫描mapper的路径
    @MapperScan("com.pingguo.mpdemo.mapper")
    public class MpConfig {

    // 乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }

    }

    3.测试乐观锁

    先新增一条测试数据:

    // 新增

    @Test
    void addUser() {
        User user = new User();
        user.setName("大周");
        user.setAge(22);
        user.setEmail("laowang@123.com");
        userMapper.insert(user);
    }
    

    新增成功,可以看到version值是0。

    ?

    再来试一下正常的修改:

    // 测试乐观锁

    @Test
    void testOptimisticLocker() {
        User user = userMapper.selectById(1342502561945915393L);
        user.setName("大周2");
        userMapper.updateById(user);
    }
    

    修改成功,可以看到version 变成了1。

    ?

    最后,模拟下并发更新,乐观锁更新失败的情况:

    // 测试乐观锁-失败

    @Test
    void testOptimisticLockerFailed() {
        User user = userMapper.selectById(1342502561945915393L);
        user.setName("大周3");
    
        User user2 = userMapper.selectById(1342502561945915393L);
        user2.setName("大周4");
    
        userMapper.updateById(user2); // 这里user2插队到user前面,先去更新
        userMapper.updateById(user); // 这里由于user2先做了更新后,版本号不对,所以更新失败
    
    }
    

    按照乐观锁的原理,user2是可以更新成功的,也就是name会修改为“大周4”,version会加1。user因为前后拿到的版本号不对,更新失败。

    ?

    结果符合预期,我们也可以看下mybatis的日志,进一步了解一下:

    可以看到上面首先是2个查询,查询到的version都是1。

    ?

    接着,第一个执行update语句的时候,where条件中version=1,可以找到数据,于是更新成功,切更新version=2。

    而第二个再执行update的时候,where条件version=1,已经找不到了,因为version已经被上面的更新成了2,所以更新失败。

    推荐阅读

    程序员年薪百万的飞马计划你听说过吗?

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    从事开发一年的程序员能拿到多少钱?

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-26

    Spring源码高级笔记之——Spring AOP应用

    Spring AOP应用

    AOP本质:在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、日志代码、事务控制代码、性能监控代码。

    第1节AOP相关术语

    1.1业务主线

    在讲解AOP术语之前,我们先来看一下下面这两张图,它们就是第三部分案例需求的扩展(针对这些扩展的需求,我们只进行分析,在此基础上去进一步回顾AOP,不进行实现)

    Spring源码高级笔记之——Spring AOP应用?

    上图描述的就是未采用AOP思想设计的程序,当我们红色框中圈定的方法时,会带来大量的重复劳动。程序中充斥着大量的重复代码,使我们程序的独立性很差。而下图中是采用了AOP思想设计的程序,它把红框部分的代码抽取出来的同时,运用动态代理技术,在运行期对需要使用的业务逻辑方法进行增强。

    Spring源码高级笔记之——Spring AOP应用?

    1.2 AOP 术语

    Spring源码高级笔记之——Spring AOP应用?

    连接点:方法开始时、结束时、正常运行完毕时、方法异常时等这些特殊的时机点,我们称之为连接点,项目中每个方法都有连接点,连接点是一种候选点

    切入点:指定AOP思想想要影响的具体方法是哪些,描述感兴趣的方法

    Advice增强:

    第一个层次:指的是横切逻辑

    第二个层次︰方位点(在某一些连接点上加入横切逻辑,那么这些连接点就叫做方位点,描述的是具体的特殊时机)

    Aspect切面:切面概念是对上述概念的一个综合

    Aspect切面=切入点+增强=切入点(锁定方法)+方位点(锁定方法中的特殊时机)+横切逻辑

    众多的概念,目的就是为了锁定要在哪个地方插入什么横切逻辑代码

    Spring中AOP的代理选择

    Spring 实现AOP思想使用的是动态代理技术

    默认情况下,Spring会根据被代理对象是否实现接口来选择使用JDK还是CGLIB。当被代理对象没有实现任何接口时,Spring会选择CGLIB。当被代理对象实现了接口,Spring会选择JDK官方的代理技术,不过我们可以通过配置的方式,让Spring强制使用CGLIB。

    Spring中AOP的配置方式

    在Spring的AOP配置中,也和loC配置一样,支持3类配置方式。

    第一类:使用XML配置

    第二类:使用XML+注解组合配置

    第三类:使用纯注解配置

    Spring中AOP实现

    需求∶横切逻辑代码是打印日志,希望把打印日志的逻辑织入到目标方法的特定位置(service层transfer方法)

    XML模式

    Spring是模块化开发的框架,使用aop就引入aop的jar

    • 坐标

    Spring源码高级笔记之——Spring AOP应用?

    • AOP核心配置

    Spring源码高级笔记之——Spring AOP应用?

    Spring源码高级笔记之——Spring AOP应用?

    • 细节
    • 关于切入点表达式

    上述配置实现了对TransferServiceImpl 的updateAccountByCardNo方法进行增强,在其执行之前,输出了记录日志的语句。这里面,我们接触了一个比较陌生的名称:切入点表达式,它是做什么的呢?我们往下看。

    • 概念及作用

    切入点表达式,也称之为AspectJ切入点表达式,指的是遵循特定语法结构的字符串,其作用是用于对符合语法格式的连接点进行增强。它是AspectJ表达式的一部分。

    • 关于AspectJ

    AspectJ是一个基于Java语言的AOP框架,Spring框架从2.0版本之后集成了AspectJ框架中切入点表达式的部分,开始支持AspectJ切入点表达式。

    • 切入点表达式使用示例

    Spring源码高级笔记之——Spring AOP应用?

    Spring源码高级笔记之——Spring AOP应用?

    • 改变代理方式的配置

    在前面我们已经说了,Spring在选择创建代理对象时,会根据被代理对象的实际情况来选择的。被代理对象实现了接口,则采用基于接口的动态代理。当被代理对象没有实现任何接口的时候,Spring会自动切换到基于子类的动态代理方式。

    但是我们都知道,无论被代理对象是否实现接口,只要不是final修饰的类都可以采用cglib提供的方式创建代理对象。所以Spring也考虑到了这个情况,提供了配置的方式实现强制使用基于子类的动态代理(即cglib的方式),配置的方式有两种

    • 使用aop:config标签配置

    Spring源码高级笔记之——Spring AOP应用?

    • 使用aop:aspectj-autoproxy标签配置

    Spring源码高级笔记之——Spring AOP应用?

    • 五种通知类型
    • 前置通知

    配置方式: aop:before标签

    Spring源码高级笔记之——Spring AOP应用?

    执行时机

    前置通知永远都会在切入点方法(业务核心方法)执行之前执行。

    细节

    前置通知可以获取切入点方法的参数,并对其进行增强。

    • 正常执行时通知

    配置方式

    Spring源码高级笔记之——Spring AOP应用?

    • 异常通知

    配置方式

    Spring源码高级笔记之——Spring AOP应用?

    执行时机

    异常通知的执行时机是在切入点方法(业务核心方法)执行产生异常之后,异常通知执行。如果切入点方法执行没有产生异常,则异常通知不会执行。

    细节

    异常通知不仅可以获取切入点方法执行的参数,也可以获取切入点方法执行产生的异常信息。

    • 最终通知

    配置方式

    Spring源码高级笔记之——Spring AOP应用?

    执行时机

    最终通知的执行时机是在切入点方法(业务核心方法)执行完成之后,切入点方法返回之前执行。换句话说,无论切入点方法执行是否产生异常,它都会在返回之前执行。

    细节

    最终通知执行时,可以获取到通知方法的参数。同时它可以做一些清理操作。

    • 环绕通知

    配置方式

    Spring源码高级笔记之——Spring AOP应用?

    Spring源码高级笔记之——Spring AOP应用?

    XML+注解模式

    • XML中开启Spring对注解AOP的支持

    Spring源码高级笔记之——Spring AOP应用?

    • 示例

    Spring源码高级笔记之——Spring AOP应用?

    Spring源码高级笔记之——Spring AOP应用?

    Spring源码高级笔记之——Spring AOP应用?

    注解模式

    在使用注解驱动开发aop时,我们要明确的就是,是注解替换掉配置文件中的下面这行配置:

    Spring源码高级笔记之——Spring AOP应用?

    在配置类中使用如下注解进行替换上述配置

    Spring源码高级笔记之——Spring AOP应用?

    Spring声明式事务的支持

    编程式事务:在业务代码中添加事务控制代码,这样的事务控制机制就叫做编程式事务声明式事务:通过xml或者注解配置的方式达到事务控制的目的,叫做声明式事务

    事务回顾

    事务的概念

    事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。从而确保了数据的准确与安全。

    例如:A——B转帐,对应于如下两条sql语句:

    Spring源码高级笔记之——Spring AOP应用?

    这两条语句的执行,要么全部成功,要么全部不成功。

    事务的四大特性

    原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

    从操作的角度来描述,事务中的各个操作要么都成功要么都失败

    一致性(Consistency)事务必须使数据库从一个一致性状态变换到另外一个一致性状态。例如转账前A有1000,B有1000。转账后A+B也得是2000。

    一致性是从数据的角度来说的,(1000,1000)(900,1100),不应该出现(900,1000)

    隔离性(lsolation)事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,每个事务不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

    比如:事务1给员工涨工资2000,但是事务1尚未被提交,员工发起事务2查询工资,发现工资涨了2000块钱,读到了事务1尚未提交的数据(脏读)

    持久性(Durability)

    持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

    事务的隔离级别

    不考虑隔离级别,会出现以下情况:(以下情况全是错误的),也即为隔离级别在解决事务并发问题

    脏读:一个线程中的事务读到了另外一个线程中未提交的数据。

    不可重复读:一个线程中的事务读到了另外一个线程中已经提交的update的数据(前后内容不一样)

    场景:

    员工A发起事务1,查询工资,工资为1w,此时事务1尚未关闭财务人员发起了事务2,给员工A张了2000块钱,并且提交了事务

    员工A通过事务1再次发起查询请求,发现工资为1.2w,原来读出来1w读不到了,叫做不可重复读虚读〈幻读)︰一个线程中的事务读到了另外一个线程中已经提交的insert或者delete的数据〈前后条数不一样)

    场景:

    事务1查询所有工资为1w的员工的总数,查询出来了10个人,此时事务尚未关闭

    事务2财务人员发起,新来员工,工资1 w,向表中插入了2条数据,并且提交了事务

    事务1再次查询工资为1w的员工个数,发现有12个人,见了鬼了

    数据库共定义了四种隔离级别:

    Serializable(串行化)︰可避免脏读、不可重复读、虚读情况的发生。(串行化)最高

    Repeatable read(可重复读)︰可避免脏读、不可重复读情况的发生。(幻读有可能发生)第二该机制下会对要update的行进行加锁

    Read committed(读已提交)︰可避免脏读情况发生。不可重复读和幻读一定会发生。第三

    Read uncommitted(读未提交)︰最低级别,以上情况均无法保证。(读未提交)最低

    注意:级别依次升高,效率依次降低

    MySQL的默认隔离级别是:REPEATABLE READ

    查询当前使用的隔离级别:select @@tx_isolation;

    设置MySQL事务的隔离级别: set session transaction isolation level xxx;(设置的是当前mysql连接会话的,并不是永久改变的)

    事务的传播行为

    事务往往在service层进行控制,如果出现service层方法A调用了另外一个service层方法B,A和B方法本身都已经被添加了事务控制,那么A调用B的时候,就需要进行事务的一些协商,这就叫做事务的传播行为。

    A调用B,我们站在B的角度来观察来定义事务的传播行为

    Spring源码高级笔记之——Spring AOP应用?

    Spring中事务的API

    mybatis: sqlSession.commit();

    hibernate: session.commit();

    PlatformTransactionManager

    Spring源码高级笔记之——Spring AOP应用?

    Spring源码高级笔记之——Spring AOP应用?

    作用

    此接口是Spring的事务管理器核心接口。Spring本身并不支持事务实现,只是负责提供标准,应用底层支持什么样的事务,需要提供具体实现类。此处也是策略模式的具体应用。在Spring框架中,也为我们内置了一些具体策略,例如:DataSourceTransactionManager,HibernateTransactionManager等等。(和HibernateTransactionManager事务管理器在spring-orm-5.1.12.RELEASE.jar中)Spring JdbcTemplate(数据库操作工具)、Mybatis (mybatis-spring.jar)—-——>

    DataSourceTransactionManager

    Hibernate框架——————> HibernateTransactionManager

    DataSourceTransactionManager归根结底是横切逻辑代码,声明式事务要做的就是使用Aop(动态代理)来将事务控制逻辑织入到业务代码

    Spring声明式事务配置

    • 纯xml模式
    • 导入jar

    Spring源码高级笔记之——Spring AOP应用?

    Spring源码高级笔记之——Spring AOP应用?

    • xml配置

    Spring源码高级笔记之——Spring AOP应用?

    • 基于XML+注解
    • xml配置

    Spring源码高级笔记之——Spring AOP应用?

    • 在接口、类或者方法上添加@Transactional注解

    Spring源码高级笔记之——Spring AOP应用?

    • 基于纯注解

    Spring基于注解驱动开发的事务控制配置,只需要把xml配置部分改为注解实现。只是需要一个注解替换掉xml配置文件中的<tx:annotation-driven transaction-

    manager="transactionManager" />配置。

    在Spring的配置类上添加@EnableTransactionManagement注解即可

    Spring源码高级笔记之——Spring AOP应用?

    今天的分享就先到这里,可以关注小编获得第一手资料哦!

    =

    推荐阅读

    **程序员年薪百万的飞马计划你听说过吗?
    **

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    从事开发一年的程序员能拿到多少钱?

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-25

    真的,千万不要给女朋友解释 什么是“羊群效应”

    千万别给自己女朋友以任何方式讲技术,问就是不知道,长寿秘诀~

    媳妇最近突然爱学习了,各种刷算法、架构方面的题,没日没夜的带娃还有这个劲头,着实让我没想到。看似一片欣欣向荣,不过,长期的生存经验告诉我,这并不是什么好事,事出反常必有妖~

    真的,千万不要给女朋友解释 什么是“羊群效应”?

    一个测试人员不变着花样找 bug,开始研究代码想制造 bug了,弯转的有点急啊,不过,不管怎么样渴望学习是好事。我这点水平忽悠她,那还不跟欺负小学生一样。

    那天突然问我:" 什么是 zookeeper 的羊群效应?",我有点惊讶,问的挺深入,看来这次是认真学了啊。那得赶紧讲不能打消人家的学习积极性。

    其实这是个挺简单的概念,羊群效应常在zookeeper实现分布式锁的场景中发生,建议没接触过ZK的同学先补习下基础知识《一文彻底搞懂 zookeeper 核心知识点》,分析一下zookeeper实现分布式锁的原理就更容易理解了,看下图:

    真的,千万不要给女朋友解释 什么是“羊群效应”?

    利用zookeeper独特的类似文件系统的数据结构,可以像创建文件夹一样随意创建节点my_lock,节点下可以创建子节点,节点还可以存储数据并生成有序自增的节点ID my_lock_00000001 .... my_lock_0000000N等。这样先创建的节点序号ID 就越小,谁的节点ID 最小则视为拿到锁,拿到锁的节点处理完业务后删除对应节点释放锁。

    而没拿到锁的线程通过设置watcher监控节点my_lock,一旦发现该节点下有线程释放锁删除子节点,其余?所有线程?重新获取my_lock下?全部子节点?比较自身节点是否为最小,最小则获得锁,一直如此重复,直到所有线程都拿到锁。

    那这样就产生一个现象,在整个分布式锁的竞争过程中,存在大量重复运行的动作,并且绝大多数都是无效操作,判断出自己并非是序号最小的节点,从而继续等待下一次通知,这就是所谓的 “羊群效应”。

    如果节点数量足够多,当删除一个节点大量客户端同时监听,比较自己自身节点是否为最小,就会产生大量的网络开销,会大大降低整个zookeeper集群的性能,所以必须对现有的分布式锁进行优化,如下图:

    真的,千万不要给女朋友解释 什么是“羊群效应”?

    既然只想判断自身是不是最小的节点,那么每次比较的时候,比如 my_lock_00000002 发现自己不是最小节点后,这时只要找到前一个节点my_lock_00000001 并watcher 监控它。当my_lock_00000001 释放锁删除节点,则会通知节点my_lock_00000002该你拿锁了,其他节点以此类推,这样有序监听就解决了“羊群效应”。

    真的,千万不要给女朋友解释 什么是“羊群效应”?

    吧啦吧啦半天,给我自己都讲嗨了,我问人家懂了嘛,她来一句:懂了一丢丢,要不你再讲一遍?

    对于这种颜值高过智商的选手,我决定换一种讲解思路,用一个故事打动她~

    咳~ 咳~ 咳~ 开始了

    学以致用

    未来的某一天,富仔(ZK)睁眼突然发现自己穿越到了大学时代,躺在某师范学院的宿舍床上,脸竟然还被换成了吴某凡的。这让原本贫瘠的颜值一下子达到巅峰,再也不用因为是班里唯一的男生,但又没女生喜欢而自卑了。

    帅归帅课还是要上的,不巧这天上课迟到了,富仔刚推开阶梯教室的大门,突然有个美女尖叫着大喊:“看,富仔今天好帅!”,顿时屋内一阵骚乱,大家左顾右盼,面面相觑。

    真的,千万不要给女朋友解释 什么是“羊群效应”?

    突然,众美女们一窝蜂的向他扑过来,这时有个叫杨某幂(线程1)的美女眼疾手快,一把抓住他的手,问能不能陪她去操场溜达一圈(处理业务),富仔这人心软,一看她楚楚可怜的样子就答应了,杨某幂立马拉着他的手飞奔向操场。其他的美女略显失落的回到座位。

    十分钟后,做完该做的,富仔还是心心念念着学业,执意坚持去上课,回到教室门口松开杨某幂的手,准备走向自己的座位。

    此时众美女们又一拥而上,这回是一个叫唐某嫣(线程2)的美女得手了,问富仔是不是也能陪她溜达一圈,富仔看她不是那么好看,委婉的拒绝了(不是最小)。

    后边的郑某爽(线程3)一把推开唐某嫣抓住富仔的手,问能不能陪她,富仔看着这妹子颜值不错,果断答应了~

    这样几次以后导员(ZK集群服务)看不下去了,严厉的与富仔交涉,虽然你的容貌惊为天人,但是你不能影响课堂纪律,同学们天天在教室练百米冲刺可不行,没法专心学习了。

    富仔一想觉得非常有道理,告诉妹子们不用天天盯着自己了,还是要专心学习。

    于是为全班妹子放了号,排了值日表,谁拿的号越靠前谁优先得到富仔溜达权,后边的人只要盯住(watcher)拿她前一个号的那个人就行,前边的人溜达完,后边的赶紧跟上,就这样富仔开始了没羞没臊的大学时光。

    真的,千万不要给女朋友解释 什么是“羊群效应”?

    我:这回懂了吗?

    啪~?一个大巴掌落我脑袋上了

    暴躁女友:你们在操场干什么了?

    我:......

    暴躁女友:你是不是早就有这想法了,想当皇帝是嘛,啊!?

    我:......

    以上故事纯属虚构,如有雷同算你牛批

    真的,千万不要给女朋友解释 什么是“羊群效应”?

    唠唠嗑

    如果有一丝收获,欢迎关注、点赞、转发,您的认可是我最大的动力。

    原文:https://mp.weixin.qq.com/s/Pi...

    =

    推荐阅读

    **为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了
    **

    从事开发一年的程序员能拿到多少钱?你酸了吗?

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-24

    万字详解 阿里面试真题:请你说说索引的原理

    前言

    相信每个IT界大佬,简历上少不了Mysql索引这个关键字,但如果被问起来,你能说出多少干货呢?先看下面几个问题测试一下吧:

    • 索引是怎么提高查询效率的?可以为了提高查询效率增加索引么?
    • mysql索引系统采用的数据结构是什么?
    • 为什么要使用B+树?
    • 聚集索引相对于非聚集索引的区别?
    • 什么是回表?
    • 什么是索引覆盖?
    • 什么是最左匹配原则?
    • 索引失效场景有哪些,如何避免?

    这些问题说不明白?不要慌!请带着问题向下看。

    ?

    1 索引原理探究

    什么是数据库索引?先来个官方一些的定义吧。

    在关系数据库中,索引是一种单独的、物理的数对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。

    这段话有点绕,其实把索引理解为图书目录,就非常好理解了。

    如果我们想在图书中查找特定内容,在没有目录的情况下只能逐页翻找。与此类似,当执行下面这样一条SQL语句时,假如没有索引,数据库如何查找到相对应的记录呢?

    SELECT?*?FROM?student?WHERE?name='叶良辰'

    搜索引擎只能扫描整个表的每一行,并依次对比判断name的值是否等于“叶良辰”。我们知道,单纯的内存运算是很快的,但从磁盘中取数据到内存中是相对慢的,当表中有大量数据时,内存与磁盘交互次数大大增加,这就导致了查询效率低下。

    1.1 B树与B+树

    相对于cpu和内存操作,磁盘IO开销很大,非常容易成为系统的性能瓶颈,因此计算机操作系统做了一些优化:

    当一次IO时,将相邻的数据也都读取到内存缓冲区内,而不是仅仅读取当前磁盘地址的数据。因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。

    为什么索引能提升数据库查询效率呢?根本原因就在于索引减少了查询过程中的IO次数。那么它是如何做到的呢?使用B+树。下面先简单了解一下B树和B+树。

    B树,即平衡多路查找树(B-Tree),是为磁盘等外存储设备设计的一种平衡查找树。

    B树简略示意图:

    ?

    观察上图可见B树的两个特点:

    1. 树内的每个节点都存储数据
    2. 叶子节点之间无指针连接

    B+树简略示意图:

    ?

    再看B+树相对于B树的两个特点:

    1. 数据只出现在叶子节点
    2. 所有叶子节点增加了一个链指针
    叶子结点是离散数学中的概念。一棵树当中没有子结点(即度为0)的结点称为叶子结点,简称“叶子”。叶子是指出度为0的结点,又称为终端结点。

    但是,为什么是B+树而不是B树呢?原因有两点:

    1. B树每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点能存储的key的数量很小,要保存同样多的key,就需要增加树的高度。树的高度每增加一层,查询时的磁盘I/O次数就增加一次,进而影响查询效率。而在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+树的高度。
    2. B+树的叶子节点上有指针进行相连,因此在做数据遍历的时候,只需要对叶子节点进行遍历即可,这个特性使得B+树非常适合做范围查询。

    1.2 聚簇索引与非聚簇索引

    首先,为了方便理解,我们先了解一下聚集索引(clustered index)和非聚集索引(secondary index,也称辅助索引或普通索引)。这两种索引是按存储方式进行区分的。

    聚集索引(clustered)也称聚簇索引,这种索引中,数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表的物理顺序只有一种情况,因此对应的聚集索引只能有一个。如果某索引不是聚集索引,则表中的行物理顺序与索引顺序不匹配,与非聚集索引相比,聚集索引有着更快的检索速度。

    如果不好理解,请看下面这个表:

    ?

    表中id和物理地址是保持一致顺序的,id较大的行,其物理地址也比较靠后。因为聚集索引的特性,它的建立有一定的特殊要求:

    1. 在Innodb中,聚簇索引默认就是主键索引。
    2. 如果表中没有定义主键,那么该表的第一个唯一非空索引被作为聚集索引。
    3. 如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,改列的值会随着数据的插入自增。
    大家还记得,自增主键和uuid作为主键的区别么?由于主键使用了聚集索引,如果主键是自增id,那么对应的数据一定也是相邻地存放在磁盘上的,写入性能比较高。如果是uuid的形式,频繁的插入会使innodb频繁地移动磁盘块,写入性能就比较低了。

    1.3 索引原理图示

    下面用一个通过主键索引查找数据的案例演示一下索引的原理。假如有student表如下,id上建立了聚集索引,name上建立非聚集索引:

    idnamescore2叶良辰784龙傲天8810赵日天5611徐胜虎77

    1.3.1 聚簇索引

    当我们执行下面的语句时,

    SELECT?name?FROM?student?WHERE?id=2

    查询过程如下图所示:

    ?

    用语言描述一下,是这样的:

    1. 先找到根节点所在磁盘块,读入内存。(第1次磁盘I/O操作)
    2. 在内存中判断id=3所在区间(0,8),找到该区间对应的指针1(第1次内存查找)
    3. 根据指针1记录的磁盘地址,找到磁盘块2并读入内存(第2次磁盘I/O操作)
    4. 在内存中判断id=3所在区间(0,4),找到该区间对应的指针2(第2次内存查找)
    5. 根据指针2记录的磁盘地址,找到磁盘块4并读入内存(第3次磁盘I/O操作)
    6. 在内存中查找到id=2对应的数据行记录(第3次内存查找)

    我们知道,磁盘I/O相对于内存运算(尤其内存中的主键是有序排列的,利用二分查找等算法效率非常高)耗时高得多,因此在数据库查询中,减少磁盘访问时数据库的性能优化的主要手段。

    而分析上面过程,发现整个查询只需要3次磁盘I/O操作(其实InnoDB引擎是将根节点常驻内存的,第1次磁盘I/O操作并不存在)和3次内存查找操作。相对于不使用索引的遍历式查找,大大减少了对磁盘的访问,因此查找效率大幅提高。但是,因为索引树要与表中数据保持一致,因此当表发生数据增删改时,索引树也要相应修改,导致写数据比没有索引时开销大一些。

    1.3.2 非聚簇索引

    好,聚集索引看完后,再看非聚集索引。

    ?

    如上图,多加一个索引,就会多生成一颗非聚簇索引树。因此,索引不能随意增加。在做写库操作的时候,需要同时维护这几颗树的变化,导致效率降低!

    另外,仔细观察的人一定会发现,不同于聚集索引,非聚集索引叶子节点上不再是真实数据,而是存储了索引字段自身值和主键索引。因此,当我们执行以下SQL语句时:

    SELECT?id,name?FROM?student?WHERE?name='叶良辰';

    整个查询过程与聚集索引的过程一样,只需要扫描一次索引树(n次磁盘I/O和内存查询),即可拿到想要的数据。

    但是,如果查询name索引树没有的数据时,情况就不一样了:

    SELECT?score?FROM?student?WHERE?name='叶良辰';

    ?

    注意看上图中的红色箭头,因为扫描完name索引后,Mysql只能获取到对应的id和name,然后用id的值再去聚集索引中去查询score的值。这个过程相对于聚集索引查询的效率下降,可以理解了吧。

    这就是通常所说的回表或者二次查询:使用聚集索引查询可以直接定位到记录,而普通索引通常需要扫描两遍索引树,即先通过普通索引定位到主键值,在通过聚集索引定位到行记录,这就是所谓的回表查询,它的性能比扫描一遍索引树低。

    既然普通索引会导致回表二次查询,那么有什么办法可以应对呢?建立联合索引!

    1.3.3 联合索引

    所谓联合索引,也称多列所谓,就是建立在多个字段上的索引,这个概念是跟单列索引相对的。联合索引依然是B+树,但联合索引的健值数量不是一个,而是多个。构建一颗B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建B+树。

    例如在a和b字段上建立联合索引,索引结构将如下图所示:

    ?

    一目了然,当我们再执行SELECT score FROM student WHERE name='叶良辰';时,可以直接通过扫描非聚集索引直接获取score的值,而不再需要到聚集索引上二次扫描了。

    最左前缀匹配

    联合索引中有一个重要的课题,就是最左前缀匹配。

    最左前缀匹配原则:在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。

    这是为什么呢?我们再仔细观察索引结构,可以看到索引key在排序上,首先按a排序,a相等的节点中,再按b排序。因此,如果查询条件是a或a和b联查时,是可以应用到索引的。如果查询条件是单独使用b,因为无法确定a的值,因此无法使用索引。

    假如在table表的a,b,c三个列上建立联合索引,简要分类分析下联合索引的最左前缀匹配。

    首先看等值查询:

    1、全值匹配查询时(where子句搜索条件顺序调换不影响索引使用,因为查询优化器会自动优化查询顺序 ),可以用到联合索引

    SELECT?*?FROM?table?WHERE?a=1?AND?b=3?AND?c=2
    SELECT?*?FROM?table?WHERE?b=3?AND?c=4?AND?a=2

    2、匹配左边的列时,可以用到联合索引

    SELECT?*?FROM?table?WHERE?a=1
    SELECT?*?FROM?table?WHERE?a=1?AND?b=3

    3、未从最左列开始时,无法用到联合索引

    SELECT?*?FROM?table?WHERE?b=1?AND?b=3

    4、查询列不连续时,无法使用联合索引(会用到a列索引,但c排序依赖于b,所以会先通过a列的索引筛选出a=1的记录,再在这些记录中遍历筛选c=3的值,是一种不完全使用索引的情况)

    SELECT?*?FROM?table?WHERE?a=1?AND?c=3

    再看范围查询:

    1、范围查询最左列,可以使用联合索引

    SELECT?*?FROM?table?WHERE?a>1?AND?a<5;

    2、精确匹配最左列并范围匹配其右一列(a值确定时,b是有序的,因此可以使用联合索引)

    SELECT?*?FROM?table?WHERE?a=1?AND?b>3;

    3、精确匹配最左列并范围匹配非右一列(a值确定时,c排序依赖b,因此无法使用联合索引,但会使用a列索引筛选出a>2的记录行,再在这些行中条件 c >3逐条过滤)

    SELECT?*?FROM?table?WHERE?a>2?AND?c>5;

    索引的原理探究到此结束,这部分内容堪称最难啃的骨头。不过,能坚持读下来的朋友,你的收获也一定良多。接下来的内容就轻松愉悦多了。

    ?

    2 索引的正确使用姿势

    索引的优点如下:

    • 通过创建唯一索引可以保证数据库表中每一行数据的唯一性。
    • 可以大大加快数据的查询速度,这是使用索引最主要的原因。
    • 在实现数据的参考完整性方面可以加速表与表之间的连接。
    • 在使用分组和排序子句进行数据查询时也可以显著减少查询中分组和排序的时间。

    既然索引这么好,那么我们是不是尽情使用索引呢?非也,索引优点明显,但相对应,也有缺点:

    • 创建和维护索引组要耗费时间,并且随着数据量的增加所耗费的时间也会增加。
    • 索引需要占磁盘空间,除了数据表占数据空间以外,每一个索引还要占一定的物理空间。
    • 当对表中的数据进行增加、删除和修改的时候,索引也要动态维护,这样就降低了数据的维护速度。

    因此,使用索引时要兼顾索引的优缺点,寻找一个最有利的平衡点。

    2.1 索引的类型区分

    以InnoDB引擎为例,Mysql索引可以做如下区分。

    首先,索引可以分为聚集索引和非聚集索引,它们的区别和含义在前文有大幅介绍,此处不再赘述。

    其次,从逻辑上,索引可以区分为:

    • 普通索引:普通索引是 MySQL 中最基本的索引类型,它没有任何限制,唯一任务就是加快系统对数据的访问速度。普通索引允许在定义索引的列中插入重复值和空值。
    • 唯一索引:唯一索引与普通索引类似,不同的是创建唯一性索引的目的不是为了提高访问速度,而是为了避免数据出现重复。唯一索引列的值必须唯一,允许有空值。如果是组合索引,则列值的组合必须唯一。创建唯一索引通常使用UNIQUE关键字。例如在student表中的id字段上建立名为index_id的索引CREATE UNIQUE INDEX index_id ON tb_student(id);
    • 主键索引:主键索引就是专门为主键字段创建的索引,也属于索引的一种。主键索引是一种特殊的唯一索引,不允许值重复或者值为空。创建主键索引通常使用PRIMARY KEY关键字。不能使用CREATE INDEX语句创建主键索引。
    • 空间索引:空间索引是对空间数据类型的字段建立的索引,空间索引主要用于地理空间数据类型 ,很少用到。
    • 全文索引:全文索引主要用来查找文本中的关键字,只能在CHAR、VARCHAR 或 TEXT类型的列上创建。在MySQL中只有MyISAM存储引擎支持全文索引。全文索引允许在索引列中插入重复值和空值。

    索引在实际使用上分为单列索引和多列索引。

    单列索引:单列索引就是索引只包含原表的一个列。在表中的单个字段上创建索引,单列索引只根据该字段进行索引。

    例如在student表中的address字段上建立名为index_addr的单列索引,address字段的数据类型为VARCHAR(20),索引的数据类型为CHAR(4)。SQL 语句如下:

    CREATE?INDEX?index_addr?ON?student(address(4)); 

    这样,查询时可以只查询 address 字段的前 4 个字符,而不需要全部查询。

    多列索引也称为复合索引或组合索引。相对于单列索引来说,组合索引是将原表的多个列共同组成一个索引。

    多列索引是在表的多个字段上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询。但是,只有查询条件中使用了这些字段中第一个字段时,索引才会被使用。

    下面在 student 表中的 name 和 address 字段上建立名为 index_na 的索引,SQL 语句如下:

    CREATE?INDEX?index_na?ON?tb_student(name,address); 

    该索引创建好了以后,查询条件中必须有 name 字段才能使用索引。

    一个表可以有多个单列索引,但这些索引不是组合索引。一个组合索引实质上为表的查询提供了多个索引,以此来加快查询速度。比如,在一个表中创建了一个组合索引(c1,c2,c3),在实际查询中,系统用来实际加速的索引有三个:单个索引(c1)、双列索引(c1,c2)和多列索引(c1,c2,c3)。

    2.2 索引的查看

    查看索引的语法格式如下:

    SHOW?INDEX?FROM?<表名>

    查询结果说明如下:

    ?

    2.3 索引的创建

    创建索引有3种方式:

    1、CREATE INDEX直接创建:

    可以使用专门用于创建索引的 CREATE INDEX 语句在一个已有的表上创建索引,但该语句不能创建主键。

    CREATE?<索引名>?ON?<表名>?(<列名>?[<长度>]?[?ASC?|?DESC])

    语法说明如下:

    • <索引名>:指定索引名。一个表可以创建多个索引,但每个索引在该表中的名称是唯一的。
    • <表名>:指定要创建索引的表名。
    • <列名>:指定要创建索引的列名。通常可以考虑将查询语句中在 JOIN 子句和 WHERE 子句里经常出现的列作为索引列。
    • <长度>:可选项。指定使用列前的 length 个字符来创建索引。使用列的一部分创建索引有利于减小索引文件的大小,节省索引列所占的空间。在某些情况下,只能对列的前缀进行索引。索引列的长度有一个最大上限 255 个字节(MyISAM 和 InnoDB 表的最大上限为 1000 个字节),如果索引列的长度超过了这个上限,就只能用列的前缀进行索引。另外,BLOB 或 TEXT 类型的列也必须使用前缀索引。
    • ASC|DESC:可选项。ASC指定索引按照升序来排列,DESC指定索引按照降序来排列,默认为ASC。

    例如,在student表name字段上创建索引:

    • 普通索引:CREATE INDEX index_name ON student (name)
    • 唯一索引:CREATE UNIQUE index_name ON student (name)

    创建普通索引使用的关键字,例如在student表name字段上创建一个普通索引index_name

    • 建表创建:CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,INDEX(name));
    • ALTER TABLE:ALTER student ADD INDEX index_name (name)

    2、CREATE TABLE时创建

    索引也可以在创建表(CREATE TABLE)的同时创建。在 CREATE TABLE 语句中添加以下语句。例如创建student表时在name字段添加索引:

    • 主键索引:CREATE TABLE student(name CHAR(45) PRIMARY KEY);
    • 唯一索引:CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,UNIQUE INDEX(name));
    • 普通索引:CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,INDEX(name));

    3、ALTER TABLE时创建

    ALTER TABLE 语句也可以在一个已有的表上创建索引。例如在student表name字段上创建一个普通索引index_name:

    • 主键索引:ALTER TABLE student ADD PRIMARY KEY (name);
    • 唯一索引:ALTER TABLE student ADD UNIQUE INDEX index_name(name);
    • 普通索引:ALTER TABLE student ADD INDEX index_name(name);

    2.4 索引失效场景

    创建了索引并不意味着高枕无忧,在很多场景下,索引会失效。下面列举了一些导致索引失效的情形,是我们写SQL语句时应尽量避免的。

    1、条件字段原因

    • 单字段有索引,WHERE条件使用多字段(含带索引的字段),例如SELECT * FROM student WHERE name ='张三' AND addr = '北京市'语句,如果name有索引而addr没索引,那么SQL语句不会使用索引。
    • 多字段索引,违反最佳左前缀原则。例如,student表如果建立了(name,addr,age)这样的索引,WHERE后的第一个查询条件一定要是name,索引才会生效。

    2、<>、NOT、in、not exists

    当查询条件为等值或范围查询时,索引可以根据查询条件去找对应的条目。否则,索引定位困难(结合我们查字典的例子去理解),执行计划此时可能更倾向于全表扫描,这类的查询条件有:<>、NOT、in、not exists

    3、查询条件中使用OR

    如果条件中有or,即使其中有条件带索引也不会使用(因此SQL语句中要尽量避免使用OR)。要想使用OR,又想让索引生效,只能将OR条件中的每个列都加上索引。

    4、查询条件使用LIKE通配符

    SQL语句中,使用后置通配符会走索引,例如查询姓张的学生(SELECT FROM student WHERE name LIKE '张%'),而前置通配符(SELECT FROM student WHERE name LIKE '%东')会导致索引失效而进行全表扫描。

    5、索引列上做操作(计算,函数,(自动或者手动)类型装换

    有以下几种例子:

    • 在索引列上使用函数:例如select from student where upper(name)='ZHANGFEI';会导致索引失效,而select from student where name=upper('ZHANGFEI');是会使用索引的。
    • 在索引列上计算:例如select * from student where age-1=17;

    6、在索引列上使用mysql的内置函数,索引失效

    例如,SELECT * FROM student WHERE create_time

    7、索引列数据类型不匹配

    例如,如果age字段有索引且类型为字符串(一般不会这么定义,此处只是举例)但条件值为非字符串,索引失效,例如SELECT * FROM student WHERE age=18会导致索引失效。

    8、索引列使用IS NOT NULL或者IS NULL可能会导致无法使用索引

    B-tree索引IS NULL不会使用索引,IS NOT NULL会使用,位图索引IS NULL、IS NOT NULL都会使用索引。

    最后,对索引的使用做一个总结吧:

    1. 索引有利于查询,但不能随意加索引,因为索引不仅会占空间,而且需要在写库时进行维护。
    2. 如果多个字段常常需要一起查询,那么在这几个字段上建立联合索引是个好办法,同时注意最左匹配原则。
    3. 不要在重复度很高的字段上加索引,例如性别。
    4. 避免查询语句导致索引失效

    推荐阅读

    **为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了
    **

    从事开发一年的程序员能拿到多少钱?

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-24

    摸透原理|一文带你了解 Redis 列表底层的实现方式

    上次我们分享?Redis 字符串的底层原理?,今天我们再来看下 Redis?List?列表的底层原理。

    Redis List 命令

    Redis?List?列表支持的相关指令比较多,比如单个元素增加、删除操作,也支持多个元素范围操作。

    Redis?List?列表支持列表表头元素插入/弹出(?LPUSH/LPOP?),也支持表尾元素插入/弹出(?RPUSH/RPOP?)。

    另外 Redis?List?列表还支持根据下标(?LINDEX?)获取元素,也支持根据根据下标覆盖相应的元素(?LSET?)。

    除此之外,Redis?List?列表还支持的范围操作,比如获取指定范围内全部元素(?LRANGE?),移除指定范围内的全部元素(?LTRIM?)。

    了解完的 Redis 相关指令,我们来看下 Redis?List?列表底层实现方式,使用两种数据结构:

    • 压缩列表(ziplist)
    • 双向列表(linkedlist)

    ?

    ps:本篇文章基于 Redis 3.2 开始进行讲解

    双向列表(linkedlist)

    上面我们知道了?List?列表支持表头/表尾元素的插入/弹出,这类操作使用链表那就非常高效,时间复杂度为 O(1)。

    Redis 双向列表(linkedlist) 由两个结构构成:

    • list
    • listnode

    结构如下:

    ?

    list?结构体中保存了表头节点,表尾节点以及链表包含的节点的数量,正因为如此操作表头/表尾元素的插入/弹出,链表长度的计算将会非常高效,时间复杂度为?O(1)?。

    listnode?结构体中除了保存节点的值以外,还会保存前后节点的指针,这样如果需要获取某个节点的前置节点与后置节点也会非常高效,时间复杂度为?O(1)?。

    另外如果需要指定位置插入/删除元素,那么只需要变动当前位置节点前后指针即可,这个插入/删除操作复杂度为?O(1)?。

    不过需要注意了,插入/删除动作前提我们需要找到这个指定位置,这个查找动作我们只能遍历链表,复杂度为?O(N)?,所以插入/删除的复杂度为?O(N)?。

    双向列表(linkedlist)除了用作在列表键以外,还广泛用于发布/订阅,慢查询等内部操作。

    既然双向列表(linkedlist)可以满足列表键的操作,那为什么 Redis 列表还采用其他的数据结构?

    其实主要是因为内存占用问题,双向链表由于使用两个结构体,而这两个结构体都需要保存一些必要信息,这必然将会占用部分内存。

    而当元素很少的时候,如果直接使用双向链表,内存还是比较浪费的。所以 Redis 引入压缩列表。

    压缩列表

    压缩列表是 Redis 为了节约内存而开发,它由一系列的特殊编码的的?连续内存块?组成的顺序型数据结构,整体结构如下:

    ?

    从上面结构可以看出来,压缩列表实际上类似与我们使用的数组,数组中每一个元素保存一个数据。

    不过与数组不同的是,压缩列表的表头存在三个字段

    zlbytes
    zltail
    zllen

    另外压缩列表的表尾还有一个字段,?zlend?里面保存一个特殊的值,?OXFE?,用于标记压缩列表的末端。

    一个压缩列表可以由多个节点构成,每个节点可以保存整数值或字节数组,结构如下:

    ?

    使用压缩列表,如果查找定位表头元素,我们只需要使用压缩列表起始地址加上表头三个字段长度就可以直接点位,查找非常快,复杂度是 O(1)。

    而压缩列表的最后一个元素,查找起来也非常轻松,我们使用压缩列表起始地址加上?zltail?包含的长度就可以直接点位,查找也非常快,复杂度是 O(1)。

    至于列表中的其他元素,就没有这么好运了,我们只能从第一个元素或者最后一个元素,遍历列表查找,此时的复杂度就是 O(N) 了。

    另外压缩列表的新增、删除元素,都将会导致重新分配内存,效率不高,平均复杂度为 O(N),最坏福复杂度为 O(N^2)。

    编码转换

    当我们创建一个 Redis 列表键,如果同时满足以下两个条件,列表对象将会使用压缩列表作为底层数据结构

    • 列表对象保存的所有字符串元素的长度都小于 64 字节
    • 列表对象中保存的元素数量小于 512 个

    如果不能同时满足这两个条件,那么默认将会使用双向列表作为底层数据结构。

    小结

    Redis 列表底层使用两种数据结构,压缩列表与双向链表。

    压缩列表由于使用了连续内存块,内存占用少,并且内存利用率高,但是新增、删除由于涉及重新分配内存,效率不高。

    双向列表呢,新增、删除元素非常方便,但是由于每个节点都是独立的内存快,内存占用比较高,且内存碎片化严重。

    这两种数据结构在表头/表尾插入与删除元素,都十分高效。但是其他操作,可能就效率较低。

    所以我们使用 Redis 列表,一定要因地制宜,可以将其当做 FIFO 队列,这样仅使用 POP/PUSH ,效率将会很高。

    原文:www.tuicool.com/articles/N7nQ73r

    参考资料

    1. Redis 设计与实现

    推荐阅读

    =====

    **从事开发一年的程序员能拿到多少钱?
    **

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    **程序员50W年薪的知识体系与成长路线。
    **

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-23

    SQL优化最干货总结-MySQL「2020年终总结版」

    前言

    BATJTMD等大厂的面试难度越来越高,但无论从大厂还是到小公司,一直未变的一个重点就是对SQL优化经验的考察。一提到数据库,先“说一说你对SQL优化的见解吧?”。

    SQL优化已经成为衡量程序员优秀与否的硬性指标,甚至在各大厂招聘岗位职能上都有明码标注,如果是你,在这个问题上能吊打面试官还是会被吊打呢?

    SQL优化最干货总结-MySQL「2020年终总结版」?

    注:如果看着模糊,可能是你撸多了

    目录

    • 前言
    • SELECT语句 - 语法顺序:
    • SELECT语句 - 执行顺序:
    • SQL优化策略
    • 一、避免不走索引的场景
    • 二、SELECT语句其他优化
    • 三、增删改 DML 语句优化
    • 四、查询条件优化
    • 五、建表优化
      • *

    有朋友疑问到,SQL优化真的有这么重要么?如下图所示,SQL优化在提升系统性能中是:(成本最低 && 优化效果最明显) 的途径。如果你的团队在SQL优化这方面搞得很优秀,对你们整个大型系统可用性方面无疑是一个质的跨越,真的能让你们老板省下不止几沓子钱。

    SQL优化最干货总结-MySQL「2020年终总结版」?

    • 优化成本:硬件>系统配置>数据库表结构>SQL及索引。
    • 优化效果:硬件<系统配置<数据库表结构<SQL及索引。
    String?result?=?"嗯,不错,";
    ?
    if?("SQL优化经验足")?{
    ????if?("熟悉事务锁")?{
    ????????if?("并发场景处理666")?{
    ????????????if?("会打王者荣耀")?{
    ????????????????result?+=?"明天入职"?
    ????????????}
    ????????}
    ????}
    }?else?{
    ????result?+=?"先回去等消息吧";
    }?
    ?
    Logger.info("面试官:"?+?result?); 

    别看了,上面这是一道送命题。

    好了我们言归正传,首先,对于MySQL层优化我一般遵从五个原则:

    1. 减少数据访问:设置合理的字段类型,启用压缩,通过索引访问等减少磁盘IO
    2. 返回更少的数据:只返回需要的字段和数据分页处理 减少磁盘io及网络io
    3. 减少交互次数:批量DML操作,函数存储等减少数据连接次数
    4. 减少服务器CPU开销:尽量减少数据库排序操作以及全表查询,减少cpu 内存占用
    5. 利用更多资源:使用表分区,可以增加并行操作,更大限度利用cpu资源

    总结到SQL优化中,就三点:

    • 最大化利用索引;
    • 尽可能避免全表扫描;
    • 减少无效数据的查询;

    理解SQL优化原理 ,首先要搞清楚SQL执行顺序:

    SELECT语句 - 语法顺序:

    1.?SELECT?
    2.?DISTINCT?<select_list>
    3.?FROM?<left_table>
    4.?<join_type>?JOIN?<right_table>
    5.?ON?<join_condition>
    6.?WHERE?<where_condition>
    7.?GROUP?BY?<group_by_list>
    8.?HAVING?<having_condition>
    9.?ORDER?BY?<order_by_condition>
    10.LIMIT?<limit_number>

    SELECT语句 - 执行顺序:

    FROM
    <表名> # 选取表,将多个表数据通过笛卡尔积变成一个表。
    ON
    <筛选条件> # 对笛卡尔积的虚表进行筛选
    JOIN <join, left join, right join...>?
    <join表> # 指定join,用于添加数据到on之后的虚表中,例如left join会将左表的剩余数据添加到虚表中
    WHERE
    <where条件> # 对上述虚表进行筛选
    GROUP BY
    <分组条件> # 分组
    <SUM()等聚合函数> # 用于having子句进行判断,在书写上这类聚合函数是写在having判断里面的
    HAVING
    <分组筛选> # 对分组后的结果进行聚合筛选
    SELECT
    <返回数据列表> # 返回的单列必须在group by子句中,聚合函数除外
    DISTINCT

    数据除重

    ORDER BY
    <排序条件> # 排序
    LIMIT
    <行数限制>

    SQL优化策略

    声明:以下SQL优化策略适用于数据量较大的场景下,如果数据量较小,没必要以此为准,以免画蛇添足。

    一、避免不走索引的场景

    1. 尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。如下:

    SELECT?*?FROM?t?WHERE?username?LIKE?'%陈%'

    优化方式:尽量在字段后面使用模糊查询。如下:

    SELECT?*?FROM?t?WHERE?username?LIKE?'陈%'

    如果需求是要在前面使用模糊查询,

    • 使用MySQL内置函数INSTR(str,substr) 来匹配,作用类似于java中的indexOf(),查询字符串出现的角标位置
    • 使用FullText全文索引,用match against 检索
    • 数据量较大的情况,建议引用ElasticSearch、solr,亿级数据量检索速度秒级
    • 当表数据量较少(几千条儿那种),别整花里胡哨的,直接用like '%xx%'。

    2. 尽量避免使用in 和not in,会导致引擎走全表扫描。如下:

    SELECT?*?FROM?t?WHERE?id?IN?(2,3)

    优化方式:如果是连续数值,可以用between代替。如下:

    SELECT?*?FROM?t?WHERE?id?BETWEEN?2?AND?3

    如果是子查询,可以用exists代替。如下:

    --?不走索引
    select?*?from?A?where?A.id?in?(select?id?from?B);
    --?走索引
    select?*?from?A?where?exists?(select?*?from?B?where?B.id?=?A.id);

    3. 尽量避免使用 or,会导致数据库引擎放弃索引进行全表扫描。如下:

    SELECT?*?FROM?t?WHERE?id?=?1?OR?id?=?3

    优化方式:可以用union代替or。如下:

    SELECT?*?FROM?t?WHERE?id?=?1
    ???UNION
    SELECT?*?FROM?t?WHERE?id?=?3

    4. 尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。如下:

    SELECT?*?FROM?t?WHERE?score?IS?NULL

    优化方式:可以给字段添加默认值0,对0值进行判断。如下:

    SELECT?*?FROM?t?WHERE?score?=?0

    5.尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。

    可以将表达式、函数操作移动到等号右侧。如下:

    --?全表扫描
    SELECT?*?FROM?T?WHERE?score/10?=?9
    --?走索引
    SELECT?*?FROM?T?WHERE?score?=?10*9

    6. 当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。如下:

    SELECT?username,?age,?sex?FROM?T?WHERE?1=1

    优化方式:用代码拼装sql时进行判断,没 where 条件就去掉 where,有where条件就加 and。

    搜索Java知音公众号,回复“后端面试”,送你一份Java面试题宝典.pdf

    7. 查询条件不能用 <> 或者 !=

    使用索引列作为条件进行查询时,需要避免使用<>或者!=等判断条件。如确实业务需要,使用到不等于符号,需要在重新评估索引建立,避免在此字段上建立索引,改由查询条件中其他索引字段代替。

    8. where条件仅包含复合索引非前置列

    如下:复合(联合)索引包含key_part1,key_part2,key_part3三列,但SQL语句没有包含索引前置列"key_part1",按照MySQL联合索引的最左匹配原则,不会走联合索引。

    select?col1?from?table?where?key_part2=1?and?key_part3=2

    9. 隐式类型转换造成不使用索引

    如下SQL语句由于索引对列类型为varchar,但给定的值为数值,涉及隐式类型转换,造成不能正确走索引。

    select?col1?from?table?where?col_varchar=123;

    10. order by 条件要与where中条件一致,否则order by不会利用索引进行排序

    --?不走age索引
    SELECT?*?FROM?t?order?by?age;
    ?
    --?走age索引
    SELECT?*?FROM?t?where?age?>?0?order?by?age;

    对于上面的语句,数据库的处理顺序是:

    • 第一步:根据where条件和统计信息生成执行计划,得到数据。
    • 第二步:将得到的数据排序。当执行处理数据(order by)时,数据库会先查看第一步的执行计划,看order by 的字段是否在执行计划中利用了索引。如果是,则可以利用索引顺序而直接取得已经排好序的数据。如果不是,则重新进行排序操作。
    • 第三步:返回排序后的数据。

    当order by 中的字段出现在where条件中时,才会利用索引而不再二次排序,更准确的说,order by 中的字段在执行计划中利用了索引时,不用排序操作。

    这个结论不仅对order by有效,对其他需要排序的操作也有效。比如group by 、union 、distinct等。

    11. 正确使用hint优化语句

    MySQL中可以使用hint指定优化器在执行时选择或忽略特定的索引。一般而言,处于版本变更带来的表结构索引变化,更建议避免使用hint,而是通过Analyze table多收集统计信息。但在特定场合下,指定hint可以排除其他索引干扰而指定更优的执行计划。

    1. USE INDEX 在你查询语句中表明的后面,添加 USE INDEX 来提供希望 MySQL 去参考的索引列表,就可以让 MySQL 不再考虑其他可用的索引。例子: SELECT col1 FROM table USE INDEX (mod_time, name)...
    2. IGNORE INDEX 如果只是单纯的想让 MySQL 忽略一个或者多个索引,可以使用 IGNORE INDEX 作为 Hint。例子: SELECT col1 FROM table IGNORE INDEX (priority) ...
    3. FORCE INDEX 为强制 MySQL 使用一个特定的索引,可在查询中使用FORCE INDEX 作为Hint。例子: SELECT col1 FROM table FORCE INDEX (mod_time) ...

    在查询的时候,数据库系统会自动分析查询语句,并选择一个最合适的索引。但是很多时候,数据库系统的查询优化器并不一定总是能使用最优索引。如果我们知道如何选择索引,可以使用FORCE INDEX强制查询使用指定的索引。

    例如:

    SELECT?*?FROM?students?FORCE?INDEX?(idx_class_id)?WHERE?class_id?=?1?ORDER?BY?id?DESC;

    二、SELECT语句其他优化

    1. 避免出现select *

    首先,select * 操作在任何类型数据库中都不是一个好的SQL编写习惯。

    使用select * 取出全部列,会让优化器无法完成索引覆盖扫描这类优化,会影响优化器对执行计划的选择,也会增加网络带宽消耗,更会带来额外的I/O,内存和CPU消耗。

    建议提出业务实际需要的列数,将指定列名以取代select *。

    2. 避免出现不确定结果的函数

    特定针对主从复制这类业务场景。由于原理上从库复制的是主库执行的语句,使用如now()、rand()、sysdate()、current_user()等不确定结果的函数很容易导致主库与从库相应的数据不一致。另外不确定值的函数,产生的SQL语句无法利用query cache。

    3.多表关联查询时,小表在前,大表在后。

    在MySQL中,执行 from 后的表关联查询是从左往右执行的(Oracle相反),第一张表会涉及到全表扫描,所以将小表放在前面,先扫小表,扫描快效率较高,再扫描后面的大表,或许只扫描大表的前100行就符合返回条件并return了。

    例如:表1有50条数据,表2有30亿条数据;如果全表扫描表2,你品,那就先去吃个饭再说吧是吧。

    4. 使用表的别名

    当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个列名上。这样就可以减少解析的时间并减少哪些友列名歧义引起的语法错误。

    5. 用where字句替换HAVING字句

    避免使用HAVING字句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤,而where则是在聚合前刷选记录,如果能通过where字句限制记录的数目,那就能减少这方面的开销。HAVING中的条件一般用于聚合函数的过滤,除此之外,应该将条件写在where字句中。

    where和having的区别:where后面不能使用组函数

    6.调整Where字句中的连接顺序

    MySQL采用从左往右,自上而下的顺序解析where子句。根据这个原理,应将过滤数据多的条件往前放,最快速度缩小结果集。

    三、增删改 DML 语句优化

    1. 大批量插入数据

    如果同时执行大量的插入,建议使用多个值的INSERT语句(方法二)。这比使用分开INSERT语句快(方法一),一般情况下批量插入效率有几倍的差别。

    方法一:

    insert?into?T?values(1,2);?
    ?
    insert?into?T?values(1,3);?
    ?
    insert?into?T?values(1,4);

    方法二:

    Insert?into?T?values(1,2),(1,3),(1,4);

    选择后一种方法的原因有三。

    • 减少SQL语句解析的操作,MySQL没有类似Oracle的share pool,采用方法二,只需要解析一次就能进行数据的插入操作;
    • 在特定场景可以减少对DB连接次数
    • SQL语句较短,可以减少网络传输的IO。

    2. 适当使用commit

    适当使用commit可以释放事务占用的资源而减少消耗,commit后能释放的资源如下:

    • 事务占用的undo数据块;
    • 事务在redo log中记录的数据块;
    • 释放事务施加的,减少锁争用影响性能。特别是在需要使用delete删除大量数据的时候,必须分解删除量并定期commit。

    3. 避免重复查询更新的数据

    针对业务中经常出现的更新行同时又希望获得改行信息的需求,MySQL并不支持PostgreSQL那样的UPDATE RETURNING语法,在MySQL中可以通过变量实现。

    例如,更新一行记录的时间戳,同时希望查询当前记录中存放的时间戳是什么,简单方法实现:

    Update?t1?set?time=now()?where?col1=1;?
    ?
    Select?time?from?t1?where?id?=1; 

    使用变量,可以重写为以下方式:

    Update?t1?set?time=now?()?where?col1=1?and?@now:?=?now?();?
    ?
    Select?@now; 

    前后二者都需要两次网络来回,但使用变量避免了再次访问数据表,特别是当t1表数据量较大时,后者比前者快很多。

    4.查询优先还是更新(insert、update、delete)优先

    MySQL 还允许改变语句调度的优先级,它可以使来自多个客户端的查询更好地协作,这样单个客户端就不会由于锁定而等待很长时间。改变优先级还可以确保特定类型的查询被处理得更快。我们首先应该确定应用的类型,判断应用是以查询为主还是以更新为主的,是确保查询效率还是确保更新的效率,决定是查询优先还是更新优先。

    下面我们提到的改变调度策略的方法主要是针对只存在表锁的存储引擎,比如 MyISAM 、MEMROY、MERGE,对于Innodb 存储引擎,语句的执行是由获得行锁的顺序决定的。MySQL 的默认的调度策略可用总结如下:

    1)写入操作优先于读取操作。

    2)对某张数据表的写入操作某一时刻只能发生一次,写入请求按照它们到达的次序来处理。

    3)对某张数据表的多个读取操作可以同时地进行。MySQL 提供了几个语句调节符,允许你修改它的调度策略:

    • LOW_PRIORITY关键字应用于DELETE、INSERT、LOAD DATA、REPLACE和UPDATE;
    • HIGH_PRIORITY关键字应用于SELECT和INSERT语句;
    • DELAYED关键字应用于INSERT和REPLACE语句。

    如果写入操作是一个 LOW_PRIORITY(低优先级)请求,那么系统就不会认为它的优先级高于读取操作。在这种情况下,如果写入者在等待的时候,第二个读取者到达了,那么就允许第二个读取者插到写入者之前。只有在没有其它的读取者的时候,才允许写入者开始操作。这种调度修改可能存在 LOW_PRIORITY写入操作永远被阻塞的情况。

    SELECT 查询的HIGH_PRIORITY(高优先级)关键字也类似。它允许SELECT 插入正在等待的写入操作之前,即使在正常情况下写入操作的优先级更高。另外一种影响是,高优先级的 SELECT 在正常的 SELECT 语句之前执行,因为这些语句会被写入操作阻塞。如果希望所有支持LOW_PRIORITY 选项的语句都默认地按照低优先级来处理,那么 请使用--low-priority-updates 选项来启动服务器。通过使用 INSERTHIGH_PRIORITY 来把 INSERT 语句提高到正常的写入优先级,可以消除该选项对单个INSERT语句的影响。

    四、查询条件优化

    1. 对于复杂的查询,可以使用中间临时表 暂存数据

    2. 优化group by语句

    默认情况下,MySQL 会对GROUP BY分组的所有值进行排序,如 “GROUP BY col1,col2,....;” 查询的方法如同在查询中指定 “ORDER BY col1,col2,...;” 如果显式包括一个包含相同的列的 ORDER BY子句,MySQL 可以毫不减速地对它进行优化,尽管仍然进行排序。

    因此,如果查询包括 GROUP BY 但你并不想对分组的值进行排序,你可以指定 ORDER BY NULL禁止排序。例如:

    SELECT?col1,?col2,?COUNT(*)?FROM?table?GROUP?BY?col1,?col2?ORDER?BY?NULL?;

    3. 优化join语句

    MySQL中可以通过子查询来使用 SELECT 语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询可以被更有效率的连接(JOIN)..替代。

    例子:假设要将所有没有订单记录的用户取出来,可以用下面这个查询完成:

    SELECT?col1?FROM?customerinfo?WHERE?CustomerID?NOT?in?(SELECT?CustomerID?FROM?salesinfo?)

    如果使用连接(JOIN).. 来完成这个查询工作,速度将会有所提升。尤其是当 salesinfo表中对 CustomerID 建有索引的话,性能将会更好,查询如下:

    SELECT?col1?FROM?customerinfo?
    ???LEFT?JOIN?salesinfoON?customerinfo.CustomerID=salesinfo.CustomerID?
    ??????WHERE?salesinfo.CustomerID?IS?NULL 

    连接(JOIN).. 之所以更有效率一些,是因为 MySQL 不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。

    4. 优化union查询

    MySQL通过创建并填充临时表的方式来执行union查询。除非确实要消除重复的行,否则建议使用union all。原因在于如果没有all这个关键词,MySQL会给临时表加上distinct选项,这会导致对整个临时表的数据做唯一性校验,这样做的消耗相当高。

    高效:

    SELECT?COL1,?COL2,?COL3?FROM?TABLE?WHERE?COL1?=?10?
    ?
    UNION?ALL?
    ?
    SELECT?COL1,?COL2,?COL3?FROM?TABLE?WHERE?COL3=?'TEST'; 

    低效:

    SELECT?COL1,?COL2,?COL3?FROM?TABLE?WHERE?COL1?=?10?
    ?
    UNION?
    ?
    SELECT?COL1,?COL2,?COL3?FROM?TABLE?WHERE?COL3=?'TEST';

    5.拆分复杂SQL为多个小SQL,避免大事务

    • 简单的SQL容易使用到MySQL的QUERY CACHE;
    • 减少锁表时间特别是使用MyISAM存储引擎的表;
    • 可以使用多核CPU。

    6. 使用truncate代替delete

    当删除全表中记录时,使用delete语句的操作会被记录到undo块中,删除记录也记录binlog,当确认需要删除全表时,会产生很大量的binlog并占用大量的undo数据块,此时既没有很好的效率也占用了大量的资源。

    使用truncate替代,不会记录可恢复的信息,数据不能被恢复。也因此使用truncate操作有其极少的资源占用与极快的时间。另外,使用truncate可以回收表的水位,使自增字段值归零。

    7. 使用合理的分页方式以提高分页效率

    使用合理的分页方式以提高分页效率 针对展现等分页需求,合适的分页方式能够提高分页的效率。

    案例1:

    select?*?from?t?where?thread_id?=?10000?and?deleted?=?0?
    ???order?by?gmt_create?asc?limit?0,?15;

    上述例子通过一次性根据过滤条件取出所有字段进行排序返回。数据访问开销=索引IO+索引全部记录结果对应的表数据IO。因此,该种写法越翻到后面执行效率越差,时间越长,尤其表数据量很大的时候。

    适用场景:当中间结果集很小(10000行以下)或者查询条件复杂(指涉及多个不同查询字段或者多表连接)时适用。

    案例2:

    select?t.*?from?(select?id?from?t?where?thread_id?=?10000?and?deleted?=?0
    ???order?by?gmt_create?asc?limit?0,?15)?a,?t?
    ??????where?a.id?=?t.id; 

    上述例子必须满足t表主键是id列,且有覆盖索引secondary key:(thread_id, deleted, gmt_create)。通过先根据过滤条件利用覆盖索引取出主键id进行排序,再进行join操作取出其他字段。数据访问开销=索引IO+索引分页后结果(例子中是15行)对应的表数据IO。因此,该写法每次翻页消耗的资源和时间都基本相同,就像翻第一页一样。

    适用场景:当查询和排序字段(即where子句和order by子句涉及的字段)有对应覆盖索引时,且中间结果集很大的情况时适用。

    五、建表优化

    1. 在表中建立索引,优先考虑where、order by使用到的字段。

    2. 尽量使用数字型字段(如性别,男:1 如:2),若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。

    这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

    3. 查询数据量大的表 会造成查询缓慢。主要的原因是扫描行数过多。这个时候可以通过程序,分段分页进行查询,循环遍历,将结果合并处理进行展示。要查询100000到100050的数据,如下:

    SELECT?*?FROM?(SELECT?ROW_NUMBER()?OVER(ORDER?BY?ID?ASC)?AS?rowid,*?
    ???FROM?infoTab)t?WHERE?t.rowid?>?100000?AND?t.rowid?<=?100050 

    4. 用varchar/nvarchar 代替 char/nchar

    尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

    不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL也包含在内),都是占用 100个字符的空间的,如果是varchar这样的变长字段, null 不占用空间。

    推荐阅读

    价值6千元的:MySQL从入门到进阶教程免费分享

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 2 收藏 2 评论 0

    MrZ 发布了文章 · 2020-12-23

    程序员科普时间:Kafka 不是数据库

    理解流式基础设施的使用和滥用,这一点很重要。

    Kafka 是一种消息代理,在过去几年中迅速流行起来。消息代理已经存在很长时间了,它们是一种专门用于在生产者和消费者系统之间“缓冲”消息的数据存储。Kafka 已经相当流行,因为它是开源的,并且能够支持海量的消息。

    消息代理通常用于解耦数据的生产者和消费者。例如,我们使用一个类似 Kafka 的消息代理来缓冲客户生成的 Webhook,然后将它们批量加载到数据仓库中。

    ?

    在这个场景中,消息代理提供了从客户发送事件到 Fivetran 将它们加载到数据仓库之间的事件持久存储。

    但是,Kafka 有时候也被描述为是一种比消息代理更大的东西。这个观点的支持者将 Kafka 定位为一种全新的数据管理方式,Kafka 取代了关系数据库,用于保存事件的最终记录。与读写传统数据库不同,在 Kafka 中,先是追加事件,然后从表示当前状态的下游视图中读取数据。这种架构被看成是对“数据库的颠覆”。

    原则上,以一种同时支持读和写的方式实现这个架构是有可能的。但是,在这个过程中,最终会遇到数据库管理系统几十年来遇到的所有难题。你或多或少需要在应用程序层开发一个功能齐全的 DBMS,而你可能不会做得太好,毕竟一个数据库需要很多年才能做好。你需要处理脏读、幻读、写偏移等问题,还要应付匆忙实现的数据库存在的所有其他问题。

    ACID 困境

    将 Kafka 作为数据存储的一个最基本的问题是它没有提供隔离机制。隔离意味着在全局内,所有事务(读和写)都是沿着某些一致的历史记录发生的。Jepsen 提供了一个隔离级别指南(?https://jepsen.io/consistency))?。

    我们举一个简单的例子来说明为什么隔离很重要:假设我们正在运营一个在线商店。当用户结账时,我们要确保他们下的订单都有足够的库存。我们是这样做的:

    1. 检查用户购物车中每个物品的库存水平。
    2. 如果某个物品没有库存,则中止结账。
    3. 如果所有物品都有库存,从库存中减去它们,并确认。

    假设我们使用 Kafka 来实现这个流程。我们的架构可能看起来像这样:

    ?

    Web 服务器从 Kafka 下游的库存视图读取库存,但它只能在 Checkouts 主题的上游提交事务。问题在于并发控制:如果有两个用户争着购买最后一件商品,那么只有一个用户可以购买成功。我们需要读取库存视图,并在一个单独的时间点确认结帐。但是,在这个架构中没有办法做到这一点。

    我们现在遇到的问题叫做写偏移。当结账事件被处理时,从库存视图中读取的数据可能已经过时。如果两个用户同时尝试购买相同的物品,他们都将购买成功,那么我们便没有足够的库存供应给他们。

    这种基于事件溯源的架构存在很多类似这样的隔离异常,让用户感到很困惑。更糟糕的是,研究表明,允许异常存在的架构也存在安全漏洞,给了黑客窃取数据的机会,正如这篇文章(?https://www.cockroachlabs.com/blog/acid-rain?)所写的那样。

    将 Kafka 作为传统数据库的补充

    如果你只是将 Kafka 作为传统数据库的补充,这些问题就可以避免:

    ?

    OLTP 数据库负责执行消息代理不太擅长的关键任务:事件的准入控制。与将消息代理作为“触发并遗忘”事件的容器不同,OLTP 数据库可以拒绝冲突性事件,确保只接收一个具有一致性的事件流。OLTP 数据库在这一核心并发控制任务上做得非常出色——可扩展到每秒处理数百万个事务。

    当使用数据库作为数据入口,从数据库读取事件的最佳方法是通过 CDC(变更数据捕获)。市场上有几个很棒的 CDC 框架,例如 Debezium(?http://debezium.io/?)和 Maxwell(?http://maxwells-daemon.io/?),以及来自现代 SQL 数据库的原生 CDC。CDC 还提供了优雅的运维解决方案。在进行数据恢复时,可以清除下游的所有内容,并从(持久化的)OLTP 数据库重新构建。

    不要随意构建错误的数据库

    几十年来,数据库社区已经总结了一些重要的经验教训。这些教训都是在造成数据损坏、数据丢失和让用户遭受损失的情况下获得的,并为此付出了惨重的代价。如果你不小心构建了一个错误的数据库,那么你会发现自己只不过是在重新经历这些经验教训。

    实时流式消息代理是管理快速变化的数据的一个很好的工具,但你仍然需要一个传统的 DBMS 来实现事务隔离。要实现一个“颠覆性的数据库”,可以使用 OLTP 数据库进行准入控制,使用 CDC 进行事件生成,并将数据的下游副本变成物化视图。

    原文链接:?https://materialize.com/kafka-is-not-a-database

    推荐阅读

    MySQL从入门到进阶教程

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-23

    震闻:2021年 微服务 即将被这个取代了!!

    “Serverless 能取代微服务吗?” 这是知乎上 Serverless 分类的高热话题。

    有人说微服务与 Serverless 是相背离的,虽然我们可以基于 Serverless 后端来构建微服务,但在微服务和 Serverless 之间并不存在直接的路径。

    也有人说,因为 Serverless 内含的 Function 可以视为更小的、原子化的服务,天然地契合微服务的一些理念,所以 Serverless 与微服务是天作之合。

    马上就要 2021 年了,Serverless 是否终将取代微服务?从微服务到 Serverless 需要经过怎样的路径??在我们深入探讨细节之前,先别急着“站队”,不妨先基于你团队的实际情况,真实的去思考是否适合使用微服务,千万不要因为 "这是趋势 "而去做选择。

    微服务在 Serverless 中的优势

    Serverless

    1.可选择的可扩展性和并发性

    Serverless 让管理并发性和可扩展性变得容易。在微服务架构中,我们最大限度地利用了这一点。每一个微服务都可以根据自己的需求对并发性/可扩展性进行设置。从不同的角度来看这非常有价值:比如减轻 DDoS 攻击可能性,降低云账单失控的财务风险,更好地分配资源......等等。

    2.细粒度的资源分配

    因为可扩展性和并发性可以自主选择,用户可以细粒度控制资源分配的优先级。在 Lambda functions 中,每个微服务都可以根据其需求,拥有不同级别的内存分配。比如,面向客户的服务可以拥有更高的内存分配,因为这将有助于加快执行时间;而对于延迟不敏感的内部服务,就可以用优化的内存设置来进行部署。

    这一特性同样适用于存储机制。比如 DynamoDB 或 Aurora Serverless 数据库就可以根据所服务的特定(微)服务的需求,拥有不同级别的容量分配。

    3.松耦合

    这是微服务的一般属性,并不是 Serverless 的独有属性,这个特性让系统中不同功能的组件更容易解耦。

    4.支持多运行环境

    Serverless 功能的配置、部署和执行的简易性,为基于多个运行时的系统提供了可能性。

    虽然 Node.js (JavaScript 运行时)是后端 Web 应用最流行的技术之一,但它不可能成为每一项任务的最佳工具。对于数据密集型任务、预测分析和任何类型的机器学习,你可能选择 Python 作为编程语言;像 SageMaker 这样的专用平台更适合大项目。

    有了 Serverless 基础架构,你无需在操作方面花费额外的精力就可以直接为常规后端 API 选择 Node.js,为数据密集型工作选择 Python。显然,这可能会给你的团队带来代码维护和团队管理的额外工作。

    5.开发团队的独立性

    不同的开发者或团队可以在各自的微服务上工作、修复 bug、扩展功能等,做到互不干扰。比如 AWS SAM、Serverless 框架等工具让开发者在操作层面更加独立。而 AWS CDK 构架的出现,可以在不损害高质量和运维标准的前提下,让开发团队拥有更高的独立性。

    微服务在 Serverless 中的劣势

    Serverless

    1.难以监控和调试

    在 Serverless 带来的众多挑战中,监控和调试可能是最有难度的。因为计算和存储系统分散在许多不同的功能和数据库中,更不用说队列、缓存等其他服务了,这些问题都是由微服务本身引起的。不过,目前已经有专业的平台可以解决所有这些问题。那么,专业的开发团队是否要引入这些专业平台也应该基于成本进行考量。

    2.可能经历更多冷启动

    当 FaaS 平台(如 Lambda)需要启动一个新的虚拟机来运行函数代码时,就会发生冷启动。如果你的函数 Workload 对延迟敏感,就很可能会遇到问题。因为冷启动会在总启动时间中增加几百毫秒到几秒的时间,当一个请求完成后,FaaS 平台通常会让 microVM 空闲一段时间,等待下一个请求,然后在 10-60 分钟后关闭(是的,变化很大)。结果是:你的功能执行的越频繁,microVM 就越有可能为传入的请求而启动并运行(避免冷启动)。

    当我们将应用分散在数百个或数千个微服务中时,我们可能在每个服务中分散调用时间,导致每个函数的调用频率降低。注意 “可能会分散调用”。根据业务逻辑和你的系统行为方式,这种负面影响可能很小,或者可以忽略不计。

    3.其他缺点

    微服务概念本身还存在其他固有的缺点。这些并不是与 Serverless 有内在联系的。尽管如此,每一个采用这种类型架构的团队都应该谨慎,以降低其潜在的风险和成本。

    • 确定服务边界并非易事,可能会招致架构问题。
    • 更广泛的攻击面
    • 服务编排费用问题
    • 同步计算和存储(在需要的时候)是不容易做到高性能和可扩展

    微服务在 Serverless 中的挑战和实践

    Serverless

    1.Serverless 中微服务应该多大?

    人们在理解 Servrless 时,"?Function as a Services(FaaS)?" 的概念很容易与编程语言中的函数语句相混淆。目前,我们正在处在一个没有办法划出完美界限的时期,但经验表明,使用非常小的 Serverless 函数并不是一个好主意。

    当你决定将一个(微)服务分拆成独立的功能时,你就将不得不面对 Serverless 难题。因此,在此提醒,只要有可能,将相关的逻辑保持在一个函数中会好很多。

    当然,决策过程也应该考虑拥有一个独立的微服务的优势

    你可以这样设想:“如果我把这个微服务分拆出来......”

    • 它能让不同的团队独立工作吗?
    • 能否从细粒度的资源分配或选择性的扩展能力中获益?

    如果不能,你应该考虑将这个服务与另一个需要类似资源、上下文关联并执行相关 Workload 的服务捆绑在一起。

    2.松耦合的架构

    通过组成 Serverless 函数来协调微服务的方法有很多。

    当需要同步通信时,可以直接调用(即 AWS Lambda RequestResponse 调用方法),但这会导致高度耦合的架构。更好的选择是使用 Lambda Layers 或 HTTP API,这样可以让以后的修改或迁移服务对客户端不构成影响。

    对于接受异步通信模型,我们有几种选择,如队列(SQS)、主题通知(SNS)、Event Bridge 或者 DynamoDB Streams。

    3.跨组件隔离

    理想情况下,微服务不应向使用者暴露细节。像 Lambda 这样的 Serverless 平台会提供一个 API 来隔离函数。但这本身就是一种实现细节的泄露,理想情况下,我们会在函数之上添加一个不可知的 HTTP API 层,使其真正隔离。

    4.使用并发限制和节流策略的重要性

    为了减轻 DDoS 攻击,在使用 AWS API Gateway 等服务时,一定要为每个面向公众的终端设置单独的并发限制和节流策略。这类服务一般在云平台中会为整个区域设置全局并发配额。如果你没有基于端点的限制,攻击者只需要将一个单一的端点作为攻击目标,就可以耗尽你的配额,并让你在该区域的整个系统瘫痪。

    推荐阅读

    手撕Spring源码系列】带你从入门到精通

    程序员50W年薪的知识体系与成长路线。

    为什么阿里巴巴的程序员成长速度这么快[
    ](https://www.bilibili.com/vide...

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-22

    由于不知线程池的bug,某Java程序员叕被祭天

    池化技术常用于缓存创建性能开销较大的对象,即事先创建一些对象成为池中之物,使用时再从池中捞出,用完归还以复用。

    手动声明线程池

    JDK的Executors工具类定义了很多便捷的方法可以快速创建线程池。
    图片

    但是阿里有话说:
    图片
    他说的弊端案例真的这么严重吗?

    newFixedThreadPool 导致?OOM

    初始化一个单线程的FixedThreadPool,向线程池提交任务,每个任务都会创建个较大字符串然后休眠

    图片

    执行程序后不久OOM:

    Exception in thread "http-nio-45678-ClientPoller" 
     java.lang.OutOfMemoryError: GC overhead limit exceeded

    图片

    newFixedThreadPool线程池的工作队列直接new个LinkedBlockingQueue
    图片

    其默认构造器竟然是一个Integer.MAX_VALUE长度的队列!所以很快就队列满了
    图片

    虽然使用newFixedThreadPool可以固定工作线程数量,但任务队列几乎无界。如果任务较多且执行较慢,队列就会快速积压,内存不够就很容易导致OOM。

    newCachedThreadPool导致OOM

    [11:30:30.487] [http-nio-45678-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread] with root cause
    java.lang.OutOfMemoryError: unable to create new native thread

    OOM是因为无法创建线程,newCachedThreadPool这种线程池的最大线程数是Integer.MAX_VALUE,基本无上限,而其工作队列SynchronousQueue是一个没有存储空间的阻塞队列。
    所以只要有请求到来,就必须找到一条工作线程处理,若当前无空闲线程就再创建一个新的。

    由于我们的任务需1小时才能执行完成,大量任务进来后会创建大量的线程。我们知道线程是需要分配一定的内存空间作为线程栈的,比如1MB,因此无限创建线程必然会导致OOM:

    public static ExecutorService newCachedThreadPool() {
     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
     60L, TimeUnit.SECONDS,
     new SynchronousQueue<Runnable>());

    参考

    • 《阿里巴巴Java开发手册》
    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-22

    被阿里、腾讯、华为追捧为最牛逼的 Java 框架你知道是什么吗?

    做Java的都知道,Spring是现在最牛逼的 Java 框架,没有之一!在实际研发中,Spring是我们经常会使用的框架,毕竟它们太火了,Spring相关的知识点也是面试必问点。

    有次被问到Spring IOC的时候,就顺带打了个比方,IOC类似于一个菜筐,以前需要自己去菜市场买菜,现在只要在家门口放一个菜框applicationcontext.xml,就会有人来给你配菜。

    除了IOC之外,AOP也是Spring整个系列的核心的概念。简而言之,将对象创建过程的职责赋予容器,通过容器管理对象的生老病死。IOC是factory加上依赖管理,通过IOC, AOP事实上形成了系统的整合。

    简言之,Spring 早已成为 Java 后端开发的行业标准,大量公司选择 Spring 作为基础的开发框架, Java 后端程序员在日常工作中也会经常接触到。因此,如何用好 Spring ,也就成为了Java程序员的必修课之一。

    但是在实际学习和使用的过程中,总是会免不了遇到这样一些问题:

    • 在代码之外,看着一堆依赖和配置,总是有些摸不清方向;
    • 看着这么多组件,总是不知道该从何入手;
    • 网上找了一些教程,觉得写得很不错,但只是介绍了某一个框架,那在面对一打框架的时候,该怎么把它们结合到一起

    所以,不仅要会用Spring框架,最重要的还是要解决平常在工作中的“怎么办”的问题?

    比如说:

    1、Spring为什么要用“三级缓存”去解决循环依赖呢?每级缓存的作用是什么?如果去掉其中某一级缓存会出现什么问题?如果一个单例bean和原型bean相互依赖会有问题吗......

    2、Mybatis和Spring进行整合时用到了哪些扩展点?如何利用的?为什么Mybatis和Spring整合后Mybatis的一级缓存会失效?

    如何学习

    书籍+视频+实战,这才是学习阅读源码的正确操作;

    接下来给大家推荐蚂蚁金服P8大佬整理的MyBatis与Springboot 两本学习笔记和一整套视频;

    这两本书籍是作者从毕业进入蚂蚁金服就开始编写的工作笔记,里面主要记载了从零基础到源码的全过程,由于篇幅原因下面就给大家仅仅展示下目录,有需要完整版的朋友可以?点击此处 免费领取;

    MyBatis源码笔记目录

    ?

    SpringBoot进阶笔记

    ?

    视频目录

    ?

    写在最后

    上面这一整套学习资料已经整理完毕,如果有需要的朋友可以关注公众号【Java斗帝】回复666 免费获取;

    最后附上看源码的心得

    ?

    推荐阅读

    =====

    程序员50W年薪的知识体系与成长路线。

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-22

    SQL:我为什么慢你心里没数吗?

    SQL 语句执行慢的原因是面试中经常会被问到的,对于服务端开发来说也是必须要关注的问题。

    在生产环境中,SQL 执行慢是很严重的事件。那么如何定位慢 SQL、慢的原因及如何防患于未然。接下来带着这些问题让我们开启本期之旅!

    ?

    • 思维导图 -

    写操作

    作为后端开发,日常操作数据库最常用的是写操作和读操作。读操作我们下边会讲,这个分类里我们主要来看看写操作时为什么会导致 SQL 变慢。

    刷脏页

    脏页的定义是这样的:内存数据页和磁盘数据页不一致时,那么称这个内存数据页为脏页。

    那为什么会出现脏页,刷脏页又怎么会导致 SQL 变慢呢?那就需要我们来看看写操作时的流程是什么样的。

    对于一条写操作的 SQL 来说,执行的过程中涉及到写日志,内存及同步磁盘这几种情况。

    ?

    • Mysql 架构图 -

    这里要提到一个日志文件,那就是 redo log,位于存储引擎层,用来存储物理日志。在写操作的时候,存储引擎(这里讨论的是 Innodb)会将记录写入到 redo log 中,并更新缓存,这样更新操作就算完成了。后续操作存储引擎会在适当的时候把操作记录同步到磁盘里。

    看到这里你可能会有个疑问,redo log 不是日志文件吗,日志文件就存储在磁盘上,那写的时候岂不很慢吗?

    其实,写redo log 的过程是顺序写磁盘的,磁盘顺序写减少了寻道等时间,速度比随机写要快很多( 类似Kafka存储原理),因此写 redo log 速度是很快的。

    好了,让我们回到开始时候的问题,为什么会出现脏页,并且脏页为什么会使 SQL 变慢。你想想,redo log 大小是一定的,且是循环写入的。在高并发场景下,redo log 很快被写满了,但是数据来不及同步到磁盘里,这时候就会产生脏页,并且还会阻塞后续的写入操作。SQL 执行自然会变慢。


    =

    写操作时 SQL 慢的另一种情况是可能遇到了锁,这个很容易理解。举个例子,你和别人合租了一间屋子,只有一个卫生间,你们俩同时都想去,但对方比你早了一丢丢。那么此时你只能等对方出来后才能进去。

    对应到 Mysql 中,当某一条 SQL 所要更改的行刚好被加了锁,那么此时只有等锁释放了后才能进行后续操作。

    但是还有一种极端情况,你的室友一直占用着卫生间,那么此时你该怎么整,总不能尿裤子吧,多丢人。对应到Mysql 里就是遇到了死锁或是锁等待的情况。这时候该如何处理呢?

    Mysql 中提供了查看当前锁情况的方式:

    ?

    通过在命令行执行图中的语句,可以查看当前运行的事务情况,这里介绍几个查询结果中重要的参数:

    ?

    当前事务如果等待时间过长或出现死锁的情况,可以通过 「kill 线程ID」 的方式释放当前的锁。

    这里的线程 ID 指表中 trx_mysql_thread_id 参数。

    读操作

    说完了写操作,读操作大家可能相对来说更熟悉一些。SQL 慢导致读操作变慢的问题在工作中是经常会被涉及到的。

    慢查询

    在讲读操作变慢的原因之前我们先来看看是如何定位慢 SQL 的。Mysql 中有一个叫作慢查询日志的东西,它是用来记录超过指定时间的 SQL 语句的。默认情况下是关闭的,通过手动配置才能开启慢查询日志进行定位。

    具体的配置方式是这样的:

    查看当前慢查询日志的开启情况:

    ?

    • 开启慢查询日志(临时):

    ?

    ?

    注意这里只是临时开启了慢查询日志,如果 mysql 重启后则会失效。可以 my.cnf 中进行配置使其永久生效。

    存在原因

    知道了如何查看执行慢的 SQL 了,那么我们接着看读操作时为什么会导致慢查询。

    (1)未命中索引

    SQL 查询慢的原因之一是可能未命中索引,关于使用索引为什么能使查询变快以及使用时的注意事项,网上已经很多了,这里就不多赘述了。

    (2)脏页问题

    另一种还是我们上边所提到的刷脏页情况,只不过和写操作不同的是,是在读时候进行刷脏页的。

    是不是有点懵逼,别急,听我娓娓道来:

    为了避免每次在读写数据时访问磁盘增加 IO 开销,Innodb 存储引擎通过把相应的数据页和索引页加载到内存的缓冲池(buffer pool)中来提高读写速度。然后按照最近最少使用原则来保留缓冲池中的缓存数据。

    那么当要读入的数据页不在内存中时,就需要到缓冲池中申请一个数据页,但缓冲池中数据页是一定的,当数据页达到上限时此时就需要把最久不使用的数据页从内存中淘汰掉。但如果淘汰的是脏页呢,那么就需要把脏页刷到磁盘里才能进行复用。

    你看,又回到了刷脏页的情况,读操作时变慢你也能理解了吧?

    防患于未然

    知道了原因,我们如何来避免或缓解这种情况呢?

    首先来看未命中索引的情况:

    不知道大家有没有使用 Mysql 中 explain 的习惯,反正我是每次都会用它来查看下当前 SQL 命中索引的情况。避免其带来一些未知的隐患。

    这里简单介绍下其使用方式,通过在所执行的 SQL 前加上 explain 就可以来分析当前 SQL 的执行计划:

    ?

    执行后的结果对应的字段概要描述如下图所示:

    ?

    这里需要重点关注以下几个字段:

    1、type

    表示 MySQL 在表中找到所需行的方式。其中常用的类型有:ALL、index、range、 ref、eq_ref、const、system、NULL 这些类型从左到右,性能逐渐变好。

    • ALL:Mysql 遍历全表来找到匹配的行;
    • index:与 ALL 区别为 index 类型只遍历索引树;
    • range:只检索给定范围的行,使用一个索引来选择行;
    • ref:表示上述表的连接匹配条件,哪些列或常量被用于查找索引列上的值;
    • eq_ref:类似ref,区别在于使用的是否为唯一索引。对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用 primary key 或者 unique key作为关联条件;
    • const、system:当 Mysql 对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于 where 列表中,Mysql 就能将该查询转换为一个常量,system 是 const类型的特例,当查询的表只有一行的情况下,使用system;
    • NULL:Mysql 在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。

    2、possible_keys

    查询时可能使用到的索引(但不一定会被使用,没有任何索引时显示为 NULL)。

    3、key

    实际使用到的索引。

    4、rows

    估算查找到对应的记录所需要的行数。

    5、Extra

    比较常见的是下面几种:

    • Useing index:表明使用了覆盖索引,无需进行回表;
    • Using where:不用读取表中所有信息,仅通过索引就可以获取所需数据,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤;
    • Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询,常见 group by,order by;
    • Using filesort:当Query中包含 order by 操作,而且无法利用索引完成的排序操作称为“文件排序”。

    对于刷脏页的情况,我们需要控制脏页的比例,不要让它经常接近 75%。同时还要控制 redo log 的写盘速度,并且通过设置 innodb_io_capacity 参数告诉 InnoDB 你的磁盘能力。

    总结

    写操作

    • 当 redo log 写满时就会进行刷脏页,此时写操作也会终止,那么 SQL 执行自然就会变慢。
    • 遇到所要修改的数据行或表加了锁时,需要等待锁释放后才能进行后续操作,SQL 执行也会变慢。

    读操作

    • 读操作慢很常见的原因是未命中索引从而导致全表扫描,可以通过 explain 方式对 SQL 语句进行分析。
    • 另一种原因是在读操作时,要读入的数据页不在内存中,需要通过淘汰脏页才能申请新的数据页从而导致执行变慢。

    推荐阅读

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    字节跳动总结的设计模式 PDF 火了,完整版开放分享

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 2 收藏 1 评论 0

    MrZ 发布了文章 · 2020-12-19

    面试阿里被质问:ConcurrentHashMap线程安全吗

    没啥深入实践的理论系同学,在使用并发工具时,总是认为把HashMap改为ConcurrentHashMap,就完美解决并发了呀。或者使用写时复制的CopyOnWriteArrayList,性能更佳呀!技术言论虽然自由,但面对魔鬼面试官时,我们更在乎的是这些真的正确吗?

    1 线程重用导致用户信息错乱

    生产环境中,有时获取到的用户信息是别人的。查看代码后,发现是使用了ThreadLocal缓存获取到的用户信息。

    ThreadLocal适用于变量在线程间隔离,而在方法或类间共享的场景。 若用户信息的获取比较昂贵(比如从DB查询),则在ThreadLocal中缓存比较合适。 问题来了,为什么有时会出现用户信息错乱?

    1.1 案例

    使用ThreadLocal存放一个Integer值,代表需要在线程中保存的用户信息,初始null。 先从ThreadLocal获取一次值,然后把外部传入的参数设置到ThreadLocal中,模拟从当前上下文获取用户信息,随后再获取一次值,最后输出两次获得的值和线程名称。

    面试阿里被质问:ConcurrentHashMap线程安全吗

    固定思维认为,在设置用户信息前第一次获取的值始终是null,但要清楚程序运行在Tomcat,执行程序的线程是Tomcat的工作线程,其基于线程池。?而线程池会重用固定线程,一旦线程重用,那么很可能首次从ThreadLocal获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal中的用户信息就是其他用户的信息。

    1.2 bug 重现

    在配置文件设置Tomcat参数-工作线程池最大线程数设为1,这样始终是同一线程在处理请求:

    server.tomcat.max-threads=1 
    • 先让用户1请求接口,第一、第二次获取到用户ID分别是null和1,符合预期
    • 用户2请求接口,bug复现!第一、第二次获取到用户ID分别是1和2,显然第一次获取到了用户1的信息,因为Tomcat线程池重用了线程。两次请求线程都是同一线程:http-nio-45678-exec-1。

    写业务代码时,首先要理解代码会跑在什么线程上:

    • Tomcat服务器下跑的业务代码,本就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题
    • 线程创建较昂贵,所以Web服务器会使用线程池处理请求,线程会被重用。使用类似ThreadLocal工具存放数据时,需注意在代码运行完后,显式清空设置的数据。

    1.3 解决方案

    在finally代码块显式清除ThreadLocal中数据。即使新请求过来,使用了之前的线程,也不会获取到错误的用户信息。 修正后代码:

    面试阿里被质问:ConcurrentHashMap线程安全吗

    ThreadLocal利用独占资源的解决线程安全问题,若就是要资源在线程间共享怎么办?就需要用到线程安全的容器。?使用了线程安全的并发工具,并不代表解决了所有线程安全问题。

    1.4 ThreadLocalRandom 可将其实例设置到静态变量,在多线程下重用吗?

    current()的时候初始化一个初始化种子到线程,每次nextseed再使用之前的种子生成新的种子:

    UNSAFE.putLong(t = Thread.currentThread(), SEED,
    r = UNSAFE.getLong(t, SEED) + GAMMA); 

    如果你通过主线程调用一次current生成一个ThreadLocalRandom实例保存,那么其它线程来获取种子的时候必然取不到初始种子,必须是每一个线程自己用的时候初始化一个种子到线程。 可以在nextSeed设置一个断点看看:

    UNSAFE.getLong(Thread.currentThread(),SEED); 

    2 ConcurrentHashMap真的安全吗?

    我们都知道ConcurrentHashMap是个线程安全的哈希表容器,但它仅保证提供的原子性读写操作线程安全。

    2.1 案例

    有个含900个元素的Map,现在再补充100个元素进去,这个补充操作由10个线程并发进行。 开发人员误以为使用ConcurrentHashMap就不会有线程安全问题,于是不加思索地写出了下面的代码:在每一个线程的代码逻辑中先通过size方法拿到当前元素数量,计算ConcurrentHashMap目前还需要补充多少元素,并在日志中输出了这个值,然后通过putAll方法把缺少的元素添加进去。

    为方便观察问题,我们输出了这个Map一开始和最后的元素个数。

    面试阿里被质问:ConcurrentHashMap线程安全吗

    • 访问接口

    分析日志输出可得:

    • 初始大小900符合预期,还需填充100个元素
    • worker13线程查询到当前需要填充的元素为49,还不是100的倍数
    • 最后HashMap的总项目数是1549,也不符合填充满1000的预期

    2.2 bug 分析

    ConcurrentHashMap就像是一个大篮子,现在这个篮子里有900个桔子,我们期望把这个篮子装满1000个桔子,也就是再装100个桔子。有10个工人来干这件事儿,大家先后到岗后会计算还需要补多少个桔子进去,最后把桔子装入篮子。 ConcurrentHashMap这篮子本身,可以确保多个工人在装东西进去时,不会相互影响干扰,但无法确保工人A看到还需要装100个桔子但是还未装时,工人B就看不到篮子中的桔子数量。你往这个篮子装100个桔子的操作不是原子性的,在别人看来可能会有一个瞬间篮子里有964个桔子,还需要补36个桔子。

    ConcurrentHashMap对外提供能力的限制:

    • 使用不代表对其的多个操作之间的状态一致,是没有其他线程在操作它的。如果需要确保需要手动加锁
    • 诸如size、isEmpty和containsValue等聚合方法,在并发下可能会反映ConcurrentHashMap的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用size方法计算差异值,是一个流程控制
    • 诸如putAll这样的聚合方法也不能确保原子性,在putAll的过程中去获取数据可能会获取到部分数据

    2.3 解决方案

    整段逻辑加锁:

    面试阿里被质问:ConcurrentHashMap线程安全吗

    • 只有一个线程查询到需补100个元素,其他9个线程查询到无需补,最后Map大小1000

    既然使用ConcurrentHashMap还要全程加锁,还不如使用HashMap呢? 不完全是这样。

    ConcurrentHashMap提供了一些原子性的简单复合逻辑方法,用好这些方法就可以发挥其威力。这就引申出代码中常见的另一个问题:在使用一些类库提供的高级工具类时,开发人员可能还是按照旧的方式去使用这些新类,因为没有使用其真实特性,所以无法发挥其威力。

    3 知己知彼,百战百胜

    3.1 案例

    使用Map来统计Key出现次数的场景。

    • 使用ConcurrentHashMap来统计,Key的范围是10
    • 使用最多10个并发,循环操作1000万次,每次操作累加随机的Key
    • 如果Key不存在的话,首次设置值为1。

    show me code:

    面试阿里被质问:ConcurrentHashMap线程安全吗

    有了上节经验,我们这直接锁住Map,再做

    • 判断
    • 读取现在的累计值
    • +1
    • 保存累加后值

    这段代码在功能上的确毫无没有问题,但却无法充分发挥ConcurrentHashMap的性能,优化后:

    面试阿里被质问:ConcurrentHashMap线程安全吗

    • ConcurrentHashMap的原子性方法computeIfAbsent做复合逻辑操作,判断K是否存在V,若不存在,则把Lambda运行后结果存入Map作为V,即新创建一个LongAdder对象,最后返回V 因为computeIfAbsent返回的V是LongAdder,是个线程安全的累加器,可直接调用其increment累加。

    这样在确保线程安全的情况下达到极致性能,且代码行数骤减。

    3.2 性能测试

    • 使用StopWatch测试两段代码的性能,最后的断言判断Map中元素的个数及所有V的和是否符合预期来校验代码正确性
    • 性能测试结果:

    比使用锁性能提升至少5倍。

    3.3 computeIfAbsent高性能之道

    Java的Unsafe实现的CAS。 它在JVM层确保写入数据的原子性,比加锁效率高:

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    } 

    所以不要以为只要用了ConcurrentHashMap并发工具就是高性能的高并发程序。

    辨明 computeIfAbsent、putIfAbsent

    • 当Key存在的时候,如果Value获取比较昂贵的话,putIfAbsent就白白浪费时间在获取这个昂贵的Value上(这个点特别注意)
    • Key不存在的时候,putIfAbsent返回null,小心空指针,而computeIfAbsent返回计算后的值
    • 当Key不存在的时候,putIfAbsent允许put null进去,而computeIfAbsent不能,之后进行containsKey查询是有区别的(当然了,此条针对HashMap,ConcurrentHashMap不允许put null value进去)

    3.4 CopyOnWriteArrayList 之殇

    再比如一段简单的非 DB操作的业务逻辑,时间消耗却超出预期时间,在修改数据时操作本地缓存比回写DB慢许多。原来是有人使用了CopyOnWriteArrayList缓存大量数据,而该业务场景下数据变化又很频繁。 CopyOnWriteArrayList虽然是一个线程安全版的ArrayList,但其每次修改数据时都会复制一份数据出来,所以只适用读多写少或无锁读场景。 所以一旦使用CopyOnWriteArrayList,一定是因为场景适宜而非炫技。

    CopyOnWriteArrayList V.S 普通加锁ArrayList读写性能

    • 测试并发写性能
    • 测试结果:高并发写,CopyOnWriteArray比同步ArrayList慢百倍
    • 测试并发读性能
    • 测试结果:高并发读(100万次get操作),CopyOnWriteArray比同步ArrayList快24倍

    高并发写时,CopyOnWriteArrayList为何这么慢呢?因为其每次add时,都用Arrays.copyOf创建新数组,频繁add时内存申请释放性能消耗大。

    4 总结

    4.1 Don't !!!

    • 不要只会用并发工具,而不熟悉线程原理
    • 不要觉得用了并发工具,就怎么都线程安全
    • 不熟悉并发工具的优化本质,就难以发挥其真正性能
    • 不要不结合当前业务场景,就随意选用并发工具,可能导致系统性能更差

    4.2 Do !!!

    • 认真阅读官方文档,理解并发工具适用场景及其各API的用法,并自行测试验证,最后再使用
    • 并发bug本就不易复现, 多自行进行性能压力测试

    推荐阅读

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-19

    如何透彻理解 Redis 核心原理?怎样才能形成 Redis 系统观?

    Redis作为高性能的内存数据库,在大数据量的情况下也会遇到性能瓶颈,日常开发中只有时刻谨记优化铁则,才能使得Redis性能发挥到极致。

    如果你是一位后端工程师,面试时八成会被问到 Redis,特别是那些大型互联网公司,不仅要求面试者能简单使用 Redis,还要深入理解其底层实现原理,具备解决常见问题的能力。可以说,熟练使用 Redis 就是后端工程师的必备技能。

    但我发现,在工作或面试时,大家还是会有这样那样的疑问,比如:如何用 Redis 实现分布式锁?Redis 怎样处理过期键?缓存雪崩、穿透、热点问题怎么解决?持久化、集群方案怎么选择?如何优雅地给 Redis 做键值分析?等等。

    这里,分享给你一张 Redis 问题画像图,帮你快速查找问题对应的 Redis 主线模块,进而定位相应的技术点。

    最近,总结了一条系统高效的 Redis 学习路径,帮你透彻理解 Redis 核心原理,并通过上手实战,掌握高并发场景下的缓存解决方案,解锁 Redis 高频面试题,让你无论在工作还是面试中,都能无往不利。

    我发现,很多人都是带着具体问题学 Redis 的,这些问题当然重要,但如果只关注零散的技术点,没有建立起完整的知识框架,你的使用能力很难得到质的提升。

    那么,怎样才能形成 Redis 系统观呢?在我看来,就是“两大维度,三大主线”:前者指系统维度和应用维度,后者就是高性能、高可靠和高可扩展。

    从系统维度上说,我们要了解 Redis 各项关键技术的设计原理,掌握一些系统设计规范,例如 run-to-complete 模型、epoll 网络模型,以便应用到后续的系统开发中。但 Redis 的知识点很零碎,所以,可以按照“三大主线”为它们进行分类:

    • 高性能主线,包括线程模型、数据结构、持久化、网络框架;
    • 高可靠主线,包括主从复制、哨兵机制;
    • 高可扩展主线,包括数据分片、负载均衡。

    其次,在应用维度上,可以按照 “应用场景驱动”和“典型案例驱动”两种方式学习,一个是“面”的梳理,一个是“点”的掌握。

    我们都知道,缓存和集群是 Redis 最广泛的两大应用场景。在这些场景中,本身就具有一条显式的技术链。比如,提到缓存就会想到缓存机制、缓存替换、缓存异常等一连串问题。

    但并不是所有都适合这种方式,比如 Redis 丰富的数据模型,以及一些隐藏得比较深、在特定业务场景下才会出现的问题,就可以用“典型案例驱动”方式,深入拆解一些对 Redis “三高”特性影响较大的案例,例如,各个大厂在万亿级访问量、数据量的情况下,对 Redis 的深度优化实践。

    这样,才能透彻理解 Redis,建立起结构化的知识体系,快速找到引发问题的关键因素,甚至整理成 Checklist,作为遇到问题时信手拈来的“锦囊妙计”。

    再具体一点说,内容主要分为五部分:

    一、Redis 基本数据结构与实战场景

    二、Redis 常见异常及解决方案

    三、分布式环境下常见的应用场景

    四、Redis 集群模式

    五、Redis 常见面试题目详解

    说了这么多,看看整体大纲图吧。

    下面是完整的目录:


    一、Redis 基本数据结构与实战场景


    二、Redis 常见异常及解决方案


    三、分布式环境下常见的应用场景


    四、Redis 集群模式


    五、Redis 常见面试题目详解


    • Redis相比memcached有哪些优势?
    • Redis支持哪几种数据类型?
    • Redis主要消耗什么物理资源?
    • Redis的全称是什么?
    • Redis有哪几种数据淘汰策略?
    • Redis官方为什么不提供Windows版本?
    • 一个字符串类型的智能存储最大容量是多少?
    • 为什么Redis需要把所有数据放到内存中?
    • Redis集群方案应该怎么做?都有哪些方案?
    • Redis集群方案什么情况下会导致整个集群不可用?
    • Redis事务相关的命令有哪几个?
    • Redis如何做内存优化?
    • Redis回收进程如何工作的?
    • Redis回收使用的是什么算法?
    • Redis如何做大型数据插入?
    • 为什么要做Redis分区?
    • 你知道有哪些Redis分区实现方案?
    • Redis分区有什么缺点?
    • Redis持久化数据和缓存怎么做扩容?
    • 分布式Redis是前期做还是后期规模上来了再做好?为什么?
    • Twemproxy是什么?
    • 支持一致性哈希的客户端有哪些?
    • Redis与其他key-value存储有什么不同?
    • Redis的内存占用情况怎么样?
    • 都有哪些办法可以降低Redis的内存使用情况呢?
    • 一个Redis实例最多能存放多少的keys?
    • Redis常见性能问题和解决方案?
    • Redis提供了哪几种持久化方式?

    本文所有资料添加v(bjmsb10)即可免费获取到

    推荐阅读


    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    字节跳动总结的设计模式 PDF 火了,完整版开放分享

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    看完三件事??


    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 1 收藏 1 评论 0

    MrZ 发布了文章 · 2020-12-18

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    1 计算器的灾难:10%+10%到底等于几?

    • 我们人类以为是 0.2,可是打开手机计算器试试呢?

    解密

    国外计算程序使用的单步计算法。于是,a+b%表示a(1+b%)。所以,手机计算器实际上在计算10%(1+10%)= 0.11。

    再通俗点一句话说清运算原理。以8+10%为例,为什么=8.8而不是8.1?一起读:8元钱,加上10%的小费,一共是8.8元。

    最早的电子计算器并没有%,是后来加的。作为后续改进,它一定解决了计算场景中的常用痛点,而绝不是脑残。我推测很可能是西方人计算折扣、小费、利息等常见场景。

    2 满目疮痍的Double

    • 浮点数四则运算
    • 结果

    由于计算机内部是以二进制存储数值的,浮点数亦是。Java采用IEEE 754标准实现浮点数的表达和运算。比如,0.1的二进制表示为0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是0.1000000000000000055511151231257827021181583404541015625。计算机无法精确表示0.1,所以浮点数计算造成精度损失。

    你可能觉得像0.1,其十进制和二进制间转换后相差很小,不会对计算产生什么严重影响。但积土成山,大量使用double作大量金钱计算,最终损失精度就是大量资金出入了。

    一位“黑客”利用银行漏洞从PayPal、Google Checkout和其它在线支付公司窃取了5万多美元,每次只偷几美分。他所利用的漏洞是:银行在开户后一般会向帐号发送小额钱去验证帐户是否有效,数额一般在几美分到几美元左右。Google Checkout和Paypal也使用相同的方法去检验与在线帐号捆绑的信用卡和借记卡帐号。 用一个自动脚本开了58,000个帐号,收集了数以千计的超小额费用,汇入到几个个人银行账户中去。从Google Checkout服务骗到了$8,000以上的现金。银行注意到了这种奇怪的现金流动,和他取得联系,Largent解释他仔细阅读过相关服务条款,相信 自己没做错事,声称需要钱去偿还债务。但Largent使用了假名,包括卡通人物的名字,假的地址和社会保障号码,因此了违反了邮件、银行和电信欺骗法律。别在中国尝试,这要判无期徒刑。

    3 救世的BigDecimal

    我们知道BigDecimal,在浮点数精确表达和运算的场景,一定要使用。不过,在使用BigDecimal时有几个坑需要避开。

    • BigDecimal之前的四则运算
    • 输出
      运算结果还是不精确,只不过是精度高了。

    3.1 BigDecimal表示/计算浮点数且使用字符串构造器

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    • 完美输出

    无法调用BigDecimal传入Double的构造器,但手头只有一个Double,如何转换为精确表达的BigDecimal?

    • Double.toString把double转换为字符串可行吗?
    • 输出
      401.5000。与上面字符串初始化100和4.015相乘得到的结果401.500相比,这里为什么多了1个0?BigDecimal有scale?小数点右边的位数precision?精度,即有效数字的长度

    new BigDecimal(Double.toString(100))得到的BigDecimal的scale=1、precision=4;而
    new BigDecimal(“100”)得到的BigDecimal的scale=0、precision=3。

    BigDecimal乘法操作,返回值的scale是两个数的scale相加。所以,初始化100的两种不同方式,导致最后结果的scale分别是4和3:

    private static void testScale() {
        BigDecimal bigDecimal1 = new BigDecimal("100");
        BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));
        BigDecimal bigDecimal3 = new BigDecimal(String.valueOf(100));
        BigDecimal bigDecimal4 = BigDecimal.valueOf(100d);
        BigDecimal bigDecimal5 = new BigDecimal(Double.toString(100));
    
        print(bigDecimal1); //scale 0 precision 3 result 401.500
        print(bigDecimal2); //scale 1 precision 4 result 401.5000
        print(bigDecimal3); //scale 0 precision 3 result 401.500
        print(bigDecimal4); //scale 1 precision 4 result 401.5000
        print(bigDecimal5); //scale 1 precision 4 result 401.5000
    }
    
    private static void print(BigDecimal bigDecimal) {
        log.info("scale {} precision {} result {}", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("4.015")));
    } 

    4 浮点数的舍入和格式化

    应考虑显式编码,通过格式化表达式或格式化工具

    4.1 明确小数位数和舍入方式

    • 通过String.format使用%.1f格式化double/float的3.35浮点数
    • 结果
      3.4和3.3

    精度问题和舍入方式共同导致:double/float的3.35实际存储表示

    3.350000000000000088817841970012523233890533447265625
    3.349999904632568359375 

    String.format采用四舍五入的方式进行舍入,取1位小数,double的3.350四舍五入为3.4,而float的3.349四舍五入为3.3。

    我们看一下Formatter类的相关源码,可以发现使用的舍入模式是HALF_UP(代码第11行):

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    若想使用其他舍入方式,可设置DecimalFormat

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    当把这俩浮点数向下舍入取2位小数时,输出分别是3.35、3.34,还是因为浮点数无法精确存储。

    所以即使通过DecimalFormat精确控制舍入方式,double/float也可能产生奇怪结果,所以

    4.2 字符串格式化也要使用BigDecimal

    • BigDecimal分别使用向下舍入、四舍五入取1位小数格式化数字3.35
    • 结果
      3.3和3.4,符合预期。

    最佳实践:应该使用BigDecimal来进行浮点数的表示、计算、格式化。

    5 equals做判等就一定对?

    包装类的比较要通过equals,而非==。那使用equals对两个BigDecimal判等,一定符合预期吗?

    • 使用equals比较1.0和1这俩BigDecimal:
      结果自然是false。BigDecimal的equals比较的是BigDecimal的value和scale:1.0的scale是1,1的scale是0,所以结果false

    若只想比较BigDecimal的value,使用compareTo

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    BigDecimal的equals和hashCode会同时考虑value和scale,若结合HashSet/HashMap可能出问题。把值为1.0的BigDecimal加入HashSet,然后判断其是否存在值为1的BigDecimal,得到false

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    5.1 解决方案

    5.1.1 使用TreeSet替换HashSet

    TreeSet不使用hashCode,也不使用equals比较元素,而使用compareTo方法。

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    5.1.2 去掉尾部的零

    把BigDecimal存入HashSet或HashMap前,先使用stripTrailingZeros方法去掉尾部的零。
    比较的时候也去掉尾部的0,确保value相同的BigDecimal,scale也是一致的:

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    6 溢出问题

    所有的基本数值类型都有超出保存范围可能性。

    • 对Long最大值+1
    • 结果是一个负数,Long的最大值+1变为了Long的最小值
      -9223372036854775808

    显然发生溢出还没抛任何异常。

    6.1 解决方案

    6.1.1 使用Math类的xxExact进行数值运算

    这些方法会在数值溢出时主动抛异常。

    阿里华为等大厂如何处理数值精度/舍入/溢出问题

    执行后,会得到ArithmeticException,这是一个RuntimeException:

    java.lang.ArithmeticException: long overflow 

    6.1.2 使用大数类BigInteger

    BigDecimal专于处理浮点数的专家,而BigInteger则专于大数的科学计算。

    • 使用BigInteger对Long最大值进行+1操作。若想把计算结果转为Long变量,可使用BigInteger#longValueExact,在转换出现溢出时,同样会抛出ArithmeticException
    • 结果
    9223372036854775808
    java.lang.ArithmeticException: BigInteger out of long range 

    通过BigInteger对Long的最大值加1无问题,但将结果转为Long时,则会提示溢出。

    推荐阅读

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    字节跳动总结的设计模式 PDF 火了,完整版开放分享

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-18

    程序员因重复记录日志撑爆ELK被辞退!

    由于日志配置繁杂,很多同学喜欢直接cv其他项目或网上博客的配置文件,而不仔细研究每个配置项。最常见的就是重复记录日志,这不但给查看和统计带来不必要麻烦,还会增加磁盘和日志收集系统负担。

    下面看几个常见导致该错误的案例,大家引以为戒,避免被辞退。

    1 logger配置继承关系错误

    定义方法记录debug、info、warn、error四种日志:

    Logback配置

    配置看没啥问题,但执行方法后明显记录重复了:

    错因

    CONSOLE这个Appender同时挂载到俩Logger,定义的<logger><root>,由于定义的<logger>继承自<root>,所以同一日志既会通过logger记录,也会发送到root记录,因此应用包下日志出现重复。

    如此配置的初衷是啥?

    本想实现自定义logger配置,让应用内日志暂时开启DEBUG级别。

    其实,这无需重复挂载Appender,去掉<logger>下挂载的Appender即可

    <logger name="org.javaedge.logging" level="DEBUG"/> 

    若自定义<logger>需把日志输出到不同Appender,比如

    • 应用日志输出到文件app.log
    • 其他框架日志输出到控制台

    可设置<logger>的additivity属性为false,就不会继承<root> Appender

    2 配置LevelFilter错误

    记录日志到控制台时,将日志按级别记录到俩文件

    执行结果

    • info.log 文件包含INFO、WARN和ERROR三级日志,不符预期
    • error.log包含WARN和ERROR俩级别日志,导致日志重复收集

    ???????

    事故问责

    一些公司使用自动化ELK方案收集日志,日志会同时输出到控制台和文件,开发人员在本地测试不会关心文件中记录的日志,而在测试和生产环境又因为开发人员没有服务器访问权限,所以原始日志文件中的重复问题难以发现。

    到底为何重复?

    ThresholdFilter源码解析

    • 日志级别 ≥ 配置级别?返回NEUTRAL,继续调用过滤器链上的下个过滤器
    • 否则返回DENY,直接拒绝记录日志

    该案例我们将?ThresholdFilter?置?WARN,因此可记录WARN和ERROR级日志。

    LevelFilter

    用于比较日志级别,然后进行相应处理。

    • 若匹配就调用onMatch定义的处理方式:默认交给下一个过滤器处理(AbstractMatcherFilter基类中定义的默认值)
    • 否则调用onMismatch定义的处理方式:默认也是交给下一个过滤器


    和ThresholdFilter不同,LevelFilter仅配置level无法真正起作用

    由于未配置onMatch和onMismatch属性,所以该过滤器失效,导致INFO以上级别日志都记录了。

    修正

    配置LevelFilter的onMatch属性为ACCEPT,表示接收INFO级别的日志;配置onMismatch属性为DENY,表示除了INFO级别都不记录:

    如此,_info.log文件只会有INFO级日志,不会再出现日志重复。

    推荐阅读

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    字节跳动总结的设计模式 PDF 火了,完整版开放分享

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-16

    太赞了!滴滴开源了一套分布式ID的生成系统...

    ID Generator id生成器 分布式id生成系统,简单易用、高性能、高可用的id生成系统

    简介

    Tinyid是用Java开发的一款分布式id生成系统,基于数据库号段算法实现,关于这个算法可以参考美团leaf或者tinyid原理介绍。Tinyid扩展了leaf-segment算法,支持了多db(master),同时提供了java-client(sdk)使id生成本地化,获得了更好的性能与可用性。Tinyid在滴滴客服部门使用,均通过tinyid-client方式接入,每天生成亿级别的id。

    tinyid系统架构图

    滴滴开源的分布式id生成系统

    下面是一些关于这个架构图的说明:

    • nextId和getNextSegmentId是tinyid-server对外提供的两个http接口
    • nextId是获取下一个id,当调用nextId时,会传入bizType,每个bizType的id数据是隔离的,生成id会使用该bizType类型生成的IdGenerator。
    • getNextSegmentId是获取下一个可用号段,tinyid-client会通过此接口来获取可用号段
    • IdGenerator是id生成的接口
    • IdGeneratorFactory是生产具体IdGenerator的工厂,每个biz_type生成一个IdGenerator实例。通过工厂,我们可以随时在db中新增biz_type,而不用重启服务
    • IdGeneratorFactory实际上有两个子类IdGeneratorFactoryServer和IdGeneratorFactoryClient,区别在于,getNextSegmentId的不同,一个是DbGet,一个是HttpGet
    • CachedIdGenerator则是具体的id生成器对象,持有currentSegmentId和nextSegmentId对象,负责nextId的核心流程。nextId最终通过AtomicLong.andAndGet(delta)方法产生。

    性能与可用性

    性能

    1. http方式访问,性能取决于http server的能力,网络传输速度
    2. java-client方式,id为本地生成,号段长度(step)越长,qps越大,如果将号段设置足够大,则qps可达1000w+

    可用性

    1. 依赖db,当db不可用时,因为server有缓存,所以还可以使用一段时间,如果配置了多个db,则只要有1个db存活,则服务可用
    2. 使用tiny-client,只要server有一台存活,则理论上可用,server全挂,因为client有缓存,也可以继续使用一段时间

    Tinyid的特性

    1. 全局唯一的long型id
    2. 趋势递增的id,即不保证下一个id一定比上一个大
    3. 非连续性
    4. 提供http和java client方式接入
    5. 支持批量获取id
    6. 支持生成1,3,5,7,9…序列的id
    7. 支持多个db的配置,无单点

    适用场景:只关心id是数字,趋势递增的系统,可以容忍id不连续,有浪费的场景 不适用场景:类似订单id的业务(因为生成的id大部分是连续的,容易被扫库、或者测算出订单量)

    推荐使用方式

    • tinyid-server推荐部署到多个机房的多台机器
    • 多机房部署可用性更高,http方式访问需使用方考虑延迟问题
    • 推荐使用tinyid-client来获取id,好处如下:
    • id为本地生成(调用AtomicLong.addAndGet方法),性能大大增加
    • client对server访问变的低频,减轻了server的压力
    • 因为低频,即便client使用方和server不在一个机房,也无须担心延迟
    • 即便所有server挂掉,因为client预加载了号段,依然可以继续使用一段时间 注:使用tinyid-client方式,如果client机器较多频繁重启,可能会浪费较多的id,这时可以考虑使用http方式
    • 推荐db配置两个或更多:
    • db配置多个时,只要有1个db存活,则服务可用 多db配置,如配置了两个db,则每次新增业务需在两个db中都写入相关数据

    tinyid的原理

    Id生成系统要点

    在简单系统中,我们常常使用db的id自增方式来标识和保存数据,随着系统的复杂,数据的增多,分库分表成为了常见的方案,db自增已无法满足要求。这时候全局唯一的id生成系统就派上了用场。当然这只是id生成其中的一种应用场景。那么id生成系统有哪些要求呢?

    1. 全局唯一的id:无论怎样都不能重复,这是最基本的要求了
    2. 高性能:基础服务尽可能耗时少,如果能够本地生成最好
    3. 高可用:虽说很难实现100%的可用性,但是也要无限接近于100%的可用性
    4. 简单易用: 能够拿来即用,接入方便,同时在系统设计和实现上要尽可能的简单

    Tinyid的实现原理

    我们先来看一下最常见的id生成方式,db的auto_increment,相信大家都非常熟悉,我也见过一些同学在实战中使用这种方案来获取一个id,这个方案的优点是简单,缺点是每次只能向db获取一个id,性能比较差,对db访问比较频繁,db的压力会比较大。那么是不是可以对这种方案优化一下呢,可否一次向db获取一批id呢?答案当然是可以的。?一批id,我们可以看成是一个id范围,例如(1000,2000],这个1000到2000也可以称为一个"号段",我们一次向db申请一个号段,加载到内存中,然后采用自增的方式来生成id,这个号段用完后,再次向db申请一个新的号段,这样对db的压力就减轻了很多,同时内存中直接生成id,性能则提高了很多。那么保存db号段的表该怎设计呢?

    DB号段算法描述

    id start_id end_id

    1 1000 2000

    如上表,我们很容易想到的是db直接存储一个范围(start_id,end_id],当这批id使用完毕后,我们做一次update操作,update start_id=2000(end_id), end_id=3000(end_id+1000),update成功了,则说明获取到了下一个id范围。仔细想想,实际上start_id并没有起什么作用,新的号段总是(end_id,end_id+1000]。所以这里我们更改一下,db设计应该是这样的

    id biz_type max_id step version

    1 1000 2000 1000 0

    • 这里我们增加了biz_type,这个代表业务类型,不同的业务的id隔离
    • max_id则是上面的end_id了,代表当前最大的可用id
    • step代表号段的长度,可以根据每个业务的qps来设置一个合理的长度
    • version是一个乐观锁,每次更新都加上version,能够保证并发更新的正确性 ?那么我们可以通过如下几个步骤来获取一个可用的号段,
    • A.查询当前的max_id信息:select id, biz_type, max_id, step, version from tiny_id_info where biz_type='test';
    • B.计算新的max_id: new_max_id = max_id + step
    • C.更新DB中的max_id:update tiny_id_info set max_id=#{new_max_id} , verison=version+1 where id=#{id} and max_id=#{max_id} and version=#{version}
    • D.如果更新成功,则可用号段获取成功,新的可用号段为(max_id, new_max_id]
    • E.如果更新失败,则号段可能被其他线程获取,回到步骤A,进行重试

    号段生成方案的简单架构

    如上我们已经完成了号段生成逻辑,那么我们的id生成服务架构可能是这样的

    滴滴开源的分布式id生成系统

    id生成系统向外提供http服务,请求经过我们的负载均衡router,到达其中一台tinyid-server,从事先加载好的号段中获取一个id,如果号段还没有加载,或者已经用完,则向db再申请一个新的可用号段,多台server之间因为号段生成算法的原子性,而保证每台server上的可用号段不重,从而使id生成不重。?可以看到如果tinyid-server如果重启了,那么号段就作废了,会浪费一部分id;同时id也不会连续;每次请求可能会打到不同的机器上,id也不是单调递增的,而是趋势递增的,不过这对于大部分业务都是可接受的。

    简单架构的问题

    到此一个简单的id生成系统就完成了,那么是否还存在问题呢?回想一下我们最开始的id生成系统要求,高性能、高可用、简单易用,在上面这套架构里,至少还存在以下问题:

    • 当id用完时需要访问db加载新的号段,db更新也可能存在version冲突,此时id生成耗时明显增加
    • db是一个单点,虽然db可以建设主从等高可用架构,但始终是一个单点
    • 使用http方式获取一个id,存在网络开销,性能和可用性都不太好

    优化办法如下:

    (1)双号段缓存

    对于号段用完需要访问db,我们很容易想到在号段用到一定程度的时候,就去异步加载下一个号段,保证内存中始终有可用号段,则可避免性能波动。

    (2)增加多db支持

    db只有一个master时,如果db不可用(down掉或者主从延迟比较大),则获取号段不可用。实际上我们可以支持多个db,比如2个db,A和B,我们获取号段可以随机从其中一台上获取。那么如果A,B都获取到了同一号段,我们怎么保证生成的id不重呢?tinyid是这么做的,让A只生成偶数id,B只生产奇数id,对应的db设计增加了两个字段,如下所示

    id biz_type max_id step delta remainder version

    1 1000 2000 1000 2 0 0

    delta代表id每次的增量,remainder代表余数,例如可以将A,B都delta都设置2,remainder分别设置为0,1则,A的号段只生成偶数号段,B是奇数号段。通过delta和remainder两个字段我们可以根据使用方的需求灵活设计db个数,同时也可以为使用方提供只生产类似奇数的id序列。

    (3) 增加tinyid-client

    使用http获取一个id,存在网络开销,是否可以本地生成id?为此我们提供了tinyid-client,我们可以向tinyid-server发送请求来获取可用号段,之后在本地构建双号段、id生成,如此id生成则变成纯本地操作,性能大大提升,因为本地有双号段缓存,则可以容忍tinyid-server一段时间的down掉,可用性也有了比较大的提升。

    (4) tinyid最终架构

    最终我们的架构可能是这样的

    滴滴开源的分布式id生成系统

    • tinyid提供http和tinyid-client两种方式接入
    • tinyid-server内部缓存两个号段
    • 号段基于db生成,具有原子性
    • db支持多个
    • tinyid-server内置easy-router选择db

    项目地址

    github地址:https://github.com/didi/tinyid

    推荐阅读

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    字节跳动总结的设计模式 PDF 火了,完整版开放分享

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-16

    从源码的角度搞懂 Java 动态代理!

    前言

    最近,看了一下关于RMI(Remote Method Invocation)相关的知识,遇到了一个动态代理的问题,然后就决定探究一下动态代理。

    这里先科普一下RMI。

    RMI

    像我们平时写的程序,对象之间互相调用方法都是在同一个JVM中进行,而RMI可以实现一个JVM上的对象调用另一个JVM上对象的方法,即远程调用。

    接口定义

    定义一个远程对象接口,实现Remote接口来进行标记。

    public?interface?UserInterface?extends?Remote?{
    ????void?sayHello()?throws?RemoteException;
    } 

    远程对象定义

    定义一个远程对象类,继承UnicastRemoteObject来实现Serializable和Remote接口,并实现接口方法。

    public?class?User?extends?UnicastRemoteObject?implements?UserInterface?{
    ????public?User()?throws?RemoteException?{}
    ????@Override
    ????public?void?sayHello()?{
    ????????System.out.println("Hello?World");
    ????}
    } 

    服务端

    启动服务端,将user对象在注册表上进行注册。

    public?class?RmiServer?{
    ????public?static?void?main(String[]?args)?throws?RemoteException,?AlreadyBoundException,?MalformedURLException?{
    ????????User?user?=?new?User();
    ????????LocateRegistry.createRegistry(8888);
    ????????Naming.bind("rmi://127.0.0.1:8888/user",?user);
    ????????System.out.println("rmi?server?is?starting...");
    ????}
    } 

    启动服务端:?

    客户端

    从服务端注册表获取远程对象,在服务端调用sayHello()方法。

    public?class?RmiClient?{
    ????public?static?void?main(String[]?args)?throws?RemoteException,?NotBoundException,?MalformedURLException?{
    ????????UserInterface?user?=?(UserInterface)?Naming.lookup("rmi://127.0.0.1:8888/user");
    ????????user.sayHello();
    ????}
    } 

    服务端运行结果:?至此,一个简单的RMI demo完成。

    动态代理

    提出问题

    看了看RMI代码,觉得UserInterface这个接口有点多余,如果客户端使用Naming.lookup()获取的对象不强转成UserInterface,直接强转成User是不是也可以,于是试了一下,就报了以下错误:?似曾相识又有点陌生的$Proxy0,翻了翻尘封的笔记找到了是动态代理的知识点,寥寥几笔带过,所以决定梳理一下动态代理,重新整理一份笔记。

    动态代理Demo

    接口定义

    public?interface?UserInterface?{
    ????void?sayHello();
    } 

    真实角色定义

    public?class?User?implements?UserInterface?{
    ????@Override
    ????public?void?sayHello()?{
    ????????System.out.println("Hello?World");
    ????}
    } 

    调用处理类定义

    代理类调用真实角色的方法时,其实是调用与真实角色绑定的处理类对象的invoke()方法,而invoke()调用的是真实角色的方法。

    这里需要实现 InvocationHandler 接口以及invoke()方法。

    public?class?UserHandler?implements?InvocationHandler?{
    ????private?User?user;
    ????public?UserProxy(User?user)?{
    ????????this.user?=?user;
    ????}
    ????@Override
    ????public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{
    ????????System.out.println("invoking?start....");
    ????????method.invoke(user);
    ????????System.out.println("invoking?stop....");
    ????????return?user;
    ????}
    } 

    执行类

    public?class?Main?{
    ????public?static?void?main(String[]?args)?{
    ????????User?user?=?new?User();
    ????????//?处理类和真实角色绑定
    ????????UserHandler?userHandler?=?new?UserHandler(user);
    ????????//?开启将代理类class文件保存到本地模式,平时可以省略
    ????????System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles",?"true");
    ????????//?动态代理生成代理对象$Proxy0
    ????????Object?o?=?Proxy.newProxyInstance(Main.class.getClassLoader(),?new?Class[]{UserInterface.class},?userHandler);
    ????????//?调用的其实是invoke()
    ????????((UserInterface)o).sayHello();
    ????} 

    运行结果:?这样动态代理的基本用法就学完了,可是还有好多问题不明白。

    1. 动态代理是怎么调用的invoke()方法?
    2. 处理类UserHandler有什么作用?
    3. 为什么要将类加载器和接口类数组当作参数传入newProxyInstance?

    假如让你去实现动态代理,你有什么设计思路?

    猜想

    动态代理,是不是和静态代理,即设计模式的代理模式有相同之处呢?

    简单捋一捋代理模式实现原理:真实角色和代理角色共同实现一个接口并实现抽象方法A,代理类持有真实角色对象,代理类在A方法中调用真实角色对象的A方法。在Main中实例化代理对象,调用其A方法,间接调用了真实角色的A方法。

    「实现代码」

    //?接口和真实角色对象就用上面代码
    //?代理类,实现UserInterface接口
    public?class?UserProxy?implements?UserInterface?{
    ???//?持有真实角色对象
    ????private?User?user?=?new?User();
    ????@Override
    ????public?void?sayHello()?{
    ????????System.out.println("invoking?start....");
    ????????//?在代理对象的sayHello()里调用真实角色的sayHello()
    ????????user.sayHello();
    ????????System.out.println("invoking?stop....");
    ????}
    }
    //?运行类
    public?class?Main?{
    ????public?static?void?main(String[]?args)?{
    ???????//?实例化代理角色对象
    ????????UserInterface?userProxy?=?new?UserProxy();
    ????????//?调用了代理对象的sayHello(),其实是调用了真实角色的sayHello()
    ????????userProxy.sayHello();
    ????} 

    拿开始的动态代理代码和静态代理比较,接口、真实角色都有了,区别就是多了一个UserHandler处理类,少了一个UserProxy代理类。

    接着对比一下两者的处理类和代理类,发现UserHandler的invoke()和UserProxy的sayHello()这两个方法的代码都是一样的。那么,是不是新建一个UserProxy类,然后实现UserInterface接口并持有UserHandler的对象,在sayHello()方法中调用UserHandler的invoke()方法,就可以动态代理了。

    「代码大概就是这样的」

    //?猜想的代理类结构,动态代理生成的代理是com.sun.proxy.$Proxy0
    public?class?UserProxy?implements?UserInterface{
    ???//?持有处理类的对象
    ????private?InvocationHandler?handler;
    ????public?UserProxy(InvocationHandler?handler)?{
    ????????this.handler?=?handler;
    ????}
    ????//?实现sayHello()方法,并调用invoke()
    ????@Override
    ????public?void?sayHello()?{
    ????????try?{
    ????????????handler.invoke(this,?UserInterface.class.getMethod("sayHello"),?null);
    ????????}?catch?(Throwable?throwable)?{
    ????????????throwable.printStackTrace();
    ????????}
    ????}
    }
    //?执行类
    public?static?void?main(String[]?args)?{
    ????????User?user?=?new?User();
    ????????UserHandler?userHandler?=?new?UserHandler(user);
    ????????UserProxy?proxy?=?new?UserProxy(userHandler);
    ????????proxy.sayHello();
    ????} 

    输出结果:?

    上面的代理类代码是写死的,而动态代理是当你调用Proxy.newProxyInstance()时,会根据你传入的参数来动态生成这个代理类代码,如果让我实现,会是以下这个流程。

    1. 根据你传入的Class[]接口数组,代理类会来实现这些接口及其方法(这里就是sayHello()),并且持有你传入的userHandler对象,使用文件流将预先设定的包名、类名、方法名等一行行代码写到本地磁盘,生成$Proxy0.java文件
    2. 使用编译器将编译成Proxy0.class
    3. 根据你传入的ClassLoader将$Proxy0.class加载到JMV中
    4. 调用Proxy.newProxyInstance()就会返回一个$Proxy0的对象,然后调用sayHello(),就执行了里面userHandler的invoke()

    以上就是对动态代理的一个猜想过程,下面就通过debug看看源码是怎么实现的。

    在困惑的日子里学会拥抱源码

    ?

    拥抱源码

    调用流程图

    这里先用PPT画一个流程图,可以跟着流程图来看后面的源码。

    ?

    流程图

    「从newProxyInstance()设置断点」?

    newProxyInstance()

    newProxyInstance()代码分为上下两部分,上部分是获取类,下部分是通过反射构建Proxy0对象。

    「上部分代码」

    ?

    newProxyInstance()

    从名字看就知道getProxyClass0()是核心方法,step into

    getProxyClass0()

    ?

    getProxyClass()

    里面调用了WeakCache对象的get()方法,这里暂停一下debug,先讲讲WeakCache类。

    WeakCache

    顾名思义,它是一个弱引用缓存。那什么是是弱引用呢,是不是还有强引用呢?

    弱引用

    WeakReference就是弱引用类,作为包装类来包装其他对象,在进行GC时,其中的包装对象会被回收,而WeakReference对象会被放到引用队列中。

    举个栗子:

     //?这就是强引用,只要不写str1?=?null,str1指向的这个字符串不就会被垃圾回收
    ?String?str1?=?new?String("hello");
    ?ReferenceQueue?referenceQueue?=?new?ReferenceQueue();
    ?//?只要垃圾回收,这个str2里面包装的对象就会被回收,但是这个弱引用对象不会被回收,即word会被回收,但是str2指向的弱引用对象不会
    ?//?每个弱引用关联一个ReferenceQueue,当包装的对象被回收,这个弱引用对象会被放入引用队列中
    ?WeakReference<String>?str2?=?new?WeakReference<>(new?String("world"),?referenceQueue);
    ?//?执行gc
    ?System.gc();
    ?Thread.sleep(3);
    ?//?输出被回收包装对象的弱引用对象:java.lang.ref.WeakReference@2077d4de
    ?//?可以debug看一下,弱引用对象的referent变量指向的包装对象已经为null
    ?System.out.println(referenceQueue.poll()); 

    WeakCache的结构

    其实整个WeakCache的都是围绕着成员变量map来工作的,构建了一个一个<K,<K,V>>格式的二级缓存,在动态代理中对应的类型是<类加载器, <接口Class, 代理Class>>,它们都使用了弱引用进行包装,这样在垃圾回收的时候就可以直接回收,减少了堆内存占用。

    //?存放已回收弱引用的队列
    private?final?ReferenceQueue<K>?refQueue?=?new?ReferenceQueue<>();
    //?使用ConcurrentMap实现的二级缓存结构
    private?final?ConcurrentMap<Object,?ConcurrentMap<Object,?Supplier<V>>>?map?=?new?ConcurrentHashMap<>();
    //?可以不关注这个,这个是用来标识二级缓存中的value是否存在的,即Supplier是否被回收
    private?final?ConcurrentMap<Supplier<V>,?Boolean>?reverseMap?=?new?ConcurrentHashMap<>();
    //?包装传入的接口class,生成二级缓存的Key
    private?final?BiFunction<K,?P,??>?subKeyFactory?=?new?KeyFactory();
    //?包装$Proxy0,生成二级缓存的Value
    private?final?BiFunction<K,?P,?V>?valueFactory?=?new?ProxyClassFactory(); 

    WeakCache的get()

    回到debug,接着进入get()方法,看看map二级缓存是怎么生成KV的。

     public?V?get(K?key,?P?parameter)?{
    ????????Objects.requireNonNull(parameter);
    ????????//?遍历refQueue,然后将缓存map中对应的失效value删除
    ????????expungeStaleEntries();
    ????????//?以ClassLoader为key,构建map的一级缓存的Key,是CacheKey对象
    ????????Object?cacheKey?=?CacheK.valueOf(key,?refQueue);
    ????????//?通过Key从map中获取一级缓存的value,即ConcurrentMap
    ????????ConcurrentMap<Object,?Supplier<V>>?valuesMap?=?map.get(cacheKey);
    ????????if?(valuesMap?==?null)?{
    ?????????//?如果Key不存在,就新建一个ConCurrentMap放入map,这里使用的是putIfAbsent
    ?????????//?如果key已经存在了,就不覆盖并返回里面的value,不存在就返回null并放入Key
    ?????????//?现在缓存map的结构就是ConCurrentMap<CacheKey,?ConCurrentMap<Object,?Supplier>>
    ????????????ConcurrentMap<Object,?Supplier<V>>?oldValuesMap?=?map.putIfAbsent(cacheKey,?valuesMap?=?new?ConcurrentHashMap<>());
    ????????????//?如果其他线程已经创建了这个Key并放入就可以复用了
    ????????????if?(oldValuesMap?!=?null)?{
    ????????????????valuesMap?=?oldValuesMap;
    ????????????}
    ????????}
    ????????//?生成二级缓存的subKey,现在缓存map的结构就是ConCurrentMap<CacheKey,?ConCurrentMap<Key1,?Supplier>>
    ????????//?看后面的<生成二级缓存Key>!!!
    ????????Object?subKey?=?Objects.requireNonNull(subKeyFactory.apply(key,?parameter));
    ????????//?根据二级缓存的subKey获取value
    ????????Supplier<V>?supplier?=?valuesMap.get(subKey);
    ????????Factory?factory?=?null;
    ????????
    //?!!!直到完成二级缓存Value的构建才结束,Value是弱引用的$Proxy0.class!!!
    ????????while?(true)?{
    ???????????//?第一次循环:suppiler肯定是null,因为还没有将放入二级缓存的KV值
    ???????????//?第二次循环:这里suppiler不为null了!!!进入if
    ????????????if?(supplier?!=?null)?{
    ????????????????//?第二次循环:真正生成代理对象,
    ????????????????//?往后翻,看<生成二级缓存Value>,核心!!!!!
    ????????????????//?看完后面回到这里:value就是弱引用后的$Proxy0.class
    ????????????????V?value?=?supplier.get();
    ????????????????if?(value?!=?null)?{
    ?????????????//?本方法及上部分的最后一行代码,跳转最后的<构建$Proxy对象>
    ????????????????????return?value;
    ????????????????}
    ????????????}
    ??????????//?第一次循环:factory肯定为null,生成二级缓存的Value
    ????????????if?(factory?==?null)?{
    ????????????????factory?=?new?Factory(key,?parameter,?subKey,?valuesMap);
    ????????????}
    ?????????//?第一次循环:将subKey和factory作为KV放入二级缓存
    ????????????if?(supplier?==?null)?{
    ????????????????supplier?=?valuesMap.putIfAbsent(subKey,?factory);
    ????????????????if?(supplier?==?null)?{
    ????????????????????//?第一次循环:赋值之后suppiler就不为空了,记住!!!!!
    ????????????????????supplier?=?factory;
    ????????????????}
    ????????????}?
    ???????????}
    ????????}
    ????} 

    生成二级缓存Key

    在get()中调用subKeyFactory.apply(key, parameter),根据你newProxyInstance()传入的接口Class[]的个数来生成二级缓存的Key,这里我们就传入了一个UserInterface.class,所以就返回了Key1对象。

    ?

    KeyFactory.apply()

    不论是Key1、Key2还是KeyX,他们都继承了WeakReference,都是包装对象是Class的弱引用类。这里看看Key1的代码。

    ?

    Key1

    生成二级缓存Value

    在上面的while循环中,第一次循环只是生成了一个空的Factory对象放入了二级缓存的ConcurrentMap中。

    在第二次循环中,才开始通过get()方法来真正的构建value。

    别回头,接着往下看。

    Factory.get()生成弱引用value

    「CacheValue」类是一个弱引用,是二级缓存的Value值,包装的是class,在这里就是$Proxy0.class,至于这个类如何生成的,根据下面代码注释一直看完Class文件的生成

    public?synchronized?V?get()?{
    ????????????//?检查是否被回收,如果被回收,会继续执行上面的while循环,重新生成Factory
    ????????????Supplier<V>?supplier?=?valuesMap.get(subKey);
    ????????????if?(supplier?!=?this)?{
    ????????????????return?null;
    ????????????}
    ????????????//?这里的V的类型是Class
    ????????????V?value?=?null;
    ????????????//?这行是核心代码,看后面<class文件的生成>,记住这里返回的是Class
    ????????????value?=?Objects.requireNonNull(valueFactory.apply(key,?parameter));
    ????????????//?将Class对象包装成弱引用
    ????????????CacheValue<V>?cacheValue?=?new?CacheValue<>(value);
    ????????????//?回到上面<WeakCache的get()方法>V?value?=?supplier.get();
    ????????????return?value;
    ????????}
    ????} 

    ?

    CacheValue

    Class文件的生成

    包名类名的定义与验证

    进入valueFactory.apply(key, parameter)方法,看看class文件是怎么生成的。

     private?static?final?String?proxyClassNamePrefix?=?"$Proxy";
    
    ?public?Class<?>?apply(ClassLoader?loader,?Class<?>[]?interfaces)?{
    ????????????Map<Class<?>,?Boolean>?interfaceSet?=?new?IdentityHashMap<>(interfaces.length);
    ????????????//?遍历你传入的Class[],我们只传入了UserInterface.class
    ????????????for?(Class<?>?intf?:?interfaces)?{
    ????????????????Class<?>?interfaceClass?=?null;
    ?????????????????//?获取接口类
    ????????????????interfaceClass?=?Class.forName(intf.getName(),?false,?loader);
    ?????????????????//?这里就很明确为什么只能传入接口类,不是接口类会报错
    ????????????????if?(!interfaceClass.isInterface())?{
    ????????????????????throw?new?IllegalArgumentException(
    ????????????????????????interfaceClass.getName()?+?"?is?not?an?interface");
    ????????????????}
    ????????????String?proxyPkg?=?null;?
    ????????????int?accessFlags?=?Modifier.PUBLIC?|?Modifier.FINAL;
    ????????????for?(Class<?>?intf?:?interfaces)?{
    ????????????????int?flags?=?intf.getModifiers();
    ????????????????//?验证接口是否是public,不是public代理类会用接口的package,因为只有在同一包内才能继承
    ????????????????//?我们的UserInterface是public,所以跳过
    ????????????????if?(!Modifier.isPublic(flags))?{
    ????????????????????accessFlags?=?Modifier.FINAL;
    ????????????????????String?name?=?intf.getName();
    ????????????????????int?n?=?name.lastIndexOf('.');
    ????????????????????String?pkg?=?((n?==?-1)???""?:?name.substring(0,?n?+?1));
    ????????????????????if?(proxyPkg?==?null)?{
    ????????????????????????proxyPkg?=?pkg;
    ????????????????????}?else?if?(!pkg.equals(proxyPkg))?{
    ????????????????????????throw?new?IllegalArgumentException(
    ????????????????????????????"non-public?interfaces?from?different?packages");
    ????????????????????}
    ????????????????}
    ????????????}
    ?????????//?如果接口类是public,则用默认的包
    ????????????if?(proxyPkg?==?null)?{
    ????????????????//?PROXY_PACKAGE?=?"com.sun.proxy";
    ????????????????proxyPkg?=?ReflectUtil.PROXY_PACKAGE?+?".";
    ????????????}
    ?????????//?原子Int,此时num?=?0
    ????????????long?num?=?nextUniqueNumber.getAndIncrement();
    ????????????// com.sun.proxy.$Proxy0,这里包名和类名就出现了!!!
    ????????????String?proxyName?=?proxyPkg?+?proxyClassNamePrefix?+?num;
    ?????????//?!!!!生成class文件,查看后面<class文件写入本地>?核心!!!!
    ????????????byte[]?proxyClassFile?=?ProxyGenerator.generateProxyClass(proxyName,?interfaces,?accessFlags);
    ????????????//?!!!看完下面再回来看这行!!!!
    ????????????//?获取了字节数组之后,获取了class的二进制流将类加载到了JVM中
    ????????????//?并且返回了$Proxy0.class,返回给Factory.get()来包装
    ????????????return?defineClass0(loader,?proxyName,proxyClassFile,?0,?proxyClassFile.length);
    ???????????
    ????????????}
    ????????}
    ????} 

    defineClass0()是Proxy类自定义的类加载的native方法,会获取class文件的二进制流加载到JVM中,以获取对应的Class对象,这一块可以参考JVM类加载器。

    class文件写入本地

    generateProxyClass()方法会将class二进制文件写入本地目录,并返回class文件的二进制流,使用你传入的类加载器加载,「这里你知道类加载器的作用了么」

     public?static?byte[]?generateProxyClass(final?String?name,
    ????????????????????????????????????????????Class[]?interfaces)
    ????{
    ????????ProxyGenerator?gen?=?new?ProxyGenerator(name,?interfaces);
    ????????//?生成class文件的二进制,查看后面<生成class文件二进制>
    ????????final?byte[]?classFile?=?gen.generateClassFile();
    ??????//?将class文件写入本地??
    ????????if?(saveGeneratedFiles)?{
    ????????????java.security.AccessController.doPrivileged(
    ????????????new?java.security.PrivilegedAction<Void>()?{
    ????????????????public?Void?run()?{
    ????????????????????try?{
    ????????????????????????FileOutputStream?file?=
    ????????????????????????????new?FileOutputStream(dotToSlash(name)?+?".class");
    ????????????????????????file.write(classFile);
    ????????????????????????file.close();
    ????????????????????????return?null;
    ????????????????????}?catch?(IOException?e)?{
    ????????????????????????throw?new?InternalError(
    ????????????????????????????"I/O?exception?saving?generated?file:?"?+?e);
    ????????????????????}
    ????????????????}
    ????????????});
    ????????}
    ??????//?返回$Proxy0.class字节数组,回到上面<class文件生成>
    ????????return?classFile;
    ????} 

    生成class文件二进制流

    generateClassFile()生成class文件,并存放到字节数组,「可以顺便学一下class结构,这里也体现了你传入的class[]的作用」

     private?byte[]?generateClassFile()?{
    ??????//?将hashcode、equals、toString是三个方法放入代理类中
    ????????addProxyMethod(hashCodeMethod,?Object.class);
    ????????addProxyMethod(equalsMethod,?Object.class);
    ????????addProxyMethod(toStringMethod,?Object.class);
    ????????for?(int?i?=?0;?i?<?interfaces.length;?i++)?{
    ????????????Method[]?methods?=?interfaces[i].getMethods();
    ????????????for?(int?j?=?0;?j?<?methods.length;?j++)?{
    ?????????????//?将接口类的方法放入新建的代理类中,这里就是sayHello()
    ????????????????addProxyMethod(methods[j],?interfaces[i]);
    ????????????}
    ????????}
    ????????for?(List<ProxyMethod>?sigmethods?:?proxyMethods.values())?{
    ????????????checkReturnTypes(sigmethods);
    ????????}
    ????????//?给代理类增加构造方法
    ????????methods.add(generateConstructor());
    ????????for?(List<ProxyMethod>?sigmethods?:?proxyMethods.values())?{
    ????????????for?(ProxyMethod?pm?:?sigmethods)?{
    ???????????????????//?将上面的四个方法都封装成Method类型成员变量
    ????????????????????fields.add(new?FieldInfo(pm.methodFieldName,
    ????????????????????????"Ljava/lang/reflect/Method;",
    ?????????????????????????ACC_PRIVATE?|?ACC_STATIC));
    ????????????????????//?generate?code?for?proxy?method?and?add?it
    ????????????????????methods.add(pm.generateMethod());
    ????????????????}
    ????????????}
    ??????//?static静态块构造
    ????????methods.add(generateStaticInitializer());
    ????????cp.getClass(dotToSlash(className));
    ????????cp.getClass(superclassName);
    ????????for?(int?i?=?0;?i?<?interfaces.length;?i++)?{
    ????????????cp.getClass(dotToSlash(interfaces[i].getName()));
    ????????}
    ????????cp.setReadOnly();
    ????????ByteArrayOutputStream?bout?=?new?ByteArrayOutputStream();
    ????????DataOutputStream?dout?=?new?DataOutputStream(bout);
    ??????// !!!核心点来了!这里就开始构建class文件了,以下都是class的结构,只写一部分
    ????????try?{???
    ????????????// u4 magic,class文件的魔数,确认是否为一个能被JVM接受的class
    ????????????dout.writeInt(0xCAFEBABE);
    ????????????//?u2?minor_version,0
    ????????????dout.writeShort(CLASSFILE_MINOR_VERSION);
    ????????????//?u2?major_version,主版本号,Java8对应的是52;
    ????????????dout.writeShort(CLASSFILE_MAJOR_VERSION);
    ????????????//?常量池
    ????????????cp.write(dout);
    ????????????//?其他结构,可参考class文件结构
    ????????????dout.writeShort(ACC_PUBLIC?|?ACC_FINAL?|?ACC_SUPER);
    ????????????dout.writeShort(cp.getClass(dotToSlash(className)));
    ????????????dout.writeShort(cp.getClass(superclassName));
    ????????????dout.writeShort(interfaces.length);
    ????????????for?(int?i?=?0;?i?<?interfaces.length;?i++)?{
    ????????????????dout.writeShort(cp.getClass(
    ????????????????????dotToSlash(interfaces[i].getName())));
    ????????????}
    ????????????dout.writeShort(fields.size());
    ????????????for?(FieldInfo?f?:?fields)?{
    ????????????????f.write(dout);
    ????????????}
    ????????????dout.writeShort(methods.size());???????????
    ????????????for?(MethodInfo?m?:?methods)?{
    ????????????????m.write(dout);
    ????????????}
    ????????????dout.writeShort(0);?
    ????????}?catch?(IOException?e)?{
    ????????????throw?new?InternalError("unexpected?I/O?Exception",?e);
    ????????}
    ????????//?将class文件字节数组返回
    ????????return?bout.toByteArray();
    ????} 

    构建$Proxy对象

    newProxyInstance()上半部分经过上面层层代码调用,获取了$Proxy0.class,接下来看下部分代码:

    ?

    newInstance

    cl就是上面获取的Proxy0.class,h就是上面传入的userHandler,被当做构造参数来创建$Proxy0对象。然后获取这个动态代理对象,调用sayHello()方法,相当于调用了UserHandler的invoke(),「这里就是UserHandler的作用」

    $Proxy.class文件

    我们开启了将代理class写到本地目录的功能,在项目下的com/sum/proxy目录下找到了$Proxy0的class文件。

    「看一下反编译的class」

    package?com.sun.proxy;
    
    import?com.test.proxy.UserInterface;
    import?java.lang.reflect.InvocationHandler;
    import?java.lang.reflect.Method;
    import?java.lang.reflect.Proxy;
    import?java.lang.reflect.UndeclaredThrowableException;
    
    public?final?class?$Proxy0?extends?Proxy?implements?UserInterface?{
    ????private?static?Method?m1;
    ????private?static?Method?m3;
    ????private?static?Method?m2;
    ????private?static?Method?m0;
    
    ????public?$Proxy0(InvocationHandler?var1)?throws??{
    ????????super(var1);
    ????}
    
    ????public?final?boolean?equals(Object?var1)?throws??{
    ????????try?{
    ????????????return?(Boolean)super.h.invoke(this,?m1,?new?Object[]{var1});
    ????????}?catch?(RuntimeException?|?Error?var3)?{
    ????????????throw?var3;
    ????????}?catch?(Throwable?var4)?{
    ????????????throw?new?UndeclaredThrowableException(var4);
    ????????}
    ????}
    
    ????public?final?void?sayHello()?throws??{
    ????????try?{
    ????????????super.h.invoke(this,?m3,?(Object[])null);
    ????????}?catch?(RuntimeException?|?Error?var2)?{
    ????????????throw?var2;
    ????????}?catch?(Throwable?var3)?{
    ????????????throw?new?UndeclaredThrowableException(var3);
    ????????}
    ????}
    
    ????public?final?String?toString()?throws??{
    ????????try?{
    ????????????return?(String)super.h.invoke(this,?m2,?(Object[])null);
    ????????}?catch?(RuntimeException?|?Error?var2)?{
    ????????????throw?var2;
    ????????}?catch?(Throwable?var3)?{
    ????????????throw?new?UndeclaredThrowableException(var3);
    ????????}
    ????}
    
    ????public?final?int?hashCode()?throws??{
    ????????try?{
    ????????????return?(Integer)super.h.invoke(this,?m0,?(Object[])null);
    ????????}?catch?(RuntimeException?|?Error?var2)?{
    ????????????throw?var2;
    ????????}?catch?(Throwable?var3)?{
    ????????????throw?new?UndeclaredThrowableException(var3);
    ????????}
    ????}
    
    ????static?{
    ????????try?{
    ????????????m1?=?Class.forName("java.lang.Object").getMethod("equals",?Class.forName("java.lang.Object"));
    ????????????m3?=?Class.forName("com.test.proxy.UserInterface").getMethod("sayHello");
    ????????????m2?=?Class.forName("java.lang.Object").getMethod("toString");
    ????????????m0?=?Class.forName("java.lang.Object").getMethod("hashCode");
    ????????}?catch?(NoSuchMethodException?var2)?{
    ????????????throw?new?NoSuchMethodError(var2.getMessage());
    ????????}?catch?(ClassNotFoundException?var3)?{
    ????????????throw?new?NoClassDefFoundError(var3.getMessage());
    ????????}
    ????}
    } 

    结语

    上面就是动态代理源码的调试过程,与之前的猜想的代理类的生成过程比较,动态代理是直接生成class文件,省去了java文件和编译这一块。

    刚开始看可能比较绕,跟着注释及跳转指引,耐心多看两遍就明白了。动态代理涉及的知识点比较多,我自己看的时候,在WeakCache这一块纠结了一阵,其实把它当成一个两层的map对待即可,只不过里面所有的KV都被弱引用包装。

    希望看到这篇文章的每个程序员最终都能成为头发茂盛的码农;

    推荐阅读

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    字节跳动总结的设计模式 PDF 火了,完整版开放分享

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    MrZ 发布了文章 · 2020-12-15

    你只修改了2行代码,为什么需要两天时间?

    “你只修改了2行代码,为什么需要两天?”

    这是程序员最常碰到的质问,表面看这是一个非常合理的问题,但它做了一些不合适的假设:

    • 代码行数 = 努力
    • 代码行数 = 价值
    • 每一行代码价值都相同

    所幸上面这些断言都不是真的。

    一个简单的修复,为什么需要花两天时间?下面列举了一些常见原因。

    • 因为如何重现问题的描述很模糊。程序员可能需要花几个小时才能重现 bug。有些开发人员会立即联系报告 bug 的用户,要求提供更多的信息再进行分析。有些程序员会试着用提供的信息做尽可能多的事情。我知道有些开发者不喜欢修复 bug,所以会不惜一切代价来摆脱困境,声称问题不能重现是一种非常好的逃避方式,它让你看起来很想解决问题,但又不需要真的动手。我知道用户报告 bug 不容易,我也很感谢这样做的用户。我想通过在打扰用户询问更多细节之前,尽量多地使用所提供的信息来表达对报告 bug 用户的感谢。
    • 因为报告的问题与特定功能有关,但程序员不熟悉这块功能。这块代码不是他开发的,以前也比较少接触。如果去修的话,需要花费更长的时间来先了解这块的流程,以及这个问题怎么出现。
    • 因为花费了时间去分析问题的真正原因,而不仅仅是看表面现象。如果一些代码抛出了错误,你可以直接用 try...catch 语句把它包起来,吞下错误。这样错误就不见了,对吧?抱歉,对我来说,把问题掩盖不等于解决问题。"吞下"一个错误,很容易导致其他意想不到的副作用。我不希望在未来某个时间点上不得不来处理它。
    • 因为我分析了是否有其他方法可以重现这个问题,而不仅仅局限于报告提出的重现步骤。某一套重现步骤,容易让错误出现在某个地方,但实际上可能是更深层次的原因导致。找到问题的确切原因,并查看所有到达那里的方法,可以得到更有价值的意见。诸如代码实际是如何使用的,其他地方可能也有需要解决的问题,或者它可能由于代码中的使用不一致,这意味着错误是只在一个代码路径中引起,但不会在另一个出现。
    • 因为我花了时间来验证代码中是否有其他部分可能受到类似的影响。如果一个错误导致了 bug,那么同样的错误也可能在代码库的其他地方发生,现在是检查这个问题的最好时机。
    • 因为当我找到问题的原因时,我会寻找最简单的方法来修复,并将引入副作用的风险降到最低。我不想要最快速的修复方法,我需要一个不会在未来带来混乱或引入其他问题的修复方法。
    • 因为我彻底地测试了这个变更,并验证了受影响的不同代码路径的各种情况。我不想依靠别人来测试我修改的代码是否正确。我不想将来某一天又出现一个 bug,在我已经淡忘这个的时候,还要回到这段代码中来。上下文切换是昂贵的,而且很糟心。让一个专门的测试人员不得不再次查看同一个问题的变更,是我想尽可能避免的。

    我不喜欢修 bug,部分原因是会让人觉得是我之前的代码质量不好造成的。我不喜欢修 bug,另一个原因是我更愿意去研究新的东西。

    有什么比修 bug 更糟心的事情?那就是反复修复同一个 bug。

    我花了更长时间,是需要确保任何一次遇到的 bug 都被完全修复,这样就不需要再次去面对这个 bug、再次分析原因、修复和测试。

    推荐阅读

    为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

    字节跳动总结的设计模式 PDF 火了,完整版开放下载

    刷Github时发现了一本阿里大神的算法笔记!标星70.5K

    程序员50W年薪的知识体系与成长路线。

    月薪在30K以下的Java程序员,可能听不懂这个项目;

    字节跳动总结的设计模式 PDF 火了,完整版开放分享

    关于【暴力递归算法】你所不知道的思路

    开辟鸿蒙,谁做系统,聊聊华为微内核

    ?
    =

    看完三件事??

    如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

    点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

    关注公众号 『 Java斗帝 』,不定期分享原创知识。

    同时可以期待后续文章ing??

    查看原文

    赞 0 收藏 0 评论 0

    bt365体育投注