【開發經驗】redis和zookeeper分佈式鎖對比

引言
如果有int count=0;10000個線程都執行count++,執行完之後count的值會是10000嗎?

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0 ;i<10000;i++){
            new Thread(()->{
                    count++;
            }).start();
        }
        //等線程執行完
        Thread.sleep(5000l);
        System.out.println(count);
    }
}

多跑幾次即可發現,有的時候得到的值不是自己想要的。這裏就是多線程安全問題了。在單機的情況下通過jdk自帶的synchronized或者ReentrantLock都可以解決如上問題(現在不考慮使用AtomicInteger的情況下)。如果是多機器的情況下呢?jdk自帶的鎖就無法達到效果了。比如:

    private IUserDao userDao;

    public synchronized void addUser(String mobile,String name){
        Boolean exists = userDao.getUserInfo(mobile);
        if(exists){
            userDao.addUser(mobile,name);
        }
    }

防止用戶操作過快,導致重複添加到數據庫中,在單機的情況下,通過synchronized可防止用戶重複添加。但是如果是多個服務器的情況,用戶通過機器刷請求,這個時候jdk自帶的synchronized也無法起到互斥的效果。這個時候就需要分佈式鎖來處理了。
上面的例子不算特別的好,大家腦補一下場景啦。

redis分佈式鎖

​ 首先必須知道的幾個redis命令

redis 命令學習

setnx

SETNX 是【SET if Not eXists】(如果不存在,則 SET)的簡寫;

當且僅當 key 不存時,將 key 的值設爲 value 。若給定的 key 已經存在,則 SETNX 不做任何動作。

返回值:

設置成功,返回 1 。

設置失敗,返回 0 。

redis> EXISTS job                # job 不存在
(integer) 0
 
redis> SETNX job "programmer"    # job 設置成功
(integer) 1
 
redis> SETNX job "code-farmer"   # 嘗試覆蓋 job ,失敗
(integer) 0
 
redis> GET job                   # 沒有被覆蓋
"programmer"

set

設置一個值到redis中。

使用setnx命令的時候有一個不足,即是無法在設置一個值的同時設置key的過期時間,只能通過設置成功之後,再通過expire設置過期時間,這個操作無法保證原子性。所以如果想在設置一個值的時候再設置過期時間保證它的原子性,只能通過set命令。如下:

set key value [EX seconds] [PX milliseconds] [NX|XX]

EX seconds:設置失效時長,單位秒
PX milliseconds:設置失效時長,單位毫秒
NX:key不存在返回ok,如果key存在返回(nil)
XX:key存在返回ok,如果key不存在失敗返回(nil)

如此高仿setnx命令,當job不存在時設置value爲aa,超時時間10秒

> set job aa ex 10 nx
OK
> ttl job
(integer) 7
> set job aa ex 10 nx
(nil)

getset

getset是【GETSET key value】

將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。

當 key 存在但不是字符串類型時,返回一個錯誤。

> exists job
(integer) 0
> getset job aa
(nil)
> set job aa
OK
> getset job bb
"aa"

redis 鎖常見思路

通常情況下c0通過setnx進行設置值,如果設置成功,即可認爲獲取鎖成功,但是如果c0一直不釋放鎖,或者說c0線程異常,釋放鎖失敗,那就會導致其他線程一直無法獲取鎖。那必須得有異常處理的邏輯,所以思路如下

網上常見的實例:

實例一
實現思路
  1. c0通過setnx設置key爲lock,如果設置成功,則通過expire設置key過期時間,獲取鎖成功;
  2. c1 ,c2(也可能是更多線程) 也同樣通過setnx設置key爲lock,這時應該設置失敗,因爲lock這個key已經被線程c0設置成功,此c1,c2獲取鎖失敗;
  3. c1 ,c2(也可能是更多線程) 也同樣通過setnx設置key爲lock,value爲鎖失效時間,這時應該設置失敗,因爲lock這個key已經被線程c0設置成功,此c1,c2獲取鎖失敗;
代碼
  public boolean lock(final String key, final String value,Long expireTime) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.expire(serializer.serialize(key), expireTime);
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key,e);
        }
        return obj != null ? (Boolean) obj : false;
    }

弊端:通過setNX和expire 並不是原子操作,如果在setnx設置成功之後系統崩盤,就會出現死鎖

