分布式鎖的三種實現方式


分布式鎖三種實現方式:1、基於數據庫實現分布式鎖;2、基於緩存(Redis等)實現分布式鎖;3、基於Zookeeper實現分布式鎖。從性能角度(從高到低)來看:“緩存方式>Zookeeper方式>=數據庫方式”。

分布式鎖三種實現方式:

1. 基於數據庫實現分布式鎖;
2. 基於緩存(Redis等)實現分布式鎖;
3. 基於Zookeeper實現分布式鎖;

一、基於數據庫實現分布式鎖

1. 悲觀鎖

利用select … where … for update 排他鎖

注意: 其他附加功能與實現一基本一致,這里需要注意的是“where name=lock ”,name字段必須要走索引,否則會鎖表。有些情況下,比如表不大,mysql優化器會不走這個索引,導致鎖表問題。

2. 樂觀鎖

所謂樂觀鎖與前邊最大區別在於基於CAS思想,是不具有互斥性,不會產生鎖等待而消耗資源,操作過程中認為不存在並發沖突,只有update version失敗后才能覺察到。我們的搶購、秒殺就是用了這種實現以防止超賣。
通過增加遞增的版本號字段實現樂觀

 

 

二、基於緩存(Redis等)實現分布式鎖

1. 使用命令介紹:
(1)SETNX
SETNX key val:當且僅當key不存在時,set一個key為val的字符串,返回1;若key存在,則什么都不做,返回0。
(2)expire
expire key timeout:為key設置一個超時時間,單位為second,超過這個時間鎖會自動釋放,避免死鎖。
(3)delete
delete key:刪除key

在使用Redis實現分布式鎖的時候,主要就會使用到這三個命令。

2. 實現思想:
(1)獲取鎖的時候,使用setnx加鎖,並使用expire命令為鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值為一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。
(2)獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
(3)釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放。

3. 分布式鎖的簡單實現代碼:

/**

   * 分布式鎖的簡單實現代碼    */

  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 / );

  

             // 獲取鎖的超時時間,超過這個時間則放棄獲取鎖

             long end = System.currentTimeMillis() + acquireTimeout;

             while (System.currentTimeMillis() < end) {

                 if (conn.setnx(lockKey, identifier) == ) {

                     conn.expire(lockKey, lockExpire);

                     // 返回value值,用於釋放鎖時間確認

                     retIdentifier = identifier;

                     return retIdentifier;

                 }

                 // 返回-代表key沒有設置超時時間,為key設置一個超時時間

                 if (conn.ttl(lockKey) == -) {

                     conn.expire(lockKey, lockExpire);

                 }

  

                 try {

                     Thread.sleep();

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

    }

}
View Code

4. 測試剛才實現的分布式鎖

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

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

public class Service {

 

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

    }

}

模擬線程進行秒殺服務;























public class ThreadA extends Thread {

    private Service service;

 

    public ThreadA(Service service) {

        this.service = service;

    }

 

    @Override

    public void run() {

        service.seckill();

    }

}

 

public class Test {

    public static void main(String[] args) {

        Service service = new Service();

        for (int i = 0; i < 50; i++) {

            ThreadA threadA = new ThreadA(service);

            threadA.start();

        }

    }

}
View Code

結果如下,結果為有序的:

若注釋掉使用鎖的部分:

public void seckill() {

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

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

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

    System.out.println(--n);

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

}
View Code

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

三、基於Zookeeper實現分布式鎖

ZooKeeper是一個為分布式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。基於ZooKeeper實現分布式鎖的步驟如下:

(1)創建一個目錄mylock;
(2)線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
(3)獲取mylock目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
(4)線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
(5)線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。

這里推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分布式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。

實現源碼如下:

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang.StringUtils;

import org.apache.curator.framework.CuratorFramework;

import org.apache.curator.framework.CuratorFrameworkFactory;

import org.apache.curator.retry.RetryNTimes;

import org.apache.zookeeper.CreateMode;

import org.apache.zookeeper.data.Stat;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.stereotype.Component;

 

/**

 * 分布式鎖Zookeeper實現

 *

 */

@Slf4j

@Component

public class ZkLock implements DistributionLock {

private String zkAddress = "zk_adress";

    private static final String root = "package root";

    private CuratorFramework zkClient;

 

    private final String LOCK_PREFIX = "/lock_";

 

    @Bean

    public DistributionLock initZkLock() {

        if (StringUtils.isBlank(root)) {

            throw new RuntimeException("zookeeper 'root' can't be null");

        }

        zkClient = CuratorFrameworkFactory

                .builder()

                .connectString(zkAddress)

                .retryPolicy(new RetryNTimes(2000, 20000))

                .namespace(root)

                .build();

        zkClient.start();

        return this;

    }

 

    public boolean tryLock(String lockName) {

        lockName = LOCK_PREFIX+lockName;

        boolean locked = true;

        try {

            Stat stat = zkClient.checkExists().forPath(lockName);

            if (stat == null) {

                log.info("tryLock:{}", lockName);

                stat = zkClient.checkExists().forPath(lockName);

                if (stat == null) {

                    zkClient

                            .create()

                            .creatingParentsIfNeeded()

                            .withMode(CreateMode.EPHEMERAL)

                            .forPath(lockName, "1".getBytes());

                } else {

                    log.warn("double-check stat.version:{}", stat.getAversion());

                    locked = false;

                }

            } else {

                log.warn("check stat.version:{}", stat.getAversion());

                locked = false;

            }

        } catch (Exception e) {

            locked = false;

        }

        return locked;

    }

 

    public boolean tryLock(String key, long timeout) {

        return false;

    }

 

    public void release(String lockName) {

        lockName = LOCK_PREFIX+lockName;

        try {

            zkClient

                    .delete()

                    .guaranteed()

                    .deletingChildrenIfNeeded()

                    .forPath(lockName);

            log.info("release:{}", lockName);

        } catch (Exception e) {

            log.error("刪除", e);

        }

    }

 

    public void setZkAddress(String zkAddress) {

        this.zkAddress = zkAddress;

    }

}
View Code

優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。

缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。

四、對比

數據庫分布式鎖實現
缺點:

1.db操作性能較差,並且有鎖表的風險
2.非阻塞操作失敗后,需要輪詢,占用cpu資源;
3.長時間不commit或者長時間輪詢,可能會占用較多連接資源

Redis(緩存)分布式鎖實現
缺點:

1.鎖刪除失敗 過期時間不好控制
2.非阻塞,操作失敗后,需要輪詢,占用cpu資源;

ZK分布式鎖實現
缺點:性能不如redis實現,主要原因是寫操作(獲取鎖釋放鎖)都需要在Leader上執行,然后同步到follower。

總之:ZooKeeper有較好的性能和可靠性。

從理解的難易程度角度(從低到高)數據庫 > 緩存 > Zookeeper

從實現的復雜性角度(從低到高)Zookeeper >= 緩存 > 數據庫

從性能角度(從高到低)緩存 > Zookeeper >= 數據庫

從可靠性角度(從高到低)Zookeeper > 緩存 > 數據庫

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM