MySQL入门笔记
MySQL的逻辑架构
第一层:
处理客户端连接、线程处理、身份验证、确保安全。每一个客户端都会在服务器进程中拥有一个线程,该连接的命令操作都只会在这个单独的线程执行。
第二层:
MySQL服务器层。主要分为解析器、优化器。
查询解析、分析、优化、以及所有的内置函数(日期、时间、数字和加密函数),所有跨存储引擎的功能也在这一层:存储过程、触发器、视图等。
第三层:
存储引擎层。存储引擎负责MySQL中数据的存储和读取。
并发控制
读写锁:
- 读锁:也称共享锁,既然是共享锁,那么多个线程读取同一个资源是不影响且不阻塞的。
- 写锁:也称排他锁,既然是排他锁,那么就是既会阻塞写入的线程也会阻塞读取的线程。
锁的粒度:
- 行级锁,服务器中实现,开销最小,但并发最低。
- 表锁,存储引擎级中实现,开销最大,但可以较大程度的支持并发。
MySQL的事务
在我们尝试理解和学习事务的概念之前,我们必须要牢记“自动提交”这个概念。在MySQL中(InnoDB),实际上是默认开启了事务的,即自动提交模式,你在MySQL客户端上执行的插入、更新、删除语句,实际上会隐式的帮你在前后加上“START TRANSACTION”和“COMMIT”这两个开始事务和提交事务的命令。
那么这里可以联想到在spring框架中的@Tansactional注解,它的作用便是帮我们开始一个多语句的事务,并帮我们提交或者在某一条语句失败时回滚事务。
Spring事务注解解析,仅提到以下三个常用的属性:
Propagation(事务的传播行为):
传播行为 | 说明 | 适用场景 |
REQUIRED(默认) | 如果当前存在事务,则加入该事务;否则新建一个事务。 | 大多数业务方法适用。 |
SUPPORTS | 如果当前存在事务,则加入;否则以非事务方式运行。 | 查询方法,允许非事务执行。 |
MANDATORY | 必须在一个已有事务中运行,否则抛出异常。 | 强制要求事务的方法。 |
REQUIRES_NEW | 始终新建事务,如果当前有事务,则挂起当前事务。 | 独立事务(如日志记录)。 |
NOT_SUPPORTED | 以非事务方式执行,如果当前有事务,则挂起。 | 不涉及事务的操作。 |
NEVER | 必须在非事务环境下执行,否则抛出异常。 | 禁止事务的方法。 |
Isolation(事务的隔离级别):
隔离级别 | 说明 | 可能的问题 |
DEFAULT | 使用数据库默认隔离级别(通常为 READ_COMMITTED)。 | 依赖数据库实现。 |
READ_UNCOMMITTED | 允许读取未提交的数据(最低隔离级别)。 | 脏读、不可重复读、幻读。 |
READ_COMMITTED | 只能读取已提交的数据(大多数数据库默认)。 | 不可重复读、幻读。 |
REPEATABLE_READ | 确保同一事务多次读取结果一致(MySQL 默认)。 | 幻读(InnoDB 通过 MVCC 避免)。 |
SERIALIZABLE | 完全串行化执行(最高隔离级别)。 | 性能低,无并发问题。 |
RollbackFor(回滚条件):
指定哪些异常触发回滚(默认仅RuntimeException和Error)。
原子性
一组事务可以是一条sql也可以是多条sql的组合,这些sql组合要么全部提交成功,要么失败一条而全部回滚。
一致性
拿转账的例子来说,一致性的意思就是A账户划走的钱,必然增加到了某一个B账户中。对于整个封闭系统来说,金额是恒定不变化。
隔离性
通常来说,一个事务所作的修改在最终提交之前,对其他事务是不可见的。这是通常情况,实际情况取决于存储引擎配置和生效的隔离级别。
隔离级别就是用于定义事务之间的可见程度的。
ANSI SQL标准定义了4中隔离级别,并且隔离级别越来越高:
读未提交:
顾名思义,即其他事务可以读取到当前事务还没有提交的改动。这样会导致其他事务读取到不正确的数据,成为“脏读”问题。
读已提交:
既然读未提交的级别会产生脏读,那么更进一步的隔离级别就是读已提交。这个隔离级别会出现“不可重复读”的问题。
试想,两个事务同时开始,事务A只需要执行一毫秒就完成并提交,事务B需要执行3毫秒才能完成并提交事务。那么在B事务开始前读取某个数据和它在第3秒中读取到的某个数据可能会出现不一致的情况,因为这个数据可能会因为事务A的提交而产生变化。
同一个事务中两次执行相同的语句,可能会看到不同的数据结果。这就是不可重复读问题。
可重复读:
那么为了解决不可重复读问题,标准又定义了可重复读的隔离级别。
这个隔离级别保证了在同一个事务中多次读取相同行的结果是一样的。
这是MySQL的默认隔离级别,但是还是会产生一个问题:“幻读”。幻读问题描述的不再是一行数据,而是强调的“行数”。在某个事务执行期间,如果相关表的行数发生了变化,则两次统计的结果也有可能不一致。可以理解幻读问题为“行数统计”的不可重复读问题。
问题:MySQL是如何实现的可重复读???
通过MVCC多版本并发控制策略,不同的事务根据自己的隔离级别可以看到不同版本快照的行记录数据。
问题:MySQL是如何解决幻读问题的呢???
通过间隙锁,InnoDB不仅锁定在查询中涉及到的行,还会对索引结构中间隙进行锁定,以防止幻行被插入。
串行化:
既然以上隔离级别都会出现各种各样的问题,那么只有上最终杀器了:那就是串行执行。隔离级别描述的多个事务的可见性程度,那么只要控制事务串行执行,就不存在可见程度的问题了,因为同一时刻只有一个事务在运行。
不过,这里说到的串行化,应该不是单纯的将MySQL服务器所有的事务都单线程执行。串行化级别下,会将涉及到的每一行数据上都会加锁,以此尽量缩小锁粒度,提高并发。
持久性
一旦提交,事务所作的修改就被永久保存到数据库中。此时即使系统崩溃,数据也不会丢失。
多版本并发控制(MVCC)
多版本并发控制,完整英文Multiversion Concurrency Control,多版本并发控制。
mvcc的具体逻辑
MVCC的工作原理是使用数据在某个时间点的快照来实现的,InnoDB引擎在每一个事务启动时分配一个事务id。当该事务中修改一条记录时,就会记录一条undo日志,并且该事务的回滚指针指向该undo日志,这样就可以实现读取到这条数据不同时间的版本的功能,这便是multi version的含义所在。
当不同的会话读取聚簇索引记录时,InnoDB会将该记录的事务id与该会话的读取视图进行比较。如果当前状态下的记录不应该可见(更改它的事务尚未提交),那么将通过undo日志读取旧的版本,直到一个符合可见条件事务id。这个过程可以一直循环到完全删除这一行,然后向视图发出这一行不存在的信号。
值得注意的是,所有undo日志的写入也都会写入redo日志。并且MVCC不适用于读未提交和串行化。
关键点:
- InnoDB 通过 undo logs 存储旧版本数据,支持事务回滚和一致性读(MVCC)。
- 每条记录包含 DB_TRX_ID(事务ID)和 DB_ROLL_PTR(回滚指针),指向 undo log 中的历史版本。
- 不同事务根据隔离级别(如 READ COMMITTED 或 REPEATABLE READ)访问合适的版本。
死锁、事务日志、复制、数据文件
死锁:指的是两个或多个事务相互持有和请求相同资源上的锁,产生了循环依赖。
为了解决这个问题,数据库系统实现了各种死锁检查和锁超时机制。比如InnoDB存储引擎,检测到循环依赖后会立即返回一个错误信息;如果真遇到了死锁,它的处理方式是将持有最少行级排他锁的事务回滚(最少行级排他锁说明回滚的成本更低)
事务日志:InnoDB存储引擎只需要更改内存中的数据副本,而不用每次修改磁盘中的数据,在每次修改内存数据前,记录事务日志,这种方式称为“预写式日志”,尽管实际数据没有落盘,但是通过事务日志仍可以在系统崩溃的情况下恢复数据到最新状态。
这里需要注意,事务日志是写入到磁盘,它比实际数据写入磁盘的成本更低,因为事务日志都是在磁盘的一小块顺序I/O,而实际数据需要根据存储引擎控制磁盘的写入,并且一般情况都是随机I/O。
复制:MySQL被设计为只能在一个节点上进行写操作,即“一主多副本”的架构,那么为了保证其他副本上的数据和主上的一致,MySQL实现了一种原生的数据同步操作,(注意这里不是直接同步数据,而是将一个节点执行的写操作分发到其他节点)称为复制。源节点为每一个副本节点提供一个线程,当写操作发生时唤醒这些线程,实现数据的同步。
隐式锁定和显式锁定
隐式锁定:InnoDB使用两阶段锁定协议(two-phase locking protocol)。在事务执行期间,随时都可以获取锁,但只有在提交或者回滚后才释放,并且所有的锁会同时释放。这部分锁定机制都是隐式的。InnoDB会根据隔离级别自动处理锁。
隐式锁定:InnoDB还支持通过特定语句显示锁定:
Select …… for update;
Select …… for share; (8.0才支持,之前的语句为Select …… lock in share mode;)
8.0版本的升级
主要变化可以总结为Schema数据InnoDB化。
- 性能优化、安全性增强
- 删除了缓存
- 默认字符集修改为utf8mb4(占四字节,可以存储emoji,之前的默认字符集为utf8,属于阉割版只占3字节),默认排序规则修改为utf8mb4_0900_ai_ci:基于 Unicode 9.0 标准,更准确
- 表元数据InnoDB化
8.0版本删除了基于文件的表元数据存储,并使用InnoDB引擎的表存储,使得 DDL语句的执行可以享受事务带来的稳定性。
- JSON数据结构的原生支持,以及相关函数和索引
- 引入了原子数据定义更改(原子DDL)
DDL语句的执行要么全部成功,要么失败回滚。