性能優化之數據庫篇2-事務與鎖


MySQL事務

事務可靠性模型ACID:

  • Atomicity: 原子性,一次事務中的操作要么成功,要么失敗
  • Consistency: 一致性,跨表,跨行,跨事務,數據庫始終保持一致狀態
  • Isolation: 隔離性,保護事務不會互相干擾,包含4種隔離級別
  • Durability: 持久性,事務提交后,不會丟數據。如系統崩潰,電源故障。

事務隔離級別

  • 讀未提交 READ UNCOMMITTED
  • 讀已提交 READ COMMITTED
  • 可重復讀 REPEATABLE READ
  • 可串行化 SERIALIZABLE

可以設置全局的默認隔離級別,也可以單獨設置會話的隔離級別

  1. 讀未提交

很少使用,不能保證一致性。

  • 問題:臟讀、幻讀、不可重復讀
  • 鎖:以非鎖的方式執行
  1. 讀已提交

每次查詢都會設置和讀取自己的新快照

  • 問題:幻讀、不可重復讀
  • 鎖:鎖定索引記錄,而不鎖定記錄之間的間隙
  1. 可重復讀(默認隔離級別

使用事務第一次讀取時創建的快照。

  • 問題:幻讀
  • 鎖:使用唯一索引的唯一查詢條件時,只鎖定查找到的索引記錄。其他查詢條件,會鎖定掃描到的索引范圍,通過間隙鎖或臨鍵鎖來阻止其他會話在這個范圍中插入值。
  1. 串行化

最嚴格的級別,事務串行執行,資源消耗最大

鎖是數據庫系統區別於文件系統的一個關鍵特性,用於管理對共享資源的並發訪問。Innodb引擎是在行級別對數據庫上鎖。

MySQL擁有4種鎖

  • 共享鎖 (S Lock),允許事務讀一行數據
  • 排他鎖(X Lock),允許事務刪除或更新一行數據

為了支持多粒度的鎖定,這種鎖定允許行級上的鎖和表級上的鎖同時存在。InnoDB還支持意向鎖。意向鎖就是把鎖定的對象分為多個層次,意向鎖意味着事務希望在更細粒度上加鎖。如下圖所示,如果需要對記錄r上X鎖,那么就需要對數據庫A、表、頁上意向鎖IX,最后對r上X鎖。如果其中任何一個部分導致等待,那么該操作需要等待粗粒度鎖的完成。

意向鎖的設計目的是為了在一個事務中揭示下一行將被請求的鎖類型

  • 意向共享鎖 (IS Lock),事務想要獲得一個表中某幾行的共享鎖
  • 意向排他鎖 (IX Lock),事務想要獲得一個表中某幾行的排他鎖

兼容關系圖:

鎖類型 IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

用戶可以通過命令查看鎖請求的信息

間隙鎖

會鎖定整個查詢范圍內的所有鍵值,即使這個鍵值不存在。

如:

select * from emp where empid > 1 and empid<10 for update;

即使emp只有2,4,7三條記錄,也會對1-10之間的“間隙”記錄加鎖。

SHOW ENGINE INNODB STATUS

悲觀鎖與樂觀鎖

悲觀鎖和樂觀鎖是人們定義出來的概念,你可以理解為一種思想。

悲觀鎖

悲觀鎖:就是對數據處理保持悲觀態度,總認為會沖突。所以在整個數據處理過程中,需要將數據鎖定。

悲觀鎖的實現通常通過數據庫提供的鎖機制實現,比如mysql中的排他鎖,select ... for update.

舉個例子。扣減庫存的情況下,我們需要先判斷商品庫存是否充足。

  1. 建表
CREATE TABLE `tb_goods_stock` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `goods_id` bigint(20) unsigned DEFAULT '0' COMMENT '商品ID',
  `nums` int(11) unsigned DEFAULT '0' COMMENT '商品庫存數量',
  `create_time` datetime DEFAULT NULL COMMENT '創建時間',
  `modify_time` datetime DEFAULT NULL COMMENT '更新時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `goods_id` (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品庫存表';
  1. 開啟事務,查詢商品,並對記錄加鎖

先關閉自動提交事務 set autocommit = 0;

BEGIN;
SELECT nums FROM tb_goods_stock WHERE id=1 FOR UPDATE;
  1. 判斷商品數量是否大於購買數量。如果不滿足,回滾事務
  2. 如果滿足條件,扣庫存,提交事務
UPDATE tb_goods_stock SET nums=nums-5 WHERE id=1 AND nums>5;
COMMIT;
  1. 事務提交時會釋放鎖。

樂觀鎖

樂觀鎖是對數據的處理保持樂觀態度,認為數據一般不會沖突,只有提交數據更新時,才會對數據釋放沖突進行檢測。

樂觀鎖的實現不依靠數據庫提供的鎖機制,而是需要我們自己實現,實現方式一般是記錄數據版本,一種是通過版本號,一種是公故時間戳。很多持久化框架已經封裝好了樂觀鎖的實現,如hibernate。

  1. 建表
CREATE TABLE `tb_goods_stock` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `goods_id` BIGINT(20) UNSIGNED DEFAULT '0' COMMENT '商品ID',
  `nums` INT(11) UNSIGNED DEFAULT '0' COMMENT '商品庫存數量',
  `create_time` DATETIME DEFAULT NULL COMMENT '創建時間',
  `modify_time` DATETIME DEFAULT NULL COMMENT '更新時間',
  `version` BIGINT(20) UNSIGNED DEFAULT '0' COMMENT '版本號',
  PRIMARY KEY (`id`),
  UNIQUE KEY `goods_id` (`goods_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品庫存表';

  1. 查詢要賣的商品和版本號
BEGIN;
SELECT nums,VERSION FROM tb_goods_stock WHERE id=1;
  1. 判斷商品數量是否大於購買數量。如果不滿足,就回滾事務。
  2. 如果滿足,扣減庫存
UPDATE tb_goods_stock SET nums=nums-5,VERSION=VERSION+1 WHERE id=1 AND nums>5 AND VERSION=0;
  1. 判斷更新是否成功,如果成功,則提交,否則就回滾。

日志

undo log :撤銷日志

保證事務的原子性

用處:事務回滾、一致性讀、崩潰恢復
記錄事務回滾時所需的撤銷操作。一條insert語句,對應一條delete的undo log,一條update語句,對應一條反向update的undo log

redo log:重做日志

確保事務的持久性,防止事務提交后未刷新到磁盤就崩潰了

事務執行過程中寫入redo log,記錄事務對數據頁做了哪些修改

可以提升性能,先寫日志,再刷磁盤。

日志文件:ib_logfile0,ib_logfile1

日志緩沖:innodb_log_buffer_size

強刷:fsync()

MVCC 多版本並發控制

可以解決的問題:

  • 使InnoDB支持一致性讀:READ COMMITTED和REPEATABLE READ。當我們在某個時間點查詢數據時,只能看到這個時間點之前的事務提交更新的結果。
  • 讓查詢不被阻塞、無需等待被其他事務持有的鎖,可以增加並發性能。

快照讀:讀取的是快照數據,不加鎖的簡單Select 都屬於快照讀

當前讀:讀取的是最新數據,加鎖的select,或者增刪改都是進行當前讀。

1. 行記錄的隱藏列

  • row_id :隱藏的行id,用來生成默認的聚集索引,如果創建表時沒有指定聚集索引,這時InnoDB就會用這個隱藏id來創建聚集索引。
  • trx_id:操作這個數據的事務id,最后一個對數據操作的事務id
  • roll_pointer:回滾指針,指向這個記錄的undo log數據

數據的快照記錄通過鏈表的結構串聯起來了,要找到歷史快照,可以通過遍歷roll_ptr的方式查找。

2. Read View

  • trx_ids 系統當前正在活躍的事務id集合
  • low_limit_id Read View生成時刻系統尚未分配的下一個事務ID
  • up_limit_id 活躍事務中最小的事務id

Read View是事務進行快照讀操作的時候生產的讀試圖,在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄並維護當前活躍事務的ID(每個事務開啟都有一個ID,這個ID是遞增的)

如果一個事務要查詢行記錄,那么應該讀哪個快照版本呢?Read View就是解決這個問題的。Read View可以幫助我們解決可見性問題,Read View保存了當前事務開啟時所有活躍的事務列表。換個角度可以理解為Read View保存了不應該讓這個事務看到的其他事務Id的列表。

如果事務隔離級別是

  • 讀已提交,那么一個事務每次的select都會去查一次Read View,每次查到的Read View不同,就可能導致不可重復讀或幻讀的情況

  • 可重復讀,那么事務只在第一次select的時候獲取一次Read View,然后后面的select會復用這個Read View。

舉個例子:
當事務2對用戶表某行數據執行了快照讀,數據庫為該行數據生成一個Read View讀視圖,假設當前事務ID為2,此時還有事務1和事務3在活躍中,事務4在事務2快照讀前一刻提交更新了

update user set name='小明1' where id=1

此時undo日志中存在如下版本鏈。

事務2會拿着trx_id字段記錄的事務id:4去跟Read View中的up_limit_id(1)和low_limit_id(5)比較

  1. 判斷db_trx_id < up_limit_id,如果為true就 結束,否則繼續判斷。
  2. db_trx_id >= low_limit_id ,如果為true就結束,否則繼續判斷
  3. 判斷db_trx_id是否在活躍列表中,若不存在,符合條件,結束。

優化實戰

  1. 建表的時候字段盡量小,如果是固定的,可以用char.

小貼士:int、bigint等后面寫的數字沒有實際意義。如int(8)

示例建表的字段類型選擇:

  1. 存儲引擎的選擇,一般用Innodb,如果是冷數據,也沒人查了,但是又不能刪,可以用TokuDB,支持高壓縮比(1:12)

  2. 注意sql中的隱式轉換

  3. 適當增加索引,注意索引的區分度,如身份證號、手機號比較好。性別、年齡就比較差。

  4. 主鍵id推薦單調遞增,這樣插入的時候速度更快

  5. 數據寫入優化

  • 通過PreparedStatement減少SQL解析
  • Multiple Values減少交互。
insert into persons 
(id_p, lastname , firstName, city )
values
(200,'haha' , 'deng' , 'shenzhen'),
(201,'haha2' , 'deng' , 'GD'),
(202,'haha3' , 'deng' , 'Beijing');
  1. 數據更新,范圍更新需要謹慎,避免鎖的范圍過大
  2. 模糊查詢

like 如果前面不加%,還是能走到索引的。如果查詢條件非常多,並且任意組合的那種場景,不建議再用sql來做,推薦solr、ES。
9. 連接查詢 避免笛卡爾積

連接查詢時,當連接on條件是非唯一字段時,會出現笛卡爾積(局部笛卡爾積);當連接on條件是唯一字段時,則不會出現笛卡爾積。
10. 注意索引失效

  • null、not、not in、函數等
  • 減少使用or,可以用union或union all(不去重,比union性能好)代替
  • 必要時可以用force index強制指定索引
  1. 查詢sql的設計
  • 考慮查詢量和查詢次數的平衡
  • 避免大量數據的傳輸
  • 避免使用臨時文件排序或臨時表
  • 分析類需求,可以用匯總表


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM