Redis實戰-Redisson-分布式鎖


1. 簡介

隨着技術的快速發展,業務系統規模的不斷擴大,分布式系統越來越普及。一個應用往往會部署到多台機器上,在一些業務場景中,為了保證數據的一致性,要求在同一時刻同一任務只在一個節點上運行,保證同一個方法同一時刻只能被一個線程執行。這時候分布式鎖就運用而生了。

分布式鎖有很多的解決方案。常見的有:

  1. 基於數據庫的:悲觀鎖,樂觀鎖。

  2. 基於zookeeper的分布式鎖。

  3. 本章中講的基於redis的分布式鎖。

2. 超賣

下單減庫存是互聯網項目中必不可少的環節。然而,如果我么考慮不得當,將會帶來很多問題。比如最不能忍受的:超賣

如下代碼,一個初始化庫存的方法和一個購買圖書的方法,我們沒有做任何的並發處理,查看下最終結果。

package com.ldx.redisson.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Objects;

/**
 * redis 實現分布式鎖
 *
 * @author ludangxin
 * @date 2021/8/15
 */
@Slf4j
@RestController
@RequestMapping("redis")
public class RedisLockTestController {
   @Resource
   private StringRedisTemplate stringRedisTemplate;
   // 商品key
   private static final String KEY = "book";
   // 庫存數量
   private static final  Long STOCK = 50L;

   /**
    * 初始化
    */
   @GetMapping("init")
   public String init() {
      stringRedisTemplate.opsForValue().set(KEY, String.valueOf(STOCK));
      return "初始化成功~";
   }

   /**
    * 購買圖書
    */
   @GetMapping("buy")
   public String buy() {
      // 獲取到當前庫存
      String buyBefore = stringRedisTemplate.opsForValue().get(KEY);
      if(Objects.isNull(buyBefore)) {
         log.error("未找到\"{}\"的庫存信息~", KEY);
         return "暫未上架~";
      }
      long buyBeforeL = Long.parseLong(buyBefore);
      if(buyBeforeL > 0) {
         // 對庫存進行-1操作
         Long buyAfter = stringRedisTemplate.opsForValue().decrement(KEY);
         log.info("剩余圖書==={}", buyAfter);
         return "購買成功~";
      }
      else {
         log.info("庫存不足~");
         return "庫存不足~";
      }
   }
}

啟動測試:

​ 這里我們使用jemter來進行並發請求。配置如下:

線程組配置:

請求配置:

請求結果:

只復制了部分日志

​ 通過日志很明顯的看到,即使在業務代碼中判斷了庫存 > 0但還是超賣了。

......
2021-08-15 21:01:22.614  INFO 66913 --- [io-8080-exec-30] c.l.r.c.RedisLockTestController          : 庫存不足~
2021-08-15 21:01:22.614  INFO 66913 --- [io-8080-exec-99] c.l.r.c.RedisLockTestController          : 剩余圖書===-42
2021-08-15 21:01:22.614  INFO 66913 --- [io-8080-exec-29] c.l.r.c.RedisLockTestController          : 庫存不足~
2021-08-15 21:01:22.622  INFO 66913 --- [io-8080-exec-89] c.l.r.c.RedisLockTestController          : 剩余圖書===-40
2021-08-15 21:01:22.622  INFO 66913 --- [io-8080-exec-90] c.l.r.c.RedisLockTestController          : 剩余圖書===-35
2021-08-15 21:01:22.622  INFO 66913 --- [o-8080-exec-135] c.l.r.c.RedisLockTestController          : 庫存不足~
2021-08-15 21:01:22.622  INFO 66913 --- [o-8080-exec-177] c.l.r.c.RedisLockTestController          : 庫存不足~
2021-08-15 21:01:22.622  INFO 66913 --- [io-8080-exec-92] c.l.r.c.RedisLockTestController          : 剩余圖書===-34
2021-08-15 21:01:22.622  INFO 66913 --- [io-8080-exec-86] c.l.r.c.RedisLockTestController          : 剩余圖書===-37
2021-08-15 21:01:22.642  INFO 66913 --- [io-8080-exec-11] c.l.r.c.RedisLockTestController          : 庫存不足~
2021-08-15 21:01:22.642  INFO 66913 --- [o-8080-exec-115] c.l.r.c.RedisLockTestController          : 庫存不足~
2021-08-15 21:01:22.642  INFO 66913 --- [io-8080-exec-72] c.l.r.c.RedisLockTestController          : 剩余圖書===-33
2021-08-15 21:01:22.643  INFO 66913 --- [nio-8080-exec-3] c.l.r.c.RedisLockTestController          : 庫存不足~

3. redis setnx

主要是用redis的 setnx (set not exists)命令實現分布式鎖。

3.1 編寫邏輯

在超買的場景中,我們了解了分布式鎖的必要性。

上面的場景如果是單機的話,直接使用jvm鎖就能解決問題,但是在分布式場景下下jvm鎖無法處理。

接下來我們將使用redis命令來解決一下超賣問題。

  1. 新增了鎖標識key。

  2. 在進行業務處理之前,給redis中setIfAbsent(LOCK_KEY, clientId, 30, TimeUnit.SECONDS)作為lock。

    LOCK_KEY:鎖的標識,比如秒殺的商品id_lock:當對該商品進行秒殺下單時,加鎖使其線性執行。

    clientId:當前請求的唯一值,為了在刪除鎖時進行鎖判斷。即只能刪除自己加的鎖。防止誤刪鎖。

    30:失效時間,防止死鎖(如果加鎖時不設置過期時間,當系統執行完加鎖還未進行解鎖時系統宕機,那其他節點也無法進行下單,因為鎖一直在)。

  3. 解鎖邏輯最好放在finally中進行,防止報錯導致死鎖。

   // 鎖標識   
   private static final String LOCK_KEY = "book_lock";

   /**
    * 購買圖書
    */
   @GetMapping("buy1")
   public String buy1() {
      String clientId = UUID.randomUUID().toString();
      /*
       * 給redis設置一個key,並設置過期時間防止死鎖
       * setIfAbsent(setnx):當key不存在時才設置值
       * flag=true:值設置成功(獲取鎖) flag=false:設置值失敗(獲取鎖失敗)
       */
      Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_KEY, clientId, 30, TimeUnit.SECONDS);
      try {
         if(Objects.nonNull(flag) && flag) {
            String buyBefore = stringRedisTemplate.opsForValue().get(KEY);
            if(Objects.isNull(buyBefore)) {
               log.error("未找到\"{}\"的庫存信息~", KEY);
               return "暫未上架~";
            }
            long buyBeforeL = Long.parseLong(buyBefore);
            if(buyBeforeL > 0) {
               Long buyAfter = stringRedisTemplate.opsForValue().decrement(KEY);
               log.info("剩余圖書==={}", buyAfter);
               return "購買成功~";
            }
            else {
               log.info("庫存不足~");
               return "庫存不足~";
            }
         }
         else {
            log.error("獲取鎖失敗~");
         }
      }
      catch(Exception e) {
         e.printStackTrace();
      }
      finally {
         // 防止誤刪鎖
         if(clientId.equals(stringRedisTemplate.opsForValue().get(LOCK_KEY))) {
            stringRedisTemplate.delete(LOCK_KEY);
         }
      }
      return "系統錯誤~";
   }

3.2 啟動測試

​ 啟動兩個服務,並配置nginx負載均衡。

​ nginx配置如下:

​ jemter配置如下:

啟動測試:

​ 部分日志如下:

redis中查看庫存:

打完收工~

3.3 小節

這里主要是用了setnx來實現分布式鎖。雖然解決了超賣問題,但其中還是有很多缺陷。比如:

  1. 當請求獲取鎖失敗時,能不能嘗試重新獲取鎖或者阻塞等待獲取鎖,而不是直接返回系統繁忙之類的提示語。
  2. 如果持有鎖的請求處理時間超過了設置的超時時間,也就是業務邏輯還沒有處理完呢,但鎖已經失效了,此時剛好又進來一個請求,又有並發問題了😂。

此時:redisson:申請出戰🙋。

4. redisson

4.1 簡介

官網

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框架。支持Redis2.8以上版本,支持Java1.6+以上版本。

Jedis 與 Redisson

  • Jedis:Jedis 只是簡單的封裝了 Redis 的API庫,可以看作是Redis客戶端,它的方法和Redis 的命令很類似,相比於Redisson 更原生一些,更靈活。

  • Redisson:Redisson 不僅封裝了 redis ,還封裝了對更多數據結構的支持,以及鎖等功能,相比於Jedis 更加大。

4.2 quick start

4.2.1 添加依賴

springboot 基礎上添加此依賴。

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson-spring-boot-starter</artifactId>
   <version>3.16.1</version>
</dependency>

4.2.2 application.yaml

因為使用的是單機redis,並且使用的是自動裝配的依賴,所以直接使用redis的基本配置即可。

server:
  port: ${port}
spring:
  # redis 配置
  redis:
    # 地址
    host: localhost
    # 端口,默認為6379
    port: 6379
    # 連接超時時間
    timeout: 10s
    lettuce:
      pool:
        # 連接池中的最小空閑連接
        min-idle: 0
        # 連接池中的最大空閑連接
        max-idle: 8
        # 連接池的最大數據庫連接數
        max-active: 8
        # #連接池最大阻塞等待時間(使用負值表示沒有限制)
        max-wait: -1ms

4.2.3 controller

package com.ldx.redisson.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;

