征信报送系统数据库部分调优指南

征信报送系统数据库部分调优指南

此文档用于征信报送系统实施过程中数据库部分调优的

目 录

  • 1.前言
  • 2.数据库调优基本原理及内容
  • 3.数据库的安装
  • 4.数据库的基本设置
  • 5.数据库的基本观测手段
  • 6.实施过程中基本调优方法
  • 7.报送系统数据库调优方法
  • 8.实施过程中常见故障及排除

1.前言

东华征信综合报送系统基于二代征信系统数据采集规范开发,系统按照统一管理,分散

处理的原则,提供数据采集、校验处理、汇总处理、总分校验、报送文件生成、报表生成、

反馈管理等一整套自动化功能。帮助金融机构降低人工干预程度,提高工作效率及质量。另

外,针对部分金融机构对于报送自动化需求不高的情况,我们也有单独的数据校验模块可供

选择,节约成本的同时保证报送数据的准确性。

报送系统具有大数据量、时效性强的特点,对后台数据库系统要求较高,属重资源用户。

因此,在实施、维护过程中,不可避免对数据库造成巨大压力,需要合理调优资源,以支撑

整个系统的正常运转。

本文以东华二代报送系统实施过程中相关经验总结而成,针对实施现场中常见问题展开

论述,作为实施人员数据库部分的简单指南。

2.数据库调优基本原理及内容

2.1 数据库调优的目标

一般来说,数据库调优的目的就是要让数据库运行得更快,响应的时间更快,吞吐量

更大。但是随着用户量的不断增加,以及应用程序复杂度的提升,数据库调优有了不同的目

标,因为用户在不同时间段访问服务器遇到的瓶颈不同,比如双十一促销的时候会带来大规

模的并发访问;还有用户在进行不同业务操作的时候,数据库的事务处理和 SQL 查询都会

有所不同。因此我们还需要更加精细的定位,去确定调优的目标。

  1. 通过用户的反馈去确定调优的目标:用户是服务对象,因此他们的反馈是最直接的。

  2. 通过日志分析:我们可以通过查看数据库日志和操作系统日志等方式找出异常情况,通

过它们来定位遇到的问题。

3.通过服务器资源使用监控:通过监控服务器的 CPU、内存、I/O 等使用情况,可以实时了

解服务器的性能使用,与历史情况进行对比。

  1. 数据库内部状况监控:在数据库的监控中,活动会话监控是一个重要的指标。通过它,

你可以清楚地了解数据库当前是否处于非常繁忙的状态,是否存在 SQL 堆积等。

2.2 数据调优的维度:

2.2.1选择适合的 DBMS

不同的DBMS有不同的特点,DBMS 的选择关系到了后面的整个设计过程,所以第一步

就是要选择适合的 DBMS。

2.2.2优化表设计

①表结构要尽量遵循第三范式的原则。这样可以让数据结构更加清晰规范,减少冗余字段,
同时也减少了在更新,插入和删除数据时等异常情况的发生。
②如果分析查询应用比较多,尤其是需要进行多表联查的时候,可以采用反范式进行优化。
反范式采用空间换时间的方式,通过增加冗余字段提高查询的效率。
③表字段的数据类型选择,关系到了查询效率的高低以及存储空间的大小。一般来说,如果
字段可以采用数值类型就不要采用字符类型;字符长度要尽可能设计得短一些。

2.2.3. 优化逻辑查询

SQL 查询优化,可以分为逻辑查询优化和物理查询优化。逻辑查询优化就是通过改变
SQL 语句的内容让 SQL 执行效率更高效,采用的方式是对 SQL 语句进行等价变换,对查
询进行重写。
SQL 的查询重写包括了子查询优化、等价谓词重写、视图重写、条件简化、连接消除
和嵌套连接消除等。
EXISTS 子查询和 IN 子查询的时候,会根据小表驱动大表的原则选择适合的子查询。
在 WHERE 子句中会尽量避免对字段进行函数运算,它们会让字段的索引失效。
eg:

SELECT ... FROM ... WHERE SUBSTRING(comment_text,1.3) = 'abc'
改写为
SELECT ... FROM ... WHERE comment_text LIKE 'abc%'

2.2.4优化物理查询

物理查询优化是在确定了逻辑查询优化之后,采用物理优化技术(比如索引等),通过
计算代价模型对各种可能的访问路径进行估算,从而找到执行方式中代价最小的作为执行计
划。这个部分的核心是高效地建立索引,并通过这些索引来做各种优化。
索引创建需要注意的情况:
1.如果数据重复度高,就不需要创建索引。通常在重复度超过 10% 的情况下,可以不创建
这个字段的索引。比如性别这个字段(取值为男和女)。
2.要注意索引列的位置对索引使用的影响。比如我们在 WHERE 子句中对索引字段进行了表
达式的计算,会造成这个字段的索引失效。
3.要注意联合索引对索引使用的影响。我们在创建联合索引的时候会对多个字段创建索引,
这时索引的顺序就很重要了。比如我们对字段 x, y, z 创建了索引,那么顺序是 (x,y,z) 还是
(z,y,x),在执行的时候就会存在差别。
4.要注意多个索引对索引使用的影响。索引不是越多越好,因为每个索引都需要存储空间,
索引多也就意味着需要更多的存储空间。此外,过多的索引也会导致优化器在进行评估的时
候增加了筛选出索引的计算时间,影响评估的效率。
5.在物理查询优化阶段会根据数据表的索引情况和数据情况确定访问路径,这就决定了执行
SQL 时所需要消耗的资源,并决定这些查询所采用的路径:
①单表查询:对于单表扫描来说,我们可以全表扫描所有的数据,也可以局部扫描。
②两张表的连接:常用的连接方式包括了嵌套循环连接、HASH 连接和合并连接。
③多张表的连接:多张数据表进行连接的时候,顺序很重要,因为不同的连接路径查询的效
率不同,搜索空间也会不同。巨大的搜索空间会占用很多的资源,因此我们需要通过调整连
接顺序,将搜索空间调整在一个可接收的范围内。

2.2.5使用 Redis 或 Memcached 作为缓存

因为数据都是存放到数据库中,我们需要从数据库层中取出数据放到内存中进行业务逻
辑的操作,当用户量增大的时候,如果频繁地进行数据查询,会消耗数据库的很多资源。如
果我们将常用的数据直接放到内存中,就会大幅提升查询的效率。
键值存储数据库Redis 和 Memcached,它们都可以将数据存放到内存中。
两者相比Redis 支持持久化,可以让我们的数据保存在硬盘上,不过这样一来性能消耗
也会比较大。而 Memcached 仅仅是内存存储,不支持持久化。并且Redis支持的数据类型
比 Memcached 要多。

2.2.6库级优化

库级优化是站在数据库的维度上进行的优化策略,比如控制一个库中的数据表数量。或
是采用主从架构来优化读写策略。
如果读和写的业务量都很大,并且它们都在同一个数据库服务器中进行操作,那么数据
库的性能就会出现瓶颈,这时为了提升系统的性能,优化用户体验,我们可以采用读写分离
的方式降低主数据库的负载,比如用主数据库(master)完成写操作,用从数据库(slave)
完成读操作。

分库:

我们还可以对数据库分库分表。当数据量级达到亿级以上时,有时候我们需要把一个数

据库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。

垂直切分和水平切分:

垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同

的服务器上,它的核心理念是专库专用。

垂直分表:将一个表按照字段分成多表,每个表存储其中一部分字段。

水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的

服务器上。

水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中。

采用垂直分表的形式,就是将一张数据表分拆成多张表,采用水平拆分的方式,就是将

单张数据量大的表按照某个属性维度分成不同的小表。分拆在提升数据库性能的同时,也会

增加维护和使用成本。

如果数据库中的数据表过多,可以采用垂直分库的方式,将关联的数据表部署在一个数

据库上。如果数据表中的列过多,可以采用垂直分表的方式,将数据表分拆成多张,把经常

一起使用的列放到同一张表里。

如果数据表中的数据达到了亿级以上,可以考虑水平分表,将大的数据表分拆成不同的

子表,每张表保持相同的表结构。比如你可以按照年份来划分,把不同年份的数据放到不同

的数据表中。 2017 年、 2018 年和 2019 年的数据就可以分别放到三张数据表中。

2.3 征信报送系统实施过程中数据库调优

在报送系统实施的过程中,系统的架构、产品的方案基本确定,数据库系统一般也由甲

方指定,实施工程师所做的主要工作就是在现有的环境上完成产品的搭建、二次开发、测试、

环境迁移、正式上线等过程。在此过程中,对数据库调优而言,主要包括以下几个方面:

1.测试数据库环境搭建(现场未提供数据库)及联通测试

2.产品脚本导入

3.ETL部分二次开发

4.产品功能测试

5.产品压力测试

6.数据库及产品参数适配

7.形成部署文档(含数据库配置要求)

8.正式生产环境迁移

9.正式生产环境测试

10.正式生产环境数据库适配

11 .正式上线

因此,本文按如下内容展开

1.数据库安装

2.数据库基本设置

3.数据库基本观测手段及举例

4.实施过程中基本调优方法

5.报送系统应用调优方法

6 .实施过程中常见故障及排除

3.数据库的安装

现场数据库多由甲方提供,基本不涉及我方安装。但部分现场测试环境,Mysql由我方
驻场工程师安装。Oracle环境大部分由甲方提供,不涉及安装部分。本章仅给出测试环境
mysql 5.7版本的安装方法。

  1. 关闭selinux 和 firewald
    永久关闭selinux: vi /etc/sysconfig/selinux ----->SELINUX=disabled
    临时关闭selinux: setenforce=0 , 查看 getenforce
    2.添加mysql用户组和用户
    groupadd mysql
    useradd -r -g mysql mysql
    3.解压压缩文件并创建符号链接
    mkdir -p /xxx_app
    cd /xxx_app
    tar -zxvf mysql-5.7.23-linux-glibc2.12-i686.tar.gz
    ln -s mysql-5.7.23-linux-glibc2.12-i686 mysql
    cd /xxx_app/mysql
    mkdir -p /xxx_app/mysql5723/data
    mkdir -p /xxx_app/mysql5723/log
    touch my.cnf
    cd log
    touch mysql-error.log
    chown -R mysql:mysql /xxx_app
    chgrp -R mysql /xxx_app

vi /xxx_app/mysql5723/my.cnf

[client]
port = 3306
socket = /xxx_app/mysql5723/mysql.sock
[mysql]
default-character-set=utf8mb
socket = /xxx_app/mysql5723/mysql.sock
[mysqld]
port = 3306
socket = /xxx_app/mysql5723/mysql.sock
basedir = /xxx_app/mysql5723/
datadir = /xxx_app/mysql5723/data
pid-file = /xxx_app/mysql5723/mysql.pid
user=mysql
bind-address = 0.0.0.
autocommit=

server-id = 1
skip-name-resolve

skip-grant-tables

max_connections = 10000
max_connect_errors = 600
max_allowed_packet = 100M
wait_timeout = 3600 #
interactive_timeout = 3600 #
read_buffer_size = 16M
read_rnd_buffer_size = 8M
join_buffer_size = 8M
tmp_table_size = 64M
max_heap_table_size = 128M
tmpdir = /tmp
default-time-zone='+8:00'
log_timestamps = SYSTEM
character_set_server=utf8mb
collation-server=utf8mb4_bin
character-set-client-handshake=FALSE
sql_mode="NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES"

bulk_insert_buffer_size=100M
log_bin = mysql-bin
binlog_format = row
log_error = /xxx_app/mysql5723/log/mysql-error.log
slow_query_log = 1
slow_query_log_file = /xxx_app/mysql5723/log/slow.log
long_query_time = 300
log_queries_not_using_indexes = 1
lower_case_table_names = 1
expire_logs_days = 1

default-storage-engine = InnoDB
innodb_file_per_table = 1
innodb_data_home_dir = /xxx_app/mysql5723/data
innodb_data_file_path =
ibdata1:100M;ibdata2:100M;ibdata3:100M;ibdata4:100M:autoextend:max:10G
注意只能成 :max:10G 不能写成 :max:10GB
innodb_temp_data_file_path=ibtmp1:100M;ibtmp2:100M:autoextend:max:10G
innodb_page_size = 16384
extend to 32G
innodb_buffer_pool_size = 32G
innodb_buffer_pool_instances = 3
innodb_log_file_size = 4G

innodb_log_buffer_size = 128M
innodb_log_group_home_dir = /xxx_app/mysql5723/data/
innodb_undo_directory = /xxx_app/mysql5723/data/
innodb_undo_tablespaces = 3
innodb_undo_log_truncate = ON
innodb_max_undo_log_size = 2G
innodb_purge_rseg_truncate_frequency = 128
innodb_buffer_pool_load_at_startup = 1
innodb_buffer_pool_dump_at_shutdown = 1
innodb_flush_method=O_DIRECT

innodb_write_io_threads = 8
innodb_purge_threads = 5
innodb_page_cleaners = 5
innodb_io_capacity = 2000

relay_log = /xxx_app/mysql5723/data/relay.log
relay_log_recovery = 1
transaction_isolation=READ-COMMITTED
master_info_repository = TABLE
relay_log_info_repository = TABLE
log_slave_updates = 1
sync_binlog = 1
gtid-mode = ON
enforce-gtid-consistency = ON
skip-slave-start = 1
sync_binlog=
innodb_flush_log_at_trx_commit=
plugin_load =
"rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"

loose_rpl_semi_sync_master_enabled = 1

loose_rpl_semi_sync_slave_enabled = 1

loose_rpl_semi_sync_master_timeout = 500

4.进入support-files目录
cp mysql.server /etc/init.d
mv mysql.server mysqld_app
chkconfig --add mysqld_app
chkconfig mysqld_app on

vi mysqld_app
把下面的两个变量设为项目的路径

basedir = /xxx_app/mysql5723/

datadir = /xxx_app/mysql5723/data

5.初始化mysql
/xxx_app/mysql5723/bin/mysqld --defaults-file=/xxx_app/mysql5723/my.cnf --user=mysql
--initialize 初始化
tail /log/mysql-error.log 找到临时密码 xxxxxpassword

  1. 启动服务
    service mysqld_app start
    对新版本7 systemctl start mysqld_app
    systemctl stop mysqld_app
    7.更改临时密码
    ps -ef | grep mysqld 可以看到服务正常启动
    使用上面输出的sock文件
    mysql -hlocalhost -uroot -p"xxxxxpassword" -P3306 -S /xxx_app/mysql5723/mysql.sock

alter user 'root'@'localhost' identified with mysql_native_password by 'test123';
flush privileges;
create user 'root'@'%' identified with mysql_native_password by 'test123';
grant all on . to 'root'@'%';
flush privileges;

mysql即可正常访问。

4.数据库的基本设置

4.1Oracle 数据库基本设置

4.1.1可用性检查

4.1.1.1 服务器进程检查

Linux系统下的Oracle是多进程结构,包括smon,pmon,checkpoint,arch等系统组件。

进程检查:
[oracle@redhat706 ~]$ ps -ef | grep ora_|grep -v grep
oracle 33132 1 0 16:28? 00:00:00 ora_w000_ORCL
oracle 85832 1 0 Aug29? 00:00:47 ora_pmon_ORCL
oracle 85840 1 0 Aug29? 00:02:32 ora_vktm_ORCL
oracle 85845 1 0 Aug29? 00:00:11 ora_gen0_ORCL
oracle 85847 1 0 Aug29? 00:00:25 ora_diag_ORCL
oracle 85849 1 0 Aug29? 00:00:11 ora_dbrm_ORCL
oracle 85851 1 0 Aug29? 00:00:14 ora_psp0_ORCL
oracle 85853 1 0 Aug29? 00:05:52 ora_dia0_ORCL
oracle 85855 1 0 Aug29? 00:00:12 ora_mman_ORCL
oracle 85857 1 0 Aug29? 00:00:29 ora_dbw0_ORCL
oracle 85859 1 0 Aug29? 00:00:20 ora_lgwr_ORCL
oracle 85861 1 0 Aug29? 00:02:21 ora_ckpt_ORCL
oracle 85863 1 0 Aug29? 00:00:31 ora_smon_ORCL
oracle 85865 1 0 Aug29? 00:00:06 ora_reco_ORCL
oracle 85867 1 0 Aug29? 00:07:07 ora_mmon_ORCL
oracle 85869 1 0 Aug29? 00:04:03 ora_mmnl_ORCL
oracle 85871 1 0 Aug29? 00:00:08 ora_d000_ORCL
oracle 85873 1 0 Aug29? 00:00:08 ora_s000_ORCL
oracle 85975 1 0 Aug29? 00:00:51 ora_cjq0_ORCL
oracle 86864 1 0 Aug29? 00:00:14 ora_smco_ORCL

ps -ef | grep smon
kill -9 pid of pmon

4.1.1.2 数据库状态检查

数据库状态检查:

[oracle@redhat706 admin]$ sqlplus / as sysdba

SQL*Plus: Release 11.2.0.1.0 Production on Thu Sep 1 16:37:23 2022

Copyright (c) 1982, 2009, Oracle. All rights reserved.

Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> select status from gv$instance;

STATUS

OPEN

如果是19C,还需要检查pdb状态

4.1.1.3 监听状态检查

监听状态检查:

[oracle@redhat706 ~]$ lsnrctl status

LSNRCTL for Linux: Version 11.2.0.1.0 - Production on 01-SEP-2022 16:32:

Copyright (c) 1991, 2009, Oracle. All rights reserved.

Connecting to (DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=10.10.122.84)(PORT=1521)))
STATUS of the LISTENER

Alias LISTENER

Version TNSLSNR for Linux: Version 11.2.0.1.0 - Production
Start Date 09-AUG-2022 10:44:
Uptime 23 days 5 hr. 48 min. 9 sec
Trace Level support
Security ON: Local OS Authentication
SNMP OFF
Listener Parameter File /u01/app/oracle/product/11.2.0/db_1/network/admin/listener.ora
Listener Log File /u01/app/oracle/diag/tnslsnr/redhat706/listener/alert/log.xml
Listener Trace File
/u01/app/oracle/diag/tnslsnr/redhat706/listener/trace/ora_32565_140737352795968.trc
Listening Endpoints Summary...
(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=10.10.122.84)(PORT=1521)))
Services Summary...
Service "ORCL" has 1 instance(s).
Instance "ORCL", status UNKNOWN, has 1 handler(s) for this service...
Service "orcl.com" has 1 instance(s).
Instance "ORCL", status READY, has 1 handler(s) for this service...
Service "orclXDB.com" has 1 instance(s).
Instance "ORCL", status READY, has 1 handler(s) for this service...
The command completed successfully

4.1.1.4 tns 连接检查

tns检查:

[oracle@redhat706 admin]$ tnsping orcl.com

TNS Ping Utility for Linux: Version 11.2.0.1.0 - Production on 01-SEP-2022 16:36:

Copyright (c) 1997, 2009, Oracle. All rights reserved.

Used parameter files:

Used TNSNAMES adapter to resolve the alias
Attempting to contact (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST =
10.10.122.84)(PORT = 1521))) (CONNECT_DATA = (SERVICE_NAME = orcl.com)))
OK (0 msec)

到此为止,数据库处于可用状态,可以接受连接。

sqlplus test/password@192.168.1.1:1521/orcl 测试连接成功!

4.1.2数据库常用配置项检查

4.1.2.1 SGA 检查

sga_target=系统内存80%80%
sga_max_target=系统内存80%80%

4.1.2.2 PGA 检查

pga_aggregate_target= sga_target=系统内存80%20%

4.1.2.3 Process 检查

一般保持在 800 以上

11G:

session=process*1.5+

4.1.2.3 Logfile 检查

select * from v$log;
生产上至少保证 3 组,每个logfile 2G;

4.1.2.4 cpu 检查

生产上至少保证16 cores

4.1.2.5 表空间检查

