初識Redis的數據類型HyperLogLog

前提

未來一段時間開發的項目或者需求會大量使用到Redis,趁着這段時間業務並不太繁忙,抽點時間預習和複習Redis的相關內容。剛好看到博客下面的UVPV統計,想到了最近看書裏面提到的HyperLogLog數據類型,於是花點時間分析一下它的使用方式和使用場景(暫時不探究HyperLogLog的實現原理)。RedisHyperLogLog數據類型是Redid 2.8.9引入的,使用的時候確保Redis版本>= 2.8.9

HyperLogLog簡介

基數計數(cardinality counting),通常用來統計一個集合中不重複的元素個數。一個很常見的例子就是統計某個文章的UVUnique Visitor,獨立訪客,一般可以理解為客戶端IP)。大數據量背景下,要實現基數計數,多數情況下不會選擇存儲全量的基數集合的元素,因為可以計算出存儲的內存成本,假設一個每個被統計的元素的平均大小為32bit,那麼如果統計一億個數據,佔用的內存大小為:

  • 32 * 100000000 / 8 / 1024 / 1024 ≈ 381M

如果有多個集合,並且允許計算多個集合的合併計數結果,那麼這個操作帶來的複雜度可能是毀滅性的。因此,不會使用BitmapTree或者HashSet等數據結構直接存儲計數元素集合的方式進行計數,而是在不追求絕對準確計數結果的前提之下,使用基數計數的概率算法進行計數,目前常見的有概率算法以下三種:

  • Linear Counting(LC)
  • LogLog Counting(LLC)
  • HyperLogLog Counting(HLL)

所以,HyperLogLog其實是一種基數計數概率算法,並不是Redis特有的,Redis基於C語言實現了HyperLogLog並且提供了相關命令API入口。

Redis的作者Antirez為了紀念Philippe Flajolet對組合數學和基數計算算法分析的研究,所以在設計HyperLogLog命令的時候使用了Philippe Flajolet姓名的英文首字母PF作為前綴。也就是說,Philippe Flajolet博士是HLL算法的重大貢獻者,但是他其實並不是RedisHyperLogLog數據類型的開發者。遺憾的是Philippe Flajolet博士於2011年3月22日因病在巴黎辭世。這個是Philippe Flajolet博士的維基百科照片:

Redis提供的HyperLogLog數據類型的特徵:

  • 基本特徵:使用HyperLogLog Counting(HLL)實現,只做基數計算,不會保存元數據
  • 內存佔用:HyperLogLog每個KEY最多佔用12K的內存空間,可以計算接近2^64個不同元素的基數,它的存儲空間採用稀疏矩陣存儲,空間佔用很小,僅僅在計數基數個數慢慢變大,稀疏矩陣佔用空間漸漸超過了閾值時才會一次性轉變成稠密矩陣,轉變成稠密矩陣之後才會佔用12K的內存空間。
  • 計數誤差範圍:基數計數的結果是一個標準誤差(Standard Error)為0.81%的近似值,當數據量不大的時候,得到的結果也可能是一個準確值。

內存佔用小(每個KEY最高佔用12K)是HyperLogLog的最大優勢,而它存在兩個相對明顯的限制:

  • 計算結果並不是準確值,存在標準誤差,這是由於它本質上是用概率算法導致的。
  • 不保存基數的元數據,這一點對需要使用元數據進行數據分析的場景並不友好。

HyperLogLog命令使用

Redis提供的HyperLogLog數據類型一共有三個命令APIPFADDPFCOUNTPFMERGE

PFADD

PFADD命令參數如下:

PFADD key element [element …]

支持此命令的Redis版本是:>= 2.8.9
時間複雜度:每添加一個元素的複雜度為O(1)

  • 功能:將所有元素參數element添加到鍵為keyHyperLogLog數據結構中。

PFADD命令的執行流程如下:

PFADD命令的使用方式如下:

127.0.0.1:6379> PFADD food apple fish
(integer) 1
127.0.0.1:6379> PFADD food apple
(integer) 0
127.0.0.1:6379> PFADD throwable
(integer) 1
127.0.0.1:6379> SET name doge
OK
127.0.0.1:6379> PFADD name throwable
(error) WRONGTYPE Key is not a valid HyperLogLog string value.

雖然HyperLogLog數據結構本質是一個字符串,但是不能在String類型的KEY使用HyperLogLog的相關命令。

PFCOUNT

PFCOUNT命令參數如下:

PFCOUNT key [key …]

支持此命令的Redis版本是:>= 2.8.9
時間複雜度:返回單個HyperLogLog的基數計數值的複雜度為O(1),平均常數時間比較低。當參數為多個key的時候,複雜度為O(N),N為key的個數。

  • PFCOUNT命令使用單個key的時候,返回儲存在給定鍵的HyperLogLog數據結構的近似基數,如果鍵不存在, 則返回0
  • PFCOUNT命令使用key的時候,返回儲存在給定的所有HyperLogLog數據結構的並集的近似基數,也就是會把所有的HyperLogLog數據結構合併到一個臨時的HyperLogLog數據結構,然後計算出近似基數。

PFCOUNT命令的使用方式如下:

127.0.0.1:6379> PFADD POST:1 ip-1 ip-2
(integer) 1
127.0.0.1:6379> PFADD POST:2 ip-2 ip-3 ip-4
(integer) 1
127.0.0.1:6379> PFCOUNT POST:1
(integer) 2
127.0.0.1:6379> PFCOUNT POST:1 POST:2
(integer) 4
127.0.0.1:6379> PFCOUNT NOT_EXIST_KEY
(integer) 0

PFMERGE

PFMERGE命令參數如下:

PFMERGE destkey sourcekey [sourcekey ...]

支持此命令的Redis版本是:>= 2.8.9
時間複雜度:O(N),其中N為被合併的HyperLogLog數據結構的數量,此命令的常數時間比較高

  • 功能:把多個HyperLogLog數據結構合併為一個新的鍵為destkeyHyperLogLog數據結構,合併后的HyperLogLog的基數接近於所有輸入HyperLogLog的可見集合(Observed Set)的並集的基數。
  • 命令返回值:只會返回字符串OK

PFMERGE命令的使用方式如下

127.0.0.1:6379> PFADD POST:1 ip-1 ip-2
(integer) 1
127.0.0.1:6379> PFADD POST:2 ip-2 ip-3 ip-4
(integer) 1
127.0.0.1:6379> PFMERGE POST:1-2 POST:1 POST:2
OK
127.0.0.1:6379> PFCOUNT POST:1-2
(integer) 4

使用HyperLogLog統計UV的案例

假設現在有個簡單的場景,就是統計博客文章的UV,要求UV的計數不需要準確,也不需要保存客戶端的IP數據。下面就這個場景,使用HyperLogLog做一個簡單的方案和編碼實施。

這個流程可能步驟的先後順序可能會有所調整,但是要做的操作是基本不變的。先簡單假設,文章的內容和統計數據都是後台服務返回的,兩個接口是分開設計。引入Redis的高級客戶端Lettuce依賴:

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

編碼如下:

public class UvTest {

    private static RedisCommands<String, String> COMMANDS;

    @BeforeClass
    public static void beforeClass() throws Exception {
        // 初始化Redis客戶端
        RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
        RedisClient redisClient = RedisClient.create(uri);
        StatefulRedisConnection<String, String> connect = redisClient.connect();
        COMMANDS = connect.sync();
    }

    @Data
    public static class PostDetail {

        private Long id;
        private String content;
    }

    private PostDetail selectPostDetail(Long id) {
        PostDetail detail = new PostDetail();
        detail.setContent("content");
        detail.setId(id);
        return detail;
    }

    private PostDetail getPostDetail(String clientIp, Long postId) {
        PostDetail detail = selectPostDetail(postId);
        String key = "puv:" + postId;
        COMMANDS.pfadd(key, clientIp);
        return detail;
    }

    private Long getPostUv(Long postId) {
        String key = "puv:" + postId;
        return COMMANDS.pfcount(key);
    }

    @Test
    public void testViewPost() throws Exception {
        Long postId = 1L;
        getPostDetail("111.111.111.111", postId);
        getPostDetail("111.111.111.222", postId);
        getPostDetail("111.111.111.333", postId);
        getPostDetail("111.111.111.444", postId);
        System.out.println(String.format("The uv count of post [%d] is %d", postId, getPostUv(postId)));
    }
}

輸出結果:

The uv count of post [1] is 4

可以適當使用更多數量的不同客戶端IP調用getPostDetail(),然後統計一下誤差。

題外話-如何準確地統計UV

如果想要準確統計UV,則需要注意幾個點:

  • 內存或者磁盤容量需要準備充足,因為就目前的基數計數算法來看,沒有任何算法可以在不保存元數據的前提下進行準確計數。
  • 如果需要做用戶行為分析,那麼元數據最終需要持久化,這一點應該依託於大數據體系,在這一方面筆者沒有經驗,所以暫時不多說。

假設在不考慮內存成本的前提下,我們依然可以使用Redis做準確和實時的UV統計,簡單就可以使用Set數據類型,增加UV只需要使用SADD命令,統計UV只需要使用SCARD命令(時間複雜度為O(1),可以放心使用)。舉例:

127.0.0.1:6379> SADD puv:1 ip-1 ip-2
(integer) 2
127.0.0.1:6379> SADD puv:1 ip-3 ip-4
(integer) 2
127.0.0.1:6379> SCARD puv:1
(integer) 4

如果這些統計數據僅僅是用戶端展示,那麼可以採用異步設計:

在體量小的時候,上面的所有應用的功能可以在同一個服務中完成,消息隊列可以用線程池的異步方案替代。

小結

這篇文章只是簡單介紹了HyperLogLog的使用和統計UV的使用場景。總的來說就是:在(1)原始數據量巨大,(2)內存佔用要求盡可能小,(3)允許計數存在一定誤差並且(4)不要求存放元數據的場景下,可以優先考慮使用HyperLogLog進行計數。

