一、mysql的分表策略
根據經驗,Mysql表數據一般達到百萬級別,查詢效率會很低,容易造成表鎖,甚至堆積很多連接,直接掛掉;
1,水平分割:
水平(橫向)拆分:將同一個表的數據進行分塊保存到不同的數據庫中,來解決單表中數據量增長出現的壓力。
表結構設計水平切分。常見的一些場景包括
a). 比如在線電子商務網站,訂單表數據量過大,按照年度、月度水平切分
b). Web 2.0網站注冊用戶、在線活躍用戶過多,按照用戶ID范圍等方式,將相關用戶以及該用戶緊密關聯的表做水平切分
c). 例如論壇的置頂帖子,因為涉及到分頁問題,每頁都需要顯示置頂貼,這種情況可以把置頂貼水平切分開來,避免取置頂帖子時從所有帖子的表中讀取
例:QQ的登錄表。假設QQ的用戶有100億,如果只有一張表,每個用戶登錄的時候數據庫都要從這100億中查找,會很慢很慢。如果將這一張表分成100份,每張表有1億條,就小了很多,比如qq0,qq1,qq1...qq99表。
用戶登錄的時候,可以將用戶的id%100,那么會得到0-99的數,查詢表的時候,將表名qq跟取模的數連接起來,就構建了表名。比如123456789用戶,取模的89,那么就到qq89表查詢,查詢的時間將會大大縮短。
這就是水平分割。
2,垂直分割:
垂直分割指的是:表的記錄並不多,但是字段卻很長,表占用空間很大,檢索表的時候需要執行大量的IO,嚴重降低了性能。這時需要把大的字段拆分到另一個表,並且該表與原表是一對一的關系。
表結構設計垂直切分。常見的一些場景包括
a). 大字段的垂直切分。單獨將大字段建在另外的表中,提高基礎表的訪問性能,原則上在性能關鍵的應用中應當避免數據庫的大字段
b). 按照使用用途垂直切分。例如企業物料屬性,可以按照基本屬性、銷售屬性、采購屬性、生產制造屬性、財務會計屬性等用途垂直切分
c). 按照訪問頻率垂直切分。例如電子商務、Web 2.0系統中,如果用戶屬性設置非常多,可以將基本、使用頻繁的屬性和不常用的屬性垂直切分開
例如學生答題表tt:有如下字段:
Id name 分數 題目 回答
其中題目和回答是比較大的字段,id name 分數比較小。
如果我們只想查詢id為8的學生的分數:select 分數 from tt where id = 8;雖然知識查詢分數,但是題目和回答這兩個大字段也是要被掃描的,很消耗性能。但是我們只關心分數,並不想查詢題目和回答。這就可以使用垂直分割。我們可以把題目單獨放到一張表中,通過id與tt表建立一對一的關系,同樣將回答單獨放到一張表中。這樣我們插敘tt中的分數的時候就不會掃描題目和回答了。
3,其他要點:
1)存放圖片、文件等大文件用文件系統存儲。數據庫只存儲路徑,圖片和文件存放在文件系統,甚至單獨存放在一台服務器
二、Spring事務的隔離級別
1. ISOLATION_DEFAULT: 這是一個PlatfromTransactionManager默認的隔離級別,使用數據庫默認的事務隔離級別.
另外四個與JDBC的隔離級別相對應
2. ISOLATION_READ_UNCOMMITTED: 這是事務最低的隔離級別,它允許另一個事務可以看到這個事務未提交的數據。
這種隔離級別會產生臟讀,不可重復讀和幻讀。
3. ISOLATION_READ_COMMITTED: 保證一個事務修改的數據提交后才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的數據
4. ISOLATION_REPEATABLE_READ: 這種事務隔離級別可以防止臟讀,不可重復讀。但是可能出現幻讀。
它除了保證一個事務不能讀取另一個事務未提交的數據外,還保證了避免下面的情況產生(不可重復讀)。
5. ISOLATION_SERIALIZABLE 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。除了防止臟讀,不可重復讀外,還避免了幻讀。
其中的一些概念的說明:
臟讀: 指當一個事務正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然后使用了這個數據。因為這個數據是還沒有提交的數據, 那么另外一 個事務讀到的這個數據是臟數據,依據臟數據所做的操作可能是不正確的。(指一個線程中的事務讀取到了另外一個線程中未提交的數據。)
不可重復讀: 指在一個事務內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務也訪問該同一數據。 那么,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那么第一個事務兩次讀到的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不一樣的,因此稱為是不可重復讀。
幻讀: 指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,這種修改涉及 到表中的全部數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那么,以后就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺一樣。
三、jdbc批量插入幾百萬數據怎么實現?
1. 使用mysql的存儲過程來實現插入萬條記錄
DROP PROCEDURE IF EXISTS proc_initData;--如果存在此存儲過程則刪掉 DELIMITER $ CREATE PROCEDURE proc_initData() BEGIN DECLARE i INT DEFAULT 1; WHILE i<=100000 DO INSERT INTO text VALUES(i,CONCAT('姓名',i),'XXXXXXXXX'); SET i = i+1; END WHILE; END $ CALL proc_initData();
花費時間很長:
2. JDBC往數據庫中普通插入方式
先來說說JDBC往數據庫中普通插入方式,簡單的代碼大致如下,循環了1000條,中間加點隨機的數值,畢竟自己要拿數據測試,數據全都一樣也不好區分
private String url = "jdbc:mysql://localhost:3306/test01"; private String user = "root"; private String password = "root"; @Test public void Test(){ Connection conn = null; PreparedStatement pstm =null; ResultSet rt = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url, user, password); String sql = "INSERT INTO userinfo(uid,uname,uphone,uaddress) VALUES(?,CONCAT('姓名',?),?,?)"; pstm = conn.prepareStatement(sql); Long startTime = System.currentTimeMillis(); Random rand = new Random(); int a,b,c,d; for (int i = 1; i <= 1000; i++) { pstm.setInt(1, i); pstm.setInt(2, i); a = rand.nextInt(10); b = rand.nextInt(10); c = rand.nextInt(10); d = rand.nextInt(10); pstm.setString(3, "188"+a+"88"+b+c+"66"+d); pstm.setString(4, "xxxxxxxxxx_"+"188"+a+"88"+b+c+"66"+d);27 pstm.executeUpdate(); } Long endTime = System.currentTimeMillis(); System.out.println("OK,用時:" + (endTime - startTime)); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); }finally{ if(pstm!=null){ try { pstm.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } if(conn!=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } } }
輸出結果:OK,用時:738199,單位毫秒,也就是說這種方式與直接數據庫中循環是差不多的。
在討論批量處理之前,先說說遇到的坑,首先,JDBC連接的url中要加rewriteBatchedStatements參數設為true是批量操作的前提,其次就是檢查mysql驅動包時候是5.1.13以上版本(低於該版本不支持),因網上隨便下載了5.1.7版本的,然后執行批量操作(100W條插入),結果因為驅動器版本太低緣故並不支持,導致停止掉java程序后,mysql還在不斷的往數據庫中插入數據,最后不得不停止掉數據庫服務才停下來...
那么低版本的驅動包是否對100W+數據插入就無力了呢?實際還有另外一種方式,效率相比來說還是可以接受的。
3. 使用事務提交方式
先將命令的提交方式設為false,即手動提交conn.setAutoCommit(false);最后在所有命令執行完之后再提交事務conn.commit();
private String url = "jdbc:mysql://localhost:3306/test01"; private String user = "root"; private String password = "123456"; @Test public void Test(){ Connection conn = null; PreparedStatement pstm =null; ResultSet rt = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url, user, password); String sql = "INSERT INTO userinfo(uid,uname,uphone,uaddress) VALUES(?,CONCAT('姓名',?),?,?)"; pstm = conn.prepareStatement(sql); conn.setAutoCommit(false); Long startTime = System.currentTimeMillis(); Random rand = new Random(); int a,b,c,d; for (int i = 1; i <= 100000; i++) { pstm.setInt(1, i); pstm.setInt(2, i); a = rand.nextInt(10); b = rand.nextInt(10); c = rand.nextInt(10); d = rand.nextInt(10); pstm.setString(3, "188"+a+"88"+b+c+"66"+d); pstm.setString(4, "xxxxxxxxxx_"+"188"+a+"88"+b+c+"66"+d); pstm.executeUpdate(); } conn.commit(); Long endTime = System.currentTimeMillis(); System.out.println("OK,用時:" + (endTime - startTime)); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); }finally{ if(pstm!=null){ try { pstm.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } if(conn!=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } } }
以上代碼插入10W條數據,輸出結果:OK,用時:18086,也就十八秒左右的時間,理論上100W也就是3分鍾這樣,勉強還可以接受。
4. 批量處理
接下來就是批量處理了,注意,一定要5.1.13以上版本的驅動包。
private String url = "jdbc:mysql://localhost:3306/test01?rewriteBatchedStatements=true";//注意url地址要加上rewriteBatchedStatements=true private String user = "root"; private String password = "123456"; @Test public void Test(){ Connection conn = null; PreparedStatement pstm =null; ResultSet rt = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url, user, password); String sql = "INSERT INTO userinfo(uid,uname,uphone,uaddress) VALUES(?,CONCAT('姓名',?),?,?)"; pstm = conn.prepareStatement(sql); conn.setAutoCommit(false); Long startTime = System.currentTimeMillis(); Random rand = new Random(); int a,b,c,d; for (int i = 1; i <= 100000; i++) { pstm.setInt(1, i); pstm.setInt(2, i); a = rand.nextInt(10); b = rand.nextInt(10); c = rand.nextInt(10); d = rand.nextInt(10); pstm.setString(3, "188"+a+"88"+b+c+"66"+d); pstm.setString(4, "xxxxxxxxxx_"+"188"+a+"88"+b+c+"66"+d); pstm.addBatch(); } pstm.executeBatch(); conn.commit(); Long endTime = System.currentTimeMillis(); System.out.println("OK,用時:" + (endTime - startTime)); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); }finally{ if(pstm!=null){ try { pstm.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } if(conn!=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } } }
10W輸出結果:OK,用時:3386,才3秒鍾.