select a.tablespace_name as "Tablespace Name",
round(a.bytes_alloc / 1024 / 1024) "Allocated (MB)",
round(nvl(b.bytes_free, 0) / 1024 / 1024) "Free (MB)",
round((a.bytes_alloc - nvl(b.bytes_free, 0)) / 1024 / 1024) "Used (MB)",
round((nvl(b.bytes_free, 0) / a.bytes_alloc) * 100) "% Free",
100 - round((nvl(b.bytes_free, 0) / a.bytes_alloc) * 100) "% Used",
round(maxbytes/1024 / 1024) "Max. Bytes (MB)"
from ( select f.tablespace_name,
sum(f.bytes) bytes_alloc,
sum(decode(f.autoextensible, 'YES',f.maxbytes,'NO', f.bytes)) maxbytes
from dba_data_files f
group by tablespace_name) a,
( select f.tablespace_name,
sum(f.bytes) bytes_free
from dba_free_space f
group by tablespace_name) b
where a.tablespace_name = b.tablespace_name (+)
union all
select
h.tablespace_name as tablespace_name,

round(sum(h.bytes_free + h.bytes_used) / 1048576) megs_alloc,
round(sum((h.bytes_free + h.bytes_used) - nvl(p.bytes_used, 0)) / 1048576) megs_free,
round(sum(nvl(p.bytes_used, 0))/ 1048576) megs_used,
round((sum((h.bytes_free + h.bytes_used) - nvl(p.bytes_used, 0)) / sum(h.bytes_used +
h.bytes_free)) * 100) Pct_Free,
100 - round((sum((h.bytes_free + h.bytes_used) - nvl(p.bytes_used, 0)) /
sum(h.bytes_used + h.bytes_free)) * 100) pct_used,
round(sum(f.maxbytes) / 1048576) max
from sys.v_$TEMP_SPACE_HEADER h, sys.v_$Temp_extent_pool p, dba_temp_files f
where p.file_id(+) = h.file_id
and p.tablespace_name(+) = h.tablespace_name
and f.file_id = h.file_id
and f.tablespace_name = h.tablespace_name
group by h.tablespace_name
ORDER BY 2;

确保产品表空间利用率60%以下

undo表空间 40G以上
temp表空间 40G 以上

4.2Mysql 数据库基本设置

  1. 2 .1可用性检查

4.2.1.1 服务器进程检查

[root@redhat706 ~]# ps -ef | grep mysqld
root 1871 1 0 Aug31? 00:00:00 /bin/sh
/usr/local/mysql/bin/mysqld_safe --datadir=/usr/local/mysql/d
ata --pid-file=/usr/local/mysql/tmp/mysqld.pid
mysql 2808 1871 0 Aug31? 00:16:18 /usr/local/mysql/bin/mysqld
--basedir=/usr/local/mysql --datadir=/usr
/local/mysql/data --plugin-dir=/usr/local/mysql/lib/plugin --user=mysql
--log-error=/usr/local/mysql/log/mysqld.log
--pid-file=/usr/local/mysql/tmp/mysqld.pid --socket=/tmp/mysql.sock --port=
root 63703 63567 0 15:23 pts/2 00:00:00 grep --color=auto mysqld

4. 2. 1. 2 监听端口检查

[root@redhat706 ~]# netstat -ntlpa | grep mysqld
tcp6 0 0 :::3306 ::😗 LISTEN
2808/mysqld
tcp6 0 0 127.0.0.1:3306 127.0.0.1:25761 ESTABLISHED
2808/mysqld

4. 2. 1. 3 服务器实例检查

mysql -hlocalhost -uroot -prootrootroot --socket=/tmp/mysql.sock --port=

使用tcp连接,模拟应用帐号连接

[root@redhat706 ~]# mysql -h127.0.0.1 -uroot -prootrootroot --port=3306
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 29
Server version: 5.7.27-log MySQL Community Server (GPL)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

4. 2. 1. 4 服务器运行日志检查

mysql> show variables like '%general_log_file%';
+------------------+-------------------------------------+
| Variable_name | Value |
+------------------+-------------------------------------+
| general_log_file | /usr/local/mysql/data/redhat706.log |
+------------------+-------------------------------------+

如果服务器启动不正常,检查运行日志,查找error

[root@redhat706 ~]# cat /usr/local/mysql/data/redhat706.log | tail -n 10 | more
2021 - 02 - 01T10:27:23.156790Z 11164 Connect rhzx2_w@10.10.122.83 on rhzx2 using
TCP/IP
2021 - 02 - 01T10:27:23.157857Z 11164 Query /* mysql-connector-java-5.1.21 ( Revision:
${bzr.revision-id} ) */SHOW VARIABLES WHERE Variable_name ='language' OR
Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR
Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variab
le_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name =
'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_nam
e = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name =
'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time
_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name =
'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode'
OR
Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name
= 'init_connect'

2021 - 02 - 01T10:27:23.162118Z 11164 Query /* mysql-connector-java-5.1.21 ( Revision:
${bzr.revision-id} ) */SELECT @@session.auto_increment_increment
2021 - 02 - 01T10:27:23.162602Z 11164 Query SHOW COLLATION
2021 - 02 - 01T10:27:23.177325Z 11164 Query SHOW CHARACTER SET
2021 - 02 - 01T10:27:23.178840Z 11164 Query SELECT @@session.autocommit
2021 - 02 - 01T10:27:23.179383Z 11164 Query SET NAMES utf8
2021 - 02 - 01T10:27:23.179802Z 11164 Query SET character_set_results = NULL
2021 - 02 - 01T10:27:23.180168Z 11164 Query SET autocommit=1
2021 - 02 - 01T10:27:23.196331Z 11164 Quit

4. 2. 1. 5 数据库表空间检查

select table_schema as "数据库",sum(table_rows) as "记录数
",sum(truncate(data_length/1024/1024, 2)) as "数据容量
(MB)",sum(truncate(index_length/1024/1024, 2)) as "索引容量(MB)"
from information_schema.tables
group by table_schema
order by sum(data_length) desc, sum(index_length) desc;

4. 2. 1. 6 数据库连接数检查

show variables like '%connect%';

5.数据库的基本观测手段

5 .1Oracle 数据库基本观测手段

现场开发运维场景中,主要面临的情况就是锁表处理、SQL跑不出,下面主要介绍介绍一下

oracler提供的基本观测方法。

5 .1.1 线程类查询

查询数据库正在执行的sql

select a.program, b.spid, c.sql_text,c.SQL_ID
from v$session a, v$process b, v$sqlarea c
where a.paddr = b.addr
and a.sql_hash_value = c.hash_value
and a.username is not null;

查询数据库的锁

select /*+ rule */ lpad('--',decode(b.block,1,0,4))||s.username user_name,
b.type,o.owner||'.'||o.object_name object_name,
s.sid,s.serial#,decode(b.request,0,'blocked','waitting') status
from dba_objects o,v$session s,v$lock v,v$lock b
where v.id1=o.object_id and v.sid=s.sid
and v.sid=b.sid and (b.block=1 or b.request>0)
and v.type='TM' and v.sid=b.sid
order by b.id2,v.id1,user_name desc;

alter system kill session 'sid,serial#'; 杀掉阻塞进程即可

5 .1. 2 执行计划类查询

oracle的执行计划内容很多,一般包括路径选择,连接顺序选择许多方面。对现场而言,很
多SQL跑不出,很多是执行计划跑错。下面简单说明下如何查执行计划。

表连接选择
oracle 数据库一共有 nestloop join ,merge join,has join 三种表连接方式,我们在书写sql时,
要根据sql的特点综合运行这三种方式。

基本点:
nestloop 适用于返回极少数据集
merge join 适用于排序数据集连接
hash join 适用于大数据集

drop table test1;
drop table test2;

select * from dba_objects;

create table test1 as select * from dba_objects;
create table test2 as select * from dba_objects;

create index idx_test1_object_name on test1(object_name);
create index idx_test2_object_name on test2(object_name);

nest loop连接
explain plan for select /*+ use_nl(a b) */ *
from test1 a,test2 b
where a.object_name=b.object_name;

select * from table(dbms_xplan.display());

merge join连接
explain plan for select /*+ use_merge(a b) */ *
from test1 a,test2 b
where a.object_name=b.object_name;

select * from table(dbms_xplan.display());

hash join连接
explain plan for select /*+ use_hash(a b) */ *
from test1 a,test2 b
where a.object_name=b.object_name;

select * from table(dbms_xplan.display());

关于in,exists 执行效率问题
in,exists的执行效率的高低取决于执行计划的异同,同样的执行计划一样的效率,不同的
计划需要根据cost综合判断,没有单纯的好与不好。

explain plan for select *
from test1 a
where a.object_name in (select object_name
from test2 b);

select * from table(dbms_xplan.display());

explain plan for select *
from test1 a
where exists in (select 1
from test2 b
where a.object_name=b.object_name);

select * from table(dbms_xplan.display());

我们可以看到,走的是同样的执行计划,因此,in和exists在此sql效率相同。

一般而言,尽量避免走出filter类的执行计划。

关于sql调优,知识点过多,请参阅专业调优书籍,这里仅给出查询执行计划的一般方法,
用于现场故障采集。

5 .1Mysql 数据库基本观测手段

现场开发运维场景中,主要面临的情况就是锁表处理、SQL跑不出,下面主要介绍介绍一下

mysql提供的基本观测方法。

5 .1.1 线程类查询

查询当前的线程

mysql> show full processlist;
+----+------+--------------------+------+-------------+---------+-----------------------------------------------------------
----+-----------------------+
| Id | User | Host | db | Command | Time | State
| Info |
+----+------+--------------------+------+-------------+---------+-----------------------------------------------------------
----+-----------------------+
| 2 | root | localhost:25761 | NULL | Binlog Dump | 3471016 | Master has sent all binlog
to slave; waiting for more updates | NULL |
| 27 | root | localhost | NULL | Sleep | 3402 |
| NULL |
| 31 | root | 10.10.122.83:34678 | cx | Sleep | 350 |
| NULL |
| 32 | root | 10.10.122.83:3574 | cx | Sleep | 350 |
| NULL |
| 33 | root | localhost | NULL | Query | 0 | starting
| show full processlist |
+----+------+--------------------+------+-------------+---------+-----------------------------------------------------------
----+-----------------------+

select * from performance_schema.threads;

查询相关的锁信息,确定阻塞者,kill thread_id即可。

select * from information_schema.innodb_locks;

有能力看innodb引擎输出的可以查看报告
show engine innodb status;

mysql> show engine innodb status \G;
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:

2022 - 09 - 06 16:32:37 0x7fffe2184700 INNODB MONITOR OUTPUT

Per second averages calculated from the last 40 seconds

BACKGROUND THREAD

srv_master_thread loops: 122 srv_active, 0 srv_shutdown, 560828 srv_idle
srv_master_thread log flush and writes: 560950

SEMAPHORES

OS WAIT ARRAY INFO: reservation count 1238
OS WAIT ARRAY INFO: signal count 1244
RW-shared spins 0, rounds 483, OS waits 145
RW-excl spins 0, rounds 790, OS waits 16
RW-sx spins 26, rounds 399, OS waits 1
Spin rounds per wait: 483.00 RW-shared, 790.00 RW-excl, 15.35 RW-sx

TRANSACTIONS

Trx id counter 2078328
Purge done for trx's n:o < 2073918 undo n:o < 0 state: running but idle
History list length 122
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 422212142161520, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422212142160608, not started

0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422212142159696, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 2078327, ACTIVE 98 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 34, OS thread handle 140736884655872, query id 5246 localhost root Sending
data
select * from pmcf_config for update
Trx read view will not see trx with id >= 2078328, sees < 2078326
------- TRX HAS BEEN WAITING 26 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26790 page no 3 n bits 152 index PRIMARY of table
testdb.pmcf_config trx id 2078327 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 12; compact format; info bits 0
0: len 1; hex 31; asc 1;;
1: len 6; hex 0000001c9900; asc ;;
2: len 7; hex d1000002bd0110; asc ;;
3: len 12; hex 706c6174666f726d486f7374; asc platformHost;;
4: len 12; hex e5b9b3e58fb0e59cb0e59d80; asc ;;
5: len 30; hex 687474703a2f2f3132372e302e302e313a383038302f637265646974706c; asc
http://127.0.0.1:8080/creditpl; (total 39 bytes);
6: len 1; hex 31; asc 1;;
7: SQL NULL;
8: SQL NULL;
9: SQL NULL;
10: SQL NULL;
11: len 1; hex 30; asc 0;;


TABLE LOCK table testdb.test1 trx id 2078327 lock mode IX
RECORD LOCKS space id 28806 page no 3 n bits 72 index GEN_CLUST_INDEX of table
testdb.test1 trx id 2078327 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

TABLE LOCK table testdb.pmcf_config trx id 2078327 lock mode IX
RECORD LOCKS space id 26790 page no 3 n bits 152 index PRIMARY of table
testdb.pmcf_config trx id 2078327 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 12; compact format; info bits 0
0: len 1; hex 31; asc 1;;
1: len 6; hex 0000001c9900; asc ;;
2: len 7; hex d1000002bd0110; asc ;;
3: len 12; hex 706c6174666f726d486f7374; asc platformHost;;
4: len 12; hex e5b9b3e58fb0e59cb0e59d80; asc ;;

5: len 30; hex 687474703a2f2f3132372e302e302e313a383038302f637265646974706c; asc
http://127.0.0.1:8080/creditpl; (total 39 bytes);
6: len 1; hex 31; asc 1;;
7: SQL NULL;
8: SQL NULL;
9: SQL NULL;
10: SQL NULL;
11: len 1; hex 30; asc 0;;

---TRANSACTION 2078326, ACTIVE 217 sec
4 lock struct(s), heap size 1136, 80 row lock(s)
MySQL thread id 33, OS thread handle 140736986629888, query id 5256 localhost root starting
show engine innodb status
TABLE LOCK table testdb.test1 trx id 2078326 lock mode IX
RECORD LOCKS space id 28806 page no 3 n bits 72 index GEN_CLUST_INDEX of table
testdb.test1 trx id 2078326 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

TABLE LOCK table testdb.pmcf_config trx id 2078326 lock mode IX
RECORD LOCKS space id 26790 page no 3 n bits 152 index PRIMARY of table
testdb.pmcf_config trx id 2078326 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
..........

具体含义请查阅相关资料。

5 .1. 2 mysql查询执行计划

mysql> explain select * from pmcf_config;
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref |
rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | pmcf_config | NULL | ALL | NULL | NULL | NULL
| NULL | 78 | 100.00 | NULL |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

explain中各列的含义

每列所包含的信息

id
id是用来顺序标识整个查询中SELELCT 语句的,在嵌套查询中id越大的语句越先执行。该
值可能为NULL,如果这一行用来说明的是其他行的联合结果。
select_type

table
表示查询涉及的表或衍生表
关联优化器会为查询选择关联顺序,左侧深度优先
当from中有子查询的时候,表名是derivedN的形式,N指向子查询,也就是explain结果中
的下一列
当有union result的时候,表名是union 1,2等的形式,1,2表示参与union的query id
注意:MySQL对待这些表和普通表一样,但是这些“临时表”是没有任何索引的。
type
type显示的是访问类型,结果值从好到坏依次是:system > const > eq_ref > ref > fulltext >
ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL ,一般来
说,得保证查询至少达到range级别,最好能达到ref。

possible_keys
显示查询使用了哪些索引,表示该索引可以进行高效地查找,但是列出来的索引对于后续优
化过程可能是没有用的。
key
key列显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。要想强制
MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者
IGNORE INDEX。
key_len
key_len列显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的
长度。在不损失精确性的情况下,长度越短越好 。
key_len计算规则:
一般地,key_len 等于索引列类型字节长度,例如int类型为 4 - bytes,bigint为 8 - bytes;
如果是字符串类型,还需要同时考虑字符集因素,例如:CHAR(30) UTF8则key_len至少是
90 - bytes;
若该列类型定义时允许NULL,其key_len还需要再加 1 - bytes;
若该列类型为变长类型,例如 VARCHAR(TEXT\BLOB不允许整列创建索引,如果创建部分

索引,也被视为动态列类型),其key_len还需要再加 2 - bytes;

ref
ref列显示使用哪个列或常数与key一起从表中选择行。
rows
rows列显示MySQL认为它执行查询时必须检查的行数。注意这是一个预估值。
Extra
Extra是EXPLAIN输出中另外一个很重要的列,该列显示MySQL在查询过程中的一些详细信
息,MySQL查询优化器执行查询的过程中对查询计划的重要补充信息。

6.实施过程中基本调优方法

6 .1Oracle 数据库基本调优方法

6.1.1 Oracle sql 执行的基本原理

oracle数据库中,一条sql的执行,包括语法分析> 语义分析> 视图转换 >表达式转换>
选择优化器 >选择连接方式 >选择连接顺序 >选择数据的搜索路径 >运行“执行计划”这几
个阶段。

相关概念介绍:

1.数据库文件

Oracle 数据库文件大概可分为 3 种,分别是:
控制文件(control files): 存放数据库本身物理结构信息
数据文件(data files): 存放数据库数据啦~
日志文件(log files): 包括重做日志文件和归档日志文件, 记录数据库数据的变化.
如下图:

2.数据库实例

但是用户和应用程序是无法直接访问数据库文件的数据的, 这时 Oracle服务器会启动
1(或多个..RAC集群啦)个实例.,用户可以通过连接这个实例来访问数据库的数据.
实例有两个组成部分,分别是:
系统全局内存区(SGA):服务器专门划分给Oracle实例使用的内存块啦.
Oracle进程: 包括服务器进程和后台进程, 后面会解析嘎啦.
2.1 系统全局内存区(SGA)
SGA可以分成 6 大块,分别是 Java pool, shared pool, database buffer cache,large pool,
streams pool, Redo log buffer

注: 可用 v$sga这个视图去查看sga各大块大小.
这篇文章介绍的是sql语句的执行流程,所以主要讲解其中的 3 个大块:
共享缓冲区(shared pool)
数据库缓冲高速缓存(database buffer cache)
重做日志缓冲区(redo log buffer)
如下图:

2.2

服务器进程

当用户(客户端)要连接Oracle数据库时, Oracle就会创建 1 个session(会话),并且在服务器
上创建 1 个专门处理这个session的进程,就是服务器进程啦.

如下图

注意啊, 每当 1 个新用户创建 1 个新的连接到数据库,Oracle都会对应创建 1 条服务器进程的.
2.3 PGA(Program Global Area) 中文自己翻译啦~
对应上面的Server Process, Oracle会在服务器上对每一条Server Process分配一定大小的
内存,就是PGA了, 注意有几个session就会有几个对应的SGA块, 所以服务器对内存需求很

大的啦~

可以用

select sum(pga_userd_mem) from v$process
语句来查看当前使用的总PGA大小.
如图:

3.客户端与服务器的SQL语句传输

这时,用户在客户端输入若干条SQL语句,例如 1 个普通的存储过程,有读和写的动作.

这条语句通过什么来传输呢,答案就是session啦, 那么服务器上用什么来接受这个sql语
句呢,答案就是Server Process啊:

如图:

server process会判断sql语句是否合法(语法,权限)
如果sql语法有错,或者对应的表或视图或Procedure没有权限,就会直接返回错误信息啦,
这个大家都懂的.
2/3.server process根据sql语句生成执行计划(execute plan).
Oracle是无法直接执行sql 语句的,必须先生成执行计划,然后 Oracle就会根据执行计
划去执行了.
而生成执行计划要访问许多数据库对象, 是 1 个比较消耗服务器资源(CPU,IO,Memory)

的动作.

而且因为同一条sql 语句可能会有多个用户多次重复的执行, 那么是否每次都生成一次
执行计划呢?
这时SGA里面的Shared pool就发挥作用了,说白了它会缓存sql的执行计划..

所以server process会首先从Shared pool里面查找有无现成的执行计划, 如果有就直接采
用.
如果无, 就自己生成 1 个,然后看情况把这个执行计划放入shared pool.