/**
 * redisson test
 *
 * @author ludangxin
 * @date 2021/8/15
 */
@Slf4j
@RestController
@RequestMapping("test")
@RequiredArgsConstructor
public class RedissonLockTestController {
   private final RedissonClient redissonClient;

   /**
    * 沒獲取到鎖的線程阻塞等待獲取鎖
    */
   @GetMapping("/lock")
   public void lock() {
      log.info("進入了測試方法~");
      RLock lock = null;
      try {
         lock = redissonClient.getLock("lock");
         lock.lock();
         log.info("獲取到鎖~");
         Thread.sleep(2000);
      }
      catch(InterruptedException e) {
         e.printStackTrace();
      }
      finally {
         //如果當前線程保持鎖定則解鎖
         if (null != lock && lock.isHeldByCurrentThread()) {
            lock.unlock();
         }
      }
   }

   /**
    * 沒獲取到鎖的線程直接返回鎖狀態
    */
   @GetMapping("tryLock")
   public void tryLock() {
      log.info("進入了測試方法~");
      RLock lock = null;
      try {
         lock = redissonClient.getLock("lock");
         if(lock.tryLock()) {
            log.info("獲取到鎖~");
            Thread.sleep(6000);
         }
         else {
            log.error("獲取鎖失敗~");
         }
      }
      catch(InterruptedException e) {
         e.printStackTrace();
      }
      finally {
         //如果當前線程保持鎖定則解鎖
         if (null != lock && lock.isHeldByCurrentThread()) {
            lock.unlock();
         }
      }
   }

   /**
    * 沒獲取到鎖的線程嘗試獲取鎖
    */
   @GetMapping("tryLockWithBlock")
   public void tryLockWithBlock() {
      log.info("進入了測試方法~");
      RLock lock = null;
      try {
         //非公平鎖,隨機取一個等待中的線程分配鎖
         lock = redissonClient.getLock("lock");
         //公平鎖,按照先后順序依次分配鎖
         //lock=redissonClient.getFairLock("lock");
         //最多等待鎖3秒,5秒后強制解鎖
         if(lock.tryLock(3, 5, TimeUnit.SECONDS)) {
            log.info("獲取到鎖~");
            Thread.sleep(1000);
         }
         else {
            log.error("獲取鎖失敗~");
         }
      }
      catch(InterruptedException e) {
         e.printStackTrace();
      }
      finally {
         //如果當前線程保持鎖定則解鎖
         if (null != lock && lock.isHeldByCurrentThread()) {
            lock.unlock();
         }
      }
   }
}

4.2.4 啟動測試

jemter 配置如下:

​ 啟動9個線程並同一時刻進行請求。

請求lock方法日志如下:

​ 所有請求同一時刻進入方法,並且請求阻塞每隔兩秒獲取到鎖。

2021-08-15 16:58:26.435  INFO 63602 --- [nio-8080-exec-5] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.435  INFO 63602 --- [nio-8080-exec-2] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.435  INFO 63602 --- [nio-8080-exec-7] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.435  INFO 63602 --- [nio-8080-exec-1] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.435  INFO 63602 --- [nio-8080-exec-6] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.435  INFO 63602 --- [nio-8080-exec-4] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.437  INFO 63602 --- [io-8080-exec-10] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.437  INFO 63602 --- [io-8080-exec-11] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.437  INFO 63602 --- [nio-8080-exec-9] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:58:26.443  INFO 63602 --- [nio-8080-exec-2] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:28.474  INFO 63602 --- [io-8080-exec-11] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:30.499  INFO 63602 --- [nio-8080-exec-6] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:32.523  INFO 63602 --- [nio-8080-exec-5] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:34.548  INFO 63602 --- [io-8080-exec-10] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:36.569  INFO 63602 --- [nio-8080-exec-7] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:38.595  INFO 63602 --- [nio-8080-exec-4] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:40.616  INFO 63602 --- [nio-8080-exec-9] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:58:42.643  INFO 63602 --- [nio-8080-exec-1] c.l.r.c.RedissonLockTestController       : 獲取到鎖~

請求tryLock方法日志如下:

​ 所有請求同一時刻進入方法,並且只有一個請求獲取到了鎖,其他請求直接返回結果。

