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

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


  • 首页

  • 归档

  • 搜索

MySQL基础篇

发表于 2021-01-25

MySQL的架构介绍

MySQL简介

概述

  • MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司。MySQL是一种关联数据库管理系统,将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性
  • Mysql是开源的,所以你不需要支付额外的费用
  • Mysql是可以定制的,采用了GPL协议,你可以修改源码来开发自己的Mysql系统
  • Mysql支持大型的数据库。可以处理拥有上千万条记录的大型数据库
  • MySQL使用标准的SQL数据语言形式
  • Mysql可以允许于多个系统上,并且支持多种语言。这些编程语言包括C、C++、Python、Java、Perl、PHP、Eiffel、Ruby和Tcl等
  • MySQL支持大型数据库,支持5000万条记录的数据仓库,32位系统表文件最大可支持4GB,64位系统支持最大的表文件为8TB

MySQL高级

  • 提示:完整的mysql优化需要很深的功底,大公司设置有专门的DBA写上述

  • MySQL内核
  • SQL优化工程师
  • MySQL服务器的优化
  • 各种参数常量设定
  • 查询语句优化
  • 主从复制
  • 软硬件升级
  • 容灾备份
  • SQL编程

Linux版MySQL安装

官网

  • 下载地址
  • image-20200903133713125

拷贝解压缩

  • xftp,将下载的压缩包拷贝至 /opt 目录下并解压
  • image-20200903110222691

检查工作

  • 检查当前系统是否安装过mysql
1
shell复制代码 rpm -qa|grep -i mysql
  • 删除命令
1
shell复制代码rpm -e --nodeps  mysql-libs
  • CentOS的默认数据库已经不再是MySQL,而是MariaDB,先删除自带的MariaDB

MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可。开发这个分支的原因之一是:甲骨文公司收购了MySQL后,有将MySQL闭源的潜在风险,因此社区采用分支的方式来避开这个风险。MariaDB的目的是完全兼容MySQL,包括API和命令行,使之能轻松成为MySQL的代替品

1
2
3
4
scss复制代码//查看mariadb
rpm -qa | grep mariadb
//删除mariadb
rpm -e --nodeps mariadb-libs-5.5.65-1.el7.x86_64

image-20200903133529134

安装

  • 依次安装
1
2
3
4
5
shell复制代码rpm -ivh mysql-community-common-5.7.31-1.el7.x86_64.rpm 
rpm -ivh mysql-community-libs-5.7.31-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-5.7.31-1.el7.x86_64.rpm
rpm -ivh mysql-community-server-5.7.31-1.el7.x86_64.rpm
rpm -ivh mysql-community-devel-5.7.31-1.el7.x86_64.rpm

image-20200903133922158

查看MySQL安装版本

  • 查看安装的mysql版本
1
shell复制代码mysqladmin --version

image-20200903134214590

  • 查看mysql的用户和用户组
+ 
1
2
3
4
shell复制代码//mysql用戶
cat /etc/passwd|grep mysql
//mysql用戶組
cat /etc/group|grep mysql
![image-20200903134718565](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/4f72957b4b9e1408bc25148ce2a5bc13074ee1daf93aec5a6d284bdba7b47ea5)

MySQL服务启动、停止

  • 启动
1
shell复制代码service mysqld start
  • 停止
1
shell复制代码service mysqld stop
  • image-20200903141400630

首次登陆

  • Mysql5.7默认安装之后root是有密码的

+ 获取MySQL的临时密码
+ 为了加强安全性,MySQL5.7为root用户随机生成了一个密码,在error log中,关于error log的位置,如果安装的是RPM包,则默认是/var/log/mysqld.log,只有启动过一次mysql才可以查看临时密码



1
shell复制代码 grep 'temporary password' /var/log/mysqld.log
![image-20200903150537738](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/1e20f4cadd0524a539e2c7b006c26a82329d7dcf089deaebe34ff78f97e53051)
  • 使用临时密码登录mysql -u root -pimage-20200903150633590
  • 使用临时密码登录后,并不能操作数据库,必须修改临时密码
  • 1
    2
    3
    4
    5
    mysql复制代码//如果修改的密码过于简单 必须先修改两个全局策略
    set global validate_password_policy=0;
    set global validate_password_length=1;
    //策略修改成功后再设置密码
    alter user 'root'@'localhost' identified by '123456';

image-20200903153336143

自启动MySQL服务

  • 设置mysql自启动
1
shell复制代码systemctl enable mysqld
  • 查看
1
shell复制代码systemctl list-unit-files | grep mysqld

MySQL的安装位置

  • 参数 路径 解释 备注
    –basedir /usr/bin 相关命令目录 mysqladmin、mysqldump等命令
    –datadir /var/lib/mysql mysql数据库文件的存放路径
    –plugin-dir /usr/lib64/mysql/plugin mysql插件存放路径
    –log-error /var/lib/mysql/xx.err mysql错误日志路径
    –pid-file /var/lib/mysql/xx.pid 进程pid文件
    –socket /var/lib/mysql/mysql.sock 本地连接时用的unix套接字文件
    /usr/share/mysql 配置文件目录 mysql脚本及配置文件
    /etc/init.d/mysql 服务启停相关脚本

修改配置文件位置

  • 查看默认配置
1
shell复制代码vim /etc/my.cnf

修改字符集

  • 连接mysql,建库建表插入数据,一切正常
1
2
3
4
5
6
7
8
9
10
mysql复制代码//建库db01
create database db01;
//使用库db01
use db01;
//创建user表
create table user(id int not null,name varchar(20));
//向user表插入数据
insert into user values(1,'z3');
//查看user表
select * from user;

image-20200903164843754

  • 向user表中插入中文

image-20200903165235850

  • 查看字符集
1
2
mysql复制代码show variables like 'character%';
show variables like '%char%';

image-20200903165925038

  • 1
    sql复制代码insert into user values(3,'张三');

插入失败,插入中文字符集需修改配置image-20200904091810703

  • 编辑 /ect/my.cnf 文件
1
shell复制代码vim /etc/my.cnf

增加一行character-set-server=utf8,保存退出并重启mysql服务 service mysqld restartimage-20200904091934966

  • 注意重新连接后,之前建立表仍然不允许,插入中文。需要重新create database并使用新建库,再重新建表

1
2
3
4
5
6
7
8
9
10
mysql复制代码//建库db02
create database db02;
//使用db02
use db02;
//创建新表users
create table users(id int not null,name varchar(20));
//插入数据
insert into users(1,'张三03');
//查看
select * form users;

image-20200904092705153

MySQL配置文件

如何配置

  • windows环境 my.ini文件
  • Linux环境 /etc/my.cnf 文件

二进制日志log-bin

  • 主从复制(先了解,后面会详细讲解):log-bin 中存放了所有的操作记录(写?),可以用于恢复。相当于 Redis 中的 AOF

错误日志log-error

  • 默认是关闭的,记录严重的警告和错误信息,每次启动和关闭的详细信息等

查询日志log

  • 默认关闭,记录查询的sql语句,如果开启会减低mysql的整体性能,因为记录日志也是需要消耗系统资源的
  • 可自定义“慢”的概念:0-10秒之间的一个数。慢查询日志会将超过这个查询事件的查询记录下来,方便找到需要优化的 sql 。用于优化sql语句是使用。

数据文件

  • 两种环境
    • Windows安装路径
    • Linux:/var/lib/mysql
  • frm文件(framework):存放表结构
  • myd文件(data):存放表数据
  • myi文件(index):存放表索引

MySQL逻辑架构介绍

总体概览

  • 设计思路

+ 和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,**插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离**。这种架构可以根据业务的需求和实际需要选择合适的存储引擎![image-20200904095911523](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/13070d85d4088fe1145b4d432a375673af4fde6f8fc0bfa24d05af55e9cb528b)
  • 层级结构
+ 1.连接层
    - 最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcp/ip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限
+ 2.服务层
    - 2.1 Management Serveices & Utilities: 系统管理和控制工具
    - 2.2 SQL Interface: SQL接口接受用户的SQL命令,并且返回用户需要查询的结果。比如select from就是调用SQL Interface
    - 2.3 Parser: 解析器,SQL命令传递到解析器的时候会被解析器验证和解析
    - 2.4 Optimizer: 查询优化器,SQL语句在查询之前会使用查询优化器对查询进行优化。 用一个例子就可以理解: select uid,name from user where gender= 1;优化器来决定先投影还是先过滤
    - 2.5 Cache和Buffer: 查询缓存,如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据,这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等
    缓存是负责读,缓冲负责写
+ 3.引擎层
    - 存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。后面介MyISAM和InnoDB
+ 4.存储层
    - 数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互

查询说明

  • 查询流程图

image-20200904110058340

  • 解释说明
+ mysql客户端通过协议与mysql服务器建连接,发送查询语句,先检查查询缓存,如果命中(一模一样的sql才能命中),直接返回结果,否则进行语句解析,也就是说,在解析查询之前,服务器会先访问查询缓存(query cache)——它存储SELECT语句以及相应的查询结果集。如果某个查询结果已经位于缓存中,服务器就不会再对查询进行解析、优化、以及执行。它仅仅将缓存中的结果返回给用户即可,这将大大提高系统的性能
+ 语法解析器和预处理:首先mysql通过关键字将SQL语句进行解析,并生成一颗对应的“解析树”。mysql解析器将使用mysql语法规则验证和解析查询;预处理器则根据一些mysql规则进一步检查解析数是否合法
+ 查询优化器当解析树被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划
+ 然后,mysql默认使用的BTREE索引,并且一个大致方向是:无论怎么折腾sql,至少在目前来说,mysql最多只用到表中的一个索引

MySQL存储引擎

查看命令

  • 查看当前mysql使用的存储引擎
1
ini复制代码show engines;

image-20200904114048624

  • 查看你的mysql当前默认的存储引擎:
1
sql复制代码show variables like '%storage_engine%';

image-20200904114229286

各个引擎简介

  • InnoDB存储引擎
    InnoDB是MySQL的默认事务型引擎,它被设计用来处理大量的短期(short-lived)事务。除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。行级锁,适合高并发情况
  • MyISAM存储引擎
    MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM不支持事务和行级锁(myisam改表时会将整个表全锁住),有一个毫无疑问的缺陷就是崩溃后无法安全恢复
  • Archive引擎
    Archive存储引擎只支持INSERT和SELECT操作,在MySQL5.1之前不支持索引
    Archive表适合日志和数据采集类应用,适合低访问量大数据等情况
    根据英文的测试结论来看,Archive表比MyISAM表要小大约75%,比支持事务处理的InnoDB表小大约83%
  • Blackhole引擎
    Blackhole引擎没有实现任何存储机制,它会丢弃所有插入的数据,不做任何保存。但服务器会记录Blackhole表的日志,所以可以用于复制数据到备库,或者简单地记录到日志。但这种应用方式会碰到很多问题,因此并不推荐
  • CSV引擎
    CSV引擎可以将普通的CSV文件作为MySQL的表来处理,但不支持索引
    CSV引擎可以作为一种数据交换的机制,非常有用
    CSV存储的数据直接可以在操作系统里,用文本编辑器,或者excel读取
  • Memory引擎
    如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,那么使用Memory表是非常有用。Memory表至少比MyISAM表要快一个数量级。(使用专业的内存数据库更快,如redis)
  • Federated引擎
    Federated引擎是访问其他MySQL服务器的一个代理,尽管该引擎看起来提供了一种很好的跨服务器的灵活性,但也经常带来问题,因此默认是禁用的

MyISAM和InnoDB(*)

对比项 MyISAM InnoDB
主外键 不支持 支持
事务 不支持 支持
行表锁 表锁,即使操作一条记录也会锁住整张表,不适合高并发的操作 行锁,操作时只锁某一行,不对其他行有影响,适合高并发的操作
缓存 只缓存索引,不缓存真实数据 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响
表空间 小 大
关注点 性能 事务
默认安装 Y Y
用户表默认使用 N Y
自带系统表使用 Y N
  • innoDB 索引,使用B+树,MyISAM 索引使用 B-树
  • innoDB 主键为聚簇索引,基于聚簇索引的增删改查效率非常高

阿里巴巴、淘宝用哪个

  • image-20200904132321344
  • Percona 为 MySQL 数据库服务器进行了改进,在功能和性能上较 MySQL 有着很显著的提升。该版本提升了在高负载情况下的 InnoDB 的性能、为 DBA 提供一些非常有用的性能诊断工具;另外有更多的参数和命令来控制服务器行为
  • 该公司新建了一款存储引擎叫xtradb完全可以替代innodb,并且在性能和并发上做得更好
  • 阿里巴巴大部分mysql数据库其实使用的是percona的原型加以修改
  • AliSql+AliRedis

索引优化分析

性能下降SQL慢

查询语句写的差

  • 能不能拆,条件过滤尽量少

索引失效

  • 单值
  • 复合
    • 条件多时,可以建共同索引(混合索引)。混合索引一般会偶先使用。有些情况下,就算有索引具体执行时也不会被使用

关联了太多join

  • 设计缺陷或不得已的需求

服务器调优及各个参数设置(缓冲、线程数等)(不重要DBA的工作)

常见通用的Join查询

SQL执行顺序

  • 手写
+ ![image-20200904135253784](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/55defadb678eafde05917cfa1de393c98874b843d5d26f368b567df18a8d1766)
  • 机读(先从From开口)
+ 随着Mysql版本的更新换代,其优化器也在不断的升级,**优化器**会分析不同执行顺序产生的性能消耗不同而**动态调整执行顺序**
+ 下面是经常出现的查询顺序:


![image-20200904135439784](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/dd36d3a66ab4043d58fe823d2b8653ab8629042be6c359c4e9ebe5667f47c974)
  • 总结
+ ![image-20200904135640745](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/4f6803d52ae9f00d5ea25136e242b950b40384685631cac3a62e1715c6c4f69a)

JOIN关系图

  • 所有的join关系

image-20200904135829735

  • 共有独有的理解

共有:满足 a.deptid = b.id 的叫共有
A独有: A 表中所有不满足 a.deptid = b.id 连接关系的数据
同时参考 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
    26
    27
    28
    29
    30
    31
    mysql复制代码CREATE TABLE `t_dept` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `deptName` VARCHAR(30) DEFAULT NULL,
    `address` VARCHAR(40) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE `t_emp` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(20) DEFAULT NULL,
    `deptId` INT(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `fk_dept_id` (`deptId`)
    #CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)
    ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;


    INSERT INTO t_dept(deptName,address) VALUES('RD',11);
    INSERT INTO t_dept(deptName,address) VALUES('HR',12);
    INSERT INTO t_dept(deptName,address) VALUES('MK',13);
    INSERT INTO t_dept(deptName,address) VALUES('MIS',14);
    INSERT INTO t_dept(deptName,address) VALUES('FD',15);

    INSERT INTO t_emp(NAME,deptId) VALUES('z3',1);
    INSERT INTO t_emp(NAME,deptId) VALUES('z4',1);
    INSERT INTO t_emp(NAME,deptId) VALUES('z5',1);
    INSERT INTO t_emp(NAME,deptId) VALUES('w5',2);
    INSERT INTO t_emp(NAME,deptId) VALUES('w6',2);
    INSERT INTO t_emp(NAME,deptId) VALUES('s7',3);
    INSERT INTO t_emp(NAME,deptId) VALUES('s8',4);
    INSERT INTO t_emp(NAME,deptId) VALUES('s9',51);
  • image-20200904170144015

7中JOIN

  • 笛卡尔积
1
mysql复制代码select * from t_dept,t_emp

image-20200904170112925

  • 两表共有的 (INNER JOIN)
1
mysql复制代码select * from t_dept d inner join t_emp e on d.id=e.deptId;

image-20200904170406256

  • 左外连接(LEFT JOIN)两表共有的加左表独有
1
mysql复制代码select * from t_dept d left join t_emp e on d.id=e.deptId;

image-20200904170644873

  • 右外连接(RIGHT JOIN)两表共有的加右表独有
1
mysql复制代码select * from t_dept d  right  join t_emp e on d.id=e.deptId;

image-20200904170906716

  • 左表独有的
1
mysql复制代码select * from t_dept d left join t_emp e on d.id=e.deptId where e.id is null;

image-20200904171847494

  • 右边独有的
1
mysql复制代码select * from t_dept d right join t_emp e on d.id=e.deptId where d.id is null;

image-20200904171832590

  • 两表全有
1
2
3
mysql复制代码#MySQL Full Join的实现 因为MySQL不支持FULL JOIN,下面是替代方法
#left join + union(可去除重复数据)+ right join
select * from t_dept d left join t_emp e on d.id=e.deptId union select * from t_dept d right join t_emp e on d.id=e.deptId;

image-20200904173127918

  • 左表独有+右表独有
1
mysql复制代码select * from t_dept d left join t_emp e on d.id=e.deptId where e.id is null union select * from t_dept d right join t_emp e on d.id=e.deptId where d.id is null;

image-20200904173108850

索引简介

是什么(*)

  • MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。可以得到索引的本质:索引是数据结构
+ 索引的目的在于提高查询效率,可以类比字典
+ 如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql
+ 如果没有索引,那么你可能需要a----z,如果我想找到Java开头的单词呢?或者Oracle开头的单词呢?是不是觉得如果没有索引,这个事情根本无法完成?
  • 你可以简单理解为“排好序的快速查找数据结构”

+ 详解(\*)


    - 在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据
    - 这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是**索引**。下图就是一种可能的索引方式示例


    ![image-20200907103123086](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/d3c5375de8e5f36631aa07125845944a411ad76d36268ea6232c53dd5dc453b2)
    - 左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到相应数据,从而快速的检索出符合条件的记录
    - 二叉树弊端之一:二叉树很可能会发生两边不平衡的情况
    B-TREE: (B:balance) 会自动根据两边的情况自动调节,使两端无限趋近于平衡状态。可以使性能最稳定。(myisam使用的方式)
    B-TREE弊端:(插入/修改操作多时,B-TREE会不断调整平衡,消耗性能)从侧面说明了索引不是越多越好
    B+TREE:Innodb 所使用的索引
+ 结论


    - > 数据本身之外,数据库还维护着一个**满足特定查找算法的数据结构**,这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法,这种数据结构就是**索引**
  • 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上
  • 我们平常所说的索引,如果没有特别指明,都是指B树(多路搜索树,并不一定是二叉的)结构组织的索引。其中聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。当然,除了B+树这种类型的索引之外,还有哈稀索引(hash index)等。

优势

  • 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

劣势

  • 实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的
  • 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE,因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息
  • 索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句

mysql索引结构

BTree索引(*)
  • BTree又叫多路平衡查找树,一颗m叉的BTree特性如下:
+ 树中每个节点最多包含m个孩子
+ 除根节点与叶子节点外,每个节点至少有[ceil(m/2)]个孩子(ceil()为向上取整)
+ 若根节点不是叶子节点,则至少有两个孩子
+ 所有的叶子节点都在同一层
+ 每个非叶子节点由n个key与n+1个指针组成,其中[ceil(m/2)-1] <= n <= m-1
  • Myisam普通索引
  • 检索原理
  • img

这是一个3叉(只是举例,真实会有很多叉)的BTree结构图,每一个方框块我们称之为一个磁盘块或者叫做一个block块,这是操作系统一次IO往内存中读的内容,一个块对应四个扇区,紫色代表的是磁盘块中的数据key,黄色代表的是数据data,蓝色代表的是指针p,指向下一个磁盘块的位置

  • 来模拟下查找key为29的data的过程:
+ 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作**1次**】
+ 磁盘块1存储17,35和三个指针数据。我们发现17<29<35,因此我们找到指针p2
+ 根据p2指针,我们定位并读取磁盘块3。【磁盘IO操作**2次**】
+ 磁盘块3存储26,30和三个指针数据。我们发现26<29<30,因此我们找到指针p2
+ 根据p2指针,我们定位并读取磁盘块8。【磁盘IO操作**3次**】
+ 磁盘块8中存储28,29。我们找到29,获取29所对应的数据data
  • 由此可见,BTree索引使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率
B+Tree索引(*)
  • B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度
  • innodb的普通索引
  • 原理图

img

B+TREE 第二级的 数据并不能直接取出来,只作索引使用。在内存有限的情况下,查询效率高于 B-TREE
B-TREE 第二级可以直接取出来,树形结构比较重,在内存无限大的时候有优势

  • B+Tree与B-Tree 的区别:结论在内存有限的情况下,B+TREE 永远比 B-TREE好。无限内存则后者方便
+ 非叶子节点只存储键值信息, 数据记录都存放在叶子节点中, 将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,所以B+Tree的高度可以被压缩到特别的低
+ 在B+Tree上通常有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。所以我们除了可以对B+Tree进行主键的范围查找和分页查找,还可以从根节点开始,进行随机查找
  • 思考:为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
+ B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了
+ B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当
  • B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)
聚簇索引与非聚簇索引
  • 聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。术语‘聚簇’表示数据行和相邻的键值紧凑的存储在一起
  • 如下图,左侧的索引就是聚簇索引,因为数据行在磁盘的排列和索引排序保持一致

image-20200907142432179

  • 聚簇索引的好处:
    按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以节省了大量的io操作
    聚簇索引的限制:
    对于mysql数据库目前只有innodb数据引擎支持聚簇索引,而Myisam并不支持聚簇索引。
    由于数据物理存储排序方式只能有一种,所以每个Mysql的表只能有一个聚簇索引。一般情况下就是该表的主键,为了充分利用聚簇索引的聚簇的特性,所以innodb表的主键列尽量选用有序的顺序id,而不建议用无序的id,比如uuid这种(参考聚簇索引的好处)
  • 这里说明了主键索引为何采用自增的方式:1、业务需求,有序 2、能使用到聚簇索引
Hash索引
full-index全文索引
R-Tree索引

mysql索引分类

单值索引
  • 即一个索引只包含单个例,一个表可以有多个单列索引
唯一索引
  • 索引列的值必须唯一,但允许有空值
复合索引
  • 即一个索引包含多个列
  • 在数据库操作期间,复合索引比单值索引所需要的开销更小(对于相同的多个列建索引)当表的行数远大于索引列的数目时可以使用复合索引
基本语法
  • 创建:ALTER mytable ADD [UNIQUE ] INDEX [indexName] ON (columnname(length)) ;
  • 删除:DROP INDEX [indexName] ON mytable;
  • 查看:SHOW INDEX FROM table_name\G;
  • 使用ALTER命令
    • ALTER TABLE tbl_name ADD PRIMARY KEY (column_list): 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL
    • ALTER TABLE tbl_name ADD UNIQUE index_name (column_list): 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)
    • ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次
    • ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT 用于全文索引

哪些情况需要创建索引

  1. 主键自动建立唯一索引
  2. 频繁作为查询条件的字段应该创建索引
  3. 查询中与其它表关联的字段,外键关系建立索引
  4. 频繁更新的字段不适合创建索引,因为每次更新不单单是更新记录还需要更新索引
  5. where条件里用不到的字段不创建索引
  6. 单键/组合索引的选择问题,who?(在高并发下倾向创建组合索引)
  7. 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
  8. 查询中统计或者分组字段

哪些情况不需要创建索引

  1. 表记录太少
  2. 经常增删改的表–提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE
    因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件
  3. 数据重复且分布平均的表字段,因此应该只为最经常查询和最经常排序的数据列建立索引。注意,如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果image-20200907143538958

性能分析

MySQL Query Optimizer
  • MySQL优化器

image-20200907153422003

MySQL常见瓶颈
  • CPU:CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据的时候
    • SQL中对大量数据进行比较、关联、排序、分组
  • IO:磁盘IO瓶颈发生在装入数据远大于内存容量的时候
    • 实例内存满足不了缓存数据或排序等需要,导致产生大量 物理 IO
    • 查询执行效率低,扫描过多数据行
  • 锁
    • 不适宜的锁的设置,导致线程阻塞,性能下降
    • 死锁,线程之间交叉调用资源,导致死锁,程序卡住
  • 服务器硬件的性能瓶颈:top,free,iostat和vmstat来查看系统的性能状态
Explain(*)
  • 是什么(查看执行计划)