server process根据执行计划去取(写)数据.
好了 当server process得到执行计划后就可以去取数据了.
Oracle的数据放在哪里呢, 放在数据文件,这个大家都懂,但是server process 是不是
就直接去访问数据文件呢?
因为计算机的时间消耗主要都在物理 IO,所以要尽量避免物理读写,所以SGA里面的
database buffer cache起作用了.
说明白点, database buffer cache就是用来缓冲Data files里的数据的. 这样就可以避免
了对数据文件的读写.
所以server process得到执行计划后,第一步是首先去database buffer cache去找有没有现
成的数据. 如果有最好, 如果无或者缓存中数据不全的话就只能去访问data files啦.
从data files获得数据后, 也不是直接发给用户客户端,而是根据情况放入database buffer
cache里面, 以便当前或其他用户 2 次使用啦.

6.逻辑读,物理读与缓存命中率.

由上图得知,所谓逻辑读,就是从缓存(一般是内存)里读取数据啦. 而物理读,也就是从

磁盘(数据文件)里读取数据啦.

至于缓存命中率,就是取出数据的过程中 逻辑读次数/ (逻辑读次数+物理读次数) 这

个比率.

当然这个比率越接近 1 越好, 因为物理读相当费时间啦, 机械硬盘瓶颈嘛~ 除非有服务

器用SSD做硬盘...无咩可能啦~

当然命中率并不是数据库健康的唯一指标,因为当逻辑读十分巨大的时候, 即使物理读

也很大,这个比率也很好看的, 所以有时要关心每秒物理读(tps).
可以在linux下使用iostat命令来查看当前磁盘的每秒物理读啦~

  1. 在缓存中修改数据.
    server process拿出数据放入缓存中,接下来就对数据进行修改啦.
    因为修改数据很可能会产生大量缓冲数据,所以这个动作实在Database buffer cache
    里完成的. 这个很容易理解.

  2. 修改数据会产生重做日志.

上面提到,日志是用来记录数据库的数据变化的,所以对数据改动产生一定量的日志

数据. 那么这些日志是不是直接就写到日志文件中呢.

写日志文件也是物理读,所以SGA就有个Redo log buffer,就是日志缓存啦, 专门实时存放
产生的日志数据啦~

  1. 最终Server Process把返回数据或信息通过session传回给用客户端
    Server Process 做完读取和修改数据的动作后,就会将结果返给用户了.

11.将数据缓存和日志缓存写入磁盘

其实到上面那一步为止, 整个sql语句执行流程已经完成了.
可能有人会问, Server process 修改的数据和产生的日志还在 SGA里面啊, 它们不用
被写入磁盘吗?
答案是肯定需要的, 但是这些动作已经不是 sql执行流程之内, 而且这些动作也不是
server process负责的, 他们分别由DBWR 和 LGWR 这个两个进程负责.
如图:

DBWR: Database writer,后台进程之一,负责将Database buffer cache里被修改的数据写入数
据文件.
LGWR: Log writer,后台进程之一,负责将Redo log buffer里的日志数据写入到日志文件.

12.为什么Oracle要将server process 和后台进程分开?
如上图,为什么写入数据文件和日志文件要交给后台进程去完成呢.
其实我们在流程可以发现, 服务器与用户打交道的就只有 1 个进程,就是Server Process
啊, 所以server Process的速度直接影响了用户的感受, 无论后台进程多么繁忙,只要server
process 响应迅捷, 用户还是觉得数据库很快的. 相反,后台进程没事做,服务器CPU 很空闲,
但server process反应慢的话, 用户就觉得数据库慢了.
所以就要尽量精简server process的动作, 看看后台进程DBWR 和 LGWR进行的是什么
动作, 磁盘写动作啊! 呢个仲慢过物理读啊.所以这些动作完全可以在sql流程执行完慢慢来
嘛.
Server Process唯一进行的物理操作就是物理读, 这个是无办法避免的, 因为数据都在
磁盘上嘛... 除非有办法预测用户要提取的数据,提前拿出来.但也没有那么大的内存啊.
13.顺便介绍其余 3 大系统进程CKPT,SMON,PMON
都说Oracle有SGA6大池 3 大数据库文件, 5大系统进程.
其中DBWR 和 LGWR上面已经介绍过了. 现在其余 3 个
CKPT : Checkpoint 检查点进程,负责更新控制文件和数据文件的头部信息, 控件文件在这篇
blog开头就已经介绍过啦,至于数据文件的头部信息? 头部信息就是当前数据块的状态信息
啦.
SMON : system monitor 系统监视器, 负责监视维护SGA和后台进程啦,例如合并SGA里面的
碎片.
PMON : process monitor 进程监视器, 这里主要指服务器进程啦,例如一个用户突然掉线了,
但是该服务器进程还在服务器,Pmon会隔一段时间把该进程清理掉并且释放SGA啦.

何为sql优化?

sql优化存在于上述过程的每一个阶段,总结讲,优化就是通过合理配置系统参数,
合理书写sql结构,提示/指定Oracle按照证明是高效的处理方式返回结果集的过程。
sql的优化是一个涉及面非常广的话题,可以从语法层面、语义层面,也可以从物理层
面、逻辑层面等等多方面进行,对现场开发维护人员,本文只从以下几个方面简单介绍,内
部原理请参考相关资料:
1.按照规范,书写结构清晰,语义明确的sql语句。
2.按照规范书写高效的sql结构
3.按照sql的功能实现,合理选择表连接方式
4.按照规范,合理安排表连接顺序
5.优化数据的选择路径
6.指示Oracle收集正确的统计信息

6.1.1 Oracle 存储结构的基本原理

Oracle存储结构
存储结构
数据库的主要功能是保存数据,所以可以将数据库看成是保存数据的容器。数据库的存
储结构也就是数据库存储数据的结构或方式、方法、方案等。
oracle 数据库的存储结构分为物理存储结构和逻辑存储结构两种。
物理存储结构主要用于描述在 oracle 数据库外部数据的存储,即在操作系统层面中如
何组织和管理数据,与具体的操作系统有关。
逻辑存储结构主要描述oracle 数据库内部数据的组织和管理方式,即在数据库管理系
统的层面中如何组织和管理数据,与操作系统没有关系。
两者的关系
oracle 数据库的物理存储结构与逻辑存储结构既相互独立又相互联系,如下图:

从上面图中可以看出数据库物理存储结构和逻辑存储结构的基本关系:

一个数据库在物理上包含多个数据文件,在逻辑上包含多个表空间

一个表空间包含一个或多个数据文件,一个数据文件只能从属于某个表空间

数据库的逻辑块由一个或多个操作系统块组成

一个逻辑区只能从属于一个数据文件,而一个数据文件可包括一个或多个逻辑区。

Oracle如何来管理空间
Oracle ASSM(Automatic Segment Space Managed)使用位图来管理数据库中的空间,具有管理
简单、高并发等优点,ASSM的整体结构是 3 层位图块+数据块,即 4 层的树状结构,如下
所示,

块结构:

实际的段头块

数据块转储:

[oracle@rac1 ~]$ cat /u01/app/oracle/admin/anqing/udump/anqing1_ora_19997.trc
*** 2011- 06 - 07 11:02:30.023
Start dump data blocks tsn: 4 file#: 4 minblk 32 maxblk 32
buffer tsn: 4 rdba:0x01000020(4/32)-- rdba的值
scn: 0x0000.0006bfdb seq: 0x10 flg: 0x06 tail: 0xbfdb0610
frmt: 0x02 chkval: 0x26a0 type: 0x06=trans data
Hex dump of block: st=0, typ_found=1
Dump of memory from 0x0ED09400 to 0x0ED0B400
ED09400 0000A206 01000020 0006BFDB 06100000[.... ...........]
ED09410 000026A0 00180001 0000C7CC 0006BFD9[.&..............]
.....
ED094A0 00000000 00000000 00000000 00000000[................]
Repeat 465 times
ED0B1C0 00000000 08012C00 2350C203 4C494D06[.....,....P#.MIL]
ED0B1D0 0552454C 52454C43 4EC2034B B6770753[LER.CLERK..NS.w.]
....
ED0B3E0 05485449 52454C43 50C2034B B4770703[ITH.CLERK..P..w.]
ED0B3F0 0101110C 09C20201 15C102FF BFDB0610[................]
Block header dump:0x01000020
Object id on Block? Y
seg/obj: 0xc7cccsc: 0x00.6bfd9itc: 2flg: Etyp: 1 - DATA
brn: 0bdba: 0x1000019 ver: 0x01 opc: 0
inc: 0exflg: 0

Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0003.011.000000f2 0x00805794.00c8.49 --U- 14 fsc 0x0000.0006bfdb
0x02 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
data_block_dump,data header at 0xed09464

tsiz: 0x1f98
hsiz: 0x2e
pbl: 0x0ed09464
bdba: 0x01000020
76543210
flag=--------
ntab=1
nrow=14
frre=- 1
fsbo=0x2e
fseo=0x1d61
avsp=0x1d33
tosp=0x1d33
0xe:pti[0]nrow=14 offs=0--该块中保存了 14 条记录。从row 0到row 13
0x12:pri[0]offs=0x1f72
0x14:pri[1]offs=0x1f47
0x16:pri[2]offs=0x1f1c
0x18:pri[3]offs=0x1ef3
0x1a:pri[4]offs=0x1ec6
0x1c:pri[5]offs=0x1e9d
0x1e:pri[6]offs=0x1e74
0x20:pri[7]offs=0x1e4c
0x22:pri[8]offs=0x1e26
0x24:pri[9]offs=0x1dfb
0x26:pri[10]offs=0x1dd5
0x28:pri[11]offs=0x1daf
0x2a:pri[12]offs=0x1d88
0x2c:pri[13]offs=0x1d61
block_row_dump:
tab 0, row 0, @0x1f72
tl: 38 fb: --H-FL-- lb: 0x1cc: 8
col0: [ 3]c2 4a 46
col1: [ 5]53 4d 49 54 48
col2: [ 5]43 4c 45 52 4b
col3: [ 3]c2 50 03
col4: [ 7]77 b4 0c 11 01 01 01
col5: [ 2]c2 09
col6: NULL
col7: [ 2]c1 15

tab 0, row 1, @0x1f47
tl: 43 fb: --H-FL-- lb: 0x1 cc: 8
col0: [ 3]c2 4b 64
col1: [ 5]41 4c 4c 45 4e
col2: [ 8]53 41 4c 45 53 4d 41 4e
col3: [ 3]c2 4d 63
col4: [ 7]77 b5 02 14 01 01 01
col5: [ 2]c2 11
col6: [ 2]c2 04
col7: [ 2]c1 1f
tab 0, row 2, @0x1f1c
tl: 43 fb: --H-FL-- lb: 0x1cc: 8
col0: [ 3]c2 4c 16
col1: [ 4]57 41 52 44
col2: [ 8]53 41 4c 45 53 4d 41 4e
col3: [ 3]c2 4d 63
col4: [ 7]77 b5 02 16 01 01 01
col5: [ 3]c2 0d 33
col6: [ 2]c2 06
col7: [ 2]c1 1f
tab 0, row 3, @0x1ef3
...
tab 0, row 13, @0x1d61
tl: 39 fb: --H-FL-- lb: 0x1cc: 8
col0: [ 3]c2 50 23
col1: [ 6]4d 49 4c 4c 45 52
col2: [ 5]43 4c 45 52 4b
col3: [ 3]c2 4e 53
col4: [ 7]77 b6 01 17 01 01 01
col5: [ 2]c2 0e
col6: NULL
col7: [ 2]c1 0b
end_of_block_dump
End dump data blocks tsn: 4 file#: 4 minblk 32 maxblk 32

undo 段头块事务表
index state cflags wrap# uel scn dba parent-xid
nub stmt_num cmt

0x00 9 0x00 0x013d 0x0007 0x0000.000c138c 0x008003c3
0x0000.000.00000000 0x00000001 0x00000000 1501725703
0x01 9 0x00 0x013d 0x0006 0x0000.000c13aa 0x008003c3
0x0000.000.00000000 0x00000001 0x00000000 1501725704
0x02 9 0x00 0x013d 0x0005 0x0000.000c146b 0x008003c3

0x0000.000.00000000 0x00000001 0x00000000 1501725963
0x03 9 0x00 0x013d 0x0011 0x0000.000c150f 0x008003c4
0x0000.000.00000000 0x00000001 0x00000000 1501726182
0x04 9 0x00 0x013d 0x0009 0x0000.000c1409 0x008003c3
0x0000.000.00000000 0x00000001 0x00000000 1501725879
0x05 9 0x00 0x013d 0x000d 0x0000.000c1487 0x008003c3
0x0000.000.00000000 0x00000001 0x00000000 1501726008
0x06 9 0x00 0x013d 0x0004 0x0000.000c13cb 0x008003c3
0x0000.000.00000000 0x00000001 0x00000000 1501725768

回滚段块

*-----------------------------
index undo for leaf key operations
KTB Redo
op: 0x04 ver: 0x01
op: L itl: xid: 0x0002. 019.00009875 uba: 0x00800be9. 2563.18
flg: C --- lkc: 0 scn: 0x0000. 04025846
Dump kdilk : itl = 2 , kdxlkflg = 0x41 sdc = 0 indexid = 0xc06cdb block = 0x00c09832
(kdxlpu): purge leaf row
key 😦 36 ):
0c 4d 47 4d 54 5f 53 59 53 5f 4a 4f 42 07 78 6f 03 09 15 17 33 07 6d 65 74
72 69 63 73 06 00 c0 66 d0 00 4c

Oracle中的锁
数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生
多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数
据,破坏数据库的一致性。
在数据库中有两种基本的锁类型:排它锁(Exclusive Locks,即X锁)和共享锁(Share Locks,
即S锁)。当数据对象被加上排它锁时,其他的事务不能对它读取和修改;加了共享锁的数
据对象可以被其他事务读取,但不能修改。
根据保护对象的不同,Oracle数据库锁可以分为以下几大类:
(1) DML lock(data locks,数据锁):用于保护数据的完整性;
(2) DDL lock(dictionary locks,字典锁):用于保护数据库对象的结构(例如表、视图、索引
的结构定义);
(3) Internal locks 和latches(内部锁与闩):保护内部数据库结构;
(4) Distributed locks(分布式锁):用于OPS(并行服务器)中;
(5) PCM locks(并行高速缓存管理锁):用于OPS(并行服务器)中。
在Oracle中最主要的锁是DML(也可称为data locks,数据锁)锁。DML锁的目的在于保证
并发情况下的数据完整性。在Oracle数据库中,DML锁主要包括TM锁和TX锁,其中TM
锁称为表级锁,TX锁称为事务锁或行级锁。
意向锁的含义是如果对一个结点加意向锁,则说明该结点的下层结点正在被加锁;对任一结
点加锁时,必须先对它的上层结点加意向锁。如:对表中的任一行加锁时,必须先对它所在
的表加意向锁,然后再对该行加锁。这样一来,事务对表加锁时,就不再需要检查表中每行

记录的锁标志位了,系统效率得以大大提高。

TM锁(表级锁)类型共有 5 种,分别称为共享锁(S锁)、排它锁(X锁)、行级共享锁(RS

锁)、行级排它锁(RX锁)、共享行级排它锁(SRX锁)

当Oracle执行DML语句时,系统自动在所要操作的表上申请TM类型的锁。当TM锁获得
后,系统再自动申请TX类型的锁,并将实际锁定的数据行的锁标志位进行置位。这样在事
务加锁前检查TX锁相容性时就不用再逐行检查锁标志,而只需检查TM锁模式的相容性即
可,大大提高了系统的效率。TM锁包括了SS、SX、S、X等多种模式,在数据库中用 0 - 6
来表示。不同的SQL操作产生不同类型的TM锁。如表 1 所示。

表1 Oracle的TM锁类型



锁描述 解释 SQL操作

0 none

1 NULL 空 Select

2 SS(Row-S)

行级共享锁,其他对象

只能查询这些数据行

Select for update、Lock for update、
Lock row share

3 SX(Row-X) 行级排它锁,在提交前
不允许做DML操作

Insert、Update、Delete、Lock row
share

4 S(Share) 共享锁 Create index、Lock share

5 SSX(S/Row-X) 共享行级排它锁 Lock share row exclusive

6 X(Exclusive) 排它锁
Alter table、Drop able、Drop index、
Truncate table 、Lock exclusive

  1. 共享锁(Share Table Lock,S):
    加锁语法:Lock Table TableName In Share Mode;
    允许的操作:一个共享锁由一个事务控制,仅允许其它事务查询被锁定的表。一个有效的共
    享锁明确地用Select ... For update形式锁定行,或执行Lock Table TableName In Share Mode
    语法锁定整个表,不允许被其它事务更新。
    禁止的操作:一个共享锁由一个事务来控制,防止其它事务更新该表或执行下面的语句:
    LOCK TABLE TableName IN SHARE ROW EXCLUSIVE MODE;
    LOCK TABLE TableName IN ROW EXCLUSIVE MODE;
  2. 排它锁(Exclusive Table Lock,X):

排它锁是在锁机制中限制最多的一种锁类型,允许加排它锁的事务独自控制对表的写权限。

加锁语法:Lock Table TableName In Exclusive Mode;
允许的操作:在一个表中只能有一个事务对该表实行排它锁,排它锁仅允许其它的事务查询
该表。
禁止的操作:拥有排外锁的事务禁止其它事务执行其它任何DML类型的语句或在该表上加
任何其它类型的锁。
定义排它锁的语法:
LOCK TABLE TableName IN EXCLUSIVE MODE;

  1. 行级锁(Row Share Table Lock,RS):
    一个行级锁(有时称为Subshare Table Lock,简称SS,子共享锁)需要该事务在被锁定行的
    表上用update的形式加锁。当有下面语句被执行的时候行级锁自动加在操作的表上。
    SELECT... FROM TableName... FOR UPDATE OF... ;
    LOCK TABLE TableName IN ROW SHARE MODE;
    行级锁(Row Share Table Lock)在锁类型中是限制最少的,也是在表的并发程度中使用程度
    最高的。
    允许的操作:行级共享锁由一个事务控制,允许其它事务查询、插入、更新、删除或同时在
    同一张表上锁定行。因此其它事务可以同时在同一张表上得到行级锁、共享行级排它锁、行
    级排它锁、排它锁。
    禁止的操作:拥有行级锁的事务不允许其它事务执行排它锁,即:
    Lock Table TableName In Exclusive Mode;
  2. 行级排它锁(Row Exclusive Table Lock,RX):
    行级排它锁(亦称为Subexclusive Table Lock,简称SX,子排它锁)通常需要事务拥有的锁在
    表上被更新一行或多行。当有下面语句被执行的时候行级排它锁被加在操作的表上。
    INSERT INTO TableName... ;
    UPDATE TableName... ;
    DELETE FROM TableName... ;
    LOCK TABLE TableName IN ROW EXCLUSIVE MODE;
    行级排它锁比行级锁稍微多一些限制。
    允许的操作:行级排它锁由一个事务拥有允许其它事务执行查询、修改、插入、删除或同时
    在同一张表上锁定行。执有行级排它锁的事务允许其它事务在同一张表上同时得到共享锁和
    行级排它锁。
    禁止的操作:行级排它锁由一个事务拥有防止其它事务手动锁定表来排除其它事务的读写权。
    因此,其它事务不允许在同一张表上使用以下的语句来执行锁事务。
    LOCK TABLE table IN SHARE MODE;
    LOCK TABLE table IN SHARE EXCLUSIVE MODE;
    LOCK TABLE table IN EXCLUSIVE MODE
  3. 共享行级排它锁(Share Row Exclusive Table Lock,SRX):
    共享行级排它锁有时也称共享子排它锁(Share Subexclusive Table Lock,SSX),它比共享锁有
    更多限制。定义共享行级排它锁的语法为:
    Lock Table TableName In Share Row Exclusive Mode;
    允许的操作:仅允许一个事务在某一时刻得到行级排它锁。拥有行级排它锁事务允许其它事
    务在被锁定的表上执行查询或使用Select ... From TableName For update...来准确在锁定行而
    不能更新行。
    禁止的操作:拥有行级排它锁的事务不允许其它事务有除共享锁外的其它形式的锁加在同一

