MySQL 不使用limit的分页查询
limit问题:limit,offset递增问题。随着offset的增加,条数不变,耗时却增加了。
limit 0,10 耗时1ms
limit 300000,10 耗时152ms
limit 600000,10 耗时312ms
毫秒级别可能没感觉。假设有1千w数据,那么limit 9000000,10 需要5s。
解决方案:可以用id获取,然后最后一个id给回去,这样只耗时1ms。
id >600000 order by id limit 10;
id >600010 order by id limit 10;
id >600020 order by id limit 10;
即使10000000,从90000000开始取10条,用id范围过滤后,也是1ms。
MySQL 优化方案
1、SQL 优化
(1)limit优化sql
MySQL 在数据库的数据量非常大的时候会存在深度分页的问题,深度分页有多种优化方案,典型的方案是采用limit优化。
(2)like模糊查询
数据库字段使用like模糊查询的时候,不要将%放在前面,因为这样会使索引失效进而全表扫描。
#like的正确使用方法
select * from student where name like "zhang%"
(3)union查询优化
union查询对两个结果集做并集操作的时候,自动地去除重复行(相当于distinct),同时进行默认规则的排序。建议采用union all替代union。union all查询对两个结果集做并集操作的时候,不会去除重复行并且不进行默认的排序;所以针对有特殊要求的业务需要在业务层做去重和排序。
(4)区分场景使用exists和in
(a)如果子查询得出的结果集数据较少,主查询中的表数据较多且查询字段又有索引时采用in
(b)如果外层的主查询数据较少,子查询中的表大且又有索引时使用exists
备注:exists和in的区别
exists以外层表为驱动表先被访问;in是先执行子查询。exists 是对外表做 loop 循环,每次 loop 循环再对内表进行查询;in把外表和内表做 hash 连接。
(5)删除表使用truncate
truncate是直接把表删除然后再重建表结构,性能更高;删除操作记录不记入日志所以不可以回滚。truncate执行后表和索引所占用的空间会恢复到初始大小。常见的删除还有delete,delete语句执行删除的过程是每次从表中删除一行所以性能较低;删除操作会作为事务记录在日志中保存,以便进行回滚操作;delete只是将被删除的记录标记为已删除,不会立即减少表或索引所占用的空间。
(6)新增操作尽量使用批操作
批量操作(即一次数据库操作中插入多个数据行)相比于sql单条操作可减少大量的IO交互和SQL解析,从而提高了插入效率。
(7)过滤先行
无论是分组还是排序或者多表join,如果条件允许的情况下,首先把用不到的记录先过滤掉,减少数据量来提高查询效率。
(8)函数在等号的右侧
select * from student where area = left("中国北京",4);
众所周知在索引上使用函数会使得索引失效,所以针对需要使用函数的索引字段,我们将函数放在等号的右侧。
(9)字段类型最小化
应该尽量使用可以正确存储数据的最小数据类型,更小的数据类型通常会使SOL执行更快,因为它占用更少的磁盘、内存、CPU缓存,并且处理时需要的CPU周期也更少。我们常用的表示数据软删除字段(isDelete),本字段的数据值一般都是有限值,所以适合采用tinyint来定义其类型。
(10)正确使用char和varchar
char是定长的字段,适合存储长度固定的数据,如身份证号、邮政编码、手机号等等;varchar是存储不定长的数据,它会单独申请一块空间记录数据的长度,然后在长度后面记录真实的数据,适合存储不确定长度的数据。char类型存取效率高;varchar更节省空间(因为char类型不够规定的长度会使用空字符串替代)把varchar的长度调整为最小可用,是可以优化sql排序性能的。
(11)添加索引的原则
(a)频繁作为查询条件的字段应该创建索引,频繁更新的字段不适合创建索引
(b)多表关联查询中的关联字段,查询中统计或者分组字段,查询中排序字段,应该创建索引
(c)尽量使用区分度高的字段创建索引
(d)多条件组合查询优先创建组合索引
(e)delete和update语句的where条件必须有索引,否则会导致锁表
(12)巧用force index
Mysql查询优化器在执行SQL语句时会选择它认为最合适的索引,但有时却并不准确,不是实际上最快的索引,此时可以用force index人为指定索引;
select * from student force index(idx_teacher_id) where teacher_id in
(123,124,154);
2、MySQL 的参数优化
下面针对innodb的调优,先来看一下MySQL 的架构图:
(1)Buffer Pool调优
Mysql在启动的时候就向内存申请一块默认大小是128M的空间,次空间就是Buffer Pool,其存在的主要目的是让尽可能多的操作在内存中。
#查询Mysql的buffer pool大小的,字节为单位
show variables like 'innodb_buffer_pool_size%';
为了评估buffer pool的性能指标,通过命令查询多少的查询在buffer pool中执行的。
show status like 'innodb_buffer_pool_read%';
命中率 = 内存缓冲页数据量请求 / (磁盘读取的数据量请求 + 内存缓冲页数据量请求) = 1398845854999 / (1398845854999 + 94952865)= 0.99。
如果当前的命中率的值低于90%,我们就需要考虑增加buffer pool的大小
#设置buffer pool的大小
set global innodb_buffer_pool_size = 6442450944
#监控调整的buffer pool大小是否生效了
show status where variable_name = 'Innodb_buffer_pool_resize_status'
(2)redo log参数调优
redo log是一种基于磁盘的数据结构,用于在宕机恢复期间更正由不完整事务写入的数据。redo log缓存区的大小一般默认是16MB,如果事务中含有blog/text等大字段,这个缓冲区会被很快填满而引起额外的IO负载。配置更大的日志缓冲区就可以有效地提高Mysql的效率。
#查询Mysql的redo log文件的缓冲区大小
show variables like 'innodb_log_buffer_size%';
#查询redo log文件大小
show variables like 'innodb_log_file_size%';
redo log文件过大过小都存在问题。文件过小就会导致刷新数据到MySQL 的频率增加,可能出现处理大事务的时候由于存储空间不足导致事务处理失败的问题。文件过大的时候,如果MySQL 宕机的时候,做数据恢复的时候就非常的消耗时间。
如何设置redo log文件的大小呢?
这个需要根据每个系统的自身业务情况来设定,可以使用预测法先预测1分钟记录的日志量,再通过1分钟的日志量来预测1小时内存储的日志大小,由于Mysql是最小支持1小时的业务redo log日志的,所以据此来设置innodb_log_file_size的大小。
MySQL 单表不超过2000W条数据原因
在MySQL数据库中,单表不能超过2000W(即2亿)条数据的限制是一项常见的规定。这个限制涉及到多个方面,包括存储引擎的限制、性能考虑以及数据库设计的最佳实践等。
存储引擎的限制
MySQL支持多种存储引擎,其中最常用的之一是InnoDB。然而,InnoDB存储引擎在处理大型数据集时存在一些限制。其中一个主要因素是B树的数据结构。虽然B树可以高效地支持数据的快速检索和插入,但是随着数据量的增加,维护B树的成本也会增加。当表中的数据量达到一定程度时,B树的维护成本会变得非常高昂,可能导致性能下降。
此外,InnoDB还需要在内存中维护数据和索引的缓存,以加速查询操作。随着数据量的增加,内存的需求也会增加,这可能会导致内存不足而影响性能。因此,为了保证查询性能和数据库的稳定性,MySQL设置了单表的数据量限制。
性能考虑
即使在不考虑存储引擎的情况下,单表中存储大量数据也会带来性能方面的挑战。查询速度可能会变慢,特别是对于没有正确索引或者查询条件的查询。此外,当需要对表进行维护操作,如删除或更新大量数据时,可能会导致锁表、长时间的等待和资源消耗等问题。为了避免这些性能问题,将数据分散到多个表中是一个更好的解决方案。通过合适的数据分区和合理的索引设计,可以提高查询性能,并降低数据库维护操作的风险。
数据库设计的最佳实践
除了存储引擎和性能考虑外,数据库设计的最佳实践也是限制单表数据量的一个重要因素。在设计数据库时,应该根据业务需求和数据访问模式来合理划分表结构,并使用适当的关联来建立表之间的关系。
将数据分散到多个表中有助于提高数据库的可扩展性和灵活性。例如,可以根据数据的逻辑关系将数据分散到不同的表中,或者根据数据的访问频率将数据分散到热数据表和冷数据表中。
此外,在数据库设计中,应该遵循规范化的原则,以减少数据的冗余和重复。规范化可以确保数据的一致性和完整性,并减少存储空间的占用。
提升 MySQL 性能:表压缩
物理存储主要是考虑是否要启用表的压缩功能。默认情况下,所有表都是非压缩的。表压缩是否会导致 MySQL 数据库的性能下降,需要根据不同场景进行区分。
表压缩
数据库中的表是由一行行记录(rows)所组成,每行记录被存储在一个页中,在 MySQL 中,一个页的大小默认为 16K,一个个页又组成了每张表的表空间。如果一个页中存放的记录数越多,数据库的性能越高。这是因为数据库表空间中的页是存放在磁盘上,MySQL 数据库先要将磁盘中的页读取到内存缓冲池,然后以页为单位来读取和管理记录。
那么,一个页中存放的记录越多,内存中能存放的记录数也就越多,那么存取效率也就越高。若想将一个页中存放的记录数变多,可以启用压缩功能。此外,启用压缩后,存储空间占用也变小了,同样单位的存储能存放的数据也变多了。若要启用压缩技术,数据库可以根据记录、页、表空间进行压缩,不过在实际工程中,我们普遍使用页压缩技术,这是为什么呢?
- 压缩每条记录:每次读写数据都要压缩和解压,对CPU的计算过于依赖,会导致性能明显下降;另外,每条数据的大小一般都不会太大,对于每条数据都进行压缩的话,压缩效率也不是很好。
- 压缩表空间:对表空间的压缩,其实压缩效率还是不错的,但是它要求表空间文件要保持静态,这对关系型数据库来讲又不现实。(不过业务中的历史数据倒是可以考虑)
而基于页的压缩,既能提升压缩效率,又能在性能之间取得一种平衡。
这里大家可能要担心了,启用页压缩的话,性能会有损失,因为压缩需要额外的CPU计算。确实,压缩会给CPU带来一定的消耗。但压缩并不意味着性能下降,有可能还能提升性能呢?因为大部分的数据库业务系统,CPU的处理能力是有余力的,也就是计算时过剩的。IO负载才是数据库的主要瓶颈。
通过页压缩技术,MySQL可以把16K的页压缩到8K或是4K。这样一来,从磁盘读取或写入时,就能将IO请求大小减半。从而提升数据库的整体性能。
MySQL 压缩表设计
COMPRESS 页压缩
COMPRESS 页压缩是 MySQL 5.7 版本之前提供的页压缩功能。只要在创建表时指定ROW_FORMAT=COMPRESS,并设置通过选项 KEY_BLOCK_SIZE 设置压缩的比例。虽然是通过选项 ROW_FORMAT 启用压缩功能,但这并不是记录级压缩,依然是根据页的维度进行压缩。
比如以下示例:将一张日志表ROW_FROMAT 设置为 COMPRESS,表示启用 COMPRESS 页压缩功能,KEY_BLOCK_SIZE 设置为 8,表示将一个 16K 的页压缩为 8K。
CREATE TABLE sys_log (logId BINARY(16) PRIMARY KEY,......
)
ROW_FORMAT=COMPRESSED
KEY_BLOCK_SIZE=8
COMPRESS 页压缩就是将一个页压缩到指定大小,比如从16K压缩到8K,但是如果无法压缩到8K,则会产生两个8K的页。
COMPRESS 页压缩,适合用于一些对性能不敏感的业务表,例如日志表、监控表、告警表等,压缩比例通常能达到 50% 左右。
虽然 COMPRESS 压缩可以有效减小存储空间,但 COMPRESS 页压缩的实现对性能的开销是巨大的,性能会有明显退化。主要原因是一个压缩页在内存缓冲池中,存在压缩和解压两个页。为了 解决压缩性能下降的问题,从MySQL 5.7 版本开始推出了 TPC 压缩功能。
TPC 压缩
在文件系统中,空洞特性允许应用程序或文件系统本身在文件中标记一部分数据为“已删除”,即使这部分数据在文件中仍然存在。这样,文件系统不会为这部分数据分配实际的磁盘空间。当读取文件时,文件系统会返回零或其他填充数据来表示这些空洞部分。
TPC(Transparent Page Compression)是 5.7 版本推出的一种新的页压缩功能,其利用文件系统的空洞(Punch Hole)特性进行压缩。可以使用下面的命令创建 TPC 压缩表:
CREATE TABLE sys_log (logid BINARY(16) PRIMARY KEY,.....
)
COMPRESSION=ZLIB | LZ4 | NONE;
要使用 TPC 压缩,首先要确认当前的操作系统是否支持空洞特性。通常来说,当前常见的 Linux 操作系统都已支持空洞特性。
由于空洞是文件系统的一个特性,利用空洞压缩只能压缩到文件系统的最小单位 4K,且其页压缩是 4K 对齐的。比如一个 16K 的页,压缩后为 7K,则实际占用空间 8K;压缩后为 3K,则实际占用空间是 4K;若压缩后是 13K,则占用空间依然为 16K。
空洞压缩的另一个好处是,它对数据库性能的侵入几乎是无影响的(小于 20%),甚至可能还能有性能的提升。这是因为不同于 COMPRESS 页压缩,TPC 压缩在内存中只有一个 16K 的解压缩后的页,对于缓冲池没有额外的存储开销。
另一方面,所有页的读写操作都和非压缩页一样,没有开销,只有当这个页需要刷新到磁盘时,才会触发页压缩功能一次。但由于一个 16K 的页被压缩为了 8K 或 4K,其实写入性能会得到一定的提升。
总结
对一些对性能不敏感的业务表,例如日志表、监控表、告警表等,它们只对存储空间有要求,因此可以使用 COMPRESS 页压缩功能。在一些较为核心的业务表上,更推荐使用 TPC压缩。因为核心信息是一种非常重要的数据,通常伴随高频、重要业务。
比如上面提到的订单数据,大部分的电商公司都会对历史订单数据去做单独存储。以确保近期订单数据可以秒查。那么针对这个业务场景,我们可以将历史数据启用TPC压缩功能,对近三个月或六个月的订单数据不启用压缩。
需要特别注意的是:通过命令 ALTER TABLE xxx COMPRESSION = ZLIB 可以启用 TPC 页压缩功能,但是这只对后续新增的数据会进行压缩,对于原有的数据则不进行压缩。所以上述ALTER TABLE 操作只是修改元数据,瞬间就能完成。若想要对整个表进行压缩,需要执行 OPTIMIZE TABLE 命令:
ALTER TABLE sys_log COMPRESSION=ZLIB;
OPTIMIZE TABLE sys_log;
MySQL 的binlog、undo log、redo log 日志
基于 MySQL 8.0.30,默认为 InnoDB 引擎;InnoDB 由 Innobase Oy公司所开发,2006年五月时由甲骨文公司并购。
binlog是 server层生成的日记,而 undo log、redo log 是Innodb 存储引擎层生成的日志。
binlog
binlog,是 binary log的英文缩写,翻译为二进制日志或者归档日志(带有业务含义),它是从 MySQL 3.23.14版本引入的。binlog是在 MySQL Server层实现,因此所有数据库引擎都可以使用它。
binlog主要包含两种信息:
1. MySQL数据库所有的表结构变更以及表数据修改的二进制日志(像 select,show这种查询类的操作,不会记录);
2. 每条语句使用更新数据多长时间的信息;
binlog的用途有 3个:
1. 归档日志
2. 主从复制
3. 数据恢复
binlog有 3种类型:
1. 语句模式(Statement-based logging): 包含产生数据更改(插入、更新、删除)的 SQL语句;
2. 行模式(Row-based logging): 用于记录单个行的更改,从 MySQL 5.1版本引入;
3. 混合模式(Mixed logging): 默认使用语句模式,可以按需自动切换到行模式,从 MySQL 5.1版本引入;
接下来,我们通过 MySQL的指令来查看下 binlog文件的信息格式。
首先,生成 binlog,这里以创建一张user表,然后对 user表进行增删改查操作为例,具体 sql执行如下图:
接着,对上面生成的 binlog进行查看,指令和结果截图如下:
# 查看 binlog是否开启,8.0.30 默认是开启的
mysql> show variables like 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | ON |
+---------------+-------+
1 row in set (0.02 sec)# 查看所有的binlog日志文件
mysql> show binary logs;# 查看某个 binlog的具体信息,通过指令也可以看出binlog是以 event的方式存储
mysql> show binlog events in 'binlog.000001'
从上面的 binlog日志我们可以看出:在 binlog文件中,并没有把我们执行的 SQL语句直接存储,而是转换成了内部的一些逻辑指令,所以, binlog它是一种逻辑日志。
undo log
undo log, 中文翻译为撤销日志或回滚日志,用于事务回滚,保证了事务 ACID 特性中的原子性(Atomicity),同时还可以配合 ReadView 实现多版本控制(MVCC)。
相关参数
可以通过 show variables like ‘%undo%’; 指令查看 undo log相关参数:
mysql> show variables like '%undo%';
+--------------------------+------------+
| Variable_name | Value |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
| innodb_undo_directory | ./ |
| innodb_undo_log_encrypt | OFF |
| innodb_undo_log_truncate | ON |
| innodb_undo_tablespaces | 2 |
+--------------------------+------------+
5 rows in set (0.01 sec)
- innodb_max_undo_log_size:一个 undo log文件对应的最大值,默认 1G;
- innodb_undo_directory:undo log文件存放的目录;
- innodb_undo_log_encrypt:是否对 undo log文件开启空间压缩,默认是关闭;
- innodb_undo_log_truncate:单个文件超过最大值时,是否对 undo log文件进行切分,默认为打开状态;
- innodb_undo_tablespaces:单个文件超过最大值时,undo log文件会被切分为几份,默认是 2;
事务回滚
在事务提交之前,MySQL 会将更新前的数据记录到 undo log 日志文件里,当事务回滚时,可以利用 undo log 来进行回滚。如下图:
每当 InnoDB 引擎执行一条更新操作(修改、删除、新增)时,就会生成对应的一条回滚指令记录在 undo log 里,比如:
- InnoDB 引擎执行 insert 操作,则会在 undo log 日志里面保存一条相反的 delete 语句。
- InnoDB 引擎执行 delete 操作,则会在 undo log 日志里面保存一条相反的 insert 语句。
- InnoDB 引擎执行 update 操作,则会在 undo log 日志里面保存一条相反的 update 语句。
undo log 和 ReadView 实现 MVCC
在 InnoDB引擎中,可以多个事务对同一条数据记录进行更新操作,当出现异常时,能及时进行数据回滚,那么 InnoDB是如何能精确地把数据回滚到具体的哪一个版本呢?这就是 InnoDB的多版本控制机制。如下图:有 3个事务分别对表中id = 1行记录进行更新操作,因此,在undo log文件中就会产生3条逻辑回滚日志 :
redo log
redo log,翻译成重做日志,用于crash-safe,即当数据库发生异常重启,可以保证之前提交的记录不会丢失,它是 InnoDB引擎独有的日志。redo log 是物理日志,记录了某个数据页做了什么修改,比如对某表空间中的某数据页某偏移量的地方做了某更新,每当执行一个事务就会产生这样的一条或者多条物理日志。
在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
为什么需要 redo log?
为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术。WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。整个过程如下图:
MySQL 如何辨别 binlog 的完整性?
- statement 格式的 binlog,文件末尾有 COMMIT;
- row 格式的 binlog,文件末尾有一个 XID event。
redo log 和 binlog 是怎么关联起来的?
它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
处于 prepare 阶段的 redo log 加上完整 binlog,重启就能恢复,MySQL 为什么要这么设计?
跟数据与备份的一致性有关。在时刻 B,也就是 binlog 写完以后 MySQL 发生崩溃,这时候 binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。
为什么需要两阶段提交呢?
两阶段提交是经典的分布式系统问题,并不是 MySQL 独有的。如果必须要举一个场景,来说明这么做的必要性的话,那就是事务的持久性问题。对于 InnoDB 引擎来说,如果 redo log 提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果 redo log 直接提交,然后 binlog 写入的时候失败,InnoDB 又回滚不了,数据和 binlog 日志又不一致了。
总结
- undo log(回滚日志):是 Innodb 存储引擎层的逻辑日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
- redo log(重做日志):是 Innodb 存储引擎层的物理日志,是循环写,实现了事务中的持久性,主要用于掉电等故障恢复。
- binlog (归档日志):是 Server 层生成的日志,所有引擎都可使用,主要用于数据备份、数据恢复和主从复制。
MySQL 死锁如何解决?
什么是 MySQL 死锁?
MySQL中的死锁是指两个或多个操作相互等待对方释放资源,结果导致他们都无法继续执行的情况。在数据库操作中,这通常发生在多个事务同时试图锁定对方已锁定的资源时。
MySQL 锁的分类
MySQL的锁可以从操作的粒度进行分类,主要包括表级锁、行级锁和页级锁。不同类型的锁在数据库操作中的应用会影响数据库的性能和并发处理能力。
表级锁
表级锁是最简单的锁类型,当操作涉及到整个表时使用。这种锁会锁定整个表,使得任何其他需要访问这个表的操作都必须等待锁被释放。例如:
--手动增加表锁
lock table 表名称 read(write),表名称2 read(write);
--查看表上加过的锁
show open tables;
--删除表锁
unlock tables;
优点是简单易管理,但缺点是并发能力较低,尤其在处理大量事务时,效率会大大降低。
行级锁
行级锁是最精细的锁,只锁定数据表中的特定行。
行级锁的特点
- 每次操作锁住一行数据。开销大,加锁慢;
- 会出现死锁;
- 锁定粒度最小,发生锁冲突的概率最低,并发度最高。
InnoDB相对于MYISAM的最大不同有两点:
- InnoDB支持事务(TRANSACTION)
- InnoDB支持行级锁
注意,InnoDB的行锁实际上是针对索引加的锁(在索引对应的索引项上做标记),不是针对整个行记录加的锁。并且该索引不能失效,否则会从行锁升级为表锁。(RR级别会升级为表锁,RC级别不会升级为表锁)
比如我们在RR级别执行如下sql:
--where条件里的name字段无索引
select * from account where name = 'lilei' for update;
则其它Session对该表任意一行记录做修改操作都会被阻塞住。
页级锁
页级锁介于表级锁和行级锁之间,不是MySQL默认使用的锁类型。它锁定数据库页(即一组行),粒度比表级锁细,但比行级锁粗。页级锁可以减少锁的数量,从而降低锁竞争,但可能会引起更多的锁冲突,尤其是当多个事务频繁修改相邻的数据时。
基于操作类型的MySQL锁分类
读锁(S锁)
读锁也被称为共享锁,它允许一个事务去读取一行数据,并防止其他事务对这行数据进行写操作,直到读锁被释放。
多个事务可以同时持有对同一资源的读锁,这意味着多个用户可以同时读取数据,而不会相互影响。
select * from T where id=1 lock in share mode
适用于那些读取操作远多于写入操作的场景。
因为读锁可以共享,它支持高并发的读取操作,使得多个用户可以同时查询数据而不会阻塞彼此。
写锁(X锁)
写锁又称为排他锁,当一个事务对数据进行修改时,它会加上写锁。这个锁会阻止其他任何事务读取或修改同一数据,直到写锁被释放。
写锁保证了数据在修改期间的完整性和一致性。
select * from T where id=1 for update
适用于需要修改数据的操作。
由于写锁会阻塞其他所有试图访问相同数据的操作,所以在写入量大或者数据频繁更新的应用中,写锁可能会成为性能瓶颈。
意向锁
意向锁是一种较为特殊的锁,它不直接锁定数据,而是表明一个事务意图对数据行进行加锁(读锁或写锁)。
意向锁主要分为:
- 意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁。
- 意向排他锁,IX锁,对整个表加排他锁之前,需要先获取到意向排他锁。
这主要用于支持多级锁定协议,通过在表级别设置意向锁,来预告即将在行级别上加的锁,这样就能在不同粒度的锁之间提供一种兼容性检查。
意向锁主要用于提高并发性能,特别是在存在多粒度锁定(即同时使用表级锁和行级锁)的情况下。
它允许数据库系统更有效地管理锁,避免不必要的锁冲突,提高操作效率。
基于操作性能的MySQL锁分类
从操作的性能可分为乐观锁和悲观锁
乐观锁
乐观锁基于这样一个假设,即冲突发生的概率较低,大多数时间不会有冲突。因此,它在操作过程中不会立即锁定资源。通常,乐观锁会通过检查数据在读取和更新期间是否被修改来实现,常用的方法是使用版本号或时间戳。只有在数据提交更新时,才会检查版本信息是否一致,如果不一致,表示数据在此期间已被其他操作更改,此时操作会失败。
适用于读多写少的场景,比如在线阅读平台的用户数据处理,因为在这种场景中,数据冲突的可能性较低,使用乐观锁可以减少锁的开销,提高系统的整体性能。
悲观锁
悲观锁恰恰相反,它假设冲突很常见,因此在整个数据处理过程中会保持数据锁定,直到事务完成。这意味着当一个事务在操作数据时,其他事务必须等待直到锁被释放。悲观锁可以通过数据库自带的锁机制实现,比如行锁或表锁。
适合写操作多的场景,比如银行账户处理,在这种场景下,数据安全和一致性比性能更为重要。悲观锁通过锁定数据来避免数据在多个事务间的冲突,确保数据操作的安全性。
InnoDB存储引擎三种行锁模式
当谈论InnoDB存储引擎中的行锁模式时,可以把它想象成一种确保数据库中数据安全和准确的机制。这里有三种主要的行锁模式,分别是记录锁、间隙锁和临键锁。
记录锁(Record Locks)
记录锁是最直接的一种锁。它就像是给数据库中某条具体数据上了一把锁。这种锁定的目的是为了防止其他用户在事务处理期间修改这条数据。
如果你正在更新一条记录,系统就会对这条记录加锁,确保没有其他人能够同时修改这条记录,直到你的事务完成。
间隙锁(Gap Locks)
间隙锁不是锁定单个记录,而是锁定一个范围,但这个范围不包括实际的记录。
想象一下,你想在两条已有记录之间插入一条新记录,间隙锁就会防止其他事务插入到这两条记录之间的空隙里。
MySQL默认级别是repeatable-read,有幻读问题,间隙锁是可以解决幻读问题的。
只要在间隙范围内锁了一条不存在的记录会锁住整个间隙范围,不锁边界记录,这样就能防止其它Session在这个间隙范围内插入数据,就解决了可重复读隔离级别的幻读问题。
临键锁(Next-Key Locks)
临键锁是记录锁和间隙锁的结合体。它同时锁定一条记录和它前面的空隙。这种锁的作用是,它不仅防止其他用户修改当前的记录,还防止他们在前面的空隙中添加新记录。这种锁模式是InnoDB用来保持数据完整性和避免幻读(即在一个事务内读取到之前不存在的数据行)的一种方式。
事务隔离级别和锁的关系
数据库事务的隔离级别
据库事务的隔离级别,目前数据库事务的隔离级别一共有 4 种,由低到高分别为:
事务的四个隔离级别:
-
未提交读(READ UNCOMMITTED):
所有事务都可以看到其他事务未提交的修改。一般很少使用; -
读已提交(READ COMMITTED):
Oracle默认隔离级别,事务之间只能看到彼此已提交的变更修改; -
可重复读(REPEATABLE READ):
MySQL默认隔离级别,同一事务中的多次查询会看到相同的数据行;可以解决不可重复读,但可能出现幻读; -
可串行化(SERIALIZABLE):
最高的隔离级别,事务串行的执行,前一个事务执行完,后面的事务会执行。读取每条数据都会加锁,会导致大量的超时和锁争用问题;
数据库一般默认的隔离级别为 读已提交 RC ,比如 Oracle,也有一些数据的默认隔离级别为 可重复读 RR,比如 Mysql。
"可重复读"(Repeatable Read)这个级别确保了对同一字段的多次读取结果是一致的,除非数据是被本身事务自己所修改。
RR它能够防止脏读、不可重复读,但可能会遇到幻读的情况。
事务隔离级别与锁之间的关联
事务隔离级别,源于SQL92标准,为数据库事务的并发控制提供了一种全面解决方案。
它实质上是将锁机制和MVCC(多版本并发控制)技术进行了封装,从而简化了底层实现的复杂性,为用户提供了一个更加友好的接口。
锁,作为数据库实现并发访问控制的核心机制,确保了事务的隔离性。
通过为不同的数据操作施加相应的锁,数据库能够防止多个事务同时读写同一数据,从而维护数据的一致性和完整性。
对于开发者而言,他们通常首先选择一个合适的事务隔离级别。仅当所选的隔离级别无法满足特定的并发需求或解决问题时,才需要考虑在代码中显式地使用锁进行更精细的控制。值得注意的是,MySQL的默认事务隔离级别是“可重复读”,但根据实际的应用需求,“读已提交”隔离级别也常被推荐,因为它在并发控制和性能之间达到了较好的平衡。
相比之下,Oracle和SQLServer的默认隔离级别则是“读已提交”。
死锁产生原因和解决方案
InnoDB与MyISAM的最大不同有两点
- 支持事务
- 采用行锁 行级锁和表级锁本来就有许多不同之处,另外,事务的引入也带来了一些问题 ,比如 死锁。
表级锁死锁
假设有两个事务,分别在操作两个表:表A和表B。事务1试图先锁定表A,然后锁定表B。而事务2试图先锁定表B,然后锁定表A。如果这两个事务同时执行,它们将会相互等待对方释放锁,从而导致死锁。
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。
- 仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理。
- 尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。
行级锁死锁
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,行锁 膨胀 为表锁( 或者等价于 表级锁)。多个这样的 锁表事务 执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或 死锁。
-
解决方案:
- SQL语句中不要使用太复杂的关联多表的查询;
- 使用explain“执行计划"对SQL语句进行分析,对于有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化。
两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁 例如:模拟死锁场景
set tx_isolation='repeatable-read';
Session_1执行:select * from account where id=1 for update;
Session_2执行:select * from account where id=2 for update;
Session_1执行:select * from account where id=2 for update;
Session_2执行:select * from account where id=1 for update;
当出现死锁时,我们可以通过查看近期的死锁日志,来确定死锁的详细信息 例如:
查看近期死锁日志信息:show engine innodb status;
大多数情况MySQL可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况MySQL没法自动检测死锁,这种情况我们可以通过日志分析找到对应事务线程id,可以通过kill杀掉。
预防死锁优化实践
1. 维持一致的锁定顺序
确保所有事务都以相同的顺序获取锁。这可以减少锁定冲突的可能性,因为事务不会因为等待其他事务释放锁而相互阻塞。
例如,如果有多个表或资源需要锁定,总是按照相同的顺序(如字典顺序)锁定这些资源。
2. 使用最小的锁粒度
尽量使用行级锁而不是表级锁。
行级锁允许更高的并发,因为它仅锁定需要修改或查询的数据行,而不是整个表。这样,即使多个事务操作同一表,它们也可能操作不同的行,从而减少死锁的风险。
3. 减少事务持续时间
尽量缩短事务的执行时间。
长事务占用锁的时间越长,与其他事务发生冲突的可能性就越大。你可以通过优化查询语句和减少事务中的操作数来减少事务持续时间。
4. 使用锁超时
在某些数据库管理系统中,可以设置锁的超时时间。
这意味着事务在等待锁超过设定的时间后将自动回滚。这不仅可以防止死锁,还可以避免一个事务无限期地等待资源。
5. 死锁检测和回滚
启用数据库的死锁检测功能,让数据库管理系统能够自动检测死锁并回滚某个事务来解锁。
这通常是最后的手段,因为它可能导致数据不一致的问题。应当只在其他方法都无法实现时使用。
6. 避免不必要的锁
审查和优化事务逻辑,确保只锁定必要的资源。
例如,如果事务只读取数据而不进行修改,可以考虑使用非锁定读(例如,在MySQL中使用SELECT ... WITH (NOLOCK))。
7. 使用乐观并发控制
在一些场景中,使用乐观并发控制(OCC)而不是悲观锁定可能更合适。OCC通过在事务提交时检查数据是否已被其他事务修改来避免锁定,适用于读多写少的场景。
8. 避免无索引行锁升级为表锁
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
9. 监控和日志记录
实施监控和日志记录来跟踪死锁和性能瓶颈。这可以帮助识别导致死锁的具体事务和操作,从而进行针对性的优化。
CHAR 和 VARCHAR 的区别
首先,CHAR和VARCHAR均为数据库中用于存储字符串数据的数据类型。它们之间的主要区别在于存储空间的使用方式。比如:CHAR为一种定长数据类型
,其长度固定且在存储时会自动加入空格以填满指定长度,长度范围为0至255;VARCHAR则为一种可变长度数据类型
,仅存储实际的字符串内容,无需填充空格。故在存储短字符串时,VARCHAR能够节省空间,长度范围为0至65535(适用于MySQL 5.0.3及之后版本)。
若采用CHAR(100),插入记录后即分配了100个字符,后续修改不引起页分裂问题;而VARCHAR(100)因未预分配存储空间,初始插入数据时,可变长度字段按实际长度储存,且数据在文件中紧密连续。若值变长,原位置无法扩展,无法覆盖原空间,MySQL将采用页分裂方式扩展字段的长度。
VARCHAR的优势:在于其可变长度的字符类型,具有更好的兼容性;然而,使用VARCHAR可能引发内存碎片问题,需要额外1到2个字节存储长度信息,并更新语句可能导致页分裂
等情况。
举例来说,适合使用VARCHAR的场景包括存储产品描述(可变长度)、用户地址(可变长度)以及用户名称(可变长度)。
CHAR的长处在于其定长字符串类型,有助于减少内存碎片,并无需额外的磁盘空间来存储长度信息;但其短板在于会消除列末尾的空格信息。
CHAR在长度不足时会补充空格,因此,当在一个长度为10的CHAR中分别存储"paidaxing "和"paidaxing"时,实际存储的均为"paidaxing ",导致原空格信息丧失。
举例来说,适合使用CHAR的情境包括存储身份证号(固定长度)、订单号(固定长度)以及国家编码(固定长度)。
索引的类型
数据库索引是数据库系统中提高数据检索效率的关键机制,通过特定的数据结构来组织数据,使得查询操作能更快定位到要查找的记录。索引的实现依赖于高效的数据结构,常见的有以下几种:
1、Hash
利用哈希函数将键转换为固定长度的哈希码,然后通过哈希码直接定位记录,适用于等值查询,查询速度快,但不支持范围查询。哈希索引适用于只有等值查询的场景,比如Memcached及其他一些NoSQL引擎。
2、有序数组
有序数组则在等值查询和范围查询方面表现优异,等值查询使用二分法就可以快速得到,这个时间复杂度是O(log(N));因为是有序数组范围查询的效率自不必说。但是,在更新数据的时候就很麻烦,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。所以,有序数组索引只适用于静态存储引擎,存储不会修改的数据。
3、二叉搜索树
二叉搜索树的特点是:父节点左子树所有节点的值小于父节点的值,右子树所有节点的值大于父节点的值,查询的时间复杂度是O(log(N))。为了维持O(log(N))的查询复杂度,需要保持这棵树是平衡二叉树,这样,更新的时间复杂度也是O(log(N))。但是,索引不止存在内存中,还要写到磁盘上。如果树高30,一次查询可能需要访问30个数据块,磁盘I/O会大大拖慢查询性能。
4、B Tree(平衡多路搜索树)
有二叉就有多叉,B树这是一种自平衡的树数据结构,B树由于存在多叉,可以大大降低树的高度,减少一次请求中访问磁盘的次数。B树的每个节点包含键值、数据及其指向子节点的指针。如下图所示:
5、B+Tree
B+Tree是B Tree的一种变体,B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据。它的所有叶子节点包含了全部数据记录的指针(或直接存储数据),并且叶子节点间通过双向指针相连,形成了一个双向链表,特别适合范围查询和全表扫描。如下图所示:
MySQL InnoDB引擎的索引采用的是哪种数据结构?原因是什么?
MySQL的InnoDB存储引擎是其默认的事务型存储引擎,广泛应用于需要高并发和数据一致性的场景。InnoDB对于索引的实现,主要采用了B+Tree数据结构,原因如下:
1、适应磁盘I/O特性
B+Tree结构能够很好地适配磁盘读取的块操作特性。由于磁盘I/O通常是按块进行的,B+Tree的每个节点可以存放多个键值对,这样每次磁盘I/O都可以读取更多的数据,减少磁盘访问次数,提高效率。
2、支持范围查询和排序
B+Tree的所有叶子节点通过指针相连,形成一个有序链表,这使得范围查询和数据排序变得非常高效,无需额外的排序操作。
3、聚簇索引
InnoDB表是基于聚簇索引(Clustered Index)构建的,即数据行与主键索引存放在一起,如果主键被定义为索引,则B+Tree的叶子节点直接包含表数据,进一步提高了数据访问速度。
4、二级索引支持
除了聚集索引,InnoDB还支持二级索引(Secondary Index),二级索引的叶子节点包含指向主键的指针,即使在非主键索引上也能高效地找到对应的数据行。
综上所述,MySQL InnoDB引擎采用B+Tree作为索引的数据结构,主要是因为它在磁盘I/O效率、范围查询支持、以及数据存储布局上的优势,这些特性共同保证了InnoDB引擎在处理大量并发事务和复杂查询时的高性能。
一棵B+树到底可以存放多少条数据呢?
一个3层的B+树就可以存放超过 2 千万条数据了。查找数据的时候,一次页的查找代表一次 I/O,当我们通过主键索引查询的时候,一般只需要3 次 I/O 就可以了。
怎么计算的呢?
假如我们的主键 ID 是 bigint 类型,长度为 8 个字节。指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节。所以非叶子节点(一页16K)可以存储 16384/14=1170 个这样的单元(键值+指针)。
如果一条数据大小是1K,那么叶子节点可以放 16 条数据。
树深度为 3 的时候,可以存储的数据为 1170*1170*16 = 21902400 条记录。
熟悉了基本原理,很多问题也都能迎刃而解了,比如模糊搜索、对列进行运算等场景,索引为什么会失效?因为这些场景下,MySQL此时无法利用B+树判断去哪个子树找数据了,B+树的特性无法被利用了,因此MySQL只能去遍历所有数据了。这样考虑后,问题是不是就简单很多啦。
索引都有哪些类型?
1、按照数据的存储位置分类
(1)聚簇索引
聚簇索引,数据和索引存储在一起,叶子节点中即存储了实际的数据行。在MySQL的InnoDB存储引擎中,每个表都有一个聚簇索引,默认情况下,如果一个表没有明确指定主键,InnoDB会自动选择一个唯一且非空的索引作为聚簇索引;如果不存在这样的索引,则InnoDB会隐式地创建一个名为ROWID
的隐藏列作为聚簇索引。
聚簇索引的优势在于,由于数据和索引在一起,查询时能够更快地获取到所需的数据,效率更高。
(2)二级索引(非聚簇索引)
二级索引,又称非聚簇索引与聚簇索引相对,其叶子节点不包含实际的数据行,而是存储指向对应数据行的指针(在InnoDB中通常是一个书签记录,即聚簇索引的键值)。这意味着,通过非聚簇索引查找数据需要先查找到索引的叶子节点,再根据叶子节点中的指针找到实际的数据行,这被称为回表操作。非聚簇索引适用于多种场景,特别是当需要快速检索非主键列时。
2、按照索引字段的特性分类
(1)主键索引
主键索引,简单来说就是建立在主键列上的索引。它是一种特殊的唯一索引,一个表只能有一个主键且不允许有空值;索引列只能出现一次且必须唯一。当在表中定义主键时,数据库会自动为该列创建索引,以提高数据访问速度。
(2)普通索引
普通索引是最基本的索引类型,没有唯一性的限制,可以创建在任何数据类型列上,为表中的一列或多列数据创建索引,以提高查询效率。这种类型的索引适用于需要提升查询速度但不强制要求数据唯一性的场景。
(3)唯一索引
唯一索引要求索引列的值必须唯一,但允许有空值(NULL)。如果试图插入重复的值,则操作会失败,从而确保数据的唯一性。这不仅有助于数据完整性,还可以加速查找速度,尤其是当经常需要基于该列进行等值查询时。主键索引是一种特殊的唯一索引,它还附加了非空约束,意味着主键列既不能有重复值也不能为NULL。
(4)前缀索引
前缀索引,说白了就是对文本的前几个字符建立索引(具体是几个字符在建立索引时指定),这样建立起来的索引更小,所以查询更快,但需要注意使用前缀搜索的场景,如 like 'abc%' 这样的查询。
(5)全文索引
全文索引是专门用于全文搜索的索引类型,可以极大地加速对文本内容的查询速度。在MySQL中,全文索引主要适用于MyISAM 和 InnoDB存储引擎(InnoDB引擎是 MySQL5.6 之后支持的),特别适合处理大量文本数据的搜索,如博客文章、新闻内容等。在MySQL中,全文索引只能应用于CHAR、VARCHAR和TEXT类型的列,并且默认只对英文有效,但可以通过配置支持其他语言。
主键索引一定是聚簇索引吗?
在MySQL中,聚簇索引是在InnoDB存储引擎中实现的,而在MyISAM存储引擎中,索引和数据是分开存储的,不支持真正的聚簇索引。
在InnoDB存储引擎中,表的主键会自动成为聚簇索引,如果没有显式定义主键,InnoDB会选择一个唯一的非空索引代替,如果也没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。聚簇索引的叶子节点包含行数据本身。因此,MySQL InnoDB引擎中的主键索引一定是聚簇索引。
什么是覆盖索引?
如果一个索引包含了查询所需要的所有列,那么这个索引就被称为覆盖索引。这意味着数据库可以直接从索引中获取所有信息,而不需要「回表」去查找实际的数据行。使用覆盖索引的优点是:
1、减少了查询操作的 I/O 成本,显著提高查询效率。
2、特别适合于SELECT语句只需读取索引中已有的列,而无需访问实际数据行的情况。
如果要利用上覆盖索引,需确保查询的语句涉及的所有列都包含在某个索引中,并且该索引能够被优化器选择使用。
索引是越多越好吗?
建立索引的数量并不是越多越好的。索引可以加快数据检索的速度,但过多的索引可能会导致以下问题:
1、写操作性能下降:每次插入、更新或删除操作都需要更新索引,如果有太多的索引,这些操作会变得更加耗时。
2、存储空间占用增加:每个索引都需要一定的存储空间,过多的索引会增加数据库的存储成本。
3、索引维护成本增加:随着索引数量的增加,索引的维护成本也会增加,包括优化、重建索引等操作。
所以,我们需要根据实际情况和业务需求来合理地选择建立索引,避免过度索引的问题。
聚簇索引直接决定了数据的存储方式,二级索引则提供了额外的访问路径,而覆盖索引则是提高特定查询效率的有效手段。
MySQL 的 where 1=1会影响性能吗?
动态拼接SQL
在 Mybatis中,动态拼接 SQL最常用的两种方式:使用 where 1=1 和 使用<where>标签。
使用where 1=1
在 iBATIS中没有<where>标签,动态 SQL的处理相对较为原始和复杂,因此使用where 1=1这种写法的用户很大一部分是还在使用 iBATIS 或者是从 iBATIS过度到 Mybatis。
使用<where>标签
Mybatis提供了<where>标签,<where>标签只有在至少一个 if条件有值的情况下才去生成 where子句,若 AND或 OR前没有有效语句,where元素会将它们去除,也就是说,如果 Mybatis通过<where>标签动态生成的语句为where AND name = '111',最终会被优化为where name = '111'。
<where>标签是在 MyBatis中引入的,所以,很多一开始就使用 MyBatis的用户对这个标签使用的比较多。
性能影响
借助show warnings命令来查看信息,在 MySQL中,show warnings命令用于显示最近执行的 SQL语句产生的警告、错误或通知信息。它可以帮助我们了解语句执行过程中的问题。如下示例:
explain select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';
show warnings;
将上述示例的 warnings信息摘出来如下:
/* select#1 */ select `yuanjava`.`user`.`id` AS `id`,`yuanjava`.`user`.`name` AS `name`,`yuanjava`.`user`.`age` AS `age`,`yuanjava`.`user`.`sex` AS `sex`,`yuanjava`.`user`.`created_at` AS `created_at`
from `yuanjava`.`user`
where (`yuanjava`.`user`.`name` = 'name-f692472e-40de-4053-9498-54b9800e9fb1')
从 warnings信息可以看出:1=1已经被查询优化器优化掉,因此,对整体的性能影响并不大。那么,有没有 MySQL的官方资料可以佐证 where 1=1确实被优化了?
答案:有,MySQL有一种 Constant-Folding Optimization(常量折叠优化)的功能。
Constant-Folding Optimization
MySQL的优化器具有一项称为 Constant-Folding Optimization(常量折叠优化)的功能,可以从查询中消除重言式表达式。Constant-Folding Optimization 是一种编译器的优化技术,用于优化编译时计算表达式的常量部分,从而减少运行时的计算量,换句话说:Constant-Folding Optimization 是发生在编译期,而不是引擎执行期间。对于上述表达的”重言式表达式”又是什么呢?
重言式
重言式(Tautology )又称为永真式,它的汉语拼音为:[Chóng yán shì],是逻辑学的名词。命题公式中有一类重言式,如果一个公式,对于它的任一解释下其真值都为真,就称为重言式(永真式)。其实,重言式在计算机领域也具有重要应用,比如”重言式表达式”(Tautological expression),它指的是那些总是为真的表达式或逻辑条件。
在 SQL查询中,重言式表达式是指无论在什么情况下,结果永远为真,它们通常会被优化器识别并优化掉,以提高查询效率。例如,如果 where中包含 1=1 或 A=A 这种重言式表达式,它们就会被优化器移除,因为对查询结果没有实际影响。另外,通过下面 MySQL架构示意图可以看出:优化器是属于 MySQL的 Server层,因此,Constant-Folding Optimization功能支持受 MySQL Server的版本影响。
如何选择?
where 1=1 和 <where> 标签 两种方案,该如何选择?
- 如果 MySQL Server版本大于等于 5.7,两个随便选,或者根据团队的要求来选;
- 如果 MySQL Server版本小于 5.7,假如使用的是 MyBatis,建议使用<where> 标签,如果使用的还是比较老的 iBATIS,只能使用where 1=1。