1. 超賣問題(多人秒殺)
1.1 原因
- 多線程并行運(yùn)行
- 多行代碼操作共享資源,但不具備原子性
例:
1.2 解決方案
針對(duì)并發(fā)安全問題,最廣為人知的解決方案就是加鎖。
從實(shí)現(xiàn)思想上來說,鎖可以分為兩大類:
- 悲觀鎖
- 樂觀鎖
悲觀鎖是一種獨(dú)占和排他的鎖機(jī)制,保守地認(rèn)為數(shù)據(jù)會(huì)被其他事務(wù)修改,所以在整個(gè)數(shù)據(jù)處理過程中將數(shù)據(jù)處于鎖定狀態(tài)。
樂觀鎖是一種較為樂觀的并發(fā)控制方法,假設(shè)多用戶并發(fā)的不會(huì)產(chǎn)生安全問題,因此無需獨(dú)占和鎖定資源。但在更新數(shù)據(jù)前,會(huì)先檢查是否有其他線程修改了該數(shù)據(jù),如果有,則認(rèn)為可能有風(fēng)險(xiǎn),會(huì)放棄修改操作。
悲觀鎖、樂觀鎖是對(duì)并發(fā)安全問題的處理態(tài)度不同:
- 悲觀鎖認(rèn)為安全問題一定會(huì)發(fā)生,所以直接獨(dú)占資源。結(jié)果就是多個(gè)線程會(huì)串行執(zhí)行被保護(hù)的代碼。
- 優(yōu)點(diǎn):安全性非常高
- 缺點(diǎn):性能較差
- 樂觀鎖則認(rèn)為安全問題不一定發(fā)生,所以不獨(dú)占資源。結(jié)果就是允許多線程并行執(zhí)行。但如果真的發(fā)生并發(fā)修改怎么辦??樂觀鎖采用CAS(Compare And Set)思想,在更新數(shù)據(jù)前先判斷數(shù)據(jù)與我之前查詢到的是否一致,不一致則證明有其它線程也在更新。為了避免出現(xiàn)安全問題,放棄本次更新或者重新嘗試一次。
樂觀鎖:
- 優(yōu)點(diǎn):性能好、安全性也好
- 缺點(diǎn):并發(fā)較高時(shí),可能出現(xiàn)更新成功率較低的問題(并行的N個(gè)線程只會(huì)有1個(gè)成功)
1.3 總結(jié)
超賣這樣的線程安全問題,解決方案有哪些?
- 悲觀鎖:添加同步鎖,讓線程串行執(zhí)行
- 優(yōu)點(diǎn):簡(jiǎn)單粗暴
- 缺點(diǎn):性能一般
- 樂觀鎖:不加鎖,在更新時(shí)判斷是否有其它線程在修改
- 優(yōu)點(diǎn):性能好
- 缺點(diǎn):存在成功率低的問題
2. 鎖失效問題(單人重復(fù)搶)
2.1 原因
使用Synchronized的代碼塊作為鎖時(shí),鎖使用的是方法中的變量時(shí),此時(shí)一般會(huì)通過tostring()方法將變量轉(zhuǎn)為常量,但由于tostring的底層方法中使用的是:
這種new出來的對(duì)象并不相同,從而導(dǎo)致鎖不相同,從而引發(fā)鎖失效的問題。
2.2 解決方案
String類中提供了intern()方法:
從描述中可以看出,只要兩個(gè)字符串equals的結(jié)果為true,那么intern就能保證得到的結(jié)果用 ==判斷也是true,其原理就是獲取字符串字面值對(duì)應(yīng)到常量池中的字符串常量。因此只要兩個(gè)字符串一樣,intern()返回的一定是同一個(gè)對(duì)象。
3. 事務(wù)邊界問題(單人重復(fù)搶)
3.1 原因
由于鎖過早釋放,導(dǎo)致了事務(wù)尚未提交,并發(fā)的線程判斷出現(xiàn)錯(cuò)誤,最終導(dǎo)致并發(fā)安全問題發(fā)生。
這其實(shí)就是事務(wù)邊界和鎖邊界的問題。
3.2 解決方案
解決方案很簡(jiǎn)單,就是調(diào)整邊界:
- 業(yè)務(wù)開始前,先獲取鎖,再開啟事務(wù)
- 業(yè)務(wù)結(jié)束后:先提交事務(wù),再釋放鎖
將加鎖的部分抽取為一個(gè)方法,在方法上加@Transactional事務(wù)注解,在調(diào)用的方法中將抽取的方法放在鎖內(nèi)即可解決
3.3 總結(jié)
在事務(wù)和鎖并行存在時(shí),一定要考慮事務(wù)和鎖的邊界問題。由于事務(wù)的隔離級(jí)別問題,可能會(huì)導(dǎo)致不同事務(wù)之間數(shù)據(jù)不可見,往往會(huì)產(chǎn)生一些不可預(yù)期的現(xiàn)象。
4. 事務(wù)失效問題
4.1 原因
雖然事務(wù)邊界問題已經(jīng)解決,但是其解決方案也埋下了新的隱患。
常見的事務(wù)失效原因:
4.1.1 事務(wù)方法非public修飾
由于Spring的事務(wù)是基于AOP的方式結(jié)合動(dòng)態(tài)代理來實(shí)現(xiàn)的。因此事務(wù)方法一定要是public的,這樣才能便于被Spring做事務(wù)的代理和增強(qiáng)。
而且,在Spring內(nèi)部也會(huì)有一個(gè) org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource類,去檢查事務(wù)方法的修飾符:
@Nullable
protected TransactionAttribute computeTransactionAttribute(
Method method, @Nullable Class<?> targetClass) {
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() &&
!Modifier.isPublic(method.getModifiers())) {
return null;
}
// ... 略
return null;
}
所以,事務(wù)方法一定要被public修飾!
4.1.2 非事務(wù)方法調(diào)用事務(wù)方法
@Service
public class OrderService {
public void createOrder(){
// ... 準(zhǔn)備訂單數(shù)據(jù)
// 生成訂單并扣減庫(kù)存
insertOrderAndReduceStock();
}
@Transactional
public void insertOrderAndReduceStock(){
// 生成訂單
insertOrder();
// 扣減庫(kù)存
reduceStock();
}
}
可以看到,insertOrderAndReduceStock方法是一個(gè)事務(wù)方法,肯定會(huì)被Spring事務(wù)管理。Spring會(huì)給OrderService類生成一個(gè)動(dòng)態(tài)代理對(duì)象,對(duì)insertOrderAndReduceStock方法做增加,實(shí)現(xiàn)事務(wù)效果。
但是現(xiàn)在createOrder方法是一個(gè)非事務(wù)方法,在其中調(diào)用了insertOrderAndReduceStock方法,這個(gè)調(diào)用其實(shí)隱含了一個(gè)this.的前綴。也就是說,這里相當(dāng)于是直接調(diào)用原始的OrderService中的普通方法,而非被Spring代理對(duì)象的代理方法。那事務(wù)肯定就失效了!
4.1.3 事務(wù)方法的異常被捕獲了
示例:
@Service
public class OrderService {
@Transactional
public void createOrder(){
// ... 準(zhǔn)備訂單數(shù)據(jù)
// 生成訂單
insertOrder();
// 扣減庫(kù)存
reduceStock();
}
private void reduceStock() {
try {
// ...扣庫(kù)存
} catch (Exception e) {
// 處理異常
}
}
}
在這段代碼中,reduceStock方法內(nèi)部直接捕獲了Exception類型的異常,也就是說方法執(zhí)行過程中即便出現(xiàn)了異常也不會(huì)向外拋出。
而Spring的事務(wù)管理就是要感知業(yè)務(wù)方法的異常,當(dāng)捕獲到異常后才會(huì)回滾事務(wù)。
現(xiàn)在事務(wù)被捕獲,就會(huì)導(dǎo)致Spring無法感知事務(wù)異常,自然不會(huì)回滾,事務(wù)就失效了。
4.1.4 事務(wù)異常類型不對(duì)
@Service
public class OrderService {
@Transactional(rollbackFor = RuntimeException.class)
public void createOrder() throws IOException {
// ... 準(zhǔn)備訂單數(shù)據(jù)
// 生成訂單
insertOrder();
// 扣減庫(kù)存
reduceStock();
throw new IOException();
}
}
Spring的事務(wù)管理默認(rèn)感知的異常類型是RuntimeException,當(dāng)事務(wù)方法內(nèi)部拋出了一個(gè)IOException時(shí),不會(huì)被Spring捕獲,因此就不會(huì)觸發(fā)事務(wù)回滾,事務(wù)就失效了。
因此,當(dāng)我們的業(yè)務(wù)中會(huì)拋出RuntimeException以外的異常時(shí),應(yīng)該通過@Transactional注解中的rollbackFor屬性來指定異常類型:
@Transactional(rollbackFor = Exception.class)
4.1.5 事務(wù)傳播行為不對(duì)
@Service
public class OrderService {
@Transactional
public void createOrder(){
// 生成訂單
insertOrder();
// 扣減庫(kù)存
reduceStock();
throw new RuntimeException("業(yè)務(wù)異常");
}
@Transactional
public void insertOrder() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reduceStock() {
}
}
在示例代碼中,事務(wù)的入口是createOrder()方法,會(huì)開啟一個(gè)事務(wù),可以成為外部事務(wù)。在createOrder()方法內(nèi)部又調(diào)用了insertOrder()方法和reduceStock()方法。這兩個(gè)都是事務(wù)方法。
不過,reduceStock()方法的事務(wù)傳播行為是REQUIRES_NEW,這會(huì)導(dǎo)致在進(jìn)入reduceStock()方法時(shí)會(huì)創(chuàng)建一個(gè)新的事務(wù),可以成為子事務(wù)。insertOrder()則是默認(rèn),因此會(huì)與createOrder()合并事務(wù)。
因此,當(dāng)createOrder方法最后拋出異常時(shí),只會(huì)導(dǎo)致insertOrder方法回滾,而不會(huì)導(dǎo)致reduceStock方法回滾,因?yàn)閞educeStock是一個(gè)獨(dú)立事務(wù)。
所以,一定要慎用傳播行為,注意外部事務(wù)與內(nèi)部事務(wù)之間的關(guān)系。文章來源:http://www.zghlxwxcb.cn/news/detail-613585.html
4.1.6 沒有被Spring管理
// @Service
public class OrderService {
@Transactional
public void createOrder(){
// 生成訂單
insertOrder();
// 扣減庫(kù)存
reduceStock();
throw new RuntimeException("業(yè)務(wù)異常");
}
@Transactional
public void insertOrder() {
}
@Transactional
public void reduceStock() {
}
}
這個(gè)示例屬于比較低級(jí)的錯(cuò)誤,OrderService類沒有添加@Service注解,因此就沒有被Spring管理。你在方法上添加的@Transactional注解根本不會(huì)有人幫你動(dòng)態(tài)代理,事務(wù)自然失效。文章來源地址http://www.zghlxwxcb.cn/news/detail-613585.html
到了這里,關(guān)于超賣等高并發(fā)秒殺場(chǎng)景的問題及解決方案的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!