Redis分佈式鎖實現及相關注重問題

0.前言

      在多線程併發的情況下,我們可以使用鎖來保證代碼在同一時間只能一個線程訪問,比如synchronize或者lock。但在分佈式的集羣環境,就需要使用分佈式鎖。
     分佈式:一個業務拆分爲多個子業務,部署在多個服務器上 。
     集羣:同一個業務,部署在多個服務器上 。

1.場景:

     a).避免不同節點(業務)重複相同工作,
     b).避免破壞數據的正確性
           比如,同一服務部署在不同的服務器上,同時接受到請求,同時對數據進行修改(如i- -),則可能出現重複- -的情況。
在這裏插入圖片描述

2.Redis分佈式鎖原理:

     Set命令附加NX和PX屬性
在這裏插入圖片描述
     主要依託了redis的 Key 不存在時才能 Set 成功的特性
     A服務set成功(獲取鎖成功),那麼B服務就無法獲取改Key的鎖,除非A服務釋放了鎖,B服務才能獲取到。,

3.Redis分佈式鎖主要關注的問題:

     a.鎖超時:
          A服務set成功(獲取了鎖),之後A服務突然掛掉了,那麼其它服務就無法獲取到鎖了,爲了避免這種問題,所以需要添加鎖過期時間,像上述圖片t2在30s後會自動失效,即釋放鎖。
     b.原子性問題:
          加鎖的時候,如果先set t2 666,然後再expire t2 30 ;這樣的操作不是原子性的,也就是說,如果set t2 666後突然服務掛了,又還沒來得及設置過期時間,則其他服務永遠也獲取不到鎖,但是set t2 666 PX 30000 NX這樣就是原子性的。
          解鎖的時候,如果直接jedis.del(key);可能會刪除不是自己加的鎖。,
          是否會想到下述的操作,先get判斷再del,但這並不是原子性的操作,比如A服務加鎖後,到了過期時間自動釋放了鎖,之後B服務獲取了鎖,此時A服務業務邏輯執行完後,直接就把鎖給刪了,服務就亂套了。

if (value.equals(jedis.get(key))) {
    // 如果這時鎖過期自動釋放,又被其他線程加鎖,該線程就會釋放不屬於自己的鎖
    jedis.del(key);
}

          正確釋放鎖,需要執行lua腳本來進行。
在這裏插入圖片描述
     c.業務執行超過自動過期時間:
          A服務獲得了鎖,業務執行時間超過了過期時間,A服務還未手動釋放鎖就自動釋放了鎖,然後B服務獲得了鎖,也有可能出現別的問題,所以爲了避免這個問題, Redis 分佈式鎖不要用於較長時間的任務。
          或者對獲得鎖的線程啓動一個守護線程,用來給鎖續期(執行expire命令),執行時間就變長了這樣。
     d.請求是否不可丟失問題:
          如果請求不允許丟失,即每個服務都要獲取鎖來執行業務邏輯,則不斷重試獲取鎖,直到獲取鎖成功。
          如果允許丟失,則嘗試重試到設置的重試時間閾值,無法獲取鎖則不管了,也就不執行業務邏輯了。

4.Redis分佈式鎖代碼實現:

     加鎖與釋放鎖的類:

package com.zwh.JedisDistributed;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;


public class RedisLock {
    public String LOCK_KEY = "reids_lock";
    private long AUTO_EXPIRE_TIME;          //鎖自動釋放閾值
    private long reTryTime;                 //重試時間閾值
    private SetParams params ;
    private static GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    public static JedisPool jedisPool = null;

    public RedisLock(Integer inventory, long AUTO_EXPIRE_TIME,long reTryTime) {
        this.AUTO_EXPIRE_TIME = AUTO_EXPIRE_TIME;
        this.params = SetParams.setParams().nx().px(AUTO_EXPIRE_TIME);
        this.reTryTime=reTryTime;
        poolConfig.setMaxIdle(inventory);
        poolConfig.setMaxTotal(inventory);
        poolConfig.setMaxWaitMillis(2000);//2s後報錯
        jedisPool = new JedisPool(poolConfig, "192.168.199.188", 6379,4000);//4s後報連接超時
    }

    /**
     *  嘗試獲取鎖,到了重試時間閾值退出,即可能請求丟失
     * */
    public String lock(String id) {
        Long start = System.currentTimeMillis();
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            for (; ; ) {
                //SET命令返回OK ,則證明獲取鎖成功
                String lock = jedis.set(LOCK_KEY, id, params);
                if ("OK".equals(lock)) {
                    return "OK";
                }
                //否則循環等待,在timeout時間內仍未獲取到鎖,則獲取失敗
                long l = System.currentTimeMillis() - start;
                if (l >= reTryTime) {
                    return "TimeOut";
                }
                Thread.sleep(200);
            }
        } catch (Exception e) {
            System.out.println("******異常*******:"+e.toString());
        } finally {
            try {
                if (jedis != null) {
//                    System.out.println(jedisPool.getNumWaiters()+"----鏈接活躍數:"+jedisPool.getNumActive()+"----空閒連接數:"+jedisPool.getNumIdle());
                    jedis.close();
//                    System.out.println(jedisPool.getNumWaiters()+"----Close後活躍數:"+jedisPool.getNumActive()+"----空閒連接數:"+jedisPool.getNumIdle());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return "";
    }
/**
 * 釋放鎖方法
 * */
    public boolean unlock(String id) {
        Jedis jedis = null;
        String script =
                "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                        "   return redis.call('del',KEYS[1]) " +
                        "else" +
                        "   return 0 " +
                        "end";
        try {
            jedis = jedisPool.getResource();
            String result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();
            return "1".equals(result) ? true : false;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                jedis.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

     測試類:

import com.zwh.JedisDistributed.RedisLock;
import redis.clients.jedis.Jedis;

import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test {
	private static Integer POOL_SIZE=1001;
	private static long AUTO_EXPIRE_TIME = 3000;
	private static int NUM =1000;
    private static long reTryTime = 50000;
	private static LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue();
	static RedisLock redisLock = new RedisLock(POOL_SIZE,AUTO_EXPIRE_TIME,reTryTime);

    public static void getDeamon() {
        Thread deamonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try (Jedis jedis = redisLock.jedisPool.getResource()) {
                    while (true){
                        Thread.sleep(2000);
                        System.out.println("ttl:"+jedis.ttl(redisLock.LOCK_KEY));
                        jedis.expire(redisLock.LOCK_KEY, 3);
//                        System.out.println("----------續期了----------"+Thread.currentThread().getName());
                    }
                } catch (Exception e) {
                    System.out.println("守護線程異常:" + e.toString());
                }
            }
        });
        deamonThread.setDaemon(true);
        deamonThread.start();
    }

    /**
     *  業務處理超過自動過期時間,守護線程爲獲取鎖續期。
     * */
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
        for (int i = 0; i <50; i++) {
            final int finalI = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    String uuid =UUID.randomUUID().toString();
                    String lockResult = redisLock.lock(uuid);   //重試獲取鎖失敗時,請求丟失
                    if ("OK".equals(lockResult)){
                        getDeamon();                            //啓動守護線程,每2s續期3s
                        try {
                            Thread.sleep(2000);  //業務邏輯處理耗時
                            NUM--;
                            System.out.println("-------庫存NUM:"+NUM);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        boolean unlock = redisLock.unlock(uuid);
//                        System.out.println("************unlock:"+unlock+"******"+Thread.currentThread().getName());
                    }else {
                        System.out.println("重試超時:"+lockResult);
                    }
                }
            });
        }
        executor.shutdown();
    }
/**
 *  業務處理超過自動過期時間,不做處理
 * */
    public static void main1(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
        for (int i = 0; i <100; i++) {
//            final int finalI = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    String uuid =UUID.randomUUID().toString();
                    String lockResult = redisLock.lock(uuid);//重試獲取鎖失敗時,請求丟失
                    if ("OK".equals(lockResult)){
                        long start = System.currentTimeMillis();
                        try {
                            Thread.sleep(100);  //業務邏輯處理耗時
                            NUM--;
                            System.out.println("-------庫存NUM:"+NUM);

                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        long end = System.currentTimeMillis();

                        if ((end-start)<=AUTO_EXPIRE_TIME){
                            boolean unlock = redisLock.unlock(uuid);    //成功獲得了鎖並且業務處理爲超時
                        }else{
                            System.out.println("------------------業務超時---------------------");
                        }
                    }else {
                        System.out.println("重試超時:"+lockResult);
                    }
                }
            });
        }
        executor.shutdown();
    }
}

參考資料:
     漫畫:什麼是分佈式鎖?
     Redis—分佈式鎖深入探究
     姍姍來遲的Redis分佈式鎖
代碼地址:
     https://github.com/OooooOz/Redis
//-------------------------------------------------------相互交流--------------------------------------------------//

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