分佈式鎖(2) ----- 基於redis的分佈式鎖

分佈式鎖系列文章

分佈式鎖(1) ----- 介紹和基於數據庫的分佈式鎖 分佈式鎖(2) ----- 基於redis的分佈式鎖 分佈式鎖(3) ----- 基於zookeeper的分佈式鎖 代碼:https://github.com/shuo123/distributeLockhtml

Redis單機版實現

set和lua實現

獲取鎖

SET resource_name my_random_value NX PX 30000

NX key不存在時才set PX 設置過時時間 my_random_value 要保證每臺客戶端的每一個鎖請求惟一,可使用UUID+ThreadID 該命令在Redis 2.6.12纔有,網上有基於setnx、epire的實現和基於setnx、get、getset的實現,這些多多少少都有點瑕疵,大機率是舊版本的redis實現,建議高版本的redis仍是使用這個實現。java

釋放鎖

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

經過上述lua腳本實現,先判斷鎖是否該請求擁有,防止誤解鎖。git

java代碼

public boolean tryLock(long waitTime, long leaseTime) {
    long end = Calendar.getInstance().getTimeInMillis() + waitTime;
    Jedis jedis = jedisPool.getResource();
    try {
        do {
            String result = jedis.set(lockName, getClientId(), "NX", "EX", leaseTime);
            if ("OK".equals(result)) {
                return true;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                return false;
            }
        } while (Calendar.getInstance().getTimeInMillis() < end);
    }finally {
        if(jedis != null) {
            jedis.close();
        }
    }
    return false;
}

public boolean unlock() {
    String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
    Jedis jedis = jedisPool.getResource();
    try {
        Object obj = jedis.eval(lua, Collections.singletonList(lockName), Collections.singletonList(getClientId()));
        if (obj.equals(1)) {
            return true;
        }
    }finally {
        if(jedis != null){
            jedis.close();
        }
    }
    return false;
}

測試代碼

public void lockTest() {
	//用線程模擬進程
    ExecutorService threadPool = Executors.newFixedThreadPool(20);
    CyclicBarrier barrier = new CyclicBarrier(20);
    for (int i = 0; i < 20; i++) {
        threadPool.execute(new RedisLockTest(barrier));
    }
    threadPool.shutdown();
    while (!threadPool.isTerminated()) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
private static class RedisLockTest implements Runnable{

    private CyclicBarrier barrier;
    RedisLockTest(CyclicBarrier barrier){
        this.barrier = barrier;
    }

    @Override
    public void run() {
		//模擬一臺客戶端一個jedisPool
        JedisPool jedisPool = new JedisPool("192.168.9.150", 6379);
        try {
            DistributeLock lock = new SingletonRedisLock(jedisPool, "lock");
            barrier.await();
            boolean flag = lock.tryLock(Integer.MAX_VALUE, 300);
            try {
                System.out.println(Thread.currentThread().getId() + "get lock:flag=" + flag);
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getId() + "get unlock");
            } finally {
                lock.unlock();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedisPool.close();
        }
    }
}

存在的問題

該實現簡單,但存在如下問題: 1.沒有實現可重入 2.沒有鎖續租,若是代碼在鎖的租期內沒有執行完成,那麼鎖過時會致使另外一個客戶端獲取鎖github

Redisson實現

Redisson是redis推薦的分佈式鎖實現開源項目。Redisson的分佈式鎖實現可重入,同時有LockWatchDogTimeout來實現鎖續約 github:https://github.com/redisson/redissonredis

private static class RedissonLockTest implements Runnable{

    private CyclicBarrier barrier;
    RedissonLockTest(CyclicBarrier barrier){
        this.barrier = barrier;
    }

    @Override
    public void run() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.9.150:6379");
        RedissonClient client = Redisson.create(config);
        try {
            RLock lock = client.getLock("lock");
            barrier.await();
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "get lock");
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName() + "get unlock");
            } finally {
                lock.unlock();
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            client.shutdown();
        }
    }
}

Redis多實例版實現

單機實如今多實例下問題

若是Redis實現了集羣,因爲主從之間時經過異步複製的,假設客戶端A在主機上得到鎖,這時在未將鎖數據複製到從機時,主機掛了,從機切換爲主機,那麼從機沒有這條數據,客戶端B一樣能夠得到鎖。算法

RedLock算法

使用多個獨立(非集羣)的實例來實現分佈式鎖,因爲實例獨立不需複製同步,因此沒有上述問題;而保證可用性的是靠數據冗餘,將數據多存放幾份在不一樣的實例上。算法以下:數據庫

  1. 使用相同的key和value從N個實例上獲取鎖;
  2. 當從大於(N/2+1)個實例獲取鎖的時間<鎖的過時時間,才獲取鎖成功
  3. 若是獲取失敗,解鎖全部實例

Redisson實現

public void redLockTest() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://192.168.9.150:7000");
    RedissonClient client = Redisson.create(config);
    Config config1 = new Config();
    config1.useSingleServer().setAddress("redis://192.168.9.150:7001");
    RedissonClient client1 = Redisson.create(config1);
    Config config2 = new Config();
    config2.useSingleServer().setAddress("redis://192.168.9.150:7002");
    RedissonClient client2 = Redisson.create(config2);
    try{
        RLock lock = client.getLock("lock");
        RLock lock1 = client1.getLock("lock");
        RLock lock2 = client2.getLock("lock");
        RedissonRedLock redLock = new RedissonRedLock(lock, lock1, lock2);
        redLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "get lock");
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + "get unlock");
        } finally {
            redLock.unlock();
        }
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        client.shutdown();
        client1.shutdown();
        client2.shutdown();
    }
}

參考資料

https://redis.io/topics/distlock https://github.com/redisson/redisson/wikidom

相關文章
相關標籤/搜索