zookeeper學習四:分佈式鎖

首先介紹以下鎖,以下面減少庫存案例講解

普通情況

代碼:

public class Stock  {
    //庫存數量
    private static int num=1;

    public boolean reduceStock(){
        if(num>0){
            num--;
            return true;
        }else{
            return false;
        }
    }

}
public class StockMain implements  Runnable {
    public void run() {
            boolean b=new Stock().reduceStock();
            if(b){
                System.out.println(Thread.currentThread().getName()+"減少庫存成功!");
            }else{
                System.out.println(Thread.currentThread().getName()+"減少庫存失敗!");
            }
    }

    public static  void main(String []args){
        new Thread(new StockMain(),"線程1").start();
        new Thread(new StockMain(),"線程2").start();
    }
}

結果:誰先搶到誰成功,也有可能前一個搶到沒執行完畢,後一個線程也進入判斷

,,

 

 

線程安全問題

public class Stock  {
    //庫存數量
    private static int num=1;

    public boolean reduceStock() throws InterruptedException {
        if(num>0){
            Thread.sleep(1000);
            num--;
            return true;
        }else{
            return false;
        }
    }

}


public class StockMain implements  Runnable {
    public void run() {
        boolean b= false;
        try {
            b = new Stock().reduceStock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(b){
                System.out.println(Thread.currentThread().getName()+"減少庫存成功!");
            }else{
                System.out.println(Thread.currentThread().getName()+"減少庫存失敗!");
            }
    }

    public static  void main(String []args){
        new Thread(new StockMain(),"線程1").start();
        new Thread(new StockMain(),"線程2").start();
    }
}

結果:先搶到線程的進入方法後,沉睡一秒,足夠後面的線程進入判斷條件,所以全部成功,產生線程安全問題

加入鎖機制

 

public class Stock  {
    //庫存數量
    private static int num=1;