參考資料:

  • antirez-Redis new data structure: the HyperLogLog
  • Redis Commands
  • 維基百科

(本文完 c-3-d e-a-20191117)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

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

【其他文章推薦】

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

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

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

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

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

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

容器技術之Docker資源限制

  上一篇我們聊到了docker容器的單機編排工具docker-compose的簡單使用,回顧請參考https://www.cnblogs.com/qiuhom-1874/p/13121678.html;今天我們主要來聊一聊docker容器的資源限制;通常情況下我們啟動一個docker容器,其內存和CPU都是同宿主機一樣大,這意味着該容器和宿主機共享相同大小的內存和CPU資源;這樣一來容器正常情況下沒有什麼問題,假如容器里運行的進程特別愛吃內存,很可能存在把宿主機上的內存全部吃掉,觸發內核OOM,從而導致docker daemon直接被內核殺死;為了避免這樣的尷尬局面,對啟動容器我們有必要對容器的資源進行限制;

  所謂OOM就是當系統上的應用申請內存資源時,發現申請不到內存,這個時候Linux內核就會啟動OOM,內核將給系統上的所有進程進行評分,通過評分得分最高的進程就會被系統第一個幹掉,從而騰出一些內存空間,如果騰出的內存空間還是不夠該應用使用,它會繼續殺得分第二高的,直到應用有足夠的內存使用;一旦發生OOM,任何進程都有可能被殺死,包括docker daemon在內,為此,docker特定調整了docker daemon的oom優先級,以免發生oom被內核殺死,但是容器的oom優先級並未做任何調整;

  那麼對於內存資源來講,在啟動為容器時,我們可以通過一些選項來指定容器的內存相關設置;如下圖

  提示:-m 或 –memory 用來指定容器最大能夠使用的內存大小,默認情況不指定表示共享物理宿主機的內存大小;–memory-swap 用來指定容器的內存和交換內存的總大小;對於這個參數的取值比較詭異;待會在說吧;–memory-swappiness該選項用來指定容器使用交換內存的傾向性,swap啟用有個好處就是在內存不夠使用的情況,它可以臨時頂替一部分,但是性能會急劇下降;所以数字越大越早使用交換內存,数字越小越晚使用交換內存,取值在0-100之間;0不代表不是用交換內存,0表示能不用交換內存,則不用,但是在迫不得已的情況還是會使用的,100表示只要有一絲可以使用交換內存的希望,就使用交換內存;通常情況在運行容器的主機上不建議使用swap設備;swap交換分區如果一旦被激活,系統性能會急劇下降,建議直接禁用;–memory-reservation該選項用來指定給系統保留的內存空間大小;–kernel-memory用來指定給內核保留的內存大小;–oom-kill-disable該選項用於指定當發生oom時,是否禁用因oom而殺死該容器進程;

  提示:通常情況–memory-swap這個選項必須同–memory選項一起使用,不可用單獨使用;

  示例:限制容器使用最大內存為256M

[root@docker_registry ~]# docker run --name test --rm -m 256M lorel/docker-stress-ng --vm 2

  提示:以上命令表示啟動一個名為test的容器,限制該容器最大使用內存大小為256M;lorel/docker-stress-ng這個進行用來壓測容器;–vm表示同時使用多少進程來做壓測;

  驗證:用docker stats看看我們啟動test容器是否只能使用256M內存?

  提示:從上面的結果可以看到,在我們啟動容器時,使用-m指定內存大小的容器limit的值就是我們指定的值,而對於沒有用-m指定的容器,默認就是同宿主機內存大小一樣;

  對於CPU來講,默認情況啟動容器時,不限制CPU的資源,此時容器是共享宿主機的CPU資源,也就是說默認情況宿主機上有幾顆cpu核心,啟動的容器就有多少顆核心;對於CPU這種可壓縮資源,不會像內存那樣,如果CPU滿載,也不會導致某個容器崩潰,原因是因為cpu是可壓縮資源;而不同於內存,內存屬於不可壓縮資源,如果申請不到內存,就會出現異常,出現oom;對啟動容器來限制cpu資源,通常也是使用選項來限定;如下圖

  提示:–cpus用來指定容器能夠使用的最大cpu核心數,例如–cpus=1.5,就表示該容器最大能夠使用1.5核的CPU資源,如果宿主機上有4顆CPU核心,那麼該容器最多可把1.5顆核心跑滿;這樣說吧,如果宿主機上有4顆核心,那麼該容器如果使用–cpus限定為1.5,那麼該容器就只能使用宿主機上的百分之150的核心;–cpu-period 和–cpu-quota該選項在docker1.13以後基本廢棄;–cpuset-cpus該選項用於指定容器能夠在哪些CPU上運行;如果宿主機上有4顆CPU,–cpuset-cpus=2,3就表示該容器只能使用第2號cpu和第3號cpu;–cpu-shares該選項用於指定容器使用cpu的比例;比如宿主機上只有一個容器,而該容器啟動時指定–cpu-shares=1024,則表示,如果沒有其他容器,則它可以使用宿主機上的所有cpu資源,如果有第二個容器啟動時,指定cpu-shares=512,那麼第一個容器會從原來使用整個宿主機的cpu變為使用整個宿主機的cpu的2/3;以此類推,如果有第三個,第四個,他們使用cpu資源都是按照給定的比例動態調整;

  示例:第一個容器使用–cpu-shares=256;第二個容器使用–cpu-shares=512,看看當第一個容器啟動后,看看cpu使用情況,然後第二個容器啟動后再看看cpu使用情況

  提示:可以看到當第一個容器啟動時,雖然設置的cpu-shares=256,但是它還是把所有核心幾乎都跑滿了;我們在跑一個容器看看,看看第二個容器啟動后,第一個容器的cpu使用情況是否有變化?

  提示:從上面的結果看,t1和t2的cpu使用比例大概是1比2;總量還是400%並沒有變化;

  示例:設置容器使用1.5個CPU核心

  提示:從上面的結果可以看到使用–cpus來限定容器使用的CPU資源,默認它會在每顆黑核心上都要使用一部分,但是重量不會超過150%;

  示例:限定容器使用CPU核心,只能在0號和3號核心上使用;

  提示:從上面的結果可以看到,限定t1容器只能使用0號和3號CPU后,1號和2號就基本不會被使用,總量也不會增加;

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

【其他文章推薦】

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

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

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

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

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

【Spring註解驅動開發】面試官:如何將Service注入到Servlet中?朋友又栽了!!

寫在前面

最近,一位讀者出去面試前準備了很久,信心滿滿的去面試。沒想到面試官的一個問題把他難住了。面試官的問題是這樣的:如何使用Spring將Service注入到Servlet中呢?這位讀者平時也是很努力的,看什麼源碼啊、多線程啊、高併發啊、設計模式啊等等。沒想到卻在一個很簡單的問題上栽了跟頭,這就說明學習知識要系統化,要有條理,切忌東學一點,西記一點,否則,到頭來,啥也學不到。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

如何實現將Service注入到Servlet中??

這裏,我們列舉兩種解決方法(推薦使用第二種)

方法一:

直接重寫Servlet的Init()方法,代碼如下:

public void init(ServletConfig servletConfig) throws ServletException {
	ServletContext servletContext = servletConfig.getServletContext();
	WebApplicationContext webApplicationContext = WebApplicationContextUtils
			.getWebApplicationContext(servletContext);
	AutowireCapableBeanFactory autowireCapableBeanFactory = webApplicationContext
			.getAutowireCapableBeanFactory();
	autowireCapableBeanFactory.configureBean(this, BEAN_NAME);
}

這裏的BEAN_NAME即為我們需要注入到Spring容器中的服務,但這並不是一個好的方法,因為我們需要在每一個Servlet中都進行這樣的操作。

方法二:

我們可以寫一個類似於“org.springframework.web.struts.DelegatingRequestProcessor”的委託的Bean,然後通過配置的方法把我們的服務注入到servlet中,具體方法如下,

Step 1:編寫委託類DelegatingServletProxy

package com.telek.pba.base.util;

import java.io.IOException;
import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * 以下是類似org.springframework.web.struts.DelegatingRequestProcessor的一個委託
 * 用於通過配置的方法,在Servlet中注入Service
 * @author binghe
 * */
public class DelegatingServletProxy extends GenericServlet{
    private static final long serialVersionUID = 1L;
    private String targetBean;
    private Servlet proxy;

   @Override
   public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
   		proxy.service(req, res);
   }
     /**
      * 初始化
      */
      public void init() throws ServletException {
          this.targetBean = getServletName();
          getServletBean();
          proxy.init(getServletConfig());
      }

     /**
      * 獲取Bean
      */
      private void getServletBean() {
          WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
          this.proxy = (Servlet) wac.getBean(targetBean);
      }
}

Step 2:修改Web.xml配置

