redis分布式鎖的實現總結

在Java中,關于鎖我想大家都很熟悉。在并發編程中,我們通過鎖,來避免由于競爭而造成的數據不一致問題。通常我們以進程鎖synchronized 、Lock來實現它,對于分布式程序,就不能用進程鎖了,這時候常用的是分布式鎖。

什么是分布式鎖

分布式鎖,是一種思想,它的實現方式有很多。比如,我們將沙灘當做分布式鎖的組件,那么它看起來應該是這樣的:

加鎖

在沙灘上踩一腳,留下自己的腳印,就對應了加鎖操作。其他進程或者線程,看到沙灘上已經有腳印,證明鎖已被別人持有,則等待。

解鎖

把腳印從沙灘上抹去,就是解鎖的過程。

鎖超時

為了避免死鎖,我們可以設置一陣風,在單位時間后刮起,將腳印自動抹去。

分布式鎖的實現有很多,比如基于數據庫、memcached、Redis、系統文件、zookeeper等。它們的核心的理念跟上面的過程大致相同。基于數據庫可以用樂觀鎖和悲觀鎖處理分頁式鎖,樂觀鎖使用對比記錄version號來實現,悲觀鎖使用類似“select * where * for update”行鎖實現。

本文討論的是基于redis實現分頁式鎖的問題,別的方面不做詳說,有相關需求可以參考和查閱別的資料。

Redis分布式鎖原理

加鎖

加鎖實際上就是在redis中,給Key鍵設置一個值,為避免死鎖,并給定一個過期時間。

SET lock_key random_value NX PX 5000

值得注意的是:
random_value 是客戶端生成的唯一的字符串。
NX 代表只在鍵不存在時,才對鍵進行設置操作。
PX 5000 設置鍵的過期時間為5000毫秒。

這樣,如果上面的命令執行成功,則證明客戶端獲取到了鎖。

解鎖

解鎖的過程就是將Key鍵刪除。但也不能亂刪,不能說客戶端1的請求將客戶端2的鎖給刪除掉。這時候random_value的作用就體現出來。

為了保證解鎖操作的原子性,我們用LUA腳本完成這一操作。先判斷當前鎖的字符串是否與傳入的值相等,是的話就刪除Key,解鎖成功。

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

jedis實現(單節點)


/**
     * 獲取分布式鎖:一分命令,保證事務的一致性。
     * @param lockKey
     * @param requestId
     * @param expireTime
     * @return
     */
    public static boolean getDistributeLock(String lockKey, String requestId, long expireTime) {
        Jedis jedis = null;
        try {
            jedis = getResource();
            String result = jedis.set(lockKey,requestId,"NX","PX",expireTime);
            if ("OK".equals(result)) {
                return true;
            }
        } catch (Exception e) {
            logger.error("getDistributeLock {}", lockKey, e);
        } finally {
            returnResource(jedis);
        }
        return false;
    }

    /**
     * 釋放分布式鎖:使用lua腳本,一個命令實現對帶有標志的鎖的釋放
     * @param lockKey
     * @param requestId
     * @return
     */
    public static boolean releaseDistributeLock(String lockKey, String requestId) {
        Jedis jedis = null;
        try {
            jedis = getResource();
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            Long RELEASE_SUCCESS = 1L;
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
        } catch (Exception e) {
            logger.error("releaseDistributeLock {}", lockKey, e);
        } finally {
            returnResource(jedis);
        }
        return false;
    }
    

注意:這里的requestId,類似客戶端口請求id,每次請求都是不同的可以使用uuid,測試和使用可以參考后面的”測試和說明“部分。

缺點:在集群包括主從、哨兵模式、集群模式不可用;鎖不具有可重入性。

redisson實現(通用)

Redisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最簡單和最便捷的方法。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。

Redisson底層采用的是Netty 框架。支持Redis 2.8以上版本,支持Java1.6+以上版本。它里面也實現了分布式鎖,而且包含多種類型的鎖:可重入鎖,公平鎖等。

具體實現如下:

JedisUtil提供

 //從配置類中獲取redisson對象
 private static Redisson redisson = JedisConfig.getRedisson();
    
 //加鎖 Redisson:適用單機、主從、哨兵和集群
    //同步方法,等待鎖返回執行 所以涉及鎖使用的,可以放在線程池中進行
    public static boolean acquire(String lockName){
        //聲明key對象
        String key = lockName;
        //獲取鎖對象
        RLock mylock = redisson.getLock(key);
        //加鎖,并且設置鎖過期時間,防止死鎖的產生
        mylock.lock(2, TimeUnit.MINUTES); // 分鐘
        //加鎖成功
        return  true;
    }
    //鎖的釋放 Redisson:適用單機、主從、哨兵和集群
    //同步方法,等待鎖返回執行  所以涉及鎖使用的,可以放在線程池中進行
    public static void release(String lockName){
        //必須是和加鎖時的同一個key
        String key =  lockName;
        //獲取所對象
        RLock mylock = redisson.getLock(key);
        //釋放鎖(解鎖)
        mylock.unlock();
    }

JedisConfig提供


private static Config config = new Config();
//聲明redisso對象
private static Redisson redisson = null;

    static{

        //可以用"redis://"來啟用SSL連接
        if (IS_CLUSTER.equals(CLUSTER_USED)) {//集群
            log.info("Redisson redis lock init cluster config:"+server1+";"+server2+";"+server3+";"+server4+";"+server5+";"+server6);
            config.useClusterServers().addNodeAddress(
                    "redis://".concat(server1),"redis://".concat(server2), "redis://".concat(server3),
                    "redis://".concat(server4),"redis://".concat(server5), "redis://".concat(server6)).setScanInterval(5000);
        } else {//單機
            log.info("Redisson redis lock init single node config:"+server1+";"+server2+";"+server3+";"+server4+";"+server5+";"+server6);
            config.useSingleServer().setAddress("redis://".concat(poolHost).concat(":").concat(poolPort));
        }

        //得到redisson對象
        redisson = (Redisson) Redisson.create(config);
    }
    
    /**
     * Redisson redis分布式鎖處理對象
    * @return
    */
    public static Redisson getRedisson() {
        return redisson;
    }
    

測試和說明

測試和使用,可以參考下面的junit測試用例。

@Slf4j
public class JedisUtilTest extends SpringTxTestCase {
    private static Logger logger = LoggerFactory.getLogger(JedisUtils.class);

    /**
     * 單機版本:加解鎖功能
     */
    @Test
    public void testSingleRedisLockAndUnlock(){
        JedisUtils.getDistributeLock("lockKey","requestId",JedisConfig.JEDIS_EXPIRE);
        {
            for (int i = 0; i < 5; i++) {
                boolean result = JedisUtils.getDistributeLock("lockKey","requestId",JedisConfig.JEDIS_EXPIRE);
                System.out.println(Thread.currentThread().getName()+":lock result:"+result);
                JedisUtils.releaseDistributeLock("lockKey","requestId");
                boolean result1 = JedisUtils.getDistributeLock("lockKey","requestId",JedisConfig.JEDIS_EXPIRE);
                System.out.println(Thread.currentThread().getName()+":unlock result1:"+result1);
            }
        }
    }
    /**
     * 單機版本:鎖測試
     */
    @Test
    public void testSingleRedisLock(){
        {
            final CyclicBarrier cbRef = new CyclicBarrier(10);
            final ReentrantLock reentrantLock=new ReentrantLock();
            for(int i=0;i<10;i++){
                Thread t=  new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            System.out.println(Thread.currentThread().getName() + "準備");
                            cbRef.await();//10個線程等待在這里 才開始執行下面的
                            //reentrantLock.lock();
                            //tryGetDistributedLock("hello","hello",10000);
                            boolean result = JedisUtils.getDistributeLock("lockKey","requestId",JedisConfig.JEDIS_EXPIRE);
                            System.out.println(Thread.currentThread().getName()+"===lock result:"+result);
                            JedisUtils.releaseDistributeLock("lockKey",UUID.randomUUID().toString());
                            boolean result1 = JedisUtils.getDistributeLock("lockKey","requestId",JedisConfig.JEDIS_EXPIRE);
                            System.out.println(Thread.currentThread().getName()+"===lock result1:"+result);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }finally {
                            //reentrantLock.unlock();
                        }
                    }
                });
                t.start();
            }
            //這一段可以不要
            try {
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "起跑");
                System.out.println( cbRef.getParties()+"--" +cbRef.getNumberWaiting());
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 單機版本redis:測試分布式鎖的使用方法
     */
    @Test
    public void testUseOfSingleRedisLock() throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(10);
        String data2Deal = "data to deal";
        final CyclicBarrier cbRef = new CyclicBarrier(10);
        for(int i=0;i<10;i++){
            Thread t=  new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "準備");
                    try {
                        cbRef.await();//10個線程等待在這里 才開始執行下面的+
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    final ReentrantLock reentrantLock=new ReentrantLock();
                    reentrantLock.lock();
                    try {
                        String data2Deal = "data to deal:" + Thread.currentThread().getName();
                        useOfSingleRedisLock(data2Deal);
                    } catch (Exception e){
                        e.printStackTrace();
                    } finally {
                        reentrantLock.unlock();
                    }
                    countDownLatch.countDown();
                }
            });
            t.start();
        }
        countDownLatch.await();
        System.out.println("所有線程都執行完了……");
    }

    /**
     * 分布式鎖的使用方法:單機redis cluster包括(集群和哨兵)不適用。
     * @param data2Deal
     */
    public void useOfSingleRedisLock(String data2Deal){
        String requestId = UUID.randomUUID().toString();
        if(JedisPoolUtils.getDistributeLock("lock_key", requestId, 1000*60*5)){
            try {
                methonNeedDisLock(data2Deal);
            } catch (Exception e) {
                logger.error("分布式鎖業務處理失?。?,e);
                e.printStackTrace();
            } finally {
                JedisPoolUtils.releaseDistributeLock("lock_key",requestId);
            }
        } else {
            try {
                Thread.sleep(1000);
                useOfSingleRedisLock(data2Deal);
            } catch (InterruptedException e) {
                logger.error(e.getMessage());
            }
        }
    }

    /*
    *  需要分布式鎖的業務代碼
    */
    public void methonNeedDisLock(String data2Deal){
        System.out.println("分布式鎖業務處理方法:"+data2Deal);
    }
    /**
     * 測試分布式鎖(Redisson)的使用方法:redis單機和哨兵、集群都適用
     * 測試說明:開啟1000個線程,對count進行累加
     */

    int count = 0;
    @Test
    public void testRedisLock() throws InterruptedException {
        int clientcount =1000;
        final CountDownLatch countDownLatch = new CountDownLatch(clientcount);
        ExecutorService executorService = Executors.newFixedThreadPool(clientcount);
        long start = System.currentTimeMillis();
        for (int i = 0;i<clientcount;i++){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    //通過Snowflake算法獲取唯一的ID字符串
                    String id = UUID.randomUUID().toString();
                    System.out.println("request id:"+id);
                    try {
                        JedisPoolUtils.getDistributeLock("lock_key", id, 1000*60*5);
                        count++;
                    }finally {
                        JedisPoolUtils.releaseDistributeLock("lock_key",id);
                    }
                    countDownLatch.countDown();
                }
            });
        }
        //控制程序結束
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("執行線程數:{"+clientcount+"},總耗時:{"+(end-start)+"},count數為:{"+count+"}");
        //logger.info("執行線程數:{},總耗時:{},count數為:{}",clientcount,end-start,count);
    }

    @Test
    public void testRedissonLock() throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        System.out.println("測試程序:開始運行……");
        Thread t1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                String key = "test123";
                System.out.println(Thread.currentThread().getName()+": t1 start");
                //加鎖
                boolean result = JedisUtils.acquire(key);
                System.out.println(Thread.currentThread().getName()+": t1 get lock time :"+new Date().toLocaleString());
                System.out.println(Thread.currentThread().getName()+": t1 get lock:"+result);
                Thread.sleep(50000);
                System.out.println(Thread.currentThread().getName()+":t1 sleep 50000.");
                System.out.println(Thread.currentThread().getName()+":t1 修改了數據庫");
                //釋放鎖
                JedisUtils.release(key);
                System.out.println(Thread.currentThread().getName()+" t1 lock release time:"+new Date().toLocaleString());
                countDownLatch.countDown();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String key = "test123";
                System.out.println(Thread.currentThread().getName()+": t2 start");
                //加鎖
                boolean result = JedisUtils.acquire(key);
                System.out.println(Thread.currentThread().getName()+": t2 get lock time :"+new Date().toLocaleString());
                System.out.println(Thread.currentThread().getName()+": t2 get lock:"+result);
                System.out.println(Thread.currentThread().getName()+":t2 修改了數據庫");
                //釋放鎖
                JedisUtils.release(key);
                System.out.println(Thread.currentThread().getName()+" t2 lock release time:"+new Date().toLocaleString());
                countDownLatch.countDown();
            }
        });
        t2.start();
        countDownLatch.await();
        System.out.println("測試程序:完成運行。");
    }

    @Test
    public void testRedissonLockSyn() throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        System.out.println("測試程序:開始運行……");
        Thread t1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                String key = "test123";
                System.out.println(Thread.currentThread().getName()+": t1 start");
                System.out.println(Thread.currentThread().getName()+": t1 sleep 50000.");
                Thread.sleep(50000);
                System.out.println(Thread.currentThread().getName()+": t1 ready for lock.");
                //加鎖
                boolean result = JedisUtils.acquire(key);
                System.out.println(Thread.currentThread().getName()+": t1 get lock time :"+new Date().toLocaleString());
                System.out.println(Thread.currentThread().getName()+": t1 get lock:"+result);
                System.out.println(Thread.currentThread().getName()+":t1 修改了數據庫");
                //釋放鎖
                JedisUtils.release(key);
                System.out.println(Thread.currentThread().getName()+" t1 lock release time:"+new Date().toLocaleString());
                countDownLatch.countDown();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                String key = "test123";
                System.out.println(Thread.currentThread().getName()+": t2 start");
                boolean result = JedisUtils.acquire(key);
                System.out.println(Thread.currentThread().getName()+": t2 get lock time :"+new Date().toLocaleString());
                System.out.println(Thread.currentThread().getName()+": t2 get lock:"+result);
                System.out.println(Thread.currentThread().getName()+": t2 sleep 500000.");
                Thread.sleep(500000);
                //釋放鎖
                JedisUtils.release(key);
                System.out.println(Thread.currentThread().getName()+" t2 lock release time:"+new Date().toLocaleString());
                countDownLatch.countDown();
            }
        });
        t2.start();
        countDownLatch.await();
        System.out.println("測試程序:完成運行。");
    }

    @Test
    public void testUseOfRedissonLock() throws InterruptedException {
        int clientcount = 1000;
        final CountDownLatch countDownLatch = new CountDownLatch(clientcount);
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        long start = System.currentTimeMillis();
        for (int i = 0;i<clientcount;i++){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    JedisUtils.acquire("lockName");
                    count++;
                    JedisUtils.release("lockName");
                    countDownLatch.countDown();
                }
            });
        }
        //控制程序結束
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("執行線程任務總數:{"+clientcount+"},總耗時:{"+(end-start)+"},count數為:{"+count+"}");
    }

}

參考

https://blog.csdn.net/u014353343/article/details/88921212

http://www.lxweimin.com/p/828aa3b44564

http://www.lxweimin.com/p/47fd7f86c848

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。