乐趣区

关于java:深入理解Mysql数据存储

欢送关注我的微信公众号【Mflyyou】获取继续更新。

github.com/zhangpanqin/MFlyYou 收集技术文章及我的系列文章,欢送 Star。

前言

本文内容

  • Mysql 数据文件阐明
  • Mysql 数据逻辑存储架构
  • Mysql 表空间,次要是零碎表空间和独立表空间
  • Mysql 数据类型

    • 时区对 datetime 和 timestamp 影响,java 中 LocalDatetime 保留时,工夫和预期不符的起因剖析和解决办法
    • varchar(n) 和 char(n) 保留时,n 能取多少,n 的含意。一行数据中 varchar 能存多少个
    • 整型、小数

本文内容基于 Mysql 8.0.21,零碎为 Centos 7。

Mysql 架构阐明

客户端链接 MysqlServer 层,Server 层会对 sql 进行语法解析和优化并生成 执行打算 ,而后调用 存储引擎 提供的接口获取要查问的数据。

存储引擎层从计算机文件系统上读取对应的文件中的数据返回给 Server 层,Server 层再将数据返回给客户端。

存储引擎不理解的话,应用 InnoDB 就行,这也是比拟罕用的。

Redis 为什么会比 Mysql 快,很大的起因是 Redis 的数据都在内存中,再加上比拟好的数据结构,查问的速度当然不是一个量级的。但同时 Redis 不会存储那么多的数据量,几个 T 的内存还是挺贵的。

Mysql 将数据贮存在硬盘上,为了进步查问速度,比拟好的做法是将索引数据和一部分热数据(常常拜访的数据)放到内存中(Mysql 的 Buffer Poll)。

当检索数据的时候,Mysql 通过索引查找,就能够晓得数据在磁盘哪块了,从硬盘对应地位读取对应的数据到内存中返回给客户端。

如果查问的时候没有走索引就须要扫描整个表数据文件,因为内存比硬盘小,会不停的从硬盘读取表中的一部分数据到内存,而后在内存中筛选出符合要求的数据,再去硬盘读取一部分数据做筛选直到整个表数据读取一遍。如果你有 20 g 数据,你想一下须要读取多长时间。

Mysql 8.0 绝对 Mysql 7.0 性能上有很大晋升,条件容许倡议应用 Mysql 8.0。

Mysql 数据存储

连贯 Mysql

# -h 指定 mysqld 的服务地址
# -P 指定连贯端口
# -u 执行用户(生产环境不倡议应用 root 用户连贯,正当应用权限治理。每个库应用不同的账号密码)# -p 输出明码
mysql -hlocalhost -P3306 -uroot -p

查看 Mysql 数据文件的目录

-- 连贯之后输出以下命令,查看数据贮存在哪里了
-- /var/lib/mysql/  Centos 7.0 存储地位
show variables like '%datadir%';

零碎表空间

零碎表空间是所有表共享的,它保留了数据表构造,事务信息 等

SHOW VARIABLES LIKE 'innodb_data_file_path%'
-- ibdata1:12M:autoextend

独立表空间

独立表空间是指每张表独自用一个文件贮存每张表对应的数据和索引,文件扩大名为 ibd。

每个数据库会有一个对应目录用于保留以后数据库中的数据文件

在 /var/lib/mysql/ 目录下
drwxr-x---    8 zhangpanqin  admin   256B  2 11  2020 leetcode
drwxr-x---    8 zhangpanqin  admin   256B 10 18  2019 mysql
-rw-r-----    1 zhangpanqin  admin    36M 11 29 18:31 mysql.ibd

当咱们在某个数据库中创立一张表时,除了在零碎表空间生成元数据和表构造,也会在对应的数据库目录下,新建一个 tablename.ibd 文件。

/usr/local/var/mysql/leetcode

-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 department.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 employee.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 logs.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 person.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 14  2020 scores.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 weather.ibd

Mysql 8.0 是默认开启独立表空间的,默认每张表应用一个文件进行保留数据和索引

-- 查看是否开启独立表空间配置
mysql> show variables like '%innodb_file_per_table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON    |
+-----------------------+-------+

InnoDB 逻辑贮存构造

表空间

InnoDB 存储引擎下,表相干的所有数据(比方业务数据和索引数据)都贮存在 表空间(tablespace)中。每张表都有一个本人的文件(.ibd)去贮存相干数据(开启独立表空间设置)。

表空间又能够细分为 segmentextentpagerow

表空间又蕴含多个段(segment),常见的数据段有:

  • Leaf node segment 数据段,存储以后表中的数据
  • Non-Leaf node segment 索引段,存储以后表中的索引

段蕴含很多个区,每个区始终为 1MB。区由多个间断间断的页组成,页的大小通常是 16KB,所以一个区能够有 64(1024/16=64)个间断页。

页是 InnoDB 与磁盘交互的最小单位。从磁盘上读取数据,一次性是读取一页数据。将内存中的数据落盘到硬盘上,也是操作一页数据。

比方咱们批改了 id=3 某行数据,数据长久化的时候,是须要将这行所在的页全副落盘在硬盘上。

update test_table set a=2 where id =3;

页也有类型,数据页,索引页等等。

-- 查看页的大小,默认是 16KB =16*1024bit
mysql> show variables like '%innodb_page_size%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+

每页寄存一行一行的数据。

mysql> SHOW TABLE STATUS LIKE "test_data_type"\G;
*************************** 1. row ***************************
           Name: test_data_type
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 22
 Avg_row_length: 14894
    Data_length: 327680
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: 243
    Create_time: 2020-09-21 01:55:51
    Update_time: 2020-09-21 02:06:59
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options:
        Comment:

Row_format 定义了一行数据在数据页中怎么保留。

VARCHAR(M)TEXT 类型的字段为变长字段,变长字段占用多少字节,记录在 变长字段长度列表

记录头信息中,记录着以后行的类型和下一条记录地位等信息。

行数据中,除了咱们表构造中本人定义的字段,还有 Mysql 增加的元数据字段。比方 行 id(当没有主键数据的时候增加),事务 id(次要用于 MVCC)和 回滚指针等。

一页能够存 16KB 数据,然而 VARCAHR(m),能够存 65535 字节。这些变长数据大于一页须要怎么存呢。这个景象也叫做行溢出。

行溢出的数据会独自存在一页中,在实在数据中对应的列中记录一个指针指向溢出的数据。

<font color=red>以上内容理解即可,只是为了了解原理及辅助表设计。</font>

数据类型

<font color=red> 表设计的时候肯定要选取适合的数据类型,能用数字就不要用字符串,一是缩小存储时空间的节约,二是缩小查问时内存的节约。</font>

整型

类型 形容 占用字节 范畴
tinyint 对应 java 中 byte 1 字节 有符号 -128 至 127。
无符号 0 至 255
smallint 对应 java 中 short 2 字节 有符号 -32768 至 32767。
无符号 0 至 65535
int 对应 java 中 int 4 字节 有符号 -2147483648 至 2147483647。
无符号 0 至 4294967295
bigint 对应 java 中 long 8 字节 有符号 -9223372036854775808 至 9223372036854775807
无符号 0 至 18446744073709551615

小数

类型 形容 占用字节
FLOAT(M, D) 对应 java 中 float 4 字节
DOUBLE(M, D) 对应 java 中 double 8 字节
DECIMAL(M, D) 对应 java 中 BigDecimal。定点数,能够准确保留小数 M 和 D 决定

M 示意小数的有效数字,D 示意小数点后的有效数字。

FLOAT(4, 1) 不能存 4000.1 会报谬误。

日期和工夫

类型 形容 占用字节 取值范畴
YEAR 年份 1 字节 1901~2155
DATE 日期,年月日 3 字节 1000-01-01~ 9999-12-31
TIME(fsp) 工夫,时分秒 3 字节 -838:59:59.000000 ~ 838:59:59.000000
DATETIME(fsp) 日期 + 工夫 5 字节 1000-01-01 00:00:00.000000 ~ 9999-12-31 23:59:59.999999
TIMESTAMP(fsp) 底层存储的是 UTC 工夫戳,
显示值会随 mysql 数据库所在时区变动
4 字节 1970-01-01 00:00:01.000000 ~ 2038-01-19 03:14:07.999999

TIMESTAMP(fsp) 中的 fsp 是指秒的精度 (x.xxx xxx),fsp 取值 0,1,2,3,4,5,6。

TIMEDATETIMETIMESTAMP 这几种类型反对小数秒。

DATETIME(0) 准确到秒,没有小数位。

DATETIME(3) 准确到豪秒,有三位小数。

<font color=red>日期和工夫存储时区的设置无关,肯定要搞清楚原理。</font>

TIMESTAMP 的显示和数据库系统设置的时区无关。TIMESTAMP 底层理论存储的是毫秒值,显示的工夫是依据设置的时区(time_zone)转换为工夫显示的。

还有 Java 1.8 新增的 LocalDateTime 须要怎么转换 Mysql 中的工夫呢。

-- 查看 mysql 的时区设置
SHOW VARIABLES LIKE "%time_zone%";
mysql> SHOW VARIABLES LIKE "%time_zone%";
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | CET    |
| time_zone        | +08:00 |
+------------------+--------+

system_time_zone Mysql 启动的时候获取计算机系统所在的时区。只有计算机的工夫精确就没有问题。

time_zone 设置的是连贯 mysql 的会话中,工夫(java.util.Date)转换为字符串时的 TimeZone。time_zone 这个值能够被 jdbc 连贯中的 serverTimezone=Asia/Shanghai 笼罩。

因为咱们是在东八区,心愿工夫都转换为东八区工夫。

[mysqld]
# 将工夫转换为东八区的工夫
default-time-zone = '+08:00'
CREATE TABLE `test_data_type` (`id` bigint(20) NOT NULL AUTO_INCREMENT,
  `test_data_time` datetime DEFAULT NULL,
  `test_timestamp` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=243 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

-- 理论保留数据的时候须要将日期转换为字符串,拼接成这样的 sql
INSERT INTO test_data_type (test_data_time,test_timestamp) VALUES ('2020-12-12 12:12:12','2020-12-12 12:12:12');

比方咱们将 java 中的 LocalDateTime 存为 datetime 类型。

@Data
@TableName(value = "test_data_type")
public class TestDataType {@TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField(value = "test_data_time")
    private LocalDateTime testDataTime;

    @TableField(value = "test_timestamp")
    private LocalDateTime testTimestamp;
}

当咱们保留数据的时候,须要依据配置的 time_zone, 将 LocalDateTime 转为 String,在替换到 sql 中的 ?

INSERT INTO test_data_type (test_data_time,test_timestamp) VALUES (?,?);
// NativeProtocol.configureTimezone 能够看到这个逻辑
@Test
public void run33() {
    // 首先获取到在服务器设置的 time_zone,如果没有设置的话,默认取 mysql 服务器所在时区
    String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
    // jdbc 链接中设置的参数
    String canonicalTimezone = getStringProperty("serverTimezone");
    if(canonicalTimezone==null||canonicalTimezone.length()<0){canonicalTimezone=configuredTimeZoneOnServer;}
    // jdbc url 参数中配置的 serverTimezone 和 time_zone 都是为了获取 TimeZone,serverTimezone 的优先级更高
     final TimeZone timeZone = TimeZone.getTimeZone(canonicalTimezone);
     // Timestamp 继承了 java.util.Date
     // 这里会将取得 LocalDateTime 获取其年月日时分秒上的值
    final Timestamp timestamp = Timestamp.valueOf(LocalDateTime.now());
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
    simpleDateFormat.applyPattern("yyyy.MM.dd HH:mm:ss");
    simpleDateFormat.setTimeZone(timeZone);
    
    // 而后将这个 time 替换?String time= simpleDateFormat.format(timestamp);
}

时区设置论断

<font color=red>当咱们保留数据的创立工夫的时候,只需获取以后工夫就行。以后时区与东八区的时差,mysql 配置的 time_zoneserverTimezone 转换成字符串时会主动加上。</font>

final Date createTime = new Date();
// LocalDateTime 肯定不要本人补时差,不然工夫会对不上
final LocalDateTime createTime2 = LocalDateTime.now();
// 上面这个用法是谬误的。这样数据库中保留的工夫比理论工夫多了八个小时
final LocalDateTime errorCreateTime =  LocalDateTime.now(ZoneId.of("UTC+8"));

字符串

varchar

varchar (M) 中 M 指的是字符数。<font color=red>Mysql 限度在一行数据中,所有 varchar 列的总字节数不能超过 65535 字节。 </font>

-- utf8mb4 理论会占用 1-3 字节
CREATE TABLE `test_varchar`  (`test_name` varchar(65535) CHARACTER SET utf8mb4  NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4;

执行上述 sql 报错为:

1074 - Column length too big for column 'test_name' (max = 16383); use BLOB or TEXT instead, Time: 0.000000s

验证所有 varchar 列总数据不能超过 65535

CREATE TABLE `test_varchar`  (`test_name1` varchar(7000) CHARACTER SET utf8mb4  NOT NULL,
    `test_name2` varchar(7000) CHARACTER SET utf8mb4  NOT NULL,
    `test_name3` varchar(7000) CHARACTER SET utf8mb4  NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4;

执行上述 sql 报错信息为

1118 - Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs, Time: 0.002000s

varchar (M) 理论占用字节数,除了数据的占用,还有数据字节数大小的记录(1-2 字节)。

<font color=red>varchar(255) 存储的 abc 的时候占用 4 个字节,然而这个数据加载到内存的时候是占用定义的时候指定的字节数(255*3 utf8 编码)所以这个数值不要轻易填写</font>

char

char(M) M 也是指的字符数,列采纳的字符集不同,char 类型数据占用大小也不一样。char 类型的数据没有达到指定字符数,数据库会主动补充空格,返回数据的时候在去掉空格。

当一个字符串太长的时候肯定要采取 text 类型的数据,text 类型的数据存储的时候会作为行溢出数据存储,就没有 65535 大小的限度。

// 能够看到占用
https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html
数据类型 总字节数 内容字节
VARCHAR(M)、VARBINARY(M) L+ 1 bytes if column values require 0 − 255 bytes,
L+ 2 bytes if values may require more than 255 bytes
TINYBLOB、TINYTEXT L+1 L<2^8
BLOB、TEXT L + 2 L<2^16
MEDIUMBLOB、MEDIUMTEXT L+ 3 L<2^24
LONGBLOB、LONGTEXT L+ 4 L<2^32

欢送关注我的微信公众号【Mflyyou】获取文章的继续更新。

github.com/zhangpanqin/MFlyYou 收集技术文章及我的系列文章,欢送 Star。


本文由 张攀钦的博客 http://www.mflyyou.cn/ 创作。可自在转载、援用,但需署名作者且注明文章出处。

如转载至微信公众号,请在文末增加作者公众号二维码。微信公众号名称:Mflyyou

退出移动版