Skip to content

-------------------数据库-------------------

MySQL支持的存储引擎及其区别

存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。

在MySQL中提供了很多的存储引擎,比较常见有InnoDBMyISAMMemory

  • InnoDB:存储引擎是mysql5.5之后是默认的引擎,它支持事务、外键、表级锁和行级锁。DML操作遵循ACID模型,支持事务。有行级锁,提高并发访问性能。支持外键,保证数据的完整性和正确性。
  • MyISAM:不支持事务、外键,只有表级锁,用的不多
  • Memory:不支持事务、外键,只有表级锁,用的也不多,特点是能把数据存储在内存里
特性InnoDBMyISAMMemory
事务安全支持不支持不支持
锁机制表锁/行锁表锁表锁
外键支持不支持不支持

PostgreSQL 的唯一存储引擎:Heap

Heap 存储引擎是 PostgreSQL 的默认存储引擎,也是唯一内置的存储引擎。它具有以下特点:

  1. 事务支持:支持 ACID、MVCC,支持多种隔离级别,如读已提交(Read Committed)、可重复读(Repeatable Read)和序列化(Serializable)。
  2. 索引支持:支持 B-Tree、Hash、GiST(通用搜索树)、SP-GiST(空间分区通用搜索树)、GIN(通用倒排索引)和 BRIN(块范围索引)。
  3. 数据类型支持:支持自定义数据类型。支持整数、浮点数、字符串、日期时间、UUID、JSON、XML、数组、范围类型等。
  4. 扩展性:支持插件和扩展,可以添加新的功能和模块。支持外部表(Foreign Tables),可以访问其他数据源的数据。
  5. 备份和恢复:支持物理备份和逻辑备份。支持点-in-time 恢复(PITR)。
  6. 性能优化:支持查询优化器,可以生成高效的查询计划。支持分区表,可以将大表分成多个小表,提高查询性能。

PostgreSQL 的扩展:虽然 PostgreSQL 只有一种默认的存储引擎,但它通过扩展机制支持其他存储层和功能。以下是一些常见的扩展:

  1. TimescaleDB
    • 一个专门为时间序列数据设计的 PostgreSQL 扩展。
    • 支持高效的时间序列数据存储和查询。
  2. Citus
    • 一个用于水平扩展 PostgreSQL 的扩展,支持分布式查询和数据分片。
    • 适用于大数据和高并发场景。
  3. PostGIS
    • 一个用于地理空间数据的扩展,支持 GIS(地理信息系统)功能。
    • 支持空间索引、空间操作和地理数据类型。
  4. pg_stat_statements
    • 一个用于收集和报告查询统计信息的扩展。
    • 帮助优化查询性能和识别慢查询。
  5. pg_partman
    • 一个用于管理和维护分区表的扩展。
    • 支持自动分区和维护分区表的生命周期。

SQL语句的执行过程(MySQL)

(参数映射、sql解析、执行和结果处理)

  • 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
  • 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
  • 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
  • 优化器: 按照 MySQL 认为最优的方案去执行。
  • 执行器: 执行语句,然后从存储引擎返回数据。
查询语句执行流程

终极SQL分析——Hikvision

sql
WITH Sales_Summary AS (
    -- 计算每个产品类别在不同地区的总销售额,并为其分配排名
    SELECT 
        p.product_category AS Product_Category,
        s.region AS Region,
        SUM(s.sale_amount) AS Total_Sales_Region,
        ROW_NUMBER() OVER (PARTITION BY p.product_category ORDER BY SUM(s.sale_amount) DESC, s.region ASC) AS rn
    FROM products_info p
    JOIN sales_info s ON p.product_id = s.product_id
    GROUP BY p.product_category, s.region
),
Product_Summary AS (
    -- 计算每个产品类别的销售总额和不同产品ID的数量
    SELECT 
        p.product_category AS Product_Category,
        SUM(s.sale_amount) AS Total_Sales,
        COUNT(DISTINCT s.product_id) AS Number_of_Different_Product_IDs
    FROM products_info p
    JOIN sales_info s ON p.product_id = s.product_id
    GROUP BY p.product_category
),
Satisfaction_Averages AS (
    -- 计算每个产品类别的平均满意度
    SELECT 
        p.product_category AS Product_Category,
        ROUND(AVG(c.satisfaction_score),2) AS Average_Satisfaction_Score
    FROM products_info p
    JOIN customer_satisfaction_info c ON p.product_id = c.product_id
    GROUP BY p.product_category
)

SELECT 
    ps.Product_Category,
    ps.Total_Sales,
    ps.Number_of_Different_Product_IDs,
    sa.Average_Satisfaction_Score,
    ss.Region AS Top_Sales_Region
FROM Product_Summary ps
JOIN Satisfaction_Averages sa ON ps.Product_Category = sa.Product_Category
JOIN Sales_Summary ss ON ps.Product_Category = ss.Product_Category AND ss.rn = 1
ORDER BY ps.Product_Category ASC;

这段 SQL 语句是一个较为复杂的查询,使用了多个公共表表达式(Common Table Expressions,简称 CTE),主要用于汇总和分析不同产品类别的销售数据、产品数量以及客户满意度,并找出每个产品类别中销售额最高的地区。下面是对这段 SQL 的详细分析:

SQL 语句分析

这段 SQL 使用 CTE 可以使查询语句更加简洁、清晰,并且提高了可维护性和可读性。

1. Sales_Summary CTE

sql
WITH Sales_Summary AS (
    -- 计算每个产品类别在不同地区的总销售额,并为其分配排名
    SELECT 
        p.product_category AS Product_Category,
        s.region AS Region,
        SUM(s.sale_amount) AS Total_Sales_Region,
        ROW_NUMBER() OVER (PARTITION BY p.product_category ORDER BY SUM(s.sale_amount) DESC, s.region ASC) AS rn
    FROM products_info p
    JOIN sales_info s ON p.product_id = s.product_id
    GROUP BY p.product_category, s.region
)
  • 作用:计算每个产品类别在不同地区的总销售额,并为每个地区分配一个排名。
  • 字段说明
    • Product_Category:产品类别。
    • Region:地区。
    • Total_Sales_Region:该地区内的总销售额。
    • rn:在相同产品类别下,按总销售额降序排列的地区排名。

2. Product_Summary CTE

sql
Product_Summary AS (
    -- 计算每个产品类别的销售总额和不同产品ID的数量
    SELECT 
        p.product_category AS Product_Category,
        SUM(s.sale_amount) AS Total_Sales,
        COUNT(DISTINCT s.product_id) AS Number_of_Different_Product_IDs
    FROM products_info p
    JOIN sales_info s ON p.product_id = s.product_id
    GROUP BY p.product_category
)
  • 作用:计算每个产品类别的销售总额和不同产品的数量。
  • 字段说明
    • Product_Category:产品类别。
    • Total_Sales:该产品类别的总销售额。
    • Number_of_Different_Product_IDs:该产品类别下不同产品的数量。

3. Satisfaction_Averages CTE

sql
Satisfaction_Averages AS (
    -- 计算每个产品类别的平均满意度
    SELECT 
        p.product_category AS Product_Category,
        ROUND(AVG(c.satisfaction_score),2) AS Average_Satisfaction_Score
    FROM products_info p
    JOIN customer_satisfaction_info c ON p.product_id = c.product_id
    GROUP BY p.product_category
)
  • 作用:计算每个产品类别的平均客户满意度。
  • 字段说明
    • Product_Category:产品类别。
    • Average_Satisfaction_Score:该产品类别的平均满意度得分(保留两位小数)。

4. 主查询

sql
SELECT 
    ps.Product_Category,
    ps.Total_Sales,
    ps.Number_of_Different_Product_IDs,
    sa.Average_Satisfaction_Score,
    ss.Region AS Top_Sales_Region
FROM Product_Summary ps
JOIN Satisfaction_Averages sa ON ps.Product_Category = sa.Product_Category
JOIN Sales_Summary ss ON ps.Product_Category = ss.Product_Category AND ss.rn = 1
ORDER BY ps.Product_Category ASC;
  • 作用:最终查询结果,展示每个产品类别的总销售额、不同产品的数量、平均满意度以及销售额最高的地区。
  • 字段说明
    • ps.Product_Category:产品类别。
    • ps.Total_Sales:该产品类别的总销售额。
    • ps.Number_of_Different_Product_IDs:该产品类别下不同产品的数量。
    • sa.Average_Satisfaction_Score:该产品类别的平均满意度得分。
    • ss.Region AS Top_Sales_Region:销售额最高的地区。

WITH ... AS 用法解释

WITH ... AS 是 SQL 中的一个构造,用于定义公共表表达式(CTE)。CTE 是一个临时的结果集,只存在于包含它的查询中。它可以简化复杂的查询语句,使其更易读和维护。

优点

  1. 提高可读性:通过将复杂的查询拆分为多个CTE,可以使查询更加模块化和清晰。
  2. 减少重复:可以多次引用同一个CTE,避免重复编写相同的子查询。
  3. 提高性能:CTE 只执行一次,并且只在主查询需要时才执行,可以减少不必要的计算。

语法

sql
WITH CTE_Name (Column1, Column2, ...)
AS (
    SELECT ...
    FROM ...
    WHERE ...
    ...
)
SELECT ...
FROM CTE_Name
JOIN ...
ON ...
WHERE ...
...

SQL优化方案

总结

  • 表的设计优化

    根据实际情况选择合适的数值类型(tinyint、int、bigint)

    根据实际情况选择合适的字符串类型(char、varchar)

  • 索引优化

    对数据量打的表创建索引

    对常作为查询条件、排序、分组的字段创建索引

    尽量创建联合索引

    控制索引的数量

    ……

  • SQL语句优化

    合理编写SQL语句(避免直接使用select *、用union all代替union、能用inner join 就不用left join、right join、避免在where子句中对字段进行表达式操作)

    避免SQL语句造成索引失效的写法(使用函数或表达式处理索引列、隐式类型转换、使用不等于(<> 或 !=)操作……)

  • 主从复制、读写分离

  • 分库分表

定位慢查询的方法

SQL执行很慢,可能有一下原因:聚合查询、多表查询、表数据量过大查询、深度分页查询

需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:

yaml
# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2

配置完毕之后,通过以下指令重新启动MySQL服务器进行测试,查看慢日志文件中记录的信息 /var/lib/mysql/localhost-slow.log

image-20240407153408047

当然,也有相关的工具:

调试工具:Arthas 运维工具:Prometheus 、Skywalking

image-20240407153456512

分析SQL语句

sql
- 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;
image-20240407153708794

然后需要关注以下字段:

  • type:当前sql的连接的类型,性能由好到差为NULL、system、const、eq_ref、ref、range、 indexall

    system:查询系统中的表 const:根据主键查询 eq_ref:主键索引查询或唯一索引查询 ref:索引查询 range:范围查询 index:索引树扫描all:全盘扫描

  • possible_key:当前sql可能会使用到的索引

  • key:当前sql实际命中的索引

  • key_len :索引占用的大小

  • Extra:额外的优化建议

    Using where; Using Index:查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据 Using index condition:查找使用了索引,但是需要回表查询数据

*例:给 JSON 类型字段添加虚拟列

eg. 以一张用户信息表为例

