天天看點

分布式鎖-超賣問題案例分析

 我們先來看一個業務場景:

分布式鎖-超賣問題案例分析

系統A是一個電商系統,目前是一台機器部署,系統中有一個使用者下訂單的接口,但是使用者下訂單之前一定要去檢查一下庫存,確定庫存足夠了才會給使用者下單。

由于系統有一定的并發,是以會預先将商品的庫存儲存在redis中,使用者下單的時候會更新redis的庫存。

假如某個時刻,redis裡面的某個商品庫存為1,此時兩個請求同時到來,其中一個請求執行到上圖的第3步,更新資料庫的庫存為0,但是第4步還沒有執行。

而另外一個請求執行到了第2步,發現庫存還是1,就繼續執行第3步。

這樣的結果,是導緻賣出了2個商品,然而其實庫存隻有1個。

很明顯不對啊!這就是典型的庫存超賣問題

此時,我們很容易想到解決方案:用鎖把2、3、4步鎖住,讓他們執行完之後,另一個線程才能進來執行第2步。

分布式鎖-超賣問題案例分析

按照上面的圖,在執行第2步時,使用Java提供的synchronized或者ReentrantLock來鎖住,然後在第4步執行完之後才釋放鎖。

這樣一來,2、3、4 這3個步驟就被“鎖”住了,多個線程之間隻能串行化執行。

但是好景不長,整個系統的并發飙升,一台機器扛不住了。現在要增加一台機器,如下圖

分布式鎖-超賣問題案例分析

增加機器之後,系統變成上圖所示,我的天!

假設此時兩個使用者的請求同時到來,但是落在了不同的機器上,那麼這兩個請求是可以同時執行了,還是會出現庫存超賣的問題。

為什麼呢?因為上圖中的兩個A系統,運作在兩個不同的JVM裡面,他們加的鎖隻對屬于自己JVM裡面的線程有效,對于其他JVM的線程是無效的。

是以,這裡的問題是:Java提供的原生鎖機制在多機部署場景下失效了

這是因為兩台機器加的鎖不是同一個鎖(兩個鎖在不同的JVM裡面)。

那麼,我們隻要保證兩台機器加的鎖是同一個鎖,問題不就解決了嗎?

此時,就該分布式鎖隆重登場了,分布式鎖的思路是:

在整個系統提供一個全局、唯一的擷取鎖的“東西”,然後每個系統在需要加鎖時,都去問這個“東西”拿到一把鎖,這樣不同的系統拿到的就可以認為是同一把鎖。 至于這個“東西”,可以是Redis、Zookeeper,也可以是資料庫。

分布式鎖-超賣問題案例分析

通過上面的分析,我們知道了庫存超賣場景在分布式部署系統的情況下使用Java原生的鎖機制無法保證線程安全,是以我們需要用到分布式鎖的方案。

那麼,如何實作分布式鎖呢?

2.分布式鎖的實作

2.1.分布式鎖的簡單實作代碼

package com.bruceliu.lock;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;

/**
 * @BelongsProject: RedisLock
 * @BelongsPackage: com.bruceliu.lock
 * @Author: bruceliu
 * @QQ:1241488705
 * @CreateTime: 2020-05-06 17:51
 * @Description: 分布式鎖簡單實作
 */
public class DistributedLock {

    private final JedisPool jedisPool;

    public DistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /**
     * 加鎖
     * @param lockName       鎖的key
     * @param acquireTimeout 擷取逾時時間
     * @param timeout        鎖的逾時時間
     * @return 鎖辨別
     */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 擷取連接配接
            conn = jedisPool.getResource();
            // 随機生成一個value
            String identifier = UUID.randomUUID().toString();
            // 鎖名,即key值
            String lockKey = "lock:" + lockName;

            // 逾時時間,上鎖後超過此時間則自動釋放鎖
            int lockExpire = (int) (timeout / 1000);

            // 擷取鎖的逾時時間,超過這個時間則放棄擷取鎖
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    // 傳回value值,用于釋放鎖時間确認
                    retIdentifier = identifier;
                    return retIdentifier;
                }
                // 傳回-1代表key沒有設定逾時時間,為key設定一個逾時時間
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }


    /**
     * 釋放鎖
     * @param lockName   鎖的key
     * @param identifier 釋放鎖的辨別
     * @return
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                // 監視lock,準備開始事務
                conn.watch(lockKey);
                // 通過前面傳回的value值判斷是不是該鎖,若是該鎖,則删除,釋放鎖
                if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}
           

2.2.測試剛才實作的分布式鎖

例子中使用50個線程模拟秒殺一個商品,使用–運算符來實作商品減少,從結果有序性就可以看出是否為加鎖狀态。

模拟秒殺服務,在其中配置了jedis線程池,在初始化的時候傳給分布式鎖,供其使用。

package com.bruceliu.lock;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @BelongsProject: RedisLock
 * @BelongsPackage: com.bruceliu.lock
 * @Author: bruceliu
 * @QQ:1241488705
 * @CreateTime: 2020-05-07 09:23
 * @Description: TODO
 */
public class SkillService {

    private static JedisPool pool = null;
    private DistributedLock lock = new DistributedLock(pool);

    int n = 500;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        // 設定最大連接配接數
        config.setMaxTotal(200);
        // 設定最大空閑數
        config.setMaxIdle(8);
        // 設定最大等待時間
        config.setMaxWaitMillis(1000 * 100);
        // 在borrow一個jedis執行個體時,是否需要驗證,若為true,則所有jedis執行個體均是可用的
        config.setTestOnBorrow(true);
        pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
    }

    public void seckill() {
        // 傳回鎖的value值,供釋放鎖時候進行判斷
        String identifier = lock.lockWithTimeout("resource", 5000, 1000);
        System.out.println(Thread.currentThread().getName() + "獲得了鎖");
        System.out.println(--n);
        lock.releaseLock("resource", identifier);
    }
}

           

 2.3.模拟線程進行秒殺服務

package com.bruceliu.lock;

/**
 * @BelongsProject: RedisLock
 * @BelongsPackage: com.bruceliu.lock
 * @Author: bruceliu
 * @QQ:1241488705
 * @CreateTime: 2020-05-07 09:24
 * @Description: TODO
 */
public class TestLock {

    public static void main(String[] args) {
        SkillService service = new SkillService();
        for (int i = 0; i < 50; i++) {
            ThreadA threadA = new ThreadA(service);
            threadA.setName("ThreadNameA->"+i);
            threadA.start();
        }
    }
}

class ThreadA extends Thread {

    private SkillService skillService;

    public ThreadA(SkillService skillService) {
        this.skillService = skillService;
    }

    @Override
    public void run() {
        skillService.seckill();
    }
}

           

2.4.測試結果 

分布式鎖-超賣問題案例分析

若注釋掉使用鎖的部分:

public void seckill() {

    // 傳回鎖的value值,供釋放鎖時候進行判斷

    //String identifier = lock.lockWithTimeout("resource", 5000, 1000);

    System.out.println(Thread.currentThread().getName() + "獲得了鎖");

    System.out.println(--n);

    //lock.releaseLock("resource", identifier);

}

從結果可以看出,有一些是異步進行的:

分布式鎖-超賣問題案例分析