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

认证与成就

  • 获得 57 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-10-07
个人主页被 2.6k 人浏览