sql
CREATE TABLE `student` (
  `stu_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `stu_name` varchar(255) DEFAULT NULL COMMENT '名字',
  `extra` json DEFAULT NULL COMMENT '备注',
--  `v_request_id` varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`extra`,'$.request_id'))) VIRTUAL,
  PRIMARY KEY (`stu_id`) USING BTREE,
--  KEY `idx_v_requiest_id` (`v_request_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

查询 json 类型的字段会走全表索引,耗时比较长,因此这时可以用虚拟列

sql
INSERT INTO student 
( stu_name, extra ) 
VALUES 
( "张三",'{ "age" : 18, "gender" : "男", "request_id":"123" }')

SELECT * 
FROM student 
WHERE v_request_id = 123

EXPLAIN SELECT * 
FROM student 
WHERE v_request_id = 123

创建虚拟列及其索引,虚拟列的值会与 json字段中的指定的键值对匹配更新,如果没有就为null,非常省心

sql
ALTER TABLE student 
ADD COLUMN `v_request_id` VARCHAR(32) 
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(`extra`, '$.request_id'))) VIRTUAL;

CREATE INDEX idx_v_requiest_id ON test_json (v_request_id)

再分析sql性能,发现已经走虚拟列索引了(如果没走索引还是走全表,可能是数据量太少了)

sql
SELECT * 
FROM student 
WHERE v_request_id = 123

EXPLAIN SELECT * 
FROM student 
WHERE v_request_id = 123

底层原理:order by 的实现原理(MySQL 5.7)

这涉及到两种排序规则:

  1. 全字段排序(有主键的情况走这种,几乎所有情况都是这样的)
  2. row id排序(没有主键的情况走这种,不太常见)

假设有以下SQL语句,

sql
SELECT name,age,city 
FROM user 
where city = '深圳'
ORDER BY age 
LIMIT 10;

全字段排序加载过程

最普遍的情况,有主键的情况下采用全字段排序

  1. 根据索引从聚集索引树中找到对应的ID;
  2. 在聚集索引树找到对应的整行数据;
  3. 查询字段(很多人会用*,导致内存消耗很大)和排序字段加载到sort buff;
  4. 在sort buff中根据关键字进行排序;
  5. 取出前10条数据,返回结果集。
image-20240913155516920

row id排序加载过程

row id排序比全字段排序多了一次回表,但是比全排序占用更少的内存

  1. 根据索引从非聚集索引树中找到对应的ID;
  2. 在聚集索引树找到对应的整行数据;
  3. 数据的主键(省内存的原因)和排序字段加载到sort buff;
  4. 在sort buff中根据关键字进行排序;
  5. 取出前10条数据,再去进行一次回表查询得到整行数据;
  6. 根据查询的字段值,返回结果集。
image-20240913155436825

拓展:sort buffer

sort buffer的作用

sort buffer 指的是用于排序操作的内存缓冲区。当执行排序操作时,例如使用 ORDER BY 子句对结果集进行排序,数据库可能会使用一个或多个排序缓冲区来存储数据。

  1. 减少I/O操作:通过在内存中暂存要排序的数据,可以减少从磁盘读取数据的次数,从而提高排序速度。
  2. 提高排序效率:在内存中进行排序通常比在磁盘上进行排序更高效。因此,使用 sort buffer 可以帮助加快排序过程。

sort buffer的工作原理

  • 数据加载:当数据库需要对查询结果进行排序时,它首先会将部分数据加载到 sort buffer 中。
  • 排序操作:数据加载完成后,数据库会在 sort buffer 内执行排序算法。如果数据量超过了 sort buffer 的容量,则可能需要将部分数据写入临时文件,并进行外部排序。
  • 结果输出;排序完成后,数据库会将排好序的数据返回给客户端或用于进一步处理。

如何配置 sort buffer

在MySQL中,sort_buffer_size是一个全局或会话级别的系统变量,用于控制每个客户端连接可用的 sort buffer 的大小。

可以通过以下命令查看或修改该参数:

sql
SHOW VARIABLES LIKE 'sort_buffer_size';
SET SESSION sort_buffer_size = value;  -- `value` 是以字节为单位的大小

调整 sort_buffer_size 可以影响排序操作的性能。如果设置得过小,可能导致频繁地将数据写入磁盘,从而降低性能;如果设置得过大,则可能消耗过多内存资源。

last_updated 字段意义的思考

  1. **数据同步和一致性。**在主从同步中,从数据库同步主数据库时,通过对比本地的 last_updated 和主节点的 last_updated,可以知道需要同步哪些数据
  2. 审计和追踪。last_updated 字段可以帮助定位最后一次更新的时间,进而确定变动的来源和责任人。
  3. 并发控制(乐观锁)。不必单独设置一个字段 version,但需要手动维护last_updated
  4. 数据备份和恢复。在数据备份和恢复过程中,last_updated 字段可以用来判断哪些数据是最新的,哪些数据需要恢复。特别是在系统发生故障或数据丢失时,备份数据可能并非实时更新,因此需要依赖last_updated字段来进行增量恢复。
  5. 数据预热。在处理定期批量更新操作时,系统只需要查询那些 last_updated 字段在某个时间范围内的数据,而不必每次都处理所有数据,减少了不必要的查询负担。

----------数据库-索引(MySQL)----------

索引

索引创建原则

  1. 数据量较大,且查询比较频繁的表
  2. 常作为查询条件、排序、分组的字段
  3. 字段内容区分度高
  4. 内容较长,使用前缀索引
  5. 尽量创建联合索引
  6. 控制索引的数量
  7. 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它

索引失效情况

  1. 违反最左前缀法则
  2. 范围查询右边的列
  3. 在索引列上进行运算操作
  4. 字符串不加单引号
  5. 以%开头的Like模糊查询

索引不一定有效的原因

  1. 选择性差:如果索引列包含大量重复值(即选择性差),则查询优化器可能会决定全表扫描比使用索引更高效。
  2. 索引列少:如果查询涉及到多个条件,而索引只覆盖了部分条件,则可能不会被使用。
  3. 索引列顺序不当:对于复合索引,如果最左边的列不是查询中最常过滤的列,则索引可能不会被有效地利用。
  4. 数据范围广:如果查询返回的数据行接近整个表的大小,那么索引可能没有帮助,因为查询优化器可能会认为全表扫描更优。
  5. 未使用合适的访问类型:如使用 LIKE 开头字符匹配或 IN 子句等,可能导致 MySQL 无法使用索引。
  6. 统计信息过时:MySQL 使用统计信息来决定是否使用索引,如果数据分布发生变化,需要更新统计信息。

B+树索引

除了B+树类型的索引,还有全表索引、哈希索引……只是不太常用

索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构

特点

  • 索引是帮助MySQL高效获取数据的数据结构(有序)
  • 提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

优点

  • 阶数更多,路径更短
  • 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
  • B+树便于扫库和区间查询,叶子节点是一个双向链表
  • 使用索引可以大大加快数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

缺点

  • 创建、维护索引或对表进行操作需要重构索引。
  • 索引需要使用物理文件存储,也会耗费一定空间。

B树与B+树的区别是什么?

B+树比B树查找效率更高的原因:

  1. B+树的所有的数据都会出现在叶子节点,所以查找时首先只需考虑如何找到索引值,而不需要比较值;
  2. B+树叶子节点是一个有序的双向链表,适合进行范围区间查询。
  • B树
image-20240407154800153
  • B+树
image-20240407154859696

聚索引 & 非聚集索引

聚集索引:非叶子节点存储主键id,叶子节点存放主键id和整行数据。一张表有且只有一个聚集索引。

非聚集索引:非叶子节点存放索引字段,叶子节点存放索引字段和主键id。一张表可以有多个非聚集索引。

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引。

  • 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。

  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。

  • 聚簇索引非聚簇索引 有的时候又称为 主索引树辅助索引树

回表查询

回表查询:通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表。

image-20240407155539271

覆盖索引

覆盖索引:是指二级索引中包含了查询所需的所有字段,从而使查询可以仅通过访问二级索引而不需要访问实际的表数据(主键索引)。

  • 使用id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
  • 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *

超大分页处理方案

在数据量比较大时,limit分页查询,需要对数据进行排序,效率低,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引+子查询形式进行优化。

例如,该查询语句可以这样优化:

sql
select * from user limit 9000000,10;
sql
select * from user u,
	(select id from user order by id limit 9000000,10) a 
where u.id = a.id;

解释:采用子查询通过主键索引查询到了第9000000行的数据,接着顺序读取10行得到10个id,然后将这10行id与外部sql做一个自连接,通过主键索引树查询直接得到了第9000000后的10行数据。

image-20240915135034179

索引的维护

索引的数据结构

首先通过两个类实现B+树非叶子节点和叶子结点:

  1. 非叶子节点类InternalNode

    java
    private List<AbstractTreeNode<K, V>> childrenNodes;  // 孩子节点
  2. 叶子节点类LeafNode

    java
    private List<K> keys;         // 叶子节点中的键,即主键索引值
    private List<V> values;       // 叶子节点中的值,即整行数据
    private LeafNode<K, V> next;  // 下一个叶子节点的指针

B+树的增删查改操作

  • 查 (Search):从根节点开始二分查找,B+树中使用二分查找可能在一个节点中找不到对应的结点,所以需要根据键值去子节点的孩子节点中遍历查找,直到找到叶子节点中对应的key和整行数据。

    img
  • 改 (Update):先去查询,如果键已存在,更新其值;如果键不存在,则修改失败。

  • 删 (Delete):先去查询,进行删除,可以用逻辑删除删除-合并

    • 逻辑删除:只清空整行记录,不清除键,保持B+树的形状。
    • 删除-合并:删除后如果节点元素过少,需要进行合并。
  • 增 (Insert):先去查询,查找插入位置,插入后判断是否需要分裂。分裂算法:new一个新的叶子节点,将当前叶子节点一半的键和键对应的值移动到新的叶子节点,然后将新的叶子节点插入到原本的叶子节点链表中。之后更新父节点的索引,将新的叶子节点中最小 的 key 传递给父节点,父节点插入这个新的 key 作为索引。如果父节点也超出了最大容量,同样会进行分裂并向上传递。当一个非叶子节点分裂时,都需要将分裂产生的新的 key 上移到父节点。如果父节点也满了,继续分裂并将 key 递归上传。

主键索引的维护

例如在执行下面这句话时:

sql
update user set id=id+1 where age=18;

数据库会从 id 索引找到对应的数据行,然后更新 id 值。对于主键索引来说,更新主键值可能导致数据页移动,因为主键值是数据页的物理位置标识。

非主键索引的维护

非聚集索引的叶子节点存储了指向实际数据行的指针,如果修改了索引列的值,那么非聚集索引对应的叶子节点也会相应更新。

例如在执行下面这句话时:

sql
update user set money=1000 where age=18;
  1. 根据 age 主键索引去查询符合条件的的记录,对找到的行上排他锁 (X lock),保证并发事务的安全性和一致性,避免脏读、不可重复读等问题;
  2. 对找到的行进行更新操作,去 money 的非主键索引树修改 money 的数据。数据库会先删除原来的数据,然后按序将之前修改删除的索引结点插入在某个叶子结点后面;
  3. 更新完成后,会释放所持有的锁,并提交事务。
image-20240920135558697

----------数据库-事务(InnoDB)----------

表锁

表级锁是最粗粒度的锁,会对整个表进行锁定,导致并发性能较差。MyISAM 支持以下两种类型的锁:

  1. 读锁(READ LOCK):当 SELECT 语句执行时,会自动获得读锁,此时其他事务可以读取数据,但不能修改数据。
  2. 写锁(WRITE LOCK):当 INSERT、UPDATE 或 DELETE 语句执行时,会自动获得写锁,此时其他事务既不能读也不能写。

由于 MyISAM 已经不再推荐使用,并且在新版本的 MySQL 中逐渐被淘汰,因此表级锁的使用也逐渐减少。

行锁