张表上或更新该表。即下面的语句是不被允许的:

LOCK TABLE TableName IN SHARE MODE;
LOCK TABLE TableName IN SHARE ROW EXCLUSIVE MODE;
LOCK TABLE TableName IN ROW EXCLUSIVE MODE;
LOCK TABLE TableName IN EXCLUSIVE MODE;
当Oracle数据库发生TX锁等待时,如果不及时处理常常会引起Oracle数据库挂起,或导致
死锁的发生,产生ORA- 60 的错误。
TX锁等待的分析
Oracle数据库中一般使用行级锁。
当Oracle检测到死锁产生时,中断并回滚死锁相关语句的执行,报ORA- 00060 的错误并记
录在数据库的日志文件alertSID.log中。同时在user_dump_dest下产生了一个跟踪文件,详
细描述死锁的相关信息。
在日常工作中,如果发现在日志文件中记录了ora- 00060 的错误信息,则表明产生了死锁。
这时需要找到对应的跟踪文件,根据跟踪文件的信息定位产生的原因。

表 2 数据字典视图说明

视图名 描述 主要字段说明

v$session

查询会话的信息和

锁的信息。

sid,serial#:表示会话信息。
program:表示会话的应用程序信息。
row_wait_obj#:表示等待的对象,和
dba_objects中的object_id相对应。

v$session_wait 查询等待的会话信
息。

sid:表示持有锁的会话信息。
Seconds_in_wait:表示等待持续的时间信息
Event:表示会话等待的事件。

v$lock

列出系统中的所有

的锁。

Sid:表示持有锁的会话信息。
Type:表示锁的类型。值包括TM和TX等。
ID1:表示锁的对象标识。
lmode,request:表示会话等待的锁模式的信
息。用数字 0 - 6 表示,和表 1 相对应。

dba_locks
对v$lock的格式化
视图。

Session_id:和v$lock中的Sid对应。
Lock_type:和v$lock中的type对应。
Lock_ID1: 和v$lock中的ID1对应。
Mode_held,mode_requested:和v$lock中
的lmode,request相对应。

v$locked_object

只包含DML的锁信

息,包括回滚段和

会话信息。

Xidusn,xidslot,xidsqn:表示回滚段信息。和
v$transaction相关联。
Object_id:表示被锁对象标识。
Session_id:表示持有锁的会话信息。
Locked_mode:表示会话等待的锁模式的信
息,和v$lock中的lmode一致。

解锁及Kill Session:

使用下面的语法查出锁并杀掉Session。
SELECT A.SID,A.SERIAL#,A.USERNAME,B.TYPE FROM V$SESSION A,V$LOCK B WHERE A.SID=B.SID;
ALTER SYSTEM KILL SESSION 'SID,SERIAL#';

Oracle中的死锁

什么是死锁?
所谓死锁,是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的
现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,
这些永远在互相等待的进程称为死锁进程。
死锁的trace文件
Oracle中产生死锁的时候会在alert告警日志文件中记录死锁的相关信息,无论单机还是RAC
环境都有Deadlock这个关键词,而且当发生死锁时都会生成一个trace文件,这个文件名在
alert文件中都有记载。由于在RAC环境中,是由LMD(Lock Manager Daemon)进程统一管
理各个节点之间的锁资源的,所以,RAC环境中trace文件是由LMD进程来生成的。
在RAC环境中,告警日志的形式如下所示:

Mon Jun 20 10:10:56 2016
Global Enqueue Services Deadlock detected. More info in file
/u01/app/oracle/diag/rdbms/raclhr/raclhr2/trace/raclhr2_lmd0_19923170.trc.

在单机环境中,告警日志的形式如下所示:

Mon Jun 20 12:10:56 2016
ORA-00060: Deadlock detected. More info in file
/u01/app/oracle/diag/rdbms/dlhr/dlhr/trace/dlhr_ora_16973880.trc.

通常来讲,对于单机环境,当有死锁发生后,在trace文件中会看到如下的日志信息:

R1 R2

S1 S2

R1 R2

R2 R1

死锁有两个条件:

1.资源的竞争

2.资源的无序访问

解决死锁的方法:

1.充足的资源

2.资源的有序访问

6.1. 2 Mysql 存储结构的基本原理

结构图

之前的文章中我们有说过,MySQL的InnoDB存储引擎以Data Page(数据页)作为磁盘和内
存之间交互的基本单位,他的大小一般为默认值16K。其实,InnoDB中还有不同类型的页,
之前MySQL各种“Buffer”之InnoDB Buffer Pool我们讲过,从数据页的作用来分,可以分为
Free Page(空闲页)、Clean Page(干净页)、Dirty Page(脏页);从类型来分的话,还可以分
成存放UNDO日志的页、存放INODE信息的页、存放表空间头部信息的页等。今天我们就
来揭开数据页的结构面纱。本文摘录自:
小孩子 4919 《MySQL是怎样运行的:从根儿上理解MySQL》
数据页结构概览
数据页结构
数据页这16KB的空间是由多个部分组成的,每个部分有着不同的功能。

从图中可以看出,一个InnoDB数据页的存储空间大致被划分成了 7 个部分,有的部分占用
的字节数是确定的,有的部分占用的字节数是不确定的。下边我们来看一下这 7 个部分都存
储了什么:

名称 中文名 大小(单位:B) 描述

File
Header 文件头部^38 页的一些通用信息^

Page
Header

页面头部 56 数据页专有的一些信息

Infimum +
Supermum

最小记录和最大记录 26 两个虚拟的行记录

User
Records

用户真实记录 不确定 实际存储的行记录内容

Free Space 空闲空间 不确定 页中尚未使用的空间

Page
Directory

页面目录 不确定 页中的某些记录的相对位置

File Trailer 文件尾部 8 校验页是否完整

用户真实记录在数据页中的存储(Free Space)
在页的 7 个组成部分中,我们自己存储的记录会按照我们指定的行格式(MySQL之InnoDB
记录结构)存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records
这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中
申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User

Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需
要去申请新的页了。这个过程的图示如下:

Infimum+Supremum & User Records(记录)
记录头信息引出的数据页“记录”结构
MySQL之InnoDB记录结构文章中,我们提到了记录头信息,下面在来一起回顾一下:

记录头信息中各个属性的含义再贴一下(目前使用Compact行格式):

名称

大小

(单

位:

bit)

描述

预留位 1 1 没有使用

预留位 2 1 没有使用

delete_mask 1 标记该记录是否被删除

min_rec_mask 1 B+树的每层叶子节点中的最小记录都会添加该记标记

n_owned 4 表示当前记录有用的记录数

heap_no 13 表示当前记录在记录堆的位置信息

record_type 3

表示当前记录的类型, 0 表示普通记录, 1 表示B+树非叶子节点记录,

2 表示最小记录, 3 表示最大记录

next_record 16 表示下一条记录的相对位置

下面就来看看记录头信息中各个属性的都是干啥的:
delete_mask:

这个属性标记着当前记录是否被删除,占用 1 个二进制位,值为 0 的时候代表记录并没有被

删除,为 1 的时候代表记录被删除掉了。

被删除的记录不立即从磁盘上移除,因为移除它们之后把其他的记录在磁盘上重新排列需要

性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链

表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表

中的话,可能把这些被删除的记录占用的存储空间覆盖掉。这个delete_mask位设置为 1 和
将被删除的记录加入到垃圾链表中是两个阶段。
min_rec_mask:
B+树的每层非叶子节点中的最小记录都会添加该标记。值为 1 ,表示该条记录是B+树的非
叶子节点中的最小记录;值为 0 ,意味着该条数据不是B+树的非叶子节点中的最小记录。
n_owned:
表示当前记录拥有的记录数,一会我们再详细介绍。
heap_no:
这个属性表示当前记录在本页中的位置。MySQL 自动给每个页里边儿加了两个记录,由于
这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录。这两个伪记录
一个代表最小记录,一个代表最大记录。
小提示记录也可以比大小,对于一条完整的记录来说,比较记录的大小就是比较主键的大小。
但是不管我们向页中插入了多少自己的记录,InnoDB 规定他们定义的两条伪记录分别为最
小记录与最大记录。这两条记录的构造十分简单,都是由 5 字节大小的记录头信息和 8 字节
大小的一个固定的部分组成的。
由于这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records部分,他
们被单独放在上文提到的Infimum + Supremum的部分,为了大家方便理解,我们创建张表、
插几条数据、画个图看一下:
CREATE TABLE page_demo( c1 INT, c2 INT, c3 VARCHAR(10000), PRIMARY KEY (c1)) CHARSET=as
cii ROW_FORMAT=Compact;INSERT INTO page_demo VALUES(1, 100, 'aaaa');INSERT INTO page_d
emo VALUES(2, 200, 'bbbb');INSERT INTO page_demo VALUES(3, 300, 'cccc');INSERT INTO page_d
emo VALUES(4, 400, 'dddd');

图中,其他信息没有画出但不代表它们不存在,只是为了大家方便理解,做了简化。最小记

录和最大记录的heap_no值分别是 0 和 1 ,也就是说它们的位置最靠前。
record_type:
这个属性表示当前记录的类型,一共有 4 种类型的记录, 0 表示普通记录, 1 表示B+树非叶
节点记录, 2 表示最小记录, 3 表示最大记录。从图中我们也可以看出来,我们自己插入的
记录就是普通记录,它们的record_type值都是 0 ,而最小记录和最大记录的record_type值
分别为 2 和 3 。至于record_type为 1 的情况,我们之后在说索引的时候会重点强调的。
next_record:

这个信息非常重要,表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。比

方说第一条记录的next_record值为 32 ,意味着从第一条记录的真实数据的地址处向后找 32
个字节便是下一条记录的真实数据。如果你熟悉数据结构的话,就立即明白了,这其实是个
链表,可以通过一条记录找到它的下一条记录。但是需要注意注意再注意的一点是,下一条
记录指的并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条
记录。而且规定Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的
用户记录,而本页中主键值最大的用户记录的下一条记录就是Supremum记录(也就是最大
记录) ,为了更形象的表示一下这个 next_record起到的作用,我们用箭头来替代一下
next_record中的地址偏移量:

从图中可以看出来,我们的记录按照主键从小到大的顺序形成了一个单链表。最大记录的

next_record的值为 0 ,这也就是说最大记录是没有下一条记录了,它是这个单链表中的最后
一个节点。如果从中删除掉一条记录,这个链表也是会跟着变化的,比如我们把第 2 条记录
删掉:
DELETE FROM page_demo WHERE c1 = 2;
删掉第 2 条记录后的示意图就是:

从图中可以看出来,删除第 2 条记录前后主要发生了这些变化:

第 2 条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为 1 。
第 2 条记录的next_record值变为了 0 ,意味着该记录没有下一条记录了。
第 1 条记录的next_record指向了第 3 条记录。
最大记录的n_owned值从 5 变成了 4 ,关于这一点的变化我们稍后会详细说明的。
所以,不论我们怎么对页中的记录做增删改操作,InnoDB 始终会维护一条记录的单链表,
链表中的各个节点是按照主键值由小到大的顺序连接起来的。
小提示
会不会觉得next_record这个指针有点儿怪,为啥要指向记录头信息和真实数据之间的位置
呢?为啥不干脆指向整条记录的开头位置,也就是记录的额外信息开头的位置呢?
因为这个位置刚刚好,向左读取就是记录头信息,向右读取就是真实数据。MySQL之InnoDB

记录结构我们还说过变长字段长度列表、NULL值列表中的信息都是逆序存放,这样可以使

记录中位置靠前的字段和它们对应的字段长度信息在内存中的距离更近,可能会提高高速缓

存的命中率。再来看一个有意思的事情,因为主键值为 2 的记录被我们删掉了,但是存储空

间却没有回收,如果我们再次把这条记录插入到表中,会发生什么事呢?

INSERT INTO page_demo VALUES(2, 200, 'bbbb');
我们看一下记录的存储情况:

从图中可以看到,InnoDB 并没有因为新记录的插入而为它申请新的存储空间,而是直接复
用了原来被删除记录的存储空间。
小提示 1 、当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这
些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。上面删除了一行记录,
又将记录原封不动插回来的情况,原来的存储空间是会被重用的。 2 、还有一种情况是不会
被重用的:删除原记录后,新插入的记录真实数据所占存储空间大于原先记录存储空间的时
候,这时原空间不会被重用且被加入垃圾链表,新插入的记录会从Free Space申请新的空间,
和已有的记录组合成新的链表。
Page Directory(页目录)
现在我们了解了记录在页中按照主键值由小到大顺序串联成一个单链表,那如果我们想根据
主键值查找页中的某条记录该咋办呢?比如说这样的查询语句:
SELECT * FROM page_demo WHERE c1 = 3;
最笨的办法:从Infimum记录(最小记录)开始,沿着链表一直往后找,总会找到。在找的
时候还能投机取巧,因为链表中各个记录的值是按照从小到大顺序排列的,所以当链表的某
个节点代表的记录的主键值大于你想要查找的主键值时,你就可以停止查找了,因为该节点
后边的节点的主键值依次递增。
但是InnoDB能用这么笨的办法么,当然是要设计一种更快的查找方式,于是乎从书的目录
中找到了灵感。我们平常想从一本书中查找某个内容的时候,一般会先看目录,找到需要查
找的内容对应的书的页码,然后到对应的页码查看内容。InnoDB为我们的记录也制作了一
个类似的目录,他们的制作过程是这样的: 1 、将所有正常的记录(包括最大和最小记录,
不包括标记为已删除的记录)划分为几个组。 2 、每个组的最后一条记录(也就是组内最大
的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有
几条记录。 3 、将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的
尾部的地方,这个地方就是所谓的Page Directory,也就是页目录。页面目录中的这些地址
偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。

从这个图中我们需要注意这么几点:

现在页目录部分中有两个槽,也就意味着我们的记录被分成了两个组,槽 1 中的值是 112 ,

代表最大记录的地址偏移量(就是从页面的 0 字节开始数,数 112 个字节);槽 0 中的值是

99 ,代表最小记录的地址偏移量。

注意最小和最大记录的头信息中的n_owned属性
1 、最小记录的n_owned值为 1 ,这就代表着以最小记录结尾的这个分组中只有 1 条记录,
也就是最小记录本身。
2 、最大记录的n_owned值为 5 ,这就代表着以最大记录结尾的这个分组中只有 5 条记录,
包括最大记录本身还有我们自己插入的 4 条记录。
99 和 112 这样的地址偏移量很不直观,我们用箭头指向的方式替代数字,这样更易于我们
理解,所以修改后的示意图就是这样:

暂时不管各条记录在存储设备上的排列方式了,单纯从逻辑上看一下这些记录和页目录的关

系:

InnoDB对每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,
最大记录所在的分组拥有的记录条数只能在1~8条之间,剩下的分组中记录的条数范围只能
在是4~8条之间。所以分组是按照下边的步骤进行的:
初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,
然后把该槽对应的记录的n_owned值加 1 ,表示本组内又添加了一条记录,直到该组中的
记录数等于 8 个。
在一个组中的记录数等于 8 个后再插入一条记录时,会将组中的记录拆分成两个组,一个组
中 4 条记录,另一个 5 条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最
大的那条记录的偏移量。
由于现在page_demo表中的记录太少,无法演示添加了页目录之后加快查找速度的过程,
所以再往page_demo表中添加一些记录:
INSERT INTO page_demo VALUES(5, 500, 'eeee');INSERT INTO page_demo VALUES(6, 600, 'ffff');I
NSERT INTO page_demo VALUES(7, 700, 'gggg');INSERT INTO page_demo VALUES(8, 800, 'hhhh');I
NSERT INTO page_demo VALUES(9, 900, 'iiii');INSERT INTO page_demo VALUES(10, 1000, 'jjjj');INS
ERT INTO page_demo VALUES(11, 1100, 'kkkk');INSERT INTO page_demo VALUES(12, 1200, 'llll');I
NSERT INTO page_demo VALUES(13, 1300, 'mmmm');INSERT INTO page_demo VALUES(14, 1400,
'nnnn');INSERT INTO page_demo VALUES(15, 1500, 'oooo');INSERT INTO page_demo VALUES(16,
1600, 'pppp');
现在页里边就一共有 18 条记录了(包括最小和最大记录),这些记录被分成了 5 个组,如图
所示:

因为把 16 条记录的全部信息都画在一张图里太占地方,让人眼花缭乱的,所以只保留了用

户记录头信息中的n_owned和next_record属性,也省略了各个记录之间的箭头,我没画不

等于没有啊!现在看怎么从这个页目录中查找记录。因为各个槽代表的记录的主键值都是从

小到大排序的,所以我们可以使用所谓的二分法来进行快速查找。 5 个槽的编号分别是: 0 、

1 、 2 、 3 、 4 ,所以初始情况下最低的槽就是low=0,最高的槽就是high=4。比方说我们想找
主键值为 6 的记录,过程是这样的: 1 、计算中间槽的位置:(0+4)/2=2
,所以查看槽 2
对应记录的主键值为 8
,又因为8 > 6
,所以设置high=2
,low
保持不变。 2 、重新计算中间槽的位置:(0+2)/2=1
,所以查看槽 1
对应的主键值为 4
,又因为4 < 6
,所以设置low=1
,high
保持不变。
3 、因为high - low
的值为 1 ,所以确定主键值为 6
的记录在槽 2
对应的组中。此刻我们需要找到槽 2
中主键值最小的那条记录,然后沿着单向链表遍历槽 2
中的记录。但是我们前边又说过,每个槽对应的记录都是该组中主键值最大的记录,这里槽
2
对应的记录是主键值为 8
的记录,怎么定位一个组中最小的记录呢?别忘了各个槽都是挨着的,我们可以很轻易的拿
到槽 1
对应的记录(主键值为 4
),该条记录的下一条记录就是槽 2
中主键值最小的记录,该记录的主键值为 5
。所以我们可以从这条主键值为 5
的记录出发,遍历槽 2
中的各条记录,直到找到主键值为 6
的那条记录即可。由于一个组中包含的记录条数只能是1~8条,所以遍历一个组中的记录的
代价是很小的。
※ 所以在一个数据页中查找指定主键值的记录的过程分为两步:
1 、通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
2 、通过记录的next_record属性遍历该槽所在的组中的各个记录。
Page Header(页面头部)
InnoDB 为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记
录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫
Page Header的部分,它是页结构的第二部分,这个部分占用固定的 56 个字节,专门存储各
种状态信息,具体各个字节的含义看下表:

名称 大小(单位:B) 描述

PAGE_N_DIR_SLOTS 2 页目录的插槽数

PAGE_HEAP_TOP 2

还未使用的空间最小地址,也就是说从该地址之后就

是Free Space

PAGE_N_HEAP 2

本页中的记录的数量(包括最小和最大记录以及标记

为删除的记录)

PAGE_FREE 2

第一个已经标记为删除的记录地址(各个已删除的记

录通过next_record也会组成一个单链表,这个单链表
中的记录可以被重新利用)

PAGE_GARBAGE 2 已删除记录占用的字节数

PAGE_LAST_INSERT 2 最后插入记录的位置

PAGE_DIRECTION 2 记录插入的方向

PAGE_N_DIRECTION 2 一个方向连续插入的记录数量

PAGE_N_RECS 2 该页中记录的数量(不包括最小和最大记录以及被标

记为删除的记录)

PAGE_MAX_TRX_ID 8 修改当前页的最大事务ID,该值仅在二级索引中定义

PAGE_LEVEL 2 当前页在B+树中所处的层级

PAGE_INDEX_ID 8 索引ID,表示当前页属于哪个索引

PAGE_BTR_SEG_LEAF 10 B+树叶子段的头部信息,仅在B+树的Root页定义

PAGE_BTR_SEG_TOP 10 B+树非叶子段的头部信息,仅在B+树的Root页定义

通过前面文章的介绍,从PAGE_N_DIR_SLOTS到PAGE_LAST_INSERT以及PAGE_N_RECS的意
思大家一定是清楚的。剩下的状态信息不要着急。我们先来看一下 PAGE_DIRECTION 和
PAGE_N_DIRECTION的意思:
PAGE_DIRECTION
假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右
边,反之则是左边。用来表示最后一条记录插入方向的状态就是PAGE_DIRECTION

PAGE_N_DIRECTION

假设连续几次插入新记录的方向都是一致的,InnoDB
会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION
这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重
新统计。至于我们没提到的那些属性,我没说是因为现在不需要大家知道。不要着急,当我
们学完了后边的内容,你再回头看,一切都是那么清晰。
File Header(文件头部)
Page Header是专门针对数据页记录的各种状态信息,比方说页里头有多少个记录、有多少
个槽。我们现在描述的File Header针对各种类型的页都通用,也就是说不同类型的页都会

以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说
这个页的编号是多少,它的上一个页、下一个页是谁...这个部分占用固定的 38 个字节,是
由下边这些内容组成的:

名称

大小

(单

位:

B)

描述

FIL_PAGE_SPACE_OR_CHKSUM 4 页的校验和(checksum值)

FIL_PAGE_OFFSET 4 页号

FIL_PAGE_PREV 4 上一个页的页号

FIL_PAGE_NEXT 4 下一个页的页号

FIL_PAGE_LSN 8

页面被最后修改时对应的日志序列位置(英文

名是:Log Sequence Number)

FIL_PAGE_TYPE 2 该页的类型

FIL_PAGE_FILE_FLUSH_LSN 8

仅在系统表空间的一个页中定义,代表文件至

少被刷新到了对应的LSN值

FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 页属于哪个表空间

对照着这个表格,我们看几个目前比较重要的部分:

FIL_PAGE_SPACE_OR_CHKSUM

这个代表当前页面的校验和(checksum)。啥是个校验和?就是对于一个很长很长的字节串
来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的
值就称为校验和
。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样
两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。
FIL_PAGE_OFFSET
每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB 通过页号来可以唯一定
位一个页。
FIL_PAGE_TYPE
这个代表当前页的类型,我们前边说过,InnoDB 为了不同的目的而把页分为不同的类型,
我们上边介绍的其实都是存储记录的数据页,其实还有很多别的类型的页,具体如下表:

类型名称 十六进

描述

FIL_PAGE_TYPE_ALLOCATED 0x0000 最新分配,还没使用

FIL_PAGE_UNDO_LOG 0x0002 Undo日志页

FIL_PAGE_INODE 0x0003 段信息节点

FIL_PAGE_IBUF_FREE_LIST 0x0004 Insert Buffer空闲列表

FIL_PAGE_IBUF_BITMAP 0x0005 Insert Buffer位图

FIL_PAGE_TYPE_SYS 0x0006 系统页

FIL_PAGE_TYPE_TRX_SYS 0x0007 事务系统数据

FIL_PAGE_TYPE_FSP_HDR 0x0008 表空间头部信息

FIL_PAGE_TYPE_XDES 0x0009 扩展描述页

FIL_PAGE_TYPE_BLOB 0x000A 溢出页

FIL_PAGE_INDEX 0x45BF 索引页,也就是我们所说的数据页

FIL_PAGE_PREV和FIL_PAGE_NEXT
我们前边强调过,InnoDB 都是以页为单位存放数据的,有时候我们存放某种类型的数据占
用的空间非常大(比方说一张表中可以有成千上万条记录),InnoDB可能不可以一次性为这
么多数据分配一个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页
关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。
这样通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连
着。需要注意的是,并不是所有类型的页都有上一个和下一个页的属性,不过我们本集中唠
叨的数据页(也就是类型为FIL_PAGE_INDEX的页)是有这两个属性的,所以所有的数据页
其实是一个双链表,就像这样:

File Trailer(文件尾部)
InnoDB 存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载
到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据
同步到磁盘中。但是在同步了一半的时候中断电了咋办,这不是莫名尴尬么?为了检测一个
页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),InnoDB在每个页的
尾部都加了一个File Trailer部分,这个部分由 8 个字节组成,可以分成 2 个小部分:
前 4 个字节代表页的校验和
这个部分是和File Header
中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,
因为File Header
在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾
部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,
那么在File Header

