1. 數據庫范式
第一范式:列不可分,eg:【聯系人】(姓名,性別,電話),一個聯系人有家庭電話和公司電話,那么這種表結構設計就沒有達到 1NF;
第二范式:有主鍵,保證完全依賴。eg:訂單明細表【OrderDetail】(OrderID,ProductID,UnitPrice,Discount,Quantity,ProductName),Discount(折扣),Quantity(數量)完全依賴(取決)於主鍵(OderID,ProductID),而 UnitPrice,ProductName 只依賴於 ProductID,不符合2NF;
第三范式:無傳遞依賴(非主鍵列 A 依賴於非主鍵列 B,非主鍵列 B 依賴於主鍵的情況),eg:訂單表【Order】(OrderID,OrderDate,CustomerID,CustomerName,CustomerAddr,CustomerCity)主鍵是(OrderID),CustomerName,CustomerAddr,CustomerCity 直接依賴的是 CustomerID(非主鍵列),而不是直接依賴於主鍵,它是通過傳遞才依賴於主鍵,所以不符合 3NF。
2.連接查詢
分類:內連接、外連接、自然連接(略)、交叉連接(略)
內連接
基本語法:左表 [inner] join 右表 on 左表.字段 = 右表.字段;
從左表中取出每一條記錄,去右表中與所有的記錄進行匹配:匹配必須是某個條件在左表中與右表中相同最終才會保留結果,否則不保留。
外連接
基本語法: 左表 left/right join 右表 on 左表.字段 = 右表.字段;
left join: 左外連接(左連接), 以左表為主表
right join: 右外連接(右連接), 以右表為主
舉例
內連接
左外連接
右外連接
3.聯合查詢
基本語法:
Select 語句 1
Union [union 選項]
Select 語句 2
……
將多次查詢(多條 select 語句), 在記錄上進行拼接(字段不會增加),每一條 select 語句獲取的字段數必須嚴格一致(但是字段類型無關)。
其中 union 選項有 2 個。ALL:保留所有;Distinct(默認):去重。
應用:查詢同一張表,但是有不同的需求;查詢多張表,多張表的結構完全一致,保存的數據也是一樣的。
在聯合查詢中,order by 不能直接使用。需要對查詢語句使用括號才行。另外需要配合 limit 使用
order by(排序)
select 字段列表/* from 表名 where 條件 order by 字段名1 asc(升序 默認)/desc(降序),
舉例
SELECT
( SELECT d.dept_name FROM departments d WHERE de.dept_no = d.dept_no ) AS 部門,
count( de.emp_no ) AS 人數
FROM
dept_emp de
WHERE
de.to_date = '9999-01-01'
GROUP BY
de.dept_no
HAVING
count( de.emp_no ) > 30000
分頁查詢
客戶端通過傳遞start(頁碼),limit(每頁顯示的條數)兩個參數去分頁查詢數據庫表中的數據,那我們知道MySql數據庫提供了分頁的函數limit m,n,但是該函數的用法和我們的需求不一樣,所以就需要我們根據實際情況去改寫適合我們自己的分頁語句
通過上面的分析,可以得出符合我們自己需求的分頁sql格式是:select * from table limit (start-1)*limit,limit; 其中start是頁碼,limit是每頁顯示的條數
4. 數據庫鎖
鎖分類
從數據庫系統角度分為三種:排他鎖、共享鎖、更新鎖。
從程序員角度分為兩種:一種是悲觀鎖,一種樂觀鎖。
傳統的關系數據庫里用到了很多這種鎖機制,比如行鎖、表鎖、讀鎖、寫鎖等,都是在操作之前先上鎖。
悲觀鎖按使用性質划分
共享鎖(Share Lock)
S鎖,也叫讀鎖,用於所有的只讀數據操作。共享鎖是非獨占的,允許多個並發事務讀取其鎖定的資源。
性質
1. 多個事務可封鎖同一個共享頁;
2. 任何事務都不能修改該頁;
3. 通常是該頁被讀取完畢,S鎖立即被釋放。
在SQL Server中,默認情況下,數據被讀取后,立即釋放共享鎖。
例如,執行查詢語句“SELECT * FROM my_table”時,首先鎖定第一頁,讀取之后,釋放對第一頁的鎖定,然后鎖定第二頁。這樣,就允許在讀操作過程中,修改未被鎖定的第一頁。
例如,語句“SELECT * FROM my_table HOLDLOCK”就要求在整個查詢過程中,保持對表的鎖定,直到查詢完成才釋放鎖定。
排他鎖(Exclusive Lock)
X鎖,也叫寫鎖,表示對數據進行寫操作。如果一個事務對對象加了排他鎖,其他事務就不能再給它加任何鎖了。(某個顧客把試衣間從里面反鎖了,其他顧客想要使用這個試衣間,就只有等待鎖從里面打開了。)
性質
1. 僅允許一個事務封鎖此頁;
2. 其他任何事務必須等到X鎖被釋放才能對該頁進行訪問;
3. X鎖一直到事務結束才能被釋放。
產生排他鎖的SQL語句如下:select * from ad_plan for update;
更新鎖
U鎖,在修改操作的初始化階段用來鎖定可能要被修改的資源,這樣可以避免使用共享鎖造成的死鎖現象
5.索引
索引的優點
- 大大加快數據的檢索速度,這也是創建索引的最主要的原因;
- 加速表和表之間的連接;
- 在使用分組和排序子句進行數據檢索時,同樣可以顯著減少查詢中分組和排序的時間;
- 通過創建唯一性索引,可以保證數據庫表中每一行數據的唯一性;
索引是對數據庫表中一個或多個列的值進行排序的數據結構,以協助快速查詢、更新數據庫表中數據。索引的實現通常使用B_TREE及其變種。索引加速了數據訪問,因為存儲引擎不會再去掃描整張表得到需要的數據;相反,它從根節點開始,根節點保存了子節點的指針,存儲引擎會根據指針快速尋找數據。
6. Mysql如何為表字段添加索引?
1.添加PRIMARY KEY(主鍵索引)
ALTER TABLE table_name ADD PRIMARY KEY ( column )
2.添加UNIQUE(唯一索引)
ALTER TABLE table_name ADD UNIQUE ( column )
3.添加INDEX(普通索引)
ALTER TABLE table_name ADD INDEX index_name ( column )
4.添加FULLTEXT(全文索引)
ALTER TABLE table_name ADD FULLTEXT ( column)
5.添加多列索引
ALTER TABLE table_name ADD INDEX index_name ( column1, column2, column3 )
7. 索引的數據結構
如果說數據庫表中的數據是一本書,那么索引就是書的目錄。索引能夠讓我們快速的定位想要查詢的數據。
索引的結構:BTree 索引和 Hash 索引。
MyISAM 和 InnoDB 存儲引擎:只支持 BTREE 索引, 也就是說默認使用 BTREE,不能夠更換。
MEMORY/HEAP 存儲引擎:支持 HASH 和 BTREE 索引。
索引的分類:單列索引(普通索引,唯一索引,主鍵索引)、組合索引、全文索引、空間索引
索引:B+,B-,全文索引
Mysql的索引是一個數據結構,旨在使數據庫高效的查找數據。
常用的數據結構是B+Tree,每個葉子節點不但存放了索引鍵的相關信息還增加了指向相鄰葉子節點的指針,這樣就形成了帶有順序訪問指針的B+Tree,做這個優化的目的是提高不同區間訪問的性能。
什么時候使用索引:
- 經常出現在group by,order by和distinc關鍵字后面的字段
- 經常與其他表進行連接的表,在連接字段上應該建立索引
- 經常出現在Where子句中的字段
- 經常出現用作查詢選擇的字段
B+數的優勢:
評價一個索引好壞主要看IO的訪問次數,B+樹紅黑樹來說,樹高很小(出度很大)即可以有效降低IO的訪問次數。B+數的高度h=logd(n),d越大,h越小,查詢效率越高。相對B樹,B+樹d可以很大,因為非葉子節點不存儲數據,只存儲key,在一個存儲頁上可以存儲更多的key值。在每個頁上可以存儲更多的key,即d很大。
外存按照頁進行邏輯划分,頁大小固定,當讀入外存數據時,會根據局部性原理每次會預讀連續的多頁數據到內存。B+樹的葉子節點是存儲是連續和有序的,在查詢時,尤其在范圍查詢時較少的IO次數可以訪問到所需的數據。
單一節點存儲更多的元素,使得查詢的IO次數更少。(應用於文件系統、數據庫系統)
所有查詢都要查找到葉子節點,查詢性能穩定。
所有葉子節點形成有序鏈表,便於范圍查詢。
通過上面的圖我們可以看到,索引的本質其實就是新建了一張表,而表本質上的數據結構就是樹形結構,所以索引也是樹形結構。但實際運用中並沒有誰用紅黑樹,avl樹這種數據結構,一般是b+樹,接下來給大家大致介紹一下b+樹的構成。
b+樹在構建時和我們之前提到的二三樹很像,只是有一些改進,b+樹的非葉子節點不包含value的信息,也就是說非葉子結點只起到一個導航的作用,所有的value放在了葉子結點里,這樣由於B+樹在內部節點上不包含數據信息,因此在內存頁中能夠存放更多的key。 數據存放的更加緊密,具有更好的空間局部性。因此訪問葉子節點上關聯的數據也具有更好的緩存命中率。通常會將b+樹進行優化,增加順序訪問指針。
在B+Tree的每個葉子節點增加一個指向相鄰葉子節點的指針,就形成了帶有順序訪問指針的B+Tree。做這個優化的目的是為了提高區間訪問的性能,例如圖中如果要查詢key為從18到49的所有數據記錄,當找到18后,只需順着節點和指針順序遍歷就可以一次性訪問到所有數據節點,極大提到了區間查詢效率。
可以看到b+樹對於表的存儲是一種很方便的數據結構。那么為什么不用紅黑樹呢,因為數據量大的時候,會導致這種二叉樹深度太深,io次數會很多,層數很少的b+樹可以有效降低io次數。
8.數據庫引擎,談談 InnoDB 和 MyIsam 兩者的區別
2.1 兩者的對比
- count運算上的區別: 因為MyISAM緩存有表meta-data(行數等),因此在做COUNT(*)時對於一個結構很好的查詢是不需要消耗多少資源的。而對於InnoDB來說,則沒有這種緩存
- 是否支持事務和崩潰后的安全恢復: MyISAM 強調的是性能,每次查詢具有原子性,其執行速度比InnoDB類型更快,但是不提供事務支持。但是 InnoDB 提供事務支持,外部鍵等高級數據庫功能。 具有事務(commit)、回滾(rollback)和崩潰修復能力(crash recovery capabilities)的事務安全(transaction-safe (ACID compliant))型表。
- 是否支持外鍵: MyISAM不支持,而InnoDB支持。
2.2 關於兩者的總結
MyISAM更適合讀密集的表,而InnoDB更適合寫密集的表。 在數據庫做主從分離的情況下,經常選擇MyISAM作為主庫的存儲引擎。
一般來說,如果需要事務支持,並且有較高的並發讀取頻率(MyISAM的表鎖的粒度太大,所以當該表寫並發量較高時,要等待的查詢就會很多了),InnoDB是不錯的選擇。如果你的數據量很大(MyISAM支持壓縮特性可以減少磁盤的空間占用),而且不需要支持事務時,MyISAM是最好的選擇。InnoDB:支持事務處理,支持外鍵,支持崩潰修復能力和並發控制。如果需要對事務的完整
性要求比較高(比如銀行),要求實現並發控制(比如售票),那選擇 InnoDB 有很大的優勢。
如果需要頻繁的更新、刪除操作的數據庫,也可以選擇 InnoDB,因為支持事務的提交(commit)和回滾(rollback)。MyISAM:插入數據快,空間和內存使用比較低。如果表主要是用於插入新記錄和讀出記錄,那么選擇 MyISAM 能實現處理高效率。如果應用的完整性、並發性要求比較低,也可以使用。
MEMORY:所有的數據都在內存中,數據的處理速度快,但是安全性不高。如果需要很快的讀寫速度,對數據的安全性要求較低,可以選擇 MEMOEY。它對表的大小有要求,不能建立太大的表。所以,這類數據庫只使用在相對較小的數據庫表
為什么說B+-tree比B 樹更適合實際應用中操作系統的文件索引和數據庫索引
B+tree的磁盤讀寫代價更低:B+tree的內部結點並沒有指向關鍵字具體信息的指針(紅色部分),因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多。一次性讀入內存中的需要查找的關鍵字也就越多,相對來說IO讀寫次數也就降低了;
B+tree的查詢效率更加穩定:由於內部結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引,所以,任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當;
數據庫索引采用B+樹而不是B樹的主要原因:B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷,而且在數據庫中基於范圍的查詢是非常頻繁的,而B樹只能中序遍歷所有節點,效率太低。
10 事物的四大特性(ACID)
原子性(Atomicity): 事務是最小的執行單位,不允許分割。事務的原子性確保動作要么全部完成,要么完全不起作用;
一致性(Consistency): 執行事務前后,數據保持一致,多個事務對同一個數據讀取的結果是相同的;
隔離性(Isolation): 並發訪問數據庫時,一個用戶的事務不被其他事務所干擾,各並發事務之間數據庫是獨立的;
持久性(Durability): 一個事務被提交之后。它對數據庫中數據的改變是持久的,即使數據庫發生故障也不應該對其有任何影響。
並發事務帶來哪些問題?
在典型的應用程序中,多個事務並發運行,經常會操作相同的數據來完成各自的任務(多個用戶對同一數據進行操作)。並發雖然是必須的,但可能會導致以下的問題。
臟讀(Dirty read): 當一個事務正在訪問數據並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時另外一個事務也訪問了這個數據,然后使用了這個數據。因為這個數據是還沒有提交的數據,那么另外一個事務讀到的這個數據是“臟數據”,依據“臟數據”所做的操作可能是不正確的。
丟失修改(Lost to modify): 指在一個事務讀取一個數據時,另外一個事務也訪問了該數據,那么在第一個事務中修改了這個數據后,第二個事務也修改了這個數據。這樣第一個事務內的修改結果就被丟失,因此稱為丟失修改。 例如:事務1讀取某表中的數據A=20,事務2也讀取A=20,事務1修改A=A-1,事務2也修改A=A-1,最終結果A=19,事務1的修改被丟失。
不可重復讀(Unrepeatableread): 指在一個事務內多次讀同一數據。在這個事務還沒有結束時,另一個事務也訪問該數據。那么,在第一個事務中的兩次讀數據之間,由於第二個事務的修改導致第一個事務兩次讀取的數據可能不太一樣。這就發生了在一個事務內兩次讀到的數據是不一樣的情況,因此稱為不可重復讀。
幻讀(Phantom read): 幻讀與不可重復讀類似。它發生在一個事務(T1)讀取了幾行數據,接着另一個並發事務(T2)插入了一些數據時。在隨后的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄,就好像發生了幻覺一樣,所以稱為幻讀。
不可重復讀和幻讀區別:
不可重復讀的重點是修改比如多次讀取一條記錄發現其中某些列的值被修改,幻讀的重點在於新增或者刪除比如多次讀取一條記錄發現記錄增多或減少了。
事務隔離級別有哪些?MySQL的默認隔離級別是?
SQL 標准定義了四個隔離級別:
READ-UNCOMMITTED(讀取未提交): 最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致臟讀、幻讀或不可重復讀。
READ-COMMITTED(讀取已提交): 允許讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生。
REPEATABLE-READ(可重復讀): 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生。
SERIALIZABLE(可串行化): 最高的隔離級別,完全服從ACID的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀。
MySQL InnoDB 存儲引擎的默認支持的隔離級別是 REPEATABLE-READ(可重讀)
13.事務通過InnoDB引擎來實現
而事務的 ACID 是通過 InnoDB 日志和鎖來保證。事務的隔離性是通過數據庫鎖的機制實現的,持久性通過 Redo Log(重做日志)來實現,原子性和一致性通過 Undo Log 來實現。
Undo Log 的原理很簡單,為了滿足事務的原子性,在操作任何數據之前,首先將數據備份到一個地方(這個存儲數據備份的地方稱為 Undo Log)。然后進行數據的修改。
如果出現了錯誤或者用戶執行了 Rollback 語句,系統可以利用 Undo Log 中的備份將數據恢復到事務開始之前的狀態。
和 Undo Log 相反,Redo Log 記錄的是新數據的備份。在事務提交前,只要將 Redo Log 持久化即可,不需要將數據持久化。
當系統崩潰時,雖然數據沒有持久化,但是 Redo Log 已經持久化。系統可以根據 Redo Log 的內容,將所有數據恢復到最新的狀態。對具體實現過程有興趣的同學可以去自行搜索擴展。
14、數據庫性能優化有哪些方式?
SQL 優化:
盡量避免使用 SELECT *;
只查詢一條記錄時使用 limit 1;
使用連接查詢代替子查詢;
盡量使用一些能通過索引查詢的關鍵字。
表結構優化:
盡量使用數字類型字段,提高比對效率;
長度不變且對查詢速度要求高的數據可以考慮使用 char,否則使用 varchar;表中字段過多時可以適當的進行垂直分割,將部分字段移動到另外一張表;表中數據量過大可以適當的進行水平分割,將部分數據移動到另外一張表。
其它優化:
對查詢頻率高的字段適當的建立索引,提高效率;根據表的用途使用合適的數據庫引擎;讀寫分離。
sql優化
1.負向查詢不能使用索引
select name from user where id not in (1,3,4);
應該修改為:
select name from user where id in (2,5,6);
2.前導模糊查詢不能使用索引
如:
select name from user where name like '%zhangsan'
非前導則可以:
select name from user where name like 'zhangsan%'
建議可以考慮使用 Lucene 等全文索引工具來代替頻繁的模糊查詢。
3.數據區分不明顯的不建議創建索引
如 user 表中的性別字段,可以明顯區分的才建議創建索引,如身份證等字段。
字段的默認值不要為 null
這樣會帶來和預期不一致的查詢結果。
4.在字段上進行計算不能命中索引
select name from user where FROM_UNIXTIME(create_time) < CURDATE();
應該修改為:
select name from user where create_time < FROM_UNIXTIME(CURDATE());
5.最左前綴問題
如果給 user 表中的 username pwd 字段創建了復合索引那么使用以下SQL 都是可以命中索引:
select username from user where username='zhangsan' and pwd ='axsedf1sd'
select username from user where pwd ='axsedf1sd' and username='zhangsan'
select username from user where username='zhangsan'
但是使用
select username from user where pwd ='axsedf1sd'
是不能命中索引的。
6.如果明確知道只有一條記錄返回
select name from user where username='zhangsan' limit 1
可以提高效率,可以讓數據庫停止游標移動。
7.不要讓數據庫幫我們做強制類型轉換
select name from user where telno=18722222222
這樣雖然可以查出數據,但是會導致全表掃描。
需要修改為
select name from user where telno='18722222222'
8.如果需要進行 join 的字段兩表的字段類型要相同
不然也不會命中索引。
大表優化
當MySQL單表記錄數過大時,數據庫的CRUD性能會明顯下降,一些常見的優化措施如下:
1. 限定數據的范圍
務必禁止不帶任何限制數據范圍條件的查詢語句。比如:我們當用戶在查詢訂單歷史的時候,我們可以控制在一個月的范圍內;
2. 讀/寫分離
經典的數據庫拆分方案,主庫負責寫,從庫負責讀;
3. 垂直分區
根據數據庫里面數據表的相關性進行拆分。 例如,用戶表中既有用戶的登錄信息又有用戶的基本信息,可以將用戶表拆分成兩個單獨的表,甚至放到單獨的庫做分庫。
簡單來說垂直拆分是指數據表列的拆分,把一張列比較多的表拆分為多張表。 如下圖所示,這樣來說大家應該就更容易理解了。[圖片上傳失敗…(image-479744-1573805100498)]
垂直拆分的優點: 可以使得列數據變小,在查詢時減少讀取的Block數,減少I/O次數。此外,垂直分區可以簡化表的結構,易於維護。
垂直拆分的缺點: 主鍵會出現冗余,需要管理冗余列,並會引起Join操作,可以通過在應用層進行Join來解決。此外,垂直分區會讓事務變得更加復雜;
4. 水平分區
保持數據表結構不變,通過某種策略存儲數據分片。這樣每一片數據分散到不同的表或者庫中,達到了分布式的目的。 水平拆分可以支撐非常大的數據量。
水平拆分是指數據表行的拆分,表的行數超過200萬行時,就會變慢,這時可以把一張的表的數據拆成多張表來存放。舉個例子:我們可以將用戶信息表拆分成多個用戶信息表,這樣就可以避免單一表數據量過大對性能造成影響。
[圖片上傳失敗…(image-d4f5ef-1573805100498)]
水平拆分可以支持非常大的數據量。需要注意的一點是:分表僅僅是解決了單一表數據過大的問題,但由於表的數據還是在同一台機器上,其實對於提升MySQL並發能力沒有什么意義,所以 水平拆分最好分庫 。
水平拆分能夠 支持非常大的數據量存儲,應用端改造也少,但 分片事務難以解決 ,跨節點Join性能較差,邏輯復雜。《Java工程師修煉之道》的作者推薦 盡量不要對數據進行分片,因為拆分會帶來邏輯、部署、運維的各種復雜度,一般的數據表在優化得當的情況下支撐千萬以下的數據量是沒有太大問題的。如果實在要分片,盡量選擇客戶端分片架構,這樣可以減少一次和中間件的網絡I/O。
下面補充一下數據庫分片的兩種常見方案:
客戶端代理: 分片邏輯在應用端,封裝在jar包中,通過修改或者封裝JDBC層來實現。 當當網的 Sharding-JDBC、阿里的TDDL是兩種比較常用的實現。
中間件代理: 在應用和數據中間加了一個代理層。分片邏輯統一維護在中間件服務中。 我們現在談的 Mycat 、360的Atlas、網易的DDB等等都是這種架構的實現。
拆分之后帶來的問題
拆分之后由一張表變為了多張表,一個庫變為了多個庫。最突出的一個問題就是事務如何保證。
兩段提交
最終一致性
如果業務對強一致性要求不是那么高那么最終一致性則是一種比較好的方案。
通常的做法就是補償,比如 一個業務是 A 調用 B,兩個執行成功才算最終成功,當 A 成功之后,B 執行失敗如何來通知 A 呢。
比較常見的做法是 失敗時 B 通過 MQ 將消息告訴 A,A 再來進行回滾。這種的前提是 A 的回滾操作得是冪等的,不然 B 重復發消息就會出現問題。
15.高並發數據庫的例子
首先假設一個業務場景:數據庫中有一條數據,需要獲取到當前的值,在當前值的基礎上+10,然后再更新回去。
如果此時有兩個線程同時並發處理,第一個線程拿到數據是10,+10=20更新回去。第二個線程原本是要在第一個線程的基礎上再+20=40,結果由於並發訪問取到更新前的數據為10,+20=30。
這就是典型的存在中間狀態,導致數據不正確。
16.悲觀鎖
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現。
悲觀鎖的特點是先獲取鎖,再進行業務操作,即“悲觀”的認為所有的操作均會導致並發安全問題,因此要先確保獲取鎖成功再進行業務操作。通常來講,在數據庫上的悲觀鎖需要數據庫本身提供支持,即通過常用的select … for update操作來實現悲觀鎖。當數據庫執行select … for update時會獲取被select中的數據行的行鎖,因此其他並發執行的select … for update如果試圖選中同一行則會發生排斥(需要等待行鎖被釋放),因此達到鎖的效果。select for update獲取的行鎖會在當前事務結束時自動釋放,因此必須在事務中使用。
簡單理解下悲觀鎖:當一個事務鎖定了一些數據之后,只有當當前鎖提交了事務,釋放了鎖,其他事務才能獲得鎖並執行操作。
這里使用select for update的方式利用數據庫開啟了悲觀鎖,鎖定了id=1的這條數據(注意:這里除非是使用了索引會啟用行級鎖,不然是會使用表鎖,將整張表都鎖住。)。之后使用commit提交事務並釋放鎖,這樣下一個線程過來拿到的就是正確的數據。
悲觀鎖一般是用於並發不是很高,並且不允許臟讀等情況。但是對數據庫資源消耗較大。
17.樂觀鎖
總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
樂觀鎖的特點先進行業務操作,只在最后實際更新數據時進行檢查數據是否被更新過,若未被更新過,則更新成功;否則,失敗重試。樂觀鎖在數據庫上的實現完全是邏輯的,不需要數據庫提供特殊的支持
那么有沒有性能好,支持的並發也更多的方式呢?
那就是樂觀鎖。
樂觀鎖是首先假設數據沖突很少,只有在數據提交修改的時候才進行校驗,如果沖突了則不會進行更新。
通常的實現方式增加一個version字段,為每一條數據加上版本。每次更新的時候version+1,並且更新時候帶上版本號
兩種鎖的使用場景
從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下(多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生沖突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
樂觀鎖常見的兩種實現方式
樂觀鎖一般會使用版本號機制或CAS算法實現。
1. 版本號機制
一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
舉一個簡單的例子: 假設數據庫中帳戶信息表中有一個 version 字段,當前值為 1 ;而當前帳戶余額字段( balance )為100.
- 操作員 A 此時將其讀出( version=1 ),並從其帳戶余額中扣除 50( 100-50 )。
- 在操作員 A 操作的過程中,操作員B 也讀入此用戶信息( version=1 ),並從其帳戶余額中扣除 20 ( 100-20 )。
- 操作員 A 完成了修改工作,將數據版本號加一( version=2 ),連同帳戶扣除后余額( balance=50 ),提交至數據庫更新,此時由於提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新為 2 。
- 操作員 B 完成了操作,也將版本號加一( version=2 )試圖向數據庫提交數據( balance=80 ),但此時比對數據庫記錄版本時發現,操作員 B 提交的數據版本號為 2 ,數據庫記錄當前版本也為 2 ,不滿足 “ 提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。
這樣,就避免了操作員 B 用基於 version=1 的舊數據修改的結果覆蓋操作員A 的操作結果的可能。
2. CAS算法
即compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操作數
- 需要讀寫的內存值 V
- 進行比較的值 A
- 擬寫入的新值 B
當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。
樂觀鎖的缺點
ABA 問題是樂觀鎖一個常見的問題
1 ABA 問題
如果一個變量V初次讀取的時候是A值,並且在准備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,因為在這段時間它的值可能被改為其他值,然后又改回A,那CAS操作就會誤認為它從來沒有被修改過。這個問題被稱為CAS操作的 "ABA"問題。
JDK 1.5 以后的 AtomicStampedReference 類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當前引用是否等於預期引用,並且當前標志是否等於預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值
2 循環時間長開銷大
自旋CAS(也就是不成功就一直循環執行直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。 如果JVM能支持處理器提供的pause指令那么效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
3 只能保證一個共享變量的原子操作
CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行 CAS 操作.所以我們可以使用鎖或者利用AtomicReference類把多個共享變量合並成一個共享變量來操作。
CAS與synchronized的使用情景
簡單的來說CAS適用於寫比較少的情況下(多讀場景,沖突一般較少),synchronized適用於寫比較多的情況下(多寫場景,沖突一般較多)
- 對於資源競爭較少(線程沖突較輕)的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗cpu資源;而CAS基於硬件實現,不需要進入內核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
- 對於資源競爭嚴重(線程沖突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低於synchronized。
補充: Java並發編程這個領域中synchronized關鍵字一直都是元老級的角色,很久之前很多人都會稱它為 “重量級鎖”。但是,在JavaSE 1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的 偏向鎖 和 輕量級鎖 以及其它各種優化之后變得在某些情況下並不是那么重了。synchronized的底層實現主要依靠 Lock-Free 的隊列,基本思路是 自旋后阻塞,競爭切換后繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。在線程沖突較少的情況下,可以獲得和CAS類似的性能;而線程沖突嚴重的情況下,性能遠高於CAS。