行级锁对表中的进行锁定,而不是整个表,这样可以大大提高并发性能。InnoDB 支持以下几种类型的锁:

  1. 共享锁(Shared Locks, S-Locks):当 SELECT 语句带有 FOR SHARE 或者事务处于可重复读隔离级别时,会请求共享锁。共享锁允许其他事务读取数据,但阻止其他事务修改同一行数据。
  2. 排他锁(Exclusive Locks, X-Locks):当事务需要写入数据时,会请求排他锁。排他锁不允许其他事务读取或修改同一行数据。

其他类型的锁

除了上述的锁类型外,InnoDB 还有一些特殊的锁机制:

  1. 意向锁(Intention Locks):这是一种元锁,它并不锁住具体的行,而是表明事务打算对表中的行加锁。例如,意向共享锁(IS)表明事务打算对某行加共享锁,意向排他锁(IX)表明事务打算对某行加排他锁。
  2. 间隙锁(Gap Locks):间隙锁锁定的是索引项之间的“间隙”,防止其他事务插入新的行到这个间隙中。在可重复读RR隔离级别下,InnoDB 默认会使用间隙锁。
  3. Next-Key Locks:Next-Key 锁是 InnoDB 默认使用的锁类型,它是共享锁或排他锁与间隙锁的组合。它不仅锁住索引项本身,还会锁住索引项之间的间隙,以防止幻读现象。

死锁检测的基本原理

  1. 定时检测:使用定时检测。如果发现等待队列(Wait Queue)增长到一定长度时,就会触发一次死锁检测。
  2. 图算法:使用图算法。它构建了一个等待图(Wait-for Graph),在等待图中,节点代表事务,边表示事务间的等待关系。如果有环路(Cycle)存在,那么就表示发生了死锁。

死锁解决机制

一旦检测到死锁,InnoDB 就会采取措施来解决它。具体做法如下:

  1. 选择牺牲者:当检测到死锁时,InnoDB 会选择一个或多个事务作为“牺牲者”,这些事务将被回滚,以解除死锁。
  2. 选择标准:InnoDB 根据一定的标准来选择牺牲者。一般情况下,InnoDB 会选取一个最小的事务作为牺牲者。这个最小事务通常是基于事务的开始时间、事务的大小(即所持有的锁的数量)等因素来决定的。
  3. 通知用户:InnoDB 在回滚了某个事务后,会生成一条错误信息(如 Error 1213 Deadlock found when trying to get lock),并通过客户端 API 返回给应用程序。应用程序可以根据这个错误信息来进行相应的处理。

事务

事务的ACID特性?事务的并发解决方案?

  • 事务的特性
    • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
    • 一致性Consistency):事务完成时,必须使所有的数据都保持一致状态。
    • 隔离性Isolation):保证事务在不受外部并发操作影响的独立环境下运行。
    • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
  • 事务并发的问题
问题描述
脏读一个事务读到另外一个事务还没有提交的数据。
不可重复读并发更新时,另一个事务前后查询相同数据时的数据不符合预期。
幻读并发新增、删除这种会产生数量变化的操作时,另一个事务前后查询相同数据时的不符合预期
  • 事务的并发解决方案:对事务进行隔离
隔离级别脏读不可重复读幻读
Read Uncommitted 读未提交:可以直接读取到其他事务未提交的事务×××
Read Committed 读已提交:要等待其他事务提交后才能读取××
Read Repeatable 可重复读 (默认) :通过MVCC机制确保一个事务内多次执行相同的查询会得到相同的结果×
Serializable 串行化:加入读锁,阻塞式处理事务

长事务可能会导致哪些问题?

会有性能问题,具体如下:

  1. 锁定资源:可能会占用大量的锁资源。
  2. 死锁(Deadlock):存在两个或更多事务互相等待对方释放资源。
  3. 内存消耗:长事务占用较多的内存资源,特别是回滚段(undo segment)的空间,从而影响系统的性能。
  4. 日志文件增长:长事务会导致日志文件快速增长,这需要更多的磁盘空间,并且在恢复时需要更多的时间。

底层原理:TRANSACTION语法的意义

START TRANSACTION;

当执行 START TRANSACTION; 命令时,MySQL 将当前的会话设置为非自动提交模式。这意味着任何随后的 SQL 操作都不会自动持久化到磁盘上,而是保存在事务的内存缓冲区中。在这个阶段,数据库会记录每一步操作的日志到redo log和undo log中,以备提交或回滚。

底层原理:

  1. 事务开启:创建一个事务记录,并初始化事务的状态为活动状态
  2. 非自动提交模式设置会话的自动提交标志为 false,这样后续的 SQL 操作不会立即生效。
  3. 记录日志:对于每一个 SQL 操作,都会记录对应的重做日志(Redo Log),用于事务提交时的数据恢复。

COMMIT;

执行 COMMIT; 命令表示事务已经成功完成,所有的事务操作都应该被永久地保存到数据库中。此时,数据库将确保所有事务中的更改都已正确地应用,并且任何后续的操作都不能影响到这些更改。

底层原理:

  1. 事务提交:事务进入提交阶段,数据库系统会将事务中的所有更改标记为永久有效。
  2. 写入磁盘将事务期间记录的所有重做日志(redo Log)写入到磁盘上的日志文件中,确保即使在系统崩溃的情况下也能恢复数据。
  3. 释放资源:事务完成后,释放事务期间占用的资源,如锁定的行或表等。
  4. 通知监听器:事务提交后,可能会通知正在等待该事务完成的其他事务或监听器。

ROLLBACK;

执行 ROLLBACK; 命令表示事务中的所有操作都将被撤销,数据库将回到事务开始前的状态。这意味着事务中所做的任何更改都不会被保存到数据库中。

底层原理:

  1. 事务回滚:事务进入回滚阶段,数据库系统会恢复到事务开始前的状态。
  2. 撤销更改通过事务日志(undo Log)来撤销事务期间所做的更改
  3. 释放资源:事务回滚后,同样会释放事务期间占用的资源,如锁定的行或表等。

WAL

