日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
并發(fā)場景下數(shù)據(jù)寫入功能的實(shí)現(xiàn)

1. 準(zhǔn)備工作

1.1 理論基礎(chǔ)

并發(fā)時(shí)寫數(shù)據(jù),需要考慮要不要上鎖,根本原因是,數(shù)據(jù)存在共享且數(shù)據(jù)會(huì)發(fā)生變化,即多線程會(huì)同時(shí)讀寫同一數(shù)據(jù)。 若數(shù)據(jù)不存在共享,即不同的線程讀寫不同的數(shù)據(jù),不需要上鎖; 若數(shù)據(jù)共享,所有線程對(duì)數(shù)據(jù)只讀不寫,也不需要上鎖 若數(shù)據(jù)共享,有的線程讀數(shù)據(jù),有的線程寫數(shù)據(jù),則需要上鎖。

當(dāng)多個(gè)線程同時(shí)訪問同一數(shù)據(jù),并且至少有一個(gè)線程會(huì)寫這個(gè)數(shù)據(jù),這種情況被稱之為“數(shù)據(jù)競爭”。

并發(fā)場景下,鎖的作用,是并發(fā)改為串行,以保證數(shù)據(jù)的一致性(更具體而言,通過上鎖,解決了并發(fā)執(zhí)行程序時(shí)的原子性、可見性和有序性問題,有興趣的同學(xué)可以近一步深入相關(guān)理論,本文以實(shí)戰(zhàn)為主,不在展開)。

故,在并發(fā)場景下讀寫數(shù)據(jù),首先要分析是否存在“數(shù)據(jù)競爭”問題,若存在,需要“上鎖”。數(shù)據(jù)競爭如果發(fā)生在本地應(yīng)用中,則用本地鎖;如果發(fā)生在分布式服務(wù)間中,則使用分布式鎖;本地鎖和分布式鎖在原理上相同,不用分別討論。

鎖相關(guān)技術(shù),有“悲觀鎖”和“樂觀鎖”兩種。

(1)悲觀鎖

我們通常說的鎖,如無特殊說明,就是指“悲觀鎖”。它是通過一些技術(shù)手段,實(shí)現(xiàn)線程或服務(wù)間的互斥和同步,其使用時(shí),有顯示(或隱式)的鎖持有/釋放操作。 由于上鎖本身要損耗性能,上鎖后并發(fā)處理變成串行,故上鎖是比較影響系統(tǒng)性能的操作;且鎖的應(yīng)用不當(dāng),會(huì)潛在死鎖/活鎖風(fēng)險(xiǎn);故悲觀鎖的使用要慎重。

(2)樂觀鎖

樂觀鎖通常又叫“無鎖技術(shù)”,它不是通過“上鎖”把并發(fā)改串行的方式保證數(shù)據(jù)一致性,而是通過CAS(Compare And Swap)方式來實(shí)現(xiàn),由于CAS通常很快,該過程也不用“上鎖”,性能損耗少。不過,通過CAS并發(fā)寫數(shù)據(jù)時(shí),通常伴有“自旋”,即當(dāng)出現(xiàn)多個(gè)寫并發(fā)時(shí),只有一個(gè)能寫入成功,其他要自旋后再次寫入,直至寫入成功或因超時(shí)/超過重試次數(shù)失敗。 自旋會(huì)帶來性能開銷,頻繁自旋的性能開銷會(huì)超過上鎖。故樂觀鎖通常用在并發(fā)不太激烈的場景中,且在該場景下性能比悲觀鎖要好,而在高并發(fā)場景下,建議使用悲觀鎖。

1.2 業(yè)務(wù)場景及分析

本文主題是介紹并發(fā)寫數(shù)據(jù)的幾種方案,為此,我們先確立幾個(gè)常用的業(yè)務(wù)場景,并做簡單分析。