+ 使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈
+ [官网介绍](http://dev.mysql.com/doc/refman/5.5/en/explain-output.html)


![image-20200907154426453](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/fe817dfe65b0b183230c4ea47c578266a0c5b669304d03b466845a0b4045b44d)
  • 能干嘛
+ 表的读取顺序---id字段
+ 数据读取操作的操作类型---select\_type
+ 哪些索引可以使用---possible\_key
+ 哪些索引被实际使用---key
+ 表之间的引用
+ 每张表有多少行被优化器查询---rows
  • 怎么玩
+ Explain + SQL语句
+ 执行计划包含的信息


![image-20200907154751077](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/47fb1f48c134178db552b5297f3126cbdd7d613c4b753d29780b54b81ca5a929)
  • 各字段解释
+ id


    - select查询的序列号,包含一组数字,表示**查询中执行select子句或操作表的顺序**
    - 三种情况


        1. id相同,执行顺序由上至下(t1--t3--t2)


        ![image-20200907161131289](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/9e6a6d262eaac46c09118af3fbe3dddbc3691dc14cca260dd38e23769d8b6021)


        id相同,执行顺序由上至下。此例中 先执行where 后的第一条语句 t1.id = t2.id 通过 t1.id 关联 t2.id ,而 t2.id 的结果建立在 t2.id=t3.id 的基础之上
        2. id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行(t3--t2--t1)


        ![image-20200907161213954](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/b459b7d54fec9361da13ebca89c3c02ddbf31e8139164db7ebde8c8503053a4e)


        id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
        3. id相同不同,同时存在(t3--derived2--t2)


        ![image-20200907161529852](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/4f3051d13651fcbdc06b8cfca0eaf8b76f65a93e2d5e5daad647105c3485b001)


        id如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行


        衍生表 = derived2 --> derived + 2 (2 表示由 id =2 的查询衍生出来的表。type 肯定是 all ,因为衍生的表没有建立索引)
+ select\_type


    - 有哪些类型



    | 类型 | 解释说明 |
    | --- | --- |
    | SIMPLE | 简单的 select 查询,查询中不包含子查询或者UNION |
    | PRIMARY | 查询中若包含任何复杂的子部分,最外层查询则被标记为Primary |
    | DERIVED | 在FROM列表中包含的子查询被标记为DERIVED(衍生),MySQL会递归执行这些子查询, 把结果放在临时表里 |
    | SUBQUERY | 在SELECT或WHERE列表中包含了子查询 |
    | DEPENDENT SUBQUERY | 在SELECT或WHERE列表中包含了子查询,子查询基于外层 |
    | UNCACHEABLE SUBQUREY | 无法被缓存的子查询 |
    | UNION | 若第二个SELECT出现在UNION之后,则被标记为UNION,若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED |
    | UNION RESULT | 从UNION表获取结果的SELECT |
    - 查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询
+ table


    - 显示这一行的数据是关于哪张表的
+ type


    - ![image-20200907163559503](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/0d6222471820ee28b996ac791fe2b1b7dbd9b68869164ccd831c8000cc6d77cd)
    - type显示的是访问类型,是较为重要的一个指标,结果值**从最好到最坏**依次是:


    system > const > eq\_ref > ref > fulltext > ref\_or\_null > index\_merge > unique\_subquery > index\_subquery > range(尽量保证) > index > ALL



    > system>const>eq\_ref>ref>range>index>ALL


    一般来说,得保证查询至少达到range级别,最好能达到ref
    - 显示查询使用了何种类型,**从最好到最差**依次是:system>const>eq\_ref>ref>range>index>ALL
    - | 类型 | 解释说明 |
    | --- | --- |
    | system | 表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计 |
    | const | 表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快如将主键置于where列表中,MySQL就能将该查询转换为一个常量 |
    | eq\_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描 |
    | ref | 非唯一性索引扫描,返回匹配某个单独值的所有行.本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体 |
    | range | 只**检索给定范围**的行,使用一个索引来选择行。key 列显示使用了哪个索引一般就是在你的where语句中出现了between、<、>、in等的查询这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引 |
    | index | Full Index Scan,index与ALL区别为**index类型只遍历索引树**。这通常比ALL快,因为索引文件通常比数据文件小。(**也就是说虽然all和Index都是读全表**,但index是从索引中读取的,而all是从硬盘中读的) |
    | all | Full Table Scan,将遍历全表以找到匹配的行 |
    |  |  |


    > 备注:一般来说,得**保证查询至少达到range级别**,最好能达到ref
+ possible\_key(理论上)


    - 显示可能应用在这张表中的索引,一个或多个。查询涉及到的字段上若存在索引,则该索引将被列出,**但不一定被查询实际使用**
+ key


    - **实际使用的索引**。如果为NULL,则没有使用索引
    - **查询中若使用了覆盖索引,则该索引和查询的select字段重叠**


    ![image-20200907171519006](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/6cf316ae6a29a95c10d50032ba02f4429a17ab115b4d25bc69723ce1c3a3d269)
+ key\_len


    - 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度,在不损失精确性的情况下,长度**越短越好**
    - key\_len显示的值为索引字段的最大可能长度,**并非实际使用长度**,即key\_len是根据表定义计算而得,不是通过表内检索出的
    - ![image-20200907172541125](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/68d3efa413ad54af3e6981d445daa3deff960f8890c8d37830a6c76973274fa7)
    - **同样的查询结果,key\_len 越小越好**
+ ref


    - 显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值


    ![image-20200907173245514](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/f894222b49c1ecd4a5942babab4e70988c5be1d4b39a558395c8385feb4fdda0)
+ rows


    - rows列显示MySQL认为它执行查询时必须检查的行数
    - 越少越好


    ![image-20200907174149254](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/1519dbb44ca392df5e3037d290bf248e22f2eba7e129d4d38bfd9c89a39f5be9)
+ Extra


    - 包含不适合在其他列中显示但**十分重要的额外信息**



    | 字段 | 解释说明 |
    | --- | --- |
    | Using filesort | 说明mysql会对数据使用一个外部的索引排序,而**不是按照表内的索引顺序进行**读取,MySQL中无法利用索引完成的排序操作称为“文件排序” |
    | Using temporary | 使了用临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。 |
    | USING index | 表示相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错!如果同时出现using where,表明索引被用来执行索引键值的查找;如果没有同时出现using where,表明索引只是用来读取数据而非利用索引执行查找 **覆盖索引(Covering Index)**:查询字段和索引字段重合 |
    | Using where | 表明使用了where过滤 |
    | using join buffer | 使用了连接缓存 |
    | impossible where | where子句的值总是false,不能用来获取任何元组 |
    | select tables optimized away | 在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(\*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化 |
    | distinct | 优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作 |
    - Using filesort


    ![image-20200908094428440](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/9e424347e35ffd194d1e43443cddfbbc95f66c481b457470359d05dbdb3df5d9)
    - Using temporary


    ![image-20200908094928494](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/16a386e2c55ffd4b28f41a9673fd3f72f1c8f12cac57322c2f85595429496ed3)
    - Using index和Using where


    ![image-20200908095558558](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/94f6afb2d956f16f891e4693ce5fd127ca1ef7ba6f3f59b28fd6f7d0b40311b0)
  • 案例Case(*)
+ 描述执行顺序------id:4-3-2-1-null![image-20200908100616270](https://gitee.com/songjianzaina/juejin_p5/raw/master/img/2be12bd22e819f5931e98f0bc0ef9b6e4ad62d82912edc96e432676b426a9233)
    - 第一行(执行顺序4):id列为1,表示union里的第一个select,select\_type列的**primary表示该查询为外层查询**,table列被标记为,表示查询结果来自一个衍生表,其中derived3中代表该查询衍生自第三个select查询,即**id为3的select** 【select d1.name......】
    - 第二行(执行顺序2):id为3,是整个查询中第三个select的一部分,因查询包含在from中,所以为derived【select id,name from t1 where other\_column=""】
    - 第三行(执行顺序3):select列表中的子查询select\_type为subquery,为整个查询中的第二个select【select id from t3】
    - 第四行(执行顺序1):select\_type为union,说明第四个select是union里的第二个select,最先执行【select name,id from t2】
    - 第五行(执行顺序5):代表从union的临时表中读取行的阶段,table列的<union1,4>表示用第一个和第四个select的结果进行union操作【两个结果union操作】

本文转载自: 掘金

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

注解Slf4j的简单使用教程(新手入门) Slf4j注解

发表于 2021-01-24

@Slf4j注解的简单使用教程

为什么使用@Slf4j?

  • 很简单的就是为了能够少写两行代码,不用每次都在类的最前边写上:
    private static final Logger logger = LoggerFactory.getLogger(this.XXX.class);

我们只需要在类前面添加注解@Slf4j,即可使用log日志的功能了

先放一张图来说明@Slf4j的方便之处:

怎么来使用@Slf4j

  • 使用十分方便,只是把以前,和以前的logger一样用,有info,debug,error等等
  • 划重点:要是用这个注解,最主要是依赖的引用!!!

依赖的引用

  1. 小型项目的使用
    maven添加依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码        <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.28</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.28</version>
</dependency>

这是三个依赖,其中slf4j-api的版本和slf4j-simple的版本要对应,你们用的时候直接复制就可以,这种最简单,不需要在配置什么的,可以直接打印在console上面,但是,这个这个。。他好像不能设置样式,输出默认是system.error级别的红色唉!(不对能设置样式,在这里因为小白没有深究,说以贴个地址,你们阔以先瞅瞅,回头在写个相关的配置:www.slf4j.org/api/org/slf…)

  1. 项目使用(slf4j+log4j12)
    着重讲一下这个的maven添加依赖(因为网上现存的教程非常杂而且加了一堆的依赖)
1
2
3
4
5
6
7
8
9
10
11
xml复制代码        <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.28</version>
</dependency>

哎!对,你没看错,只需要加两个依赖即可,就是这么简单,为什么呢?

因为:

slf4j-log4j12本身就依赖如图中的两个包,所以,加这么一个依赖,idea会自动将他所以来的两个包下载下来
(以下呢是参考于网络)

最关键的是这一步,配置log4j.properties

  1. 在resources文件夹下生成log4j.properties文件(创建的maven工程可能没有resources文件夹,那就创建一个,位置随便,但是一般是放在main文件夹下,和java文件夹同级的位置,需要把resources文件夹标志为source root,在idea编辑器下通过鼠标右键点击就可以);或者把该文件放在工程目录下,即和src同级的位置,因为该位置是默认的类路径,程序会自动去此处查找log4j.xml或log4j.properties文件。(idea如图:)
  2. 配置文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码### 设置###
log4j.rootLogger = info,stdout,D,E

### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n

### 输出DEBUG 级别以上的日志到=logs/error.log ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

### 输出ERROR 级别以上的日志到=logs/error.log ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File =logs/error.log
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

如此,你就可以开心的使用@Slf4j的注解啦

本文转载自: 掘金

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

通过 JFR 与日志深入探索 JVM - TLAB 原理详解

发表于 2021-01-24

全系列目录:通过 JFR 与日志深入探索 JVM - 总览篇

什么是 TLAB?

TLAB(Thread Local Allocation Buffer)线程本地分配缓存区,这是一个线程专用的内存分配区域。既然是一个内存分配区域,我们就先要搞清楚 Java 内存大概是如何分配的。

我们一般认为 Java 中 new 的对象都是在堆上分配,这个说法不够准确,应该是大部分对象在堆上的 TLAB分配,还有一部分在 栈上分配 或者是 堆上直接分配,可能 Eden 区也可能年老代。同时,对于一些的 GC 算法,还可能直接在老年代上面分配,例如 G1 GC 中的 humongous allocations(大对象分配),就是对象在超过 Region 一半大小的时候,直接在老年代的连续空间分配。

这里,我们先只关心 TLAB 分配。 对于单线程应用,每次分配内存,会记录上次分配对象内存地址末尾的指针,之后分配对象会从这个指针开始检索分配。这个机制叫做 bump-the-pointer (撞针)。 对于多线程应用来说,内存分配需要考虑线程安全。最直接的想法就是通过全局锁,但是这个性能会很差。为了优化这个性能,我们考虑可以每个线程分配一个线程本地私有的内存池,然后采用 bump-the-pointer 机制进行内存分配。这个线程本地私有的内存池,就是 TLAB。只有 TLAB 满了,再去申请内存的时候,需要扩充 TLAB 或者使用新的 TLAB,这时候才需要锁。这样大大减少了锁使用。

TLAB 相关 JVM 参数详解

我们先来浏览下 TLAB 相关的 JVM 参数以及其含义,在下一小节会深入源码分析原理以及设计这个参数是为何。

以下参数与默认值均来自于 OpenJDK 11

1. UseTLAB

说明:是否启用 TLAB,默认是启用的。

默认:true

举例:如果想关闭:-XX:-UseTLAB

2. ResizeTLAB

说明:TLAB 是否是自适应可变的,默认为是。

默认:true

举例:如果想关闭:-XX:-ResizeTLAB

3. TLABSize

说明:初始 TLAB 大小。单位是字节

默认:0, 0 就是不主动设置 TLAB 初始大小,而是通过 JVM 自己计算每一个线程的初始大小

举例:-XX:TLABSize=65536

4. MinTLABSize

说明:最小 TLAB 大小。单位是字节

默认:2048

举例:-XX:TLABSize=4096

5. TLABWasteTargetPercent

说明:TLAB 的大小计算涉及到了 Eden 区的大小以及可以浪费的比率。TLAB 浪费占用 Eden 的百分比,这个参数的作用会在接下来的原理说明内详细说明

默认:1

举例:-XX:TLABWasteTargetPercent=10

6. TLABAllocationWeight

说明: TLAB 大小计算和线程数量有关,但是线程是动态创建销毁的。所以需要基于历史线程个数推测接下来的线程个数来计算 TLAB 大小。一般 JVM 内像这种预测函数都采用了 EMA (Exponential Moving Average 指数平均数)算法进行预测,会在接下来的原理说明内详细说明。这个参数代表权重,权重越高,最近的数据占比影响越大。

默认:35

举例:-XX:TLABAllocationWeight=70

7. TLABRefillWasteFraction

说明: 在一次 TLAB 再填充(refill)发生的时候,最大的 TLAB 浪费。至于什么是再填充(refill),什么是 TLAB 浪费,会在接下来的原理说明内详细说明

默认:64

举例:-XX:TLABRefillWasteFraction=32

8. TLABWasteIncrement

说明: TLAB 缓慢分配时允许的 TLAB 浪费增量,什么是 TLAB 浪费,什么是 TLAB 缓慢分配,会在接下来的原理说明内详细说明。单位不是字节,而是MarkWord个数,也就是 Java 堆的内存最小单元

默认:4

举例:-XX:TLABWasteIncrement=4

9. ZeroTLAB

说明: 是否将新创建的 TLAB 内的对象所有字段归零

默认:false

举例:-XX:+ZeroTLAB

TLAB 生命周期与原理详解

TLAB 是从堆上 Eden 区的分配的一块线程本地私有内存。线程初始化的时候,如果 JVM 启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则会创建并初始化 TLAB。同时,在 GC 扫描对象发生之后,线程第一次尝试分配对象的时候,也会创建并初始化 TLAB 。在 TLAB 已经满了或者接近于满了的时候,TLAB 可能会被释放回 Eden。GC 扫描对象发生时,TLAB 会被释放回 Eden。TLAB 的生命周期期望只存在于一个 GC 扫描周期内。在 JVM 中,一个 GC 扫描周期,就是一个epoch。那么,可以知道,TLAB 内分配内存一定是线性分配的。

TLAB 的最小大小:通过MinTLABSize指定

TLAB 的最大大小:不同的 GC 中不同,G1 GC 中为大对象(humongous object)大小,也就是 G1 region 大小的一半。因为开头提到过,在 G1 GC 中,大对象不能在 TLAB 分配,而是老年代。ZGC 中为页大小的 8 分之一,类似的在大部分情况下 Shenandoah GC 也是每个 Region 大小的 8 分之一。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度。对于其他的 GC,则是 int 数组的最大大小,这个和为了填充 dummy object 表示 TLAB 的空区域有关。

image

为何要填充 dummy object ?

由于 TLAB 仅线程内知道哪些被分配了,在 GC 扫描发生时返回 Eden 区,如果不填充的话,外部并不知道哪一部分被使用哪一部分没有,需要做额外的检查,如果填充已经确认会被回收的对象,也就是 dummy object, GC 会直接标记之后跳过这块内存,增加扫描效率。反正这块内存已经属于 TLAB,其他线程在下次扫描结束前是无法使用的。这个 dummy object 就是 int 数组。为了一定能有填充 dummy object 的空间,一般 TLAB 大小都会预留一个 dummy object 的 header 的空间,也是一个 int[] 的 header,所以 TLAB 的大小不能超过int 数组的最大大小,否则无法用 dummy object 填满未使用的空间。

TLAB 的大小: 如果指定了TLABSize,就用这个大小作为初始大小。如果没有指定,则按照如下的公式进行计算: Eden 区大小 / (当前 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置)

当前 epcoh 内会分配对象期望线程个数,也就是会创建并初始化 TLAB 的线程个数,这个从之前提到的 EMA (Exponential Moving Average 指数平均数)算法采集预测而来。算法是:

1
2
3
4
5
6
markdown复制代码采样次数小于等于 100 时,每次采样:
1. 次数权重 = 100 / 次数
2. 计算权重 = 次数权重 与 TLABAllocationWeight 中大的那个
3. 新的平均值 = (100% - 计算权重%) * 之前的平均值 + 计算权重% * 当前采样值
采样次数大于 100 时,每次采样:
新的平均值 = (100% - TLABAllocationWeight %) * 之前的平均值 + TLABAllocationWeight % * 当前采样值

可以看出 TLABAllocationWeight 越大,则最近的线程数量对于这个下个 epcoh 内会分配对象期望线程个数影响越大。

每个 epoch 内期望 refill 次数就是在每个 GC 扫描周期内,refill 的次数。那么什么是 refill 呢?

在 TLAB 内存充足的时候分配对象就是快分配,否则在 TLAB 内存不足的时候分配对象就是慢分配,慢分配可能会发生两种处理:

1.线程获取新的 TLAB。老的 TLAB 回归 Eden,之后线程获取新的 TLAB 分配对象。 image2.对象在 TLAB 外分配,也就 Eden 区。 image

这两种处理主要由TLAB最大浪费空间决定,这是一个动态值。初始TLAB最大浪费空间 = TLAB 的大小 / TLABRefillWasteFraction。根据前面提到的这个 JVM 参数,默认为TLAB 的大小的 64 分之一。之后,伴随着每次慢分配,这个TLAB最大浪费空间会每次递增 TLABWasteIncrement 大小的空间。如果当前 TLAB 的剩余容量大于TLAB最大浪费空间,就不在当前TLAB分配,直接在 Eden 区进行分配。如果剩余容量小于TLAB最大浪费空间,就丢弃当前 TLAB 回归 Eden,线程获取新的 TLAB 分配对象。refill 指的就是这种线程获取新的 TLAB 分配对象的行为。

那么,也就好理解为何要尽量满足 TLAB 的大小 = Eden 区大小 / (下个 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置)了。尽量让所有对象在 TLAB 内分配,也就是 TLAB 可能要占满 Eden。在下次 GC 扫描前,refill 回 Eden 的内存别的线程是不能用的,因为剩余空间已经填满了 dummy object。所以所有线程使用内存大小就是 下个 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置,对象一般都在 Eden 区由某个线程分配,也就所有线程使用内存大小就最好是整个 Eden。但是这种情况太过于理想,总会有内存被填充了 dummy object而造成了浪费,因为 GC 扫描随时可能发生。假设平均下来,GC 扫描的时候,每个线程当前的 TLAB 都有一半的内存被浪费,这个每个线程使用内存的浪费的百分比率(也就是 TLABWasteTargetPercent),也就是等于(注意,仅最新的那个 TLAB 有浪费,之前 refill 退回的假设是没有浪费的):

1/2 * (每个 epoch 内每个线程期望 refill 次数) * 100

那么每个 epoch 内每个线程 refill 次数配置就等于 50 / TLABWasteTargetPercent, 默认也就是 50 次。

当 TLABResize 设置为 true 的时候,在每个 epoch 当线程需要分配对象的时候, TLAB 大小都会被重新计算,并用这个最新的大小去从 Eden 申请内存。如果没有对象分配则不重新计算,也不申请(废话~)。主要是为了能让线程 TLAB 的 refill 次数 接近于 每个 epoch 内每个线程 refill 次数配置。这样就能让浪费比例接近于用户配置的 TLABWasteTargetPercent.这个大小重新计算的公式为: TLAB 最新大小 * EMA refill 次数 / 每个 epoch 内每个线程 refill 次数配置。

TLAB 相关源码详解

1. TLAB 类构成

线程初始化的时候,如果 JVM 启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则会初始化 TLAB。

TLAB 包括如下几个 field (HeapWord* 可以理解为堆中的内存地址): src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码//静态全局变量
static size_t _max_size; // 所有 TLAB 的最大大小
static int _reserve_for_allocation_prefetch; // CPU 缓存优化 Allocation Prefetch 的保留空间,这里先不用关心
static unsigned _target_refills; //每个 epoch 周期内期望的 refill 次数

//以下是 TLAB 的主要构成 field
HeapWord* _start; // TLAB 起始地址,表示堆内存地址都用 HeapWord*
HeapWord* _top; // 上次分配的内存地址
HeapWord* _end; // TLAB 结束地址
size_t _desired_size; // TLAB 大小 包括保留空间,表示内存大小都需要通过 size_t 类型,也就是实际字节数除以 HeapWordSize 的值
size_t _refill_waste_limit; // TLAB最大浪费空间,剩余空间不足分配浪费空间限制。在TLAB剩余空间不足的时候,根据这个值决定分配策略,如果浪费空间大于这个值则直接在 Eden 区分配,如果小于这个值则将当前 TLAB 放回 Eden 区管理并从 Eden 申请新的 TLAB 进行分配。
AdaptiveWeightedAverage _allocation_fraction; // 当前 TLAB 占用所有TLAB最大空间(一般是Eden大小)的期望比例,通过 EMA 算法采集预测

//以下是我们这里不用太关心的 field
HeapWord* _allocation_end; // TLAB 真正可以用来分配内存的结束地址,这个是 _end 结束地址排除保留空间,至于为何需要保留空间我们这里先不用关心,稍后我们会解释这个参数
HeapWord* _pf_top; // Allocation Prefetch CPU 缓存优化机制相关需要的参数,这里先不用考虑
size_t _allocated_before_last_gc; // GC统计数据采集相关,例如线程内存申请数据统计等等,这里先不用关心
unsigned _number_of_refills; // 线程分配内存数据采集相关,TLAB 剩余空间不足分配次数
unsigned _fast_refill_waste; // 线程分配内存数据采集相关,TLAB 快速分配浪费,什么是快速分配,待会会说到
unsigned _slow_refill_waste; // 线程分配内存数据采集相关,TLAB 慢速分配浪费,什么是慢速分配,待会会说到
unsigned _gc_waste; // 线程分配内存数据采集相关,gc浪费
unsigned _slow_allocations; // 线程分配内存数据采集相关,TLAB 慢速分配计数
size_t _allocated_size; //分配的内存大小
size_t _bytes_since_last_sample_point; // JVM TI 采集指标相关 field,这里不用关心

2. TLAB 初始化

首先是 JVM 启动的时候,全局 TLAB 需要初始化:src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

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
scss复制代码void ThreadLocalAllocBuffer::startup_initialization() {
//初始化,也就是归零统计数据
ThreadLocalAllocStats::initialize();

// 假设平均下来,GC 扫描的时候,每个线程当前的 TLAB 都有一半的内存被浪费,这个每个线程使用内存的浪费的百分比率(也就是 TLABWasteTargetPercent),也就是等于(注意,仅最新的那个 TLAB 有浪费,之前 refill 退回的假设是没有浪费的):1/2 * (每个 epoch 内每个线程期望 refill 次数) * 100
//那么每个 epoch 内每个线程 refill 次数配置就等于 50 / TLABWasteTargetPercent, 默认也就是 50 次。
_target_refills = 100 / (2 * TLABWasteTargetPercent);
// 但是初始的 _target_refills 需要设置最多不超过 2 次来减少 VM 初始化时候 GC 的可能性
_target_refills = MAX2(_target_refills, 2U);

//如果 C2 JIT 编译存在并启用,则保留 CPU 缓存优化 Allocation Prefetch 空间,这个这里先不用关心,会在别的章节讲述
#ifdef COMPILER2
if (is_server_compilation_mode_vm()) {
int lines = MAX2(AllocatePrefetchLines, AllocateInstancePrefetchLines) + 2;
_reserve_for_allocation_prefetch = (AllocatePrefetchDistance + AllocatePrefetchStepSize * lines) /
(int)HeapWordSize;
}
#endif

// 初始化 main 线程的 TLAB
guarantee(Thread::current()->is_Java_thread(), "tlab initialization thread not Java thread");
Thread::current()->tlab().initialize();
log_develop_trace(gc, tlab)("TLAB min: " SIZE_FORMAT " initial: " SIZE_FORMAT " max: " SIZE_FORMAT,
min_size(), Thread::current()->tlab().initial_desired_size(), max_size());
}

每个线程维护自己的 TLAB,同时每个线程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小,线程数量,还有线程的对象分配速率决定。 在 Java 线程开始运行时,会先分配 TLAB:src/hotspot/share/runtime/thread.cpp

1
2
3
4
5
javascript复制代码void JavaThread::run() {
// initialize thread-local alloc buffer related fields
this->initialize_tlab();
//剩余代码忽略
}

分配 TLAB 其实就是调用 ThreadLocalAllocBuffer 的 initialize 方法。 src/hotspot/share/runtime/thread.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码void initialize_tlab() {
//如果没有通过 -XX:-UseTLAB 禁用 TLAB,则初始化TLAB
if (UseTLAB) {
tlab().initialize();
}
}

// Thread-Local Allocation Buffer (TLAB) support
ThreadLocalAllocBuffer& tlab() {
return _tlab;
}

ThreadLocalAllocBuffer _tlab;

ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的我们要关心的各种 field:src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码void ThreadLocalAllocBuffer::initialize() {
//设置初始指针,由于还没有从 Eden 分配内存,所以这里都设置为 NULL
initialize(NULL, // start
NULL, // top
NULL); // end
//计算初始期望大小,并设置
set_desired_size(initial_desired_size());
//所有 TLAB 总大小,不同的 GC 实现有不同的 TLAB 容量, 一般是 Eden 区大小
//例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解为年轻代减去Survivor区,也就是Eden区
size_t capacity = Universe::heap()->tlab_capacity(thread()) / HeapWordSize;
//计算这个线程的 TLAB 期望占用所有 TLAB 总体大小比例
//TLAB 期望占用大小也就是这个 TLAB 大小乘以期望 refill 的次数
float alloc_frac = desired_size() * target_refills() / (float) capacity;
//记录下来,用于计算 EMA
_allocation_fraction.sample(alloc_frac);
//计算初始 refill 最大浪费空间,并设置
//如前面原理部分所述,初始大小就是 TLAB 的大小(_desired_size) / TLABRefillWasteFraction
set_refill_waste_limit(initial_refill_waste_limit());
//重置统计
reset_statistics();
}

2.1. 初始期望大小是如何计算的呢?

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

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
scss复制代码//计算初始大小
size_t ThreadLocalAllocBuffer::initial_desired_size() {
size_t init_sz = 0;
//如果通过 -XX:TLABSize 设置了 TLAB 大小,则用这个值作为初始期望大小
//表示堆内存占用大小都需要用占用几个 HeapWord 表示,所以用TLABSize / HeapWordSize
if (TLABSize > 0) {
init_sz = TLABSize / HeapWordSize;
} else {
//获取当前epoch内线程数量期望,这个如之前所述通过 EMA 预测
unsigned int nof_threads = ThreadLocalAllocStats::allocating_threads_avg();
//不同的 GC 实现有不同的 TLAB 容量,Universe::heap()->tlab_capacity(thread()) 一般是 Eden 区大小
//例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解为年轻代减去Survivor区,也就是Eden区
//整体大小等于 Eden区大小/(当前 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置)
//target_refills已经在 JVM 初始化所有 TLAB 全局配置的时候初始化好了
init_sz = (Universe::heap()->tlab_capacity(thread()) / HeapWordSize) /
(nof_threads * target_refills());
//考虑对象对齐,得出最后的大小
init_sz = align_object_size(init_sz);
}
//保持大小在 min_size() 还有 max_size() 之间
//min_size主要由 MinTLABSize 决定
init_sz = MIN2(MAX2(init_sz, min_size()), max_size());
return init_sz;
}

//最小大小由 MinTLABSize 决定,需要表示为 HeapWordSize,并且考虑对象对齐,最后的 alignment_reserve 是 dummy object 填充的对象头大小(这里先不考虑 JVM 的 CPU 缓存 prematch,我们会在其他章节详细分析)。
static size_t min_size() {
return align_object_size(MinTLABSize / HeapWordSize) + alignment_reserve();
}

2.2. TLAB 最大大小是怎样决定的呢?

不同的 GC 方式,有不同的方式:

G1 GC 中为大对象(humongous object)大小,也就是 G1 region 大小的一半:src/hotspot/share/gc/g1/g1CollectedHeap.cpp

1
2
3
4
5
arduino复制代码// For G1 TLABs should not contain humongous objects, so the maximum TLAB size
// must be equal to the humongous object limit.
size_t G1CollectedHeap::max_tlab_size() const {
return align_down(_humongous_object_threshold_in_words, MinObjAlignment);
}

ZGC 中为页大小的 8 分之一,类似的在大部分情况下 Shenandoah GC 也是每个 Region 大小的 8 分之一。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度: src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp

1
ini复制代码MaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords);

src/hotspot/share/gc/z/zHeap.cpp

1
ini复制代码const size_t      ZObjectSizeLimitSmall         = ZPageSizeSmall / 8;

对于其他的 GC,则是 int 数组的最大大小,这个和为了填充 dummy object 表示 TLAB 的空区域有关。这个原因之前已经说明了。

3. TLAB 分配内存

当 new 一个对象时,需要调用instanceOop InstanceKlass::allocate_instance(TRAPS)src/hotspot/share/oops/instanceKlass.cpp

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码instanceOop InstanceKlass::allocate_instance(TRAPS) {
bool has_finalizer_flag = has_finalizer(); // Query before possible GC
int size = size_helper(); // Query before forming handle.

instanceOop i;

i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
i = register_finalizer(i, CHECK_NULL);
}
return i;
}

其核心就是heap()->obj_allocate(this, size, CHECK_NULL)从堆上面分配内存:src/hotspot/share/gc/shared/collectedHeap.inline.hpp

1
2
3
4
arduino复制代码inline oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
ObjAllocator allocator(klass, size, THREAD);
return allocator.allocate();
}

使用全局的 ObjAllocator 实现进行对象内存分配:src/hotspot/share/gc/shared/memAllocator.cpp

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
scss复制代码oop MemAllocator::allocate() const {
oop obj = NULL;
{
Allocation allocation(*this, &obj);
//分配堆内存,继续看下面一个方法
HeapWord* mem = mem_allocate(allocation);
if (mem != NULL) {
obj = initialize(mem);
} else {
// The unhandled oop detector will poison local variable obj,
// so reset it to NULL if mem is NULL.
obj = NULL;
}
}
return obj;
}
HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
//如果使用了 TLAB,则从 TLAB 分配,分配代码继续看下面一个方法
if (UseTLAB) {
HeapWord* result = allocate_inside_tlab(allocation);
if (result != NULL) {
return result;
}
}
//否则直接从 tlab 外分配
return allocate_outside_tlab(allocation);
}
HeapWord* MemAllocator::allocate_inside_tlab(Allocation& allocation) const {
assert(UseTLAB, "should use UseTLAB");

//从当前线程的 TLAB 分配内存,TLAB 快分配
HeapWord* mem = _thread->tlab().allocate(_word_size);
//如果没有分配失败则返回
if (mem != NULL) {
return mem;
}

//如果分配失败则走 TLAB 慢分配,需要 refill 或者直接从 Eden 分配
return allocate_inside_tlab_slow(allocation);
}

3.1. TLAB 快分配

src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {
//验证各个内存指针有效,也就是 _top 在 _start 和 _end 范围内
invariants();
HeapWord* obj = top();
//如果空间足够,则分配内存
if (pointer_delta(end(), obj) >= size) {
set_top(obj + size);
invariants();
return obj;
}
return NULL;
}

3.2. TLAB 慢分配

src/hotspot/share/gc/shared/memAllocator.cpp

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
arduino复制代码HeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation& allocation) const {
HeapWord* mem = NULL;
ThreadLocalAllocBuffer& tlab = _thread->tlab();

// 如果 TLAB 剩余空间大于 最大浪费空间,则记录并让最大浪费空间递增
if (tlab.free() > tlab.refill_waste_limit()) {
tlab.record_slow_allocation(_word_size);
return NULL;
}

//重新计算 TLAB 大小
size_t new_tlab_size = tlab.compute_size(_word_size);
//TLAB 放回 Eden 区
tlab.retire_before_allocation();

if (new_tlab_size == 0) {
return NULL;
}

// 计算最小大小
size_t min_tlab_size = ThreadLocalAllocBuffer::compute_min_size(_word_size);
//分配新的 TLAB 空间,并在里面分配对象
mem = Universe::heap()->allocate_new_tlab(min_tlab_size, new_tlab_size, &allocation._allocated_tlab_size);
if (mem == NULL) {
assert(allocation._allocated_tlab_size == 0,
"Allocation failed, but actual size was updated. min: " SIZE_FORMAT
", desired: " SIZE_FORMAT ", actual: " SIZE_FORMAT,
min_tlab_size, new_tlab_size, allocation._allocated_tlab_size);
return NULL;
}
assert(allocation._allocated_tlab_size != 0, "Allocation succeeded but actual size not updated. mem at: "
PTR_FORMAT " min: " SIZE_FORMAT ", desired: " SIZE_FORMAT,
p2i(mem), min_tlab_size, new_tlab_size);
//如果启用了 ZeroTLAB 这个 JVM 参数,则将对象所有字段置零值
if (ZeroTLAB) {
// ..and clear it.
Copy::zero_to_words(mem, allocation._allocated_tlab_size);
} else {
// ...and zap just allocated object.
}

//设置新的 TLAB 空间为当前线程的 TLAB
tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size);
//返回分配的对象内存地址
return mem;
}
3.2.1 TLAB最大浪费空间

TLAB最大浪费空间 _refill_waste_limit 初始值为 TLAB 大小除以 TLABRefillWasteFraction:src/hotspot/share/gc/shared/threadLocalAllocBuffer.hpp

1
scss复制代码size_t initial_refill_waste_limit()            { return desired_size() / TLABRefillWasteFraction; }

每次慢分配,调用record_slow_allocation(size_t obj_size)记录慢分配的同时,增加 TLAB 最大浪费空间的大小:

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码void ThreadLocalAllocBuffer::record_slow_allocation(size_t obj_size) {
//每次慢分配,_refill_waste_limit 增加 refill_waste_limit_increment,也就是 TLABWasteIncrement
set_refill_waste_limit(refill_waste_limit() + refill_waste_limit_increment());
_slow_allocations++;
log_develop_trace(gc, tlab)("TLAB: %s thread: " INTPTR_FORMAT " [id: %2d]"
" obj: " SIZE_FORMAT
" free: " SIZE_FORMAT
" waste: " SIZE_FORMAT,
"slow", p2i(thread()), thread()->osthread()->thread_id(),
obj_size, free(), refill_waste_limit());
}
//refill_waste_limit_increment 就是 JVM 参数 TLABWasteIncrement
static size_t refill_waste_limit_increment() { return TLABWasteIncrement; }
3.2.2. 重新计算 TLAB 大小

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp_desired_size是什么时候变得呢?怎么变得呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码void ThreadLocalAllocBuffer::resize() {
assert(ResizeTLAB, "Should not call this otherwise");
//根据 _allocation_fraction 这个 EMA 采集得出平均数乘以Eden区大小,得出 TLAB 当前预测占用内存比例
size_t alloc = (size_t)(_allocation_fraction.average() *
(Universe::heap()->tlab_capacity(thread()) / HeapWordSize));
//除以目标 refill 次数就是新的 TLAB 大小,和初始化时候的结算方法差不多
size_t new_size = alloc / _target_refills;
//保证在 min_size 还有 max_size 之间
new_size = clamp(new_size, min_size(), max_size());

size_t aligned_new_size = align_object_size(new_size);

log_trace(gc, tlab)("TLAB new size: thread: " INTPTR_FORMAT " [id: %2d]"
" refills %d alloc: %8.6f desired_size: " SIZE_FORMAT " -> " SIZE_FORMAT,
p2i(thread()), thread()->osthread()->thread_id(),
_target_refills, _allocation_fraction.average(), desired_size(), aligned_new_size);
//设置新的 TLAB 大小
set_desired_size(aligned_new_size);
//重置 TLAB 最大浪费空间
set_refill_waste_limit(initial_refill_waste_limit());
}

那是什么时候调用 resize 的呢?一般是每次** GC 完成的时候**。大部分的 GC 都是在gc_epilogue方法里面调用,将每个线程的 TLAB 均 resize 掉。

4. TLAB 回收

TLAB 回收就是指线程将当前的 TLAB 丢弃回 Eden 区。TLAB 回收有两个时机:一个是之前提到的在分配对象时,剩余 TLAB 空间不足,在 TLAB 满但是浪费空间小于最大浪费空间的情况下,回收当前的 TLAB 并获取一个新的。另一个就是在发生 GC 时,其实更准确的说是在 GC 开始扫描时。不同的 GC 可能实现不一样,但是时机是基本一样的,这里以 G1 GC 为例:

src/hotspot/share/gc/g1/g1CollectedHeap.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码void G1CollectedHeap::gc_prologue(bool full) {
//省略其他代码

// Fill TLAB's and such
{
Ticks start = Ticks::now();
//确保堆内存是可以解析的
ensure_parsability(true);
Tickspan dt = Ticks::now() - start;
phase_times()->record_prepare_tlab_time_ms(dt.seconds() * MILLIUNITS);
}
//省略其他代码
}

为何要确保堆内存是可以解析的呢?这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码void CollectedHeap::ensure_parsability(bool retire_tlabs) {
//真正的 GC 肯定发生在安全点上,这个在后面安全点章节会详细说明
assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(),
"Should only be called at a safepoint or at start-up");

ThreadLocalAllocStats stats;
for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next();) {
BarrierSet::barrier_set()->make_parsable(thread);
//如果全局启用了 TLAB
if (UseTLAB) {
//如果指定要回收,则回收 TLAB
if (retire_tlabs) {
//回收 TLAB 其实就是将 ThreadLocalAllocBuffer 的堆内存指针 MarkWord 置为 NULL
thread->tlab().retire(&stats);
} else {
//当前如果不回收,则将 TLAB 填充 Dummy Object 利于解析
thread->tlab().make_parsable();
}
}
}

stats.publish();
}

TLAB 主要流程总结

image

image

image

image

JFR 对于 TLAB 的监控

根据上面的原理以及源代码分析,可以得知 TLAB 是 Eden 区的一部分,主要用于线程本地的对象分配。在 TLAB 满的时候分配对象内存,可能会发生两种处理:

  1. 线程获取新的 TLAB。老的 TLAB 回归 Eden,Eden进行管理,之后线程通过新的 TLAB 分配对象。
  2. 对象在 TLAB 外分配,也就 Eden 区。

对于 线程获取新的 TLAB 这种处理,也就是 refill,按照 TLAB 设计原理,这个是经常会发生的,每个 epoch 内可能会都会发生几次。但是对象直接在 Eden 区分配,是我们要避免的。JFR 对于

JFR 针对这两种处理有不同的事件可以监控。分别是jdk.ObjectAllocationOutsideTLAB和jdk.ObjectAllocationInNewTLAB。jdk.ObjectAllocationInNewTLAB对应 refill,这个一般我们没有监控的必要(在你没有修改默认的 TLAB 参数的前提下),用这个测试并学习 TLAB 的意义比监控的意义更大。jdk.ObjectAllocationOutsideTLAB对应对象直接在 Eden 区分配,是我们需要监控的。至于怎么不影响线上性能安全的监控,怎么查看并分析,怎么解决,以及测试生成这两个事件,会在下一节详细分析。

同时

本文转载自: 掘金

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

每日一面 - java里的wait()和sleep()的区别

发表于 2021-01-24

一句话总结:sleep方法是当前线程休眠,让出cpu,不释放锁,这是Thread的静态方法;wait方法是当前线程等待,释放锁,这是Object的方法。同时要注意,Java 14 之后引入的 inline class 是没有 wait 方法的

Sleep()原理

1
java复制代码public static native void sleep(long millis) throws InterruptedException;

sleep()是Thread中的static方法,也是native实现。就是调用底层的 sleep 函数实现:

1
2
3
4
5
6
7
arduino复制代码void THREAD_sleep(int seconds) {
#ifdef windows
Sleep(1000L * seconds);
#else
sleep(seconds);
#endif
}

linux的sleep函数参考 sleep: man7.org/linux/man-p…

wait(), notify(), notifyAll()

这些属于基本的Java多线程同步类的API,都是native实现:

1
2
3
java复制代码public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
public final native void notifyAll();

那么底层实现是怎么回事呢? 首先我们需要先明确JDK底层实现共享内存锁的基本机制。 每个Object都有一个ObjectMonitor,这个ObjectMonitor中包含三个特殊的数据结构,分别是CXQ(实际上是Contention List),EntryList还有WaitSet;一个线程在同一时间只会出现在他们三个中的一个中。首先来看下CXQ: 这里写图片描述一个尝试获取Object锁的线程,如果首次尝试(就是尝试CAS更新轻量锁)失败,那么会进入CXQ;进入的方法就是CAS更新CXQ指针指向自己,如果成功,自己的next指向剩余队列;CXQ是一个LIFO队列,设计成LIFO主要是为了:

  1. 进入CXQ队列后,每个线程先进入一段时间的spin自旋状态,尝试获取锁,获取失败的话则进入park状态。这个自旋的意义在于,假设锁的hold时间非常短,如果直接进入park状态的话,程序在用户态和系统态之间的切换会影响锁性能。这个spin可以减少切换;
  2. 进入spin状态如果成功获取到锁的话,需要出队列,出队列需要更新自己的头指针,如果位于队列前列,那么需要操作的时间会减少 但是,如果全部依靠这个机制,那么理所当然的,CAS更新队列头的操作会非常频繁。所以,引入了EntryList来减少争用: 这里写图片描述假设Thread A是当前锁的Owner,接下来他要释放锁了,那么如果EntryList为null并且cxq不为null,就会从cxq末尾取出一个线程,放入EntryList(注意,EntryList为双向队列),并且标记EntryList其中一个线程为Successor(一般是头节点,这个EntryList的大小可能大于一,一般在notify时,后面会说到),这个Successor接下来会进入spin状态尝试获取锁(注意,在第一次自旋过去后,之后线程一直处于park状态)。如果获取成功,则成为owner,否则,回到EntryList中。 这种利用两个队列减少争用的算法,可以参考: Michael Scott’s “2Q” algorithm 接下来,进入我们的正题,wait方法。如果一个线程成为owner后,执行了wait方法,则会进入WaitSet: Object.wait()底层实现

这里写图片描述

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
scss复制代码void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {

//检查线程合法性
Thread *const Self = THREAD;
assert(Self->is_Java_thread(), "Must be Java thread!");
JavaThread *jt = (JavaThread *) THREAD;
DeferredInitialize();
//检查当前线程是否拥有锁
CHECK_OWNER();
EventJavaMonitorWait event;
// 检查中断位
if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
if (JvmtiExport::should_post_monitor_waited()) {
JvmtiExport::post_monitor_waited(jt, this, false);
}
if (event.should_commit()) {
post_monitor_wait_event(&event, 0, millis, false);
}
TEVENT(Wait - ThrowIEX);
THROW(vmSymbols::java_lang_InterruptedException());
return;
}
TEVENT(Wait);
assert(Self->_Stalled == 0, "invariant");
Self->_Stalled = intptr_t(this);
jt->set_current_waiting_monitor(this);

//建立放入WaitSet中的这个线程的封装对象
ObjectWaiter node(Self);
node.TState = ObjectWaiter::TS_WAIT;
Self->_ParkEvent->reset();
OrderAccess::fence();
//用自旋方式获取操作waitset的lock,因为一般只有owner线程会操作这个waitset(无论是wait还是notify),所以竞争概率很小(除非响应interrupt事件才会有争用),采用spin方式效率高
Thread::SpinAcquire(&_WaitSetLock, "WaitSet - add");
//添加到waitset
AddWaiter(&node);
//释放锁,代表现在线程已经进入了waitset,接下来要park了
Thread::SpinRelease(&_WaitSetLock);

if ((SyncFlags & 4) == 0) {
_Responsible = NULL;
}
intptr_t save = _recursions; // record the old recursion count
_waiters++; // increment the number of waiters
_recursions = 0; // set the recursion level to be 1
exit(true, Self); // exit the monitor
guarantee(_owner != Self, "invariant");

// 确保没有unpark事件冲突影响本次park,方法就是主动post一次unpark
if (node._notified != 0 && _succ == Self) {
node._event->unpark();
}

// 接下来就是park操作了
。。。。。。。。。
。。。。。。。。。
}

当另一个owner线程调用notify时,根据Knob_MoveNotifyee这个值,决定将从waitset里面取出的一个线程放到哪里(cxq或者EntrySet)Object.notify()底层实现

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
ini复制代码void ObjectMonitor::notify(TRAPS) {
//检查当前线程是否拥有锁
CHECK_OWNER();
if (_WaitSet == NULL) {
TEVENT(Empty - Notify);
return;
}
DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);
//决定取出来的线程放在哪里
int Policy = Knob_MoveNotifyee;
//同样的,用自旋方式获取操作waitset的lock
Thread::SpinAcquire(&_WaitSetLock, "WaitSet - notify");
ObjectWaiter *iterator = DequeueWaiter();
if (iterator != NULL) {
TEVENT(Notify1 - Transfer);
guarantee(iterator->TState == ObjectWaiter::TS_WAIT, "invariant");
guarantee(iterator->_notified == 0, "invariant");
if (Policy != 4) {
iterator->TState = ObjectWaiter::TS_ENTER;
}
iterator->_notified = 1;
Thread *Self = THREAD;
iterator->_notifier_tid = Self->osthread()->thread_id();

ObjectWaiter *List = _EntryList;
if (List != NULL) {
assert(List->_prev == NULL, "invariant");
assert(List->TState == ObjectWaiter::TS_ENTER, "invariant");
assert(List != iterator, "invariant");
}

if (Policy == 0) { // prepend to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL;
_EntryList = iterator;
} else {
List->_prev = iterator;
iterator->_next = List;
iterator->_prev = NULL;
_EntryList = iterator;
}
} else if (Policy == 1) { // append to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL;
_EntryList = iterator;
} else {
// CONSIDER: finding the tail currently requires a linear-time walk of
// the EntryList. We can make tail access constant-time by converting to
// a CDLL instead of using our current DLL.
ObjectWaiter *Tail;
for (Tail = List; Tail->_next != NULL; Tail = Tail->_next);
assert(Tail != NULL && Tail->_next == NULL, "invariant");
Tail->_next = iterator;
iterator->_prev = Tail;
iterator->_next = NULL;
}
} else if (Policy == 2) { // prepend to cxq
// prepend to cxq
if (List == NULL) {
iterator->_next = iterator->_prev = NULL;
_EntryList = iterator;
} else {
iterator->TState = ObjectWaiter::TS_CXQ;
for (;;) {
ObjectWaiter *Front = _cxq;
iterator->_next = Front;
if (Atomic::cmpxchg_ptr(iterator, &_cxq, Front) == Front) {
break;
}
}
}
} else if (Policy == 3) { // append to cxq
iterator->TState = ObjectWaiter::TS_CXQ;
for (;;) {
ObjectWaiter *Tail;
Tail = _cxq;
if (Tail == NULL) {
iterator->_next = NULL;
if (Atomic::cmpxchg_ptr(iterator, &_cxq, NULL) == NULL) {
break;
}
} else {
while (Tail->_next != NULL) Tail = Tail->_next;
Tail->_next = iterator;
iterator->_prev = Tail;
iterator->_next = NULL;
break;
}
}
} else {
ParkEvent *ev = iterator->_event;
iterator->TState = ObjectWaiter::TS_RUN;
OrderAccess::fence();
ev->unpark();
}

