MySQL(八)MySQL性能优化
MySQL的優化最終目的時為了更快的查詢數據,一個SQL的執行又由多個環節組成,每個環節都會消耗時間,為了提高整個SQL的執行效率,就需要從每一個環節入手。
SQL的執行流程:
配置優化--連接
執行SQL的第一步是客戶端連接到服務端。
在連接的時候可能出現服務端連接數不夠導致應用程序獲取不到連接。比如報了一個 Mysql: error 1040: Too many connections 的錯誤。
可以從兩個方面來解決連接數不夠的問題:
1.從服務端來說,我們可以增加服務端的可用連接數。
(1)修改配置參數增加可用連接數,修改max_connections的大小
(2)及時釋放不活動的連接。交互式和非交互式的客戶端的默認超時時間都是28800秒,8小時,我們可以把這個值調小
show variables like ?'max_connections '; -- 修改最大連接數
show global variables like 'wait_timeout'; --及時釋放不活動的連接
2、從客戶端來說,可以減少從服務端獲取的連接數,可以引入連接池,實現連接的重用。
我們可以在ORM層面使用連接池(MyBatis自帶了一個連接池)或者使用專用的連接池工具(DBCP、C3P0、阿里的Druid等)。
注意:連接池并不是越大越好,只要維護一定數量大小的連接池,其他的客戶端排隊等待獲取連接就可以了。
每一個連接,服務端都需要創建一個線程去處理它。連接數越多,服務端創建的線程數就會越多。
如果線程數遠遠超過它的核數大小,那么就需要通過時間片進行頻繁的上下文切換來執行這些線程,這會造成比較大的性能開銷。
在Hikari的github文檔中,給出了一個PostgreSQL數據庫建議的設置連接池大小的公式:
https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
它的建議是機器核數乘以2加1。也就是說,4 核的機器,連接池維護9個連接就夠了。
架構優化
緩存
我們可以引入緩存來緩解數據庫的壓力,從而達到提升數據庫性能的效果,比方說Redis或者自己在應用中對熱點數據做緩存都是可行的
主從復制
如果單臺數據庫服務滿足不了訪問需求,那我們可以做數據庫的集群方案。
這就涉及到不同節點的數據一致性,需要使用MySQL的主從復制來實現
其原理就是依賴MySQL數據庫的binlog,每次操作數據庫的SQL都會記錄到binlog,這是一種邏輯日志。
從服務器會獲取主服務器的 binlog文件,然后解析里面的 SQL語句,在從服務器上面執行一遍,從而保持主從數據庫的一致性
I/O Thread : 連接到master獲取binlog,并且解析binlog寫入中繼日志
Log Dump Thread :?發送binlog給slave
SQL Thread : 讀取relay log,把數據寫入到數據庫
有了主從復制,我們就可以只把數據寫入master節點,而查詢的請求可以分擔到slave節點。這種方案就叫做讀寫分離
讀寫分離可以一定程度低減輕對數據庫服務器的訪問壓力,但是需要特別注意主從復制的數據同步延遲問題
為什么會有主從復制數據同步延遲呢
在早期的MySQL中,slave的SQL線程是單線程,而Master是可以支持SQL語句的并行執行的。那么從數據庫的SQL卻只能單線程排隊執行,在master并發量很大的情況下,同步數據肯定會出現延遲。
之所以SQL Thread不設計成多線程的,是因為binlog只有一個,而不是所有的SQL都可以隨機執行的。比方說用戶首先新增了書數據,然后又進行了刪除,這兩個SQL在從庫的執行順序肯定是不能顛倒的
怎么減少主從復制的延遲?
半同步復制
在主從復制的過程中,MySQL默認是異步復制的。也就是說,對于Master節點來說,只要寫入binlog,事務結束,就直接返回數據結果給客戶端了。master不關心slave的數據有沒有寫入成功。
如果要減少延遲,那是不是就可以等待全部從庫的數據執行完畢,再返回給客戶端,這樣的方式叫做全同步復制。從庫寫完數據,主庫才返會給客戶端。
這種方式雖然可以保證在讀之前,數據已經同步成功了,但是會導致SQL執行的時間變長,使得master節點性能下降。
既然這樣,那么不妨考慮折中一下--- 一種介于異步復制和全同步復制之間的半同步復制
所謂的半同步復制就是在主庫執行完客戶端提交的SQL后不是立刻返回給客戶端,而是等待至少一個從庫接收到bin log并寫到relay log中才返回給客戶端。master不需要等待很長的時間,但是返回給客戶端的時候,數據就即將寫入成功了,因為它只剩最后一步了----讀取relaylog,寫入從庫。
對于多個slave,必然有多個I/O Thread同時從master獲取binlog。因為多個slave的復制是并行的,所以至少有一個slave寫入到relay log時,代表其他slave可能也已經接收了bin log。這個slave返回給master后,客戶端發起查詢,其他slave很大概率也已經應用到數據庫了。當然在極端情形下可能就只有那么一個slave節點即將寫入,如果查詢的時候負載到其他從庫,那么可能還是會出現數據不一致的問題,但是半同步復制本身就是一種折中的方案,目的是為了減少主從復制的延遲,并不能保證消除主從復制的延遲
如果我們要在數據庫里面用半同步復制,需要安裝一個插件,這個是谷歌的一位工程師貢獻的。目錄如下:
需要在從庫和主庫分別執行如下命令:
-- 主庫執行 INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; set global rpl_semi_sync_master_enabled=1; show variables like '%semi_sync%';-- 從庫執行 INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; set global rpl_semi_sync_slave_enabled=1; show global variables like '%semi%';相對于異步復制,半同步復制提高了數據的安全性,但它同時也不可避免的揮有一定程度的延遲,因為需要等待一個slave寫入中繼日志。所以,半同步復制最好在低延時的網絡中使用。
多庫并行復制
如果3條語句是在三個數據庫執行,操作各自的數據庫,就不需要會產生并發的問題
所以如果是操作三個數據庫,這三個數據庫的從庫的SQL線程可以并發執行。這是MySQL5.6版本里面支持的多庫并行復制。
但是在大部分的情況下,我們都是單庫多表的情況,這就需要在一個數據庫里面實現并行復制。我們的主庫本身就是支持多個事務同時操作的,那么在主庫上可以并行執行的事務,在從庫上肯定也是可以并行執行。
異步復制之 GTID 復制?
https://dev.mysql.com/doc/refman/5.7/en/replication-gtids.html
我們可以把那些在主庫上并行執行的事務,分為一個組,并且給他們編號,這一個組的事務在從庫上也可以并行執行。這個編號,我們把它叫做 GTID(Global Transaction Identifiers),這種主從復制的方式,叫做基于GTID的復制
如果我們要使用GTID復制,我們可以通過修改配置參數來打開它,默認是關閉的:
show global variables like 'gtid_mode';
分庫分表
垂直分庫,減少并發壓力。水平分表,解決存儲瓶頸。
垂直分庫的做法,把一個數據庫按照業務拆分成不同的數據庫:比方說用戶表和訂單表分別存儲在用戶數據庫和訂單數據庫里
水平分庫分表的做法,就是把單張表的數據按照一定的規則分布到多個數據庫。
通過主從或者分庫分表都可以減少單個數據庫節點的訪問壓力和存儲壓力,達到提升數據庫性能的目的。
但是如果master節點掛了,所有的設計就都沒用了,那就需要高可用方案
高可用
基于主從復制 :傳統的HAProxy + keepalived的方案,?
MGR
https://dev.mysql.com/doc/refman/5.7/en/group-replication.html
https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster.html
MySQL 5.7.17 版本推出的 InnoDB Cluster,也叫 MySQL Group Replicatioin(MGR)。
高可用HA方案需要解決的問題就是當一個master節點宕機的時候,如何選舉一個數據最新的slave成為master。如果同時運行多個master,又必須要解決master之間的數據復制問題,以及對于客戶端來說連接路由的問題。各個服務的高可用方案的本質都是差不多的(比放說MySQL,Redis等)
SQL 語句分析與優化 —— 優化器
慢查詢日志 slow query log?
https://dev.mysql.com/doc/refman/5.7/en/slow-query-log.html
1.打開慢查詢開關
因為開啟慢查詢日志是會消耗資源,所以它默認是關閉的:
#查看慢查詢開關 show variables like 'slow_query%';#控制執行超過多長時間的SQL才記錄到慢日志,默認是10秒。 show variables like '%long_query_time%';我們可以直接動態修改參數(重啟后會失效)。 set@@global.slow_query_log=1; --1 開啟,0 關閉 set@@global.long_query_time=3; -- mysql 默認的慢查詢時間是 10 秒或者修改配置文件my.cnf,定義了慢查詢日志的開關、慢查詢的時間、日志文件的存放路徑。 slow_query_log=ON long_query_time=2 slow_query_log_file=/var/lib/mysql/localhost-slow.log2.慢查詢日志分析
通過show variables like 'slow_query%';可以查看慢查詢日志的位置
可以通過mysqldumpslow來分析慢日志,mysqldumpslow 由MySQL提供,在MySQL的bin目錄下。
例如:查詢用時最多的20條慢SQL:
mysqldumpslow -s t -t 20 -g 'select' /var/lib/mysql/2064471e0de5-slow.log
還可以通過SHOW PROFILE來查看SQL語句執行的時候使用的資源,比如CPU、IO的消耗情況。
https://dev.mysql.com/doc/refman/5.7/en/show-profile.html?
#查看是否開啟 1--開啟 0--未開啟 select@@profiling; set@@profiling=1;查看統計信息 show profiles;
查看最后一個SQL的執行詳細信息 : show profile
根據ID查看執行詳細信息,在后面帶上for query + ID eg. show profile for query 1 ;
#其他系統命令 #查看用戶運行線程 show processlist;#查看服務器運行狀態 show status ;#查看存儲引擎運行信息 #show engine用來顯示存儲引擎的當前運行信息,包括事務持有的表鎖、行鎖信息;事務的鎖等待情況; #線程信號量等待;文件IO請求;buffer pool統計信息等 show engine ;eg.查看innodb的運行信息 show engine innodb status;#我們還可以將監控信息輸出到錯誤信息errorlog中(15秒鐘一次) show variables like 'innodb_status_output%'; #開啟輸出 SET GLOBAL innodb_status_output=ON; SET GLOBAL innodb_status_output_locks=ON;知道了慢SQL之后,就需要具體分析SQL為什么會慢了。通過EXPLAIN 我們可以模擬優化器執行SQL 語句的過程,來知道 MySQL 是怎么處理一條SQL語句的。MySQL5.6.3以前只能分析SELECT; MySQL5.6.3以后就可以分析update、 delete、
insert 了
EXPLAIN 執行計劃
創建SQL CREATE TABLE `course` (`cid` int(3) DEFAULT NULL,`cname` varchar(20) DEFAULT NULL,`tid` int(3) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE `teacher` (`tid` int(3) DEFAULT NULL,`tname` varchar(20) DEFAULT NULL,`tcid` int(3) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE `teacher_contact` (`tcid` int(3) DEFAULT NULL,`phone` varchar(200) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;INSERT INTO `course` VALUES ('1', 'mysql', '1'); INSERT INTO `course` VALUES ('2', 'jvm', '1'); INSERT INTO `course` VALUES ('3', 'mybatis', '2'); INSERT INTO `course` VALUES ('4', 'spring', '3');INSERT INTO `teacher` VALUES ('1', 'Bob', '1'); INSERT INTO `teacher` VALUES ('2', 'mon', '2'); INSERT INTO `teacher` VALUES ('3', 'chenpp', '3');INSERT INTO `teacher_contact` VALUES ('1', '12221323232'); INSERT INTO `teacher_contact` VALUES ('2', '13392729343'); INSERT INTO `teacher_contact` VALUES ('3', '18923723923');1.id?查詢序列編號
id不同時,先執行id大的;id相同則從上往下執行
執行如下SQL:
id不同時,先執行id大的
id相同則從上往下執行
數據量不同的時候順序會發生變化 -- 這個是由笛卡爾積決定的
因為MySQL要把查詢的結果,包括中間結果和最終結果都保存到內存,所以MySQL會優先選擇中間結果數據量比較小的順序進行查詢(簡單理解小表驅動大表)
select type 查詢類型?
SIMPLE: 簡單查詢,不包含子查詢,不包含關聯查詢union
EXPLAIN SELECT * FROM teacher
前面執行的子查詢SQL:
PRIMARY
子查詢SQL語句中的主查詢,也就是最外面的那層查詢。
SUBQUERY
子查詢中所有的內層查詢都是SUBQUERY類型的。
DERIVED
衍生查詢,表示在得到最終查詢結果之前會用到臨時表
-- 查詢ID為1或2的老師教授的課程 EXPLAIN SELECT cr.cname FROM ( SELECT * FROM course WHERE tid = 1 UNION SELECT * FROM course WHERE tid = 2 ) cr;UNION
使用了UNION的SQL,如上
對于關聯查詢,先執行右邊的 table(UNION),再執行左邊的 table,類型是DERIVED
UNION RESULT:主要是顯示哪些表之間存在UNION 查詢。<union2,3>代表 id=2 和 id=3 的查詢存在UNION
type 連接類型?
https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types
在常用的鏈接類型中:system > const > eq_ref > ref > range > index > all
以上訪問類型除了all,都能用到索引。
Const: 主鍵索引或者唯一索引,只能查到一條數據的SQL。
CREATE TABLE single_data( id int(3) PRIMARY KEY, content varchar(20) ); insert into single_data values(1,'a'); insert into single_data values(2,'b');EXPLAIN SELECT * FROM single_data where id=1;System : system是const的特例:只有一行滿足條件。eg.只有一條數據的系統表。
EXPLAIN SELECT * FROM mysql.proxies_priv;eq_ref : 通常出現在多表的 join 查詢,表示對于前表的每一個結果,,都只能匹配到后表的一行結果。一般是唯一性索引的查詢(UNIQUE或PRIMARY KEY)。
#為teacher_contact表的tcid(第一個字段)創建主鍵索引。 ALTER TABLE teacher_contact ADD PRIMARY KEY(tcid);#為teacher表的tcid(第三個字段)創建普通索引。 ALTER TABLE teacher ADD INDEX idx_tcid (tcid);explain select t.tcid from teacher t,teacher_contact tc where t.tcid = tc.tcid;以上三種system,const,eq_ref,都是可遇而不可求的,基本上很難優化到這個狀態。
Ref: 查詢用到了非唯一性索引,或者關聯操作只使用了索引的最左前綴。
#使用前面創建的teacher上的tcid字段的普通索引查詢: explain SELECT * FROM teacher where tcid=3;Range: 索引范圍掃描。
? ? ? ? ? ? ? ?如果where后面是 betweenand 或 <或 > 或 >= 或 <=或in這些, type類型就為range
#添加索引,否則會變成全表掃描 ALTER TABLE teacher ADD INDEX idx_tid(tid);EXPLAIN SELECT * FROM teacher t WHERE t.tid <3;IN查詢想要走range要求字段上有主鍵索引
EXPLAIN SELECT * FROM teacher_contact t WHERE tcid in (1,2,3);Index : Full Index Scan,查詢全部索引中的數據(比不走索引要快)
EXPLAIN SELECT tid FROM teacher;All : FullTableScan,如果沒有索引或者沒有用到索引,type就是ALL。代表全表掃描
NULL: 不用訪問表或者索引就能得到結果
EXPLAIN select 1 from dual where 1=1;小結:
一般來說,需要保證查詢至少達到range級別,最好能達到ref。ALL(全表掃描)和index(查詢全部索引)都是需要優化的。
possible_key、key?
可能用到的索引和實際用到的索引。如果是NULL就代表沒有用到索引。
possible_key可以有一個或者多個,表示可能用到的索引,不代表一定用到索引
反過來,possible_key為空,key可能有值嗎? 可能
CREATE TABLE `user_innodb` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`phone` varchar(11) DEFAULT NULL,PRIMARY KEY (`id`),KEY `comidx_name_phone` (`name`,`phone`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;insert into user_innodb values (1,'chenpp','18818212112'); insert into user_innodb values (2,'bob','152818217212'); insert into user_innodb values (3,'mon','13823723922'); explain select phone from user_innodb where phone='126';
這里是覆蓋索引的情況。注意這里雖然使用到了索引,但是index的類型(查詢全部索引中的數據),因為查詢的字段就是索引字段,所以不需要進行全表掃描,但是這種情形也是需要優化的
key_len
索引的長度(使用的字節數)。跟索引字段的類型、長度有關。?
rows
MySQL認為掃描多少行才能返回請求的數據,是一個預估值。一般來說行數越少越好。?
filtered?
這個字段表示存儲引擎返回的數據在server層過濾后,剩下多少滿足查詢的記錄數量的比例,它是一個百分比。這個比例越高越高,說明返回給server層的數據有效數據比例越大,SQL性能越好
ref
使用哪個列或者常數和索引一起從表中篩選數據。?
Extra
執行計劃給出的額外的信息說明。
using index
表示用到了覆蓋索引,不需要回表。
EXPLAIN SELECT tid FROM teacher;
using where
使用了where過濾,表示存儲引擎返回的記錄并不是所有的都滿足查詢條件,需要在server層進行過濾(跟是否使用索引沒有關系),此時filtered也可能為100。
Using index condition(索引條件下推)
這個在索引里就講過了
MySQL(四)索引的使用--索引下推
using filesort
表示不能使用索引來排序,用到了額外的排序(跟磁盤或文件沒有關系)。這是需要優化的。
EXPLAIN select * from user_innodb where name ='chen' order by id;using temporary
使用到了臨時表。以下不是全部的情況:
1、distinct 非索引列
2、group by 非索引列
3、使用join的時候,group任意列
EXPLAIN select t.tid from teacher t join course c on t.tid = c.tid group by t.tid;
這種情形下也是需要優化的,比如創建復合索引。
當我們的SQL語句比較復雜,有多個關聯和子查詢的時候,就要分析SQL語句有沒有改寫的方法。
舉個例子,一模一樣的數據:
-- 大偏移量的 limit
select * from user_innodb limit 900000,10;
?-- 改成先過濾 ID,再 limit
select * from user_innodb where id>=900000 limit 10;
存儲引擎
選擇存儲引擎
為不同的業務表選擇不同的存儲引擎,例如:查詢插入操作多的業務表用MyISAM。臨時數據用Memeroy。常規的并發大更新多的表用InnoDB
分區或分表
分區不推薦,比較建議分表
字段定義
原則:使用可以正確存儲數據的最小數據類型。為每一列選擇合適的字段類型?
- 整數類型 :?INT有8種類型,不同的類型的最大存儲范圍是不一樣的。像性別 就推薦使用TINYINT。
- 字符類型:變長情況下,varchar更節省空間,但是對于varchar字段,需要一個字節來記錄長度。所以固定長度的用char,不要 ? ? ? ? ? ? ? ? ? ? ? 用varchar
- 非空: 非空字段盡量定義成NOT NULL,提供默認值,或者使用特殊值、空串代替null。因為NULL類型的存儲、優化、使用都 ? ? ? ? ? ? ? ?會存在問題。
- 不建議使用外鍵、觸發器、視圖 : 會降低了可讀性;影響數據庫性能,可以把計算的事情交給程序,數據庫專注于存儲;
- 大文件存儲: 不要用數據庫存儲圖片(比如base64編碼)或者大文件; 把文件放在文件服務器上,數據庫只需要存儲URI就行
- 表拆分: ?將不常用的字段拆分出去,避免列數過多和數據量過大。
總結
除了上面的對于代碼、SQL語句、表定義、架構、配置優化之外,業務層面的優化也不能忽視。比方說雙十一之前開啟預售就助于分流。
在應用層面同樣有很多其他的方案來優化,達到減輕數據庫的壓力的目的,比如限流,或者引入MQ削峰等等
總結
以上是生活随笔為你收集整理的MySQL(八)MySQL性能优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL(七)关于MySQL不同版本下
- 下一篇: MySQL(二)InnoDB的内存结构和