在純Servlet模式下,我們的配置方式如下(以下由於代碼高亮插件的問題,請將代碼中的#替換成尖括號)

<servlet>
  <description>活動發起模塊活動查詢分頁Servlet</description>
  <display-name>launchActivityQueryServlet</display>
  <servlet-name>LaunchActivityQueryServlet</servlet-name>
  <servlet-class>com.telek.pba.launch.servlet.LaunchActivityQueryServlet</servlet-class>
<servlet>

<servlet-mapping>
  <servlet-name>LaunchActivityQueryServlet</servlet-name>
  <url-pattern>/servlet/launch/LaunchActivityQueryServlet</url-pattern>
</servlet-mapping>
</servlet>

如果採用我們這種代理的方法,則配置應該修改為:

<servlet>
  <description>活動發起模塊活動查詢分頁Servlet</description>
  <display-name>launchActivityQueryServlet</display>
  <servlet-name>launchActivityQueryServlet</servlet-name>
  <servlet-class>com.telek.pba.base.util.DelegatingServletProxy</servlet-class>
<servlet>

<servlet-mapping>
  <servlet-name>launchActivityQuery</servlet-name>
  <url-pattern>/servlet/launch/LaunchActivityQueryServlet</url-pattern>
</servlet-mapping>
</servlet> 

注意:默認情況下,Servlet的配置中,LaunchActivityQuery的首字母一般為大寫,而我們的標題中已註明,我們採用Spring的註解模式,如果是自動掃描註解的話,默認情況下,註解的value值為首字母小寫,即:launchActivityQuery,因此,在我們新的配置中,要注意將首字母改為小寫,否則會報無法找到Bean的錯誤。

Step 3:至此,我們就可以像SSH的注入方式一樣,注入Servlet了,以下是個小示例:

package com.telek.pba.launch.servlet;

import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import com.telek.pba.base.model.PbaUserInfo;
import com.telek.pba.launch.dao.IPbaActivityInfoCurrentDAO;

@Component
public class LaunchActivityQueryServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	 
	//注入IPbaActivityInfoCurrentDAO
	@Resource
	private IPbaActivityInfoCurrentDAO pbaActivityInfoCurrentDAO;

	public LaunchActivityQueryServlet() {
		super();
	}
	 
	public void destroy() {
		super.destroy(); // Just puts "destroy" string in log
		// Put your code here
	}
	 
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		//sth to do
	}
	 
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		//sth to do
	}
	 
	public void init() throws ServletException {
		// Put your code here
	}
}

最後,請留心在Spring配置文件中,配置上自動掃描包的路徑:

<context:component-scan base-package="com.telek.pba.*.dao.impl,
 					com.telek.pba.*.service.impl,
 					com.telek.pba.*.servlet"/>

大功告成!

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

小師妹學JVM之:GC的垃圾回收算法

目錄

  • 簡介
  • 對象的生命周期
  • 垃圾回收算法
    • Mark and sweep
    • Concurrent mark sweep (CMS)
    • Serial garbage collection
    • Parallel garbage collection
    • G1 garbage collection
    • Z Garbage Collection
  • 怎麼選擇
  • 總結

簡介

JVM的重要性不言而喻了,如果把java的應用程序比作一輛跑車,那麼JVM就是這輛車的發動機,沒有它,java程序就成了空中樓閣,無根浮萍。而在JVM中有一塊內存區域叫做運行時數據區域,存儲了運行時所需要的所有對象,而Heap Area則是其中最大的一塊。

內存畢竟不是無限的,所以就需要一種機制來將不再使用的對象進行回收,這種機制就是今天我們要講的GC。

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

對象的生命周期

小師妹:F師兄,你相信這個世界有輪迴嗎?

師兄我是一個堅定的無神論者,活在當下就好了,何必操心後面的輪迴呢?

小師妹:F師兄,這個你就不懂了,意識是組成腦的原子群的一種組合模式,我們大腦的物質基礎和一塊石頭沒有什麼不同。當我們掌握大腦的組合方式,然後重構,我們的意識就重現了,這就是輪迴。這可是量子理論中提到的觀念哦。

哇,小師妹什麼時候這麼厲害了,都開始探討這麼高深的話題了。F師兄我實在是跟不上節奏啊。

小師妹,F師兄,我是怕你尷尬,想引出java對象的生命周期這個話題嘛。

量子理論我不熟,java對象我還沒怕過誰。

對象的生命周期其實很簡單:創建,使用中,最後被銷毀。

  1. 創建對象

舉個最簡單的創建對象的例子:

Object obj = new Object();

對象創建的時候,將會為該對象分配特定的空間。

  1. 使用對象

對象創建之後,就可以被其他的對象使用,如果其他的對象有使用該對象,那麼我們成為該對象被引用了。

  1. 對象銷毀

當一個對象沒有被其他對象引用的時候,我們就稱為該對象可以被回收了。在Java中,對象的回收是由GC來負責的。

垃圾回收算法

小師妹:F師兄,我覺得垃圾回收好像挺簡單的,我們為每個對象維持一個指針計數器,每引用一次就加一,這樣不就可以實現垃圾回收器了嗎?

底層原理是這麼一個道理,但是JVM需要一種更加高效的算法來保證垃圾回收的效率,同時也不會影響正在運行的程序。

接下來我們將會介紹一下,在JVM中比較常用幾個垃圾回收算法:

Mark and sweep

Mark and sweep是最最簡單的垃圾回收算法,簡單點講,它可以分為兩個步驟:

  1. 標記live對象

標記live對象聽起來很簡單,就是掃描堆中的對象,看這些對象是否被引入。

但是這裡有一個問題,如果是兩個對象互相引用的時候,而這兩個對象實際上並沒有被外部的對象所引用,那麼這兩個對象其實是應該被回收的。所以我們還需要解決一個關鍵性的問題:從哪裡開始掃描的問題。

JVM定義了一些Root對象,從這些對象開始,找出他們引用的對象,組成一個對象圖。所有在這個圖裡面的對象都是有效的對象,反之不在對象圖中的對象就應該被回收。有效的對象將會被Mark為alive。

這些Root對象包括:正在執行的方法中的本地對象和輸入參數。活動的線程,加載類中的static字段和JNI引用。

注意,這種遍歷其實是有個缺點的,因為為了找到對象圖中哪些對象是live的,必須暫停整個應用程序,讓對象變成靜止狀態,這樣才能構建有效的對象圖。後面我們會介紹更加有效的垃圾回收算法。

  1. 刪除對象

掃描對象之後,我們就可以將未標記的對象刪除了。

刪除有三種方式,第一種方式是正常刪除。但是正常刪除會導致內存碎片的產生。所以第二種方式就是刪除之後進行壓縮,以減少內存碎片。還有一種方式叫做刪除拷貝,也就是說將alive的對象拷貝到新的內存區域,這樣同樣可以解決內存碎片的問題。

Concurrent mark sweep (CMS)

在講CMS之前,我們先講一下垃圾回收器中的Eden,Old和Survivor space幾個大家應該都很熟悉的分代技術。

Young Gen被劃分為1個Eden Space和2個Suvivor Space。當對象剛剛被創建的時候,是放在Eden space。垃圾回收的時候,會掃描Eden Space和一個Suvivor Space。如果在垃圾回收的時候發現Eden Space中的對象仍然有效,則會將其複製到另外一個Suvivor Space。

就這樣不斷的掃描,最後經過多次掃描發現任然有效的對象會被放入Old Gen表示其生命周期比較長,可以減少垃圾回收時間。

之後要將的幾個垃圾回收器,除了ZGC,其他都使用的是分代的技術。

好了,現在繼續講CMS,CMS是mark and swap的升級版本,它使用多個線程來對heap區域進行掃描,從而提升效率。

CMS在Young Generation中使用的是mark-copy,而在Old Generation主要使用的是mark-sweep。

使用CMS的命令很簡單:

-XX:+UseConcMarkSweepGC

上面是列出的一些CMS的調優參數。

Serial garbage collection

Serial garbage collection使用單一的線程來進行垃圾回收操作,其好處就是不需要和其他的線程進行交互。如果你是單核的CPU,那麼最好就是選擇Serial garbage collection,因為你不能充分利用多核的好處。同樣的它也常常用在比較小型的項目中。

Serial garbage collection在Young Generation中使用的是mark-copy,而在Old Generation主要使用的是 mark-sweep-compact。

下面是開啟命令:

-XX:+UseSerialGC

Parallel garbage collection

和serial GC類似,它在Young Generation中使用的是mark-copy,而在Old Generation主要使用的是 mark-sweep-compact。不同的是它是并行的。

可以通過下面的命令來指定併發的線程:

-XX:ParallelGCThreads=N

如果你是多核處理器,那麼Parallel GC可能是你的選擇。

Parallel GC是JDK8中的默認GC。而在JDK9之後, G1是默認的GC。

使用下面的命令來開啟Parallel GC:

-XX:+UseParallelGC

G1 garbage collection

為什麼叫G1呢,G1=Garbage First,它是為替換CMS而生的,最早出現在java7中。

G1將heap區域劃分成為多個更小的區域,每個小區域都被標記成為young generation 或者old generation。從而運行GC在更小的範圍里運行,而不是影響整個heap區域。

可以使用下面的命令來開啟:

-XX:+UseG1GC 

Z Garbage Collection

ZGC是一個可擴展的,低延遲的GC。ZGC是併發的,而且不需要停止正在運行的線程。

使用下面的命令來開啟:

 -XX:+UseZGC 

ZGC是在JDK11中被引入的。

怎麼選擇

小師妹:F師兄,你講了這麼多個GC,到底我該用哪個呢?

高射炮不能用來打蚊子,所以選擇合適的GC才是最終要的。這裏F師兄給你幾個建議:

  1. 如果你的應用程序內存本來就很小,那麼使用serial collector : -XX:+UseSerialGC.

  2. 如果你的程序運行在單核的CPU上,並且也沒有程序暫停時間的限制,那麼還是使用serial collector : -XX:+UseSerialGC.

  3. 如果對峰值期的性能要求比較高,但是對程序暫停時間沒多大的要求,那麼可以使用 parallel collector: -XX:+UseParallelGC。

  4. 如果更加關注響應時間,並且GC的對程序的暫停時間必須要小,那麼可以使用-XX:+UseG1GC。

  5. 如果響應時間非常重要,並且你在使用大容量的heap空間,那麼可以考慮使用ZGC: -XX:UseZGC。

總結

本文介紹了幾種GC的算法,大家可以根據需要選用。

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/jvm-gc-algorithms/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

Node.js躬行記(4)——自建前端監控系統

  這套前端監控系統用到的技術棧是:React+MongoDB+Node.js+Koa2。將性能和錯誤量化。因為自己平時喜歡吃菠蘿,所以就取名叫菠蘿系統。其實在很早以前就有這個想法,當時已經實現了前端的參數搜集,只是後台遲遲沒有動手,也就拖着。

  • 目前完成的還只是個雛形,僅僅是搜集了錯誤和相關的性能參數。

  • 後台樣式採用了封裝過的matrix。

  • 分析功能還很薄弱,只是做了簡單的演示,並且各種基礎功能還有待完善。

  • 後面打算強化數據分析,並且還要實現錯誤的回放機制,思路的話以前也調研過,參考之前的一篇文章

  現在的這個系統還只能算是個玩具,後期還需要雕琢雕琢。下面是這套系統的目錄結構。

├── pingapple --------------------------------- 菠蘿監控系統
│   ├── client -------------------------------- 系統的前端部分
│   ├── sdk ----------------------------------- 信息搜集代碼庫
│   ├── server -------------------------------- 系統的後端部分

一、SDK

1)primus.js

  在之前的《前端頁面性能參數搜集》一文中,詳細記載了各類性能指標的計算規則,並整理到了primus.js中。

  本次將在primus.js的基礎上做適當的修改,包括刪除代理、測速、資源信息等功能,改變部分性能指標的計算規則,例如從瀏覽器發起HTTP請求算起,忽略瀏覽器重定向的時間等。

2)錯誤處理

  完善錯誤處理,將錯誤分成三類:runtime、load和Promise。在window的error事件中,處理前兩種錯誤。像img元素載入的圖片地址不存在,就會執行formatLoadError()函數;像變量未定義,就會執行formatRuntimerError()函數。

window.addEventListener("error", function (event) {
    var errorTarget = event.target;
    // 過濾 target 為 window 的異常
    if (
      errorTarget !== window &&
      errorTarget.nodeName &&
      LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]
    ) {
      handleError(formatLoadError(errorTarget));
    } else {
      handleError(
        formatRuntimerError(
          event.message,
          event.filename,
          event.lineno,
          event.colno,
          event.error
        )
      );
    }
  }, true
);

  將window綁定unhandledrejection事件后,就會在Promise被拒絕且沒有reject的回調函數時觸發。

window.addEventListener(
  "unhandledrejection",
  function (event) {
    // console.log('Unhandled Rejection at:', event.promise, 'reason:', event.reason);
    handleError({
      type: ERROR_PROMISE,
      desc: event.reason,
      stack: "no stack"
    });
  },
  true
);

3)初始化

  由於要計算白屏時間,DOM時間等,所以位置不能隨便放,得要放在head的最後面。

<head>
  <script>
    window.pineapple || (pineapple = {});
    pineapple.param = {
      "token": "dsadasd2323dsad23dsada"
    };
  </script>
  <script src="js/pineapple.js"></script>
</head>

二、服務端

1)Koa

  Koa是由Express原班人馬打造的Web輕量框架,通過組合各種中間件來避免繁瑣的回調函數嵌套,當前使用的版本是V2。

npm install --save koa

  使用的Koa腳手架:koa-generator,創建項目的結構,並且在此基礎上做了調整(目錄如下所示)。暫時還不會用到靜態資源和視圖層。

npm install -g koa-generator
├── server --------------------------------- 服務端
│   ├── bin -------------------------------- 命令
│   ├── config ----------------------------- 配置目錄
│   ├── controllers ------------------------ MVC中的邏輯層
│   ├── db --------------------------------- MVC中的數據層
│   ├── public ----------------------------- 靜態資源
│   ├── routes ----------------------------- 路由
│   ├── utils ------------------------------ 工具庫
│   ├── views ------------------------------ MVC中的視圖層
│   ├── app.js ----------------------------- 入口文件

  為了區分開發環境和生產環境,通過cross-env統一不同系統設置環境變量的方式。

npm install --save cross-env

  package.json中的命令如下,添加了環境配置。

"scripts": {
  "start": "node bin/www",
  "dev": "cross-env NODE_ENV=development ./node_modules/.bin/nodemon bin/www",
  "prd": "cross-env NODE_ENV=production pm2 start bin/www"
}

  prd按字面意思應該是生產環境的命令,其中使用了pm2,默認沒有安裝。還沒部署過Node.js,還不清楚裏面有多少坑。

npm install --save pm2

2)MongoDB

  MongoDB是一個開源的非關係型數據庫(圖1是下載界面),既沒有表、行等概念,也沒有固定的模式和結構,所有的數據以文檔(一個對象)的形式存儲。但其使用方式和關係型數據庫相似,並且還支持對數據建立索引,適用於高併發讀寫、海量數據存儲和實時分析等。

圖1

  注意,在安裝時默認會下載MongoDB Compress(一個可視化的MongoDB工具),默認下載會非常慢,建議自行下載,該工具的界面還是蠻清爽的,如圖2所示。

圖2

  在Mac上配置MongoDB比較麻煩,不像Windows那樣一件安裝,需要一些步驟,廢了點力氣才裝好,下面是執行的命令。

sudo mongod --dbpath=/Users/pw/data

3)Mongoose

  Mongoose是MongoDB的一個ORM(Object-Document Mapper,對象文檔映射)工具,可在Node.js環境中執行,封裝了MongoDB操作文檔的常用方法,包括引入數據庫連接(connect),定義模型(model),聲明文檔結構(scheme),實例化模型等操作數據庫的方法。

npm install --save mongoose

  借鑒了以前PHP數據分層的思想,單獨分離出數據庫的連接,並抽象通用的Model層(如下所示)。

const mongoose = require("./db");
class Mongodb {
  constructor(name, schema) {
    //聲明結構
    const mySchema = new mongoose.Schema(schema, { typeKey: "$type" });
    this.model = mongoose.model(name, mySchema);
  }
  //保存
  save(obj) {
    obj.created = Date.now();         //日期
    const doc = new this.model(obj);
    return new Promise((resolve, reject) => {
      doc.save((err, row) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(row);
      });
    });
  }
}
module.exports = {
  model: Mongodb,
  mongoose
};

4)路由

  由於發送的地址是一張gif圖片,因此在處理路由時,返回本地的一張gif圖,如下所示,圖像地址得是絕對路徑,否則無法讀取。

router.get('/pa.gif', async (ctx, next) => {
  const ctr = new indexController();
  ctr.collect(ctx);
  const url = path.resolve(__dirname, "../public/images/blank.gif");
  ctx.body = fs.readFileSync(url);    //空白gif圖
});

5)代理分析

  在接收參數的時候分析代理所帶的信息,例如瀏覽器、操作系統、設備等。使用的是一個第三方庫:UAParser.js,四年前就關注過,當時GitHub上只有1K多個關注量,現在已經翻了4倍。

npm install --save ua-parser-js

6)假數據

  製作一套合適的假數據,新增命令“npm run data”,初始化數據,便於展示。

三、後台

1)UI

  後台模板採用了之前封裝過的Matrix,但不會依賴Bootstrap框架。

  將整個頁面分成五塊,分別是導航、側邊欄、麵包屑、底部欄以及主體。

  安裝react-router的history,用於路由。

npm install --save history

  期間也會安裝各類依賴包,例如不支持在類中直接聲明屬性等。

  在使用的過程中,ESLint會不時的彈出各種錯誤和警告,期間就不停的修改問題或查找相關配置忽略部分限制。

  後台的側邊欄和麵包屑等部分,會隨着URL的不同而發生狀態變化,本來想用多頁實現,但配置要改很多,就依然做成一個SPA,只是稍微做了些改動。

  組件庫採用了流行的Ant Design,調用了按鈕、單選框、日期等組件。

npm install --save antd

  圖表庫使用的是ECharts,目前只用到了折線圖和餅圖。在引用圖表時,為了優化構建,採取了按需引用的手段。

npm install --save echarts

2)項目管理

  首先建立一個項目,然後才能分析該項目的性能和錯誤,如圖3所示。

圖3

  用彈框的形式來創建項目,使用了Ant Design的Model、Form等組件,如圖4所示。

圖4

3)性能分析

  在第一個折線圖標籤中的過濾條件包括項目、字段、日期等,性能指標按平均值呈現,可看到每個性能指標的趨勢,如圖5所示。

圖5

  按分時日統計性能平均數,在MongoDB中計算。原先創建日期是以時間戳的形式存儲的,為了便於使用Aggregate,改成字符串形式。碰到一個坑,MongoDB中的Date類型採用的是格林尼治時間,而不是當前時區的時間,也就是說存在數據庫中的時間會比當前時間早8小時。

  在第二個列表標籤中,可以詳細看到每條記錄的信息,包括代理、網絡等,便於在了解趨勢的前提下,獲悉更為細節的內容,如圖6所示。

圖6

  點擊ajax那一列,可彈出具體的異步請求信息,如圖7所示。

圖7

4)錯誤分析

  有三個標籤,第一個也是折線圖,描繪的是某個時間的錯誤個數;第二個是錯誤列表,會給出具體的錯誤信息,如圖8所示。

圖8

  第三個是餅圖,餅圖主要體現的是發生錯誤的瀏覽器分佈情況(如圖9所示),點擊某一塊可查看瀏覽器的具體版本(如圖10所示)。

圖9

圖10

 

 

【參考資料】
PerformanceTiming

unhandledrejection 處理沒有顯式捕獲的 Promise 異常

狼書(卷2)

Node-區分環境

Koa從零搭建到Api實現—項目部署

koa如何連接MongoDB

Koa2進階學習筆記

如何計算首屏加載時間 

Mongoose Schema Error: “Cast to string failed for value” when pushing object to empty array

Support for the experimental syntax ‘classProperties’ isn’t currently enabled

Template string failing with Cannot read property ‘range’ of null

Disallow JSX props spreading (react/jsx-props-no-spreading)

TypeError: Cannot read property ‘range’ of null from template-curly-spacing

echarts項目的優化

使用 happypack 提升 Webpack 項目構建速度

mac下的mongoDB的安裝和啟動

安裝MongoDB報錯 mkdir: /data/db: Read-only file system

$sum mongoose

 

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

【其他文章推薦】

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

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

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

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

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

網頁設計最專業,超強功能平台可客製化

python工業互聯網應用實戰2—從需求開始