2021-08-15 16:59:17.925  INFO 63602 --- [nio-8080-exec-9] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.925  INFO 63602 --- [nio-8080-exec-1] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.925  INFO 63602 --- [nio-8080-exec-5] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.928  INFO 63602 --- [nio-8080-exec-4] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.931  INFO 63602 --- [nio-8080-exec-6] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.931  INFO 63602 --- [io-8080-exec-11] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.933  INFO 63602 --- [nio-8080-exec-2] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.933  INFO 63602 --- [nio-8080-exec-8] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.933  INFO 63602 --- [io-8080-exec-10] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 16:59:17.937 ERROR 63602 --- [nio-8080-exec-9] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 16:59:17.937  INFO 63602 --- [nio-8080-exec-5] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 16:59:17.938 ERROR 63602 --- [nio-8080-exec-4] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 16:59:17.937 ERROR 63602 --- [nio-8080-exec-1] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 16:59:17.939 ERROR 63602 --- [nio-8080-exec-8] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 16:59:17.939 ERROR 63602 --- [nio-8080-exec-2] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 16:59:17.939 ERROR 63602 --- [nio-8080-exec-6] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 16:59:17.939 ERROR 63602 --- [io-8080-exec-11] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 16:59:17.939 ERROR 63602 --- [io-8080-exec-10] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~

請求tryLockWithBlock方法日志如下:

​ 所有請求同一時刻進入方法,並且只有三個請求獲取到了鎖。

2021-08-15 23:34:41.617  INFO 70413 --- [nio-8080-exec-6] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.617  INFO 70413 --- [nio-8080-exec-4] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.617  INFO 70413 --- [nio-8080-exec-7] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.617  INFO 70413 --- [nio-8080-exec-8] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.618  INFO 70413 --- [nio-8080-exec-5] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.618  INFO 70413 --- [nio-8080-exec-2] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.618  INFO 70413 --- [nio-8080-exec-9] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.618  INFO 70413 --- [nio-8080-exec-1] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.617  INFO 70413 --- [nio-8080-exec-3] c.l.r.c.RedissonLockTestController       : 進入了測試方法~
2021-08-15 23:34:41.658  INFO 70413 --- [nio-8080-exec-8] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 23:34:42.681  INFO 70413 --- [nio-8080-exec-7] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 23:34:43.697  INFO 70413 --- [nio-8080-exec-3] c.l.r.c.RedissonLockTestController       : 獲取到鎖~
2021-08-15 23:34:44.624 ERROR 70413 --- [nio-8080-exec-9] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 23:34:44.624 ERROR 70413 --- [nio-8080-exec-1] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 23:34:44.624 ERROR 70413 --- [nio-8080-exec-5] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 23:34:44.624 ERROR 70413 --- [nio-8080-exec-2] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 23:34:44.625 ERROR 70413 --- [nio-8080-exec-6] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~
2021-08-15 23:34:44.630 ERROR 70413 --- [nio-8080-exec-4] c.l.r.c.RedissonLockTestController       : 獲取鎖失敗~

4.2.5 小節

redisson 提供了lock()tryLock()tryLock(long time, TimeUnit unit)tryLock(long waitTime, long leaseTime, TimeUnit unit)方法。

  1. lock():會阻塞未獲取鎖的請求,默認持有30s鎖,但當業務方法在30s內沒有執行完時,會有看門狗(默認每隔10s)給當前鎖續時30s
  2. tryLock():嘗試獲取鎖,獲取不到則直接返回獲取失敗,默認持有30s鎖,但當業務方法在30s內沒有執行完時,會有看門狗(默認每隔10s)給當前鎖續時30s
  3. tryLock(long time, TimeUnit unit):嘗試獲取鎖,等待time TimeUnit,默認持有30s鎖,但當業務方法在30s內沒有執行完時,會有看門狗(默認每隔10s)給當前鎖續時30s
  4. tryLock(long waitTime, long leaseTime, TimeUnit unit):嘗試獲取鎖,等待waitTime TimeUnit鎖最長持有leaseTime TimeUnit,當業務方法在leaseTime TimeUnit時長內沒有執行完時,會強制解鎖。

4.3 解決超賣

   private static final String KEY = "book";

   /**
    * 購買圖書
    */
   @GetMapping("buy1")
   public String buy1() {
      RLock lock = null;
      try {
         lock = redissonClient.getLock("lock");
         if(lock.tryLock(3, TimeUnit.SECONDS)) {
            RAtomicLong buyBefore = redissonClient.getAtomicLong(KEY);
            if(Objects.isNull(buyBefore)) {
               log.error("未找到\"{}\"的庫存信息~", KEY);
               return "暫未上架~";
            }
            long buyBeforeL = buyBefore.get();
            if(buyBeforeL > 0) {
               Long buyAfter = buyBefore.decrementAndGet();
               log.info("剩余圖書==={}", buyAfter);
               return "購買成功~";
            }
            else {
               log.info("庫存不足~");
               return "庫存不足~";
            }
         }
         else {
            log.error("獲取鎖失敗~");
         }
      }
      catch(Exception e) {
         e.printStackTrace();
      }
      finally {
         //如果當前線程保持鎖定則解鎖
         if(null != lock && lock.isHeldByCurrentThread()) {
            lock.unlock();
         }
      }
      return "系統錯誤~";
   }

經測試不會存在超賣問題。

並且避免了3.3小節中提到的問題。

4.4 小節

方便,好用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM