<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>

          【329期】如何利用redis分布式鎖,解決秒殺場景下的訂單超賣問題

          共 17934字,需瀏覽 36分鐘

           ·

          2021-09-14 21:21

          1. 秒殺場景

          Controller層:

          @RestController
          @RequestMapping("/skill")
          @Slf4j
          public class SecKillController {
              @Autowired
              private SecKillService secKillService;
           
              //查詢秒殺活動(dòng)特價(jià)商品的信息
              @GetMapping("/query/{productId}")
              public String query(@PathVariable String productId)throws Exception {
                  return secKillService.querySecKillProductInfo(productId);
              }

              //秒殺
              @GetMapping("/order/{productId}")
              public String skill(@PathVariable String productId)throws Exception {
                  log.info("@skill request, productId:" + productId);
                  secKillService.orderProductMockDiffUser(productId);
                  return secKillService.querySecKillProductInfo(productId);
              }
          }

          Service層:

          @Service
          public class SecKillServiceImpl implements SecKillService {
              
              private static final int TIMEOUT = 10 * 1000//超時(shí)時(shí)間 10s
              
              @Autowired
              private RedisLock redisLock;

             // 雅詩蘭黛特價(jià)小棕瓶,限量100000份
              static Map<String,Integer> products;
              static Map<String,Integer> stock;
              static Map<String,String> orders;
              static {
                  //模擬多個(gè)表,商品信息表,庫存表,秒殺成功訂單表
                  products = new HashMap<>();
                  stock = new HashMap<>();
                  orders = new HashMap<>();
                  //商品Id---商品庫存
                  products.put("123456"100000);
                  //商品id---商品庫存
                  stock.put("123456"100000);
              }

              private String queryMap(String productId) {
                  return "雅詩蘭黛小棕瓶特價(jià),限量份"
                          + products.get(productId)
                          +" 還剩:" + stock.get(productId)+" 份"
                          +" 該商品成功下單用戶數(shù)目:"
                          +  orders.size() +" 人" ;
              }

              @Override
              public String querySecKillProductInfo(String productId) {
                  return this.queryMap(productId);
              }

              //秒殺的邏輯:可以在該方法生加上Synchronized解決超賣
              @Override
              public void  orderProductMockDiffUser(String productId) {
                  //1.查詢該商品庫存,為0則活動(dòng)結(jié)束。
                  int stockNum = stock.get(productId);
                  if(stockNum == 0) {
                      throw new SellException(100,"活動(dòng)結(jié)束");
                  }else {
                      //2.下單(模擬不同用戶openid不同)
                      orders.put(KeyUtil.genUniqueKey(),productId);
                      //3.減庫存
                      stockNum =stockNum-1;
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      //4.更新庫存
                      stock.put(productId,stockNum);
                  }
              }
          }

          使用壓測工具測試結(jié)果:雅詩蘭黛小棕瓶特價(jià),限量份100000 還剩:99938份 該商品成功下單用戶數(shù)目:646人

          很明顯訂單超賣了,如何解決?可以在秒殺方法上加上Synchronized,但是只適合單點(diǎn)服務(wù)器,且性能低

          2. Redis分布式鎖解決訂單超賣

          2.1 兩個(gè)命令介紹

          業(yè)務(wù)場景:

          天貓雙11熱賣過程中,對已經(jīng)售罄的貨物追加補(bǔ)貨,且補(bǔ)貨完成。客戶購買熱情高漲, 3秒內(nèi)將所有商品購買完畢。本次補(bǔ)貨已經(jīng)將庫存全部清空,如何避免最后一件商品不被多人同時(shí)購買?【超賣問題】就是說如果只剩一件商品,但是有5個(gè)人要買,如何保證不被超賣???

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

          使用watch監(jiān)控一個(gè)key有沒有改變已經(jīng)不能解決問題,此處要監(jiān)控的是具體數(shù)據(jù) ,我們要監(jiān)控的是商品的數(shù)量什么時(shí)候到1,這個(gè)商品的數(shù)量是一直變化的,不可能別每次變化,都放棄執(zhí)行。

          解決方案:

          使用 setnx 設(shè)置一個(gè)公共鎖:setnx lock-key value,操作完畢通過del操作釋放鎖

          利用setnx命令的返回值特征,有值則返回設(shè)置失敗,無值則返回設(shè)置成功

          對于返回設(shè)置成功的,擁有控制權(quán)進(jìn)行下一步的具體業(yè)務(wù)操作,對于返回設(shè)置失敗的,不具有控制權(quán),排隊(duì)或等待

          127.0.0.1:6379> set num 10
          OK
          127.0.0.1:6379> setnx lock-num 1 -- 加鎖
          (integer) 1
          127.0.0.1:6379> incrby num -1 
          (integer) 9
          127.0.0.1:6379> del lock-num  -- 釋放鎖
          (integer) 1

          127.0.0.1:6379> setnx lock-num 1  --  當(dāng)前客戶端加鎖
          (integer) 1 

          127.0.0.1:6379> setnx lock-num 1  -- 其他客戶端獲取不到鎖
          (integer) 0

          死鎖: 如果加了鎖,但是沒有釋放,就會(huì)導(dǎo)致死鎖,其他客戶端一直獲取不到鎖。

          使用expire為鎖key添加時(shí)間限定,到時(shí)間不釋放,放棄鎖

          由于操作通常都是微秒或毫秒級,因此該鎖定時(shí)間不宜設(shè)置過大。具體時(shí)間需要業(yè)務(wù)測試后確認(rèn)。推薦:Java進(jìn)階視頻資源

          • expire lock-key second
          • pexpire lock-key milliseconds
          127.0.0.1:6379> set name 123
          OK
          127.0.0.1:6379> setnx lock-name 1 -- 鎖的名稱key
          (integer) 1
          127.0.0.1:6379> expire lock-name 20 -- 使用expire為鎖key添加時(shí)間限定
          (integer) 1
          127.0.0.1:6379> get name
          "123"

          GETSET key value

          將給定 key 的值設(shè)為 value ,并返回 key 的舊值(old value)。

          redis> GETSET db mongodb    # 沒有舊值,返回 nil
          (nil)

          redis> GET db
          "mongodb"

          redis> GETSET db redis      # 返回舊值 mongodb
          "mongodb"

          redis> GET db
          "redis"

          2.2 RedisLock

          @Component
          @Slf4j
          public class RedisLock {

              @Autowired
              private StringRedisTemplate redisTemplate;

              /**
               * 加鎖
               * @param key:productId
               * @param value 當(dāng)前時(shí)間+超時(shí)時(shí)間
               */

              public boolean lock(String key, String value) {
                  //setnx----對應(yīng)方法 setIfAbsent(key, value),如果可以加鎖返回true,不可以加鎖返回false
                  if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
                      return true;
                  }
           
                  //下面這段代碼時(shí)為了解決可能出現(xiàn)的死鎖情況
                  String currentValue = redisTemplate.opsForValue().get(key);
                  //如果鎖過期
                  if (!StringUtils.isEmpty(currentValue)
                          && Long.parseLong(currentValue) < System.currentTimeMillis()) {
                      //獲取上一個(gè)鎖的時(shí)間:重新設(shè)置鎖的過期時(shí)間value,并返回上一個(gè)過期時(shí)間
                      String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
                      //currentValue =2020-12-28,兩個(gè)線程的value=2020-12-29,只會(huì)有一個(gè)線程拿到鎖
                      if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                          return true;
                      }
                  }
                  return false;
              }

              //解鎖
              public void unlock(String key, String value) {
                  try {
                      String currentValue = redisTemplate.opsForValue().get(key);
                      if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
                          redisTemplate.opsForValue().getOperations().delete(key);
                      }
                  }catch (Exception e) {
                      log.error("【redis分布式鎖】解鎖異常, {}", e);
                  }
              }
          }

          2.3 將redis分布式鎖應(yīng)用于秒殺業(yè)務(wù)

          @Override
          public void orderProductMockDiffUser(String productId) {
              //加鎖
              //鎖的過期時(shí)間為當(dāng)前時(shí)間+過期時(shí)長
              long time = System.currentTimeMillis()+TIMEOUT;
              if(!redisLock.lock(productId,String.valueOf(time))){
                  throw new SellException(101,"人太多,稍后再來");
              }
              //1.查詢該商品庫存,為0則活動(dòng)結(jié)束。
              int stockNum = stock.get(productId);
              if(stockNum == 0) {
                  throw new SellException(100,"活動(dòng)結(jié)束");
              }else {
                  //2.下單(模擬不同用戶openid不同)
                  orders.put(KeyUtil.genUniqueKey(),productId);
                  //3.減庫存
                  stockNum =stockNum-1;
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  //4.更新庫存
                  stock.put(productId,stockNum);
              }
              //解鎖
              redisLock.unlock(productId,String.valueOf(time));
          }

          2.4 分析RedisLock

          重點(diǎn)分析加鎖邏輯,有兩個(gè)邏輯需要考慮:

          public boolean lock(String key, String value) {

              if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
                  return true;
              }
              //下面的代碼是為了解決可能出現(xiàn)的死鎖的情況????
              String currentValue = redisTemplate.opsForValue().get(key);
              if (!StringUtils.isEmpty(currentValue)
                  && Long.parseLong(currentValue) < System.currentTimeMillis()) {
                  //下面這個(gè)邏輯又怎么理解????
                  String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
                  if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                      return true;
                  }
              }
              return false;
          }

          為了解決可能出現(xiàn)的死鎖的情況????

          //秒殺業(yè)務(wù)方法
          @Override
          public void orderProductMockDiffUser(String productId) {
              //加鎖
              //鎖的過期時(shí)間為當(dāng)前時(shí)間+過期時(shí)長
              long time = System.currentTimeMillis()+TIMEOUT;
              if(!redisLock.lock(productId,String.valueOf(time))){
                  throw new SellException(101,"人太多,稍后再來");
              }
              //1.查詢該商品庫存,為0則活動(dòng)結(jié)束。
              int stockNum = stock.get(productId);
              if(stockNum == 0) {
                  throw new SellException(100,"活動(dòng)結(jié)束");
              }else {
                  //2.下單
                  orders.put(KeyUtil.genUniqueKey(),productId);
                  //3.減庫存
                  stockNum =stockNum-1;
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  //4.更新庫存
                  stock.put(productId,stockNum);
              }
              //解鎖
              redisLock.unlock(productId,String.valueOf(time));
          }

          假如我們將中間那段邏輯去掉會(huì)出現(xiàn)聲明情況???

          public boolean lock(String key, String value) {
              if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
                  return true;
              }
              return false;
          }

          ① 線程A執(zhí)行秒殺的業(yè)務(wù)邏輯方法,并對這個(gè)方法加了鎖,key=proeuctId,value=加鎖時(shí)間+過期時(shí)長,然后開始執(zhí)行下單----》減庫存-----》更新庫存等操作,如果在執(zhí)行的過程中,這段代碼發(fā)生了異常,那么線程A是不會(huì)釋放鎖的,導(dǎo)致其他線程都無法獲取到鎖導(dǎo)致死鎖的產(chǎn)生,所以下面的邏輯是很有必要加的,即如果當(dāng)前時(shí)間晚于鎖的過期時(shí)間,那么就會(huì)向下走if()條件:

          //下面的代碼是為了解決可能出現(xiàn)的死鎖的情況????
          String currentValue = redisTemplate.opsForValue().get(key);
          if (!StringUtils.isEmpty(currentValue)
              && Long.parseLong(currentValue) < System.currentTimeMillis()) {
                  //下面這個(gè)邏輯又怎么理解????
                  String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
                  if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                  return true;
              }
          }

          ② 可是下面的if()條件怎么理解?

          currentValue=2020-12-18

          假如現(xiàn)在兩個(gè)線程A和B同時(shí)執(zhí)行l(wèi)ock()方法,也就是這兩個(gè)線程的value是完全相同的,都為value=2020-12-19,而他們都執(zhí)行 String oldValue = redisTemplate.opsForValue().getAndSet(key, value);,會(huì)有一個(gè)先執(zhí)行一個(gè)后執(zhí)行:

          假如線程A先執(zhí)行,返回的oldValue=2020-12-18,同時(shí)設(shè)置value = 2020-12-19,由于oldvalue=currentValue返回true,即A線程加了鎖;

          此時(shí)B線程繼續(xù)執(zhí)行 ,返回的oldValue=2020-12-19,oldvalue!=currentValue,返回false,加鎖失敗

          所以這段代碼的邏輯是只會(huì)讓一個(gè)線程加鎖

          public boolean lock(String key, String value) {
              if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
                  return true;
              }
              String currentValue = redisTemplate.opsForValue().get(key);
              if (!StringUtils.isEmpty(currentValue)
                  && Long.parseLong(currentValue) < System.currentTimeMillis()) {
                  //下面這個(gè)邏輯又怎么理解????
                  String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
                  if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                      return true;
                  }
              }
              return false;
          }

          感謝閱讀,希望對你有所幫助 :) 

          來源:hengheng.blog.csdn.net/article/details/107827649

          最近給大家找了  通用權(quán)限系統(tǒng)


          資源,怎么領(lǐng)取?


          掃二維碼,加我微信,回復(fù):通用權(quán)限系統(tǒng)

           注意,不要亂回復(fù) 

          沒錯(cuò),不是機(jī)器人
          記得一定要等待,等待才有好東西
          瀏覽 55
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  豆花成人免费进入18 | 淫秽视频免费看 | 五月丁香综合在线 | 国产乱妇交换做爰XXXⅩ麻豆 | 玖玖视频免费在线观看 |