MySQL/InnoDB中的鎖、悲觀鎖、共享鎖、排它鎖、行鎖、表鎖、死鎖與MySQL讀寫分離
MySQL/InnoDB的加鎖,一直是一個面試中常問的話題。例如,資料庫如果有高併發請求,如何保證資料完整性?產生死鎖問題如何排查並解決?我在工作過程中,也會經常用到,樂觀鎖,排它鎖,等。於是今天就對這幾個概念進行學習,屢屢思路,記錄一下。
注:MySQL是一個支援外掛式儲存引擎的資料庫系統。本文下面的所有介紹,都是基於InnoDB儲存引擎,其他引擎的表現,會有較大的區別。
儲存引擎檢視
MySQL給開發者提供了查詢儲存引擎的功能,我這裡使用的是MySQL5.6.4,可以使用:
SHOW ENGINES
樂觀鎖
用資料版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂資料版本?即為資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的 “version” 欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加1。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。
舉例
1、資料庫表設計
三個欄位,分別是 id,value、version
selectid,value,versionfromTABLEwhereid=#{id}
2、每次更新表中的value欄位時,為了防止發生衝突,需要這樣操作
updateTABLE
setvalue=2,version=version+1
whereid=#{id} andversion=#{version};
悲觀鎖
與樂觀鎖相對應的就是悲觀鎖了。悲觀鎖就是在操作資料時,認為此操作會出現資料衝突,所以在進行每次操作時都要通過獲取鎖才能進行對相同資料的操作,這點跟java中的synchronized很相似,所以悲觀鎖需要耗費較多的時間。另外與樂觀鎖相對應的,悲觀鎖是由資料庫自己實現了的,要用的時候,我們直接呼叫資料庫的相關語句就可以了。
說到這裡,由悲觀鎖涉及到的另外兩個鎖概念就出來了,它們就是共享鎖與排它鎖。 共享鎖和排它鎖是悲觀鎖的不同的實現 ,它倆都屬於悲觀鎖的範疇。
使用,排它鎖舉例
要使用悲觀鎖,我們必須關閉mysql資料庫的自動提交屬性,因為MySQL預設使用autocommit模式,也就是說,當你執行一個更新操作後,MySQL會立刻將結果進行提交。
我們可以使用命令設定MySQL為非autocommit模式:
set autocommit=0;
# 設定完autocommit後,我們就可以執行我們的正常業務了。具體如下:
# 1. 開始事務
begin;/begin work;/start transaction; (三者選一就可以)
# 2. 查詢表資訊
select status from TABLE where id=1for update;
# 3. 插入一條資料
insert intoTABLE (id,value) values (2,2);
# 4. 修改資料為
update TABLE setvalue=2where id=1;
# 5. 提交事務
commit;/commit work;
共享鎖
共享鎖又稱 讀鎖 read lock ,是讀取操作建立的鎖。其他使用者可以併發讀取資料,但任何事務都不能對資料進行修改(獲取資料上的排他鎖),直到已釋放所有共享鎖。
如果事務T對資料A加上共享鎖後,則其他事務只能對A再加共享鎖,不能加排他鎖。獲得共享鎖的事務只能讀資料,不能修改資料
開啟第一個查詢視窗
begin;/beginwork;/starttransaction; (三者選一就可以)
SELECT * fromTABLEwhereid = 1 lockinsharemode;
然後在另一個查詢視窗中,對id為1的資料進行更新
update TABLEsetname="www.souyunku.com"whereid =1;
此時,操作介面進入了卡頓狀態,過了超時間,提示錯誤資訊
如果在超時前,執行 commit ,此更新語句就會成功。
[SQL]update test_one setname="www.souyunku.com"whereid =1;
[Err] 1205 - Lockwaittimeout exceeded; tryrestarting transaction
加上共享鎖後,也提示錯誤資訊
update test_one setname="www.souyunku.com"whereid =1lockinsharemode;
[SQL]update test_one setname="www.souyunku.com"whereid =1lockinsharemode;
[Err] 1064 - You have an error in yourSQL syntax; check the manual thatcorresponds to your MySQLserverversionfor the right syntax touse near 'lock in sharemode'at line 1
在查詢語句後面增加 LOCK IN SHARE MODE,Mysql會對查詢結果中的每行都加共享鎖,當沒有其他執行緒對查詢結果集中的任何一行使用排他鎖時,可以成功申請共享鎖,否則會被阻塞。其他執行緒也可以讀取使用了共享鎖的表,而且這些執行緒讀取的是同一個版本的資料。
加上共享鎖後,對於 update,insert,delete 語句會自動加排它鎖。
排它鎖
排他鎖 exclusive lock(也叫writer lock)又稱 寫鎖 。
排它鎖是悲觀鎖的一種實現,在上面悲觀鎖也介紹過。
若事務 1 對資料物件A加上X鎖,事務 1 可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到事物 1 釋放A上的鎖。這保證了其他事務在事物 1 釋放A上的鎖之前不能再讀取和修改A。排它鎖會阻塞所有的排它鎖和共享鎖
讀取為什麼要加讀鎖呢:防止資料在被讀取的時候被別的執行緒加上寫鎖,
使用方式:在需要執行的語句後面加上 for update 就可以了
行鎖
行鎖又分 共享鎖 和 排他鎖 ,由字面意思理解,就是給某一行加上鎖,也就是一條記錄加上鎖。
注意:行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖。
共享鎖:
名詞解釋:共享鎖又叫做讀鎖,所有的事務只能對其進行讀操作不能寫操作,加上共享鎖後在事務結束之前其他事務只能再加共享鎖,除此之外其他任何型別的鎖都不能再加了。
SELECT * fromTABLEwhereid = "1" lockinsharemode; 結果集的資料都會加共享鎖
排他鎖:
名詞解釋:若某個事物對某一行加上了排他鎖,只能這個事務對其進行讀寫,在此事務結束之前,其他事務不能對其進行加任何鎖,其他程序可以讀取,不能進行寫操作,需等待其釋放。
selectstatusfromTABLEwhereid=1forupdate;
可以參考之前演示的共享鎖,排它鎖語句
由於對於表中,id欄位為主鍵,就也相當於索引。執行加鎖時,會將id這個索引為1的記錄加上鎖,那麼這個鎖就是行鎖。
表鎖
如何加表鎖
innodb 的行鎖是在有索引的情況下,沒有索引的表是鎖定全表的.
Innodb中的行鎖與表鎖
前面提到過,在Innodb引擎中既支援行鎖也支援表鎖,那麼什麼時候會鎖住整張表,什麼時候或只鎖住一行呢?只有通過索引條件檢索資料,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖衝突,從而影響併發效能。
行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖。行級鎖的缺點是:由於需要請求大量的鎖資源,所以速度慢,記憶體消耗大。
死鎖
死鎖(Deadlock) 所謂死鎖:是指兩個或兩個以上的程序在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。由於資源佔用是互斥的,當某個程序提出申請資源後,使得有關程序在無外力協助下,永遠分配不到必需的資源而無法繼續執行,這就產生了一種特殊現象死鎖。
解除正在死鎖的狀態有兩種方法:
第一種:
1.查詢是否鎖表
showOPENTABLESwhere In_use > 0;
2.查詢程序(如果您有SUPER許可權,您可以看到所有執行緒。否則,您只能看到您自己的執行緒)
showprocesslist
3.殺死程序id(就是上面命令的id列)
killid
第二種:
1:檢視當前的事務
SELECT * FROMINFORMATION_SCHEMA.INNODB_TRX;
2:檢視當前鎖定的事務
SELECT * FROMINFORMATION_SCHEMA.INNODB_LOCKS;
3:檢視當前等鎖的事務
SELECT * FROMINFORMATION_SCHEMA.INNODB_LOCK_WAITS;
殺死程序
kill執行緒ID
如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程序執行推進順序與速度不同,也可能產生死鎖。產生死鎖的四個必要條件:
(1)互斥條件:一個資源每次只能被一個程序使用。(2)請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。(3)不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。(4)迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
雖然不能完全避免死鎖,但可以使死鎖的數量減至最少。將死鎖減至最少可以增加事務的吞吐量並減少系統開銷,因為只有很少的事務回滾,而回滾會取消事務執行的所有工作。由於死鎖時回滾而由應用程式重新提交。
下列方法有助於最大限度地降低死鎖:
(1)按同一順序訪問物件。(2)避免事務中的使用者互動。(3)保持事務簡短並在一個批處理中。(4)使用低隔離級別。(5)使用繫結連線。
參考 :
https://blog.csdn.net/puhaiyang/article/details/72284702
https://www.jb51.net/article/78088.htm
MySQL讀寫分離
1、 簡介
當今MySQL使用相當廣泛,隨著使用者的增多以及資料量的增大,高併發隨之而來。然而我們有很多辦法可以緩解資料庫的壓力。分散式資料庫、負載均衡、讀寫分離、增加快取伺服器等等。這裡我們將採用讀寫分離技術進展緩解資料庫的壓力。
其中實現讀寫分離的技術有很多方法,這裡我們將採用mysql-proxy這個中間軟體來實現。這個軟體中含有一個讀寫分離的lua檔案,這也是我們使用mysql-proxy實現讀寫分離必用的檔案,它需要lua解析器進行解析。因此我們還需要安裝一個lua解析器。
2、基本環境
三臺linux虛擬主機
Linux版本CentOS6.6、MySQL 5.5
mysql-proxy-0.8.5
lua-5.1.4
ip:192.168.95.11(寫)、192.168.95.12(讀)、192.168.95.13(mysql-proxy)
3、配置主從複製
詳細可以參考:mysql主從複製與主主複製
http://www.cnblogs.com/phpstudy2015-6/p/6485819.html#_label2
粗略介紹一下資料庫的主從複製的配置:
第一步:
在192.168.95.11中建立一個192.168.95.12主機中可以登入的MySQL使用者
使用者:mysql12
密碼:mysql12
mysql>
GRANT
REPLICATION
SLAVE
ON
*.*
TO
‘mysql12’@’
192.168.95.12
’
IDENTIFIED
BY
‘mysql12’;
mysql>
FLUSH
PRIVILEGES
;
第二步:
檢視192.168.95.11MySQL伺服器二進位制檔名與位置
mysql>
SHOW MASTER STATUS;
第三步:
告知二進位制檔名與位置
在192.168.95.12中執行:
mysql> change master to
-> master_host=
'192.168.95.11'
,
->
master_user=
'mysql12'
,
->
master_password=
'mysql12'
,
->
master_log_file=
'mysql-bin.000124'
,
->
master_log_pos=
586
;
第四步:
在192.168.95.12中
mysql>
SLAVE START;
#
開啟複製
mysql>
SHOW SLAVE STATUS\G
#
檢視主從複製是否配置成功
主從複製配置成功!
(注意:上面Relicate_Do_DB:aa表示主從複製只針對資料庫aa【這是我之前設定的就沒改了】,這裡就不講這個了,要想去了解學醫這個的話可以參考文章:
http://www.cnblogs.com/phpstudy2015-6/p/6485819.html#_label7
4、MySQL讀寫分離配置
百度雲下載:
連結:http://pan.baidu.com/s/1slTl18L
密碼:9j0m
4.1、安裝lua
官網下載:http://www.lua.org/download.html
Lua 是一個小巧的指令碼語言。Lua由標準C編寫而成,程式碼簡潔優美,幾乎在所有作業系統和平臺上都可以編譯,執行。
一個完整的Lua直譯器不過200k,在目前所有指令碼引擎中,Lua的速度是最快的。這一切都決定了Lua是作為嵌入式指令碼的最佳選擇。
1)、安裝lua需要依賴很多軟體包。
可以通過rpm -qa | grep name檢查以下軟體是否安裝:
gcc*、gcc-c++*、autoconf*、automake*、zlib*、libxml*、ncurses-devel*、libmcrypt*、libtool*、flex*、pkgconfig*、libevent*、glib*
若缺少相關的軟體包,可通過yum -y install方式線上安裝,或直接從系統安裝光碟中找到並通過rpm -ivh方式安裝。(我的話一般是直接在系統光碟軟體庫中找到直接rpm安裝的,有些找不到,則先在網上下載然後在ftp傳給linux再進行安裝)
2)、依賴軟體安裝完畢後則進行編譯安裝lua
MySQL-Proxy的讀寫分離主要是通過rw-splitting.lua指令碼實現的,因此需要安裝lua。
官網下載:
http://www.lua.org/download.html(下載原始碼包)
#
wget http://www.lua.org/ftp/lua-5.1.4.tar.gz
#
tar zxvf lua-5.1.4.tar.gz
#
cd
lua-5.1.4
#
make linux
#
make install
#
export
LUA_CFLAGS=
"-I/usr/local/include"
LUA_LIBS=
"-L/usr/local/lib -llua -ldl"
LDFLAGS=
"-lm"
(我安裝的時候是直接在光碟軟體庫中找到,直接rpm安裝)
4.2、安裝mysql-proxy
1)、首先檢視linux版本確認是32位還是64為系統
檢視linux核心版本
# cat /etc/issue
檢視linux版本
# cat /proc/version
2)、按系統位數下載(上面百度雲連結64位的檔案)
3)、安裝
#
tar –zxvf mysql-proxy-0.8.5- linux-rhel5-x86-64bit.tar.gz
#
mkdir /usr/
local
/mysql-proxy
#
cp ./ mysql-proxy-0.8.5-linux-rhel5-x86-64bit/* /usr/
local
/mysql-proxy
#
cd
/usr/
local
/mysql-proxy
安裝成功
5、MySQL讀寫分離測試
1)、修改rw-splitting.lua檔案
修改預設連線,進行快速測試,不修改的話要達到連線數為4時才啟用讀寫分離
#cp/usr/local/mysql-proxy/share/doc/mysql-proxy/rw-splitting.lua ./
# vi rw-splitting.lua
2)、修改完成後,啟動mysql-proxy
#
cd
/usr/
local
/mysql/bin
#
./mysql-proxy --proxy-read-only-backend-addresses=192.168.95.12:3306 --proxy-backend-addresses=192.168.95.11:3306 --proxy-lua-script=/usr/
local
/mysql-proxy/rw-splitting.lua &
引數:
--proxy-read-only-backend-addresses
#
只讀伺服器地址(ip)
--proxy-backend-addresses
#
伺服器地址(主伺服器)
--proxy-lua-script
#lua
指令碼路勁
&
#
表示後臺執行
3)、建立用於讀寫分離的資料庫連線使用者
使用者名稱:proxy1
密 碼:321
mysql>grant
all
on
*.*
to
'proxy1'
@
'192.168.95.13'
identified
by
'321'
;
mysql>use
aa;
mysql>create
table
tab1(id
int
auto_increment,name
varchar
(
32
)
not
null
,primary
key
(id));
【因為已經開啟了主從複製所以,11、12主機mysql中都建立了這個使用者】
4)、測試登陸賬號[email protected]進行新增資料
可以使用任意ip客戶端登陸這個賬號
在192.168.95.13登陸:
#
./mysql -u proxy1 -P4040 -h192.168.95.13 –p
在兩個mysql中檢視結果:一致
結果表明:賬號使用
(ps:id是自增長,之前高主主複製的時候更改了配置檔案,還沒更改回來,就將就用著先吧)
5)、關閉12mysql的從複製
mysql> stop slave;
6)、證明寫分離
使用[email protected]賬號開啟多個客戶端進行插入資料
開啟三個mysql客戶端分別插入2條資料:
mysql>
insert
into
tab1 (name)
values
(
'stop_slave11111'
);
….
mysql>
insert
into
tab1 (name)
values
(
'stop_slave6666’);
檢視:
分別登陸11mysql與12mysql檢視aa.tab1中的資料
主資料庫:
從資料庫:
結果中顯示插入的資料存在與主資料庫,而從資料庫沒有,所以證明寫能夠分離。
7)、證明讀分離
使用[email protected]賬號登陸mysql,檢視aa.tab1中的資料
mysql>use
aa;mysql>select*from
tab1;
結果中顯示只有從資料庫的資料,結合上面的測試,可以證明讀分離。
6、建議
為了方便啟動與管理mysql-proxy可以建立mysql-proxy服務管理指令碼
下面這個管理指令碼僅適合以上我給出的安裝路徑位置
【此管理指令碼需要按照自己的安裝路徑做出相應的修改方可使用】
#!/bin/sh
#
# mysql-proxy This script starts and stops the mysql-proxy daemon
#
# chkconfig: - 78 30
# processname: mysql-proxy
# description: mysql-proxy is a proxy daemon to mysql
# Source function library.
. /etc/rc.d/init.d/
functions
#PROXY_PATH=/usr/local/bin
PROXY_PATH=/usr/
local
/mysql-proxy/bin
prog=
"mysql-proxy"
# Source networking configuration.
. /etc/sysconfig/network
# Check that networking is up.
[
${NETWORKING}
=
"no"
] &&
exit
0
# Set default mysql-proxy configuration.
#PROXY_OPTIONS="--daemon"
PROXY_OPTIONS=
"--proxy-read-only-backend-addresses=192.168.95.12:3306 --proxy-backend-addresses=192.168.95.11:3306 --proxy-lua-script=/usr/local/mysql-proxy/rw-splitting.lua"
PROXY_PID=/usr/
local
/mysql-proxy/run/mysql-proxy.pid
# Source mysql-proxy configuration.
if
[ -f /etc/sysconfig/mysql-proxy ];
then
. /etc/sysconfig/mysql-proxy
fi
PATH=
$PATH
:/usr/bin:/usr/
local
/bin:
$PROXY_PATH
# By default it's all good
RETVAL=0
# See how we were called.
case
"
$1
"
in
start)
# Start daemon.
echo
-n $
"Starting
$prog
: "
$NICELEVEL
$PROXY_PATH
/mysql-proxy
$PROXY_OPTIONS
--daemon --pid-file=
$PROXY_PID
--user=root --
log
-level=debug --
log
-file=/usr/
local
/mysql-proxy/
log
/mysql-proxy.log
RETVAL=$?
echo
if
[
$RETVAL
= 0 ];
then
touch /var/lock/subsys/mysql-proxy]
echo
"ok"
fi
;;
stop)
# Stop daemons.
echo
-n $
"Stopping
$prog
: "
killproc
$prog
RETVAL=$?
echo
if
[
$RETVAL
= 0 ];
then
rm -f /var/lock/subsys/mysql-proxy
rm -f
$PROXY_PID
fi
;;
restart)
$0
stop
sleep 3
$0
start
;;
condrestart)
[ -e /var/lock/subsys/mysql-proxy ] &&
$0
restart
;;
status)
status mysql-proxy
RETVAL=$?
;;
*)
echo
"Usage:
$0
{start|stop|restart|status|condrestart}"
RETVAL=1
;;
esac
exit
$RETVAL
#
---
我將mysql-proxy服務管理指令碼放在了/usr/
local
/mysql-proxy/init.d/
資料夾裡
#
---
給執行許可權,建立相應目錄
#
chmod +x /usr/
local
/mysql-proxy/init.d/mysql-proxy
#
mkdir /usr/
local
/mysql-proxy/run
#
mkdir /usr/
local
/mysql-proxy/
log
#
cd
/usr/
local
/mysql-proxy/init.d/
#
---
啟動mysql-proxy
#
./mysql-proxy start
#
---
停止mysql-proxy
#
./mysql-proxy stop
#
---
重啟mysql-proxy
#
./mysql-proxy restart
一些相關引數:
PROXY_PATH=/usr/local/mysql-proxy/bin //
定義mysql-proxy服務二進位制檔案路徑
--proxy-read-only-backend-addresses=
192.168
.
95.12
:
3306
//
定義後端只讀從伺服器地址
--proxy-backend-addresses=
192.168
.
95.11
:
3306
//
定義後端主伺服器地址
--proxy-lua-script=/usr/local/mysql-proxy/rw-splitting.lua
//
定義lua讀寫分離指令碼路徑
PROXY_PID=/usr/local/mysql-proxy/run/mysql-proxy.pid //
定義mysql-proxy PID檔案路徑
--daemon //
定義以守護程序模式啟動
--keepalive //
使程序在異常關閉後能夠自動恢復【上面的管理指令碼沒有加上此引數】
--user=root //
以root使用者身份啟動服務
--
log
-level=debug
//
定義
log
日誌級別,由高到低分別有(error|warning|info|message|debug)
--
log
-file=/usr/local/mysql-proxy/
log
/mysql-proxy.
log
//
定義
log
日誌檔案路徑