开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

2018-2021我的开源项目总结

发表于 2021-01-26

corwd-admin

本人18年6月份毕业在武汉找了第一份java开发工作4500(面试时被hr压了500,武汉当时行情第一年5000), 做的oa、库存管理相关系统,公司内系统架构主要是ssh,页面模板使用jsp😂,
干了差不多大半年觉得提升有限,在19年3月份辞职,想着在家写一套自己的后台管理系统, 于是就写出了crowd-adnin的第一个版本。当时写这个项目真的是废寝忘食, 参考了网上很多的管理项目后,取百家之所长,写出来满满的成就感😁。

项目介绍

crowd-admin是一个通用后台权限管理系统,集成了rbac权限管理、消息推送、邮件发送、任务调度、 代码生成、elfinder文件管理等常用功能,系统内各个业务按照模块划分,前台使用H+模板。
是一个java新人易于上手,学习之后能够快速融入企业开发的指导项目

主要特性
  • 项目按功能模块化,提升开发,测试效率
  • 支持后台消息推送
  • 集成elfinder进行文件管理
  • 支持数据字典
  • 支持邮件发送,采用activeMQ异步解耦
  • 支持在线用户监控,登出等操作
  • 支持redis/ehcache切换使用
  • 支持ip2region本地化
  • 支持多数据源操作
  • 集成日志切面,方便日志记录
  • 前端js代码简洁,清晰,避免过度封装
  • 支持统一输出异常,避免繁琐的判断
  • 在线地址
实例截图

系统登陆
系统登陆
首页
首页
用户管理
用户管理
通知管理
通知管理
文件管理
文件管理


waynboot-sso

本人在19年初辞职后找的第二份工作一份外包工作(当时外包人事开的7000)996, 现在想想996应该要10000😢, 外包项目结束辞职后在家写的一个单点登录项目, 因为在甲方公司的子项目中用到了单点登录技术, 就想自己写出来一套,
于是写了waynboot-sso项目, 把crwod-admin项目并用springboot重写了一遍作用子模块集成了进来, 当时写这个sso单点登陆还花了两张登陆登出的流程图,贴在下面见笑了😂

项目介绍

基于SpringBoot,Shiro,Redis,Mybatis-Plus,SSO的多模块系统,包含了SSO单点登陆, 通用后台管理,新蜂商城,每日一文等多个模块,支持Shiro与SSO模块的集成,易于上手,学习,二次开发。

主要特性
  • 项目按系统模块化,提升开发,测试效率
  • ssoserver为SSO模块,支持单点登录登出
  • admin模块支持Shiro + SSO使用
  • 新蜂商城包含前台和后端,后台系统支持SSO使用
  • 使用hessian作为各系统间rpc通信
  • 使用Mybatis-Plus作为数据层框架,代码简介高效
  • 页面模板使用thymeleaf,配置灵活
  • js代码简洁,清晰,避免过度封装
  • 支持统一输出异常,避免繁琐的判断
内置模块
  1. wayn-admin 后台权限管理系统
  2. wayn-cmomon 后台权限系统的通用类聚集模块
  3. wayn-framework 后台权限系统的核心配置模块,包含shiro,数据源等配置
  4. wayn-mall newbee-mall商城系统,包含前后端系统
  5. wayn-others 集成framework的爬虫模块,包含每日一文
  6. wayn-ssocore sso单点登录的核心模块
  7. wayn-ssoserver sso单点登录系统,供其他系统集成使用
单点登陆流程

sso登陆
sso登出


newbee-mall

题主20年春节之后在家赋闲写完了waynboot-sso项目心血来潮想着写一个商城系统😎, 在发现newbee-mall项目后,就在此基础上写了一个商城项目,添加了秒杀专区和优惠卷使用,
并在最近完善了秒杀专区,支持万人秒杀。说实话很感谢这个项目, 这个项目是题主github仓库第一个到达60star的项目😆

项目介绍
  1. 后台管理模块添加了优惠卷管理、秒杀管理,统计分析
  2. 前台添加了秒杀专区,可以购买秒杀商品
  3. 前台添加了优惠卷领取页面,再订单结算页面可以选择优惠卷使用
  4. 支付时添加了支付宝沙箱支付
  5. 本项目秉持原作者简单易用的原则,代码书写清晰,注释完整,便于新人理解,快速上手
  6. 在线地址
2021年1月14日 秒杀接口升级

本次升级主要在原有秒杀功能的基础上进行了完善,秒杀优化如下:

  1. 秒杀页面静态化
  2. 添加了秒杀接口限流,基于springAOP实现
  3. 添加了秒杀接口防止重复提交,基于spring拦截器实现
  4. 使用令牌桶算法过滤用户请求
  5. 使用redis-set数据结构判断用户是否买过秒杀商品
  6. 使用redis配合lua脚本进行原子自减,判断商品缓存库存是否大于0
  7. 获取商品缓存,判断秒杀商品是否再有效期内
  8. 执行存储过程(减库存 + 记录购买行为)
  9. 使用redis-set数据结构记录购买过的用户
  10. 返回用户秒杀成功VO
  11. 下单后启用秒杀订单5分钟未支付超期任务
  12. 订单5分钟内未支付则自动取消订单并回退库存
秒杀截图

秒杀专区为用户展示了后台设置的秒杀商品,在秒杀有效期内可以进行商品秒杀操作. 秒杀接口使用了接口限流、Redis以及储存过程提高秒杀操作的tps




感谢

newbee-mall 项目原作者十三提供的基础项目支持


2020-2021

众所周知2020年由于特殊原因题主没有外出打工于是在家附近找了个公司上班,这家公司是php技术栈搞互联网的, (题主做梦也没想到我老家这十八线城市还有一家搞互联网的公司) 当时题主想了今年不出去了,
于是在家自学了2个星期php之后就去家附近的公司上班了(其实是家里这边没有搞技术的其他公司了) , 工资给的4500,工资又给干回去了 😥, 干了快一年发现php干快速开发迭代确实比java快很多😂(此处求javaer放过),
题主就这样在这一年一边干php一边继续完善上述3个项目,附一张去年的开源贡献图

2021

一眨眼2021年农历春节就要到了,题主也不知道年后是该继续在家干php(干了大半年工资涨到6000了) 还是去大城市干java,在家干确实是真的很舒服,而且题主今年在家附近找到了女朋友,女朋友也很爱我, 真是纠结😂

结尾

其实说了这么多,还是希望大家能给题主点个star😘,如果这些项目对你们又帮助的话。 希望新的一年新冠疫情能快点结束,大家能早日回归正常生活

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:CAS和AQS底层原理了解?我:一篇文章堵住你的嘴

发表于 2021-01-26

写在前面

用XMind画了一张导图记录Java并发编程的学习笔记和一些面试解析(源文件对部分节点有详细备注和参考资料,欢迎关注我的公众号:阿风的架构笔记后台发送【并发】拿下载链接,已经完善更新):

CAS(Compare And Swap)原理分析

字面意思是比较和交换,先看看下面场景(A 和 B 线程同时执行下面的代码):

1
2
java复制代码int i = 10;  //代码 1
i = 20; //代码 2

场景 1:A 线程执行代码 1 和代码 2,然后 B 线程执行代码 1 和代码 2,CAS 成功。

场景 2:A 线程执行代码 1,此时 B 线程执行代码 1 和代码 2,A 线程执行代码 2,CAS 不成功,为什么呢?

因为 A 线程执行代码 1 时候会旧值(i 的内存地址的值 10)保存起来,执行代码 2 的时候先判断 i 的最新值(可能被其他线程修改了)跟旧值比较,如果相等则把 i 赋值为 20,如果不是则 CAS 不成功。CAS 是一个原子性操作,要么成功要么失败,CAS 操作用得比较多的是 sun.misc 包的 Unsafe 类,而 Java 并发包大量使用 Unsafe 类的 CAS 操作,比如:AtomicInteger 整数原子类(本质是自旋锁 + CAS),CAS 不需加锁,提高代码运行效率。也是一种乐观锁方式,我们通常认为在大多数场景下不会出现竞争资源的情况,如果 CAS 操作失败,会不断重试直到成功。

CAS 优点:资源竞争不大的场景系统开销小。

CAS 缺点:

  • 如果 CAS 长时间操作失败,即长时间自旋,会导致 CPU 开销大,但是可以使用 CPU 提供的 pause 指令,这个 pause 指令可以让自旋重试失败时 CPU 先睡眠一小段时间后再继续自旋重试 CAS 操作,jvm 支持 pause 指令,可以让性能提升一些。
  • 存在 ABA 问题,即原来内存地址的值是 A,然后被改为了 B,再被改为 A 值,此时 CAS 操作时认为该值未被改动过,ABA 问题可以引入版本号来解决,每次改动都让版本号 +1。Java 中处理 ABA 的一个方案是 AtomicStampedReference 类,它是使用一个 int 类型的字段作为版本号,每次修改之前都先获取版本号和当前线程持有的版本号比对,如果一致才进行修改操作,并把版本号 +1。
  • 无法保证代码块的原子性,CAS 只能保证单个变量的原子性操作,如果要保证多个变量的原子性操作就要使用悲观锁了。

AQS(AbstractQueuedSynchronizer)原理分析

字面意思是抽象的队列同步器,AQS 是一个同步器框架,它制定了一套多线程场景下访问共享资源的方案,Java 中很多同步类底层都是使用 AQS 实现,比如:ReentrantLock、CountDownLatch、ReentrantReadWriteLock,这些 java 同步类的内部会使用一个 Sync 内部类,而这个 Sync 继承了 AbstractQueuedSynchronizer 类,这是一种模板方法模式,所以说这些同步类的底层是使用 AQS 实现。

面试官:CAS和AQS底层原理了解?我:一篇文章堵住你的嘴

AQS 内部维护了一个 volatile 修饰的 int state 属性(共享资源)和一个先进先出的线程等待队列(即多线程竞争共享资源时被阻塞的线程会进入这个队列)。因为 state 是使用 volatile 修饰,所以在多线程之前可见,访问 state 的方式有 3 种,getState()、setState()和 compareAndSetState()。

AQS 定义了 3 种资源共享方式:

  • 独占锁(exclusive),保证只有一条线程执行,比如 ReentrantLock、AtomicInteger。
  • 共享锁(shared),允许多个线程同时执行,比如 CountDownLatch、Semaphore。
  • 同时实现独占和共享,比如 ReentrantReadWriteLock,允许多个线程同时执行读操作,只允许一条线程执行写操作。

ReentrantLock 和 CountDownLatch 都是自定义同步器,它们的内部类 Sync 都是继承了 AbstractQueuedSynchronizer,独占锁和共享锁的区别在于各自重写的获取和释放共享资源的方式不一样,至于线程获取资源失败、唤醒出队、中断等操作 AQS 已经实现好了。

ReentrantLock

state 的初始值是 0,即没有被锁定,当 A 线程 tryAcquire() 时会独占锁住 state,并且把 state+1,然后 B 线程(即其他线程)tryAcquire() 时就会失败进入等待队列,直到 A 线程 tryRelease() 释放锁把 state-1,此时也有可能出现重入锁的情况,state-1 后的值不是 0 而是一个正整数,因为重入锁也会 state+1,只有当 state=0 时,才代表其他线程可以 tryAcquire() 获取锁。

CountDownLatch

8 人赛跑场景,即开启 8 个线程进行赛跑,state 的初始值设置为 8(必须与线程数一致),每个参赛者跑到终点(即线程执行完毕)则调用 countDown(),使用 CAS 操作把 state-1,直到 8 个参赛者都跑到终点了(即 state=0),此时调用 await() 判断 state 是否为 0,如果是 0 则不阻塞继续执行后面的代码。

tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared() 的详细流程分析

tryAcquire() 详细流程如下:

  1. 调用 tryAcquire() 尝试获取共享资源,如果成功则返回 true;
  2. 如果不成功,则调用 addWaiter() 把此线程构造一个 Node 节点(标记为独占模式),并使用 CAS 操作把节点追加到等待队列的尾部,然后该 Node 节点的线程进入自旋状态;
  3. 线程自旋时,判断自旋节点的前驱节点是不是头结点,并且已经释放共享资源(即 state=0),自旋节点是否成功获取共享资源(即 state=1),如果三个条件都成立则自旋节点设置为头节点,如果不成立则把自旋节点的线程挂起,等待前驱节点唤醒。

面试官:CAS和AQS底层原理了解?我:一篇文章堵住你的嘴

tryRelease() 详细流程如下:

  1. 调用 tryRelease() 释放共享资源,即 state=0,然后唤醒没有被中断的后驱节点的线程;
  2. 被唤醒的线程自旋,判断自旋节点的前驱节点是不是头结点,是否已经释放共享资源(即 state=0),自旋节点是否成功获取共享资源(即 state=1),如果三个条件都成立则自旋节点设置为头节点,如果不成立则把自旋节点的线程挂起,等待被前驱节点唤醒。

tryAcquireShared() 详细流程如下:

  1. 调用 tryAcquireShared() 尝试获取共享资源,如果 state>=0,则表示同步状态(state)有剩余还可以让其他线程获取共享资源,此时获取成功返回;
  2. 如果 state<0,则表示获取共享资源失败,把此线程构造一个 Node 节点(标记为共享模式),并使用 CAS 操作把节点追加到等待队列的尾部,然后该 Node 节点的线程进入自旋状态;
  3. 线程自旋时,判断自旋节点的前驱节点是不是头结点,是否已经释放共享资源(即 state=0),再调用 tryAcquireShared() 尝试获取共享资源,如果三个条件都成立,则表示自旋节点可执行,同时把自旋节点设置为头节点,并且唤醒所有后继节点的线程。
  4. 如果不成立,挂起自旋的线程,等待被前驱节点唤醒。

tryReleaseShared() 详细流程如下:

  1. 调用 tryReleaseShared() 释放共享资源,即 state-1,然后遍历整个队列,唤醒所有没有被中断的后驱节点的线程;
  2. 被唤醒的线程自旋,判断自旋节点的前驱节点是不是头结点,是否已经释放共享资源(即 state=0),再调用 tryAcquireShared() 尝试获取共享资源,如果三个条件都成立,则表示自旋节点可执行,同时把自旋节点设置为头节点,并且唤醒所有后继节点的线程。
  3. 如果不成立,挂起自旋的线程,等待被前驱节点唤醒。

看完三件事❤️

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

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. 关注后回复【666】扫码即可获取架构进阶学习资料包

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

肝了一个月的Netty知识点(上)

发表于 2021-01-26

有情怀,有干货,微信搜索【三太子敖丙】关注这个不一样的程序员。

本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

高能预警,本文是我一个月前就开始写的,所以内容会非常长,当然也非常硬核,dubbo源码系列结束之后我就想着写一下netty系列的,但是netty的源码概念又非常多,所以才写到了现在。

我相信90%的读者都不会一口气看完的,因为实在太长了,长到我现在顶配的mbp打字编辑框都是卡的,但是我希望大家日后想看netty或者在面试前需要了解的朋友回头翻一下就够了,那我写这个文章的意义也就有了。

也不多BB,直接开整。

NIO 基本概念

阻塞(Block)与非阻塞(Non-Block)

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。

阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。

非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

阻塞 IO :

非阻塞 IO :

同步(Synchronous)与异步(Asynchronous)

同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。比如

**同步:**是应用程序要直接参与 IO 读写的操作。

**异步:**所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。

同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO事件的方式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的 IO 操作,当操作完成 IO 后,会给我们的应用程序一个通知。

所以异步相比较于同步带来的直接好处就是在我们处理IO数据的时候,异步的方式我们可以把这部分等待所消耗的资源用于处理其他事务,提升我们服务自身的性能。

同步 IO :

异步 IO :

Java BIO与NIO对比

BIO(传统IO):

BIO是一个同步并阻塞的IO模式,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。

NIO(Non-blocking/New I/O)

NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发

BIO与NIO的对比

IO模型 BIO NIO
通信 面向流 面向缓冲
处理 阻塞 IO 非阻塞 IO
触发 无 选择器

NIO 的 Server 通信的简单模型:

BIO 的 Server 通信的简单模型:

NIO的特点:

  1. 一个线程可以处理多个通道,减少线程创建数量;
  2. 读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费

Reactor 模型

单线程的 Reactor 模型

多线程的 Reactor 模型

多线程主从 Reactor 模型

Netty 基础概念

Netty 简介

Netty 是一个 NIO 客户端服务器框架,可快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了网络编程,例如 TCP 和 UDP 套接字服务器。

“快速简便”并不意味着最终的应用程序将遭受可维护性或性能问题的困扰。Netty 经过精心设计,结合了许多协议(例如FTP,SMTP,HTTP 以及各种基于二进制和文本的旧式协议)的实施经验。结果,Netty 成功地找到了一种无需妥协即可轻松实现开发,性能,稳定性和灵活性的方法。

Netty 执行流程

Netty 核心组件

Channel

​ Channel是 Java NIO 的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。

EventLoop 与 EventLoopGroup

​ EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。

​ EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。

​ Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。

​ 一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。

ServerBootstrap 与 Bootstrap

​ Bootstarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。

​ Bootstrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。

​ ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。

ChannelHandler 与 ChannelPipeline

​ ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。

ChannelFuture

​ Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener() 方法为该异步操作添加监 NIO 网络编程框架 Netty 听器,为其注册回调:当结果出来后马上调用执行。

​ Netty 的异步编程模型都是建立在 Future 与回调概念之上的。

Netty 源码阅读

源码阅读,最好可以再 Debug 的情况下进行,这样更容易帮助理解,因此在分析 Netty 前的我准备一个客户端和服务端的代码。

Netty - Server 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup parentGroup = new NioEventLoopGroup();
EventLoopGroup childGroup = new NioEventLoopGroup();
try {

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {

@Override
protected void initChannel(SocketChannel ch) throws Exception {

ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new SomeSocketServerHandler());
}
});

ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("服务器已启动。。。");

future.channel().closeFuture().sync();
} finally {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}

Server 端 Handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class DemoSocketServerHandler
extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("Client Address ====== " + ctx.channel().remoteAddress());
ctx.channel().writeAndFlush("from server:" + UUID.randomUUID());
ctx.fireChannelActive();
TimeUnit.MILLISECONDS.sleep(500);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

Netty - Client 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class NettyClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new DemoSocketClientHandler());
}
});

ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
future.channel().closeFuture().sync();
} finally {
if(eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
}
}
}

Client 端 Handler :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class DemoSocketClientHandler
extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println(msg);
ctx.channel().writeAndFlush("from client: " + System.currentTimeMillis());
TimeUnit.MILLISECONDS.sleep(5000);
}

@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
ctx.channel().writeAndFlush("from client:begin talking");
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

NioEventLoopGroup 初始化分析

首先根据 Server 服务端代码,分析 NioEventLoopGroup 的初始化过程。而在分析 NioEventLoopGroup 之前,有必要简单的说一说 NioEventLoopGroup 与 NioEventLoop ,方便后续源码的理解。

NioEventLoop 源码分析前了解

NioEventLoop 的继承体系

从 NioEventLoop 的继承体系中可以看到,NioEventLoop 本身就是一个 Executor,并且还是一个 单线程的 Executor。Executor 必然拥有一个 execute(Runnable command) 的实现方法,而 NioEventLoop 的 execute() 实现方法在其父类 SingleThreadEventExecutor 中,找到具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码    public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}

boolean inEventLoop = inEventLoop();
addTask(task);
if (!inEventLoop) {
startThread();
if (isShutdown()) {
boolean reject = false;
try {
if (removeTask(task)) {
reject = true;
}
} catch (UnsupportedOperationException e) {
// The task queue does not support removal so the best thing we can do is to just move on and
// hope we will be able to pick-up the task before its completely terminated.
// In worst case we will log on termination.
}
if (reject) {
reject();
}
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}

这里不细说,但是贴出这段代码主要为了引出 startThread(); 这句代码,在跟这句代码会发现,它最终调用了 NioEventLoop 的一个成员 Executor 执行了当前成员的 execute() 方法。对应的成员 io.netty.util.concurrent.SingleThreadEventExecutor#executor

而 executor 成员的初始化也是在当前代码执行时创建的匿名 Executor ,也就是执行到即新建并且执行当前 匿名 executr() 方法。

总结:

  1. NioEventLoop 本身就是一个 Executor。
  2. NioEventLoop 内部封装这一个新的线程 Executor 成员。
  3. NioEventLoop 有两个 execute 方法,除了本身的 execute() 方法对应的还有成员属性 Executor 对应的 execute() 方法。

备注: 因为这里出现了四个 Executor,为了区分,我们给其新的名称:

NioEventLoop 本身 Executor:NioEventLoop

NioEventLoop 的成员 Executor:子 Executor

NioEventLoopGroup 本身 Executor :NioEventLoopGroup

NioEventLoopGroup 的构造参数 Executor :总Executor

NioEventLoopGroup 的继承体系

看到继承体系可以直接知道 NioEventLoopGroup 也是一个 Executor,并且是一个线程池的 Executor,所以他也有 execute() 方法。对应的实现再其父类之中:io.netty.util.concurrent.AbstractEventExecutorGroup#execute

而这里还需要说到的一点是:在 NioEventLoopGroup 的构造中,再其父类 MultithreadEventExecutorGroup 的构造再次引入了一个新的 Executor,

之所以这里提到这个 Executor,是因为这个 Executor 是对应的 execute() 就是在 NioEventLoop 中的成员 Executor 的 execute() 执行时调用的。也就是下面对应的代码调用。io.netty.util.internal.ThreadExecutorMap#apply(java.util.concurrent.Executor, io.netty.util.concurrent.EventExecutor)

到这如果不明白,没关系,因为只是为了引入 NioEventLoopGroup 和 NioEventLoop 的对应的两个 Executor,和两个 Executor 对应的两个 execute() 方法。这个后面还会有详细分析。

总结:

  1. NioEventLoopGroup 是一个线程池线程 Executor。
  2. NioEventLoopGroup 也封装了一个线程 Executor。
  3. NioEventLoopGroup 也有两个 execute()方法。

NioEventLoopGroup 初始化代码分析

上面说了基本的了解内容,下面具体分析,从 NioEventLoopGroup 的初始化进入源码分析。

入口我们直接找 NioEventLoopGroup 的无参构造。

1
2
3
java复制代码    public NioEventLoopGroup() {
this(0);
}
1
2
3
4
java复制代码    public NioEventLoopGroup(int nThreads) {
// 第二个参数是这个group所包含的executor
this(nThreads, (Executor) null);
}
1
2
3
4
5
java复制代码    public NioEventLoopGroup(int nThreads, Executor executor) {
// 第三个参数是provider,其用于提供selector及selectable的channel,
// 这个provider是当前JVM中唯一的一个单例的provider
this(nThreads, executor, SelectorProvider.provider());
}
1
2
3
4
5
java复制代码    public NioEventLoopGroup(
int nThreads, Executor executor, final SelectorProvider selectorProvider) {
// 第四个参数是一个选择策略工厂实例
this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
}
1
2
3
4
java复制代码    public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
final SelectStrategyFactory selectStrategyFactory) {
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
1
2
3
java复制代码    protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
1
2
3
4
java复制代码    protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
// 第三个参数是选择器工厂实例
this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}

跟到此,可以发现无参构造的基本参数被初始化, nThreads :DEFAULT_EVENT_LOOP_THREADS//默认当前CPU逻辑核心数的两倍,selectorProvide:SelectorProvider.provider()//当前JVM中唯一的一个单例的provider,SelectStrategyFactory:DefaultSelectStrategyFactory.INSTANCE//默认选择策略工厂实例,chooserFactory:DefaultEventExecutorChooserFactory.INSTANCE//选择器工厂实例。到这里只是基本的初始化参数,重点方法为MultithreadEventExecutorGroup 的构造方法。下面重点分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
java复制代码    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}

if (executor == null) {
// 这个executor是group所包含的executor,其将来会为其所包含的每个eventLoop创建一个线程
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}

children = new EventExecutor[nThreads];

for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// 创建eventLoop
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
// 在创建这些eventLoop过程中,只要有一个创建失败,则关闭之前所有已经创建好的eventLoop
if (!success) {
// 关闭之前所有已经创建好的eventLoop
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}

// 终止所有eventLoop上所执行的任务
for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}

// 创建一个选择器
chooser = chooserFactory.newChooser(children);

final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
};

for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}

Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}

根据无参构造直接往下跟,可以看到核心部分在最后一个父类的构造里。也就是 io.netty.util.concurrent.MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, java.util.concurrent.Executor, io.netty.util.concurrent.EventExecutorChooserFactory, java.lang.Object...)。

再这里完成整个 NioEventLoopGroup 的实例初始化,这里分析下,然后再画个图回顾下。

初始化构造参数中的 Executor 参数,当其为空时,将其初始化

1
java复制代码executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());

首先 newDefaultThreadFactory()) 创建默认的线程工厂,有兴趣可以跟进去看看。然后再创建ThreadPerTaskExecutor线程 Executor 对象。(PS:这里创建的 Executor 就是 NioEventLoopGroup 内的 Executor 对象,并不是当前 NioEventLoopGroup 自身,可以称其为 总 Executor)。

然后可以看到这里创建了一个 children 数组,根据需要创建的线程数创建对应数量的数组。

1
java复制代码children = new EventExecutor[nThreads];

因为每个 NioEventLoopGroup 都是 NioEventLoop 的集合,所以这里的 children 数组就是当前 NioEventLoopGroup 的 NioEventLoop。所以 NioEventLoop 的创建的实在 NioEventLoopGroup 初始化的时候。下面看 NioEventLoop 的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码// 逐个创建nioEventLoop实例
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// 创建eventLoop
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
// 在创建这些eventLoop过程中,只要有一个创建失败,则关闭之前所有已经创建好的eventLoop
if (!success) {
// 闭之前所有已经创建好的eventLoop
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}

// 终止所有eventLoop上所执行的任务
for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
}

先整体看这段 NioEventLoop 的创建代码,可以看到整个过程中存在一个成功标志,catch 每个 NioEventLoop 创建完成过程,如果发生异常则将所有已经创建的 NioEventLoop 关闭。重点的代码也就在 NioEventLoop 的创建了。所以我们继续跟:children[i] = newChild(executor, args);往下走,直接找到 io.netty.channel.nio.NioEventLoopGroup#newChild ,因为当前是 NioEventLoopGroup 的创建,所以知道找到子类的 newChild 实现。

1
2
3
4
5
java复制代码@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}

又将之前合并的 args 参数强转回来,继续跟进 NioEventLoop 构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
}
if (strategy == null) {
throw new NullPointerException("selectStrategy");
}
provider = selectorProvider;
// 创建一个selector的二元组
final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;
unwrappedSelector = selectorTuple.unwrappedSelector;
selectStrategy = strategy;
}

这里我们先整体看下,将之前的默认参数初始化到 NioEventLoop 属性中。其中有两处:openSelector() 和 super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler)。这里先看父类构造:

往下跟,直接就是 SingleThreadEventLoop -> SingleThreadEventExecutor 的初始化,这些也可以在 NioEventLoop 的继承体系可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// io.netty.channel.SingleThreadEventLoop#SingleThreadEventLoop
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,
boolean addTaskWakesUp, int maxPendingTasks,
RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);
// 创建一个收尾队列
tailTasks = newTaskQueue(maxPendingTasks);
}

// io.netty.util.concurrent.SingleThreadEventExecutor#SingleThreadEventExecutor
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
boolean addTaskWakesUp, int maxPendingTasks,
RejectedExecutionHandler rejectedHandler) {
super(parent);
this.addTaskWakesUp = addTaskWakesUp;
this.maxPendingTasks = Math.max(16, maxPendingTasks);
// 这是当前NioEventLoop所包含的executor
this.executor = ThreadExecutorMap.apply(executor, this);
// 创建一个任务队列
taskQueue = newTaskQueue(this.maxPendingTasks);
rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}

这里首先创建的是 SingleThreadEventExecutor ,这里重点需要关注的代码是:

1
java复制代码this.executor = ThreadExecutorMap.apply(executor, this);

这里this 是 NioEventLoop ,所以this.executor 就是前面说的 NioEventLoop 里的 Executor,这里我们先称为 子 Executor(子:对应的就是 NioEventLoop ,前面说的 总:对应的是 NioEventLoopGroup )。

而这里 子 Executor 的初始化是由一个 executor 参数的,这个就是前面 NioEventLoopGroup 构造方法一直带入的 总 Executor。那我们继续往下跟,看看这个子 Executor 是如何完成的初始化的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    public static Executor apply(final Executor executor, final EventExecutor eventExecutor) {
ObjectUtil.checkNotNull(executor, "executor");
ObjectUtil.checkNotNull(eventExecutor, "eventExecutor");
// 这里创建的executor是子executor
return new Executor() {
// 这个execute()是子executor的execute()
@Override
public void execute(final Runnable command) {
// 这里调用了NioEventLoopGroup所包含的executor的execute()
// 即调用了“总的executor”的execute()
executor.execute(apply(command, eventExecutor));
}
};
}

这段代码细看就会明白,这里创建的 子 Executor的创建也就是一个线程的创建,但是重点却在这个线程 Executor 的 execute()方法实现,只做了一件事情:就是调用 传入的 总 Executor 的 execute()方法。所以这里 子 Executor 做的事情就是调用 总 Executor 的 execute()。不要觉得这里绕,因为这还只是初始化,后面这里执行会更绕。[手动捂脸哭]

其实这里的 apply(command, eventExecutor),这里再执行 总 Executor 的 execute() 时还是会记录当前正在执行的线程,并且再执行完成时将当前记录值删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static Runnable apply(final Runnable command, final EventExecutor eventExecutor) {
ObjectUtil.checkNotNull(command, "command");
ObjectUtil.checkNotNull(eventExecutor, "eventExecutor");
return new Runnable() {
@Override
public void run() {
setCurrentEventExecutor(eventExecutor);
try {
command.run();
} finally {
setCurrentEventExecutor(null);
}
}
};
}

这里再 NioEventLoop 的属性 Executor 创建完成时,又去创建了一个普通任务队列taskQueue = newTaskQueue(this.maxPendingTasks);并且还创建了一个收尾任务队列tailTasks = newTaskQueue(maxPendingTasks);。这几个队列后面会说到。这里继续跟 NioEventLoop 主流程初始化。

到这我们再回去看看 openSelector(),这里我们要先知道 SelectorTuple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码private static final class SelectorTuple {
final Selector unwrappedSelector; // NIO原生selector
final Selector selector; // 优化过的selector

SelectorTuple(Selector unwrappedSelector) {
this.unwrappedSelector = unwrappedSelector;
this.selector = unwrappedSelector;
}

SelectorTuple(Selector unwrappedSelector, Selector selector) {
this.unwrappedSelector = unwrappedSelector;
this.selector = selector;
}
}

SelectorTuple 只是一个包含两个 Selector 的内部类,用于封装优化前后的 Selector。而 openSelector() 方法就是为了返回 Selector 并且根据配置判断是否需要优化当前 Selector 。下面看具体代码:

而具体的优化过程有兴趣的可以自己去看看,这里只要知道,若是禁用了优化则 SelectorTuple 的优化后的 Selector 和为优化的 Selector 均为 Nio 原生的 Selector。

而这io.netty.util.concurrent.MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, java.util.concurrent.Executor, io.netty.util.concurrent.EventExecutorChooserFactory, java.lang.Object...)后面还有在 NioEventLoop 数组创建完成后,还有选择器创建和关闭监听器绑定等,感兴趣可以自己看看,这里不再介绍。

到这一个 NioEventLoop 的创建过程的代码也全部看完了。我想如果只看这个肯定还是有点懵,源码这个东西需要自己跟进去去看,debug 一点点的跟,跟着运行的代码去想为何这么实现,不过这里我也画个图,让大家更直观的了解到 NioEventLoopGroup 的创建流程以及主要操作。

我想大家结合这个图,再结合上面的分析过程,最好可以自己找到源码,跟一遍,应该可以理解 NioEvnetLoopGroup 的创建。

ServerBootstrap与 ServerBootstrap 属性配置分析

继承体系:

入口代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
// (非必备)打印日志
.handler(new LoggingHandler(LogLevel.INFO))
// 4.指定 IO 模型
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定义客户端消息的业务处理逻辑
p.addLast(new HelloServerHandler());
}
});

Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new SomeSocketClientHandler());
}
});

ServerBootstrap与 Bootstrap 都是启动配置类,唯一不同的是,ServerBootstrap是服务端的启动配置类,Bootstrap 则是客户端的启动配置类,主要用于绑定我们创建的 EventLoopGroup,指定 Channel 的类型以及绑定 Channel 处理器等操作,主要做的都是给 ServerBootstrap与 Bootstrap 的属性赋值操作,所以称其为配置类。可以进入 group() 方法里看一眼:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (childGroup == null) {
throw new NullPointerException("childGroup");
}
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}

其他的方法也是一样,感兴趣可以自己进去看看。这里只是初始化,都是为了后面的操作做准备。

服务端 bind 方法 ServerBootstrap.bind() 源码解析

这里我们从这里进入:

1
java复制代码b.bind(port).sync();

直接从 bind() 方法跟进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// io.netty.bootstrap.AbstractBootstrap#bind(int)
public ChannelFuture bind(int inetPort) {
return bind(new InetSocketAddress(inetPort));
}

// 继续跟进
public ChannelFuture bind(SocketAddress localAddress) {
// 验证group与channelFactory是否为null
validate();
if (localAddress == null) {
throw new NullPointerException("localAddress");
}
// 这里是一处重点逻辑
return doBind(localAddress);
}

这里显示校验了 Bootstrap 的 group 与 channelFactory 是否绑定成功。然后继续跟进 doBind() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码private ChannelFuture doBind(final SocketAddress localAddress) {
// 创建、初始化channel,并将其注册到selector,返回一个异步结果
final ChannelFuture regFuture = initAndRegister();
// 从异步结果中获取channel
final Channel channel = regFuture.channel();
// 若异步操作执行过程中出现了异常,则直接返回异步对象(直接结束)
if (regFuture.cause() != null) {
return regFuture;
}

// 处理异步操作完成的情况(可能是正常结束,或发生异常,或任务取消,这些情况都属于有结果的情况)
if (regFuture.isDone()) {
ChannelPromise promise = channel.newPromise();
// 绑定指定的端口
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else { // 处理异步操作尚未有结果的情况
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
// 为异步操作添加监听
regFuture.addListener(new ChannelFutureListener() {
// 若异步操作具有了结果(即完成),则触发该方法的执行
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) { // 异步操作执行过程中出现了问题
promise.setFailure(cause);
} else { // 异步操作正常结果
promise.registered();
// 绑定指定的端口
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}

首先再这里,我们先把这个方法整体的逻辑搞清楚,然后再再去研究他的每一步具体的操作,画个图,先理解这个方法做了什么:

可以在图中结合代码,找到整个 dobind() 的大局处理思路,然后呢,到这里我们还有很多重点细节需要继续跟进,也就是图中标记的 Tag 1、Tag 2。为了方便后面跟进去代码之后方便回来,这里以此标记,然后下面在具体分析 Tag 标记的源码:

补充 Tag 0 :

ChannelPromise 与 ChannelFuture 了解。

Tag 1 :

异步创建、初始化channel,并将其注册到selector

final ChannelFuture regFuture = initAndRegister();

Tag 2 :

绑定指定的端口号:

doBind0(regFuture, channel, localAddress, promise);

补充 Tag 0:ChannelPromise 与 ChannelFuture

ChannelPromise 是一个特殊的 ChannelFuture,是一个可修改的 ChannelFuture。内部提供了修改当前 Future 状态的方法。在 ChannelFuture 的基础上实现了设置最终状态的修改方法。

而 ChannelFuture 只可以查询当前异步操作的结果,不可以修改当前异步结果的 Future 。这里需要知道的就是 ChannelPromise 可以修改当前异步结果的状态,并且在修改状态是会触发监听器。在 doBind 方法中主要用于在处理异步执行一直未结束的的操作,将异步结果存在异常的时,将异常赋值给 ChannelPromise 并返回。

Tag 1 : initAndRegister() 初始化并注册 Channel

先找到代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码final ChannelFuture initAndRegister() {
Channel channel = null;
try {
// 创建channel
channel = channelFactory.newChannel();
// 初始化channel
init(channel);
} catch (Throwable t) {
if (channel != null) {
channel.unsafe().closeForcibly();
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
}

// 将channel注册到selector
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}

嗯?!代码意一看,咋就这么点,也就做了三件事,可是这三件事做的每一个都不是一句代码的可以完成的。这里我们一个一个分析,除了这三件事情,其他的也就是异常后的处理逻辑,所以主流程就是下面的三句代码,也为了跟进继续打上标记吧:

Tag 1.1 创建channel
channel = channelFactory.newChannel();

Tag 1.2 初始化channel
init(channel);

Tag 1.3 将channel注册到selector
ChannelFuture regFuture = config().group().register(channel);

针对这三处,还是要一处一处分析。

Tag 1.1 channelFactory.newChannel() 创建 Channel

找到对应的代码:io.netty.channel.ReflectiveChannelFactory#newChannel

1
2
3
4
5
6
7
8
9
java复制代码@Override
public T newChannel() {
try {
// 调用无参构造器创建channel
return constructor.newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);
}
}

这里为什么直接找到 ReflectiveChannelFactory ,需要提一下,在分析 ServerBootstrap与 Bootstrap 启动配置类的时候,设置 channel 的方法,跟进去可以找到针对属性 channelFactory 的赋值代码:

1
2
3
4
5
6
java复制代码public B channel(Class<? extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}

可以看到这里 new 的就是 ReflectiveChannelFactory 工厂类,然后再看 ReflectiveChannelFactory 的构造:

1
2
3
4
5
6
7
8
9
10
java复制代码public ReflectiveChannelFactory(Class<? extends T> clazz) {
ObjectUtil.checkNotNull(clazz, "clazz");
try {
// 将NioServerSocketChannel的无参构造器初始化到constructor
this.constructor = clazz.getConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) +
" does not have a public non-arg constructor", e);
}
}

看到的是 ReflectiveChannelFactory 在创建时初始化了 constructor 属性,将传入的 channel 类 clazz 中获取构造赋值给了 ReflectiveChannelFactory 反射工厂的 constructor 属性。

而我们再 Server 端传入的 channel 类为NioServerSocketChannel.class ,所以上面看的 constructor.newInstance(); 对应的也就是 NioServerSocketChannel 的无参构造。这样我们就继续跟进 NioServerSocketChannel :

1
2
3
4
5
6
7
java复制代码// NIO中的provider,其用于创建selector与channel。并且是单例的
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();

public NioServerSocketChannel() {
// DEFAULT_SELECTOR_PROVIDER 静态变量
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

继续跟进 newSocket() :

1
2
3
4
5
6
7
8
9
java复制代码private static ServerSocketChannel newSocket(SelectorProvider provider) {
try {
// 创建NIO原生的channel => ServerSocketChannel
return provider.openServerSocketChannel();
} catch (IOException e) {
throw new ChannelException(
"Failed to open a server socket.", e);
}
}

就是返回了一个 Java NIO 原生的 Channel,最后将 NIO 原生的Channel 包装成 NioServerSocketChannel,继续跟进 this(newSocket(DEFAULT_SELECTOR_PROVIDER)) 找到有参构造具体代码:

1
2
3
4
5
6
7
8
java复制代码public NioServerSocketChannel(ServerSocketChannel channel) {
// 参数1:父channel
// 参数2:NIO原生channel
// 参数3:指定当前channel所关注的事件为 接受连接
super(null, channel, SelectionKey.OP_ACCEPT);
// 用于对channel进行配置的属性集合
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

这里主要做了两件事情,1. 调用父类构造,2. 对 channel 进行配置属性集合。

这里先说下 new NioServerSocketChannelConfig(),这部操作就是给当前 Channel 的 config 进行赋值,用来保存当前 Channel 的属性配置的集合。好了,这个说了我们继续跟主线:super(null, channel, SelectionKey.OP_ACCEPT)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码// io.netty.channel.nio.AbstractNioMessageChannel#AbstractNioMessageChannel
protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent, ch, readInterestOp);
}

// io.netty.channel.nio.AbstractNioChannel#AbstractNioChannel
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
// 这里的this.ch为NIO原生channel
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
// NIO,非阻塞
ch.configureBlocking(false);
} catch (IOException e) {
try {
ch.close();
} catch (IOException e2) {
if (logger.isWarnEnabled()) {
logger.warn(
"Failed to close a partially initialized socket.", e2);
}
}
throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}

直接找到 AbstractNioChannel 父类构造,这也第一步也是调用父类构造 super(parent); 先记着,先看除了调用父类构造还做了什么事情:

  1. 调用父类构造 super(parent);
  2. 将前面创建的原生 Channel 复制给属性保存 this.ch = ch;
  3. 当前 channel 的关注事件属性赋值 this.readInterestOp = readInterestOp; // SelectionKey.OP_ACCEPT 接受事件
  4. 将 NIO 原生 Channel 设置为非阻塞 ch.configureBlocking(false);

在 AbstractNioChannel 构造中就做了这么四件事情,主要需要说的还是其调用父类构造又做了什么事情,找到代码:

1
2
3
4
5
6
7
8
9
10
java复制代码// io.netty.channel.AbstractChannel#AbstractChannel(io.netty.channel.Channel)
protected AbstractChannel(Channel parent) {
this.parent = parent;
// 为channel生成id,由五部分构成
id = newId();
// 生成一个底层操作对象unsafe
unsafe = newUnsafe();
// 创建与这个channel相绑定的channelPipeline
pipeline = newChannelPipeline();
}

在 AbstractChannel 构造中主要做了三件事:

  1. 为当前 Channel 生成 id newId(),感兴趣可以跟进去看看。
  2. 生成一个底层操作对象 unsafe,用于 I/O 线程调用传输时使用,用户代码无法调用。newUnsafe()
  3. 创建与这个channel相绑定的channelPipeline,这也是一个重点操作,不过在这里先不展开细说,后面会单独细跟 channelPipeline 的代码。

所以到此 **Tag 1 : initAndRegister() ** 中的 **Tag 1.1 newChannel() ** 创建 Channel 才算跟完。针对 Tag 1.1 newChannel() 我们也画图简图整理下思路:

根据图,在结合上面代码的分析,最好自己再可以跟一遍代码,我想这一块的理解还是没什么问题的。到这也只是创建了 Channel。Tag 1.1 的 Channel 创建结束,接着跟进 Tag 1.2 init(channel).

Tag 1.2 init(channel) 初始化 Channel

这里我们是从 ServerBootstrap 中的doBind 进入的,所以这里直接找到 io.netty.bootstrap.ServerBootstrap#init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
java复制代码void init(Channel channel) throws Exception {
// 获取serverBootstrap中的options属性
final Map<ChannelOption<?>, Object> options = options0();
// 将options属性设置到channel
synchronized (options) {
setChannelOptions(channel, options, logger);
}

// 获取serverBootstrap中的attrs属性
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
// 遍历attrs属性
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
// 将当前遍历的attr初始化到channel
channel.attr(key).set(e.getValue());
}
}

