1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        最強分布式鎖工具:Redisson

        共 44516字,需瀏覽 90分鐘

         ·

        2022-08-03 20:14

        點擊關注公眾號,Java干貨及時送達

                

        一、Redisson概述

        什么是Redisson?

        Redisson是一個在Redis的基礎上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(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),從而讓使用者能夠?qū)⒕Ω械胤旁谔幚順I(yè)務邏輯上。

        一個基于Redis實現(xiàn)的分布式工具,有基本分布式對象和高級又抽象的分布式服務,為每個試圖再造分布式輪子的程序員帶來了大部分分布式問題的解決辦法。

        Redisson和Jedis、Lettuce有什么區(qū)別?倒也不是雷鋒和雷鋒塔

        Redisson和它倆的區(qū)別就像一個用鼠標操作圖形化界面,一個用命令行操作文件。Redisson是更高層的抽象,Jedis和Lettuce是Redis命令的封裝。

        • Jedis是Redis官方推出的用于通過Java連接Redis客戶端的一個工具包,提供了Redis的各種命令支持

        • Lettuce是一種可擴展的線程安全的 Redis 客戶端,通訊框架基于Netty,支持高級的 Redis 特性,比如哨兵,集群,管道,自動重新連接和Redis數(shù)據(jù)模型。Spring Boot 2.x 開始 Lettuce 已取代 Jedis 成為首選 Redis 的客戶端。

        • Redisson是架設在Redis基礎上,通訊基于Netty的綜合的、新型的中間件,企業(yè)級開發(fā)中使用Redis的最佳范本

        Jedis把Redis命令封裝好,Lettuce則進一步有了更豐富的Api,也支持集群等模式。但是兩者也都點到為止,只給了你操作Redis數(shù)據(jù)庫的腳手架,而Redisson則是基于Redis、Lua和Netty建立起了成熟的分布式解決方案,甚至redis官方都推薦的一種工具集。

        二、分布式鎖

        分布式鎖怎么實現(xiàn)?

        分布式鎖是并發(fā)業(yè)務下的剛需,雖然實現(xiàn)五花八門:ZooKeeper有Znode順序節(jié)點,數(shù)據(jù)庫有表級鎖和樂/悲觀鎖,Redis有setNx,但是殊途同歸,最終還是要回到互斥上來,本篇介紹Redisson,那就以redis為例。

        怎么寫一個簡單的Redis分布式鎖?

        以Spring Data Redis為例,用RedisTemplate來操作Redis(setIfAbsent已經(jīng)是setNx + expire的合并命令),如下

        // 加鎖
        public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
            return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
        }
        // 解鎖,防止刪錯別人的鎖,以uuid為value校驗是否自己的鎖
        public void unlock(String lockName, String uuid) {
            if(uuid.equals(redisTemplate.opsForValue().get(lockName)){        redisTemplate.opsForValue().del(lockName);    }
        }

        // 結(jié)構
        if(tryLock){
            // todo
        }finally{
            unlock;
        }

        簡單1.0版本完成,聰明的小張一眼看出,這是鎖沒錯,但get和del操作非原子性,并發(fā)一旦大了,無法保證進程安全。于是小張?zhí)嶙h,用Lua腳本

        Lua腳本是什么?

        Lua腳本是redis已經(jīng)內(nèi)置的一種輕量小巧語言,其執(zhí)行是通過redis的eval/evalsha命令來運行,把操作封裝成一個Lua腳本,如論如何都是一次執(zhí)行的原子操作。

        于是2.0版本通過Lua腳本刪除

        lockDel.lua如下

        if redis.call('get', KEYS[1]) == ARGV[1] 
            then 
         -- 執(zhí)行刪除操作
                return redis.call('del', KEYS[1]) 
            else 
         -- 不成功,返回0
                return 0 
        end

        delete操作時執(zhí)行Lua命令

        // 解鎖腳本
        DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
        unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua")));

        // 執(zhí)行l(wèi)ua腳本解鎖
        redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);

        2.0似乎更像一把鎖,但好像又缺少了什么,小張一拍腦袋,synchronized和ReentrantLock都很絲滑,因為他們都是可重入鎖,一個線程多次拿鎖也不會死鎖,我們需要可重入。

        怎么保證可重入?

        重入就是,同一個線程多次獲取同一把鎖是允許的,不會造成死鎖,這一點synchronized偏向鎖提供了很好的思路,synchronized的實現(xiàn)重入是在JVM層面,JAVA對象頭MARK WORD中便藏有線程ID和計數(shù)器來對當前線程做重入判斷,避免每次CAS。

        當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖標志是否設置成1:沒有則CAS競爭;設置了,則CAS將對象頭偏向鎖指向當前線程。

        再維護一個計數(shù)器,同個線程進入則自增1,離開再減1,直到為0才能釋放

        可重入鎖

        仿造該方案,我們需改造Lua腳本:

        1.需要存儲 鎖名稱lockName、獲得該鎖的線程id和對應線程的進入次數(shù)count

        2.加鎖

        每次線程獲取鎖時,判斷是否已存在該鎖

        • 不存在

        • 設置hash的key為線程id,value初始化為1

        • 設置過期時間

        • 返回獲取鎖成功true

        • 存在

        • 繼續(xù)判斷是否存在當前線程id的hash key

        • 存在,線程key的value + 1,重入次數(shù)增加1,設置過期時間

        • 不存在,返回加鎖失敗

        3.解鎖

        每次線程來解鎖時,判斷是否已存在該鎖

        • 存在

        • 是否有該線程的id的hash key,有則減1,無則返回解鎖失敗

        • 減1后,判斷剩余count是否為0,為0則說明不再需要這把鎖,執(zhí)行del命令刪除

        1.存儲結(jié)構

        為了方便維護這個對象,我們用Hash結(jié)構來存儲這些字段。Redis的Hash類似Java的HashMap,適合存儲對象。

        hset lockname1 threadId 1

        設置一個名字為lockname1的hash結(jié)構,該hash結(jié)構key為threadId,值value為1

        hget lockname1 threadId

        獲取lockname1的threadId的值

        存儲結(jié)構為

        lockname 鎖名稱
            key1:   threadId   唯一鍵,線程id
            value1:  count     計數(shù)器,記錄該線程獲取鎖的次數(shù)

        redis中的結(jié)構

        2.計數(shù)器的加減

        當同一個線程獲取同一把鎖時,我們需要對對應線程的計數(shù)器count做加減

        判斷一個redis key是否存在,可以用exists,而判斷一個hash的key是否存在,可以用hexists

        而redis也有hash自增的命令hincrby

        每次自增1時 hincrby lockname1 threadId 1,自減1時 hincrby lockname1 threadId -1

        3.解鎖的判斷

        當一把鎖不再被需要了,每次解鎖一次,count減1,直到為0時,執(zhí)行刪除

        綜合上述的存儲結(jié)構和判斷流程,加鎖和解鎖Lua如下

        加鎖 lock.lua

        local key = KEYS[1];
        local threadId = ARGV[1];
        local releaseTime = ARGV[2];

        -- lockname不存在
        if(redis.call('exists', key) == 0) then
            redis.call('hset', key, threadId, '1');
            redis.call('expire', key, releaseTime);
            return 1;
        end;

        -- 當前線程已id存在
        if(redis.call('hexists', key, threadId) == 1) then
            redis.call('hincrby', key, threadId, '1');
            redis.call('expire', key, releaseTime);
            return 1;
        end;
        return 0;

        解鎖 unlock.lua

        local key = KEYS[1];
        local threadId = ARGV[1];

        -- lockname、threadId不存在
        if (redis.call('hexists', key, threadId) == 0) then
            return nil;
        end;

        -- 計數(shù)器-1
        local count = redis.call('hincrby', key, threadId, -1);

        -- 刪除lock
        if (count == 0) then
            redis.call('del', key);
            return nil;
        end;

        代碼

        /**
         * @description 原生redis實現(xiàn)分布式鎖
         **/

        @Getter
        @Setter
        public class RedisLock {

            private RedisTemplate redisTemplate;
            private DefaultRedisScript<Long> lockScript;
            private DefaultRedisScript<Object> unlockScript;

            public RedisLock(RedisTemplate redisTemplate) {
                this.redisTemplate = redisTemplate;
                // 加載加鎖的腳本
                lockScript = new DefaultRedisScript<>();
                this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
                this.lockScript.setResultType(Long.class);
                // 加載釋放鎖的腳本
                unlockScript = new DefaultRedisScript<>();
                this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
            }

            /**
             * 獲取鎖
             */

            public String tryLock(String lockName, long releaseTime) {
                // 存入的線程信息的前綴
                String key = UUID.randomUUID().toString();

                // 執(zhí)行腳本
                Long result = (Long) redisTemplate.execute(
                        lockScript,
                        Collections.singletonList(lockName),
                        key + Thread.currentThread().getId(),
                        releaseTime);

                if (result != null && result.intValue() == 1) {
                    return key;
                } else {
                    return null;
                }
            }

            /**
             * 解鎖
             * @param lockName
             * @param key
             */

            public void unlock(String lockName, String key) {
                redisTemplate.execute(unlockScript,
                        Collections.singletonList(lockName),
                        key + Thread.currentThread().getId()
                        );
            }
        }

        至此已經(jīng)完成了一把分布式鎖,符合互斥、可重入、防死鎖的基本特點。

        嚴謹?shù)男堄X得雖然當個普通互斥鎖,已經(jīng)穩(wěn)穩(wěn)夠用,可是業(yè)務里總是又很多特殊情況的,比如A進程在獲取到鎖的時候,因業(yè)務操作時間太長,鎖釋放了但是業(yè)務還在執(zhí)行,而此刻B進程又可以正常拿到鎖做業(yè)務操作,兩個進程操作就會存在依舊有共享資源的問題。

        而且如果負責儲存這個分布式鎖的Redis節(jié)點宕機以后,而且這個鎖正好處于鎖住的狀態(tài)時,這個鎖會出現(xiàn)鎖死的狀態(tài)。

        小張不是杠精,因為庫存操作總有這樣那樣的特殊。

        所以我們希望在這種情況時,可以延長鎖的releaseTime延遲釋放鎖來直到完成業(yè)務期望結(jié)果,這種不斷延長鎖過期時間來保證業(yè)務執(zhí)行完成的操作就是鎖續(xù)約。

        讀寫分離也是常見,一個讀多寫少的業(yè)務為了性能,常常是有讀鎖和寫鎖的。

        而此刻的擴展已經(jīng)超出了一把簡單輪子的復雜程度,光是處理續(xù)約,就夠小張喝一壺,何況在性能(鎖的最大等待時間)、優(yōu)雅(無效鎖申請)、重試(失敗重試機制)等方面還要下功夫研究。

        在小張苦思冥想時,旁邊的小白湊過來看了看小張,很好奇,都2021年了,為什么不直接用redisson呢?

        Redisson就有這把你要的鎖。

        三、Redisson分布式鎖

        號稱簡單的Redisson分布式鎖的使用姿勢是什么?

        1.依賴

        <!-- 原生,本章使用-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

        <!-- 另一種Spring集成starter,本章未使用 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.6</version>
        </dependency>

        2.配置

        @Configuration
        public class RedissionConfig {
            @Value("${spring.redis.host}")
            private String redisHost;

            @Value("${spring.redis.password}")
            private String password;

            private int port = 6379;

            @Bean
            public RedissonClient getRedisson() {
                Config config = new Config();
                config.useSingleServer().
                        setAddress("redis://" + redisHost + ":" + port).
                        setPassword(password);
                config.setCodec(new JsonJacksonCodec());
                return Redisson.create(config);
            }
        }

        3.啟用分布式鎖

        @Resource
        private RedissonClient redissonClient;

        RLock rLock = redissonClient.getLock(lockName);
        try {
            boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS);
            if (isLocked) {
                // TODO
                        }
            } catch (Exception e) {
                    rLock.unlock();
            }

        簡潔明了,只需要一個RLock,既然推薦Redisson,就往里面看看他是怎么實現(xiàn)的。

        四、RLock

        RLock是Redisson分布式鎖的最核心接口,繼承了concurrent包的Lock接口和自己的RLockAsync接口,RLockAsync的返回值都是RFuture,是Redisson執(zhí)行異步實現(xiàn)的核心邏輯,也是Netty發(fā)揮的主要陣地。

        RLock如何加鎖?

        從RLock進入,找到RedissonLock類,找到tryLock方法再遞進到干事的tryAcquireOnceAsync方法,這是加鎖的主要代碼(版本不一此處實現(xiàn)有差別,和最新3.15.x有一定出入,但是核心邏輯依然未變。此處以3.13.6為例)

        private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
                if (leaseTime != -1L) {
                    return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
                } else {
                    RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
                    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                        if (e == null) {
                            if (ttlRemaining) {
                                this.scheduleExpirationRenewal(threadId);
                            }

                        }
                    });
                    return ttlRemainingFuture;
                }
            }

        此處出現(xiàn)leaseTime時間判斷的2個分支,實際上就是加鎖時是否設置過期時間,未設置過期時間(-1)時則會有watchDog鎖續(xù)約(下文),一個注冊了加鎖事件的續(xù)約任務。我們先來看有過期時間tryLockInnerAsync部分,

        evalWriteAsync是eval命令執(zhí)行l(wèi)ua的入口

        <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
                this.internalLockLeaseTime = unit.toMillis(leaseTime);
                return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
            }

        這里揭開真面目,eval命令執(zhí)行Lua腳本的地方,此處的Lua腳本展開

        -- 不存在該key時
        if (redis.call('exists', KEYS[1]) == 0then 
          -- 新增該鎖并且hash中該線程id對應的count置1
          redis.call('hincrby', KEYS[1], ARGV[2], 1); 
          -- 設置過期時間
          redis.call('pexpire', KEYS[1], ARGV[1]); 
          return nil
        end

        -- 存在該key 并且 hash中線程id的key也存在
        if (redis.call('hexists', KEYS[1], ARGV[2]) == 1then 
          -- 線程重入次數(shù)++
          redis.call('hincrby', KEYS[1], ARGV[2], 1); 
          redis.call('pexpire', KEYS[1], ARGV[1]); 
          return nil
        end
        return redis.call('pttl', KEYS[1]);

        和前面我們寫自定義的分布式鎖的腳本幾乎一致,看來redisson也是一樣的實現(xiàn),具體參數(shù)分析:

        // keyName
        KEYS[1] = Collections.singletonList(this.getName())
        // leaseTime
        ARGV[1] = this.internalLockLeaseTime
        // uuid+threadId組合的唯一值
        ARGV[2] = this.getLockName(threadId)

        總共3個參數(shù)完成了一段邏輯:

        判斷該鎖是否已經(jīng)有對應hash表存在,

        ? 沒有對應的hash表:則set該hash表中一個entry的key為鎖名稱,value為1,之后設置該hash表失效時間為leaseTime

        ? 存在對應的hash表:則將該lockName的value執(zhí)行+1操作,也就是計算進入次數(shù),再設置失效時間leaseTime

        ? 最后返回這把鎖的ttl剩余時間

        也和上述自定義鎖沒有區(qū)別

        既然如此,那解鎖的步驟也肯定有對應的-1操作,再看unlock方法,同樣查找方法名,一路到

        protected RFuture<Boolean> unlockInnerAsync(long threadId) {
                return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
            }

        掏出Lua部分

        -- 不存在key
        if (redis.call('hexists', KEYS[1], ARGV[3]) == 0then 
          return nil;
        end;
        -- 計數(shù)器 -1
        local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
        if (counter > 0then 
          -- 過期時間重設
          redis.call('pexpire', KEYS[1], ARGV[2]); 
          return 0
        else
          -- 刪除并發(fā)布解鎖消息
          redis.call('del', KEYS[1]); 
          redis.call('publish', KEYS[2], ARGV[1]); 
          return 1;
        end
        return nil;

        該Lua KEYS有2個Arrays.asList(getName(), getChannelName())

        name 鎖名稱
        channelName,用于pubSub發(fā)布消息的channel名稱

        ARGV變量有三個LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)

        LockPubSub.UNLOCK_MESSAGE,channel發(fā)送消息的類別,此處解鎖為0
        internalLockLeaseTime,watchDog配置的超時時間,默認為30s
        lockName 這里的lockName指的是uuid和threadId組合的唯一值

        步驟如下:

        1.如果該鎖不存在則返回nil;

        2.如果該鎖存在則將其線程的hash key計數(shù)器-1,

        3.計數(shù)器counter>0,重置下失效時間,返回0;否則,刪除該鎖,發(fā)布解鎖消息unlockMessage,返回1;

        其中unLock的時候使用到了Redis發(fā)布訂閱PubSub完成消息通知。

        而訂閱的步驟就在RedissonLock的加鎖入口的lock方法里

        long threadId = Thread.currentThread().getId();
                Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
                if (ttl != null) {
                    // 訂閱
                    RFuture<RedissonLockEntry> future = this.subscribe(threadId);
                    if (interruptibly) {
                        this.commandExecutor.syncSubscriptionInterrupted(future);
                    } else {
                        this.commandExecutor.syncSubscription(future);
                    }
                    // 省略

        當鎖被其他線程占用時,通過監(jiān)聽鎖的釋放通知(在其他線程通過RedissonLock釋放鎖時,會通過發(fā)布訂閱pub/sub功能發(fā)起通知),等待鎖被其他線程釋放,也是為了避免自旋的一種常用效率手段。

        1.解鎖消息

        為了一探究竟通知了什么,通知后又做了什么,進入LockPubSub。

        這里只有一個明顯的監(jiān)聽方法onMessage,其訂閱和信號量的釋放都在父類PublishSubscribe,我們只關注監(jiān)聽事件的實際操作

        protected void onMessage(RedissonLockEntry value, Long message) {
                Runnable runnableToExecute;
                if (message.equals(unlockMessage)) {
                    // 從監(jiān)聽器隊列取監(jiān)聽線程執(zhí)行監(jiān)聽回調(diào)
                    runnableToExecute = (Runnable)value.getListeners().poll();
                    if (runnableToExecute != null) {
                        runnableToExecute.run();
                    }
                    // getLatch()返回的是Semaphore,信號量,此處是釋放信號量
                    // 釋放信號量后會喚醒等待的entry.getLatch().tryAcquire去再次嘗試申請鎖
                    value.getLatch().release();
                } else if (message.equals(readUnlockMessage)) {
                    while(true) {
                        runnableToExecute = (Runnable)value.getListeners().poll();
                        if (runnableToExecute == null) {
                            value.getLatch().release(value.getLatch().getQueueLength());
                            break;
                        }
                        runnableToExecute.run();
                    }
                }
            }

        發(fā)現(xiàn)一個是默認解鎖消息,一個是讀鎖解鎖消息,因為redisson是有提供讀寫鎖的,而讀寫鎖讀讀情況和讀寫、寫寫情況互斥情況不同,我們只看上面的默認解鎖消息unlockMessage分支

        LockPubSub監(jiān)聽最終執(zhí)行了2件事

        1. runnableToExecute.run() 執(zhí)行監(jiān)聽回調(diào)

        2. value.getLatch().release(); 釋放信號量

        Redisson通過LockPubSub監(jiān)聽解鎖消息,執(zhí)行監(jiān)聽回調(diào)和釋放信號量通知等待線程可以重新?lián)屾i。

        這時再回來看tryAcquireOnceAsync另一分支

        private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
                if (leaseTime != -1L) {
                    return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
                } else {
                    RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
                    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                        if (e == null) {
                            if (ttlRemaining) {
                                this.scheduleExpirationRenewal(threadId);
                            }

                        }
                    });
                    return ttlRemainingFuture;
                }
            }

        可以看到,無超時時間時,在執(zhí)行加鎖操作后,還執(zhí)行了一段費解的邏輯

        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                        if (e == null) {
                            if (ttlRemaining) {
                                this.scheduleExpirationRenewal(threadId);
                            }

                        }
                    })                   }                 }             }) 復制代碼

        此處涉及到Netty的Future/Promise-Listener模型,Redisson中幾乎全部以這種方式通信(所以說Redisson是基于Netty通信機制實現(xiàn)的),理解這段邏輯可以試著先理解

        在 Java 的 Future 中,業(yè)務邏輯為一個 Callable 或 Runnable 實現(xiàn)類,該類的 call()或 run()執(zhí)行完畢意味著業(yè)務邏輯的完結(jié),在 Promise 機制中,可以在業(yè)務邏輯中人工設置業(yè)務邏輯的成功與失敗,這樣更加方便的監(jiān)控自己的業(yè)務邏輯。

        這塊代碼的表面意義就是,在執(zhí)行異步加鎖的操作后,加鎖成功則根據(jù)加鎖完成返回的ttl是否過期來確認是否執(zhí)行一段定時任務。

        這段定時任務的就是watchDog的核心。

        2.鎖續(xù)約

        查看RedissonLock.this.scheduleExpirationRenewal(threadId)

        private void scheduleExpirationRenewal(long threadId) {
                RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
                RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
                if (oldEntry != null) {
                    oldEntry.addThreadId(threadId);
                } else {
                    entry.addThreadId(threadId);
                    this.renewExpiration();
                }

            }

        private void renewExpiration() {
                RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
                if (ee != null) {
                    Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                        public void run(Timeout timeout) throws Exception {
                            RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                            if (ent != null) {
                                Long threadId = ent.getFirstThreadId();
                                if (threadId != null) {
                                    RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                                    future.onComplete((res, e) -> {
                                        if (e != null) {
                                            RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                        } else {
                                            if (res) {
                                                RedissonLock.this.renewExpiration();
                                            }

                                        }
                                    });
                                }
                            }
                        }
                    }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
                    ee.setTimeout(task);
                }
            }

        拆分來看,這段連續(xù)嵌套且冗長的代碼實際上做了幾步

        ? 添加一個netty的Timeout回調(diào)任務,每(internalLockLeaseTime / 3)毫秒執(zhí)行一次,執(zhí)行的方法是renewExpirationAsync

        renewExpirationAsync重置了鎖超時時間,又注冊一個監(jiān)聽器,監(jiān)聽回調(diào)又執(zhí)行了renewExpiration

        renewExpirationAsync 的Lua如下

        protected RFuture<Boolean> renewExpirationAsync(long threadId) {
                return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
            }

        if (redis.call('hexists', KEYS[1], ARGV[2]) == 1then 
          redis.call('pexpire', KEYS[1], ARGV[1]); 
          return 1
        end
        return 0;

        重新設置了超時時間。

        Redisson加這段邏輯的目的是什么?

        目的是為了某種場景下保證業(yè)務不影響,如任務執(zhí)行超時但未結(jié)束,鎖已經(jīng)釋放的問題。

        當一個線程持有了一把鎖,由于并未設置超時時間leaseTime,Redisson默認配置了30S,開啟watchDog,每10S對該鎖進行一次續(xù)約,維持30S的超時時間,直到任務完成再刪除鎖。

        這就是Redisson的鎖續(xù)約,也就是WatchDog實現(xiàn)的基本思路。

        3.流程概括

        通過整體的介紹,流程簡單概括:

        1. A、B線程爭搶一把鎖,A獲取到后,B阻塞

        2. B線程阻塞時并非主動CAS,而是PubSub方式訂閱該鎖的廣播消息

        3. A操作完成釋放了鎖,B線程收到訂閱消息通知

        4. B被喚醒開始繼續(xù)搶鎖,拿到鎖

        詳細加鎖解鎖流程總結(jié)如下圖:

        五、公平鎖

        以上介紹的可重入鎖是非公平鎖,Redisson還基于Redis的隊列(List)和ZSet實現(xiàn)了公平鎖

        公平的定義是什么?

        公平就是按照客戶端的請求先來后到排隊來獲取鎖,先到先得,也就是FIFO,所以隊列和容器順序編排必不可少

        FairSync

        回顧JUC的ReentrantLock公平鎖的實現(xiàn)

        /**
         * Sync object for fair locks
         */

        static final class FairSync extends Sync {
            private static final long serialVersionUID = -3000897897090466540L;

            final void lock() {
                acquire(1);
            }

            /**
             * Fair version of tryAcquire.  Don't grant access unless
             * recursive call or no waiters or is first.
             */

            protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
        }

        AQS已經(jīng)提供了整個實現(xiàn),是否公平取決于實現(xiàn)類取出節(jié)點邏輯是否順序取

        AbstractQueuedSynchronizer是用來構建鎖或者其他同步組件的基礎框架,通過內(nèi)置FIFO隊列來完成資源獲取線程的排隊工作,他自身沒有實現(xiàn)同步接口,僅僅定義了若干同步狀態(tài)獲取和釋放的方法來供自定義同步組件使用(上圖),支持獨占和共享獲取,這是基于模版方法模式的一種設計,給公平/非公平提供了土壤。

        我們用2張圖來簡單解釋AQS的等待流程(出自《JAVA并發(fā)編程的藝術》)

        一張是同步隊列(FIFO雙向隊列)管理 獲取同步狀態(tài)失?。〒屾i失?。┑木€程引用、等待狀態(tài)和前驅(qū)后繼節(jié)點的流程圖

        一張是獨占式獲取同步狀態(tài)的總流程,核心acquire(int arg)方法調(diào)用流程

        可以看出鎖的獲取流程

        AQS維護一個同步隊列,獲取狀態(tài)失敗的線程都會加入到隊列中進行自旋,移出隊列或停止自旋的條件是前驅(qū)節(jié)點為頭節(jié)點切成功獲取了同步狀態(tài)。

        而比較另一段非公平鎖類NonfairSync可以發(fā)現(xiàn),控制公平和非公平的關鍵代碼,在于hasQueuedPredecessors方法。

        static final class NonfairSync extends Sync {
            private static final long serialVersionUID = 7316153563782823691L;

            /**
             * Performs lock.  Try immediate barge, backing up to normal
             * acquire on failure.
             */

            final void lock() {
                if (compareAndSetState(01))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }

            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
        }

        NonfairSync減少了了hasQueuedPredecessors判斷條件,該方法的作用就是

        查看同步隊列中當前節(jié)點是否有前驅(qū)節(jié)點,如果有比當前線程更早請求獲取鎖則返回true。

        保證每次都取隊列的第一個節(jié)點(線程)來獲取鎖,這就是公平規(guī)則

        為什么JUC以默認非公平鎖呢?

        因為當一個線程請求鎖時,只要獲取來同步狀態(tài)即成功獲取。在此前提下,剛釋放的線程再次獲取同步狀態(tài)的幾率會非常大,使得其他線程只能在同步隊列中等待。但這樣帶來的好處是,非公平鎖大大減少了系統(tǒng)線程上下文的切換開銷。

        可見公平的代價是性能與吞吐量。

        Redis里沒有AQS,但是有List和zSet,看看Redisson是怎么實現(xiàn)公平的。

        RedissonFairLock

        RedissonFairLock 用法依然很簡單

        RLock fairLock = redissonClient.getFairLock(lockName);

        fairLock.lock();

        RedissonFairLock繼承自RedissonLock,同樣一路向下找到加鎖實現(xiàn)方法tryLockInnerAsync。

        這里有2段冗長的Lua,但是Debug發(fā)現(xiàn),公平鎖的入口在 command == RedisCommands.EVAL_LONG 之后,此段Lua較長,參數(shù)也多,我們著重分析Lua的實現(xiàn)規(guī)則

        參數(shù)

        -- lua中的幾個參數(shù)
        KEYS = Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName)
        KEYS[1]: lock_name, 鎖名稱                   
        KEYS[2]: "redisson_lock_queue:{xxx}"  線程隊列
        KEYS[3]: "redisson_lock_timeout:{xxx}"  線程id對應的超時集合

        ARGV =  internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime
        ARGV[1]: "{leaseTime}" 過期時間
        ARGV[2]: "{Redisson.UUID}:{threadId}"   
        ARGV[3] = 當前時間 + 線程等待時間:(10:00:00) + 5000毫秒 = 10:00:05
        ARGV[4] = 當前時間(10:00:00)  部署服務器時間,非redis-server服務器時間

        公平鎖實現(xiàn)的Lua腳本

        -- 1.死循環(huán)清除過期key
        while true do 
          -- 獲取頭節(jié)點
            local firstThreadId2 = redis.call('lindex', KEYS[2], 0);
            -- 首次獲取必空跳出循環(huán)
          if firstThreadId2 == false then 
            break;
          end;
          -- 清除過期key
          local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));
          if timeout <= tonumber(ARGV[4]) then
            redis.call('zrem', KEYS[3], firstThreadId2);
            redis.call('lpop', KEYS[2]);
          else
            break;
          end;
        end;

        -- 2.不存在該鎖 && (不存在線程等待隊列 || 存在線程等待隊列而且第一個節(jié)點就是此線程ID),加鎖部分主要邏輯
        if (redis.call('exists', KEYS[1]) == 0and 
          ((redis.call('exists', KEYS[2]) == 0)  or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then
          -- 彈出隊列中線程id元素,刪除Zset中該線程id對應的元素
          redis.call('lpop', KEYS[2]);
          redis.call('zrem', KEYS[3], ARGV[2]);
          local keys = redis.call('zrange', KEYS[3], 0-1);
          -- 遍歷zSet所有key,將key的超時時間(score) - 當前時間ms
          for i = 1, #keys, 1 do 
            redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);
          end;
            -- 加鎖設置鎖過期時間
          redis.call('hset', KEYS[1], ARGV[2], 1);
          redis.call('pexpire', KEYS[1], ARGV[1]);
          return nil;
        end;

        -- 3.線程存在,重入判斷
        if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
          redis.call('hincrby', KEYS[1], ARGV[2],1);
          redis.call('pexpire', KEYS[1], ARGV[1]);
          return nil;
        end;

        -- 4.返回當前線程剩余存活時間
        local timeout = redis.call('zscore', KEYS[3], ARGV[2]);
            if timeout ~= false then
          -- 過期時間timeout的值在下方設置,此處的減法算出的依舊是當前線程的ttl
          return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);
        end;

        -- 5.尾節(jié)點剩余存活時間
        local lastThreadId = redis.call('lindex', KEYS[2], -1);
        local ttl;
        -- 尾節(jié)點不空 && 尾節(jié)點非當前線程
        if lastThreadId ~= false and lastThreadId ~= ARGV[2then
          -- 計算隊尾節(jié)點剩余存活時間
          ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);
        else
          -- 獲取lock_name剩余存活時間
          ttl = redis.call('pttl', KEYS[1]);
        end;

        -- 6.末尾排隊
        -- zSet 超時時間(score),尾節(jié)點ttl + 當前時間 + 5000ms + 當前時間,無則新增,有則更新
        -- 線程id放入隊列尾部排隊,無則插入,有則不再插入
        local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);
        if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then
          redis.call('rpush', KEYS[2], ARGV[2]);
        end;
        return ttl;

        1.公平鎖加鎖步驟

        通過以上Lua,可以發(fā)現(xiàn),lua操作的關鍵結(jié)構是列表(list)和有序集合(zSet)。

        其中l(wèi)ist維護了一個等待的線程隊列redisson_lock_queue:{xxx},zSet維護了一個線程超時情況的有序集合redisson_lock_timeout:{xxx},盡管lua較長,但是可以拆分為6個步驟

        1.隊列清理

        • 保證隊列中只有未過期的等待線程

        2.首次加鎖

        • hset加鎖,pexpire過期時間

        3.重入判斷

        • 此處同可重入鎖lua

        4.返回ttl

        5.計算尾節(jié)點ttl

        • 初始值為鎖的剩余過期時間

        6.末尾排隊

        • ttl + 2 * currentTime + waitTime是score的默認值計算公式

        2.模擬

        如果模擬以下順序,就會明了redisson公平鎖整個加鎖流程

        假設 t1 10:00:00 < t2 10:00:10 < t3 10:00:20

        t1:當線程1初次獲取鎖

        1.等待隊列無頭節(jié)點,跳出死循環(huán)->2

        2.不存在該鎖 && 不存在線程等待隊列 成立

        2.1 lpop和zerm、zincrby都是無效操作,只有加鎖生效,說明是首次加鎖,加鎖后返回nil

        加鎖成功,線程1獲取到鎖,結(jié)束

        t2:線程2嘗試獲取鎖(線程1未釋放鎖)

        1.等待隊列無頭節(jié)點,跳出死循環(huán)->2

        2.不存在該鎖 不成立->3

        3.非重入線程 ->4

        4.score無值 ->5

        5.尾節(jié)點為空,設置ttl初始值為lock_name的ttl -> 6

        6.按照ttl + waitTime + currentTime + currentTime 來設置zSet超時時間score,并且加入等待隊列,線程2為頭節(jié)點

        score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10

        t3:線程3嘗試獲取鎖(線程1未釋放鎖)

        1.等待隊列有頭節(jié)點

        1.1未過期->2

        2.不存在該鎖不成立->3

        3.非重入線程->4

        4.score無值 ->5

        5.尾節(jié)點不為空 && 尾節(jié)點線程為2,非當前線程

        5.1取出之前設置的score,減去當前時間:ttl = score - currentTime ->6

        6.按照ttl + waitTime + currentTime + currentTime 來設置zSet超時時間score,并且加入等待隊列

        score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20

        如此一來,三個需要搶奪一把鎖的線程,完成了一次排隊,在list中排列他們等待線程id,在zSet中存放過期時間(便于排列優(yōu)先級)。其中返回ttl的線程2客戶端、線程3客戶端將會一直按一定間隔自旋重復執(zhí)行該段Lua,嘗試加鎖,如此一來便和AQS有了異曲同工之處。

        而當線程1釋放鎖之后(這里依舊有通過Pub/Sub發(fā)布解鎖消息,通知其他線程獲取)

        10:00:30 線程2嘗試獲取鎖(線程1已釋放鎖)

        1.等待隊列有頭節(jié)點,未過期->2

        2.不存在該鎖 & 等待隊列頭節(jié)點是當前線程 成立

        2.1刪除當前線程的隊列信息和zSet信息,超時時間為:

        線程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15

        線程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25

        2.2線程2獲取到鎖,重新設置過期時間

        加鎖成功,線程2獲取到鎖,結(jié)束

        排隊結(jié)構如圖

        公平鎖的釋放腳本和重入鎖類似,多了一步加鎖開頭的清理過期key的while true邏輯,在此不再展開篇幅描述。

        由上可以看出,Redisson公平鎖的玩法類似于延遲隊列的玩法,核心都在Redis的List和zSet結(jié)構的搭配,但又借鑒了AQS實現(xiàn),在定時判斷頭節(jié)點上如出一轍(watchDog),保證了鎖的競爭公平和互斥。并發(fā)場景下,lua腳本里,zSet的score很好地解決了順序插入的問題,排列好優(yōu)先級。

        并且為了防止因異常而退出的線程無法清理,每次請求都會判斷頭節(jié)點的過期情況給予清理,最后釋放時通過CHANNEL通知訂閱線程可以來獲取鎖,重復一開始的步驟,順利交接到下一個順序線程。

        六、總結(jié)

        Redisson整體實現(xiàn)分布式加解鎖流程的實現(xiàn)稍顯復雜,作者Rui Gu對Netty和JUC、Redis研究深入,利用了很多高級特性和語義,值得深入學習,本次介紹也只是單機Redis下鎖實現(xiàn)。

        Redisson也提供了多機情況下的聯(lián)鎖MultiLock:

        https://github.com/redisson/redisson/wiki/8.-分布式鎖和同步器#81-可重入鎖reentrant-lock

        和官方推薦的紅鎖RedLock:

        https://github.com/redisson/redisson/wiki/8.-分布式鎖和同步器#84-紅鎖redlock

        所以,當你真的需要分布式鎖時,不妨先來Redisson里找找。

        來源:juejin.cn/post/6961380552519712798

          

        1、社區(qū)糾紛不斷:程序員何苦為難程序員?

        2、該死的單元測試,寫起來到底有多痛?

        3、互聯(lián)網(wǎng)人為什么學不會擺爛

        4、為什么國外JetBrains做 IDE 就可以養(yǎng)活自己,國內(nèi)不行?區(qū)別在哪?

        5、相比高人氣的Rust、Go,為何 Java、C 在工具層面進展緩慢?

        6、讓程序員早點下班的《技術寫作指南》

        點在看

        瀏覽 34
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            成人毛片av| 国产不卡视频| 中文字幕无码影院| 亚洲日韩毛片| 一区二区在线视频| 午夜A区| 久久精品在线播放| 久草综合网| 在线观看三级网址| 久久久成人网| 无码a区| 青娱乐偷拍视频| 色欲成人网| 久久日韩视频| 日韩高清国产一区在线| 毛片传媒| 天天视频黄色| 麻豆成人精品国产免费| 成人777777免费视频色| 国产精品欧美性爱| 国产乱伦不卡| 天天干天天添| 操逼影片| 国产嫩BBwBBw高潮| 亚洲天堂在线看| 羞羞涩漫无码免费网站入口| 麻豆疯狂做受XXXX高潮视频| 亚洲视频在线播放| 久久水蜜桃| 欧美成人午夜福利| 大香蕉AV在线观看| A片黄色视频| 国产一级a毛片| 狼人亚洲伊人| 欧美性爱小说| 色色看片| 国产毛片一区二区| 怡红院成人av| 新BBWBBWBBWBBW| 精品人人人人| 成人在线观看无码| 三级片AAA成人免费| 双飞少妇| 操逼视频无码| 很很干在线视频| 国产精品久久久久久无码人妻| 婷婷无码成人精品俺来俺去| 国产精品久久久久久最猛| 欧美日逼小视频| 日本欧美在线| 国产黄页| 国产主播福利| www.激情五月天| 欧美后门菊门交3p| 在线观看免费一区| 热久精品| 亚洲日韩在线a成| 国产AV一区二区三区四区五区| 91爱搞搞| 1024在线视频| 日本一区二区三区在线视频| 俺也去啦WWW色官网| 久久久久成人精品无码| 中文字幕亚洲第一| 人妻啪啪| 欧一美一婬一伦一区?| 99热6| 五月天青青草超碰免费公开在线观看 | 成人片毛片| 国产一区二区视频在线| 欧美老熟妇BBBBB搡BBB| 在线v片| 无码不卡视频在线| 五月激情婷婷基地| 四虎Av| 亚洲天媒在线播放| 国产欧美日韩| 九九九免费视频| 国产一级特黄A片| 欧美一区视频| 操逼观看| 青青草视频| 天天日天天搞| 91成人在线播放| 欧美囗交大荫蒂免费| 亚洲成人在线观看视频| 日本黄色A片免费看| 日逼高清无码| 黄色小说在线看| 国产波霸爆乳一区二区| 日韩一区二区三区在线视频| 国产伊人自拍| 強暴人妻一区二区三区| 韩国三级中文字幕HD久久精品| 美女一级变态毛片| 成人黄色电影| 久草中文在线| 色网在线| 性久久久久| 爽好紧别夹喷水网站| 波多野结衣AV在线播放| 日韩无码AV电影| 日韩AV高清| 天天日天天射天天干| 人人妻人人爱人人操| 国产精品成人99一区无码| 日韩欧美国产精品| 中文字幕成人视频| 91丨熟女丨对白| 骚妇大战黑人15P| AV网站免费在线观看| 欧亚AV| 久久6精品| 中文字幕A片| 在线观看的AV| 色婷婷AV一区二区三区之e本道| 少妇婷婷| 18禁av在线| 亚洲网站免费| 思思热视频在线观看| 中文字幕在线视频免费观看| 羞羞视频com.入口| 欧美成人手机在线| 久久久无码精品亚洲| 一级黄色电影免费看| 九九成人视频| 免费看无码一级A片放24小时| 黄色成人18| 二区无码| 久草黄色电影在线观看| 韩国中文字幕HD久久精品| 中文字幕乱码亚洲无线码按摩| 国产中文字幕AV| 亚洲一级婬片A片AAAA网址| 精品无码一区二区三区四区五区| 日本天堂网在线观看| 久久99视频免费观看| 91精品人妻一区二区三区四区| 日韩视频二区| 午夜性爱视频| 91九色91蝌蚪91成人| 五月天成人网址| 亚洲国产精品成人久久蜜臀| 9i看片成人免费视频| 亚洲精品国产精品国自产在线| 亚洲图片欧美色图| 日韩日逼| 人妻性爱| 日韩中文无码一级A片| 五月丁香婷婷激情综合| 天堂网中文字幕| 欧美成人手机在线看片| 人人操人人干人人看| 日本A在线观看| 国产精品久久久久久久久夜色| www.一区二区三区| 无码一区在线观看| 超碰人人操| 操逼AV无码| 国产51视频| 日韩干| 精品免费在线| 亚州在线中文字幕经典a| 黄色电影网站在线观看| 性无码一区二区| 亚洲无码A片在线观看| 日本三级无码| 国产一级A片| 高清无码不卡AV| 国产欧美一区二区三区特黄手机版 | AV天堂中文字幕| 久久大鸡巴| 亚洲AV小说| 久久综合中文| 女人毛片| 正在播放吴梦梦淫行| 天天摸天天肏| 91av在线观看视频| 182在线视频| 欧美性BBB槡BBB槡BBB| 日本精品在线视频| 手机看片久久| 亚洲欧洲久久电影| 99资源站| 欧美aaa在线| 高清无码免费看| 51av在线| 国产熟妇婬乱一区二区| 五月天青青草超碰免费公开在线观看 | AV天堂国产| 久操新在线| 翔田千里一区二区三区精品播放 | 激情综合在线| 亚洲国产激情视频| www.一区二区三区| 欧美日韩国产在线观看| 人妻少妇被猛烈进入中文字幕| 久久久久久久久久久久成人 | 精品一区二区免费视频| 亚洲高清无码视频在线观看| 欧美日逼视频| 欧美色图狠狠操| 翔田千里一区二区三区精品播放 | 激情综| 激情五月在线| 日韩福利在线观看| 国产成人AV免费观看| 亚洲无码色| 俺也去也| 蜜桃精品视频| 人人操人人操人人操人人操人人操| 99re88| 日韩无码不卡视频| 黑人一级| 人妻夜夜爽天天爽三区麻豆AV网站| 肏逼视频免费看| 午夜福利在线播放| 亚洲中文字幕在| 91成人一区二区三区| 激情小视频国产在线播放| 91人妻无码一区二区久久| 国产无遮挡又黄又爽又色视频软件| 人妻无码一区二区三区| 天天爽夜夜爽人人爽| 丁香五月在线视频| 在线成人视频网站大香蕉在线网站| 国产人成| 俺去俺来也WWW色老板| 中文字幕一区二区三区四虎在线| 色高清无码免费视频| 黃色毛片A片AAAA级20| 国产噜噜噜噜久久久久久久久| 精品人妻| 一区二区久久| 中文字幕第一页亚洲| 美女靠逼视频| 精品国产AⅤ麻豆| 网址你懂的| 91无码高清视频| 久久人人超碰| 亚洲国产精品久久| 老司机永久免费91| 秋霞午夜久久| 日韩欧美操逼视频| 久热在线| 亚洲激情偷拍| 日韩99在线| av资源站| 日本免费黄色视频| 欧洲a视频| 欧美一级特黄AAAAAA片| 国产一区二区在线视频| 1024国产在线| 久久国产亚洲| 91豆花成人网站| 中文字幕精品人妻在线| www.jiujiujiu| 开心色情| 久久精品视频在线观看| 亚洲va欧美va天堂v国产综合 | 婷婷内射| 青青操在线视频| 家庭乱伦影视| 国产麻豆精品成人毛片| 午夜社区| 无码孕妇| 欧美亚洲综合在线| 亚洲无码视频播放| 亚洲伦理一区二区| 免费啪啪网| a在线免费观看| 国产欧美二区综合中文字幕精品一 | 久久永久免费视频| 无码人妻av黄色一区二区三区| 国产老熟女高潮毛片A片仙踪林 | 亚洲中文字幕av| 最新中文| 国产美女av| 亚洲中文字幕av| 五月天操逼网站| 国产美女av| 日本一级做a爱片| 日韩毛片一区二区| 日本激情视频| www.国产豆花精品区| 米奇狠狠干| 中文在线字幕高清电视剧| 日本成人视频在线免费播放| 韩国无码高清视频| 日韩精品一区在线观看| 精品国产一区二区三区性色AV| 5D肉蒲团| 日本操逼网站| 亚洲午夜福利一区二区三区| 国产视频激情| 国产乱国产乱老熟300部视频 | 日韩毛片在线看| 免费视频a| 丁香五月天在线播放| 少妇无码在线| 亚洲一级内射| 一级片AA| 日韩乱伦AV| 日韩中文视频| 亚洲AV无码乱码国产| 成人在线免费电影| 欧美日韩一区二区三区视频| 日本中文字幕中文翻译歌词| 亚洲无码在线电影| 国产—级a毛—a毛免费视频| 成人精品视频网站| 国产成人电影| 天天日天天色天天干| 最近中文字幕免费MV第一季歌词怀孕| 97无码人妻一区二区三区| 久久精品黄色| 丁香五月在线观看| 尻屄视频免费| 人人操碰| 十八禁网站在线观看| 欧美毛片在线观看| 欧美三级视频| 人人天天爽| 东北嫖老熟女一区二区视频网站| 2018天天操天天干| 日韩高清无码专区| 婷婷久草网| 欧美av| 国产美女被操| 国产美女做爱视频| 99视频内射三四| 先锋AV资源站| 亚洲AV激情无码专区在线播放| 国产美女18毛片水真多| 国产精品秘久久久久久1-~/\v7-/ 囯产精品一区二区三区线一牛影视1 | 天天爽夜夜爽夜夜爽| 99爱在线观看| 五月天在线电影| 成人片无码| 大香蕉福利视频导航| 中文字幕高清无码在线| 青青草视频在线观看| 超碰青青青| 女人久久久| 无码人妻丰满熟妇啪啪| 天天爽夜夜爽人人爽| 豆花精品视频| 又大又黄又爽| 欧美亚洲国产精品| 911国产精品| 东京热久久综合色五月老师| 日韩成人影视| 久久久国产视频| 国产精品扒开腿做爽爽爽视频| 天天视频黄| 少妇一级婬片内射视频| 西西444WWW大胆无视频软件亮点 | 午夜免费福利视频| 91人人视频| 免费看特别黄色视频| 北条麻妃在线中文字幕| 大奶一区二区| 中国国产乱子伦| 苗条一区小视频| 99re6热在线精品视频| 人妻一区二区三区| 青青草伊人大香蕉| 久久一区二区三区四区| 成人无码国产| 91熟女视频| 日韩区在线| 99视频精品| 少妇搡BBBB搡BBB搡HD(| 国产中文| 欧美AAA在线观看| 不卡AV在线| 91在线成人视频| 国产精品成人99一区无码| 2016av天堂网| 夜夜撸一撸| 亚洲色小说| 夜夜AV| 久色亚洲| 黄片无码| 少妇搡BBBB搡BBB搡造水多/| 大香蕉伊人网| 黄片无遮挡| 午夜视频在线| 自拍做爱视频| 黄色精品久久| 国产操逼视频网站| 99在线观看视频在线高清| 成人视频在线观看免费| 北条麻妃九九九在线视频| 国产三级自拍视频| 九九内射| 99久久99| 91鲁| 午夜做爱福利视频| 国产永久精品| 无码电影视频| 91人妻在线视频| 亚洲AV无码成人精品区www | 在线免费黄片| 日韩高清无码毛片| 午夜av电影| 亚洲AV电影网| 精品国产免费无码久久噜噜噜AV| 久久久久一区| 亚洲秘无码一区二区三区胖子| 一区二区三区精品无码| 亚洲一区二区AV| 国产嫩草久久久一二三久久免费观看 | 中文无码影院| 国产av福利| 9久9久9久9久女女女女| 色色欧美色色| 特级毛片WWW| 中国熟睡妇BBwBBw| 久久99深爱久久99精品| 91丨九色丨老熟女探花| 欧美成人伦理片网| 精品亚洲无码视频| 亚洲人成高清| 蜜桃视频无码| 老熟女伦一区二区三区| 北条麻妃一区二区三区-免费免费高清观看| 日韩不卡在线观看| 成人一区二区在线| 人人操av| 日本视频精品| 国产免费精彩视频| 91精品大屁股白浆自慰久久久| 91资源在线| 亚洲免费天堂| aa无码视频| 人人操人人摸人人看| 最新一区二区| 西西人体视频| 国产P片内射天涯海角| 亚洲精品成人一二三区| 操美女影院| 大伊人久久| 在线视频内射| 欧美色图在线观看| 亚洲精品久久久久久久久久久| 亚洲超级高清无码第一在线视频观看 | 91丨露脸丨熟女抽搐| 成人毛片一区二区三区| 三级小说| 欧美成年人视频| 日韩做爱| 伊人天天干| 日韩人妻无码电影| 欧美日韩操逼片| www中文字幕| 日韩AV无码一区二区| 久久夜色精品噜噜亚洲AV| 黄色成人网站在线观看免费| 国产精品久久一区二区三区影音先锋| 久久精品一区二区三区四区五区| 日韩在线免费看| 69超碰| 一区二区三区四区五区无码| 久久日韩视频| 另类色综合| 日韩免费看片| 成人丁香五月天| 日本乱伦视频| 一二三久久| JIZZJIZZ国产精品喷水| 国产免费久久| 中文字幕在线观看视频免费| 怡红院麻豆| 亚洲无码性爱| 国产操逼电影| 欧美日韩中| 青青操在线视频| 国内精品久久久久| 亚洲日韩欧美色图| 无码啪啪| 91免费小视频| 人人看人人搂人人摸| 日韩极品视频在线| 越南小嫩嫩BBWBBw| 久久久人妻熟妇精品无码蜜桃| 国产精品伊人| www久久| 天天操人妻| 中文字幕乱码无码人妻系列蜜桃| 国产丝袜AV| 先锋资源AV| 肏屄视频网站| 69激情网| 丁香六月婷婷久久综合| 午夜成人爽| 久久午夜夜伦鲁鲁一区二区| 最新日韩无码| 无码一区视频| 亚洲秘无码一区二区三区胖子| 午夜性爱剧场| 成人黄色免费看| 无码精品一区二区三区同学聚会| 国产精品久久毛片| 婷婷成人小说| 国产色视频| 午夜色婷婷| 免费无码毛片一区二区A片| 波多野结衣av在线观看窜天猴| 学生妹一级J人片内射视频| 日皮视频在线免费观看| 熟女伦乱| 日韩大片免费观看| 青娱乐99| 97日日| 视频一区二| www.怡春院| 先锋无码| 亚洲日韩欧美视频| 1插菊花网| 欧美日韩h| 欧美操| 欧美日韩中文字幕| 亚洲中文字幕2019| 国产精品嫩草久久久久yw193| 女同二人91| 成人视频一区二区| 69xx视频| 91久久久久久久久久久久18| 国产亚洲欧美在线| 91丨PORNY丨丰满人妻网站| 激情伊人五月天| 激情小视频在线观看| 日韩成人无码| 99热精品在线播放| 婷婷亚洲色| 亚洲国产精品尤物yw在线观看| 专业操美女视频网站| 婷婷五月天综合网| 毛片毛片毛片毛片毛片| 激情色色| 9l视频自拍蝌蚪9l视频成人| 免费AV网站| 日韩精品久久久久久久| 2022黄片| 国产成人午夜高潮毛片| 台湾成人在线视频| 国产男女性爱视频播放| 俺去也视频| 国产精品93333333| 中文字幕人妻在线中文乱码怎么解决| 99插插插| 亚州精品国产精品乱码不99勇敢| 中文字幕有码视频| 人人妻人人爱人人操| 成人免费A片喷| 日韩成人AV毛片| 亚州黄色电影| 国产一级黄片| 黄色毛片在线观看| 大香蕉久热| 免费十无码| 亚洲精品麻豆| 中文字幕精品综合| 青青五月天| 中文字幕视频在线| 黄色无码在线观看| 全国男人的天堂网站| 成人高清在线| 亚洲日韩精品欧美一区二区yw| 国产精品视频免费观看| 日韩人妻精品中文字幕免费| 成人在线观看网站| 日韩,变态,另类,中文,人妻| 久久黄色成人视频| 波多野结衣无码流出| 97超碰碰| 久久国产精品网站| 欧美综合精品| 北条麻纪无码视频| 人人操人人模| 欧美插插| 国产激情123区| 亚洲AV无码一区二区三竹菊| 91久久欧美极品XXXXⅩ| 91美女视频| 精品国产欧美一区二区三区成人 | 人人操人人操人人操人人操| AV大全在线免费观看| 韩国三级中文字幕HD久久精品| 伊人黄| 国产suv精品一区二区| 无码AV一区| 国产做受91一片二片老头| 日韩欧美国产综合| 伊人久久AV| 久久久无码电影| 伊人操逼网| 久久草成人网| 国产乱子伦真实精品| 操B视频在线观看| 国产激情福利| 九九激情| 色噜噜狠狠一区二区三区300部 | 国产av日韩| 日本国产在线观看| 欧美一级特黄A片免费| 久操网址| 三级黄色毛片| 婷婷视频在线观看| 青青草手机在线视频| 国产精品成人影视| 奇米97| 亚洲午夜精品成人毛片| 精品国产区一区二| 国产黄色Av| 18成人在线观看| 国产综合亚洲精品一区二| 欧美一级黄色性爱视频| 国产av资源| 婷婷久久久| 亚洲无码三区| 欧美国产日韩在线| 久久噜噜噜精品国产亚洲综合| 91麻豆精品国产91久久久吃药 | 六月婷婷五月| 国产色色视频| 国产探花一区二区三区| 无码视频一二三区| 亚洲日韩精品在线视频| 91国产福利| 免费看欧美成人A片| 亚洲AV无码成人精品区天堂小说| 五月婷婷中文字幕| 亚洲无码电影在线| 黄色国产在线观看| 无码人妻丰满熟妇区毛片视频| 自拍偷拍成人视频| 五月丁香婷婷色| 欧美黄色免费观看| 青青草原在线视频免费观看| 影音先锋色先锋| 久操中文| 综合久久99| 麻豆AV电影| 国产做受精品网站在线观看| 五月丁香人妻| 国产免费无码视频| 精品人妻系列| 国产美女操逼网站| 国产色秘乱码一区二区三区| 成人抽插视频| 日韩黄色A级片| 亚洲福利免费观看| 能看的av网站| 豆花在线视频| 日逼黄色视频| 欧美日韩国产成人电影| 欧美日黄| 无码电影在线播放| 四虎成人在线| 天天干91| 四川少BBB搡BBB爽爽爽| 亚洲制服中文字幕| 免费视频在线观看一区| 亚洲人免费视频| 人人看,人人摸| 17c白丝喷水自慰| 黄色成人免费视频| japanese在线观看| 91操b| 中文字幕亚洲视频在线观看| 亚洲中文无码电影| 三级网站在线| 亚洲成人免费| 免费在线观看黄| 伊人毛片| 久久大香蕉视频| 操噜噜噜噜噜插| 在线观看AV91| 天天干天天操综合| 亚洲福利一区二区| 黄片91| 日本黄色视| 伊人免费视频| 日本色网址| 天天操狠狠操| 北条麻妃99精彩视频| 国产精品小电影| 大香蕉久热| 成人av黄色三级片在线观看| 日韩欧美黄色片| 国产精品无码一区二区三区| 亚洲a在线观看| 成人一级A片| 色一区二区| 大香蕉av一区二区三区在线观看| 国产精品HongKong麻豆| 久久久亚洲AV无码精品色午夜| 日韩性爱视频在线播放| 国产99999| 久久久久久久久免费视频| 蜜桃精品无码| 亚洲伦乱| 青青草成人在线观看| 日日操夜夜| 亚洲区成人777777精品| 91丨国产丨熟女熟女| 3D动漫啪啪精品一区二区中文字幕| 在线看黄片| www黄色视频| 久久一区二区三区四区| 强伦轩人妻一区二区三区最新版本更新内容 | 青草久久视频| 天天干天天操| 粉嫩AV在线| 日韩无码国产精品| 日本中文字幕在线免费观看| 无码人妻精品一区二区三千菊电影 | 麻豆视频免费观看| 性久久久久久久| 国产在线无码视频| 黄色特级片| JUY-579被丈夫的上司侵犯后的第7天,我 | 在线成人一区二区| 日韩无码视频二区| 久久九九视频| 亚洲无码一级视频| 91黑人丨人妻丨国产丨| 日韩精品在线免费| 在线观看国产一区| 91丨熟女丨首页| 女人毛片| 无码射精电影| 牛牛影视一区二区| 国产a片视频| av性爱在线| 性爱AV天堂| 日本无码在线视频| www天天干| 免费操逼视频网站| 综合伊人| AV天堂国产| 国产91页| 四虎综合网| 五月天婷婷激情视频| 久久77777| 人人妻人人爽人人精品| 亚洲欧洲无码视频| 亚洲AV一二三区| 国产视频不卡| 婷婷五月天国产| 欧美精品久久久久久| 亚洲在线观看| 亚洲色情在线播放| 欧美熟妇另类久久久久久不卡 | 色婷婷综合视频| 水蜜桃视频免费| 国产一级性爱视频| 日韩在线一级| 国产中文字幕AV| 日韩黄色三级| 黄色电影一级| AV中文字幕在线播放| 国产亚洲99久久精品| 久久婷婷六月| 高清不卡一区二区| 91av一区二区三区| 俄罗斯白嫩BBwBBwBBw91| 深夜福利网| 特级西西444www高清| 亚洲aaa| 一本久道综合| 伊人色综合网| 自拍视频国产| 丰满人妻一区二区| 人人插人人操| gay成人在线观看| 99国产精品久久久久久久| 四川搡BBBBB搡BBB| 国产九色| 国产精品美女毛片真酒店| 黄色日逼视频| 啪啪网网站| 日韩乱伦网站| 大香蕉免费网站| 黑人大荫蒂女同互磨| 亚色天堂| 久久91| 久久香视频| 天天天天日天天干| 日韩欧美国产精品| 91视频免费网站| 永久免费看片视频5355| 亚洲日本中文字幕在线| 91麻豆精品国产91久久久久久| 日韩av免费看| 无码国产精品一区二区免费式直播 | 中文字幕一区二区三区人妻在线视频| 在线a视频免费观看| 性生活无码| 中文字幕乱码中文字乱码影响大吗 | 天堂国产一区二区三区| 国产精品高潮呻吟久久| 成人性生活影视av| 日本高清无码在线观看| 操美女的逼| 三级片欧美| 亚洲国产成人91精品| 日本一区二区三区视频在线观看| 日韩一区二区视频在线观看| ww免费视频| 无码一道本一区二区无码| 中文字幕在线第一页| 嫩BBB槡BBBB槡BBB小号| 九九热在线精品视频| 可以免费观看的av| 日韩十八禁| 国产成人片色情AAAA片| 日本亚洲国产| 国产免费a片| 狠狠撸视频| www.久久网| 亚洲男同Gay一区二区| 日本爽妇网| 亚洲日韩精品中文字幕| 熟女资源站| 99re66| 国产一级AV国产免费| 色婷婷久久| 黄色高清视频在线观看| 精品动漫一区二区三区| 台湾成人视频| 日韩一区二区在线观看| 91视频大全| 亚洲操色| 亚洲天堂免费观看| 91视频美女内射| 撸一撸免费视频| 久久久久久久久久久成人| 插插视频| 自拍偷拍福利视频网站| AAA成人| 欧美国产激情| 日韩一级一片内射视频4K| 秋霞理伦| 蜜桃av一区二区三区| 在线二区| 国产91页| 欧美sesese| 久操视频免费观看| 久久A视频| 亚洲GV成人无码久久精品| 国产又色又爽又黄又免费| 久久91久久久久麻豆精品| 久久久精品中文字幕麻豆发布 | 日韩无码AV一区二区三区| 日韩va中文字幕无码免费| 高h网站| 无码毛片一区二区三区人口| 色婷婷av| 中文字幕中文字幕无码| 91秦先生在线播放| 国产黄片视频| 91AV天天在线观看| 国产精品秘久久久久久1-~/\v7-/ 囯产精品一区二区三区线一牛影视1 | 先锋av资源在线| 在线观看中文字幕AV| 四川w搡BBB搡wBBB搡| 久久婷婷青青| 亚洲黄在线观看| 超碰在线人人操| 欧美作爱| 囯产精品久久久| 中文字幕一区二区三区四区50岁| 性饥渴欧美老妇XXXXX| 三p视频| 日韩欧美在线免费观看| 亚洲高清无码免费在线观看| 中文字幕牛牛婷婷| 91操B| 在线观看三级网址| chinese搡老熟老妇人| 少妇嫩搡BBBB搡BBBB| av在线影院| 久久婷婷国产麻豆91天堂| 久久久亚洲| 中文字幕日韩欧美在线| 日韩无码不卡视频| 亚洲成人高清无码| 干少妇视频| 久久色资源| 一区二区精品| 国产一道本| 日日干天天射| 九热大香蕉| 国产精品美女久久久久AV爽| 中文无码人妻| 人人操人人看人人干| 伊人大香蕉综合在线|