Spring Boot2+Resilience4j實現容錯之Bulkhead

Resilience4j是一個輕量級、易於使用的容錯庫,其靈感來自Netflix Hystrix,但專為Java 8和函數式編程設計。輕量級,因為庫只使用Vavr,它沒有任何其他外部庫依賴項。相比之下,Netflix Hystrix對Archaius有一個編譯依賴關係,Archaius有更多的外部庫依賴關係,如Guava和Apache Commons。

Resilience4j提供高階函數(decorators)來增強任何功能接口、lambda表達式或方法引用,包括斷路器、速率限制器、重試或艙壁。可以在任何函數接口、lambda表達式或方法引用上使用多個裝飾器。優點是您可以選擇所需的裝飾器,而無需其他任何東西。

有了Resilience4j,你不必全力以赴,你可以選擇你需要的。

https://resilience4j.readme.io/docs/getting-started

概覽

Resilience4j提供了兩種艙壁模式(Bulkhead),可用於限制併發執行的次數:

  • SemaphoreBulkhead(信號量艙壁,默認),基於Java併發庫中的Semaphore實現。
  • FixedThreadPoolBulkhead(固定線程池艙壁),它使用一個有界隊列和一個固定線程池。

本文將演示在Spring Boot2中集成Resilience4j庫,以及在多併發情況下實現如上兩種艙壁模式。

引入依賴

在Spring Boot2項目中引入Resilience4j相關依賴

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
    <version>1.4.0</version>
</dependency>

由於Resilience4j的Bulkhead依賴於Spring AOP,所以我們需要引入Spring Boot AOP相關依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

我們可能還希望了解Resilience4j在程序中的運行時狀態,所以需要通過Spring Boot Actuator將其暴露出來

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

實現SemaphoreBulkhead(信號量艙壁)

resilience4j-spring-boot2實現了對resilience4j的自動配置,因此我們僅需在項目中的yml/properties文件中編寫配置即可。

SemaphoreBulkhead的配置項如下:

屬性配置 默認值 含義
maxConcurrentCalls 25 艙壁允許的最大并行執行量
maxWaitDuration 0 嘗試進入飽和艙壁時,應阻塞線程的最長時間。

添加配置

示例(使用yml):

resilience4j.bulkhead:
  configs:
    default:
      maxConcurrentCalls: 5
      maxWaitDuration: 20ms
  instances:
    backendA:
      baseConfig: default
    backendB:
      maxWaitDuration: 10ms
      maxConcurrentCalls: 20

如上,我們配置了SemaphoreBulkhead的默認配置為maxConcurrentCalls: 5,maxWaitDuration: 20ms。並在backendA實例上應用了默認配置,而在backendB實例上使用自定義的配置。這裏的實例可以理解為一個方法/lambda表達式等等的可執行單元。

編寫Bulkhead邏輯

定義一個受SemaphoreBulkhead管理的Service類:

@Service
public class BulkheadService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private BulkheadRegistry bulkheadRegistry;

    @Bulkhead(name = "backendA")
    public JsonNode getJsonObject() throws InterruptedException {
        io.github.resilience4j.bulkhead.Bulkhead.Metrics metrics = bulkheadRegistry.bulkhead("backendA").getMetrics();
        logger.info("now i enter the method!!!,{}<<<<<<{}", metrics.getAvailableConcurrentCalls(), metrics.getMaxAllowedConcurrentCalls());
        Thread.sleep(1000L);
        logger.info("now i exist the method!!!");
        return new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis());
    }
}

如上,我們將@Bulkhead註解放到需要管理的方法上面。並且通過name屬性指定該方法對應的Bulkhead實例名字(這裏我們指定的實例名字為backendA,所以該方法將會利用默認的配置)。

定義接口類:

@RestController
public class BulkheadResource {
    @Autowired
    private BulkheadService bulkheadService;

    @GetMapping("/json-object")
    public ResponseEntity<JsonNode> getJsonObject() throws InterruptedException {
        return ResponseEntity.ok(bulkheadService.getJsonObject());
    }
}

編寫測試:

首先添加測試相關依賴

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>3.0.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>4.0.2</version>
    <scope>test</scope>
</dependency>

這裏我們使用rest-assured和awaitility編寫多併發情況下的API測試

public class SemaphoreBulkheadTests extends Resilience4jDemoApplicationTests {
    @LocalServerPort
    private int port;
    @BeforeEach
    public void init() {
        RestAssured.baseURI = "http://localhost";
        RestAssured.port = port;
    }

    @Test
    public void 多併發訪問情況下的SemaphoreBulkhead測試() {
        CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
        IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
                statusList.add(given().get("/json-object").statusCode());
            }
        ));
        await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
        System.out.println(statusList);
        assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(5);
        assertThat(statusList.stream().filter(i -> i == 500).count()).isEqualTo(3);
    }
}

可以看到所有請求中只有前五個順利通過了,其餘三個都因為超時而導致接口報500異常。我們可能並不希望這種不友好的提示,因此Resilience4j提供了自定義的失敗回退方法。當請求併發量過大時,無法正常執行的請求將進入回退方法。

首先我們定義一個回退方法

private JsonNode fallback(BulkheadFullException exception) {
        return new ObjectMapper().createObjectNode().put("errorFile", System.currentTimeMillis());
    }

注意:回退方法應該和調用方法放置在同一類中,並且必須具有相同的方法簽名,並且僅帶有一個額外的目標異常參數。

然後在@Bulkhead註解中指定回退方法:@Bulkhead(name = "backendA", fallbackMethod = "fallback")

最後修改API測試代碼:

@Test
public void 多併發訪問情況下的SemaphoreBulkhead測試使用回退方法() {
    CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
    IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
            statusList.add(given().get("/json-object").statusCode());
        }
    ));
    await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
    System.out.println(statusList);
    assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(8);
}

運行單元測試,成功!可以看到,我們定義的回退方法,在請求過量時起作用了。

實現FixedThreadPoolBulkhead(固定線程池艙壁)

FixedThreadPoolBulkhead的配置項如下:

配置名稱 默認值 含義
maxThreadPoolSize Runtime.getRuntime().availableProcessors() 配置最大線程池大小
coreThreadPoolSize Runtime.getRuntime().availableProcessors() - 1 配置核心線程池大小
queueCapacity 100 配置隊列的容量
keepAliveDuration 20ms 當線程數大於核心時,這是多餘空閑線程在終止前等待新任務的最長時間

添加配置

示例(使用yml):

resilience4j.thread-pool-bulkhead:
  configs:
    default:
      maxThreadPoolSize: 4
      coreThreadPoolSize: 2
      queueCapacity: 2
  instances:
    backendA:
      baseConfig: default
    backendB:
      maxThreadPoolSize: 1
      coreThreadPoolSize: 1
      queueCapacity: 1

如上,我們定義了一段簡單的FixedThreadPoolBulkhead配置,我們指定的默認配置為:maxThreadPoolSize: 4,coreThreadPoolSize: 2,queueCapacity: 2,並且指定了兩個實例,其中backendA使用了默認配置而backendB使用了自定義的配置。

編寫Bulkhead邏輯

定義一個受FixedThreadPoolBulkhead管理的方法:

@Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<JsonNode> getJsonObjectByThreadPool() throws InterruptedException {
    io.github.resilience4j.bulkhead.ThreadPoolBulkhead.Metrics metrics = threadPoolBulkheadRegistry.bulkhead("backendA").getMetrics();
    logger.info("now i enter the method!!!,{}", metrics);
    Thread.sleep(1000L);
    logger.info("now i exist the method!!!");
    return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis()));
}

如上定義和SemaphoreBulkhead的方法大同小異,其中@Bulkhead显示指定了type的屬性為Bulkhead.Type.THREADPOOL,表明其方法受FixedThreadPoolBulkhead管理。由於@Bulkhead默認的BulkheadSemaphoreBulkhead,所以在未指定type的情況下為SemaphoreBulkhead。另外,FixedThreadPoolBulkhead只對CompletableFuture方法有效,所以我們必創建返回CompletableFuture類型的方法。

定義接口類方法

@GetMapping("/json-object-with-threadpool")
public ResponseEntity<JsonNode> getJsonObjectWithThreadPool() throws InterruptedException, ExecutionException {
    return ResponseEntity.ok(bulkheadService.getJsonObjectByThreadPool().get());
}

編寫測試代碼

@Test
public void 多併發訪問情況下的ThreadPoolBulkhead測試() {
    CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
    IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
            statusList.add(given().get("/json-object-with-threadpool").statusCode());
        }
    ));
    await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
    System.out.println(statusList);
    assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(6);
    assertThat(statusList.stream().filter(i -> i == 500).count()).isEqualTo(2);
}

測試中我們并行請求了8次,其中6次請求成功,2次失敗。根據FixedThreadPoolBulkhead的默認配置,最多能容納maxThreadPoolSize+queueCapacity次請求(根據我們上面的配置為6次)。

同樣,我們可能並不希望這種不友好的提示,那麼我們可以指定回退方法,在請求無法正常執行時使用回退方法。