if (Policy < 4) {
iterator->wait_reenter_begin(this);
}

// _WaitSetLock protects the wait queue, not the EntryList. We could
// move the add-to-EntryList operation, above, outside the critical section
// protected by _WaitSetLock. In practice that's not useful. With the
// exception of wait() timeouts and interrupts the monitor owner
// is the only thread that grabs _WaitSetLock. There's almost no contention
// on _WaitSetLock so it's not profitable to reduce the length of the
// critical section.
}
//释放waitset的lock
Thread::SpinRelease(&_WaitSetLock);

if (iterator != NULL && ObjectMonitor::_sync_Notifications != NULL) {
ObjectMonitor::_sync_Notifications->inc();
}
}

对于NotifyAll就很好推测了,这里不再赘述;

本文转载自: 掘金

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

利用SpirngBoot实现文件上传功能 零、本篇要点 一、

发表于 2021-01-24

零、本篇要点

  • 介绍SpringBoot对文件上传的自动配置。
  • 介绍MultipartFile接口。
  • 介绍SpringBoot+Thymeleaf文件上传demo的整合。
  • 介绍对文件类型,文件名长度等判断方法。

一、SpringBoot对文件处理相关自动配置

自动配置是SpringBoot为我们提供的便利之一,开发者可以在不作任何配置的情况下,使用SpringBoot提供的默认设置,如处理文件需要的MultipartResolver。

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
less复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {

private final MultipartProperties multipartProperties;

public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}

@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}

@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}

}
  • Spring3.1之后支持StandardServletMultipartResolver,且默认使用StandardServletMultipartResolver,它的优点在于:使用Servlet所提供的功能支持,不需要依赖任何其他的项目。
  • 想要自动配置生效,需要配置spring.servlet.multipart.enabled=true,当然这个配置默认就是true。
  • 相关的配置设置在MultipartProperties中,其中字段就是对应的属性设置,经典字段有:enabled:是否开启文件上传自动配置,默认开启。location:上传文件的临时目录。maxFileSize:最大文件大小,以字节为单位,默认为1M。maxRequestSize:整个请求的最大容量,默认为10M。fileSizeThreshold:文件大小达到该阈值,将写入临时目录,默认为0,即所有文件都会直接写入磁盘临时文件中。resolveLazily:是否惰性处理请求,默认为false。
  • 我们也可以自定义处理的细节,需要实现MultipartResolver接口。

二、处理上传文件MultipartFile接口

SpringBoot为我们提供了MultipartFile强大接口,让我们能够获取上传文件的详细信息,如原始文件名,内容类型等等,接口内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码public interface MultipartFile extends InputStreamSource {
String getName(); //获取参数名
@Nullable
String getOriginalFilename();//原始的文件名
@Nullable
String getContentType();//内容类型
boolean isEmpty();
long getSize(); //大小
byte[] getBytes() throws IOException;// 获取字节数组
InputStream getInputStream() throws IOException;//以流方式进行读取
default Resource getResource() {
return new MultipartFileResource(this);
}
// 将上传的文件写入文件系统
void transferTo(File var1) throws IOException, IllegalStateException;
// 写入指定path
default void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));
}
}

三、SpringBoot+Thymeleaf整合demo

1、编写控制器

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
java复制代码/**
* 文件上传
*
* @author Summerday
*/
@Controller
public class FileUploadController {

private static final String UPLOADED_FOLDER = System.getProperty("user.dir");

@GetMapping("/")
public String index() {
return "file";
}

@PostMapping("/upload")
public String singleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) throws IOException {

if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("msg", "文件为空,请选择你的文件上传");
return "redirect:uploadStatus";

}
saveFile(file);
redirectAttributes.addFlashAttribute("msg", "上传文件" + file.getOriginalFilename() + "成功");
redirectAttributes.addFlashAttribute("url", "/upload/" + file.getOriginalFilename());
return "redirect:uploadStatus";
}

private void saveFile(MultipartFile file) throws IOException {
Path path = Paths.get(UPLOADED_FOLDER + "/" + file.getOriginalFilename());
file.transferTo(path);
}

@GetMapping("/uploadStatus")
public String uploadStatus() {
return "uploadStatus";
}
}

2、编写页面file.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<html xmlns:th="http://www.thymeleaf.org">
<!--suppress ALL-->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传界面</title>
</head>
<body>
<div>
<form method="POST" enctype="multipart/form-data" action="/upload">
<table>
<tr><td><input type="file" name="file" /></td></tr>
<tr><td></td><td><input type="submit" value="上传" /></td></tr>
</table>
</form>

</div>
</body>
</html>

3、编写页面uploadStatus.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<!--suppress ALL-->
<html xmlns:th="http://www.thymeleaf.org">

<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传界面</title>
</head>
<body>
<div th:if="${msg}">
<h2 th:text="${msg}"/>
</div>
<div >
<img src="" th:src="${url}" alt="">
</div>
</body>
</html>

4、编写配置

1
2
3
ini复制代码server.port=8081
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

5、配置虚拟路径映射

这一步是非常重要的,我们将文件上传到服务器上时,我们需要将我们的请求路径和服务器上的路径进行对应,不然很有可能文件上传成功,但访问失败:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
public class MvcConfig implements WebMvcConfigurer {

private static final String UPLOADED_FOLDER = System.getProperty("user.dir");

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:///" + UPLOADED_FOLDER + "/");
}
}

对应关系需要自己去定义,如果访问失败,可以试着打印以下路径,看看是否缺失了路径分隔符。

注意:如果addResourceHandler不要写成处理/**,这样会拦截掉其他的请求

6、测试页面

执行mvn spring-boot:run,启动程序,访问http://localhost:8081/,选择文件,点击上传按钮,我们的项目目录下出现了mongo.jpg,并且页面也成功显示:

利用SpirngBoot实现文件上传功能

四、SpringBoot的Restful风格,返回url

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
arduino复制代码/**
* 文件上传
*
* @author Summerday
*/
@RestController
public class FileUploadRestController {

/**
* 文件名长度
*/
private static final int DEFAULT_FILE_NAME_LENGTH = 100;

/**
* 允许的文件类型
*/
private static final String[] ALLOWED_EXTENSIONS = {
"jpg", "img", "png", "gif"
};

/**
* 项目路径
*/
private static final String UPLOADED_FOLDER = System.getProperty("user.dir");

@PostMapping("/restUpload")
public Map<String,Object> singleFileUpload(@RequestParam("file") MultipartFile file) throws Exception {

if (file.isEmpty()) {
throw new Exception("文件为空!");
}
String filename = upload(file);
String url = "/upload/" + filename;
Map<String,Object> map = new HashMap<>(2);
map.put("msg","上传成功");
map.put("url",url);
return map;
}


/**
* 上传方法
*/
private String upload(MultipartFile file) throws Exception {
int len = file.getOriginalFilename().length();
if (len > DEFAULT_FILE_NAME_LENGTH) {
throw new Exception("文件名超出限制!");
}
String extension = getExtension(file);
if(!isValidExtension(extension)){
throw new Exception("文件格式不正确");
}
// 自定义文件名
String filename = getPathName(file);
// 获取file对象
File desc = getFile(filename);
// 写入file
file.transferTo(desc);
return filename;
}

/**
* 获取file对象
*/
private File getFile(String filename) throws IOException {
File file = new File(UPLOADED_FOLDER + "/" + filename);
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
if(!file.exists()){
file.createNewFile();
}
return file;
}

/**
* 验证文件类型是否正确
*/
private boolean isValidExtension(String extension) {
for (String allowedExtension : ALLOWED_EXTENSIONS) {
if(extension.equalsIgnoreCase(allowedExtension)){
return true;
}
}
return false;
}

/**
* 此处自定义文件名,uuid + extension
*/
private String getPathName(MultipartFile file) {
String extension = getExtension(file);
return UUID.randomUUID().toString() + "." + extension;
}

/**
* 获取扩展名
*/
private String getExtension(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
return originalFilename.substring(originalFilename.lastIndexOf('.') + 1);
}
}

看完三件事❤️

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

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

本文转载自: 掘金

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

聊聊 SpringBoot 自动装配原理

发表于 2021-01-23

本文已经收录进 Github 95k+ Star 的Java项目JavaGuide 。JavaGuide项目地址 : github.com/Snailclimb/… 。

作者:Miki-byte-1024 & Snailclimb

每次问到 Spring Boot, 面试官非常喜欢问这个问题:“讲述一下 SpringBoot 自动装配原理?”。

我觉得我们可以从以下几个方面回答:

  1. 什么是 SpringBoot 自动装配?
  2. SpringBoot 是如何实现自动装配的?如何实现按需加载?
  3. 如何实现一个 Starter?

篇幅问题,这篇文章并没有深入,小伙伴们也可以直接使用 debug 的方式去看看 SpringBoot 自动装配部分的源代码。

前言

使用过 Spring 的小伙伴,一定有被 XML 配置统治的恐惧。即使 Spring 后面引入了基于注解的配置,我们在开启某些 Spring 特性或者引入第三方依赖的时候,还是需要用 XML 或 Java 进行显式配置。

举个例子。没有 Spring Boot 的时候,我们写一个 RestFul Web 服务,还首先需要进行如下配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Configuration
public class RESTConfiguration
{
@Bean
public View jsonTemplate() {
MappingJackson2JsonView view = new MappingJackson2JsonView();
view.setPrettyPrint(true);
return view;
}

@Bean
public ViewResolver viewResolver() {
return new BeanNameViewResolver();
}
}

spring-servlet.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context/ http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc/ http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<context:component-scan base-package="com.howtodoinjava.demo" />
<mvc:annotation-driven />

<!-- JSON Support -->
<bean name="viewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean name="jsonTemplate" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>

</beans>

但是,Spring Boot 项目,我们只需要添加相关依赖,无需配置,通过启动下面的 main 方法即可。

1
2
3
4
5
6
java复制代码@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

并且,我们通过 Spring Boot 的全局配置文件 application.properties或application.yml即可对项目进行设置比如更换端口号,配置 JPA 属性等等。

为什么 Spring Boot 使用起来这么酸爽呢? 这得益于其自动装配。自动装配可以说是 Spring Boot 的核心,那究竟什么是自动装配呢?

什么是 SpringBoot 自动装配?

我们现在提到自动装配的时候,一般会和 Spring Boot 联系在一起。但是,实际上 Spring Framework 早就实现了这个功能。Spring Boot 只是在其基础上,通过 SPI 的方式,做了进一步优化。

SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。

没有 Spring Boot 的情况下,如果我们需要引入第三方依赖,需要手动配置,非常麻烦。但是,Spring Boot 中,我们直接引入一个 starter 即可。比如你想要在项目中使用 redis 的话,直接在项目中引入对应的 starter 即可。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

引入 starter 之后,我们通过少量注解和一些简单的配置就能使用第三方组件提供的功能了。

在我看来,自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。

SpringBoot 是如何实现自动装配的?

我们先看一下 SpringBoot 的核心注解 SpringBootApplication 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
<1.>@SpringBootConfiguration
<2.>@ComponentScan
<3.>@EnableAutoConfiguration
public @interface SpringBootApplication {

}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration //实际上它也是一个配置类
public @interface SpringBootConfiguration {
}

大概可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
  • @ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除TypeExcludeFilter和AutoConfigurationExcludeFilter。

@EnableAutoConfiguration 是实现自动装配的重要注解,我们以这个注解入手。

@EnableAutoConfiguration:实现自动装配的核心注解

EnableAutoConfiguration 只是一个简单地注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector类。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //作用:将main包下的所欲组件注册到容器中
@Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

我们现在重点分析下AutoConfigurationImportSelector 类到底做了什么?

AutoConfigurationImportSelector:加载自动装配类

AutoConfigurationImportSelector类的继承体系如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

}

public interface DeferredImportSelector extends ImportSelector {

}

public interface ImportSelector {
String[] selectImports(AnnotationMetadata var1);
}

可以看出,AutoConfigurationImportSelector 类实现了 ImportSelector接口,也就实现了这个接口中的 selectImports方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private static final String[] NO_IMPORTS = new String[0];

public String[] selectImports(AnnotationMetadata annotationMetadata) {
// <1>.判断自动装配开关是否打开
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
//<2>.获取所有需要装配的bean
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}

这里我们需要重点关注一下getAutoConfigurationEntry()方法,这个方法主要负责加载自动配置类的。

该方法调用链如下:

现在我们结合getAutoConfigurationEntry()的源码来详细分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
//<1>.
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//<2>.
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//<3>.
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//<4>.
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}

第 1 步:

判断自动装配开关是否打开。默认spring.boot.enableautoconfiguration=true,可在 application.properties 或 application.yml 中设置

第 2 步 :

用于获取EnableAutoConfiguration注解中的 exclude 和 excludeName。

第 3 步

获取需要自动装配的所有配置类,读取META-INF/spring.factories

1
bash复制代码spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

从下图可以看到这个文件的配置内容都被我们读取到了。XXXAutoConfiguration的作用就是按需加载组件。

不光是这个依赖下的META-INF/spring.factories被读取到,所有 Spring Boot Starter 下的META-INF/spring.factories都会被读取到。

所以,你可以清楚滴看到, druid 数据库连接池的 Spring Boot Starter 就创建了META-INF/spring.factories文件。

如果,我们自己要创建一个 Spring Boot Starter,这一步是必不可少的。

第 4 步 :

到这里可能面试官会问你:“spring.factories中这么多配置,每次启动都要全部加载么?”。

很明显,这是不现实的。我们 debug 到后面你会发现,configurations 的值变小了。

因为,这一步有经历了一遍筛选,@ConditionalOnXXX 中的所有条件都满足,该类才会生效。

1
2
3
4
5
6
7
8
java复制代码@Configuration
// 检查相关的类:RabbitTemplate 和 Channel是否存在
// 存在才会加载
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
}

有兴趣的童鞋可以详细了解下 Spring Boot 提供的条件注解

  • @ConditionalOnBean:当容器里有指定 Bean 的条件下
  • @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下
  • @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
  • @ConditionalOnClass:当类路径下有指定类的条件下
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下
  • @ConditionalOnProperty:指定的属性是否有指定的值
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnExpression:基于 SpEL 表达式作为判断条件
  • @ConditionalOnJava:基于 Java 版本作为判断条件
  • @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
  • @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
  • @ConditionalOnWebApplication:当前项目是 Web 项 目的条件下

如何实现一个 Starter

光说不练假把式,现在就来撸一个 starter,实现自定义线程池