前言:隨着國家工業2025戰略的推進,工業互聯網發展將會提速,將迎來一個新的發展時期,越來越多的企業開始逐步的把產線自動化,去年年底投產的小米亦庄的智能工廠就是一個熱議的新聞。小米/華為智能工廠只能說是中國製造2025的一個代表,產業轉型和製造升級,筆者從事的企業領域就越到越來越多的(製造)企業開始悄悄的自動化/智能化。這裏肯定有國家政策推動的大背景,同時,也有着企業自身不斷提高生產率的“剛需”。

  本序列我們也從一個需求問題開始;然後,拆解需求(問題);其次,解決拆解需求(問題)的點;再次,通過的不斷技術摸索和迭代過程找到一個個合理的解決需求的方法和手段,最終,完成了這個需求(問題)的項目實戰。我們會在文中描述演化過程、方法論、過程保證機制和實用的工具等,最終帶着大家完成這樣一個實際項目需求的項目過程項目涉及的Django的多基礎知識請大家閱讀筆者早期的《Python開發入門與實戰系列》。

  本文的過程採用python3.6和django2.1版本

項目需求:這是一個簡版物流自動化倉庫實例,倉庫控制系統(以下簡稱WCS)需要調度AGV小車運送一個實托盤(搬運單元)從1樓入庫站台經由提升機搬運到2樓指定的倉庫貨位。 

1.需求分析

  倉庫控制系統(以下簡稱WCS)需要調度AGV小車運送一個實托盤(搬運單元)從1樓入庫站台經由提升機搬運到2樓指定的倉庫貨位。這句簡要描述常常是我們在項目URS看到的需求描述,接下來我們需要對拿到的需求進行分析,形成一個個可度量的開發功能點。

1.1.用例說明

  需求用例說明文檔能夠很好的對需求進行詳細的分析,主要的包含內容包括:前置條件、事件流程和後置條件(執行結果)用例描述如下錶,通過用例分析我們能夠很好的把握需求的具體事件執行流程,並通過文檔清晰的描述出來,便於後期設計編碼時作為開發設計人員的指導。

  經過團隊頭腦風暴討論,大家基本上達成了如下錶的需求用例說明,我們把這個調度設備的搬運過程設計成一個序列順序執行的步驟(子任務),這些子任務對應着設備的執行分解動作。

用例編碼

1.1

執行角色

WCS

用例名稱

托盤入庫用例

前提條件

  1. WMS已完成任務貨位的分配,任務核心信息:托盤條碼、源地址、目標地址;
  2. AGV不能跨樓層作業,1樓與2樓分別是不同的AGV小車執行任務;

需求描述

1 WCS分解搬運任務成3個設備作業

2 調度AGV入庫站台搬運托盤到提升機1樓門口工位;

3 調度提升機到1樓並打開廊門

4 調度AGV提升機1樓門口工位到提升機並卸貨,返回提升機門口工位;

5 調度提升機1樓提升到2樓並開門;

6 調度AGV進入提升機並載貨搬運到2樓門口工位;

7 調度提升機關門;

8 調度AGV從2樓門口工位搬運到庫存貨位。

備選過程

 

擴展過程

 

 

原始需求描述

參見 《XXX URS》

特殊需求

後置條件

WCS調度相關設備完成實托盤從入庫站台搬運到庫存貨位,反饋結果給WMS 

2. 需求功能點

  從上面的用例說明的需求描述事件流程來看,我們需要先把這一趟搬運任務分解成設備子任務,並串行的方式順序下達到設備執行,任務清單如下錶:

序號

作業描述

執行設備

1

調度AGV入庫站台搬運托盤到提升機1樓門口工位

1AGV

2

調度提升機到1樓並打開門

提升機

3

調度AGV提升機1樓門口工位到提升機並卸貨,返回提升機門口工位

1AGV

4

調度提升機1樓提升到2樓並開門

提升機

5

調度AGV進入提升機並載貨搬運到2樓門口工位

2AGV

6

調度提升機關門

提升機

7

調度AGV從2樓門口工位搬運到庫存貨位

2AGV

  也就是說這一趟搬運任務,WCS需要分解成7個設備作業子任務,並順序下達給相應的執行設備執行,最終完成任務的執行(當前的任務劃分粒度實際對接AGV和提升機廠家來說會有調整,最終以上步驟會依賴與實際對接的情況,但是主流程不會有太大變化)。 

   經過分析從1樓入庫站台運送托盤到二樓某個指定貨位這樣一個任務,系統需要分解成7個子任務,下達給設備順序執行。系統活動圖如下:

3. 活動圖

  經過分析從1樓入庫站台運送托盤到2樓某個指定貨位這樣一個任務,系統需要分解成7個子任務,下達給設備順序執行。我們還可以通過UML活動圖來進一步詳細的描述作業的執行順序如下圖: 

    從圖中我們可以看出來一次入庫任務,系統分解為7個設備子任務(作業)來執行完整的托盤入庫流程,只有所有子任務(作業)執行完成,托盤的入庫才算完成。

 4. 功能模塊

  對於這樣一個看似簡單的需求來說,包含兩大主要功能模塊

    1. 任務分解:依據物理設備處理任務的條件,對任務進行分解,任務分解的粒度是設備能夠識別並執行(動作)
    2. 任務調度:任務調度就是按照順序執行的邏輯,把任務順序和逐一下達給設備 

  這裏也有幾個基本邏輯就是,設備在某一個時間點上只能執行一個子任務,只有這個任務執行完畢後方能下達新的子任務。多重任務邏輯只會導致設備無法完成任務(不知道到底該執行那個動作)。

 4.1. 實體關係

  我們從上面需求分析整理當前至少包括2個實體,包含的屬性(字段)如下: 

1任務

任務ID,任務號,源地址(從哪兒),目標地址(到哪兒),開始時間,結束時間,狀態 

2子任務:

子任務ID,任務ID,源地址(從哪兒 上一個子任務的目標地址), 目標地址(到哪兒 下一個子任務的源地址), 執行機構,開始時間,結束時間,狀態

5. 小節

  本章我們從一個需求問題開始,然後經過需求分析,把需求問題分解為功能點和數據實體,實體是下一步我們設計表或ORM model的基礎原型,上面的實體只是一個初步的需求分析主要字段要求,實體屬性(字段)會設計時會增加特定的其它屬性(字段)。 下一章節我們將描繪如何把這個需求逐步演化到模型設計。

 

 

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

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

GitHub 熱點速覽 Vol.24:程序員自我增值,優雅賺零花錢

摘要:升職加薪,出任 CTO,迎娶白富美/高帥富,走向人生巔峰是很多人的夢想。在本期的熱點速覽中你將了解自由作者 Easy 如何優雅賺取零花錢的方法,以及定投改變命運 —— 讓時間陪你慢慢變富。說到程序員自我增值,除了優雅賺錢之外,還可以研究下各種生活中小工具的代碼實現,例如,收錄 20+ Web 小應用的 vanillawebprojects。將技術應用在生活中點滴,展現你的技術輔助日常“肝”口袋妖精,或者偶爾用技術給自己生活添加點小樂趣,用遺傳算法製作一個繪製圖像過程的小玩具。

以下內容摘錄自微博@HelloGitHub 的 GitHub Trending,選項標準:新發布 | 實用 | 有趣,根據項目 release 時間分類,發布時間不超過 7 day 的項目會標註 New,無該標誌則說明項目 release 超過一周。由於本文篇幅有限,還有部分項目未能在本文展示,望周知

  • 本文目錄
      1. 本周特推
      • 1.1 遺傳算法玩具:genetic-drawing
      • 1.2 馬斯克火箭:SpaceX-API
      1. GitHub Trending 周榜
      • 2.1 Go 語法書:go-ast-book
      • 2.2 數據庫好搭檔:xgenecloud
      • 2.3 前端小玩意:vanillawebprojects
      • 2.4 統計代碼:lihang-code
      • 2.5 Poke 輔助工具:Pokedex
      • 2.6 高性能框架:Fastapi
      • 2.7 JS 面經:javascript-questions
      1. 本周 GitHub Trending #程序員增值# 主題的主力軍
      • 3.1 優雅賺錢:howto-make-more-money
      • 3.2 定投改變命運:regular-investing-in-box
      • 3.3 機器學習課程個人筆記:Coursera-ML-AndrewNg-Notes
      1. 推薦閱讀

1. 本周特推

1.1 遺傳算法玩具:genetic-drawing

本周 star 增長數:1200+

Newgenetic-drawing 作者在 2017 年做的模仿給定目標圖像的繪製過程的玩具項目,效果見下圖。項目受到互聯網上許多基因繪製示例的啟發,由於項目深受歡迎,作者便在近日將其開源。

GitHub 地址→https://github.com/anopara/genetic-drawing

1.2 馬斯克火箭:SpaceX-API

本周 star 增長數:900+

SpaceX-API 是一個用於火箭、核心艙、太空艙、發射台和發射數據的開源 REST API。技術棧

  • 部署在美國中部 Linode 服務器上
  • 使用了 Nodejs 的 Koa 框架
  • 使用了 Redis、Nginx 和 Cloudflare 進行內容緩存
  • 使用了 Jest 和 Supertest 做測試
  • 使用了 Circle CI 進行持續集成/部署
  • 所有的數據存儲在 MongoDB Atlas 3 節點的副本集集群中
  • 使用 mongodump 在晚上進行數據備份

GitHub 地址→https://github.com/r-spacex/SpaceX-API

2. GitHub Trending 周榜

2.1 Go 語法書:go-ast-book

本周 star 增長數:1000+

go-ast-book 是一個 Go 語法樹入門項目。讓我們語法樹這個維度重新審視 Go 語言程序,我們將得到創建Go語言本身的技術。本書簡單介紹語法樹相關包的使用。

GitHub 地址→https://github.com/chai2010/go-ast-book

2.2 數據庫好搭檔:xgenecloud

本周 star 增長數:800+

New xgenecloud 是一個能即時生成任何數據庫上的 REST 和 GraphQL API 工具,它支持 MySQL、PostgreSQL、MsSQL、SQLite、MariaDB。特性:

  • 為現有數據庫生成 REST API
  • 提供用於調試的 GUI
  • 生成的 API 均可基於 Serverless 部署在任意雲平台

GitHub 地址→https://github.com/xgenecloud/xgenecloud

2.3 前端小玩意:vanillawebprojects

本周 star 增長數:1100+

vanillawebprojects 收錄了用前端技術(Javascript、CSS、HTML5)開發的 20+ 款小應用,包括:表單驗證、匯率計算、打字遊戲、語音閱讀、新年倒計時等等。

GitHub 地址→https://github.com/bradtraversy/vanillawebprojects

2.4 統計代碼:lihang-code

本周 star 增長數:10900+

《統計學習方法》可以說是機器學習的入門寶典,許多機器學習培訓班、互聯網企業的面試、筆試題目,很多都參考這本書。本項目收錄了該書的所有代碼實現,特別是監督學習方法,包括感知機、k 近鄰法、樸素貝恭弘=叶 恭弘斯法、決策樹、邏輯斯諦回歸與支持向量機、提升方法、em 算法、隱馬爾可夫模型和條件隨機場等。

GitHub 地址→https://github.com/fengdu78/lihang-code

2.5 Poke 輔助工具:Pokedex

本周 star 增長數:500+

NewPokedex 使用基於 MVVM 架構的 Dagger Hilt、Motion、Coroutines、Jetpack 開發的 Poke(口袋妖精)輔助工具。這個項目專註實現依賴注入的新庫,支持從網絡獲取數據,並通過存儲庫模式集成數據庫中的持久化數據。

GitHub 地址→https://github.com/skydoves/Pokedex

2.6 高性能框架:Fastapi

本周 star 增長數:1300+

Fastapi 是一個基於 python 的框架,該框架鼓勵使用 Pydantic 和 OpenAPI 進行文檔編製,使用 Docker 進行快速開發和部署以及基於 Starlette 框架進行的簡單測試。特性:

  • 高性能
  • 快速編寫代碼:將功能開發的速度提高大約 200% 至 300%
  • 錯誤更少:減少約40%的人為錯誤(開發人員)
  • 直觀:強大的編輯器支持。完成無處不在。調試時間更少
  • 簡易:旨在易於使用和學習。減少閱讀文檔的時間
  • 短:最小化代碼重複。每個參數聲明中的多個功能,更少的錯誤
  • 健壯:獲取可用於生產的代碼,具有自動交互式文檔。
  • 基於標準:基於(並完全兼容)API的開放標準

GitHub 地址→https://github.com/tiangolo/fastapi

2.7 JS 面經:javascript-questions

本周 star 增長數:800+

從基礎到高級,JavaScript Questions 收錄了 JS 相關的面試題及解法。

GitHub 地址→https://github.com/lydiahallie/javascript-questions

3. 本周 GitHub Trending #程序員增值#主題的主力軍

在本期主題模塊,小魚乾這裏選取了 3 個和增值相關的小工具,希望能提高你生活、工作的幸福值。

3.1 優雅賺錢:howto-make-more-money

howto-make-more-money 是一個程序員@Easy 現身講述優雅的掙零花錢的項目,雖然是一個教你如何賺零花錢的項目,但是通過閱讀本賺零花錢小書你可理清自己的核心資源,以及如何創造資產。

GitHub 地址→https://github.com/easychen/howto-make-more-money

3.2 定投改變命運:regular-investing-in-box

定投改變命運 —— 讓時間陪你慢慢變富。regular-investing-in-box 這本書要講的是普通人擺脫階層固化的路徑 —— 絕對可行,毫無水分,並且全靠你自己。這裏所說的普通人,不分國界、不分地域、不分種族、不分性別、不分年齡、不分高矮胖瘦美醜、不分何種性取向…… 關鍵在於,甚至壓根不分智商和學歷!換言之,這個解決方案,甚至對在北京跑腿送外賣的小哥都適用……

GitHub 地址→https://github.com/xiaolai/regular-investing-in-box

3.3 機器學習課程個人筆記:Coursera-ML-AndrewNg-Notes

Coursera-ML-AndrewNg-Notes 是吳恩達老師的機器學習課程個人筆記,旨在提供了一個廣泛的介紹機器學習、數據挖掘、統計模式識別的課程。主題包括:

  • 監督學習(參數/非參數算法,支持向量機,核函數,神經網絡)。
  • 無監督學習(聚類,降維,推薦系統,深入學習推薦)。
  • 在機器學習的最佳實踐(偏差/方差理論;在機器學習和人工智能創新過程)。

項目還將使用大量的案例研究,你可學習到如何運用學習算法構建智能機器人(感知,控制),文本的理解(Web 搜索,反垃圾郵件),計算機視覺,醫療信息,音頻,數據挖掘,和其他領域。

GitHub 地址→https://github.com/fengdu78/Coursera-ML-AndrewNg-Notes

推薦閱讀

  • GitHub 熱點速覽 Vol.23:前後端最佳實踐
  • GitHub 熱點速覽 Vol.22:如何打造超級技術棧
  • GitHub 熱點速覽 Vol.21:Go 新手起手式,學就完事兒了

以上為 2020 年第 23 個工作周的 GitHub Trending 如果你 Pick 其他好玩、實用的 GitHub 項目,記得來 HelloGitHub issue 區和我們分享下喲

HelloGitHub 交流群現已全面開放,添加微信號:HelloGitHub 為好友入群,可同前端、Java、Go 等各界大佬談笑風生、切磋技術~

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

【其他文章推薦】

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

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

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

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了後台分類模塊的所有功能,本篇繼續將標籤模塊和友情鏈接模塊的增刪改查完成。

標籤管理

實現方式和之前的分類管理是一樣的,在Admin文件夾下面添加Tags.razor組件,設置路由@page "/admin/tags"

同樣的內容也需要放在AdminLayout組件下面,添加幾個參數:彈窗狀態bool Open、新增或更新時標籤字段string tagName, displayName、更新時的標籤Idint id、API返回的標籤列表接收參數ServiceResult<IEnumerable<QueryTagForAdminDto>> tags

/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;

/// <summary>
/// 新增或者更新時候的標籤字段值
/// </summary>
private string tagName, displayName;

/// <summary>
/// 更新標籤的Id值
/// </summary>
private int id;

/// <summary>
/// API返回的標籤列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryTagForAdminDto>> tags;
//QueryTagForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class QueryTagForAdminDto : QueryTagDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}

在初始化方法OnInitializedAsync()中獲取數據。

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    tags = await FetchData();
}

/// <summary>
/// 獲取數據
/// </summary>
/// <returns></returns>
private async Task<ServiceResult<IEnumerable<QueryTagForAdminDto>>> FetchData()
{
    return await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryTagForAdminDto>>>("/blog/admin/tags");
}

注意需要設置請求頭,進行授權訪問,然後頁面上綁定數據。

<AdminLayout>
    @if (tags == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap tags">
            <h2 class="post-title">-&nbsp;Tags&nbsp;-</h2>
            @if (tags.Success && tags.Result.Any())
            {
                <div class="categories-card">
                    @foreach (var item in tags.Result)
                    {
                        <div class="card-item">
                            <div class="categories">
                                <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(item.Id))"></NavLink>
                                <NavLink title="編輯" @onclick="@(() => ShowBox(item))"></NavLink>
                                <NavLink target="_blank" href="@($"/tag/{item.DisplayName}")">
                                    <h3>@item.TagName</h3>
                                    <small>(@item.Count)</small>
                                </NavLink>
                            </div>
                        </div>
                    }
                    <div class="card-item">
                        <div class="categories">
                            <NavLink><h3 @onclick="@(() => ShowBox())">~~~ 新增標籤 ~~~</h3></NavLink>
                        </div>
                    </div>
                </div>
            }
            else
            {
                <ErrorTip />
            }
        </div>

        <Box OnClickCallback="@SubmitAsync" Open="@Open">
            <div class="box-item">
                <b>DisplayName:</b><input type="text" @bind="@displayName" @bind:event="oninput" />
            </div>
            <div class="box-item">
                <b>TagName:</b><input type="text" @bind="@tagName" @bind:event="oninput" />
            </div>
        </Box>
    }
</AdminLayout>

tags沒獲取到數據的時候显示<Loading />組件內容,循環遍曆數據進行綁定,刪除按鈕綁定點擊事件調用DeleteAsync()方法。新增和編輯按鈕點擊事件調用ShowBox()方法显示彈窗。新增的時候不需要傳遞參數,編輯的時候需要將當前item即QueryTagForAdminDto傳遞進去。

<Box>組件中綁定了標籤的兩個參數,是否打開參數Opne和確認按鈕回調事件方法SubmitAsync()

刪除標籤的方法DeleteAsync(...)如下:

// 彈窗確認
bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這個該死的標籤嗎");

if (confirmed)
{
    var response = await Http.DeleteAsync($"/blog/tag?id={id}");

    var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

    if (result.Success)
    {
        tags = await FetchData();
    }
}

刪除之前進行二次確認,避免誤傷,刪除成功重新加載一遍數據。

彈窗的方法ShowBox(...)如下:

/// <summary>
/// 显示box,綁定字段
/// </summary>
/// <param name="dto"></param>
private void ShowBox(QueryTagForAdminDto dto = null)
{
    Open = true;
    id = 0;

    // 新增
    if (dto == null)
    {
        displayName = null;
        tagName = null;
    }
    else // 更新
    {
        id = dto.Id;
        displayName = dto.DisplayName;
        tagName = dto.TagName;
    }
}

最後在彈窗中確認按鈕的回調事件方法SubmitAsync()如下:

/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
    var input = new EditTagInput()
    {
        DisplayName = displayName.Trim(),
        TagName = tagName.Trim()
    };

    if (string.IsNullOrEmpty(input.DisplayName) || string.IsNullOrEmpty(input.TagName))
    {
        return;
    }

    var responseMessage = new HttpResponseMessage();

    if (id > 0)
        responseMessage = await Http.PutAsJsonAsync($"/blog/tag?id={id}", input);
    else
        responseMessage = await Http.PostAsJsonAsync("/blog/tag", input);

    var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
    if (result.Success)
    {
        tags = await FetchData();
        Open = false;
    }
}

輸入參數EditTagInput

namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class EditTagInput : TagDto
    {
    }
}

最終執行新增或者更新數據都在點擊事件中進行,將變量的值賦值給EditTagInput,根據id判斷走新增還是更新,成功后重新加載數據,關掉彈窗。

標籤管理頁面全部代碼如下:

點擊查看代碼

@page "/admin/categories"

<AdminLayout>
    @if (categories == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap categories">
            <h2 class="post-title">-&nbsp;Categories&nbsp;-</h2>
            @if (categories.Success && categories.Result.Any())
            {
                <div class="categories-card">
                    @foreach (var item in categories.Result)
                    {
                        <div class="card-item">
                            <div class="categories">
                                <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(item.Id))"></NavLink>
                                <NavLink title="編輯" @onclick="@(() => ShowBox(item))"></NavLink>
                                <NavLink target="_blank" href="@($"/category/{item.DisplayName}")">
                                    <h3>@item.CategoryName</h3>
                                    <small>(@item.Count)</small>
                                </NavLink>
                            </div>
                        </div>
                    }
                    <div class="card-item">
                        <div class="categories">
                            <NavLink><h3 @onclick="@(() => ShowBox())">~~~ 新增分類 ~~~</h3></NavLink>
                        </div>
                    </div>
                </div>
            }
            else
            {
                <ErrorTip />
            }
        </div>

        <Box OnClickCallback="@SubmitAsync" Open="@Open">
            <div class="box-item">
                <b>DisplayName:</b><input type="text" @bind="@displayName" @bind:event="oninput" />
            </div>
            <div class="box-item">
                <b>CategoryName:</b><input type="text" @bind="@categoryName" @bind:event="oninput" />
            </div>
        </Box>
    }
</AdminLayout>

@code {
    /// <summary>
    /// 默認隱藏Box
    /// </summary>
    private bool Open { get; set; } = false;

    /// <summary>
    /// 新增或者更新時候的分類字段值
    /// </summary>
    private string categoryName, displayName;

    /// <summary>
    /// 更新分類的Id值
    /// </summary>
    private int id;

    /// <summary>
    /// API返回的分類列表數據
    /// </summary>
    private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;

    /// <summary>
    /// 初始化
    /// </summary>
    /// <returns></returns>
    protected override async Task OnInitializedAsync()
    {
        var token = await Common.GetStorageAsync("token");
        Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

        categories = await FetchData();
    }

    /// <summary>
    /// 獲取數據
    /// </summary>
    /// <returns></returns>
    private async Task<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>> FetchData()
    {
        return await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");
    }

    /// <summary>
    /// 刪除分類
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    private async Task DeleteAsync(int id)
    {
        Open = false;

        // 彈窗確認
        bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這個該死的分類嗎");

        if (confirmed)
        {
            var response = await Http.DeleteAsync($"/blog/category?id={id}");

            var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

            if (result.Success)
            {
                categories = await FetchData();
            }
        }
    }

    /// <summary>
    /// 显示box,綁定字段
    /// </summary>
    /// <param name="dto"></param>
    private void ShowBox(QueryCategoryForAdminDto dto = null)
    {
        Open = true;
        id = 0;

        // 新增
        if (dto == null)
        {
            displayName = null;
            categoryName = null;
        }
        else // 更新
        {
            id = dto.Id;
            displayName = dto.DisplayName;
            categoryName = dto.CategoryName;
        }
    }

    /// <summary>
    /// 確認按鈕點擊事件
    /// </summary>
    /// <returns></returns>
    private async Task SubmitAsync()
    {
        var input = new EditCategoryInput()
        {
            DisplayName = displayName.Trim(),
            CategoryName = categoryName.Trim()
        };

        if (string.IsNullOrEmpty(input.DisplayName) || string.IsNullOrEmpty(input.CategoryName))
        {
            return;
        }

        var responseMessage = new HttpResponseMessage();

        if (id > 0)
            responseMessage = await Http.PutAsJsonAsync($"/blog/category?id={id}", input);
        else
            responseMessage = await Http.PostAsJsonAsync("/blog/category", input);

        var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
        if (result.Success)
        {
            categories = await FetchData();
            Open = false;
        }
    }
}

友鏈管理

實現方式都是一樣的,這個就不多說了,直接上代碼。

先將API返回的接收參數和新增編輯的輸入參數添加一下。

//QueryFriendLinkForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class QueryFriendLinkForAdminDto : FriendLinkDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}

//EditFriendLinkInput.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class EditFriendLinkInput : FriendLinkDto
    {
    }
}
@page "/admin/friendlinks"

<AdminLayout>
    @if (friendlinks == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap categories">
            <h2 class="post-title">-&nbsp;FriendLinks&nbsp;-</h2>
            @if (friendlinks.Success && friendlinks.Result.Any())
            {
                <div class="categories-card">
                    @foreach (var item in friendlinks.Result)
                    {
                        <div class="card-item">
                            <div class="categories">
                                <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(item.Id))"></NavLink>
                                <NavLink title="編輯" @onclick="@(() => ShowBox(item))"></NavLink>
                                <NavLink target="_blank" href="@item.LinkUrl">
                                    <h3>@item.Title</h3>
                                </NavLink>
                            </div>
                        </div>
                    }
                    <div class="card-item">
                        <div class="categories">
                            <NavLink><h3 @onclick="@(() => ShowBox())">~~~ 新增友鏈 ~~~</h3></NavLink>
                        </div>
                    </div>
                </div>
            }
            else
            {
                <ErrorTip />
            }
        </div>

        <Box OnClickCallback="@SubmitAsync" Open="@Open">
            <div class="box-item">
                <b>Title:</b><input type="text" @bind="@title" @bind:event="oninput" />
            </div>
            <div class="box-item">
                <b>LinkUrl:</b><input type="text" @bind="@linkUrl" @bind:event="oninput" />
            </div>
        </Box>
    }
</AdminLayout>

@code {
    /// <summary>
    /// 默認隱藏Box
    /// </summary>
    private bool Open { get; set; } = false;

    /// <summary>
    /// 新增或者更新時候的友鏈字段值
    /// </summary>
    private string title, linkUrl;

    /// <summary>
    /// 更新友鏈的Id值
    /// </summary>
    private int id;

    /// <summary>
    /// API返回的友鏈列表數據
    /// </summary>
    private ServiceResult<IEnumerable<QueryFriendLinkForAdminDto>> friendlinks;

    /// <summary>
    /// 初始化
    /// </summary>
    /// <returns></returns>
    protected override async Task OnInitializedAsync()
    {
        var token = await Common.GetStorageAsync("token");
        Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

        friendlinks = await FetchData();
    }

    /// <summary>
    /// 獲取數據
    /// </summary>
    /// <returns></returns>
    private async Task<ServiceResult<IEnumerable<QueryFriendLinkForAdminDto>>> FetchData()
    {
        return await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryFriendLinkForAdminDto>>>("/blog/admin/friendlinks");
    }

    /// <summary>
    /// 刪除分類
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    private async Task DeleteAsync(int id)
    {
        Open = false;

        // 彈窗確認
        bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這個該死的分類嗎");

        if (confirmed)
        {
            var response = await Http.DeleteAsync($"/blog/friendlink?id={id}");

            var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

            if (result.Success)
            {
                friendlinks = await FetchData();
            }
        }
    }

    /// <summary>
    /// 显示box,綁定字段
    /// </summary>
    /// <param name="dto"></param>
    private void ShowBox(QueryFriendLinkForAdminDto dto = null)
    {
        Open = true;
        id = 0;

        // 新增
        if (dto == null)
        {
            title = null;
            linkUrl = null;
        }
        else // 更新
        {
            id = dto.Id;
            title = dto.Title;
            linkUrl = dto.LinkUrl;
        }
    }

    /// <summary>
    /// 確認按鈕點擊事件
    /// </summary>
    /// <returns></returns>
    private async Task SubmitAsync()
    {
        var input = new EditFriendLinkInput()
        {
            Title = title.Trim(),
            LinkUrl = linkUrl.Trim()
        };

        if (string.IsNullOrEmpty(input.Title) || string.IsNullOrEmpty(input.LinkUrl))
        {
            return;
        }

        var responseMessage = new HttpResponseMessage();

        if (id > 0)
            responseMessage = await Http.PutAsJsonAsync($"/blog/friendlink?id={id}", input);
        else
            responseMessage = await Http.PostAsJsonAsync("/blog/friendlink", input);

        var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
        if (result.Success)
        {
            friendlinks = await FetchData();
            Open = false;
        }
    }
}

截至目前為止,還剩下文章模塊的功能還沒做了,今天到這裏吧,明天繼續剛,未完待續…

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

C# 9.0 新特性之目標類型推導 new 表達式

閱讀本文大概需要 2 分鐘。

呼~~,每次過完一個周末,寫作就失去了動力,一两天才能緩過來。儘管如此,還是要堅持寫好每一篇文章的。寧缺毋濫嘛,寧願發文的頻率低一點,也要保證文章的質量,至少排版不能差,行文要流暢,錯別字不能有。

關於類型推導想必大家都很熟悉,它是在 var 關鍵字引入的時候引入 C# 的。

var i = 10;
var u = new User();

編譯器會通過右邊的字面量自動推導左邊變量的類型,這種推導方式可以歸納為:從上下文右邊推導出左邊的類型。我們不妨把它稱為源類型推導(Source-typed inferring,參考 Target-typed 自創的術語)。