    public boolean reduceStock() throws InterruptedException {
        if(num>0){
            Thread.sleep(1000);
            num--;
            return true;
        }else{
            return false;
        }
    }

}

public class StockMain implements  Runnable {
    private static Lock lock=new ReentrantLock();
    public void run() {
        boolean b= false;
        try {
            //上鎖
            lock.lock();;
            b = new Stock().reduceStock();
            //解鎖
            lock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(b){
                System.out.println(Thread.currentThread().getName()+"減少庫存成功!");
            }else{
                System.out.println(Thread.currentThread().getName()+"減少庫存失敗!");
            }
    }

    public static  void main(String []args){
        new Thread(new StockMain(),"線程1").start();
        new Thread(new StockMain(),"線程2").start();
    }
}

結果:只有前一個線程執行完畢後,後一個纔會執行

 

引出問題:分佈式系統集羣環境下,負載均衡之後,服務不可能只發給一臺機器,鎖機制已經不滿足現狀,分佈式鎖出現。

分佈式鎖

1.數據庫實現分佈式鎖的思路分析(操作同一數據庫)

數據庫分佈式鎖實現

創建表

CREATE TABLE `lock_record` (
	`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
	`lock_name` VARCHAR ( 50 ) DEFAULT NULL COMMENT '鎖名稱',
	PRIMARY KEY ( `id` ),
UNIQUE KEY `lock_name` ( `lock_name` ) 
) ENGINE = INNODB AUTO_INCREMENT = 38 DEFAULT CHARSET = utf8

 

定義鎖

實現Lock接口,tryLock()嘗試獲取鎖,從鎖表中查詢指定的鎖記 錄,如果查詢到記錄,說明 已經上鎖,不能再上鎖

 

上鎖

lock方法獲取鎖之前先調用tryLock()方法嘗試獲取鎖,如果未加鎖則向鎖表中插入一條鎖記錄來獲取 鎖,這裏我們通過循環,如果上鎖我們一致等待鎖的釋放

釋放鎖

即是將數據庫中對應的鎖表記錄刪除

注意在嘗試獲取鎖的方法tryLock中,存在多個線程同時獲取鎖的情況,可以簡單通過synchronized解決
import com.itheima.demo.bean.LockRecord;
import com.itheima.demo.mapper.LockRecordMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

@Component
public class DbLock implements Lock {

    private static final String LOCK_NAME = "db_lock_stock";


    @Autowired
    private LockRecordMapper lockRecordMapper;

    //上鎖
    @Override
    public void lock() {
        while(true){
            if(tryLock()){
                //向鎖表中插入一條記錄
                LockRecord lockRecord = new LockRecord();
                lockRecord.setLockName(LOCK_NAME);
                lockRecordMapper.insertSelective(lockRecord);
                return;
            }else{
                System.out.println("等待鎖.......");
            }
        }
    }

    //嘗試獲取鎖
    @Override
    public boolean tryLock() {
        //查詢lockRecord的記錄
        Example example = new Example(LockRecord.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("lockName",LOCK_NAME);
        LockRecord lockRecord = lockRecordMapper.selectOneByExample(example);
        if(lockRecord==null){
            return true;
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    //釋放鎖的操作
    @Override
    public void unlock() {
        Example example = new Example(LockRecord.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("lockName",LOCK_NAME);
        lockRecordMapper.deleteByExample(example);
    }

    @Override
    public Condition newCondition() {
        return null;
    }


    @Override
    public void lockInterruptibly() throws InterruptedException {

    }
}

2.redis實現分佈式鎖

 

redis分佈式鎖的實現基於setnxset if not exists),設置成功,返回1;設置失敗,返回0,

釋放鎖的操作通過del指令來完成 如果設置鎖後在執行中間過程時,程序拋出異常,導致del指令沒有調用,鎖永遠無法釋放,這樣就會 陷入死鎖。所以我們拿到鎖之後會給鎖加上一個過期時間,這樣即使中間出現異常,過期時間到後會自動釋放鎖。同時在setnx expire 如果進程掛掉,expire不能執行也會死鎖。所以要保證setnxexpire是一個原子性操作即可。

redis 2.8之後推出了setnxexpire的組合指令


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

@Component
public class RedisLock implements Lock {

    private static final String LOCK_NAME = "redis_stock_lock";

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void lock() {
        while(true){
            //上鎖 setnx
          //  Boolean isLock = redisTemplate.opsForValue().setIfAbsent("lockName", LOCK_NAME);
            Boolean isLock =  redisTemplate.opsForValue().setIfAbsent("lockName",LOCK_NAME,10,TimeUnit.SECONDS);
            //思考 鎖過期怎麼辦?如何保證鎖不過期 鎖的自動續期 20 18 20 20
            if(isLock){
                return;
            }else{
                System.out.println("等待鎖........");
            }
        }

    }


    @Override
    public void unlock() {
        // 刪除指定的鎖的key
        redisTemplate.delete("lockName");
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }



    @Override
    public Condition newCondition() {
        return null;
    }
}
redis實現分佈式鎖存在的問題,爲了解決redis單點問題,我們會部署redis集羣,在 Sentinel 集羣中, 主節點突然掛掉了。同時主節點中有把鎖還沒有來得及同步到從節點。這樣就會導致系統中同樣一把鎖 被兩個客戶端同時持有,不安全性由此產生。redis官方爲了解決這個問題,推出了Redlock 算法解決這 個問題。但是帶來的網絡消耗較大。

 

分佈式鎖的redisson實現:

<dependency> 
    <groupId>org.redisson</groupId> 
    <artifactId>redisson</artifactId> 
    <version>3.6.5</version>
</dependency>

獲取鎖,釋放鎖

Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDataba se(0);
Redisson redisson = (Redisson) Redisson.create(config);
RLock mylock = redisson.getLock(key); 
//獲取鎖
mylock.lock();
//釋放鎖
mylock.unlock();

3.zookeeper實現分佈式鎖

原理:

zookeeper通過創建臨時序列節點來實現分佈式鎖,適用於順序執行的程序,大體思路就是創建 臨時序列節點,找出最小的序列節點,獲取分佈式鎖,程序執行完成之後此序列節點消失,通過watch 來監控節點的變化,從剩下的節點的找到最小的序列節點,獲取分佈式鎖,執行相應處理,依次類推

原生實現
首先在ZkLock的構造方法中,連接zk,創建lock根節點
添加watch監聽臨時順序節點的刪除
獲取鎖操作
釋放鎖
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class ZkLock implements Lock {


    //zk客戶端
    private ZooKeeper zk;
    //zk是一個目錄結構,locks
    private String root = "/locks";
    //鎖的名稱
    private String lockName;
    //當前線程創建的序列node
    private ThreadLocal<String> nodeId = new ThreadLocal<>();
    //用來同步等待zkclient鏈接到了服務端
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    private final static int sessionTimeout = 3000;
    private final static byte[] data= new byte[0];


    public ZkLock(String config, String lockName) {
        this.lockName = lockName;

        try {
            zk = new ZooKeeper(config, sessionTimeout, new Watcher() {

                @Override
                public void process(WatchedEvent event) {
                    // 建立連接
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        connectedSignal.countDown();
                    }
                }

            });

            connectedSignal.await();
            Stat stat = zk.exists(root, false);
            if (null == stat) {
                // 創建根節點
                zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    class LockWatcher implements Watcher {
        private CountDownLatch latch = null;

        public LockWatcher(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void process(WatchedEvent event) {

            if (event.getType() == Event.EventType.NodeDeleted)
                latch.countDown();
        }
    }

    @Override
    public void lock() {
        try {
            // 創建臨時子節點
            String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(Thread.currentThread().getName()+myNode+ "created");

            // 取出所有子節點
            List<String> subNodes = zk.getChildren(root, false);
            TreeSet<String> sortedNodes = new TreeSet<>();
            for(String node :subNodes) {
                sortedNodes.add(root +"/" +node);
            }

            String smallNode = sortedNodes.first();


            if (myNode.equals( smallNode)) {
                // 如果是最小的節點,則表示取得鎖
                System.out.println(Thread.currentThread().getName()+ myNode+"get lock");
                this.nodeId.set(myNode);
                return;
            }

            String preNode = sortedNodes.lower(myNode);

            CountDownLatch latch = new CountDownLatch(1);
            Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同時註冊監聽。
            // 判斷比自己小一個數的節點是否存在,如果不存在則無需等待鎖,同時註冊監聽
            if (stat != null) {
                System.out.println(Thread.currentThread().getName()+myNode+
                        " waiting for " + root + "/" + preNode + " released lock");

                latch.await();// 等待,這裏應該一直等待其他線程釋放鎖
                nodeId.set(myNode);
                latch = null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }


    @Override
    public void unlock() {
        try {
            System.out.println(Thread.currentThread().getName()+ "unlock ");
            if (null != nodeId) {
                zk.delete(nodeId.get(), -1);
            }
            nodeId.remove();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }



    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }



    @Override
    public Condition newCondition() {
        return null;
    }
}
分佈式隊列
隊列特性:FIFO(先入先出),zookeeper實現分佈式隊列的步驟:
  • 在隊列節點下創建臨時順序節點 例如/queue_info/192.168.1.1-0000001
  • 調用getChildren()接口來獲取/queue_info節點下所有子節點,獲取隊列中所有元素
  • 比較自己節點是否是序號最小的節點,如果不是,則等待其他節點出隊列,在序號最小的節點註冊 watcher
  • 獲取watcher通知後,重複步驟

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