第一步,创建threadpool-spring-boot-starter工程

第二步,引入 Spring Boot 相关依赖

第三步,创建ThreadPoolAutoConfiguration

第四步,在threadpool-spring-boot-starter工程的 resources 包下创建META-INF/spring.factories文件

最后新建工程引入threadpool-spring-boot-starter

测试通过!!!

总结

Spring Boot 通过@EnableAutoConfiguration开启自动装配,通过 SpringFactoriesLoader 最终加载META-INF/spring.factories中的自动配置类实现自动装配,自动配置类其实就是通过@Conditional按需加载的配置类,想要其生效必须引入spring-boot-starter-xxx包实现起步依赖

文章结尾

我是 Guide 哥,一 Java 后端开发,会一点前端,自由的少年。我们下期再见!微信搜“JavaGuide”回复“面试突击”领取我整理的 4 本原创PDF

本文转载自: 掘金

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

go-zero教程——服务划分与项目创建

发表于 2021-01-23

在正式创建项目之前,我们还需要重新梳理一下各服务之间的划分。

我将该项目命名为 foodguides

foodguides 项目下有两大块功能,用户管理 usermanage、食物管理 foodmanage。

分析用户管理 usermanage

用户管理 usermanage 下同样也具有两大块服务,api 服务和 rpc 服务。

api 服务需要对外提供三个 api 接口。

  • login: 用户登录接口
  • register:用户注册接口
  • userinfo:用户信息接口

rpc 服务需要对外提供三个接口。

  • login: 用户登录接口
  • register:用户注册接口
  • userinfo:用户信息接口

虽然两块服务都提供能相同的功能,但是这里我们需要区分两块服务的服务对象,api 服务是对外的,比如我们这里是 app 调用了 api 服务。 rpc 服务是对内的,比如 api 服务调用了 rpc 服务。

  • 如果把 foodguides 比作一个餐厅的话,那 api 服务相当于是服务员,rpc 服务相当于是厨师。
  • 服务员是直接服务客人的,客人可以找服务员说我要一份沙茶面。( app 客户端调用了 api 服务 login 接口)
  • 厨师是服务于服务人员的,接到客人订单后告诉厨师要一份沙茶面。( api 服务 login 接口调用了 rpc 服务 login 接口)
  • 厨师做好沙茶面后交给服务员,服务员再给客人上菜。这样就完成了一次点餐服务。( rpc 服务 login 接口处理完数据后后响应 api 服务 login 接口, api 服务 login 接口响应 app 客户端的调用,登录成功)

这样就完成了一次服务的调用。

食物管理 foodmanage 也是同理。现有这一层的理解,对于新手来说能更好理清楚项目结构。

项目创建

在Coding中创建一个项目,选择 DevOps 模板,命名 FoodGuides

file

在 FoodGuides 项目中,选择代码仓库,创建一个新的代码仓库,命名 FoodGuides

在 FoodGuides 项目中,选择制品管理——制品仓库,创建一个新的制品仓库,类型选择 Docker,仓库地址命名 dockerimages。

file

在 FoodGuides 项目中,选择代码仓库,在仓库设置中复制 git 地址,clone 到本地。

cd 到在 FoodGuides

1
go复制代码go mod init FoodGuides

在 FoodGuides 创建两个文件夹

1
go复制代码mkdir -p usermanage & mkdir -p foodmanage

这样我们就初步完成了项目的创建。

上一篇 《go-zero教程—— 工具下载与环境搭建》

下一篇 《go-zero教程——用户管理API Gateway》

本文转载自: 掘金

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

SpringBoot开发日志注解记录字段变更内容

发表于 2021-01-23

正在公司摸鱼的我突然接到了大佬给我的一个人任务,开发一个日志注解,来记录当方法中的每一个参数的名字,并记录每次的参数修改的值信息。接到任务的我瑟瑟发抖。

1. 数据库选择MongoDb


因为MongoDb具有以下特点

  • MongoDb为文档型数据库。操作起来比较简单和容易。
  • Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
  • MongoDb采用Bson(类json的一种二进制形式的存储格式)存储数据,跟新和查询的速度很快
    当然了MongoDb还有很多优点我就不一一赘述了,大家可以去官网看看文档。

2. 开发实体类保存到数据库中


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
typescript复制代码@Data
public class SysLogEntity implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

// 用户名
private String username;

// 用户操作
private String operation;

// 请求方法
private String method;

// 请求参数
private String params;

// 执行时长(毫秒)
private Long time;

// IP地址
private String ip;

// 创建时间
private Date createDate;

//所有参数名称 以逗号分割
private String parametersName;

//修改内容
private String modifyContent;

//操作类型
private String operationType;

//key值
private String logKey;

}
  • 在数据库中保存的数据格式大体是这样的

3. 思考问题


当时我在做这一个功能的时候在想,你无法确定方法的入参到底是什么类型,有可能为实体类型,有可能为Map类型,有可能为String类型,如果参数为实体类型的需要去进行反射获取其中的所有字段,这是一个很耗时的操作,那么我为什么不拿出来在SpringBoot初始化的时候来做这个事情呢,于是就有了下面的操作。

  • 首先创建一个实体类用来保存当前方法中的参数名,参数类型,字段值,和当前字段值对应的具体位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ruby复制代码@Data
public class MethodParametersInfo {
/**
* 参数名称
*/
String parameterName;
/**
* 参数类型
*/
Class<?> classType;
/**
*字段
*/
Field[] fields;
/**
* 字段
*/
Integer position;
}
  • 开发日志注解,
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
less复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModifyLog {
/**
* 方法描述
* @return
*/
String value() default "";

/**
* 方法类型
* @return
*/
LogTypeEnum type();
/**
* @return 是否需要默认的改动比较
*/
boolean needDefaultCompare() default false;

/**
* key值可以根据spel表达式来填写
* @return
*/
String key();

}
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
arduino复制代码public enum LogTypeEnum {
/**
* 保存
*/
Save("save"),
/**
* 修改
*/
Update("update"),
/**
* 删除
*/
Delete("delete"),
/**
* 保存或修改
*/
SaveOrUpdate("saveOrUpdate");

LogTypeEnum(String key){
this.key = key;
}

public String getKey() {
return key;
}


private final String key;


}
  • 将实体类需要保存的字段进行细分,于是便又开发了一个注解,用来确定实体类中具体需要把那些字段信息保存到日志中
1
2
3
4
5
6
7
8
9
10
less复制代码@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DataName {
/**
* @return 字段名称
*/
String name() default "";

}
  • 在Springboot进行初始化的时候把方法中的参数进行缓存,若参数为实体类,进行反射获取其中所有的字段属性
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
ini复制代码@Component
public class ModifyLogInitialization {

@Autowired
private RequestMappingHandlerMapping mapping;

public static Map<String,Map<String, MethodParametersInfo>> modifyLogMap = new HashMap<>();


/**
* @Author: lin
* @Description: 初始化Controller层上带有 @ModifyLog 注解的方法 缓存到map中
* @DateTime: 2020/12/25 15:52
* @Params: [event]
* @Return void
*/
@EventListener
public void initializationMethod(WebServerInitializedEvent event){


//获取所有方法
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

handlerMethods.forEach((k,v) -> {
//判断Controller层方法上注解
if(v.getMethodAnnotation(ModifyLog.class)!=null){
Class<?> beanType = v.getBeanType();
//获取方法对象
Method method = v.getMethod();
//方法参数
MethodParameter[] parameters = v.getMethodParameters();
//参数名作为key 缓存参数信息
HashMap<String, MethodParametersInfo> methodMap = new HashMap<>();
//类路径加方法名作为key
String methodKey = beanType.getName()+"."+method.getName()+"()";
modifyLogMap.put(methodKey,methodMap);

int i = 0;
//循环方法参数
for (MethodParameter parameter : parameters) {
MethodParametersInfo info = new MethodParametersInfo();
//参数位置
info.setPosition(i);
//参数名称
String parameterName = parameter.getParameter().getName();
info.setParameterName(parameterName);
//参数类型
Class<?> parameterType = parameter.getParameterType();
info.setClassType(parameterType);

//获取所有字段
Field[] fields = parameterType.getDeclaredFields();
if(!parameterType.isAssignableFrom(String.class) & !parameterType.isAssignableFrom(Map.class)){
info.setFields(fields);
}
//加入到Map中
methodMap.put(parameterName,info);
i++;
}
}

});
}

}

用监听的方式监听WebServerInitializedEvent在启动的时候做缓存,RequestMappingHandlerMapping可以获取所有Controllec层标注@RequestMapping的方法

4. 用Aop来保存参数内容


  • 可以用spel表达式加入到注解中,这样就可以在aop中去解析获取关键值,可以参考 juejin.cn/post/684490… 篇文章
  • 为了免去使用if和else来判断参数类型我使用了适配器模式,并把每一个参数类的解析抽出取来,这样当你添加不同参数类型的解析的时候可以避免代码的侵入性
  • 下面是一个类型判断的接口,可以判断当前参数的类型,并且去调用当前参数类型的解析器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public interface TypeAdapter {
/**
* @Author: lin
* @Description: 判断支持类型
* @DateTime: 2021/1/4 9:16
* @Params: [classType]
* @Return boolean
*/
boolean supprot(Class<?> classType);

/**
* @Author: lin
* @Description: 获取文本
* @DateTime: 2021/1/4 9:16
* @Params: [sb, k, v, oldObjectList, newObjectList]
* @Return void
*/
void getContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList);
  • 这是map的类型判断并且实现map类型的参数解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码@Component
public class ClassTypeAdapter implements TypeAdapter {

@Autowired
ContentParse classParse;

@Override
public boolean supprot(Class<?> classType) {
return !classType.isAssignableFrom(String.class) && !classType.isAssignableFrom(Map.class);
}

@Override
public void getContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList) {
classParse.getDifferentContent(sb,k,v,oldObjectList,newObjectList);
}
}
  • 下面是一个参数类型的解析接口
1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public interface ContentParse {

/**
* @Author: lin
* @Description: 根据字段不同类型进行解析
* @DateTime: 2020/12/28 15:13
* @Params: [sb, k, v]
* @Return void
*/
void getDifferentContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList,List<Object> newObjectList);

}
  • 参数类型解析接口的实现
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
scss复制代码@Component("classParse")
public class ClassParse implements ContentParse {
@Override
public void getDifferentContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList) {
//获取所有字段
Field[] fields = v.getFields();
//根据字段名称跟字段映射成Map
Map<String, List<Field>> fieldMap = Arrays.stream(fields).collect(Collectors.groupingBy(Field::getName));
//记录位置
Integer position = v.getPosition();
//取出当前位置的 参数对应的值
Object oldObject = oldObjectList.get(position);
Object newObject = newObjectList.get(position);
//转换为Map
Map<String, Object> oldMap = JSONUtil.parseObj(oldObject);
Map<String, Object> newMap = JSONUtil.parseObj(newObject);
oldMap.forEach((oldK,oldV) -> {
Object newV = newMap.get(oldK);
if(!newV.equals(oldV)){
//值不相等,根据当前字段名(k值)取出当前字段
List<Field> fieldList = fieldMap.get(oldK);
Field field = fieldList.get(0);
//有没有DataName注解
if(field.isAnnotationPresent(DataName.class)){
sb.append("[参数: ").append(k)
.append("中属性").append(field.getName()).append("]从[")
.append(oldV).append("]改为了[").append(newV).append("];");
}
}
});
}
}
  • 之后我们就可以写一个Util类用来获取数据库中根据当前类路径跟方法名所对应的参数值,并跟当前传的新值作比较,判断那个参数的值有修改
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
typescript复制代码@Component
public class ModifyLogUtil {

/**
* @Author: lin
* @Description: 获取参数名称保存到数据库中以逗号分割
* @DateTime: 2020/12/24 14:46
* @Params: [methodKey]
* @Return java.lang.String
*/

@Autowired
private List<TypeAdapter> typeAdapterList;

public String getParametersName(String methodKey){
// 类路径跟方法名获取参数信息
Map<String, Map<String, MethodParametersInfo>> modifyLogMap = ModifyLogInitialization.modifyLogMap;
//参数信息 key是参数名 value参数信息
Map<String, MethodParametersInfo> parameterMap = modifyLogMap.get(methodKey);

StringBuilder sb = new StringBuilder();

if(parameterMap != null){
parameterMap.forEach((k,v) ->{
sb.append(k);
sb.append(",");
});
sb.deleteCharAt(sb.length()-1);
}

return sb.toString();
}

/**
* @Author: lin
* @Description: 根据参数名称获取参数信息进行比对
* @DateTime: 2020/12/24 15:05
* @Params: [methodKey, parameter]
* @Return java.lang.String
*/
public String getContentName(String methodKey, String params, ProceedingJoinPoint joinPoint){
//解析旧数据
List<Object> oldObjectList = parseOldObject(params);
//解析新数据
List<Object> newObjectList = parseNewObject(joinPoint);

// 类路径跟方法名获取参数信息
Map<String, Map<String, MethodParametersInfo>> modifyLogMap = ModifyLogInitialization.modifyLogMap;
//参数信息 key是参数名 value参数信息
Map<String, MethodParametersInfo> parameterMap = modifyLogMap.get(methodKey);

//记录
StringBuilder sb = new StringBuilder();
parameterMap.forEach((k,v) ->{
Class<?> classType = v.getClassType();

typeAdapterList.stream().filter(typeAdapter -> typeAdapter.supprot(classType)).findFirst()
.get().getContent(sb,k,v,oldObjectList,newObjectList);
});
log.info("参数改变的值为: {}",sb.toString());
return sb.toString();
}
/**
* @Author: lin
* @Description: 解析旧参数
* @DateTime: 2020/12/25 14:02
* @Params: [params]
* @Return java.util.List<java.lang.Object>
*/
public List<Object> parseOldObject(String params){
JSONArray array = JSONUtil.parseArray(params);
return new ArrayList<>(array);
}

/**
* @Author: lin
* @Description: 解析新参数
* @DateTime: 2020/12/25 14:02
* @Params: [joinPoint]
* @Return java.util.List<java.lang.Object>
*/
public List<Object> parseNewObject(ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
return new ArrayList<>(Arrays.asList(args));
}
}
  • 接下来就是在aop中使用这个util工具
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
154
155
156
157
158
159
scss复制代码@Slf4j
@Aspect
@Component
public class SysLogAspect {

@Autowired
private KeyResolver keyResolver;

@Autowired
private ModifyLogUtil modifyLogUtil;

@Autowired
MongoTemplate mongoTemplate;




@Around("@annotation(modifyLog)")
public Object modifyLogAround(ProceedingJoinPoint point,ModifyLog modifyLog) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = point.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;

saveUpdateSysLog(point,time,modifyLog);

return result;
}