中的校验和就代表着已经修改过的页,而在File Trailer
中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
后 4 个字节代表页面被最后修改时对应的日志序列位置(LSN)
这个部分也是为了校验页的完整性的,只不过我们目前还没说LSN
是个什么意思,所以大家可以先不用管这个属性。这个File Trailer与File Header类似,都是
所有类型的页通用的。

本章较上一章记录结构知识点更多,原书作者画了很多图,看完后,我是一脸懵。所以我整

理了一下,按照我理解的方式把知识点串成一张结构图,比较重要的点都在图中标记出来了,

详细释义,还请大家看文章讲解。虽然这两章内容理论知识偏多,但是为我们后面理解索引

原理打下坚实基础,所以大家一定要理解文中重要的知识点。站在巨人的肩膀上,每天进步

一点点。

什么是MVCC

MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,
Lock-Based Concurrency Control)是一种基于多版本的并发控制协议,只有在InnoDB引擎下存
在。MVCC是为了实现事务的隔离性,通过版本号,避免同一数据在不同事务间的竞争,你
可以把它当成基于多版本号的一种乐观锁。当然,这种乐观锁只在事务级别提交读和可重复
读有效。MVCC 最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的
OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。

MVCC的实现机制
InnoDB 在每行数据都增加三个隐藏字段,一个唯一行号,一个记录创建的版本号,一个记
录回滚的版本号。

在多版本并发控制中,为了保证数据操作在多线程过程中,保证事务隔离的机制,降低锁竞

争的压力,保证较高的并发量。在每开启一个事务时,会生成一个事务的版本号,被操作的

数据会生成一条新的数据行(临时),但是在提交前对其他事务是不可见的,对于数据的更

新(包括增删改)操作成功,会将这个版本号更新到数据的行中,事务提交成功,将新的版

本号更新到此数据行中,这样保证了每个事务操作的数据,都是互不影响的,也不存在锁的

问题。

mysql加锁:

MySQL加锁处理分析--何登成
时间:2013- 12 - 14 14:09来源:何登成的技术博客 编辑:何登成的技术博客 点击: 次
背景 MySQL/InnoDB的加锁分析,一直是一个比较困难的话题。我在工作过程中,经常会有
同事咨询这方面的问题。同时,微博上也经常会收到MySQL锁相关的私信,让我帮助解决
一些死锁的问
背景
MySQL/InnoDB的加锁分析,一直是一个比较困难的话题。我在工作过程中,经常会有同事
咨询这方面的问题。同时,微博上也经常会收到MySQL锁相关的私信,让我帮助解决一些
死锁的问题。本文,准备就MySQL/InnoDB的加锁问题,展开较为深入的分析与讨论,主要
是介绍一种思路,运用此思路,拿到任何一条SQL语句,都能完整的分析出这条语句会加
什么锁?会有什么样的使用风险?甚至是分析线上的一个死锁场景,了解死锁产生的原因。
注:MySQL是一个支持插件式存储引擎的数据库系统。本文下面的所有介绍,都是基于
InnoDB存储引擎,其他引擎的表现,会有较大的区别。
MVCC:Snapshot Read vs Current Read
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version
Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency
Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多些少的
OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现
阶段,几乎所有的RDBMS,都支持了MVCC。
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记

录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条

记录。

在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以

MySQL InnoDB为例:

快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
select * from table where ?;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where? lock in share mode; S
select * from table where? for update;
insert into table values (...);
update table set? where ?;
delete from table where ?;

所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他
并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S
锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

为什么将插入/更新/删除操作,都归为当前读?可以看看下面这个更新操作,在数据库中的
执行流程:

从图中,可以看到,一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL

Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录
返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个
Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件
的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert
操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会
进行一个当前读。对unique key和delete mark 的记示加S锁。

注:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一
条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给
MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕。

Cluster Index:聚簇索引
InnoDB存储引擎的数据组织方式,是聚簇索引表:完整的记录,存储在主键索引中,通过
主键索引,就可以获取记录所有的列。关于聚簇索引表的组织方式,可以参考MySQL的官
方文档:Clustered and Secondary Indexes 。本文假设读者对这个,已经有了一定的认识,就
不再做具体的介绍。接下来的部分,主键索引/聚簇索引两个名称,会有一些混用,望读者
知晓。

2PL:Two-Phase Locking
传统RDBMS加锁的一个原则,就是2PL (二阶段锁):Two-Phase Locking。相对而言,2PL比
较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁
阶段不相交。下面,仍旧以MySQL为例,来简单看看2PL在MySQL中的实现。

从上图可以看出,2PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,

不放锁。解锁阶段:只放锁,不加锁。

Isolation Level
隔离级别:Isolation Level,也是RDBMS的一个关键特性。相信对数据库有所了解的朋友,
对于 4 种隔离级别:Read Uncommited,Read Committed,Repeatable Read,Serializable,都
有了深入的认识。本文不打算讨论数据库理论中,是如何定义这 4 种隔离级别的含义的,而
是跟大家介绍一下MySQL/InnoDB是如何定义这 4 种隔离级别的。

MySQL/InnoDB定义的 4 种隔离级别:
Read Uncommited
可以读取未提交记录。此隔离级别,不会使用,忽略。

Read Committed (RC)
快照读忽略,本文不考虑。
针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。

Repeatable Read (RR)
快照读忽略,本文不考虑。
针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,

新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

Serializable
从MVCC并发控制退化为基于锁的并发控制。部分快照读与当前读,所有的读操作均为当前
读,读加读锁 (S锁),写加写锁 (X锁)。
Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。

一条简单SQL的加锁实现分析
在介绍完一些背景知识之后,本文接下来将选择几个有代表性的例子,来详细分析MySQL
的加锁处理。当然,还是从最简单的例子说起。经常有朋友发给我一个SQL,然后问我,这
个SQL加什么锁?就如同下面两条简单的SQL,他们加什么锁?
SQL1:select * from t1 where id = 10;
SQL2:delete from t1 where id = 10;

针对这个问题,该怎么回答?我能想象到的一个答案是:
SQL1:不加锁。因为MySQL是使用多版本并发控制的,读不加锁。
SQL2:对id = 10的记录加写锁 (走主键索引)。

这个答案对吗?说不上来。即可能是正确的,也有可能是错误的,已知条件不足,这个问题
没有答案。如果让我来回答这个问题,我必须还要知道以下的一些前提,前提不同,我能给
出的答案也就不同。要回答这个问题,还缺少哪些前提条件?
前提一:id列是不是主键?
前提二:当前系统的隔离级别是什么?
前提三:id列如果不是主键,那么id列上有索引吗?
前提四:id列上如果有二级索引,那么这个索引是唯一索引吗?
前提五:两个SQL的执行计划是什么?索引扫描?全表扫描?

没有这些前提,直接就给定一条SQL,然后问这个SQL会加什么锁,都是很业余的表现。而
当这些问题有了明确的答案之后,给定的SQL会加什么锁,也就一目了然。下面,我将这
些问题的答案进行组合,然后按照从易到难的顺序,逐个分析每种组合下,对应的SQL会
加哪些锁?

注:下面的这些组合,我做了一个前提假设,也就是有索引时,执行计划一定会选择使用索
引进行过滤 (索引扫描)。但实际情况会复杂很多,真正的执行计划,还是需要根据MySQL
输出的为准。
组合一:id列是主键,RC隔离级别
组合二:id列是二级唯一索引,RC隔离级别
组合三:id列是二级非唯一索引,RC隔离级别
组合四:id列上没有索引,RC隔离级别
组合五:id列是主键,RR隔离级别
组合六:id列是二级唯一索引,RR隔离级别
组合七:id列是二级非唯一索引,RR隔离级别
组合八:id列上没有索引,RR隔离级别
组合九:Serializable隔离级别

排列组合还没有列举完全,但是看起来,已经很多了。真的有必要这么复杂吗?事实上,要

分析加锁,就是需要这么复杂。但是从另一个角度来说,只要你选定了一种组合,SQL需要

加哪些锁,其实也就确定了。接下来,就让我们来逐个分析这 9 种组合下的SQL加锁策略。

注:在前面八种组合下,也就是RC,RR隔离级别下,SQL1:select操作均不加锁,采用的
是快照读,因此在下面的讨论中就忽略了,主要讨论SQL2:delete操作的加锁。

组合一:id主键+RC
这个组合,是最简单,最容易分析的组合。id是主键,Read Committed隔离级别,给定SQL:
delete from t1 where id = 10; 只需要将主键上,id = 10的记录加上X锁即可。如下图所示:

结论:id是主键时,此SQL只需要在id=10这条记录上加X锁即可。

组合二:id唯一索引+RC
这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete
from t1 where id = 10; 需要加什么锁呢?见下图:

此组合中,id是unique索引,而主键是name列。此时,加锁的情况由于组合一有所不同。
由于id是unique索引,因此delete语句会选择走id列的索引进行where条件的过滤,在找
到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取
到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name = ‘d’ 对应的主键索引项
加X锁。为什么聚簇索引上的记录也要加锁?试想一下,如果并发的一个SQL,是通过主键
索引来更新:update t1 set id = 100 where name = ‘d’; 此时,如果delete语句没有将主键索引
上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的
更新/删除需要串行执行的约束。

结论:若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id
unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name='d',id=10]的记录。

组合三:id非唯一索引+RC
相对于组合一、二,组合三又发生了变化,隔离级别仍旧是RC不变,但是id列上的约束又
降低了,id列不再唯一,只有一个普通的索引。假设delete from t1 where id = 10; 语句,仍
旧选择id列上的索引进行过滤where条件,那么此时会持有哪些锁?同样见下图:

根据此图,可以看到,首先,id列索引上,满足id = 10查询条件的记录,均已加锁。同时,
这些记录对应的主键索引上的记录也都加上了锁。与组合二唯一的区别在于,组合二最多只
有一个满足等值查询的记录,而组合三会将所有满足查询条件的记录都加锁。

结论:若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。
同时,这些记录在主键索引上的记录,也会被加锁。

组合四:id无索引+RC
相对于前面三个组合,这是一个比较特殊的情况。id列上没有索引,where id = 10;这个过滤
条件,没法通过索引进行过滤,那么只能走全表扫描做过滤。对应于这个组合,SQL会加什
么锁?或者是换句话说,全表扫描时,会加什么锁?这个答案也有很多:有人说会在表上加
X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。那么实际情况呢?请看
下图:

由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除
条件的记录有两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条
件,全部被加上X锁。既不是加表锁,也不是在满足条件的记录上加行锁。

有人可能会问?为什么不是只在满足条件的记录上加锁呢?这是由于MySQL的实现决定
的。如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,
然后由MySQL Server层进行过滤。因此也就把所有的记录,都锁上了。

注:在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不满足后,会
调用unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最
后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。
这一条仅在Update语句中,半一致性读才会现现

结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server
层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,
MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的
记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL
的约束。
原文:http://hedengcheng.com/?p=771

上面的四个组合,都是在Read Committed隔离级别下的加锁行为,接下来的四个组合,是
在Repeatable Read隔离级别下的加锁行为。
组合五,id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条
SQL,加锁与组合一:[id主键,Read Committed]一致。
组合六:id唯一索引+RR
与组合五类似,组合六的加锁,与组合二:[id唯一索引,Read Committed]一致。两个X锁,
id唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。
组合七:id非唯一索引+RR
还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别允许幻读,而RR隔离级
别,不允许存在幻读。但是在组合五、组合六中,加锁行为又是与RC下的加锁行为完全一
致。那么RR隔离级别下,如何防止幻读呢?问题的答案,就在组合七中揭晓。
组合七,Repeatable Read隔离级别,id上有一个非唯一索引,执行delete from t1 where id =
10; 假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?同样看下面这
幅图:

图,相对于组合三:[id列上非唯一锁,Read Committed]看似相同,其实却有很大的区别。
最大的区别在于,这幅图中多了一个GAP锁,而且GAP锁看起来也不是加在记录上的,倒
像是加载两条记录之间的位置,GAP锁有何用?
其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键。
确实,GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。所谓幻读,就是同
一个事务,连续做两次当前读 (例如:select * from t1 where id = 10 for update;),那么这两次

当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),第二次的当前读,不

会比第一次返回更多的记录 (幻象)。

如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他

的事务不会插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生。

如图中所示,有哪些位置可以插入新的满足条件的项 (id = 10),考虑到B+树索引的有序性,
满足条件的项一定是连续存放的。记录[6,c]之前,不会插入id=10的记录;[6,c]与[10,b]间可
以插入[10, aa];[10,b]与[10,d]间,可以插入新的[10,bb],[10,c]等;[10,d]与[11,f]间可以插入满
足条件的[10,e],[10,z]等;而[11,f]之后也不会插入满足条件的记录。因此,为了保证[6,c]与[10,b]
间,[10,b]与[10,d]间,[10,d]与[11,f]不会插入新的满足条件的记录,MySQL选择了用GAP锁,
将这三个GAP给锁起来。
Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP
是否已经被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅
将满足条件的记录锁上 (X锁),与组合三类似。同时还是增加 3 把GAP锁,将可能插入满
足条件记录的 3 个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了
同一事务的第二次当前读,出现幻象的情况。
有心的朋友看到这儿,可以会问:既然防止幻读,需要靠GAP锁的保护,为什么组合五、
组合六,也是RR隔离级别,去不需要加GAP锁呢?
首先,这是一个好问题。其次,回答这个问题,也很简单。GAP锁的目的,是为了防止同一
事务的两次当前读,出现幻读的情况。而组合五,id是主键;组合六,id是unique键,都
能够保证唯一性。一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定
不会在新插入进来,因此也就避免了GAP锁的使用。其实,针对此问题,还有一个更深入
的问题:如果组合五、组合六下,针对SQL:select * from t1 where id = 10 for update; 第一
次查询,没有找到满足查询条件的记录,那么GAP锁是否还能够省略?此问题留给大家思
考。GAP 锁不可以省略,它会锁住 10 前一条记录和后一条记录之间的间隙。
结论:Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where
id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP
上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。
直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP
锁,最后返回结束。

组合八:id无索引+RR
组合八,Repeatable Read隔离级别下的最后一种情况,id列上没有索引。此时SQL:delete from
t1 where id = 10; 没有其他的路径可以选择,只能进行全表扫描。最终的加锁情况,如下图
所示:

图,这是一个很恐怖的现象。首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇

索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有 6 条记录,一共

需要 6 个记录锁, 7 个GAP锁。试想,如果表上有 1000 万条记录呢?

在这种情况下,这个表上,除了不加锁的快照读,其他任何加锁的并发SQL,均不能执行,

不能更新,不能删除,不能插入,全表被锁死。

当然,跟组合四:[id无索引, Read Committed]类似,这个情况下,MySQL也做了一些优化,
就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件
的记录,MySQL会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有
的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read
committed隔离级别;要么是Repeatable Read隔离级别,但是设置
了 innodb_locks_unsafe_for_binlog 参数。更详细的关于semi-consistent read的介绍,可参考
我之前的一篇博客:MySQL+InnoDB semi-consitent read原理及实现分析。

MySQL+InnoDB semi-consitent read原理及实现分析
semi-consistent简介 1
semi-consistent实现 2
MySQL Server层 2
InnoDB Engine层 2
是否采用semi-consistent read 2

Fetch Next特殊处理逻辑 3
优化:Unlock unmatched row 4
semi-consistent优缺点分析 4
优点 4
缺点 5
测试用例 5
构造semi-consistent read 5
构造unlock unmatched row 5

semi-consistent简介
对于熟悉MySQL,或者是看过InnoDB源码的朋友们来说,可能会听说过一个新鲜的名词:
semi-consistent read 。何谓semi-consistent read?以下一段文字,摘于semi-consistent read
一文:

A type of read operation used for UPDATE statements, that is a combination of read
committed and consistent read. When an UPDATE statement examines a row that is already
locked, InnoDB returns the latest committed version to MySQL so that MySQL can determine
whether the row matches the WHERE condition of the UPDATE. If the row matches (must be
updated), MySQL reads the row again, and this time InnoDB either locks it or waits for a lock on it.
This type of read operation can only happen when the transaction has the read
committed isolation level, or when the innodb_locks_unsafe_for_binlog option is enabled.

简单来说,semi-consistent read是read committed与consistent read两者的结合。一个update
语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上
层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一
次读操作,此时会读取行的最新版本(并加锁)。
semi-consistent read 只 会 发 生 在 read committed 隔 离 级 别 下 , 或 者 是 参 数
innodb_locks_unsafe_for_binlog被设置为true。

MySQL server与InnoDB引擎是如何进行交互?InnoDB引擎如何实现semi-consistent read?
请见下面的详细分析。
semi-consistent实现
MySQL Server层
从上面的描述中可以看出,semi-consistent read仅仅针对于update操作,因此在sql_update.cc
的mysql_update方法中,有如下调用:
sql_update.cc::mysql_update()
// 通知底层引擎,尝试进行semi consistent read
// 是否真正进行semi consistent read,由底层引擎决定
table->file->try_semi_consistent_read(1);
// InnoDB引擎决定当前update是否可以进行semi-consistent read
// 具体的处理方法,在下节中分析
ha_innodb.cc::try_semi_consistent_read(bool yes);
// 进行update的读与更新操作
...

// update操作完成之后,关闭semi-consistent read
table->file->try_semi_consistent_read(0);

MySQL Server层处理semi-consistent较为简单,接下来看看InnoDB Engine的处理方式。
InnoDB Engine层
InnoDB Engine层面,对于semi-consistent read的处理,包括两方面的逻辑:

判断当前语句是否可以支持semi-consistent read
fetch next时,对于semi-consistent read的特殊处理
是否采用semi-consistent read
前面提到,MySQL Server在update时,会调用引擎的try_semi_consistent_read方法,来尝
试进行semi-consistent read,而是否进行semi-consistent read,则交由底层处理。

ha_innodb.cc::try_semi_consistent_read()
if (yes &&
(srv_locks_unsafe_for_binlog
|| prebuilt->trx->isolation_level <= TRX_ISO_READ_COMMITTED))
prebuilt->row_read_type = ROW_READ_TRY_SEMI_CONSISTENT;

简单分析下,当用户设置系统参数innodb_locks_unsafe_for_binlog为true,或者是采用的事
务隔离级别为 read committed(或以下)时,设置 prebuilt->row_read_type参数,标识当前
update语句使用semi-consistent read,fetch next时需要有针对性的做特殊处理。
Fetch Next特殊处理逻辑
InnoDB fetch next 的主函数入口是 row_search_for_mysql,此函数如何针对性的处理
semi-consistent read呢?

row0sel.c::row_search_for_mysql()
...
// 尝试对于定位到的记录加锁
err = sel_set_rec_lock();
...
case DB_LOCK_WAIT:
// 如果加锁需要等待,则判断是否可以进行semi-consistent read
// 判断条件为:
// 1. prebuilt->row_read_type必须设置为ROW_READ_TRY_SEMI_CONSISTEN
// 2. 当前scan必须是range scan或者是全表扫描,而非unique scan
// 3. 当前索引必须是聚簇索引
// 4. 不满足以上三个条件,就不能进行semi-consistent read,进行加锁等待
// 注意:若不需要加锁等待,那么也不需要进行semi-consistent read,直接
// 读取记录的最新版本即可,没有加锁等待的开销。
if ((prebuilt->row_read_type != ROW_READ_TRY_SEMI_CONSISTENT)
|| unique_search
|| index != clust_index)
goto lock_wait_or_error;

// 可以进行semi-consistent read,根据记录的当前版本,构造最新的commit版本
// 若没有commit版本,当前版本为最新版本,则直接读取下一条记录
// 若存在commit版本,则设置did_semi_consistent_read为TRUE
row_sel_build_committed_vers_for_mysql();
if (old_vers == NULL)
goto next_rec;
did_semi_consistent_read = TRUE;
...
// 若本次update scan,由于加锁等待,使用了semi-consistent,则设置相应的参数
// 该参数,在下一小节提到的MySQL针对semi-consistent优化中有用
if (did_semi_consistent_read)
prebuilt->row_read_type = ROW_READ_DID_SEMI_CONSISTENT;
else
prebuilt->row_read_type = ROW_READ_TRY_SEMI_CONSISTENT;
// 至此,InnoDB的fetch next针对semi-consistent read的处理完毕
优化:Unlock unmatched row
上面提到的是semi-consistent read的功能实现,除此之外,MySQL针对semi-consistent read,
还做了优化措施:对于update scan返回的不满足条件的记录,提前放锁。

MySQL Server层流程:
sql_update.cc::mysql_update()
// 判断当前scan返回的记录,是否满足update的where条件
// 若满足,则进行update操作
if (!(select && select->skip_record())
...
// 若不满足update的where条件,则选择将当前记录上的行锁提前释放
else
table->file->unlock_row();

InnoDB Engine层流程:
ha_innobd.cc::unlock_row();
switch (prebuilt->row_read_type)
// 若系统未设置参数innodb_locks_unsafe_for_binlog,同时隔离级别大于
// TRX_ISO_READ_COMMITTED,则不可提前释放不满足条件的行锁
// 否则可以提前释放不满足条件的行锁
case ROW_READ_WITH_LOCKS:
if (!srv_locks_unsafe_for_binlog &&
prebuilt->trx->isolation_level > TRX_ISO_READ_COMMITTED)
break;
// 若当前系统已采用SEMI_CONSISTENT read,但是没有锁等待,加锁直接成功
// 那么此时直接释放不满足条件的行锁
case ROW_READ_TRY_SEMI_CONSISTENT:
row_unlock_for_mysql();

// 若当前系统已采用SEMI_CONSISTENT read,并且有锁等待,构造了commit版本
// 没有在commit版本上加锁,因此也无锁可放,直接返回即可
case ROW_READ_DID_SEMI_CONSISTENT:
prebuilt->row_read_type = ROW_READ_TRY_SEMI_CONSISTENT;
break;
semi-consistent优缺点分析
优点
减少了更新同一行记录时的冲突,减少锁等待。
无并发冲突,读记录最新版本并加锁;有并发冲突,读事务最新的commit版本,不加锁,
无需锁等待。
可以提前放锁,进一步减少并发冲突概率。
对于不满足update更新条件的记录,可以提前放锁,减少并发冲突的概率。
在理解了semi-consistent read原理及实现方案的基础上,可以酌情考虑使用semi-consistent
read,提高系统的并发性能。
缺点
非冲突串行化策略,因此对于binlog来说,是不安全的
两条语句,根据执行顺序与提交顺序的不同,通过binlog复制到备库后的结果也会不同。不
是完全的冲突串行化结果。
因 此 只 能 在 事 务 的 隔 离 级 别 为 read committed(或以下), 或 者 设 置 了
innodb_locks_unsafe_for_binlog参数的情况下才能够使用。
测试用例
构造semi-consistent read
set binlog_format=mixed;
set session transaction isolation level repeatable read;
create table t1(a int not null) engine=innodb DEFAULT CHARSET=latin1;
insert into t1 values (1),(2),(3),(4),(5),(6),(7);

session 1: session 2:
set autocommit=0;
update t1 set a = a + 10;
set binlog_format=mixed;
set session transaction isolation level read committed;
update t1 set a = a + 100 where a > 10;

此时,session 2不需要等待session 1,虽然session 1的更新后项满足session 2的条件,但
是由于session 2进行了semi-consistent read,读取到的记录的前项为(1-7),不满足session 2
的更新where条件,因此session 2直接返回。

session 2直接返回,0 rows affected。
构造unlock unmatched row
set binlog_format=mixed;
set session transaction isolation level repeatable read;
create table t1(a int not null) engine=innodb DEFAULT CHARSET=latin1;
insert into t1 values (1),(2),(3),(4),(5),(6),(7);

session 1: session 2:
set autocommit=0;
update t1 set a = a + 10;
commit;
set binlog_format=mixed;
set session transaction isolation level repeatable read;
set autocommit = 0;
update t1 set a = a + 100 where a < 10;

select * from t1 lock in share mode;

session 1在session 2开始前已经提交,session 2可以进行semi-consistent read。并且读到的
都是session 1的更新后项,完成加锁。但是由于更新后项均不满足session 2的where条件,
session 2会释放所有行上的锁(由MySQL Server层判断并调用unlock_row方法释放行锁)。

此时,session 1再次执行select * from t1 lock in share mode语句,直接成功。因为session 2
已经将所有的行锁提前释放。

朋友们可以试试将session 2的隔离级别改为repeatable read,那么此时session 1就会等待
session 2提交。

结论:在Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所
有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发更新/删除/插入操作。当然,
也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read
本身也会带来其他问题,不建议使用。

组合九:Serializable
针对前面提到的简单的SQL,最后一个情况:Serializable隔离级别。对于SQL2:delete from
t1 where id = 10; 来说,Serializable隔离级别与Repeatable Read隔离级别完全一致,因此不
做介绍。

Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR
隔离级别下,都是快照读,不加锁。但是在Serializable隔离级别,SQL1会加读锁S,也就
是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。

结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关
的。Serializable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读。

一条复杂的SQL
写到这里,其实MySQL的加锁实现也已经介绍的八八九九。只要将本文上面的分析思路,
大部分的SQL,都能分析出其会加哪些锁。而这里,再来看一个稍微复杂点的SQL,用于说
明MySQL加锁的另外一个逻辑。SQL用例如下:

图中的SQL,会加什么锁?假定在Repeatable Read隔离级别下 (Read Committed隔离级别下
的加锁情况,留给读者分析。),同时,假设SQL走的是idx_t1_pu索引。
在详细分析这条SQL的加锁情况前,还需要有一个知识储备,那就是一个SQL中的where
条件如何拆分?具体的介绍,建议阅读我之前的一篇文章:SQL中的where条件,在数据库
中提取与应用浅析。在这里,我直接给出分析后的结果:Index key:pubtime > 1 and puptime
< 20。此条件,用于确定SQL在idx_t1_pu索引上的查询范围。Index Filter:userid = ‘hdc’ 。
此条件,可以在idx_t1_pu索引上进行过滤,但不属于Index Key。Table Filter:comment is not
NULL。此条件,在idx_t1_pu索引上无法过滤,只能在聚簇索引上过滤。
在分析出SQL where条件的构成之后,再来看看这条SQL的加锁情况 (RR隔离级别),如下
图所示:

图中可以看出,在Repeatable Read隔离级别下,由Index Key所确定的范围,被加上了GAP
锁;Index Filter锁给定的条件 (userid = ‘hdc’)何时过滤,视MySQL的版本而定,在MySQL 5.6
版本之前,不支持Index Condition Pushdown(ICP),因此Index Filter在MySQL Server层过滤,
在5.6后支持了Index Condition Pushdown,则在index上过滤。若不支持ICP,不满足Index
Filter的记录,也需要加上记录X锁,若支持ICP,则不满足Index Filter的记录,无需加记录
X锁 (图中,用红色箭头标出的X锁,是否要加,视是否支持ICP而定);而Table Filter对应
的过滤条件,则在聚簇索引中读取后,在MySQL Server层面过滤,因此聚簇索引上也需要X
锁。最后,选取出了一条满足条件的记录[8,hdc,d,5,good],但是加锁的数量,要远远大于满
足条件的记录数量。
结论:在Repeatable Read隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。
Index Key确定的范围,需要加上GAP锁;Index Filter过滤条件,视MySQL版本是否支持ICP,
若支持ICP,则不满足Index Filter的记录,不加X锁,否则需要X锁;Table Filter过滤条件,
无论是否满足,都需要加X锁。
死锁原理与分析
本文前面的部分,基本上已经涵盖了MySQL/InnoDB所有的加锁规则。深入理解MySQL如
何加锁,有两个比较重要的作用:可以根据MySQL的加锁规则,写出不会发生死锁的SQL;
可以根据MySQL的加锁规则,定位出线上产生死锁的原因;
下面,来看看两个死锁的例子 (一个是两个Session的两条SQL产生死锁;另一个是两个

Session的一条SQL,产生死锁):

面的两个死锁用例。第一个非常好理解,也是最常见的死锁,每个事务执行两条SQL,分别

持有了一把锁,然后加另一把锁,产生死锁。

create table t_lock(id int primary key ,name varchar(20),pubtime int,key
idx_name(name),key idx_pubtime(pubtime));
insert into t_lock values(1,hdc,100),(6,hdc,10);

session 1: delete from t_lock where name='hdc';
session 2: delete from t_lock where pubtime in(100,10);
并发执行时,很容易发生死锁。或者引入session 3来阻塞 session 1 session 2
第二个用例,虽然每个Session都只有一条语句,仍旧会产生死锁。要分析这个死锁,首先
必须用到本文前面提到的MySQL加锁的规则。针对Session 1,从 name索引出发,读到的[hdc,
1],[hdc, 6]均满足条件,不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X
锁,加锁顺序为先[1,hdc,100],后[6,hdc,10]。而 Session 2,从 pubtime索引出发,[10,6],[100,1]
均满足过滤条件,同样也会加聚簇索引上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。
发现没有,跟Session 1的加锁顺序正好相反,如果两个Session恰好都持有了第一把锁,请
求加第二把锁,死锁就发生了。
结论:死锁的发生与否,并不在于事务中有多少条SQL语句,死锁的关键在于:两个(或以
上)的Session加锁的顺序不一致。而使用本文上面提到的,分析MySQL每条SQL语句的加
锁规则,分析出每条语句的加锁顺序,然后检查多个并发SQL间是否存在以相反的顺序加
锁的情况,就可以分析出各种潜在的死锁情况,也可以分析出线上死锁发生的原因。

总结
写到这儿,本文也告一段落,做一个简单的总结,要做的完全掌握MySQL/InnoDB的加锁规
则,甚至是其他任何数据库的加锁规则,需要具备以下的一些知识点:

了解数据库的一些基本理论知识:数据的存储格式 (堆组织表 vs 聚簇索引表);并发控制协
议 (MVCC vs Lock-Based CC);Two-Phase Locking;数据库的隔离级别定义 (Isolation Level);
了解SQL本身的执行计划 (主键扫描 vs 唯一键扫描 vs 范围扫描 vs 全表扫描);了解数据
库本身的一些实现细节 (过滤条件提取;Index Condition Pushdown;Semi-Consistent Read );
了解死锁产生的原因及分析的方法 (加锁顺序不一致;分析每个SQL的加锁顺序)
有了这些知识点,再加上适当的实战经验,全面掌控MySQL/InnoDB的加锁规则,当不在话
下。转载请保留固定链接: https://www.linuxeye.com/database/1986.html

6.1. 3 索引的基本原理及优化

6.1.3.1 Oracle 索引

1 、什么是索引?
索引是建立在表的一列或多个列上的辅助对象,目的是加快访问表中的数据;Oracle存
储索引的数据结构是B树,位图索引也是如此,只不过是叶子节点不同B数索引;索引由
根节点、分支节点和叶子节点组成,上级索引块包含下级索引块的索引数据,叶节点包含索
引数据和确定行实际位置的rowid。

2 、使用索引的目的
当查询返回的记录数排序表<40%非排序表 <7%且表的碎片较多(频繁增加、删除)时
可以加快查询速度减少I/O操作消除磁盘排序

二、索引的分类及结构
1 、逻辑上:
Single column/Concatenated 单行索引/多行索引
Unique/NonUnique 唯一索引/非唯一索引

2 、物理上:
B-tree B树索引
Bitmap 位图索引
REVERSE 反向索引
HASHHASH索引
Function-based基于函数的索引
Partitioned/NonPartitioned 分区索引/非分区索引
Domain 域索引

三、各种索引详解
1 、 B树索引
Oracle数据库中最常见的索引类型是b-tree索引,也就是B-树索引,以其同名的计算科
学结构命名。CREATE INDEX语句时,默认就是在创建b-tree索引。没有特别规定可用于任何
情况。

( 1 )特点:

适合与大量的增、删、改(OLTP)

不能用包含OR操作符的查询;

适合高基数的列(唯一值多)

典型的树状结构;

每个结点都是数据块;

大多都是物理上一层、两层或三层不定,逻辑上三层;

叶子块数据是排序的,从左向右递增;

在分支块和根块中放的是索引的范围;

( 2 )技巧:

索引列的值都存储在索引中。因此,可以建立一个组合(复合)索引,这些索引可以直接

满足查询,而不用访问表。这就不用从表中检索数据,从而减少了I/O量。

2 、位图索引

位图索引非常适合于决策支持系统(Decision Support System,DSS)和数据仓库,它们不应
该用于通过事务处理应用程序访问的表。它们可以使用较少到中等基数(不同值的数量)的列
访问非常大的表。尽管位图索引最多可达 30 个列,但通常它们都只用于少量的列。
例如,您的表可能包含一个称为Sex的列,它有两个可能值:男和女。这个基数只为 2 ,如
果用户频繁地根据Sex列的值查询该表,这就是位图索引的基列。当一个表内包含了多个位
图索引时,您可以体会到位图索引的真正威力。如果有多个可用的位图索引,Oracle就可以
合并从每个位图索引得到的结果集,快速删除不必要的数据。

( 1 )特点:
适合与决策支持系统;
做UPDATE代价非常高;
非常适合OR操作符的查询;
基数比较少的时候才能建位图索引;

( 2 )技巧:
对于有较低基数的列需要使用位图索引。性别列就是这样一个例子,它有两个可能值:
男或女(基数仅为2)。位图对于低基数(少量的不同值)列来说非常快,这是因为索引的尺寸相
对于B树索引来说小了很多。因为这些索引是低基数的B树索引,所以非常小,因此您可
以经常检索表中超过半数的行,并且仍使用位图索引。
当大多数条目不会向位图添加新的值时,位图索引在批处理(单用户)操作中加载表(插入
操作)方面通常要比B树做得好。当多个会话同时向表中插入行时不应该使用位图索引,在
大多数事务处理应用程序中都会发生这种情况。
在一个查询中合并多个位图索引后,可以使性能显著提高。位图索引使用固定长度的数
据类型要比可变长度的数据类型好。较大尺寸的块也会提高对位图索引的存储和读取性能。

3 、 反向索引
这个索引不常见,但是特定情况特别有效,比如一个varchar(5)位字段(员工编号)含值
(10001,10002,10033,10005,10016...)
这种情况默认索引分布过于密集,不能利用好服务器的并行
但是反向之后10001,20001,33001,50001,61001就有了一个很好的分布,能高效的利用好并

行运算。

( 1 )特点:

不可以将反转键索引与位图索引或索引组织表结合使用。因为不能对位图索引和索引组

织表进行反转键处理。

( 2 )技巧:

如果您的磁盘容量有限,同时还要执行大量的有序载入,就可以使用反转键索引。

4 、HASH索引

使用HASH索引必须要使用HASH集群。建立一个集群或HASH集群的同时,也就定义

了一个集群键。这个键告诉Oracle如何在集群上存储表。在存储数据时,所有与这个集群
键相关的行都被存储在一个数据库块上。如果数据都存储在同一个数据库块上,并且将HASH
索引作为WHERE子句中的确切匹配,Oracle就可以通过执行一个HASH函数和I/O来访问数
据——而通过使用一个二元高度为 4 的B树索引来访问数据,则需要在检索数据时使用 4
个I/O。其中的查询是一个等价查询,用于匹配HASH列和确切的值。Oracle可以快速使用
该值,基于HASH函数确定行的物理存储位置。
HASH索引可能是访问数据库中数据的最快方法,但它也有自身的缺点。集群键上不同
值的数目必须在创建HASH集群之前就要知道。需要在创建HASH集群的时候指定这个值。
低估了集群键的不同值的数字可能会造成集群的冲突(两个集群的键值拥有相同的HASH值)。
这种冲突是非常消耗资源的。冲突会造成用来存储额外行的缓冲溢出,然后造成额外的I/O。
如果不同HASH值的数目已经被低估,您就必须在重建这个集群之后改变这个值。
ALTER CLUSTER命令不能改变HASH键的数目。HASH集群还可能浪费空间。如果无法确
定需要多少空间来维护某个集群键上的所有行,就可能造成空间的浪费。如果不能为集群的
未来增长分配好附加的空间,HASH集群可能就不是最好的选择。如果应用程序经常在集群
表上进行全表扫描,HASH集群可能也不是最好的选择。由于需要为未来的增长分配好集群
的剩余空间量,全表扫描可能非常消耗资源。
在实现HASH集群之前一定要小心。您需要全面地观察应用程序,保证在实现这个选项之前
已经了解关于表和数据的大量信息。通常,HASH对于一些包含有序值的静态数据非常有效。

( 1 )特点:
可以在表中创建基于函数的索引。如果没有基于函数的索引,任何在列上执行了函数的
查询都不能使用这个列的索引。例如,下面的查询就不能使用JOB列上的索引,除非它是基
于函数的索引:

select * from emp where UPPER(job) = 'MGR';
下面的查询使用JOB列上的索引,但是它将不会返回JOB列具有Mgr或mgr值的行:

select * from emp where job = 'MGR';
可以创建这样的索引,允许索引访问支持基于函数的列或数据。可以对列表达式
UPPER(job)创建索引,而不是直接在JOB列上建立索引,如:

create index EMP$UPPER_JOB on emp(UPPER(job));
尽管基于函数的索引非常有用,但在建立它们之前必须先考虑下面一些问题:
能限制在这个列上使用的函数吗?如果能,能限制所有在这个列上执行的所有函数吗
是否有足够应付额外索引的存储空间?

在每列上增加的索引数量会对针对该表执行的DML语句的性能带来何种影响?

基于函数的索引非常有用,但在实现时必须小心。在表上创建的索引越多,INSERT、

UPDATE和DELETE语句的执行就会花费越多的时间。

5 、函数索引

可以在表中创建基于函数的索引。如果没有基于函数的索引,任何在列上执行了函数的

查询都不能使用这个列的索引。例如,下面的查询就不能使用JOB列上的索引,除非它是基

于函数的索引:

select * from emp where UPPER(job) = 'MGR';
下面的查询使用JOB列上的索引,但是它将不会返回JOB列具有Mgr或mgr值的行:

select * from emp where job = 'MGR';
可以创建这样的索引,允许索引访问支持基于函数的列或数据。可以对列表达式
UPPER(job)创建索引,而不是直接在JOB列上建立索引,如:

create index EMP$UPPER_JOB on emp(UPPER(job));

尽管基于函数的索引非常有用,但在建立它们之前必须先考虑下面一些问题:
能限制在这个列上使用的函数吗?如果能,能限制所有在这个列上执行的所有函数吗
是否有足够应付额外索引的存储空间?
在每列上增加的索引数量会对针对该表执行的DML语句的性能带来何种影响?
基于函数的索引非常有用,但在实现时必须小心。在表上创建的索引越多,INSERT、UPDATE
和DELETE语句的执行就会花费越多的时间。

6 、分区索引和全局索引
分区索引就是简单地把一个索引分成多个片断。通过把一个索引分成多个片断,可以访
问更小的片断(也更快),并且可以把这些片断分别存放在不同的磁盘驱动器上(避免I/O问题)。
B树和位图索引都可以被分区,而HASH索引不可以被分区。可以有好几种分区方法:表被
分区而索引未被分区;表未被分区而索引被分区;表和索引都被分区。不管采用哪种方法,
都必须使用基于成本的优化器。分区能够提供更多可以提高性能和可维护性的可能性
有两种类型的分区索引:本地分区索引和全局分区索引。每个类型都有两个子类型,有前缀
索引和无前缀索引。表各列上的索引可以有各种类型索引的组合。如果使用了位图索引,就
必须是本地索引。把索引分区最主要的原因是可以减少所需读取的索引的大小,另外把分区
放在不同的表空间中可以提高分区的可用性和可靠性。
在使用分区后的表和索引时,Oracle还支持并行查询和并行DML。这样就可以同时执行
多个进程,从而加快处理这条语句。
可以使用与表相同的分区键和范围界限来对本地索引分区。每个本地索引的分区只包含
了它所关联的表分区的键和ROWID。本地索引可以是B树或位图索引。如果是B树索引,
它可以是唯一或不唯一的索引。
这种类型的索引支持分区独立性,这就意味着对于单独的分区,可以进行增加、截取、
删除、分割、脱机等处理,而不用同时删除或重建索引。Oracle自动维护这些本地索引。本
地索引分区还可以被单独重建,而其他分区不会受到影响。

7 、域索引 Domain
域索引实际为用户自定义索引,域索引主要对存储在数据库中的媒体,图像数据进行索
引,这些数据在oracle中基本上以BLOB类型存储,不同的应用存储格式也不同,oracle不
可能提供某一种现成的算法对这些数据进行索引,为了能够对这些类型数据快速访问,oracle
提供了现成的接口函数,用户可以针对自己的数据格式实现这些接口函数,以达到对这些数
据的快速访问。

6.1.3.1 Mysql 索引

参见数据库基础培训(金融大数据事业部) p160

6.1.4 查询优化

1.merge join Cartesian

查询隐含变量:
col name for a30;
col value for a10;
select
x.ksppinm name,
y.ksppstvl value,
y.ksppstdf isdefault,
decode(bitand(y.ksppstvf,7),1,'MODIFIED',4,'SYSTEM_MOD','FALSE') ismod,
decode(bitand(y.ksppstvf,2),2,'TRUE','FALSE') isadj
from
sys.x$ksppi x,
sys.x$ksppcv y
where
x.inst_id = userenv('Instance') and
y.inst_id = userenv('Instance') and
x.indx = y.indx and x.ksppinm ='_optimizer_mjc_enabled'
order by
translate(x.ksppinm, ' _', ' ')
/
现象:

监控到此sql语句,这个语句有union all,跑不出来。将union all更改为union后,成功解决。

后期研究,发现真实的原因并不是union all 改为了union 。 查询跑出的执行计划和跑不出
的执行计划,发现跑不出的执行计划走了merge join Cartesian

将此特性关闭后

原因:

统计信息不准确

exec dbms_stats.gather_table_stats(ownname=>'CSSP',tabname=>'T1',
method_opt=>'FOR ALL COLUMNS SIZE AUTO',cascade=>TRUE,
no_invalidate=>false,degree=>24);

解决方法:
instance禁用
SQL> alter system set "_optimizer_mjc_enabled" = false;

session禁用
SQL> alter session set "_optimizer_mjc_enabled" = false;

sql添加 hint
Select/*+ OPT_PARAM('_optimizer_mjc_enabled','false') */

2.Oracle update 大表更新的改写一例

现象:
update test1 set col3=to_date(substr(col2,1,14),'yyyymmdd'));

机器配置不行,执行比较慢。

下面是一个变通的办法:

create table test1_bak as ( select col1,
col2,
to_date(substr(col2,1,14),'yyyymmdd')) as col3

from test1);

很快执行完毕,表改一下名即可

注意,计算列一定要使用别名,并且与test1的列名保持一致。

3.关联表更新的四种方法

两张表关联更新,更新数据来自另一张表。
创建数据表T1和T2并插入数据:
CREATE TABLE T1(
FNAME VARCHAR2(50),
FMONEY NUMBER
);
CREATE TABLE T2(
FNAME VARCHAR2(50),
FMONEY NUMBER
);

INSERT INTO T1(FNAME,FMONEY)values ('A',20);
INSERT INTO T1(FNAME,FMONEY)values ('B',30);
INSERT INTO T2(FNAME,FMONEY)values ('A',100);
INSERT INTO T2(FNAME,FMONEY)values ('C',20);
INSERT INTO T2(FNAME,FMONEY)values ('D',10);
数据插入完成如下图所示:

现需求:参照T2表,修改T1表,修改条件为两表的fname列内容一致。

方法一:直接update更新
UPDATE T1 SET T1.FMONEY = (select T2.FMONEY from T2 where T2.FNAME = T1.FNAME);
更新后的T1表数据如下:

可以发现,A记录对应的值由原来的 20 被更新为 100 ,B的值由原来的 30 被更新为空。这

是因为B在T2表中没有被找到对应记录导致的。这里要特别注意,如果要求B对应的记录

不被更改,这种写法是不适合的。

方法二:使用EXISTS更新

UPDATE T1

SET T1.FMONEY = (select T2.FMONEY from t2 where T2.FNAME = T1.FNAME)
WHERE EXISTS(SELECT 1 FROM T2 WHERE T2.FNAME = T1.FNAME);
更新后的T1表数据如下:

更新后A记录对应的值被更新为 100 ,B记录对应的值没有被改动。这种方法解决了方法一

中B的值被更新为空的问题。

方法三:内联视图更新

这种更新方式有一个前提条件,T2表的FNAME字段必须为主键。

先为T2表添加主键:

ALTER TABLE T2 ADD CONSTRAINT pk_test2 PRIMARY KEY(FNAME);
然后执行更新语句:
update (
select t1.fmoney fmoney1,t2.fmoney fmoney2 from t1,t2 where t1.fname = t2.fname
)t
set fmoney1 =fmoney2;
更新后的T1表数据如下:

可以看到这种方法执行结果跟方法二的执行结果一致,如果在T2表中根据FNAME匹配到的

记录就更新,匹配不到的记录不做操作。

方法四:merge合并更新 推荐

merge into t1
using (select t2.fname,t2.fmoney from t2) t
on (t.fname = t1.fname)
when matched then
update set t1.fmoney = t.fmoney;
更新后的T1表数据如下:

可以看到这种方法执行结果跟方法二的执行结果一致,如果在T2表中根据FNAME匹配到的

记录就更新,匹配不到的记录不做操作。

oracle中的merge合并函数,使用的业务场景一般为:新增数据的时候要先查询数据库是否
已经存在该条记录,存在就更新数据,不存在就新增数据。
改动前面的需求,如果T1表中的数据根据FNAME字段在T2表中存在,就更新T1表的
FMONEY字段,如果T1表的数据根据FNAME字段没有在T2表中匹配到记录,就把T2表的
记录插入到T1中,语句就可以这样写:
merge into t1
using (select t2.fname,t2.fmoney from t2) t
on (t.fname = t1.fname)
when matched then
update set t1.fmoney = t.fmoney
when not matched then
insert(fname,fmoney)values(t.fname,t.fmoney);
更新后的T1表数据如下:

T2中FNAME为A的记录的FMONEY为 100 ,所以T1表中FNAME为A的记录的FMONEY字

段被更新为 100 。

T1表FNAME为B的记录没有在T2表中找到,所以T1中FNAME为B的记录没有被更新。

T2表FNAME为C和D的记录,在T1表不存在,所以执行了insert的操作,将T2表中FNAME
为C和D的记录插入到T1表。

4.NULL对反连接的影响

测试准备:
create table t1(col1 number,col2 varchar2(1));
create table t2(col2 varchar2(1),col3 varchar2(2));
insert into t1 values(1,'A');
insert into t1 values(2,'B');
insert into t1 values(3,'C');
insert into t2 values('A','A2');
insert into t2 values('B','B2');
insert into t2 values('D','D2');
commit;

SQL> set autot on
SQL> select * from t1 where col2 not in (select col2 from t2);

COL1 C


3 C

Execution Plan

Plan hash value: 1275484728


| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 3 | 51 | 6 (0)| 00:00:01 |

|* 1 | HASH JOIN ANTI NA | | 3 | 51 | 6 (0)| 00:00:01 |

| 2 | TABLE ACCESS FULL| T1 | 3 | 45 | 3 (0)| 00:00:01 |

| 3 | TABLE ACCESS FULL| T2 | 3 | 6 | 3 (0)| 00:00:01 |


SQL> select * from t1 where col2 <> all (select col2 from t2);
COL1 C


3 C
Execution Plan

Plan hash value: 1275484728

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 3 | 51 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI NA | | 3 | 51 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 3 | 45 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 3 | 6 | 3 (0)| 00:00:01 |

SQL> select * from t1 where not exists (select 1 from t2 where col2=t1.col2);
COL1 C


3 C
Execution Plan

Plan hash value: 2706079091

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 3 | 51 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI | | 3 | 51 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 3 | 45 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 3 | 6 | 3 (0)| 00:00:01 |

SQL>

如果T1表中col2有null值:
insert into t1(col1) values('4');
commit;
SQL> select * from t1 where col2 not in (select col2 from t2);
COL1 C


3 C
Execution Plan


Plan hash value: 1275484728

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 4 | 68 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI NA | | 4 | 68 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 4 | 60 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 3 | 6 | 3 (0)| 00:00:01 |

SQL> select * from t1 where col2 <> all (select col2 from t2);
COL1 C


3 C
Execution Plan

Plan hash value: 1275484728

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 4 | 68 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI NA | | 4 | 68 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 4 | 60 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 3 | 6 | 3 (0)| 00:00:01 |

SQL> select * from t1 where not exists (select 1 from t2 where col2=t1.col2);
COL1 C


4
3 C
Execution Plan

Plan hash value: 2706079091

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 4 | 68 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI | | 4 | 68 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 4 | 60 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 3 | 6 | 3 (0)| 00:00:01 |

SQL>
如果T2中col2有null值:
delete from t1 where col1=4;

insert into t2(col3) values('E2');
commit;
SQL> select * from t1 where col2 not in (select col2 from t2);
no rows selected
Execution Plan

Plan hash value: 1275484728

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 3 | 51 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI NA | | 3 | 51 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 3 | 45 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 4 | 8 | 3 (0)| 00:00:01 |

SQL> select * from t1 where col2 <> all (select col2 from t2);
no rows selected
Execution Plan

Plan hash value: 1275484728

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 3 | 51 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI NA | | 3 | 51 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 3 | 45 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 4 | 8 | 3 (0)| 00:00:01 |

SQL> select * from t1 where not exists (select 1 from t2 where col2=t1.col2);
COL1 C


3 C
Execution Plan

Plan hash value: 2706079091

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

| 0 | SELECT STATEMENT | | 3 | 51 | 6 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI | | 3 | 51 | 6 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T1 | 3 | 45 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| T2 | 4 | 8 | 3 (0)| 00:00:01 |

SQL>

not in、<> all对null值敏感,即not in、、<> all后面的子查询或者常亮集合一旦有null
值出现,整个sql的执行结果就为null。

not exists对null值不敏感,即null值对执行结果不会有什么影响。

Oracle 中null 值相关问题

test1 1,2,null
test2 1,2,null
select * from test1 where a not in(select * from test2);

not in 相当于 (null not =null) or ( null not=1) or( null not =2)=null

select * from test1 where not exists (select a from test2 where test1.a=test2.a);
not exists相当于 not (null exists null) or not(null exists 1) or not(null exists 2)
=not false or not false or not false=true or true or true=true

5.通过索引避免全表扫描

一个表只取按时间倒序的前十列,表数据量1400W

select /*+ gather_plan_statistics */ *
from ( select *
from person t
order by temp1 desc)
where rownum<=10;

解决:将全表扫描转化为索引+stop key

建单列索引
CREATE INDEX IDX_CPQ_RI_UPTM ON CPQ_RESULTINFO(UPDATE_TIME DESC);
收集表及索引信息
EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME =>' 用户名',TABNAME =>
'CPQ_RESULTINFO' ,METHOD_OPT => 'FOR ALL COLUMNS SIZE AUTO' ,CASCADE => TRUE);

下例建了复合索引,oracle并没有用到,因为有stopkey的存在,只扫update_time,然后用
oper_org过滤即可,因为update_time 可空,你会发现并不会走索引,原因如下篇

6.Oracle null值不走索引问题及rownum

表person

drop index idx_person_temp1;
create index idx_person_temp1 on person(temp1 desc);
exec dbms_stats.gather_table_stats(ownname=>'TEST1',tabname=>'PERSON',method_opt=>'for
all columns size auto',cascade=>true);

select count(*) from person;
640,000

select /*+ gather_plan_statistics */ *
from ( select *
from person t
order by temp1 desc)
where rownum<=10;
select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

原因分析:

因为temp1列可空,temp1上的索引并不存null值,只通过temp1上的索引并不能确保能
够扫描到所有的数据,因此也无从排序,oracle为确保数据的正确性,只能全表扫描后排序
给出值。
解决方案:
1.想办法让索引存null值
建常数复合索引
CREATE INDEX IDX_CPQ_RI_UPTM ON CPQ_RESULTINFO(UPDATE_TIME DESC,‘1’);
收集表及索引信息
EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME =>'用户名',TABNAME =>
'CPQ_RESULTINFO' ,METHOD_OPT => 'FOR ALL COLUMNS SIZE AUTO' ,CASCADE => TRUE);
确保走类如下索引

2.保证数据不返回null值

select /*+ gather_plan_statistics */ *
from ( select *
from person t
where t.temp1 is not null
order by temp1 desc)
where rownum<=10;
select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

7.报送系统数据库调优方法

报送系统的调优请参考报送的帮助文档,这里仅对数据库相

关的几个参数说明。

现象:

更改下列参数:

批处理每批次入库条数: 100

第二阶段消费者数量: 36

第三阶段消费者数量: 48

核心思想就是事务要适中,并发适量增多,更改后,效率有较大程度提升。

结论:在数据库系统中,如果观察到有较明显的IO压力,就应该适当调小事务,适当频度

提交,适度并发。

相关命令:

top
vmstat 1
iostat -mx 1

8.实施过程中常见故障及排除

1.单列索引引起deadlock解决

现象:

程序设计方案是

update队列 |---- 1 ----- 2 ----- 3 ----》.....

delete列列 |---- 1 ----- 2 ----- 3 ----》.....
多线程并发,分别从队列中取出 1 , 2 , 3 ,业务保证
update队列,delete队列中没有交集。

并发执行后发生了DeadLock,经排查,举例说明如下。
表结构:
create table test1(a int primary key ,
b int,
c int,
key idx_b(b),
key idx_c(c));
最初b,c上设单列索引。
mysql 的隔离集别在 READ_COMMITTED.

在 update 队列: where b=xx and c='Y';
在 delete 队列: where b=xx and c='N';
从以上说明,二者意图锁定的记录不会有交集。
建立测试用例:

T1:

begin;
select * from test1 where b=2 and c=2 for update;
此时走索引idx_b
锁定b的键值为 2 的索引项,实际为b=2,主键a=2

T2:

begin;
select * from test1 where b=2 and c=3 for update;
此时走idx_b,此时会锁定idx_b上键值为 2 的项,却发现键值为 2 的记录上已经有一个a=2,b=2
的x锁,因此发生了锁等待。

而请求的锁已经被事务 1 占用

因此,发生了等待

将以上例子推广:假设test2与test1类似结构
事务 1 事务 2
begin;
select * from test1
where b=2 and c=3 for update

begin;
select * from test2
where b=3 and c=2 for update;

select * from test2
where b=3 and c=3 for update;

select * from test1
where b=2 and c=3 for update

参考上例 ,在仅使用ibx_b索引情况下,就会发生
事务 1 和事务 2 的互相等待,产生deadlock。

解决方法:
将索引的粒度细化,将单列索引变成复合索引,则键值更分散,
alter table test1 add key idx_bc(b,c);
此时,索引的键值就被更细一步划分
key(2,2),(2,3),(2,4)
因业务保证update ,delete不会有交集,通过c列区分,因此,就不会发生同时在一个key
上加锁的情况,因此并发能顺利执行。
如果,两个队列存在使用同一个key值的情况,则死锁情况仍不可避免,因此编写sql语句
时要注意。

2.修改主键导致使用id更新变慢

