<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          秒殺場(chǎng)景下如何保證數(shù)據(jù)一致性

          共 8648字,需瀏覽 18分鐘

           ·

          2021-11-30 12:21

          本文主要討論秒殺場(chǎng)景的解決方案。

          什么是秒殺?

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

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

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

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

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

          /**
          * 商品秒殺
          */

          @Service
          public 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) {
          // 查詢庫(kù)存
          int stock = goodsDao.selectStock(goodsId);
          try {
          // 這里睡2秒是為了模擬等并發(fā)都來到這,模擬真實(shí)大量請(qǐng)求涌入
          Thread.sleep(2000);
          } catch (InterruptedException e) {
          e.printStackTrace();
          }

          // 庫(kù)存大于0,扣件庫(kù)存,保存訂單
          if (stock > 0) {
          goodsDao.updateStock(goodsId, stock - 1);
          orderDao.insert(goodsId, userId);
          return true;
          }
          return false;
          }
          }
          復(fù)制代碼
          @Service("grabNoLockService")
          public class GrabNoLockServiceImpl implements GrabService {

          @Autowired
          OrderService orderService;

          /**
          * 無鎖的搶購(gòu)邏輯
          *
          * @param goodsId
          * @param userId
          * @return
          */

          @Override
          public String grabOrder(int goodsId, int userId) {
          try {
          System.out.println("用戶:" + userId + " 執(zhí)行搶購(gòu)邏輯");
          boolean b = orderService.grab(goodsId, userId);
          if (b) {
          System.out.println("用戶:" + userId + " 搶購(gòu)成功");
          } else {
          System.out.println("用戶:" + userId + " 搶購(gòu)失敗");
          }
          } finally {

          }
          return null;
          }
          }
          復(fù)制代碼
          1. 庫(kù)存設(shè)置為2個(gè);

          1. 使用jmeter開10個(gè)線程壓測(cè)。

          • 壓測(cè)結(jié)果

            • 搶購(gòu)訂單:10

            • 庫(kù)存剩余:1

          出問題了!出大問題了?。?/p>

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

          問題分析

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

          那么怎么解決這個(gè)問題呢,說去來也挺簡(jiǎn)單,加鎖就行了。

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

          加JVM鎖

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

          @Service("grabJvmLockService")
          public class GrabJvmLockServiceImpl implements GrabService {

          @Autowired
          OrderService orderService;

          /**
          * JVM鎖的搶購(gòu)邏輯
          *
          * @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í)行搶購(gòu)邏輯");
          boolean b = orderService.grab(goodsId, userId);
          if (b) {
          System.out.println("用戶:" + userId + " 搶購(gòu)成功");
          } else {
          System.out.println("用戶:" + userId + " 搶購(gòu)失敗");
          }
          } finally {

          }
          }
          return null;
          }
          }
          復(fù)制代碼

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

          • 壓測(cè)結(jié)果

            • 搶購(gòu)訂單:2

            • 庫(kù)存剩余:0

          大功告成!

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

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

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

          • 壓測(cè)結(jié)果

            • 搶購(gòu)訂單:4

            • 庫(kù)存剩余:0

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

          集群模式下的解決方案

          問題分析

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

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

          分布式鎖

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

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

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

          分布式鎖--MySQL

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

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

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

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

          /**
          * MySQL寫的分布式鎖
          */

          @Service
          @Data
          public class MysqlLock implements Lock {

          @Autowired
          private GoodsLockDao goodsLockDao;

          private ThreadLocal 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("加鎖對(duì)象:" + goodsLockThreadLocal.get());
          return true;
          } catch (Exception e) {
          return false;
          }
          }

          @Override
          public void unlock() {
          goodsLockDao.delete(goodsLockThreadLocal.get().getGoodsId());
          System.out.println("解鎖對(duì)象:" + 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;
          }
          }
          復(fù)制代碼
          • 搶購(gòu)邏輯

          @Service("grabMysqlLockService")
          public class GrabMysqlLockServiceImpl implements GrabService {

          @Autowired
          private MysqlLock lock;

          @Autowired
          OrderService orderService;

          ThreadLocal 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í)行搶購(gòu)邏輯");

          boolean b = orderService.grab(goodsId, userId);
          if(b) {
          System.out.println("用戶:"+userId+" 搶購(gòu)成功");
          }else {
          System.out.println("用戶:"+userId+" 搶購(gòu)失敗");
          }
          } finally {
          // 釋放鎖
          lock.unlock();
          }
          return null;
          }
          }
          復(fù)制代碼

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

          • 壓測(cè)結(jié)果

            • 搶購(gòu)成功:2

            • 剩余庫(kù)存:0

          問題與解決方案

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

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

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

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

          分布式鎖--Redis

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

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

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

          • 加鎖:setnx key value

          • 解鎖:del key

          Redis分布式鎖--死鎖問題

          產(chǎn)生原因

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

          解決方案

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

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

          setnx key value;
          expire key time_in_second;
          復(fù)制代碼

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

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

          set key value nx ex time_in_second;
          復(fù)制代碼

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

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

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

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

          解決方案

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

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

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

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

          @Service
          public class RenewGrabLockServiceImpl implements RenewGrabLockService {

          @Autowired
          private RedisTemplate 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);
          }
          }
          復(fù)制代碼

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

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

          解決方案

          使用多臺(tái)Redis。

          首先來分析一個(gè)問題,多臺(tái)Redis之間可以做主從嗎?

          Redis主從問題

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

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

          還有一種方案是紅鎖。

          紅鎖

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

          加鎖時(shí),在一臺(tái)Redis上加鎖成功后,馬上去下一臺(tái)Redis上加鎖,最終若在過半的Redis上加鎖成功,則加鎖成功,否則加鎖失敗。

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

          會(huì)!

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

          • 解決方案:延遲重啟掛掉的Redis,延遲一天啟動(dòng)也沒有問題,重啟太快才會(huì)有問題。

          終極問題

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

          并沒有!

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

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

          解決方案
          • 方案一:鴕鳥算法

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

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

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

          • 加鎖時(shí),在zookeeper中創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),創(chuàng)建成功后zookeeper會(huì)生成一個(gè)序號(hào),將這個(gè)序號(hào)存到MySQL中的verson字段做校驗(yàn);

            • 如果鎖未釋放,發(fā)生了STW,緊接著鎖過期,其他服務(wù)去加鎖后,會(huì)將MySQL中的version字段變掉;

          • 解鎖時(shí),驗(yàn)證version字段是否是自己加鎖時(shí)的內(nèi)容

            • 如果是,刪除節(jié)點(diǎn),釋放鎖;

            • 如果不是,說明自己已經(jīng)昏睡過了,執(zhí)行失敗。

          世界變得清靜了。

          相關(guān)代碼

          • gitee: distributed-lock


          作者:三生oo
          鏈接:https://juejin.cn/post/6944967816562884644
          來源:稀土掘金
          著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。



          瀏覽 91
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久精品视频三级 | 久久久影视四色777 | 影音先锋AV啪啪资源 | 黄色影院在线免费观看 | 成人做爰黄A片免费视频网站野外 |