記一次mysql事務(wù)并發(fā)優(yōu)化

背景

事情的情況大致是這樣的。一個(gè)扣減庫(kù)存的業(yè)務(wù)上線以后,隔幾天會(huì)報(bào)一次錯(cuò),錯(cuò)誤內(nèi)容如下:

ERROR - exception: UncategorizedSQLException,"detail":"org.springframework.jdbc.UncategorizedSQLException:
### Error updating database.  Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLQueryInterruptedException: Query execution was interrupted
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: UPDATE a SET stock = stock - ? WHERE id = ? and stock >= ?
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLQueryInterruptedException: Query execution was interrupted
; uncategorized SQLException for SQL []; SQL state [70100]; error code [1317]; Query execution was interrupted; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLQueryInterruptedException: Query execution was interrupted
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:84)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
at com.sun.proxy.$Proxy64.update(null:-1)
at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:294)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)

這塊業(yè)務(wù)之前一直都是正常的,遷移以后做的唯一大的改動(dòng)就是加了java事務(wù)注解,所以懷疑是否和事務(wù)有關(guān)。

排查過(guò)程

首先現(xiàn)尋找報(bào)錯(cuò)的關(guān)鍵信息,第一個(gè)看到的是UncategorizedSQLException。查看這個(gè)類(lèi)的源代碼,說(shuō)明如下:

/**
 * Exception thrown when we can't classify a SQLException into
 * one of our generic data access exceptions.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 */

可以看到,這個(gè)類(lèi)是Spring無(wú)法歸類(lèi)的一個(gè)SQL異常,所以從這個(gè)異常我們是看不出什么內(nèi)容的,繼續(xù)往下。

MySQLQueryInterruptedException: Query execution was interrupted

發(fā)現(xiàn)這個(gè)執(zhí)行是超時(shí)被kill了。一般來(lái)講,我們公司dba設(shè)置的mysql超時(shí)時(shí)間是500ms。是否是因?yàn)閿?shù)據(jù)量太大,沒(méi)有走到索引,才導(dǎo)致update操作執(zhí)行時(shí)間太長(zhǎng)被kill了呢?看一下update語(yǔ)句里

UPDATE a SET stock = stock - ? WHERE id = ? and stock >= ?

馬上排除了這個(gè)可能性。因?yàn)閣here條件里有id查詢(xún),必定會(huì)走主鍵索引,不可能沒(méi)有走到索引。那會(huì)是什么原因呢?
上面我們提到,這個(gè)業(yè)務(wù)操作里是開(kāi)啟了事務(wù)的,還原一下大體的執(zhí)行情況。

start transaction;
UPDATE a SET stock = stock - 1 WHERE id = 100 and stock >= 1;
INSERT INTO a (num) values (1);
commit;

查了一下當(dāng)時(shí)的日志,發(fā)現(xiàn)1秒內(nèi)有大約200條請(qǐng)求對(duì)一條記錄做更新庫(kù)存的操作。
線索漸漸清晰起來(lái)了,事故現(xiàn)場(chǎng)大致應(yīng)該是這樣的:

T1 T2
begin begin
UPDATE a SET stock = stock - 1 WHERE id = 100 and stock >= 1;
UPDATE a SET stock = stock - 1 WHERE id = 100 and stock >= 1;
INSERT INTO a (num) values (1);
T2執(zhí)行完成,1 rows affected
T1 Query execution was interrupted

結(jié)論

由于開(kāi)啟了事務(wù),在高并發(fā)地對(duì)一條記錄進(jìn)行更新的情況下,多個(gè)請(qǐng)求會(huì)進(jìn)入排隊(duì)系統(tǒng)。由于鎖的競(jìng)爭(zhēng)是不公平的,當(dāng)多個(gè)事務(wù)同時(shí)對(duì)一條記錄進(jìn)行更新時(shí),極端情況下,就可能會(huì)出現(xiàn)一個(gè)更新操作進(jìn)去排隊(duì)系統(tǒng)以后,一直拿不到鎖,超過(guò)500ms被kill了。

細(xì)節(jié)分析

以上的業(yè)務(wù)操作,update會(huì)先申請(qǐng)行鎖,拿到行鎖以后進(jìn)行更新,更新完以后會(huì)執(zhí)行插入操作。那么在插入操作的時(shí)候是否需要申請(qǐng)鎖呢?
答案是肯定的,不過(guò)這里的插入操作使用的是自增鎖。那自增鎖是什么級(jí)別的鎖呢?
如果存在自增字段,MySQL會(huì)維護(hù)一個(gè)自增鎖,和自增鎖相關(guān)的一個(gè)參數(shù)為(5.1.22版本之后加入)
innodb_autoinc_lock_mode:可以設(shè)定3個(gè)值,0,1,2

0:traditonal (每次都會(huì)產(chǎn)生表鎖)
1:consecutive (會(huì)產(chǎn)生一個(gè)輕量鎖,simple insert會(huì)獲得批量的鎖,保證連續(xù)插入)
2:interleaved (不會(huì)鎖表,來(lái)一個(gè)處理一個(gè),并發(fā)最高)

Myisam引擎均為traditional,InnoDB默認(rèn)為1,輕量鎖。所以在InnoDB的情況下,這里的insert操作的性能比update操作更高。

優(yōu)化

有了以上結(jié)論以后,那如何優(yōu)化呢?
最簡(jiǎn)單的方案就是減少持有鎖的時(shí)間,處理方式非常簡(jiǎn)單,將更新操作放到最后執(zhí)行,從而縮短更新鎖的持有時(shí)間,避免類(lèi)似的超時(shí)問(wèn)題。

start transaction;
INSERT INTO a (num) values (1);
UPDATE a SET stock = stock - 1 WHERE id = 100 and stock >= 1;
commit;

參考資料:
業(yè)務(wù)優(yōu)化案例一則
mysql并發(fā)insert死鎖問(wèn)題——gap、插入意向鎖沖突
InnoDB并發(fā)插入,居然使用意向鎖?
MySQL自增鎖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容