/**
* @Author: lin
* @Description: 当方法为Update类型的时候要获取其中不同项
* @DateTime: 2020/12/22 15:51
* @Params: [joinPoint, time]
* @Return void
*/
private void saveUpdateSysLog(ProceedingJoinPoint joinPoint, long time,ModifyLog modifyLog){
//日志实体类
SysLogEntity currentSysLogEntity = new SysLogEntity();
currentSysLogEntity.setId(SnowflakeUtil.snowflakeId());

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取key值
String key = keyResolver.resolver(modifyLog, joinPoint);
currentSysLogEntity.setLogKey(key);
log.info("key值{}",key);

//日志类型
String logType = modifyLog.type().getKey();
// 注解上的描述
currentSysLogEntity.setOperation(modifyLog.value());
//操作类型
currentSysLogEntity.setOperationType(logType);
//设置实体类字段
commonMethod(joinPoint, time, signature, currentSysLogEntity);
//根据类名加方法名去缓存中查找
String parametersName = modifyLogUtil.getParametersName(currentSysLogEntity.getMethod());
//方法中参数名称
currentSysLogEntity.setParametersName(parametersName);
//当前类型为save则保存
if(Constant.MODIFY_LOG_SAVE.equals(logType)){
mongoTemplate.insert(currentSysLogEntity);
return;
}
//当前类型为删除记录删除的字段值
if(Constant.MODIFY_LOG_DELETE.equals(logType)){
Object[] args = joinPoint.getArgs();
String deleteParam = JSONUtil.toJsonStr(args);
currentSysLogEntity.setModifyContent("当前删除的参数信息是:" + deleteParam);
mongoTemplate.insert(currentSysLogEntity);
return;
}
//当前类型为保存或修改
if(Constant.MODIFY_LOG_SAVE_OR_UPDATE.equals(logType)){

SysLogEntity oldSysLogEntity = getEntity(currentSysLogEntity);
//没有查询到就插入信息
if(oldSysLogEntity == null){
mongoTemplate.insert(currentSysLogEntity);
return;
}
//有信息则判断是否需要插入修改字段的信息
if(modifyLog.needDefaultCompare()){
String content = modifyLogContent(joinPoint,oldSysLogEntity);
//设置修改字段属性
currentSysLogEntity.setModifyContent(content);
}
mongoTemplate.insert(currentSysLogEntity);
}
}


/**
* @Author: lin
* @Description: 设置实体类属性公用方法
* @DateTime: 2020/12/22 16:07
* @Params: [joinPoint, time, signature, sysLogEntity]
* @Return void
*/
private void commonMethod(ProceedingJoinPoint joinPoint, long time, MethodSignature signature, SysLogEntity sysLogEntity) {
//请求的类名
String className = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = signature.getName();
sysLogEntity.setMethod(className + "." + methodName + "()");

//请求的参数
Object[] args = joinPoint.getArgs();

String argsStr = JSONUtil.toJsonStr(args);

//保存参数
sysLogEntity.setParams(argsStr);
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 设置IP地址
sysLogEntity.setIp(IPUtils.getIpAddr(request));

//用户名
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
sysLogEntity.setUsername(username);
sysLogEntity.setTime(time);
sysLogEntity.setCreateDate(new Date());
}

/**
* @Author: lin
* @Description: 是update操作则进行比对判断是那些 字段值有改变
* @DateTime: 2020/12/24 14:55
* @Params: []
* @Return void
*/
private String modifyLogContent(ProceedingJoinPoint joinPoint,SysLogEntity oldSysLogEntity){

//实际参数
String params = oldSysLogEntity.getParams();
//取出修改的参数属性
String contentName = modifyLogUtil.getContentName(oldSysLogEntity.getMethod(), params, joinPoint);

return contentName;
}

private SysLogEntity getEntity(SysLogEntity sysLogEntity){
Query query = new Query();
Criteria criteria = new Criteria();
criteria.and("logKey").is(sysLogEntity.getLogKey());
criteria.and("method").is(sysLogEntity.getMethod());
query.addCriteria(criteria);
//根据日期排序
query.with(Sort.by(Sort.Order.desc("createDate")));
query.limit(1);
SysLogEntity one = mongoTemplate.findOne(query, SysLogEntity.class);
return one;

}

}

5. 小结


  • 作为一个刚入门半年的菜鸟我还是又很多地方需要学习,这个方法肯定还能在进一步的优化,希望各位大佬能给指点一下。希望大家共同进步。

本文转载自: 掘金

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

如何让你的简历在众多候选人的简历中脱颖而出?

发表于 2021-01-23

简历是面试官对候选人的第一印象。简历写的好坏,直接影响到你是否能通过简历筛选。毫不夸张的说,简历好坏对最终是否能拿到Offer起到50%的决定性作用,甚至,很多面试官在看完候选人简历之后,就已经形成是否要录用的潜意识了,之后的面试只不过是找证据来确凿自己内心的这个潜意识而已。

我工作有10多年,大厂、小厂、创业公司都待过,从在Google开始就做技术面试官,零零总总面试过几百人,简历看过上千份,各种层次的候选人都见过,也算是一个有资历的面试官。既然简历如此重要,所以本篇文章,我就结合自己的经验,聊一聊如何写出一份漂亮的求职简历?

01 格式紧凑,结构清晰

尽管对于应聘研发岗来说,简历不需要做的很花哨,更不需要封面和照片,但起码要做到结构清晰。最好写得跟“八股文”一样,能让面试官一眼就能看清简历主要包含哪些部分,每个部分的起止在哪里。

一般来讲,简历主要包含这样几个方面:个人联系方式、技术能力概括、工作经历、项目经历,教育背景、其他成绩(比如竞赛、论文、奖励、开源项目和技术分享)、自我评价等。当然,有些不是必选项,比如其他成绩部分。

自我评价可写可不写,如果非要写的话,最好不要超过一行。太主观的自我评价,一般面试官都一眼带过,写太多反倒是个干扰项。比如下面这个个人评价就是一个反面教材,还不如写成:“自学能力强、听话、皮实、吃苦耐劳”,一行就能搞定。

简历格式尽量紧凑,不要浪费简历中的空白位置,每行都有充足的信息。比如下面这样的描述就不够紧凑,没写多少信息就占用了10几行版面。”公司名称“、”工作时间“、”职位“、”部门“等完全可以放到一行来写,“项目名称”、“项目时间“也可以直接写在一行,除此之外,”公司名称“、”职位“、“项目经验”、“项目描述”等这些小标题都可以不写。

相反,下面这个简历就特别紧凑,各个模块起止清晰,一目了然。

02 重点突出,切忌流水账

这一条主要针对工作年限比较久的候选人。我曾经见过,有的候选人的简历写超过10页,项目零零总总罗列几十个,个人评价写得跟散文一样,占大半页纸。HR每天要阅读很多简历,技术面试官的工作也比较忙。如果简历写得跟流水账一样,我们很难期望HR和面试官会认真读完每一行。

总之,简历不能写太长,对于应届生来说,一页纸就足够了,最多不超过两页。对于项目经历比较丰富的社招候选人来说,4页简历是上限,尽量压缩在在2~3页内。毕竟简历的主要目的并不是要扒候选人老底,而只是希望候选人提供证明自己能力、匹配职位的证据。

如果项目很多,我们可以选择有技术含量、体现技术能力的来详细写,其他的可以简写或者不写,一句带过就好。相反,如果罗列了太多没有技术含量的项目,反倒会带来不好的影响,面试官会觉得候选人尽管工作时间很长,但大部分时间都在做些没有技术含量的项目,5年的工作经历可能只能顶别人2年的工作积累。所以,这也应征了那句话:少即是多!

除此之外,我们还要把简历中的亮点,最能证明自己能力的证据(竞赛、论文、专利、star多的开源项目等等),在简历中突出出来(比如列在单独的一个模块里),不要淹没在平淡的项目描述中。不要期望面试官在简历中找你的亮点,而是将亮点主动放在最显眼的地方,让他一眼就看到,并且印象深刻。

03 亮点是一份简历的含金量

现在的竞争越来越激烈,一个职位往往有很多人投递简历,特别是大厂,更是千里挑一、万里挑一。如何在众多的候选人中脱颖而出,一眼就被HR或面试官相中呢?这就需要我们的简历不能过于平淡,要有一些区别于大部分人的亮点。

这里我重点讲下什么是亮点,你可以对照着自己的经历挖掘一下。

首先,对于候选人来讲,大厂、知名企业的工作经历是很重要的能力背书,俗称硬通货、敲门砖。所以,如果想要职场上有更好的发展,有段知名公司的工作经历,哪怕只是渡下金,也是很有必要的,就业的起点会高很多。

其次,好的教育背景也是简历的亮点,比如清北复交毕业,特别是对于应届生来说,这点就极为重要。毕竟在走向社会之前,大部分人还都是白纸,教育背景就是少之又少的能区分候选人的东西。虽然都说做技术学历不重要,但一般来说,好学校的学生对计算机基础知识、通识知识掌握的更好,学习能力更强,更自律更努力。他们在工作中的执行力和快速交付能力往往更高,工作中的表现更优秀。如果没有好的教育背景,那也别灰心,努力积累其他亮点,争取让面试官可以因为其他亮点而忽略这个短板。

再次,比较有技术含量的项目经历也是亮点。所谓技术含金量高,就是做的东西复杂、用到的技术高深、解决的问题比较有难度。比如,候选人简历中写着“负责百万用户的电商交易系统的设计与实现”。仅仅这一句话,我们就可以解读出很多东西。

第一,“负责”两个字,说明候选人有独当一面的能力,招进来可以直接用,不用手把手教。

第二,“百万用户”这几个字,说明候选人做的系统有性能压力,性能驱动技术。

第三,“电商交易系统”这几个字,说明候选人做的系统比较复杂。毕竟电商系统本身就是一个比较复杂的系统,而交易系统又是其中最核心的。

第四,“设计与实现”这几个字,说明候选人有一定的架构能力,而不只是照着别人的设计做代码实现。

如果项目没啥技术含金量,那也没关系。因为有的时候,公司做什么项目,也不是我们技术人能决定的。如果既没有含金量的项目,也没有知名公司的工作经历,面试官就会看,候选人有没有其他东西,可以证明技术不错。比如,”某某竞赛获奖“、”发表过论文专利“、“阅读过某某源码”、“维护star较多的开源项目”、”给某某著名开源项目提交过代码“、”有高质量的技术博客“等等。这些都可以作为简历中的亮点。

04 像打磨产品一样打磨简历

这点我觉得很重要。不管是产品、技术,还是代码,都是一个迭代优化的过程。写简历也不例外。第一遍写完,可能并不会很好,这个时候,千万别立马就拿它去投递职位了,而是需要多打磨几遍。

人的主观性是很强的,刚写完简历之后,你会觉得不错,实际上,等过几天,再来看的时候,就会发现有很多可以继续优化、没有考虑全面的地方。所以,我一般都是在找工作前1个月、甚至几个月,就开始准备简历了。先写好第一版草稿,然后,每过一段时间,再去读一下,就会发现又很多值得优化的地方。有的时候,脑袋里突然想到某个技术点,我也会再补充到简历中。又或者看到别人对某个技术点有更专业的描述方法,我也会去借鉴修改自己的简历。

给自己充分的时间,去迭代、优化自己的简历,而不是写完就觉得完事了。当你修改几遍简历之后,你会发现,初稿跟最终稿的差距会非常大。这是我的一个真实的体会,你可以实践一下,看是否有用。

除此之外,简历也要适度包装,当然,这不是教你去造假,而是用更加专业的方式去描述项目、描述技术。从我的个人作为面试官看简历的经历来看,即便是做同样的项目,每个人写出来的简历也相差很大,有的人善于发现、善于总结,能挖掘出项目的难点,能很好的总结项目中用到的技术点。而有的人,明明项目挺有难度的,写出来却平平淡淡,难点、技术点也总结不到位,这就有点吃亏了。

所以,多借鉴一下大牛的简历,看看人家是如何用更专业的术语来描述项目的,或者找身边的牛人帮你改下简历,这能让简历增色不少。有些”平淡无奇“的简历有可能因此就变得”牛逼哄哄“。

05 最后,总结

招聘就像相亲大会,而简历就像一个人的外表,尽管内涵很重要,但能在短暂的接触后脱颖而出,获得进一步交往的资格,外表起着非常重要的作用。实际上,大部分面试,特别是校招,面试官都是捋着简历来问问题的,所以,简历写好,也有益于面试发挥。

作者王争,前Google工程师,15万人订阅的《数据结构与算法之美》《设计模式之美》作者。微信公众号:小争哥,关注微信公众号回复PDF,获取100+页Google工程师的算法学习和面试经验分享。

本文转载自: 掘金

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

别再问我们用什么画图的了!问就是excalidraw

发表于 2021-01-23

每次发 github.com/tal-tech/go… 相关文章时,都会有读者问我们用什么画图的。

这图什么工具画的呀?好看!

这个手绘风格真好看,用啥工具画的呀?

可不可以介绍下这个画图的工具?

诸如此类的问题,所以我决定写篇短文介绍下我们最常用的画图工具 excalidraw.com/

我们手绘风格的流程图、架构图等等都是通过 excalidraw.com/ 画的

  • 一个开源免费的画图软件
  • 个人目前看到的最舒服的画图软件
  • 支持多人协作

一些经验分享

  • 要把图形和文字放到一起拖动或者缩放,按住 shift 键一起选中,然后右键点击 group selection
  • 导出图片的四周空白(margin)太小不美观,官方拒绝解决(也没找到合适的默认margin尺寸),我们可以不使用导出图片,而是直接截屏,就可以控制margin大小了
  • 不要截屏保存图片后要记得保存 .excalidraw 文件,这样如有需要就可以直接修改了
  • 我们一般用 Code 风格的 Font family

再给大家几张我们画的图,让大家感受下 excalidraw.com/ 的魅力

  • 系统架构图
  • 原理讲解

  • 流程图

声明:这是个开源软件,我也没发现商业版,也没付过钱,也不认识作者(貌似国外的)。纯属用的舒服+问的人多,才简单写了这篇文章哈。非广告,也没收过任何费用,跟excalidraw.com官方无关,个人行为哈,好工具共分享而已


我为大家整理了 go-zero 作者去年广受好评的分享视频,详细分享了 go-zero 的设计理念和最佳实践。关注公众号「微服务实践」,回复 视频 获取;还可以回复 进群 和数千 go-zero 使用者交流学习。

本文转载自: 掘金

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

1…730731732…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%