2023年3月

本文已经收录到Github仓库,该仓库包含
计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享
等核心知识点,欢迎star~

Github地址:
https://github.com/Tyson0314/Java-learning


事务的四大特性?

事务特性ACID

原子性

Atomicity
)、
一致性

Consistency
)、
隔离性

Isolation
)、
持久性

Durability
)。

  • 原子性
    是指事务包含的所有操作要么全部成功,要么全部失败回滚。
  • 一致性
    是指一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账之后无论成功还是失败,它们的账户总和还是1000。
  • 隔离性
    。跟隔离级别相关,如
    read committed
    ,一个事务只能读到已经提交的修改。
  • 持久性
    是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

数据库的三大范式

第一范式1NF

确保数据库表字段的原子性。

比如字段
userInfo
:
广东省 10086'
,依照第一范式必须拆分成
userInfo
:
广东省
userTel
:
10086
两个字段。

第二范式2NF

首先要满足第一范式,另外包含两部分内容,一是表必须有一个主键;二是非主键列必须完全依赖于主键,而不能只依赖于主键的一部分。

举个例子。假定选课关系表为
student_course
(student_no, student_name, age, course_name, grade, credit),主键为(student_no, course_name)。其中学分完全依赖于课程名称,姓名年龄完全依赖学号,不符合第二范式,会导致数据冗余(学生选n门课,姓名年龄有n条记录)、插入异常(插入一门新课,因为没有学号,无法保存新课记录)等问题。

应该拆分成三个表:学生:
student
(stuent_no, student_name, 年龄);课程:
course
(course_name, credit);选课关系:
student_course_relation
(student_no, course_name, grade)。

第三范式3NF

首先要满足第二范式,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。

假定学生关系表为Student(student_no, student_name, age, academy_id, academy_telephone),主键为"学号",其中学院id依赖于学号,而学院地点和学院电话依赖于学院id,存在传递依赖,不符合第三范式。

可以把学生关系表分为如下两个表:学生:(student_no, student_name, age, academy_id);学院:(academy_id, academy_telephone)。

2NF和3NF的区别?

  • 2NF依据是非主键列是否完全依赖于主键,还是依赖于主键的一部分。
  • 3NF依据是非主键列是直接依赖于主键,还是直接依赖于非主键。

事务隔离级别有哪些?

先了解下几个概念:脏读、不可重复读、幻读。

  • 脏读
    是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
  • 不可重复读
    是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。
  • 幻读
    是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录。对幻读的正确理解是一个事务内的读取操作的结论不能支撑之后业务的执行。假设事务要新增一条记录,主键为id,在新增之前执行了select,没有发现id为xxx的记录,但插入时出现主键冲突,这就属于幻读,读取不到记录却发现主键冲突是因为记录实际上已经被其他的事务插入了,但当前事务不可见。

不可重复读和脏读的区别
是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

事务隔离就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。

MySQL数据库为我们提供的四种隔离级别:

  • Serializable
    (串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。
  • Repeatable read
    (可重复读):MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。
  • Read committed
    (读已提交):一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。
  • Read uncommitted
    (读未提交):所有事务都可以看到其他未提交事务的执行结果。

查看隔离级别:

select @@transaction_isolation;

设置隔离级别:

set session transaction isolation level read uncommitted;

生产环境数据库一般用的什么隔离级别呢?

生产环境大多使用RC
。为什么不是RR呢?

可重复读(Repeatable Read),简称为RR
读已提交(Read Commited),简称为RC

缘由一:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!
缘由二:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行!

也就是说,RC的并发性高于RR。

并且大部分场景下,不可重复读问题是可以接受的。毕竟数据都已经提交了,读出来本身就没有太大问题!

互联网项目中mysql应该选什么事务隔离级别

编码和字符集的关系

我们平时可以在编辑器上输入各种中文英文字母,但这些都是给人读的,不是给计算机读的,其实计算机真正保存和传输数据都是以
二进制
0101的格式进行的。

那么就需要有一个规则,把中文和英文字母转化为二进制。其中d对应十六进制下的64,它可以转换为01二进制的格式。于是字母和数字就这样一一对应起来了,这就是
ASCII编码
格式。

它用
一个字节
,也就是
8位
来标识字符,基础符号有128个,扩展符号也是128个。也就只能表示下
英文字母和数字

这明显不够用。于是,为了标识
中文
,出现了
GB2312
的编码格式。为了标识
希腊语
,出现了
greek
编码格式,为了标识
俄语
,整了
cp866
编码格式。

为了统一它们,于是出现了
Unicode编码格式
,它用了2~4个字节来表示字符,这样理论上所有符号都能被收录进去,并且它还完全兼容ASCII的编码,也就是说,同样是字母d,在ASCII用64表示,在Unicode里还是用64来表示。


不同的地方是ASCII编码用1个字节来表示,而Unicode用则两个字节来表示。

同样都是字母d,unicode比ascii多使用了一个字节,如下:

D   ASCII:           01100100
D Unicode:  00000000 01100100

可以看到,上面的unicode编码,前面的都是0,其实用不上,但还占了个字节,有点浪费。如果我们能做到该隐藏时隐藏,这样就能省下不少空间,按这个思路,就是就有了
UTF-8编码

总结一下,按照一定规则把符号和二进制码对应起来,这就是
编码
。而把n多这种已经编码的字符聚在一起,就是我们常说的
字符集

比如utf-8字符集就是所有utf-8编码格式的字符的合集。

想看下mysql支持哪些字符集。可以执行
show charset;

utf8和utf8mb4的区别

上面提到utf-8是在unicode的基础上做的优化,既然unicode有办法表示所有字符,那utf-8也一样可以表示所有字符,为了避免混淆,我在后面叫它
大utf8

mysql支持的字符集中有utf8和utf8mb4。

先说
utf8mb4
编码,mb4就是
most bytes 4
的意思,从上图最右边的
Maxlen
可以看到,它最大支持用
4个字节
来表示字符,它几乎可以用来表示目前已知的所有的字符。

再说mysql字符集里的
utf8
,它是数据库的
默认字符集
。但注意,
此utf8非彼utf8
,我们叫它
小utf8
字符集。为什么这么说,因为从Maxlen可以看出,它最多支持用3个字节去表示字符,按utf8mb4的命名方式,准确点应该叫它
utf8mb3

utf8 就像是阉割版的utf8mb4,只支持部分字符。比如
emoji
表情,它就不支持。

而mysql支持的字符集里,第三列,
collation
,它是指
字符集的比较规则

比如,"debug"和"Debug"是同一个单词,但它们大小写不同,该不该判为同一个单词呢。

这时候就需要用到collation了。

通过
SHOW COLLATION WHERE Charset = 'utf8mb4';
可以查看到
utf8mb4
下支持什么比较规则。

如果
collation = utf8mb4_general_ci
,是指使用utf8mb4字符集的前提下,
挨个字符进行比较

general
),并且不区分大小写(
_ci,case insensitice
)。

这种情况下,"debug"和"Debug"是同一个单词。

如果改成
collation=utf8mb4_bin
,就是指
挨个比较二进制位大小

于是"debug"和"Debug"就不是同一个单词。

那utf8mb4对比utf8有什么劣势吗?

我们知道数据库表里,字段类型如果是
char(2)
的话,里面的
2
是指
字符个数
,也就是说
不管这张表用的是什么编码的字符集
,都能放上2个字符。

而char又是
固定长度
,为了能放下2个utf8mb4的字符,char会默认保留
2*4(maxlen=4)= 8
个字节的空间。

如果是utf8mb3,则会默认保留
2 * 3 (maxlen=3) = 6
个字节的空间。也就是说,在这种情况下,
utf8mb4会比utf8mb3多使用一些空间。

索引

什么是索引?

索引是存储引擎用于提高数据库表的访问速度的一种
数据结构
。它可以比作一本字典的目录,可以帮你快速找到对应的记录。

索引一般存储在磁盘的文件中,它是占用物理空间的。

索引的优缺点?

优点:

  • 加快数据查找的速度
  • 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
  • 加快表与表之间的连接

缺点:

  • 建立索引需要
    占用物理空间
  • 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行
    动态维护索引
    ,导致增删改时间变长

索引的作用?

数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。

什么情况下需要建索引?

  1. 经常用于查询的字段
  2. 经常用于连接的字段建立索引,可以加快连接的速度
  3. 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度

什么情况下不建索引?

  1. where
    条件中用不到的字段不适合建立索引
  2. 表记录较少。比如只有几百条数据,没必要加索引。
  3. 需要经常增删改。需要评估是否适合加索引
  4. 参与列计算
    的列不适合建索引
  5. 区分度不高
    的字段不适合建立索引,如性别,只有男/女/未知三个值。加了索引,查询效率也不会提高。

索引的数据结构

索引的数据结构主要有B+树和哈希表,对应的索引分别为B+树索引和哈希索引。InnoDB引擎的索引类型有B+树索引和哈希索引,默认的索引类型为B+树索引。

B+树索引

B+ 树是基于B 树和叶子节点顺序访问指针进行实现,它具有B树的平衡性,并且通过顺序访问指针来提高区间查询的性能。

在 B+ 树中,节点中的
key
从左到右递增排列,如果某个指针的左右相邻
key
分别是 key
i
和 key
i+1
,则该指针指向节点的所有
key
大于等于 key
i
且小于等于 key
i+1

进行查找操作时,首先在根节点进行二分查找,找到
key
所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出
key
所对应的数据项。

MySQL 数据库使用最多的索引类型是
BTREE
索引,底层基于B+树数据结构来实现。

mysql> show index from blog\G;
*************************** 1. row ***************************
        Table: blog
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: blog_id
    Collation: A
  Cardinality: 4
     Sub_part: NULL
       Packed: NULL
         Null:
   Index_type: BTREE
      Comment:
Index_comment:
      Visible: YES
   Expression: NULL

哈希索引

哈希索引是基于哈希表实现的,对于每一行数据,存储引擎会对索引列进行哈希计算得到哈希码,并且哈希算法要尽量保证不同的列值计算出的哈希码值是不同的,将哈希码的值作为哈希表的key值,将指向数据行的指针作为哈希表的value值。这样查找一个数据的时间复杂度就是O(1),一般多用于精确查找。

Hash索引和B+树索引的区别?

  • 哈希索引
    不支持排序
    ,因为哈希表是无序的。
  • 哈希索引
    不支持范围查找
  • 哈希索引
    不支持模糊查询
    及多列索引的最左前缀匹配。
  • 因为哈希表中会
    存在哈希冲突
    ,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。

为什么B+树比B树更适合实现数据库索引?

  • 由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。

  • B+树的节点只存储索引key值,具体信息的地址存在于叶子节点的地址中。这就使以页为单位的索引中可以存放更多的节点。减少更多的I/O支出。

  • B+树的查询效率更加稳定,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

索引有什么分类?

1、
主键索引
:名为primary的唯一非空索引,不允许有空值。

2、
唯一索引
:索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是:唯一索引字段可以为null且可以存在多个null值,而主键索引字段不可以为null。唯一索引的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。创建唯一索引的SQL语句如下:

ALTER TABLE table_name
ADD CONSTRAINT constraint_name UNIQUE KEY(column_1,column_2,...);

3、
组合索引
:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时需遵循最左前缀原则。

4、
全文索引
:只能在
CHAR

VARCHAR

TEXT
类型字段上使用全文索引。

5、
普通索引
:普通索引是最基本的索引,它没有任何限制,值可以为空。

什么是最左匹配原则?

如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(
>

<

between

like
)就会停止匹配,后面的字段不会用到索引。


(a,b,c)
建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。


(a,b,c,d)
建立索引,查询条件为
a = 1 and b = 2 and c > 3 and d = 4
,那么a、b和c三个字段能用到索引,而d无法使用索引。因为遇到了范围查询。

如下图,对(a, b) 建立索引,a 在索引树中是全局有序的,而 b 是全局无序,局部有序(当a相等时,会根据b进行排序)。直接执行
b = 2
这种查询条件无法使用索引。

最左前缀

当a的值确定的时候,b是有序的。例如
a = 1
时,b值为1,2是有序的状态。当
a = 2
时候,b的值为1,4也是有序状态。 当执行
a = 1 and b = 2
时a和b字段能用到索引。而执行
a > 1 and b = 2
时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段无法使用索引。

什么是聚集索引?

InnoDB使用表的主键构造主键索引树,同时叶子节点中存放的即为整张表的记录数据。聚集索引叶子节点的存储是逻辑上连续的,使用双向链表连接,叶子节点按照主键的顺序排序,因此对于主键的排序查找和范围查找速度比较快。

聚集索引的叶子节点就是整张表的行记录。InnoDB 主键使用的是聚簇索引。聚集索引要比非聚集索引查询效率高很多。

对于
InnoDB
来说,聚集索引一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为
NULL
的唯一索引。如果没有主键也没有合适的唯一索引,那么
InnoDB
内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键长度为6个字节,它的值会随着数据的插入自增。

什么是覆盖索引?

select
的数据列只用从索引中就能够取得,不需要
回表
进行二次查询,也就是说查询列要被所使用的索引覆盖。对于
innodb
表的二级索引,如果索引能覆盖到查询的列,那么就可以避免对主键索引的二次查询。

不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值,而哈希索引、全文索引不存储索引列的值,所以MySQL使用b+树索引做覆盖索引。

对于使用了覆盖索引的查询,在查询前面使用
explain
,输出的extra列会显示为
using index

比如
user_like
用户点赞表,组合索引为
(user_id, blog_id)

user_id

blog_id
都不为
null

explain select blog_id from user_like where user_id = 13;

explain
结果的
Extra
列为
Using index
,查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过
索引查找
就能直接找到符合条件的数据,不需要回表查询数据。

explain select user_id from user_like where blog_id = 1;

explain
结果的
Extra
列为
Using where; Using index
, 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过
索引扫描
找到符合条件的数据,也不需要回表查询数据。

索引的设计原则?

  • 对于经常作为查询条件的字段,应该建立索引,以提高查询速度
  • 为经常需要排序、分组和联合操作的字段建立索引
  • 索引列的
    区分度越高
    ,索引的效果越好。比如使用性别这种区分度很低的列作为索引,效果就会很差。
  • 避免给"大字段"建立索引。尽量使用数据量小的字段作为索引。因为
    MySQL
    在维护索引的时候是会将字段值一起维护的,那这样必然会导致索引占用更多的空间,另外在排序的时候需要花费更多的时间去对比。
  • 尽量使用
    短索引
    ,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,查询速度更快。
  • 索引不是越多越好,每个索引都需要额外的物理空间,维护也需要花费时间。
  • 频繁增删改的字段不要建立索引。假设某个字段频繁修改,那就意味着需要频繁的重建索引,这必然影响MySQL的性能
  • 利用
    最左前缀原则

索引什么时候会失效?

导致索引失效的情况:

  • 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引
  • 以%开头的like查询如
    %abc
    ,无法使用索引;非%开头的like查询如
    abc%
    ,相当于范围查询,会使用索引
  • 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效
  • 判断索引列是否不等于某个值时
  • 对索引列进行运算
  • 查询条件使用
    or
    连接,也会导致索引失效

什么是前缀索引?

有时需要在很长的字符列上创建索引,这会造成索引特别大且慢。使用前缀索引可以避免这个问题。

前缀索引是指对文本或者字符串的前几个字符建立索引,这样索引的长度更短,查询速度更快。

创建前缀索引的关键在于选择足够长的前缀以
保证较高的索引选择性
。索引选择性越高查询效率就越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的数据行。

建立前缀索引的方式:

// email列创建前缀索引
ALTER TABLE table_name ADD KEY(column_name(prefix_length));

索引下推

参考我的另一篇文章:
图解索引下推!

常见的存储引擎有哪些?

MySQL中常用的四种存储引擎分别是:
MyISAM

InnoDB

MEMORY

ARCHIVE
。MySQL 5.5版本后默认的存储引擎为
InnoDB

InnoDB存储引擎

InnoDB是MySQL
默认的事务型存储引擎
,使用最广泛,基于聚簇索引建立的。InnoDB内部做了很多优化,如能够自动在内存中创建自适应hash索引,以加速读操作。

优点
:支持事务和崩溃修复能力;引入了行级锁和外键约束。

缺点
:占用的数据空间相对较大。

适用场景
:需要事务支持,并且有较高的并发读写频率。

MyISAM存储引擎

数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。MyISAM会将表存储在两个文件中,数据文件
.MYD
和索引文件
.MYI

优点
:访问速度快。

缺点
:MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,也不支持外键。

适用场景
:对事务完整性没有要求;表的数据都会只读的。

MEMORY存储引擎

MEMORY引擎将数据全部放在内存中,访问速度较快,但是一旦系统奔溃的话,数据都会丢失。

MEMORY引擎默认使用哈希索引,将键的哈希值和指向数据行的指针保存在哈希索引中。

优点
:访问速度较快。

缺点

  1. 哈希索引数据不是按照索引值顺序存储,无法用于排序。
  2. 不支持部分索引匹配查找,因为哈希索引是使用索引列的全部内容来计算哈希值的。
  3. 只支持等值比较,不支持范围查询。
  4. 当出现哈希冲突时,存储引擎需要遍历链表中所有的行指针,逐行进行比较,直到找到符合条件的行。

ARCHIVE存储引擎

ARCHIVE存储引擎非常适合存储大量独立的、作为历史记录的数据。ARCHIVE提供了压缩功能,拥有高效的插入速度,但是这种引擎不支持索引,所以查询性能较差。

MyISAM和InnoDB的区别?

  1. 存储结构的区别
    。每个MyISAM在磁盘上存储成三个文件。文件的名字以表的名字开始,扩展名指出文件类型。 .frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。索引文件的扩展名是.MYI (MYIndex)。InnoDB所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。
  2. 存储空间的区别
    。MyISAM支持支持三种不同的存储格式:静态表(默认,但是注意数据末尾不能有空格,会被去掉)、动态表、压缩表。当表在创建之后并导入数据之后,不会再进行修改操作,可以使用压缩表,极大的减少磁盘的空间占用。InnoDB需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。
  3. 可移植性、备份及恢复
    。MyISAM数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。对于InnoDB,可行的方案是拷贝数据文件、备份 binlog,或者用mysqldump,在数据量达到几十G的时候就相对麻烦了。
  4. 是否支持行级锁
    。MyISAM 只支持表级锁,用户在操作myisam表时,select,update,delete,insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。而InnoDB 支持行级锁和表级锁,默认为行级锁。行锁大幅度提高了多用户并发操作的性能。
  5. 是否支持事务和崩溃后的安全恢复
    。 MyISAM 不提供事务支持。而InnoDB 提供事务支持,具有事务、回滚和崩溃修复能力。
  6. 是否支持外键
    。MyISAM不支持,而InnoDB支持。
  7. 是否支持MVCC
    。MyISAM不支持,InnoDB支持。应对高并发事务,MVCC比单纯的加锁更高效。
  8. 是否支持聚集索引
    。MyISAM不支持聚集索引,InnoDB支持聚集索引。
  9. 全文索引
    。MyISAM支持 FULLTEXT类型的全文索引。InnoDB不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。
  10. 表主键
    。MyISAM允许没有任何索引和主键的表存在,索引都是保存行的地址。对于InnoDB,如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见)。
  11. 表的行数
    。MyISAM保存有表的总行数,如果
    select count(*) from table
    ;会直接取出该值。InnoDB没有保存表的总行数,如果使用select count(*) from table;就会遍历整个表,消耗相当大,但是在加了where条件后,MyISAM和InnoDB处理的方式都一样。

MySQL有哪些锁?

按锁粒度分类
,有行级锁、表级锁和页级锁。

  1. 行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁的类型主要有三类:
    • Record Lock,记录锁,也就是仅仅把一条记录锁上;
    • Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
    • Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
  2. 表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。
  3. 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。

按锁级别分类
,有共享锁、排他锁和意向锁。

  1. 共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
  2. 排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
  3. 意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB 中的两个表锁:

意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁;

意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。

意向锁是 InnoDB 自动加的,不需要用户干预。

对于INSERT、UPDATE和DELETE,InnoDB 会自动给涉及的数据加排他锁;对于一般的SELECT语句,InnoDB 不会加任何锁,事务可以通过以下语句显式加共享锁或排他锁。

共享锁:
SELECT … LOCK IN SHARE MODE;

排他锁:
SELECT … FOR UPDATE;

MVCC 实现原理?