相應的,有源類型推導就有目標類型推導 (Target-typed inferring),它是指從上下文左邊推導出右邊的類型。比如數組的初始化和 Lambda 表達式常常是目標類型推導的表達式。舉個例子:

// 沒有使用類型推導
string[] s = new string[] { "a", "b" };
// 目標類型推導(左推右)
string[] s = new { "a", "b" };
string[] s = new [] { "a", "b" };

// 沒有使用類型推導
Users.FirstOrDefault<User>(u => u.id = 123);
// 目標類型推導(左推右)
Users.FirstOrDefault(u => u.id = 123);

這次在 C# 9 中,增加了用戶定義類型 new 表達式的目標類型推導,即通過上下文左邊自動推導 new 表達式的類型,從而在使用 new 構造時省略類型的指定,請看示例:

// C# 9 之前
Point p = new Point(3, 5);

// C# 9
Point p = new (3, 5);

除此之外,C# 9 也增加了操作符 ???: 的目標類型推導支持。之前這兩個操作符必須要求兩邊的操作對象都是相同的類型,否則會編譯報錯。而在 C# 9 中,只要目標類型是操作對象共同的基類就不再會編譯報錯了,比如:

// Student 和 Customer 擁有共同的父類 Person
Person person = (Person)(student ?? customer); // C# 9 之前
Person person = student ?? customer; // C# 9

// 可空類型,0 和 null 都可以隱式轉換為 int? 類型
int? result = b ? 0 : (int?)null; // C# 9 之前
int? result = b ? 0 : null; // C# 9

其實本文的核心就一句代碼:

Point p = new (3, 5);

卻一不小心啰嗦了這麼一堆。但講真,學習新的知識不是要死記硬背,而要學會歸類推理,舉一反三,經常思考,最好能形成自己的一種思維習慣,這樣學習才會變成一件水到渠成的事。多看我的文章,希望你能學到的不僅僅是生硬的編程知識點,也希望我的行文風格和思維習慣對你有所啟發。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

什麼?一個核同時執行兩個線程?

CPU里的時間

Hi,好久不見,我是CPU一號車間的阿Q,不認識我的話,可以看看:完了!CPU一味求快出事兒了!

真的是好久不見了,人類有個說法叫天上一天,地上一年,而在我們的世界里,人類一天,我們不知要過多少年~~

在我所在的CPU這座工廠里,時間的概念有些不太一樣。工廠大門外的中央廣場上掛着一個大大的鐘錶,整個計算機世界里的居民能夠掐着時間過日子全都仰仗它,你們人類把它叫做晶振

這個鐘錶每隔66000000分之一秒就會報一次時,比人類的鐘錶不知道快到哪裡去了。

早些年還是夠用的,不過隨着我們CPU工廠生產效率的不斷提升,我們多次向晶振提出提升報時的精度,想讓他報時報的的更快一些,不過都被拒絕了。給我們的理由是內存那傢伙聯合主板上其他單位帶頭反對,說他們受條件限制,沒辦法像我們這麼快。

靠人不如靠己,為此,咱們工廠專門設立了一個叫倍頻器的部門進一步把這個報時細分,達到了3600000000分之1秒,作為我們工廠內部工作作息的時鐘周期,這数字實在是太長了,人類為了好記,取了一個叫主頻的名字,表示1秒鐘報時的次數,就是3.6GHz。

一不小心扯遠了,這次想給大家說一件事兒······

指令依賴

我們這座工廠的任務就是不斷的執行人類編寫的程序指令,咱廠里有8個車間,大家開足了馬力,就能同時執行8個線程,那速度那叫一個快。

可是廠里的老闆還是嫌我們不夠快,那天居然告訴我們要每個車間執行兩個線程,實現八核十六線程,是要把我們的勞動力壓榨到極致!我們都滿肚子怨言······

事情的起因是這樣的~~

有一次,我們一號車間的四人組趁着工作的空當,又鬥起了地主,突然領導過來視察。

“你們怎麼又在玩?是工作量不飽和嗎?”,見我們幾個閑着,領導一下就不高興了。

我趕緊上前解釋到:“不好意思領導,咱們剛剛執行了一條指令,需要內存中的一塊數據,剛好又不在緩存中,所以找內存那傢伙要數據去了,這不您也知道那傢伙向來很慢,我們閑着也是閑着所以就稍微放鬆了一下······”

聽了我的話領導一下皺起了眉頭,“還給我狡辯,廠里現在不是用上了亂序執行技術嗎?有這閑功夫你們可以先執行後面的指令啊”

“這我們當然知道,這不您看,我們把後面那幾條指令也都處理了,現在遇到了一條沒法提前執行的指令才停下來的”

領導看了一下問到:“為啥那條不能提前執行?”

“那是一個加法指令,加數依賴於現在正在處理的指令的運算結果呢,所以內存那傢伙不來消息,我們只能擱置着了”,我繼續解釋到。

領導聽完,一臉不高興的離開了。

資源閑置

過了幾天,領導又來到咱們一號車間來了,也不知道怎麼回事,這明明有八個車間,領導怎麼老愛往我們這邊跑。

不過這一次,我們沒有斗地主,正在辛辛苦苦的工作着。

當時,我正在執行一個浮點數運算,領導過來一看,拍了拍我的肩膀說到:“喲,阿Q,忙着吶,這是在做什麼啊?”

我笑着說到:“領導好,我剛剛用浮點數運算電路單元做了一個浮點數乘法,正在等待計算結果呢”

領導點了點頭,往周邊巡視一圈,指着一堆設備問到:“這一堆是什麼?”

“哦,那是整數運算電路單元,這條指令用不到它”

領導再次點了點頭,若有所思的離開了。

超線程技術

又過了幾天,廠里召開了一次會議,八個車間都派了代表參會。

會上,領導發話了:“前段時間我到各個車間視察,發現現在咱們廠里資源浪費的情況很嚴重!”

二號車間的虎子一聽就坐不住了,“領導,咱們大傢伙工作都挺賣力的,哪裡有浪費啊?”

領導瞥了一眼,繼續說到:“一方面,廠里的計算資源——電路設備得不到充分利用,另一方面,又因為內存讀取緩慢、指令依賴等方面的原因,浪費大家太多時間花在等待上”

八號車間的代表向來愛拍馬屁,接着領導的話問到:“領導是有什麼指示?我們八號車間絕對支持!”

“我們幾個管理層經過討論,決定讓你們一個車間由現在執行一個線程,變成執行兩個線程!

領導這話一出,會場竊竊私語此起彼伏。虎子偏頭小聲對我說到:“這資本家改不了剝削的本色,這壓榨的也太狠了!”

領導咳嗽了幾聲,會場再次安靜了下來。

我起身問到:“領導,這咱們一個車間怎麼能執行兩個線程呢,每個車間的寄存器只有一套,這用起來豈不是要亂掉?”

“這個你不用擔心,我們會給每個車間配兩套寄存器!”

五號車間的代表一聽說到:“要不再給我們添點人手吧,這樣效率肯定提升快!”

領導一聽笑着說到:“還添人手?要不要再給你們添點運算設備?那我不如再增加幾個車間,還開這會幹嘛?這次會議的主題就是如何讓我們現有的資源得到最大程度的利用,減少浪費現象!”

會場一度陷入了尷尬又緊張的氛圍。

還是虎子打破了安靜,“領導,這兩個線程的工作該怎麼開展,我們心底沒有數啊!”

領導滿意的笑了一下:“這才是你們該問的問題嘛!每個車間回去重新分配一下工作,劃分為兩套班子,各自維護一套寄存器,對外宣稱你們是兩個不同的物理核心,但各車間的緩存和計算資源還是只有一套。你們內部協調好,在執行代碼指令的時候,充分利用等待的時間執行另一個線程的指令,這樣也不用擔心指令依賴的問題。”

大家一邊聽一邊做着筆記。

“還有,如果遇到資源閑置的情況,也可以同時執行兩個線程的指令。比如一個線程是執行整數運算指令,一個線程是執行浮點數運算指令,就可以一起來,讓工廠的計算資源充分用起來,別閑置。”

看我們都認真的記着筆記,領導露出了滿意的笑容,“都記好了吧,我們給這項革命性的技術取了個特別酷的名字,叫超線程技術!”

散會後,大家都紛紛抱怨,把大家逼得這麼緊,以後上班看來是沒法摸魚了,這日子真是越來越難過了。

毀譽參半的超線程

不過,抱怨歸抱怨,大家還是得按照新規來執行。

很快,廠里就落地了這項技術,咱們一個車間搖身一變,變成了倆,咱們原來八核八線程的CPU一下變成了八核十六線程。操作系統那幫人都被我們給騙了,還以為咱們是十六核的CPU呢!

不過畢竟計算資源還是只有一份,遇到兩個線程都要使用同樣的計算單元時,還是得要排隊,還要花時間在兩個線程之前的協調工作上,所以整體工作效率的根本沒有2倍,絕大多數時候能提升個20%-30%就不錯了。

不僅如此,車間改造后,增加了新的邏輯電路單元,咱這CPU工廠的功耗也更大了,工廠門口那座巨大的風扇也得加大馬力給我們降溫了。

廠子里對這項技術的反對聲音開始不絕於耳。

不過後來發生了一件事,讓人們不得不關閉這項技術。聽聞這個消息,我們都樂開了花,看來又可以繼續摸魚了······

彩蛋

每當有網絡數據包到來,網卡那傢伙就通過中斷告訴我們CPU去處理。

可咱明明有8個車間,它非得一個勁的只給我們車間發中斷,搞得我們都沒法好好工作。

終於,我忍不住了······

預知後事如何,請關注後續精彩······

說明

超線程技術出現時間其實早於多核技術。本故事僅為敘述方便,不代表二者真實的發展順序。

往期TOP5文章

真慘!連各大編程語言都擺起地攤了!

因為一個跨域請求,我差點丟了飯碗

完了!CPU一味求快出事兒了!

哈希表哪家強?幾大編程語言吵起來了!

一個HTTP數據包的奇幻之旅

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!