0%

本章是整理知识内容,为强化知识长期更新。

Mysql基础架构图

mysql基础架构图

简单介绍上图相关模块功能

  • 连接器:管理连接,权限验证

  • 查询缓存:命中缓存则直接返回结果

  • 语法解析:词法分析,语法分析

  • 查询优化:执行计划生成,索引选择

  • 执行器:操作引擎,返回结果

  • 存储引擎:存储数据,提供读写结构。

连接器

我们需要连接到mysql服务端才能进行各种操作。

1
2
# 使用密码登录到时候我们会使用这种格式。
mysql -h$ip -P$port -u$user -p

执行上述命令会提示输入密码。输入完密码后就进入了mysql。

  • 如果用户名或者密码输入错误,会提示一个Access denied for user到错误,然后客户端程序结束执行。

  • 如果用户名和密码认证通过,连接器会去权限表中查询该用户拥有的所有权限。此后该连接里面的权限逻辑判断都会依赖认证成功时候读取到的权限。

因此当修改用户的权限之后,如果被修改用户的在登录状态是不会修改已经连接的权限。需要让被修改权限用户从新登录。

  • 实际上连接的方式有多种,上面通过密码的方式只是其中的一种方式。当客户端与服务端mysql进程进程建立连接,服务器的进程就会创建一个单独的线程来专门处理与这个客户端的交互,当该客户断开与服务端连接时,服务端并不会马上销毁对应的交互线程,而是缓存起来。当另一个新的客户端连接进来的时候,在把这个缓存的线程分配给新的客户端。这样就避免的频繁创建和修改线程,节省系统的开销。但是连接线程多了也会影响服务端,所以也有默认的参数限制客户端的连接数量。
1
2
# 这里是查询服务端存在的连接。
SHOW PROCESSLIST;

我使用的阿里云的数据库,这里展示下结果集。这里需要注意,登录的账号需要有PROCESS的权限否则只能看到自己的连接信息(线程),我这里采用的root所以看到全部到。

连接完成后如果没有执行后续操作则 Command会显示Sleep的状态,长时间Sleep会导致连接自动断开。默认的时间是8小时,也就是参数 wait_timeout。当客户端连接断开后,若客户端再次发出请求就会提示一个

Lost connection to MySQL server during query,此时就只能重新建立连接了。

半双工
  • MySQL客户端/服务端通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。一旦一端开始发送消息,另一端要接收完整个消息才能响应它,所以我们无法也无须将一个消息切成小块独立发送,也没有办法进行流量控制。
  • 服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯,这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。
长连接与短连接
  • 在数据库里面,长连接是指数据库建立连接,如果客户端有持续的请求则使用同一个连接,短连接是指每次执行几次短的查询就断开连接,下次查询再次创建连接。由于创建的过程复杂,一般来说尽量减少创建连接的动作,尽可能使用长连接。特别是在开发的时候出现大伙突然连接不上测试库,此时就检查下连接是不是太多了。因为连接多了会导致内存消耗特别快。

    • mysql的执行过程中临时使用的内存管理是在连接线程对象里面,服务端断开的时候才会释放,为了避免内存消耗过大,每次执行较大的操作之后,可以通过mysql_reset_connection来重新初始化连接资源。这个操作是不需要重新做权限,只是恢复到创建的初始状态。
  • MySQL mysql_reset_connection 官方文档

查询缓存

一个不被建议使用的功能,在新版的8.0中已经被删除了。总结就是弊大于利。

  • 建立完成后,假设执行一次SELECT语句,执行逻辑就会到第二部查询缓存。mysql拿到一个查询之后,会先到缓存中寻找释放有完全对应的,因为之前执行的查询语句和结果集会直接被缓存起来,以Keys-value的形式。keys是查询语句-value是查询结果集。如果能命中这个key则直接返回value。咋一看挺有用的,但实际上查询缓存的实效的太频繁。并且keys的命中条件太不聪明了,如果两次查询语句在任何的字符上存在不通(空格、大小写)都不会缓存命中。为什么会频繁失效,因为如果对查询语句中的表进行更新,就会导致缓存失效。比如INSERT、UPDATE 、DELETE 、TRUNCATE TABLE 、ALTER TABLE 等等就会导致缓存失效。

语法解析

如果没有命中查询缓存,就要开始执行sql语句了。mysql需要做什么,因此需要对sql进行解析。MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。

  • 本质上发送过来的是一个文本信息,这里就需要对该文本信息进行编译,涉及到词法解析语法解析语义解析等阶段。需要注意的是,这里会对sql语句进行一些检查,比如设计的相关表、表字段。这里检查通过的才会进入查询优化模块。
  • 根据SQL语言的功能可以划分成4个部分
    • DDL,英文叫做 Data Definition Language,也就是数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用 DDL,我们可以创建,删除和修改数据库和表结构。
    • DML,英文叫做 Data Manipulation Language,数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。
    • DCL,英文叫做 Data Control Language,数据控制语言,我们用它来定义访问权限和安全级别。
    • DQL,英文叫做 Data Query Language,数据查询语言,我们用它查询想要的记录,它是 SQL 语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。

查询优化

通过之前的语法解析,基本可以判定语法树是合法的,此时mysql就知道文本内容要做什么。

  • mysql会对查询语句进行优化,优化的结果就是生成一个执行计划,这个计划会表明使用了那些查询索引,表之间的连接是什么样子的。有时候出现多种执行方式,只是效率不通,优化器会决定使用哪一个执行方案,这个时候就涉及到EXPLAIN语句,该语句可以查看sql到执行计划,这将涉及到查询优化。
  • MySQL的查询优化器是一个非常复杂的部件,它使用了非常多的优化策略来生成一个最优的执行计划:
    • 重新定义表的关联顺序(多张表关联查询时,并不一定按照SQL中指定的顺序进行,但有一些技巧可以指定关联顺序)
    • 优化函数
    • 提前终止查询(比如:使用Limit时,查找到满足数量的结果集后会立即终止查询)
    • 优化排序(在老版本MySQL会使用两次传输排序,即先读取行指针和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于I/O密集型应用,效率会高很多。

执行器

进入到执行sql到环节。

  • 通过之前到查询优化,进入执行阶段,此时会先判断该连接用户是否有相关表的权限,如果没有会返回权限错误。如果有权限就会打开表,执行器会根据表定义的引擎去使用这个引擎提供的接口完成流程。

存储引擎

关于存储引擎,这里明确提醒。不是三种也不是四种。而是多种因为随时可能出现新的引擎。

  • 我们常用的就是InnoDBMyISAM,其它的不常见就不做过多介绍。mysql现在默认的引擎是InnoDB所以主要也是了解InnoDB
1
2
# 查看当前服务支持的存储引擎
SHOW ENGINES;

Read more »

本章是整理知识内容,为强化知识长期更新。

面向对象思想

  • 面向对象是一个思想,时间万物皆可以被看做一个对象。

封装

  • 隐藏对象的属性和实现的具体细节,只对外暴露公共访问方式。

继承

  • 当多个类出现相同代码逻辑时,我们通常将相同的代码重构到一个类中,如果是绑定关系就可以使用继承。
  • Java中类是单继承。

多态

  • 一个事物的的多种状态,比如女人、男人都是人的性别。人的性别就分为女人、男人。

抽象

  • 在逻辑上看似相关的,想要把他们联系起来。这样可以提高效率。矩形、圆形,都可以具有周长和面积两个方法,但是计算的方式完全不同,矩形和圆形之间肯定不能构成子父类的关系,那么只能是同时去继承一个父类。这时,就引出了抽象的概念。

重写和重载

  • 重载:同一个类中,方法名相同,参数个数或者类型不相同,返回类型可以不相同。
  • 重写:类的继承关系中体现,子类重写父类的方法。
区别点 重载方法 重写方法
发生范围 同一个类 子类
参数列表 必须修改 一定不能修改
返回类型 可修改 子类方法返回值类型应比父类方法返回值类型更小或相等
异常 可修改 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
访问修饰符 可修改 一定不能做更严格的限制(可以降低限制)
发生阶段 编译期 运行期
  • 重写(override)

    • 存在父类和子类之间。
    • 方法名、参数、返回值相同。
    • 方法被final修饰不能被重写。
    • 子类重写父类方法后,不能抛出比父类方法的异常。子类不能缩写父类的方法访问权限
  • 重载(overload)

    • 参数类型、个数、顺序至少有一个不相同。
    • 不能重载只有返回值不同的方法名。
    • 存在于父类和子类、同类中。
  • Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。

介绍下 Java 基本数据类型

  • Java中存在8个原生数据类型,同时又分成四种:整形、浮点型、char、Boolean。它们之间存在自动类型转换,规则是从小到大。并且都存在自动装箱拆箱特性,但是这种操作是隐式操作而且在某些情况会导致CG压力增大。
类型 存储需求 取值范围
int 4字节 -2 147 483 638 ~ 2 147 483 637
short 2字节 -32 768 ~ 32 767
long 8字节 -9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807
byte 1字节 - 128 ~ 127
  • 整型的范围与运行Java运行的硬件没有关系,所有的数据类型所占的字节数量与平台无关。
类型 存储需求 取值范围
float 4字节 大约 $\pm$ 3.402 823 37F + 38F (有效位数为7~8位)
double 8字节 大约 $\pm$ 1.797 693 134 862 315 70E + 308 (有效位数为15位)
  • double这种类型的精度是float的两倍。

  • 所有浮点数值计算都遵循IEEE 754规范,下面是溢出和出错的情况的三种特殊的浮点数值。

    • 正无穷大
    • 负无穷大
    • NaN ( 不是一个数字 )
    • 一个整整数除以0的结果为正无穷大,计算0/0或者负数的平方根结果为NaN。
  • char类型

    • char类型表示单个字符,属于Unicode编码表。因为历史原因,不建议在程序中使用。除非确实要对UTF-16代码单元进行操作。
    • char字节大小
      • Java中无论是汉字还是英文字母都是用Unicode编码来表示的,一个Unicode是16位,每字节是8位,所以一个Unicode码占两字节。但是英文字母比较特殊,源自于8位(1字节)的ASCII吗,于是在Unicode码仅使用了低8位(1字节)就可以表示。
  • boolean类型

    • 布尔类型,只有两个值false、true。基本用于判定条件。
    • boolean字节大小
      • Java规范中并未明确规定boolean类型的大小。
  • 自动类型转换

    • 整型、实型(常量)、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算。

      转换从低级到高级。

      1
      byte,short,char—> int —> long—> float —> double
    • 不能对boolean进行类型转换、不能把对象类型转换成不相关的对象、把大容量的对象转换成小容量对象时需要强制类型转换、转换过程中间可能出现精度损失。

装箱和拆箱boxing or unboxing

装箱:将基础类型用它们对应类型包装起来

拆箱:将包装类型转换成基本数据类型

原语 对应的JDK类
int java.lang.Integer
short java.lang.Short
long java.lang.Long
byte java.lang.Byte
char java.lang.Character
double java.lang.Double
float java.lang.Float
boolean java.lang.Boolean
  • Java中只有原生数据类型是特殊的,它们不是对象。其它的都是对象。那么就一个尴尬的问题,集合类都是存放的对象,JDK5之后考虑到这个问题就自动进行逆行拆箱装箱的操作。

    1
    2
    3
    //比如所在泛型中是不能存放原生数据类型的,如要要存放原生数据类型的数据,需要装箱。
    Collection<int> c = new ArrayList<int>(); //这是无法编译成功的。
    Collection<Integer> cc = new ArrayList<Integer>(); //这样才行。
  • 每个 JDK 类都提供了相应方法来解析它的内部表示,并将其转换为相应的原语类型。

  • 但是注意装箱拆箱操作其实是非常消耗内存的举动,在该过程中可能会生成生成无用对象增加GC压力。所以尽量避免这中操作。

    1
    2
    3
    4
    Integer sum = 0;
    for(int index = 1000; index < 5000; index ++){
    sum+=index;
    }

    比如这种,每次sum都需要自动拆箱。

  • 默认情况下整数的类型都是int、浮点型的数都是double。

    1
    float d = 1.1f; //在后面添加f,大小写不区分。隐式强制类型转换
Read more »

本章是整理知识内容,为强化知识长期更新。

Redis 简介

Redis 是完全开源免费的,遵守 BSD 协议,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

  • Redis 与 其他 key - value 缓存产品有以下三个特点:
    • Redis 支持数据持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
    • Redis 不仅仅支持简单的 key - value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储
    • Redis 支持数据的备份,即 master - slave 模式的数据备份。
  • Redis是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快,读的速度是 110000 次 /s, 写的速度是 81000 次 /s。另一方面是因为它的数据结构。键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础。
  • Redis使用基于哈西槽(slot)的数据划分方式。

Redis数据类型与数据结构

数据结构时间复杂度

名称 时间复杂度
哈希表 O(1)
跳表 O(logN)
双向链表 O(N)
压缩列表 O(N)
整数数组 O(N)

Redis单线程

  • 我们通常说,Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。Redis 的单线程设计机制以及多路复用机制
  • Linux 中的 IO 多路复用机制
    • Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

Redis单线程处理IO请求性能瓶颈

  • 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
    • 操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
    • 使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
    • 大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
    • 淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
    • AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
    • 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
  • 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
  • Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
  • Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。

Redis持久化机制

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。

  • Redis 4.0 之前数据持久化方式有两种:AOF 方式和 RDB 方式。

    • RDB(Redis DataBase,快照方式)是将某一个时刻的内存数据,以二进制的方式写入磁盘。RDB 默认的保存文件为 dump.rdb,优点是以二进制存储的,因此占用的空间更小、数据存储更紧凑,并且与 AOF 相比,RDB 具备更快的重启恢复能力。
    • AOF(Append Only File,文件追加方式)是指将所有的操作命令,以文本的形式追加到文件中。AOF 默认的保存文件为 appendonly.aof,它的优点是存储频率更高,因此丢失数据的风险就越低,并且 AOF 并不是以二进制存储的,所以它的存储信息更易懂。缺点是占用空间大,重启之后的数据恢复速度比较慢。
  • 将RDB做镜像全量持久化,AOF做增量持久化。RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。

    • 实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程内存释放。
    • RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
  • AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

AOF日志

AOF持久化机制是以日志的形式记录Redis中的每一次的增删改操作,不会记录查询操作,以文本的形式记录,打开记录的日志文件就可以查看操作记录。

  • AOF更好保证数据不会被丢失,最多只丢失一秒内的数据,通过foek一个子进程处理持久化操作,保证了主进程不会进程io操作,能高效的处理客户端的请求。
  • AOF的日志文件的记录可读性非常的高,即使某一时刻有人执行flushall清空了所有数据,只需要拿到aof的日志文件,然后把最后一条的flushall给删除掉,就可以恢复数据。

AOF重写机制

在写入所有的操作到日志文件中时,就会出现日志文件很多重复的操作,甚至是无效的操作,导致日志文件越来越大。就浪费了资源空间,所以在Redis中出现了rewrite机制。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

  • 重写AOF的日志文件不是读取旧的日志文件瘦身,而是将内存中的数据用命令的方式重写一个AOF文件,重新保存替换原来旧的日志文件,因此内存中的数据才是最新的。

  • 重写操作也会fork一个子进程来处理重写操作,重写以内存中的数据作为重写的源,避免了操作的冗余性,保证了数据的最新。

  • 写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志。

AOF 潜在的风险
  • 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。
  • AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
AOF三种写回策略
  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
    • 同步写回可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能。
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    • 每秒写回采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
    • 操作系统控制的写回在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了。

RDB快照

RDB持久化就是将当前进程的数据以生成快照的形式持久化到磁盘中。对于快照的理解,我们可以理解为将当前线程的数据以拍照的形式保存下来。

  • 恢复数据速度快
  • 写入持久化文件快
  • RDB持久化后的文件是紧凑的二进制文件,适合于备份、全量复制、大规模数据恢复的场景,对数据完整性和一致性要求不高,RDB会丢失最后一次快照的数据。
RDB原理
  • fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

  • Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。

  • 对于 Redis 而言,它的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作,所以,针对任何操作,我们都会提一个灵魂之问:“它会阻塞主线程吗?”RDB 文件的生成是否会阻塞主线程,这就关系到是否会降低 Redis 的性能。

  • Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。

    • save:在主线程中执行,会导致阻塞;
    • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

Redis启动持久化文件加载机制

Redis是基于内存的非关系型K-V数据库,它是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了持久化,即把数据保存到磁盘。

缓存带来的问题

缓存雪崩

  • 我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
  • 解决办法:
    • 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就是将缓存失效时间分散开。
    • 服务层增加熔断、降级、限流等操作,防止系统被流量攻溃。
    • 增加redis的监控措施,并采用高可用架构处理。

缓存穿透

  • 缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
  • 解决办法
  1. 缓存空对象:代码维护较简单,但是效果不好。
  2. 布隆过滤器:代码维护复杂,空间效率和查询时间都远远超过一般的算法,效果很好。有一定的误识别率,删除困难。
  3. 请求参数检验:防止恶意请求进入业务系统,将部分恶意请求拦截掉。
  • 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
  • 缓存空对象,一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
  • 布隆过滤器(推荐)
    • 就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
布隆过滤器

布隆过滤器是一种基于概率数据结构,主要用来判断某个元素是否在集合内,它具有运行速度快(时间效率),占用内存小的优点(空间效率),但是有一定的误识别率删除困难的问题。它只能告诉你某个元素一定不在集合内或可能在集合内。

  1. 一个非常大的二进制位数组 (数组里只有0和1)
  2. 若干个哈希函数
  3. 空间效率查询效率高
  4. 不存在漏报(False Negative):某个元素在某个集合中,肯定能报出来。
  5. 可能存在误报(False Positive):某个元素不在某个集合中,可能也被爆出来。
  6. 不提供删除方法,代码维护困难。
  7. 位数组初始化都为0,它不存元素的具体值,当元素经过哈希函数哈希后的值(也就是数组下标)对应的数组位置值改为1。
布隆过滤器案例

当查询一件商品的缓存信息,布隆过滤器只能精确判断数据不存在情况,对于存在我们只能说是可能,因为存在Hash冲突情况,当然这个概率非常低。

  1. 当查询一件商品的缓存信息
  2. 然后,在布隆数组中查找访问对应的位值,0或1
  3. 判断,三个值中,只要有一个不是1,那么我们认为数据是不存在的。
布隆过滤器删除问题

直接删除肯定不行,存在hash冲突,存在误删。

  • 定时任务,定期创建一个新的布隆过滤器。
如何减少布隆过滤器的误判
  • 增加二进制位数组的长度。这样经过hash后数据会更加的离散化,出现冲突的概率会大大降低。
  • 增加Hash的次数,变相的增加数据特征。特征数据越多,冲突的概率越小。

缓存击穿

缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。

  • 当用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了缓存击穿。即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁单机的话用普通的锁(synchronizedLock)就够了。
    • 使用互斥锁方案缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
    • 持续不过期,通过定时任务的方式同步数据到缓存中。防止热key数据过期。

缓存预热

  • 缓存预热这个应该是一个比较常见的概念,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存更新

  • 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
    • 定时去清理过期的缓存;
    • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
  • 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

缓存降级

  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
    • 以参考日志级别设置预案:
      • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
      • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
      • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
      • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
  • 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

数据一致性

缓存一致性问题作为日常开发长期存在的问题。

缓存常见的操作
  • 写入:缓存和数据库是两个不同的组件,只要涉及双写,就存在只有一个写成功的可能性,造成数据不一致。

  • 更新:更新的情况类似,需要更新两个不同的组件。

  • 读取:读取要保证从缓存中读到的信息是最新的,是和数据库中的是一致的。

  • 删除:当删除数据库记录的时候,如何把缓存中的数据也删掉?

同步直写
  • 写数据库的时候也写入缓存,通过代码检查机制使数据库和缓存数据统一。通常会采用事务、重试机制来保证数据。
延时双删
  • 先更新再删除,但是执行删除动作,在不久之后再执行一次,比如 1-5 秒之后。
集中更新
  • 弱化数据库,将数据优先存放到redis总,在通过定时器或扫描逻辑代码将数据同步到mysql中。
触发式加载

使用懒加载的方式,可以让缓存的同步变得非常简单

  • 当读取缓存的时候,如果缓存里没有相关数据,则执行相关的业务逻辑,构造缓存数据存入到缓存系统;

  • 当与缓存项相关的资源有变动,则先删除相应的缓存项,然后再对资源进行更新,这个时候,即使是资源更新失败,也是没有问题的。

缓存热key

什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。

  • 如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
    • Redis集群扩容:增加分片副本,均衡读流量;
    • 使用二级缓存,即JVM本地缓存,减少Redis的读请求。

Redis 的线程模型

  • redis 内部使⽤⽂件事件处理器file event handler,这个⽂件事件处理器是单线程的,所以redis 才叫做单线程的模型。它采⽤ IO 多路复⽤机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进⾏处理。

单线程的Redis为什么这么快

  • 存内存操作。
  • 单线程操作,避免平凡的上下文切换。
  • 采用了非阻塞I/O多路复用操作。(这里的I/O是指网络I/O)

Redis的过期策略以及内存淘汰机制

  • redis采用的是定期删除+惰性删除策略。

    • 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
  • 定时删除

    • 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 内存淘汰策略

当Redis内存快耗尽时,Redis会启动内存淘汰机制,将部分key清掉以腾出内存。

  • 主要有5种处理机制
    • LRU 最近最少使用
    • LFU 最近使用频率低
    • Random 随机淘汰
    • TTL 过期时间
    • No-Enviction 驱逐(什么都不做直接返回错误)
  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
  • volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。
  • allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰。
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
  • no-enviction(驱逐):禁止驱逐数据,新写入操作会报错。
  • 如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

Redis 的过期策略和内存淘汰机制有什么区别?

  • 新增 Redis 缓存时可以设置缓存的过期时间,该时间保证了数据在规定的时间内失效。通过这种方式可以完成某些场景的业务开发。对对于已经过期的数据,Redis 将使用两种策略来删除这些过期键,它们分别是惰性删除和定期删除。

  • 惰性删除是指 Redis 服务器不主动删除过期的键值,而是当访问键值时,再检查当前的键值是否过期,如果过期则执行删除并返回 null 给客户端;如果没过期则正常返回值信息给客户端。

  • 定期删除是指 Redis 服务器每隔一段时间会检查一下数据库,看看是否有过期键可以被清除。默认情况下 Redis 定期检查的频率是每秒扫描 10 次,用于定期清除过期键。当然此值还可以通过配置文件进行设置,在 redis.conf 中修改配置“hz”即可,默认的值为“hz 10”。Redis 服务器为了保证过期删除策略不会导致线程卡死,会给过期扫描增加了最大执行时间为 25ms。

  • 当 Redis 的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,这和过期策略是完全不同的两个概念,经常有人把二者搞混,这两者一个是在正常情况下清除过期键,一个是在非正常情况下为了保证 Redis 顺利运行的保护策略。

Read more »

本章是整理知识内容,为强化知识长期更新。

JVM的内存布局

  • 堆(Java Heap) 也叫 Java 堆或者是 GC 堆,它是一个线程共享的内存区域,也是 JVM 中占用内存最大的一块区域,Java 中所有的对象都存储在这里。

    • 存储的是我们new来的对象,不存放基本类型和对象引用。

    • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。

    • 线程共享区域,因此是线程不安全的。

    • 能够发生内存溢出,主要有OutOfMemoryError和StackOverflowError。

    • 那么什么时候发生OutOfMemoryError,什么时候发生StackOverflowError?虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常,线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常

    • Java堆区还可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。具体比例参数的话,可以看一下下面这张图。

方法区

  • 方法区(Method Area) 也被称为非堆区,用于和“Java 堆”的概念进行区分,它也是线程共享的内存区域,用于存储已经被 JVM 加载的类型信息、常量、静态变量、代码缓存等数据。
程序计数器
  • 程序计数器(Program Counter Register) 线程独有一块很小的内存区域,保存当前线程所执行字节码的位置,包括正在执行的指令、跳转、分支、循环、异常处理等。
虚拟机栈
  • 虚拟机栈也叫 Java 虚拟机栈(Java Virtual Machine Stack),和程序计数器相同它也是线程独享的,用来描述 Java 方法的执行,在每个方法被执行时就会同步创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。当调用方法时执行入栈,而方法返回时执行出栈。

    • 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。

    • 存放基本数据类型以及对象的引用。

    • 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。方法栈帧是以先进后出的方式虚拟机栈的。

    • 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。

      • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
本地方法栈
  • 本地方法栈(Native Method Stacks)与虚拟机栈类似,它是线程独享的,并且作用也和虚拟机栈类似。只不过虚拟机栈是为虚拟机中执行的 Java 方法服务的,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

  • JVM 的执行流程是,首先先把 Java 代码(.java)转化成字节码(.class),然后通过类加载器将字节码加载到内存中,所谓的内存也就是我们上面介绍的运行时数据区,但字节码并不是可以直接交给操作系统执行的机器码,而是一套 JVM 的指令集。这个时候需要使用特定的命令解析器也就是我们俗称的执行引擎(Execution Engine)将字节码翻译成可以被底层操作系统执行的指令再去执行,这样就实现了整个 Java 程序的运行,这也是 JVM 的整体执行流程。

类的加载机制

符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

  • 类的生命周期会经历以下 7 个阶段:

  1. 加载阶段(Loading)
  2. 验证阶段(Verification)
  3. 准备阶段(Preparation)
  4. 解析阶段(Resolution)
  5. 初始化阶段(Initialization)
  6. 使用阶段(Using)
  7. 卸载阶段(Unloading)

加载阶段

此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 java.lang.Class 对象,作为其他数据访问的入口。

  • Java语言的类型可以分为两大类:基本类型、引用类型。基本类型是由虚拟机预先定义好的,所以不会经历单独的类加载过程。而引用类型又分为四种:类、接口、数组类、泛型参数。由于泛型参数会在编译的过程中被擦除(关于类型擦除的知识,大家可以查下资料),所以在Java中只有类、接口、数组类三种类型需要经历JVM对其进行连接和初始化的过程。

验证阶段

此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。

  • 验证的主要动作大概有以下几个:
    • 文件格式校验包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等;
    • 元数据校验包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等;
    • 字节码校验,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑;
    • 符号引用校验,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。

准备阶段

此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。这些变量所使用的内存都将在方法区(<Jdk1.8)元数据区(>=Jdk1.8)中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化的时候随对象一起分配在Java堆中。

解析阶段

此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。

  • 所谓的符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

  • 符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

初始化

初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。

  1. JVM 规范枚举了下述多种触发情况:
    1. 当虚拟机启动时,初始化用户指定的主类;
    2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
    3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
    4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;子类的初始化会触发父类的初始化;
    5. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
    6. 使用反射 API 对某个类进行反射调用时,初始化这个类;
    7. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
Read more »

本章是整理知识内容,为强化知识长期更新。

整体架构

基础层

处理层

接口层

Mybatis执行流程

  • 获取sqlSessionFactory对象:解析文件的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession;注意:MappedStatement:代表一个增删改查的详细信息。
  • 获取sqlSession对象,返回一个DefaultSQlSession对象,包含Executor和Configuration;这一步会创建Executor对象;
  • 获取接口的代理对象(MapperProxy),getMapper,使用MapperProxyFactory创建一个MapperProxy的代理对象。

Mybatis拦截器

代理对象里面包含了,DefaultSqlSession(Executor)

  • 执行增删改查方法。
    • 调用DefaultSqlSession的增删改查(Executor)
    • 创建一个StatementHandler对象、且同时创建出ParameterHandler和ResultSetHandler。
    • 调用StatementHandler预编译参数以及设置参数值;使用ParameterHandler来给sql设置参数
    • 调用StatementHandler的增删改查方法;
    • ResultSetHandler封装结果
  • MyBatis 拦截签名 拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性
    • type: 拦截器需要拦截的接口,有 4 个可选项,分别是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
    • method: 拦截器所拦截接口中的方法名,也就是前面四个接口中的方法名,接口和方法要对应上。
    • args: 拦截器所拦截方法的参数类型,通过方法名和参数类型可以锁定唯一一个方法。
  • 被拦截的对象
    • org.apache.ibatis.executor.Executor
    • org.apache.ibatis.executor.statement.StatementHandler
    • org.apache.ibatis.executor.statement.ParameterHandler
    • org.apache.ibatis.executor.resultset.ResultSetHandler
  • Executor
    • update:该方法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦截这些操作,可以通过该方法实现。
    • query:该方法会在 SELECT 查询方法执行时被调用,方法参数携带了很多有用的信息,如果需要获取,可以通过该方法实现。
    • queryCursor:当 SELECT 的返回类型是 Cursor 时,该方法会被调用。
    • flushStatements:当 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有 @Flush 注解时该方法会被触发。
    • commit:当 SqlSession 方法调用 commit 方法时该方法会被触发。
    • rollback:当 SqlSession 方法调用 rollback 方法时该方法会被触发。
    • getTransaction:当 SqlSession 方法获取数据库连接时该方法会被触发。
    • close:该方法在懒加载获取新的 Executor 后会被触发。
    • isClosed:该方法在懒加载执行查询前会被触发。
  • StatementHandler
    • prepare:该方法在数据库执行前被触发。
    • parameterize:该方法在 prepare 方法之后执行,用来处理参数信息。
    • batch:如果 MyBatis 的全剧配置中配置了 defaultExecutorType=”BATCH”,执行数据操作时该方法会被调用。
    • update:更新操作时该方法会被触发。
    • query:该方法在 SELECT 方法执行时会被触发。
    • queryCursor:该方法在 SELECT 方法执行时,并且返回值为 Cursor 时会被触发。
  • ParameterHandler
    • getParameterObject:在执行存储过程处理出参的时候该方法会被触发。
    • setParameters:设置 SQL 参数时该方法会被触发。
  • ResultSetHandler
    • handleResultSets:该方法会在所有的查询方法中被触发(除去返回值类型为 Cursor 的查询方法),一般来说,如果我们想对查询结果进行二次处理,可以通过拦截该方法实现。
    • handleCursorResultSets:当查询方法的返回值类型为 Cursor 时,该方法会被触发。
    • handleOutputParameters:使用存储过程处理出参的时候该方法会被调用。

MyBatis插件

MyBatis 将插件单独分离出一个模块,位于 org.apache.ibatis.plugin 包中,在该模块中主要使用了两种设计模式:代理模式和责任链模式。

  • 插件接口

    • org.apache.ibatis.plugin.Interceptor

    • intercept:它将直接覆盖你所拦截的对象,有个参数Invocation对象,通过该对象,可以反射调度原来对象的方法;

    • plugin:target是被拦截的对象,它的作用是给被拦截对象生成一个代理对象;

    • setProperties:允许在plugin元素中配置所需参数,该方法在插件初始化的时候会被调用一次;

  • MyBatis允许我们自定义 Interceptor 拦截 SQL 语句执行过程中的某些关键逻辑,允许拦截的方法有:Executor 类中的 update()、query()、flushStatements()、commit()、rollback()、getTransaction()、close()、isClosed()方法,ParameterHandler 中的 setParameters()、getParameterObject() 方法,ResultSetHandler中的 handleOutputParameters()、handleResultSets()方法,以及StatementHandler 中的parameterize()、prepare()、batch()、update()、query()方法。

MyBatis中的设计模式

  • 工厂模式
    • 工厂模式在 MyBatis 中的典型代表是 SqlSessionFactory。
    • SqlSession 是 MyBatis 中的重要 Java 接口,可以通过该接口来执行 SQL 命令、获取映射器示例和管理事务,而 SqlSessionFactory 正是用来产生 SqlSession 对象的,所以它在 MyBatis 中是比较核心的接口之一。
    • 工厂模式应用解析:SqlSessionFactory 是一个接口类,它的子类 DefaultSqlSessionFactory 有一个 openSession(ExecutorType execType) 的方法,其中使用了工厂模式。
  • 建造者模式
    • MyBatis 中的典型代表是 SqlSessionFactoryBuilder。
    • 普通的对象都是通过 new 关键字直接创建的,但是如果创建对象需要的构造参数很多,且不能保证每个参数都是正确的或者不能一次性得到构建所需的所有参数,那么就需要将构建逻辑从对象本身抽离出来,让对象只关注功能,把构建交给构建类,这样可以简化对象的构建,也可以达到分步构建对象的目的,而 SqlSessionFactoryBuilder 的构建过程正是如此。
    • 在 SqlSessionFactoryBuilder 中构建 SqlSessionFactory 对象的过程是这样的,首先需要通过 XMLConfigBuilder 对象读取并解析 XML 的配置文件,然后再将读取到的配置信息存入到 Configuration 类中,然后再通过 build 方法生成我们需要的 DefaultSqlSessionFactory 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
  • 单例模式
    • 单例模式在 MyBatis 中的典型代表是 ErrorContext。
    • 使用 private 修饰的 ThreadLocal 来保证每个线程拥有一个 ErrorContext 对象,在调用 instance() 方法时再从 ThreadLocal 中获取此单例对象。
1
2
3
4
5
6
7
8
9
10
public class ErrorContext {

private static final String LINE_SEPARATOR = System.lineSeparator();
// 每个线程存储的容器
private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);

public static ErrorContext instance() {
return LOCAL.get();
}
}
  • 适配器模式

    • MyBatis 中的典型代表是 Log。
      • SLF4J
      • Apache Commons Logging
      • Log4j 2
      • Log4j
      • JDK logging
  • 代理模式

    • 代理模式在 MyBatis 中的典型代表是 MapperProxyFactory。
  • 模版方法模式

    • 模板方法在 MyBatis 中的典型代表是 BaseExecutor。
  • 装饰器模式

    • 装饰器模式在 MyBatis 中的典型代表是 Cache。

Mybatis一些疑问

#{} 于 ${}

使用#{}可以有效的防止SQL注入,提高系统安全性。

  • #{}是预编译,${}是字符串替换。
    • Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
    • Mybatis在处理${}时,就是把${}替换成变量的值。

MyBatis Dao 接口的工作原理

  • Dao 接口的全限定名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值,接口方法内的参数,就是传递给 SQL 的参数。Mapper 接口是没有实现类的,当调用接口方法时,接口全限定名 + 方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 MyBatis 中,每一个 select、insert、update、delete 标签,都会被解析为一个 MapperStatement 对象。

Dao 接口里的方法可以重载吗

  • Mapper 接口的工作原理是 JDK 动态代理,MyBatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 SQL,然后将 SQL 执行结果返回。所以是不能重载的

本章是整理知识内容,为强化知识长期更新。

Java集合

​ 集合是Java提供的工具包、包含常用的数据结构:集合、链表、队列、栈、数组、映射等。Collection的包是java.util.*。

Collection

Map

实际上还有Cloneable、Serializable接口是都集合类需要实现的,通用所以不不画上去了。

Java集合主要划分4个部分:

  • List(列队)
  • Set(集合)
  • Map(映射)
  • 工具类(Iterator迭代器、Enumeration枚举类、Arrays、Collections)

划重点

  • Conllection
    • List
      • ArrayList
      • Vector
        • Stack
      • LinkedList
    • Set
      • HashSet
      • TreeSet
      • LinkedHashSet
    • Queue
  • Map
    • HashMap
    • HashTable
    • TreeMap
  • 工具
    • Arrays
    • Collections
    • Enumeration

Java类库中具体集合

集合类型 概括
ArrayList 一种可以动态增长和缩减的索引序列,访问速度很快但是插入和删除比ArrayList慢。
LinkedList 一种可以在任何位置进行高效地插入和删除操作的有序序列,但是访问比较ArrayList慢。
CopyOnWriteArrayList CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口,支持高并发。
ArrayDeque 一种循环数组实现的双端序列。
HashSet 一种没有重复元素的无序集合。
TreeSet 一种有序集合。
EnumSet<E extends Enum> 一种包含枚举类型的集合。
LinkedHashSet 一种可以记住元素插入顺序的集合。
PriorityQueue 一种允许高效删除最小元素的集合。
HashMap<K , V> 一个存储 键 / 值 关联的数据结构。
TreeMap<K , V> 一种键值有序排列的映射表。
EnumMap<K extends Enum, V> 一种键属于枚举类型的映射表。
LinkedHashMap<K ,V > 一种可以记住键 / 值 项添加顺序的映射表。
WeakHashMap<K , V > 一种其值无用武之地后可以被垃圾回收回收的映射表。
IdentityHashMap<K , V> 一种用 == 而不是用用 equals 比较的映射表。
Read more »