寫數(shù)據(jù),我們討論最常見的把數(shù)據(jù)寫入數(shù)據(jù)庫的場景,主要包括新建數(shù)據(jù)和修改數(shù)據(jù)兩種具體場景,兩個(gè)場景不完全一致,分別討論。

(1) 往數(shù)據(jù)庫寫新數(shù)據(jù)

往數(shù)據(jù)庫里寫信數(shù)據(jù)時(shí),如果數(shù)據(jù)直接相互獨(dú)立,即不存在“數(shù)據(jù)競爭”,則按照1.1節(jié)的理論,此時(shí)不需要考慮鎖的問題。

對(duì)于存在“數(shù)據(jù)競爭”的場景,我們考慮寫入流水碼的場景:假設(shè)創(chuàng)建數(shù)據(jù)有個(gè)編碼字段,形如“CON_0001”,其后半段的“0001”是流水碼,需要根據(jù)當(dāng)前最大的流水碼+1 來計(jì)算待創(chuàng)建數(shù)據(jù)的流水碼。這里存在數(shù)據(jù)范圍的競爭,并發(fā)創(chuàng)建數(shù)據(jù)時(shí),如果不做并發(fā)控制,會(huì)創(chuàng)建多個(gè)編碼相同的數(shù)據(jù)。

(2)更新數(shù)據(jù)庫記錄

并發(fā)更新數(shù)據(jù)庫記錄時(shí),如果可以確保各并發(fā)請(qǐng)求要更新的數(shù)據(jù)各不相同,則不存在“數(shù)據(jù)競爭”,不需要上鎖;而并發(fā)更新同一數(shù)據(jù)記錄時(shí),如果不做并發(fā)控制,可能出現(xiàn)一個(gè)寫請(qǐng)求覆蓋另一個(gè)寫請(qǐng)求的情況,導(dǎo)致最終結(jié)果錯(cuò)誤。

這里我們考慮“訪問量+1”的場景,即設(shè)計(jì)一個(gè)訪問量表,每次請(qǐng)求給訪問量+1,如當(dāng)前訪問量為5,若5個(gè)并發(fā)請(qǐng)求同時(shí)為該記錄+1,正確的結(jié)果為10,但如果不加并發(fā)控制,結(jié)果通常會(huì)<10。

2. 并發(fā)插入流水碼的實(shí)現(xiàn)方案

2.1 業(yè)務(wù)邏輯分析

業(yè)務(wù)邏輯如下:

  1. 取出當(dāng)前數(shù)據(jù)庫中流水碼最大的一條記錄
  2. 從編碼字段中解析出當(dāng)前最大流水碼
  3. 流水碼+1,創(chuàng)建新紀(jì)錄入庫

代碼實(shí)現(xiàn)即:

private Integer addEntity() {
ConcurrentEntity entity = dataMapper.getLatestConcurrentEntity();
int nextNumber = entity == null ? 1 : getNextNumberByCode(entity.getCode());
String code = String.format("CON_%04d", nextNumber);
return dataMapper.insertConcurrentEntity(new ConcurrentEntity(code));
}

private int getNextNumberByCode(String code) {
int index = code.lastIndexOf("_");
String number = code.substring(index + 1);
return Integer.parseInt(number) + 1;
}

該業(yè)務(wù)在并發(fā)場景下,主要存在原子性隱患,即 addEntity() 中的代碼,需要作為一個(gè)整體全部執(zhí)行完,若多個(gè)線程交替執(zhí)行執(zhí)行逐行代碼,某個(gè)線程讀取到最新流水碼后,該碼被其他線程改了,本線程不可知,導(dǎo)致寫入錯(cuò)誤數(shù)據(jù)。

故本業(yè)務(wù)場景的并發(fā)中,主要避免原子性和可見性問題,最直接的方式,是通過上鎖解決。

2.2 實(shí)現(xiàn)方案

2.2.1 方案1:在代碼中上鎖

private final Lock lock = new ReentrantLock(false);


public Integer addEntityByLock() {
synchronized (this) {
return addEntity();
}
}

public Integer addEntityByLock2() {
lock.lock();
try {
return addEntity();
} finally {
lock.unlock();
}
}

在分布式系統(tǒng)中,lock可以用redisson或curator提供的分布式鎖進(jìn)行實(shí)例化。

2.2.2 方案2: 在數(shù)據(jù)庫中上鎖

鎖除了可以在代碼中用,也可以直接用到數(shù)據(jù)庫上, select ... for update 語句即可在數(shù)據(jù)庫中上寫鎖,另由于業(yè)務(wù)執(zhí)行的原子性問題,需要把 addEntity() 中的邏輯放到同一個(gè)事務(wù)中去。

@Transactional(isolation = Isolation.REPEATABLE_READ)
public Integer addEntityByTransactionWithLock() {
return addEntity();
}

這里的事務(wù)隔離級(jí)別,選擇RC或RR均可。

注,這個(gè)實(shí)現(xiàn)中,addEntity()中對(duì) ConcurrentEntity 的查詢,改成了加鎖讀的方式:

@Select("SELECT * FROM concurrent_entity\n" +
"ORDER BY code DESC\n" +
"LIMIT 1\n" +
"FOR UPDATE;")
ConcurrentEntity getLatestConcurrentEntityWithWriteLock();

2.2.3 性能對(duì)比

對(duì)于1000個(gè)并發(fā)請(qǐng)求,三種方法性能對(duì)比如下:

executeConcurrentAddByLock1: 1000 并發(fā),花費(fèi)時(shí)間 1179 ms
executeConcurrentAddByLock2: 1000 并發(fā),花費(fèi)時(shí)間 863 ms
executeConcurrentAddByTransactionWithLock: 1000 并發(fā),花費(fèi)時(shí)間 1284 ms

2.3 其他方案

本業(yè)務(wù)場景中,由于流水碼的計(jì)算,存在數(shù)據(jù)競爭問題,所以并發(fā)時(shí)需要上鎖,如果能避免數(shù)據(jù)競爭,就可以避免并發(fā)問題。針對(duì)本案例,可以把流水碼獲取的邏輯放到redis中去,redis本身是單線程的,避免了流水碼的數(shù)據(jù)競爭,進(jìn)而避免了上鎖的開銷,而redis本身又是高性能的,故這個(gè)方案理論上比上述方案的性能只高不低。

3. 并發(fā)更新訪問量的實(shí)現(xiàn)方案

3.1 業(yè)務(wù)分析

并發(fā)更新數(shù)據(jù)庫中的訪問量時(shí),存在的“數(shù)據(jù)競爭”問題,也是“原子性”隱患。如果更新本身是一個(gè)原子操作,則不存在并發(fā)問題;如果更新操作分兩步,先讀取當(dāng)前數(shù)據(jù),然后+1后重新寫入,則該操作不是原子的,需要上鎖。

3.2 實(shí)現(xiàn)方案

3.2.1 方案1: 原子更新

public Integer increaseVisitCountAtomically(int id) {
return dataMapper.increaseConcurrentVisitAtomic(id);
}

@Update("UPDATE concurrent_visit\n" +
"SET visit = visit + 1, update_time = NOW()\n" +
"WHERE id = #{id};")
Integer increaseConcurrentVisitAtomic(int id);

3.2.2 方案2: 代碼中上鎖

public Integer increaseVisitCountByLock(int id) {
synchronized (this) {
return increaseVisitCount(id);
}
}

private Integer increaseVisitCount(int id) {
ConcurrentVisit concurrentVisit = dataMapper.getConcurrentVisitObject(id);
concurrentVisit.increaseVisit().updateUpdateTime();
return dataMapper.updateConcurrentVisit(concurrentVisit);
}

3.2.3 方案3: 數(shù)據(jù)庫中上鎖

@Transactional()
public Integer increaseVisitCountByTransaction(int id) {
return increaseVisitCount(id, true);
}

private Integer increaseVisitCount(int id, boolean withLock) {
ConcurrentVisit concurrentVisit = withLock ? dataMapper.getConcurrentVisitObjectWithLock(id)
: dataMapper.getConcurrentVisitObject(id);
return dataMapper.increaseConcurrentVisit(concurrentVisit.increaseVisit().updateUpdateTime());
}

@Select("SELECT * FROM concurrent_visit\n" +
"WHERE id = #{id}\n" +
"FOR UPDATE;")
ConcurrentVisit getConcurrentVisitObjectWithLock(int id);

3.2.4 方案4: 使用樂觀鎖

使用樂觀鎖時(shí),需要一個(gè)遞增的版本字段( version ),每次update 成功時(shí),version都要 +1,version要作為更新前的compare字段,若當(dāng)前讀到的version與數(shù)據(jù)庫中的version不一致,則更新失敗。

public Integer increaseVisitCountOptimistically(int id) {
ConcurrentVisit concurrentVisit = dataMapper.getConcurrentVisitObject(id);
return dataMapper.increaseConcurrentVisitOptimistically(concurrentVisit.increaseVisit().updateUpdateTime());
}

@Update("UPDATE concurrent_visit\n" +
"SET visit = #{visit}, update_time = #{updateTime}, version = #{version} + 1\n" +
"WHERE id = #{id} and version = #{version};")
Integer increaseConcurrentVisitOptimistically(ConcurrentVisit concurrentVisit);

使用樂觀鎖時(shí),compare不一致,會(huì)更新失敗,這時(shí)需要自旋重試,故上述代碼可以優(yōu)化為:

public Integer increaseVisitCountOptimisticallyWithRetry(int id) {
int result = 0;
int maxRetry = 5;
long interval = 20L;

for (int i = 0; i < maxRetry; i++) {
result = increaseVisitCountOptimistically(id);
if (result > 0) {
break;
}
interval = interval + i * 50;
helper.sleep(interval);

}
return result;
}

3.2.5 性能比較

發(fā)起10000個(gè)并發(fā)更新操作,結(jié)果如下:

executeConcurrentAddAtomically: 10000 并發(fā),花費(fèi)時(shí)間 2112 ms
executeConcurrentAddByLock: 10000 并發(fā),花費(fèi)時(shí)間 5796 ms
executeConcurrentAddByTransaction: 10000 并發(fā),花費(fèi)時(shí)間 3902 ms
executeConcurrentAddOptimisticallyWithRetry: 10000 并發(fā),花費(fèi)時(shí)間 5998 ms

mysql> select * from concurrent_visit;
+----+-------------+-------+---------+---------------------+---------------------+
| id | resourceKey | visit | version | create_time | update_time |
+----+-------------+-------+---------+---------------------+---------------------+
| 1 | resource1 | 39925 | 9925 | 2022-03-31 11:42:54 | 2022-04-01 12:06:36 |
+----+-------------+-------+---------+---------------------+---------------------+

可以看到:

  1. 并發(fā)比較激烈時(shí),樂觀鎖性能最差,而且有些請(qǐng)求,即使超過了最大重試次數(shù),也沒更新成功
  2. 把鎖上在數(shù)據(jù)庫中,性能比所在代碼上還要好,原因是,數(shù)據(jù)庫上的鎖是經(jīng)過充分的性能優(yōu)化的,而且鎖的顆粒度更小,而我們這個(gè)業(yè)務(wù)場景下,代碼中鎖的顆粒度已經(jīng)很難再縮小了。鎖的顆粒度,決定了并發(fā)程度,并發(fā)場景下,鎖的顆粒度越小越好

本文標(biāo)題:并發(fā)場景下數(shù)據(jù)寫入功能的實(shí)現(xiàn)
本文鏈接:http://www.5511xx.com/article/cohdicc.html