// 获取channel的pipeline
ChannelPipeline p = channel.pipeline();

// 将serverBootstrap中所有以child开头的属性写入到局部变量,
// 然后将它们初始化到childChannel中
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));
}

p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}

ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
// 将ServerBootstrapAcceptor处理器添加到pipeline
// ServerBootstrapAcceptor处理器用于接收ServerBootstrap中的属性值,
// 我们通常称其为连接处理器
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

这里做的事情还是很多的,基本操作我在上面注释上也标注出来,还有一些需要继续跟下去的主要操作,还是先标记 Tag 然后继续跟下去。这里说一下这里的 options 与 attrs 属性的赋值,其实就是讲我们 ServerBootstrap 与 Bootstrap 在调用 doBind() 之前通过 option() 与 attr() 设置的参数值,其中 options 属性设置到了 Channel 的 config 属性中,attrs 是直接被设置在了 Channel 上的。

在设置完 options 属性与 attrs 属性时,接着获取了当前 channel 的 pipeline,接下来还是获取我们在 doBind() 之前设置的属性值,以 child 开头的方法 childOption() 与 childAttr() 设置的属性值。

这里使用局部变量记录了所有 Child 相关的值 currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs 主要用于初始化 childChannel 的属性,new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)) 主要是创建 连接处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}

ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
// 将ServerBootstrapAcceptor处理器添加到pipeline
// ServerBootstrapAcceptor处理器用于接收ServerBootstrap中的属性值,
// 我们通常称其为连接处理器
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});

首先这里想做的事情是:将当前 channel 的 pipeline 中绑定一个初始化处理器 ChannelInitializer ,因为是抽象类,所以需要匿名实现 initChannel方法。 而这些主要的操作是处理 childGroup 里面的 channel 的初始化操作。这里我只想主要讲一下这个连接处理器 ServerBootstrapAcceptor 主要做了什么,其他的具体会在后面的 handler 和 pipeline 的时候细说。

**补充:**这里因为 ServerBootstrap 服务端是对用的有两个 EventLoopGroup,在服务端,parentGroup 是用于接收客户端的连接,在 parentGroup 接收到连接之后是将只是将当前转给了 childGroup去处理后续操作,而 childGroup 是用来专门处理连接后的操作的,不关心 channel 的连接任务。这个其实就是 Netty-Server 的 Reactor 线程池模型的处理逻辑。

这里主要往下说一下这个连接处理器: ServerBootstrapAcceptor 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码ServerBootstrapAcceptor(
final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler,
Entry<ChannelOption<?>, Object>[] childOptions, Entry<AttributeKey<?>, Object>[] childAttrs) {
this.childGroup = childGroup;
this.childHandler = childHandler;
this.childOptions = childOptions;
this.childAttrs = childAttrs;

// See https://github.com/netty/netty/issues/1328
enableAutoReadTask = new Runnable() {
@Override
public void run() {
channel.config().setAutoRead(true);
}
};
}

ServerBootstrapAcceptor 构造只是将 ServerBootstrap 中配置的 Child 属性设置保存下来。而这里一直说这是连接处理器,是因为当客户端连接发送到服务端时,这个处理器会接收客户端的连接并处理。主要是处理方法是 channelRead 中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public void channelRead(ChannelHandlerContext ctx, Object msg) {
// msg为客户端发送来的数据,其为NioSocketChannel,即子channel,childChannel
final Channel child = (Channel) msg;

// 将来自于ServerBootstrap的child开头属性初始化到childChannel中(childHandler、childOptions、childAttrs)
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}

try {
// 将childChannel注册到selector 需要注意的是,这里的selector与父channel所注册的selector不是同一个
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}

这里主要就做了两件事情:

  1. 初始化 childChannel
  2. 将成功从 client 连接过来的 channel 注册到 selector 上。

这里一直说子channel,就是因为这里注册的是两个 EventLoopGroup,在 Server 端的处理上 netty 线程模型采用“服务端监听线程”和“IO线程”分离的方式。所以这里 channelRead 方法就是在 client 端请求连接到 server 端时,用于将当前连接的 IO 线程绑定到 childChannel 同时注册到 ChildGroup 中的 Selector 中。线程,模型可以参考下面的图:

好了,到这里 **Tag 1.2 initChannel ** 代码也分析完了,有些关于 pipeline 、handler、selector 的部分没有细说因为后面会单独说,在这里没有直接展开。

这里也画个图:到时候将这些图在整合到一起,现在是的分析过程就像是化整为零,最后在整合到一起化零为整。

这里除了 init(channel) 方法之外,还主要说了下 ServerBootstrapAcceptor 连接处理器。其实主要是 netty-server 的线程模型与代码的结合理解。

本文太长了,导致会超出大部分博客网站的字数限制,所以我分上下发了

我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞、收藏和评论,我们下期见!


文章持续更新,可以微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Laravel-API实践教程

发表于 2021-01-26

概述

Laravel 是一个非常流行的 PHP 框架,我们可以使用它快速构建一个 WEB 应用程序。而现在 WEB 应用中多会采用前后端分离技术,所以我们经常会遇到使用 Laravel 搭建 API 项目的需求。
Laravel 在提供 API 这方面,很多地方都只是提供了一个规范,并没有告诉我们如何去实现它。这样带来的好处是 Laravel 放开了限制,使大家可以按照自己的习惯去使用它。
但这样做也给刚接触 Laravel 不久的同学带来了一些困扰:到底怎样使用这个框架才更优雅些呢,有没有例子可以参考下呢。

本项目就是为了给大家提供一个参考而建立的,这是一个使用 Laravel 框架实现的 API 项目,项目中提供了一些常见功能的示例。
本项目从零开始,在重要修改部分末尾都会添加 [commit] 链接, 可自行查看变更记录。

需要注意的是:

  1. 这并不是一个 Laravel 的新手教程,文中很多地方需要你了解 Laravel 的基础知识。 Laravel官网文档
  2. 如果你已经有自己的实现方式,可以略过此教程,这个项目的实现方式可能也没比你的实现方式更优雅。
  3. Laravel 各个版本之前实现方式还是有一些区别的,要注意区分,但整体思路是一样的。

安装

这里使用 composer 安装 Laravel ,下面命令会安装最新版本的 Laravel 。此项目创建时的 Laravel 版本为 v8.21.0

1
lua复制代码composer create-project --prefer-dist laravel/laravel laravel-api-example

安装好之后需要自行配置项目使其可以对外访问,当在浏览器中输入项目地址进入到 Laravel 的欢迎页时,就可以继续向下阅读了。

路由

在欢迎页我们可以看到,Laravel 返回的信息是一个 web 页面,也就是 html 代码。这个默认的路由是在 routes/web.php 中定义的,我们需要把它给移除掉。[commit]

我们所有的路由都要定义到 routes/api.php 中,这个是专门用来定义 API 路由的文件,当然如果你的路由特别多你也可以在 routes 中定义其他路由文件,然后在 RouteServiceProvider 中按照同样的方式去加载它们。
在 RouteServiceProvider 中我们可以看到,我们在 routes/api.php 中定义的路由会默认加上 api 前缀,这对 WEB 和 API 混写在同一个项目中很有必要,但单独的 API 项目一般也会单独域名。如:

1
vbnet复制代码https://api.apihubs.cn/holiday/get

所以我们要移除这个 api 前缀或换成其他前缀如接口版本号 V1 [commit]

在 routes/api.php 中默认的路由是一个需要身份验证的 /user

我们使用浏览器或 postman 访问的时候,会得到一个错误页面,其中的主要信息为:Route [login] not defined.

我们使用 ajax 或者在 postman 的 header 中添加 X-Requested-With:XMLHttpRequest 头信息后又会得到一个 JSON 的错误信息:{“message”: “Unauthenticated.”}

这实际上都是未登录的原因,在未登录访问需要鉴权的接口时 Laravel 会抛出一个 AuthenticationException ,而在响应类 Response 中会根据请求的 header 头自动做出响应。
也就是如果是以页面形式调用的,就会跳转到登录页面,因为项目中还没定义登录页面的路由就出现我们上面看到的那个错误。如果是以接口形式访问的就会 401 状态码并返回 JSON 信息。

但我们这是一个 API 项目,提供出去的都是接口地址,在接口地址中一会返回 JSON 一会又返回一个页面这是不是显得很尴尬。

这里我们需要新增一个 Middleware 来解决这个问题。然后在 Kernel 中注册这个 Middleware 使其全局生效。 [commit]

1
go复制代码php artisan make:middleware JsonApplication

这样我们已经配置好一个 JSON 应用了,在 Laravel 抛出任何异常时,无论我们以什么方式访问都会始终得到 JSON 响应信息。

需要注意的是我们依然不能在路由的闭包或控制器中使用 return view($view) ,因为这会强制返回一个 html 页面响应。
我们应该在路由的闭包或控制器中始终 return 一个对象或数组,这两种格式会使 Laravel 自动为我们返回正确的 JSON 信息。

错误码

通过上面的配置,我们的应用可以始终返回 JSON 信息了。比如在出现异常的时候:

  • 鉴权失败会返回 http 状态码 401 的 {“message”: “Unauthenticated.”}
  • 请求的 method 不正确会返回 http 状态码 405 的 {“message”:”The POST method is not supported for this route. Supported methods: GET, HEAD.”,”exception”:”Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException”,…}
  • 请求的 URL 地址不存在会返回 404 的 {“message”:””,”exception”:”Symfony\Component\HttpKernel\Exception\NotFoundHttpException”,…}
  • …

但这样的返回信息也有问题:

  1. 返回信息的格式并不统一,JSON 中的 key 时有时无,接口调用方在很多时候找不到要以什么作为依据进行判断
  2. 许多异常信息为服务端敏感信息,会直接报漏给用户,存在安全隐患
  3. 异常信息都是通过 HTTP 状态码抛出的,会导致许多错误的 HTTP 状态码相同,比如 500

而通常的做法是需要根据不同的业务场景定义不同的错误代码和错误信息, 然后始终返会 http 状态码 200 的 {“code”:””,”msg”:””,”data”:””}

在这里我们需要引入一个第三方的库(这个库会在项目中许多地方使用,也是本教程的核心,当然这个库的功能是仿照 Java 枚举而来的) phpenum 。 [commit]

1
bash复制代码composer require phpenum/phpenum

这是一个枚举库,在这里用来定义和管理错误码和错误信息,错误码的位数应该是固定的,至少一个模块下的错误码位数是固定的,这里使用 5 位错误码,你可以根据实际使用场景来定义。
我们在 app 目录下新建一个 Enums 目录,然后添加 ErrorEnum 为不同的错误和异常定义不同的错误码。 [commit]

定义好错误码后,我们还需要借助 Laravel 的 渲染异常 来渲染自定义异常类 ApiException 。 [commit]

1
go复制代码php artisan make:exception ApiException

在上面这个 commit 中,我们对常见的异常都做了处理,使他们返回固定的错误码和错误信息,尤其对数据验证失败在 data 中返回了详细的错误信息,你也可以在 Handler 中添加一些其他需要处理的异常。

而未特殊定义状态码的异常会统一返回错误码 99999 的 未知错误,生产环境中是不应该出现这个错误码的,这个异常一般出现在调试阶段,我们需要解决掉它。

由于接口不再返回任何错误信息了,我们排查问题的方式也只能通过日志来排查 默认的日志在你本地的这个目录下 Laravel的日志也是非常强大,你可以随意更改存储的位置和介质,这里就不展开介绍了。

到这里我们就配置好统一错误码了,接下来无论在项目中出现什么错误,抛出什么异常,接口返回的信息始终保持为http状态码200的 {“code”:””,”msg”:””,”data”:””}

但这些状态码都是系统产生异常时返回的,我们要自己返回自定义状态码要怎么做呢? 非常简单,你只需要在任何你想返回自定义状态码的地方抛出自定义异常就可以了(但除了 controller 层,其他层可能会以非 web 的方式掉用,比如 console ,它不应该捕获到 ApiException ,所以尽量保证在 controller 抛出 ApiException)

1
2
css复制代码throw new ApiException(ErrorEnum::UNKNOWN_ERROR()); // {"code":99999,"msg":"服务器繁忙,请稍后再试","data":""}
throw new ApiException(ErrorEnum::UNKNOWN_ERROR(), 'This is an data'); // {"code":99999,"msg":"服务器繁忙,请稍后再试","data":"This is an data"}

那要是返回成功信息要怎么办呢,这个实现方式有很多,可以用官方文档示例中的 响应宏 , 也可以使用帮助类,还可以使用…
这里我们选 Laravel 的 Resource , 新建一个 Resource 类 JsonResponse, 在里面处理了常见的 Laravel 对象和添加分页处理。 [commit]

这样当我们想返回成功信息时只需要 return 这个实例就可以了。

1
php复制代码return new JsonResponse($mixed);

配置信息

Laravel 的配置信息都保存在 config 中,你也可以自定义自己的配置信息,获取时只需要使用 config(‘filename.array_key’) (支持多层级)就能轻松的获取到配置信息。
Laravel 的配置信息多是搭配了各个服务的门面模式来定义的,就比如 cache ,你只需要在 config 中修改 default driver ,就可以轻松的在 file 或 redis 或 database 等等等诸多存储介质之间切换,你还可以自定义存储介质。关于门面模式你可以自行查看 文档 和 源码 ,这里不展开介绍了。

这里还有个问题,就是比如数据库配置,一般我们都会区分开发、测试、生产环境,但是 config 中的配置只能在仓库中保存一份,这里我们就要用到另外一个特殊配置文件 .env

所有配置文件中以类似于 env(‘DB_HOST’, ‘127.0.0.1’) 这种方式定义的都会读取 .env 文件,第一个参数作为配置名称,如果未在 .env 文件中定义,则使用第二个参数默认值返回

一般我们会把所有区分环境的配置都定义在这个文件中

注意这个文件是不能提交到仓库的,所以你拉去代码后很有可能看不到这个文件,只需要将 .env.example copy 一份为 .env 即可 (首次安装 Laravel 会自动执行 copy) 然后配置好正确的数据库连接信息

数据库迁移(生产环境使用需谨慎)

文档

大多数框架都有数据库迁移功能,它对保持数据库结构的一致性起到非常大的作用,但这个功能如果没有合理使用则风险非常大,我之前就有同事使用了这个功能不小心把所有表都给重置了,还好数据是有备份的,及时进行了恢复。
Laravel 的数据迁移文件在 database/migrations 中,由于 Laravel 框架是国外开发者开发的,他们对用户的信息是以 email 为主,我们要在 user 表中增加手机号码字段。[commit]

添加完成后要在 model 添加该字段 [commit]

这里我们是直接对表迁移文件进行修改,是因为我们还没有进行数据库迁移,当你执行过数据库迁移后,Laravel 会在数据库中记录你已经迁移过的文件,这时如果再想修改应使用 更新表 的操作

接下来就可以执行数据库迁移操作了 (开发环境)

1
复制代码php artisan migrate

验证规则

Laravel 提供了非常多的 验证规则 ,这些验证规则可以满足大数据的验证场景,部分特殊验证需要我们自定义验证规则,比如手机号码
我们通过以下命令来创建一个 手机号码的验证规则 [commit]

1
go复制代码php artisan make:rule PhoneNumber

添加好规则后我们可以在 ServiceProvider 中为规则配置别名 [commit]

参数验证

验证规则一般可以直接写在 controller 中,也可以单独定义 Requests 进行管理,这里我们使用第二种方式统一在 Requests 中定义管理验证逻辑。

我们先来创建一个获取验证码的 Request [commit]

1
go复制代码php artisan make:request GetSmsCodeRequest

Request 中一般我们要在 messages 方法中重新定义错误信息,添加好 Request 后,我们就可以直接在 controller 中使用,结合之前我们的配置,当验证不通过时会返回以下信息

1
2
3
4
5
6
7
8
9
css复制代码{
"code": 10004,
"msg": "数据验证失败",
"data": {
"phone_number": [
"请输入您的手机号码"
]
}
}

获取验证码

到这里我们的基础配置就完成了,让我们来实际的添加一个接口吧,首先在 routes/api.php 中添加一条获取验证码的路由(限制访问频次 30 分钟 100 次) [commit]

1
css复制代码Route::middleware('throttle:100,30')->post('getSmsCode', [AuthController::class, 'getSmsCode']);

然后在添加 Controller Contract Service 以及在 ServiceProvider 中绑定 Contract 和 Service 的关系 [commit]

1
go复制代码php artisan make:controller AuthController

这种实现方式是参考了 Laravel 的 自动注入 和 Laravel 的 将接口绑定到实现 而实现的

这里为了方便测试,验证码直接在接口中返回,实际使用需要修改为短信发送

注册

按照上面的教程,我们先来添加一个注册的路由 [commit]

1
css复制代码Route::middleware('throttle:100,30')->post('register', [AuthController::class, 'register']);

然后添加一个验证码的 rule 并为其添加别名 [commit]

使用验证码规则创建注册的 request [commit]

最后在添加 Controller Contract Service 中的方法 [commit]

未完待续,优先在GITHUB更新

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

全网最牛X的MySQL两阶段提交串讲 一、吹个牛 二、事务及

发表于 2021-01-26

一、吹个牛

面试官的一句:“了解MySQL的两阶段提交吗?” 不知道问凉了多少人!

这篇文章白日梦就和大家分享什么是MySQL的两阶提交到底是怎么回事!不管你原来晓不晓得两阶段提交,相信我!这篇文章中你一定能get到新的知识!

在说两阶段提交之前,大家要了解undo-log、redo-log、binlog。

先了解它们,才能更好的理解什么是两阶段提交

二、事务及它的特性

在说两阶段提交事物之前,我们先来说说事务。

一般当我们的功能函数中有批量的增删改时,我们会添加一个事物包裹这一系列的操作,要么这一组操作全部执行成功,只要有一条SQL执行失败了我们就全部回滚。相信你一定听说过这个比较经典的转账的Case。有一定工作经验的同学都知道,这么做其实是保护我们的数据库中不出现脏数据。整体数据会变得可控。

对MySQL来说你可以通过下面的命令显示的开启、提交、回滚事务

1
2
3
4
5
6
7
8
9
10
11
ini复制代码Copy# 开启事务
begin;

# 或者下面这条命令
start transaction;

# 提交
commit;

# 回滚
rollback;

但是日常开发中大家普遍使用编程语言操作数据库。比如Java、Golang… 在使用这种具体编程语言持久层的框架时,它们一般都支持事务操作,比如:在Spring中你可以对一个方法添加注解@Transctional显示的开启事务。Golang的beego中也提供了让你可以显示的开启事务的函数。

有一点不太好的地方是:大家在享受这种编程框架带来的便利的同时,它也屏蔽了你对MySQL事务认知。让人们懒得去往细了看事务

你可以看看我下面这个很简单的Case。

我有一张数据表

1
2
3
4
5
sql复制代码CopyCREATE TABLE `test_backup` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后我往这个表中insert几条数据

1
2
3
sql复制代码Copymysql> insert into test_backup values(1,'tom');
mysql> insert into test_backup values(2,'jerry');
mysql> insert into test_backup values(1,'herry');

再去查看binlog。

全网最牛X的!!!MySQL两阶段提交串讲

你会不会诧异?我上面明明没有显示的添加begin、commit命令,但是MySQL实际执行我的SQL时,竟然为我添加上了!

原因很简单:跟大家分享一个参数如下:

全网最牛X的!!!MySQL两阶段提交串讲

一般大家的线上库都会将这个参数置为ON,你的SQL会自动的开启一个事物,并且MySQL会自动的帮你把它提交。

也就是说: 当这个参数为ON时,你使用的DAO持久层框架发送给数据库的SQL其实都会被放在一个事物中执行,然后这个事物被自动提交,而我们对这个过程是无感知的。 具体一点,比如你使用某框架的@Transctional注解,或者在golang中可以像下面的方式获得一个事物:

1
2
3
4
5
6
7
go复制代码Copydb := mysql.Client
ops := &sql.TxOptions{
Isolation: 0,
ReadOnly: false,
}
tx, err := db.BeginTx(ctx, ops)
// todo with tx

然后你所有的操作都放在这个事物中执行。

这时你使用的持久层框架肯定会向MySQL发送一条命令:begin;或者是start transcation;来保证你这一组SQL中执行一条SQL后,开启的事物不会被MySQL自动帮你提交了。

其实还是推荐将这个参数设置成ON的,当然你也可以像下面这样将它关闭

1
ini复制代码Copymysql> set autocommit = 0;

但是关闭它之后,MySQL不会帮你自动提交事物,全靠研发同学自己来维护就容易会出现长事物,在内存中产生一个极其长的undo log链条。坏处多多。

三、简单看下两阶段提交的流程

了解了什么是事物,再来看下什么是两阶段提交。其实所谓的两阶段就是把一个事物分成两个阶段来提交。就像下图这样。

全网最牛X的!!!MySQL两阶段提交串讲

上图为两阶段提交的时序图。

你可以粗略地观察一下上图,MySQL想要准备事务的时候会先写redolog、binlog分成两个阶段。

两阶段提交的第一阶段 (prepare阶段):写rodo-log 并将其标记为prepare状态。

紧接着写binlog

两阶段提交的第二阶段(commit阶段):写bin-log 并将其标记为commit状态。

不了解这些日志是什么有啥用也没关系

四、两阶段写日志用意?

你有没有想过这样一件事,binlog默认都是不开启的状态!

也就是说,如果你根本不需要binlog带给你的特性(比如数据备份恢复、搭建MySQL主从集群),那你根本就用不着让MySQL写binlog,也用不着什么两阶段提交。

只用一个redolog就够了。无论你的数据库如何crash,redolog中记录的内容总能让你MySQL内存中的数据恢复成crash之前的状态。

所以说,两阶段提交的主要用意是:为了保证redolog和binlog数据的安全一致性。只有在这两个日志文件逻辑上高度一致了。你才能放心地使用redolog帮你将数据库中的状态恢复成crash之前的状态,使用binlog实现数据备份、恢复、以及主从复制。而两阶段提交的机制可以保证这两个日志文件的逻辑是高度一致的。没有错误、没有冲突。

当然,两阶段提交能做到足够的安全还需要你合理地设置redolog和binlog的fsync的时机

五、加餐:sync_binlog = 1 问题

如果你看懂了我下面说的这些话,能帮你更好的理解两阶段提交哦!纯干货!

在前面的分享binlog的文章中有跟大家提到过一个参数sync_binlog=1。这个参数控制binlog的落盘时机,并且你们公司线上数据库的该参数一定被设置成了1。

Notice!!! 这个参数为1时,表示当事物提交时会将binlog落盘。

现在你用15s中的时间,思考一下,蓝色句子中说的事物提交时会将binlog落盘,这个提交时,是下图中的step1时刻呢?还是step2时刻呢?

全网最牛X的!!!MySQL两阶段提交串讲

答案是:step1时刻!

知道这个知识点很重要,下面我来描述这样一个场景。

假如要执行一条update语句,那你肯定知道,先写redolog(便于后续对update事务的回滚)。然后你的update逻辑将Buffer Pool中的缓存页修改成了脏页。

当你准备提交事物时(也就是step1阶段),会写redolog,并将其标记为prepare阶段。然后再写binlog,并将binlog落盘。

然后发生了意外,MySQL宕机了。

那我问你,当你重启MySQL后,update对BufferPool中做出的修改是会被回滚还是会被提交呢?

答案是:会根据redolog将修改后的recovery出来,然后提交。

那为什么会这样做呢?

其实总的来说,不论mysql什么时刻crash,最终是commit还是rollback完全取决于MySQL能不能判断出binlog和redolog在逻辑上是否达成了一致。只要逻辑上达成了一致就可以commit,否则只能rollback。

比如还是上面描述的场景,binlog已经写了,但是MySQL最终选择了回滚。那代表你的binlog比BufferPool(或者Disk)中的真实数据多出一条更新,日后你用这份binlog做数据恢复,是不是结果一定是错误的?

六、如何判断binlog和redolog是否达成了一致

这个知识点可是纯干货!

当MySQL写完redolog并将它标记为prepare状态时,并且会在redolog中记录一个XID,它全局唯一的标识着这个事务。而当你设置sync\_binlog=1时,做完了上面第一阶段写redolog后,mysql就会对应binlog并且会直接将其刷新到磁盘中。

下图就是磁盘上的row格式的binlog记录。binlog结束的位置上也有一个XID。

只要这个XID和redolog中记录的XID是一致的,MySQL就会认为binlog和redolog逻辑上一致。就上面的场景来说就会commit,而如果仅仅是rodolog中记录了XID,binlog中没有,MySQL就会RollBack

全网最牛X的!!!MySQL两阶段提交串讲

七、两阶段提交设计的初衷 - 分布式事务

其实两阶段提交更多的被使用在分布式事务的场景。

我用大白话描述一个这样的场景,大家自行脑补一下:

MySQL单机本来是支持事务的,但是这里所谓的分布式事务实际上指的是跨数据库、跨集群的事务。比如说你公司的业务太火爆了,每天都产生大量的数据,这些数据不仅单表存不下,甚至单库都存不下了(已经达到了服务器硬件存储的瓶颈)

那你怎么办?是不是只能将单库拆分成多库?

那你拆分成多库就会面临这样一个新的问题。假设Tom给Jerry转账,但是由于你拆分了数据库,原本在同库同表上的Tom和Jerry的信息,被你拆分进A库a表和B库b表。那你再发起转账逻辑时,万一失败了。如何回滚保证数据的安全?这就是分布式事务的要解决的问题。

通常各大公司都有自己的支持分布式事务中间件,中间件的作用本质上就是处理好各个数据库节点之间两阶段提交的问题。

简单来说:就是中间件要协调各个数据节点。

第一阶段:中间件告诉各数据库节点,让它们开启XA事务,然后判断所有数据库节点是否已经处于prepare状态

第二阶段:中间件判断事务提交还是回滚的阶段。如果所有节点都prepare那就统一提交。但凡出现一个失败的节点,统一回滚。

这里只是稍微提及一下:两阶段提交和分布式事务的渊源。

八、再看MySQL两阶段写日志

那我们再将思路拉回到MySQL两阶段写日志的话题。

其实说到这里,你大概也能直接想到,其实上一篇文章中的两阶段提交,表面上其实就是两阶段写入日志。

通过我前面的描述,你也一定知道了两份日志文件逻辑对齐的标记是有一份相同的XID。

就是这种两阶段的机制保证了两个日志(在分布式事务中就是多个数据节点)在逻辑上能达到一致的效果。

九、留一个彩蛋

如果你仔细想一下,上面第三部分在分享 sync_binlog=1 加餐时,我所描述的示例场景其实是适用于单机MySQL的简单场景。

其实这个场景还能再复杂一些!

串联MySQL集群、将同步、半同步、异步的主从复制关系以及这里的两阶段提交、日志的落盘时机、幽灵事务!结合成一个场景效果会更好。

看完三件事❤️

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

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. .关注后回复【666】扫码即可获取学习资料包

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

全网最详细的负载均衡原理图解 负载均衡由来 负载均衡类型 负

发表于 2021-01-26

负载均衡由来

在业务初期,我们一般会先使用单台服务器对外提供服务。随着业务流量越来越大,单台服务器无论如何优化,无论采用多好的硬件,总会有性能天花板,当单服务器的性能无法满足业务需求时,就需要把多台服务器组成集群系统提高整体的处理性能。

基于上述需求,我们要使用统一的流量入口来对外提供服务,本质上就是需要一个流量调度器,通过均衡的算法,将用户大量的请求流量均衡地分发到集群中不同的服务器上。这其实就是我们今天要说的负载均衡。

使用负载均衡可以给我们带来的几个好处:

  • 提高了系统的整体性能;
  • 提高了系统的扩展性;
  • 提高了系统的可用性;

负载均衡类型

广义上的负载均衡器大概可以分为 3 类,包括:DNS 方式实现负载均衡、硬件负载均衡、软件负载均衡。

(一)DNS 实现负载均衡

DNS 实现负载均衡是最基础简单的方式。一个域名通过 DNS 解析到多个 IP,每个 IP 对应不同的服务器实例,这样就完成了流量的调度,虽然没有使用常规的负载均衡器,但实现了简单的负载均衡功能。

全网最详细的负载均衡原理图解

通过 DNS 实现负载均衡的方式,最大的优点就是实现简单,成本低,无需自己开发或维护负载均衡设备,不过存在一些缺点:

  • 服务器故障切换延迟大,服务器升级不方便

。我们知道 DNS 与用户之间是层层的缓存,即便是在故障发生时及时通过 DNS 修改或摘除故障服务器,但中间经过运营商的 DNS 缓存,且缓存很有可能不遵循 TTL 规则,导致 DNS 生效时间变得非常缓慢,有时候一天后还会有些许的请求流量。

  • 流量调度不均衡,粒度太粗

。DNS 调度的均衡性,受地区运营商 LocalDNS 返回 IP 列表的策略有关系,有的运营商并不会轮询返回多个不同的 IP 地址。另外,某个运营商 LocalDNS 背后服务了多少用户,这也会构成流量调度不均的重要因素。

  • 流量分配策略太简单,支持的算法太少

。DNS 一般只支持 rr 的轮询方式,流量分配策略比较简单,不支持权重、Hash 等调度算法。

  • DNS 支持的 IP 列表有限制

。我们知道 DNS 使用 UDP 报文进行信息传递,每个 UDP 报文大小受链路的 MTU 限制,所以报文中存储的 IP 地址数量也是非常有限的,阿里 DNS 系统针对同一个域名支持配置 10 个不同的 IP 地址。

实际上生产环境中很少使用这种方式来实现负载均衡,毕竟缺点很明显。文中之所以描述 DNS 负载均衡方式,是为了能够更清楚地解释负载均衡的概念。

像 BAT 体量的公司一般会利用 DNS 来实现地理级别的全局负载均衡,实现就近访问,提高访问速度,这种方式一般是入口流量的基础负载均衡,下层会有更专业的负载均衡设备实现的负载架构。

(二)硬件负载均衡

硬件负载均衡是通过专门的硬件设备来实现负载均衡功能,是专用的负载均衡设备。目前业界典型的硬件负载均衡设备有两款:F5 和 A10。

这类设备性能强劲、功能强大,但价格非常昂贵,一般只有土豪公司才会使用此类设备,中小公司一般负担不起,业务量没那么大,用这些设备也是挺浪费的。

硬件负载均衡的优点:

  • 功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法。
  • 性能强大:性能远超常见的软件负载均衡器。
  • 稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。
  • 安全防护:还具备防火墙、防 DDoS 攻击等安全功能,以及支持 SNAT 功能。

硬件负载均衡的缺点也很明显:

  • 价格贵;
  • 扩展性差,无法进行扩展和定制;
  • 调试和维护比较麻烦,需要专业人员;

(三)软件负载均衡

软件负载均衡,可以在普通的服务器上运行负载均衡软件,实现负载均衡功能。目前常见的有 Nginx、HAproxy、LVS。其中的区别:

  • Nginx:七层负载均衡,支持 HTTP、E-mail 协议,同时也支持 4 层负载均衡;
  • HAproxy:支持七层规则的,性能也很不错。OpenStack 默认使用的负载均衡软件就是 HAproxy;
  • LVS:运行在内核态,性能是软件负载均衡中最高的,严格来说工作在三层,所以更通用一些,适用各种应用服务。

软件负载均衡的优点:

  • 易操作:无论是部署还是维护都相对比较简单;
  • 便宜:只需要服务器的成本,软件是免费的;
  • 灵活:4 层和 7 层负载均衡可以根据业务特点进行选择,方便进行扩展和定制功能。

负载均衡LVS

软件负载均衡主要包括:Nginx、HAproxy 和 LVS,三款软件都比较常用。四层负载均衡基本上都会使用 LVS,据了解 BAT 等大厂都是 LVS 重度使用者,就是因为 LVS 非常出色的性能,能为公司节省巨大的成本。

LVS,全称 Linux Virtual Server 是由国人章文嵩博士发起的一个开源的项目,在社区具有很大的热度,是一个基于四层、具有强大性能的反向代理服务器。

它现在是标准内核的一部分,它具备可靠性、高性能、可扩展性和可操作性的特点,从而以低廉的成本实现最优的性能。

Netfilter基础原理

LVS 是基于 Linux 内核中 netfilter 框架实现的负载均衡功能,所以要学习 LVS 之前必须要先简单了解 netfilter 基本工作原理。netfilter 其实很复杂,平时我们说的 Linux 防火墙就是 netfilter,不过我们平时操作的都是 iptables,iptables 只是用户空间编写和传递规则的工具而已,真正工作的是 netfilter。通过下图可以简单了解下 netfilter 的工作机制:

全网最详细的负载均衡原理图解

netfilter 是内核态的 Linux 防火墙机制,作为一个通用、抽象的框架,提供了一整套的 hook 函数管理机制,提供诸如数据包过滤、网络地址转换、基于协议类型的连接跟踪的功能。

通俗点讲,就是 netfilter 提供一种机制,可以在数据包流经过程中,根据规则设置若干个关卡(hook 函数)来执行相关的操作。netfilter 总共设置了 5 个点,包括:PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING

  • PREROUTING :刚刚进入网络层,还未进行路由查找的包,通过此处
  • INPUT :通过路由查找,确定发往本机的包,通过此处
  • FORWARD :经路由查找后,要转发的包,在POST_ROUTING之前
  • OUTPUT :从本机进程刚发出的包,通过此处
  • POSTROUTING :进入网络层已经经过路由查找,确定转发,将要离开本设备的包,通过此处

当一个数据包进入网卡,经过链路层之后进入网络层就会到达 PREROUTING,接着根据目标 IP 地址进行路由查找,如果目标 IP 是本机,数据包继续传递到 INPUT 上,经过协议栈后根据端口将数据送到相应的应用程序。

应用程序处理请求后将响应数据包发送到 OUTPUT 上,最终通过 POSTROUTING 后发送出网卡。

如果目标 IP 不是本机,而且服务器开启了 forward 参数,就会将数据包递送给 FORWARD 上,最后通过 POSTROUTING 后发送出网卡。

LVS基础原理

LVS 是基于 netfilter 框架,主要工作于 INPUT 链上,在 INPUT 上注册 ip_vs_inHOOK 函数,进行 IPVS 主流程,大概原理如图所示:

全网最详细的负载均衡原理图解

  • 当用户访问 www.sina.com.cn 时,用户数据通过层层网络,最后通过交换机进入 LVS 服务器网卡,并进入内核网络层。
  • 进入 PREROUTING 后经过路由查找,确定访问的目的 VIP 是本机 IP 地址,所以数据包进入到 INPUT 链上
  • LVS 是工作在 INPUT 链上,会根据访问的 IP:Port 判断请求是否是 LVS 服务,如果是则进行 LVS 主流程,强行修改数据包的相关数据,并将数据包发往 POSTROUTING 链上。
  • POSTROUTING 上收到数据包后,根据目标 IP 地址(后端真实服务器),通过路由选路,将数据包最终发往后端的服务器上。

开源 LVS 版本有 3 种工作模式,每种模式工作原理都不同,每种模式都有自己的优缺点和不同的应用场景,包括以下三种模式:

  • DR 模式
  • NAT 模式
  • Tunnel 模式

这里必须要提另外一种模式是 FullNAT,这个模式在开源版本中是模式没有的。这个模式最早起源于百度,后来又在阿里发扬光大,由阿里团队开源,代码地址如下:

  • github.com/alibaba/lvs

LVS 官网也有相关下载地址,不过并没有合进到内核主线版本。

后面会有专门章节详细介绍 FullNAT 模式。下边分别就 DR、NAT、Tunnel 模式分别详细介绍原理。

DR 模式实现原理

LVS 基本原理图中描述的比较简单,表述的是比较通用流程。下边会针对 DR 模式的具体实现原理,详细的阐述 DR 模式是如何工作的。

全网最详细的负载均衡原理图解

其实 DR 是最常用的工作模式,因为它的强大的性能。下边试图以某个请求和响应数据流的过程来描述 DR 模式的工作原理

(一)实现原理过程

① 当客户端请求 www.sina.com.cn 主页,请求数据包穿过网络到达 Sina 的 LVS 服务器网卡:源 IP 是客户端 IP 地址 CIP,目的 IP 是新浪对外的服务器 IP 地址,也就是 VIP;此时源 MAC 地址是 CMAC,其实是 LVS 连接的路由器的 MAC 地址(为了容易理解记为 CMAC),目标 MAC 地址是 VIP 对应的 MAC,记为 VMAC。

② 数据包经过链路层到达 PREROUTING 位置(刚进入网络层),查找路由发现目的 IP 是 LVS 的 VIP,就会递送到 INPUT 链上,此时数据包 MAC、IP、Port 都没有修改。

③ 数据包到达 INPUT 链,INPUT 是 LVS 主要工作的位置。此时 LVS 会根据目的 IP 和 Port 来确认是否是 LVS 定义的服务,如果是定义过的 VIP 服务,就会根据配置信息,从真实服务器列表 中选择一个作为 RS1,然后以 RS1 作为目标查找 Out 方向的路由,确定一下跳信息以及数据包要通过哪个网卡发出。最后将数据包投递到 OUTPUT 链上。

④ 数据包通过 POSTROUTING 链后,从网络层转到链路层,将目的 MAC 地址修改为 RealServer 服务器 MAC 地址,记为 RMAC;而源 MAC 地址修改为 LVS 与 RS 同网段的 selfIP 对应的 MAC 地址,记为 DMAC。此时,数据包通过交换机转发给了 RealServer 服务器(注:

为了简单图中没有画交换机

)。

⑤ 请求数据包到达后端真实服务器后,链路层检查目的 MAC 是自己网卡地址。到了网络层,查找路由,目的 IP 是 VIP(lo 上配置了 VIP),判定是本地主机的数据包,经过协议栈拷贝至应用程序(比如 nginx 服务器),nginx 响应请求后,产生响应数据包。

然后以 CIP 查找出方向的路由,确定下一跳信息和发送网卡设备信息。此时数据包源、目的 IP 分别是 VIP、CIP,而源 MAC 地址是 RS1 的 RMAC,目的 MAC 是下一跳(路由器)的 MAC 地址,记为 CMAC(为了容易理解,记为 CMAC)。然后数据包通过 RS 相连的路由器转发给真正客户端,完成了请求响应的全过程。

从整个过程可以看出,DR 模式 LVS 逻辑比较简单,数据包通过直接路由方式转发给后端服务器,而且响应数据包是由 RS 服务器直接发送给客户端,不经过 LVS。

我们知道通常请求数据包会比较小,响应报文较大,经过 LVS 的数据包基本上都是小包,所以这也是 LVS 的 DR 模式性能强大的主要原因。

(二)优缺点和使用场景

  • DR 模式的优点

响应数据不经过 lvs,性能高

对数据包修改小,信息保存完整(携带客户端源 IP)

  • DR 模式的缺点

lvs 与 rs 必须在同一个物理网络(不支持跨机房)

服务器上必须配置 lo 和其它内核参数

不支持端口映射

  • DR 模式的使用场景

如果对性能要求非常高,可以首选 DR 模式,而且可以透传客户端源 IP 地址。

NAT 模式实现原理

lvs 的第 2 种工作模式是 NAT 模式,下图详细介绍了数据包从客户端进入 lvs 后转发到 rs,后经 rs 再次将响应数据转发给 lvs,由 lvs 将数据包回复给客户端的整个过程。

全网最详细的负载均衡原理图解

(一)实现原理与过程

①用户请求数据包经过层层网络,到达 lvs 网卡,此时数据包源 IP 是 CIP,目的 IP 是 VIP。

② 经过网卡进入网络层 prerouting 位置,根据目的 IP 查找路由,确认是本机 IP,将数据包转发到 INPUT 上,此时源、目的 IP 都未发生变化。

③到达 lvs 后,通过目的 IP 和目的 port 查找是否为 IPVS 服务。若是 IPVS 服务,则会选择一个 RS 作为后端服务器,将数据包目的 IP 修改为 RIP,并以 RIP 为目的 IP 查找路由信息,确定下一跳和出口信息,将数据包转发至 output 上。

④修改后的数据包经过 postrouting 和链路层处理后,到达 RS 服务器,此时的数据包源 IP 是 CIP,目的 IP 是 RIP。

⑤到达 RS 服务器的数据包经过链路层和网络层检查后,被送往用户空间 nginx 程序。nginx 程序处理完毕,发送响应数据包,由于 RS 上默认网关配置为 lvs 设备 IP,所以 nginx 服务器会将数据包转发至下一跳,也就是 lvs 服务器。此时数据包源 IP 是 RIP,目的 IP 是 CIP。

⑥lvs 服务器收到 RS 响应数据包后,根据路由查找,发现目的 IP 不是本机 IP,且 lvs 服务器开启了转发模式,所以将数据包转发给 forward 链,此时数据包未作修改。

⑦lvs 收到响应数据包后,根据目的 IP 和目的 port 查找服务和连接表,将源 IP 改为 VIP,通过路由查找,确定下一跳和出口信息,将数据包发送至网关,经过复杂的网络到达用户客户端,最终完成了一次请求和响应的交互。

NAT 模式双向流量都经过 LVS,因此 NAT 模式性能会存在一定的瓶颈。不过与其它模式区别的是,NAT 支持端口映射,且支持 windows 操作系统。

(二)优点、缺点与使用场景

  • NAT 模式优点

能够支持 windows 操作系统

支持端口映射。如果 rs 端口与 vport 不一致,lvs 除了修改目的 IP,也会修改 dport 以支持端口映射。

  • NAT 模式缺点

后端 RS 需要配置网关

双向流量对 lvs 负载压力比较大

  • NAT 模式的使用场景

如果你是 windows 系统,使用 lvs 的话,则必须选择 NAT 模式了。

Tunnel 模式实现原理

Tunnel 模式在国内使用的比较少,不过据说腾讯使用了大量的 Tunnel 模式。它也是一种单臂的模式,只有请求数据会经过 lvs,响应数据直接从后端服务器发送给客户端,性能也很强大,同时支持跨机房。下边继续看图分析原理。

全网最详细的负载均衡原理图解

(一)实现原理与过程

①用户请求数据包经过多层网络,到达 lvs 网卡,此时数据包源 IP 是 cip,目的 ip 是 vip。

②经过网卡进入网络层 prerouting 位置,根据目的 ip 查找路由,确认是本机 ip,将数据包转发到 input 链上,到达 lvs,此时源、目的 ip 都未发生变化。

③到达 lvs 后,通过目的 ip 和目的 port 查找是否为 IPVS 服务。若是 IPVS 服务,则会选择一个 rs 作为后端服务器,以 rip 为目的 ip 查找路由信息,确定下一跳、dev 等信息,然后 IP 头部前边额外增加了一个 IP 头(以 dip 为源,rip 为目的 ip),将数据包转发至 output 上。

④数据包根据路由信息经最终经过 lvs 网卡,发送至路由器网关,通过网络到达后端服务器。

⑤后端服务器收到数据包后,ipip 模块将 Tunnel 头部卸载,正常看到的源 ip 是 cip,目的 ip 是 vip,由于在 tunl0 上配置 vip,路由查找后判定为本机 ip,送往应用程序。应用程序 nginx 正常响应数据后以 vip 为源 ip,cip 为目的 ip 数据包发送出网卡,最终到达客户端。

Tunnel 模式具备 DR 模式的高性能,又支持跨机房访问,听起来比较完美。不过国内运营商有一定特色性,比如 RS 的响应数据包的源 IP 为 VIP,VIP 与后端服务器有可能存在跨运营商的情况,很有可能被运营商的策略封掉,Tunnel 在生产环境确实没有使用过,在国内推行 Tunnel 可能会有一定的难度吧。

(二)优点、缺点与使用场景

  • Tunnel 模式的优点

单臂模式,对 lvs 负载压力小

对数据包修改较小,信息保存完整

可跨机房(不过在国内实现有难度)

  • Tunnel 模式的缺点

需要在后端服务器安装配置 ipip 模块

需要在后端服务器 tunl0 配置 vip

隧道头部的加入可能导致分片,影响服务器性能

隧道头部 IP 地址固定,后端服务器网卡 hash 可能不均

不支持端口映射

  • Tunnel 模式的使用场景

理论上,如果对转发性能要求较高,且有跨机房需求,Tunnel 可能是较好的选择。

到此为止,已经将 LVS 原理讲清楚了,内容比较多,建议多看两遍,由于文章篇幅太长,实践操作的内容就放到下篇文章再来讲好了。

看完三件事❤️

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

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. .关注后回复【666】扫码即可获取学习资料包

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在微服务项目中,Spring Security 比 Shir

发表于 2021-01-26

虽然目前 Spring Security 一片火热,但是 Shiro 的市场依然存在,今天我就来稍微的说一说这两个框架的,方便大家在实际项目中选择适合自己的安全管理框架。

首先我要声明一点,框架无所谓好坏,关键是适合当前项目场景,作为一个年轻的程序员更不应该厚此薄彼,或者拒绝学习某一个框架。

小孩子才做选择题,成年人两个都要学!

所以接下来主要结合我自己的经验来说一说这两个框架的优缺点,没有提到的地方也欢迎大家留言补充。

  1. Spring Security

1.1 因为 SpringBoot 而火

Spring Security 并非一个新生的事物,它最早不叫 Spring Security ,叫 Acegi Security,叫 Acegi Security 并不是说它和 Spring 就没有关系了,它依然是为 Spring 框架提供安全支持的。事实上,Java 领域的框架,很少有框架能够脱离 Spring 框架独立存在。

当 Spring Security 还叫 Acegi Security 的时候,虽然功能也还可以,但是实际上这个东西并没有广泛流行开来。最重要的原因就是它的配置太过于繁琐,当时网上流传一句话:“每当有人要使用 Acegi Security,就会有一个精灵死去。” 足见 Acegi Security 的配置是多么可怕。直到今天,当人们谈起 Spring Security 的时候,依然在吐槽它的配置繁琐。

后来 Acegi Security 投入 Spring 的怀抱,改名叫 Spring Security,事情才慢慢开始发生变化。新的开发团队一直在尽力简化 Spring Security 的配置,Spring Security 的配置相比 Acegi Security 确实简化了很多。但是在最初的几年里,Spring Security 依然无法得到广泛的使用。

直到有一天 Spring Boot 像谜一般出现在江湖边缘,彻底颠覆了 JavaEE 的世界。一人得道鸡犬升天,自从 Spring Boot 火了之后,Spring 家族的产品都被带了一把,Spring Security 就是受益者之一,从此飞上枝头变凤凰。

Spring Boot/Spring Cloud 现在作为 Java 开发领域最最主流的技术栈,这一点大家应该都没有什么异议,而在 Spring Boot/Spring Cloud 中做安全管理,Spring Security 无疑是最方便的。

你想保护 Spring Boot 中的接口,添加一个 Spring Security 的依赖即可,事情就搞定了,所有接口就保护起来了,甚至不需要一行配置。

有小伙伴可能觉得这个太笼统了,我再举一个实际点的例子。

在微服务架构的项目中,我们可能使用 Eureka 做服务注册中心,默认情况下,Eureka 没有做安全管理,如果你想给 Eureka 添加安全管理,只需要添加 Spring Security 依赖,然后在application.properties 中配置一下用户名密码即可,Eureka 就自动被保护起来了,别人无法轻易访问;然后各个微服务在注册的时候,只需要把注册地址改为 http://username:password@localhost:8080/eureka 即可。类似的例子还有 Spring Cloud Config 中的安全管理。

在微服务这种场景下,如果你想用 Shiro 代替 Spring Security,那 Shiro 代码量绝对非常可观,Spring Security 则可以非常容易的集成到现在流行的 Spring Boot/Spring Cloud 技术栈中,可以和 Spring Boot、Spring Cloud、Spring Social、WebSocket 等非常方便的整合。

所以我说,因为 Spring Boot/Spring Cloud 火爆,让 Spring Security 跟着沾了一把光。

1.2 配置臃肿吗?

「有的人觉得 Spring Security 配置臃肿。」

如果是 SSM + Spring Security 的话,我觉得这话有一定道理。

但是如果是 Spring Boot 项目的话,其实并不见得臃肿。Spring Boot 中,通过自动化配置 starter 已经极大的简化了 Spring Security 的配置,我们只需要做少量的定制的就可以实现认证和授权了。

「有人觉得 Spring Security 中概念复杂。」这个是这样的,没错。

Spring Security 由于功能比较多,支持 OAuth2 等原因,就显得比较重量级,不像 Shiro 那样轻便。

但是如果换一个角度,你可能会有不一样的感受。

在 Spring Security 中你会学习到许多安全管理相关的概念,以及常见的安全攻击。这些安全攻击,如果你不是 web 安全方面的专家,很多可能存在的 web 攻击和漏洞你可能很难想到,而 Spring Security 则把这些安全问题都给我们罗列出来并且给出了相应的解决方案。

所以我说,我们学习 Spring Security 的过程,也是在学习 web 安全,各种各样的安全攻击、各种各样的登录方式、各种各样你能想到或者想不到的安全问题,Spring Security 都给我们罗列出来了,并且给出了解决方案,从这个角度来看,你会发现 Spring Security 好像也不是那么让人讨厌。

1.3 结合微服务的优势

除了前面和大家介绍的 Spring Security 优势,在微服务中,Spring 官方推出了 Spring Cloud Security 和 Spring Cloud OAuth2,结合微服务这种分布式特性,可以让我们更加方便的在微服务中使用 Spring Security 和 OAuth2,松哥前面的 OAuth2 系列实际上都是基于 Spring Cloud Security 来做的。

可以看到,Spring 官方一直在积极进取,让 Spring Security 能够更好的集成进微服务中。

  1. Shiro

接下来我们再说说 Apache Shiro。

Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro 框架具有直观、易用等特性,同时也能提供健壮的安全性,虽然它的功能不如 Spring Security 那么强大,但是在常规的企业级应用中,其实也够用了。

2.1 由来

Shiro 的前身是 JSecurity,2004 年,Les Hazlewood 和 Jeremy Haile 创办了 Jsecurity。当时他们找不到适用于应用程序级别的合适 Java 安全框架,同时又对 JAAS 非常失望,于是就搞了这个东西。

2004 年到 2008 年期间,JSecurity 托管在 SourceForge 上,贡献者包括 Peter Ledbrook、Alan Ditzel 和 Tim Veil。

2008 年,JSecurity 项目贡献给了 Apache 软件基金会(ASF),并被接纳成为 Apache Incubator 项目,由导师管理,目标是成为一个顶级 Apache 项目。期间,Jsecurity 曾短暂更名为 Ki,随后因商标问题被社区更名为“Shiro”。随后项目持续在 Apache Incubator 中孵化,并增加了贡献者 Kalle Korhonen。

2010 年 7 月,Shiro 社区发布了 1.0 版,随后社区创建了其项目管理委员会,并选举 Les Hazlewood 为主席。2010 年 9 月 22 日,Shrio 成为 Apache 软件基金会的顶级项目(TLP)。

2.2 有哪些功能

Apache Shiro 是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,授权,企业会话管理和加密。Apache Shiro 的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架应该尽可能掩盖复杂的地方,露出一个干净而直观的 API,来简化开发人员在应用程序安全上所花费的时间。

以下是你可以用 Apache Shiro 所做的事情:

  1. 验证用户来核实他们的身份
  2. 对用户执行访问控制,如:判断用户是否被分配了一个确定的安全角色;判断用户是否被允许做某事
  3. 在任何环境下使用Session API,即使没有Web容器
  4. 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应
  5. 聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”
  6. 单点登录(SSO)功能
  7. 为没有关联到登录的用户启用”Remember Me”服务
  8. …

Apache Shiro 是一个拥有许多功能的综合性的程序安全框架。下面的图表展示了 Shiro 的重点:

在微服务项目中,Spring Security 比 Shiro 强在哪?

Shiro 中有四大基石——身份验证,授权,会话管理和加密。

  1. Authentication:有时也简称为“登录”,这是一个证明用户是谁的行为。
  2. Authorization:访问控制的过程,也就是决定“谁”去访问“什么”。
  3. Session Management:管理用户特定的会话,即使在非 Web 或 EJB 应用程序。
  4. Cryptography:通过使用加密算法保持数据安全同时易于使用。

除此之外,Shiro 也提供了额外的功能来解决在不同环境下所面临的安全问题,尤其是以下这些:

  1. Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
  2. Caching:缓存是 Apache Shiro 中的第一层公民,来确保安全操作快速而又高效。
  3. Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
  4. Testing:测试支持的存在来帮助你编写单元测试和集成测试。
  5. “Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
  6. “Remember Me”:在会话中记住用户的身份,这样用户只需要在强制登录时候登录。

2.3 学习资料

Shiro 的学习资料并不多,没看到有相关的书籍。张开涛的《跟我学Shiro》是一个非常不错的资料,小伙伴可以搜索了解下。也可以在公众号后台回复 2TB,有相关的视频教程。

2.4 优势和劣势

就目前而言,Shiro 最大的问题在于和 Spring 家族的产品进行整合的时候非常不便,在 Spring Boot 推出的很长一段时间里,Shiro 都没有提供相应的 starter,后来虽然有一个 shiro-spring-boot-web-starter 出来,但是其实配置并没有简化多少。所以在 Spring Boot/Spring Cloud 技术栈的微服务项目中,Shiro 几乎不存在优势。

但是如果你是传统的 SSM 项目,不是微服务项目,那么无疑使用 Shiro 是最方便省事的,因为它足够简单,足够轻量级。

  1. 如何取舍

在公司里做开发,这两个要如何取舍,还是要考虑蛮多东西的。

首先,如果是基于 Spring Boot/Spring Cloud 的微服务项目,Spring Security 无疑是最方便的。

如果是就是普通的 SSM 项目,那么 Shiro 基本上也够用。

另外,选择技术栈的时候,我们可能也要考虑团队内工程师的技术栈,如果工程师更擅长 Shiro,那么无疑 Shiro 是合适的,毕竟让工程师去学习一门新的技术,一来可能影响项目进度,而来也可能给项目埋下许多未知的雷。

对于我们个人来说,小孩子才做选择题,成年人两个都要学。

看完三件事❤️

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

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. .关注后回复【666】扫码即可获取学习资料包

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springBoot聚合websocket如何实现单机10万

发表于 2021-01-25

1、springboot项目聚合websockert代码。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
java复制代码/**
* 开启WebSocket支持
*
*/
@Configuration
public class WebSocketConfig {
/**
* 扫描并注册带有@ServerEndpoint注解的所有服务端
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}



package com.zntech.cpms.script.provider.config;

import com.zntech.cpms.script.provider.constant.ConstantUtils;
import com.zntech.cpms.script.provider.enums.MessageTypeEnum;
import com.zntech.cpms.script.provider.enums.RunStatusEnum;
import com.zntech.cpms.script.provider.pojo.ClientInfo;
import com.zntech.cpms.script.provider.pojo.MessageRequest;
import com.zntech.cpms.script.provider.pojo.ProjectTask;
import com.zntech.cpms.script.provider.pojo.TaskFailInfo;
import com.zntech.cpms.script.provider.service.ClientInfoService;
import com.zntech.cpms.script.provider.service.ProjectTaskService;
import com.zntech.cpms.script.provider.service.TaskFailInfoService;
import com.zntech.cpms.script.provider.service.impl.MessageHandlerServiceImpl;
import com.zntech.cpms.script.provider.util.CollectionsUtil;
import com.zntech.cpms.script.provider.util.JacksonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;


import javax.validation.constraints.Max;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
* websocket服务端,多例的,一次websocket连接对应一个实例
* @ServerEndpoint 注解的值为URI,映射客户端输入的URL来连接到WebSocket服务器端
*/
@Component
@ServerEndpoint("/{name}")
@Slf4j
public class WebSocketServe {
/** 用来记录当前在线连接数。设计成线程安全的。*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/** 用于保存uri对应的连接服务,{uri:WebSocketServer},设计成线程安全的 */
private static ConcurrentHashMap<String, WebSocketServe> webSocketServerMAP = new ConcurrentHashMap<>();
private Session session;// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private String name; //客户端消息发送者
private String uri; //连接的uri

/**
* 连接建立成功时触发,绑定参数
* @param session
* 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
* @param name
* @param toName
* @throws IOException
*/
@OnOpen
public void onOpen(Session session, @PathParam("name") String name, @PathParam("toName") String toName) throws IOException {
this.session = session;
this.name = name;
this.uri = session.getRequestURI().toString();
WebSocketServe webSocketServer = webSocketServerMAP.get(uri);
if(webSocketServer != null){ //同样业务的连接已经在线,则把原来的挤下线。
webSocketServer.session.getBasicRemote().sendText(uri + "重复连接被挤下线了");
webSocketServer.session.close();//关闭连接,触发关闭连接方法onClose()
}
webSocketServerMAP.put(uri, this);//保存uri对应的连接服务
addOnlineCount(); // 在线数加1

}

/**
* 连接关闭时触发,注意不能向客户端发送消息了
* @throws IOException
*/
@OnClose
public void onClose() throws IOException {
webSocketServerMAP.remove(uri);//删除uri对应的连接服务
reduceOnlineCount(); // 在线数减1
}

/**
* 收到客户端消息后触发
*
* @param message
* 客户端发送过来的消息
* @throws IOException
*/
@OnMessage
public void onMessage(String message) {
log.info("收到消息:" + message);

}

/**
* 通信发生错误时触发
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
try {
log.info("{}:通信发生错误,连接关闭",name);
webSocketServerMAP.remove(uri);//删除uri对应的连接服务
}catch (Exception e){
}
}

/**
* 获取在线连接数
* @return
*/
public static int getOnlineCount() {
return onlineCount.get();
}

/**
* 原子性操作,在线连接数加一
*/
public static void addOnlineCount() {
onlineCount.getAndIncrement();
}

/**
* 原子性操作,在线连接数减一
*/
public static void reduceOnlineCount() {
onlineCount.getAndDecrement();
}
}

我用的是gradle项目。当然别忘记了引入websocket的jar

附上gradle的jar.

1
2
3
arduino复制代码 implementation ("org.springframework.boot:spring-boot-starter-websocket:2.3.0.RELEASE") {
exclude module: "spring-boot-starter-tomcat"
}

maven项目 jar

1
xml复制代码<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-websocket</artifactId>    <version>${websocket.version}</version>    <exclusions>        <exclusion>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-tomcat</artifactId>        </exclusion>    </exclusions></dependency>

下面开始重点总结:

一开始这个项目发布到服务器上面,只能支持一万的长连接。top命令看了linux的参数,发现CPU内存都还有很大空闲。那么就瓶颈就不在CPU。又看了网络带宽之类的参数,都没有问题。最终锁定发现springboot项目默认的启动容器是tomcat,而tomcat默认支持1W的连接数量。超过就会拒绝。既然问题出在tomcat,那么现在就有两个方案。1、调大tomcat的连接数量。2、容器换成jetty。对于需要保持数十万的长连接,jetty无疑更适合作为启动容器。

启动容器替换成jetty,只需要在jar引用的的时候排除掉tomcat,并且加上jetty的jar.附上jar引用代码

gradle项目jar

1
2
3
4
5
6
7
sql复制代码 implementation("org.springframework.boot:spring-boot-starter-web") {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-jetty:2.3.1.RELEASE'
implementation ("org.springframework.boot:spring-boot-starter-websocket:2.3.0.RELEASE") {
exclude module: "spring-boot-starter-tomcat"
}

maven项目 jar

1
2
3
xml复制代码<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-websocket</artifactId>    <version>${websocket.version}</version>    <exclusions>        <exclusion>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-tomcat</artifactId>        </exclusion>    </exclusions></dependency>
<!-- 引入jetty作为启动容器 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> <version>2.3.1.RELEASE</version></dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <version>${websocket.version}</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions></dependency>

这里有个坑,引入websocket jar的时候一定也要排除tomcat的依赖。不然启动容器依然是tomcat。

现在这个代码放上linux服务器。按理来说应该可以支持起码好几万的长连接了吧。但是事与愿违,经过测试,发现居然只能建立1.6W+长连接。但是服务器的内存跟cpu明显还有空余。那么问题出在哪里?既然替换了容器,这个项目现在支持的长连接应该是看服务器的配置的。问题应该不在于框架了。那会不会是linux本身有什么限制。导致只能维持1.6W+的长连接呢?

ulimit -a 参看linux的各种参数限制。

在这里插入图片描述

max locked memory (kbytes, -l) 16384

这个参数跟长连接的数量及其相似,初步猜测是不是这个参数的问题。

ulimit -l unlimited 把这个参数调成了无限制。现在再来测试长连接的数量。

已经可以达到5W+。因为测试工具的原因。没有做更高的压测,但是初步观察。保持10万的长连接应该是支持的。

附上永久修改 max locked memory 的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
markdown复制代码  1)、解除 Linux 系统的最大进程数和最大文件打开数限制:

​ vi /etc/security/limits.conf

​ \# 添加如下的行

root soft nofile 1048576

root hard nofile 1048576

* soft nofile 1048576

* hard nofile 1048576

​

root soft memlock 102400

root hard memlock 102400

* soft memlock 102400

* hard memlock 102400

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

每个程序员都该有个自己的博客,分享我的四种博客搭建教程!

发表于 2021-01-25

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

压测了,小傅哥一天能搭4个博客!

好学、乐学、博学、恒学、会学和用学,学以致用。一起学习成长的很多同好以及我自己,都是同样喜欢折腾的人。

最早大家都喜欢倒腾自己的QQ空间,装修的各式各样,可那炫耀。但终究这个QQ空间里面,还有很多东西不能让自己随意摆弄。不知道是不是此类好奇和爱好,让很多人走上了编程开发的道路。

就折腾博客而言,在大学开始就不停的折腾。从一个网页能被宿舍人访问、被校友访问、被家人看到,那个兴奋劲还是十足的。哪怕是半夜也是一遍遍的折腾写着html,虽然丑了吧唧的!


最近有不少粉丝问小傅哥,自己也想搭建个自己的博客系统写写文章,但不知道怎么弄。正好小傅哥也确实折腾过各种博客的搭建,了解一些坑坑洼洼,算是给后面的司机开开路。

本文主要向大家介绍:

  1. 4类静态博客,hexo、docsify、jekyll、vuepress,的差异和特点
  2. 在 GitPage 上部署自己的博客
  3. 独立域名+个人服务器,部署博客
  4. 另外小傅哥把这些博客脚手架统一放到Github仓库,方便大家使用时候可以更方便。关注公众号:bugstack虫洞栈,回复:博客系统

有了这些参考,大家就可以选择适合自己的博客系统了,开心的写博客。

二、你要准备的东西

  • 简单记录:Github账号或者Gitee账号,使用两家的免费静态网页托管服务即可。
  • 绑定域名:如果你想通过自己的域名访问博客,Github与Gitee都支持配置,但Gitee需要付费。不过Gitee对于国内的访问速度要好一些。
  • 访问速度:当你的博客想被更多人访问并且也在意网页的打开速度和体验,那么就需要一个独立的服务器和域名了。这个服务器可以部署静态网页即可

综上,是每一个人建博客的不同目的和需要的内容,按需选择即可。

另外,GitPage配置参考:docsify.js.org/#/zh-cn/dep… 在Github的配置中,可以选择根目录和docs两个文件夹,作为静态博客的仓库。所以在选择下面四类博客中,都是把docs文件夹预留出来,方便使用。

三、4种博客的搭建

  • 小傅哥把四类比较常用的博客,源码部分放到这个集中的仓库,方便大家在使用的时候直接克隆走。
  • 关于这四类博客的建设,会在以后陆续的完善内容。如果你感兴趣也可以参与到项目中。
  • 下载地址:github.com/BlogGuide

1. hexo

http://hexo.blog.itedus.cn/

  • 介绍:Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。
  • 官网:hexo.bootcss.com
  • 案例:hexo.blog.itedus.cn
  • 源码:github.com/BlogGuide/h… - 克隆到自己的仓库
  • 命令:
1
2
3
4
5
6
java复制代码npm install hexo-cli -g
hexo init blog
cd blog
npm install
hexo generate # 生成
hexo server # 启动服务
  • 特点:
    • hexo的主题特别多,选择性很高
    • 需要本地编译后,把编译文件推送到Github
  • 其他:
    • 因为需要编译和推送,如果只是想简单的写博客,不推荐使用。
    • 但如果想把静态博客部署到个人的服务器,那么就非常适合了。

2. docsify

http://docsify.blog.itedus.cn/

  • 介绍:docsify 可以快速帮你生成文档网站。不同于 GitBook、Hexo 的地方是它不会生成静态的 .html 文件,所有转换工作都是在运行时。如果你想要开始使用它,只需要创建一个 index.html 就可以开始编写文档并直接部署在 GitHub Pages。
  • 官网:docsify.js.org/#/zh-cn
  • 案例:docsify.blog.itedus.cn
  • 源码:github.com/BlogGuide/d… - 克隆到自己的仓库
  • 命令:
1
2
3
java复制代码npm i docsify-cli -g # 全局快速安装
docsify init ./docs # 初始化项目
docsify serve docs # 本地预览
  • 特点:非常简单、干净,直接把工程文件和md博客推送到Github即可,不需要本地维护编译。

3. jekyll

http://jekyll.blog.itedus.cn

  • 介绍:一个简单的,基于引导的主题。特别是对于那些喜欢在网站上展示自己的项目并喜欢做笔记的开发人员。还有一些神奇的特征需要发现。
  • 官网:github.com/DONGChuan/Y…
  • 案例:jekyll.blog.itedus.cn
  • 源码:github.com/BlogGuide/j… - 克隆到自己的仓库
  • 命令:
1
2
3
4
5
6
7
java复制代码Fork code and clone
Run bower install to install all dependencies in bower.json
Run bundle install to install all dependencies in Gemfile
Update _config.yml with your own settings.
Add posts in /_posts
Commit to your own Username.github.io repository.
Then come back to star this theme!
  • 特点:这个博客的主题其实有点重,在写博客的时候需要人工维护的内容较多。但同样这个主题有一个好处就是如果使用Github,那么就直接把项目和博客传到Github即可,不需要本地编译。

4. vuepress

http://vuepress.blog.itedus.cn

  • 介绍:VuePress 由两部分组成:第一部分是一个极简静态网站生成器 (opens new window),它包含由 Vue 驱动的主题系统和插件 API,另一个部分是为书写技术文档而优化的默认主题,它的诞生初衷是为了支持 Vue 及其子项目的文档需求。
  • 官网:vuepress.vuejs.org/zh
  • 案例:vuepress.blog.itedus.cn
  • 源码:github.com/BlogGuide/v… - 克隆到自己的仓库
  • 命令:
1
2
3
java复制代码npm install -g vuepress # 安装
vuepress build docs # 构建,生成html,可以用于部署
vuepress dev docs # 启动,http://localhost:8080/
  • 特点:基于vue实现的博客,功能很多适合扩展。很适合部署到个人独立的服务器,如果是部署到Github,可以参考源码,在一个工程中提供docs用于存放生成的网页,这样在Github就不需要再维护额外的分支。

四、部署到自己的服务器

  • 博客:vuepress
  • 软件:Idea、ftp[可选]
  • 环境:域名、备案、SSL证书、服务器

vuepress的博客项目放IDEA中打开和日常维护就可以了,而且IDEA只提供了FTP的功能,也可以方便上传服务到远程服务器。

关于域名和服务器等需要购买,另外还需要备案才能正常使用。如果你想域名有一个小锁头的安全提示,则需要ssl证书,一般可以免费获取。

其实小傅哥已经有一个 <bugstack.cn> 博客,本次是又申请了一个新的域名 <itedus.cn> 想着再搭建一个玩玩,折腾!

1. IDEA 配置 FTP

在IDEA的菜单栏上,Tools 中有一个 Deployment 的选项,可以配置FTP以及其他SFTP。

IDEA 配置 FTP

  • Host:你购买的服务器都会提供FTP功能,在里面有host地址
  • User name:用户名
  • Password:密码
  • 配置完成后,在Deployment打开的菜单选项中,会有一个 Browse Remote Host 打开以后可以在IDEA中看到了。

2. 上传静态网页

上传静态网页

  • 到这就可以直接上传了你的静态网页到服务器了
  • 其实你还可以基于 Github 的 Webhooks 配置自动推送,但整体配置和实现的内容比较多

五、总结

  • 与CSDN、掘金、思否、开源中国等提供的博客相比,自己维护的博客开发还是需要一些时间精力和运营成本的。但如果想给自己的知识一个实践的机会,就值得折腾。
  • hexo、docsify、jekyll、vuepress,四类博客各有自己的特点,有的需要编译上传,有的直接推送Github即可。但想有自己的域名和整体的体验,就需要购买服务器和备案域名。
  • 本篇文章只为送给那些想折腾一下的伙伴提供一些可实现的路径,但这条路径上如果你想真的搭出一个称心如意的博客,要搞的东西还很多。甚至你会像我一样折腾到公众号开发与博客联动等等,好!助力你做个喜欢折腾的人!

六、系列推荐

  • 讲道理,只要你是一个爱折腾的程序员,毕业找工作真的不需要再花钱培训!
  • 20年3月27日,Github被攻击。我的GitPage博客也挂了,紧急修复之路,也教会你搭建 Jekyll 博客!
  • 为了省钱,我用1天时间把PHP学了!
  • 工作两年简历写成这样,谁要你呀!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL高级篇 - 性能优化

发表于 2021-01-25
  • 生产过程中优化的过程
    • 观察,至少跑一天,看看生产的慢SQL情况
    • 开启慢查询日志,设置阈值,比如超过5秒钟的就是慢SQL,并将它抓取出来
    • Explain+慢SQL分析
    • show profile
    • 运维经理 or DBA,进行SQL数据库服务器的参数调优
  • 目标
    • 慢查询的开启并捕获
    • explain+慢SQL分析
    • show profile查询SQL在MySQL服务器里面的执行细节和生命周期情况
    • SQL数据库服务器的参数调优### 索引优化 (*)

索引分析

单表
  • 建表SQL
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql复制代码//建表
CREATE TABLE IF NOT EXISTS `article` (
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`author_id` INT(10) UNSIGNED NOT NULL,
`category_id` INT(10) UNSIGNED NOT NULL,
`views` INT(10) UNSIGNED NOT NULL,
`comments` INT(10) UNSIGNED NOT NULL,
`title` VARBINARY(255) NOT NULL,
`content` TEXT NOT NULL
);
//插入数据
INSERT INTO `article`(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES
(1, 1, 1, 1, '1', '1'),
(2, 2, 2, 2, '2', '2'),
(1, 1, 3, 3, '3', '3');
//查看表数据
SELECT * FROM article;
![image-20200908105638509](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/717dc85007140d564835d4dcd50ba7bb46a93996ff4011a9c1f172346dc02067)
  • 案例分析
+ 查询 category\_id 为1 且 comments 大于 1 的情况下,views 最多的 article\_id


    - 
1
mysql复制代码SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
![image-20200908105741514](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/3b57b22861c27709c4ee1a1184d76e6d8a9442606df66b0e4c10a7d97b01b6ac) - Explain分析 ![image-20200908105821004](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/8498d988beb48d6f7303af3b9e41255cb9326c9ac772e79cee2e89744d76137a) 结论:很显然,type是ALL,即最坏的情况,Rxtra里还出现了Using filesort,也是最坏的情况,优化是必须的 - 查看文章表已有的索引
1
mysql复制代码show index from article;
![image-20200908105925547](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5cdfe7b6b1122783cc3bbd080151ce94dbccee2629b8fbe62b5b4106a50dfa94) - 开始优化 1. 第一次优化:创建复合索引
1
mysql复制代码create index idx_article_ccv on article(category_id,comments,views);
![image-20200908110417751](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/e0a5fdf46149f78755159870e2d481d08223882600d5dc42cb406eda9ffca896) 第二次执行Explain ![image-20200908110705812](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/0fc9d17e488a96ab01fde1f264497fcd3930b72e7b6823f59931e7a0dfb8fa78) 结论: type 变成了 range,这是可以忍受的。但是 extra 里使用 Using filesort 仍是无法接受的。 但是我们已经建立了索引,为啥没用呢? 这是因为按照 BTree 索引的工作原理,先排序 category\_id,如果遇到相同的 category\_id 则再排序 comments,如果遇到相同的 comments 则再排序 views,当 comments 字段在联合索引里处于中间位置时,因comments > 1 条件是一个范围值(所谓 range),MySQL 无法利用索引再对后面的 views 部分进行检索,**即 range 类型查询字段后面的索引无效** 2. 第二次优化:删除第一次优化建立的索引,重建索引
1
2
3
4
5
mysql复制代码//删除索引
DROP INDEX idx_article_ccv ON article;
//重新创建索引
#ALTER TABLE `article` ADD INDEX idx_article_cv ( `category_id` , `views` ) ;
create index idx_article_cv on article(category_id,views);
![image-20200908111610172](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/95469291a400564bbe0288d09dcf4ce327577885fc6a3ba54f16e2e5bbdd2a66) 第三次执行Explain ![image-20200908111900342](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/554d88c0a7c87285827082594bb7ca7cd5e4839e1ed620949be0fc93a7e5fbdd) - 结论:可以看到,type 变为了 ref,Extra 中的 Using filesort 也消失了,结果非常理想
两表(关联查询)
  • 建表SQL
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
mysql复制代码 
CREATE TABLE IF NOT EXISTS `class` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);

INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));

INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
  • 案例分析