现象,某使用id的update速度较慢 update t1 set c1='xxx' where id='xxxx';
经排查,原来id是主键,但是uuid 32位,为了加快insert 速度,添加了newid bigint
autoincrement primary key, 但是id没有加索引,则上面的语句就只能走newid primary key,
相当于全表扫描了。

把id加上索引后,update 语句使用Id索引,避免了全表扫描。

3.测试服务器同sid多oracle_home引起的无法登录问题解决

关于数据库的启停,以前一直认为是最简单,最没有技术含量的任务,但是接手的环境越多,

越来越证明我最初的想法是错误的。数据库的启停是一个敏感操作,重启一定是有着特定的

原因和需求。而且这个过程中存在这太多的可能性,很可能在同一会话窗口中,停掉数据库

再次启动就会报错;很可能硬件扩容,也会导致数据库实例无法启动;这个时候重启前的准

备和分析就尤为重要。

在此大体把启停中的问题归为三类,数据库无法启动,数据库无法登录,数据库宕机。我们

在此主要讨论数据库无法启动的场景。

看起来很简单的一件事情,重启的过程中总是可能节外生枝,总是感觉数据库实例有时候不

是那么配合,总是在启动过程中会发牢骚。从我的经历来看,我碰到的绝大多数问题都发生

在open阶段。
对于数据库无法启动的原因大体有以下几个方面需要考虑。
1)系统内核参数设置不当,比如当前的内核参数设置在需要增加数据库级的配置的情况下是
否能够满足,或者在硬件扩容的情况下,现有的内核参数是否需要做相应的调整。举个例子,
之前看到一个数据库环境中的内存为16G,但是实际上process只设置了 150 ,很明显可以
充分利用这部分资源, 在申请维护窗口重启的过程中,发现调整了process大小之后,数据
库实例无法启动,根本原因就是内核参数shmmax设置过低导致。
2)数据库参数变量设置不当,数据库参数或者变量的一些设置可能会和现有的资源使用情况
冲突,在这种情况下,数据库参数的设置很可能过高或者过低,导致硬件资源的使用无法满

足。比如数据库层面的process大小还是需要和系统内核参数有一个映射,设置不能太高。
我们以一个真实的案例来说明
这是我接手的一套测试环境。这是一套11gR2的环境。
当我准备连接到环境的时候,首先查看数据库的进程情况。
可以看到目前的环境存在两个数据库实例newtest和test04,在此我们需要连接newtest.
$ ps - ef|grep smon
oracle 1451 1 0 Feb02? 00:00:30 ora_smon_newtest
oracle 9133 1 0 Feb03? 00:00:58 ora_smon_test04
oracle 24734 24596 0 17:36 pts/0 00:00:00 grep smon
但是使用sqlplus登录的时候却碰到了一个非常奇怪的问题。
$ sqlplus / as sysdba
SQL*Plus: Release 11.2.0.4.0 Production on Wed Feb 17 17:36:08 2016
Copyright (c) 1982, 2013, Oracle. All rights reserved.
Connected to an idle instance.
SQL>
按理说应该直接使用sys用户连接到了数据库实例,但是在此却显示是一个空实例。这到底
是哪里出了问题呢。首先排除了ORACLE_SID大小写,乱码的问题。
查看数据库日志也没有发现任何异常信息,实例还是active的。
在此我们先卖个关子,继续往下看。
因为是测试环境,所以也可以做一些简单的尝试,于是我就尝试启动数据库实例。
Nomount阶段竟然没有报出任何的错误。
SQL> startup nomount
ORACLE instance started.
Total System Global Area 4993982464 bytes
Fixed Size 2261808 bytes
Variable Size 1006636240 bytes
Database Buffers 3976200192 bytes
Redo Buffers 8884224 bytes
这个时候都有些怀疑是否之前的分析是正确的。
继续把实例置为mount阶段,这个时候就抛出了下面的问题。
SQL> alter database mount;
alter database mount
*
ERROR at line 1:
ORA-00205: error in identifying control file, check alert log for more info
这个时候,查看日志就是一个很好的办法,在11g中可以使用adr的方式来查看,或者使用
下面的方式来直接找到日志所在目录。
SQL> show parameter background_dump_dest
NAME TYPE VALUE


background_dump_dest string /U01/app/oracle/diag/rdbms/new
test/newtest/trace
得到的日志如下:
MMNL started with pid=16, OS id=24683

starting up 1 dispatcher(s) for network address '(ADDRESS=(PARTIAL=YES)(PROTOCOL=TCP))'...
starting up 1 shared server(s) ...
ORACLE_BASE from environment = /DATA/app/oracle
Wed Feb 17 17 :36:21 2016
alter database mount
ORA-00210: cannot open the specified control file
ORA-00202: control file: '/U01/app/oracle/fast_recovery_area/newtest/control02.ctl'
ORA-27086: unable to lock file - already in use
Linux-x86_64 Error: 11: Resource temporarily unavailable
Additional information: 8
Additional information: 1449
ORA-00210: cannot open the specified control file
ORA-00202: control file: '/U01/app/oracle/oradata/newtest/control01.ctl'
ORA-27086: unable to lock file - already in use
Linux-x86_64 Error: 11: Resource temporarily unavailable
Additional information: 8
Additional information: 1449
ORA- 205 signalled during: alter database mount...
通过上面的错误信息可以很清晰看到控制文件已经被占用了。
这个时候查看数据库实例的情况,发现结果让人大跌眼镜,竟然有两个实例为newtest.
[oracle@BX_133_45 trace]$ ps - ef|grep smon
oracle 1451 1 0 Feb02? 00:00:30 ora_smon_newtest
oracle 9133 1 0 Feb03? 00:00:58 ora_smon_test04
oracle 24677 1 0 17:36? 00:00:00 ora_smon_newtest
oracle 24734 24596 0 17:36 pts/0 00:00:00 grep smon
那么这个问题该怎么解释呢,在Unix,Linux系统中,SID和ORACLE_HOME在一起哈希后会
得到一个唯一的值作为SGA的key。
当oracle实例启动时,在操作系统上的fork进程会根据Oracle_SID来创建相关后台进程。
Oracle 11g 支持Oracle_SID 的长度为 12 位,db_name的长度为 8 位。而在很早的版本中
ORACLE_SID只支持 4 位,这也就是我们经常看到ORCL,PROD这样的数据库的一个原因吧。
在这个场景中,ORACLE_SID 没有任何问题,那么仔细来品味上面的话,另外一个可能就是
ORACLE_HOME了。
我们首先把刚刚没有启动的实例先停掉,避免有更多的干扰。
查看共享内存段的情况如下,可见数据库实例还是没有受到影响。
$ ipcs - m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 1114113 oracle 640 33554432 23
0x00000000 1146882 oracle 640 4982833152 23
0x849f1498 1179651 oracle 640 2097152 23
0x00000000 3211268 oracle 640 33554432 23
0x00000000 3244037 oracle 640 4982833152 23
0x9d2300b0 3276806 oracle 640 2097152 23
这个时候再次观察实例的smon进程,发现原来的进程依然存在。

$ ps - ef|grep smon
oracle 1451 1 0 Feb02? 00:00:30 ora_smon_newtest
oracle 9133 1 0 Feb03? 00:00:58 ora_smon_test04
oracle 24779 24596 0 17: 37 pts/0 00:00:00 grep smon
再次确认ORACLE_SID
$ echo $ORACLE_SID
newtest
确认ORACLE_HOME
$ echo $ORACLE_HOME
/DATA/app/oracle/11.2.0.4
好了,我们来开始分析:
找到系统级所在的句柄,根据smon进程对应的进程号 1451 在/proc/1451下面,查看environ
的设置情况,可以使用下面的方式来查看这个进程对应的环境变量ORACLE_HOME
$ cat /proc/1451/environ|xargs - 0 - n1 |grep ORACLE_HOME
ORACLE_HOME=/U01/app/oracle/product/11.2.0.2/db_1
这个时候真相浮出水面,原来是ORACLE_HOME设置不同。
手工指定ORACLE_HOME,然后再次尝试
$ export ORACLE_HOME=/U01/app/oracle/product/11.2.0.2/db_1
$ sqlplus / as sysdba
SQL*Plus: Release 11.2.0.4.0 Production on Wed Feb 17 17:49:00 2016
Copyright (c) 1982, 2013, Oracle. All rights reserved.
Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.3.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options
这个时候就达到了预期的效果
SQL> select database_role from v$database;
DATABASE_ROLE
PRIMARY

通过这个案例可以发现,在环境维护中需要遵循一定的规范,如果不严谨不规范,就会出现
一些看似奇怪的问题;对于数据库实例的启动过程需要有深刻的理解,需要不断的反问自己
为什么,怎么求证,能够说服自己才能让别人信服。

另一个案例:

一台oracle 登录不上,
SQL> sqlplus / as sysdba
SQL> alter database mount;
alter database mount
*
ERROR at line 1:
ORA-00205: error in identifying control file, check alert log for more info

查看smon

[oracle@BX_133_45 trace]$ ps - ef|grep smon
oracle 1451 1 0 Feb02? 00:00:30 ora_smon_orcl
oracle 24677 1 0 17:36? 00:00:00 ora_smon_orcl
查看ORACLE_SID,ORACLE_HOME配置,没有错误
SQL>sqlplus / as sysdba
SQL>shutdown immediate;
关闭实例
[oracle@BX_133_45 trace]$ ps - ef|grep smon
oracle 1451 1 0 Feb02? 00:00:30 ora_smon_orcl

kill -9 1451 杀掉这个oracle进程

SQL>sqlplus / as sysdba
SQL>startup;
数据库正常启动

原因未明,可能是oracle进程因故没有正常结束

  1. DNS设置引起的Oracle登录延迟

先给出解决方案:

在/etc目录下,有文件resolv.conf
如果里面使用nameserver,请将其注释掉

关联案例

566.监听相关报错:TNS-12535: TNS:operation timed out、TNS-00505: Operation timed out、
ORA- 609 ,TNS- 12170

最后结论:

因为DNS配置的原因,在 /etc/resolv.conf 上配置了一个不存在的nameserver,而oracle服务
器的监听的配置文件,使用的 hostname而不是ip地址,客户端的请求发送到服务器时,
监听接受来到的请求时,都要去解析,而这个空的nameserver, 需要长时间才返回结果。从
而引起了认证超时,通过SQLNET.INBOUND_CONNECT_TIMEOUT=0(默认60S)关闭这个认
证超时。但连接依然缓慢,表现在程序上就是处理时间增加。

5.Oracle 监听SQLNET.EXPIRE_TIME参数详解

重点:
sqlnet.expire_time一定要小于防火墙的保活时间,比如防火墙设的 5 分钟,如果长连接某个
sql 超过 5 分钟未完成,则有可能被防火墙断开。则设一个小于 5 分钟的expire_time,在这
个时间段内,oracle会发出一个probe探测包,这样防火墙就会重新计算这个超时值,从而
起到保活的作用。
报错一般为:

ORA-01013: user requested cancel of current operation

在这边数据库加固有如下一个加固项,使用SQLNET.EXPIRE_TIME可以来断开在session里面
超时的状态为inactive的连接。
检查是否设置超时时间
注意事项及影响:
作用:非活动会话超过 10 分钟,连接断开
该项需要与业务侧确认是否可以操作
对于11g (如果有grid)只加固ORACLE用户下的sqlnet.ora ,如果没有则创建
该项加固后,可能在alert 文件中存在ORA-07445: exception encountered: core dump
[snstimsane()+43] 报错,(文档 ID 3934729.8),影响版本如下

序号 操作内容 操作步骤 责任人 时间

1 登陆主机 su - oracle

2

检查监听和数据库

状态

lsnrctl status
sqlplus ‘/as sysdba’
select open_mode from v$database;
3 进入oracle_home cd $ORACLE_HOME/network/admin
4 备份sqlnet.ora cp sqlnet.ora sqlnet.ora_bak
5 编辑sqlnet.ora

增加下面的内容

SQLNET.EXPIRE_TIME = 10

6 检查数据库状态 Select open_mode from v$database;

SQLNET.EXPIRE_TIME

Purpose

Use parameter SQLNET.EXPIRE_TIME to specify a the time interval, in minutes, to send a probe to
verify that client/server connections are active. Setting a value greater than 0 ensures that
connections are not left open indefinitely, due to an abnormal client termination. If the probe
finds a terminated connection, or a connection that is no longer in use, it returns an error,
causing the server process to exit. This parameter is primarily intended for the database server,
which typically handles multiple connections at any one time.
Limitations on using this terminated connection detection feature are:
It is not allowed on bequeathed connections.
Though very small, a probe packet generates additional traffic that may downgrade network
performance.
Depending on which operating system is in use, the server may need to perform additional
processing to distinguish the connection probing event from other events that occur. This can
also result in degraded network performance.
Default
0
Minimum Value
0
Recommended Value
10
Example
SQLNET.EXPIRE_TIME=10

目的
使用参数SQLNET.EXPIRE_TIME指定发送探针以验证客户端/服务器连接是否处于活动状态的
时间间隔(以分钟为单位)。设置大于 0 的值可确保由于客户端终止异常,连接无法无限期
保持打开状态。如果探测发现终止连接或不再使用的连接,则会返回错误,导致服务器进程
退出。此参数主要用于数据库服务器,该服务器通常一次处理多个连接。
使用此终止连接检测功能的限制是:
遗留连接不允许这样做。
虽然非常小,但探测数据包会产生额外的流量,可能会降低网络性能。
根据正在使用的操作系统,服务器可能需要执行额外的处理以将连接探测事件与发生的其他
事件区分开。这也可能导致网络性能下降。

默认
0
最低值
0
推荐值
10

SQLNET.EXPIRE_TIME = 10

DCD: Dead Connection Detection ,可以用于检测、标记僵死而没有断开会session,再由PMON
进行清理,释放资源。开启DCD,只需要在服务端的sqlnet.ora文件中SQLNET.EXPIRE_TIME

参数,单位为分钟。

如果时间达到这个值,server端就是发出一个”probe” packet 给客户端,如要客户断是正常
的,这个packet就被忽略,timer重新计时;如果客户端异常中断,则server端就会收到一
个消息,用以释放连接。

SQLNET.EXPIRE_TIME设置客户端连接会话超时时间(单位分钟)
定期检测客户端是否还是活动的,设置为 0 不检测
SQLNET.EXPIRE_TIME = 10

6 .kettle字符集设置引起的数据长度超长错误

起因

客户现场在使用kettle对数据进行入库时,中途报错,查看日志发现部分数据长度超出了字
段设置报错,如下

初步怀疑是字符集的问题,因为oracle数据库字符集不同,在存汉字得时候编码所占字节不
同(常见的utf8:三字节,gbk:两字节)
跟现场要了一份相同的数据导入本地测试发现确实有部分表的数据超长,但也有部分数据在
kettle日志中报错在数据库中为报错,

本地测试数据库字符集是utf8,询问了现场同事生产使用oracle数据库的字符集为GBK。按
道理如果现场库字符集是GBK,那么插入的数据长度都符合条件。同时也排除了kettle调用
的文件的字符集变码不对所导致的可能。

既然数据库方面看不出什么问题,就只有继续从kettle上下手寻找原因,于是跟现场要来了
ktr文件,拿到本地测试
终于发现原因:

原来在kettle中也有对编码方式的设置,由于kettle中设置为utf8,而数据库字符集为GBK,
所以导致了数据超长,
此处改为与数据库字符集一致的GBK后问题解决。
字符集的设置不当会导致各种奇怪的问题,常见的还有数据乱码,为了避免出现问题,建议
在使用数据库过程中,不论是文件还是客户端或其他软件,最好字符集保持一致。

7 .mysqlmetadata lock引起的锁表

起因

最近开发使用mysql库跑程序时,执行途中程序卡住不动无法继续,发现是在truncate表这
步卡住,初步判断是表被锁
进数据库中查询,通过show processlist;
发现下图状态(模拟)waiting for table metadata lock

从结果可以看到对应的truncate在等待metadatalock。

接下来大致了解下metadata lock(MDL)

官网解释:
先说下在MDL出现前的一个问题(MySQL5.5之前),假定现在有事务A正在执行DML
(insert)操作且尚未提交(执行时间长,大事务),此时另外的session开始事务B(alter
某个表字段),执行DDL操作,由于事务按提交顺序执行,事务A并不会阻止事务B的提交,
因此先提交执行事务B,此时表结构发生变化,事务A提交出错。
MDL的出现就是为了解决此类DML操作与DDL操作之间不协调导致的问题。同样是上
面的事务A事务B,事务A在执行DML操作时先获取MDL(假设类型 1 ),此时,事务A不
提交,则事务B无法获取MDL(假设类型 2 ,且与类型 1 互斥)事务B无法提交,这时如果
有事务C也是DML操作,则可以获取和类型 1 一样的共享锁,则C可以提交。等到事务A
提交或者回滚释放DML后,事务B才能提交DDL操作。
MDL 最大的作用应该就是保护一个处于事务中的表的结构不被修改。另外需要注意的
是,MDL不需要显式使用。

发现了有metadata lock等待后,就继续通过performance_schema.metadata_locks查看具体
信息
开启metadata lock并查看具体信息如下:

执行mysql> select * from performance_schema.metadata_locks\G;
请看黑框中的注释:

mysql> select * from performance_schema.metadata_locks\G(模拟结果)
*************************** 1. row ***************************
1.row表示某个查询获取到MDL的SHARED_READ锁*
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: test
OBJECT_NAME: my_test
OBJECT_INSTANCE_BEGIN: 235208064

LOCK_TYPE: SHARED_READ

LOCK_DURATION: TRANSACTION

LOCK_STATUS: GRANTED

SOURCE:

OWNER_THREAD_ID: 92

OWNER_EVENT_ID: 17

*************************** 2. row ***************************
执行DDL操作前,获取到MDL的全局意向排它锁INTENTION_EXCLUSIVE*
OBJECT_TYPE: GLOBAL
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 235207104
LOCK_TYPE: INTENTION_EXCLUSIVE
LOCK_DURATION: STATEMENT
LOCK_STATUS: GRANTED
SOURCE:
OWNER_THREAD_ID: 91
OWNER_EVENT_ID: 89
*************************** 3. row ***************************
执行DDL操作前,获取到MDL的SCHEMA级别的意向排它锁INTENTION_EXCLUSIVE
OBJECT_TYPE: SCHEMA
OBJECT_SCHEMA: test
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 235208256
LOCK_TYPE: INTENTION_EXCLUSIVE
LOCK_DURATION: TRANSACTION
LOCK_STATUS: GRANTED
SOURCE:
OWNER_THREAD_ID: 91
OWNER_EVENT_ID: 89
*************************** 4. row ***************************
DDL操作等待获取MDL的排它锁EXCLUSIVE
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: test
OBJECT_NAME: my_test
OBJECT_INSTANCE_BEGIN: 235206720
LOCK_TYPE: EXCLUSIVE
LOCK_DURATION: TRANSACTION
LOCK_STATUS: PENDING
SOURCE:
OWNER_THREAD_ID: 91
OWNER_EVENT_ID: 89

从输出的信息可以了解到,有查询已经获取到了MDL的SHARED_READ锁, truncate操作在

等待获取MDL的排他锁。

造成这种等待原因就是前面的事务中有对该表的查询未结束,或事物未提交,由于长时间未

释放,猜测到是事务未提交概率大。

由于无法从其它系统表中获取到具体是哪条sql阻塞了truncate,进而决定开启general_log日
志来查看程序直接过程中都执行了哪些sql,
是否有涉及该表的未提交事务。

开启general_log获取到如下信息:

从日志中可以看出在执行truncate操作之前,果真有一条相同表的查询操作事务未提交,后
开发人员修改此处后问题解决。

今天分享到此结束!感谢大家捧场!

相关资料,请联系DBA组索取!

下面是交流时间,大家提出问题,共同学习!

posted @ 2023-04-20 09:40  城市幽灵  阅读(173)  评论(0编辑  收藏  举报