1. 程式人生 > >MySQL/InnoDB中的鎖、悲觀鎖、共享鎖、排它鎖、行鎖、表鎖、死鎖與MySQL讀寫分離

MySQL/InnoDB中的鎖、悲觀鎖、共享鎖、排它鎖、行鎖、表鎖、死鎖與MySQL讀寫分離

MySQL/InnoDB的加鎖,一直是一個面試中常問的話題。例如,資料庫如果有高併發請求,如何保證資料完整性?產生死鎖問題如何排查並解決?我在工作過程中,也會經常用到,樂觀鎖,排它鎖,等。於是今天就對這幾個概念進行學習,屢屢思路,記錄一下。

注:MySQL是一個支援外掛式儲存引擎的資料庫系統。本文下面的所有介紹,都是基於InnoDB儲存引擎,其他引擎的表現,會有較大的區別。

儲存引擎檢視

MySQL給開發者提供了查詢儲存引擎的功能,我這裡使用的是MySQL5.6.4,可以使用:

SHOW ENGINES

樂觀鎖

用資料版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂資料版本?即為資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的 “version” 欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加1。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。

舉例

1、資料庫表設計

三個欄位,分別是 id,valueversion

selectid,value,versionfromTABLEwhereid=#{id}

2、每次更新表中的value欄位時,為了防止發生衝突,需要這樣操作

updateTABLE

setvalue=2,version=version+1

whereid=#{idandversion=#{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,valuevalues (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日誌檔案路徑