WAL(预写日志,Write-Ahead Logging)是一种数据保护机制,它在对数据进行实际写操作之前,先将这些操作记录到日志文件中,确保在数据库对外部变化(如崩溃、电源中断等)进行恢复时,数据的一致性持久性

WAL 的工作过程

  1. 日志条目的写入:当数据库要进行更改时,不是直接更改数据文件,而是先将这些更改记录到 WAL 文件中。只有在这些日志条目已经安全地存储到磁盘之后,数据库才会开始更新实际的数据。这种方法允许数据库在意外崩溃之后,通过查阅 WAL 文件来恢复所有未完成的事务,进而恢复到一致的状态。
  2. 检查点机制:为了保障 WAL 文件不会无限制地增长,数据库会定期创建检查点。这些检查点允许删除旧的 WAL 文件,并减少恢复所需的时间。

WAL 的优点

  1. 数据恢复:WAL 是保证数据一致性的重要手段。它提供了一种在出现故障时快速恢复数据库的方法。通过读取 WAL 文件,数据库可以重做(redo)在崩溃前进行的所有操作,从而恢复到崩溃时的状态。
  2. 写性能优化:由于可以将多个更改合并成一个大块进行写入,WAL 有助于优化磁盘写入性能。这减少了对磁盘的频繁小量写操作。
  3. 异步备份:WAL 文件可以用于进行异步日志传输,提供了数据库的备份和恢复方案,支持只读副本和灾难恢复。

WAL 与 MVCC

  1. MVCC 机制用来管理并发事务,
  2. WAL 文件则用来记录事务的更改历史。

在故障恢复时,先使用 WAL 恢复未提交的事务,然后根据 MVCC 的版本控制进行数据的回滚

MVCC

MVCC是如何保证数据的可恢复性的?

MVCC需要保障如下两条原则:

  1. 正在进行的事务不会读取未提交的事务产生的数据。

  2. 正在进行的事务不会修改未提交的事务修改或产生的数据。

于是,MVCC就通过如下步骤实现了数据回滚:

MVCC通过维护每个事务的开始时间和版本号来判断是否需要回滚。当事务试图读取或修改数据时,系统会比较当前数据版本与事务的开始时间。如果事务尝试访问的版本在其开始时间之后已被其他事务提交,那么该事务会被标记为需要回滚,因为它基于过时的数据进行操作。这种机制确保了数据的一致性和隔离性。

行的可见性判定

每一行都有两个特殊的字段:

  • xmin :创建这个行的事务 ID
  • xmax:删除/更新它的事务 ID

当一个事务读取数据时,它会根据 xminxmax 判断当前事务的可见性。

那么一条记录什么时候是可见的呢?

满足如下两个条件:

  1. xmin 对应的事务已经提交
  2. xmax 对应的事务未提交或未开始

什么是事务日志 / redo log,undo log?

总结:

redo log: 记录的是数据页的物理变化,服务宕机可用来同步数据
undo log :记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
redo log保证了事务的持久性,undo log保证了事务的原子性和一致性

redo log:重做日志,确保了已提交的事务在数据库崩溃重启后,能够保持数据的持久性一致性redo log是物理日志,它包含以下两种类型的信息:

  1. 物理页的变化:某些数据库系统(如Oracle)可能直接记录数据页变化后的状态。这意味着在重做日志中,你会看到一个数据页在某次操作之后的样子。
  2. 操作的描述:另一些数据库系统(如MySQL的InnoDB存储引擎)则记录了如何重做某个特定操作的信息,即记录了需要对哪些页进行什么样的更新才能重现这些页在事务提交后的状态。

undo log:回滚日志,确保了未提交的事务在数据库崩溃重启后,不会对数据库的数据造成影响,实现隔离性。此外在可重复读取隔离等级下,undo log 还可以维持读取视图的一致性,即保证同一个查询在事务内多次执行时返回相同的结果。undo log是逻辑日志,它的基本结构特点如下:

  1. 版本链(Version Chain)
    • 在 InnoDB 存储引擎中,每个数据页都有一个版本链,其中包含了该页上所有行的多个版本。这些版本信息是由 Undo Log 维护的。
  2. 重做片段(Undo Segments)
    • Undo Log 通常被组织成 Undo Segments,每一个 Undo Segment 包含一个或多个 Undo Records。Undo Segments 可以进一步分为两类:Insert Undo Segments 和 General Undo Segments。
      • Insert Undo Segments 主要用于插入操作的事务,当事务只包含插入操作时,可以使用 Insert Undo Segments。一旦事务提交,这部分 Undo Log 就不再需要,可以被重用。
      • General Undo Segments 用于包含删除、更新等操作的事务,这类事务提交后,Undo Log 需要保留一段时间,直到不再有活跃事务需要访问这些旧版本。
  3. 重做记录(Undo Records)
    • 每个 Undo Record 包含了数据项在某个时间点的值,以及指向其前后版本的指针。这样可以构建出一个版本链,用于追踪数据项的历史版本。
  4. 回滚指针(Rollback Pointer)
    • 每个事务都有一个 Rollback Pointer 指向 Undo Log 中的一个位置,这个位置标识了事务开始时的数据状态。当事务需要回滚时,系统可以根据 Rollback Pointer 从该位置开始恢复数据到事务开始前的状态。
  5. 时间戳(Timestamps)
    • Undo Log 中还包括时间戳信息,这有助于判断版本的有效性,特别是在 MVCC 环境下,用于决定哪个版本对于给定的查询是可见的。
image-20240407162732189

MVCC 实现一致性和隔离性的原理

MVCC机制下一条记录会有多个版本,每次修改记录都会存储这条记录被修改之前的版本。多版本之间串联起来就形成了一条版本链,这样不同时刻启动的事务可以无锁地获得不同版本的数据(普通读)。此时读(普通读)写操作不会阻塞,写操作可以继续写,无非就是多加了一个版本,历史版本记录可供已经启动的事务读取。

这一切的实现主要依赖于每条记录中的隐式字段undo log日志ReadView

1. 隐式字段

隐藏字段含义
DB_TRX_ID最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
DB_ROLL_PTR回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。
(DB_ROW_ID)隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。)

2. undo log

  • 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

  • undo log版本链:不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录

    image-20240407163613418

3. ReadView 读取视图

ReadView解决了一个事务查询选择版本的问题,根据ReadView的匹配规则和当前的一些事务id判断该访问那个版本的数据。

ReadView是一个事务在开始时可见的数据快照。每当一个事务启动时,系统会创建一个ReadView,记录当前活跃事务的列表和事务的时间戳。通过这个视图,事务可以访问在其开始时已经提交的版本,而忽略后续提交的变更。这确保了事务的隔离性,使得它在执行过程中看到的数据始终保持一致,避免了幻读和脏读问题。

不同的隔离级别快照读:RC(读已提交):每一次执行快照读时生成ReadView、RR(可重复读):仅在事务中第一次执行快照读时生成ReadView,后续复用。

  • 工作过程:快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。

  • 当前读:写操作时(update、insert、delete(排他锁),select ... lock in share mode(共享锁),select ... for update),读取的是记录的最新版本,读取时会对读取的记录进行加锁,保证其他并发事务不能修改当前记录。

  • 快照读select时,非阻塞式地读取记录数据的可见版本,有可能是历史数据

    • Read Committed:每次执行select,都生成一个快照读,这个新生成的快照读可能会造成不可重复读
    • Repeatable Read(默认):仅在事务开始时生成ReadView,后续复用
  • ReadView是一个数据结构,包含了四个核心字段:

    字段含义
    m_ids当前活跃的事务ID集合
    min_trx_id最小活跃事务ID
    max_trx_id预分配的事务ID,当前最大事务ID+1
    creator_trx_idReadView创建者的事务ID

----------------分布式数据库----------------

数据库集群

主从同步的原理

主从复制的核心就是二进制日志

主从复制步骤:

  1. Master 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中。
  2. 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
  3. Slave重做中继日志中的事件,将改变反映它自己的数据。
image-20240407164836130

如何保证主从的数据一致性?

  1. 利用读写分离框架特性,如ShardingJDBC可以要求下一条SELECT强制走主库。但会增大主库压力,可能出现性能瓶颈。
  2. 采用MGR(MySQL Group Replication)全同步复制,强一致数据同步没完成主从同步之前,jdbc.insert()方法无法得到结果新项目推荐:无需改代码,真正的一致性方案,老项目不推荐,传统应用集群向MGR迁移成本高、风险大

分库分表时机

  1. 项目业务数据逐渐增多,或业务发展比较迅速
  2. 优化已解决不了性能问题(主从读写分离、查询索引…)
  3. IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)

分表分库策略

  1. 垂直分库,根据业务进行拆分,高并发下提高磁盘IO和网络连接数
  2. 垂直分表,冷热数据分离,多表互不影响
  3. 水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
  4. 水平分表,解决单表存储和性能的问题

分库

分库(Database Sharding)是指将一个大的数据库拆分成多个小的数据库,每个数据库称为一个“分片”(Shard)。每个分片包含部分数据,通过某种策略将数据分布到不同的分片中。

分库策略

  1. 范围分片:根据某个字段的值范围进行分片,例如按用户 ID 的范围。
  2. 哈希分片:根据某个字段的哈希值进行分片,例如按用户 ID 的哈希值。
  3. 列表分片:根据某个字段的具体值进行分片,例如按城市名称。

分库的实现方式

  1. 应用层分片:在应用层实现分片逻辑,通过路由算法将请求分发到不同的数据库分片。
  2. 中间件分片:使用数据库中间件(如 MyCAT、ShardingSphere)来管理分片,提供透明的分片和路由功能。

优点、缺点

  • 提高性能:通过将数据分散到多个数据库中,可以减少单个数据库的负载,提高查询和写入性能。

  • 增加可伸缩性:可以轻松地通过增加更多的分片来扩展系统。

  • 提高可用性:即使某个分片出现故障,其他分片仍然可以正常工作,提高了系统的可用性。

  • 复杂性增加:需要管理多个数据库实例,增加了系统的复杂性。

  • 跨分片查询:跨分片的查询和事务管理更加复杂,可能需要额外的中间件支持。

  • 数据迁移:随着数据量的增长,可能需要重新分片,数据迁移和维护成本较高。

分表

分表(Table Partitioning)是指将一个大的表拆分成多个小的表,每个小表称为一个“分区”(Partition)。每个分区包含部分数据,通过某种策略将数据分布到不同的分区中。

分表策略

  1. 范围分区:根据某个字段的值范围进行分区,例如按日期范围。
  2. 列表分区:根据某个字段的具体值进行分区,例如按地区代码。
  3. 哈希分区:根据某个字段的哈希值进行分区,例如按用户 ID 的哈希值。
  4. 复合分区:结合多种分区策略,例如先按日期范围分区,再按地区代码分区。

分表的实现方式

  1. 物理分区:在数据库中创建多个物理表,每个表存储部分数据。
  2. 逻辑分区:使用数据库的分区功能(如 PostgreSQL 的表分区、MySQL 的分区表)来创建逻辑分区。

优点、缺点

  • 提高查询性能:通过将数据分散到多个分区中,可以减少单个分区的数据量,提高查询性能。

  • 优化存储:可以将冷数据和热数据分开存储,优化存储空间和访问效率。

  • 简化维护:可以单独对某个分区进行维护操作,如备份、索引重建等。

  • 复杂性增加:需要管理多个表分区,增加了系统的复杂性。

  • 跨分区查询:跨分区的查询和事务管理更加复杂,可能需要额外的优化和索引支持。

  • 分区管理:需要定期维护分区,例如添加新的分区、删除旧的分区等。

分片

分片(Database Sharding)涵盖了分库和分表的概念。分片是指将数据分散到多个节点或分区中,每个节点或分区包含部分数据,通过某种策略将数据分布到不同的节点或分区中。

分片的类型

  1. 水平分割:数据库分片是指将数据库中的数据水平地分割成多个部分,并将这些部分分布到不同的物理数据库服务器上。这意味着每台服务器上只存储一部分数据,而不是全部数据。
  2. 垂直分割:与水平分割相对的是垂直分割(Vertical Partitioning),即将不同的表分割到不同的服务器上。然而,垂直分割通常不是我们讨论的“分片”的主要内容,而是另一种优化策略。

分片的目的

  • 提高性能:通过将数据分布在多个服务器上,可以并行处理更多的请求,从而提高整体性能。
  • 负载均衡:分片可以将负载均衡到多个服务器上,避免单点过载。
  • 提高可用性:如果一个分片服务器宕机,其他分片服务器仍然可以继续提供服务,提高了系统的可用性。
  • 数据地理分布:分片还允许将数据存储在不同的地理位置,以减少延迟并满足数据驻留法规要求。

分片的实现方式

  • 应用层分片:在应用层实现分片逻辑,通过路由算法将请求分发到不同的节点或分区。
  • 中间件分片:使用数据库中间件(如 MyCAT、ShardingSphere)来管理分片,提供透明的分片和路由功能。

分片带来的挑战

  1. 数据一致性:在分布式环境中,保持数据的一致性是一个挑战,需要使用如分布式事务、分布式锁等技术来保证。
  2. 数据迁移:当需要增加或移除分片时,涉及到大量的数据迁移工作。
  3. 查询复杂性:跨分片的查询变得复杂,可能需要在多个分片上执行查询并合并结果。
  4. 故障恢复:分片系统需要设计有效的故障恢复机制,以确保在某个分片失效时系统仍然可以正常运行。

分库、分表、分片的示例

分库示例

假设你有一个电商系统,用户分布在不同的城市。可以按城市进行分库:

  • 数据库1:北京用户
  • 数据库2:上海用户
  • 数据库3:广州用户

分表示例

假设你有一个订单表,订单数据按日期进行分表:

  • 订单表1:2023年1月的数据
  • 订单表2:2023年2月的数据
  • 订单表3:2023年3月的数据

分片示例

假设你有一个全球用户系统,用户分布在不同的国家和地区。可以按国家和城市进行分片:

  • 分片1:中国北京用户
  • 分片2:中国上海用户
  • 分片3:美国纽约用户
  • 分片4:英国伦敦用户

总结

  • 分库:适用于大规模数据和高并发场景,通过将数据分散到多个数据库中,提高系统的可伸缩性和性能。
  • 分表:适用于单个表数据量过大的场景,通过将数据分散到多个表中,提高查询性能和存储效率。
  • 分片:涵盖了分库和分表的概念,通过将数据分散到多个节点或分区中,提高系统的性能和可伸缩性。

怎么设计数据库分片?

分片键

确定分片键(Sharding Key),这将决定数据如何分配到不同分片。

常见的分片键包括:

  • 用户ID:适用于社交网络、电子商务等场景。
  • 地理位置:适用于需要根据地理位置存储数据的应用。
  • 时间戳:适用于日志记录、历史数据存储等场景。

分片算法

根据选定的分片键,设计分片算法。

常见的分片算法包括:

  • 范围分片:根据某个字段的值范围进行分片,例如按用户 ID 的范围。

    • 数据根据某个字段(如用户ID)的值范围分配到不同的分片上。
    • 优点是可以根据数据的自然分布来进行分片,易于理解和实现。
    • 缺点是如果数据分布不均,可能导致某些分片负载过高。
  • 哈希分片:根据某个字段的哈希值进行分片,例如按用户 ID 的哈希值。

    • 数据根据一个或多个字段的哈希值分配到不同的分片上。
    • 优点是哈希值的均匀分布可以较好地平衡各分片的负载。
    • 缺点是当增加或移除分片时,需要重新哈希分配数据,可能导致大规模的数据迁移。
  • 列表分片:根据某个字段的具体值进行分片,例如按城市名称。

  • 复合分片:结合多种分片策略,例如先按日期范围分片,再按地区代码分片。

一致性哈希算法

  • 一种特殊的哈希分片算法,用于解决哈希分片在动态调整分片数量时的问题。

  • 通过虚拟节点来模拟环形拓扑结构,使得在添加或移除分片时,只需要重新分配受影响的数据部分。

如何保证数据的分布一致性?

数据映射规则

需要定义一个映射规则,将数据映射到具体的分片上。

例如,可以使用模运算来实现哈希分片:

java
int shardId = userId.hashCode() % numberOfShards;

分片存储

每个分片存储在不同的数据库实例上,需要确保每个实例上的数据是相互独立的。可以使用如下方法来实现:

  • 使用不同的数据库实例:每个分片对应一个数据库实例。
  • 使用同一个数据库实例的不同表或Schema:适合数据量不大,且对性能要求不高的场景。

如何保证分片数据的一致性?

分布式事务

分片后,跨分片的事务处理变得更加复杂。需要使用分布式事务来保证数据的一致性。常用的方法包括:

  • 两阶段提交(Two-Phase Commit, 2PC):协调多个分片上的事务。
  • 三阶段提交(Three-Phase Commit, 3PC):在2PC的基础上增加了预准备阶段,提高了可靠性。
  • 最终一致性(Eventual Consistency):通过消息队列或事件驱动的方式,在事务完成后异步同步数据。

分布式锁

在并发场景下,需要使用分布式锁来保证数据的一致性。常用的技术包括:

  • ZooKeeper:提供分布式锁服务。
  • Redis:使用Redis的SETNX等命令实现分布式锁。

数据同步

对于需要实时同步的数据,可以使用以下方法:

  • 主从复制(Master-Slave Replication):将数据从主分片复制到其他分片。
  • 异步消息队列:使用Kafka、RabbitMQ等消息队列进行数据同步。