掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
作者:老鄭 2021-04-12 08:02:12
開發(fā)
前端
分布式 對于商品秒殺的場景,我們需要防止庫存超賣或者重復(fù)扣款等并發(fā)問題,我們通常需要使用分布式鎖,來解決共享資源競爭導(dǎo)致數(shù)據(jù)不一致的問題。本篇就講解如何用分布式鎖的來解決此類問題。

成都創(chuàng)新互聯(lián)是專業(yè)的豐滿網(wǎng)站建設(shè)公司,豐滿接單;提供網(wǎng)站設(shè)計、成都網(wǎng)站制作,網(wǎng)頁設(shè)計,網(wǎng)站設(shè)計,建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進行豐滿網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團隊,希望更多企業(yè)前來合作!
對于商品秒殺的場景,我們需要防止庫存超賣或者重復(fù)扣款等并發(fā)問題,我們通常需要使用分布式鎖,來解決共享資源競爭導(dǎo)致數(shù)據(jù)不一致的問題。
以手機秒殺的場景為例子,在搶購的過程中通常我們有三個步驟:
扣掉對應(yīng)商品的庫存;2. 創(chuàng)建商品的訂單;3. 用戶支付。
對于這樣的場景我們就可以采用分布式鎖的來解決,比如我們在用戶進入秒殺 “下單“ 鏈接的過程中,我們可以對商品庫存進行加鎖,然后完成扣庫存和其他操作,操作完成后。釋放鎖,讓下一個用戶繼續(xù)進入保證庫存的安全性;也可以減少因為秒殺失敗,導(dǎo)致 DB 回滾的次數(shù)。整個流程如下圖所示:
注:對于鎖的粒度要根據(jù)具體的場景和需求來權(quán)衡。
對于 Zookeeper 的分布式鎖實現(xiàn),主要是利用 Zookeeper 的兩個特征來實現(xiàn):
對于非公平鎖,我們在加鎖的過程如下圖所示。
其實上面的實現(xiàn)有優(yōu)點也有缺點:
優(yōu)點:
實現(xiàn)比較簡單,有通知機制,能提供較快的響應(yīng),有點類似 ReentrantLock 的思想,對于節(jié)點刪除失敗的場景由 Session 超時保證節(jié)點能夠刪除掉。
缺點:
重量級,同時在大量鎖的情況下會有 “驚群” 的問題。
“驚群” 就是在一個節(jié)點刪除的時候,大量對這個節(jié)點的刪除動作有訂閱 Watcher 的線程會進行回調(diào),這對Zk集群是十分不利的。所以需要避免這種現(xiàn)象的發(fā)生。
為了解決“驚群“問題,我們需要放棄訂閱一個節(jié)點的策略,那么怎么做呢?
基于非公平鎖的缺點,我們可以通過一下的方案來規(guī)避。
優(yōu)點: 如上借助于臨時順序節(jié)點,可以避免同時多個節(jié)點的并發(fā)競爭鎖,緩解了服務(wù)端壓力。
缺點: 對于讀寫場景來說,無法解決一致性的問題,如果讀的時候也去獲取鎖的話,這樣會導(dǎo)致性能下降,對于這樣的問題,我們可以通過讀寫鎖來實現(xiàn)如類似 jdk 中的 ReadWriteLock
對于讀寫鎖的特點:讀寫鎖在如果多個線程都是在讀的時候,是可以并發(fā)讀的,就是一個無鎖的狀態(tài),如果有寫鎖正在操作的時候,那么讀鎖需要等待寫鎖。在加寫鎖的時候,由于前面的讀鎖都是并發(fā),所以需要監(jiān)聽最后一個讀鎖完成后執(zhí)行寫鎖。步驟如下:
本文源碼中使用環(huán)境:JDK 1.8 、Zookeeper 3.6.x
org.apache.curator curator-framework 2.13.0 org.apache.curator curator-recipes 2.13.0
由于 Zookeeper 非公平鎖的 “驚群” 效應(yīng),非公平鎖在 Zookeeper 中其實并不是最好的選擇。下面是一個模擬秒殺的例子來使用 Zookeeper 分布式鎖。
- public class MutexTest {
- static ExecutorService executor = Executors.newFixedThreadPool(8);
- static AtomicInteger stock = new AtomicInteger(3);
- public static void main(String[] args) throws InterruptedException {
- CuratorFramework client = getZkClient();
- String key = "/lock/lockId_111/111";
- final InterProcessMutex mutex = new InterProcessMutex(client, key);
- for (int i = 0; i < 99; i++) {
- executor.submit(() -> {
- if (stock.get() < 0) {
- System.err.println("庫存不足, 直接返回");
- return;
- }
- try {
- boolean acquire = mutex.acquire(200, TimeUnit.MILLISECONDS);
- if (acquire) {
- int s = stock.decrementAndGet();
- if (s < 0) {
- System.err.println("進入秒殺,庫存不足");
- } else {
- System.out.println("購買成功, 剩余庫存: " + s);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (mutex.isAcquiredInThisProcess())
- mutex.release();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
- while (true) {
- if (executor.isTerminated()) {
- executor.shutdown();
- System.out.println("秒殺完畢剩余庫存為:" + stock.get());
- }
- TimeUnit.MILLISECONDS.sleep(100);
- }
- }
- private static CuratorFramework getZkClient() {
- String zkServerAddress = "127.0.0.1:2181";
- ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
- CuratorFramework zkClient = CuratorFrameworkFactory.builder()
- .connectString(zkServerAddress)
- .sessionTimeoutMs(5000)
- .connectionTimeoutMs(5000)
- .retryPolicy(retryPolicy)
- .build();
- zkClient.start();
- return zkClient;
- }
- }
讀寫鎖可以用來保證緩存雙寫的強一致性的,因為讀寫鎖在多線程讀的時候是無鎖的, 只有在前面有寫鎖的時候才會等待寫鎖完成后訪問數(shù)據(jù)。
- public class ReadWriteLockTest {
- static ExecutorService executor = Executors.newFixedThreadPool(8);
- static AtomicInteger stock = new AtomicInteger(3);
- static InterProcessMutex readLock;
- static InterProcessMutex writeLock;
- public static void main(String[] args) throws InterruptedException {
- CuratorFramework client = getZkClient();
- String key = "/lock/lockId_111/1111";
- InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, key);
- readLock = readWriteLock.readLock();
- writeLock = readWriteLock.writeLock();
- for (int i = 0; i < 16; i++) {
- executor.submit(() -> {
- try {
- boolean read = readLock.acquire(2000, TimeUnit.MILLISECONDS);
- if (read) {
- int num = stock.get();
- System.out.println("讀取庫存,當(dāng)前庫存為: " + num);
- if (num < 0) {
- System.err.println("庫存不足, 直接返回");
- return;
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- if (readLock.isAcquiredInThisProcess()) {
- try {
- readLock.release();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- try {
- boolean acquire = writeLock.acquire(2000, TimeUnit.MILLISECONDS);
- if (acquire) {
- int s = stock.get();
- if (s <= 0) {
- System.err.println("進入秒殺,庫存不足");
- } else {
- s = stock.decrementAndGet();
- System.out.println("購買成功, 剩余庫存: " + s);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (writeLock.isAcquiredInThisProcess())
- writeLock.release();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
- while (true) {
- if (executor.isTerminated()) {
- executor.shutdown();
- System.out.println("秒殺完畢剩余庫存為:" + stock.get());
- }
- TimeUnit.MILLISECONDS.sleep(100);
- }
- }
- private static CuratorFramework getZkClient() {
- String zkServerAddress = "127.0.0.1:2181";
- ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
- CuratorFramework zkClient = CuratorFrameworkFactory.builder()
- .connectString(zkServerAddress)
- .sessionTimeoutMs(5000)
- .connectionTimeoutMs(5000)
- .retryPolicy(retryPolicy)
- .build();
- zkClient.start();
- return zkClient;
- }
- }
打印結(jié)果如下,一開始會有 8 個輸出結(jié)果為 讀取庫存,當(dāng)前庫存為: 3 然后在寫鎖中回去順序的扣減少庫存。
- 讀取庫存,當(dāng)前庫存為: 3
- 讀取庫存,當(dāng)前庫存為: 3
- 讀取庫存,當(dāng)前庫存為: 3
- 讀取庫存,當(dāng)前庫存為: 3
- 讀取庫存,當(dāng)前庫存為: 3
- 讀取庫存,當(dāng)前庫存為: 3
- 讀取庫存,當(dāng)前庫存為: 3
- 讀取庫存,當(dāng)前庫存為: 3
- 購買成功, 剩余庫存: 2
- 購買成功, 剩余庫存: 1
- 購買成功, 剩余庫存: 0
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 讀取庫存,當(dāng)前庫存為: 0
- 讀取庫存,當(dāng)前庫存為: 0
- 讀取庫存,當(dāng)前庫存為: 0
- 讀取庫存,當(dāng)前庫存為: 0
- 讀取庫存,當(dāng)前庫存為: 0
- 讀取庫存,當(dāng)前庫存為: 0
- 讀取庫存,當(dāng)前庫存為: 0
- 讀取庫存,當(dāng)前庫存為: 0
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
- 進入秒殺,庫存不足
咱們最常用的就是 Redis 的分布式鎖和 Zookeeper 的分布式鎖,在性能方面 Redis 的每秒鐘 TPS 可以上輕松上萬。在大規(guī)模的高并發(fā)場景我推薦使用 Redis 分布式鎖來作為推薦的技術(shù)方案。如果對并發(fā)要求不是特別高的場景可以使用 Zookeeper 分布式來處理。
https://www.cnblogs.com/leeego-123/p/12162220.html
http://curator.apache.org/
https://blog.csdn.net/hosaos/article/details/89521537

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流