四、mysql核心-鎖與事務

前言:

關於什麼是鎖,爲什麼需要鎖等概念這裏不做解釋了,可以看下我的另一諞關於jdk中併發的欄目,其中介紹了併發的各種只是框架,包括鎖以及jdk的鎖實現等等。併發編程、鎖等知識框架

一、mysql中的鎖

本章節講述表鎖和行鎖,gap鎖在本文後邊事務中會講到
因爲MyISAM支持表鎖,InnoDb支持表、行鎖。所以按照引擎來講述下這兩個鎖

1、表鎖

特性
開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發 度最低。

適用場景
表級鎖更適合於以查詢爲主,只有少量按索引條件更新數據的應用,如 OLAP 系統

2、行鎖

特性
開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發 度也最高
適用場景
行級鎖則更適合於有大量按索引條件併發更新少量不同數據,同時又有併發查詢的應用,如 一些在線事務處理(OLTP)系統。

3、MyISAM鎖

MySQL 的表級鎖有兩種模式: 表共享讀鎖(Table Read Lock) 表獨佔寫鎖(Table Write Lock)

3.1共享讀鎖

# session1 鎖testmysam表
lock table testmysam READ 
# session2
select * from testmysam # 可以查詢 
#session1
insert into testmysam value(2); # 報錯
update testmysam set id=2 where id=1; # 報錯
# session2 
insert into testmysam value(2); # 等待 
#session1 
insert into account value(4,'aa',123); #報錯
select * from account ; #報錯 
# session2 中
insert into account value(4,'aa',123); #成功 
# session1  
select s.* from lock table 表名 as 別名 read;

總結

  • 對錶1加讀鎖之後,只能對1進行讀操作,不能寫表1,也不能操作其他表
  • 對MyISAM表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求

3.2獨佔寫鎖

# session1 對testmysam加寫鎖
lock table testmysam WRITE
# session1對testmysam增刪改查都可以
insert testmysam value(3);
delete from testmysam where id = 3 
select * from testmysam
# session1對其他表操作報錯
select s.* from testmysam s
insert into account value(4,'aa',123);
#session 2 會等待,因爲寫鎖會阻塞讀操作
select * from testmysam

總結:

  • 寫鎖,對 MyISAM 表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作
  • 對 MyISAM 表的寫操作,當前 session 可以對本表做 CRUD,但對其他表進行操作會報錯

4、InnoDB鎖

共享鎖又稱:讀鎖:當一個事務對某幾行上讀鎖時,允許其他事務對這幾行進行讀操作,但 不允許其進行寫操作,也不允許其他事務給這幾行上排它鎖,但允許上讀鎖。
排它鎖又稱:寫鎖:當一個事務對某幾個上寫鎖時,不允許其他事務寫,但允許讀。更不允 許其他事務給這幾行上任何鎖。包括寫鎖。

注:

  1. 兩個事務不能鎖同一個索引。
  2. insert ,delete , update 在事務中都會自動默認加上排它鎖。
  3. 行鎖必須有索引才能實現,否則會自動鎖全表,那麼就不是行鎖了。
# 建表
CREATE TABLE testdemo (
`id` int(255) NOT NULL ,
`c1` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
`c2` int(50) NULL DEFAULT NULL ,
PRIMARY KEY (`id`),
INDEX `idx_c2` (`c2`) USING BTREE )
ENGINE=InnoDB;
# 插入數據
insert into testdemo VALUES(1,'1',1),(2,'2',2);
# 開啓事務1
BEGIN
select * from testdemo where id =1 for update
# 另一個session
update testdemo set c1 = '1' where id = 2 #成功 
update testdemo set c1 = '1' where id = 1 #等待
BEGIN
update testdemo set c1 = '1' where id = 1 
# 在另外一個 session 中
update testdemo set c1 = '1' where id = 1 等待
BEGIN
update testdemo set c1 = '1' where c1 = '1'
# 在另外一個 session 中
update testdemo set c1 = '2' where c1 = '2' #等待,因爲c1行沒有索引,直接會進行鎖表操作
# 第一個 session 中
select * from testdemo where id =1 for update
# 第二個 session
select * from testdemo where id =1 lock in share mode #阻塞,寫鎖會阻塞讀鎖

5、鎖的等待問題

工作中可能會遇到這種問題。

#程序員甲,正直調試代碼
BEGIN
SELECT * FROM testdemo WHERE id = 1 FOR UPDATE
# 你正直完成的功能也要經過那部分的代碼,你得上個讀鎖,然後你就只能等待他釋放鎖。
BEGIN
SELECT * FROM testdemo WHERE id = 1 lock in share mode

解決方案

# 查看mysql鎖詳情
select * from information_schema.INNODB_LOCKS;

發現有兩個鎖在對同一個數據進行操作,X (寫鎖),另外一個是 S(讀鎖)

# 查看mysql正在等待鎖的query
select * from sys.innodb_lock_waits

在這裏插入圖片描述
我現在執行的這個 sql 語句有了,另外看下最下面,kill 命令,你在工作中完全可以通過 kill 吧阻塞了的 sql 語句給幹掉,你就可以繼續運行了,不過這命令也要注意使用過,如果某同 事正在做比較重要的調試,你 kill,被他發現可能會被暴打一頓。

二、事務

1、事務的概念

現在的很多軟件都是多用戶,多程序,多線程的,對同一個表可能同時有很多人在用,爲保 持數據的一致性,所以提出了事務的概念。

舉例:A 給 B 要劃錢,A 的賬戶-1000 元, B 的賬戶就要+1000 元,這兩個 update 語句必須作爲 一個整體來執行,不然 A 扣錢了,B 沒有加錢這種情況很難處理。

2、事務的特性

事務應該具有 4 個屬性:原子性、一致性、隔離性、持久性。這四個屬性通常稱爲 ACID 特性。

  • 原子性(atomicity)
    一個事務必須被視爲一個不可分割的最小單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗,對於一個事務來說,不可能只執行其中的一部分操作
  • 一致性(consistency)
    一致性是指事務將數據庫從一種一致性轉換到另外一種一致性狀態,在事務開始之前和事務結束之後數據庫中數據的完整性沒有被破壞
  • 隔離性(isolation)
    一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據對併發的其他事 務是隔離的,併發執行的各個事務之間不能互相干擾。 (對數據庫的並行執行,應該像串行執行一樣)
  • 持久性(durability)
    一旦事務提交,則其所做的修改就會永久保存到數據庫中。此時即使系統崩潰,已經提交的修改數據也不會丟失

3、事務併發存在的問題

3.1 髒讀

事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據

3.2不可重複度

事務 A 多次讀取同一數據,事務 B 在事務 A 多次讀取的過程中,對數據作了更新並提交,導致事務 A 多次讀取同一數據時,結果 不一致。

3.3幻讀

和不可重複讀唯一的卻別就是通過插入或者刪除數據導致的,比如事務A插入主鍵id=1的數據,未提交。事務B去插入id=1的數據,報錯:主鍵衝突。但是事務B查詢的時候卻查詢不到id=1的數據,事務A一臉懵逼。幻讀無疑發生了

4、解決事務併發問題-事務隔離級別

4.1未提交讀(READ UNCOMMITED)

未提交讀我們可以看到3.1、3.2、3.3三個問題都是存在的。因爲事務幾乎沒有起到作用。

4.2已提交讀 (READ COMMITED)

4.2.1解決了髒讀問題
# 查看事務的隔離級別
show variables like '%tx_isolation%';
# 設置事務隔離級別
set SESSION TRANSACTION ISOLATION LEVEL read committed;
# session1 中
start TRANSACTION
update account set balance = balance -50 where id = 1

# session2 中查詢 (數據並沒改變) 
select * from account
# 回到第一個 session2 中
commit
# session2中數據已經改變
select * from account # (數據已經改變)
4.2.2存在不可重複度問題
# 設置隔離級別
set SESSION TRANSACTION ISOLATION LEVEL read committed;
# 事務A
start TRANSACTION
update account set balance = balance -50 where id = 1
# 事務B,查詢數據未變化,因爲A還未提交
select * from account
# 事務 A
commit
# 事務B中,發現事務已經改變,事務B兩次讀取的數據不一致,則存在不可重複讀問題
select * from account 

4.3可重複讀(REPEATABLE READ)

4.3.1解決了不可重複讀

例子和4.2.2中一樣,只不過修改了隔離級別爲可重複讀

set SESSION TRANSACTION ISOLATION LEVEL repeatable read;

發現事務B兩次讀取到的數據是一致的,即解決了不可重複讀問題。
但是,B讀取不到A已經提交的數據了,這難道不會存在問題嗎?

  • 假設數據本來100
  • A事務減去50變爲50
  • B事務讀取是100
  • A事務commit
  • B事務讀取還是100,再進行減50
  • B事務commit

那數據庫中數據豈不是是50 ,但是我們明明是減了兩次。這就違反了數據庫完整性約束了呀。別慌,我們去數據庫查看數據的時候,發現數據是0,並沒有違反數據庫完整性約束。爲什麼呢
因爲可重複讀的隔離級別下使用了MVCC機制,select操作不會更新版本號,是快照讀(歷史版本);insert、update和delete會更新版本號,是當前讀(當前版本)。

4.3.2存在幻讀問題
# 事務A
begin
insert into account (id) values(1)
# 事務B
begin
insert into account (id) values(1) # 報錯,主鍵衝突
select * from  account where id=1; # 查詢不到數據,一臉懵逼,你告訴我主鍵衝突,但是我卻查詢不到數據,幻讀發生了

mysql在可重複度的隔離級別下其實已經解決的幻讀的問題,使用的是間隙鎖(gap鎖)

# 事務A
begin
# 插入數據並且對ID=1進行加鎖(對索引加鎖)
insert into account (id) values(1) for update
# 事務B
begin
# 發現id=1已經被鎖住了,所以會阻塞等待
insert into account (id) values(1) # 報錯,主鍵衝突
# 等到事務Acommit後就會報主鍵衝突,這時候再去查找id=1的數據就可以查找到了

4.4可串行化(SERIALIZABLE)

相當於鎖表,性能太差,所以一般不建議使用

show variables like '%tx_isolation%';
set SESSION TRANSACTION ISOLATION LEVEL serializable;
# 事務A
begin
select * from account
# 事務B
begin
select * from account
# 發現根本就不讓插入,因爲事務A對整張表進行了加鎖,我們必須等待事務Acommit釋放鎖之後才能進行寫操作
insert into account VALUES(4,'deer',500) 

gap鎖小甜點

select @@tx_isolation;
create table t_lock_1 (a int primary key);
insert into t_lock_1 values(10),(11),(13),(20),(40);
# 事務A
begin
select * from t_lock_1 where a <= 13 for update;
# 事務B
insert into t_lock_1 values(21) 
insert into t_lock_1 values(19) #阻塞
# 因爲在 rr 隔離級別中者個會掃描到當前值(13)的下一個值(20),並把這些數據全部加鎖,所以20之前的額數據都被加鎖了
create table t_lock_2 (
a int primary key,
b int, key (b)); 
insert into t_lock_2 values(1,1),(3,1),(5,3),(8,6),(10,8);
# 事務A
BEGIN
select * from t_lock_2 where b=3 for update;
# 事務B
// 不可執行,因爲 a=5 上有一把記錄鎖 
select * from t_lock_2 where a = 5 lock in share mode; 
//  不可以執行,因爲 b=2 在(1, 3]內
insert into t_lock_2 values(4, 2); 
// 不可以執行,因爲 b=5 在(3, 6)內 
insert into t_lock_2 values(6, 5); 
// 可以執行,(2, 0)均不在鎖住的範圍內
insert into t_lock_2 values(2, 0);
// 可以執行,(6, 7)均不在鎖住的範圍內 
insert into t_lock_2 values(6, 7); 

二級索引鎖住的範圍是 (1, 3],(3, 6) 主鍵索引只鎖住了 a=5 的這條記錄 [5]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章