MySQL千萬級資料處理
目錄
第一篇,優化篇
單表優化
除非單表資料未來會一直不斷上漲,否則不要一開始就考慮拆分,拆分會帶來邏輯、部署、運維的各種複雜度,一般以整型值為主的表在千萬級以下,字串為主的表在五百萬以下是沒有太大問題的。而事實上很多時候MySQL單表的效能依然有不少優化空間,甚至能正常支撐千萬級以上的資料量:
欄位
1、儘量使用TINYINT、SMALLINT、MEDIUM_INT作為整數型別而非INT,如果非負則加上UNSIGNED
2、VARCHAR的長度只分配真正需要的空間
3、使用列舉或整數代替字串型別
4、儘量使用TIMESTAMP而非DATETIME,
5、單表不要有太多欄位,建議在20以內
6、避免使用NULL欄位,很難查詢優化且佔用額外索引空間
7、用整型來存IP
索引
索引的種類:
1、主鍵索引 (把某列設為主鍵,則就是主鍵索引)
2、唯一索引(unique) (即該列具有唯一性,同時又是索引)
3、index (普通索引)
4、全文索引(FULLTEXT)
5、複合索引(多列和在一起)
索引建立注意事項:
1、索引並不是越多越好,要根據查詢有針對性的建立,考慮在WHERE和ORDER BY命令上涉及的列建立索引,可根據EXPLAIN來檢視是否用了索引還是全表掃描
2、應儘量避免在WHERE子句中對欄位進行NULL值判斷,否則將導致引擎放棄使用索引而進行全表掃描如:select id from t where num is null 可以在num上設定預設值0,確保表中num列沒有null值,然後這樣查詢:select id from t where num = 0
3、值分佈很稀少的欄位不適合建索引,例如"性別"這種只有兩三個值的欄位
4、字元欄位只建字首索引
5、並不是所有索引對查詢都有效,SQL是根據表中資料來進行查詢優化的,當索引列有大量資料重複時,SQL查詢可能不會去利用索引,如一表中有欄位 sex,male、female幾乎各一半,那麼即使在sex上建了索引也對查詢效率起不了作用。
6、應儘可能的避免更新 clustered 索引資料列,因為 clustered 索引資料列的順序就是表記錄的物理儲存順序,一旦該列值改變將導致整個表記錄的順序的調整,會耗費相當大的資源。若應用系統需要頻繁更新 clustered 索引資料列,那麼需要考慮是否應將該索引建為 clustered 索引。
7、應儘量避免在 where 子句中使用!=或<>操作符,否則將引擎放棄使用索引而進行全表掃描。
8、儘量避免在 where 子句中使用 or 來連線條件,否則將導致引擎放棄使用索引而進行全表掃描,如:select id from t where num = 10 or num = 20
可以這樣查詢:
select id from t where num = 10
union all
select id from t where num = 20
9、下面的查詢也將導致全表掃描:select id from t where name like ‘%abc%’
若要提高效率,可以考慮全文檢索。
10、in 和 not in 也要慎用,否則會導致全表掃描,如:select id from t where num in(1,2,3)
對於連續的數值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
11、如果在 where 子句中使用引數,也會導致全表掃描。因為SQL只有在執行時才會解析區域性變數,但優化程式不能將訪問計劃的選擇推遲到執行時;它必須在編譯時進行選擇。然而,如果在編譯時建立訪問計劃,變數的值還是未知的,因而無法作為索引選擇的輸入項。如下面語句將進行全表掃描:select id from t where num = @num
可以改為強制查詢使用索引:
select id from t with(index(索引名)) where num = @num
12、不要在 where 子句中的“=”左邊進行函式、算術運算或其他表示式運算,否則系統將可能無法正確使用索引。如:select id from t where num / 2 = 100
select id from t where substring(name, 1 ,3) = ’abc’查詢name以abc開頭的id列表
分別應改為:
select id from t where num = 100 * 2
select id from t where name like ‘abc%’
13、在使用索引欄位作為條件時,如果該索引是複合索引,那麼必須使用到該索引中的第一個欄位作為條件時才能保證系統使用該索引,否則該索引將不會被使用,並且應儘可能的讓欄位順序與索引順序相一致。
14、很多時候用 exists 代替 in 是一個好的選擇:select num from a where num in(select num from b)
用下面的語句替換:
select num from a where exists(select 1 from b where num = a.num)
引擎
目前廣泛使用的是MyISAM和InnoDB兩種引擎:
MyISAM:
MyISAM引擎是MySQL 5.1及之前版本的預設引擎,它的特點是:
- 不支援行鎖,讀取時對需要讀到的所有表加鎖,寫入時則對錶加排它鎖
- 不支援事務
- 不支援外來鍵
- 不支援崩潰後的安全恢復
- 在表有讀取查詢的同時,支援往表中插入新紀錄
- 支援BLOB和TEXT的前500個字元索引,支援全文索引
- 支援延遲更新索引,極大提升寫入效能
- 對於不會進行修改的表,支援壓縮表,極大減少磁碟空間佔用
- 建立一張表,對會應三個檔案, *.frm 記錄表結構, *.myd 資料, *.myi 索引檔案
InnoDB:
InnoDB在MySQL 5.5後成為預設索引,它的特點是:
- 支援行鎖,採用MVCC來支援高併發
- 支援事務
- 支援外來鍵
- 支援崩潰後的安全恢復
- 不支援全文索引
- 建立一張表,對會應一個檔案 *.frm,資料存放到ibdata1
總體來講,MyISAM適合SELECT密集型的表,而InnoDB適合INSERT和UPDATE密集型的表
其他注意事項:
1、AND型查詢要點(排除越多的條件放在前面):假設要查詢滿足條件A,B和C的文件,滿足A的文件有4萬,滿足B的有9K,滿足C的是200,那麼應該用C and B and A 這樣只需要查詢200條記錄。
2、OR型查詢要點(符合越多的條件放在前面):OR型查詢與AND查詢恰好相反,匹配最多的查詢語句放在最前面。
3、查詢資料不建議使用 select * from table ,應用具體的欄位列表代替“*”,不要返回用不到的無關欄位,尤其是大資料列。
4、在分頁查詢中使用 limit關鍵字時,應重複考慮使用索引欄位來篩選來避免全表掃描,如:
select c1, c2, c3 from table order by id asc limit 100, 100
應儘量配合where條件來使用(大資料量情況查詢效率提升10倍):
select c1, c2, c3 from table where id > 100 order by id asc limit 0, 100
5、在新建臨時表時,如果一次性插入資料量很大,那麼可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果資料量不大,為了緩和系統表的資源,應先 create table,然後再insert。
6、如果使用到了臨時表,在儲存過程的最後務必將所有的臨時表顯式刪除,先 truncate table ,然後 drop table ,這樣可以避免系統表的較長時間鎖定。但是,避免頻繁建立和刪除臨時表,以減少系統表資源的消耗。
7、儘量避免使用遊標,因為遊標的效率較差,如果遊標操作的資料超過1萬行,那麼就應該考慮改寫。因此,使用基於遊標的方法或臨時表方法之前,應先尋找基於集的解決方案來解決問題,基於集的方法通常更有效。
8、與臨時表一樣,遊標並不是不可使用。對小型資料集使用 FAST_FORWARD 遊標通常要優於其他逐行處理方法,尤其是在必須引用幾個表才能獲得所需的資料時。在結果集中包括“合計”的例程通常要比使用遊標執行的速度快。如果開發時間允許,基於遊標的方法和基於集的方法都可以嘗試一下,看哪一種方法的效果更好。
9、在所有的儲存過程和觸發器的開始處設定 SET NOCOUNT ON ,在結束時設定 SET NOCOUNT OFF 。無需在執行儲存過程和觸發器的每個語句後向客戶端傳送 DONE_IN_PROC 訊息。
10、為提高系統併發能力,應儘量避免大事務操作,儘量避免向客戶端返回大資料量,若資料量過大,應該考慮相應需求是否合理。
引數調優:
wait_timeout:
資料庫連線閒置時間(長連線),閒置連線會佔用記憶體資源。可以從預設的8小時減到半小時。
max_user_connection:
最大連線數,預設為0(無上限),最好設一個合理上限。
thread_concurrency:
併發執行緒數,設為CPU核數的兩倍。
key_buffer_size:
索引塊的快取大小,增加會提升索引處理速度,對MyISAM表效能影響最大。對於記憶體4G左右,可設為256M或384M,通過查詢show status like 'key_read%',保證key_reads / key_read_requests在0.1%以下最好
innodb_buffer_pool_size:
快取資料塊和索引塊,對InnoDB表效能影響最大。通過查詢 show status like 'Innodb_buffer_pool_read%',
保證 (Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests 越高越好
read_buffer_size:
MySql讀入緩衝區大小。對錶進行順序掃描的請求將分配一個讀入緩衝區,MySql會為它分配一段記憶體緩衝區。如果對錶的順序掃描請求非常頻繁,可以通過增加該變數值以及記憶體緩衝區大小提高其效能
sort_buffer_size:
MySql執行排序使用的緩衝大小。如果想要增加ORDER BY的速度,首先看是否可以讓MySQL使用索引而不是額外的排序階段。如果不能,可以嘗試增加sort_buffer_size變數的大小
其他引數
讀寫分離
也是目前常用的優化,從庫讀主庫寫,一般不要採用雙主或多主引入很多複雜性,儘量採用文中的其他方案來提高效能。同時目前很多拆分的解決方案同時也兼顧考慮了讀寫分離。
分庫分表
水平拆分
垂直拆分
升級硬體
根據MySQL是CPU密集型還是I/O密集型,通過提升CPU和記憶體、使用SSD,都能顯著提升MySQL效能。
快取應用
MySQL內部:
在系統調優引數介紹了相關設定
資料訪問層:
比如MyBatis針對SQL語句做快取,而Hibernate可以精確到單個記錄,這裡快取的物件主要是持久化物件 Persistence Object
應用服務層:
這裡可以通過程式設計手段對快取做到更精準的控制和更多的實現策略,這裡快取的物件是資料傳輸物件Data Transfer Object
Web層:針對web頁面做快取
使用者端的快取:
可以根據實際情況在一個層次或多個層次結合加入快取。這裡重點介紹下服務層的快取實現,目前主要有兩種方式:1、直寫式(Write Through):在資料寫入資料庫後,同時更新快取,維持資料庫與快取的一致性。這也是當前大多數應用快取框架如Spring Cache的工作方式。這種實現非常簡單,同步好,但效率一般。2、回寫式(Write Back):當有資料要寫入資料庫時,只會更新快取,然後非同步批量的將快取資料同步到資料庫上。這種實現比較複雜,需要較多的應用邏輯,同時可能會產生資料庫與快取的不同步,但效率非常高。
第二篇,案例篇
實際案例:
比如針對 1000w 條老資料加密處理
解決的思路:
查詢優化 + 記憶體調優 + 分批處理 + 高效資料來源 + 多執行緒(執行緒池) + 反覆除錯
考慮客觀因素:
硬體設施、網路寬頻
假設1000w,分配20個執行緒,每頁1萬條
則分頁總數:1000 / 1 = 1000頁
執行緒平均處理分頁數:(int)Math.floor(1000 / 20) = 50頁
執行緒處理最大分頁數:50頁 + 1000 % 20 = 50頁
則第一個執行緒處理 1 - 20頁,
第二個執行緒處理 21- 40頁,
第三個執行緒處理 41- 60頁,
...
依次內推,第三十個執行緒,處理981 - 1000頁
程式碼片段:
/**
* maysql 多執行緒 + 連線池,分頁查詢 + 批量更新示例
* 注意:limit 引數不支援使用佔位符?
*
* 10個執行緒,60萬條資料,每頁1w條,進行批量更新總耗時:53558毫秒,約0.90分鐘(Where條件 + Limit N )
* 20個執行緒,60萬條資料,每頁1w條,進行批量更新總耗時:53558毫秒,約0.89分鐘(Where條件 + Limit N )
* 20個執行緒,200萬條資料,每頁1w條,進行批量更新總耗時:160132毫秒,約2.67分鐘(Where條件 + Limit N )
* 40個執行緒,200萬條資料,每頁1w條,進行批量更新總耗時:199220毫秒,約3.32分鐘(Where條件 + Limit N )
*
* 截止4月27日finance_ant_loan資料庫資料:
* LendingDetail, 總記錄數 8729
* LoanDetail, 總記錄數 550417
*
* 截止4月27日finance_jd_loan資料庫資料:
* CUS, 總記錄數 1006179
* Loan,總記錄數 32395990
*/
@Test
public void testMultiThreadBatchUpdate2(){
Long beginTime = System.currentTimeMillis();
int pageSize = 10000;
int threads = 20;
int resultCount = findCount();
if (resultCount <= 0){
log.info("未找到符合條件的記錄!");
return;
}
int pageTotal = (resultCount % pageSize == 0) ? (resultCount / pageSize) : ((int)Math.floor(resultCount / pageSize) + 1);
log.info("查詢出資料庫總計錄數:{}", resultCount);
log.info("每頁數量:{}", pageSize);
log.info("分頁總數:{}", pageTotal);
threads = getActualThreads(pageTotal, threads);
int avgPages = (int)Math.floor(pageTotal / threads);
int restPages = pageTotal % threads;
log.info("子執行緒數:{}", threads);
log.info("子執行緒平均處理分頁數:{}", avgPages);
log.info("子執行緒處理最大分頁數:{}", avgPages + restPages);
MultiThreadHandler handler = new MultiParallelThreadHandler();
for (int i = 0; i < threads; i++){
int fromPage = i * avgPages + 1;
int endPage = i * avgPages + avgPages;
if (i == threads - 1) {
endPage = endPage + restPages;
}
String threadName = "thread" + (i + 1);
log.info("Query child thread:{} process paging interval: [{}, {}]", threadName, fromPage, endPage);
handler.addTask(new TestThread2(fromPage, endPage, pageSize));
}
try {
handler.run();
} catch (ChildThreadException e) {
log.error(e.getAllStackTraceMessage());
}
log.info("【分頁查詢+批量更新】結束,受影響總記錄數:{},總耗時:{}毫秒", resultCount, System.currentTimeMillis() - beginTime);
}
private int findCount(){
Connection connection = AntConnPool.getConnection();
PreparedStatement preparedStatement;
try {
preparedStatement = connection.prepareStatement("SELECT COUNT(1) FROM test2 WHERE 1 = 1");
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()){
return resultSet.getInt(1);
}
}catch (Exception e){
e.printStackTrace();
log.error("資料庫操作異常!!!");
}finally {
ConnMgr.closeConn(connection);
}
return 0;
}
/**
* @description 獲得實際執行緒數
* @author Zack
* @date 15:28 2018/5/7
* @param pageTotal
* @param threads
* @return int
*/
private int getActualThreads(final int pageTotal, final int threads){
if ((int)Math.floor(pageTotal / threads) < 1 && threads > 1){
return getActualThreads(pageTotal, threads - 1);
}
return threads;
}
/**
*@description 多執行緒處理分頁
*@author Zack
*@date 2018/4/27
*@version 1.0
*/
@Slf4j
public class TestThread2 extends MysqlParallelThread{
public TestThread2(int fromPage, int endPage, int pageSize){
super(fromPage, endPage, pageSize);
}
@Override
public void run() {
log.info("Query child thread:{} process paging interval: [{}, {}] started.", Thread.currentThread().getName(), getFromPage(), getEndPage());
Long beginTime = System.currentTimeMillis();
int maxId = 0;
int fromIndex = (getFromPage() - 1) * getPageSize();
Connection connection = AntConnPool.getConnection();
try{
for (int pageNo = getFromPage(); pageNo <= getEndPage(); pageNo++){
if (maxId != 0){
fromIndex = 0;
}
maxId = batchUpdate(findList(connection, fromIndex, maxId), pageNo);
}
}catch (Exception e){
throw new RuntimeException(Thread.currentThread().getName() + ": throw exception");
}finally {
ConnMgr.closeConn(connection);
log.info("Query child thread:{} process paging interval: [{}, {}] end. cost:{} ms.", Thread.currentThread().getName(), getFromPage(), getEndPage(), (System.currentTimeMillis() - beginTime));
}
}
private ResultSet findList(Connection connection, int fromIndex, int maxId){
try {
PreparedStatement preparedStatement = connection.prepareStatement("SELECT id, name, total FROM test2 WHERE id > ? ORDER BY id ASC limit "+fromIndex+", " + getPageSize(), ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
preparedStatement.setInt(1, maxId);
return preparedStatement.executeQuery();
}catch (Exception e){
e.printStackTrace();
log.error("資料庫操作異常!!!");
}
return null;
}
private int batchUpdate(ResultSet resultSet, int pageNo) {
Long beginTime = System.currentTimeMillis();
Connection connection = AntConnPool.getConnection();
PreparedStatement preparedStatement;
try {
connection.setAutoCommit(false);
preparedStatement = connection.prepareStatement("UPDATE test2 SET name = ?, total = ? WHERE id = ?");
if (Objects.isNull(resultSet) || resultSet.wasNull()){
log.info("查詢第{}頁資料為空!", pageNo);
return 0;
}
while (resultSet.next()){
// 加密處理
preparedStatement.setString(1, SecUtil.encryption(resultSet.getString(2)));
preparedStatement.setInt(2, resultSet.getInt(3) + 1);
preparedStatement.setInt(3, resultSet.getInt(1));
preparedStatement.addBatch();
}
int[] countArray = preparedStatement.executeBatch();
connection.commit();
// 遊標移至最後一行
resultSet.last();
int maxId = resultSet.getInt(1);
log.info("子執行緒{}批量更新MYSQL第{}頁結束,maxId:{}, 受影響記錄數:{},耗時:{}毫秒", Thread.currentThread().getName(), pageNo, maxId, countArray.length, System.currentTimeMillis() - beginTime);
return maxId;
}catch (Exception e){
e.printStackTrace();
log.error("資料庫操作異常!!!");
}finally {
ConnMgr.closeConn(connection);
}
return 0;
}
}
總結:
使用多執行緒合理分配執行任務,避免資料重複執行和漏執行。
擴充套件閱讀