性能文章>数据库系列:聊聊MySQL并发控制>

数据库系列:聊聊MySQL并发控制原创

1年前
319401

数据库系列:MySQL慢查询分析和性能优化
数据库系列:MySQL索引优化总结(综合版)
数据库系列:高并发下的数据字段变更
数据库系列:覆盖索引和规避回表
数据库系列:数据库高可用及无损扩容
数据库系列:使用高区分度索引列提升性能
数据库系列:前缀索引和索引长度的取舍
数据库系列:MySQL引擎MyISAM和InnoDB的比较

1 介绍

并发控制是为了防止多用户并发使用数据库时造成数据错误和程序运行错误,保证数据的完整性。当多个事务并发地存取数据库时,就会产生同时读取和/或修改同一数据的情况。若对并发操作不加控制就可能会存取和存储不正确的数据,破坏数据库的一致性(Consistency)。因此,数据库中间件必须提供并发控制(Concurrency Control)机制能力,而MySQL的InnoDB引擎,很好的支持了这一块。

2 InnoDB并发控制

MySQL 是一个流行的关系型数据库管理系统,它支持多用户并发访问。并发控制是确保数据库一致性和完整性的重要机制。在 MySQL 中,有几种方法可以实施并发控制:

  • 读写锁(Read-Write Locks):MySQL 使用了读写锁来控制对数据的并发访问。读写锁是共享的,多个客户端可以同时持有读锁,但只有一个客户端可以持有写锁。当一个客户端获得写锁时,其他客户端无法获得读锁或写锁,直到写锁被释放。
  • 事务隔离级别 :MySQL 提供了不同的事务隔离级别,包括读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。较低的隔离级别允许更多的并发访问,但可能导致数据不一致;较高的隔离级别可以确保数据一致性,但会限制并发访问。
  • 锁等待和死锁:MySQL 提供了锁等待和死锁检测机制。当一个事务尝试获取一个已经被其他事务持有的锁时,MySQL 会阻塞等待,直到锁被释放。如果事务之间形成死锁,MySQL 会检测到并终止其中一个事务以解除死锁。
  • 分段锁定(Segmented Locking):MySQL 还支持分段锁定,它允许对数据库的特定部分进行锁定,而不是对整个数据库进行锁定。这对于处理大型数据集时非常有用,因为它可以减少锁定范围,提高并发性能。
  • 乐观并发控制(Optimistic Concurrency Control):MySQL 还支持乐观并发控制,它假设冲突不太可能发生,因此不会立即锁定数据。而是在更新时检查是否存在冲突,如果存在冲突,则进行适当的处理,比如回滚或重试。
  • 多版本并发控制(MVCC):MVCC允许在事务隔离级别下执行一致性读操作,以提高并发性能。

通过合理配置和使用上述机制,可以在 MySQL 中实现有效的并发控制,来保证在数据库中执行一致性和数据完整性。
下面详细来说说通过并发控制保证数据一致性的常见手段:

  • 锁(Locking)
  • 数据多版本(Multi Versioning)

3 锁的基本实现

MySQL 使用锁来保持数据的一致性。在并发控制中,锁是用来防止多个事务同时对同一数据进行修改或删除,以保持数据的一致性。MySQL 中的锁机制包括共享锁和排他锁。
锁的基本实现,一般是这样的:

  • 当用户对数据进行操作前,锁住,实施互斥,不允许其它任务的操作;
  • 当前操作完成后,释放锁之后,其他任务才可以执行;

但是存在一个问题,他的执行本质是串行的,无论读写,都无法并行,这样性能太差了,也不符合互联网高并发需求。
于是MySQL 中的锁机制实现了共享锁和排他锁:

  • 共享锁(Shared Lock):多个事务可以同时持有共享锁,用于读取数据,但不允许修改数据。共享锁允许并发读取,提高了并发性能。
  • 排他锁(Exclusive Lock):只有一个事务可以持有排他锁,用于修改数据,不允许其他事务同时修改。排他锁会阻塞其他事务的读写操作,降低了并发性能。
    简而言之就是:
  • 共享锁之间不互斥,即读读可以并行,这样提高了数据读取的并发能力
  • 排他锁与任何锁互斥,所以写读,写写不可以并行,其他线程的读写操作都在锁释放之后才能执行,这样对并发度是有较大影响的

所以说,单纯的锁机制,还是不满足需求,为了保证写任务没有完成之时,其他读的任务也可以并发执行,我们就需要使用另外一个能力来补充。
那就是数据多版本(Multi Versioning)

4 数据多版本的实现原理

MySQL的并发控制是通过多版本并发控制(MVCC)实现的。MVCC允许在事务隔离级别下执行一致性读操作,以提高并发性能。
在MySQL的MVCC中,每个数据行都有多个版本,每个版本都表示该行在不同时间点的状态。当一个事务读取数据时,它只看到该事务开始之前存在的数据版本,而不是当前最新的数据版本。这种方式允许并发读取多个数据版本,而不会相互阻塞,进一步提高并发的效果。
详细拆分开来,读写同步执行的原理是这样的:

  • 执行写任务发时,Clone一份数据,打上新的版本号,与原版本号区分
  • 写任务实际操作的是克隆那个版本的数据,直至操作并提交完成后
  • 读任务可以并发执行,持续读取,读的是原版本的数据,并不会造成阻塞

image.png

如图所示,可以分成这几个步骤去解读:

  1. 初始数据版本为V1.0
  2. T1发起了一个写任务,这时候把数据clone了一份,进行修改,版本变为V2.0,这时候修改进行中,任务还未完成
  3. T2并发了一个读任务,依然读的是V1.0版本的数据
  4. T3又并发了一个读任务,依然不会阻塞,读的还是V1.0的版本
  5. 这时候数据修改,V1.0的数据变为V2.0的数据
  6. T4这时候发起的度任务,读到的就是V2.0的数据了

从这边可以看出,数据多版本,读写之间不需要阻塞,能够极大提高任务的并发能力。

  • 普通意义上的锁机制,本质是串行执行,效率十分低下
  • 读写锁,可以实现读读并发,但是写读依然是互斥的,也不符合互联网的机制
  • 数据多版本(Multi Versioning),才是实现读写并发的要素

5 MySQL 数据多版本的相关实现

5.1 概念介绍

在MySQL的InnoDB存储引擎中,使用MVCC(多版本并发控制,Multiversion Concurrency Control)实现多版本控制。
MVCC的实现主要基于undo日志、redo日志、rollback segment 回滚存储区间、和read view。undo日志用于回滚操作,而read view用于生成数据行的历史版本。通过这种方式,InnoDB实现了非阻塞的一致性读操作。

  • redo日志
    数据库事务提交后,必须将更新的数据刷到磁盘上,以保证ACID特性。磁盘随机写性能较低,且过度频繁的刷盘,会极大影响数据库的吞吐量。
    优化方式是将修改行为先写到redo日志里,这样随机就变成了有序性,再按照时间周期将数据持久化到磁盘上,极大提高了性能。
    另外一方面,即使数据库崩溃,恢复之后也可以从redo日志里面获取操作Log,重新提交事务操作,然后刷盘,最终保证数据的一致性。

  • undo日志
    数据库事务未提交时,会将事务修改数据的Mirror Data(修改前的版本 )存放到 Undo Log中,它的主要作用是在事务执行过程中,如果发生错误或者需要回滚操作,可以通过Undo Log中的记录来撤销已经执行的操作,恢复数据到事务开始之前的状态。
    另外一方面,数据库奔溃时,也可以使用undo日志,撤销未提交事务,保证事务的ACID特性。

    • insert操作:undo日志存储新数据的PK(ROW_ID),回滚时执行删除即可。
    • delete/update操作:undo日志存储旧数据row(整行数据),回滚时直接恢复。
  • rollback segment
    Rollback Segment(回滚存储区)是数据库中的一部分存储空间,用于临时保存当数据库数据发生改变时的先前值。它主要有两个作用:

    • 通过Rollback操作来取消数据操作,使之恢复到改变之前的原始值,在transaction的过程才有效。如果执行了commit命令,那么Rollback Segment里面的值就会标识为失效,数据的改变将永久化。
    • select读取的同时另一个事务也在修改这个表的值,那么select出来的数据是修改前的值,因为修改之前的原数据存入到了Rollback Segment中,所以不会被阻塞到。

5.2 示例说明

5.2.1 初始数据

# 表结构
t_userinfo(id PK, name, ***, age);

# 默认数据
1,Brand,0,22
2,Helenlyn,1,19
3,Sol,0,21

image.png

先初始化一个默认的表,里面模拟几条数据。此时没有任何的事务未提交操作,所以回滚段是空的,如上图。

5.2.2 事务操作示例

start transaction;
delete from t_userinfo where id = 1;
update t_userinfo set name = 'Helenlyn...' where id = 2;
insert  into t_userinfo(name, ***, age) valus ('Lili', 1, 18);

还未执行commit 或者 rollback,所以事务处于未提交的状态

image.png

综上,我们可以看出Commit之前我们进行如下操作:

  • 正式提交删除前,id=1的数据作为旧版本的数据,进入了回滚存储区;
  • 正式提交修改前,id=2的数据作为旧版本的数据,进入了回滚存储区;
  • 新插入的数据 (‘Lili’, 1, 18), id= 4,在正式提交之前,也进入了回滚段;

我们上面说了,不是所有的操作最终都会commit,如果失败,事务rollback,就可以通过回滚存储区中的undo日志对操作进行回滚。

如果成功commit,则整体提交成功了
image.png
可以看到:

  • id=1 数据删除成功;
  • id=2 字段更新成功;
  • id=4 数据行插入成功;
  • 回滚存储区相关日志清掉

如果失败并执行rollback,则全部回滚
image.png

  • 数据删除的恢复了
  • 被修改的旧数据也恢复
  • 新增写入的数据删除
  • 回滚存储区相关日志清掉

6 总结

  • MySQL实现并发控制,保证数据一致性的方法有锁,数据多版本等
  • 普通锁串行,读写锁读读并行,Multi Versioning 读写并行;
  • redo日志保证已提交事务的ACID特性, undo日志用来回滚未提交的事务,rollback segment 为临时回滚存储区;
  • InnoDB是基于多版本并发控制的存储引擎;
  • InnoDB用的多版本是快照读不加锁,所有select都是快照读,这些数据不会被修改,并发性能特别高;

image.png
欢迎关注公众号【架构与思维】:撰稿者为bat、字节的几位高阶研发/架构。努力分享优质技术。

点赞收藏
Pinocao
请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

Redis系列:RDB内存快照提供持久化能力

Redis系列:RDB内存快照提供持久化能力

API性能调优
日常Bug排查-偶发性读数据不一致

日常Bug排查-偶发性读数据不一致

1
0