+ 
1
mysql复制代码Explain select * from class c left join book b on c.card=b.card;
第一次执行Explain![image-20200908113345526](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/cb786959672db0aaedeff560c0baac95365979c3d215099c35788012a0474c97) + 第一次优化 ,book表(右表)card字段 添加索引
1
mysql复制代码ALTER TABLE `book` ADD INDEX Y (`card`);
![image-20200908113748153](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/0a3206a8eb40b2acf6b74f68923934c2cff92eba2375bc1eca8bd22dfb0c90ce) 加索引后,第二次执行Explain ![image-20200908113936575](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/6cf59f32ef84d375125d3071eff9df8a51ff797a9e230fb0d8b4c952fac4b3de) 结论:可以看到第二行的 type 变为了 ref,rows 也变成了**优化比较明显** + 第二次优化,删除book表的索引,在class表(左表)的card字段,创建索引
1
2
3
mysql复制代码drop index Y on book;
ALTER TABLE class ADD INDEX X (`card`);
show index from class;
![image-20200908114627413](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/d73a48ec0cf5027ea1d2d88a2b6853479d16a394c8e76d4eb64cf7a30f80bb74) 第三次执行Explain ![image-20200908114836689](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/92f27f6aef9e8be2edc03e15a1cb2f7d3230b50fcc7165a1e5e6f2c9d4dd0427) 优化效果不明显 + **结论**:由左连接特性决定的,LEFT JOIN 条件用于确定如何从右表搜索行,左边一定都有,所以**右边是我们的关键点,一定需要建立索引**;同理右连接,左边表一定要建立索引
三表
  • 建表SQL
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mysql复制代码CREATE TABLE IF NOT EXISTS `phone` (
`phoneid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`phoneid`)
)ENGINE=INNODB;

INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
  • 案例分析
+ 
1
mysql复制代码Explain select * from class inner join book on class.card=book.card inner join phone on book.card=phone.card;
三表关联,第一次Explain![image-20200908133931153](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/73e95afa3f4a8c8cd28a63367d445664d50c88045613c547723f5d2ed725d0e7) + 跟phone表和book表的 card字段 创建索引
1
2
mysql复制代码ALTER TABLE phone ADD INDEX Z (`card`);
ALTER TABLE book ADD INDEX Y (`card`);
![image-20200908134438178](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/2323362eb323a3464c83e4029ac916a96ba23d61cfc0f324326064b549e2f49c) 创建完索引后,第二次Explain![image-20200908134559319](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/b9bb1e5fe95918d5d713f70c20a4d82b7551d50542f39bd012fefe61cbae6a7f) 优化明显,后两行的type都是ref且总的必须检查的记录数rows优化很好,效果不错。因此**索引最好设置在需要经常查询的字段中** + 结论:join语句的优化 - 尽可能减少Join语句中的NestedLoop的循环总次数:**永远用小的结果集驱动大的结果集** - **优先**优化NestedLoop的**内层循环** - **保证Join语句中被驱动表上Join条件字段已经被索引** - 当无法保证被驱动表的Join条件字段被索引且内存资源充足的情况下,不要太吝惜**JoinBuffe**r的设置

索引失效(应该避免)

  • 建表SQL
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql复制代码CREATE TABLE IF NOT EXISTS `staffs` (
`id` INT(10) Primary key AUTO_INCREMENT,
name varchar(24) not null default ' ' comment '姓名',
age INT not NULL default 0 comment '年龄',
pos VARCHAR(20) not null default ' ' comment '职位',
add_time TimeStamp not null default current_timestamp comment '入职时间'
)charset utf8 comment '员工记录表';

INSERT INTO staffs(NAME,age,pos,add_time)values('z3',22,'manager',now());
INSERT INTO staffs(NAME,age,pos,add_time)values('july',23,'dev',now());
INSERT INTO staffs(NAME,age,pos,add_time)values('2000',23,'dev',now());

select * from staffs;

//创建复合索引
ALTER TABLE staffs ADD INDEX idx_staffs_nameAgePos(name,age,pos);
![image-20200908143352651](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/a01705457bfe8c073a811fe1e7570fec15d7fcde32cf9a4b0b5a6873f2bcd7c9)
案例(索引失效)
  1. 全值匹配最喜欢看到
* 
1
mysql复制代码 Explain Select * from staffs where name='july' and age=25 and pos='manager';
![image-20200908155852496](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/fa60ba45999638266d995a9948567d20ef39f430e2fa69d7cd0a93b3360290fe)
  1. 最佳左前缀法则:如果索引了多列,要遵守最左前缀法则,指的是查询从索引的最左前列开始并且不跳过索引中的列
* 当使用**覆盖索引**的方式时,(select name,age,pos from staffs where age=10 (后面没有其他没有索引的字段条件)),即使不是以 name 开头,也会使用 idx\_nameAgePos 索引
* 如果中间有跳过的列 name、pos,则只会**部分使用**索引
* 索引 idx\_staffs\_nameAgePos 建立索引时 以 name , age ,pos 的顺序建立的。全值匹配表示 按顺序匹配的



1
2
3
4
5
6
7
8
mysql复制代码Explain select * from staffs where name='july';
Explain select * from staffs where name='july' AND age=23;
Explain select * from staffs where name='july' AND age=23 AND pos='dev';

```![image-20200908143856734](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/ba454d54419fa17d2e3898082107ea2573b98425854b8698e2e5feac8fcf18ab)


* 改变查询语句
mysql复制代码Explain select * from staffs where age=23 AND pos='dev'; Explain select * from staffs where pos='dev';
1
2
3
4
5
6
7
8
9
10
11


未遵循最佳左前缀法则,导致了索引失效
3. 不要在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描


* 索引列 name 上做了函数操作![image-20200908155049042](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/3103ab05117ac7f18fb20fdc96b475c2c392b2f1c6cee38702423e41a75d2eb3)
4. 存储引擎不能使用索引中范围条件右边的列,**范围之后全失效**


*
mysql复制代码Explain Select * from staffs where name='july' and age=25 and pos='manager'; Explain Select * from staffs where name='july' and age>25 and pos='manager';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	
![image-20200908160325007](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/f8436ad28ee443ac4fd5572f784c2c80d9915cfe3748b3606af961a4be1ac6a4)
* 范围 若有索引则能使用到索引,**范围条件右边的索引会失效**(范围条件右边与范围条件使用的同一个组合索引,右边的才会失效。若是不同索引则不会失效)
5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select \*


![image-20200908161321041](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/1a8a68bbe53295801d1f172a1b2b40ee4d6085c093ac469faa35780f68a7a1ec)
6. mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描


![image-20200908162939596](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/bf2da93fa238a89c6b9dba2c022582c6e45c36c0220036e6573891b0ce8931c2)
7. is not null 也无法使用索引,但是is null是可以使用索引的


![image-20200908170126343](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/948403ff259f60befa73513f48c50814e8a2894f7a30e6e0a3b5390ee9bccdf5)
8. like以通配符**开头**(’%abc‘)mysql索引失效会变成全表扫描的操作


* % 写在右边可以避免索引失效


![image-20200908170639510](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/0b358c70ff6ffb98553226d7ea864193adb7cbda61e4b0ea83c0b1013c832687)
* 问题:解决like ‘%字符串%’时索引不被使用的方法??------**覆盖索引**(完全重合或包含,但不能超过)


+ **覆盖索引**:建的索引和查询的字段,最好完全一致或者包含,但查询字段不能超出索引列
+ 建表SQL
mysql复制代码CREATE TABLE `tbl_user` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `NAME` VARCHAR(20) DEFAULT NULL, `age` INT(11) DEFAULT NULL, email VARCHAR(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO tbl_user(NAME,age,email) VALUES('1aa1',21,'b@163.com'); INSERT INTO tbl_user(NAME,age,email) VALUES('2aa2',222,'a@163.com'); INSERT INTO tbl_user(NAME,age,email) VALUES('3aa3',265,'c@163.com'); INSERT INTO tbl_user(NAME,age,email) VALUES('4aa4',21,'d@163.com'); INSERT INTO tbl_user(NAME,age,email) VALUES('aa',121,'e@163.com');
1
+ before index 未创建索引
mysql复制代码EXPLAIN SELECT NAME,age FROM tbl_user WHERE NAME LIKE '%aa%'; EXPLAIN SELECT id FROM tbl_user WHERE NAME LIKE '%aa%'; EXPLAIN SELECT NAME FROM tbl_user WHERE NAME LIKE '%aa%'; EXPLAIN SELECT age FROM tbl_user WHERE NAME LIKE '%aa%'; EXPLAIN SELECT id,NAME FROM tbl_user WHERE NAME LIKE '%aa%'; EXPLAIN SELECT id,NAME,age FROM tbl_user WHERE NAME LIKE '%aa%'; EXPLAIN SELECT NAME,age FROM tbl_user WHERE NAME LIKE '%aa%';
1
2
3

![image-20200908171856221](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/2e0a5ebe7ad7587a0fffef7d02d1d7b1fd5a50340f6a8caa3207738752ce31bd)
+ 创建索引后,再观察变化(使用**覆盖索引**来解决like 导致索引失效的问题)
mysql复制代码CREATE INDEX idx_user_nameAge ON tbl_user(NAME,age); #DROP INDEX idx_user_nameAge ON tbl_user EXPLAIN SELECT name,age FROM tbl_user WHERE NAME like '%aa%'; EXPLAIN SELECT name FROM tbl_user WHERE NAME like '%aa%'; EXPLAIN SELECT age FROM tbl_user WHERE NAME like '%aa%'; EXPLAIN SELECT id,name FROM tbl_user WHERE NAME like '%aa%'; EXPLAIN SELECT id,name,age FROM tbl_user WHERE NAME like '%aa%'; EXPLAIN SELECT name,age FROM tbl_user WHERE NAME like '%aa%';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
		
![image-20200908172216943](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/40454f35a2635113e82943022c9fcf060444545d9deb6ca207c443e7e9c99891)


完全一致或者包含的情况,成功使用覆盖索引(match匹配)![image-20200908172847457](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/692eca0aec2520460878cf2e0704453991e71be20c970a5a3f673f7979d8e1e4)


查询字段,不一致或者超出建立的索引列![image-20200908173437343](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/eaf674dddf5b5f78ce8e064ca70b6695fb95f5d4cdd66c1bb0465e03764e8ad5)
9. 字符串不加单引号索引失效


* mysql 优化分析,会将int类型的777 **自动转化**为String类型,但违背了不能再索引列上进行手动或自动的转换(索引失效--案例3),导致索引失效
* ![image-20200908173702985](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/215d2c88155f000212bb0b3b9080ae2309c23e90477f79e619accd0b30901b00)
10. 少用or,用它来连接时会索引失效


* ![image-20200908173856916](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/0bd2d538fbbc362617d1ce3f5f2e4f54d57caa068016bf136ca3b684e374ddee)
11. 小总结


* ![image-20200908174229766](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/9c448701e08464656bc5455c0827d95aa6ad8dfb4cbbad7218701d7a92687807)


##### 面试题讲解(\*)


* 题目SQL


+
mysql复制代码create table test03( id int primary key not null auto_increment, c1 char(10), c2 char(10), c3 char(10), c4 char(10), c5 char(10) ); insert into test03(c1,c2,c3,c4,c5) values('a1','a2','a3','a4','a5'); insert into test03(c1,c2,c3,c4,c5) values('b1','b2','b3','b4','b5'); insert into test03(c1,c2,c3,c4,c5) values('c1','c2','c3','c4','c5'); insert into test03(c1,c2,c3,c4,c5) values('d1','d2','d3','d4','d5'); insert into test03(c1,c2,c3,c4,c5) values('e1','e2','e3','e4','e5'); select * from test03;
1
2
3

![image-20200909104337018](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/0d0a2dc09e8ebcfd554e988c26aa20dbbeacb80e3575213b767174c33f77aede)
+
mysql复制代码//创建复合索引 create index idx_test03_c1234 on test03(c1,c2,c3,c4); show index from test03;
1
2
3
4
5
6

![image-20200909104517859](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/3684dcebe8433533dec062d95aa0895812b30a16350aaf4b34ed5ded3e553eb9)
+ 问题:创建了复合索引`idx_test03_c1234` ,根据以下SQL分析索引使用情况?


-
mysql复制代码#基础 explain select * from test03 where c1='a1'; explain select * from test03 where c1='a1' and c2='a2'; explain select * from test03 where c1='a1' and c2='a2' and c3='a3'; explain select * from test03 where c1='a1' and c2='a2' and c3='a3' and c4='a4';
1
2
3

1.基础使用![image-20200909104854866](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/d67f93ec147a69d7acfc7f94071bd0140e0c6ba1ada32e548aa1e0d201f62f43)
-
mysql复制代码explain select * from test03 where c1='a1' and c2='a2' and c4='a4' and c3='a3';
1
2
3

2.效果与 c1、c2、c3、c4按顺序使用一样,mysql底层优化器会自动优化语句,尽量保持顺序一致,可避免底层做一次翻译![image-20200909105521154](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/9028df316a3b29daa26edcae5a81419295c96bd7108c4f5a49c78506bd7cc33f)
-
mysql复制代码 explain select * from test03 where c1='a1' and c2='a2' and c3>'a3' and c4='a4';
1
2
3

3.索引只用到部分c1、c2、c3(只用来排序,无法查找)。范围之后全失效,c4完全没有用到![image-20200909110720517](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/7f874e492c4e8eee92251e6e9848723aeaaa9c40f1b455ed2548ea038df60bce)
-
mysql复制代码explain select * from test03 where c1='a1' and c2='a2' and c4>'a4' and c3='a3';
1
2
3

4.同理2,mysql底层优化器会自动调整语句顺序,因此索引c1、2、3、4全起效![image-20200909110245254](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/0ebe5ed3bcda35099b784198f7765e7d1f697d7300759ea26326d5c7d00290d3)
-
mysql复制代码explain select * from test03 where c1='a1' and c2='a2' and c4='a4' order by c3;
1
2
3

5.c3作用在排序而不是查找(因此没有统计到ref) c1、c2、c3![image-20200909110900400](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/e5056b79293fddfd53c98365a571f272767731e4786960b78612676516d582dd)
-
mysql复制代码explain select * from test03 where c1='a1' and c2='a2' order by c3;
1
2
3

6.同理5 c1、c2、c3![image-20200909111139157](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/ead1a338c1eac9258dd630fe755eca0154c1163efddb44754347c9ced6a458f3)
-
mysql复制代码explain select * from test03 where c1='a1' and c2='a2' order by c4;
1
2
3

7.出现filesort 文件排序 c1、c2![image-20200909111358756](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5a436e62ee7611f540ab8e5c3f16141d3d0896721760fe5c00858c70ee239b0e)
-
mysql复制代码explain select * from test03 where c1='a1' and c5='a5' order by c2,c3;
1
2

8.1只用c1一个字段索引查找,但是c2、c3用于排序,无filesort![image-20200909111732039](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/fb635d949284dc7af6547daf2f55212cc898b9b278bfec18df3c689cc7554761)
mysql复制代码explain select * from test03 where c1='a1' and c5='a5' order by c3,c2;
1
2
3

8.2出现了filesort,我们建的索引是1234,它没有按照顺序来,3 2 颠倒了![image-20200909111845428](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/54b7a8306e035e51da453f91d28b2b0fda9b96c924a47bb77667ca58367ed8b0)
-
mysql复制代码explain select * from test03 where c1='a1' and c2='a2' order by c2,c3;
1
2
3

9.用c1、c2两个字段索引,但是c2、c3用于排序,无filesort![image-20200909112054301](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/addbf53cf35d0994de8e07eb24174854122ece092d448040d75efe694a47dad4)
-
mysql复制代码explain select * from test03 where c1='a1' and c2='a2' and c5='a5' order by c3,c2;
1
2
3
4
5
6

10.本例对比8.2 多了c2常量(无需排序),不会导致filesort


![image-20200909112606114](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/6e23cdccec2f7af0b2aaee37f76e767c78a416c71baa6bd03be9e01c9a1add82)
-
mysql复制代码explain select c2,c3 from test03 where c1='a1' and c4='a4' group by c2,c3; explain select c2,c3 from test03 where c1='a1' and c4='a4' group by c3,c2;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
		
11.**group by 分组之前必排序**![image-20200909113433673](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/4a939a555bed9dfef2592d85e32bf70cfb6d211fb0b2cafa78bd752e398fa523)
* > 定值(常量)、范围(范围之后皆失效)还是排序(**索引包含查找排序两部分**),一般order by是给个范围
* group by 基本上是需要进行排序,会有临时表产生


#### 一般性建议


* 对于单键索引,尽量选择针对当前query过滤性更好的索引
* 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前(左)越好
* 在选择组合索引的时候,尽量选择能够包含当前query中的where字句更多字段的索引
* 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的


查看截取分析
------


* 生产过程中优化的过程
+ 观察,至少跑一天,看看生产的慢SQL情况
+ 开启慢查询日志,设置阈值,比如超过5秒钟的就是慢SQL,并将它抓取出来
+ Explain+慢SQL分析
+ show profile
+ 运维经理 or DBA,进行SQL数据库服务器的参数调优
* 总结
+ 慢查询的开启并捕获
+ explain+慢SQL分析
+ show profile查询SQL在MySQL服务器里面的执行细节和生命周期情况
+ SQL数据库服务器的参数调优


### 查询优化


#### 永远小表驱动大表(子查询)


* 案例


+ 优化原则:小表驱动大表,即小的数据集驱动大的数据集


- 原理 `in`
mysql复制代码select * from A where id in(select id From B); ##等价于(嵌套循环) for select id from B for select * from A where A.id=B.id
1
2
- 当B表的数据集必须小于A表的数据集时,用`in`优于`exists`
- 原理 `exists`
mysql复制代码select ...from table where exists(subQuery)
1
2
3
4
5
6
7
8
9

该语法可以理解为:**将主查询的数据,放到子查询中做条件验证,根据验证结果(true或false)来决定主查询的数据结果是否得以保留**
- 提示:


1. Exists(subquery)只返回TRUE或FALSE,因此子查询中的SELECT \* 也可以是SELECT 1 或其他,官方说法是实际执行时会忽略掉SELECT清单,因此没有区别
2. Exists子查询的实际执行过程可能经过了优化而不是我们理解上的逐条对比,如果担忧效率问题,可进行实际校验以确定是否有效率问题
3. Exists子查询往往也可以用条件表达式、其他子查询或者JOIN来替代,何种最优需要计提问题具体分析
- 当A表的数据集小于B表的数据集时,用`exists`优于`in`
mysql复制代码select * from A where exists (select 1 from B where B.id=A.id) ##等价于 for select * from A for select * from B where B.id=A.id
1
2
3
4
5
6
7
8
9
10
		- 注意:A表与B表的ID字段应建立索引


#### ORDER BY 关键字优化


* ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序


+ 建表SQL
mysql复制代码 CREATE TABLE tblA( id int primary key not null auto_increment, age INT, birth TIMESTAMP NOT NULL, name varchar(200) ); INSERT INTO tblA(age,birth,name) VALUES(22,NOW(),'abc'); INSERT INTO tblA(age,birth,name) VALUES(23,NOW(),'bcd'); INSERT INTO tblA(age,birth,name) VALUES(24,NOW(),'def'); CREATE INDEX idx_A_ageBirth ON tblA(age,birth,name); SELECT * FROM tblA;
1
2
3

![image-20200909161345508](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/37af6dfe563f6b70b7cdacceda87c8fc6e25eb895dd3b5ff41eb48465cd6bfd1)
+ Case
mysql复制代码Explain Select * From tblA where age>20 order by age; Explain Select * From tblA where age>20 order by age,birth; #是否产生filesort Explain Select * From tblA where age>20 order by birth; Explain Select * From tblA where age>20 order by birth,age;
1
2

![image-20200909161938105](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/49bccb8712b9bcceb36f1b6cc4906a712c640d1e2aef06f27b71515370fa5f2c)
mysql复制代码explain select * From tblA order by birth; explain select * From tblA Where birth >'2020-9-09 00:00:00' order by birth; explain select * From tblA Where birth >'2020-9-09 00:00:00' order by age; explain select * From tblA order by age ASC,birth DESC; #mysql默认升序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
	
![image-20200909162616690](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/4a5778c4450f9ee186edb52cf013af19304d0a84a41620a815ee837f023fd217)
+ MySQL支持两种方式排序,FileSort和Index,Index效率高,它指MySQL扫描索引本身完成排序,FileSort方式效率较低
+ ORDER BY满足两种情况,会使用Index方式排序:


- ORDER BY语句使用索引最左前列
- 使用WHERE子句与ORDER BY子句条件列组合满足索引最左前列
* 尽可能在索引列上完成排序操作,遵循索引建的**最佳左前缀**
* 如果不在索引列上,filesort有两种算法:


+ 双路排序
- MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据,读取行指针和orderby列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出
- 从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段
- 取一批数据,要对磁盘进行了**两次扫描**,众所周知,I/O是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序
+ 单路排序
- 从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了
+ 结论及引申出的问题
- 由于单路是后出的,总体而言好过双路
- 但是用单路有问题: 在sort\_buffer中,方法B比方法A要多占用很多空间,因为方法B是把所有字段都取出, 所以有可能取出的数据的总大小超出了sort\_buffer的容量,导致每次只能取sort\_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取取sort\_buffer容量大小,再排……从而多次I/O,本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失
* 优化策略


+ 增大sort\_buffer\_size参数的设置
- 用于单路排序的内存大小
+ 增大max\_length\_for\_sort\_data参数的设置
- 单次排序字段大小(单次排序请求)
+ 提高ORDER BY的速度
- Order by时select \* 是一个大忌只Query需要的字段, 这点非常重要。在这里的影响是:
* 当Query的字段大小总和小于max\_length\_for\_sort\_data 而且排序字段不是 TEXT|BLOB 类型时,会用改进后的算法——单路排序, 否则用老算法——多路排序
* 两种算法的数据都有可能超出sort\_buffer的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort\_buffer\_size
- 尝试提高 sort\_buffer\_size。不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的
- 尝试提高 max\_length\_for\_sort\_data。提高这个参数,会增加用改进算法的概率。但是如果设的太高,数据总容量超出sort\_buffer\_size的概率就增大,明显症状是高的磁盘I/O活动和低的处理器使用率
* 小总结(\*)


+ 为排序使用索引
- MySQL两种排序方式:文件排序或扫描有序索引排序
- MySQL能为排序与查询使用相同的索引
- KEY a\_b\_c(a,b,c)
* order by 能使用索引最左前缀
+ ORDER BY a
+ ORDER BY a,b
+ ORDER BY a,b,c
+ ORDER BY a DESC,b DESC,c DESC
* 如果WHERE使用索引的最左前缀定义为常量,则ORDER BY能使用索引
+ WHERE a = const ORDER BY b,c
+ WHERE a = const AND b=const ORDER BY c
+ WHERE a = const AND b > const ORDER BY b,c
* **不能**使用索引进行排序
+ ORDER BY a ASC,b DESC,c DESC //排序不一致
+ WHERE g = const ORDER BY b,c // 丢失a索引
+ WHERE a = const ORDER BY c //丢失b索引
+ WHERE a = const ORDER BY a,d //d不是索引的一部分


#### GROUP BY 关键字优化


* GROUP BY 实质是先排序后进行分组,遵照索引建的最佳左前缀(其他大致同 ORDER BY)
* 当无法使用索引列,增大max\_length\_for\_sort\_data 参数的设置 + 增大sort\_buffer\_size参数的设置
* WHERE高于HAVING,能写在WHERE限定的条件就不要去HAVING限定了


### 慢查询日志


#### 是什么


* MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中**响应时间超过阀值**的语句,具体指运行时间超过long\_query\_time值的SQL,则会被记录到慢查询日志中
* 具体指运行时间超过long\_query\_time值的SQL,则会被记录到慢查询日志中。long\_query\_time的**默认值为10**,意思是运行10秒以上的语句
* 由他来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合之前explain进行全面分析


#### 怎么玩


##### 说明


* 默认情况下,MySQL数据库**没有开启慢查询日志**,需要我们手动来设置这个参数。当然,**如果不是调优需要的话,一般不建议启动该参数**,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件


##### 查看是否开启及如何开启


* 默认


+
mysql复制代码SHOW VARIABLES LIKE '%slow_query_log%';
1
2
3
4
5
6
7
8
	+ 默认情况下slow\_query\_log的值为OFF,表示慢查询日志是禁用的,可以通过设置slow\_query\_log的值来开启


![image-20200910092846735](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/50a042d2be00fa477015ff7bd50b31cd68e379c18b1eee9c361f778e68f69c0e)
* 开启


+
mysql复制代码set global slow_query_log=1;
1
2
3
4
5
6
7
8
+ 使用该命令开启了慢查询日志只对当前数据库生效,如果MySQL**重启后则会失效**


![image-20200910093127655](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/e41b2116f80ba7368171c5223af86202003dc62a0f6a85f180daef32e7975020)
+ 如果要永久生效,就必须修改配置文件my.cnf(其它系统变量也是如此)


修改my.cnf文件,[mysqld]下增加或修改参数`slow_query_log`和`slow_query_log_file`后,重启mysql服务器
c复制代码slow_query_log=1 slow_query_log_file=/var/lib/mysql/touchair-slow.log
1
2
3
4
5
6
7
8
9
10
	+ 关于慢查询的参数 slow\_query\_log\_file, 它指定慢查询日志文件的存放路径,**系统默认会给一个缺省的文件 host\_name-slow,log** (如果没有指定参数 slow\_query\_log\_file 的话)


##### 查看慢查询内容


* 这个是**由参数long\_query\_time控制**,默认情况下long\_query\_time的值为**10秒**;


+ 命令:
mysql复制代码SHOW VARIABLES LIKE 'long_query_time%';
1
2
3
4
5
6
7
8
9
	
![image-20200910094442577](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/255285f12c3abdc96df95397ca3d216c996dd728e2f0a2ab7625bc3ac22bfa60)


假如运行时间正好等于long\_query\_time的情况,并不会被记录下来。也就是说,在mysql源码里是判断**大于long\_query\_time,而非大于等于**
* 使用命令设置阙值超过3秒钟就是慢SQL


+
mysql复制代码set global long_query_time=3; //再次查看 SHOW VARIABLES LIKE 'long_query_time%';
1
2
3
4
5
* 会发现查看 long\_query\_time 的值并没有改变?原因:


+ 需要重新连接或者新开一个会话才能看到修改值
+ 或修改查看命令
mysql复制代码SHOW global VARIABLES LIKE 'long_query_time%';
1
2
3
4
5
6
7
8
9
10
11
	
![image-20200910094852252](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/8f5b595883f8f9f53e6b0f204aea6d9d0882b5182a400e3afb6462419cdf189e)


##### Case


* 记录慢SQL,并后续分析


+ 执行一条休眠4秒的SQL
mysql复制代码SELECT SLEEP(4);
1
2
3
4
+ 查看日志文件


前面配置的日志文件路径或者默认路径
shell复制代码cat /var/lib/mysql/localhost-slow.log
1
2
3
4
5
6
	
![image-20200910100054048](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/b459c995068d8d950ed90b0683fe09cf5d0d340b2a839d007d34639fe7f223c8)
* 查询当前系统中有多少条慢查询记录


+
mysql复制代码show global status like '%Slow_queries%';
1
2
3
4
5
6
7
8
	
![image-20200910100526099](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/44c092ccb23f1d92dbb50253d08edbec30a61487234dad095b8d6d1a3230b0e6)


##### 配置版


* 【mysqld】下配置:

c复制代码slow_query_log=1;
slow_query_log_file=/var/lib/mysql/touchair-slow.log
long_query_time=3;
log_output=FILE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26


#### 日志分析工具mysqldumpslow(\*)


* 查看mysqldumpslow的帮助信息


+ mysqldumpslow --help;


![image-20200910101515571](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/4dbb72201307baace2902131511572163681c1bdc02b74c43bc9c0d483646ed6)
+ s: 是表示按照何种方式排序;
+ c: 访问次数
+ l: 锁定时间
+ r: 返回记录
+ t: 查询行数
+ al:平均锁定时间
+ ar:平均返回记录数
+ at:平均查询时间
+ t:即为返回前面多少条的数据;
+ g:后边搭配一个正则匹配模式,大小写不敏感的;
* 工作常用参考


+ 得到返回记录集最多的10个SQL
shell复制代码mysqldumpslow -s r -t 10 /var/lib/mysql/localhost-slow.log

1
+ 得到访问次数最多的10个SQL
shell复制代码mysqldumpslow -s c -t 10 /var/lib/mysql/localhost-slow.log
1
+ 得到安装时间排序的前10条里面含有左连接的查询语句
mysql复制代码mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/localhost-slow.log
1
+ 另外建议在使用这些命令时,结合 | 和 more使用,否则有可能出现爆屏情况
shell复制代码mysqldumpslow -s r -t 10 /var/lib/mysql/localhost-slow.log | more
1
2
3
4
5
6
7
8
9


### 批量数据库脚本(模拟大批量数据)


* 往数据库表里插1000w条数据


1. 建表SQL
mysql复制代码# 新建库 create database bigData; use bigData; #1 建表dept CREATE TABLE dept( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, deptno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0, dname VARCHAR(20) NOT NULL DEFAULT "", loc VARCHAR(13) NOT NULL DEFAULT "" ) ENGINE=INNODB DEFAULT CHARSET=UTF8 ; #2 建表emp CREATE TABLE emp ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, empno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0, /*编号*/ ename VARCHAR(20) NOT NULL DEFAULT "", /*名字*/ job VARCHAR(9) NOT NULL DEFAULT "",/*工作*/ mgr MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,/*上级编号*/ hiredate DATE NOT NULL,/*入职时间*/ sal DECIMAL(7,2) NOT NULL,/*薪水*/ comm DECIMAL(7,2) NOT NULL,/*红利*/ deptno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0 /*部门编号*/ )ENGINE=INNODB DEFAULT CHARSET=UTF8 ;
1
2
3
4
5
2. 设置参数log\_bin\_trust\_function\_creators


+ 创建函数,假如报错:This function has none of DETERMINISTIC......,由于开启过慢查询日志,因为我们开启了 bin-log, 我们就必须为我们的function指定一个参数
+
mysql复制代码show variables like 'log_bin_trust_function_creators'; set global log_bin_trust_function_creators=1;
1
2
3
4
5
6
7
8
9
10
11
	
![image-20200910104106990](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/3865ebf7d4d34f79f1f04f001982a7e921eb13ecb2813dee140fa83ea91d81cc)
+ 这样添加了参数以后,如果mysqld重启,上述参数又会消失,永久方法:


- windows下my.ini[mysqld]加上log\_bin\_trust\_function\_creators=1
- linux下 /etc/my.cnf下my.cnf[mysqld]加上log\_bin\_trust\_function\_creators=1
3. 创建函数,保证每条数据都不同


+ 随机产生字符串
mysql复制代码DELIMITER $$ CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255) BEGIN ##方法开始 DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ'; ##声明一个 字符窜长度为 100 的变量 chars_str ,默认值 DECLARE return_str VARCHAR(255) DEFAULT ''; DECLARE i INT DEFAULT 0; ##循环开始 WHILE i < n DO SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1)); ##concat 连接函数 ,substring(a,index,length) 从index处开始截取 SET i = i + 1; END WHILE; RETURN return_str; END $$ #假如要删除 #drop function rand_string;
1
+ 随机产生部门编号
mysql复制代码#用于随机产生部门编号 DELIMITER $$ CREATE FUNCTION rand_num( ) RETURNS INT(5) BEGIN DECLARE i INT DEFAULT 0; SET i = FLOOR(100+RAND()*10); RETURN i; END $$ #假如要删除 #drop function rand_num;
1
2
3
4
4. 创建存储过程


+ 创建往emp表中插入数据的存储过程
mysql复制代码DELIMITER $$ CREATE PROCEDURE insert_emp(IN START INT(10),IN max_num INT(10)) BEGIN DECLARE i INT DEFAULT 0; #set autocommit =0 把autocommit设置成0 ;提高执行效率 SET autocommit = 0; REPEAT ##重复 SET i = i + 1; INSERT INTO emp(empno, ename ,job ,mgr ,hiredate ,sal ,comm ,deptno ) VALUES ((START+i) ,rand_string(6),'SALESMAN',0001,CURDATE(),FLOOR(1+RAND()*20000),FLOOR(1+RAND()*1000),rand_num()); UNTIL i = max_num ##直到 上面也是一个循环 END REPEAT; ##满足条件后结束循环 COMMIT; ##执行完成后一起提交 END $$ #删除 # DELIMITER ; # drop PROCEDURE insert_emp;
1
+ 创建往dept表中插入数据的存储过程
mysql复制代码#执行存储过程,往dept表添加随机数据 DELIMITER $$ CREATE PROCEDURE insert_dept(IN START INT(10),IN max_num INT(10)) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO dept (deptno ,dname,loc ) VALUES (START +i ,rand_string(10),rand_string(8)); UNTIL i = max_num END REPEAT; COMMIT; END $$ #删除 # DELIMITER ; # drop PROCEDURE insert_dept;
1
2
3
4
5. 调用存储过程


+ dept
mysql复制代码DELIMITER ; CALL insert_dept(100,10);
1
+ emp
mysql复制代码#执行存储过程,往emp表添加50万条数据 DELIMITER ; #将 结束标志换回 ; CALL insert_emp(100001,500000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
				
插入50w条数据,耗时约24s![image-20200910110608068](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5a94e1a624d898bdc0f2a7a161d8090d8d8dc7c8b59a69e490972317c015ad0a)


查询50w条数据,耗时约0.67s![image-20200910110438640](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/fd4ad2f027d23a7f8c984aea99b255f4f063c3f7c5bc87bea25985725f5e5392)


### Show Profile(生命周期)


#### 是什么


* 是mysql提供可以用来分析当前会话中语句执行的**资源消耗情况**。可以用于SQL的调优的测量
* [官网](https://dev.mysql.com/doc/refman/5.7/en/preface.html)


#### 默认情况下,参数处于关闭状态,并保存最近15次的运行结果


#### 分析步骤


##### 1.是否支持,看看当前的mysql版本是否支持


* 默认关闭,使用前需要开启
* 查看状态

mysql复制代码SHOW VARIABLES LIKE ‘profiling’;

1
2
3
4
5
6
7
8

![image-20200910112854968](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/7c1d524ce0d0624716ad50c8bde9e0282236c0936850f0d2b46724153ca40aef)


##### 2.开启功能,默认是关闭,使用前需要开启


* 开启命令

mysql复制代码set profiling=1;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

![image-20200910113003706](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/f014a542d2bd9c1530b92dd82e45983f5c5a99f77b78cabddf36f2cffc4ec52d)


##### 3.运行SQL


* select \* from emp group by id%10 limit 150000;


+ 执行不通过,原因:


- SQL 标准中不允许 SELECT 列表,HAVING 条件语句,或 ORDER BY 语句中出现 GROUP BY 中未列表的可聚合列。而 MySQL 中有一个状态 ONLY\_FULL\_GROUP\_BY 来标识是否遵从这一标准,默认为开启状态
- 关闭命令
mysql复制代码SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY,',''));

1
2
3
4
5
6
7
8
9
10
11
	+ ![image-20200910114442250](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/088df23d46f431d3afd5430b0572b3b4e9ecc3d793dfb4001179fa9e4d577ff1)
* select \* from emp group by id%20 order by 5;


+ ![image-20200910114602169](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/06a670a6f6fdc38878f33a1181ff041a4cebafd78e006f0d486f704239a2a34d)


##### 4.查看结果,show profiles


* 命令

mysql复制代码show profiles;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

![image-20200910133856680](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/ee68380454adcc6134692023ae5abec94f2a1f8055f48864d93eef07e64ccb7a)


##### 5.诊断SQL


* show profile cpu,block io for query n (n为上一步前面的问题SQL数字号码)
* 参数说明



| TYPE | 解释说明 |
| --- | --- |
| |ALL | 显示所有的开销信息 |
| |BLOCK IO | 显示块IO相关开销 |
| |CONTEXT SWITCHES | 上下文切换相关开销 |
| |CPU | 显示CPU相关开销信息 |
| |IPC | 显示发送和接收相关开销信息 |
| |MEMORY | 显示内存相关开销信息 |
| |PAGE FAULTS | 显示页面错误相关开销信息 |
| |SOURCE | 显示和Source\_function,Source\_file,Source\_line相关的开销信息 |
| |SWAPS | 显示交换次数相关开销的信息 |
*

mysql复制代码show profile cpu,block io for query 17;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

![image-20200910133815297](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5064d402db2e103be00911d1321d9ee26a0dc15965443a401a3dc53bb5529d2a)


##### 6.日常开发**需要注意**的结论


* 出现 `converting HEAP to MyISAM` :查询结果太大,内存都不够用了往磁盘上搬了
* 出现 `Creating tmp table`:创建临时表
+ 拷贝数据到临时表
+ 用完再删除
* 出现 `Copying to tmp table on disk`:把内存中临时表复制到磁盘,危险!!!
* 出现 `locked`


### 全局日志查询


#### 配置启用


* 在mysql的my.cnf中,设置如下:

c复制代码#开启
general_log=1

记录日志文件的路径

general_log_file=/path/logfile
#输出格式
log_output=FILE

1
2
3
4
5
6


#### 编码启用


* 开启命令

mysql复制代码set global general_log=1;

1
* 全局日志可以存放到日志文件中,也可以存放到Mysql系统表中。存放到日志中性能更好一些,存储到表中

mysql复制代码set global log_output=’TABLE’;

1
* 此后 ,你所编写的sql语句,将会记录到mysql库里的general\_log表,可以用下面的命令查看

mysql复制代码select * from mysql.general_log;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

![image-20200910135010674](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/c5e58b4a572e8fc39cf27e7d499a67a422861d9706660686d68a370f97275b65)


![image-20200910135021407](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/56ddd189a10a676fde6570f148607acf5483fe05e9fc48b99eeb6070cd360c36)


#### 尽量不要在生产环境开启这个功能


MySQL锁机制
--------


### 概述


#### 定义


* 锁是计算机**协调**多个进程或线程并发访问某一资源的机制
* 在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂


#### 锁的分类


##### 从对数据操作的类型分(读/写)


* 读锁(**共享锁**):针对同一份数据,多个读操作可以同时进行而不会互相影响
* 写锁(**排它锁**):当前写操作没有完成前,它会阻断其他写锁和读锁


##### 从对数据操作的粒度分


* 为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取,检查,释放锁等动作),因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念
* 一种提高共享资源并发发性的方式是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。更理想的方式是,只对会修改的数据片进行精确的锁定。任何时候,在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可
* **表锁**
* **行锁**


### 三锁


* 开销、加锁速度、死锁、粒度、并发性能;只能就具体应用的特点来说哪种锁更合适


#### 表锁(偏读)


##### 特点


* 偏向MyISAM存储引擎,开销小,加锁快;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低


##### 案例分析


* 建表SQL

mysql复制代码create table mylock(
id int not null primary key auto_increment,
name varchar(20)
)engine myisam;

insert into mylock(name) values(‘a’);
insert into mylock(name) values(‘b’);
insert into mylock(name) values(‘c’);
insert into mylock(name) values(‘d’);
insert into mylock(name) values(‘e’);

select * from mylock;

1
2
3

![image-20200910140755801](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/19c067ee558b642150a4f8e4c54931934d524de95bcb34d7445196c7280d0cc2)
* 手动增加表锁

mysql复制代码lock table 表名字1 read(write),表名字2 read(write),其它;

1
* 查看表上加过的锁

mysql复制代码show open tables;

1
2
3

IN\_USE 0 代表当前没有锁![image-20200910141334740](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/d3e5b336a2d1c15b4e7018b729e1e812407205ba9284ad2cdcd9fc3a35cf3d13)
* 释放表锁

mysql复制代码unlock tables;

1
* 给`mylock`、`dept`表分别添加读锁和写锁

mysql复制代码lock table mylock read,dept write;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

![image-20200910142014886](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/1c4030bf778ef0dd2ca7c28279756e92cded6b3f11d6d6cdaa7e46d343caa2a0)


再次查看表上加过的锁


![image-20200910142041863](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/98c83ad77c8c8c040b578b4c009ab2c7353391a5237bf9e0513a7ecfc3d10d31)


![image-20200910142110811](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/9e3bd53ca5de3dca0be21fdd0381be08aa88a138e1c93fdf2891f920335055f6)


![image-20200910142131876](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/bbd48d11915636821ebec07142d1e9528534723e0a0a4b18852bfc3bef9bb568)


释放锁


![image-20200910142241609](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/a114e8a8188d6bc1b99ad87960d64d029ca4006ad4d5874a13c8c0ac56ec285c)
* 加读锁


+ 新建一个MySQL会话,方便测试
+ | session1 | session2 |
| --- | --- |
| session1可以读image-20200910142937434 | session2可以读image-20200910142911311 |
| session1无法查询其它没有锁定的表image-20200910143231126 | session2查询其它表不受影响image-20200910143302934 |
| 当前session1插入或者更新读锁锁定的表,会直接报错image-20200910143414509 | session2插入或更新会一直等待获取锁image-20200910143501469 |
| 当前session1释放锁image-20200910143646421 | session2获得锁资源:完成上一步一直等待的更新操作image-20200910143627432 |
* 加写锁


+ **mylockwrite(MyISAM)**
+
mysql复制代码lock table mylock write;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	
![image-20200910152552489](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/d79ba957cc052e8f2343810ec0e167ea31be6e8e9640ae2fc3b0a97c1a329e5b)

| session1 | session2 |
| --- | --- |
| 当前session1对锁定的表的查询+更新+插入操作都可以执行image-20200910153707795 | 其他session对锁定的表的查询被阻塞,需等待锁被释放image-20200910153758366 |
| | 在锁表前,如果session2有数据缓存,锁表以后,在锁住的表不发生改变的情况下session2可以读出缓存数据,一旦数据发生改变,缓存将失效,操作将被阻塞住,最好使用不同的id进行测试 |
| session1释放锁image-20200910154022340 | session2获得锁,返回查询结果image-20200910154105524 |


##### 案例结论


* MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁
* MySQL的表级锁有两种模式:


+ 表共享读锁(Table Read Lock)
+ 表独占写锁(Table Write Lock)

| 锁类型 | 可否兼容 | 读操作 | 写操作 |
| --- | --- | --- | --- |
| 读锁 | 是 | 是 | 否 |
| 写锁 | 是 | 否 | 否 |
* 结论:


结合上表,所有对MyISAM表进行操作,会有以下情况:


+ 对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求,只有当读锁释放后,才会执行其他进程的写操作
+ 对MyISAM表的写操作(加写锁),会阻塞其他进程读同一表的读和写操作,只有当写锁释放后,才会执行其他进程的读写操作
+ 简而言之,就是读锁会阻塞写,但不阻塞读操作。而写操作则会把读和写都阻塞


##### 表锁分析


* 看看哪些表被加锁了

mysql复制代码show open tables;

1
* 如何分析表锁定

mysql复制代码show status like ‘table%’;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

通过检查table\_locks\_waited 和 table\_locks\_immediate 状态变量来分析系统上的表锁定


![image-20200910155628663](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/12c5bca477a8e9b9d98ae8942cbec454721ae305810f48a0db30b245b463ba75)
* 这里有两个状态变更记录MySQL内部表级锁定的情况,两个变量说明如下:


+ `Table_locks_immediate`:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1
+ `Table_locks_waited`:出现表级锁定争用而发生等待次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在着较严重的表级锁争用情况
* 此外,MyISAM的读写锁调度是写优先,**这也是MyISAM不适合做写为主的表的引擎**,因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成**永久阻塞**


#### 行锁(偏写)


##### 特点


* 偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
* InnoDB与MyISAM的最大不同有两点:
+ 一是**支持事务**(TRANSACTION)
+ 二是采用了**行级锁**


##### 复习老知识点


* 事务及其ACID属性


+ 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
+ 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的
+ 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然
+ 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持
* 并发事务处理带来的问题


+ 更新丢失(Lost Update)
- 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新
+ 脏读(Dirty Reads)
- 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”
- 一句话:事务A读取到了事务B已修改但尚未提交的的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求
+ 不可重复读(Non-Repeatable Reads)
- 在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复
- 句话:一个事务范围内两个**相同的查询**却返回了**不同数据**
+ 幻读(Phantom Reads)
- 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”
- 一句话:事务A 读取到了事务B提交的新增数据,不符合隔离性
* 事务隔离级别


+ 脏读”、“不可重复读”和“幻读”,其实都是数据库**读一致性**问题,必须由数据库提供一定的**事务隔离机制**来解决
+ | 读数据一致性及允许的并发副作用隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
| --- | --- | --- | --- | --- |
| 未提交读(Read uncommitted) | 最低级别,只能保证物理上损坏的数据 | 是 | 是 | 是 |
| 以提交读(Read committed) | 语句级 | 否 | 是 | 是 |
| 可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 |
| 可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
+ 数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力
+ **差看当前数据库的事务隔离级别**:show variables like 'tx\_isolation';


##### 案例分析


* 建表SQL

mysql复制代码
create table test_innodb_lock (a int(11),b varchar(16))engine=innodb;

insert into test_innodb_lock values(1,’b2’);
insert into test_innodb_lock values(3,’3’);
insert into test_innodb_lock values(4,’4000’);
insert into test_innodb_lock values(5,’5000’);
insert into test_innodb_lock values(6,’6000’);
insert into test_innodb_lock values(7,’7000’);
insert into test_innodb_lock values(8,’8000’);
insert into test_innodb_lock values(9,’9000’);
insert into test_innodb_lock values(1,’b1’);

create index test_innodb_a_ind on test_innodb_lock(a);

create index test_innodb_lock_b_ind on test_innodb_lock(b);

select * from test_innodb_lock;

1
2
3
4
5
6

![image-20200910163316885](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/0b74021f2c17a791b27d77ea233d1b0235518c54bda4f14b4ad2af7e57278ed2)
* 行锁定基本演示


+ 关闭自动提交 session1、session2
mysql复制代码set autocommit=0;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
	
![image-20200910163643444](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/b2e85495aca6012ca29ce6809f77a512c1b5d95d81bae5a54fdf7e5880dd9f6b)
+ 正常情况,各自**锁定各自的行**,互相不影响,一个2000另一个3000
* **无索引行锁升级为表锁**


+ 正常情况,各自锁定**各自的行**,**互相不影响**
+ 由于在column字段b上面建了索引,如果没有正常使用,会导致行锁变表锁
- 比如没加单引号导致索引失效,行锁变表锁
- 被阻塞,等待。只到Session\_1提交后才阻塞解除,完成更新




| session1 | session2 |
| --- | --- |
| 更新session1中的一条记录,未手动commit,故意写错b的类型image-20200911092824894 | |
| session1,commit提交image-20200911093141141 | 更新session2,阻塞等待锁释放image-20200911093043823 |
| | session2完成update |


* 间隙锁危害
+ 什么是间隙锁
- 当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(GAP Lock)
+ 危害
- 因为Query执行过程中通过过范围查找的话,他会**锁定整个范围内所有的索引键值,即使这个键值并不存在**
- 间隙锁有一个比较**致命的弱点**,就是当锁定一个范围键值之后,即使某些**不存在**的键值**也会被无辜的锁定**,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害


##### 面试题


* 常考如何锁定某一行


![image-20200911094557313](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/eb619ec45615f027d9594ac325c38a43b72629498cfcede62716e788662e9fd7)


##### 案例结论


* Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,Innodb的整体性能和MyISAM相比就会有比较明显的优势了
* 但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MyISAM高,甚至可能会更差


##### 行锁分析


* 如何分析行锁定


+ 通过检查InnoDB\_row\_lock状态变量来分析系统上的行锁的争夺情况
mysql复制代码show status like 'innodb_row_lock%';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	
![image-20200911095119419](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/e0a0135ee41bb7bbc316fcd6430456970c041ca4d20b25ad3e206a1488851452)
* 各个状态量说明


+ Innodb\_row\_lock\_current\_waits:当前正在等待锁定的数量
+ Innodb\_row\_lock\_time:从系统启动到现在锁定总时间长度
+ Innodb\_row\_lock\_time\_avg:每次等待所花平均时间
+ Innodb\_row\_lock\_time\_max:从系统启动到现在等待最常的一次所花的时间
+ Innodb\_row\_lock\_waits:系统启动后到现在总共等待的次数
* 对于这5个状态变量,**比较重要的主要是**:


+ **Innodb\_row\_lock\_time\_avg(等待平均时长)**
+ **Innodb\_row\_lock\_waits(等待总次数)**
+ **Innodb\_row\_lock\_time(等待总时长)**
+ **尤其是当等待次数很高**,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划
* **查询正在被锁阻塞的sql语句**

mysql复制代码SELECT * FROM information_schema.INNODB_TRX\G;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

![image-20200911095622273](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/4d550556bc4b9cad6a13af1b1cf68b838b121cb615e8270cc11b9a9cff475e20)


##### 优化建议


* 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
* 尽可能较少检索条件,避免间隙锁
* 尽量控制事务大小,减少锁定资源量和时间长度
* 锁住某行后,尽量不要去调别的行或表,赶紧处理被锁住的行然后释放掉锁
* 涉及相同表的事务,对于调用表的顺序尽量保持一致
* 在业务环境允许的情况下,尽可能低级别事务隔离


#### 页锁


* 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
* 了解一下即可,目前使用较少


主从复制
----


### 复制的基本原理


#### slave会从master读取binlong来进行数据同步


#### 三步骤+原理图


![image-20200911100510799](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/a69acd9503397eb0f4867857645ad6bfb70f5365f934275bd04192a3d3c0f728)


MySQL复制过程分成三步:


1. master将改变记录到**二进制日志**(binary log),这些记录过程叫做二进制日志事件,binary log events
2. slave将master的binary log events拷贝到它的**中继日志**(relay log)
3. slave重做中继日志中的事件,将改变应用到自己的数据库中,MySQL复制是**异步的且串行化的**


### 复制的基本原则


#### 每个slave只有一个Master


#### 每个slave只能有一个唯一的服务器ID


#### 每个Master可以有多个Slave


### 复制的最大问题


#### 延时


### 一主一从常见配置


#### 主从配置


* 1.mysql版本一致且后台以服务运行 (同5.7,本机win和虚拟机centos)
* 2.主从都配置在[mysqld]结点下,都是小写
* 3.主机(win)修改my.ini配置文件


+ windows+r --- services.msc 找到mysql服务 右击属性查看配置文件`my.ini`的位置


![image-20200911114926051](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/9aca15353b7106a91bdde9ed57bb38dc734841d76c60a565549baf36149463b6)
+ 1.[必须]主服务器唯一ID


- server-id=1
+ 2.[必须]启用二进制日志


- log-bin=自己本地的路径/data/mysqlbin
- log-bin=D:\Mysql\mysql-5.7.28-winx64\mysql-5.7.28-winx64\data\mysqlbin
+ 3.[可选]启用错误日志


- log-err=D:\Mysql\mysql-5.7.28-winx64\mysql-5.7.28-winx64\data\mysqlerr
+ 4.[可选]根目录


- basedir=D:\Mysql\mysql-5.7.28-winx64\mysql-5.7.28-winx64
+ 5.[可选]临时目录


- tmpdir=D:\Mysql\mysql-5.7.28-winx64\mysql-5.7.28-winx64
+ 6.[可选]数据目录


- datadir=D:\Mysql\mysql-5.7.28-winx64\mysql-5.7.28-winx64\data
+ 7.read-only=0


- 主机,读写都可以
+ 8.[可选]设置不要复制的数据库


- binlog-ignore-db=mysql
+ 9.[可选]设置需要复制的数据库


- binlog-do-db=需要复制的主数据库名字![image-20200911131817659](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/30d7adb5ad8f5b248f45701a87ad8f2f08db203cca001fdde6019b0510b56b22)
+ 10.配置完成后重启mysql服务
* 4.从机修改my.cnf配置文件


+ [必须]从服务器唯一ID
+ [可选]启用二进制日志
+ vim /etc/my.cnf


![image-20200911105241412](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/732935b1966d6a629d0c7e5926d47882318c6310d93e1793875171987b6427ca)
+ 重启mysql服务
shell复制代码service mysqld restart

1
2
3
4
5
6
7
8
9
* 5.主机从机都关闭防火墙


+ windows手动关闭
+ 关闭虚拟机linux防火墙 service iptables stop
* 6.在Windows主机上建立帐户并授权slave


+ 授权命令
mysql复制代码GRANT REPLICATION SLAVE ON *.* TO 'zhangsan'@'从机器数据库IP' IDENTIFIED BY '123456';
1
	
mysql复制代码GRANT REPLICATION SLAVE ON *.* TO 'root'@'192.168.83.133' IDENTIFIED BY '123456';
1
2
3
4
5
6
7
8
9
10

![image-20200911110354863](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/1686bb89df4b14236c26092cfa7095e9733663efd5ab95c49d7ed573f5b60cc4)
+ flush privileges; (刷新)


![image-20200911110611897](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/7008d6704bf92922b78786bc1970f311751d7f994e3f7b9615c851bd903be2bd)
+ 查询master的状态


-
mysql复制代码show master status;
1
2
3
4
5
6
7
8
9
10
		
![image-20200911135427088](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/e95c9631ccb901006948439220d5201b7969f416d9af3b998aa760b20a363634)


记下File 和 Position 的值
+ 执行完此步骤后不要再操作主服务器MYSQL,防止主服务器状态值变化
* 7.在Linux从机上配置需要复制的主机


+ 从机命令(如果之前同步过,先停止(stop slave;)再次授权)
mysql复制代码CHANGE MASTER TO MASTER_HOST='192.168.1.8', MASTER_USER='root', MASTER_PASSWORD='123456', #MASTER_LOG_FILE='mysqlbin.具体数字',MASTER_LOG_POS=具体值; MASTER_LOG_FILE='mysqlbin.000002',MASTER_LOG_POS=154;
1
2
3

![image-20200911135453266](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5888591c5398f3574f051a307c4000722f85854c0d8636cf65c7504a91f83e2d)
+ 启动从服务器复制功能
mysql复制代码start slave;
1
+ 查看slave状态
mysql复制代码show slave status\G;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
	
![image-20200911135552224](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/c9dac6bd2841be76b9ef3e2ac8cd42f46b97003f77a4f45ee78e43689d7ab8b9)`Slave_IO_Running: Yes`


`Slave_SQL_Running:Yes`


两个参数都是Yes,说明主从配置成功!


#### 测试主从效果


* 主机新建库、新建表、insert记录,从机复制

mysql复制代码#建库
create database mydb77;
#建表
create table touchair(id int not null,name varchar(20));
#插入数据
insert into touchair valies(1,’a’);
insert into touchair values(2,’b’);

1
2
3
4
5

![image-20200911140232776](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/c04c2025cb6a36078bdede9b52ab2a0a289712f1b3a44214348e635c95efee74)


+ 从机上查看是否有库、表、数据
mysql复制代码use mydb77;
select * from touchair;

1
2
3
4
	
![image-20200911140341601](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/b66d6ac3d64109986f93f98fd0cf7a2a3a96f2e763f6628c7bc75b003443fd6b)
* 主从复制成功!
* 如何停止从服务复制功能

mysql复制代码stop slave;



**本文转载自:** [掘金](https://juejin.cn/post/6921495541897510919)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*
1…729730731…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%