大併發熱點行更新的兩個騷操做

要想db操做的性能足夠高,巧妙的設計很重要,事務的操做範圍要儘可能的小。通常狀況下咱們都是使用某個orm框架來操做db,這一類框架多數的實現方式都是誇網絡屢次交互來開啓事務上下文和執行sql操做,是個黑盒子,包括對 autocommit 設置的時機也會有一些差別,稍微不注意就會踩坑。mysql

在大併發的狀況下加上誇網絡屢次交互,就不可避免的因爲網絡延遲、丟包等緣由致使事務的執行時間過長,出現雪崩機率會大大增長。建議在性能和併發要求比較高的場景下儘可能少用orm,若是非要用盡可能控制好事務的範圍和執行時間。sql

大併發db操做的原則就是事務操做盡可能少跨網絡交互,一旦跨網絡使用事務儘可能用樂觀鎖來解決,少用悲觀鎖,儘可能縮短當前 session 持有鎖的時間。服務器

下面分享兩個在mysql innodb engine 上的大併發更新行的騷操做,這兩個騷操做都是儘量的縮小db鎖的範圍和時間。網絡

轉化update爲insert

比較常見的大併發場景之一就是熱點數據的 update,好比具備預算類的庫存、帳戶等。session

update從原理上須要innodb engine 先獲取row數據,而後進行row format轉換到mysql服務層,再經過mysql服務器層進行數據修改,最後再經過innodb engine寫回。數據結構

這整個過程每個環節都有必定的開銷,首先須要一次innodb查詢,而後須要一次row format(若是row比較寬的話性能損失仍是比較大的),最後還須要一次更新和一次寫入,大概須要四個小階段。併發

一次update就須要上述四過程開銷。此時若是qps很是大,必然會有必定性能開銷(這裏暫不考慮cache、mq之類的削峯)。那麼咱們能不能將單個行的熱點分散開來,同時將update轉換成insert,咱們來看下如何騷操做。mvc

咱們引入 slot 概念,原來一個row 咱們經過多個row來表示,結果經過sum來彙總。爲了避免讓slot成爲瓶頸,咱們 rand slot,而後將update轉換成insert,經過 on duplicate key update 子句來解決衝突問題。框架

咱們建立一個sku庫存表。性能

CREATE TABLE `tb_sku_stock` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `sku_id` bigint(20) NOT NULL,
  `sku_stock` int(11) DEFAULT '0',
  `slot` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_sku_slot` (`sku_id`,`slot`),
  KEY `idx_sku_id` (`sku_id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4

表中惟一性索引 idx_sku_slot 用來約束同一個 sku_id 不一樣 slot 。

庫存增長操做和減小操做要分開來處理,咱們先看增長操做。

insert into tb_sku_stock (sku_id,sku_stock,slot)
values(101010101, 10, round(rand()*9)+1) 
on  duplicate key update sku_stock=sku_stock+values(sku_stock)

咱們給 sku_id=101010101 增長10個庫存,經過 round(rand()*9)+1 將slot控制在10個之內(能夠根據狀況放寬或縮小),當 unique key 不衝突的話就一直是insert,一旦發生 duplicate 就會執行 update。(update也是分散的)

咱們來看下減小庫存,減小庫存沒有增長庫存那麼簡單,最大的問題是要作前置檢查,不能超扣。

咱們先看庫存總數檢查,好比咱們扣減10個庫存數。

select sku_id, sum(sku_stock) as ss
from tb_sku_stock
where sku_id= 101010101
group by sku_id having ss>= 10 for update

mysql的查詢是使用mvcc來實現無鎖併發,因此爲了實時一致性咱們須要加上for update來作實時檢查。
若是庫存是夠扣減的話咱們就執行 insert into select 插入操做。

insert into tb_sku_stock (sku_id, sku_stock, slot)
select sku_id,-10 as sku_stock,round(rand() *9+ 1)
from(
    select sku_id, sum(sku_stock) as ss
    from tb_sku_stock
    where sku_id= 101010101
    group by sku_id having ss>= 10 for update) as tmp
on duplicate key update sku_stock= sku_stock+ values(sku_stock)

整個操做都是在一次db交互中執行完成,若是控制好單表的數據量加上 unique key 配合性能是很是高的。

消除 select...for update

大型OLTP系統,都會有一些須要週期性執行的任務,好比按期結算的訂單、按期取消的協議等,還有不少兜底的檢查、對帳程序等都會檢查必定時間範圍內的狀態數據,這些任務通常都須要掃描表裏的某個狀態字段。

這些查詢基本基於相似status狀態字段,因爲區分度很是低,因此索引基本上在這類場景下沒有太大做用。

爲了保證掃描出來的數據不會發生併發重複執行的問題會對數據加排他鎖,一般就是 select...for update,那麼這部分數據就不會被重複讀取到。可是也就意味着當前db線程將block在此鎖上,就是一個串行操做。

因爲是排查鎖,數據的 insert、update 都會受到影響,在 repeatable read (可重複讀)且沒有 unqiue key 的場合下還會觸發Gap lock(間隙鎖)。

咱們能夠經過一個方式來消除 select...for update,而且提升數據併發處理能力。

CREATE TABLE `tb_order` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `order_id` bigint(20) NOT NULL,
  `order_status` int(11) NOT NULL DEFAULT '0',
  `task_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

咱們簡單建立一個訂單表,task_id 是任務id,先讓數據結構支持多任務並行。

select order_id from tb_order where order_status=0 limit 10 for update

通常作法是經過select...for update 鎖住行。咱們換個方法實現一樣的效果同時不會存在併發執行問題。

update tb_order set task_id=10 where order_status=0 limit 10;
Query OK, 4 rows affected
select order_id from tb_order where task_id=10 limit 4;

假設咱們當前有不少並行任務(1-10),假設task_id=10任務執行,先update搶佔本身的數據行。這個操做基本上在單數ms內,而後再經過select 帶上本身的taskid獲取到屬於當前task的行,同時能夠帶上準確的limit,由於update是會返回受影響行數。

這裏會有一個問題,就是執行的task若是因爲某個緣由終止了怎麼辦,簡單方法就是用一個兜底job按期檢查超過必定時間的task,而後將task_id置爲空。

做者:王清培(趣頭條 Tech Leader)

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息