MVCC(
Multiversion concurrency control
) 就是同一份数据保留多版本的一种方式,进而实现并发控制。在查询的时候,通过
read view
和版本链找到对应版本的数据。

作用:提升并发性能。对于高并发场景,MVCC比行级锁开销更小。

MVCC 实现原理如下:

MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。

  • DB_TRX_ID
    :当前事务id,通过事务id的大小判断事务的时间顺序。
  • DB_ROLL_PTR
    :回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成
    undo log
    版本链。
  • DB_ROW_ID
    :主键,如果数据表没有主键,InnoDB会自动生成主键。

每条表记录大概是这样的:

使用事务更新行记录的时候,就会生成版本链,执行过程如下:

  1. 用排他锁锁住该行;
  2. 将该行原本的值拷贝到
    undo log
    ,作为旧版本用于回滚;
  3. 修改当前行的值,生成一个新版本,更新事务id,使回滚指针指向旧版本的记录,这样就形成一条版本链。

下面举个例子方便大家理解。

1、初始数据如下,其中
DB_ROW_ID

DB_ROLL_PTR
为空。

2、事务A对该行数据做了修改,将
age
修改为12,效果如下:

3、之后事务B也对该行记录做了修改,将
age
修改为8,效果如下:

4、此时undo log有两行记录,并且通过回滚指针连在一起。

接下来了解下read view的概念。

read view
可以理解成将数据在每个时刻的状态拍成“照片”记录下来。在获取某时刻t的数据时,到t时间点拍的“照片”上取数据。


read view
内部维护一个活跃事务链表,表示生成
read view
的时候还在活跃的事务。这个链表包含在创建
read view
之前还未提交的事务,不包含创建
read view
之后提交的事务。

不同隔离级别创建read view的时机不同。

  • read committed:每次执行select都会创建新的read_view,保证能读取到其他事务已经提交的修改。

  • repeatable read:在一个事务范围内,第一次select时更新这个read_view,以后不会再更新,后续所有的select都是复用之前的read_view。这样可以保证事务范围内每次读取的内容都一样,即可重复读。

read view的记录筛选方式

前提

DATA_TRX_ID
表示每个数据行的最新的事务ID;
up_limit_id
表示当前快照中的最先开始的事务;
low_limit_id
表示当前快照中的最慢开始的事务,即最后一个事务。

  • 如果
    DATA_TRX_ID
    <
    up_limit_id
    :说明在创建
    read view
    时,修改该数据行的事务已提交,该版本的记录可被当前事务读取到。
  • 如果
    DATA_TRX_ID
    >=
    low_limit_id
    :说明当前版本的记录的事务是在创建
    read view
    之后生成的,该版本的数据行不可以被当前事务访问。此时需要通过版本链找到上一个版本,然后重新判断该版本的记录对当前事务的可见性。
  • 如果
    up_limit_id
    <=
    DATA_TRX_ID
    <
    low_limit_i

    1. 需要在活跃事务链表中查找是否存在ID为
      DATA_TRX_ID
      的值的事务。
    2. 如果存在,因为在活跃事务链表中的事务是未提交的,所以该记录是不可见的。此时需要通过版本链找到上一个版本,然后重新判断该版本的可见性。
    3. 如果不存在,说明事务trx_id 已经提交了,这行记录是可见的。

总结
:InnoDB 的
MVCC
是通过
read view
和版本链实现的,版本链保存有历史版本记录,通过
read view
判断当前版本的数据是否可见,如果不可见,再从版本链中找到上一个版本,继续进行判断,直到找到一个可见的版本。

快照读和当前读

表记录有两种读取方式。

  • 快照读:读取的是快照版本。普通的
    SELECT
    就是快照读。通过mvcc来进行并发控制的,不用加锁。

  • 当前读:读取的是最新版本。
    UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE
    是当前读。

快照读情况下,InnoDB通过
mvcc
机制避免了幻读现象。而
mvcc
机制无法避免当前读情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。

下面举个例子说明下:

1、首先,user表只有两条记录,具体如下:

2、事务a和事务b同时开启事务
start transaction

3、事务a插入数据然后提交;

insert into user(user_name, user_password, user_mail, user_state) values('tyson', 'a', 'a', 0);

4、事务b执行全表的update;

update user set user_name = 'a';

5、事务b然后执行查询,查到了事务a中插入的数据。(下图左边是事务b,右边是事务a。事务开始之前只有两条记录,事务a插入一条数据之后,事务b查询出来是三条数据)

以上就是当前读出现的幻读现象。

那么MySQL是如何避免幻读?

  • 在快照读情况下,MySQL通过
    mvcc
    来避免幻读。
  • 在当前读情况下,MySQL通过
    next-key
    来避免幻读(加行锁和间隙锁来实现的)。

next-key包括两部分:行锁和间隙锁。行锁是加在索引上的锁,间隙锁是加在索引之间的。

Serializable
隔离级别也可以避免幻读,会锁住整张表,并发性极低,一般不会使用。

共享锁和排他锁

SELECT 的读取锁定主要分为两种方式:共享锁和排他锁。

select * from table where id<6 lock in share mode;--共享锁
select * from table where id<6 for update;--排他锁

这两种方式主要的不同在于
LOCK IN SHARE MODE
多个事务同时更新同一个表单时很容易造成死锁。

申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。在进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程对这些数据的更改或删除操作会被阻塞(只能读操作),直到该语句的事务被
commit
语句或
rollback
语句结束为止。

SELECT... FOR UPDATE
使用注意事项:

  1. for update
    仅适用于innodb,且必须在事务范围内才能生效。
  2. 根据主键进行查询,查询条件为
    like
    或者不等于,主键字段产生
    表锁
  3. 根据非索引字段进行查询,会产生
    表锁

bin log/redo log/undo log

MySQL日志主要包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等。其中比较重要的是
bin log
(二进制日志)和
redo log
(重做日志)和
undo log
(回滚日志)。

bin log

bin log
是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会记录select和show语句,主要用于恢复数据库和同步数据库。

redo log

redo log
是innodb引擎级别,用来记录innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。当数据库发生故障,innoDB存储引擎会使用
redo log
恢复到发生故障前的时刻,以此来保证数据的完整性。将参数
innodb_flush_log_at_tx_commit
设置为1,那么在执行commit时会将
redo log
同步写到磁盘。

undo log

除了记录
redo log
外,当进行数据修改时还会记录
undo log

undo log
用于数据的撤回操作,它保留了记录修改前的内容。通过
undo log
可以实现事务回滚,并且可以根据
undo log
回溯到某个特定的版本的数据,
实现MVCC

bin log和redo log有什么区别?

  1. bin log
    会记录所有日志记录,包括InnoDB、MyISAM等存储引擎的日志;
    redo log
    只记录innoDB自身的事务日志。
  2. bin log
    只在事务提交前写入到磁盘,一个事务只写一次;而在事务进行过程,会有
    redo log
    不断写入磁盘。
  3. bin log
    是逻辑日志,记录的是SQL语句的原始逻辑;
    redo log
    是物理日志,记录的是在某个数据页上做了什么修改。

讲一下MySQL架构?

MySQL主要分为 Server 层和存储引擎层:

  • Server 层
    :主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
  • 存储引擎
    : 主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。

Server 层基本组件

  • 连接器:
    当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。
  • 查询缓存:
    执行查询语句的时候,会先查询缓存,先校验这个 sql 是否执行过,如果有缓存这个 sql,就会直接返回给客户端,如果没有命中,就会执行后续的操作。
  • 分析器:
    没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先看 SQL 语句要做什么,再检查 SQL 语句语法是否正确。
  • 优化器:
    优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。
  • 执行器:
    首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会根据执行计划去调用引擎的接口,返回结果。

分库分表

当单表的数据量达到1000W或100G以后,优化索引、添加从库等可能对数据库性能提升效果不明显,此时就要考虑对其进行切分了。切分的目的就在于减少数据库的负担,缩短查询的时间。

数据切分可以分为两种方式:垂直划分和水平划分。

垂直划分

垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。

优点
:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。

缺点

  • 主键出现冗余,需要管理冗余列;
  • 会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力;
  • 依然存在单表数据量过大的问题。

水平划分

水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。

优点
:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。

缺点

  • 分片事务一致性难以解决
  • 跨节点
    join
    性能差,逻辑复杂
  • 数据分片在扩容时需要迁移

什么是分区表?

分区是把一张表的数据分成N多个区块。分区表是一个独立的逻辑表,但是底层由多个物理子表组成。

当查询条件的数据分布在某一个分区的时候,查询引擎只会去某一个分区查询,而不是遍历整个表。在管理层面,如果需要删除某一个分区的数据,只需要删除对应的分区即可。

分区一般都是放在单机里的,用的比较多的是时间范围分区,方便归档。只不过分库分表需要代码实现,分区则是mysql内部实现。分库分表和分区并不冲突,可以结合使用。

分区表类型

range分区
,按照范围分区。比如按照时间范围分区

CREATE TABLE test_range_partition(
       id INT auto_increment,
       createdate DATETIME,
       primary key (id,createdate)
   ) 
   PARTITION BY RANGE (TO_DAYS(createdate) ) (
      PARTITION p201801 VALUES LESS THAN ( TO_DAYS('20180201') ),
      PARTITION p201802 VALUES LESS THAN ( TO_DAYS('20180301') ),
      PARTITION p201803 VALUES LESS THAN ( TO_DAYS('20180401') ),
      PARTITION p201804 VALUES LESS THAN ( TO_DAYS('20180501') ),
      PARTITION p201805 VALUES LESS THAN ( TO_DAYS('20180601') ),
      PARTITION p201806 VALUES LESS THAN ( TO_DAYS('20180701') ),
      PARTITION p201807 VALUES LESS THAN ( TO_DAYS('20180801') ),
      PARTITION p201808 VALUES LESS THAN ( TO_DAYS('20180901') ),
      PARTITION p201809 VALUES LESS THAN ( TO_DAYS('20181001') ),
      PARTITION p201810 VALUES LESS THAN ( TO_DAYS('20181101') ),
      PARTITION p201811 VALUES LESS THAN ( TO_DAYS('20181201') ),
      PARTITION p201812 VALUES LESS THAN ( TO_DAYS('20190101') )
   );


/var/lib/mysql/data/
可以找到对应的数据文件,每个分区表都有一个使用#分隔命名的表文件:

   -rw-r----- 1 MySQL MySQL    65 Mar 14 21:47 db.opt
   -rw-r----- 1 MySQL MySQL  8598 Mar 14 21:50 test_range_partition.frm
   -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201801.ibd
   -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201802.ibd
   -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201803.ibd
...

list分区

list分区和range分区相似,主要区别在于list是枚举值列表的集合,range是连续的区间值的集合。对于list分区,分区字段必须是已知的,如果插入的字段不在分区时的枚举值中,将无法插入。

create table test_list_partiotion
   (
       id int auto_increment,
       data_type tinyint,
       primary key(id,data_type)
   )partition by list(data_type)
   (
       partition p0 values in (0,1,2,3,4,5,6),
       partition p1 values in (7,8,9,10,11,12),
       partition p2 values in (13,14,15,16,17)
   );

hash分区

可以将数据均匀地分布到预先定义的分区中。

create table test_hash_partiotion
   (
       id int auto_increment,
       create_date datetime,
       primary key(id,create_date)
   )partition by hash(year(create_date)) partitions 10;

分区的问题?

  1. 打开和锁住所有底层表的成本可能很高。当查询访问分区表时,MySQL 需要打开并锁住所有的底层表,这个操作在分区过滤之前发生,所以无法通过分区过滤来降低此开销,会影响到查询速度。可以通过批量操作来降低此类开销,比如批量插入、
    LOAD DATA INFILE
    和一次删除多行数据。
  2. 维护分区的成本可能很高。例如重组分区,会先创建一个临时分区,然后将数据复制到其中,最后再删除原分区。
  3. 所有分区必须使用相同的存储引擎。

查询语句执行流程?

查询语句的执行流程如下:权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎。

举个例子,查询语句如下:

select * from user where id > 1 and name = '大彬';
  1. 首先检查权限,没有权限则返回错误;
  2. MySQL8.0以前会查询缓存,缓存命中则直接返回,没有则执行下一步;
  3. 词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;
  4. 两种执行方案,先查
    id > 1
    还是
    name = '大彬'
    ,优化器根据自己的优化算法选择执行效率最好的方案;
  5. 校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。

更新语句执行过程?

更新语句执行流程如下:分析器、权限校验、执行器、引擎、
redo log

prepare
状态)、
binlog

redo log

commit
状态)

举个例子,更新语句如下:

update user set name = '大彬' where id = 1;
  1. 先查询到 id 为1的记录,有缓存会使用缓存。
  2. 拿到查询结果,将 name 更新为大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录
    redo log
    ,此时
    redo log
    进入
    prepare
    状态。
  3. 执行器收到通知后记录
    binlog
    ,然后调用引擎接口,提交
    redo log

    commit
    状态。
  4. 更新完成。

为什么记录完
redo log
,不直接提交,而是先进入
prepare
状态?

假设先写
redo log
直接提交,然后写
binlog
,写完
redo log
后,机器挂了,
binlog
日志没有被写入,那么机器重启后,这台机器会通过
redo log
恢复数据,但是这个时候
binlog
并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。

exist和in的区别?

exists
用于对外表记录做筛选。
exists
会遍历外表,将外查询表的每一行,代入内查询进行判断。当
exists
里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果
exists
里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。

select a.* from A awhere exists(select 1 from B b where a.id=b.id)

in
是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。

select * from Awhere id in(select id from B)

子查询的表比较大的时候
,使用
exists
可以有效减少总的循环次数来提升速度;
当外查询的表比较大的时候
,使用
in
可以有效减少对外查询表循环遍历来提升速度。

truncate、delete与drop区别?

相同点:

  1. truncate
    和不带
    where
    子句的
    delete
    、以及
    drop
    都会删除表内的数据。

  2. drop

    truncate
    都是
    DDL
    语句(数据定义语言),执行后会自动提交。

不同点:

  1. truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引;
  2. 一般来说,执行速度: drop > truncate > delete。

MySQL中int(10)和char(10)的区别?

int(10)中的10表示的是显示数据的长度,而char(10)表示的是存储数据的长度。

having和where区别?

  • 二者作用的对象不同,
    where
    子句作用于表和视图,
    having
    作用于组。
  • where
    在数据分组前进行过滤,
    having
    在数据分组后进行过滤。

为什么要做主从同步?

  1. 读写分离,使数据库能支撑更大的并发。
  2. 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能。
  3. 数据备份,保证数据的安全。

什么是MySQL主从同步?

主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(
master
),其余的服务器充当从服务器(
slave
)。

因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。

乐观锁和悲观锁是什么?

数据库中的并发控制是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观锁和悲观锁是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发冲突,会对操作的数据进行加锁,直到提交事务,才会释放锁,其他事务才能进行修改。实现方式:使用数据库中的锁机制。
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加
    version
    字段,在修改提交之前检查
    version
    与原来取到的
    version
    值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或
    CAS
    算法实现。

用过processlist吗?

show processlist

show full processlist
可以查看当前 MySQL 是否有压力,正在运行的
SQL
,有没有慢
SQL
正在执行。返回参数如下:

  1. id
    :线程ID,可以用
    kill id
    杀死某个线程
  2. db
    :数据库名称
  3. user
    :数据库用户
  4. host
    :数据库实例的IP
  5. command
    :当前执行的命令,比如
    Sleep

    Query

    Connect
  6. time
    :消耗时间,单位秒
  7. state
    :执行状态,主要有以下状态:
    • Sleep,线程正在等待客户端发送新的请求
    • Locked,线程正在等待锁
    • Sending data,正在处理
      SELECT
      查询的记录,同时把结果发送给客户端
    • Kill,正在执行
      kill
      语句,杀死指定线程
    • Connect,一个从节点连上了主节点
    • Quit,线程正在退出
    • Sorting for group,正在为
      GROUP BY
      做排序
    • Sorting for order,正在为
      ORDER BY
      做排序
  8. info
    :正在执行的
    SQL
    语句

MySQL查询 limit 1000,10 和limit 10 速度一样快吗?

两种查询方式。对应
limit offset, size

limit size
两种方式。

而其实
limit size
,相当于
limit 0, size
。也就是从0开始取size条数据。

也就是说,两种方式的
区别在于offset是否为0。

先来看下limit sql的内部执行逻辑。

MySQL内部分为
server层

存储引擎层
。一般情况下存储引擎都用innodb。

server层有很多模块,其中需要关注的是
执行器
是用于跟存储引擎打交道的组件。

执行器可以通过调用存储引擎提供的接口,将一行行数据取出,当这些数据完全符合要求(比如满足其他where条件),则会放到
结果集
中,最后返回给调用mysql的
客户端

以主键索引的limit执行过程为例:

执行
select * from xxx order by id limit 0, 10;
,select后面带的是
星号
,也就是要求获得行数据的
所有字段信息。

server层会调用innodb的接口,在innodb里的主键索引中获取到第0到10条
完整行数据
,依次返回给server层,并放到server层的结果集中,返回给客户端。

把offset搞大点,比如执行的是:
select * from xxx order by id limit 500000, 10;

server层会调用innodb的接口,由于这次的offset=500000,会在innodb里的主键索引中获取到第0到(500000 + 10)条
完整行数据

返回给server层之后根据offset的值挨个抛弃,最后只留下最后面的size条
,也就是10条数据,放到server层的结果集中,返回给客户端。

可以看出,当offset非0时,server层会从引擎层获取到
很多无用的数据
,而获取的这些无用数据都是要耗时的。

因此,mysql查询中 limit 1000,10 会比 limit 10 更慢。原因是 limit 1000,10 会取出1000+10条数据,并抛弃前1000条,这部分耗时更大。

深分页怎么优化?

还是以上面的SQL为空:
select * from xxx order by id limit 500000, 10;

方法一

从上面的分析可以看出,当offset非常大时,server层会从引擎层获取到很多无用的数据,而当select后面是*号时,就需要拷贝完整的行信息,
拷贝完整数据
相比
只拷贝行数据里的其中一两个列字段
更耗费时间。

因为前面的offset条数据最后都是不要的,没有必要拷贝完整字段,所以可以将sql语句修改成:

select * from xxx  where id >=(select id from xxx order by id limit 500000, 1) order by id limit 10;

先执行子查询
select id from xxx by id limit 500000, 1
, 这个操作,其实也是将在innodb中的主键索引中获取到
500000+1
条数据,然后server层会抛弃前500000条,只保留最后一条数据的id。

但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。

在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了

select * from xxx  where id >=500000 order by id limit 10;

这样innodb再走一次
主键索引
,通过B+树快速定位到id=500000的行数据,时间复杂度是lg(n),然后向后取10条数据。

方法二:

将所有的数据
根据id主键进行排序
,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。

select * from xxx where id > start_id order by id limit 10;

mysql

通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。

高度为3的B+树,可以存放多少数据?

InnoDB存储引擎有自己的最小储存单元——页(Page)。

查询InnoDB页大小的命令如下:

mysql> show global status like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+

可以看出 innodb 默认的一页大小为 16384B = 16384/1024 = 16kb。

在MySQL中,B+树一个节点的大小设为一页或页的倍数最为合适。因为如果一个节点的大小 < 1页,那么读取这个节点的时候其实读取的还是一页,这样就造成了资源的浪费。

B+树中
非叶子节点存的是key + 指针

叶子节点存的是数据行

对于叶子节点,如果一行数据大小为1k,那么一页就能存16条数据。

对于非叶子节点,如果key使用的是bigint,则为8字节,指针在MySQL中为6字节,一共是14字节,则16k能存放 16 * 1024 / 14 = 1170 个索引指针。

于是可以算出,对于一颗高度为2的B+树,根节点存储索引指针节点,那么它有1170个叶子节点存储数据,每个叶子节点可以存储16条数据,一共 1170 x 16 = 18720 条数据。而对于高度为3的B+树,就可以存放 1170 x 1170 x 16 = 21902400 条数据(
两千多万条数据
),也就是对于两千多万条的数据,我们只需要
高度为3
的B+树就可以完成,通过主键查询只需要3次IO操作就能查到对应数据。

所以在 InnoDB 中B+树高度一般为3层时,就能满足千万级的数据存储。

参考:
https://www.cnblogs.com/leefreeman/p/8315844.html

MySQL单表多大进行分库分表?

目前主流的有两种说法:

  1. MySQL 单表数据量大于 2000 万行,性能会明显下降,考虑进行分库分表。
  2. 阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为MySQL为了提高性能,会将表的索引装载到内存中。在InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。

因此,对于分库分表,需要结合实际需求,不宜过度设计,在项目一开始不采用分库与分表设计,而是随着业务的增长,在无法继续优化的情况下,再考虑分库与分表提高系统的性能。对此,阿里巴巴《Java 开发手册》补充到:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

至于MySQL单表多大进行分库分表,应当根据机器资源进行评估。

大表查询慢怎么优化?

某个表有近千万数据,查询比较慢,如何优化?

当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下:

  • 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描
  • 索引优化,SQL优化。最左匹配原则等,参考:
    https://topjavaer.cn/database/mysql.html#什么是覆盖索引
  • 建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能
  • 利用缓存。利用Redis等缓存热点数据,提高查询效率
  • 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内
  • 读写分离。经典的数据库拆分方案,主库负责写,从库负责读
  • 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分
  • 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDERBY命令上涉及的列建立索引
  1. 数据异构到es
  2. 冷热数据分离。几个月之前不常用的数据放到冷库中,最新的数据比较新的数据放到热库中
  3. 升级数据库类型,换一种能兼容MySQL的数据库(OceanBase、tidb)

说说count(1)、count(*)和count(字段名)的区别

嗯,先说说count(1) and count(字段名)的区别。

两者的主要区别是

  1. count(1) 会统计表中的所有的记录数,包含字段为null 的记录。
  2. count(字段名) 会统计该字段在表中出现的次数,忽略字段为null 的情况。即不统计字段为null 的记录。

接下来看看三者之间的区别。

执行效果上:

  • count(*)包括了所有的列,相当于行数,在统计结果的时候,
    不会忽略列值为NULL
  • count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,
    不会忽略列值为NULL
  • count(字段名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,
    即某个字段值为NULL时,不统计

执行效率上:

  • 列名为主键,count(字段名)会比count(1)快
  • 列名不为主键,count(1)会比count(列名)快
  • 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
  • 如果有主键,则 select count(主键)的执行效率是最优的
  • 如果表只有一个字段,则 select count(*)最优。

MySQL中DATETIME 和 TIMESTAMP有什么区别?

嗯,
TIMESTAMP

DATETIME
都可以用来存储时间,它们主要有以下区别:

1.表示范围

  • DATETIME:1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999
  • TIMESTAMP:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-09 03:14:07.999999' UTC

TIMESTAMP
支持的时间范围比
DATATIME
要小,容易出现超出的情况。

2.空间占用

  • TIMESTAMP :占 4 个字节
  • DATETIME:在 MySQL 5.6.4 之前,占 8 个字节 ,之后版本,占 5 个字节

3.存入时间是否会自动转换

TIMESTAMP
类型在默认情况下,insert、update 数据时,
TIMESTAMP
列会自动以当前时间(
CURRENT_TIMESTAMP
)填充/更新。
DATETIME
则不会做任何转换,也不会检测时区,你给什么数据,它存什么数据。

4.
TIMESTAMP
比较受时区timezone的影响以及MYSQL版本和服务器的SQL MODE的影响。因为
TIMESTAMP
存的是时间戳,在不同的时区得出的时间不一致。

5.如果存进NULL,两者实际存储的值不同。

  • TIMESTAMP:会自动存储当前时间 now() 。
  • DATETIME:不会自动存储当前时间,会直接存入 NULL 值。

说说为什么不建议用外键?

外键是一种约束,这个约束的存在,会保证表间数据的关系始终完整。外键的存在,并非全然没有优点。

外键可以保证数据的完整性和一致性,级联操作方便。而且使用外键可以将数据完整性判断托付给了数据库完成,减少了程序的代码量。

虽然外键能够保证数据的完整性,但是会给系统带来很多缺陷。

1、并发问题。在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁。若是在高并发大流量事务场景,使用外键更容易造成死锁。

2、扩展性问题。比如从
MySQL
迁移到
Oracle
,外键依赖于数据库本身的特性,做迁移可能不方便。

3、不利于分库分表。在水平拆分和分库的情况下,外键是无法生效的。将数据间关系的维护,放入应用程序中,为将来的分库分表省去很多的麻烦。

使用自增主键有什么好处?

自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑,在查询的时候,效率也就更高。

InnoDB的自增值为什么不能回收利用?

主要为了提升插入数据的效率和并行度。

假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。

假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。

事务 B 正确提交了,但事务 A 出现了唯一键冲突。

如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。

接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。

而为了解决这个主键冲突,有两种方法:

  • 每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。
  • 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。

可见,这两个方法都会导致性能问题。

因此,InnoDB 放弃了“允许自增 id 回退”这个设计,语句执行失败也不回退自增 id。

自增主键保存在什么地方?

不同的引擎对于自增值的保存策略不同:

  • MyISAM引擎的自增值保存在数据文件中。
  • 在MySQL8.0以前,InnoDB引擎的自增值是存在内存中。MySQL重启之后内存中的这个值就丢失了,每次重启后第一次打开表的时候,会找自增值的最大值max(id),然后将最大值加1作为这个表的自增值;MySQL8.0版本会将自增值的变更记录在redo log中,重启时依靠redo log恢复。

自增主键一定是连续的吗?

不一定,有几种情况会导致自增主键不连续。

1、唯一键冲突导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果违反表中定义的唯一索引的唯一约束,会导致插入数据失败。此时表的自增主键的键值是会向后加1滚动的。下次再次插入数据的时候,就不能再使用上次因插入数据失败而滚动生成的键值了,必须使用新滚动生成的键值。

2、事务回滚导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果显式开启了事务,然后因为某种原因最后回滚了事务,此时表的自增值也会发生滚动,而接下里新插入的数据,也将不能使用滚动过的自增值,而是需要重新申请一个新的自增值。

3、批量插入导致自增值不连续。MySQL有一个批量申请自增id的策略:

  • 语句执行过程中,第一次申请自增id,分配1个自增id
  • 1个用完以后,第二次申请,会分配2个自增id
  • 2个用完以后,第三次申请,会分配4个自增id
  • 依次类推,每次申请都是上一次的两倍(最后一次申请不一定全部使用)

如果下一个事务再次插入数据的时候,则会基于上一个事务申请后的自增值基础上再申请。此时就出现自增值不连续的情况出现。

4、自增步长不是1,也会导致自增主键不连续。

MySQL数据如何同步到Redis缓存?

参考:
https://cloud.tencent.com/developer/article/1805755

有两种方案:

1、通过MySQL自动同步刷新Redis,
MySQL触发器+UDF函数
实现。

过程大致如下:

  1. 在MySQL中对要操作的数据设置触发器Trigger,监听操作
  2. 客户端向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数
  3. UDF函数可以把数据写入到Redis中,从而达到同步的效果

2、
解析MySQL的binlog
,实现将数据库中的数据同步到Redis。可以通过canal实现。canal是阿里巴巴旗下的一款开源项目,基于数据库增量日志解析,提供增量数据订阅&消费。

canal的原理如下:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  2. mysql master收到dump请求,开始推送binary log给canal
  3. canal解析binary log对象(原始为byte流),将数据同步写入Redis。

为什么阿里Java手册禁止使用存储过程?

先看看什么是存储过程。

存储过程是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。

存储过程主要有以下几个缺点。

  1. 存储过程难以调试
    。存储过程的开发一直缺少有效的 IDE 环境。SQL 本身经常很长,调试式要把句子拆开分别独立执行,非常麻烦。
  2. 移植性差
    。存储过程的移植困难,一般业务系统总会不可避免地用到数据库独有的特性和语法,更换数据库时这部分代码就需要重写,成本较高。
  3. 管理困难
    。存储过程的目录是扁平的,而不是文件系统那样的树形结构,脚本少的时候还好办,一旦多起来,目录就会陷入混乱。
  4. 存储过程是
    只优化一次
    ,有的时候随着数据量的增加或者数据结构的变化,原来存储过程选择的执行计划也许并不是最优的了,所以这个时候需要手动干预或者重新编译了。


最后给大家分享一个Github仓库,上面有大彬整理的
300多本经典的计算机书籍PDF
,包括
C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生
等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~

Github地址

https://github.com/Tyson0314/java-books

概念

MyBatis是一款持久层框架,用于简化JDBC操作(JavaEE三层架构:表现层(用来做页面的代码),业务层(业务逻辑代码),持久层(对数据库操作的代码))(框架就是一个半成品软件,是一套可重用的、通用的代码基础模型)

mybatis基本流程

  1. 创建数据表
  2. 创建项目,导入坐标(需要的包)
  3. 编写maven核心配置文件,链接数据库
  4. 编写sql映射文件
  5. 编码执行sql实现增删改查

代码如下:

  1. 数据库代码就不复制了,直接展示数据表

  2. 创建一个maven项目,在pom.xml里面导入mybatis的依赖、mysql驱动、junit单元测试,注意mysql驱动一定要用电脑上安装的,否则运行时会报错,具体可以查看这篇博客
    Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 解决办法

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>mybatis-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- mybatis的依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>

        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <!-- junit单元测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>

        <!-- 添加slf4j日志api -->
<!--        <dependency>-->
<!--            <groupId>org.slf4j</groupId>-->
<!--            <artifactId>slf4j-log4j12</artifactId>-->
<!--            <version>1.7.19</version>-->
<!--        </dependency>-->
    </dependencies>

</project>
  1. 编写MyBatis核心配置文件,在resources目录下创建mybatis-config.xml文件,在其中复制以下代码,记得根据注释修改信息。也可以去官网获得代码,官网的入门案例给好了接口。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 日志 -->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    <!-- 别名,UserMapper.xml文件中resultType属性就可以写pojo下的类名(类名还可以不区分大小写) -->
    <typeAliases>
        <package name="com.itheima.pojo"/>
    </typeAliases>
    
    <!--
    environments:配置数据库链接环境信息,可以配置多个environment,通过切换default属性切换不同的environment
    -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!-- 数据库链接信息 -->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>

        <environment id="test">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!-- 数据库链接信息 -->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!-- 加载sql映射文件 -->
        <!-- <mapper resource="com/itheima/mapper/UserMapper.xml"/>-->

        <!-- mapper代理方式 -->
        <package name="com.itheima.mapper"/>
    </mappers>
</configuration>
  1. 编写sql映射文件
    (1)在java目录下创建pojo包,在其下创建实体类,这是我对应数据库的user类
package com.itheima.pojo;

//整列操作快捷键   Alt+鼠标左键
//格式化快捷键    Ctrl+Alt+L
public class User {
    private Integer id;
    private String username;
    private String PASSWORD;
    private String gender;
    private String addr;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPASSWORD() {
        return PASSWORD;
    }

    public void setPASSWORD(String PASSWORD) {
        this.PASSWORD = PASSWORD;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", PASSWORD='" + PASSWORD + '\'' +
                ", gender='" + gender + '\'' +
                ", addr='" + addr + '\'' +
                '}';
    }
}

(2)还在java目录下创建mapper包,存放sql映射的接口,在里面写所要执行的sql方法

package com.itheima.mapper;

import com.itheima.pojo.User;

import java.util.List;

public interface UserMapper {


    List<User> selectAll();

    User selectById(int id);
}

(3)编写sql映射文件,即真正的sql语句。在resources目录下创建与Java对应的mapper包,注意这里创建时要用/隔开,(创建对应的包是为了方便打包时,接口与映射文件在同一目录下),在包下创建UserMapper.xml文件,编写sql语句。同样代码可以从官网获得,官网给了案例的模板。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
    namespace: 名称空间

-->
<mapper namespace="com.itheima.mapper.UserMapper">
    <select id="selectAll" resultType="user">
    select * from tb_user;
  </select>
    <select id="selectById" resultType="user">
        select *
        from tb_user where id = #{id};
    </select>
</mapper>
  1. 编写代码执行sql,在Java目录下创建MyBatisDemo类,用于执行sql
package com.itheima;

import com.itheima.mapper.UserMapper;
import com.itheima.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * MyBatis代理开发
 */
public class MyBatisDemo2 {
    public static void main(String[] args) throws IOException {
        //1. 加载MyBatis配置文件,获取SqlSessionFactory对象
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2. 获取SqlSession对象,用它执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3. 执行sql
        //List<User> users = sqlSession.selectList("test.selectAll");

        //3.1获取UserMapper代理对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = userMapper.selectAll();

        System.out.println(users);

        //4. 释放资源
        sqlSession.close();
    }
}


MyBatis核心配置文件

常用的几个属性(属性之间配置具有先后顺序):

    <!-- 别名,UserMapper.xml文件中resultType属性就可以写pojo下的类名(类名还可以不区分大小写) -->
    <typeAliases>
        <package name="com.itheima.pojo"/>
    </typeAliases>
    <!--
    environments:配置数据库链接环境信息,可以配置多个environment,通过切换default属性切换不同的environment
    -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!-- 数据库链接信息 -->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>

        <environment id="test">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!-- 数据库链接信息 -->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
 <mappers>
            <!-- 加载sql映射文件 -->
    <!--        <mapper resource="com/itheima/mapper/UserMapper.xml"/>-->
    
            <!-- mapper代理方式 -->
            <package name="com.itheima.mapper"/>
        </mappers>

MyBatis实际案例

此处我们先安装一个插件,MyBatisX,方便我们进行开发。安装方法也很简单,idea中settings——>plugins,搜索MyBatisX,install。
安装完成后我们进行实际案例的开发,首先我们要建表。sqlyog中执行以下代码,在mybatis中建立tb_brand表:(其中mybatis数据库我已经提前建好)

USE mybatis;

DROP TABLE IF EXISTS tb_brand;

CREATE TABLE tb_brand
(
id       	INT PRIMARY KEY AUTO_INCREMENT,
brand_name	VARCHAR(20),
company_name 	VARCHAR(20),
ordered 	INT,
description 	VARCHAR(100),
STATUS 		INT
);

INSERT INTO tb_brand (brand_name,company_name,ordered,description,STATUS)
VALUES('三只松鼠','三只松鼠股份有限公司',5,'好吃不上火',0),
      ('华为','华为科技有限公司',100,'huawfasfak',1),
      ('小米','小米科技有限公司',50,'ababababbab',1);
      
SELECT * FROM tb_brand;

建完表之后,我们在idea项目中的pojo文件下建立brand实体类,代码如下:(我们只要定义变量即可,set、get和toString方法可以快捷创建)

package com.itheima.pojo;

public class Brand {
    private Integer id;
    private String brandName;
    private String companyName;
    private Integer ordered;
    private String description;
    private Integer status;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getBrandName() {
        return brandName;
    }

    public void setBrandName(String brandName) {
        this.brandName = brandName;
    }

    public String getCompanyName() {
        return companyName;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }

    public Integer getOrdered() {
        return ordered;
    }

    public void setOrdered(Integer ordered) {
        this.ordered = ordered;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Brand{" +
                "id=" + id +
                ", brandName='" + brandName + '\'' +
                ", companyName='" + companyName + '\'' +
                ", ordered=" + ordered +
                ", description='" + description + '\'' +
                ", status=" + status +
                '}';
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

}

第三步我们需要编写接口方法,在mapper包下建立BrandMapper来编写接口,代码如下

package com.itheima.mapper;


import com.itheima.pojo.Brand;

import java.util.List;

public interface BrandMapper {

    /**
     * 查询所有
     */
    public List<Brand> selectAll();

}

第四步我们需要编写sql语句,其实MyBatisX就可以帮我们快速编写sql语句,我们在上一步编写接口时,快速alt+enter,就可以在sql映射文件中快速创建sql语句,我们只需要改写ResultType和编写sql语句即可。代码如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
    namespace: 名称空间

-->
<mapper namespace="com.itheima.mapper.BrandMapper">

    <select id="selectAll" resultType="brand">
        select * from tb_brand;
    </select>
</mapper>

第五步,编写测试代码,我们在test目录下创建测试类,进行编码测试,代码如下:

package com.itheima.test;

import com.itheima.mapper.BrandMapper;
import com.itheima.pojo.Brand;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class MyBatisTest {

    @Test
    public void testSelectAll() throws IOException {
        //1. 获取sqlsessionFactory

        //1. 加载MyBatis配置文件,获取SqlSessionFactory对象
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2. 获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3. 获取Mapper接口的代理对象
        BrandMapper brandmapper = sqlSession.getMapper(BrandMapper.class);

        //4. 执行方法
        List<Brand> brands = brandmapper.selectAll();
        System.out.println(brands);

        //5. 释放资源
        sqlSession.close();

    }
}

至此,一个查询所有的实际案例就写完了,我把增删改查全部的代码放到下一个博客,流程大多是类似的,差异主要是参数的设置,还有就是动态的操作,对这些细节想看一看的可以看下一篇博客,我稍后把链接发上来。增删改查的流程主要是先创建数据表,之后再编写实体类,之后编写接口方法,再编写sql语句,最后编码执行sql。
另外,此处用的是配置文件完成增删改查,还可以用注解完成增删改查,就是在接口文件中用@+相应的语句来实现,下面是按id查询的一个例子(sql映射文件不用写,接口文件写以下代码即可):

@Select("select * from tb_user where id = #{id}")
public User selectById(int id);

但是注解只能完成简单的功能,所以此处对注解就不再多说。

mybatis参数传递

mybatis对于不同的参数有着不同的处理,对于多个参数还会进行封装。这边我就直接放结论了:接口文件中,对于单个参数的话,如果是pojo或map集合,可以直接使用,属性名/键名与参数占位符保持一致即可。如果是Collection,List,Array类型的话,需要@Param参数注解,替换默认的arg键名。多个参数也是@Param注解,替换默认键名。@Param参数注解代码例子如下

void delByIds(@Param("ids") int[] ids);

如何使用nvm来管理不同版本的 Node.js,然后使用不同的 Node.js 版本来运行不同版本的 React 应用?

要使用 nvm 来管理不同版本的 Node.js,可以按照以下步骤进行操作:

  1. 安装 nvm

    可以在 nvm 的 GitHub 仓库中找到安装说明,根据自己的操作系统和需求进行安装。

  2. 安装多个 Node.js 版本

    在安装了 nvm 后,可以使用
    nvm install
    命令来安装多个 Node.js 版本,例如:

nvm install 10.24.1
nvm install 14.17.6

  1. 使用指定版本的 Node.js

    安装完多个 Node.js 版本后,可以使用
    nvm use
    命令来切换使用不同版本的 Node.js,例如:

    nvm use 10.24.1

    这将会把当前的 Node.js 版本切换为 10.24.1。

    如果想在当前终端会话中永久切换到某个版本的 Node.js,可以使用
    nvm alias
    命令,例如:

    nvm alias default 14.17.6

    这将会把默认使用的 Node.js 版本切换为 14.17.6。

  2. 运行 React 应用

    使用不同版本的 Node.js 运行 React 应用时,需要在项目的根目录下创建一个
    .nvmrc
    文件,并在文件中指定所需的 Node.js 版本,例如:

# .nvmrc
10.24.1

当然也可以在里面使用别名
# .nvmrc
my-node
然后在终端中进入项目根目录,使用 `nvm use` 命令来切换到指定版本的 Node.js,然后使用 `npm start` 命令来启动 React 应用,例如:
nvm use
npm start
这将会在当前终端会话中切换到项目指定的 Node.js 版本,并启动 React 应用。如果要在其他终端会话中也使用相同版本的 Node.js,需要再次使用 `nvm use` 命令来切换版本。

可以自己命名nvm的不同环境吗?

可以使用 nvm 创建自定义别名(alias)来为每个环境指定您喜欢的名称。使用以下命令创建自定义别名:

nvm alias myapp 14.16.1

这将创建一个名为
myapp
的别名,指向
14.16.1
版本的 Node.js。您可以使用
nvm use
命令切换到该版本:

nvm use myapp

同样,您可以创建多个不同的别名,每个别名可以指向不同版本的 Node.js。这样,您就可以在同一台计算机上同时运行多个 React 应用,每个应用使用不同版本的 Node.js 环境。

可以给同一个版本的node创建多个环境吗

是的,您可以使用 nvm 创建多个相同版本的 Node.js 环境。这在您需要在不同的项目中使用相同版本的 Node.js 时非常有用,但每个项目可能需要不同的全局模块。

例如,要创建名为
myproject1
的新环境,可以使用以下命令:

nvm install 14.16.1 --reinstall-packages-from=default --alias=myproject1

这将在
14.16.1
版本的 Node.js 中创建一个新环境,并将
default
环境的全局模块重新安装到
myproject1
环境中。

要使用新环境,请使用
nvm use
命令:

nvm use myproject1

现在,您可以安装项目特定的全局模块,并在
myproject1
环境中运行应用程序。

您还可以使用类似的方式创建其他项目所需的环境,每个环境可以拥有不同的全局模块。

那我已经有了npm创建的环境,想要将现在的环境作为nvm默认环境,然后创建其他环境,应该如何操作?

如果您已经使用
npm
安装了 Node.js,并且希望将其设置为
nvm
的默认版本,则可以使用以下命令:

nvm use system

这将使用系统安装的 Node.js 版本,并将其设置为
nvm
的默认版本。

接下来,您可以使用
nvm install
命令来安装其他版本的 Node.js,并使用
nvm use
命令切换到特定版本的 Node.js。

例如,如果您要安装
14.16.1
版本的 Node.js,请使用以下命令:

nvm install 14.16.1

这将安装
14.16.1
版本的 Node.js。

然后,您可以使用以下命令将默认版本设置为
14.16.1

nvm alias default 14.16.1

这将使
14.16.1
版本的 Node.js 成为默认版本。现在,每次打开新的终端窗口时,都会使用
14.16.1
版本的 Node.js。

您可以使用
nvm use
命令来切换到其他版本的 Node.js。例如,如果您要切换到
12.22.1
版本的 Node.js,请使用以下命令:

nvm use 12.22.1

这将使
12.22.1
版本的 Node.js 成为当前版本。

用nacos作为服务注册中心,如何注册.NetCore服务,如何在Java中调用.NetCore服务呢?可以分为下面几个步骤:

0.运行nacos

1.开发.net core服务,然后调用nacos提供的.net core sdk注册服务。

2.开发Java服务,然后注册服务。

3.用RestTemplate调用.net core服务。

4.用OpenFeign调用服务

下面来看具体步骤:

0.参考我之前的文章
分布式配置nacos搭建踩坑指南(下)
,首先运行nacos.

1.首先开发一个.net core web api,我们返回的数据是天气预报消息,新建一个WeatherForecastController,代码如下:

usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.Extensions.Logging;usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Threading.Tasks;
namespaceWebApi.Controllers
{
[ApiController]
[Route(
"[controller]")]public classWeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"};
private readonly ILogger<WeatherForecastController>_logger;
public WeatherForecastController(ILogger<WeatherForecastController>logger)
{
_logger
=logger;
}

[HttpGet]
public IEnumerable<WeatherForecast>Get()
{
var rng = newRandom();return Enumerable.Range(1, 5).Select(index => newWeatherForecast
{
Date
=DateTime.Now.AddDays(index),
TemperatureC
= rng.Next(-20, 55),
Summary
=Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
//public String Get()//{//return "sunny";//}
}
}

然后设置好访问的url,在launchSettings.json的修改 "applicationUrl": "http://192.168.1.110:5000",注意这里去掉了https://192.168.1.110:5001,是为了避免在后面Java调用时需要证书的麻烦。

最后我们在cmd中输入dotnet run,当服务正常运行起来后,在浏览器中输入:http://192.168.1.110:5000/weatherforecast,发现成功返回天气数据,格式为json,截图如下:

2.net core项目中引入nuget包:nacos-sdk-csharp,截图如下:

3.调用nacos-sdk-csharp,进行服务注册,代码如下:

usingSystem;usingMicrosoft.Extensions.DependencyInjection;usingNacos.V2;usingNacos.V2.DependencyInjection;usingSystem.Collections.Generic;usingSystem.Threading.Tasks;namespaceNacosDiscoveryProviderNetCoreTest1
{
classProgram
{
static async Task Main(string[] args)
{
string serverAddr = "http://localhost:8848";string dataId = "config2";string group = "DEFAULT_GROUP";

IServiceCollection services
= newServiceCollection();//register service services.AddNacosV2Naming(
x
=>{
x.ServerAddresses
= new List<string>() { serverAddr };//x.ConfigUseRpc = true;
}
);
IServiceProvider serviceProvider
=services.BuildServiceProvider();var namingSvc = serviceProvider.GetService<INacosNamingService>();await namingSvc.RegisterInstance("weatherforecast", "192.168.1.110", 5000);
Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(
await namingSvc.GetAllInstances("weatherforecast")));
Console.ReadKey();


}
}
}

我们进入nacos后台,如果服务注册成功,我们就会在服务列表中看到weatherforecast服务了,如下所示:

有两个地方必须切记注意:

1).namingSvc.RegisterInstance("weatherforecast", "192.168.1.110", 5000);是一句很关键的代码,意思是注册一个名为weatherforecast,地址为:192.168.1.110,端口为:5000的服务。

2)launchSettings.json里的applicationUrl必须去掉包含https的设置,只保留http的设置,即只保留:"applicationUrl": "http://192.168.1.110:5000",否则在Java中调用会报证书错误。

4.参考
nacos服务注册
,利用阿里巴巴Spring boot脚手架,引入:spring-boot-starter-web,spring-cloud-starter-alibaba-nacos-discovery,spring-cloud-starter,spring-boot-starter-test,spring-cloud-starter-loadbalancer,spring-cloud-starter-openfeign。完整的pom如下:

<?xml version="1.0" encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.11</version>
        <relativePath/> <!--lookup parent from repository-->
    </parent>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>nocos-discovery-consumer-sample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>nocos-discovery-consumer-sample</name>
    <description>Demo project for Spring Boot</description><properties>
        <java.version>1.8</java.version>
        <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
        <spring-cloud.version>2021.0.4</spring-cloud.version>
    </properties><dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency><dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
          <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
          <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>

<dependency> <groupId>org.netbeans.external</groupId> <artifactId>org-apache-commons-httpclient</artifactId> <version>RELEASE126</version> </dependency>
</dependencies><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement><build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>

5.application.properties的设置同前面的文章里介绍的设置一样,代码如下所示:

spring.application.name=nocos-discovery-consumer-sample
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.namespace=public
spring.main.allow-circular-references=true
server.port=9091

6.新建一个名为WeatherService的接口,代码如下:

importorg.springframework.cloud.openfeign.FeignClient;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;


@FeignClient(
"weatherforecast")
@LoadBalancerClient(
"weatherforecast")public interfaceWeatherService {

@GetMapping(
"/Weatherforecast")publicString getWeather();


}

7.新建一个RestTemplateController,代码如下:

importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RestController;importorg.springframework.web.client.RestTemplate;
importcom.alibaba.fastjson.JSONObject;
importorg.springframework.context.annotation.Bean;importorg.apache.commons.httpclient.methods.GetMethod;//import org.apache.http.client.HttpClient; importorg.apache.commons.httpclient.HttpClient;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.cloud.client.ServiceInstance;importorg.springframework.cloud.client.discovery.DiscoveryClient;importorg.springframework.cloud.client.loadbalancer.LoadBalanced;import java.io.*;



@RestController
public classRestTemplateController {
//@LoadBalanced @AutowiredpublicRestTemplate resttemplate;//@LoadBalanced @BeanpublicRestTemplate restTemplate() {return newRestTemplate();
}

@Autowired
privateEchoService echoService;

@Autowired
privateWeatherService weatherService;

@Autowired DiscoveryClient discoveryClient;
//resttemplate test @GetMapping("/call/echo")publicString callEcho() {

System.out.println(
"callEcho");


ServiceInstance serviceInstance
=discoveryClient.getInstances("weatherforecast").get(0);
System.out.println(
"Host is: "+serviceInstance.getHost()+" ,port is: "+serviceInstance.getPort());
String urlString
=serviceInstance.getHost()+":"+serviceInstance.getPort()+"/weatherforecast";
urlString
="http://"+urlString;//RestTemplate test return resttemplate.getForObject(urlString, String.class);

}
//openFeign test @GetMapping("/getWeather")publicString getWeather() {returnweatherService.getWeather();

}
}

其中要注意的几点:

1) ServiceInstanceserviceInstance=discoveryClient.getInstances(
"weatherforecast").
get(
0);是一句关键的代码,用于获取weatherforecast服务的实例。

2)callEcho()是调用RestTemplage访问netcore服务

3)getWeather是调用openFeiign访问netcore服务

8.启动类代码如下:

importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;
importorg.springframework.cloud.client.discovery.EnableDiscoveryClient;importorg.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public classNocosDiscoveryConsumerSampleApplication {
public static voidmain(String[] args) {
SpringApplication.run(NocosDiscoveryConsumerSampleApplication.
class, args);
}

}

9.运行,访问http://192.168.1.110:5000/weatherforecast和http://localhost:9091/getWeather:

Blazor Server,即运行在服务器上的 Blazor 应用程序,它的优点是应用程序在首次运行时,客户端不需要下载运行时。但它的代码是在服务器上执行的,然后通过 SignalR 通信来更新客户端的 UI,所以它要求必须建立 Web Socket 连接。

用于 Blazor 应用的 SignalR Hub 是 ComponentHub,默认的连接地址是 /_blazor。多数时候我们不需要修改它,但人是一种喜欢折腾的动物,既然 MapBlazorHub 方法的重载也允许我们修改地址,那咱们何不试试。

app.MapBlazorHub("/myapp");
app.MapFallbackToPage(
"/_Host");

我把 ComponentHub 的通信地址改为 /myapp。这时候,客户端上就不能使用 blazor.server.js 中的默认行为了,咱们必须手动启动 Blazor 应用了(因为自动启动用的是默认的 /_blazor 地址)。

<scriptsrc="_framework/blazor.server.js"autostart="false"></script>
<script>Blazor.start({
configureSignalR: (connbuilder)
=>{
connbuilder.withUrl(
"myapp");
}
});
</script>

在引用 blazor.server.js 文件时,加上一个 autostart = "false",表示 blazor 应用手动启动。哦,这个 autostart 是怎么来的?来,咱们看看源代码。在 BootCommon.ts 文件中,定义有一个名为 shouldAutoStart 的函数,而且它已导出。看名字就知道,它用来判断是否自动启动 Blazor 应用。

export function shouldAutoStart(): boolean{return !!(document &&document.currentScript&&document.currentScript.getAttribute('autostart') !== 'false');
}

现在,你明白这个 autostart 特性是怎么回事了吧。

在调用 Blazor.start 方法时咱们要设定一个配置项—— configureSignalR。它指定一个函数,函数的参数是 HubConnectionBuilder 对象。这是 signalR.js 中的类型。再调用 withUrl 方法更改连接地址,默认的代码是这样的。

const connectionBuilder = newHubConnectionBuilder()
.withUrl(
'_blazor')
.withHubProtocol(hubProtocol);

很遗憾的是,运行后发现并不成功。

其实咱们的代码并没有错,问题其实是出在 Blazor 自身的“八阿哥”上。别急,老周接下来一层层剥出这个问题,你会感叹,官方团队竟然会犯“高级错误”。

咱们先来解释这个奇葩的错误信息,什么JSON格式不对?什么无效的字符“<”?这个错误信息很容易误导你,咱们看看下面的图。

在请求到 blazor.server.js 脚本后,访问了一个 /initializers 地址。这个 fetch 是 Blazor 应用发出的,其目的是问一下服务器,在 Blazor 应用启动前后,有没用自定义的初始化脚本。

为啥会有这个请求?我们来看看服务器端的源代码。

public staticComponentEndpointConventionBuilder MapBlazorHub(thisIEndpointRouteBuilder endpoints,stringpath,
Action
<HttpConnectionDispatcherOptions>configureOptions)
{
……
var hubEndpoint = endpoints.MapHub<ComponentHub>(path, configureOptions);var disconnectEndpoint =endpoints.Map(
(path.EndsWith(
'/') ? path : path + "/") + "disconnect/"
,
endpoints.CreateApplicationBuilder().UseMiddleware
<CircuitDisconnectMiddleware>().Build())
.WithDisplayName(
"Blazor disconnect");var jsInitializersEndpoint =endpoints.Map(
(path.EndsWith(
'/') ? path : path + "/") + "initializers/"
,
endpoints.CreateApplicationBuilder().UseMiddleware
<CircuitJavaScriptInitializationMiddleware>().Build())
.WithDisplayName(
"Blazor initializers");return newComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint);
}

如你所见,当你调用 MapBlazorHub 方法时,它同时注册了两个终结点:

a、/_blazor/disconnect/:断开 SignalR 连接时访问,由 CircuitDisconnectMiddleware 中间件负责处理。

b、/_blazor/initializers/:对,这个就是咱们在浏览器中看到的那个,请求初始化脚本的。由 CircuitJavaScriptInitializationMiddleware 中间件负责处理。

老周改了 Hub 地址是 myapp,所以这两路径应变为 /myapp/disconnect/ 和 /myapp/initializers/。

这个 initialicaers/ 地址返回的数据要求是 JSON 格式的,是一个字符串数组,表示初始化脚本的文件路径。

咱们继续跟踪,找到 CircuitJavaScriptInitializationMiddleware 中间件。

public asyncTask InvokeAsync(HttpContext context)
{
awaitcontext.Response.WriteAsJsonAsync(_initializers);
}

就这?对,就一行,_initializers 是选项类 CircuitOptions 的 JavaScriptInitializers 属性。

 internal IList<string> JavaScriptInitializers { get; } = new List<string>();

这厮还是 internal 的,也就是说你写的代码不能修改它。它是用 CircuitOptionsJavaScriptInitializersConfiguration 对象来设置的。

public voidConfigure(CircuitOptions options)
{
var file = _environment.WebRootFileProvider.GetFileInfo($"{_environment.ApplicationName}.modules.json");if(file.Exists)
{
var initializers = JsonSerializer.Deserialize<string[]>(file.CreateReadStream());for (var i = 0; i < initializers.Length; i++)
{
var initializer =initializers[i];
options.JavaScriptInitializers.Add(initializer);
}
}
}

老周解释一下:上面代码是说在 Web 目录下(默认就是静态文件专用的 wwwroot)下找到一个名为 {你的应用}.modules.json 的文件,然后读出来,再添加到 JavaScriptInitializers 属性中。

假如我们应用程序叫 BugApp,那么要找的这个JSON文件就是 BugApp.modules.json。这个JSON文件既可以自动生成,也可以你手动添加。

你在 wwwroot 目录下添加一个 js 文件,命名为 BugApp.lib.module.js。在生成项目时,会自动产生这个 JSON 文件。在生成后你是看不到 BugApp.modules.json 文件的,而是在 Debug|Release 目录下有个 BugApp.staticwebassets.runtime.json。

{"ContentRoots": ["C:\\XXXX\\BugApp\\wwwroot\\","C:\\XXXX\\BugApp\\obj\\Debug\\net7.0\\jsmodules\\"],"Root": {
……
"BugApp.modules.json": {"Children": null,"Asset": {"ContentRootIndex": 1,"SubPath": "jsmodules.build.manifest.json"},"Patterns": null}
},
……
}
}

终于见到它了,它指向的是 obj 目录下的 jsmodules.build.manifest.json 文件,ContentRootIndex : 1 表示 ContentRoots 节点中的第二个元素,即 obj\Debug\net7.0\jsmodules\jsmodules.build.manifest.json。打开这个文件看看有啥。

["BugApp.lib.module.js"]

如果你找不到这个文件,说明你没有生成项目,生成一下就有了。
注意,这个文件只有你【发布】项目后才会出现在 wwwroot 目录下的

看到没?就是一个 JSON 数组,然后列出我刚刚添加的 js 脚本。客户端访问 ./initializers 就是为了获得这个文件。现在你回想一下浏览器报的那个错误,是不是知道为什么会说无效的 JSON 文件了吧。

客户端所请求的地址仍是默认的 /_blazor/initializers/ ,而我已经改为 /myapp 了,它本应该请求 /myapp/initializers 的,可是,blazor 并没这么做。那,我们能在 js 代码中配置吗?唉!官方团队犯的“高级”错误,居然把 URL 写死在代码中。可以看看 JSInitializers.Server.ts 文件中是怎么写的。

export async function fetchAndInvokeInitializers(options: Partial<CircuitStartOptions>) : Promise<JSInitializer>{
const jsInitializersResponse
= await fetch('_blazor/initializers', {
method:
'GET',
credentials:
'include',
cache:
'no-cache',
});

……
}

你看是不是这样?都 TM 的硬编码了,还怎么配置?哦,还没回答一个问题:既然找到问题所在了,那为什么会报无效 JSON 格式的错误?答:因为 /_blazor 被我改了,所以请求 /_blazor/initializers 是 404 的,但,我们为了让 Blazor 能启动,调用了 MapFallbackToPage 方法作为后备。

app.MapFallbackToPage("/_Host");

这样就导致在访问 /_blazor/initializers 得到404后转而返回 /_Host,也就是说,/initializers 获取一个 HTML 文档,HTML 文档的第一个字符不就是“<”吗,所以就是无效字符了,不是JSON。

所以,你说,这不是“八阿哥”是啥?如果你非要改掉默认地址,又想正常获取初始化脚本,咋整?

A方案:下载 TypeScript 源码,自己修改,然后编译。

B方案:我们在 HTTP 管道上加个中间件,把 /myapp 改回 /_blazor。

这里老周演示一下 B 方案。

//Blazor signalR Hub 的自定义地址
const string NewBlazorHubUrl = "/myapp";
app.UseStaticFiles();
//要在路由中间件之前改地址 app.Use(async (context, next) =>{if(context.Request.Path.StartsWithSegments("/_blazor", StringComparison.OrdinalIgnoreCase))
{
var repl = context.Request.Path.ToString().Replace("/_blazor", NewBlazorHubUrl);
context.Request.Path
=repl;
}
awaitnext();
});
//注意顺序 app.UseRouting();

app.MapBlazorHub(NewBlazorHubUrl);
app.MapFallbackToPage(
"/_Host");

因为新地址是 /myapp 开头,我们只要把以 /_blazor 开头的地址改为 /myapp 开头就行了。这个中间件一定要在路由中间件之前改地址。改地址后再做路由匹配才有意义。

当然,想简洁一点的,还可以用 URL Rewrite。

var rwtopt = newRewriteOptions()
.AddRewrite(
"^_blazor/(.+)", "myapp/$1", true);
app.UseRewriter(rwtopt);
app.UseRouting();

app.MapBlazorHub(
"/myapp");
app.MapFallbackToPage(
"/_Host");

app.Run();

替换时用的正则表达式,我们匹配 _blazor 后的内容,即 initializers,然后替换为 myapp + initializers。“$1”引用正则中匹配的分组,即“.+”,匹配一个以上任意字符。URL 重写时,不需要指定开头的“/”,所以处理的是 _blazor/... 而不是 /_blazor/...。

前面我们提到了 BugApp.lib.module.js 脚本。干脆咱们也写一个自定义脚本。在 wwwroot 目录下添加一个 BugApp.lib.module.js 文件。BugApp 是项目名称,你要根据实际来改。

export functionbeforeStart() {
console.log(
"Blazor应用即将启动");
}

export
functionafterStarted() {
console.log(
"Blazor应用已启动");
}

在这个脚本中,我们要导出两个函数:

beforeStart:在 Blazor 启动之前被调用。

afterStarted:在 Blazor 启动之后被调用。

现在,再次运行程序,用开发人员工具查看“控制台”消息,会看到这两条输出。

想玩直观一点的话,也可以修改 HTML 文档。

export functionbeforeStart() {
let ele
= document.createElement("div");//设置样式 ele.style = 'color: green; margin-top: 16px';//文本 ele.textContent = "Blazor应用即将启动";
document.body.append(ele);
}

export
functionafterStarted() {
let ele
= document.createElement("div");
ele.style
= 'color: orange; margin-top: 15px';
ele.textContent
= "Blazor应用已经启动";
document.body.append(ele);
}

运行之后,页面上会动态加了两个 <div> 元素。

XXX.lib.module.js 这个文件名是固定的,如果想自定义文件名,或想返回多个 js 文件,可以自己手动处理。

在 wwwroot 目录下添加一个名为 BugApp.modules.json 的文件。

["abc.js","def.js","opq.js"]

以JSON数组的格式把你想用的初始化脚本写上。再次运行程序,就会下载这个文件,读取三个文件并将其下载。

你得注意,你指定的这些脚本必须是可访问,有效的,不然 Blazor 会启动失败。

好了,今天就说到这儿了,主要是发现了一个“八阿哥”。