本章是整理知识内容,为强化知识长期更新。

Git速度查询表

下面是常用 的Git 命令清单。几个专用名词的译名如下:

  • Workspace:工作区
  • Index / Stage:暂存区
  • Repository:仓库区(或本地仓库)
  • Remote:远程仓库

最小配置

为什么要最小配置,每次提交代码需要告诉git。所以需要简单设置下user信息。

1
2
git config --global user.name 'your name'
git config --global user.email 'your email'

这里需要注意config的三个作用域

1
2
3
git config --local  #某个git本地仓库有效
git config --global #当前用户所有仓库有效,就是你系统的登陆用户。
git config --system #对系统所有的仓库都有效果。

我们一般只会使用local 和 global这两个配置。当我们配置好user.name user.email之后,我们可以检查下配置信息。

1
2
3
git config --list --local # 注意,必须在某个仓库里面才能看到。
git config --list --global
git config --list --system

创建一个本地仓库

一般来说git仓库有两种情况,一种是没有仓库,另外一种就是已经有仓库了。这里对第一种情况演示,因为工作中习惯在web上创建仓库。这里还是了解下。

1
git init git_learning

创建一个README文件。

1
touch README.md

这里需要说明下,本地仓库创建完成后需要与远程仓库进行管理。

1
git remote add origin github.com:yourname/git_learning.git # 这里的yourname 是你的git账号

建议手动去github上面创建一个叫git_learning的仓库。上面的命令就是做下关联。

1
2
3
git add . # 将当前文件添加至暂存区
git commit -m "upload file" # 将暂存区的文件提交到本地仓库
git push origin mastet # 将本地仓库的改动信息推送到远程仓库中

可控一个远程仓库

1
2
# 克隆一个仓库
git clone www.**/**.git

文件追踪

有些时候不想提交部分文件,但是git add .的时候添加进去了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 添加文件到暂存区
git add [file] [file]

## 添加文件夹暂存区
git add [dir]

## 添加当前目录所有文件暂存区
git add .

## 如果是对所有文件都取消跟踪的话,就是
git rm -r --cached .    ## 不删除本地文件
git rm -r --f .    ## 删除本地文件

##对某个文件取消跟踪
git rm --cached readme1.txt ## 删除readme1.txt的跟踪,并保留在本地。
git rm --f readme1.txt ## 删除readme1.txt的跟踪,并且删除本地文件s

提交仓库

如果发现message写错了,可以修改下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 修改最新一次commit提交message。
git commit --amend

# 提交暂存区的指定文件到仓库区
git commit [file1] [file2] ... -m [message]

# 将本地暂存区的文件推送到本地仓库。 当修改已经通过`git add <change file>`将其添加到`stage`,可以通过`git commit -m "<message>"`为这所有已经进入`stage`的改变添加一个`commit`信息。
git commit -m

#可以直接使用`git commit -am "<message>"`,将所有修改,但未进`stage`的改动加入`stage`,并记录`commit`信息。(某种程度上相当于`git add`和`git commit -m`的组合技,前提是被改动文件已经是`tracked`)
git commit -am

#这个命令经常会出现在本地仓库与远程仓库发生冲突的时候,需要强制推送更新。
git push -f origin master

# 推送删除远程分支操作
git push origin : xxxx

# 推送删除远程分支操作
git push origin --delete xxxx
  • 如果想修改以往的commit的message。注意是不连续的commit。
    • 首先查看log 里面的信息,然后获取commitid进行合并
Read more »

本章是整理知识内容,为强化知识长期更新。

Spring

  • Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。

  • 主要由以下几个模块组成:

    • Spring Core:核心类库,提供IOC服务
    • Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等)
    • Spring AOP:AOP服务切面编程
    • Spring DAO:对JDBC的抽象,简化了数据访问异常的处理
    • Spring ORM:对现有的ORM框架的支持
    • Spring Web:提供了基本的面向Web的综合特性,例如多方文件上传
    • Spring MVC:提供面向Web应用的Model-View-Controller实现
  • IoC(Inversion of Control,翻译为“控制反转”)不是一个具体的技术,而是一种设计思想。与传统控制流相比,IoC 会颠倒控制流,在传统的编程中需要开发者自行创建并销毁对象,而在 IoC 中会把这些操作交给框架来处理,这样开发者就不用关注具体的实现细节了,拿来直接用就可以了,这就是控制反转。

  • IoC 很好的体现出了面向对象的设计法则之一——好莱坞法则:“别找我们,我们找你”。即由 IoC 容器帮对象找到相应的依赖对象并注入,而不是由对象主动去找。

  • DI(Dependency Injection,翻译为“依赖注入”)表示组件间的依赖关系交由容器在运行期自动生成,也就是说,由容器动态的将某个依赖关系注入到组件之中,这样就能提升组件的重用频率。通过依赖注入机制,我们只需要通过简单的配置,就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心资源来自哪里、由谁实现等问题。

  • IoC 和 DI 其实是同一个概念从不同角度的描述的,由于控制反转这个概念比较含糊(可能只理解成了容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以 2004 年被开发者尊称为“教父”的 Martin Fowler(世界顶级专家,敏捷开发方法的创始人之一)又给出了一个新的名字“依赖注入”,相对 IoC 而言,“依赖注入”明确描述了“被注入对象依赖 IoC 容器配置依赖对象”。

依赖注入

  • 平常的Java开发中,程序员在某个类中需要依赖其它类的方法。通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理。 Spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过Spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。依赖注入的另一种说法是”控制反转”。通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员。而控制反转是指new实例工作不由我们程序员来做而是交给Spring容器来做。
  • IOC对于Spring框架来说,就是由Spring来负责控制对象的生命周期和对象间的关系。
  • 所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中,所以,以来注入DI 和控制反转IOC 是从不同的角度描述的同一间事情,就是值通过引用IOC容器,利用以来关系注入的方式,实现对象之间的解耦。

注入方式

  • Set注入。
  • 构造方法注入。
  • 工厂的方法注入。
  • 通过属性注入。
  • 通过List注入。
  • 通过Map注入。

Spring AOP

  • AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

    • Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDK Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候Spring AOP会使⽤Cglib ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理。

    • Spring Aop是运行时增强。
    • Cglib Aop是编译器增强。
  • AOP专门用于处理系统中分布于各个模块中交叉关注点的问题,在JavaEE应用中,常常通过AOP来处理一些横切性质的系统级服务,如事物管理、安全检查、缓存对象池管理。

    • 1.)切面 -Aspect 切面类 比如日志类
    • 2.) 连接点 - Join Point 加入切点的那个点
    • 3.)通知 -Advice 是在切面某个特定的连接点上的动作。
    • 4.)切入点 - Point Cut 匹配连接点的断点
    • 5.)引入 - Introduction
    • 6.)目标对象 - Target Object 被一个或多个通知对象,永远被代理的对象。
    • 7.) AOP代理 - AOP Proxy AOP创建的对象用来实现切面对象。
    • 8.)织入 - Weaving
    • 9.)AOP通俗的理解:
      • 一个组建A,不关心其他常用的服务组件B,但是这个组件A使用组件B的时候不是组建A自身去调用,而是通过配置等其他方式,比如Spring中可以通过XML配置文件。这样就使的A压根不需要知道服务组件B是怎么样的,A只关心自己的业务逻辑,具体A使用B的时候,配置文件去做,与具体的A组件无关。

Spring框架中的设计模式

  • 工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例
  • 单例模式:Bean默认为单例模式
  • 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
  • 模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate
  • 观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener,Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。
  • 适配器模式 :Spring AOP 的增强或通知(Advice)使⽤到了适配器模式、spring MVC 中也是⽤到了适配器模式适配 Controller 。
  • 策略模式: Bean的实例化的时候决定采用何种方式初始化bean实例(反射或者CGLIB动态字节码生成),Resource 接口是具体资源访问策略的抽象。
  • 装饰器模式:Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。

Spring中Autowired和Resource关键字

  • @Resource和@Autowired都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。
  • Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualififier注解一起使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestServiceImpl { 
// 下面两种@Autowired只要使用一种即可
@Autowired
private UserDao userDao; // 用于字段上

@Autowired // 用于属性的方法上
public void setUserDao(UserDao userDao)
{
this.userDao = userDao;
}
}
// @Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允
// 许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结
// 合@Qualifier注解一起使用。如下:
public class TestServiceImpl {
@Autowired
@Qualifier("userDao") private UserDao userDao;
}
  • Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestServiceImpl { 
// 下面两种@Resource只要使用一种即可

// 用于字段上
@Resource(name="userDao")
private UserDao userDao;

// 用于属性的setter方法上
@Resource(name="userDao")
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}

Bean注册方式

  • XML 配置文件的注册方式
1
2
3
4
<bean id="person" class="org.springframework.beans.Person">
   <property name="id" value="1"/>
   <property name="name" value="Java"/>
</bean>

Java 注解的注册方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 可以使用 @Component 注解方式来注册 Bean
@Component
public class Person {
   private Integer id;
   private String name
   // 忽略其他方法
}

// @Bean 注解方式来注册 Bean
@Configuration
public class Person {
   @Bean
   public Person  person(){
      return new Person();
   }
   // 忽略其他方法
}

Java API 的注册方式

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 BeanDefinitionRegistry.registerBeanDefinition() 方法的方式注册 Bean	
public class CustomBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
RootBeanDefinition personBean = new RootBeanDefinition(Person.class);
// 新增 Bean
registry.registerBeanDefinition("person", personBean);
}
}
Read more »

本章是整理知识内容,为强化知识长期更新。

Vector

概述

  • Vector是Java Collection Franmework成员。
  • 列队允许添加重复元素
  • 列队允许null的存在
  • 列队从0开始,也就是列队头部的下标是0。
  • 列队支持泛型,这样可以避免ClassCastException异常。

与ArrayList的区别

Read more »

本章是整理知识内容,为强化知识长期更新。

Stack

  • 栈是元素的集合,其包含了两个基本操作:push 操作可以用于将元素压入栈,pop 操作可以将栈顶元素移除。
  • 遵循后入先出(LIFO)原则。
  • 时间复杂度:
  • 索引: O(n)
  • 搜索: O(n)
  • 插入: O(1)
  • 移除: O(1)

Stack

Collection成员之一,继承了Vector,通过重写Vector来实现LIFO(Last-in-First-out 后进先出)特性。

概述

  • Java Stack是LIFO对象。它扩展了Vector类。

源码分析Stack

Read more »