實例二
實現思路
  1. c0通過setnx設置key爲lock,value爲鎖失效時間,可以爲當前時間向後10秒,設置成功,成功獲取鎖;
  2. c1 ,c2(也可能是更多線程) 也同樣通過setnx設置key爲lock,value爲鎖失效時間,這時應該設置失敗,因爲lock這個key已經被線程c0設置成功,此c1,c2獲取鎖失敗;
  3. c1,c2獲取lock的過期時間,發現鎖過期,進行再次爭奪鎖;
  4. c1,c2(或者更多線程)發現鎖已經過期這個時候每個線程中一定存有lock的value,通過getset 命令設置lock的value,value也是鎖的失效時間,如果返回的值和lock之前的value進行對比相同的話,即可認爲獲取鎖成功;

此實例算是實例一的補救;

代碼
 /**
     * 加鎖
     */
    public  long lock(String lockKey, String threadName) {
        log.info(threadName + "開始執行加鎖");
        //鎖時間   過期時間
        Long lock_timeout = currtTimeForRedis() + lockTimeout + 1;
        // 鎖失效時間爲10 秒,value裏存入過期時間,如果超過過期時間視爲無效鎖,其他線程可以重新獲取鎖
        if (setNX(lockKey, String.valueOf(lock_timeout),10l)) {
            //如果加鎖成功
            log.info(threadName + "加鎖成功+1");
            //設置超時時間,釋放內存
 
            return lock_timeout;
        } else {
            //獲取redis裏面的時間
            Object result = get(lockKey);
            Long currt_lock_timeout_str = result == null ? null : Long.parseLong(result.toString());
            //鎖已經失效
            if (currt_lock_timeout_str != null && currt_lock_timeout_str < currtTimeForRedis()) {
                //判斷是否爲空,不爲空時,如果被其他線程設置了值,則第二個條件判斷無法執行
                //獲取上一個鎖到期時間,並設置現在的鎖到期時間
                Long old_lock_timeout_Str = Long.valueOf(getSet(lockKey, String.valueOf(lock_timeout)));
                if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_str)) {
                    //多線程運行時,多個線程簽好都到了這裏,但只有一個線程的設置值和當前值相同,它纔有權利獲取鎖
                    log.info(threadName + "加鎖成功+2");
                    //設置超時間,釋放內存  這個是 10 L
                    expire(lockKey, 10l);
                    //返回加鎖時間
                    return lock_timeout;
                }
            }
        }
        return -1;
    }
  public boolean setNX(final String key, final String value,Long expireTime) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.expire(serializer.serialize(key), expireTime);
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key,e);
        }
        return obj != null ? (Boolean) obj : false;
    }

  public String getSet(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            log.error("getSet redis error, key : {}", key,e);
        }
        return obj != null ? obj.toString() : null;
    }

  /**
     * 多服務器集羣,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!!
     *
     * @return
     */
    public long currtTimeForRedis() {
        Object obj = redisTemplate.execute(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.time();
            }
        });
        return obj == null ? -1 : Long.parseLong(obj.toString());
    }
 public boolean expire(final String lockKey, final Long expireTime) {
        Object obj = null;
        try {
            obj =  redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    boolean bool = connection.expire(serializer.serialize(lockKey), expireTime);
                    connection.close();
                    return bool;
                }
            });
            log.info("設置過期時間"+expireTime);
            return (Boolean)obj;
        } catch (Exception e) {
            log.error("expire redis error, key : {}", lockKey,e);
        }
        return false;
    }

弊端:多線程一起通過getset設置lock的value和之前的value進行比對,最後lock的value並不一定是成功獲取鎖線程設置的,即成功獲取鎖的線程的失效時間並不一定是自己想要的失效時間,但是誤差極小,而且不影響鎖的使用,所以可以忽略;由此設置的鎖不具有擁有者的標識,即任何線程都能對此鎖釋放;

注:在此列舉常見的兩個實例供大家參考;

redisson引入

貼一段概念:原爲地址 https://www.cnblogs.com/liyan492/p/9858548.html

概念:

Jedis:是Redis的Java實現客戶端,提供了比較全面的Redis命令的支持,

Redisson:實現了分佈式和可擴展的Java數據結構。

Lettuce:高級Redis客戶端,用於線程安全同步,異步和響應使用,支持集羣,Sentinel,管道和編碼器。

優點:

Jedis:比較全面的提供了Redis的操作特性

Redisson:促使使用者對Redis的關注分離,提供很多分佈式相關操作服務,例如,分佈式鎖,分佈式集合,可通過Redis支持延遲隊列

Lettuce:主要在一些分佈式緩存框架上使用比較多

可伸縮:

Jedis:使用阻塞的I/O,且其方法調用都是同步的,程序流需要等到sockets處理完I/O才能執行,不支持異步。Jedis客戶端實例不是線程安全的,所以需要通過連接池來使用Jedis。

Redisson:基於Netty框架的事件驅動的通信層,其方法調用是異步的。Redisson的API是線程安全的,所以可以操作單個Redisson連接來完成各種操作