private CompletableFuture<JsonNode> fallbackByThreadPool(BulkheadFullException exception) {
    return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("errorFile", System.currentTimeMillis()));
}
@Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL, fallbackMethod = "fallbackByThreadPool")
public CompletableFuture<JsonNode> getJsonObjectByThreadPoolWithFallback() throws InterruptedException {
    io.github.resilience4j.bulkhead.ThreadPoolBulkhead.Metrics metrics = threadPoolBulkheadRegistry.bulkhead("backendA").getMetrics();
    logger.info("now i enter the method!!!,{}", metrics);
    Thread.sleep(1000L);
    logger.info("now i exist the method!!!");
    return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis()));
}

編寫測試代碼

@Test
public void 多併發訪問情況下的ThreadPoolBulkhead測試使用回退方法() {
    CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
    IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
            statusList.add(given().get("/json-object-by-threadpool-with-fallback").statusCode());
        }
    ));
    await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
    System.out.println(statusList);
    assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(8);
}

由於指定了回退方法,所有請求的響應狀態都為正常了。

總結

本文首先簡單介紹了Resilience4j的功能及使用場景,然後具體介紹了Resilience4j中的Bulkhead。演示了如何在Spring Boot2項目中引入Resilience4j庫,使用代碼示例演示了如何在Spring Boot2項目中實現Resilience4j中的兩種Bulkhead(SemaphoreBulkhead和FixedThreadPoolBulkhead),並編寫API測試驗證我們的示例。

本文示例代碼地址:https://github.com/cg837718548/resilience4j-demo

歡迎訪問筆者博客:blog.dongxishaonian.tech

關注筆者公眾號,推送各類原創/優質技術文章 ⬇️

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

多線程高併發編程(12) — 阻塞算法實現ArrayBlockingQueue源碼分析

一.前言

  前文探究了非阻塞算法的實現ConcurrentLinkedQueue安全隊列,也說明了阻塞算法實現的兩種方式,使用一把鎖(出隊和入隊同一把鎖ArrayBlockingQueue)和兩把鎖(出隊和入隊各一把鎖LinkedBlockingQueue)來實現,今天來探究下ArrayBlockingQueue。

  ArrayBlockingQueue是一個阻塞隊列,底層使用數組結構實現,按照先進先出(FIFO)的原則對元素進行排序。

  ArrayBlockingQueue是一個線程安全的集合,通過ReentrantLock鎖來實現,在併發情況下可以保證數據的一致性。

  此外,ArrayBlockingQueue的容量是有限的,數組的大小在初始化時就固定了,不會隨着隊列元素的增加而出現擴容的情況,也就是說ArrayBlockingQueue是一個“有界緩存區”。

  從下圖可以看出,ArrayBlockingQueue是使用一個數組存儲元素的,當向隊列插入元素時,首先會插入到數組下標索引為6的位置,再有新元素進來時插入到索引為7的位置,依次類推,如果滿了就不會再插入。

  當元素出隊時,先移除索引為2的元素3,與入隊一樣,依次類推,移除索引3、4、5…上的元素。這也形成了“先進先出”。

 

二.源碼解析

  1. 構造方法

    public class ArrayBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
    
        //隊列實現:數組
        final Object[] items;
    
        //當讀取元素時數組的下標(下一個被取出元素的索引)
        int takeIndex;
    
        //添加元素時數組的下標 (下一個被添加元素的索引)
        int putIndex;
    
        //隊列中元素個數:
        int count;
    
        //可重入鎖:
        final ReentrantLock lock;
    
        //入隊操作時是否讓線程等待
        private final Condition notEmpty;
    
        //出隊操作時是否讓線程等待
        private final Condition notFull;
    
        /**
         * 初始化隊列容量構造:由於公平鎖會降低隊列的性能,因而使用非公平鎖(默認)。
         */
        public ArrayBlockingQueue(int capacity) {
            this(capacity, false);
        }
    
        //帶初始容量大小和公平鎖隊列(公平鎖通過ReentrantLock實現):
        public ArrayBlockingQueue(int capacity, boolean fair) {
            if (capacity <= 0)
                throw new IllegalArgumentException();
            this.items = new Object[capacity];
            lock = new ReentrantLock(fair);
            notEmpty = lock.newCondition();
            notFull =  lock.newCondition();
        }
    }
    •  在多線程中,默認不保證線程公平的訪問隊列;

    •  在ArrayBlockingQueue中為了保證數據的安全,使用了ReentrantLock鎖。由於鎖的引入,導致了線程之間的競爭。當有一個線程獲取到鎖時,其餘線程處於等待狀態。當鎖被釋放時,所有等待線程為奪鎖而競爭;

    • 鎖有公平鎖和非公平鎖:

      •  公平鎖:等待的線程在獲取鎖而競爭時,按照等待的先後順序FIFO進行獲取操作;公平鎖可以應用在比如併發下的日誌輸出隊列中,保證了日誌輸出的順序完整性;
        •  優點:等待鎖的線程不會餓死,和非公平鎖相比,在獲得鎖和保證鎖分配的均衡性差異較小;
        • 缺點:使用公平鎖的程序在多線程訪問時表現為很低的吞吐量(即速度很慢),等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖的大;公平鎖不能保證線程調度的公平性,因此,使用公平鎖的眾多線程中的一員可能獲得多倍的成功機會,這種情況發生在其他活動線程沒有被處理並且目前並未持有鎖時【ReentrantLock源碼對公平鎖的定義】;
           Note however, that fairness of locks does not guarantee
           fairness of thread scheduling. Thus, one of many threads using a
           fair lock may obtain it multiple times in succession while other
           active threads are not progressing and not currently holding the
           lock.
          •  上面這句話有重入鎖的概念,一個線程可以在已經獲取鎖的情況下再次進入獲取到鎖,不需要競爭;同時,如果一個線程獲取到了鎖,然後釋放,在其他線程來獲取之前再次是可以獲取到鎖的。
            A: Request Lock -> Release Lock -> Request Lock Again (Succeeds) 
                                                   B: Request Lock (Denied)... 
            -----------------------   Time   --------------------------------->
      •  非公平鎖:在獲取鎖時,無論是先等待還是后等待的線程,均有可能獲取到鎖。即根據搶佔機制,是隨機獲取鎖的,和公平鎖不一樣的是先來的不一定能獲取到鎖,有可能一直拿不到鎖,這樣會造成“飢餓”現象;
        • 優點:非公平鎖性能高於公平鎖性能。首先,在恢復一個被掛起的線程與該線程真正運行之間存在着嚴重的延遲,而且,非公平鎖更能充分的利用CPU的時間片,盡量減少CPU空閑的狀態時間;即可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲取到鎖,CPU不必喚醒其他所有線程;
        • 缺點:處於等待隊列中的線程可能會餓死或者等很久才會獲得鎖;
      • 產生“飢餓”的原因:
        • 高優先級吞噬所有低優先級的CPU時間片,優先級越高,就會獲得越高的CPU執行機會; —> 使用默認的優先級;
        • 線程被永久阻塞在一個等待進入同步塊synchronized的狀態(長時間執行) ,同時synchronized並不保障等待線程的順序(鎖釋放后,隨機競爭,由OS調度),這會存在一個可能是某個線程總是搶鎖搶不到導致一直等待狀態 —> 避免持有鎖的線程長時間執行、使用显示lock來代替synchronized;
          synchronized(obj) {
                  while (true) {
               // .... infinite loop
               }
        •  等待的線程永遠不被喚醒:如果多個線程處在wait方法執行上,而對其調用notify方法不會保證哪一個線程會獲得喚醒,喚醒是無序的,跟VM/OS調度有關,甚至底層是隨機選取一個或是隊列中的第一個,任何線程都有可能處於繼續等待的狀態,因此存在這樣一個風險,即一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒 —> 使用显示lock來代替synchronized;
      •  比如ReentrantLock:
        •  在公平鎖中,如果有另一個線程持有鎖或者有其他線程在等待隊列中等待這個鎖,那麼新發出的請求的線程將被放入到隊列中;
        • 非公平鎖中, 根據搶佔機制,擁有鎖的線程在釋放鎖資源的時候, 新發出請求的線程可以和等待隊列中的第一個線程競爭鎖資源, 新線程競爭失敗才放入隊列中,但是已經進入等待隊列的線程, 依然是按照先進先出的順序獲取鎖資源;
  2. 入隊:有阻塞式和非阻塞式

    1. 阻塞式:當隊列中的元素已滿時,則會將此線程停止,讓其處於等待狀態,直到隊列中有空餘位置產生

      public void put(E e) throws InterruptedException {
              checkNotNull(e);
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();//獲取鎖
              try {
                  //隊列中元素 == 數組長度(隊列滿了),則線程等待
                  while (count == items.length)
                      notFull.await();
                  enqueue(e);//元素加入隊列
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
      • lockInterruptibly:
        • 如果當前線程未被中斷,則獲取鎖。
        • 如果該鎖沒有被另一個線程保持,則獲取該鎖並立即返回,將鎖的保持計數設置為 1。
        • 如果當前線程已經保持此鎖,則將保持計數加 1,並且該方法立即返回。
        • 如果鎖被另一個線程保持,則出於線程調度目的,禁用當前線程,並且在發生以下兩種情況之一以前,該線程將一直處於休眠狀態:1)鎖由當前線程獲得;2)其他某個線程中斷當前線程
    2. 非阻塞式:當隊列中的元素已滿時,並不會阻塞此線程的操作,而是讓其返回又或者是拋出異常

      public boolean add(E e) {
              return super.add(e);// AbstractQueue.add
          }
          public boolean add(E e) {
              if (offer(e))//調用實現接口
                  return true;
              else
                  throw new IllegalStateException("Queue full");
          }
          public boolean offer(E e) {
              checkNotNull(e);//檢測是否有空指針異常
              final ReentrantLock lock = this.lock;//獲得鎖對象
              lock.lock();//加鎖
              try {
                  //如果隊列滿了,返回false
                  if (count == items.length)
                      return false;
                  else {
                      //元素加入隊列
                      enqueue(e);
                      return true;
                  }
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
          private void enqueue(E x) {
              // assert lock.getHoldCount() == 1;
              // assert items[putIndex] == null;
              //獲得數組
              final Object[] items = this.items;
              //槽位填充元素
              items[putIndex] = x;
              //獲得下一個被添加元素的索引,如果值等於數組長度,表示到達尾部了,需要從頭開始填充
              if (++putIndex == items.length)
                  putIndex = 0;
              count++;//數量+1
              notEmpty.signal();//喚醒出隊上的等待線程,表示有元素可以消費了
          }
      • enqueue中++putIndex == items.length,putIndex=0:這是因為當前隊列執行元素出隊時總是從隊列頭部獲取,而添加元素的索引從隊列尾部獲取所以當隊列索引(從0開始)與數組長度相等時,下次我們就需要從數組頭部開始添加了
    3. 阻塞式和非阻塞式的結合:offer(E e, long timeout, TimeUnit unit),向隊列尾部添加元素,可以設置線程等待時間,如果超過指定時間隊列還是滿的,則返回false;

      public boolean offer(E e, long timeout, TimeUnit unit)
              throws InterruptedException {
      
              checkNotNull(e);//檢測是否為空
              long nanos = unit.toNanos(timeout);//轉換成超時時間閥值
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();//加鎖
              try {
                  //隊列是否滿了的判斷
                  while (count == items.length) {
                      if (nanos <= 0)//等待超時結束返回false
                          return false;
                      nanos = notFull.awaitNanos(nanos);//隊列滿了,等待出隊有空位填充
                  }
                  enqueue(e);//加入隊列中
                  return true;
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
  3. 出隊:同樣有阻塞式和非阻塞式

    1. 阻塞式:當隊列中的元素已空時,則會將此線程停止,讓其處於等待狀態,直到隊列中有元素插入

      public E take() throws InterruptedException {
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();
              try {
                  //隊列為空,進行等待
                  while (count == 0)
                      notEmpty.await();
                  return dequeue();//返回出隊元素
              } finally {
                  lock.unlock();
              }
          }
    2. 非阻塞式:當隊列中的元素已滿時,並不會阻塞此線程的操作,而是讓其返回null或元素【裏面的迭代器比較複雜,留待下文探究】

      public E poll() {
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
                  //隊列為空,返回null,否則返回元素
                  return (count == 0) ? null : dequeue();
              } finally {
                  lock.unlock();
              }
          }
          private E dequeue() {
              // assert lock.getHoldCount() == 1;
              // assert items[takeIndex] != null;
              final Object[] items = this.items;//獲得隊列
              @SuppressWarnings("unchecked")
              E x = (E) items[takeIndex];//獲得出隊元素
              items[takeIndex] = null;//出隊槽位元素置為null
              //下一個被取出元素的索引+1,如果值等於長度,表示後面沒有元素了,需要從頭開始取出
              if (++takeIndex == items.length)
                  takeIndex = 0;
              count--;//數量-1
              if (itrs != null)//迭代器不為空
                  itrs.elementDequeued();//同時更新迭代器中的元素數據
              notFull.signal();//喚醒入隊線程
              return x;//返回出隊元素
          }
    3. 阻塞式和非阻塞式的結合:poll(long timeout, TimeUnit unit),出隊獲取元素,可以設置線程等待時間,如果超過指定時間隊列還是空的,則返回null;

      public E poll(long timeout, TimeUnit unit) throws InterruptedException {
              long nanos = unit.toNanos(timeout);//轉換成超時時間閥值
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();//加鎖
              try {
                  while (count == 0) {//隊列空了,等待
                      if (nanos <= 0)//超時了返回null
                          return null;
                      nanos = notEmpty.awaitNanos(nanos);//等待入隊填充元素
                  }
                  return dequeue();//返回出隊元素
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
  4. 移除元素remove:

    public boolean remove(Object o) {
            //要移除的元素為空返回false
            if (o == null) return false;
            //獲得隊列數組
            final Object[] items = this.items;
            final ReentrantLock lock = this.lock;
            lock.lock();//加鎖
            try {
                //隊列有元素
                if (count > 0) {
                    final int putIndex = this.putIndex;//獲得下一個被添加元素的索引
                    int i = takeIndex;//下一個被取出元素的索引
                    do {
                        if (o.equals(items[i])) {//從takeIndex下標開始,找到要被刪除的元素
                            removeAt(i);//移除
                            return true;
                        }
                        if (++i == items.length)//下一個被取出元素的索引+1並判斷是否等於隊列長度,如果是,表示需要從頭開始遍歷
                            i = 0;
                    } while (i != putIndex);//繼續查找,直到找到最後一個元素
                }
                return false;
            } finally {
                lock.unlock();//解鎖
            }
        }
    
      /**
       * 根據下標移除元素,那麼會分成兩種情況一個是移除的是隊首元素,一個是移除的是非隊首元素,移除隊首元素,就相當於出隊操作,
       * 移除非隊首元素那麼中間就有空位了,後面元素需要依次補上,然後如果是隊尾元素,那麼putIndex也就是插入操作的下標也就需要跟着移動。
       */
        void removeAt(final int removeIndex) {
            // assert lock.getHoldCount() == 1;
            // assert items[removeIndex] != null;
            // assert removeIndex >= 0 && removeIndex < items.length;
            final Object[] items = this.items;//獲得隊列
            if (removeIndex == takeIndex) {//移除的是隊首元素
                // removing front item; just advance
                items[takeIndex] = null;//隊首置為null
                if (++takeIndex == items.length)//下一個被取出元素的索引+1並判斷是否等於隊列長度
                    takeIndex = 0;
                count--;//數量-1
                if (itrs != null)//迭代器不為空
                    itrs.elementDequeued();//更新迭代器元素
            } else {//移除的不是隊首元素,而是中間元素
                // an "interior" remove
    
                // slide over all others up through putIndex.
                final int putIndex = this.putIndex;//下一個被添加元素的索引
                for (int i = removeIndex;;) {//對隊列進行遍歷,因為是隊列中間的值被移除了,所有後面的元素都要挨個遷移
                    int next = i + 1;//獲取移除元素的下一個坐標
                    if (next == items.length)//判斷是否等於隊列長度
                        next = 0;
                    if (next != putIndex) {//獲取移除元素的下一個坐標!=下一個被添加元素的索引,表示移除元素的索引後面有值
                        items[i] = items[next];//當前要移除的元素置為後面的元素,即對後面的元素往前遷移,覆蓋要移除的元素
                        i = next;//下一個遷移的索引
                    } else {//移除的元素是最後一個,後面沒有值了
                        items[i] = null;//移除元素,直接置為null
                        this.putIndex = i;//更新下一個被添加元素的索引
                        break;//結束
                    }
                }
                count--;//數量-1
                if (itrs != null)//迭代器不為空
                    itrs.removedAt(removeIndex);//更新迭代器元素
            }
            notFull.signal();//喚醒入隊線程,可以添加元素了
        }
  5. 清空元素clear:用於清空ArrayBlockingQueue,並且會釋放所有等待notFull條件的線程(存放元素的線程)

    public void clear() {
            final Object[] items = this.items;//獲得隊列
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                int k = count;//獲取元素數量
                if (k > 0) {//有元素,表示隊列不為空
                    final int putIndex = this.putIndex;//下一個被添加元素的索引
                    int i = takeIndex;//下一個被取出元素的索引
                    do {
                        items[i] = null;//對每個有元素的槽位置為null
                        if (++i == items.length)
                            i = 0;
                    } while (i != putIndex);//從有元素的第一個槽位開始遍歷,直到槽位元素為null
                    takeIndex = putIndex;//更新取出和添加的索引
                    count = 0;//數量更新為0
                    if (itrs != null)//迭代器不為空
                        itrs.queueIsEmpty();//更新迭代器為空
                    //若有等待notFull條件的線程,則逐一喚醒
                    for (; k > 0 && lock.hasWaiters(notFull); k--)
                        notFull.signal();//喚醒入隊線程,可以添加元素了
                }
            } finally {
                lock.unlock();
            }
        }
  6. offer(E e, long timeout, TimeUnit unit)和poll(long timeout, TimeUnit unit)裏面有awaitNanos,下面探討該功能實現:對當前線程或等待的入/出隊線程進行掛起,如果有入/出隊操作進行了喚醒出/入隊操作,則acquireQueued自旋獲取到鎖,然後出/入隊中的ReentrantLock是重入鎖,可以重入獲取到鎖進行出/入隊操作

        AbstractQueuedSynchronizer:
        //進行超時控制
        public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            //如果當前線程中斷了拋出中斷異常
            if (Thread.interrupted())
                throw new InterruptedException();
            //當前線程加入到Condition隊列中
            Node node = addConditionWaiter();
            //鎖釋放是否成功:釋放當前線程的lock,從AQS的隊列中移出
            int savedState = fullyRelease(node);
            //到達等待時間點
            final long deadline = System.nanoTime() + nanosTimeout;
            //中斷標識
            int interruptMode = 0;
            //當前節點是否在同步隊列中,否表示不在,進入掛起判斷操作,如果已經在Sync隊列中,則退出循環
            //那什麼時候會把當前線程又加入到Sync隊列中呢?當然是調用signal方法的時候,因為這裏需要喚醒之前調用await方法的線程,喚醒之後進行下面的獲取鎖等操作
            while (!isOnSyncQueue(node)) {
                //如果超時了,將線程掛起,然後停止遍歷
                if (nanosTimeout <= 0L) {
                    transferAfterCancelledWait(node);
                    break;
                }
                //如果等待時間間隔超過了1000,繼續掛起
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                //線程中斷了停止遍歷
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                //獲得剩餘的等待時間間隔
                nanosTimeout = deadline - System.nanoTime();
            }
            //結束掛起,acquireQueued自旋對當前線程的隊列出隊進行獲取鎖並返回線程是否中斷
            //如果線程被中斷,並且中斷的方式不是拋出異常,則設置中斷後續的處理方式設置為REINTERRUPT
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;//中斷標識更新為退出等待時重新中斷
            if (node.nextWaiter != null)//當前節點後面還有節點,多併發操作了
                unlinkCancelledWaiters();//從頭到尾遍歷Condition隊列,移除被cancel的節點
            //如果線程已經被中斷,則根據之前獲取的interruptMode的值來判斷是繼續中斷還是拋出異常
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();//返回剩餘等待時間
        }
  7. drainTo可以一次性獲取隊列中所有的元素,它減少了鎖定隊列的次數,使用得當在某些場景下對性能有不錯的提升

    //最多從此隊列中移除給定數量的可用元素,並將這些元素添加到給定collection中
        public int drainTo(Collection<? super E> c) {
            return drainTo(c, Integer.MAX_VALUE);
        }
        public int drainTo(Collection<? super E> c, int maxElements) {
            checkNotNull(c);//檢查是否為空
            if (c == this)//如果集合類型相同拋出參數異常
                throw new IllegalArgumentException();
            if (maxElements <= 0)//如果給定移除數量小於0,返回0,表示不做移除操作
                return 0;
            final Object[] items = this.items;//獲得隊列
            final ReentrantLock lock = this.lock;
            lock.lock();//加鎖
            try {
                int n = Math.min(maxElements, count);//獲得元素的最小數量
                int take = takeIndex;//下一個被取出元素的索引
                int i = 0;
                try {
                    while (i < n) {//遍歷移除和添加
                        @SuppressWarnings("unchecked")
                        E x = (E) items[take];//獲得移除元素
                        c.add(x);//元素添加到直到集合中
                        items[take] = null;//元素原先隊列位置置為null
                        if (++take == items.length)//如果取出索引到達尾部,從頭開始遍歷取出
                            take = 0;
                        i++;//移除的數量+1,如果達到了移除的最小數量,結束遍歷
                    }
                    return n;//返回一共移除並添加了多少個元素
                } finally {
                    // Restore invariants even if c.add() threw
                    if (i > 0) {//如果有移除操作
                        count -= i;//隊列元素數量-i
                        takeIndex = take;//重置下一個被取出元素的索引
                        if (itrs != null) {//迭代器不為空
                            if (count == 0)//隊列空了
                                itrs.queueIsEmpty();//迭代器清空
                            else if (i > take)//說明take中間變成0了,通知itr
                                itrs.takeIndexWrapped();
                        }
                        //喚醒在因為隊列滿而等待的入隊線程,最多喚醒i個,避免線程被喚醒了因為隊列又滿了而阻塞
                        for (; i > 0 && lock.hasWaiters(notFull); i--)
                            notFull.signal();
                    }
                }
            } finally {
                lock.unlock();
            }
        }

 

三.Logback 框架中異步日誌打印中ArrayBlockingQueue的使用

  1. 在高併發並且響應時間要求比較小的系統中同步打日誌已經滿足不了需求了,這是因為打日誌本身是需要同步寫磁盤的,會造成 響應時間 增加,如下圖同步日誌打印模型為:

  2. 異步模型是業務線程把要打印的日誌任務寫入一個隊列后直接返回,然後使用一個線程專門負責從隊列中獲取日誌任務寫入磁盤,其模型具體如下圖:

    • 如圖可知其實 logback 的異步日誌模型是一個多生產者單消費者模型,通過使用隊列把同步日誌打印轉換為了異步,業務線程調用異步 appender 只需要把日誌任務放入日誌隊列,日誌線程則負責使用同步的 appender 進行具體的日誌打印到磁盤;
  3. 接下來看看異步日誌打印具體實現,要把同步日誌打印改為異步需要修改 logback 的 xml 配置文件:

    <appender name="PROJECT" class="ch.qos.logback.core.FileAppender">
            <file>project.log</file>
            <encoding>UTF-8</encoding>
            <append>true</append>
    
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- daily rollover -->
                <fileNamePattern>project.log.%d{yyyy-MM-dd}</fileNamePattern>
                <!-- keep 7 days' worth of history -->
                <maxHistory>7</maxHistory>
            </rollingPolicy>
            <layout class="ch.qos.logback.classic.PatternLayout">
                <pattern>
                    <![CDATA[%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer},
                    ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n]]>
                </pattern>
            </layout>
        </appender>
    
        <appender name="asyncProject" class="ch.qos.logback.classic.AsyncAppender">
            <discardingThreshold>0</discardingThreshold>
            <queueSize>1024</queueSize>
            <neverBlock>true</neverBlock>
            <appender-ref ref="PROJECT" />
        </appender>
         <logger name="PROJECT_LOGGER" additivity="false">
            <level value="WARN" />
            <appender-ref ref="asyncProject" />
        </logger>
  4. 從上面可知 AsyncAppender 是實現異步日誌的關鍵,下面探究它的原理:

    1. 如上圖可知 AsyncAppender 繼承自 AsyncAppenderBase,其中後者具體實現了異步日誌模型的主要功能,前者只是重寫了其中的一些方法。另外從類圖可知 logback 中的異步日誌隊列是一個阻塞隊列, 後面會知道其實是一個有界阻塞隊列 ArrayBlockingQueue, 其中 queueSize 是有界隊列的元素個數默認為 256;
    2. worker則是工作線程,也就是異步打印日誌的消費者線程,aai則是一個appender的裝飾器,裡邊存放的同步日誌的appender,其中appenderCount記錄aai裡邊附加的同步appender的個數(這個和配置文件相對應,一個異步的appender對應一個同步的appender),neverBlock用來指示當同步隊列已滿時是否阻塞打印日誌線程(如果配置neverBlock=true,當隊列滿了之後,後面阻塞的線程想要輸出的消息就直接被丟棄,從而線程不會阻塞),discardingThreshold是一個閾值,當日誌隊列裡邊的空閑元素個數小於該值時,新來的某些級別的日誌就會直接被丟棄。
  5.  接下來看下何時創建的日誌隊列以及何時啟動的消費線程,這需要看下 AsyncAppenderBase 的 start 方法,該方法是在解析完畢配置 AsyncAppenderBase 的 xml 的節點元素后被調用 :

    public void start() {
            if (isStarted())
                return;
            if (appenderCount == 0) {
                addError("No attached appenders found.");
                return;
            }
            if (queueSize < 1) {
                addError("Invalid queue size [" + queueSize + "]");
                return;
            }
            // 創建一個ArrayBlockingQueue阻塞隊列,queueSize默認為256,創建阻塞隊列的原因是:防止生產者過多,造成隊列中元素過多,產生OOM異常
            blockingQueue = new ArrayBlockingQueue<E>(queueSize);
            // 如果discardingThreshold未定義的話,默認為queueSize的1/5
            if (discardingThreshold == UNDEFINED)
                discardingThreshold = queueSize / 5;
            addInfo("Setting discardingThreshold to " + discardingThreshold);
            // 將工作線程設置為守護線程,即當jvm停止時,即使隊列中有未處理的元素,也不會在進行處理
            worker.setDaemon(true);
            // 為線程設置name便於調試
            worker.setName("AsyncAppender-Worker-" + getName());
            // make sure this instance is marked as "started" before staring the worker Thread
            // 啟動線程
            super.start();
            worker.start();
        }
    1. logback 使用的隊列是有界隊列 ArrayBlockingQueue,之所以使用有界隊列是考慮到內存溢出問題,在高併發下寫日誌的 qps 會很高如果設置為無界隊列隊列本身會佔用很大內存,很可能會造成 內存溢出。
    2. 這裏消費日誌隊列的 worker 線程被設置為了守護線程,意味着當主線程運行結束並且當前沒有用戶線程時候該 worker 線程會隨着 JVM 的退出而終止,而不管日誌隊列裏面是否還有日誌任務未被處理。另外這裏設置了線程的名稱是個很好的習慣,因為這在查找問題的時候很有幫助,根據線程名字就可以定位到是哪個線程。
  6. 既然是有界隊列那麼肯定需要考慮如果隊列滿了,該如何處置,是丟棄老的日誌任務,還是阻塞日誌打印線程直到隊列有空餘元素那?下面看append 方法:

    protected void append(E eventObject) {
            // 判斷隊列中的元素數量是否小於discardingThreshold,如果小於的話,並且日誌等級小於info的話,則直接丟棄這些日誌任務
            if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
                return;
            }
            preprocess(eventObject);
            // 日誌入隊
            put(eventObject);
        }
        private boolean isQueueBelowDiscardingThreshold() {
            return (blockingQueue.remainingCapacity() < discardingThreshold);
        }
    
       // 子類重寫的方法   判斷日誌等級
        protected boolean isDiscardable(ILoggingEvent event) {
            Level level = event.getLevel();
            return level.toInt() <= Level.INFO_INT;
        }    
    • 日誌入隊put:從下面可知如果 neverBlock 設置為 false(默認為 false)則會調用阻塞隊列的 put 方法,而 put 是阻塞的,也就是說如果當前隊列滿了,如果再企圖調用 put 方法向隊列放入一個元素則調用線程會被阻塞直到隊列有空餘空間。這裡有必要提下其中blockingQueue.put(eventObject)當日誌隊列滿了的時候 put 方法會調用 await() 方法阻塞當前線程,如果其它線程中斷了該線程,那麼該線程會拋出 InterruptedException 異常,那麼當前的日誌任務就會被丟棄了。如果 neverBlock 設置為了 true 則會調用阻塞隊列的 offer 方法,而該方法是非阻塞的,如果當前隊列滿了,則會直接返回,也就是丟棄當前日誌任務。
      private void put(E eventObject) {
              // 判斷是否阻塞(默認為false),則會調用阻塞隊列的put方法
              if (neverBlock) {
                  blockingQueue.offer(eventObject);
              } else {
                  putUninterruptibly(eventObject);
              }
      }
          // 可中斷的阻塞put方法
          private void putUninterruptibly(E eventObject) {
              boolean interrupted = false;
              try {
                  while (true) {
                      try {
                          blockingQueue.put(eventObject);
                          break;
                      } catch (InterruptedException e) {
                          interrupted = true;
                      }
                  }
              } finally {
                  if (interrupted) {
                      Thread.currentThread().interrupt();
                  }
              }
          }
  7. 最後看下 addAppender 方法,可以看出,一個異步的appender只能綁定一個同步appender,這個appender會被放入AppenderAttachableImpl的appenderList列表裡邊

    public void addAppender(Appender<E> newAppender) {
            if (appenderCount == 0) {
                appenderCount++;
                addInfo("Attaching appender named [" + newAppender.getName() + "] to AsyncAppender.");
                aai.addAppender(newAppender);
            } else {
                addWarn("One and only one appender may be attached to AsyncAppender.");
                addWarn("Ignoring additional appender named [" + newAppender.getName() + "]");
            }
    }
  8. 通過上面我們已經分析完了日誌生產線程放入日誌任務到日誌隊列的實現,下面一起來看下消費線程是如何從隊列裏面消費日誌任務並寫入磁盤的,由於消費線程是一個線程,那就從 worker 的 run 方法看起(消費者,將日誌寫入磁盤的線程方法):

    class Worker extends Thread {
    
            public void run() {
                AsyncAppenderBase<E> parent = AsyncAppenderBase.this;
                AppenderAttachableImpl<E> aai = parent.aai;
    
                // loop while the parent is started 一直循環知道線程被中斷
                while (parent.isStarted()) {
                    try {// 從阻塞隊列中獲取元素,交由給同步的appender將日誌打印到磁盤
                        E e = parent.blockingQueue.take();
                        aai.appendLoopOnAppenders(e);
                    } catch (InterruptedException ie) {
                        break;
                    }
                }
    
                addInfo("Worker thread will flush remaining events before exiting. ");
                //執行到這裏說明該線程被中斷,則把隊列裡邊的剩餘日誌任務刷新到磁盤
                for (E e : parent.blockingQueue) {
                    aai.appendLoopOnAppenders(e);
                    parent.blockingQueue.remove(e);
                }
    
                aai.detachAndStopAllAppenders();
            }
        }
    • try邏輯中從日誌隊列使用 take 方法獲取一個日誌任務,如果當前隊列為空則當前線程會阻塞到 take 方法直到隊列不為空才返回,獲取到日誌任務後會調用 AppenderAttachableImpl 的 aai.appendLoopOnAppenders 方法,該方法會循環調用通過 addAppender 注入的同步日誌 appener 具體實現日誌打印到磁盤的任務。

四.參考:

  1. 公平鎖的使用場景:https://stackoverflow.com/questions/26455578/when-to-use-fairness-mode-in-java-concurrency
  2. 公平鎖和非公平鎖的區別的提問:https://segmentfault.com/q/1010000006439146
  3. 公平鎖不能保證線程調度的公平性:https://stackoverflow.com/questions/60903107/understanding-fair-reentrantlock-in-java
  4. logback異步日誌打印中的ArrayBlockingQueue的使用:https://my.oschina.net/u/4410397/blog/3428573

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

如果人生也能存檔——C#中的備忘錄模式

大家好,老胡又和大家見面了。首先承認今天的博客有點標題黨了,人生是沒有存檔,也沒有後悔葯的。有存檔和後悔葯的,那是遊戲,不知道這是不是遊戲讓人格外放鬆的原因之一。

今天恰逢端午放假,就讓我們來試着做一個小遊戲吧,順帶看看備忘錄模式是如何在這種情況下面工作的。

遊戲背景

這是一個簡單的打怪遊戲,有玩家,有怪獸,玩家作為主角光環,有如下三個特殊能力

  • 攻擊怪獸有暴擊幾率
  • 有幾率迴避怪獸攻擊
  • 可以自己治療一定生命值

遊戲實現

角色類
角色基類

首先是角色類,角色類提供玩家和怪獸最基本的抽象,比如血量、攻擊力、攻擊和治療。(對於怪獸來說,治療是沒有提供實現的,壞人肯定不能再治療了)

class Character
{
    public int HealthPoint { get; set; }
    public int AttackPoint { get; set; }        

    public virtual void AttackChracter(Character opponent)
    {
        opponent.HealthPoint -= this.AttackPoint;
        if (opponent.HealthPoint < 0)
        {
            opponent.HealthPoint = 0;
        }
    }

    public virtual void Cure()
    {
		//故意留空給子類實現
    }
}
玩家類

玩家實現了治療功能並且有暴擊幾率。

class Player : Character
{
    private float playerCriticalPossible;
    public Player(float critical)
    {
        playerCriticalPossible = critical;
    }

    public override void AttackChracter(Character opponent)
    {
        base.AttackChracter(opponent);
        Console.WriteLine("Player Attacked Monster");

        Random r = new Random();
        bool critical = r.Next(0, 100) < playerCriticalPossible * 100;
        if (critical)
        {
            base.AttackChracter(opponent);
            Console.WriteLine("Player Attacked Monster again");
        }
    }

    public override void Cure()
    {
        Random r = new Random();
        HealthPoint += r.Next(5, 10);
        Console.WriteLine("Player cured himself");
    }
}
怪獸類

怪獸沒有治療能力但是有一定的幾率丟失攻擊目標。

class Monster : Character
{
    private float monsterMissingPossible;
    public Monster(float missing)
    {
        monsterMissingPossible = missing;
    }

    public override void AttackChracter(Character opponent)
    {
        Random r = new Random();
        bool missing = r.Next(0, 100) < monsterMissingPossible * 100;
        if (missing)
        {
            Console.WriteLine("Monster missed it");
        }
        else
        {
            base.AttackChracter(opponent);
            Console.WriteLine("Monster Attacked player");
        }
    }
}
遊戲類

遊戲類負責實例化玩家和怪獸、記錄回合數、判斷遊戲是否結束,暴露可調用的公共方法給遊戲操作類。

class Game
{
    private Character m_player;
    private Character m_monster;
    private int m_round;
    private float playerCriticalPossible = 0.6f;
    private float monsterMissingPossible = 0.2f;
    
    public Game()
    {
        m_player = new Player(playerCriticalPossible)
        {
            HealthPoint = 15,
            AttackPoint = 2
        };
        m_monster = new Monster(monsterMissingPossible)
        {
            HealthPoint = 20,
            AttackPoint = 6
        };
    }

    public bool IsGameOver => m_monster.HealthPoint == 0 || m_player.HealthPoint == 0;

    public void AttackMonster()
    {            
        m_player.AttackChracter(m_monster);
    }

    public void AttackPlayer()
    {
        m_monster.AttackChracter(m_player);
    }

    public void CurePlayer()
    {
        m_player.Cure();
    }

    public void BeginNewRound()
    {
        m_round++;
    }

    public void ShowGameState()
    {
        Console.WriteLine("".PadLeft(20, '-'));
        Console.WriteLine("Round:{0}", m_round);
        Console.WriteLine("player health:{0}", "".PadLeft(m_player.HealthPoint, '*'));
        Console.WriteLine("monster health:{0}", "".PadLeft(m_monster.HealthPoint, '*'));
    }
}
遊戲操作類

在我們這個簡易遊戲中,沒有UI代碼,遊戲操作類負責在用戶輸入和遊戲中搭建一個橋樑,解釋用戶的輸入。

class GameRunner
{
    private Game m_game;
    public GameRunner(Game game)
    {
        m_game = game;
    }

    public void Run()
    {
        while (!m_game.IsGameOver)
        {
            m_game.BeginNewRound();
            bool validSelection = false;
            while (!validSelection)
            {
            	m_game.ShowGameState();
                Console.WriteLine("Make your choice: 1. attack 2. Cure");
                var str = Console.ReadLine();
                if (str.Length != 1)
                {
                    continue;
                }
                switch (str[0])
                {
                    case '1':
                        {
                            validSelection = true;
                            m_game.AttackMonster();
                            break;
                        }
                    case '2':
                        {
                            validSelection = true;
                            m_game.CurePlayer();
                            break;
                        }
                    default:
                        break;
                }
            }
            if(!m_game.IsGameOver)
            {
                m_game.AttackPlayer();
            }
        }            
    }
}
客戶端

客戶端的代碼就非常簡單了,只需要實例化一個遊戲操作類,然後讓其運行就可以了。

class Program
{
    static void Main(string[] args)
    {
        Game game = new Game();
        GameRunner runner = new GameRunner(game);
        runner.Run();
    }
}

試着運行一下,

看起來一切都好。

 

加上存檔

雖然遊戲可以正常運行,但是總感覺還是少了點什麼。嗯,存檔功能,一個遊戲沒有存檔是不健全的,畢竟,人生雖然沒有存檔,但是遊戲可是有的!讓我們加上存檔功能吧,首先想想怎麼設計。
 

需要存檔的數據

首先我們要明確,有哪些數據是需要存檔的,在這個遊戲中,玩家的生命值、攻擊力、暴擊率;怪獸的生命值、攻擊力和丟失率,遊戲的回合數,都是需要存儲的對象。
 

存檔定義

這是一個需要仔細思考的地方,一般來說,需要考慮以下幾個地方:

  • 存檔需要訪問一些遊戲中的私有字段,比如暴擊率,需要在不破壞遊戲封裝的情況下實現這個功能
  • 存檔自身需要實現信息隱藏,即除了遊戲,其他類不應該訪問存檔的詳細信息
  • 存檔不應該和遊戲存放在一起,以防不經意間遊戲破壞了存檔數據,應該有專門的類存放存檔

 

備忘錄模式出場

這個時候應該是主角出場的時候了。看看備忘錄模式的定義

在不破壞封閉的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態。這樣以後就可將該對象恢復到原先保存的狀態

再看看UML,

看起來完全符合我們的需求啊,Originator就是遊戲類,知道如何創造存檔和從存檔中恢復狀態,Memento類就是存檔類,Caretaker是一個新類,負責保存存檔。

經過思考,我們決定採取備忘錄模式,同時加入以下措施:

  • 將存檔定義為遊戲中的私有嵌套類,這樣存檔可以毫無壓力的訪問遊戲中的私有字段,同時外界永遠沒有辦法去實例化或者嘗試通過轉型來獲得這個類,完美的保護了存檔類
  • 存檔類是一個簡單的數據集合,不包含任何其他邏輯
  • 添加一個存檔管理器,可以放在遊戲操作類中,可以通過它看到我們當前有沒有存檔
  • 存檔放在存檔管理器中
  • 存檔實現一個空接口,在存檔管理器中以空接口形式出現,這樣外部類在訪問存檔的時候,僅能看到這個空接口。而在遊戲類內部,我們在使用存檔之前先通過向下轉型實現類型轉換(是的,向下轉型不怎麼好,但是偶爾可以用一下)
代碼實現
空接口
interface IGameSave
{

}
私有嵌套存檔類

該類存放在game裏面,無壓力地在不破壞封裝的情況下訪問game私有字段

private class GameSave : IGameSave
{
    public int PlayerHealth { get; set; }
    public int PlayerAttack { get; set; }
    public float PlayerCritialAttackPossible { get; set; }
    public int MonsterHealth { get; set; }
    public int MonsterAttack { get; set; }
    public float MonsterMissingPossible { get; set; }
    public int GameRound { get; set; }
}
創建存檔和從存檔恢復

game中添加創建存檔和從存檔恢復的代碼,在從存檔恢復的時候,使用了向下轉型,因為從存檔管理器讀出來的只是空接口而已

public IGameSave CreateSave()
{
    var save = new GameSave()
    {
        PlayerHealth = m_player.HealthPoint,
        PlayerAttack = m_player.AttackPoint,
        PlayerCritialAttackPossible = playerCriticalPossible,
        MonsterAttack = m_monster.AttackPoint,
        MonsterHealth = m_monster.HealthPoint,
        MonsterMissingPossible = monsterMissingPossible,
        GameRound = m_round
    };
    Console.WriteLine("game saved");
    return save;
}

public void RestoreFromGameSave(IGameSave gamesave)
{
    GameSave save = gamesave as GameSave;
    if(save != null)
    {
        m_player = new Player(save.PlayerCritialAttackPossible) { HealthPoint = save.PlayerHealth, AttackPoint = save.PlayerAttack };
        m_monster = new Player(save.MonsterMissingPossible) { HealthPoint = save.MonsterHealth, AttackPoint = save.MonsterAttack };
        m_round = save.GameRound;
    }
    Console.WriteLine("game restored");
}	
存檔管理器類

添加一個類專門管理存檔,此類非常簡單,只有一個存檔,要支持多存檔可以考慮使用List

    class GameSaveStore
    {
        public IGameSave GameSave { get; set; }
    }
在遊戲操作類添加玩家選項

首先在遊戲操作類中添加一個存檔管理器

private GameSaveStore m_gameSaveStore = new GameSaveStore();

接着修改Run方法添加用戶操作

public void Run()
{
    while (!m_game.IsGameOver)
    {
        m_game.BeginNewRound();
        bool validSelection = false;
        while (!validSelection)
        {
            m_game.ShowGameState();
            Console.WriteLine("Make your choice: 1. attack 2. Cure 3. Save 4. Load");
            var str = Console.ReadLine();
            if (str.Length != 1)
            {
                continue;
            }
            switch (str[0])
            {
                case '1':
                    {
                        validSelection = true;
                        m_game.AttackMonster();
                        break;
                    }
                case '2':
                    {
                        validSelection = true;
                        m_game.CurePlayer();
                        break;
                    }
                case '3':
                    {
                        validSelection = false;
                        m_gameSaveStore.GameSave = m_game.CreateSave();
                        break;
                    }
                case '4':
                    {
                        validSelection = false;
                        if(m_gameSaveStore.GameSave == null)
                        {
                            Console.WriteLine("no save to load");
                        }
                        else
                        {
                            m_game.RestoreFromGameSave(m_gameSaveStore.GameSave);
                        }
                        break;
                    }
                default:
                    break;
            }
        }
        if(!m_game.IsGameOver)
        {
            m_game.AttackPlayer();
        }
    }            
}

注意,上面的3和4是新添加的存檔相關的操作。試着運行一下。

看起來一切正常,這樣我們就使用備忘錄模式,完成了存檔讀檔的功能。

 

結語

這就是備忘錄模式的使用,如果大家以後遇到這種場景

  • 想要保存狀態,又不想破壞封裝
  • 需要把狀態保存到其他地方

那麼就可以考慮使用這個模式。

遊戲有存檔,人生沒存檔,願我們把握當下,天天努力。
祝大家端午安康,下次見。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

JAVA設計模式 3【創建型】理解工廠模式與抽象工廠模式

上一節我們已經學習了原型模式,稍微複習一下:通過重寫Object 類的clone() 方法實現淺克隆,淺克隆也要實現Cloneable 標記接口。而深克隆則是將對象通過序列化和反序列化 的方式進行創建和還原。

本小結將學習的是工廠模式,這個模式在平時是特別常用的,還需好好理解。我也將舉一些例子給大家

從生活出發

假設我們今天不想做飯,想出去吃飯、肯定選定一家好吃的川菜館,然後我們找好位置坐下,給廚師說,我想吃糖醋排骨。 稍微等待幾分鐘、菜就可以做好,然後給你呈上來。

這是一個很經典的例子,我們平時也經常有這樣的實際問題,你發現了么?

  • 我只需要告訴廚師菜名,我要吃啥就行了
  • 我不關注這個菜是怎麼生產的 new()

開始擼代碼

/**
 * 抽象產品 菜
 */
public interface FoodProduct {
    void show();
}
------------
/**
 * 具體產品
 */
public class HongShao implements FoodProduct {
    @Override
    public void show() {
        System.out.println("紅燒排骨");
    }
}
------------
public class TangCu implements FoodProduct {
    @Override
    public void show() {
        System.out.println("糖醋魚");
    }
}

創建廚房工廠

創建一個廚房類,廚房可以用來生產食物產品,我們只需要告訴廚房,這裡是通過id 編號的形式告訴廚房的。我們只需要告訴廚房所需要的食物 而不關心這個食物產品是如何創建出來的。

public class KitchenFactory {
    /**
     * id=1 上菜紅燒肉 id=2 糖醋魚
     * @param id
     */
    public FoodProduct cooking(int id) {
        if (1 == id) {
            return new HongShao();
        } else {
            return new TangCu();
        }
    }
}
KitchenFactory kitchen = new KitchenFactory();
FoodProduct food = kitchen.cooking(1);
food.show();
--------
紅燒排骨

理解工廠模式

通過這個簡單的例子,我們可以學習到:

  • 無需關注對象是如何創建的。只需通過指定的關鍵字 就能拿到我需要的產品,這就是簡單工廠模式。

抽象工廠

http://c.biancheng.net/view/1351.html

抽象工廠,就是簡單工廠的抽象版、如何理解呢?我們上面的工廠(廚房)它已經是一個確定的對象 了。而抽象工廠,則是在廚房 的基礎上,再次衍生出一個接口,我們的廚房 則是這個抽象類的一個具體實例化。

代碼源於生活

我又要開始舉栗子了。請細細品

我們都知道小米 小米既可以生產手機 也可以生產電器用品 那麼這就是一個很好的例子。

小米抽象工廠 是一個巨大的工廠,它裏面有小米手機工廠 以及小米電器工廠 而不同的工廠,則生產不同的產品

代碼教學開始

首先,我們得需要一個抽象工廠,這個工廠可以包含手機工廠和電器工廠。

public interface AbstractFactory {
    /**
     * 創建手機工廠
     * @return
     */
    PhoneFactory phoneFactory();
    /**
     * 創建電器工廠
     * @return
     */
    ElectricalFactory electricalFactory();
}

當然,手機工廠不知道是具體哪個工廠,反正它可以做一些事情,比如創建手機。

public interface PhoneFactory {
    /**
     * 手機工廠可以做的事情
     */
    void show();
}
-----------
public interface ElectricalFactory {
    /**
     * 電器工廠可以生產電器
     */
    void show();
}

上手實際創建一個小米工廠


public class XiaoMiFactory implements AbstractFactory {
    
    @Override
    public PhoneFactory phoneFactory() {
        return new XiaoMiPhoneFactory();
    }
    @Override
    public ElectricalFactory electricalFactory() {
        return new XiaoMiElectricalFactory();
    }
}
-------------
public class XiaoMiElectricalFactory implements ElectricalFactory {
    @Override
    public void show() {
        System.out.println("小米電器工廠可以生產電器。。比如小米掃地機器人");
    }
}
-------------
public class XiaoMiPhoneFactory implements PhoneFactory {
    @Override
    public void show() {
        System.out.println("小米手機工廠可以生產小米手機。。。");
    }
}

當然,小米工廠實現抽象工廠,那小米工廠就必須要包含兩個子工廠,手機工廠和電器工廠了。我們也可以創建一個華為工廠,其實是一樣的道理。

AbstractFactory factory = new XiaoMiFactory();

PhoneFactory phoneFactory = factory.phoneFactory();
phoneFactory.show();
--------
小米手機工廠可以生產小米手機。。。

我們從創建的小米工廠中拿出小米手機工廠 然後再執行手機工廠可以做事情,抽象工廠,就是在上面的簡單工廠的層次上進行了再次的抽象,將具體的工廠進行抽象。

假設我們按照上面的邏輯。對於一個工廠,我想要一部手機 我給工廠說一聲就行了。我不關心這個手機 是如何生產出來的。我該怎麼操作?

我稍微將之前的幾個接口作為稍微的改造。

public interface PhoneFactory {
    /**
     * 手機工廠可以做的事情
     */
    PhoneProduct show();
}
---------------
public class XiaoMiPhoneFactory implements PhoneFactory {
    @Override
    public PhoneProduct show() {
        return new PhoneProduct(1, "小米10 Pro");
    }
}
----------
//手機對象
public class PhoneProduct {

    private int id;

    private String name;
}

我們可以創建這樣一個訪問器,通過訪問器對象,將我們需要的對象名稱傳入就好比下單 它能自動匹配工廠,並且調用工廠創建產品 的方法,將我們需要的產品進行創建。

public class AbstractFactoryClient {

    public PhoneProduct createPhone(String name) {

        AbstractFactory factory = null;

        if ("xiaomi" == name) {
            factory = new XiaoMiFactory();
        } else {
            factory = new HuaweiFactory();
        }
        PhoneFactory phoneFactory = factory.phoneFactory();
        
        return phoneFactory.show();
    }
}

測試一下

AbstractFactoryClient factoryClient = new AbstractFactoryClient();
PhoneProduct product = factoryClient.createPhone("xiaomi");
System.out.println(product);
-----------
手機銘牌 編號:1,型號:小米10 Pro

小結

學習完本節,是否對於工廠模式和抽象工廠有了一個深入的了解呢?工廠模式其實在平時的代碼中,還是比較常用的。所以還是需要更加努力學習和使用!

代碼示例

https://gitee.com/mrc1999/Dev-Examples

參考

http://c.biancheng.net/view/1351.html

歡迎關注

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

kubernetes資源均衡器Descheduler

背景

Kubernetes中的調度是將待處理的pod綁定到節點的過程,由Kubernetes的一個名為kube-scheduler的組件執行。調度程序的決定,無論是否可以或不能調度容器,都由其可配置策略指導,該策略包括一組規則,稱為謂詞和優先級。調度程序的決定受到其在第一次調度時出現新pod時的Kubernetes集群視圖的影響。由於Kubernetes集群非常動態且狀態隨時間而變化,因此可能需要將已經運行的pod移動到其他節點,原因如下:

  • 一些節點不足或過度使用。
  • 原始調度決策不再適用,因為在節點中添加或刪除了污點或標籤,不再滿足pod / node親和性要求。
  • 某些節點發生故障,其pod已移至其他節點。
  • 新節點將添加到群集中。

因此,可能會在群集中不太理想的節點上安排多個pod。Descheduler根據其政策,發現可以移動並移除它們的pod。請注意,在當前的實現中,descheduler不會安排更換被驅逐的pod,而是依賴於默認的調度程序。

Descheduler二次調度

GitHub地址:https://github.com/kubernetes-sigs/descheduler

下面是重要的配置

  • configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: descheduler-policy-configmap
  namespace: kube-system
data:
  policy.yaml: |
    apiVersion: "descheduler/v1alpha1"
    kind: "DeschedulerPolicy"
    strategies:
      "RemoveDuplicates":
         enabled: true
      "RemovePodsViolatingInterPodAntiAffinity":
         enabled: true
      "LowNodeUtilization":
         enabled: true
         params:
           nodeResourceUtilizationThresholds:
             thresholds:
               "cpu" : 30
               "memory": 40
               "pods": 50
             targetThresholds:
               "cpu" : 20
               "memory": 25
               "pods": 15

RemoveDuplicates策略

該策略發現未充分利用的節點,並且如果可能的話,從其他節點驅逐pod,希望在這些未充分利用的節點上安排被驅逐的pod的重新創建。此策略的參數配置在nodeResourceUtilizationThresholds

節點的利用率低是由可配置的閾值決定的thresholdsthresholds可以按百分比為cpu,內存和pod數量配置閾值 。如果節點的使用率低於所有(cpu,內存和pod數)的閾值,則該節點被視為未充分利用。目前,pods的請求資源需求被考慮用於計算節點資源利用率。

還有另一個可配置的閾值,targetThresholds用於計算可以驅逐pod的潛在節點。任何節點,所述閾值之間,thresholds並且targetThresholds被視為適當地利用,並且不考慮驅逐。閾值targetThresholds也可以按百分比配置為cpu,內存和pod數量。

簡單的說:thresholds是沒有達到資源使用的node視為資源使用率低可以分配做為預選節點, targetThresholds是已經滿足這個條件的node資源緊張要把上面的pod遷移。

  • cronjob.yaml
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: descheduler-cronjob
  namespace: kube-system
spec:
  #定時任務時間可調 schedule:
"*/10 * * * *" concurrencyPolicy: "Forbid" jobTemplate: spec: template: metadata: name: descheduler-pod spec: priorityClassName: system-cluster-critical containers: - name: descheduler image: aveshagarwal/descheduler #image: us.gcr.io/k8s-artifacts-prod/descheduler:v0.10.0 volumeMounts: - mountPath: /policy-dir name: policy-volume command: - "/bin/descheduler" args: - "--policy-config-file" - "/policy-dir/policy.yaml" - "--v" - "3" restartPolicy: "Never" serviceAccountName: descheduler-sa volumes: - name: policy-volume configMap: name: descheduler-policy-configmap
  • rbac.yaml
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: descheduler-cluster-role
  namespace: kube-system
rules:
- apiGroups: [""]
  resources: ["events"]
  verbs: ["create", "update"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get", "watch", "list"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list", "delete"]
- apiGroups: [""]
  resources: ["pods/eviction"]
  verbs: ["create"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: descheduler-sa
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: descheduler-cluster-role-binding
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: descheduler-cluster-role
subjects:
  - name: descheduler-sa
    kind: ServiceAccount
    namespace: kube-system

kubectl apply -f 執行上面三個文件,查看日誌如有滿足再次調度條件的 會重新發起二次調度均衡node資源。

 

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

DNS篇(詳解DNS)

*文章來源:https://blog.egsec.cn/archives/601

*本文將主要說明:本文主要敘述什麼是DNS、域名的層級、DNS 解析過程、DNS的緩存時間、DNS 的記錄類型、DNS 報文結構、DNS劫持與HTTP劫持以及手動清理本地緩存的方法。

 

DNS屬於應用層。DNS即域名系統,其作用是將字符串域名解析成相對於的服務器IP地址,免除人們記憶IP地址的單調和苦惱,屬於為用戶排憂解難之舉,因此劃歸為應用層。DNS不屬於協議,它是域名解析。

 

什麼是DNS

DNS是 Domain Name System 的縮寫,也就是 域名解析系統,它的作用非常簡單,就是根據域名查出對應的 IP地址。

你可以把它想象成一本巨大的電話本,比如當你要訪問域名www.egsec.cn,首先要通過DNS查出它的IP地址是118.31.61.137。

域名層級

DNS 的解析過程中,需要對域名的層級有了解:

  • 根域名 :.root 或者 . ,通常是省略的
  • 頂級域名,如 .com,.cn 等
  • 次級域名,如 baidu.com 里的 baidu,這個是用戶可以進行註冊購買的
  • 主機域名,比如 baike.baidu.com 里的baike,這個是用戶可分配的 
主機名.次級域名.頂級域名.根域名
baike.baidu.com.root

DNS 解析過程

咱們以訪問 www.egsec.cn 這個域名為例,來看一看當你訪問 www.egsec.cn 時,會發生哪些事:

  1. 先查找本地 DNS 緩存(自己的電腦上),有則返回,沒有則進入下一步
  2. 查看本地 hosts 文件有沒有相應的映射記錄,有則返回,沒有則進入下一步
  3. 向本地 DNS 服務器(一般都是你的網絡接入服務器商提供,比如中國電信,中國移動)發送請求進行查詢,本地DNS服務器收到請求后,會先查下自己的緩存記錄,如果查到了直接返回就結束了,如果沒有查到,本地DNS服務器就會向DNS的根域名服務器發起查詢請求:請問老大, www.egsec.cn 的ip是啥?
  4. 根域名服務器收到請求后,看到這是個 .cn 的域名,就回信說:這個域名是由 .cn 老弟管理的,你去問他好了,這是.cn老弟的聯繫方式(ip1)。
  5. 本地 DNS 服務器接收到回信后,照着老大哥給的聯繫方式(ip1),馬上給 .cn 這個頂級域名服務器發起請求:請問 .cn 大大,www.egsec.cn 的ip 是啥?
  6. .cn 頂級域名服務器接收到請求后,看到這是 egsec.cn 的域名,就回信說:這個域名是 .egsec.cn 老弟管理的,你就去問他就行了,這是他的聯繫方式(ip2)
  7. 本地 DNS 服務器接收到回信后,按照前輩的指引(ip2),又向 .egsec.cn 這個權威域名服務器發起請求:請問 egsec.cn 大大,請問 www.egsec.cn 的ip是啥?
  8. egsec.cn 權威域名服務器接收到請求后,確認了是自己管理的域名,馬上查了下自己的小本本,把 www.egsec.cn 的ip告訴了 本地DNS服務器。
  9. 本地DNS服務器接收到回信后,非常地開心,這下總算拿到了www.egsec.cn的ip了,馬上把這個消息告訴了要求查詢的客戶(就是你的電腦)。由於這個過程比較漫長,本地DNS服務器為了節省時間,也為了盡量不去打擾各位老大哥,就把這個查詢結果偷偷地記在了自己的小本本上,方便下次有人來查詢時,可以快速回應。

總結起來就是三句話:

  • 從”根域名服務器”查到”頂級域名服務器”的NS記錄和A記錄(IP地址)
  • 從”頂級域名服務器”查到”次級域名服務器”的NS記錄和A記錄(IP地址)
  • 從”次級域名服務器”查出”主機名”的IP地址

DNS的緩存時間

上面的幾個步驟里,可以看到有兩個地方會緩存 DNS 的查詢記錄,有了緩存,在一定程度上會提高查詢效率,但同時在準確率上會有所損失。

因此我們在配置 DNS 解析的時候,會有一個 TTL 參數(Time To Live),意思就是這個緩存可以存活多長時間,過了這個時間,本地 DNS 就會刪除這條記錄,刪除了緩存后,你再訪問,就要重新走一遍上面的流程,獲取最新的地址。

DNS 的記錄類型

當我們在阿里雲買了一個域名后,可以配置我們主機域名解析規則,也就是 記錄。  

阿里雲域名雲解析(不管是哪個服務商都一樣):

常見的 DNS 記錄類型如下

  • A:地址記錄(Address),返回域名指向的IP地址。
  • NS:域名服務器記錄(Name Server),返回保存下一級域名信息的服務器地址。該記錄只能設置為域名,不能設置為IP地址。
  • MX:郵件記錄(Mail eXchange),返回接收电子郵件的服務器地址。
  • CNAME:規範名稱記錄(Canonical Name),返回另一個域名,即當前查詢的域名是另一個域名的跳轉,詳見下文。
  • PTR:逆向查詢記錄(Pointer Record),只用於從IP地址查詢域名,詳見下文。

DNS報文結構

  1. 事務 ID:DNS 報文的 ID 標識。對於請求報文和其對應的應答報文,該字段的值是相同的。通過它可以區分 DNS 應答報文是對哪個請求進行響應的。
  2. 標誌:DNS 報文中的標誌字段。
  3. 問題計數:DNS 查詢請求的數目。
  4. 回答資源記錄數:DNS 響應的數目。
  5. 權威名稱服務器計數:權威名稱服務器的數目。
  6. 附加資源記錄數:額外的記錄數目(權威名稱服務器對應 IP 地址的數目)

DNS劫持與HTTP劫持

通過上面的講解,我們都知道了,DNS 完成了一次域名到 IP 的映射查詢,當你在訪問 www.egsec.cn 時,能正確返回給你 我網站首頁的 ip。

但如果此時 DNS 解析出現了一些問題,當你想要訪問 www.egsec.cn 時,卻返回給你 www.baidu.com 的ip,這就是我們常說的 DNS 劫持。

與之容易混淆的有 HTTP 劫持。

什麼是 HTTP 劫持?

你一定見過當你在訪問 某個網站時,右下角也突然彈出了一個扎眼的廣告彈窗。這就是 HTTP 劫持。

藉助別人文章里的例子,它們倆的區別就好比是

  • DNS劫持是你想去機場的時候,把你給丟到火車站。
  • HTTP劫持是你去機場途中,有人給你塞小廣告。

DNS劫持 是如何產生的?

下面大概說幾種DNS劫持方法:

1.本機DNS劫持

攻擊者通過某些手段使用戶的計算機感染上木馬病毒,或者惡意軟件之後,惡意修改本地DNS配置,比如修改本地hosts文件,緩存等

2. 路由DNS劫持

很多用戶默認路由器的默認密碼,攻擊者可以侵入到路由管理員賬號中,修改路由器的默認配置

3.攻擊DNS服務器

直接攻擊DNS服務器,例如對DNS服務器進行DDOS攻擊,可以是DNS服務器宕機,出現異常請求,還可以利用某些手段感染dns服務器的緩存,使給用戶返回來的是惡意的ip地址

如何在本地查詢 DNS 解析結果?

nslookup命令:

命令格式:nslookup [查詢的域名] [指定DNS服務器]

你也可以指定公網的域名服務器進行查詢,比如常見的 114.114.114.114

手動清理DNS緩存

MacOS:

sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

Windows:

$ ipconfig /flushdns

Linux:

使用NSCD的DNS緩存
$ sudo /etc/init.d/nscd restart

# 服務器或者路由器使用DNSMASQ
$ sudo dnsmasq restart

  

DNS詳解篇完

轉發請註明出處(EG Blog:blog.egsec.cn),謝謝!

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

世界最快螞蟻 步頻是波爾特10倍以上

摘錄自2019年10月17日自由時報報導

報導,德國烏爾姆大學(University of Ulm University)生物行為專家普費弗(Sarah Pfeffer)所率領的團隊,發現撒哈拉銀蟻是世界上最快的螞蟻,每秒秒速將近1公尺,看起來雖然不多,但驚人步頻(跑步時腳步交換的頻率)可是世界百米紀錄保持人波爾特(Usain Bolt)的10倍以上。

撒哈拉銀蟻可以用每秒47步跑完85.5公分,這距離達到了牠們體長的108倍之多,如果拿家貓的體型來比喻的話,撒哈拉銀蟻的速度大約是貓貓以時速193公里奔跑。這是因為牠們全速奔跑時,6隻腿會一起騰空再落地,看起來就像是在短暫飛行,在每次換步時,銀蟻每條腿的觸地時間不到7毫秒。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

「敢檢討核電海嘯對策就開除你喔!」 解析處處矛盾的福島核災判決

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

綠色認證棕櫚油需要支持 監督機構訂新規 企業採購量不足將受罰

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

台塑美國德州廠污染案 台塑同意15.33億和解

摘錄自2019年10月16日自由時報報導

台塑集團德州廠遭居民指控排放塑膠顆粒、污染水資源,聯邦地區法院法官在今年6月裁定,台塑違反廢棄物排放許可證和聯邦淨水法,恐面臨鉅額罰款;台塑當時否認非法傾倒,聲稱排放的塑膠顆粒並未超過許可證允許數量。

《美聯社》15日報導,非營利組織Texas RioGrande Legal Aid(TRLA)週二宣布與台塑達成和解協議,並表示基於雙方同意的判決,台塑同意實行「零排放」並清理既有的汙染;協議還需要經過法官批准,通過後,5000萬美元將在5年內用於改善當地河川的水質。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!