小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

 好漢勃士 2021-03-30

本文主要討論秒殺場景的解決方案。

什么是秒殺?

從字面意思理解,所謂秒殺,就是在極短時間內(nèi),大量的請求涌入,處理不當(dāng)時容易出現(xiàn)服務(wù)崩潰或數(shù)據(jù)不一致等問題的高并發(fā)場景。

常見的秒殺場景有淘寶雙十一、網(wǎng)約車司機(jī)搶單、12306搶票等等。

高并發(fā)場景下秒殺超賣Bug復(fù)現(xiàn)

在這里準(zhǔn)備了一個商品秒殺的小案例,

1.按照正常的邏輯編寫代碼,請求進(jìn)來先查庫存,庫存大于0時扣減庫存,然后執(zhí)行其他訂單邏輯業(yè)務(wù)代碼;

/** * 商品秒殺 */@Servicepublic class GoodsOrderServiceImpl implements OrderService { @Autowired private GoodsDao goodsDao; @Autowired private OrderDao orderDao; /** * 下單 * * @param goodsId 商品ID * @param userId 用戶ID * @return */ @Override public boolean grab(int goodsId, int userId) { // 查詢庫存 int stock = goodsDao.selectStock(goodsId); try { // 這里睡2秒是為了模擬等并發(fā)都來到這,模擬真實大量請求涌入 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 庫存大于0,扣件庫存,保存訂單 if (stock > 0) { goodsDao.updateStock(goodsId, stock - 1); orderDao.insert(goodsId, userId); return true; } return false; }}
@Service('grabNoLockService')public class GrabNoLockServiceImpl implements GrabService {    @Autowired    OrderService orderService;    /**     * 無鎖的搶購邏輯     *     * @param goodsId     * @param userId     * @return     */    @Override    public String grabOrder(int goodsId, int userId) {        try {            System.out.println('用戶:' + userId + ' 執(zhí)行搶購邏輯');            boolean b = orderService.grab(goodsId, userId);            if (b) {                System.out.println('用戶:' + userId + ' 搶購成功');            } else {                System.out.println('用戶:' + userId + ' 搶購失敗');            }        } finally {        }        return null;    }}

2.庫存設(shè)置為2個;

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

3.使用jmeter開10個線程壓測。

  • 壓測結(jié)果

庫存剩余: 1

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

搶購訂單: 10

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

出問題了!出大問題了!!

本來有兩個庫存,現(xiàn)在還剩一個,而秒殺成功的卻有10個,出現(xiàn)了嚴(yán)重的超賣問題!

問題分析:

問題其實很簡單,當(dāng)秒殺開始,10個請求同時進(jìn)來,同時去查庫存,發(fā)現(xiàn)庫存=2,然后都去扣減庫存,把庫存變?yōu)?,秒殺成功,共賣出商品10件,庫存減1。

那么怎么解決這個問題呢,說起來也挺簡單,加鎖就行了。

單機(jī)模式下的解決方案

加JVM鎖

首先在單機(jī)模式下,服務(wù)只有一個,加JVM鎖就OK,synchronized和Lock都可。

@Service('grabJvmLockService')public class GrabJvmLockServiceImpl implements GrabService { @Autowired OrderService orderService; /** * JVM鎖的搶購邏輯 * * @param goodsId * @param userId * @return */ @Override public String grabOrder(int goodsId, int userId) { String lock = (goodsId + ''); synchronized (lock.intern()) { try { System.out.println('用戶:' + userId + ' 執(zhí)行搶購邏輯'); boolean b = orderService.grab(goodsId, userId); if (b) { System.out.println('用戶:' + userId + ' 搶購成功'); } else { System.out.println('用戶:' + userId + ' 搶購失敗'); } } finally { } } return null; }}

這里以synchronized為例,加鎖之后恢復(fù)庫存重新壓測,結(jié)果:

  • 壓測結(jié)果

庫存剩余: 0

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

搶購訂單: 2

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

大功告成!

JVM鎖在集群模式下還有效果嗎?

單機(jī)模式下的問題解決了,那么在集群模式下,加JVM級別的鎖還有效嗎?

這里起了兩個服務(wù),并且加了一層網(wǎng)關(guān),用來做負(fù)載均衡,重新壓測,

  • 壓測結(jié)果

庫存剩余: 0

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

搶購訂單: 4

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

答案是顯而易見的,鎖無效??!

集群模式下的解決方案

問題分析:

出現(xiàn)這種問題的原因是,JVM級別的鎖在兩個服務(wù)中是不同的兩把鎖,兩個服務(wù)各拿個的,各賣各的,不具有互斥性。

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

那怎么辦呢?也好辦,把鎖獨(dú)立出來就好了,讓兩個服務(wù)去拿同一把鎖,也就是分布式鎖。

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

分布式鎖:

分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。

在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動作。如果不同的系統(tǒng)或是同一個系統(tǒng)的不同主機(jī)之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,這個時候,便需要使用到分布式鎖。

常見的分布式鎖的實現(xiàn)方式有MySQL、Redis、Zookeeper等。

分布式鎖--MySQL:

MySQL實現(xiàn)鎖的方案是:準(zhǔn)備一張表作為鎖,

  • 加鎖時將要搶購的商品ID作為主鍵或者唯一索引插入作為鎖的表中,這樣其他線程來加鎖時就會插入失敗,從而保證互斥性;

  • 解鎖時將這條記錄刪除,其他線程可以繼續(xù)加鎖。

按照上面的方案,編寫的部分代碼:

/** * MySQL寫的分布式鎖 */@Service@Datapublic class MysqlLock implements Lock {    @Autowired    private GoodsLockDao goodsLockDao;    private ThreadLocal<GoodsLock> goodsLockThreadLocal;    @Override    public void lock() {        // 1、嘗試加鎖        if (tryLock()) {            System.out.println('嘗試加鎖');            return;        }        // 2.休眠        try {            Thread.sleep(10);        } catch (InterruptedException e) {            e.printStackTrace();        }        // 3.遞歸再次調(diào)用        lock();    }    /**     * 非阻塞式加鎖,成功,就成功,失敗就失敗。直接返回     */    @Override    public boolean tryLock() {        try {            GoodsLock goodsLock = goodsLockThreadLocal.get();            goodsLockDao.insert(goodsLock);            System.out.println('加鎖對象:' + goodsLockThreadLocal.get());            return true;        } catch (Exception e) {            return false;        }    }    @Override    public void unlock() {        goodsLockDao.delete(goodsLockThreadLocal.get().getGoodsId());        System.out.println('解鎖對象:' + goodsLockThreadLocal.get());        goodsLockThreadLocal.remove();    }    @Override    public void lockInterruptibly() throws InterruptedException {        // TODO Auto-generated method stub    }    @Override    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {        // TODO Auto-generated method stub        return false;    }        @Override    public Condition newCondition() {        // TODO Auto-generated method stub        return null;    }}
  • 搶購邏輯

@Service('grabMysqlLockService')public class GrabMysqlLockServiceImpl implements GrabService { @Autowired private MysqlLock lock; @Autowired OrderService orderService; ThreadLocal<GoodsLock> goodsLock = new ThreadLocal<>(); @Override public String grabOrder(int goodsId, int userId) { // 生成key GoodsLock gl = new GoodsLock(); gl.setGoodsId(goodsId); gl.setUserId(userId); goodsLock.set(gl); lock.setGoodsLockThreadLocal(goodsLock); // lock lock.lock(); // 執(zhí)行業(yè)務(wù) try { System.out.println('用戶:'+userId+' 執(zhí)行搶購邏輯'); boolean b = orderService.grab(goodsId, userId); if(b) { System.out.println('用戶:'+userId+' 搶購成功'); }else { System.out.println('用戶:'+userId+' 搶購失敗'); } } finally { // 釋放鎖 lock.unlock(); } return null; }}

恢復(fù)庫存后繼續(xù)壓測,結(jié)果符合預(yù)期,數(shù)據(jù)一致。

  • 壓測結(jié)果

剩余庫存:0

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

搶購成功:2

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

問題與解決方案:

  1. 由于突然斷網(wǎng)等原因,導(dǎo)致鎖沒有釋放成功怎么辦?

:在作為鎖的表中加開始時間結(jié)束時間兩個字段作為鎖的有效期,由于各種原因?qū)е骆i沒有及時釋放時,可以根據(jù)有效期進(jìn)行判斷鎖是否有效。

  1. 給鎖加了有效期后,若有效期結(jié)束,線程任務(wù)還沒有執(zhí)行完畢怎么辦?

:可以引入watch dog機(jī)制,在任務(wù)未執(zhí)行結(jié)束前,給鎖續(xù)期,這個在后面再詳細(xì)解釋。

分布式鎖--Redis:

在一些中小型項目中可以使用MySQL方案,在大型項目中,給MySQL的配置加上去也可以使用,但用得最多的還是Redis。

Redis加鎖的實現(xiàn)方式是使用setnx命令,格式:setnx key value。

setnx是「set if not exists」的縮寫;若key不存在,則將key的值設(shè)置為value;當(dāng)key存在時,不做任何操作。

  • 加鎖:setnx key value

  • 解鎖:del key

Redis分布式鎖--死鎖問題

產(chǎn)生原因

已經(jīng)加鎖的服務(wù)在執(zhí)行過程中掛掉了,沒有來得及釋放鎖,鎖一直存在在Redis中,導(dǎo)致其他服務(wù)無法加鎖。

解決方案

設(shè)置key的過期時間,讓key自動過期,過期后,key就不存在了,其他服務(wù)就能繼續(xù)加鎖。

  • 要注意的是,添加過期時間時,不能使用這種方式:

setnx key value;expire key time_in_second;

這種方式也可能在第一句setnx成功后掛掉,過期時間沒有設(shè)置,導(dǎo)致死鎖。

  • 有效的方案是通過一行命令加鎖并設(shè)置過期時間,格式如下:

set key value nx ex time_in_second;

這種方式在 Redis 2.6.12 版本開始支持,老版本的Redis可以使用LuaScript。

過期時間引發(fā)的問題

問題一:假設(shè)鎖過期時間設(shè)置為10秒,服務(wù)1加鎖后執(zhí)行10秒還未結(jié)束,此時鎖過期了,服務(wù)2來加鎖也能成功,導(dǎo)致兩個服務(wù)同時拿到鎖。

問題二:服務(wù)1在執(zhí)行了14秒后結(jié)束去釋放鎖,會把服務(wù)2加的鎖釋放掉,此時服務(wù)3又能加鎖成功。

解決方案:

問題二容易解決,在釋放鎖的時候判斷一下是不是自己加的鎖,如果是自己加的鎖,就釋放;如果不是則略過。

問題一解決方案:就是上面說的 Watch Dog(看門狗)機(jī)制

簡單的理解就是另起一個子線程(看門狗),幫主線程看著過期時間,當(dāng)主線程在執(zhí)行業(yè)務(wù)邏輯沒有結(jié)束時,過期時間每過三分之一,子線程(看門狗)就把過期時間續(xù)滿,從而保證主線程沒有結(jié)束,鎖就不會過期。

  • Watch Dog(看門狗)機(jī)制的實現(xiàn)

@Servicepublic class RenewGrabLockServiceImpl implements RenewGrabLockService {    @Autowired    private RedisTemplate<String, String> redisTemplate;    @Override    @Async    public void renewLock(String key, String value, int time) {        System.out.println('續(xù)命'+key+'  '+value);        String v = redisTemplate.opsForValue().get(key);        // 寫成死循環(huán),加判斷        if (StringUtils.isNotBlank(v) && v.equals(value)){            int sleepTime = time / 3;            try {                Thread.sleep(sleepTime * 1000);            } catch (InterruptedException e) {                e.printStackTrace();            }            redisTemplate.expire(key,time,TimeUnit.SECONDS);            renewLock(key,value,time);        }    }

Redis單節(jié)點(diǎn)故障:

如果執(zhí)行過程中Redis掛掉了,所有服務(wù)來加鎖都加不上鎖,這就是單節(jié)點(diǎn)故障問題。

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

解決方案:

使用多臺Redis。

首先來分析一個問題,多臺Redis之間可以做主從嗎?

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

Redis主從問題:

當(dāng)一個線程加鎖成功后,key還沒有被同步過去,Redis Master節(jié)點(diǎn)掛了,此時Slave節(jié)點(diǎn)中沒有key的存在,另一個服務(wù)來加鎖依然可以加鎖成功。

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

所以,不能使用主從方案。

還有一種方案是紅鎖。

紅鎖:

紅鎖方案也是使用多臺Redis,但是多臺Redis之間沒有任何關(guān)系,就是獨(dú)立的Redis。

加鎖時,在一臺Redis上加鎖成功后,馬上去下一臺Redis上加鎖,最終若在過半的Redis上加鎖成功,則加鎖成功,否則加鎖失敗。

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

紅鎖會不會出現(xiàn)超賣問題?

會!。

如果運(yùn)維小哥很勤快,做了自動化,Redis掛掉之后,馬上重啟了一臺,那么重啟的Redis里沒有之前加鎖的key,其他線程依然能夠加鎖成功,這就導(dǎo)致兩個線程同時拿到鎖。

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案
  • 解決方案:延遲重啟掛掉的Redis,延遲一天啟動也沒有問題,重啟太快才會有問題。

終極問題:

到現(xiàn)在為止程序已經(jīng)完美了嗎?

并沒有!

當(dāng)程序在執(zhí)行的時候,鎖也加上了,狗(watch dog)也開始不停地續(xù)期,一切看似很美好,但是Java里還有一個終極問題--STW(Stop The World)。

當(dāng)遇到FullGC時,JVM會發(fā)生STW(Stop The World),此時,世界被按下了暫停鍵,執(zhí)行任務(wù)的主線程暫停了,用來續(xù)期的狗(watch dog)也不會再續(xù)期,Redis中的鎖會慢慢過期,當(dāng)鎖過期之后,其他JVM又可以來成功加鎖,原來的問題又出現(xiàn)了,同時有兩個服務(wù)拿到鎖。

秒殺場景下如何保證數(shù)據(jù)一致性?就這個問題我給出了最詳細(xì)的方案

解決方案:

  • 方案一: 鴕鳥算法

  • 方案二: 終極方案 -- Zookeeper+MySQL樂觀鎖

分布式鎖--Zookeeper+MySQL樂觀鎖

Zookeeper是怎么解決STW問題的呢?

  • 加鎖時,在zookeeper中創(chuàng)建一個臨時順序節(jié)點(diǎn),創(chuàng)建成功后zookeeper會生成一個序號,將這個序號存到MySQL中的verson字段做校驗; 如果鎖未釋放,發(fā)生了STW,緊接著鎖過期,其他服務(wù)去加鎖后,會將MySQL中的version字段變掉;

  • 解鎖時,驗證version字段是否是自己加鎖時的內(nèi)容 如果是,刪除節(jié)點(diǎn),釋放鎖; 如果不是,說明自己已經(jīng)昏睡過了,執(zhí)行失敗。

世界變得清靜了。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多