Lettuce:基於Netty框架的事件驅動的通信層,其方法調用是異步的。Lettuce的API是線程安全的,所以可以操作單個Lettuce連接來完成各種操作

redisson RLock

​ RLock實現了java.util.concurrent.locks.Lock接口(點擊跳轉->溫習lock接口)。而且RLock是使用Lua腳本完成分佈式鎖效率更高。

如下:

RLock lock = redisson.getLock("anyLock");
// 最常見的使用方法
lock.lock();

通過anyLockkey獲取鎖,如果負責儲存這個分佈式鎖的Redis節點宕機以後,而且這個鎖正好處於鎖住的狀態時,這個鎖會出現鎖死的狀態。爲了避免這種情況的發生,Redisson內部提供了一個監控鎖的看門狗,它的作用是在Redisson實例被關閉前,不斷的延長鎖的有效期。默認情況下,看門狗的檢查鎖的超時時間是30秒鐘,也可以通過修改Config.lockWatchdogTimeout來另行指定。

​ 而且也可以通過leaseTime自行控制超時時間

// 加鎖以後10秒鐘自動解鎖
// 無需調用unlock方法手動解鎖
lock.lock(10, TimeUnit.SECONDS);

// 嘗試加鎖,最多等待100秒,上鎖以後10秒自動解鎖
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...//代碼邏輯
lock.unlock();

另外redisson還支持公平鎖,聯鎖,紅鎖,讀寫鎖,信號量,可過期性信號量,閉鎖

官網地址:https://redisson.org/

文檔地址:https://github.com/redisson/redisson#quick-start

推薦一個不錯的參考文章:https://yq.aliyun.com/articles/551423

Zookeeper分佈式鎖

zookeeper基礎

znode 一共4中類型:持久的,臨時的,有序持久,有序臨時

​ 持久的znode,如/path,只能通過調用delete來進行刪除。臨時的znode與之相反,當創建該節點的客戶端崩潰或關閉了與ZooKeeper的連接時,這個節點就會被刪除。

​ 一個znode還可以設置爲有序(sequential)節點。一個有序znode節
點被分配唯一個單調遞增的整數。當創建有序節點時,一個序號會被追
加到路徑之後。例如,如果一個客戶端創建了一個有序znode節點,其
路徑爲/tasks/task-,那麼ZooKeeper將會分配一個序號,如1,並將這個
數字追加到路徑之後,最後該znode節點爲/tasks/task-1。有序znode通過
提供了創建具有唯一名稱的znode的簡單方式。同時也通過這種方式可
以直觀地查看znode的創建順序。

zookeeper watch機制

​ 當node節點有變化時,爲了替換客戶端的輪詢,我們選擇了基於通知(notification)的機制,客戶端向ZooKeeper註冊需要接收通知的
znode,通過對znode設置監視點(watch)來接收通知。

​ ZooKeeper可以定義不同類型的通知,這依賴於設置監視點對應的
通知類型。客戶端可以設置多種監視點,如監控znode的數據變化、監
控znode子節點的變化、監控znode的創建或刪除。

看了上面的一些官方的不知道會不會懵逼,簡單的說,就是觀察者模式,讓客戶端對node配置監聽,如果node有變回,就會有通知回來。

簡單測試

  1. 查看zk節點

    [zk: localhost:2181(CONNECTED) 3] ls /
    [zookeeper]
    
  2. 創建/test節點

    [zk: localhost:2181(CONNECTED) 4] create /test ""
    Created /test
    [zk: localhost:2181(CONNECTED) 5] ls /
    [zookeeper, test]
    
  3. 啓動客戶端2對test節點監聽

    [zk: localhost:2181(CONNECTED) 1] ls /
    [zookeeper, test]
    [zk: localhost:2181(CONNECTED) 2] get /test true #true 是打開watch
    
    cZxid = 0x4d
    ctime = Fri Nov 22 21:41:15 CST 2019
    mZxid = 0x4d
    mtime = Fri Nov 22 21:41:15 CST 2019
    pZxid = 0x4d
    cversion = 0
    dataVersion = 0
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 0
    numChildren = 0
    [zk: localhost:2181(CONNECTED) 3]
    
  4. 第一個客戶端刪除/test節點

    [zk: localhost:2181(CONNECTED) 6] delete /test
    [zk: localhost:2181(CONNECTED) 7]
    
  5. 接着馬上客戶端2就會有通知

    [zk: localhost:2181(CONNECTED) 3]
    WATCHER::
    
    WatchedEvent state:SyncConnected type:NodeDeleted path:/test
    

zookeeper分佈式鎖思路

思路一

​ 假設有一個應用由n個進程組成,這些進程嘗試獲取一個鎖。再次
強調,ZooKeeper並未直接暴露原語,因此我們使用ZooKeeper的接口來
管理znode,以此來實現鎖。爲了獲得一個鎖,每個進程p嘗試創建
znode,名爲/lock。如果進程p成功創建了znode,就表示它獲得了鎖並
可以繼續執行其臨界區域的代碼。不過一個潛在的問題是進程p可能崩
潰,導致這個鎖永遠無法釋放。在這種情況下,沒有任何其他進程可以
再次獲得這個鎖,整個系統可能因死鎖而失靈。爲了避免這種情況,我
們不得不在創建這個節點時指定/lock爲臨時節點。

思路二

​ n個進程一起創建順序節點,如果發現當前線程創建的順序節點是第一個,則成功獲取鎖,如果不是則可以鑑定前面的鎖釋放,如果前面鎖釋放,則自己成功獲取鎖。

zookeeper Curator 框架

​ Curator是Netflix公司開源的一套Zookeeper客戶端框架。瞭解過Zookeeper原生API都會清楚其複雜度。Curator幫助我們在其基礎上進行封裝、實現一些開發細節,包括接連重連、反覆註冊Watcher和NodeExistsException等。目前已經作爲Apache的頂級項目出現,是最流行的Zookeeper客戶端之一。從編碼風格上來講,它提供了基於Fluent的編程風格支持。

除此之外,Curator還提供了Zookeeper的各種應用場景:Recipe、共享鎖服務、Master選舉機制和分佈式計數器等。

​ zookeeper 分佈式鎖通過acquire進行獲取鎖,通過release進行釋放鎖

curator框架分佈式鎖

共享可重入鎖
@RestController
public class ZookeeperController {
    @Autowired
    private CuratorFramework curatorFramework;

    @RequestMapping("/zookeeper/makeorder")
    public String makeOrder(Long userId){
        // 生成鎖,防止用戶頻繁點擊
        InterProcessMutex lock = new InterProcessMutex  (curatorFramework, "/makeorder"+userId);
        try {
            // 如果可能請儘量設置超時時間,免得一直等待
            lock.acquire();
            System.out.println("獲取鎖成功");
            //todo
            Thread.sleep(10000L);
            lock.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "true";
    }
    @RequestMapping("/zookeeper/makeorder/trylock")
    public String makeOrderTrylock(Long userId){
        // 生成鎖,防止用戶頻繁點擊
        InterProcessMutex lock = new InterProcessMutex  (curatorFramework, "/makeorder"+userId);
        try {
            Boolean getlock = lock.acquire(1000l, TimeUnit.MILLISECONDS);
            if(getlock){
                //todo
                System.out.println("試着獲取鎖成功");
                lock.release();
                Thread.sleep(4000l);
                return "true";
            }else{
                System.out.println("獲取鎖失敗,獲取超時");
                return "false";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "error";
    }
}

讀寫鎖

    @RequestMapping("/zookeeper/makeorder/readlock")
// 讀取鎖
    public String makeOrderReadlock(Long userId){
        // 生成鎖,防止用戶頻繁點擊
        InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock   (curatorFramework, "/makeorderReadlock"+userId);
        InterProcessMutex lock = interProcessReadWriteLock.readLock();
        try {
            Boolean getlock = lock.acquire(1000l, TimeUnit.MILLISECONDS);
            if(getlock){
                //todo
                System.out.println("試着獲取readlock鎖成功");
                lock.release();
                return "true";
            }else{
                System.out.println("獲取鎖失敗,獲取超時");
                return "false";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "error";
    }
    @RequestMapping("/zookeeper/makeorder/writelock")
// 寫入鎖
    public String makeOrderWritelock(Long userId){
        // 生成鎖,防止用戶頻繁點擊
        InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock   (curatorFramework, "/makeorderReadlock"+userId);
        try {
            InterProcessMutex lock = interProcessReadWriteLock.writeLock();
            boolean getlock = lock.acquire(2000l, TimeUnit.MILLISECONDS);
            if(getlock){
                //todo
                System.out.println("試着獲取writelock鎖成功");
                Thread.sleep(10000l);
                lock.release();
                return "true";
            }else{
                System.out.println("獲取鎖失敗,獲取超時");
                return "false";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "error";
    }

curator對分佈式鎖分裝的比較完善,可以很輕鬆的實現分佈式鎖,另外還有共享鎖不可重入共享信號量聯鎖隊列等等。

官網地址:http://curator.apache.org/curator-recipes/index.html

共享鎖一篇源碼分析,寫的好,供大家參考https://www.cnblogs.com/shileibrave/p/9854921.html

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