分佈鎖-mysql,redis,zk

一、爲什麼要用到分佈鎖

1、多環境中才需要
2、任務都需要對同一共享資源時空行寫操作
3、對資源訪問互斥
鎖競爭4個步驟:
1、競爭鎖
2、佔有鎖
3、任務阻賽
4、釋放鎖

二、分佈式鎖幾種方案及比較

在這裏插入圖片描述

在實現zk鎖前,先簡單說明一下模版方法模式
在父類中編排主流程,將步驟實現延遲到子類去實現。
比如下圖:
在這裏插入圖片描述
在模版類中定義清點商品,計算價目,支付,送貨上門主流程方法,並實現了清點商品,計算價目,送貨上門方法,但在子中實現不同支付方法。

public abstract class AbstractTemplate {
    public void goumai(){
        shangPing();
        jiShuan();
        pay();
        fahuo();
    }
    public void shangPing(){
        System.out.println("清點商品");
    }
    public void jiShuan(){
        System.out.println("計算");
    }
    public void fahuo(){
        System.out.println("發貨");
    }
    public abstract void pay();
}
public class WeiPayTemplate extends AbstractTemplate {
    @Override
    public void pay() {
        System.out.println("weipay");
    }

    public static void main(String[] args){
        WeiPayTemplate w = new WeiPayTemplate();
        w.goumai();
    }
}

那麼我們實現鎖也是這個過成程,在抽象類中定義獲取鎖,競爭鎖,等待鎖,釋放鎖
定義一lock類,Mysql redis zk子類都都繼承這個抽象類具體在子類實現各自的方法。

1 、 mysql鎖

新建一張表
method_Lock

