最近在阅读林晓斌老师的《MySQL实战45讲》,结合已经阅读的《InnoDB 存储引擎》,做了一些笔记记录。作为学习笔记,只会记录知识点,老师举的用例基本都不作记录,当然,对一些知识点也会做适当补充。
个人感觉内容不太适合初学者阅读,比较适合有一定基础,想要系统化学习的同学,当然,也非常适合需要面试复习的同学。
目录
一条 SQL 查询语句是如何执行的
MySQL 基础架构
大体来说,MySQL 可以分为 Server 层 和 存储引擎层 。
Server 层 包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
存储引擎层 负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。
连接器
负责跟客户端建立连接、获取权限、维持和管理连接。需要注意的是:MySQL 在执行过程中 临时使用的内存是管理在连接对象里面 的,这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM)。
针对该问题,老师给出以下两种解决方案:
- 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
- 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程 不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
分析器
包含词法分析he语法分析。 词法分析:识别 SQL 语句里各个字符串代表的意义; 语法分析:根据语法规则,判断输入的 SQL 语句是否满足 SQL 语法。
优化器
决定最终的 SQL 是怎么执行。比如选择哪个索引、多表查询过程中的优先顺序等。
执行器
首先检查执行权限,然后根据表的引擎定义,使用对应引擎提供的接口。
小结问答
Q: 如果表 T 中没有字段 k,执行 select * from T where k=1, 那肯定是会报不存在这个列的错误: “Unknown column ‘k’ in ‘where clause’”。请问这个错误是在哪个阶段返回的?
A:分析器。词法分析主要做的是根据 mysql 的关键字进行验证和解析,而语法分析会在词法解析的基础上进一步做表名和字段名称的验证和解析。
一条 SQL 更新语句是如何执行的
更新 SQL 的执行链路和查询 SQL一样,区别在于更新 SQL 会多记录两个日志: redo log(重做日志)和 binlog(归档日志)。
我觉得该小节对这两个日志的介绍不够全面,而本人以前也写过一篇相关的博文,所以这里就不再记录老师所讲内容。
下面是本人之前记录的博文:
为什么你改了我还看不见
从标题大致可以看出来这一篇讲的是 事务的隔离性。
SQL 标准事务隔离级别
- 读未提交(RU): 一个事务还没提交时,它做的变更就能被其他事务看到;
- 读已提交(RC):一个事务提交之后,他做的变更才能被其他事务看到;
- 可重复读(RR):一个事务执行过程中看到的数据,跟它在启动时看到的数据是一致的;
- 串行化(SR):读写分别加锁。
事务隔离的实现
在 InnoDB 中,每个事务都有一个唯一的 事务 ID(transaction id),它是在事务开始的时候向 InnoDB 事务系统申请的,并且严格递增。
记录在每次变更的时候,会生成一个新的数据版本,版本号就等于该事务 ID,同时会记录一条 回滚日志(undo log)。此外,新版本的记录会有一个指针指向回滚日志中该记录的旧版本值(这个值并不是物理真实存在的,而是根据回滚日志计算得到的),也就是说,同一条记录在系统中可以存着多个版本,这也被称为 多版本并发控制(MVCC)。
在进行查询时,会生成 当前所有活跃事务的一个视图(read-view),不同时刻启动的事务会生成不同的 read-view,根据 read-view 的可见性规则,不同事务就会看到不同的值。
读已提交和可重复读都是通过 视图 来实现的,区别在于视图的创建时间不同:
- 读已提交: 每个 SQL 查询开始执行时创建,即 同一个事务的不同查询看到的不是同一个视图;
- 可重复读:在事务启动时创建,整个事务存在期间都用这个视图。
PS. 这里老师没有对 read-view 做详细的介绍,为了不打断文章的思路,我们后面再补充。
回滚日志什么时候删除?
当没有事务再需要用到这些日志的时候,回滚日志才会被删除。
也就是说,直到系统里没有 包含比这个回滚日志更早的记录的 read-view 时才会删除回滚日志。
所以,我们一定要 避免使用长事务。(因为长事务的存在,会导致回滚日志一直不能删除)
事务的启动方式
1、显示启动事务语句: begin 或 start transaction (需要注意,执行 begin / start transaction 只表示要启动一个事务,但是事务真正开始的时间点是 start transaction 之后执行的第一条快照读语句。如果你想要将 start transaction 作为事务的起始时间点,必须使用 start transaction with consistent snapshot);
2、set autocommit = 0 命令;
小结问答
Q:我们已经知道,系统要尽量避免长事务,那有什么方案来避免出现或者处理它呢?
A: 分别从应用开发端和数据库端来处理这个问题:
- 从应用开发端来看:
- 确认是否使用了 set autocommit=0,如果是,则修改为 autucommit=1。
- 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。
- 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。
- 从数据库端来看:
- 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 或者 kill;
- Percona 的 pt-kill 这个工具不错,推荐使用;
- 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;
- 如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。
补充 read-view 相关知识
具体可以参考博文 多版本并发控制 MVCC。
隐藏列
InnoDB 会为每一个数据行添加 三个隐藏列:
- DB_TRX_ID 6 byte,最近修改(修改/插入)的事务ID;删除也被视为修改,只是通过一个特殊标记位来表示已删除;
- DB_ROLL_PTR 7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)
- DB_ROW_ID 6 byte,自增ID(隐藏主键),如果数据表没有主键,也不存在唯一索引,InnoDB 会以 DB_ROW_ID 作为聚簇索引列。
MVCC 里主要涉及到 DB_TRX_ID 和 DB_ROLL_PTR 两个字段。
Read-View
Read View 是某一事务 执行快照读时产生的 当前所有活跃事务的一个视图,用作可见性判断。
Read View 有四个重要字段:
- m_ids:生成 Read View 时,当前系统中 尚未提交的 事务 id 列表;
- min_trx_id:生成 Read View 时,当前系统中 尚未提交的事务列表中最小的事务 id,即 m_ids 中的最小值;
- max_trx_id:生成 Read View 时,系统中 应该分配给下一个事务的 id 值;即 m_ids 中的最大值 + 1;
- creator_trx_id:创建该 Read View 的事务 id。
可见性规则:
1、如果被访问的记录版本号 DB_TRX_ID 等于 Read View 中的 creator_trx_id ,表明当前事务访问的是自己修改的记录,可以被访问;
2、如果被访问的记录版本号 DB_TRX_ID 小于 min_trx_id,表明该版本的数据在当前事务生成 Read View 之前已经提交,可以被访问;
3、如果被访问的记录版本号 DB_TRX_ID 大于或等于 max_trx_id 值,表明生成该版本数据的事务在当前事务生成 Read View 后才开启,不可以被访问,需要沿着回滚指针寻找该记录的历史版本继续判断;
4、如果被访问的记录版本号 DB_TRX_ID 介于 min_trx_id 和 max_trx_id 之间,需要进一步判断 DB_TRX_ID 是不是在活跃事务列表 m_ids 中:
如果在:说明创建 Read View 时生成该版本的事务尚未提交,该版本数据不可以被访问,需要沿着回滚指针寻找该记录的历史版本继续判断; 如果不在:说明创建 Read View 时生成该版本的事务已经被提交,可以被访问,直接返回。
深入浅出索引(上)
索引常见模型
添加索引是为了提高查询效率,实现索引的方式有多种,常见的有:哈希表、有序数组、搜索树。
从使用的角度来比较以上三种模型:
哈希表
哈希表是一种 键 - 值 结构,这种结构在 等值查询 时效率很高,可以达到 O(1) 的时间复杂度。
但是这种结构 不适用于范围查询。
有序数组
有序数组就是一个排了序的数组,这种结构的 等值查询 和 范围查询 性能都非常优秀。
但是,在更新数据的时候就很麻烦了,比如,我们要往一个排序数组中间插一个值,就必须挪动后面的所有值,因此,这种结构只适用于 静态存储引擎,即存储不会修改的数据。
搜索树
大家熟知的 二叉搜索树,它的特点是:每个节点的左儿子小于父节点,右儿子大于父节点,查询和更新时间复杂度都是 O(log(N))。
但是二叉树并不适用于存储引擎,因为每个节点只有 2 个子节点,节点很多的时候,树就会很高。关系型数据库一般都是基于磁盘存储的,树越高,代表需要产生越多的磁盘 I/O,显然效率越低。
由此引申到 多叉树(B 树、B+ 树都属于多叉树),即每个节点有多个子节点,然后 保证处于同一层的节点从左到右是按照从小到大排序的。
对比二叉树,假设多叉树的一个节点有 1200 个子节点,那么四层高的树就可以存储接近 17 亿的值,这就大大减少了与磁盘的交互次数。(准确的计算其实涉及到数据页相关的概念,以后文章会详细介绍,这里主要是为了和二叉树做个比较,有个概念就好)
InnoDB 索引模型
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为 索引组织表。
InnoDB 使用了 B+ 树索引模型,数据都存储在 B+ 树的叶子节点上。
每一个索引在 InnoDB 里面都对应一棵 B+ 树。
根据叶子节点的内容,索引类型分为 主键索引 和 非主键索引:
- 主键索引 的叶子节点存的是 整行数据,主键索引也被称为 聚簇索引(clustered index)。
- 非主键索引 的叶子节点内容是 主键的值,非主键索引也被称为 二级索引(secondary index)。
因此,如果是用主键作为查询条件,扫描一次主键索引树就可以得到所有数据,而如果是通过非主键索引查询数据,需要扫描非主键索引得到主键值后,再通过主键索引得到数据。这个过程也被成为 回表。(这里其实还有一个概念,就是如果要查询的列从索引中就能够取得,那就不用回表查询了,这叫 索引覆盖)
下图展示了 2 种索引的结构,其中 ID 为主键索引, k 为非主键索引。
可以看到,ID 索引叶子节点上是 R1 R2...等一整行数据; k 索引叶子节点上是 100、200.. 等主键值。
索引维护
B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。B+ 树的维护是很复杂的,老师在这里也只粗略地介绍了以下几种情况:
1、新插入的值比现有的值都大;这个很简单,直接插到最后面就可以了;
2、新插入的值位于现有节点的中间某个位置,这时可能会涉及到 页分裂;
3、当相邻页由于删除数据,利用率变得很低之后,会发生 页合并。
真实的 B+ 树维护是极其复杂的,而 InnoDB 针对 B+ 树索引的维护 比起单纯的 B+ 树维护还有自己的一些优化处理。大家有兴趣的可以去阅读下 《InnoDB 存储引擎》 这本书。
知道了造成页分裂的大致原因,现在你应该知道为什么建议表使用自增主键了吧。(因为自增主键的插入数据模式,符合前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂)
其实,自增主键不止有这一点好处。从存储空间的角度来看,自增 ID 可以选用 整形(4 字节)或 长整行(8字节),这比起使用类似雪花算法生成的这种唯一 ID (一般是64位)来说,也是一大优势;毕竟索引值越小,代表一个页(16KB)可以存放的值就越多,在等高的索引树上,能存放的记录也就越多。
当然,也有例外,比如,有些业务场景只需要一个索引,并且它是唯一的。这种场景就需要权衡一下是否还有必要使用 自增ID 作为主键了,因为毕竟使用自增 ID 作为主键,意味着大部分的查询都会产生回表,而直接使用 业务数据作为主键,可以减少一次回表。
小结问答
Q: 在索引维护中提到,索引因为删除、页分裂等原因,导致数据页有空洞,此时可能需要重建索引。重建索引会创建一个新的索引,将数据按顺序插入,这样可以提高页面的利用率。重建索引一般的执行过程是先删除索引,然后新建索引,这种作法适用于普通索引和主键索引吗?
A: 适用于普通索引,但是不适用于主键索引。对于主键索引,无论是删除还是创建,都会将整个表重建,因此,对于上述先删除再重建的过程,删除这个步骤完全是多余的。对于主键索引的重建,老师给了参考答案:通过执行 alter table T engine=InnoDB 来替代 先删除后新建 这两个操作。
深入浅出索引(下)
覆盖索引
这个其实在上节介绍索引回表的时候我还补充了,原来老师放在这里单独讲了,再复习一遍吧。
假如有下表,其中 ID 为主键索引,k 为二级索引。
create table T (
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k)
)engine=InnoDB;
insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');
如果执行的语句是 select ID from T where k between 3 and 5,注意这个查询 只需要返回 ID 的值,而不是整行数据, 而 ID 值其实已经在 k 索引树上了(二级索引叶子节点的值就是主键值),因此不再需要回表。也就是说,在这个查询里面,索引 k 已经 “覆盖了” 我们的查询需求,我们称为 覆盖索引。
可以看到,覆盖索引可以减少一次对主键索引树的遍历,所以使用覆盖索引是一个常用的性能优化手段。
最左前缀原则
假设有一张客户信息表,其中,存在一个联合索引 (name, age),这个索引树的叶子节点如下图:
可以看到它的排序规则:先按 name 排序,在 name 相等的条件下,再按 age 排序。
最左前缀包含两个表示方式:
1、以 name 整个字段做查询,比如 select * from T where name = '张三' ;
2、以 name 的最左的 M 个 字符做查询, 比如 select * from T where name like '张%' ;
而仅仅以 age 或 name like ’%三‘ 等做查询是不能使用这个联合索引的。
索引下推
还以上述联合索引为例,假如有个查询需求:检索出表中 “名字第一个字是张,而且年龄是 10 岁的所有人”。SQL语句很简单: select * from tuser where name like '张 %' and age=10;
根据最左前缀规则,这个查询会用到 “张” 在联合索引树上定位到记录 ID3,然后再判断其他字段是否满足条件:
- 在 MySQL 5.6 之前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。
- MySQL 5.6 引入的 索引下推 优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
比如这个查询,在没有索引下推的优化下,需要回表 4 次,而在有索引下推优化的时候,对于 age != 10 的记录就可以直接跳过,不需要再回表判断了,因此,这里就只需要 2 次回表。
小结问答
Q: 假如有下表:
CREATE TABLE geek (
a int(11) NOT NULL,
b int(11) NOT NULL,
c int(11) NOT NULL,
d int(11) NOT NULL,
PRIMARY KEY (a,b),
KEY c (c),
KEY ca (c,a),
KEY cb (c,b) ) ENGINE=InnoDB;
在存在以下两种查询的条件下,是否有必要创建 “ca” “cb”这两个索引 ?:
select * from geek where c=N order by a limit 1;
select * from geek where c=N order by b limit 1;
A: 首先,可以根据实际数据来判断,如果根据 c 列条件选择后的记录数不多,那么 "ca","cb" 两个索引都是没有必要的,因为记录数很少的情况下,排序也不花什么时间;
当然,如果根据 c 列条件选择后的记录数很多,那么此时 "cb" 这个索引就有存在的必要了,而 "ca" 索引其实是多余的。根据二级索引的结构,在索引 c 这个二级索引树上,其主键值已经是按 先 a 后 b 的顺序做了排序,因此,第一个查询根据 a 排序是不需要多余的索引来构建的。
全局锁和表锁
数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则,锁就是用来实现这些访问规则的重要数据结构。
根据加锁的范围,MySQL 里面的锁大致可以分成 全局锁、表级锁和行锁 三类。
全局锁
全局锁 即对整个数据库实例加锁。命令: flush tables with read lock(FTWRL)。它会让整个库处于一个只读的状态,之后其他线程所有的 数据更新语句以及 DDL 等语句都会被阻塞。
全局锁的典型使用场景是做 全库逻辑备份。
但是毕竟是锁住整个库,相当于在这期间,整个业务都得停摆,这对于实时应用来说肯定是不可以接受的。
官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数 –single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。由于 MVCC 的支持,这个过程中数据是可以正常更新的。
这里需要注意的是,single-transaction 方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。
表级锁
MySQL 表级锁有两种:表锁 和 元数据锁(MDL)。
表锁
加表锁的语法是 lock tables table_name read/write。可以用 unlock tablestable_name 主动释放锁,也可以等客户端断开的时候自动释放。
对于 InnoDB 这种支持行锁的引擎来说,基本也不会使用表锁来控制并发,毕竟锁住整个表也是蛮大影响的。
元数据锁(MDL)
MDL 锁是在 访问表的时候自动加上的,它的作用是用来 保证读写的正确性。比如说一个查询正在遍历一个表中的数据,另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
MDL 锁也分为 MDL 读锁 和 MDL 写锁:
- 在对表进行增删改查等语句时,加 MDL 读锁,读锁之间不互斥;
- 在对表结构做变更等 DDL 语句时,加 MDL 写锁,读锁与写锁 以及 写锁之间都是互斥的。
因为 MDL 写锁的存在,会导致所有的增删改查等语句都被阻塞,因此,在对数据表进行 DDL 等操作时,要特别注意。
为避免 MDL 锁造成事故,你可能需要注意以下2点:
1、首先要避免长事务,因为只要事务不提交,它就会一直占着 MDL 锁。想像一下,如果 T1 时刻一个长事务占着 MDL 读锁,在 T2 时刻对表结构进行变更,此时需要加上 MDL 写锁,并且会被阻塞;现在问题就是在 T2 时刻以后所有的增删改查都需要加 MDL 读锁,并且都会被阻塞,相当于整个表都被锁住了一样。因此,在执行 DDL 等语句时,最好要先确认下是否有长事务存在。在 MySQL 的 information_schema 库的 innodb_trx 表中,可以查到当前执行中的事务,如果刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
2、如果你要操作的表会被频繁访问,最好是在 alter table 语句里面 设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃,之后再重试。
-- 如果没有获取到直接返回失败
ALTER TABLE tbl_name NOWAIT add column ...
-- 在 n 秒内没有获取到锁则返回失败
ALTER TABLE tbl_name WAIT N add column ...
小结问答
Q: 备份一般都会在备库上执行,在用 –single-transaction 方法做逻辑备份的过程中,如果主库上的一个小表做了一个 DDL,比如给一个表上加了一列。这时候,从备库上会看到什么现象?
A: 先看下备份过程几个关键的步骤:
Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT;
/* other tables */
Q3:SAVEPOINT sp;
/* 时刻 1 */
Q4:show create table `t1`;
/* 时刻 2 */
Q5:SELECT * FROM `t1`;
/* 时刻 3 */
Q6:ROLLBACK TO SAVEPOINT sp;
/* 时刻 4 */
/* other tables */
Q2:获取一致性视图;Q3:设置一个保存点;Q4:获取表结构;
Q5:导数据;Q6:回滚到 SAVEPOINT sp,释放 t1 的 MDL 锁。
分时刻讨论:
- 如果在 Q4 语句执行之前到达,现象:没有影响,备份拿到的是 DDL 后的表结构。
- 如果在 时刻 2 到达,表结构已被修改,Q5 执行的时候,报 Table definition has changed, please retry transaction,现象:mysqldump 终止;
- 如果在 时刻 2 和 时刻 3 之间到达,mysqldump 占着 t1 的 MDL 读锁,binlog 被阻塞,现象:主从延迟,直到 Q6 执行完成。
- 从 时刻 4 开始,mysqldump 释放了 MDL 读锁,现象:没有影响,备份拿到的是 DDL 前的表结构。
怎么减少行锁对性能的影响
行锁 即针对表中行记录的锁,相比于表锁,它的粒度更小,因此,在并发控制上有更好的性能。
MySQL 的行锁是在引擎层实现的,因此,我们说 MySQL 是否支持行锁其实还要看用的是哪个存储引擎。比如常用来比较的 MyISAM 引擎就不支持行锁,而 InnoDB 则支持。
InnoDB 行锁遵循两阶段锁协议。
两阶段锁协议
两阶段锁协议:将事务分成 2 个阶段,加锁阶段 和 解锁阶段。在执行 sql 语句的时候加锁,但是并不是执行完就释放锁,而是 等到事务结束时才释放。因此,在一个需要对多行加锁的事务中,可以把最可能造成锁冲突、最可能影响并发度的锁放在最后面执行,这可以最大程度地减少事务之间的锁等待,提高并发度。
因为加锁后要等到事务结束时才会释放,并且在加锁阶段没有任何的顺序要求,因此,两阶段锁协议不能防止死锁的发生。
死锁和死锁检测
死锁
产生死锁的四个必要条件:
- 互斥:一个资源每次只能被一个进程/线程使用。
- 请求与保持:一个进程/线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程/线程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程/线程之间形成一种头尾相接的循环等待状态。
遵循两阶段协议的行锁是可能发生死锁的,比如两个事务分别申请了A, B 锁,接着又申请对方的锁,此时就进入了死锁状态。
当出现死锁后,有两种解决策略:
1、等待,直到超时,超时时间可以通过参数 innodb_lock_wait_timeout (mysql 默认 50s)来设置;
2、发起 死锁检测,发现死锁后,主动回滚死锁链中的某一个事务(mysql 选择杀死小的事务,这里的小指的是执行的 insert,update, 影响语句数目小的事务),让其他事务继续执行。
死锁检测
InnoDB 提供了 wait-for graph 算法 来主动进行死锁检测,每当加锁请求无法立即满足需要进入等待时,wait-for graph 算法就会被触发。当数据库检测到两个事务不同方向地给同一个资源加锁,它就认为发生了死锁。 死锁检测本身是有很大消耗的,因为每个事务只要发生锁等待,就需要去检测是否造成了死锁,这本身是一个复杂度为 O(n) 的操作,如果有 1000 个并发线程要同时更新同一行,那死锁检测就是 100 万的量级。
那针对这种 热点行更新导致的性能问题,要怎么解决呢?
- 走河边的方案:临时关掉死锁检测;
- 不太好落地的方案:控制客户端并发度,依赖于你有好的客户群;
- 大佬方案:修改 MySQL 源码,针对相同行的更新,在进入引擎之前排队;
- 普通人方案:在服务端通过中间件来控制并发度,或者将一行改成逻辑上的多行来减少冲突。比如,你要对一个账户做转账,你可以对这个账户设置10个子账户,客户端可以和 10 个子账户交互,最后再汇总到最终的那个账户。
小结问答
Q: 如果你要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:
- 第一种,直接执行 delete from T limit 10000;
- 第二种,在一个连接中循环执行 20 次 delete from T limit 500;
- 第三种,在 20 个连接中同时执行 delete from T limit 500。
A: 第二种方式相对来说是最好的。
第一种方式单个语句占用时间长,锁的时间也比较长,属于大事务了;
第三种方式是属于没有困难,制造困难,人为制造锁冲突。
事务到底是隔离的还是不隔离的
这篇文章其实主要讲的还是 MVCC。如果之前你已经阅读了 多版本并发控制 MVCC 这篇博文,这一小节就没什么新的内容了,还没阅读过的赶紧去阅读下吧。
这里罗列一下几个知识点,如果你能知道他们的概念,就没啥问题了:
1、什么是当前读?什么是快照读?它们和一致性读是一个东西吗?
2、MVCC 是怎么实现的?
3、你能分析出以下结果吗 ?假设 k 的初始值是 1。
答案:
RR 下, 事务 A 返回 1, 事务 B 返回 3;
RC 下,事务 A 返回 2, 事务 B 返回 3;