CREATE TABLE `method_Lock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
  `desc` varchar(1024) NOT NULL DEFAULT '備註信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE,   
   current_timestamp COMMENT '保存數據時間,自動生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

當我們想要鎖住某個方法時,執行以下SQL:

insert into method_Lock (method_name,desc) values (‘method_name’,‘desc’)

如果新增成功那麼,獲取鎖成功,失敗那麼獲取鎖失敗。
當方法執行完畢之後,想要釋放鎖的話,需要執行以下Sql:

delete from method_Lock where method_name ='method_name'

模版方法:

public abstract class AbstractLock implements Lock {
    public void getLock(){
        //1 、競爭鎖
        if(tryLock()){
            System.out.print("##get lock鎖的資源###");
        }else{
            //2、任務阻塞
            waitLock();
            //重新獲取鎖
            getLock();
        }
    }
    public abstract void waitLock();

    public abstract void unLock();

    public abstract boolean tryLock();
}

mysql鎖實現類

@Service
public class MysqlLock extends AbstractLock {
    private static final int LOCK_ID = 1;
    @Autowired
    private LockMapper lockMapper;

    @Override
    //非阻塞式加鎖
    public boolean tryLock() {
        try {
            lockMapper.insert(LOCK_ID);
        }catch (Exception e){
            return false;
        }
        return true;
    }
    //讓當前線程休眠一段時間
    @Override
    public void waitLock() {
        try{
            Thread.currentThread().sleep(10);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    //釋放鎖
    @Override
    public void unLock() {
        lockMapper.delete(LOCK_ID);
    }

}

調用

public class MysqlTest extends AbstratApplicationBaseBootTest {
    private static int i = 1;
    @Resource
    private MysqlLock mysqlLock;
    public void getNum(){
        try{
            mysqlLock.getLock();
            Date d = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println("當前時間:" + sdf.format(d));
            String date = sdf.format(d);
            System.out.println(Thread.currentThread().getName()+"生成訂單號"+date+"  "+i);
            i = i+1;
        }finally {
            mysqlLock.unLock();
        }
    }

    @Test
    public void test() throws InterruptedException {
        System.out.println("生成唯一訂單號");
        for(int i =1;i<=20;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    getNum();
                }
            }).start();
        }

        Thread.sleep(10000);

    }
}

結果:生成20個不同的訂單號

##get lock鎖的資源###當前時間:2019-10-13 01:51:42
Thread-28生成訂單號2019-10-13 01:51:42  1
[2019-10-13 01:51:42,921] [Thread-15] [INFO ] org.springframework.beans.factory.xml.XmlBeanDefinitionReader 316 -- Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
[2019-10-13 01:51:42,975] [Thread-15] [INFO ] org.springframework.jdbc.support.SQLErrorCodesFactory 128 -- SQLErrorCodes loaded: [DB2, Derby, H2, HDB, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
##get lock鎖的資源###當前時間:2019-10-13 01:51:43
Thread-22生成訂單號2019-10-13 01:51:43  2
##get lock鎖的資源###當前時間:2019-10-13 01:51:43
Thread-18生成訂單號2019-10-13 01:51:43  3
##get lock鎖的資源###當前時間:2019-10-13 01:51:43
Thread-24生成訂單號2019-10-13 01:51:43  4
##get lock鎖的資源###當前時間:2019-10-13 01:51:43
Thread-23生成訂單號2019-10-13 01:51:43  5
##get lock鎖的資源###當前時間:2019-10-13 01:51:43
Thread-16生成訂單號2019-10-13 01:51:43  6
##get lock鎖的資源###當前時間:2019-10-13 01:51:44
Thread-27生成訂單號2019-10-13 01:51:44  7
##get lock鎖的資源###當前時間:2019-10-13 01:51:44
Thread-30生成訂單號2019-10-13 01:51:44  8
##get lock鎖的資源###當前時間:2019-10-13 01:51:44
Thread-31生成訂單號2019-10-13 01:51:44  9
##get lock鎖的資源###當前時間:2019-10-13 01:51:44
Thread-33生成訂單號2019-10-13 01:51:44  10
##get lock鎖的資源###當前時間:2019-10-13 01:51:44
Thread-15生成訂單號2019-10-13 01:51:44  11
##get lock鎖的資源###當前時間:2019-10-13 01:51:44
Thread-19生成訂單號2019-10-13 01:51:44  12
##get lock鎖的資源###當前時間:2019-10-13 01:51:45
Thread-20生成訂單號2019-10-13 01:51:45  13
##get lock鎖的資源###當前時間:2019-10-13 01:51:45
Thread-25生成訂單號2019-10-13 01:51:45  14
##get lock鎖的資源###當前時間:2019-10-13 01:51:45
Thread-32生成訂單號2019-10-13 01:51:45  15
##get lock鎖的資源###當前時間:2019-10-13 01:51:45
Thread-34生成訂單號2019-10-13 01:51:45  16
##get lock鎖的資源###當前時間:2019-10-13 01:51:45
Thread-26生成訂單號2019-10-13 01:51:45  17
##get lock鎖的資源###當前時間:2019-10-13 01:51:45
Thread-29生成訂單號2019-10-13 01:51:45  18
##get lock鎖的資源###當前時間:2019-10-13 01:51:45
Thread-21生成訂單號2019-10-13 01:51:45  19
##get lock鎖的資源###當前時間:2019-10-13 01:51:46
Thread-17生成訂單號2019-10-13 01:51:46  20

1.1、利用mysql實現的鎖缺點:

性能差,最多併發量700~1000 ,差點的機器可以才100~200併發,滿足不了大的併發需求
死鎖,如果一個線程獲取鎖後,程序執行中斷,那麼鎖沒有釋放,就會死鎖
不優雅,查看wailLock這段代碼,是休眠一段時間後再去獲鎖,沒法在鎖釋放後自動監聽鎖被釋放,而需要休眠一段時間 不斷去查看鎖是否被釋放掉。
非阻塞的?需要自己搞一個while循環,直到insert成功再返回成功。或通過遞歸重複獲取鎖,直到成功才返回
這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用。
這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。

我們也可以有其他方式解決上面的問題。

  • 數據庫是單點?搞兩個數據庫,數據之前雙向同步。一旦掛掉快速切換到備庫上。
  • 沒有失效時間?只要做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍。
  • 非重入的?在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了

1.2、基於數據庫排他鎖

除了可以通過增刪操作數據表中的記錄以外,其實還可以藉助數據中自帶的鎖來實現分佈式的鎖。

我們還用剛剛創建的那張數據庫表。可以通過數據庫的排他鎖來實現分佈式鎖。 基於MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作:

public boolean lock(){
    connection.setAutoCommit(false)
    while(true){
        try{
            result = select * from method_Lock where method_name=xxx for update;
            if(result==null){
                return true;
            }
        }catch(Exception e){

        }
        sleep(1000);
    }
    return false;
}

在查詢語句後面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖(這裏再多提一句,InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候纔會使用行級鎖,否則會使用表級鎖。這裏我們希望使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引一定要創建成唯一索引,否則會出現多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數類型也加上。)。當某條記錄被加上排他鎖之後,其他線程無法再在該行記錄上增加排他鎖。

我們可以認爲獲得排它鎖的線程即可獲得分佈式鎖,當獲取到鎖之後,可以執行方法的業務邏輯,執行完方法之後,再通過以下方法解鎖:

public void unlock(){
    connection.commit();
}

通過connection.commit()操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。

  • 阻塞鎖? for update語句會在執行成功後立即返回,在執行失敗時一直處於阻塞狀態,直到成功。
  • 鎖定之後服務宕機,無法釋放?使用這種方式,服務宕機之後數據庫會自己把鎖釋放掉。

但是還是無法直接解決數據庫單點和可重入問題。

這裏還可能存在另外一個問題,雖然我們對method_name 使用了唯一索引,並且顯示使用for update來使用行級鎖。但是,MySql會對查詢進行優化,即便在條件中使用了索引字段,但是否使用索引來檢索數據是由 MySQL 通過判斷不同執行計劃的代價來決定的,如果 MySQL 認爲全表掃效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。如果發生這種情況就悲劇了。。。

還有一個問題,就是我們要使用排他鎖來進行分佈式鎖的lock,那麼一個排他鎖長時間不提交,就會佔用數據庫連接。一旦類似的連接變得多了,就可能把數據庫連接池撐爆

1.3、總結

總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是通過表中的記錄的存在情況確定當前是否有鎖存在,另外一種是通過數據庫的排他鎖來實現分佈式鎖。

數據庫實現分佈式鎖的優點

直接藉助數據庫,容易理解。

數據庫實現分佈式鎖的缺點

會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越複雜。

操作數據庫需要一定的開銷,性能問題需要考慮。

使用數據庫的行級鎖並不一定靠譜,尤其是當我們的鎖表並不大的時候。

2 、zk鎖

先了解一下zk
zk是一個Nosql的數據庫 有監聽機制
zk 與redis類似 都是 no sql
k v結構的數據
創建一個節點命令:
create /deer 1
如果再次執上面那個命令,會執行失敗,那麼我們就是利用zk這一特性來實現鎖的

2.1、下面是zk原生客戶端通過臨時節點實現一種分佈式鎖如下:

public abstract class ZookeeperAbstractLock extends AbstractLock {

    private static final String CONNECTSTRING = "127.0.0.1:2181";

    protected ZkClient zkClient = new ZkClient(CONNECTSTRING);

    protected static final String PATH = "/lock";
    protected static final String PATH2 = "/lock2";


}
public  class ZookeeperLock extends ZookeeperAbstractLock {
    private CountDownLatch countDownLatch = null;
    @Override
    public void waitLock() {
        //創建節點刪除監聽事件
        IZkDataListener iZkDataListener = new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {
            }
            @Override
            public void handleDataDeleted(String s) throws Exception {
                if(countDownLatch != null){
                    countDownLatch.countDown();
                }
            }
        };
        //註冊事件
        zkClient.subscribeDataChanges(PATH,iZkDataListener);
        //如果節點存在
        if(zkClient.exists(PATH)){
            countDownLatch = new CountDownLatch(1);
            try{
                countDownLatch.await();
            }catch (Exception e){

            }
        }
        //刪除監聽
        zkClient.unsubscribeDataChanges(PATH,iZkDataListener);

    }
    @Override
    public void unLock() {
        if(zkClient!=null){
            zkClient.delete(PATH);
            zkClient.close();
            System.out.print("釋放資源");
        }
    }
    @Override
    public boolean tryLock() {
        try{
            //創建臨時節點
            zkClient.createEphemeral(PATH);
            return true;
        }catch (Exception e){
            return false;
        }
    }
}

運行

public class OrderService {
    private AbstractLock lock = new ZookeeperLock();
    private static  int i = 1;
    public void getNumber(){
        try{
            lock.getLock();
            Date d = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println("當前時間:" + sdf.format(d));
            String date = sdf.format(d);
            System.out.println(Thread.currentThread().getName()+"生成訂單號"+date+"  "+i);
            i = i+1;
        }finally {
            lock.unLock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        System.out.println("生成唯一訂單號");
        for(int i =1;i<=20;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    new OrderService().getNumber();
                }
            }).start();
        }
        Thread.sleep(10000);
    }
}

zk分佈鎖問題:
會出現死鎖問題:通過臨時節點可以解決死鎖問題。
臨時節點特性:在創建節點後,如果程序突中斷,那麼節點會被自動刪除,就不會出現死鎖情況。
zk共4種節點類型
1、持久 不會被刪除 可以有子節點
2、臨時 運行一半,客戶端和服務器斷開連接 那麼節點會被自動刪除 不能有子節點
3、臨時帶序號
4、持久帶序號

2.2 基於Curator這個開源框架來實現zk分佈式鎖

基於Curator這個開源框架來實現zk分佈式鎖,實際上就是利用臨時帶序號節點實現的
可查看另一篇文章:5-zk實戰,zk應用

3、基於緩存鎖 reids

基於緩存鎖redis目前也有開源框架redisson,可設置鎖過期時間 防止死鎖,可watch dog機制實現鎖續期,可重入等。具體實現可以查看我的另一篇文章:官方redis實現分佈式鎖算法-RedLock

三、三種方案的比較

上面幾種方式,哪種方式都無法做到完美。就像CAP一樣,在複雜性、可靠性、性能等方面無法同時滿足,所以,根據不同的應用場景選擇最適合自己的纔是王道。

從理解的難易程度角度(從低到高)

數據庫 > 緩存 > Zookeeper

從實現的複雜性角度(從低到高)

Zookeeper >= 緩存 > 數據庫

從性能角度(從高到低)

緩存 > Zookeeper >= 數據庫

從可靠性角度(從高到低)

Zookeeper > 緩存 > 數據庫

redis
redis簡單使用
存1T數據怎麼存?
用集羣
配置一個讀寫分離? slave of
redis主節點有幾個子節點 用什麼命令: info replication
哨兵 RESP redis底層通信協議
resp手寫一個jedis分表分庫 手動一個讀寫分離
redis重啓後,數據會丟失嗎? 持久化 2種機制:RDB AOF 2種

redis怎麼實現分佈鎖
setnx 命令
redis怎麼實現購物車? hash
redis怎麼實現優惠券功能? 30過期,發佈訂閱監聽機制,監聽失效後 修改數據庫中狀態

數據庫
mysql優化
建表語句 一些索引
寫一些sql語句,這些sql是否高效 ,是否用到索引?
index: namd age pos
select * from staffs where name = 1; 1是int ,會有隱式轉換,所以索引失效
沒有指定列不會用到索引
explain
sql 語句 查看執行計劃 ,查看key是否有值 ,有值用到索引,沒值沒用到索引
select * from staffs where age = 1; 沒有用到索引
最佳左前綴原則:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章